synth_backend/arm_encoder.rs
1//! ARM Code Encoder - Converts ARM instructions to binary machine code
2//!
3//! Generates ARM32/Thumb-2 machine code from ARM instruction structures
4
5use synth_core::Result;
6use synth_core::target::FPUPrecision;
7use synth_synthesis::contracts::encoding as encoding_contracts;
8use synth_synthesis::{ArmOp, MemAddr, MveSize, Operand2, QReg, Reg, VfpReg};
9
10/// ARM instruction encoding
11pub struct ArmEncoder {
12 /// Use Thumb mode (vs ARM mode)
13 thumb_mode: bool,
14 /// FPU capability for VFP instruction encoding
15 #[allow(dead_code)]
16 fpu: Option<FPUPrecision>,
17}
18
19impl ArmEncoder {
20 /// Create a new ARM encoder in ARM32 mode
21 pub fn new_arm32() -> Self {
22 Self {
23 thumb_mode: false,
24 fpu: None,
25 }
26 }
27
28 /// Create a new ARM encoder in Thumb-2 mode
29 pub fn new_thumb2() -> Self {
30 Self {
31 thumb_mode: true,
32 fpu: None,
33 }
34 }
35
36 /// Create a new Thumb-2 encoder with FPU capability
37 pub fn new_thumb2_with_fpu(fpu: Option<FPUPrecision>) -> Self {
38 Self {
39 thumb_mode: true,
40 fpu,
41 }
42 }
43
44 /// Encode a single ARM instruction to bytes
45 pub fn encode(&self, op: &ArmOp) -> Result<Vec<u8>> {
46 if self.thumb_mode {
47 self.encode_thumb(op)
48 } else {
49 self.encode_arm(op)
50 }
51 }
52
53 /// Encode an ARM instruction in ARM32 mode (32-bit instructions)
54 /// #206 / RQ-63-ARMI64OFF (#1165): encode an ARM32 (A32) word/sub-word
55 /// load/store whose effective base must be computed into IP — because the
56 /// address carries a register offset (`[rn, rm{, #off}]`, #206) and/or
57 /// because its static offset exceeds the form's immediate field (imm12 for
58 /// LDR/STR/LDRB/STRB, imm8 for LDRH/STRH/LDRSB/LDRSH; #1165 — the
59 /// immediate arms used to MASK such an offset to a wrong address). Returns
60 /// `None` for a plain in-range immediate access (the caller falls through
61 /// to the immediate-form arms, byte-identical). Otherwise
62 /// `a32_effective_base` materializes `ip` and the op is re-encoded against
63 /// `[ip, #residual]`, which is uniform for word/byte/halfword/signed forms.
64 /// IP (R12) is the scratch register the selector already treats as
65 /// clobberable across memory ops.
66 fn encode_arm_reg_offset_mem(&self, op: &ArmOp) -> Result<Option<Vec<u8>>> {
67 let (addr, imm_max) = match op {
68 ArmOp::Ldr { addr, .. }
69 | ArmOp::Str { addr, .. }
70 | ArmOp::Ldrb { addr, .. }
71 | ArmOp::Strb { addr, .. } => (addr, A32_LDST_IMM12_MAX),
72 ArmOp::Ldrh { addr, .. }
73 | ArmOp::Strh { addr, .. }
74 | ArmOp::Ldrsb { addr, .. }
75 | ArmOp::Ldrsh { addr, .. } => (addr, A32_LDST_IMM8_MAX),
76 _ => return Ok(None),
77 };
78 if addr.offset_reg.is_none() && (addr.offset as u32) <= imm_max {
79 return Ok(None);
80 }
81 let mut bytes = Vec::new();
82 let (base, residual) = a32_effective_base(&mut bytes, addr, imm_max)?;
83 // Re-encode the op against [base, #residual] (immediate form, in range
84 // by construction → this recursion hits the immediate arms, not this
85 // helper again).
86 let imm_addr = MemAddr::imm(base, residual as i32);
87 let imm_op = match op {
88 ArmOp::Ldr { rd, .. } => ArmOp::Ldr {
89 rd: *rd,
90 addr: imm_addr,
91 },
92 ArmOp::Str { rd, .. } => ArmOp::Str {
93 rd: *rd,
94 addr: imm_addr,
95 },
96 ArmOp::Ldrb { rd, .. } => ArmOp::Ldrb {
97 rd: *rd,
98 addr: imm_addr,
99 },
100 ArmOp::Strb { rd, .. } => ArmOp::Strb {
101 rd: *rd,
102 addr: imm_addr,
103 },
104 ArmOp::Ldrh { rd, .. } => ArmOp::Ldrh {
105 rd: *rd,
106 addr: imm_addr,
107 },
108 ArmOp::Strh { rd, .. } => ArmOp::Strh {
109 rd: *rd,
110 addr: imm_addr,
111 },
112 ArmOp::Ldrsb { rd, .. } => ArmOp::Ldrsb {
113 rd: *rd,
114 addr: imm_addr,
115 },
116 ArmOp::Ldrsh { rd, .. } => ArmOp::Ldrsh {
117 rd: *rd,
118 addr: imm_addr,
119 },
120 _ => unreachable!(),
121 };
122 bytes.extend(self.encode_arm(&imm_op)?);
123 Ok(Some(bytes))
124 }
125
126 /// #594: A32 expansion of `ArmOp::CallIndirect` — mirror of the Thumb-2
127 /// arm (same contract: R11 holds the function-pointer table base, entry
128 /// `i` is a 4-byte code address, R12 is the encoder-scratch register):
129 ///
130 /// ```text
131 /// MOVW r12, #size ; #642: table size (compile-time immediate)
132 /// [MOVT r12, #size>>16] ; only when size exceeds 16 bits
133 /// CMP idx, r12 ; bounds guard: index >= size must TRAP
134 /// BLO +1 insn ; skip the trap when in bounds
135 /// UDF ; WASM Core §4.4.8 out-of-bounds trap
136 /// MOV r12, idx, LSL #2 ; table byte offset
137 /// LDR r12, [r11, r12] ; load function pointer
138 /// BLX r12 ; indirect call
139 /// ```
140 ///
141 /// #650, `table_byte_offset != 0` (a non-zero table of the contiguous
142 /// R11 region): the pointer load becomes
143 /// `ADD r12, r11, r12; LDR r12, [r12, #offset]` — offset 0 keeps the
144 /// single-load form (single-table modules byte-identical by
145 /// construction).
146 ///
147 /// #664, `null_check` (the table has null slots, linked as ZERO words
148 /// per the layout contract): `CMP r12, #0; BNE +1; UDF` between the
149 /// pointer load and the `BLX` — a call reaching an uninitialized slot
150 /// traps (§4.4.8). `false` keeps the expansion byte-identical.
151 ///
152 /// #676, `type_check` (heterogeneous table): the §4.4.8 type check is
153 /// discharged at RUNTIME against the type-id sidecar — after the bounds
154 /// guard, `MOV r12, idx, LSL #2; ADD r12, r11, r12;
155 /// LDR r12, [r12, #type_off]; CMP r12, #expected_id; BEQ +1; UDF`
156 /// (mirror of the Thumb-2 arm; the dispatch tail recomputes `idx*4`).
157 /// Null slots carry the reserved class id 0, subsuming the #664 null
158 /// trap. `None` (every homogeneous table — the verdict discharged at
159 /// COMPILE time by the closed-world verification, see the #642 selector
160 /// guard) emits nothing and keeps the expansion byte-identical.
161 fn encode_arm_call_indirect(
162 table_index_reg: &Reg,
163 table_size: u32,
164 table_byte_offset: u32,
165 null_check: bool,
166 type_check: Option<(u32, u32)>,
167 ) -> Vec<u8> {
168 let idx = reg_to_bits(table_index_reg);
169 let mut bytes = Vec::with_capacity(32);
170 // MOVW r12, #(size & 0xFFFF) — cond=E 0011 0000 imm4 Rd imm12.
171 let size_lo = table_size & 0xFFFF;
172 let movw: u32 = 0xE300_0000 | ((size_lo >> 12) << 16) | (12 << 12) | (size_lo & 0xFFF);
173 bytes.extend_from_slice(&movw.to_le_bytes());
174 // MOVT r12, #(size >> 16) — only for a table size above 16 bits.
175 let size_hi = table_size >> 16;
176 if size_hi != 0 {
177 let movt: u32 = 0xE340_0000 | ((size_hi >> 12) << 16) | (12 << 12) | (size_hi & 0xFFF);
178 bytes.extend_from_slice(&movt.to_le_bytes());
179 }
180 // CMP idx, r12 — cond=E, opcode=1010, S=1, Rn=idx, Rm=r12.
181 let cmp: u32 = 0xE150_000C | (idx << 16);
182 bytes.extend_from_slice(&cmp.to_le_bytes());
183 // BLO +1 insn (skip the UDF when index < size) — cond=LO(0011),
184 // imm24=0: target = branch + 8.
185 bytes.extend_from_slice(&0x3A00_0000u32.to_le_bytes());
186 // UDF — permanently undefined (same trap idiom as the A32 div-by-zero
187 // guards): call_indirect out-of-bounds trap.
188 bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
189 // #676: runtime type check for a heterogeneous table — load the
190 // indexed slot's structural class id from the type-id sidecar and
191 // trap on mismatch (§4.4.8). Mirror of the Thumb-2 arm; `None`
192 // emits nothing (homogeneous tables byte-identical by construction).
193 if let Some((expected_id, type_off)) = type_check {
194 // RQ-61-IMMRANGE (#1072): these `debug_assert`s are compiled out
195 // in release, where the `& 0xFF` / `& 0xFFF` masks below would
196 // silently TRUNCATE an out-of-range value (id 256 compares as 0,
197 // letting a NULL slot pass the §4.4.8 check). The enforcement
198 // claim is DEMONSTRATED, not assumed: the sole `Some` producer is
199 // `resolve_runtime_type_check` (instruction_selector.rs), which
200 // loud-declines id > 255 and offset > 4095, and
201 // `test_676_call_indirect_runtime_check_range_declines` trips
202 // both declines (mutation-checked: disabling either turns it red).
203 debug_assert!(expected_id <= 255, "selector enforces the CMP imm8 range");
204 debug_assert!(type_off <= 4095, "selector enforces the LDR imm12 range");
205 // MOV r12, idx, LSL #2 (same as the dispatch tail's scale).
206 bytes.extend_from_slice(&(0xE1A0C000u32 | (2 << 7) | idx).to_le_bytes());
207 // ADD r12, r11, r12 — data-processing ADD (register).
208 bytes.extend_from_slice(&0xE08BC00Cu32.to_le_bytes());
209 // LDR r12, [r12, #type_off] — immediate offset, P=1 U=1 L=1.
210 bytes.extend_from_slice(&(0xE59CC000u32 | (type_off & 0xFFF)).to_le_bytes());
211 // CMP r12, #expected_id — data-processing CMP (immediate).
212 bytes.extend_from_slice(&(0xE35C_0000u32 | (expected_id & 0xFF)).to_le_bytes());
213 // BEQ +1 insn (skip the UDF when the class id matches) —
214 // cond=EQ(0000), imm24=0: target = branch + 8.
215 bytes.extend_from_slice(&0x0A00_0000u32.to_le_bytes());
216 // UDF — the §4.4.8 type-mismatch trap.
217 bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
218 }
219 // MOV r12, idx, LSL #2 — data-processing MOV, register op2 with
220 // imm5=2/LSL: cond=E, opcode=1101, S=0, Rd=r12.
221 let mov: u32 = 0xE1A0C000 | (2 << 7) | idx;
222 bytes.extend_from_slice(&mov.to_le_bytes());
223 if table_byte_offset == 0 {
224 // Table 0 (base = R11 itself): the pre-#650 single-load form.
225 // LDR r12, [r11, r12] — register offset, P=1 U=1 B=0 W=0 L=1.
226 let ldr: u32 = 0xE79BC00C;
227 bytes.extend_from_slice(&ldr.to_le_bytes());
228 } else {
229 // #650: fold the table's compile-time base offset into the
230 // pointer load via the LDR imm12 form.
231 assert!(
232 table_byte_offset <= 4095,
233 "call_indirect table base offset {table_byte_offset} exceeds \
234 LDR imm12 — the selector must have declined this (#650)"
235 );
236 // ADD r12, r11, r12 — data-processing ADD (register).
237 bytes.extend_from_slice(&0xE08BC00Cu32.to_le_bytes());
238 // LDR r12, [r12, #offset] — immediate offset, P=1 U=1 L=1.
239 let ldr: u32 = 0xE59CC000 | (table_byte_offset & 0xFFF);
240 bytes.extend_from_slice(&ldr.to_le_bytes());
241 }
242 // #664: null-slot trap — only when the table image has null slots
243 // (zero-linked words). A fully-initialized table keeps the pre-#664
244 // bytes identical by construction.
245 if null_check {
246 // CMP r12, #0 — data-processing CMP (immediate), Rn=r12.
247 bytes.extend_from_slice(&0xE35C_0000u32.to_le_bytes());
248 // BNE +1 insn (skip the UDF when the pointer is non-null) —
249 // cond=NE(0001), imm24=0: target = branch + 8.
250 bytes.extend_from_slice(&0x1A00_0000u32.to_le_bytes());
251 // UDF — the §4.4.8 uninitialized-element trap (same idiom as
252 // the bounds guard).
253 bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
254 }
255 // BLX r12 — cond=E, 0001 0010 1111 1111 1111 0011, Rm=r12.
256 let blx: u32 = 0xE12FFF3C;
257 bytes.extend_from_slice(&blx.to_le_bytes());
258 bytes
259 }
260
261 /// #615: A32 (ARM-mode) expansions for the multi-instruction ops that the
262 /// Thumb-2 encoder expands but the A32 arm previously encoded as a single
263 /// literal NOP (`0xE1A00000`) — i64 mul / shifts / rotates / comparisons /
264 /// eqz, plus i64 const/load/store/extend/wrap and the i32 SetCond /
265 /// SelectMove pseudo-ops. Each expansion mirrors its Thumb-2 twin's
266 /// register contract and semantics exactly (A32 conditional execution
267 /// replaces the IT blocks). Returns `Ok(None)` for ops this helper does
268 /// not handle; the caller's match encodes or loudly rejects those.
269 fn encode_arm_expanded(&self, op: &ArmOp) -> Result<Option<Vec<u8>>> {
270 use synth_synthesis::Condition;
271
272 /// A32 condition-field bits (instruction bits [31:28]).
273 fn cond_bits(cond: &Condition) -> u32 {
274 match cond {
275 Condition::EQ => 0x0,
276 Condition::NE => 0x1,
277 Condition::HS => 0x2, // CS: unsigned >=
278 Condition::LO => 0x3, // CC: unsigned <
279 Condition::HI => 0x8, // unsigned >
280 Condition::LS => 0x9, // unsigned <=
281 Condition::GE => 0xA,
282 Condition::LT => 0xB,
283 Condition::GT => 0xC,
284 Condition::LE => 0xD,
285 }
286 }
287 fn w(b: &mut Vec<u8>, word: u32) {
288 b.extend_from_slice(&word.to_le_bytes());
289 }
290 /// MOV<cond> rd, #imm (rotated-immediate form; only 0/1 used here).
291 fn mov_cond_imm(b: &mut Vec<u8>, cond: u32, rd: u32, imm: u32) {
292 w(b, (cond << 28) | 0x03A0_0000 | (rd << 12) | imm);
293 }
294 /// After a flag-setting pair: MOV<cond> rd,#1 ; MOV<!cond> rd,#0.
295 fn set_cond(b: &mut Vec<u8>, cond: &Condition, rd: u32) {
296 mov_cond_imm(b, cond_bits(cond), rd, 1);
297 mov_cond_imm(b, cond_bits(&cond.invert()), rd, 0);
298 }
299 /// CMP rn, rm (register form).
300 fn cmp_reg(b: &mut Vec<u8>, rn: u32, rm: u32) {
301 w(b, 0xE150_0000 | (rn << 16) | rm);
302 }
303 /// SBCS rd, rn, rm — the 64-bit compare idiom's high-word subtract.
304 fn sbcs(b: &mut Vec<u8>, rd: u32, rn: u32, rm: u32) {
305 w(b, 0xE0D0_0000 | (rn << 16) | (rd << 12) | rm);
306 }
307 /// MOVW rd, #imm16.
308 fn movw(b: &mut Vec<u8>, rd: u32, v: u32) {
309 w(
310 b,
311 0xE300_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
312 );
313 }
314 /// MOVT rd, #imm16.
315 fn movt(b: &mut Vec<u8>, rd: u32, v: u32) {
316 w(
317 b,
318 0xE340_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
319 );
320 }
321 /// Register-controlled shift: MOV rd, rn, <LSL|LSR|ASR> rs.
322 /// `ty`: 0=LSL, 1=LSR, 2=ASR. A32 uses the bottom byte of rs;
323 /// amounts of 32 or more yield 0 (LSL/LSR) or all-sign (ASR) — same
324 /// semantics the Thumb-2 expansions rely on.
325 fn shift_reg(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, rs: u32) {
326 w(b, 0xE1A0_0010 | (rd << 12) | (rs << 8) | (ty << 5) | rn);
327 }
328 const LSL: u32 = 0;
329 const LSR: u32 = 1;
330 const ASR: u32 = 2;
331 /// Immediate-shift move: MOV rd, rn, <LSL|LSR|ASR> #imm.
332 fn shift_imm(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, imm: u32) {
333 w(
334 b,
335 0xE1A0_0000 | (rd << 12) | ((imm & 0x1F) << 7) | (ty << 5) | rn,
336 );
337 }
338 /// Data-processing register form: `base | rn<<16 | rd<<12 | rm`.
339 /// `base` carries cond/opcode/S (e.g. 0xE090_0000 = ADDS).
340 fn dp_reg(b: &mut Vec<u8>, base: u32, rd: u32, rn: u32, rm: u32) {
341 w(b, base | (rn << 16) | (rd << 12) | rm);
342 }
343 /// Data-processing with an immediate-shifted register operand:
344 /// `<op> rd, rn, rm, <LSL|LSR|ASR> #imm` — the A32 barrel shifter
345 /// folds a shift into the second operand for free. #1021 uses this to
346 /// run the popcnt SWAR fold on R12 alone (no second scratch, so R11 —
347 /// the linear-memory base — is never touched).
348 fn dp_reg_shift(b: &mut Vec<u8>, base: u32, rd: u32, rn: u32, rm: u32, ty: u32, imm: u32) {
349 w(
350 b,
351 base | (rn << 16) | (rd << 12) | ((imm & 0x1F) << 7) | (ty << 5) | rm,
352 );
353 }
354 /// ORR rd, rd, rm, LSR #31 — the carry-propagation idiom of the
355 /// shift-subtract division loop (bring rm's MSB into rd's bit 0).
356 fn orr_lsr31(b: &mut Vec<u8>, rd: u32, rm: u32) {
357 w(
358 b,
359 0xE180_0000 | (rd << 16) | (rd << 12) | (31 << 7) | (1 << 5) | rm,
360 );
361 }
362 /// 64-bit two's-complement negate of the lo:hi pair (MVN/MVN/ADDS/ADC).
363 fn negate64(b: &mut Vec<u8>, lo: u32, hi: u32) {
364 w(b, 0xE1E0_0000 | (lo << 12) | lo); // MVN lo, lo
365 w(b, 0xE1E0_0000 | (hi << 12) | hi); // MVN hi, hi
366 w(b, 0xE290_0001 | (lo << 16) | (lo << 12)); // ADDS lo, lo, #1
367 w(b, 0xE2A0_0000 | (hi << 16) | (hi << 12)); // ADC hi, hi, #0
368 }
369 /// TST x, x ; BPL +4-instructions — the "skip the negate64 when the
370 /// sign bit is clear" guard of the signed div/rem arms.
371 fn skip_negate_if_positive(b: &mut Vec<u8>, x: u32) {
372 w(b, 0xE110_0000 | (x << 16) | x); // TST x, x
373 w(b, 0x5A00_0003); // BPL +4 insns (past negate64)
374 }
375 /// The 64-iteration shift-subtract division loop — A32 transcription
376 /// of the Thumb-2 #610 core: dividend R0:R1, divisor R2:R3, quotient
377 /// R4:R5, remainder R6:R7, loop counter in `counter` (R12 or R8).
378 fn div_loop(b: &mut Vec<u8>, counter: u32) {
379 w(b, 0xE3A0_0040 | (counter << 12)); // MOV counter, #64
380 let loop_start = b.len();
381 // quotient <<= 1
382 shift_imm(b, LSL, 5, 5, 1);
383 orr_lsr31(b, 5, 4);
384 shift_imm(b, LSL, 4, 4, 1);
385 // remainder <<= 1, OR in dividend MSB
386 shift_imm(b, LSL, 7, 7, 1);
387 orr_lsr31(b, 7, 6);
388 shift_imm(b, LSL, 6, 6, 1);
389 orr_lsr31(b, 6, 1);
390 // dividend <<= 1
391 shift_imm(b, LSL, 1, 1, 1);
392 orr_lsr31(b, 1, 0);
393 shift_imm(b, LSL, 0, 0, 1);
394 // if remainder >= divisor (64-bit unsigned): subtract, set q bit
395 w(b, 0xE157_0003); // CMP R7, R3 (high words)
396 w(b, 0x8A00_0002); // BHI .subtract (+2 insns)
397 w(b, 0x3A00_0004); // BLO .next (+4 insns)
398 w(b, 0xE156_0002); // CMP R6, R2 (low words, highs equal)
399 w(b, 0x3A00_0002); // BLO .next (+2 insns)
400 w(b, 0xE056_6002); // .subtract: SUBS R6, R6, R2
401 w(b, 0xE0C7_7003); // SBC R7, R7, R3
402 w(b, 0xE384_4001); // ORR R4, R4, #1
403 // .next: decrement and loop
404 w(b, 0xE250_0001 | (counter << 16) | (counter << 12)); // SUBS counter, #1
405 let diff = (loop_start as i64) - (b.len() as i64 + 8);
406 w(b, 0x1A00_0000 | (((diff / 4) as u32) & 0x00FF_FFFF)); // BNE loop
407 }
408 /// 32-bit population count on working register `x` — A32 transcription
409 /// of the Thumb-2 I64Popcnt per-word core (mul-based fold): `c` is the
410 /// constant register, R12 the shifted temp. Both are clobbered.
411 fn popcnt_word(b: &mut Vec<u8>, x: u32, c: u32) {
412 // x = x - ((x >> 1) & 0x55555555)
413 shift_imm(b, LSR, 12, x, 1);
414 movw(b, c, 0x5555);
415 movt(b, c, 0x5555);
416 dp_reg(b, 0xE000_0000, 12, 12, c); // AND R12, R12, c
417 dp_reg(b, 0xE040_0000, x, x, 12); // SUB x, x, R12
418 // x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
419 movw(b, c, 0x3333);
420 movt(b, c, 0x3333);
421 dp_reg(b, 0xE000_0000, 12, x, c); // AND R12, x, c
422 shift_imm(b, LSR, x, x, 2);
423 dp_reg(b, 0xE000_0000, x, x, c); // AND x, x, c
424 dp_reg(b, 0xE080_0000, x, x, 12); // ADD x, x, R12
425 // x = (x + (x >> 4)) & 0x0F0F0F0F
426 shift_imm(b, LSR, 12, x, 4);
427 dp_reg(b, 0xE080_0000, x, x, 12); // ADD x, x, R12
428 movw(b, c, 0x0F0F);
429 movt(b, c, 0x0F0F);
430 dp_reg(b, 0xE000_0000, x, x, c); // AND x, x, c
431 // x = (x * 0x01010101) >> 24
432 movw(b, c, 0x0101);
433 movt(b, c, 0x0101);
434 w(b, 0xE000_0090 | (x << 16) | (c << 8) | x); // MUL x, x, c
435 shift_imm(b, LSR, x, x, 24);
436 }
437
438 let mut b: Vec<u8> = Vec::new();
439 match op {
440 // SetCond: materialize a flags-predicate as 0/1 — the A32 twin of
441 // the Thumb `ITE cond; MOV rd,#1; MOV rd,#0`.
442 ArmOp::SetCond { rd, cond } => {
443 set_cond(&mut b, cond, reg_to_bits(rd));
444 }
445
446 // SelectMove: conditional register move (Thumb: IT cond; MOV).
447 ArmOp::SelectMove { rd, rm, cond } => {
448 w(
449 &mut b,
450 (cond_bits(cond) << 28)
451 | 0x01A0_0000
452 | (reg_to_bits(rd) << 12)
453 | reg_to_bits(rm),
454 );
455 }
456
457 // I64SetCond: compare two i64 register pairs, 0/1 into rd.
458 // EQ/NE: CMP lo,lo; CMPEQ hi,hi (only if lows equal); set.
459 // Ordered: CMP lo,lo; SBCS rd,hi,hi; set — with the same
460 // operand-swap + condition mapping as the Thumb-2 arm.
461 ArmOp::I64SetCond {
462 rd,
463 rn_lo,
464 rn_hi,
465 rm_lo,
466 rm_hi,
467 cond,
468 } => {
469 let rd_b = reg_to_bits(rd);
470 let (n_lo, n_hi, m_lo, m_hi) = (
471 reg_to_bits(rn_lo),
472 reg_to_bits(rn_hi),
473 reg_to_bits(rm_lo),
474 reg_to_bits(rm_hi),
475 );
476 match cond {
477 Condition::EQ | Condition::NE => {
478 cmp_reg(&mut b, n_lo, m_lo);
479 // CMP<EQ> rn_hi, rm_hi — compare highs only if lows equal.
480 w(&mut b, 0x0150_0000 | (n_hi << 16) | m_hi);
481 set_cond(&mut b, cond, rd_b);
482 }
483 // (swap operands?, condition after SBCS) per the Thumb arm:
484 // LT/GE/LO/HS compare (rn, rm); GT/LE/HI/LS swap to (rm, rn).
485 Condition::LT => {
486 cmp_reg(&mut b, n_lo, m_lo);
487 sbcs(&mut b, rd_b, n_hi, m_hi);
488 set_cond(&mut b, &Condition::LT, rd_b);
489 }
490 Condition::GE => {
491 cmp_reg(&mut b, n_lo, m_lo);
492 sbcs(&mut b, rd_b, n_hi, m_hi);
493 set_cond(&mut b, &Condition::GE, rd_b);
494 }
495 Condition::GT => {
496 cmp_reg(&mut b, m_lo, n_lo);
497 sbcs(&mut b, rd_b, m_hi, n_hi);
498 set_cond(&mut b, &Condition::LT, rd_b);
499 }
500 Condition::LE => {
501 cmp_reg(&mut b, m_lo, n_lo);
502 sbcs(&mut b, rd_b, m_hi, n_hi);
503 set_cond(&mut b, &Condition::GE, rd_b);
504 }
505 Condition::LO => {
506 cmp_reg(&mut b, n_lo, m_lo);
507 sbcs(&mut b, rd_b, n_hi, m_hi);
508 set_cond(&mut b, &Condition::LO, rd_b);
509 }
510 Condition::HS => {
511 cmp_reg(&mut b, n_lo, m_lo);
512 sbcs(&mut b, rd_b, n_hi, m_hi);
513 set_cond(&mut b, &Condition::HS, rd_b);
514 }
515 Condition::HI => {
516 cmp_reg(&mut b, m_lo, n_lo);
517 sbcs(&mut b, rd_b, m_hi, n_hi);
518 set_cond(&mut b, &Condition::LO, rd_b);
519 }
520 Condition::LS => {
521 cmp_reg(&mut b, m_lo, n_lo);
522 sbcs(&mut b, rd_b, m_hi, n_hi);
523 set_cond(&mut b, &Condition::HS, rd_b);
524 }
525 }
526 }
527
528 // I64SetCondZ: ORRS rd, lo, hi sets Z iff the pair is zero.
529 ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
530 let rd_b = reg_to_bits(rd);
531 w(
532 &mut b,
533 0xE190_0000 | (reg_to_bits(rn_lo) << 16) | (rd_b << 12) | reg_to_bits(rn_hi),
534 );
535 set_cond(&mut b, &Condition::EQ, rd_b);
536 }
537
538 // i64 comparison wrappers: delegate to I64SetCond/Z, mirroring the
539 // Thumb-2 delegation arms.
540 ArmOp::I64Eqz { rd, rnlo, rnhi } => {
541 return self
542 .encode_arm(&ArmOp::I64SetCondZ {
543 rd: *rd,
544 rn_lo: *rnlo,
545 rn_hi: *rnhi,
546 })
547 .map(Some);
548 }
549 ArmOp::I64Eq {
550 rd,
551 rnlo,
552 rnhi,
553 rmlo,
554 rmhi,
555 }
556 | ArmOp::I64Ne {
557 rd,
558 rnlo,
559 rnhi,
560 rmlo,
561 rmhi,
562 }
563 | ArmOp::I64LtS {
564 rd,
565 rnlo,
566 rnhi,
567 rmlo,
568 rmhi,
569 }
570 | ArmOp::I64LtU {
571 rd,
572 rnlo,
573 rnhi,
574 rmlo,
575 rmhi,
576 }
577 | ArmOp::I64LeS {
578 rd,
579 rnlo,
580 rnhi,
581 rmlo,
582 rmhi,
583 }
584 | ArmOp::I64LeU {
585 rd,
586 rnlo,
587 rnhi,
588 rmlo,
589 rmhi,
590 }
591 | ArmOp::I64GtS {
592 rd,
593 rnlo,
594 rnhi,
595 rmlo,
596 rmhi,
597 }
598 | ArmOp::I64GtU {
599 rd,
600 rnlo,
601 rnhi,
602 rmlo,
603 rmhi,
604 }
605 | ArmOp::I64GeS {
606 rd,
607 rnlo,
608 rnhi,
609 rmlo,
610 rmhi,
611 }
612 | ArmOp::I64GeU {
613 rd,
614 rnlo,
615 rnhi,
616 rmlo,
617 rmhi,
618 } => {
619 let cond = match op {
620 ArmOp::I64Eq { .. } => Condition::EQ,
621 ArmOp::I64Ne { .. } => Condition::NE,
622 ArmOp::I64LtS { .. } => Condition::LT,
623 ArmOp::I64LtU { .. } => Condition::LO,
624 ArmOp::I64LeS { .. } => Condition::LE,
625 ArmOp::I64LeU { .. } => Condition::LS,
626 ArmOp::I64GtS { .. } => Condition::GT,
627 ArmOp::I64GtU { .. } => Condition::HI,
628 ArmOp::I64GeS { .. } => Condition::GE,
629 _ => Condition::HS,
630 };
631 return self
632 .encode_arm(&ArmOp::I64SetCond {
633 rd: *rd,
634 rn_lo: *rnlo,
635 rn_hi: *rnhi,
636 rm_lo: *rmlo,
637 rm_hi: *rmhi,
638 cond,
639 })
640 .map(Some);
641 }
642
643 // I64Mul: cross products into R12, then UMULL — same sequence and
644 // ordering as the Thumb-2 arm (R12 is encoder scratch, #212).
645 ArmOp::I64Mul {
646 rd_lo,
647 rd_hi,
648 rn_lo,
649 rn_hi,
650 rm_lo,
651 rm_hi,
652 } => {
653 let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
654 let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
655 let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
656 // MUL R12, rn_lo, rm_hi (R12 = a_lo * b_hi)
657 w(&mut b, 0xE000_0090 | (12 << 16) | (mh << 8) | nl);
658 // MLA R12, rn_hi, rm_lo, R12 (R12 += a_hi * b_lo)
659 w(
660 &mut b,
661 0xE020_0090 | (12 << 16) | (12 << 12) | (ml << 8) | nh,
662 );
663 // UMULL rd_lo, rd_hi, rn_lo, rm_lo
664 w(
665 &mut b,
666 0xE080_0090 | (dh << 16) | (dl << 12) | (ml << 8) | nl,
667 );
668 // ADD rd_hi, rd_hi, R12
669 w(&mut b, 0xE080_0000 | (dh << 16) | (dh << 12) | 12);
670 }
671
672 // I64Shl / I64ShrU / I64ShrS: same small/large-shift structure as
673 // the Thumb-2 arms. #1048: the expansion must never write its own
674 // input operands — the pre-#1048 A32 arms masked the amount in
675 // place (`AND ml, ml, #63`) and used the amount's home high
676 // register as scratch, identically to the Thumb-2 defect. R12
677 // (encoder scratch, never allocatable, #212) is the ONLY
678 // temporary; the masked amount is re-derived from the untouched
679 // rm_lo where a second live temp would otherwise be needed.
680 // Register-controlled shifts >= 32 yield 0, which the small path
681 // relies on for n = 0. Same #1039-style loud alias guards as the
682 // Thumb-2 arms.
683 ArmOp::I64Shl {
684 rd_lo,
685 rd_hi,
686 rn_lo,
687 rn_hi,
688 rm_lo,
689 rm_hi: _,
690 } => {
691 let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
692 let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
693 let ml = reg_to_bits(rm_lo);
694 if dh == nl || dh == ml {
695 return Err(synth_core::Error::synthesis(format!(
696 "I64Shl (A32): rd_hi {rd_hi:?} aliases an input ({rn_lo:?}/{rm_lo:?}) still live inside the expansion (#1048)"
697 )));
698 }
699 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
700 w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
701 w(&mut b, 0x5A00_0007); // BPL .large
702 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
703 shift_reg(&mut b, LSL, dh, nh, 12); // dh = hi << n
704 w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB r12, r12, #32
705 shift_reg(&mut b, LSR, 12, nl, 12); // r12 = lo >> (32-n)
706 w(&mut b, 0xE180_0000 | (dh << 16) | (dh << 12) | 12); // ORR dh, dh, r12
707 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
708 shift_reg(&mut b, LSL, dl, nl, 12); // dl = lo << n
709 w(&mut b, 0xEA00_0001); // B .done
710 shift_reg(&mut b, LSL, dh, nl, 12); // .large: dh = lo << (n-32)
711 w(&mut b, 0xE3A0_0000 | (dl << 12)); // MOV dl, #0
712 }
713 ArmOp::I64ShrU {
714 rd_lo,
715 rd_hi,
716 rn_lo,
717 rn_hi,
718 rm_lo,
719 rm_hi: _,
720 } => {
721 let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
722 let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
723 let ml = reg_to_bits(rm_lo);
724 if dl == nh || dl == ml {
725 return Err(synth_core::Error::synthesis(format!(
726 "I64ShrU (A32): rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
727 )));
728 }
729 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
730 w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
731 w(&mut b, 0x5A00_0007); // BPL .large
732 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
733 shift_reg(&mut b, LSR, dl, nl, 12); // dl = lo >> n
734 w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB r12, r12, #32
735 shift_reg(&mut b, LSL, 12, nh, 12); // r12 = hi << (32-n)
736 w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | 12); // ORR dl, dl, r12
737 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
738 shift_reg(&mut b, LSR, dh, nh, 12); // dh = hi >> n
739 w(&mut b, 0xEA00_0001); // B .done
740 shift_reg(&mut b, LSR, dl, nh, 12); // .large: dl = hi >> (n-32)
741 w(&mut b, 0xE3A0_0000 | (dh << 12)); // MOV dh, #0
742 }
743 ArmOp::I64ShrS {
744 rd_lo,
745 rd_hi,
746 rn_lo,
747 rn_hi,
748 rm_lo,
749 rm_hi: _,
750 } => {
751 let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
752 let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
753 let ml = reg_to_bits(rm_lo);
754 if dl == nh || dl == ml {
755 return Err(synth_core::Error::synthesis(format!(
756 "I64ShrS (A32): rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
757 )));
758 }
759 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
760 w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
761 w(&mut b, 0x5A00_0007); // BPL .large
762 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
763 shift_reg(&mut b, LSR, dl, nl, 12); // dl = lo >> n
764 w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB r12, r12, #32
765 shift_reg(&mut b, LSL, 12, nh, 12); // r12 = hi << (32-n)
766 w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | 12); // ORR dl, dl, r12
767 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
768 shift_reg(&mut b, ASR, dh, nh, 12); // dh = hi >> n (arith)
769 w(&mut b, 0xEA00_0001); // B .done
770 shift_reg(&mut b, ASR, dl, nh, 12); // .large: dl = hi >> (n-32)
771 w(&mut b, 0xE1A0_0040 | (dh << 12) | (31 << 7) | nh); // ASR dh, nh, #31
772 }
773
774 // I64Rotl / I64Rotr: the #610 fixed-ABI wrapper (A32 form) around
775 // the same fixed-register core as the Thumb-2 arms — value in
776 // R0:R1, amount in R2, scratch R3 + R12.
777 ArmOp::I64Rotl {
778 rdlo,
779 rdhi,
780 rnlo,
781 rnhi,
782 shift,
783 } => {
784 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
785 for word in [
786 0xE202_203Fu32, // AND R2, R2, #63 (mask amount mod 64)
787 0xE252_3020, // SUBS R3, R2, #32 (R3 = n-32, sets N)
788 0x5A00_0007, // BPL .large (n >= 32)
789 // --- small rotation (n < 32) ---
790 0xE262_3020, // RSB R3, R2, #32 (R3 = 32-n)
791 0xE1A0_C330, // LSR R12, R0, R3 (lo >> (32-n))
792 0xE1A0_3331, // LSR R3, R1, R3 (hi >> (32-n))
793 0xE1A0_1211, // LSL R1, R1, R2 (hi << n)
794 0xE181_100C, // ORR R1, R1, R12 (new_hi)
795 0xE1A0_0210, // LSL R0, R0, R2 (lo << n)
796 0xE180_0003, // ORR R0, R0, R3 (new_lo)
797 0xEA00_0007, // B .done
798 // --- large rotation (n >= 32), R3 = m = n-32 ---
799 0xE263_2020, // RSB R2, R3, #32 (R2 = 32-m = 64-n)
800 0xE1A0_C231, // LSR R12, R1, R2 (hi >> (64-n))
801 0xE1A0_2230, // LSR R2, R0, R2 (lo >> (64-n))
802 0xE1A0_0310, // LSL R0, R0, R3 (lo << m)
803 0xE1A0_1311, // LSL R1, R1, R3 (hi << m)
804 0xE180_C00C, // ORR R12, R0, R12 (new_hi = (lo<<m)|(hi>>(64-n)))
805 0xE181_0002, // ORR R0, R1, R2 (new_lo = (hi<<m)|(lo>>(64-n)))
806 0xE1A0_100C, // MOV R1, R12 (new_hi into place)
807 ] {
808 w(&mut b, word);
809 }
810 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
811 }
812 ArmOp::I64Rotr {
813 rdlo,
814 rdhi,
815 rnlo,
816 rnhi,
817 shift,
818 } => {
819 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
820 for word in [
821 0xE202_203Fu32, // AND R2, R2, #63 (mask amount mod 64)
822 0xE252_3020, // SUBS R3, R2, #32 (R3 = n-32, sets N)
823 0x5A00_0007, // BPL .large (n >= 32)
824 // --- small rotation (n < 32) ---
825 0xE262_3020, // RSB R3, R2, #32 (R3 = 32-n)
826 0xE1A0_C311, // LSL R12, R1, R3 (hi << (32-n))
827 0xE1A0_3310, // LSL R3, R0, R3 (lo << (32-n))
828 0xE1A0_0230, // LSR R0, R0, R2 (lo >> n)
829 0xE180_000C, // ORR R0, R0, R12 (new_lo)
830 0xE1A0_1231, // LSR R1, R1, R2 (hi >> n)
831 0xE181_1003, // ORR R1, R1, R3 (new_hi)
832 0xEA00_0007, // B .done
833 // --- large rotation (n >= 32), R3 = m = n-32 ---
834 0xE263_2020, // RSB R2, R3, #32 (R2 = 32-m = 64-n)
835 0xE1A0_C210, // LSL R12, R0, R2 (lo << (64-n))
836 0xE1A0_2211, // LSL R2, R1, R2 (hi << (64-n))
837 0xE1A0_1331, // LSR R1, R1, R3 (hi >> m)
838 0xE181_C00C, // ORR R12, R1, R12 (new_lo = (hi>>m)|(lo<<(64-n)))
839 0xE1A0_1330, // LSR R1, R0, R3 (lo >> m)
840 0xE181_1002, // ORR R1, R1, R2 (new_hi = (lo>>m)|(hi<<(64-n)))
841 0xE1A0_000C, // MOV R0, R12 (new_lo into place)
842 ] {
843 w(&mut b, word);
844 }
845 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
846 }
847
848 // I64Clz: CLZ(hi), or 32 + CLZ(lo) when hi == 0. Conditional
849 // execution replaces the Thumb branches; like the Thumb arm, the
850 // high word of the result pair (rnhi) is cleared last.
851 ArmOp::I64Clz { rd, rnlo, rnhi } => {
852 let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
853 w(&mut b, 0xE350_0000 | (hi << 16)); // CMP rnhi, #0
854 w(&mut b, 0x116F_0F10 | (rd_b << 12) | hi); // CLZNE rd, rnhi
855 w(&mut b, 0x016F_0F10 | (rd_b << 12) | lo); // CLZEQ rd, rnlo
856 w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
857 // #1048: the former trailing `MOV rnhi, #0` is GONE — it
858 // wrote the OPERAND's home high register (see the Thumb-2
859 // I64Clz comment). Callers that relied on the implicit clear
860 // emit their own explicit hi-zero op.
861 }
862
863 // I64Ctz: CLZ(RBIT(lo)), or 32 + CLZ(RBIT(hi)) when lo == 0.
864 // RBIT/CLZ leave the flags intact, so the CMP's Z survives to the
865 // conditional ADD.
866 ArmOp::I64Ctz { rd, rnlo, rnhi } => {
867 let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
868 w(&mut b, 0xE350_0000 | (lo << 16)); // CMP rnlo, #0
869 w(&mut b, 0x16FF_0F30 | (rd_b << 12) | lo); // RBITNE rd, rnlo
870 w(&mut b, 0x06FF_0F30 | (rd_b << 12) | hi); // RBITEQ rd, rnhi
871 w(&mut b, 0xE16F_0F10 | (rd_b << 12) | rd_b); // CLZ rd, rd
872 w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
873 // #1048: no trailing `MOV rnhi, #0` — see I64Clz above.
874 }
875
876 // I64Const: MOVW/MOVT per half (MOVT elided when the half fits in
877 // 16 bits, mirroring the Thumb-2 arm).
878 ArmOp::I64Const { rdlo, rdhi, value } => {
879 let lo32 = *value as u32;
880 let hi32 = (*value >> 32) as u32;
881 movw(&mut b, reg_to_bits(rdlo), lo32 & 0xFFFF);
882 if lo32 > 0xFFFF {
883 movt(&mut b, reg_to_bits(rdlo), lo32 >> 16);
884 }
885 movw(&mut b, reg_to_bits(rdhi), hi32 & 0xFFFF);
886 if hi32 > 0xFFFF {
887 movt(&mut b, reg_to_bits(rdhi), hi32 >> 16);
888 }
889 }
890
891 // I64Ldr / I64Str: two word accesses at [base, #off] / #off+4.
892 // A register offset is materialized into IP once (the #206/#372
893 // hazard: dropping it would read the wrong address).
894 // RQ-63-ARMI64OFF (#1165): an offset the pair form cannot hold
895 // (> 0xFFB, so the high half's +4 leaves imm12) used to be a loud
896 // decline here — 46 of the 110 core-module declines in v0.62's ARM
897 // census, all on the `-b arm` (Arm32) default target. It is now
898 // MATERIALIZED (MOVW/MOVT + ADD, the A32 mirror of #382's Thumb-2
899 // `i64_effective_base`); in-range offsets stay byte-identical.
900 ArmOp::I64Ldr { rdlo, rdhi, addr } | ArmOp::I64Str { rdlo, rdhi, addr } => {
901 let (base, off) = a32_effective_base(&mut b, addr, A32_I64_PAIR_IMM12_MAX)?;
902 let base = reg_to_bits(&base);
903 let opc: u32 = if matches!(op, ArmOp::I64Ldr { .. }) {
904 0xE590_0000 // LDR
905 } else {
906 0xE580_0000 // STR
907 };
908 w(&mut b, opc | (base << 16) | (reg_to_bits(rdlo) << 12) | off);
909 w(
910 &mut b,
911 opc | (base << 16) | (reg_to_bits(rdhi) << 12) | (off + 4),
912 );
913 }
914
915 // I64ExtendI32S: rdlo = rn; rdhi = rdlo >> 31 (arithmetic).
916 ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
917 if rdlo != rn {
918 w(
919 &mut b,
920 0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
921 );
922 }
923 w(
924 &mut b,
925 0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
926 );
927 }
928
929 // I64ExtendI32U: rdlo = rn; rdhi = 0.
930 ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
931 if rdlo != rn {
932 w(
933 &mut b,
934 0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
935 );
936 }
937 w(&mut b, 0xE3A0_0000 | (reg_to_bits(rdhi) << 12));
938 }
939
940 // I64Extend8S / I64Extend16S: SXTB/SXTH then sign-fill the high word.
941 ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
942 w(
943 &mut b,
944 0xE6AF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
945 );
946 w(
947 &mut b,
948 0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
949 );
950 }
951 ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
952 w(
953 &mut b,
954 0xE6BF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
955 );
956 w(
957 &mut b,
958 0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
959 );
960 }
961 ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
962 if rdlo != rnlo {
963 w(
964 &mut b,
965 0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
966 );
967 }
968 w(
969 &mut b,
970 0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rnlo),
971 );
972 }
973
974 // I32WrapI64: take the low word. When rd == rnlo this is a genuine
975 // no-op (the one case where a NOP word is the correct encoding).
976 ArmOp::I32WrapI64 { rd, rnlo } => {
977 w(
978 &mut b,
979 0xE1A0_0000 | (reg_to_bits(rd) << 12) | reg_to_bits(rnlo),
980 );
981 }
982
983 // I64Add / I64Sub: the classic pair — ADDS lo + ADC hi (SUBS/SBC).
984 // The selector emits these as separate Adds/Adc ops; the fused
985 // variants are verification-constructed, but they encode for real.
986 ArmOp::I64Add {
987 rdlo,
988 rdhi,
989 rnlo,
990 rnhi,
991 rmlo,
992 rmhi,
993 } => {
994 dp_reg(
995 &mut b,
996 0xE090_0000, // ADDS
997 reg_to_bits(rdlo),
998 reg_to_bits(rnlo),
999 reg_to_bits(rmlo),
1000 );
1001 dp_reg(
1002 &mut b,
1003 0xE0A0_0000, // ADC
1004 reg_to_bits(rdhi),
1005 reg_to_bits(rnhi),
1006 reg_to_bits(rmhi),
1007 );
1008 }
1009 ArmOp::I64Sub {
1010 rdlo,
1011 rdhi,
1012 rnlo,
1013 rnhi,
1014 rmlo,
1015 rmhi,
1016 } => {
1017 dp_reg(
1018 &mut b,
1019 0xE050_0000, // SUBS
1020 reg_to_bits(rdlo),
1021 reg_to_bits(rnlo),
1022 reg_to_bits(rmlo),
1023 );
1024 dp_reg(
1025 &mut b,
1026 0xE0C0_0000, // SBC
1027 reg_to_bits(rdhi),
1028 reg_to_bits(rnhi),
1029 reg_to_bits(rmhi),
1030 );
1031 }
1032
1033 // I64And / I64Or / I64Xor: two independent word ops.
1034 ArmOp::I64And {
1035 rdlo,
1036 rdhi,
1037 rnlo,
1038 rnhi,
1039 rmlo,
1040 rmhi,
1041 }
1042 | ArmOp::I64Or {
1043 rdlo,
1044 rdhi,
1045 rnlo,
1046 rnhi,
1047 rmlo,
1048 rmhi,
1049 }
1050 | ArmOp::I64Xor {
1051 rdlo,
1052 rdhi,
1053 rnlo,
1054 rnhi,
1055 rmlo,
1056 rmhi,
1057 } => {
1058 let base = match op {
1059 ArmOp::I64And { .. } => 0xE000_0000, // AND
1060 ArmOp::I64Or { .. } => 0xE180_0000, // ORR
1061 _ => 0xE020_0000, // EOR
1062 };
1063 dp_reg(
1064 &mut b,
1065 base,
1066 reg_to_bits(rdlo),
1067 reg_to_bits(rnlo),
1068 reg_to_bits(rmlo),
1069 );
1070 dp_reg(
1071 &mut b,
1072 base,
1073 reg_to_bits(rdhi),
1074 reg_to_bits(rnhi),
1075 reg_to_bits(rmhi),
1076 );
1077 }
1078
1079 // I64DivU: binary long division — A32 transcription of the Thumb-2
1080 // #610/#613 arm (fixed-ABI marshal, zero-divisor trap, 64-round
1081 // shift-subtract core, quotient to R0:R1, result to rd pair).
1082 ArmOp::I64DivU {
1083 rdlo,
1084 rdhi,
1085 rnlo,
1086 rnhi,
1087 rmlo,
1088 rmhi,
1089 elide_zero_guard,
1090 } => {
1091 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1092 // #494 phase 2b: elided only under a certificate-discharged
1093 // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
1094 if !elide_zero_guard {
1095 emit_a32_i64_divisor_zero_trap(&mut b);
1096 }
1097 w(&mut b, 0xE92D_00F0); // PUSH {R4-R7}
1098 for r in 4..8u32 {
1099 w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1100 }
1101 div_loop(&mut b, 12); // counter in R12 (encoder scratch)
1102 w(&mut b, 0xE1A0_0004); // MOV R0, R4 (quotient lo)
1103 w(&mut b, 0xE1A0_1005); // MOV R1, R5 (quotient hi)
1104 w(&mut b, 0xE8BD_00F0); // POP {R4-R7}
1105 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1106 }
1107
1108 // I64DivS: sign-extract, unsigned core, conditional negate —
1109 // A32 transcription of the Thumb-2 arm.
1110 ArmOp::I64DivS {
1111 rdlo,
1112 rdhi,
1113 rnlo,
1114 rnhi,
1115 rmlo,
1116 rmhi,
1117 elide_zero_guard,
1118 elide_overflow_guard,
1119 } => {
1120 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1121 // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
1122 // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
1123 // the #633 overflow guard falls ONLY to
1124 // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
1125 // divisor-nonzero fact alone must keep it.
1126 if !elide_zero_guard {
1127 emit_a32_i64_divisor_zero_trap(&mut b);
1128 }
1129 if !elide_overflow_guard {
1130 // #633: INT64_MIN / -1 overflows — trap like the i32 path
1131 // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
1132 emit_a32_i64_divs_overflow_trap(&mut b);
1133 }
1134 w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1135 w(&mut b, 0xE021_9003); // EOR R9, R1, R3 (result sign in MSB)
1136 skip_negate_if_positive(&mut b, 1);
1137 negate64(&mut b, 0, 1);
1138 skip_negate_if_positive(&mut b, 3);
1139 negate64(&mut b, 2, 3);
1140 for r in 4..8u32 {
1141 w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1142 }
1143 div_loop(&mut b, 8); // counter in R8 (saved above)
1144 w(&mut b, 0xE1A0_0004); // MOV R0, R4
1145 w(&mut b, 0xE1A0_1005); // MOV R1, R5
1146 skip_negate_if_positive(&mut b, 9);
1147 negate64(&mut b, 0, 1);
1148 w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1149 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1150 }
1151
1152 // I64RemU: same core as I64DivU, returns the remainder (R6:R7).
1153 ArmOp::I64RemU {
1154 rdlo,
1155 rdhi,
1156 rnlo,
1157 rnhi,
1158 rmlo,
1159 rmhi,
1160 elide_zero_guard,
1161 } => {
1162 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1163 if !elide_zero_guard {
1164 emit_a32_i64_divisor_zero_trap(&mut b);
1165 }
1166 w(&mut b, 0xE92D_01F0); // PUSH {R4-R8}
1167 for r in 4..8u32 {
1168 w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1169 }
1170 div_loop(&mut b, 8);
1171 w(&mut b, 0xE1A0_0006); // MOV R0, R6 (remainder lo)
1172 w(&mut b, 0xE1A0_1007); // MOV R1, R7 (remainder hi)
1173 w(&mut b, 0xE8BD_01F0); // POP {R4-R8}
1174 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1175 }
1176
1177 // I64RemS: remainder takes the DIVIDEND's sign (WASM semantics).
1178 ArmOp::I64RemS {
1179 rdlo,
1180 rdhi,
1181 rnlo,
1182 rnhi,
1183 rmlo,
1184 rmhi,
1185 elide_zero_guard,
1186 } => {
1187 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1188 if !elide_zero_guard {
1189 emit_a32_i64_divisor_zero_trap(&mut b);
1190 }
1191 w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1192 w(&mut b, 0xE1A0_9001); // MOV R9, R1 (dividend sign)
1193 skip_negate_if_positive(&mut b, 1);
1194 negate64(&mut b, 0, 1);
1195 skip_negate_if_positive(&mut b, 3);
1196 negate64(&mut b, 2, 3);
1197 for r in 4..8u32 {
1198 w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1199 }
1200 div_loop(&mut b, 8);
1201 w(&mut b, 0xE1A0_0006); // MOV R0, R6
1202 w(&mut b, 0xE1A0_1007); // MOV R1, R7
1203 skip_negate_if_positive(&mut b, 9);
1204 negate64(&mut b, 0, 1);
1205 w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1206 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1207 }
1208
1209 // Popcnt (i32): bit-twiddle expansion (no native A32 popcount),
1210 // mirroring the Thumb-2 arm's #1021 register contract: R12 is the
1211 // ONLY scratch. The previous transcription copied the old Thumb
1212 // contract's R11 borrow — but R11 is the linear-memory base on
1213 // this path too, so it inherited the same live miscompile. A32
1214 // has no ThumbExpandImm for 0xXYXYXYXY masks, so instead the
1215 // barrel shifter folds each shift into the mask AND itself
1216 // (`AND R12, R12, rd, LSR #n`), and step 2 recovers `x & C` from
1217 // one term via `x - (((x >> 2) & C) << 2) = x & C` — the second
1218 // temp disappears algebraically. Straight-line, no PUSH/POP,
1219 // nothing to skip on a trap edge.
1220 ArmOp::Popcnt { rd, rm } => {
1221 let rd_b = reg_to_bits(rd);
1222 // Defensive (#1021), same contract as the Thumb-2 arm.
1223 if rd_b >= 11 {
1224 return Err(synth_core::Error::synthesis(
1225 "Popcnt destination must be R0-R10: R11 is the linear-memory \
1226 base and R12 is the expansion's scratch (#1021)",
1227 ));
1228 }
1229 if rd != rm {
1230 w(&mut b, 0xE1A0_0000 | (rd_b << 12) | reg_to_bits(rm)); // MOV rd, rm
1231 }
1232 // x = x - ((x >> 1) & 0x55555555)
1233 movw(&mut b, 12, 0x5555);
1234 movt(&mut b, 12, 0x5555);
1235 dp_reg_shift(&mut b, 0xE000_0000, 12, 12, rd_b, LSR, 1); // AND R12, R12, rd, LSR #1
1236 dp_reg(&mut b, 0xE040_0000, rd_b, rd_b, 12); // SUB rd, rd, R12
1237 // x = (x & 0x33333333) + ((x >> 2) & 0x33333333), one temp:
1238 // R12 = (x >> 2) & C; x - (R12 << 2) = x & C; then + R12.
1239 movw(&mut b, 12, 0x3333);
1240 movt(&mut b, 12, 0x3333);
1241 dp_reg_shift(&mut b, 0xE000_0000, 12, 12, rd_b, LSR, 2); // AND R12, R12, rd, LSR #2
1242 dp_reg_shift(&mut b, 0xE040_0000, rd_b, rd_b, 12, LSL, 2); // SUB rd, rd, R12, LSL #2
1243 dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 12); // ADD rd, rd, R12
1244 // x = (x + (x >> 4)) & 0x0F0F0F0F
1245 dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 4); // ADD rd, rd, rd, LSR #4
1246 movw(&mut b, 12, 0x0F0F);
1247 movt(&mut b, 12, 0x0F0F);
1248 dp_reg(&mut b, 0xE000_0000, rd_b, rd_b, 12); // AND rd, rd, R12
1249 // x += x >> 8; x += x >> 16; x &= 0x3F
1250 dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 8);
1251 dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 16);
1252 w(&mut b, 0xE200_003F | (rd_b << 16) | (rd_b << 12)); // AND rd, rd, #63
1253 }
1254
1255 // I64Popcnt: POPCNT(lo) + POPCNT(hi) — A32 transcription of the
1256 // Thumb-2 arm (R3/R4/R5 saved, mul-based per-word fold, high
1257 // result word rnhi cleared last, mirroring the Thumb contract).
1258 ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
1259 let hi = reg_to_bits(rnhi);
1260 w(&mut b, 0xE92D_0038); // PUSH {R3, R4, R5}
1261 // #632 audit: route rnlo through R12 so a pair living at
1262 // (R3,R4) cannot read a clobbered R4 (sources read before any
1263 // scratch register they could occupy is written).
1264 w(&mut b, 0xE1A0_C000 | reg_to_bits(rnlo)); // MOV R12, rnlo
1265 w(&mut b, 0xE1A0_5000 | hi); // MOV R5, rnhi
1266 w(&mut b, 0xE1A0_400C); // MOV R4, R12
1267 popcnt_word(&mut b, 4, 3);
1268 popcnt_word(&mut b, 5, 3);
1269 // #632: carry the count across the scratch restore in R12 —
1270 // rd is allocator-assigned and can land inside {R3,R4,R5};
1271 // the old `ADD rd, R4, R5` before the POP was destroyed by
1272 // the restore. R12 is never allocatable and never restored.
1273 dp_reg(&mut b, 0xE080_0000, 12, 4, 5); // ADD R12, R4, R5
1274 w(&mut b, 0xE8BD_0038); // POP {R3, R4, R5}
1275 w(&mut b, 0xE1A0_0000 | (reg_to_bits(rd) << 12) | 12); // MOV rd, R12
1276 // #1048: no trailing `MOV rnhi, #0` — the hi-word clear wrote
1277 // the OPERAND's home high register; callers emit it explicitly.
1278 }
1279
1280 _ => return Ok(None),
1281 }
1282 Ok(Some(b))
1283 }
1284
1285 fn encode_arm(&self, op: &ArmOp) -> Result<Vec<u8>> {
1286 // #615: A32 multi-instruction expansions (i64 arithmetic/shift/rotate/
1287 // compare, SetCond/SelectMove, popcnt, ...). These ops were literal
1288 // NOPs on the A32 path — user-reachable via `--target cortex-r5` —
1289 // so the value silently vanished. Mirror of the #594 CallIndirect
1290 // early-return: if the expansion helper covers the op, its bytes are
1291 // the encoding.
1292 if let Some(bytes) = self.encode_arm_expanded(op)? {
1293 return Ok(bytes);
1294 }
1295 // #206: ARM32 register-offset loads/stores. `encode_mem_addr` only
1296 // returns the 12-bit immediate, so the immediate-form arms below
1297 // silently DROP `addr.offset_reg` — a runtime address index vanished,
1298 // turning `ldr rd,[rn,rm,#off]` into `ldr rd,[rn,#off]` (the access went
1299 // to the wrong address). Compute the effective base into IP and re-encode
1300 // against `[ip, #off]`, which is uniform for word/byte/halfword/signed.
1301 if let Some(bytes) = self.encode_arm_reg_offset_mem(op)? {
1302 return Ok(bytes);
1303 }
1304 // #594: call_indirect was encoded as a literal NOP on the A32 path
1305 // (`--target cortex-r5`) — the call never happened and the function
1306 // silently returned garbage. Emit the same three-instruction expansion
1307 // as the Thumb-2 path (R11 = function-pointer table base, R12 scratch):
1308 // MOV r12, idx, LSL #2 ; LDR r12, [r11, r12] ; BLX r12
1309 if let ArmOp::CallIndirect {
1310 table_index_reg,
1311 table_size,
1312 table_byte_offset,
1313 null_check,
1314 type_check,
1315 ..
1316 } = op
1317 {
1318 return Ok(Self::encode_arm_call_indirect(
1319 table_index_reg,
1320 *table_size,
1321 *table_byte_offset,
1322 *null_check,
1323 *type_check,
1324 ));
1325 }
1326 let instr: u32 = match op {
1327 // Data processing instructions
1328 ArmOp::Add { rd, rn, op2 } => {
1329 let rd_bits = reg_to_bits(rd);
1330 let rn_bits = reg_to_bits(rn);
1331 let (op2_bits, i_flag) = encode_operand2(op2)?;
1332
1333 // ADD encoding: cond(4) | 00 | I(1) | 0100 | S(1) | Rn(4) | Rd(4) | operand2(12)
1334 0xE0800000 // condition=always(E), opcode=ADD(0100), S=0
1335 | (i_flag << 25)
1336 | (rn_bits << 16)
1337 | (rd_bits << 12)
1338 | op2_bits
1339 }
1340
1341 ArmOp::Sub { rd, rn, op2 } => {
1342 let rd_bits = reg_to_bits(rd);
1343 let rn_bits = reg_to_bits(rn);
1344 let (op2_bits, i_flag) = encode_operand2(op2)?;
1345
1346 // SUB encoding: opcode=0010
1347 0xE0400000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1348 }
1349
1350 // i64 support: ADDS, ADC, SUBS, SBC for ARM32
1351 ArmOp::Adds { rd, rn, op2 } => {
1352 let rd_bits = reg_to_bits(rd);
1353 let rn_bits = reg_to_bits(rn);
1354 let (op2_bits, i_flag) = encode_operand2(op2)?;
1355
1356 // ADDS encoding: opcode=0100, S=1
1357 0xE0900000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1358 }
1359
1360 ArmOp::Adc { rd, rn, op2 } => {
1361 let rd_bits = reg_to_bits(rd);
1362 let rn_bits = reg_to_bits(rn);
1363 let (op2_bits, i_flag) = encode_operand2(op2)?;
1364
1365 // ADC encoding: opcode=0101
1366 0xE0A00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1367 }
1368
1369 ArmOp::Subs { rd, rn, op2 } => {
1370 let rd_bits = reg_to_bits(rd);
1371 let rn_bits = reg_to_bits(rn);
1372 let (op2_bits, i_flag) = encode_operand2(op2)?;
1373
1374 // SUBS encoding: opcode=0010, S=1
1375 0xE0500000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1376 }
1377
1378 ArmOp::Sbc { rd, rn, op2 } => {
1379 let rd_bits = reg_to_bits(rd);
1380 let rn_bits = reg_to_bits(rn);
1381 let (op2_bits, i_flag) = encode_operand2(op2)?;
1382
1383 // SBC encoding: opcode=0110
1384 0xE0C00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1385 }
1386
1387 ArmOp::Mul { rd, rn, rm } => {
1388 let rd_bits = reg_to_bits(rd);
1389 let rn_bits = reg_to_bits(rn);
1390 let rm_bits = reg_to_bits(rm);
1391
1392 // MUL encoding: cond(4) | 000000 | A(1) | S(1) | Rd(4) | Rn(4) | Rs(4) | 1001 | Rm(4)
1393 0xE0000090 | (rd_bits << 16) | (rn_bits << 8) | rm_bits
1394 }
1395
1396 ArmOp::Umull { rdlo, rdhi, rn, rm } => {
1397 let rdlo_bits = reg_to_bits(rdlo);
1398 let rdhi_bits = reg_to_bits(rdhi);
1399 let rn_bits = reg_to_bits(rn);
1400 let rm_bits = reg_to_bits(rm);
1401
1402 // UMULL encoding: cond(4) | 0000 1000 | RdHi(4) | RdLo(4) | Rm(4) | 1001 | Rn(4)
1403 0xE0800090 | (rdhi_bits << 16) | (rdlo_bits << 12) | (rm_bits << 8) | rn_bits
1404 }
1405
1406 ArmOp::Sdiv { rd, rn, rm } => {
1407 let rd_bits = reg_to_bits(rd);
1408 let rn_bits = reg_to_bits(rn);
1409 let rm_bits = reg_to_bits(rm);
1410
1411 // SDIV encoding: cond(4) | 01110001 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1412 // ARMv7-M and above
1413 0xE710F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1414 }
1415
1416 ArmOp::Udiv { rd, rn, rm } => {
1417 let rd_bits = reg_to_bits(rd);
1418 let rn_bits = reg_to_bits(rn);
1419 let rm_bits = reg_to_bits(rm);
1420
1421 // UDIV encoding: cond(4) | 01110011 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1422 // ARMv7-M and above
1423 0xE730F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1424 }
1425
1426 ArmOp::Mls { rd, rn, rm, ra } => {
1427 let rd_bits = reg_to_bits(rd);
1428 let rn_bits = reg_to_bits(rn);
1429 let rm_bits = reg_to_bits(rm);
1430 let ra_bits = reg_to_bits(ra);
1431
1432 // MLS encoding: cond(4) | 00000110 | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1433 // Rd = Ra - (Rn * Rm)
1434 0xE0600090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1435 }
1436
1437 ArmOp::Mla { rd, rn, rm, ra } => {
1438 let rd_bits = reg_to_bits(rd);
1439 let rn_bits = reg_to_bits(rn);
1440 let rm_bits = reg_to_bits(rm);
1441 let ra_bits = reg_to_bits(ra);
1442
1443 // MLA encoding: cond(4) | 0000001 S | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1444 // Rd = Ra + (Rn * Rm). Base 0xE0200090 (S=0).
1445 0xE0200090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1446 }
1447
1448 ArmOp::And { rd, rn, op2 } => {
1449 let rd_bits = reg_to_bits(rd);
1450 let rn_bits = reg_to_bits(rn);
1451 let (op2_bits, i_flag) = encode_operand2(op2)?;
1452
1453 // AND encoding: opcode=0000
1454 0xE0000000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1455 }
1456
1457 ArmOp::Orr { rd, rn, op2 } => {
1458 let rd_bits = reg_to_bits(rd);
1459 let rn_bits = reg_to_bits(rn);
1460 let (op2_bits, i_flag) = encode_operand2(op2)?;
1461
1462 // ORR encoding: opcode=1100
1463 0xE1800000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1464 }
1465
1466 ArmOp::Eor { rd, rn, op2 } => {
1467 let rd_bits = reg_to_bits(rd);
1468 let rn_bits = reg_to_bits(rn);
1469 let (op2_bits, i_flag) = encode_operand2(op2)?;
1470
1471 // EOR encoding: opcode=0001
1472 0xE0200000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1473 }
1474
1475 // Shift instructions
1476 ArmOp::Lsl { rd, rn, shift } => {
1477 let rd_bits = reg_to_bits(rd);
1478 let rn_bits = reg_to_bits(rn);
1479 let shift_bits = *shift & 0x1F;
1480
1481 // LSL encoding: MOV with shift
1482 0xE1A00000 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1483 }
1484
1485 ArmOp::Lsr { rd, rn, shift } => {
1486 let rd_bits = reg_to_bits(rd);
1487 let rn_bits = reg_to_bits(rn);
1488 let shift_bits = *shift & 0x1F;
1489
1490 // LSR encoding
1491 0xE1A00020 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1492 }
1493
1494 ArmOp::Asr { rd, rn, shift } => {
1495 let rd_bits = reg_to_bits(rd);
1496 let rn_bits = reg_to_bits(rn);
1497 let shift_bits = *shift & 0x1F;
1498
1499 // ASR encoding
1500 0xE1A00040 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1501 }
1502
1503 ArmOp::Ror { rd, rn, shift } => {
1504 let rd_bits = reg_to_bits(rd);
1505 let rn_bits = reg_to_bits(rn);
1506 let shift_bits = *shift & 0x1F;
1507
1508 // ROR encoding: MOV with ROR shift
1509 0xE1A00060 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1510 }
1511
1512 // Register-based shifts (ARM32)
1513 // LSL Rd, Rn, Rm: cond 0001101S 0000 Rd Rs 0001 Rn
1514 ArmOp::LslReg { rd, rn, rm } => {
1515 let rd_bits = reg_to_bits(rd);
1516 let rn_bits = reg_to_bits(rn);
1517 let rm_bits = reg_to_bits(rm);
1518 0xE1A00010 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1519 }
1520 ArmOp::LsrReg { rd, rn, rm } => {
1521 let rd_bits = reg_to_bits(rd);
1522 let rn_bits = reg_to_bits(rn);
1523 let rm_bits = reg_to_bits(rm);
1524 0xE1A00030 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1525 }
1526 ArmOp::AsrReg { rd, rn, rm } => {
1527 let rd_bits = reg_to_bits(rd);
1528 let rn_bits = reg_to_bits(rn);
1529 let rm_bits = reg_to_bits(rm);
1530 0xE1A00050 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1531 }
1532 ArmOp::RorReg { rd, rn, rm } => {
1533 let rd_bits = reg_to_bits(rd);
1534 let rn_bits = reg_to_bits(rn);
1535 let rm_bits = reg_to_bits(rm);
1536 0xE1A00070 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1537 }
1538
1539 // RSB (Reverse Subtract): Rd = imm - Rn
1540 ArmOp::Rsb { rd, rn, imm } => {
1541 let rd_bits = reg_to_bits(rd);
1542 let rn_bits = reg_to_bits(rn);
1543 // RSB encoding: cond(4) | 00 1 0011 S | Rn(4) | Rd(4) | imm12
1544 // Opcode for RSB = 0011, I=1 (immediate), S=0
1545 //
1546 // #681 class audit: the A32 imm12 is a rotate(4):imm8 modified
1547 // immediate; `*imm & 0xFF` silently encoded a WRONG constant
1548 // for imm > 0xFF (#378 masking class). All current emitters use
1549 // imm 32, so erroring here is byte-identical for real codegen.
1550 if *imm > 0xFF {
1551 return Err(synth_core::Error::synthesis(
1552 "A32 RSB immediate > 0xFF requires a rotated-immediate encoding \
1553 (not supported) — materialize into a register",
1554 ));
1555 }
1556 0xE2600000 | (rn_bits << 16) | (rd_bits << 12) | (*imm & 0xFF)
1557 }
1558
1559 // Bit manipulation instructions
1560 ArmOp::Clz { rd, rm } => {
1561 let rd_bits = reg_to_bits(rd);
1562 let rm_bits = reg_to_bits(rm);
1563
1564 // CLZ encoding: cond(4) | 00010110 | 1111 | Rd(4) | 1111 | 0001 | Rm(4)
1565 // ARMv5T and above
1566 0xE16F0F10 | (rd_bits << 12) | rm_bits
1567 }
1568
1569 ArmOp::Rbit { rd, rm } => {
1570 let rd_bits = reg_to_bits(rd);
1571 let rm_bits = reg_to_bits(rm);
1572
1573 // RBIT encoding: cond(4) | 01101111 | 1111 | Rd(4) | 1111 | 0011 | Rm(4)
1574 // ARMv6T2 and above
1575 0xE6FF0F30 | (rd_bits << 12) | rm_bits
1576 }
1577
1578 ArmOp::Sxtb { rd, rm } => {
1579 let rd_bits = reg_to_bits(rd);
1580 let rm_bits = reg_to_bits(rm);
1581
1582 // SXTB encoding: cond(4) | 01101010 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1583 // ARMv6 and above. rotate=00 for no rotation
1584 0xE6AF0070 | (rd_bits << 12) | rm_bits
1585 }
1586
1587 ArmOp::Sxth { rd, rm } => {
1588 let rd_bits = reg_to_bits(rd);
1589 let rm_bits = reg_to_bits(rm);
1590
1591 // SXTH encoding: cond(4) | 01101011 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1592 // ARMv6 and above. rotate=00 for no rotation
1593 0xE6BF0070 | (rd_bits << 12) | rm_bits
1594 }
1595
1596 ArmOp::Uxtb { rd, rm } => {
1597 let rd_bits = reg_to_bits(rd);
1598 let rm_bits = reg_to_bits(rm);
1599 // UXTB encoding: cond | 01101110 1111 Rd rotate 00 0111 Rm (rotate=00)
1600 0xE6EF0070 | (rd_bits << 12) | rm_bits
1601 }
1602
1603 ArmOp::Uxth { rd, rm } => {
1604 let rd_bits = reg_to_bits(rd);
1605 let rm_bits = reg_to_bits(rm);
1606 // UXTH encoding: cond | 01101111 1111 Rd rotate 00 0111 Rm (rotate=00)
1607 0xE6FF0070 | (rd_bits << 12) | rm_bits
1608 }
1609
1610 // Move instructions
1611 ArmOp::Mov { rd, op2 } => {
1612 let rd_bits = reg_to_bits(rd);
1613 let (op2_bits, i_flag) = encode_operand2(op2)?;
1614
1615 // MOV encoding: opcode=1101
1616 0xE1A00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1617 }
1618
1619 ArmOp::Mvn { rd, op2 } => {
1620 let rd_bits = reg_to_bits(rd);
1621 let (op2_bits, i_flag) = encode_operand2(op2)?;
1622
1623 // MVN encoding: opcode=1111
1624 0xE1E00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1625 }
1626
1627 // MOVW - Move Wide (ARM32)
1628 // Encoding: cond(4) | 0011 0000 | imm4(4) | Rd(4) | imm12(12)
1629 ArmOp::Movw { rd, imm16 } => {
1630 let rd_bits = reg_to_bits(rd);
1631 let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1632 let imm12 = (*imm16 as u32) & 0xFFF;
1633 0xE3000000 | (imm4 << 16) | (rd_bits << 12) | imm12
1634 }
1635
1636 // MOVT - Move Top (ARM32)
1637 // Encoding: cond(4) | 0011 0100 | imm4(4) | Rd(4) | imm12(12)
1638 ArmOp::Movt { rd, imm16 } => {
1639 let rd_bits = reg_to_bits(rd);
1640 let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1641 let imm12 = (*imm16 as u32) & 0xFFF;
1642 0xE3400000 | (imm4 << 16) | (rd_bits << 12) | imm12
1643 }
1644
1645 // #237: symbol-relative MOVW/MOVT (ARM mode) — addend in place, the
1646 // backend records the MOVW_ABS/MOVT_ABS relocation against `symbol`.
1647 ArmOp::MovwSym { rd, addend, .. } => {
1648 let rd_bits = reg_to_bits(rd);
1649 let v = (*addend as u32) & 0xffff;
1650 0xE3000000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1651 }
1652 ArmOp::MovtSym { rd, addend, .. } => {
1653 let rd_bits = reg_to_bits(rd);
1654 let v = ((*addend as u32) >> 16) & 0xffff;
1655 0xE3400000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1656 }
1657
1658 // #345: LdrSym is the Thumb-2 literal-pool address load. A32 mode is
1659 // not used for relocatable native-pointer objects; fail loudly rather
1660 // than miscompile if it is ever reached here.
1661 ArmOp::LdrSym { .. } => {
1662 return Err(synth_core::Error::synthesis(
1663 "LdrSym (literal-pool address load) is Thumb-2-only",
1664 ));
1665 }
1666
1667 // Compare
1668 ArmOp::Cmp { rn, op2 } => {
1669 let rn_bits = reg_to_bits(rn);
1670 let (op2_bits, i_flag) = encode_operand2(op2)?;
1671
1672 // CMP encoding: opcode=1010, S=1
1673 0xE1500000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1674 }
1675
1676 // Compare Negative (CMN) - computes Rn + op2 and sets flags
1677 ArmOp::Cmn { rn, op2 } => {
1678 let rn_bits = reg_to_bits(rn);
1679 let (op2_bits, i_flag) = encode_operand2(op2)?;
1680
1681 // CMN encoding: opcode=1011, S=1
1682 0xE1700000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1683 }
1684
1685 // Load/Store
1686 ArmOp::Ldr { rd, addr } => {
1687 let rd_bits = reg_to_bits(rd);
1688 let (base_bits, offset_bits) = encode_mem_addr(addr)?;
1689
1690 // LDR encoding: cond(4) | 01 | I(1) | P(1) | U(1) | B(1) | W(1) | L(1) | Rn(4) | Rd(4) | offset(12)
1691 // P=1 (pre-indexed), U=1 (add offset), L=1 (load)
1692 0xE5900000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1693 }
1694
1695 ArmOp::Str { rd, addr } => {
1696 let rd_bits = reg_to_bits(rd);
1697 let (base_bits, offset_bits) = encode_mem_addr(addr)?;
1698
1699 // STR encoding: L=0 (store)
1700 0xE5800000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1701 }
1702
1703 // Sub-word loads (ARM32 encoding)
1704 ArmOp::Ldrb { rd, addr } => {
1705 let rd_bits = reg_to_bits(rd);
1706 let (base_bits, offset_bits) = encode_mem_addr(addr)?;
1707 // LDRB: LDR with B=1 (byte): cond|01|I|P|U|1|W|L|Rn|Rd|offset
1708 0xE5D00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1709 }
1710
1711 ArmOp::Ldrsb { rd, addr } => {
1712 let rd_bits = reg_to_bits(rd);
1713 let (base_bits, offset_val) = encode_mem_addr_imm8(addr)?;
1714 // LDRSB (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1101|imm4L
1715 let imm4h = (offset_val >> 4) & 0xF;
1716 let imm4l = offset_val & 0xF;
1717 0xE1D000D0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1718 }
1719
1720 ArmOp::Ldrh { rd, addr } => {
1721 let rd_bits = reg_to_bits(rd);
1722 let (base_bits, offset_val) = encode_mem_addr_imm8(addr)?;
1723 // LDRH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1011|imm4L
1724 let imm4h = (offset_val >> 4) & 0xF;
1725 let imm4l = offset_val & 0xF;
1726 0xE1D000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1727 }
1728
1729 ArmOp::Ldrsh { rd, addr } => {
1730 let rd_bits = reg_to_bits(rd);
1731 let (base_bits, offset_val) = encode_mem_addr_imm8(addr)?;
1732 // LDRSH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1111|imm4L
1733 let imm4h = (offset_val >> 4) & 0xF;
1734 let imm4l = offset_val & 0xF;
1735 0xE1D000F0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1736 }
1737
1738 // Sub-word stores (ARM32 encoding)
1739 ArmOp::Strb { rd, addr } => {
1740 let rd_bits = reg_to_bits(rd);
1741 let (base_bits, offset_bits) = encode_mem_addr(addr)?;
1742 // STRB: STR with B=1 (byte): cond|01|I|P|U|1|W|0|Rn|Rd|offset
1743 0xE5C00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1744 }
1745
1746 ArmOp::Strh { rd, addr } => {
1747 let rd_bits = reg_to_bits(rd);
1748 let (base_bits, offset_val) = encode_mem_addr_imm8(addr)?;
1749 // STRH (misc store): cond|000|P|U|1|W|0|Rn|Rd|imm4H|1011|imm4L
1750 let imm4h = (offset_val >> 4) & 0xF;
1751 let imm4l = offset_val & 0xF;
1752 0xE1C000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1753 }
1754
1755 // Memory management (ARM32 encoding)
1756 ArmOp::MemorySize { rd } => {
1757 let rd_bits = reg_to_bits(rd);
1758 // MOV rd, R10, LSR #16 (memory size in bytes / 65536 = pages)
1759 // cond|000|1101|S|0000|Rd|shift5|type|0|Rm
1760 // LSR #16: shift5=10000, type=01
1761 0xE1A00820 | (rd_bits << 12) | 0x0A // Rm=R10, shift=16, LSR
1762 }
1763
1764 ArmOp::MemoryGrow { rd, .. } => {
1765 let rd_bits = reg_to_bits(rd);
1766 // On embedded, always fail: MOV rd, #-1
1767 0xE3E00000 | (rd_bits << 12) // MVN rd, #0 = MOV rd, #-1
1768 }
1769
1770 // Label pseudo-instruction: emits no machine code
1771 ArmOp::Label { .. } => {
1772 return Ok(Vec::new());
1773 }
1774
1775 // Branch instructions
1776 ArmOp::B { label: _ } => {
1777 // B encoding: cond(4) | 1010 | offset(24)
1778 // Simplified: branch to offset 0 (will be patched by linker/resolver)
1779 0xEA000000
1780 }
1781
1782 // Conditional branch to label (generic)
1783 ArmOp::Bcc { cond, label: _ } => {
1784 use synth_synthesis::Condition;
1785 let cond_bits: u32 = match cond {
1786 Condition::EQ => 0x0,
1787 Condition::NE => 0x1,
1788 Condition::HS => 0x2,
1789 Condition::LO => 0x3,
1790 Condition::HI => 0x8,
1791 Condition::LS => 0x9,
1792 Condition::GE => 0xA,
1793 Condition::LT => 0xB,
1794 Condition::GT => 0xC,
1795 Condition::LE => 0xD,
1796 };
1797 // B<cond> with offset 0 (will be patched)
1798 (cond_bits << 28) | 0x0A000000
1799 }
1800
1801 // BHS (Branch if Higher or Same) - used for bounds checking
1802 ArmOp::Bhs { label: _ } => {
1803 // BHS encoding: cond(2=HS) | 1010 | offset(24)
1804 0x2A000000 // BHS with offset 0
1805 }
1806
1807 // BLO (Branch if Lower) - complementary to BHS
1808 ArmOp::Blo { label: _ } => {
1809 // BLO encoding: cond(3=LO) | 1010 | offset(24)
1810 0x3A000000 // BLO with offset 0
1811 }
1812
1813 // Branch with numeric offset (in instructions)
1814 // ARM32 B instruction: offset is in instructions, stored as words
1815 // The offset is relative to PC+8 (due to ARM pipeline)
1816 ArmOp::BOffset { offset } => {
1817 // B encoding: cond(4) | 1010 | offset(24)
1818 // Offset is signed, in words (4-byte units)
1819 // ARM adds PC+8 to the offset, so we need to adjust:
1820 // target = PC + 8 + (offset * 4)
1821 // For backward branch of N instructions: offset = -(N + 2)
1822 // wrapping_sub keeps the encoder total under fuzzing (#186): an
1823 // extreme i32::MIN offset would otherwise overflow-panic; for any
1824 // real branch offset this is identical to `- 2`.
1825 let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1826 let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1827 0xEA000000 | offset_bits
1828 }
1829
1830 // Conditional branch with numeric offset
1831 ArmOp::BCondOffset { cond, offset } => {
1832 use synth_synthesis::Condition;
1833 let cond_bits: u32 = match cond {
1834 Condition::EQ => 0x0,
1835 Condition::NE => 0x1,
1836 Condition::HS => 0x2,
1837 Condition::LO => 0x3,
1838 Condition::HI => 0x8,
1839 Condition::LS => 0x9,
1840 Condition::GE => 0xA,
1841 Condition::LT => 0xB,
1842 Condition::GT => 0xC,
1843 Condition::LE => 0xD,
1844 };
1845 // B<cond> encoding: cond(4) | 1010 | offset(24)
1846 // wrapping_sub: total under fuzzing (#186), identical for real offsets.
1847 let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1848 let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1849 (cond_bits << 28) | 0x0A000000 | offset_bits
1850 }
1851
1852 ArmOp::Bl { label: _ } => {
1853 // BL encoding: cond(4) | 1011 | offset(24). Relocatable
1854 // placeholder; an R_ARM_CALL relocation patches the target.
1855 //
1856 // #1040: the placeholder must carry an embedded addend of -8,
1857 // not 0. A32 `BL` computes `target = P + 8 + (imm24 << 2)`, so
1858 // under REL semantics a 0 addend (`eb000000`) resolves two
1859 // instructions PAST the callee entry — the A32 twin of the
1860 // Thumb #174 bug. The correct word is what `gas` emits for
1861 // `bl <extern>` in ARM mode:
1862 // ebfffffe -> `bl <self>` (imm24 = -2, offset = -8),
1863 // which nets to exactly S. Verified against
1864 // `arm-none-eabi-as -march=armv7-r`, which emits `ebfffffe`
1865 // with an R_ARM_CALL relocation.
1866 0xEBFFFFFE
1867 }
1868
1869 ArmOp::Bx { rm } => {
1870 let rm_bits = reg_to_bits(rm);
1871
1872 // BX encoding: cond(4) | 000100101111111111110001 | Rm(4)
1873 0xE12FFF10 | rm_bits
1874 }
1875
1876 ArmOp::Blx { rm } => {
1877 let rm_bits = reg_to_bits(rm);
1878
1879 // BLX (register) encoding: cond(4) | 000100101111111111110011 | Rm(4)
1880 0xE12FFF30 | rm_bits
1881 }
1882
1883 ArmOp::Push { regs } => {
1884 // STMDB SP!, {regs} encoding: cond(4) | 100100 | 10 | 1101 | register_list(16)
1885 let mut reg_list: u32 = 0;
1886 for r in regs {
1887 reg_list |= 1 << reg_to_bits(r);
1888 }
1889 0xE92D0000 | reg_list
1890 }
1891
1892 ArmOp::Pop { regs } => {
1893 // LDMIA SP!, {regs} encoding: cond(4) | 100010 | 11 | 1101 | register_list(16)
1894 let mut reg_list: u32 = 0;
1895 for r in regs {
1896 reg_list |= 1 << reg_to_bits(r);
1897 }
1898 0xE8BD0000 | reg_list
1899 }
1900
1901 ArmOp::Nop => {
1902 // NOP encoding: MOV R0, R0
1903 0xE1A00000
1904 }
1905
1906 ArmOp::Udf { imm } => {
1907 // UDF (Undefined) encoding in ARM: 0xE7F000F0 | (imm12_hi << 8) | imm4_lo
1908 // We only use imm8, so split into imm4_hi and imm4_lo
1909 let imm8 = *imm as u32;
1910 0xE7F000F0 | ((imm8 & 0xF0) << 4) | (imm8 & 0x0F)
1911 }
1912
1913 // #615: handled by the `encode_arm_expanded` early return at the
1914 // top of this function — a real MOV{cond}/MOV pair now, never a
1915 // silent NOP again.
1916 ArmOp::Popcnt { .. } | ArmOp::SetCond { .. } | ArmOp::SelectMove { .. } => {
1917 unreachable!("handled by encode_arm_expanded (#615)")
1918 }
1919
1920 // Verification-only pseudo-ops: `synth-verify`'s ArmSemantics
1921 // models these, but NO codegen path constructs them (the selector
1922 // lowers select/locals/globals/br_table/call to real instruction
1923 // sequences before the encoder). Encoding one as a NOP silently
1924 // dropped the operation (#615 class); a typed Err keeps the
1925 // encoder total (Ok-or-Err, the `encoder_no_panic` contract)
1926 // while making any future reachability LOUD.
1927 ArmOp::Select { .. }
1928 | ArmOp::LocalGet { .. }
1929 | ArmOp::LocalSet { .. }
1930 | ArmOp::LocalTee { .. }
1931 | ArmOp::GlobalGet { .. }
1932 | ArmOp::GlobalSet { .. }
1933 | ArmOp::BrTable { .. }
1934 | ArmOp::Call { .. } => {
1935 return Err(synth_core::Error::synthesis(format!(
1936 "verification-only pseudo-op {op:?} reached the A32 encoder — \
1937 codegen lowers it before encoding; refusing to emit a silent NOP (#615)"
1938 )));
1939 }
1940
1941 // #594: CallIndirect is expanded to a real multi-instruction
1942 // sequence by the early return at the top of this function —
1943 // it must NEVER fall through to a silent NOP again.
1944 ArmOp::CallIndirect { .. } => {
1945 unreachable!("CallIndirect handled by encode_arm_call_indirect (#594)")
1946 }
1947
1948 // #615: every i64 op (and I32WrapI64) is expanded to a real A32
1949 // multi-instruction sequence by `encode_arm_expanded` — the
1950 // "encode as NOP for now" era ended with the value silently
1951 // vanishing on `--target cortex-r5`.
1952 ArmOp::I64Add { .. }
1953 | ArmOp::I64Sub { .. }
1954 | ArmOp::I64DivS { .. }
1955 | ArmOp::I64DivU { .. }
1956 | ArmOp::I64RemS { .. }
1957 | ArmOp::I64RemU { .. }
1958 | ArmOp::I64Clz { .. }
1959 | ArmOp::I64Ctz { .. }
1960 | ArmOp::I64Popcnt { .. }
1961 | ArmOp::I64And { .. }
1962 | ArmOp::I64Or { .. }
1963 | ArmOp::I64Xor { .. }
1964 | ArmOp::I64Eqz { .. }
1965 | ArmOp::I64Eq { .. }
1966 | ArmOp::I64Ne { .. }
1967 | ArmOp::I64LtS { .. }
1968 | ArmOp::I64LtU { .. }
1969 | ArmOp::I64LeS { .. }
1970 | ArmOp::I64LeU { .. }
1971 | ArmOp::I64GtS { .. }
1972 | ArmOp::I64GtU { .. }
1973 | ArmOp::I64GeS { .. }
1974 | ArmOp::I64GeU { .. }
1975 | ArmOp::I64Const { .. }
1976 | ArmOp::I64Ldr { .. }
1977 | ArmOp::I64Str { .. }
1978 | ArmOp::I64ExtendI32S { .. }
1979 | ArmOp::I64ExtendI32U { .. }
1980 | ArmOp::I64Extend8S { .. }
1981 | ArmOp::I64Extend16S { .. }
1982 | ArmOp::I64Extend32S { .. }
1983 | ArmOp::I32WrapI64 { .. } => {
1984 unreachable!("handled by encode_arm_expanded (#615)")
1985 }
1986
1987 // f32 VFP single-precision instructions
1988 ArmOp::F32Add { sd, sn, sm } => encode_vfp_3reg(0xEE300A00, sd, sn, sm)?,
1989 ArmOp::F32Sub { sd, sn, sm } => encode_vfp_3reg(0xEE300A40, sd, sn, sm)?,
1990 ArmOp::F32Mul { sd, sn, sm } => encode_vfp_3reg(0xEE200A00, sd, sn, sm)?,
1991 ArmOp::F32Div { sd, sn, sm } => encode_vfp_3reg(0xEE800A00, sd, sn, sm)?,
1992 ArmOp::F32Abs { sd, sm } => encode_vfp_2reg(0xEEB00AC0, sd, sm)?,
1993 ArmOp::F32Neg { sd, sm } => encode_vfp_2reg(0xEEB10A40, sd, sm)?,
1994 ArmOp::F32Sqrt { sd, sm } => encode_vfp_2reg(0xEEB10AC0, sd, sm)?,
1995
1996 // f32 pseudo-ops — multi-instruction sequences
1997 // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
1998 ArmOp::F32Ceil { sd, sm } => {
1999 return self.encode_arm_f32_rounding(sd, sm, 0b01); // Round toward +Inf
2000 }
2001 ArmOp::F32Floor { sd, sm } => {
2002 return self.encode_arm_f32_rounding(sd, sm, 0b10); // Round toward -Inf
2003 }
2004 ArmOp::F32Trunc { sd, sm } => {
2005 return self.encode_arm_f32_rounding(sd, sm, 0b11); // VCVT toward zero
2006 }
2007 ArmOp::F32Nearest { sd, sm } => {
2008 return self.encode_arm_f32_rounding(sd, sm, 0b00); // VCVT to nearest
2009 }
2010 ArmOp::F32Min { sd, sn, sm } => {
2011 return self.encode_arm_f32_minmax(sd, sn, sm, true);
2012 }
2013 ArmOp::F32Max { sd, sn, sm } => {
2014 return self.encode_arm_f32_minmax(sd, sn, sm, false);
2015 }
2016 ArmOp::F32Copysign { sd, sn, sm } => {
2017 return self.encode_arm_f32_copysign(sd, sn, sm);
2018 }
2019
2020 // f32 comparisons — multi-instruction: VCMP + VMRS + conditional MOV
2021 ArmOp::F32Eq { rd, sn, sm } => {
2022 return self.encode_arm_f32_compare(rd, sn, sm, 0x0); // EQ
2023 }
2024 ArmOp::F32Ne { rd, sn, sm } => {
2025 return self.encode_arm_f32_compare(rd, sn, sm, 0x1); // NE
2026 }
2027 ArmOp::F32Lt { rd, sn, sm } => {
2028 return self.encode_arm_f32_compare(rd, sn, sm, 0x4); // MI (less than)
2029 }
2030 ArmOp::F32Le { rd, sn, sm } => {
2031 return self.encode_arm_f32_compare(rd, sn, sm, 0x9); // LS (less or same)
2032 }
2033 ArmOp::F32Gt { rd, sn, sm } => {
2034 return self.encode_arm_f32_compare(rd, sn, sm, 0xC); // GT
2035 }
2036 ArmOp::F32Ge { rd, sn, sm } => {
2037 return self.encode_arm_f32_compare(rd, sn, sm, 0xA); // GE
2038 }
2039
2040 // f32 const — multi-instruction: MOVW + MOVT + VMOV
2041 ArmOp::F32Const { sd, value } => {
2042 return self.encode_arm_f32_const(sd, *value);
2043 }
2044
2045 ArmOp::F32Load { sd, addr } => encode_vfp_ldst(0xED900A00, sd, addr)?,
2046 ArmOp::F32Store { sd, addr } => encode_vfp_ldst(0xED800A00, sd, addr)?,
2047
2048 // f32 conversions — multi-instruction sequences
2049 ArmOp::F32ConvertI32S { sd, rm } => {
2050 return self.encode_arm_f32_convert_i32(sd, rm, true);
2051 }
2052 ArmOp::F32ConvertI32U { sd, rm } => {
2053 return self.encode_arm_f32_convert_i32(sd, rm, false);
2054 }
2055 ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
2056 return Err(synth_core::Error::synthesis(
2057 "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2058 ));
2059 }
2060 // RQ-67-VFPREACH (#1267): the callee-saved VFP save/restore is
2061 // emitted only by the Cortex-M (Thumb-2) VFP-pressure rung. The A32
2062 // path has no such rung, so reaching here means a caller built the
2063 // op for the wrong profile — refuse LOUDLY rather than encode
2064 // something plausible (#615: expand-or-loud-reject, never a silent
2065 // NOP).
2066 ArmOp::VPushCalleeSavedVfp | ArmOp::VPopCalleeSavedVfp => {
2067 return Err(synth_core::Error::synthesis(
2068 "VPUSH/VPOP {d8-d15} is Thumb-2 only in this backend (RQ-67-VFPREACH, #1267)",
2069 ));
2070 }
2071 ArmOp::F32ReinterpretI32 { sd, rm } => encode_vmov_core_sreg(true, sd, rm)?,
2072 ArmOp::I32ReinterpretF32 { rd, sm } => encode_vmov_core_sreg(false, sm, rd)?,
2073 ArmOp::I32TruncF32S { rd, sm } => {
2074 return self.encode_arm_i32_trunc_f32(rd, sm, true);
2075 }
2076 ArmOp::I32TruncF32U { rd, sm } => {
2077 return self.encode_arm_i32_trunc_f32(rd, sm, false);
2078 }
2079
2080 // f64 VFP double-precision instructions (ARM32)
2081 // F64 arithmetic: same as F32 but with sz=1 (bit 8 = 1, cp11 = 0xB)
2082 ArmOp::F64Add { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B00, dd, dn, dm)?,
2083 ArmOp::F64Sub { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B40, dd, dn, dm)?,
2084 ArmOp::F64Mul { dd, dn, dm } => encode_vfp_3reg_f64(0xEE200B00, dd, dn, dm)?,
2085 ArmOp::F64Div { dd, dn, dm } => encode_vfp_3reg_f64(0xEE800B00, dd, dn, dm)?,
2086 ArmOp::F64Abs { dd, dm } => encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?,
2087 ArmOp::F64Neg { dd, dm } => encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?,
2088 ArmOp::F64Sqrt { dd, dm } => encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?,
2089
2090 // f64 pseudo-ops
2091 // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
2092 ArmOp::F64Ceil { dd, dm } => {
2093 return self.encode_arm_f64_rounding(dd, dm, 0b01);
2094 }
2095 ArmOp::F64Floor { dd, dm } => {
2096 return self.encode_arm_f64_rounding(dd, dm, 0b10);
2097 }
2098 ArmOp::F64Trunc { dd, dm } => {
2099 return self.encode_arm_f64_rounding(dd, dm, 0b11);
2100 }
2101 ArmOp::F64Nearest { dd, dm } => {
2102 return self.encode_arm_f64_rounding(dd, dm, 0b00);
2103 }
2104 ArmOp::F64Min { dd, dn, dm } => {
2105 return self.encode_arm_f64_minmax(dd, dn, dm, true);
2106 }
2107 ArmOp::F64Max { dd, dn, dm } => {
2108 return self.encode_arm_f64_minmax(dd, dn, dm, false);
2109 }
2110 ArmOp::F64Copysign { dd, dn, dm } => {
2111 return self.encode_arm_f64_copysign(dd, dn, dm);
2112 }
2113
2114 // f64 comparisons
2115 ArmOp::F64Eq { rd, dn, dm } => {
2116 return self.encode_arm_f64_compare(rd, dn, dm, 0x0);
2117 }
2118 ArmOp::F64Ne { rd, dn, dm } => {
2119 return self.encode_arm_f64_compare(rd, dn, dm, 0x1);
2120 }
2121 ArmOp::F64Lt { rd, dn, dm } => {
2122 return self.encode_arm_f64_compare(rd, dn, dm, 0x4);
2123 }
2124 ArmOp::F64Le { rd, dn, dm } => {
2125 return self.encode_arm_f64_compare(rd, dn, dm, 0x9);
2126 }
2127 ArmOp::F64Gt { rd, dn, dm } => {
2128 return self.encode_arm_f64_compare(rd, dn, dm, 0xC);
2129 }
2130 ArmOp::F64Ge { rd, dn, dm } => {
2131 return self.encode_arm_f64_compare(rd, dn, dm, 0xA);
2132 }
2133
2134 ArmOp::F64Const { dd, value } => {
2135 return self.encode_arm_f64_const(dd, *value);
2136 }
2137
2138 ArmOp::F64Load { dd, addr } => encode_vfp_ldst_f64(0xED900B00, dd, addr)?,
2139 ArmOp::F64Store { dd, addr } => encode_vfp_ldst_f64(0xED800B00, dd, addr)?,
2140
2141 ArmOp::F64ConvertI32S { dd, rm } => {
2142 return self.encode_arm_f64_convert_i32(dd, rm, true);
2143 }
2144 ArmOp::F64ConvertI32U { dd, rm } => {
2145 return self.encode_arm_f64_convert_i32(dd, rm, false);
2146 }
2147 ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
2148 return Err(synth_core::Error::synthesis(
2149 "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2150 ));
2151 }
2152 ArmOp::F64PromoteF32 { dd, sm } => {
2153 return self.encode_arm_f64_promote_f32(dd, sm);
2154 }
2155 // GI-FPU-002 (#369): no synth A32 target carries an FPU (cortex-r5
2156 // has none — the selector declines every float op there), so the
2157 // A32 encoder refuses loudly instead of shipping an untested
2158 // encoding (#615: never a silent wrong byte).
2159 ArmOp::F32DemoteF64 { .. } => {
2160 return Err(synth_core::Error::synthesis(
2161 "F32DemoteF64 has no A32 encoding (no A32 target has an FPU)",
2162 ));
2163 }
2164 ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => {
2165 encode_vmov_core_dreg(true, dd, rmlo, rmhi)?
2166 }
2167 ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => {
2168 encode_vmov_core_dreg(false, dm, rdlo, rdhi)?
2169 }
2170 ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
2171 return Err(synth_core::Error::synthesis(
2172 "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
2173 ));
2174 }
2175 ArmOp::I32TruncF64S { rd, dm } => {
2176 return self.encode_arm_i32_trunc_f64(rd, dm, true);
2177 }
2178 ArmOp::I32TruncF64U { rd, dm } => {
2179 return self.encode_arm_i32_trunc_f64(rd, dm, false);
2180 }
2181 // #615: multi-instruction i64 sequences — expanded to real A32 by
2182 // `encode_arm_expanded`, no longer "Thumb-2 only" NOPs.
2183 ArmOp::I64SetCond { .. }
2184 | ArmOp::I64SetCondZ { .. }
2185 | ArmOp::I64Mul { .. }
2186 | ArmOp::I64Shl { .. }
2187 | ArmOp::I64ShrS { .. }
2188 | ArmOp::I64ShrU { .. }
2189 | ArmOp::I64Rotl { .. }
2190 | ArmOp::I64Rotr { .. } => {
2191 unreachable!("handled by encode_arm_expanded (#615)")
2192 }
2193
2194 // MVE instructions — Thumb-2 only (Cortex-M55 is always Thumb-2)
2195 ArmOp::MveLoad { .. }
2196 | ArmOp::MveStore { .. }
2197 | ArmOp::MveConst { .. }
2198 | ArmOp::MveAnd { .. }
2199 | ArmOp::MveOrr { .. }
2200 | ArmOp::MveEor { .. }
2201 | ArmOp::MveMvn { .. }
2202 | ArmOp::MveBic { .. }
2203 | ArmOp::MveAddI { .. }
2204 | ArmOp::MveSubI { .. }
2205 | ArmOp::MveMulI { .. }
2206 | ArmOp::MveNegI { .. }
2207 | ArmOp::MveCmpEqI { .. }
2208 | ArmOp::MveCmpNeI { .. }
2209 | ArmOp::MveCmpLtS { .. }
2210 | ArmOp::MveCmpLtU { .. }
2211 | ArmOp::MveCmpGtS { .. }
2212 | ArmOp::MveCmpGtU { .. }
2213 | ArmOp::MveCmpLeS { .. }
2214 | ArmOp::MveCmpLeU { .. }
2215 | ArmOp::MveCmpGeS { .. }
2216 | ArmOp::MveCmpGeU { .. }
2217 | ArmOp::MveDup { .. }
2218 | ArmOp::MveExtractLane { .. }
2219 | ArmOp::MveInsertLane { .. }
2220 | ArmOp::MveAddF32 { .. }
2221 | ArmOp::MveSubF32 { .. }
2222 | ArmOp::MveMulF32 { .. }
2223 | ArmOp::MveNegF32 { .. }
2224 | ArmOp::MveAbsF32 { .. }
2225 | ArmOp::MveCmpEqF32 { .. }
2226 | ArmOp::MveCmpNeF32 { .. }
2227 | ArmOp::MveCmpLtF32 { .. }
2228 | ArmOp::MveCmpLeF32 { .. }
2229 | ArmOp::MveCmpGtF32 { .. }
2230 | ArmOp::MveCmpGeF32 { .. }
2231 | ArmOp::MveDupF32 { .. }
2232 | ArmOp::MveExtractLaneF32 { .. }
2233 | ArmOp::MveReplaceLaneF32 { .. }
2234 | ArmOp::MveDivF32 { .. }
2235 | ArmOp::MveSqrtF32 { .. } => {
2236 // MVE (Helium) is a Thumb-2-only extension (Cortex-M55); there
2237 // is no A32 encoding. The selector only emits MVE ops for
2238 // Thumb targets — a NOP here silently dropped the vector op
2239 // if that invariant ever broke (#615 class). Err keeps the
2240 // encoder total and the failure loud.
2241 return Err(synth_core::Error::synthesis(format!(
2242 "MVE op {op:?} has no A32 (ARM-mode) encoding — MVE is Thumb-2 only (#615)"
2243 )));
2244 }
2245 };
2246
2247 // ARM32 instructions are little-endian
2248 Ok(instr.to_le_bytes().to_vec())
2249 }
2250
2251 // === ARM32 VFP multi-instruction helpers ===
2252
2253 /// Encode F32 comparison as ARM32: VCMP.F32 + VMRS + MOV rd,#0 + MOVcond rd,#1
2254 fn encode_arm_f32_compare(
2255 &self,
2256 rd: &Reg,
2257 sn: &VfpReg,
2258 sm: &VfpReg,
2259 cond_code: u32,
2260 ) -> Result<Vec<u8>> {
2261 let mut bytes = Vec::new();
2262
2263 // VCMP.F32 Sn, Sm: 0xEEB40A40 with Sn in Vd position, Sm in Vm position
2264 let sn_num = vfp_sreg_to_num(sn)?;
2265 let sm_num = vfp_sreg_to_num(sm)?;
2266 let (vd, d) = encode_sreg(sn_num);
2267 let (vm, m) = encode_sreg(sm_num);
2268 let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2269 bytes.extend_from_slice(&vcmp.to_le_bytes());
2270
2271 // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
2272 bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2273
2274 // MOV rd, #0: 0xE3A0_0000 | (rd << 12)
2275 let rd_bits = reg_to_bits(rd);
2276 let mov_zero = 0xE3A00000 | (rd_bits << 12);
2277 bytes.extend_from_slice(&mov_zero.to_le_bytes());
2278
2279 // MOVcond rd, #1: cond(4) | 0011 1010 0000 rd(4) 0000 0000 0001
2280 let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2281 bytes.extend_from_slice(&mov_one.to_le_bytes());
2282
2283 Ok(bytes)
2284 }
2285
2286 /// Encode F32 constant load as ARM32: MOVW Rt,#lo16 + MOVT Rt,#hi16 + VMOV Sd,Rt
2287 fn encode_arm_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
2288 let mut bytes = Vec::new();
2289 let bits = value.to_bits();
2290
2291 // Use R12 as temp register for constant loading
2292 let rt: u32 = 12; // R12/IP
2293
2294 // MOVW R12, #lo16: 0xE300_C000 | (imm4 << 16) | imm12
2295 let lo16 = bits & 0xFFFF;
2296 let movw = 0xE3000000 | (rt << 12) | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2297 bytes.extend_from_slice(&movw.to_le_bytes());
2298
2299 // MOVT R12, #hi16: 0xE340_C000 | (imm4 << 16) | imm12
2300 let hi16 = (bits >> 16) & 0xFFFF;
2301 let movt = 0xE3400000 | (rt << 12) | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2302 bytes.extend_from_slice(&movt.to_le_bytes());
2303
2304 // VMOV Sd, R12
2305 let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
2306 bytes.extend_from_slice(&vmov.to_le_bytes());
2307
2308 Ok(bytes)
2309 }
2310
2311 /// Encode VMOV + VCVT.F32.S32/U32 as ARM32
2312 fn encode_arm_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2313 let mut bytes = Vec::new();
2314
2315 // VMOV Sd, Rm — move integer to VFP register
2316 let vmov = encode_vmov_core_sreg(true, sd, rm)?;
2317 bytes.extend_from_slice(&vmov.to_le_bytes());
2318
2319 // VCVT.F32.S32 Sd, Sd (signed) or VCVT.F32.U32 Sd, Sd (unsigned).
2320 // The "op" bit (bit 7) selects signedness: 1 = signed (S32), 0 =
2321 // unsigned (U32). So signed = 0xEEB80AC0, unsigned = 0xEEB80A40 —
2322 // objdump confirms 0xEEB80A40 decodes to `vcvt.f32.u32` (GI-FPU-002:
2323 // the two were previously swapped, silently making `convert_i32_s`
2324 // an unsigned conversion).
2325 let sd_num = vfp_sreg_to_num(sd)?;
2326 let (vd, d) = encode_sreg(sd_num);
2327 let (vm, m) = encode_sreg(sd_num); // same register as source
2328 let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
2329 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2330 bytes.extend_from_slice(&vcvt.to_le_bytes());
2331
2332 Ok(bytes)
2333 }
2334
2335 /// Encode F32 rounding pseudo-op as ARM32 via VCVT to integer and back.
2336 /// mode: 0b00=nearest, 0b01=floor(-Inf), 0b10=ceil(+Inf), 0b11=trunc(zero)
2337 /// Strategy: VCVT.S32.F32 Sd, Sm (toward zero), then VCVT.F32.S32 Sd, Sd
2338 /// For ceil/floor/nearest, we use VCVTR (round toward mode) + convert back.
2339 /// Simplified: convert to int (toward zero for trunc) then back to float.
2340 /// Encode F32 rounding as ARM32.
2341 /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2342 ///
2343 /// For trunc (mode=0b11): uses VCVTR.S32.F32 (always rounds toward zero).
2344 /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant
2345 /// which honours FPSCR rmode), then restores FPSCR.
2346 fn encode_arm_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2347 let mut bytes = Vec::new();
2348 let sm_num = vfp_sreg_to_num(sm)?;
2349 let sd_num = vfp_sreg_to_num(sd)?;
2350 let (vd_s, d_s) = encode_sreg(sd_num);
2351 let (vm_s, m_s) = encode_sreg(sm_num);
2352
2353 if mode == 0b11 {
2354 // Trunc (toward zero): VCVTR.S32.F32 — the "R" variant always truncates.
2355 // 0xEEBD0AC0: bit[7]=1 => round toward zero regardless of FPSCR
2356 let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2357 bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2358 } else {
2359 // ceil/floor/nearest: manipulate FPSCR rounding mode
2360 let rt: u32 = 12; // R12/IP as temp
2361
2362 // VMRS R12, FPSCR
2363 let vmrs = 0xEEF10A10 | (rt << 12);
2364 bytes.extend_from_slice(&vmrs.to_le_bytes());
2365
2366 // BIC R12, R12, #(3 << 22) — clear RMode bits [23:22]
2367 // 3<<22 = 0x00C00000. ARM rotated imm: 0x03 ror 10 (rotation=5, imm8=0x03)
2368 let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2369 bytes.extend_from_slice(&bic.to_le_bytes());
2370
2371 // ORR R12, R12, #(mode << 22) — set desired rounding mode
2372 if mode != 0 {
2373 // mode<<22: rotation=5, imm8=mode
2374 let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2375 bytes.extend_from_slice(&orr.to_le_bytes());
2376 }
2377
2378 // VMSR FPSCR, R12
2379 let vmsr = 0xEEE10A10 | (rt << 12);
2380 bytes.extend_from_slice(&vmsr.to_le_bytes());
2381
2382 // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rounding mode
2383 let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2384 bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2385
2386 // Restore FPSCR: clear rmode bits back to nearest (default)
2387 bytes.extend_from_slice(&vmrs.to_le_bytes());
2388 bytes.extend_from_slice(&bic.to_le_bytes());
2389 bytes.extend_from_slice(&vmsr.to_le_bytes());
2390 }
2391
2392 // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
2393 let (vd2, d2) = encode_sreg(sd_num);
2394 let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
2395 bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2396
2397 Ok(bytes)
2398 }
2399
2400 /// Encode F32 min/max as ARM32: VCMP + VMRS + conditional VMOV
2401 fn encode_arm_f32_minmax(
2402 &self,
2403 sd: &VfpReg,
2404 sn: &VfpReg,
2405 sm: &VfpReg,
2406 is_min: bool,
2407 ) -> Result<Vec<u8>> {
2408 let mut bytes = Vec::new();
2409 let sn_num = vfp_sreg_to_num(sn)?;
2410 let sm_num = vfp_sreg_to_num(sm)?;
2411 let sd_num = vfp_sreg_to_num(sd)?;
2412
2413 // VMOV Sd, Sn (start with first operand)
2414 let (vd, d) = encode_sreg(sd_num);
2415 let (vn, n) = encode_sreg(sn_num);
2416 let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2417 bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2418
2419 // VCMP.F32 Sn, Sm
2420 let (vm, m) = encode_sreg(sm_num);
2421 let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2422 bytes.extend_from_slice(&vcmp.to_le_bytes());
2423
2424 // VMRS APSR_nzcv, FPSCR
2425 bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2426
2427 // For min: if Sn > Sm (GT), use Sm. Condition = GT (0xC)
2428 // For max: if Sn < Sm (MI/LT), use Sm. Condition = MI (0x4)
2429 let cond = if is_min { 0xCu32 } else { 0x4u32 };
2430
2431 // VMOV{cond} Sd, Sm — conditional VMOV
2432 let vmov_cond = (cond << 28) | 0x0EB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2433 bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2434
2435 Ok(bytes)
2436 }
2437
2438 /// Encode F32 copysign as ARM32: extract sign from Sm, magnitude from Sn
2439 fn encode_arm_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2440 let mut bytes = Vec::new();
2441
2442 // VMOV R12, Sm (get sign source bits)
2443 let vmov_sm = encode_vmov_core_sreg(false, sm, &Reg::R12)?;
2444 bytes.extend_from_slice(&vmov_sm.to_le_bytes());
2445
2446 // VMOV R0, Sn (get magnitude source bits) — use R0 as temp
2447 let vmov_sn = encode_vmov_core_sreg(false, sn, &Reg::R0)?;
2448 bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2449
2450 // AND R12, R12, #0x80000000 (keep only sign bit)
2451 // Thumb-2 constant 0x80000000 needs special encoding; in ARM32 use rotated imm
2452 // 0x80000000 = 0x02 rotated right by 2 (rotation=1, imm8=0x02)
2453 let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2454 bytes.extend_from_slice(&and_sign.to_le_bytes());
2455
2456 // BIC R0, R0, #0x80000000 (clear sign bit from magnitude)
2457 // R0 = register 0, so Rn and Rd fields are 0
2458 let bic_sign = 0xE3C00000u32 | (1 << 8) | 0x02;
2459 bytes.extend_from_slice(&bic_sign.to_le_bytes());
2460
2461 // ORR R0, R0, R12 (combine sign + magnitude)
2462 // R0 = register 0, so Rn and Rd fields are 0
2463 let orr = 0xE1800000u32 | 12;
2464 bytes.extend_from_slice(&orr.to_le_bytes());
2465
2466 // VMOV Sd, R0
2467 let vmov_result = encode_vmov_core_sreg(true, sd, &Reg::R0)?;
2468 bytes.extend_from_slice(&vmov_result.to_le_bytes());
2469
2470 Ok(bytes)
2471 }
2472
2473 /// Encode F64 comparison as ARM32: VCMP.F64 + VMRS + MOV rd,#0 + MOVcond rd,#1
2474 fn encode_arm_f64_compare(
2475 &self,
2476 rd: &Reg,
2477 dn: &VfpReg,
2478 dm: &VfpReg,
2479 cond_code: u32,
2480 ) -> Result<Vec<u8>> {
2481 let mut bytes = Vec::new();
2482
2483 // VCMP.F64 Dn, Dm: 0xEEB40B40 with Dn in Vd position, Dm in Vm position
2484 let dn_num = vfp_dreg_to_num(dn)?;
2485 let dm_num = vfp_dreg_to_num(dm)?;
2486 let (vd, d) = encode_dreg(dn_num);
2487 let (vm, m) = encode_dreg(dm_num);
2488 let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2489 bytes.extend_from_slice(&vcmp.to_le_bytes());
2490
2491 // VMRS APSR_nzcv, FPSCR
2492 bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2493
2494 // MOV rd, #0
2495 let rd_bits = reg_to_bits(rd);
2496 let mov_zero = 0xE3A00000 | (rd_bits << 12);
2497 bytes.extend_from_slice(&mov_zero.to_le_bytes());
2498
2499 // MOVcond rd, #1
2500 let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2501 bytes.extend_from_slice(&mov_one.to_le_bytes());
2502
2503 Ok(bytes)
2504 }
2505
2506 /// Encode F64 constant load as ARM32: MOVW + MOVT + MOVW + MOVT + VMOV
2507 fn encode_arm_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
2508 let mut bytes = Vec::new();
2509 let bits = value.to_bits();
2510 let lo32 = bits as u32;
2511 let hi32 = (bits >> 32) as u32;
2512
2513 // Load low 32 bits into R0 (Rd field = 0 for R0)
2514 let lo16 = lo32 & 0xFFFF;
2515 let movw_r0 = 0xE3000000 | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2516 bytes.extend_from_slice(&movw_r0.to_le_bytes());
2517 let hi16 = (lo32 >> 16) & 0xFFFF;
2518 let movt_r0 = 0xE3400000 | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2519 bytes.extend_from_slice(&movt_r0.to_le_bytes());
2520
2521 // Load high 32 bits into R12
2522 let lo16 = hi32 & 0xFFFF;
2523 let movw_r12 = 0xE3000000 | ((lo16 >> 12) << 16) | (12 << 12) | (lo16 & 0xFFF);
2524 bytes.extend_from_slice(&movw_r12.to_le_bytes());
2525 let hi16 = (hi32 >> 16) & 0xFFFF;
2526 let movt_r12 = 0xE3400000 | ((hi16 >> 12) << 16) | (12 << 12) | (hi16 & 0xFFF);
2527 bytes.extend_from_slice(&movt_r12.to_le_bytes());
2528
2529 // VMOV Dd, R0, R12
2530 let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
2531 bytes.extend_from_slice(&vmov.to_le_bytes());
2532
2533 Ok(bytes)
2534 }
2535
2536 /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as ARM32
2537 fn encode_arm_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2538 let mut bytes = Vec::new();
2539
2540 // Use S0 as intermediate: VMOV S0, Rm
2541 let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
2542 bytes.extend_from_slice(&vmov.to_le_bytes());
2543
2544 // VCVT.F64.S32 Dd, S0 (signed) or VCVT.F64.U32 Dd, S0 (unsigned)
2545 // Base: 0xEEB80B40 (signed) or 0xEEB80BC0 (unsigned)
2546 let dd_num = vfp_dreg_to_num(dd)?;
2547 let (vd, d) = encode_dreg(dd_num);
2548 let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
2549 // S0 is register 0: Vm=0, M=0
2550 let vcvt = base | (d << 22) | (vd << 12);
2551 bytes.extend_from_slice(&vcvt.to_le_bytes());
2552
2553 Ok(bytes)
2554 }
2555
2556 /// Encode VCVT.F64.F32 Dd, Sm as ARM32 (f32 to f64 promotion)
2557 fn encode_arm_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2558 let dd_num = vfp_dreg_to_num(dd)?;
2559 let sm_num = vfp_sreg_to_num(sm)?;
2560 let (vd, d) = encode_dreg(dd_num);
2561 let (vm, m) = encode_sreg(sm_num);
2562
2563 // VCVT.F64.F32 Dd, Sm: 0xEEB70AC0
2564 let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
2565 Ok(vcvt.to_le_bytes().to_vec())
2566 }
2567
2568 /// Encode VCVT.S32/U32.F64 Sd, Dm + VMOV Rd, Sd as ARM32
2569 fn encode_arm_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2570 let mut bytes = Vec::new();
2571 let dm_num = vfp_dreg_to_num(dm)?;
2572 let (vm, m) = encode_dreg(dm_num);
2573
2574 // VCVT.S32.F64 S0, Dm (toward zero) or VCVT.U32.F64 S0, Dm
2575 // S0: Vd=0, D=0
2576 let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
2577 let vcvt = base | (m << 5) | vm;
2578 bytes.extend_from_slice(&vcvt.to_le_bytes());
2579
2580 // VMOV Rd, S0
2581 let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
2582 bytes.extend_from_slice(&vmov.to_le_bytes());
2583
2584 Ok(bytes)
2585 }
2586
2587 /// Encode F64 rounding pseudo-op as ARM32 via VCVT to integer and back.
2588 /// Encode F64 rounding as ARM32.
2589 /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2590 ///
2591 /// For trunc: uses VCVTR.S32.F64 (always truncates).
2592 /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F64 (non-R variant),
2593 /// then restores FPSCR.
2594 fn encode_arm_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2595 let mut bytes = Vec::new();
2596 let dm_num = vfp_dreg_to_num(dm)?;
2597 let dd_num = vfp_dreg_to_num(dd)?;
2598 let (vm, m) = encode_dreg(dm_num);
2599 let (vd, d) = encode_dreg(dd_num);
2600
2601 if mode == 0b11 {
2602 // Trunc (toward zero): VCVTR.S32.F64 — bit[7]=1, always truncates
2603 let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
2604 bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2605 } else {
2606 // ceil/floor/nearest: manipulate FPSCR rounding mode
2607 let rt: u32 = 12;
2608
2609 // VMRS R12, FPSCR
2610 let vmrs = 0xEEF10A10 | (rt << 12);
2611 bytes.extend_from_slice(&vmrs.to_le_bytes());
2612
2613 // BIC R12, R12, #(3 << 22)
2614 let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2615 bytes.extend_from_slice(&bic.to_le_bytes());
2616
2617 // ORR R12, R12, #(mode << 22)
2618 if mode != 0 {
2619 let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2620 bytes.extend_from_slice(&orr.to_le_bytes());
2621 }
2622
2623 // VMSR FPSCR, R12
2624 let vmsr = 0xEEE10A10 | (rt << 12);
2625 bytes.extend_from_slice(&vmsr.to_le_bytes());
2626
2627 // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0), uses FPSCR rmode
2628 let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
2629 bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2630
2631 // Restore FPSCR
2632 bytes.extend_from_slice(&vmrs.to_le_bytes());
2633 bytes.extend_from_slice(&bic.to_le_bytes());
2634 bytes.extend_from_slice(&vmsr.to_le_bytes());
2635 }
2636
2637 // VCVT.F64.S32 Dd, S0 (convert back to double)
2638 let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
2639 bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2640
2641 Ok(bytes)
2642 }
2643
2644 /// Encode F64 min/max as ARM32: VMOV + VCMP + VMRS + conditional VMOV
2645 fn encode_arm_f64_minmax(
2646 &self,
2647 dd: &VfpReg,
2648 dn: &VfpReg,
2649 dm: &VfpReg,
2650 is_min: bool,
2651 ) -> Result<Vec<u8>> {
2652 let mut bytes = Vec::new();
2653 let dn_num = vfp_dreg_to_num(dn)?;
2654 let dm_num = vfp_dreg_to_num(dm)?;
2655 let dd_num = vfp_dreg_to_num(dd)?;
2656
2657 // VMOV.F64 Dd, Dn (start with first operand)
2658 let (vd, d) = encode_dreg(dd_num);
2659 let (vn, n) = encode_dreg(dn_num);
2660 let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2661 bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2662
2663 // VCMP.F64 Dn, Dm
2664 let (vm, m) = encode_dreg(dm_num);
2665 let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2666 bytes.extend_from_slice(&vcmp.to_le_bytes());
2667
2668 // VMRS APSR_nzcv, FPSCR
2669 bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2670
2671 let cond = if is_min { 0xCu32 } else { 0x4u32 };
2672 let vmov_cond = (cond << 28) | 0x0EB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2673 bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2674
2675 Ok(bytes)
2676 }
2677
2678 /// Encode F64 copysign as ARM32
2679 fn encode_arm_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
2680 let mut bytes = Vec::new();
2681
2682 // VMOV R0, R12, Dm (get sign source bits)
2683 let vmov_dm = encode_vmov_core_dreg(false, dm, &Reg::R0, &Reg::R12)?;
2684 bytes.extend_from_slice(&vmov_dm.to_le_bytes());
2685
2686 // VMOV R1, R2, Dn (get magnitude source bits)
2687 // We use R1 (lo) and R2 (hi) for the magnitude
2688 let vmov_dn = encode_vmov_core_dreg(false, dn, &Reg::R1, &Reg::R2)?;
2689 bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2690
2691 // AND R12, R12, #0x80000000 (keep only sign bit from hi word)
2692 let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2693 bytes.extend_from_slice(&and_sign.to_le_bytes());
2694
2695 // BIC R2, R2, #0x80000000 (clear sign bit from magnitude hi word)
2696 let bic_sign = 0xE3C00000u32 | (2 << 16) | (2 << 12) | (1 << 8) | 0x02;
2697 bytes.extend_from_slice(&bic_sign.to_le_bytes());
2698
2699 // ORR R2, R2, R12 (combine sign + magnitude)
2700 let orr = 0xE1800000u32 | (2 << 16) | (2 << 12) | 12;
2701 bytes.extend_from_slice(&orr.to_le_bytes());
2702
2703 // VMOV Dd, R1, R2
2704 let vmov_result = encode_vmov_core_dreg(true, dd, &Reg::R1, &Reg::R2)?;
2705 bytes.extend_from_slice(&vmov_result.to_le_bytes());
2706
2707 Ok(bytes)
2708 }
2709
2710 /// Encode VCVT.S32/U32.F32 + VMOV as ARM32
2711 fn encode_arm_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2712 let mut bytes = Vec::new();
2713
2714 // VCVT.S32.F32 Sd, Sm (toward zero) or VCVT.U32.F32 Sd, Sm
2715 // We use Sm as both source and destination for the intermediate result
2716 let sm_num = vfp_sreg_to_num(sm)?;
2717 let (vd, d) = encode_sreg(sm_num);
2718 let (vm, m) = encode_sreg(sm_num);
2719 let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
2720 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2721 bytes.extend_from_slice(&vcvt.to_le_bytes());
2722
2723 // VMOV Rd, Sm — move result back to core register
2724 let vmov = encode_vmov_core_sreg(false, sm, rd)?;
2725 bytes.extend_from_slice(&vmov.to_le_bytes());
2726
2727 Ok(bytes)
2728 }
2729
2730 /// Encode an ARM instruction in Thumb-2 mode (16-bit or 32-bit instructions)
2731 fn encode_thumb(&self, op: &ArmOp) -> Result<Vec<u8>> {
2732 // Thumb-2 supports both 16-bit and 32-bit instructions
2733 // 32-bit instructions are encoded as two 16-bit halfwords (big-endian order)
2734 match op {
2735 // === 16-bit Thumb encodings ===
2736 ArmOp::Add { rd, rn, op2 } => {
2737 let rd_bits = reg_to_bits(rd) as u16;
2738 let rn_bits = reg_to_bits(rn) as u16;
2739
2740 if let Operand2::Reg(rm) = op2 {
2741 let rm_bits = reg_to_bits(rm) as u16;
2742 // 16-bit ADDS only has 3-bit register fields (R0-R7). For
2743 // high registers (e.g. R12, the MemLoad/MemStore base
2744 // scratch) the bits overflow into adjacent fields, silently
2745 // corrupting the operands — issue #178/#180: `add ip,ip,r0`
2746 // was emitted as `adds r4,r5,r1`. Guard on all three regs
2747 // being low and fall back to 32-bit ADD.W otherwise, exactly
2748 // as the Sub handler below does.
2749 if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2750 // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2751 let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2752 Ok(instr.to_le_bytes().to_vec())
2753 } else {
2754 // ADD.W Rd, Rn, Rm (32-bit) for high registers
2755 self.encode_thumb32_add_reg_raw(
2756 rd_bits as u32,
2757 rn_bits as u32,
2758 rm_bits as u32,
2759 )
2760 }
2761 } else if let Operand2::Imm(imm) = op2 {
2762 if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2763 // ADDS Rd, Rn, #imm3 (16-bit): 0001 110 imm3 Rn Rd
2764 let instr: u16 = 0x1C00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2765 Ok(instr.to_le_bytes().to_vec())
2766 } else {
2767 // Use 32-bit ADD for larger immediates
2768 self.encode_thumb32_add(rd, rn, *imm as u32)
2769 }
2770 } else {
2771 // Fallback to 32-bit encoding
2772 self.encode_thumb32_add(rd, rn, 0)
2773 }
2774 }
2775
2776 ArmOp::Sub { rd, rn, op2 } => {
2777 let rd_bits = reg_to_bits(rd) as u16;
2778 let rn_bits = reg_to_bits(rn) as u16;
2779
2780 if let Operand2::Reg(rm) = op2 {
2781 let rm_bits = reg_to_bits(rm) as u16;
2782 // 16-bit SUBS can only use low registers (R0-R7)
2783 if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2784 // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2785 let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2786 Ok(instr.to_le_bytes().to_vec())
2787 } else {
2788 // Use 32-bit SUB.W for high registers
2789 self.encode_thumb32_sub_reg_raw(
2790 rd_bits as u32,
2791 rn_bits as u32,
2792 rm_bits as u32,
2793 )
2794 }
2795 } else if let Operand2::Imm(imm) = op2 {
2796 if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2797 // SUBS Rd, Rn, #imm3 (16-bit): 0001 111 imm3 Rn Rd
2798 let instr: u16 = 0x1E00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2799 Ok(instr.to_le_bytes().to_vec())
2800 } else {
2801 self.encode_thumb32_sub(rd, rn, *imm as u32)
2802 }
2803 } else {
2804 self.encode_thumb32_sub(rd, rn, 0)
2805 }
2806 }
2807
2808 ArmOp::Mov { rd, op2 } => {
2809 let rd_bits = reg_to_bits(rd) as u16;
2810
2811 if let Operand2::Imm(imm) = op2 {
2812 // #498: the old test here was the SIGNED `*imm <= 255`,
2813 // so a negative immediate (e.g. -1) fell into the 16-bit
2814 // MOVS arm and encoded the wrong VALUE (#(imm & 0xFF) =
2815 // #0xFF). A positive imm above 0xFFFF was equally wrong:
2816 // MOVW truncates to 16 bits. Split on the UNSIGNED value:
2817 // imm8 → MOVS, imm16 → MOVW, anything wider (negative or
2818 // >0xFFFF) → the full-value MOVW+MOVT pair. No emitter
2819 // produces the wide shape today (both selectors
2820 // materialize wide constants as explicit Movw/Movt or
2821 // Movw+Mvn), so this is byte-identical on shipped paths —
2822 // it retires the latent wrong-value encodings the
2823 // `estimator_encoder_agreement` oracle had pinned.
2824 let uimm = *imm as u32;
2825 if uimm <= 255 && rd_bits < 8 {
2826 // MOVS Rd, #imm8 (16-bit): 0010 0 Rd imm8
2827 let imm_bits = (*imm as u16) & 0xFF;
2828 let instr: u16 = 0x2000 | (rd_bits << 8) | imm_bits;
2829 Ok(instr.to_le_bytes().to_vec())
2830 } else if uimm <= 0xFFFF {
2831 // Use 32-bit MOVW for 16-bit immediates
2832 self.encode_thumb32_movw(rd, uimm)
2833 } else {
2834 // Full 32-bit value: MOVW low16 + MOVT high16
2835 let mut bytes = self.encode_thumb32_movw(rd, uimm & 0xFFFF)?;
2836 bytes.extend(self.encode_thumb32_movt_raw(reg_to_bits(rd), uimm >> 16)?);
2837 Ok(bytes)
2838 }
2839 } else if let Operand2::Reg(rm) = op2 {
2840 let rm_bits = reg_to_bits(rm) as u16;
2841 // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
2842 // D = Rd[3], Rd[2:0] in lower bits
2843 let d_bit = (rd_bits >> 3) & 1;
2844 let instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
2845 Ok(instr.to_le_bytes().to_vec())
2846 } else {
2847 // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
2848 // operand form the arm does not encode (Imm and Reg only) -- #615's
2849 // silent-miscompile class, on the DEFAULT target. Refuse instead.
2850 Err(synth_core::Error::synthesis(format!(
2851 "Thumb-2 MOV with operand {op2:?} has no Thumb-2 encoding here (Imm and Reg \
2852 are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
2853 )))
2854 }
2855 }
2856
2857 ArmOp::Push { regs } => {
2858 // Thumb-2 PUSH encoding:
2859 // If all regs in R0-R7 + LR, use 16-bit: 1011 010 M rrrrrrrr
2860 // Otherwise use 32-bit: STMDB SP!, {regs} = 1110 1001 0010 1101 | 0M0 reglist(13)
2861 let mut reg_list: u16 = 0;
2862 let mut need_32bit = false;
2863 for r in regs {
2864 let bit = reg_to_bits(r);
2865 if bit >= 8 && *r != Reg::LR {
2866 need_32bit = true;
2867 }
2868 reg_list |= 1 << bit;
2869 }
2870 if !need_32bit {
2871 // 16-bit PUSH: 1011 010 M rrrrrrrr
2872 let m_bit = if reg_list & (1 << 14) != 0 {
2873 1u16
2874 } else {
2875 0u16
2876 };
2877 let low_regs = reg_list & 0xFF;
2878 let instr: u16 = 0xB400 | (m_bit << 8) | low_regs;
2879 Ok(instr.to_le_bytes().to_vec())
2880 } else {
2881 // 32-bit STMDB SP!, {regs}: E92D | reglist(16)
2882 let hw1: u16 = 0xE92D;
2883 let hw2: u16 = reg_list;
2884 let mut bytes = hw1.to_le_bytes().to_vec();
2885 bytes.extend_from_slice(&hw2.to_le_bytes());
2886 Ok(bytes)
2887 }
2888 }
2889
2890 ArmOp::Pop { regs } => {
2891 // Thumb-2 POP encoding:
2892 // If all regs in R0-R7 + PC, use 16-bit: 1011 110 P rrrrrrrr
2893 // Otherwise use 32-bit: LDMIA SP!, {regs} = 1110 1000 1011 1101 | PM0 reglist(13)
2894 let mut reg_list: u16 = 0;
2895 let mut need_32bit = false;
2896 for r in regs {
2897 let bit = reg_to_bits(r);
2898 if bit >= 8 && *r != Reg::PC {
2899 need_32bit = true;
2900 }
2901 reg_list |= 1 << bit;
2902 }
2903 if !need_32bit {
2904 // 16-bit POP: 1011 110 P rrrrrrrr
2905 let p_bit = if reg_list & (1 << 15) != 0 {
2906 1u16
2907 } else {
2908 0u16
2909 };
2910 let low_regs = reg_list & 0xFF;
2911 let instr: u16 = 0xBC00 | (p_bit << 8) | low_regs;
2912 Ok(instr.to_le_bytes().to_vec())
2913 } else {
2914 // 32-bit LDMIA SP!, {regs}: E8BD | reglist(16)
2915 let hw1: u16 = 0xE8BD;
2916 let hw2: u16 = reg_list;
2917 let mut bytes = hw1.to_le_bytes().to_vec();
2918 bytes.extend_from_slice(&hw2.to_le_bytes());
2919 Ok(bytes)
2920 }
2921 }
2922
2923 ArmOp::Nop => {
2924 let instr: u16 = 0xBF00; // NOP in Thumb-2
2925 Ok(instr.to_le_bytes().to_vec())
2926 }
2927
2928 ArmOp::Udf { imm } => {
2929 // UDF (Undefined) in Thumb-2: 16-bit encoding is 0xDE00 | imm8
2930 // This triggers UsageFault/HardFault, used for WASM traps
2931 let instr: u16 = 0xDE00 | (*imm as u16);
2932 let bytes = instr.to_le_bytes().to_vec();
2933 encoding_contracts::verify_thumb16(&bytes);
2934 Ok(bytes)
2935 }
2936
2937 // i64 support: ADDS, ADC, SUBS, SBC for register pair arithmetic
2938 // ADDS sets flags (carry), ADC uses carry from previous ADDS
2939 ArmOp::Adds { rd, rn, op2 } => {
2940 let rd_bits = reg_to_bits(rd) as u16;
2941 let rn_bits = reg_to_bits(rn) as u16;
2942
2943 if let Operand2::Reg(rm) = op2 {
2944 let rm_bits = reg_to_bits(rm) as u16;
2945 // 16-bit ADDS is R0-R7 only; i64 pair allocation can place
2946 // operands in R8-R11, which would overflow the 3-bit fields
2947 // and corrupt the operands (#178/#180 class). Guard and fall
2948 // back to 32-bit ADDS.W for high registers.
2949 if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2950 // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2951 let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2952 Ok(instr.to_le_bytes().to_vec())
2953 } else {
2954 self.encode_thumb32_adds_reg_raw(
2955 rd_bits as u32,
2956 rn_bits as u32,
2957 rm_bits as u32,
2958 )
2959 }
2960 } else {
2961 // 32-bit Thumb-2 ADDS with immediate
2962 self.encode_thumb32_adds(rd, rn, 0)
2963 }
2964 }
2965
2966 // ADC: Add with Carry (Thumb-2 32-bit)
2967 // ADC.W Rd, Rn, Rm: EB40 Rn | 00 Rd 00 Rm
2968 ArmOp::Adc { rd, rn, op2 } => {
2969 let rd_bits = reg_to_bits(rd);
2970 let rn_bits = reg_to_bits(rn);
2971
2972 if let Operand2::Reg(rm) = op2 {
2973 let rm_bits = reg_to_bits(rm);
2974 // ADC.W Rd, Rn, Rm (T2): 1110 1011 0100 Rn | 0 000 Rd 00 00 Rm
2975 let hw1: u16 = (0xEB40 | rn_bits) as u16;
2976 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2977
2978 let mut bytes = hw1.to_le_bytes().to_vec();
2979 bytes.extend_from_slice(&hw2.to_le_bytes());
2980 Ok(bytes)
2981 } else {
2982 // ADC with immediate - use 32-bit encoding
2983 let hw1: u16 = (0xF140 | rn_bits) as u16;
2984 let hw2: u16 = (rd_bits << 8) as u16;
2985 let mut bytes = hw1.to_le_bytes().to_vec();
2986 bytes.extend_from_slice(&hw2.to_le_bytes());
2987 Ok(bytes)
2988 }
2989 }
2990
2991 // SUBS sets flags (borrow), SBC uses borrow from previous SUBS
2992 ArmOp::Subs { rd, rn, op2 } => {
2993 let rd_bits = reg_to_bits(rd) as u16;
2994 let rn_bits = reg_to_bits(rn) as u16;
2995
2996 if let Operand2::Reg(rm) = op2 {
2997 let rm_bits = reg_to_bits(rm) as u16;
2998 // 16-bit SUBS is R0-R7 only; high-register i64 pair operands
2999 // would overflow the 3-bit fields (#178/#180 class). Guard
3000 // and fall back to 32-bit SUBS.W for high registers.
3001 if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
3002 // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
3003 let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
3004 Ok(instr.to_le_bytes().to_vec())
3005 } else {
3006 self.encode_thumb32_subs_reg_raw(
3007 rd_bits as u32,
3008 rn_bits as u32,
3009 rm_bits as u32,
3010 )
3011 }
3012 } else {
3013 // 32-bit Thumb-2 SUBS with immediate
3014 self.encode_thumb32_subs(rd, rn, 0)
3015 }
3016 }
3017
3018 // SBC: Subtract with Carry (Thumb-2 32-bit)
3019 // SBC.W Rd, Rn, Rm: EB60 Rn | 00 Rd 00 Rm
3020 ArmOp::Sbc { rd, rn, op2 } => {
3021 let rd_bits = reg_to_bits(rd);
3022 let rn_bits = reg_to_bits(rn);
3023
3024 if let Operand2::Reg(rm) = op2 {
3025 let rm_bits = reg_to_bits(rm);
3026 // SBC.W Rd, Rn, Rm (T2): 1110 1011 0110 Rn | 0 000 Rd 00 00 Rm
3027 let hw1: u16 = (0xEB60 | rn_bits) as u16;
3028 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3029
3030 let mut bytes = hw1.to_le_bytes().to_vec();
3031 bytes.extend_from_slice(&hw2.to_le_bytes());
3032 Ok(bytes)
3033 } else {
3034 // SBC with immediate - use 32-bit encoding
3035 let hw1: u16 = (0xF160 | rn_bits) as u16;
3036 let hw2: u16 = (rd_bits << 8) as u16;
3037 let mut bytes = hw1.to_le_bytes().to_vec();
3038 bytes.extend_from_slice(&hw2.to_le_bytes());
3039 Ok(bytes)
3040 }
3041 }
3042
3043 // === 32-bit Thumb-2 encodings ===
3044
3045 // SDIV: 11111011 1001 Rn 1111 Rd 1111 Rm
3046 ArmOp::Sdiv { rd, rn, rm } => {
3047 let rd_bits = reg_to_bits(rd);
3048 let rn_bits = reg_to_bits(rn);
3049 let rm_bits = reg_to_bits(rm);
3050 reg_bits_checked(rd_bits)?;
3051 reg_bits_checked(rn_bits)?;
3052 reg_bits_checked(rm_bits)?;
3053
3054 // Thumb-2 SDIV: FB90 F0F0 | Rn<<16 | Rd<<8 | Rm
3055 // First halfword: 1111 1011 1001 Rn = 0xFB90 | Rn
3056 // Second halfword: 1111 Rd 1111 Rm = 0xF0F0 | Rd<<8 | Rm
3057 let hw1: u16 = (0xFB90 | rn_bits) as u16;
3058 let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
3059
3060 // Thumb-2 32-bit instructions: first halfword, then second halfword (little-endian each)
3061 let mut bytes = hw1.to_le_bytes().to_vec();
3062 bytes.extend_from_slice(&hw2.to_le_bytes());
3063 encoding_contracts::verify_thumb32(&bytes);
3064 Ok(bytes)
3065 }
3066
3067 // UDIV: 11111011 1011 Rn 1111 Rd 1111 Rm
3068 ArmOp::Udiv { rd, rn, rm } => {
3069 let rd_bits = reg_to_bits(rd);
3070 let rn_bits = reg_to_bits(rn);
3071 let rm_bits = reg_to_bits(rm);
3072 reg_bits_checked(rd_bits)?;
3073 reg_bits_checked(rn_bits)?;
3074 reg_bits_checked(rm_bits)?;
3075
3076 // Thumb-2 UDIV: FBB0 F0F0 | Rn<<16 | Rd<<8 | Rm
3077 let hw1: u16 = (0xFBB0 | rn_bits) as u16;
3078 let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
3079
3080 let mut bytes = hw1.to_le_bytes().to_vec();
3081 bytes.extend_from_slice(&hw2.to_le_bytes());
3082 encoding_contracts::verify_thumb32(&bytes);
3083 Ok(bytes)
3084 }
3085
3086 ArmOp::Umull { rdlo, rdhi, rn, rm } => {
3087 let rdlo_bits = reg_to_bits(rdlo);
3088 let rdhi_bits = reg_to_bits(rdhi);
3089 let rn_bits = reg_to_bits(rn);
3090 let rm_bits = reg_to_bits(rm);
3091 reg_bits_checked(rdlo_bits)?;
3092 reg_bits_checked(rdhi_bits)?;
3093 reg_bits_checked(rn_bits)?;
3094 reg_bits_checked(rm_bits)?;
3095
3096 // Thumb-2 UMULL: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm
3097 let hw1: u16 = (0xFBA0 | rn_bits) as u16;
3098 let hw2: u16 = ((rdlo_bits << 12) | (rdhi_bits << 8) | rm_bits) as u16;
3099
3100 let mut bytes = hw1.to_le_bytes().to_vec();
3101 bytes.extend_from_slice(&hw2.to_le_bytes());
3102 encoding_contracts::verify_thumb32(&bytes);
3103 Ok(bytes)
3104 }
3105
3106 // MUL (Thumb-2 32-bit): MUL Rd, Rn, Rm
3107 ArmOp::Mul { rd, rn, rm } => {
3108 let rd_bits = reg_to_bits(rd);
3109 let rn_bits = reg_to_bits(rn);
3110 let rm_bits = reg_to_bits(rm);
3111
3112 // Thumb-2 MUL: FB00 F000 | Rn | Rd<<8 | Rm
3113 // 11111011 0000 Rn | 1111 Rd 0000 Rm
3114 let hw1: u16 = (0xFB00 | rn_bits) as u16;
3115 let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
3116
3117 let mut bytes = hw1.to_le_bytes().to_vec();
3118 bytes.extend_from_slice(&hw2.to_le_bytes());
3119 Ok(bytes)
3120 }
3121
3122 // MLS: Rd = Ra - Rn * Rm
3123 ArmOp::Mls { rd, rn, rm, ra } => {
3124 let rd_bits = reg_to_bits(rd);
3125 let rn_bits = reg_to_bits(rn);
3126 let rm_bits = reg_to_bits(rm);
3127 let ra_bits = reg_to_bits(ra);
3128
3129 // Thumb-2 MLS: FB00 Rn | Ra Rd 0001 Rm
3130 // 11111011 0000 Rn | Ra Rd 0001 Rm
3131 let hw1: u16 = (0xFB00 | rn_bits) as u16;
3132 let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | 0x10 | rm_bits) as u16;
3133
3134 let mut bytes = hw1.to_le_bytes().to_vec();
3135 bytes.extend_from_slice(&hw2.to_le_bytes());
3136 Ok(bytes)
3137 }
3138
3139 ArmOp::Mla { rd, rn, rm, ra } => {
3140 let rd_bits = reg_to_bits(rd);
3141 let rn_bits = reg_to_bits(rn);
3142 let rm_bits = reg_to_bits(rm);
3143 let ra_bits = reg_to_bits(ra);
3144
3145 // Thumb-2 MLA: FB00 Rn | Ra Rd 0000 Rm — same as MLS without the
3146 // bit-4 (0x10) op flag. rd = ra + rn*rm.
3147 let hw1: u16 = (0xFB00 | rn_bits) as u16;
3148 let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | rm_bits) as u16;
3149
3150 let mut bytes = hw1.to_le_bytes().to_vec();
3151 bytes.extend_from_slice(&hw2.to_le_bytes());
3152 Ok(bytes)
3153 }
3154
3155 // AND (Thumb-2 32-bit)
3156 ArmOp::And { rd, rn, op2 } => {
3157 if let Operand2::Reg(rm) = op2 {
3158 let rd_bits = reg_to_bits(rd);
3159 let rn_bits = reg_to_bits(rn);
3160 let rm_bits = reg_to_bits(rm);
3161
3162 // Thumb-2 AND register: EA00 Rn | 0 Rd 00 00 Rm
3163 let hw1: u16 = (0xEA00 | rn_bits) as u16;
3164 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3165
3166 let mut bytes = hw1.to_le_bytes().to_vec();
3167 bytes.extend_from_slice(&hw2.to_le_bytes());
3168 Ok(bytes)
3169 } else if let Operand2::Imm(imm) = op2 {
3170 let rd_bits = reg_to_bits(rd);
3171 let rn_bits = reg_to_bits(rn);
3172
3173 // Thumb-2 AND.W immediate T1: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8.
3174 // The i:imm3:imm8 field is a ThumbExpandImm modified immediate —
3175 // encode it correctly (or error on an un-encodable value)
3176 // rather than packing raw bits, closing the silent-miscompile
3177 // class for AND alongside ORR/EOR (#251) / ADD/SUB (#253) /
3178 // CMP (#255).
3179 let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3180 synth_core::Error::synthesis(
3181 "AND immediate is not a valid ThumbExpandImm — materialize into a register",
3182 )
3183 })?;
3184 let i_bit = (field >> 11) & 1;
3185 let imm3 = (field >> 8) & 0x7;
3186 let imm8 = field & 0xFF;
3187
3188 let hw1: u16 = (0xF000 | (i_bit << 10) | rn_bits) as u16;
3189 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3190
3191 let mut bytes = hw1.to_le_bytes().to_vec();
3192 bytes.extend_from_slice(&hw2.to_le_bytes());
3193 Ok(bytes)
3194 } else {
3195 // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
3196 // operand form the arm does not encode (Reg and Imm only) -- #615's
3197 // silent-miscompile class, on the DEFAULT target. Refuse instead.
3198 Err(synth_core::Error::synthesis(format!(
3199 "Thumb-2 AND with operand {op2:?} has no Thumb-2 encoding here (Reg and Imm \
3200 are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
3201 )))
3202 }
3203 }
3204
3205 // ORR (Thumb-2 32-bit)
3206 ArmOp::Orr { rd, rn, op2 } => {
3207 if let Operand2::Reg(rm) = op2 {
3208 let rd_bits = reg_to_bits(rd);
3209 let rn_bits = reg_to_bits(rn);
3210 let rm_bits = reg_to_bits(rm);
3211
3212 // Thumb-2 ORR: EA40 Rn | 0 Rd 00 00 Rm
3213 let hw1: u16 = (0xEA40 | rn_bits) as u16;
3214 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3215
3216 let mut bytes = hw1.to_le_bytes().to_vec();
3217 bytes.extend_from_slice(&hw2.to_le_bytes());
3218 Ok(bytes)
3219 } else if let Operand2::Imm(imm) = op2 {
3220 // ORR.W immediate T1: 11110 i 0 0010 S Rn | 0 imm3 Rd imm8.
3221 // Only the zero-extended byte form (imm <= 0xFF) is encoded;
3222 // larger modified immediates need ThumbExpandImm — return an
3223 // error rather than silently emit a NOP (Ok-or-Err, #180/#185).
3224 let imm_val = *imm as u32;
3225 if imm_val > 0xFF {
3226 return Err(synth_core::Error::synthesis(
3227 "ORR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3228 ));
3229 }
3230 let rd_bits = reg_to_bits(rd);
3231 let rn_bits = reg_to_bits(rn);
3232 let hw1: u16 = (0xF040 | rn_bits) as u16;
3233 let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3234 let mut bytes = hw1.to_le_bytes().to_vec();
3235 bytes.extend_from_slice(&hw2.to_le_bytes());
3236 Ok(bytes)
3237 } else {
3238 // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
3239 // operand form the arm does not encode (Reg and Imm only) -- #615's
3240 // silent-miscompile class, on the DEFAULT target. Refuse instead.
3241 Err(synth_core::Error::synthesis(format!(
3242 "Thumb-2 ORR with operand {op2:?} has no Thumb-2 encoding here (Reg and Imm \
3243 are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
3244 )))
3245 }
3246 }
3247
3248 // EOR (Thumb-2 32-bit)
3249 ArmOp::Eor { rd, rn, op2 } => {
3250 if let Operand2::Reg(rm) = op2 {
3251 let rd_bits = reg_to_bits(rd);
3252 let rn_bits = reg_to_bits(rn);
3253 let rm_bits = reg_to_bits(rm);
3254
3255 // Thumb-2 EOR: EA80 Rn | 0 Rd 00 00 Rm
3256 let hw1: u16 = (0xEA80 | rn_bits) as u16;
3257 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3258
3259 let mut bytes = hw1.to_le_bytes().to_vec();
3260 bytes.extend_from_slice(&hw2.to_le_bytes());
3261 Ok(bytes)
3262 } else if let Operand2::Imm(imm) = op2 {
3263 // EOR.W immediate T1: 11110 i 0 0100 S Rn | 0 imm3 Rd imm8.
3264 // Byte form only (imm <= 0xFF); larger needs ThumbExpandImm —
3265 // error, not a silent NOP (Ok-or-Err, #180/#185).
3266 let imm_val = *imm as u32;
3267 if imm_val > 0xFF {
3268 return Err(synth_core::Error::synthesis(
3269 "EOR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3270 ));
3271 }
3272 let rd_bits = reg_to_bits(rd);
3273 let rn_bits = reg_to_bits(rn);
3274 let hw1: u16 = (0xF080 | rn_bits) as u16;
3275 let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3276 let mut bytes = hw1.to_le_bytes().to_vec();
3277 bytes.extend_from_slice(&hw2.to_le_bytes());
3278 Ok(bytes)
3279 } else {
3280 // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
3281 // operand form the arm does not encode (Reg and Imm only) -- #615's
3282 // silent-miscompile class, on the DEFAULT target. Refuse instead.
3283 Err(synth_core::Error::synthesis(format!(
3284 "Thumb-2 EOR with operand {op2:?} has no Thumb-2 encoding here (Reg and Imm \
3285 are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
3286 )))
3287 }
3288 }
3289
3290 // Shift operations (16-bit for low registers)
3291 ArmOp::Lsl { rd, rn, shift } => {
3292 let rd_bits = reg_to_bits(rd) as u16;
3293 let rn_bits = reg_to_bits(rn) as u16;
3294 let shift_bits = (*shift as u16) & 0x1F;
3295
3296 if rd_bits < 8 && rn_bits < 8 {
3297 // LSLS Rd, Rm, #imm5 (16-bit): 0000 0 imm5 Rm Rd
3298 let instr: u16 = (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3299 Ok(instr.to_le_bytes().to_vec())
3300 } else {
3301 // Use 32-bit encoding for high registers
3302 self.encode_thumb32_shift(rd, rn, *shift, 0b00) // LSL type
3303 }
3304 }
3305
3306 ArmOp::Lsr { rd, rn, shift } => {
3307 let rd_bits = reg_to_bits(rd) as u16;
3308 let rn_bits = reg_to_bits(rn) as u16;
3309 let shift_bits = (*shift as u16) & 0x1F;
3310
3311 if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3312 // LSRS Rd, Rm, #imm5 (16-bit): 0000 1 imm5 Rm Rd
3313 let instr: u16 = 0x0800 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3314 Ok(instr.to_le_bytes().to_vec())
3315 } else {
3316 self.encode_thumb32_shift(rd, rn, *shift, 0b01) // LSR type
3317 }
3318 }
3319
3320 ArmOp::Asr { rd, rn, shift } => {
3321 let rd_bits = reg_to_bits(rd) as u16;
3322 let rn_bits = reg_to_bits(rn) as u16;
3323 let shift_bits = (*shift as u16) & 0x1F;
3324
3325 if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3326 // ASRS Rd, Rm, #imm5 (16-bit): 0001 0 imm5 Rm Rd
3327 let instr: u16 = 0x1000 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3328 Ok(instr.to_le_bytes().to_vec())
3329 } else {
3330 self.encode_thumb32_shift(rd, rn, *shift, 0b10) // ASR type
3331 }
3332 }
3333
3334 ArmOp::Ror { rd, rn, shift } => {
3335 // ROR doesn't have a 16-bit immediate form, use 32-bit
3336 self.encode_thumb32_shift(rd, rn, *shift, 0b11) // ROR type
3337 }
3338
3339 // Register-based shifts (Thumb-2 32-bit)
3340 // Encoding: 11111010 0xxS Rn 1111 Rd 0000 Rm
3341 // xx = shift type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
3342 ArmOp::LslReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b00),
3343 ArmOp::LsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b01),
3344 ArmOp::AsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b10),
3345 ArmOp::RorReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b11),
3346
3347 // RSB (Reverse Subtract): Rd = imm - Rn
3348 // Thumb-2 T2 encoding: 11110 i 0 1110 S Rn | 0 imm3 Rd imm8
3349 ArmOp::Rsb { rd, rn, imm } => {
3350 let rd_bits = reg_to_bits(rd);
3351 let rn_bits = reg_to_bits(rn);
3352
3353 // #681 class audit: the T2 `i:imm3:imm8` field is a
3354 // ThumbExpandImm modified immediate and RSB has NO plain-imm12
3355 // (T4-style) form — packing a raw value > 0xFF silently encodes
3356 // a different constant (#253/#255 class). All current emitters
3357 // use imm 32 (shift complement), which expands to itself, so
3358 // this gate is byte-identical for existing codegen.
3359 let field = try_thumb_expand_imm(*imm).ok_or_else(|| {
3360 synth_core::Error::synthesis(
3361 "RSB immediate is not a valid ThumbExpandImm — materialize into a register",
3362 )
3363 })?;
3364 let i_bit = (field >> 11) & 1;
3365 let imm3 = (field >> 8) & 0x7;
3366 let imm8 = field & 0xFF;
3367
3368 // hw1: 11110 i 01110 0 Rn (S=0)
3369 let hw1: u16 = (0xF1C0 | (i_bit << 10) | rn_bits) as u16;
3370 // hw2: 0 imm3 Rd imm8
3371 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3372
3373 let mut bytes = hw1.to_le_bytes().to_vec();
3374 bytes.extend_from_slice(&hw2.to_le_bytes());
3375 Ok(bytes)
3376 }
3377
3378 // CLZ (Thumb-2 32-bit)
3379 ArmOp::Clz { rd, rm } => {
3380 let rd_bits = reg_to_bits(rd);
3381 let rm_bits = reg_to_bits(rm);
3382
3383 // Thumb-2 CLZ: FAB0 Rm | F8 Rd Rm
3384 // 11111010 1011 Rm | 1111 1000 Rd Rm
3385 let hw1: u16 = (0xFAB0 | rm_bits) as u16;
3386 let hw2: u16 = (0xF080 | (rd_bits << 8) | rm_bits) as u16;
3387
3388 let mut bytes = hw1.to_le_bytes().to_vec();
3389 bytes.extend_from_slice(&hw2.to_le_bytes());
3390 Ok(bytes)
3391 }
3392
3393 // RBIT (Thumb-2 32-bit)
3394 ArmOp::Rbit { rd, rm } => {
3395 let rd_bits = reg_to_bits(rd);
3396 let rm_bits = reg_to_bits(rm);
3397
3398 // Thumb-2 RBIT: FA90 Rm | F0 Rd A0 Rm
3399 // 11111010 1001 Rm | 1111 Rd 1010 Rm
3400 let hw1: u16 = (0xFA90 | rm_bits) as u16;
3401 let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rm_bits) as u16;
3402
3403 let mut bytes = hw1.to_le_bytes().to_vec();
3404 bytes.extend_from_slice(&hw2.to_le_bytes());
3405 Ok(bytes)
3406 }
3407
3408 // SXTB (16-bit for low registers)
3409 ArmOp::Sxtb { rd, rm } => {
3410 let rd_bits = reg_to_bits(rd) as u16;
3411 let rm_bits = reg_to_bits(rm) as u16;
3412
3413 if rd_bits < 8 && rm_bits < 8 {
3414 // SXTB Rd, Rm (16-bit): 1011 0010 01 Rm Rd
3415 let instr: u16 = 0xB240 | (rm_bits << 3) | rd_bits;
3416 Ok(instr.to_le_bytes().to_vec())
3417 } else {
3418 // Thumb-2 SXTB.W: FA4F F(rd)80 (rm)
3419 // 11111010 0100 1111 | 1111 Rd 10 rotate Rm
3420 let rd_bits32 = rd_bits as u32;
3421 let rm_bits32 = rm_bits as u32;
3422 let hw1: u16 = 0xFA4F;
3423 let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3424 let mut bytes = hw1.to_le_bytes().to_vec();
3425 bytes.extend_from_slice(&hw2.to_le_bytes());
3426 Ok(bytes)
3427 }
3428 }
3429
3430 // SXTH (16-bit for low registers)
3431 ArmOp::Sxth { rd, rm } => {
3432 let rd_bits = reg_to_bits(rd) as u16;
3433 let rm_bits = reg_to_bits(rm) as u16;
3434
3435 if rd_bits < 8 && rm_bits < 8 {
3436 // SXTH Rd, Rm (16-bit): 1011 0010 00 Rm Rd
3437 let instr: u16 = 0xB200 | (rm_bits << 3) | rd_bits;
3438 Ok(instr.to_le_bytes().to_vec())
3439 } else {
3440 // Thumb-2 SXTH.W: FA0F F(rd)80 (rm)
3441 // 11111010 0000 1111 | 1111 Rd 10 rotate Rm
3442 let rd_bits32 = rd_bits as u32;
3443 let rm_bits32 = rm_bits as u32;
3444 let hw1: u16 = 0xFA0F;
3445 let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3446 let mut bytes = hw1.to_le_bytes().to_vec();
3447 bytes.extend_from_slice(&hw2.to_le_bytes());
3448 Ok(bytes)
3449 }
3450 }
3451
3452 // UXTB Rd,Rm — zero-extend byte (rd = rm & 0xff)
3453 ArmOp::Uxtb { rd, rm } => {
3454 let rd_bits = reg_to_bits(rd) as u16;
3455 let rm_bits = reg_to_bits(rm) as u16;
3456 if rd_bits < 8 && rm_bits < 8 {
3457 // UXTB Rd, Rm (16-bit): 1011 0010 11 Rm Rd
3458 let instr: u16 = 0xB2C0 | (rm_bits << 3) | rd_bits;
3459 Ok(instr.to_le_bytes().to_vec())
3460 } else {
3461 // Thumb-2 UXTB.W: FA5F F(rd)80 (rm)
3462 let hw1: u16 = 0xFA5F;
3463 let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3464 let mut bytes = hw1.to_le_bytes().to_vec();
3465 bytes.extend_from_slice(&hw2.to_le_bytes());
3466 Ok(bytes)
3467 }
3468 }
3469
3470 // UXTH Rd,Rm — zero-extend halfword (rd = rm & 0xffff)
3471 ArmOp::Uxth { rd, rm } => {
3472 let rd_bits = reg_to_bits(rd) as u16;
3473 let rm_bits = reg_to_bits(rm) as u16;
3474 if rd_bits < 8 && rm_bits < 8 {
3475 // UXTH Rd, Rm (16-bit): 1011 0010 10 Rm Rd
3476 let instr: u16 = 0xB280 | (rm_bits << 3) | rd_bits;
3477 Ok(instr.to_le_bytes().to_vec())
3478 } else {
3479 // Thumb-2 UXTH.W: FA1F F(rd)80 (rm)
3480 let hw1: u16 = 0xFA1F;
3481 let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3482 let mut bytes = hw1.to_le_bytes().to_vec();
3483 bytes.extend_from_slice(&hw2.to_le_bytes());
3484 Ok(bytes)
3485 }
3486 }
3487
3488 // CMP (can be 16-bit for low registers)
3489 ArmOp::Cmp { rn, op2 } => {
3490 let rn_bits = reg_to_bits(rn) as u16;
3491
3492 if let Operand2::Imm(imm) = op2 {
3493 // Only use 16-bit encoding for non-negative immediates 0-255
3494 // Negative immediates must use 32-bit encoding
3495 if *imm >= 0 && *imm <= 255 && rn_bits < 8 {
3496 // CMP Rn, #imm8 (16-bit): 0010 1 Rn imm8
3497 let instr: u16 = 0x2800 | (rn_bits << 8) | (*imm as u16 & 0xFF);
3498 Ok(instr.to_le_bytes().to_vec())
3499 } else {
3500 self.encode_thumb32_cmp_imm(rn, *imm as u32)
3501 }
3502 } else if let Operand2::Reg(rm) = op2 {
3503 let rm_bits = reg_to_bits(rm) as u16;
3504 if rn_bits < 8 && rm_bits < 8 {
3505 // CMP Rn, Rm (16-bit low): 0100 0010 10 Rm Rn
3506 let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
3507 Ok(instr.to_le_bytes().to_vec())
3508 } else {
3509 // CMP Rn, Rm (16-bit high): 0100 0101 N Rm Rn[2:0]
3510 let n_bit = (rn_bits >> 3) & 1;
3511 let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
3512 Ok(instr.to_le_bytes().to_vec())
3513 }
3514 } else {
3515 // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
3516 // operand form the arm does not encode (Imm and Reg only) -- #615's
3517 // silent-miscompile class, on the DEFAULT target. Refuse instead.
3518 Err(synth_core::Error::synthesis(format!(
3519 "Thumb-2 CMP with operand {op2:?} has no Thumb-2 encoding here (Imm and Reg \
3520 are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
3521 )))
3522 }
3523 }
3524
3525 // CMN (Compare Negative) - computes Rn + op2 and sets flags
3526 // CMN Rn, #1 sets Z flag if Rn == -1 (since -1 + 1 = 0)
3527 ArmOp::Cmn { rn, op2 } => {
3528 let rn_bits = reg_to_bits(rn) as u16;
3529
3530 if let Operand2::Imm(imm) = op2 {
3531 // CMN.W Rn, #imm (32-bit): i:imm3:imm8 is a ThumbExpandImm
3532 // modified immediate (the field sits in imm3=hw2[14:12],
3533 // imm8=hw2[7:0], i=hw1[10]). Encode it correctly, or error on
3534 // an un-encodable value — replacing the old silent `0xBF00`
3535 // NOP (the last of the silent-miscompile data-proc encoders).
3536 let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3537 synth_core::Error::synthesis(
3538 "CMN immediate is not a valid ThumbExpandImm — materialize into a register",
3539 )
3540 })?;
3541 let i_bit = (field >> 11) & 1;
3542 let imm3 = (field >> 8) & 0x7;
3543 let imm8 = field & 0xFF;
3544 let hw1: u16 = (0xF110 | (i_bit << 10) as u16) | rn_bits;
3545 let hw2: u16 = (imm3 << 12) as u16 | 0x0F00 | imm8 as u16;
3546 let mut bytes = hw1.to_le_bytes().to_vec();
3547 bytes.extend_from_slice(&hw2.to_le_bytes());
3548 Ok(bytes)
3549 } else if let Operand2::Reg(rm) = op2 {
3550 let rm_bits = reg_to_bits(rm) as u16;
3551 // 16-bit CMN (T1) only encodes R0-R7; high registers overflow
3552 // the 3-bit fields and corrupt the operands (#184, the #180
3553 // class). CMN has no high-register 16-bit form, so fall back
3554 // to 32-bit CMN.W (T2): EB10 Rn | 0F00 Rm (ADD.W with S=1 and
3555 // Rd discarded as PC/1111).
3556 if rn_bits < 8 && rm_bits < 8 {
3557 // CMN Rn, Rm (16-bit): 0100 0010 11 Rm Rn
3558 let instr: u16 = 0x42C0 | (rm_bits << 3) | rn_bits;
3559 Ok(instr.to_le_bytes().to_vec())
3560 } else {
3561 let hw1: u16 = 0xEB10 | rn_bits;
3562 let hw2: u16 = 0x0F00 | rm_bits;
3563 let mut bytes = hw1.to_le_bytes().to_vec();
3564 bytes.extend_from_slice(&hw2.to_le_bytes());
3565 Ok(bytes)
3566 }
3567 } else {
3568 Ok(vec![0xBF, 0x00])
3569 }
3570 }
3571
3572 // LDR (can be 16-bit for simple cases)
3573 ArmOp::Ldr { rd, addr } => {
3574 let rd_bits = reg_to_bits(rd);
3575 let base_bits = reg_to_bits(&addr.base);
3576
3577 // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3578 if let Some(offset_reg) = &addr.offset_reg {
3579 let rm_bits = reg_to_bits(offset_reg);
3580
3581 // If there's also an immediate offset, we need to ADD it first
3582 if addr.offset != 0 {
3583 // Use R12 (IP) as scratch to avoid clobbering the address register
3584 // ADD R12, Rm, #offset; LDR Rd, [base, R12]
3585 let scratch = Reg::R12;
3586 let mut bytes =
3587 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3588 bytes.extend(self.encode_thumb32_ldr_reg(rd, &addr.base, &scratch)?);
3589 return Ok(bytes);
3590 }
3591
3592 // Simple register offset: LDR Rd, [Rn, Rm]
3593 // 16-bit: only if Rd, Rn, Rm < R8
3594 if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3595 // LDR Rd, [Rn, Rm] (16-bit): 0101 100 Rm Rn Rd
3596 let instr: u16 = 0x5800
3597 | ((rm_bits as u16) << 6)
3598 | ((base_bits as u16) << 3)
3599 | (rd_bits as u16);
3600 return Ok(instr.to_le_bytes().to_vec());
3601 }
3602
3603 // 32-bit register offset
3604 return self.encode_thumb32_ldr_reg(rd, &addr.base, offset_reg);
3605 }
3606
3607 // Immediate offset mode [base, #imm]
3608 let offset = addr.offset as u32;
3609
3610 if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3611 // LDR Rd, [Rn, #imm5*4] (16-bit): 0110 1 imm5 Rn Rd
3612 let imm5 = (offset >> 2) as u16;
3613 let instr: u16 =
3614 0x6800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3615 Ok(instr.to_le_bytes().to_vec())
3616 } else {
3617 self.encode_thumb32_ldr(rd, &addr.base, offset)
3618 }
3619 }
3620
3621 // STR (can be 16-bit for simple cases)
3622 ArmOp::Str { rd, addr } => {
3623 let rd_bits = reg_to_bits(rd);
3624 let base_bits = reg_to_bits(&addr.base);
3625
3626 // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3627 if let Some(offset_reg) = &addr.offset_reg {
3628 let rm_bits = reg_to_bits(offset_reg);
3629
3630 // If there's also an immediate offset, we need to ADD it first
3631 if addr.offset != 0 {
3632 // Use R12 (IP) as scratch to avoid clobbering the address register
3633 // ADD R12, Rm, #offset; STR Rd, [base, R12]
3634 let scratch = Reg::R12;
3635 let mut bytes =
3636 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3637 bytes.extend(self.encode_thumb32_str_reg(rd, &addr.base, &scratch)?);
3638 return Ok(bytes);
3639 }
3640
3641 // Simple register offset: STR Rd, [Rn, Rm]
3642 // 16-bit: only if Rd, Rn, Rm < R8
3643 if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3644 // STR Rd, [Rn, Rm] (16-bit): 0101 000 Rm Rn Rd
3645 let instr: u16 = 0x5000
3646 | ((rm_bits as u16) << 6)
3647 | ((base_bits as u16) << 3)
3648 | (rd_bits as u16);
3649 return Ok(instr.to_le_bytes().to_vec());
3650 }
3651
3652 // 32-bit register offset
3653 return self.encode_thumb32_str_reg(rd, &addr.base, offset_reg);
3654 }
3655
3656 // Immediate offset mode [base, #imm]
3657 let offset = addr.offset as u32;
3658
3659 if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3660 // STR Rd, [Rn, #imm5*4] (16-bit): 0110 0 imm5 Rn Rd
3661 let imm5 = (offset >> 2) as u16;
3662 let instr: u16 =
3663 0x6000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3664 Ok(instr.to_le_bytes().to_vec())
3665 } else {
3666 self.encode_thumb32_str(rd, &addr.base, offset)
3667 }
3668 }
3669
3670 // LDRB (Thumb-2)
3671 ArmOp::Ldrb { rd, addr } => {
3672 let rd_bits = reg_to_bits(rd);
3673 let base_bits = reg_to_bits(&addr.base);
3674
3675 if let Some(offset_reg) = &addr.offset_reg {
3676 if addr.offset != 0 {
3677 let scratch = Reg::R12;
3678 let mut bytes =
3679 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3680 bytes.extend(self.encode_thumb32_ldrb_reg(rd, &addr.base, &scratch)?);
3681 return Ok(bytes);
3682 }
3683 return self.encode_thumb32_ldrb_reg(rd, &addr.base, offset_reg);
3684 }
3685
3686 let offset = addr.offset as u32;
3687 if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3688 // LDRB Rd, [Rn, #imm5] (16-bit): 0111 1 imm5 Rn Rd
3689 let instr: u16 = 0x7800
3690 | ((offset as u16) << 6)
3691 | ((base_bits as u16) << 3)
3692 | (rd_bits as u16);
3693 Ok(instr.to_le_bytes().to_vec())
3694 } else {
3695 self.encode_thumb32_ldrb_imm(rd, &addr.base, offset)
3696 }
3697 }
3698
3699 // LDRSB (Thumb-2)
3700 ArmOp::Ldrsb { rd, addr } => {
3701 let rd_bits = reg_to_bits(rd);
3702 let base_bits = reg_to_bits(&addr.base);
3703
3704 if let Some(offset_reg) = &addr.offset_reg {
3705 if addr.offset != 0 {
3706 let scratch = Reg::R12;
3707 let mut bytes =
3708 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3709 bytes.extend(self.encode_thumb32_ldrsb_reg(rd, &addr.base, &scratch)?);
3710 return Ok(bytes);
3711 }
3712 return self.encode_thumb32_ldrsb_reg(rd, &addr.base, offset_reg);
3713 }
3714
3715 let offset = addr.offset as u32;
3716 // LDRSB has no 16-bit immediate form (only register)
3717 // For 16-bit reg form: only if Rd, Rn, Rm < R8
3718 if rd_bits < 8 && base_bits < 8 && offset == 0 {
3719 // No immediate 16-bit encoding for LDRSB; use 32-bit
3720 self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3721 } else {
3722 self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3723 }
3724 }
3725
3726 // LDRH (Thumb-2)
3727 ArmOp::Ldrh { rd, addr } => {
3728 let rd_bits = reg_to_bits(rd);
3729 let base_bits = reg_to_bits(&addr.base);
3730
3731 if let Some(offset_reg) = &addr.offset_reg {
3732 if addr.offset != 0 {
3733 let scratch = Reg::R12;
3734 let mut bytes =
3735 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3736 bytes.extend(self.encode_thumb32_ldrh_reg(rd, &addr.base, &scratch)?);
3737 return Ok(bytes);
3738 }
3739 return self.encode_thumb32_ldrh_reg(rd, &addr.base, offset_reg);
3740 }
3741
3742 let offset = addr.offset as u32;
3743 if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3744 // LDRH Rd, [Rn, #imm5*2] (16-bit): 1000 1 imm5 Rn Rd
3745 let imm5 = (offset >> 1) as u16;
3746 let instr: u16 =
3747 0x8800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3748 Ok(instr.to_le_bytes().to_vec())
3749 } else {
3750 self.encode_thumb32_ldrh_imm(rd, &addr.base, offset)
3751 }
3752 }
3753
3754 // LDRSH (Thumb-2)
3755 ArmOp::Ldrsh { rd, addr } => {
3756 if let Some(offset_reg) = &addr.offset_reg {
3757 if addr.offset != 0 {
3758 let scratch = Reg::R12;
3759 let mut bytes =
3760 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3761 bytes.extend(self.encode_thumb32_ldrsh_reg(rd, &addr.base, &scratch)?);
3762 return Ok(bytes);
3763 }
3764 return self.encode_thumb32_ldrsh_reg(rd, &addr.base, offset_reg);
3765 }
3766
3767 let offset = addr.offset as u32;
3768 self.encode_thumb32_ldrsh_imm(rd, &addr.base, offset)
3769 }
3770
3771 // STRB (Thumb-2)
3772 ArmOp::Strb { rd, addr } => {
3773 let rd_bits = reg_to_bits(rd);
3774 let base_bits = reg_to_bits(&addr.base);
3775
3776 if let Some(offset_reg) = &addr.offset_reg {
3777 if addr.offset != 0 {
3778 let scratch = Reg::R12;
3779 let mut bytes =
3780 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3781 bytes.extend(self.encode_thumb32_strb_reg(rd, &addr.base, &scratch)?);
3782 return Ok(bytes);
3783 }
3784 return self.encode_thumb32_strb_reg(rd, &addr.base, offset_reg);
3785 }
3786
3787 let offset = addr.offset as u32;
3788 if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3789 // STRB Rd, [Rn, #imm5] (16-bit): 0111 0 imm5 Rn Rd
3790 let instr: u16 = 0x7000
3791 | ((offset as u16) << 6)
3792 | ((base_bits as u16) << 3)
3793 | (rd_bits as u16);
3794 Ok(instr.to_le_bytes().to_vec())
3795 } else {
3796 self.encode_thumb32_strb_imm(rd, &addr.base, offset)
3797 }
3798 }
3799
3800 // STRH (Thumb-2)
3801 ArmOp::Strh { rd, addr } => {
3802 let rd_bits = reg_to_bits(rd);
3803 let base_bits = reg_to_bits(&addr.base);
3804
3805 if let Some(offset_reg) = &addr.offset_reg {
3806 if addr.offset != 0 {
3807 let scratch = Reg::R12;
3808 let mut bytes =
3809 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3810 bytes.extend(self.encode_thumb32_strh_reg(rd, &addr.base, &scratch)?);
3811 return Ok(bytes);
3812 }
3813 return self.encode_thumb32_strh_reg(rd, &addr.base, offset_reg);
3814 }
3815
3816 let offset = addr.offset as u32;
3817 if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3818 // STRH Rd, [Rn, #imm5*2] (16-bit): 1000 0 imm5 Rn Rd
3819 let imm5 = (offset >> 1) as u16;
3820 let instr: u16 =
3821 0x8000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3822 Ok(instr.to_le_bytes().to_vec())
3823 } else {
3824 self.encode_thumb32_strh_imm(rd, &addr.base, offset)
3825 }
3826 }
3827
3828 // MemorySize (Thumb-2)
3829 ArmOp::MemorySize { rd } => {
3830 // LSR rd, R10, #16 — memory size in bytes / 65536 = pages
3831 // Thumb-2 16-bit: LSRS Rd, Rm, #imm5 — 0000 1 imm5 Rm Rd
3832 let rd_bits = reg_to_bits(rd);
3833 let r10_bits = reg_to_bits(&Reg::R10);
3834 if rd_bits < 8 && r10_bits < 8 {
3835 let instr: u16 =
3836 0x0800 | (16u16 << 6) | ((r10_bits as u16) << 3) | (rd_bits as u16);
3837 Ok(instr.to_le_bytes().to_vec())
3838 } else {
3839 // Thumb-2 32-bit LSR: 1110 1010 010 0 1111 | 0 imm3 Rd imm2 01 Rm
3840 let imm5: u32 = 16;
3841 let imm3 = (imm5 >> 2) & 0x7;
3842 let imm2 = imm5 & 0x3;
3843 let hw1: u16 = 0xEA4F;
3844 let hw2: u16 =
3845 ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | 0x10 | r10_bits) as u16;
3846 let mut bytes = hw1.to_le_bytes().to_vec();
3847 bytes.extend_from_slice(&hw2.to_le_bytes());
3848 Ok(bytes)
3849 }
3850 }
3851
3852 // MemoryGrow (Thumb-2)
3853 ArmOp::MemoryGrow { rd, .. } => {
3854 // On embedded with fixed memory, always return -1 (failure)
3855 // MVN rd, #0 → MOV rd, #-1
3856 // Thumb-2 32-bit: MVN: 1111 0 i 0 0 0 1 1 0 1111 | 0 imm3 Rd imm8
3857 let rd_bits = reg_to_bits(rd);
3858 let hw1: u16 = 0xF06F; // MVN with i=0
3859 let hw2: u16 = (rd_bits << 8) as u16; // imm8=0 → ~0 = 0xFFFFFFFF = -1
3860 let mut bytes = hw1.to_le_bytes().to_vec();
3861 bytes.extend_from_slice(&hw2.to_le_bytes());
3862 Ok(bytes)
3863 }
3864
3865 // BX (16-bit)
3866 ArmOp::Bx { rm } => {
3867 let rm_bits = reg_to_bits(rm) as u16;
3868 // BX Rm (16-bit): 0100 0111 0 Rm 000
3869 let instr: u16 = 0x4700 | (rm_bits << 3);
3870 Ok(instr.to_le_bytes().to_vec())
3871 }
3872
3873 // BLX (16-bit) - Branch with Link and Exchange
3874 // BLX Rm: 0100 0111 1 Rm 000
3875 ArmOp::Blx { rm } => {
3876 let rm_bits = reg_to_bits(rm) as u16;
3877 let instr: u16 = 0x4780 | (rm_bits << 3);
3878 Ok(instr.to_le_bytes().to_vec())
3879 }
3880
3881 // CallIndirect - indirect function call via table lookup
3882 // table_index_reg contains the table index
3883 // Generates (#642): MOVW ip,#size [; MOVT]; CMP idx,ip; BLO +1;
3884 // UDF #0; LSL R12,idx,#2; LDR R12,[R11,R12]; BLX R12
3885 // #650, table_byte_offset != 0 (a non-zero table in the contiguous
3886 // R11 region): the pointer load becomes
3887 // ADD R12,R11,R12; LDR R12,[R12,#offset]
3888 // #664, null_check (the table has null slots, linked as ZERO
3889 // words): the loaded pointer is null-checked before the BLX —
3890 // CMP.W R12,#0; BNE +1; UDF #0
3891 // #676, type_check (heterogeneous table — runtime §4.4.8 type
3892 // check against the type-id sidecar at R11+off): after the
3893 // bounds guard —
3894 // LSL R12,idx,#2; ADD R12,R11,R12;
3895 // LDR R12,[R12,#type_off]; CMP.W R12,#id;
3896 // BEQ +1; UDF #0
3897 // (the dispatch tail then recomputes idx*4 — idx stays live).
3898 ArmOp::CallIndirect {
3899 rd: _,
3900 type_idx: _,
3901 table_index_reg,
3902 table_size,
3903 table_byte_offset,
3904 null_check,
3905 type_check,
3906 } => {
3907 let idx_reg = reg_to_bits(table_index_reg);
3908 let mut bytes = Vec::new();
3909
3910 // The expansion:
3911 // 1. Bounds guard (#642): trap (UDF #0, WASM Core §4.4.8) when
3912 // index >= table size. Without it an out-of-bounds index
3913 // reads past the table and BLXes whatever word lies there —
3914 // an uncontrolled indirect branch instead of a trap.
3915 // 2. Multiplies index by 4 (function pointer size)
3916 // 3. Loads function pointer from table (table base in R11)
3917 // 4. Calls the function via BLX
3918 //
3919 // Table base setup must be done by caller/runtime. The type
3920 // check §4.4.8 also requires is discharged at COMPILE time:
3921 // the selector only emits this op after verifying the closed-
3922 // world property that every table entry's signature equals the
3923 // expected type (the raw code-pointer table carries no runtime
3924 // type ids to compare) — see the #642 selector guard.
3925
3926 // MOVW R12, #(size & 0xFFFF) — Thumb-2 T3:
3927 // 11110 i 100100 imm4 | 0 imm3 Rd imm8 (Rd=R12).
3928 let size_lo = *table_size & 0xFFFF;
3929 let hw1: u16 =
3930 (0xF240 | (((size_lo >> 11) & 1) << 10) | ((size_lo >> 12) & 0xF)) as u16;
3931 let hw2: u16 =
3932 ((((size_lo >> 8) & 0x7) << 12) | (12 << 8) | (size_lo & 0xFF)) as u16;
3933 bytes.extend_from_slice(&hw1.to_le_bytes());
3934 bytes.extend_from_slice(&hw2.to_le_bytes());
3935 // MOVT R12, #(size >> 16) — only when the table size exceeds
3936 // 16 bits (never in practice, but the guard must not compare
3937 // against a truncated size).
3938 let size_hi = *table_size >> 16;
3939 if size_hi != 0 {
3940 let hw1: u16 =
3941 (0xF2C0 | (((size_hi >> 11) & 1) << 10) | ((size_hi >> 12) & 0xF)) as u16;
3942 let hw2: u16 =
3943 ((((size_hi >> 8) & 0x7) << 12) | (12 << 8) | (size_hi & 0xFF)) as u16;
3944 bytes.extend_from_slice(&hw1.to_le_bytes());
3945 bytes.extend_from_slice(&hw2.to_le_bytes());
3946 }
3947 // CMP idx, R12 — 16-bit T2 (high-register capable):
3948 // 010001 01 N Rm(4) Rn(3), Rn full = N:Rn3.
3949 let cmp: u16 = (0x4500 | ((idx_reg & 8) << 4) | (12 << 3) | (idx_reg & 7)) as u16;
3950 bytes.extend_from_slice(&cmp.to_le_bytes());
3951 // BLO +1 insn (skip the UDF when index < size) — B<cond>.N
3952 // imm8=0: target = branch + 4. LO = unsigned lower.
3953 bytes.extend_from_slice(&0xD300u16.to_le_bytes());
3954 // UDF #0 — call_indirect out-of-bounds trap (same trap idiom as
3955 // the div-by-zero guards).
3956 bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3957
3958 // #676: runtime type check — ONLY for a heterogeneous table
3959 // (mixed signatures, closed-world verdict impossible). Load
3960 // the indexed slot's structural class id from the type-id
3961 // sidecar (`R11 + type_off + idx*4`; `type_off` = sidecar
3962 // base + this table's base offset, a compile-time constant)
3963 // and compare it against the expected type's class id — a
3964 // mismatch is the WASM Core §4.4.8 type trap. Null slots
3965 // carry the reserved id 0, so this compare subsumes the
3966 // #664 null trap (the selector passes `null_check: false`).
3967 // `None` emits NOTHING: every homogeneous table keeps the
3968 // pre-#676 bytes identical BY CONSTRUCTION. R12 stays the
3969 // only scratch (#212); the dispatch tail below recomputes
3970 // idx*4 — the index register is never clobbered here.
3971 if let Some((expected_id, type_off)) = type_check {
3972 // RQ-61-IMMRANGE (#1072): compiled out in release, where
3973 // the masks below silently TRUNCATE (id 256 compares as
3974 // 0 — a NULL slot would pass the §4.4.8 check). The
3975 // enforcement claim is DEMONSTRATED: the sole `Some`
3976 // producer, `resolve_runtime_type_check`, loud-declines
3977 // id > 255 / offset > 4095, tripped by
3978 // `test_676_call_indirect_runtime_check_range_declines`
3979 // (mutation-checked — see the A32 twin above).
3980 debug_assert!(*expected_id <= 255, "selector enforces the CMP imm8 range");
3981 debug_assert!(*type_off <= 4095, "selector enforces the LDR imm12 range");
3982 // MOV.W R12, idx, LSL #2 (same encoding as the dispatch
3983 // tail's index scale below).
3984 bytes.extend_from_slice(&0xEA4Fu16.to_le_bytes());
3985 bytes.extend_from_slice(
3986 &(((0x0C00 | (0b10 << 6)) | idx_reg) as u16).to_le_bytes(),
3987 );
3988 // ADD.W R12, R11, R12 (the #650 base-add form).
3989 bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
3990 bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
3991 // LDR.W R12, [R12, #type_off] — T3 LDR (immediate):
3992 // 1111 1000 1101 Rn=1100 | Rt=1100 imm12.
3993 bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
3994 bytes.extend_from_slice(
3995 &(0xC000u16 | (*type_off as u16 & 0x0FFF)).to_le_bytes(),
3996 );
3997 // CMP.W R12, #expected_id — T2 CMP (immediate), imm8
3998 // (same form as the #664 null check's CMP.W R12, #0).
3999 bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
4000 bytes.extend_from_slice(
4001 &(0x0F00u16 | (*expected_id as u16 & 0xFF)).to_le_bytes(),
4002 );
4003 // BEQ +1 insn (skip the UDF when the class id matches) —
4004 // B<cond>.N imm8=0: target = branch + 4. EQ.
4005 bytes.extend_from_slice(&0xD000u16.to_le_bytes());
4006 // UDF #0 — the §4.4.8 type-mismatch trap (same idiom as
4007 // the bounds guard above).
4008 bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
4009 }
4010
4011 // LSL R12, idx_reg, #2 (multiply index by 4)
4012 // Thumb-2 MOV with shift: 11101010 010 S 1111 | 0 imm3 Rd imm2 type Rm
4013 // LSL: type=00 (bits 5:4), imm5=2 -> imm3=000, imm2=10 (bits 7:6)
4014 // #597: the shift amount was previously shifted into bits 5:4 —
4015 // the TYPE field — encoding `mov.w ip, rm, ASR #32`, which
4016 // destroyed the index and dispatched table entry 0 for every
4017 // call. imm2 lives at bits 7:6.
4018 let hw1: u16 = 0xEA4F_u16; // MOV.W R12, Rm, LSL #2
4019 let hw2: u16 = ((0x0C00 | (0b10 << 6)) | idx_reg) as u16;
4020 bytes.extend_from_slice(&hw1.to_le_bytes());
4021 bytes.extend_from_slice(&hw2.to_le_bytes());
4022
4023 if *table_byte_offset == 0 {
4024 // Table 0 (base = R11 itself): the pre-#650 single-load
4025 // form — a single-table module's bytes stay identical BY
4026 // CONSTRUCTION.
4027 // LDR R12, [R11, R12] - load function pointer
4028 // Thumb-2 LDR (register): 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
4029 // Rn=R11, Rt=R12, Rm=R12, imm2=00 (no shift)
4030 let ldr_hw1: u16 = 0xF85B; // LDR.W Rt, [R11, Rm]
4031 let ldr_hw2: u16 = 0xC00C; // Rt=R12, imm2=00, Rm=R12
4032 bytes.extend_from_slice(&ldr_hw1.to_le_bytes());
4033 bytes.extend_from_slice(&ldr_hw2.to_le_bytes());
4034 } else {
4035 // #650: table N of the contiguous R11 region — fold the
4036 // compile-time base offset into the pointer load via the
4037 // LDR imm12 form (R12 stays the only scratch, per the
4038 // #212 convention).
4039 assert!(
4040 *table_byte_offset <= 4095,
4041 "call_indirect table base offset {table_byte_offset} exceeds \
4042 LDR imm12 — the selector must have declined this (#650)"
4043 );
4044 // ADD.W R12, R11, R12 — T3 ADD (register):
4045 // 11101011000 S=0 Rn=1011 | 0 imm3=000 Rd=1100 imm2=00 type=00 Rm=1100
4046 bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
4047 bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
4048 // LDR.W R12, [R12, #offset] — T3 LDR (immediate):
4049 // 1111 1000 1101 Rn=1100 | Rt=1100 imm12
4050 bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
4051 bytes.extend_from_slice(
4052 &((0xC000u16) | (*table_byte_offset as u16 & 0x0FFF)).to_le_bytes(),
4053 );
4054 }
4055
4056 // #664: null-slot trap — ONLY when the table image carries
4057 // null (uninitialized) slots, which the layout contract
4058 // requires to be linked as ZERO words. A fully-initialized
4059 // table skips this branch entirely, keeping the pre-#664
4060 // expansion byte-identical BY CONSTRUCTION (the #650
4061 // offset-0 trick).
4062 if *null_check {
4063 // CMP.W R12, #0 — T2 CMP (immediate): 11110 i 0 1101 1
4064 // Rn(4) | 0 imm3 1111 imm8, Rn=R12, imm=0.
4065 bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
4066 bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
4067 // BNE +1 insn (skip the UDF when the pointer is non-null)
4068 // — B<cond>.N imm8=0: target = branch + 4. NE.
4069 bytes.extend_from_slice(&0xD100u16.to_le_bytes());
4070 // UDF #0 — call_indirect null-funcref trap (WASM Core
4071 // §4.4.8: calling an uninitialized element traps; same
4072 // trap idiom as the bounds guard above).
4073 bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
4074 }
4075
4076 // BLX R12 (call function indirectly)
4077 // BLX Rm (16-bit): 0100 0111 1 Rm 000
4078 let blx: u16 = 0x47E0; // BLX R12
4079 bytes.extend_from_slice(&blx.to_le_bytes());
4080
4081 Ok(bytes)
4082 }
4083
4084 // Label pseudo-instruction: emits no machine code
4085 ArmOp::Label { .. } => Ok(Vec::new()),
4086
4087 // Conditional branch to label (generic) - offset 0, will be patched
4088 ArmOp::Bcc { cond, label: _ } => {
4089 use synth_synthesis::Condition;
4090 let cond_bits: u16 = match cond {
4091 Condition::EQ => 0x0,
4092 Condition::NE => 0x1,
4093 Condition::HS => 0x2,
4094 Condition::LO => 0x3,
4095 Condition::HI => 0x8,
4096 Condition::LS => 0x9,
4097 Condition::GE => 0xA,
4098 Condition::LT => 0xB,
4099 Condition::GT => 0xC,
4100 Condition::LE => 0xD,
4101 };
4102 // 16-bit B<cond> with offset 0: 1101 cond imm8
4103 let instr: u16 = 0xD000 | (cond_bits << 8);
4104 Ok(instr.to_le_bytes().to_vec())
4105 }
4106
4107 // Branch instructions
4108 ArmOp::B { label: _ } => {
4109 // Simplified: B.N with offset 0
4110 // For real usage, would need label resolution
4111 let instr: u16 = 0xE000; // B.N #0
4112 Ok(instr.to_le_bytes().to_vec())
4113 }
4114
4115 // BHS (Branch if Higher or Same) - used for bounds checking
4116 // Condition code: 0x2 (C set)
4117 ArmOp::Bhs { label: _ } => {
4118 // 16-bit B<cond> with offset 0: 1101 cond imm8
4119 // cond = 0x2 (HS)
4120 let instr: u16 = 0xD200; // BHS.N #0
4121 Ok(instr.to_le_bytes().to_vec())
4122 }
4123
4124 // BLO (Branch if Lower) - complementary to BHS
4125 // Condition code: 0x3 (C clear)
4126 ArmOp::Blo { label: _ } => {
4127 // 16-bit B<cond> with offset 0: 1101 cond imm8
4128 // cond = 0x3 (LO)
4129 let instr: u16 = 0xD300; // BLO.N #0
4130 Ok(instr.to_le_bytes().to_vec())
4131 }
4132
4133 // Branch with numeric offset (Thumb-2)
4134 // Thumb-2 B.W instruction: 32-bit with +-16MB range
4135 ArmOp::BOffset { offset } => {
4136 // offset is already the halfword displacement: (target - branch - 4) / 2
4137 // This is the raw encoded value, accounting for variable-length instructions
4138 let halfword_offset = *offset;
4139
4140 // 16-bit B.N encoding: 1110 0 imm11 (11-bit signed halfword offset)
4141 // Range: -1024 to +1022 halfwords
4142 if (-1024..=1022).contains(&halfword_offset) {
4143 // 16-bit B.N encoding: 1110 0 imm11
4144 let imm11 = (halfword_offset as u16) & 0x7FF;
4145 let instr: u16 = 0xE000 | imm11;
4146 Ok(instr.to_le_bytes().to_vec())
4147 } else {
4148 // 32-bit B.W encoding for larger offsets
4149 // First halfword: 1111 0 S imm10
4150 // Second halfword: 10 J1 0 J2 imm11
4151 // Total offset = SignExtend(S:I1:I2:imm10:imm11:0)
4152 // where I1 = NOT(J1 XOR S), I2 = NOT(J2 XOR S)
4153
4154 // The B.W (T4) encoding packs the signed offset as:
4155 // S:I1:I2:imm10:imm11:0 (25-bit signed, halfword-aligned)
4156 // where J1 = NOT(I1 XOR S), J2 = NOT(I2 XOR S)
4157 // Input halfword_offset already equals (target - PC - 4) / 2,
4158 // so the full byte offset = halfword_offset << 1.
4159 // The encoding fields split that 25-bit signed value (including the
4160 // implicit trailing zero) as: S | imm10 | imm11
4161 // with I1 = bit 23 and I2 = bit 22 of the signed offset.
4162 let signed_offset = halfword_offset << 1; // byte offset
4163 let s = if signed_offset < 0 { 1u32 } else { 0u32 };
4164 let uoffset = signed_offset as u32;
4165 let imm10 = (uoffset >> 12) & 0x3FF; // bits [21:12]
4166 let imm11 = (uoffset >> 1) & 0x7FF; // bits [11:1]
4167 let i1 = (uoffset >> 23) & 1; // bit 23
4168 let i2 = (uoffset >> 22) & 1; // bit 22
4169 let j1 = (!(i1 ^ s)) & 1; // J1 = NOT(I1 XOR S)
4170 let j2 = (!(i2 ^ s)) & 1; // J2 = NOT(I2 XOR S)
4171
4172 let hw1: u16 = (0xF000 | (s << 10) | imm10) as u16;
4173 let hw2: u16 = (0x9000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4174
4175 let mut bytes = hw1.to_le_bytes().to_vec();
4176 bytes.extend_from_slice(&hw2.to_le_bytes());
4177 Ok(bytes)
4178 }
4179 }
4180
4181 // Conditional branch with numeric offset (Thumb-2)
4182 ArmOp::BCondOffset { cond, offset } => {
4183 use synth_synthesis::Condition;
4184 let cond_bits: u16 = match cond {
4185 Condition::EQ => 0x0,
4186 Condition::NE => 0x1,
4187 Condition::HS => 0x2,
4188 Condition::LO => 0x3,
4189 Condition::HI => 0x8,
4190 Condition::LS => 0x9,
4191 Condition::GE => 0xA,
4192 Condition::LT => 0xB,
4193 Condition::GT => 0xC,
4194 Condition::LE => 0xD,
4195 };
4196
4197 // offset is already the halfword displacement: (target - branch - 4) / 2
4198 // This is the raw imm8 value for 16-bit B<cond> encoding
4199 let halfword_offset = *offset;
4200
4201 // 16-bit B<cond> encoding: 1101 cond imm8
4202 // Range: -256 to +254 halfwords (imm8 is sign-extended and shifted left 1)
4203 if (-128..=127).contains(&halfword_offset) {
4204 let imm8 = (halfword_offset as u16) & 0xFF;
4205 let instr: u16 = 0xD000 | (cond_bits << 8) | imm8;
4206 Ok(instr.to_le_bytes().to_vec())
4207 } else {
4208 // 32-bit B<cond>.W (encoding T3) for larger offsets
4209 // First halfword: 1111 0 S cond(4) imm6
4210 // Second halfword: 10 J1 0 J2 imm11
4211 //
4212 // Per ARMv7-M, the branch BYTE offset is
4213 // SignExtend(S:J2:J1:imm6:imm11:'0'), i.e. the field value
4214 // S:J2:J1:imm6:imm11 IS the signed 20-bit HALFWORD offset —
4215 // imm11/imm6/J1/J2/S take `halfword_offset` bits [10:0],
4216 // [16:11], 17, 18 and 19 directly (mirroring the T4
4217 // unconditional arm above).
4218 //
4219 // #740: this arm previously packed `halfword_offset >> 1`
4220 // into imm6:imm11 — HALVING the displacement — so every
4221 // wide conditional branch (span > 254 bytes) landed at half
4222 // its intended offset: gust_poll's loop-head `br_if` to an
4223 // outer block end jumped mid-shape. Narrow (16-bit) B<cond>
4224 // encodings were unaffected, which is why short-range CF
4225 // fixtures never caught it.
4226 if !(-(1 << 19)..(1 << 19)).contains(&halfword_offset) {
4227 return Err(synth_core::Error::synthesis(format!(
4228 "B<cond>.W (T3) halfword offset {halfword_offset} exceeds \
4229 the signed 20-bit encoding range (±1 MB) — refusing to \
4230 emit a truncated branch"
4231 )));
4232 }
4233 let u = halfword_offset as u32;
4234 let imm11 = u & 0x7FF; // halfword offset bits [10:0]
4235 let imm6 = (u >> 11) & 0x3F; // bits [16:11]
4236 let j1 = (u >> 17) & 1; // bit 17
4237 let j2 = (u >> 18) & 1; // bit 18
4238 let s = (u >> 19) & 1; // sign (range-checked above)
4239
4240 let hw1: u16 = (0xF000 | (s << 10) | ((cond_bits as u32) << 6) | imm6) as u16;
4241 let hw2: u16 = (0x8000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4242
4243 let mut bytes = hw1.to_le_bytes().to_vec();
4244 bytes.extend_from_slice(&hw2.to_le_bytes());
4245 Ok(bytes)
4246 }
4247 }
4248
4249 ArmOp::Bl { label: _ } => {
4250 // BL is always 32-bit in Thumb-2, encoded here as a relocatable
4251 // placeholder; an R_ARM_THM_CALL relocation patches the target
4252 // (see arm_backend.rs). The placeholder must carry an embedded
4253 // addend of -4 so the relocation nets to exactly the symbol S.
4254 //
4255 // Thumb BL computes `target = (P + 4) + signed_offset`. Under
4256 // R_ARM_THM_CALL the linker resolves using the in-place addend;
4257 // a 0xF800 placeholder (addend 0) lands at S+4 — every call one
4258 // instruction past the callee entry (#174). The correct
4259 // placeholder is what `gas` emits for `bl <extern>`:
4260 // f7ff fffe -> `bl <self>` (S=1, J1=J2=1, imm = -4 addend),
4261 // i.e. hw1=0xF7FF, hw2=0xFFFE. This nets to S, not S+4.
4262 // (The earlier 0xD000 was worse still — a ~+0x600000 addend,
4263 // the garbage `bl c0000c` and "truncated to fit" of #167.)
4264 let hw1: u16 = 0xF7FF;
4265 let hw2: u16 = 0xFFFE;
4266 let mut bytes = hw1.to_le_bytes().to_vec();
4267 bytes.extend_from_slice(&hw2.to_le_bytes());
4268 Ok(bytes)
4269 }
4270
4271 // MVN
4272 ArmOp::Mvn { rd, op2 } => {
4273 if let Operand2::Reg(rm) = op2 {
4274 let rd_bits = reg_to_bits(rd) as u16;
4275 let rm_bits = reg_to_bits(rm) as u16;
4276
4277 if rd_bits < 8 && rm_bits < 8 {
4278 // MVNS Rd, Rm (16-bit): 0100 0011 11 Rm Rd
4279 let instr: u16 = 0x43C0 | (rm_bits << 3) | rd_bits;
4280 Ok(instr.to_le_bytes().to_vec())
4281 } else {
4282 // 32-bit MVN
4283 let hw1: u16 = 0xEA6F_u16;
4284 let hw2: u16 = ((reg_to_bits(rd) << 8) | reg_to_bits(rm)) as u16;
4285 let mut bytes = hw1.to_le_bytes().to_vec();
4286 bytes.extend_from_slice(&hw2.to_le_bytes());
4287 Ok(bytes)
4288 }
4289 } else {
4290 // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
4291 // operand form the arm does not encode (Reg only) -- #615's
4292 // silent-miscompile class, on the DEFAULT target. Refuse instead.
4293 Err(synth_core::Error::synthesis(format!(
4294 "Thumb-2 MVN with operand {op2:?} has no Thumb-2 encoding here (Reg \
4295 are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
4296 )))
4297 }
4298 }
4299
4300 // MOVW - Move Wide (Thumb-2 32-bit)
4301 ArmOp::Movw { rd, imm16 } => {
4302 self.encode_thumb32_movw_raw(reg_to_bits(rd), *imm16 as u32)
4303 }
4304
4305 // MOVT - Move Top (Thumb-2 32-bit)
4306 ArmOp::Movt { rd, imm16 } => {
4307 self.encode_thumb32_movt_raw(reg_to_bits(rd), *imm16 as u32)
4308 }
4309
4310 // #237: symbol-relative MOVW/MOVT. Encode the addend's low/high 16
4311 // bits in place; the backend records an R_ARM_MOVW_ABS_NC /
4312 // R_ARM_MOVT_ABS relocation against `symbol`, so the linker adds the
4313 // symbol's final address to the in-place addend (REL semantics).
4314 ArmOp::MovwSym { rd, addend, .. } => {
4315 self.encode_thumb32_movw_raw(reg_to_bits(rd), (*addend as u32) & 0xffff)
4316 }
4317 ArmOp::MovtSym { rd, addend, .. } => {
4318 self.encode_thumb32_movt_raw(reg_to_bits(rd), ((*addend as u32) >> 16) & 0xffff)
4319 }
4320
4321 // #345: literal-pool address load — emit a PLACEHOLDER `LDR.W rd,
4322 // [pc, #0]` (U=1, imm12=0). The backend (arm_backend.rs) places the
4323 // 4-byte pool word at the end of the function, records the R_ARM_ABS32
4324 // relocation against `symbol+addend`, and patches the imm12 with the
4325 // real PC-relative distance once the pool offset is known.
4326 // Encoding T2: 1111 1000 1101 1111 | Rt(4) imm12(12), with the literal
4327 // base = Align(PC,4) and PC = address of this instruction + 4.
4328 ArmOp::LdrSym { rd, .. } => {
4329 let rt = reg_to_bits(rd) as u16;
4330 let hw1: u16 = 0xF8DF; // LDR.W (literal), U=1
4331 let hw2: u16 = rt << 12; // imm12 = 0 placeholder
4332 let mut bytes = Vec::with_capacity(4);
4333 bytes.extend_from_slice(&hw1.to_le_bytes());
4334 bytes.extend_from_slice(&hw2.to_le_bytes());
4335 Ok(bytes)
4336 }
4337
4338 // SetCond: Materialize condition flag into register (0 or 1)
4339 // Strategy: ITE <cond>; MOV Rd, #1; MOV Rd, #0
4340 // IMPORTANT: Must use ITE (If-Then-Else) because 16-bit Thumb MOV
4341 // always sets flags (MOVS). We need to evaluate the condition BEFORE
4342 // any MOV instruction clobbers the flags from CMP.
4343 ArmOp::SetCond { rd, cond } => {
4344 let rd_bits = reg_to_bits(rd) as u16;
4345
4346 // Condition code encoding for IT block
4347 use synth_synthesis::Condition;
4348 let cond_bits: u16 = match cond {
4349 Condition::EQ => 0x0,
4350 Condition::NE => 0x1,
4351 Condition::LT => 0xB,
4352 Condition::LE => 0xD,
4353 Condition::GT => 0xC,
4354 Condition::GE => 0xA,
4355 Condition::LO => 0x3, // CC/LO (unsigned <)
4356 Condition::LS => 0x9, // LS (unsigned <=)
4357 Condition::HI => 0x8, // HI (unsigned >)
4358 Condition::HS => 0x2, // CS/HS (unsigned >=)
4359 };
4360
4361 // ITE <cond>: encodes If-Then-Else block
4362 // The mask field depends on firstcond[0]:
4363 // - If firstcond[0] = 0: mask = 0xC for TE pattern (ITE EQ = BF0C)
4364 // - If firstcond[0] = 1: mask = 0x4 for TE pattern (ITE NE = BF14)
4365 let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4366 let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4367
4368 // Materialize 0/1 into Rd. The 16-bit MOVS (T1) encodes Rd in a
4369 // 3-bit field (bits[10:8]) — only R0–R7. For a high register
4370 // (R8–R12) `rd_bits << 8` overflows into bit 11 and silently
4371 // turns MOVS into CMP (00100 → 00101), corrupting the result
4372 // (this mis-materialized gale's `has_waiter`, so its `local.set`
4373 // stored a stale register → the binary-sem WAKE dispatch read
4374 // garbage). Use the 32-bit MOV.W (T2) for high registers, which
4375 // has a 4-bit Rd field. MOV.W with S=0 doesn't set flags, which
4376 // is fine inside the ITE (the materialized value is the result;
4377 // the flags are not consumed afterwards).
4378 let mut bytes = ite_instr.to_le_bytes().to_vec();
4379 let push_mov = |bytes: &mut Vec<u8>, imm: u16| {
4380 if rd_bits <= 7 {
4381 let m: u16 = 0x2000 | (rd_bits << 8) | imm; // 16-bit MOVS Rd,#imm
4382 bytes.extend_from_slice(&m.to_le_bytes());
4383 } else {
4384 // 32-bit MOV.W Rd, #imm (T2): F04F | (Rd<<8) | imm8
4385 let hw1: u16 = 0xF04F;
4386 let hw2: u16 = (rd_bits << 8) | imm;
4387 bytes.extend_from_slice(&hw1.to_le_bytes());
4388 bytes.extend_from_slice(&hw2.to_le_bytes());
4389 }
4390 };
4391 push_mov(&mut bytes, 1); // Then branch (condition true) → 1
4392 push_mov(&mut bytes, 0); // Else branch (condition false) → 0
4393 Ok(bytes)
4394 }
4395
4396 // I64SetCond: Compare two i64 register pairs, result 0/1 in rd
4397 // EQ/NE: CMP lo,lo; IT EQ; CMPEQ hi,hi; ITE <cond>; MOV 1; MOV 0
4398 // LT: CMP lo,lo; SBCS rd,hi,hi; ITE LT; MOV 1; MOV 0
4399 // GT: CMP lo,lo (swapped); SBCS rd,hi,hi (swapped); ITE LT; MOV 1; MOV 0
4400 ArmOp::I64SetCond {
4401 rd,
4402 rn_lo,
4403 rn_hi,
4404 rm_lo,
4405 rm_hi,
4406 cond,
4407 } => {
4408 use synth_synthesis::Condition;
4409 let rd_bits = reg_to_bits(rd) as u16;
4410 let mut bytes = Vec::new();
4411
4412 // Helper: encode CMP Rn, Rm (16-bit)
4413 let encode_cmp_reg = |rn: &synth_synthesis::Reg,
4414 rm: &synth_synthesis::Reg|
4415 -> Vec<u8> {
4416 let rn_bits = reg_to_bits(rn) as u16;
4417 let rm_bits = reg_to_bits(rm) as u16;
4418 if rn_bits < 8 && rm_bits < 8 {
4419 let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
4420 instr.to_le_bytes().to_vec()
4421 } else {
4422 let n_bit = (rn_bits >> 3) & 1;
4423 let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
4424 instr.to_le_bytes().to_vec()
4425 }
4426 };
4427
4428 // Helper: encode ITE <cond> (2 bytes)
4429 let encode_ite = |cond_bits: u16| -> Vec<u8> {
4430 let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4431 let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4432 ite_instr.to_le_bytes().to_vec()
4433 };
4434
4435 // Helper: encode SetCond (ITE + MOV #1 + MOV #0) for given condition
4436 let encode_setcond = |cond_bits: u16, rd_bits: u16| -> Vec<u8> {
4437 let mut b = encode_ite(cond_bits);
4438 if rd_bits < 8 {
4439 let mov_one: u16 = 0x2001 | (rd_bits << 8);
4440 let mov_zero: u16 = 0x2000 | (rd_bits << 8);
4441 b.extend_from_slice(&mov_one.to_le_bytes());
4442 b.extend_from_slice(&mov_zero.to_le_bytes());
4443 } else {
4444 // #311: rd >= R8 — the 16-bit MOV imm8 form has a 3-bit
4445 // rd field; rd_bits<<8 overflows into bit 11 and
4446 // TRANSMUTES the MOV into CMP (0x2001|0x0800 = 0x2801 =
4447 // CMP r0,#1): the boolean dies in the flags and the
4448 // consumer reads a stale register. Use the 32-bit
4449 // MOV.W (T2: F04F 0000|rd<<8|imm8) — IT-legal,
4450 // flag-preserving. Same class as H-CODE-9 / #180.
4451 for imm in [1u16, 0u16] {
4452 let hw1: u16 = 0xF04F;
4453 let hw2: u16 = (rd_bits << 8) | imm;
4454 b.extend_from_slice(&hw1.to_le_bytes());
4455 b.extend_from_slice(&hw2.to_le_bytes());
4456 }
4457 }
4458 b
4459 };
4460
4461 match cond {
4462 Condition::EQ | Condition::NE => {
4463 // CMP rn_lo, rm_lo (compare low words)
4464 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4465
4466 // IT EQ (execute next instruction only if Z=1)
4467 let it_eq: u16 = 0xBF08; // IT EQ: cond=0000, mask=1000
4468 bytes.extend_from_slice(&it_eq.to_le_bytes());
4469
4470 // CMPEQ rn_hi, rm_hi (compare high words, only if low equal)
4471 bytes.extend_from_slice(&encode_cmp_reg(rn_hi, rm_hi));
4472
4473 // ITE <cond>; MOV rd, #1; MOV rd, #0
4474 let cond_bits: u16 = match cond {
4475 Condition::EQ => 0x0,
4476 Condition::NE => 0x1,
4477 _ => unreachable!(),
4478 };
4479 bytes.extend_from_slice(&encode_setcond(cond_bits, rd_bits));
4480 }
4481
4482 Condition::LT => {
4483 // CMP rn_lo, rm_lo (sets C flag for borrow)
4484 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4485
4486 // SBCS rd, rn_hi, rm_hi (subtract with carry, sets N,V flags)
4487 // SBCS.W Rd, Rn, Rm: EB70 Rn | 0000 Rd 0000 Rm
4488 let rn_hi_bits = reg_to_bits(rn_hi);
4489 let rm_hi_bits = reg_to_bits(rm_hi);
4490 let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4491 let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4492 bytes.extend_from_slice(&hw1.to_le_bytes());
4493 bytes.extend_from_slice(&hw2.to_le_bytes());
4494
4495 // ITE LT; MOV rd, #1; MOV rd, #0
4496 bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4497 }
4498
4499 Condition::GT => {
4500 // GT(a,b) = LT(b,a): swap operands
4501 // CMP rm_lo, rn_lo (swapped)
4502 bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4503
4504 // SBCS rd, rm_hi, rn_hi (swapped)
4505 let rm_hi_bits = reg_to_bits(rm_hi);
4506 let rn_hi_bits = reg_to_bits(rn_hi);
4507 let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4508 let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4509 bytes.extend_from_slice(&hw1.to_le_bytes());
4510 bytes.extend_from_slice(&hw2.to_le_bytes());
4511
4512 // ITE LT; MOV rd, #1; MOV rd, #0
4513 bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4514 }
4515
4516 Condition::LE => {
4517 // LE(a,b) = !GT(a,b): use GT logic but invert result
4518 // GT(a,b) = LT(b,a): so we do CMP(b,a) and check LT, then invert
4519 // CMP rm_lo, rn_lo (swapped, same as GT)
4520 bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4521
4522 // SBCS rd, rm_hi, rn_hi (swapped)
4523 let rm_hi_bits = reg_to_bits(rm_hi);
4524 let rn_hi_bits = reg_to_bits(rn_hi);
4525 let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4526 let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4527 bytes.extend_from_slice(&hw1.to_le_bytes());
4528 bytes.extend_from_slice(&hw2.to_le_bytes());
4529
4530 // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT, so inverting GT result)
4531 bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4532 }
4533
4534 Condition::GE => {
4535 // GE(a,b) = !LT(a,b): use LT logic but invert result
4536 // CMP rn_lo, rm_lo (same as LT)
4537 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4538
4539 // SBCS rd, rn_hi, rm_hi (same as LT)
4540 let rn_hi_bits = reg_to_bits(rn_hi);
4541 let rm_hi_bits = reg_to_bits(rm_hi);
4542 let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4543 let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4544 bytes.extend_from_slice(&hw1.to_le_bytes());
4545 bytes.extend_from_slice(&hw2.to_le_bytes());
4546
4547 // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT)
4548 bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4549 }
4550
4551 // Unsigned comparisons - same instruction sequence, different conditions
4552 Condition::LO => {
4553 // LO (unsigned LT): CMP lo, SBCS hi, check C=0
4554 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4555 let rn_hi_bits = reg_to_bits(rn_hi);
4556 let rm_hi_bits = reg_to_bits(rm_hi);
4557 let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4558 let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4559 bytes.extend_from_slice(&hw1.to_le_bytes());
4560 bytes.extend_from_slice(&hw2.to_le_bytes());
4561 bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4562 }
4563
4564 Condition::HI => {
4565 // HI (unsigned GT): swap operands and check LO
4566 bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4567 let rm_hi_bits = reg_to_bits(rm_hi);
4568 let rn_hi_bits = reg_to_bits(rn_hi);
4569 let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4570 let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4571 bytes.extend_from_slice(&hw1.to_le_bytes());
4572 bytes.extend_from_slice(&hw2.to_le_bytes());
4573 bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4574 }
4575
4576 Condition::LS => {
4577 // LS (unsigned LE): !(a > b) = !(HI), so do HI and invert
4578 bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4579 let rm_hi_bits = reg_to_bits(rm_hi);
4580 let rn_hi_bits = reg_to_bits(rn_hi);
4581 let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4582 let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4583 bytes.extend_from_slice(&hw1.to_le_bytes());
4584 bytes.extend_from_slice(&hw2.to_le_bytes());
4585 bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4586 }
4587
4588 Condition::HS => {
4589 // HS (unsigned GE): !(a < b) = !(LO)
4590 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4591 let rn_hi_bits = reg_to_bits(rn_hi);
4592 let rm_hi_bits = reg_to_bits(rm_hi);
4593 let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4594 let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4595 bytes.extend_from_slice(&hw1.to_le_bytes());
4596 bytes.extend_from_slice(&hw2.to_le_bytes());
4597 bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4598 }
4599 }
4600
4601 Ok(bytes)
4602 }
4603
4604 // I64SetCondZ: Test if i64 register pair is zero, result 0/1 in rd
4605 // ORR.W rd, rn_lo, rn_hi; CMP rd, #0; ITE EQ; MOV 1; MOV 0
4606 ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
4607 let rd_bits = reg_to_bits(rd);
4608 let rn_lo_bits = reg_to_bits(rn_lo);
4609 let rn_hi_bits = reg_to_bits(rn_hi);
4610 let mut bytes = Vec::new();
4611
4612 // ORR.W rd, rn_lo, rn_hi: EA40 rn_lo | 0000 rd 0000 rn_hi
4613 let hw1: u16 = (0xEA40 | rn_lo_bits) as u16;
4614 let hw2: u16 = ((rd_bits << 8) | rn_hi_bits) as u16;
4615 bytes.extend_from_slice(&hw1.to_le_bytes());
4616 bytes.extend_from_slice(&hw2.to_le_bytes());
4617
4618 // CMP rd, #0 — 16-bit form only for r0-r7 (3-bit rd field);
4619 // high registers take CMP.W (T2: F1B0|rn 0F00|imm8). This was
4620 // H-CODE-9: rd_bits<<8 overflowing the field compared the
4621 // WRONG register. Same hardening as the #311 SetCond fix.
4622 if rd_bits < 8 {
4623 let cmp_instr: u16 = 0x2800 | ((rd_bits as u16) << 8);
4624 bytes.extend_from_slice(&cmp_instr.to_le_bytes());
4625 } else {
4626 let hw1: u16 = 0xF1B0 | (rd_bits as u16);
4627 let hw2: u16 = 0x0F00;
4628 bytes.extend_from_slice(&hw1.to_le_bytes());
4629 bytes.extend_from_slice(&hw2.to_le_bytes());
4630 }
4631
4632 // ITE EQ; MOV rd, #1; MOV rd, #0 (32-bit MOV.W for rd >= R8,
4633 // #311 — see I64SetCond)
4634 let mask = 0xC_u16; // ITE EQ mask: firstcond[0]=0, mask=0xC
4635 let ite_instr: u16 = 0xBF00 | mask;
4636 bytes.extend_from_slice(&ite_instr.to_le_bytes());
4637 if rd_bits < 8 {
4638 let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
4639 let mov_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
4640 bytes.extend_from_slice(&mov_one.to_le_bytes());
4641 bytes.extend_from_slice(&mov_zero.to_le_bytes());
4642 } else {
4643 for imm in [1u16, 0u16] {
4644 let hw1: u16 = 0xF04F;
4645 let hw2: u16 = ((rd_bits as u16) << 8) | imm;
4646 bytes.extend_from_slice(&hw1.to_le_bytes());
4647 bytes.extend_from_slice(&hw2.to_le_bytes());
4648 }
4649 }
4650
4651 Ok(bytes)
4652 }
4653
4654 // I64Mul: 64-bit multiply using UMULL + MLA cross products
4655 // Formula: result = (a_lo * b_lo) + ((a_lo * b_hi + a_hi * b_lo) << 32)
4656 // Uses R12 as scratch register
4657 ArmOp::I64Mul {
4658 rd_lo,
4659 rd_hi,
4660 rn_lo,
4661 rn_hi,
4662 rm_lo,
4663 rm_hi,
4664 } => {
4665 let rd_lo_bits = reg_to_bits(rd_lo);
4666 let rd_hi_bits = reg_to_bits(rd_hi);
4667 let rn_lo_bits = reg_to_bits(rn_lo);
4668 let rn_hi_bits = reg_to_bits(rn_hi);
4669 let rm_lo_bits = reg_to_bits(rm_lo);
4670 let rm_hi_bits = reg_to_bits(rm_hi);
4671 let r12: u32 = 12; // IP scratch register
4672 let mut bytes = Vec::new();
4673
4674 // 1. MUL R12, rn_lo, rm_hi (R12 = a_lo * b_hi)
4675 // Thumb-2 MUL: hw1=0xFB00|Rn, hw2=0xF000|(Rd<<8)|Rm
4676 let hw1: u16 = (0xFB00 | rn_lo_bits) as u16;
4677 let hw2: u16 = (0xF000 | (r12 << 8) | rm_hi_bits) as u16;
4678 bytes.extend_from_slice(&hw1.to_le_bytes());
4679 bytes.extend_from_slice(&hw2.to_le_bytes());
4680
4681 // 2. MLA R12, rn_hi, rm_lo, R12 (R12 += a_hi * b_lo)
4682 // Thumb-2 MLA: hw1=0xFB00|Rn, hw2=(Ra<<12)|(Rd<<8)|Rm
4683 let hw1: u16 = (0xFB00 | rn_hi_bits) as u16;
4684 let hw2: u16 = ((r12 << 12) | (r12 << 8) | rm_lo_bits) as u16;
4685 bytes.extend_from_slice(&hw1.to_le_bytes());
4686 bytes.extend_from_slice(&hw2.to_le_bytes());
4687
4688 // 3. UMULL rd_lo, rd_hi, rn_lo, rm_lo (rd_lo:rd_hi = a_lo * b_lo)
4689 // Thumb-2 UMULL: hw1=0xFBA0|Rn, hw2=(RdLo<<12)|(RdHi<<8)|Rm
4690 let hw1: u16 = (0xFBA0 | rn_lo_bits) as u16;
4691 let hw2: u16 = ((rd_lo_bits << 12) | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4692 bytes.extend_from_slice(&hw1.to_le_bytes());
4693 bytes.extend_from_slice(&hw2.to_le_bytes());
4694
4695 // 4. ADD rd_hi, R12 (rd_hi += cross products)
4696 // 16-bit high reg ADD: 01000100 D Rm Rdn[2:0]
4697 let d_bit = (rd_hi_bits >> 3) & 1;
4698 let add_instr: u16 =
4699 (0x4400 | (d_bit << 7) | (r12 << 3) | (rd_hi_bits & 0x7)) as u16;
4700 bytes.extend_from_slice(&add_instr.to_le_bytes());
4701
4702 Ok(bytes)
4703 }
4704
4705 // I64Shl: 64-bit shift left with branch for n<32 vs n>=32.
4706 //
4707 // #1048: the expansion must NEVER write its own input operands.
4708 // The pre-#1048 sequence masked the amount IN PLACE
4709 // (`AND.W rm_lo, rm_lo, #63`) and used the amount's home high
4710 // register `rm_hi` as scratch (`SUBS.W rm_hi, rm_lo, #32`, RSB,
4711 // LSR) — so re-reading the amount after the shift returned a
4712 // mangled value (amt=64 read back 0, amt=67 read back 3). The
4713 // rewrite uses R12 — encoder scratch, never allocatable (#212) —
4714 // as the ONLY temporary, re-deriving the masked amount from the
4715 // untouched rm_lo whenever a second live temp would otherwise be
4716 // needed. This matches the Rocq/SMT pseudo-op models
4717 // (I64ShlPseudo writes rd_lo/rd_hi ONLY), which were proven over
4718 // exactly this non-clobbering contract all along.
4719 ArmOp::I64Shl {
4720 rd_lo,
4721 rd_hi,
4722 rn_lo,
4723 rn_hi,
4724 rm_lo,
4725 rm_hi: _,
4726 } => {
4727 let rd_lo_bits = reg_to_bits(rd_lo);
4728 let rd_hi_bits = reg_to_bits(rd_hi);
4729 let rn_lo_bits = reg_to_bits(rn_lo);
4730 let rn_hi_bits = reg_to_bits(rn_hi);
4731 let rm_lo_bits = reg_to_bits(rm_lo);
4732 let r12: u32 = 12; // the only scratch — never allocatable
4733 let mut bytes = Vec::new();
4734
4735 // #1039 house style: refuse a destination that would collide
4736 // with an input still needed after the destination is first
4737 // written, loudly — never misassemble. rd_hi is written before
4738 // rn_lo and rm_lo are last read; the in-place form
4739 // rd == rn (select_default) has rd_hi == rn_hi and stays legal.
4740 if rd_hi_bits == rn_lo_bits || rd_hi_bits == rm_lo_bits {
4741 return Err(synth_core::Error::synthesis(format!(
4742 "I64Shl: rd_hi {rd_hi:?} aliases an input ({rn_lo:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4743 )));
4744 }
4745
4746 // AND.W R12, rm_lo, #63 (n — the amount register is only READ)
4747 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4748 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4749 bytes.extend_from_slice(&hw1.to_le_bytes());
4750 bytes.extend_from_slice(&hw2.to_le_bytes());
4751
4752 // SUBS.W R12, R12, #32 (R12 = n-32, sets flags)
4753 let hw1: u16 = (0xF1B0 | r12) as u16;
4754 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4755 bytes.extend_from_slice(&hw1.to_le_bytes());
4756 bytes.extend_from_slice(&hw2.to_le_bytes());
4757
4758 // BPL .large (branch if n >= 32, offset = +14 halfwords)
4759 let bpl: u16 = 0xD50E;
4760 bytes.extend_from_slice(&bpl.to_le_bytes());
4761
4762 // --- Small shift (n < 32) ---
4763 // AND.W R12, rm_lo, #63 (n again — R12 held n-32)
4764 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4765 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4766 bytes.extend_from_slice(&hw1.to_le_bytes());
4767 bytes.extend_from_slice(&hw2.to_le_bytes());
4768
4769 // LSL.W rd_hi, rn_hi, R12 (hi << n; rn_hi's last read)
4770 let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4771 let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4772 bytes.extend_from_slice(&hw1.to_le_bytes());
4773 bytes.extend_from_slice(&hw2.to_le_bytes());
4774
4775 // RSB.W R12, R12, #32 (R12 = 32-n)
4776 let hw1: u16 = (0xF1C0 | r12) as u16;
4777 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4778 bytes.extend_from_slice(&hw1.to_le_bytes());
4779 bytes.extend_from_slice(&hw2.to_le_bytes());
4780
4781 // LSR.W R12, rn_lo, R12 (overflow = lo >> (32-n); n=0 gives
4782 // a register shift by 32 which yields 0 — exact)
4783 let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4784 let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4785 bytes.extend_from_slice(&hw1.to_le_bytes());
4786 bytes.extend_from_slice(&hw2.to_le_bytes());
4787
4788 // ORR.W rd_hi, rd_hi, R12 (hi |= overflow bits from lo)
4789 let hw1: u16 = (0xEA40 | rd_hi_bits) as u16;
4790 let hw2: u16 = ((rd_hi_bits << 8) | r12) as u16;
4791 bytes.extend_from_slice(&hw1.to_le_bytes());
4792 bytes.extend_from_slice(&hw2.to_le_bytes());
4793
4794 // AND.W R12, rm_lo, #63 (n once more for the low half)
4795 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4796 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4797 bytes.extend_from_slice(&hw1.to_le_bytes());
4798 bytes.extend_from_slice(&hw2.to_le_bytes());
4799
4800 // LSL.W rd_lo, rn_lo, R12 (lo << n)
4801 let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4802 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4803 bytes.extend_from_slice(&hw1.to_le_bytes());
4804 bytes.extend_from_slice(&hw2.to_le_bytes());
4805
4806 // B .done — `.done` is the END of the expansion, i.e. PAST the
4807 // large-shift arm's trailing zero-fill. #916: that zero-fill is
4808 // 1 halfword for a low rd_lo but 2 for R8-R12 (MOV.W), so the
4809 // displacement is DERIVED from its real width instead of the
4810 // hard-coded 0xE002 — widening the MOV without this would
4811 // overshoot `.done` and turn a data miscompile into a
4812 // control-flow one. Thumb `B` reads PC as its own address + 4
4813 // (= +2 halfwords), so imm11 = (large-arm halfwords) - 1.
4814 let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_lo_bits);
4815 let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4816 bytes.extend_from_slice(&b_done.to_le_bytes());
4817
4818 // --- Large shift (n >= 32) --- (R12 still holds n-32)
4819 // LSL.W rd_hi, rn_lo, R12 (hi = lo << (n-32))
4820 let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4821 let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4822 bytes.extend_from_slice(&hw1.to_le_bytes());
4823 bytes.extend_from_slice(&hw2.to_le_bytes());
4824
4825 // MOV rd_lo, #0 (#916: MOV.W for rd_lo >= R8). NOTE the order
4826 // is load-bearing — zeroing rd_lo BEFORE the LSL.W would
4827 // destroy rn_lo in the in-place case rd_lo == rn_lo, so this
4828 // cannot be reordered to dodge the displacement change.
4829 emit_thumb_zero_fill(&mut bytes, rd_lo_bits);
4830
4831 Ok(bytes) // 46 bytes (48 when rd_lo >= R8 takes MOV.W)
4832 }
4833
4834 // I64ShrU: 64-bit logical shift right with branch for n<32 vs
4835 // n>=32. #1048: R12-only scratch, operands never written — see
4836 // the I64Shl comment for the full rationale.
4837 ArmOp::I64ShrU {
4838 rd_lo,
4839 rd_hi,
4840 rn_lo,
4841 rn_hi,
4842 rm_lo,
4843 rm_hi: _,
4844 } => {
4845 let rd_lo_bits = reg_to_bits(rd_lo);
4846 let rd_hi_bits = reg_to_bits(rd_hi);
4847 let rn_lo_bits = reg_to_bits(rn_lo);
4848 let rn_hi_bits = reg_to_bits(rn_hi);
4849 let rm_lo_bits = reg_to_bits(rm_lo);
4850 let r12: u32 = 12; // the only scratch — never allocatable
4851 let mut bytes = Vec::new();
4852
4853 // #1039 house style: rd_lo is written before rn_hi and rm_lo
4854 // are last read — refuse the collision loudly. The in-place
4855 // form rd == rn (select_default) has rd_lo == rn_lo and stays
4856 // legal.
4857 if rd_lo_bits == rn_hi_bits || rd_lo_bits == rm_lo_bits {
4858 return Err(synth_core::Error::synthesis(format!(
4859 "I64ShrU: rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4860 )));
4861 }
4862
4863 // AND.W R12, rm_lo, #63 (n — the amount register is only READ)
4864 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4865 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4866 bytes.extend_from_slice(&hw1.to_le_bytes());
4867 bytes.extend_from_slice(&hw2.to_le_bytes());
4868
4869 // SUBS.W R12, R12, #32 (R12 = n-32, sets flags)
4870 let hw1: u16 = (0xF1B0 | r12) as u16;
4871 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4872 bytes.extend_from_slice(&hw1.to_le_bytes());
4873 bytes.extend_from_slice(&hw2.to_le_bytes());
4874
4875 // BPL .large (+14 halfwords)
4876 let bpl: u16 = 0xD50E;
4877 bytes.extend_from_slice(&bpl.to_le_bytes());
4878
4879 // --- Small shift (n < 32) ---
4880 // AND.W R12, rm_lo, #63 (n again — R12 held n-32)
4881 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4882 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4883 bytes.extend_from_slice(&hw1.to_le_bytes());
4884 bytes.extend_from_slice(&hw2.to_le_bytes());
4885
4886 // LSR.W rd_lo, rn_lo, R12 (lo >> n; rn_lo's last read)
4887 let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4888 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4889 bytes.extend_from_slice(&hw1.to_le_bytes());
4890 bytes.extend_from_slice(&hw2.to_le_bytes());
4891
4892 // RSB.W R12, R12, #32 (R12 = 32-n)
4893 let hw1: u16 = (0xF1C0 | r12) as u16;
4894 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4895 bytes.extend_from_slice(&hw1.to_le_bytes());
4896 bytes.extend_from_slice(&hw2.to_le_bytes());
4897
4898 // LSL.W R12, rn_hi, R12 (overflow = hi << (32-n); n=0 gives
4899 // a register shift by 32 which yields 0 — exact)
4900 let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4901 let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4902 bytes.extend_from_slice(&hw1.to_le_bytes());
4903 bytes.extend_from_slice(&hw2.to_le_bytes());
4904
4905 // ORR.W rd_lo, rd_lo, R12 (lo |= overflow from hi)
4906 let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4907 let hw2: u16 = ((rd_lo_bits << 8) | r12) as u16;
4908 bytes.extend_from_slice(&hw1.to_le_bytes());
4909 bytes.extend_from_slice(&hw2.to_le_bytes());
4910
4911 // AND.W R12, rm_lo, #63 (n once more for the high half)
4912 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4913 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4914 bytes.extend_from_slice(&hw1.to_le_bytes());
4915 bytes.extend_from_slice(&hw2.to_le_bytes());
4916
4917 // LSR.W rd_hi, rn_hi, R12 (hi >> n, logical)
4918 let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4919 let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4920 bytes.extend_from_slice(&hw1.to_le_bytes());
4921 bytes.extend_from_slice(&hw2.to_le_bytes());
4922
4923 // B .done — see I64Shl: `.done` is the END of the expansion,
4924 // past the trailing zero-fill, so the displacement is derived
4925 // from that zero-fill's real width (#916).
4926 let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_hi_bits);
4927 let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4928 bytes.extend_from_slice(&b_done.to_le_bytes());
4929
4930 // --- Large shift (n >= 32) --- (R12 still holds n-32)
4931 // LSR.W rd_lo, rn_hi, R12 (lo = hi >> (n-32))
4932 let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4933 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4934 bytes.extend_from_slice(&hw1.to_le_bytes());
4935 bytes.extend_from_slice(&hw2.to_le_bytes());
4936
4937 // MOV rd_hi, #0 (#916: MOV.W for rd_hi >= R8). Order is
4938 // load-bearing: the LSR.W above reads rn_hi, which may BE
4939 // rd_hi in the in-place case.
4940 emit_thumb_zero_fill(&mut bytes, rd_hi_bits);
4941
4942 Ok(bytes) // 46 bytes (48 when rd_hi >= R8 takes MOV.W)
4943 }
4944
4945 // I64ShrS: 64-bit arithmetic shift right with branch for n<32 vs
4946 // n>=32. #1048: R12-only scratch, operands never written — see
4947 // the I64Shl comment for the full rationale.
4948 ArmOp::I64ShrS {
4949 rd_lo,
4950 rd_hi,
4951 rn_lo,
4952 rn_hi,
4953 rm_lo,
4954 rm_hi: _,
4955 } => {
4956 let rd_lo_bits = reg_to_bits(rd_lo);
4957 let rd_hi_bits = reg_to_bits(rd_hi);
4958 let rn_lo_bits = reg_to_bits(rn_lo);
4959 let rn_hi_bits = reg_to_bits(rn_hi);
4960 let rm_lo_bits = reg_to_bits(rm_lo);
4961 let r12: u32 = 12; // the only scratch — never allocatable
4962 let mut bytes = Vec::new();
4963
4964 // #1039 house style: rd_lo is written before rn_hi and rm_lo
4965 // are last read (on BOTH arms of the diamond — the large arm's
4966 // trailing `ASR rd_hi, rn_hi, #31` also reads rn_hi after
4967 // rd_lo is written). The in-place form rd == rn stays legal.
4968 if rd_lo_bits == rn_hi_bits || rd_lo_bits == rm_lo_bits {
4969 return Err(synth_core::Error::synthesis(format!(
4970 "I64ShrS: rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4971 )));
4972 }
4973
4974 // AND.W R12, rm_lo, #63 (n — the amount register is only READ)
4975 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4976 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4977 bytes.extend_from_slice(&hw1.to_le_bytes());
4978 bytes.extend_from_slice(&hw2.to_le_bytes());
4979
4980 // SUBS.W R12, R12, #32 (R12 = n-32, sets flags)
4981 let hw1: u16 = (0xF1B0 | r12) as u16;
4982 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4983 bytes.extend_from_slice(&hw1.to_le_bytes());
4984 bytes.extend_from_slice(&hw2.to_le_bytes());
4985
4986 // BPL .large (+14 halfwords)
4987 let bpl: u16 = 0xD50E;
4988 bytes.extend_from_slice(&bpl.to_le_bytes());
4989
4990 // --- Small shift (n < 32) ---
4991 // AND.W R12, rm_lo, #63 (n again — R12 held n-32)
4992 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4993 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4994 bytes.extend_from_slice(&hw1.to_le_bytes());
4995 bytes.extend_from_slice(&hw2.to_le_bytes());
4996
4997 // LSR.W rd_lo, rn_lo, R12 (lo >> n, logical for lo word)
4998 let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4999 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
5000 bytes.extend_from_slice(&hw1.to_le_bytes());
5001 bytes.extend_from_slice(&hw2.to_le_bytes());
5002
5003 // RSB.W R12, R12, #32 (R12 = 32-n)
5004 let hw1: u16 = (0xF1C0 | r12) as u16;
5005 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
5006 bytes.extend_from_slice(&hw1.to_le_bytes());
5007 bytes.extend_from_slice(&hw2.to_le_bytes());
5008
5009 // LSL.W R12, rn_hi, R12 (overflow = hi << (32-n); n=0 gives
5010 // a register shift by 32 which yields 0 — exact)
5011 let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
5012 let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
5013 bytes.extend_from_slice(&hw1.to_le_bytes());
5014 bytes.extend_from_slice(&hw2.to_le_bytes());
5015
5016 // ORR.W rd_lo, rd_lo, R12 (lo |= overflow from hi)
5017 let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
5018 let hw2: u16 = ((rd_lo_bits << 8) | r12) as u16;
5019 bytes.extend_from_slice(&hw1.to_le_bytes());
5020 bytes.extend_from_slice(&hw2.to_le_bytes());
5021
5022 // AND.W R12, rm_lo, #63 (n once more for the high half)
5023 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
5024 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
5025 bytes.extend_from_slice(&hw1.to_le_bytes());
5026 bytes.extend_from_slice(&hw2.to_le_bytes());
5027
5028 // ASR.W rd_hi, rn_hi, R12 (hi >> n, arithmetic/sign-extending)
5029 let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
5030 let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
5031 bytes.extend_from_slice(&hw1.to_le_bytes());
5032 bytes.extend_from_slice(&hw2.to_le_bytes());
5033
5034 // B .done (+3 halfwords, large shift is 8 bytes)
5035 let b_done: u16 = 0xE003;
5036 bytes.extend_from_slice(&b_done.to_le_bytes());
5037
5038 // --- Large shift (n >= 32) --- (R12 still holds n-32)
5039 // ASR.W rd_lo, rn_hi, R12 (lo = hi >>> (n-32))
5040 let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
5041 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
5042 bytes.extend_from_slice(&hw1.to_le_bytes());
5043 bytes.extend_from_slice(&hw2.to_le_bytes());
5044
5045 // ASR.W rd_hi, rn_hi, #31 (hi = sign extension, all 0s or all 1s)
5046 // Thumb-2 ASR immediate: hw1=0xEA4F, hw2=imm3:Rd:imm2:10:Rm
5047 // imm5=31=11111 → imm3=111, imm2=11
5048 let hw1: u16 = 0xEA4F;
5049 let hw2: u16 = (0x7000 | (rd_hi_bits << 8) | 0x00E0 | rn_hi_bits) as u16;
5050 bytes.extend_from_slice(&hw1.to_le_bytes());
5051 bytes.extend_from_slice(&hw2.to_le_bytes());
5052
5053 Ok(bytes) // Total: 48 bytes
5054 }
5055
5056 // I64Rotl: 64-bit rotate left (#610 rewrite).
5057 // For n < 32: new_hi = (hi << n) | (lo >> (32-n)), new_lo = (lo << n) | (hi >> (32-n))
5058 // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
5059 //
5060 // Fixed-reg core: value in R0:R1, amount in R2, scratch R3 + R12
5061 // (all four saved/marshaled by the #610 fixed-ABI wrapper; the
5062 // pre-#610 expansion wrote through the selector's registers with
5063 // colliding R3/R4 scratch and restored the saved R4 OVER the
5064 // result). Relies on ARM register-shift semantics: amounts >= 32
5065 // yield 0 for LSL/LSR, which makes n = 0 and n = 32 exact.
5066 ArmOp::I64Rotl {
5067 rdlo,
5068 rdhi,
5069 rnlo,
5070 rnhi,
5071 shift,
5072 } => {
5073 let mut bytes = Vec::new();
5074 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
5075
5076 let core: [u16; 35] = [
5077 0xF002, 0x023F, // AND.W R2, R2, #63 (mask amount mod 64)
5078 0xF1B2, 0x0320, // SUBS.W R3, R2, #32 (R3 = n-32, sets N)
5079 0xD50E, // BPL .large (n >= 32)
5080 // --- small rotation (n < 32) ---
5081 0xF1C2, 0x0320, // RSB.W R3, R2, #32 (R3 = 32-n)
5082 0xFA20, 0xFC03, // LSR.W R12, R0, R3 (lo >> (32-n))
5083 0xFA21, 0xF303, // LSR.W R3, R1, R3 (hi >> (32-n))
5084 0xFA01, 0xF102, // LSL.W R1, R1, R2 (hi << n)
5085 0xEA41, 0x010C, // ORR.W R1, R1, R12 (new_hi)
5086 0xFA00, 0xF002, // LSL.W R0, R0, R2 (lo << n)
5087 0xEA40, 0x0003, // ORR.W R0, R0, R3 (new_lo)
5088 0xE00E, // B .done
5089 // --- large rotation (n >= 32), R3 = m = n-32 ---
5090 0xF1C3, 0x0220, // RSB.W R2, R3, #32 (R2 = 32-m = 64-n)
5091 0xFA21, 0xFC02, // LSR.W R12, R1, R2 (hi >> (64-n))
5092 0xFA20, 0xF202, // LSR.W R2, R0, R2 (lo >> (64-n))
5093 0xFA00, 0xF003, // LSL.W R0, R0, R3 (lo << m)
5094 0xFA01, 0xF103, // LSL.W R1, R1, R3 (hi << m)
5095 0xEA40, 0x0C0C, // ORR.W R12, R0, R12 (new_hi = (lo<<m)|(hi>>(64-n)))
5096 0xEA41, 0x0002, // ORR.W R0, R1, R2 (new_lo = (hi<<m)|(lo>>(64-n)))
5097 0x4661, // MOV R1, R12 (new_hi into place)
5098 // .done: result in R0:R1
5099 ];
5100 for hw in core {
5101 bytes.extend_from_slice(&hw.to_le_bytes());
5102 }
5103
5104 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5105 Ok(bytes) // Total: 102 bytes
5106 }
5107
5108 // I64Rotr: 64-bit rotate right (#610 rewrite).
5109 // For n < 32: new_lo = (lo >> n) | (hi << (32-n)), new_hi = (hi >> n) | (lo << (32-n))
5110 // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
5111 //
5112 // Same fixed-reg core contract as I64Rotl: value in R0:R1, amount
5113 // in R2, scratch R3 + R12, all covered by the fixed-ABI wrapper.
5114 ArmOp::I64Rotr {
5115 rdlo,
5116 rdhi,
5117 rnlo,
5118 rnhi,
5119 shift,
5120 } => {
5121 let mut bytes = Vec::new();
5122 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
5123
5124 let core: [u16; 35] = [
5125 0xF002, 0x023F, // AND.W R2, R2, #63 (mask amount mod 64)
5126 0xF1B2, 0x0320, // SUBS.W R3, R2, #32 (R3 = n-32, sets N)
5127 0xD50E, // BPL .large (n >= 32)
5128 // --- small rotation (n < 32) ---
5129 0xF1C2, 0x0320, // RSB.W R3, R2, #32 (R3 = 32-n)
5130 0xFA01, 0xFC03, // LSL.W R12, R1, R3 (hi << (32-n))
5131 0xFA00, 0xF303, // LSL.W R3, R0, R3 (lo << (32-n))
5132 0xFA20, 0xF002, // LSR.W R0, R0, R2 (lo >> n)
5133 0xEA40, 0x000C, // ORR.W R0, R0, R12 (new_lo)
5134 0xFA21, 0xF102, // LSR.W R1, R1, R2 (hi >> n)
5135 0xEA41, 0x0103, // ORR.W R1, R1, R3 (new_hi)
5136 0xE00E, // B .done
5137 // --- large rotation (n >= 32), R3 = m = n-32 ---
5138 0xF1C3, 0x0220, // RSB.W R2, R3, #32 (R2 = 32-m = 64-n)
5139 0xFA00, 0xFC02, // LSL.W R12, R0, R2 (lo << (64-n))
5140 0xFA01, 0xF202, // LSL.W R2, R1, R2 (hi << (64-n))
5141 0xFA21, 0xF103, // LSR.W R1, R1, R3 (hi >> m)
5142 0xEA41, 0x0C0C, // ORR.W R12, R1, R12 (new_lo = (hi>>m)|(lo<<(64-n)))
5143 0xFA20, 0xF103, // LSR.W R1, R0, R3 (lo >> m)
5144 0xEA41, 0x0102, // ORR.W R1, R1, R2 (new_hi = (lo>>m)|(hi<<(64-n)))
5145 0x4660, // MOV R0, R12 (new_lo into place)
5146 // .done: result in R0:R1
5147 ];
5148 for hw in core {
5149 bytes.extend_from_slice(&hw.to_le_bytes());
5150 }
5151
5152 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5153 Ok(bytes) // Total: 102 bytes
5154 }
5155
5156 // I64Clz: Count leading zeros in 64-bit value
5157 // If hi != 0: result = CLZ(hi)
5158 // If hi == 0: result = 32 + CLZ(lo)
5159 //
5160 // Layout (using CMP+BNE approach for consistency):
5161 // 0: CMP.W rnhi, #0 (4 bytes)
5162 // 4: BEQ .hi_zero (2 bytes) - branch forward to offset 14
5163 // 6: CLZ.W rd, rnhi (4 bytes)
5164 // 10: B .done (2 bytes) - branch forward to offset 22
5165 // 12: NOP (2 bytes) - padding for alignment
5166 // 14: .hi_zero: CLZ.W rd, rnlo (4 bytes)
5167 // 18: ADD.W rd, rd, #32 (4 bytes)
5168 // 22: .done
5169 ArmOp::I64Clz { rd, rnlo, rnhi } => {
5170 let rd_bits = reg_to_bits(rd);
5171 let rn_lo_bits = reg_to_bits(rnlo);
5172 let rn_hi_bits = reg_to_bits(rnhi);
5173 let mut bytes = Vec::new();
5174
5175 // CMP.W rnhi, #0 (4 bytes at offset 0)
5176 let hw1: u16 = (0xF1B0 | rn_hi_bits) as u16;
5177 let hw2: u16 = 0x0F00;
5178 bytes.extend_from_slice(&hw1.to_le_bytes());
5179 bytes.extend_from_slice(&hw2.to_le_bytes());
5180
5181 // BEQ .hi_zero (2 bytes at offset 4)
5182 // PC = 4 + 4 = 8, target = 14, offset = 6, imm8 = 3
5183 let beq: u16 = 0xD003;
5184 bytes.extend_from_slice(&beq.to_le_bytes());
5185
5186 // CLZ.W rd, rnhi (4 bytes at offset 6)
5187 // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5188 let hw1: u16 = (0xFAB0 | rn_hi_bits) as u16;
5189 let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_hi_bits) as u16;
5190 bytes.extend_from_slice(&hw1.to_le_bytes());
5191 bytes.extend_from_slice(&hw2.to_le_bytes());
5192
5193 // B .done (2 bytes at offset 10)
5194 // PC = 10 + 4 = 14, target = 22, offset = 8, imm11 = 4
5195 let b_done: u16 = 0xE004;
5196 bytes.extend_from_slice(&b_done.to_le_bytes());
5197
5198 // NOP (2 bytes at offset 12) - padding
5199 bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5200
5201 // .hi_zero: (offset 14)
5202 // CLZ.W rd, rnlo (4 bytes)
5203 // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5204 let hw1: u16 = (0xFAB0 | rn_lo_bits) as u16;
5205 let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_lo_bits) as u16;
5206 bytes.extend_from_slice(&hw1.to_le_bytes());
5207 bytes.extend_from_slice(&hw2.to_le_bytes());
5208
5209 // ADD.W rd, rd, #32 (4 bytes at offset 18)
5210 let hw1: u16 = (0xF100 | rd_bits) as u16;
5211 let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5212 bytes.extend_from_slice(&hw1.to_le_bytes());
5213 bytes.extend_from_slice(&hw2.to_le_bytes());
5214
5215 // .done: (offset 22 — the end of the expansion)
5216 //
5217 // #1048: the former trailing hi-word clear (`MOV rnhi, #0`)
5218 // is GONE. It was aimed at the RESULT's high half but wrote
5219 // the OPERAND's home high register — a real executed
5220 // miscompile on the direct selector, which allocates a fresh
5221 // destination pair and zeroes its own dst_hi, leaving the
5222 // operand's hi limb destroyed for any later re-read. The
5223 // Rocq/SMT models of I64ClzPseudo always said "writes rd
5224 // ONLY"; the callers that relied on the implicit clear
5225 // (select_default, optimizer_bridge) now emit their own
5226 // explicit hi-zero op. `B .done` above targets offset 22 =
5227 // past-the-end, `BEQ` targets offset 14 — no displacement
5228 // moves.
5229
5230 Ok(bytes) // 22 bytes, register-independent
5231 }
5232
5233 // I64Ctz: Count trailing zeros in 64-bit value
5234 // If lo != 0: result = CTZ(lo) = CLZ(RBIT(lo))
5235 // If lo == 0: result = 32 + CTZ(hi) = 32 + CLZ(RBIT(hi))
5236 //
5237 // Layout:
5238 // 0: CMP.W rnlo, #0 (4 bytes)
5239 // 4: BEQ .lo_zero (2 bytes) - branch to offset 18
5240 // 6: RBIT.W rd, rnlo (4 bytes)
5241 // 10: CLZ.W rd, rd (4 bytes)
5242 // 14: B .done (2 bytes) - branch to offset 30
5243 // 16: NOP (2 bytes) - padding
5244 // 18: .lo_zero: RBIT.W rd, rnhi (4 bytes)
5245 // 22: CLZ.W rd, rd (4 bytes)
5246 // 26: ADD.W rd, rd, #32 (4 bytes)
5247 // 30: .done
5248 ArmOp::I64Ctz { rd, rnlo, rnhi } => {
5249 let rd_bits = reg_to_bits(rd);
5250 let rn_lo_bits = reg_to_bits(rnlo);
5251 let rn_hi_bits = reg_to_bits(rnhi);
5252 let mut bytes = Vec::new();
5253
5254 // CMP.W rnlo, #0 (4 bytes at offset 0)
5255 let hw1: u16 = (0xF1B0 | rn_lo_bits) as u16;
5256 let hw2: u16 = 0x0F00;
5257 bytes.extend_from_slice(&hw1.to_le_bytes());
5258 bytes.extend_from_slice(&hw2.to_le_bytes());
5259
5260 // BEQ .lo_zero (2 bytes at offset 4)
5261 // PC = 4 + 4 = 8, target = 18, offset = 10, imm8 = 5
5262 let beq: u16 = 0xD005;
5263 bytes.extend_from_slice(&beq.to_le_bytes());
5264
5265 // RBIT.W rd, rnlo (4 bytes at offset 6)
5266 // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5267 let hw1: u16 = (0xFA90 | rn_lo_bits) as u16;
5268 let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_lo_bits) as u16;
5269 bytes.extend_from_slice(&hw1.to_le_bytes());
5270 bytes.extend_from_slice(&hw2.to_le_bytes());
5271
5272 // CLZ.W rd, rd (4 bytes at offset 10)
5273 // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5274 let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5275 let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5276 bytes.extend_from_slice(&hw1.to_le_bytes());
5277 bytes.extend_from_slice(&hw2.to_le_bytes());
5278
5279 // B .done (2 bytes at offset 14)
5280 // PC = 14 + 4 = 18, target = 30, offset = 12, imm11 = 6
5281 let b_done: u16 = 0xE006;
5282 bytes.extend_from_slice(&b_done.to_le_bytes());
5283
5284 // NOP (2 bytes at offset 16) - padding
5285 bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5286
5287 // .lo_zero: (offset 18)
5288 // RBIT.W rd, rnhi (4 bytes)
5289 // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5290 let hw1: u16 = (0xFA90 | rn_hi_bits) as u16;
5291 let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_hi_bits) as u16;
5292 bytes.extend_from_slice(&hw1.to_le_bytes());
5293 bytes.extend_from_slice(&hw2.to_le_bytes());
5294
5295 // CLZ.W rd, rd (4 bytes at offset 22)
5296 // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5297 let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5298 let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5299 bytes.extend_from_slice(&hw1.to_le_bytes());
5300 bytes.extend_from_slice(&hw2.to_le_bytes());
5301
5302 // ADD.W rd, rd, #32 (4 bytes at offset 26)
5303 let hw1: u16 = (0xF100 | rd_bits) as u16;
5304 let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5305 bytes.extend_from_slice(&hw1.to_le_bytes());
5306 bytes.extend_from_slice(&hw2.to_le_bytes());
5307
5308 // .done: (offset 30 — the end of the expansion)
5309 // #1048: the former trailing `MOV rnhi, #0` is GONE — it
5310 // wrote the OPERAND's home high register (see the I64Clz
5311 // comment above). `B .done` targets offset 30 = past-the-end,
5312 // `BEQ` targets offset 18 — no displacement moves.
5313
5314 Ok(bytes) // 30 bytes, register-independent
5315 }
5316
5317 // I64Popcnt: Population count of 64-bit value
5318 // result = POPCNT(lo) + POPCNT(hi)
5319 // Using SIMD-style parallel bit counting algorithm
5320 ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
5321 let rd_bits = reg_to_bits(rd);
5322 let rn_lo_bits = reg_to_bits(rnlo);
5323 let rn_hi_bits = reg_to_bits(rnhi);
5324 let r12: u32 = 12; // IP scratch
5325 let r3: u32 = 3; // Scratch for hi popcnt result
5326 let mut bytes = Vec::new();
5327
5328 // PUSH {R3, R4, R5} - save scratch registers
5329 bytes.extend_from_slice(&0xB438u16.to_le_bytes());
5330
5331 // Strategy: compute popcnt(lo) -> R4, popcnt(hi) -> R5, add them -> rd
5332 // Using lookup table approach for each byte would be too large
5333 // Using shift-and-add approach instead
5334
5335 // For simplicity and correctness, use the efficient parallel algorithm
5336 // but implement it as a series of inline operations
5337
5338 // Marshal the operand pair into the fixed scratch regs, routing
5339 // rnlo through R12 (#632 audit): writing R4 first corrupted the
5340 // rnhi read for a pair living at (R3,R4) — every source is read
5341 // before any scratch register it could occupy is written.
5342 // MOV R12, rnlo
5343 let mov: u16 = (0x4600 | (1 << 7) | (rn_lo_bits << 3) | 4) as u16;
5344 bytes.extend_from_slice(&mov.to_le_bytes());
5345 // MOV R5, rnhi (R4 untouched so far; rnhi == R5 is a no-op)
5346 let mov: u16 = (0x4600 | (rn_hi_bits << 3) | 5) as u16;
5347 bytes.extend_from_slice(&mov.to_le_bytes());
5348 // MOV R4, R12
5349 bytes.extend_from_slice(&0x4664u16.to_le_bytes());
5350
5351 // --- POPCNT for R4 (lo word) ---
5352 // Step 1: x = x - ((x >> 1) & 0x55555555)
5353 // LSR.W R12, R4, #1
5354 let hw1: u16 = 0xEA4F;
5355 let hw2: u16 = ((r12 << 8) | 0x50 | 4) as u16;
5356 bytes.extend_from_slice(&hw1.to_le_bytes());
5357 bytes.extend_from_slice(&hw2.to_le_bytes());
5358
5359 // Load 0x55555555 into R3 using MOVW/MOVT
5360 // MOVW R3, #0x5555
5361 bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5362 bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5363 // MOVT R3, #0x5555
5364 bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5365 bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5366
5367 // AND.W R12, R12, R3
5368 let hw1: u16 = (0xEA00 | r12) as u16;
5369 let hw2: u16 = ((r12 << 8) | r3) as u16;
5370 bytes.extend_from_slice(&hw1.to_le_bytes());
5371 bytes.extend_from_slice(&hw2.to_le_bytes());
5372
5373 // SUB.W R4, R4, R12
5374 let hw1: u16 = (0xEBA0 | 4) as u16;
5375 let hw2: u16 = ((4 << 8) | r12) as u16;
5376 bytes.extend_from_slice(&hw1.to_le_bytes());
5377 bytes.extend_from_slice(&hw2.to_le_bytes());
5378
5379 // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5380 // Load 0x33333333 into R3
5381 // MOVW R3, #0x3333
5382 bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5383 bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5384 // MOVT R3, #0x3333
5385 bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5386 bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5387
5388 // AND.W R12, R4, R3
5389 let hw1: u16 = (0xEA00 | 4) as u16;
5390 let hw2: u16 = ((r12 << 8) | r3) as u16;
5391 bytes.extend_from_slice(&hw1.to_le_bytes());
5392 bytes.extend_from_slice(&hw2.to_le_bytes());
5393
5394 // LSR.W R4, R4, #2
5395 let hw1: u16 = 0xEA4F;
5396 let hw2: u16 = ((4 << 8) | 0x90 | 4) as u16;
5397 bytes.extend_from_slice(&hw1.to_le_bytes());
5398 bytes.extend_from_slice(&hw2.to_le_bytes());
5399
5400 // AND.W R4, R4, R3
5401 let hw1: u16 = (0xEA00 | 4) as u16;
5402 let hw2: u16 = ((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 // ADD.W R4, R4, R12
5407 let hw1: u16 = (0xEB00 | 4) as u16;
5408 let hw2: u16 = ((4 << 8) | r12) as u16;
5409 bytes.extend_from_slice(&hw1.to_le_bytes());
5410 bytes.extend_from_slice(&hw2.to_le_bytes());
5411
5412 // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5413 // LSR.W R12, R4, #4
5414 // hw2 = (imm3 << 12) | (Rd << 8) | (imm2 << 6) | (type << 4) | Rm
5415 // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5416 let hw1: u16 = 0xEA4F;
5417 let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 4) as u16;
5418 bytes.extend_from_slice(&hw1.to_le_bytes());
5419 bytes.extend_from_slice(&hw2.to_le_bytes());
5420
5421 // ADD.W R4, R4, R12
5422 let hw1: u16 = (0xEB00 | 4) as u16;
5423 let hw2: u16 = ((4 << 8) | r12) as u16;
5424 bytes.extend_from_slice(&hw1.to_le_bytes());
5425 bytes.extend_from_slice(&hw2.to_le_bytes());
5426
5427 // Load 0x0F0F0F0F into R3
5428 // MOVW R3, #0x0F0F (imm4=0, i=1, imm3=7, imm8=0x0F)
5429 // hw1 = 11110 1 10 0100 0000 = 0xF640
5430 // hw2 = 0 111 0011 00001111 = 0x730F
5431 bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5432 bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5433 // MOVT R3, #0x0F0F
5434 bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5435 bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5436
5437 // AND.W R4, R4, R3
5438 let hw1: u16 = (0xEA00 | 4) as u16;
5439 let hw2: u16 = ((4 << 8) | r3) as u16;
5440 bytes.extend_from_slice(&hw1.to_le_bytes());
5441 bytes.extend_from_slice(&hw2.to_le_bytes());
5442
5443 // Step 4: x = x * 0x01010101 >> 24
5444 // Load 0x01010101 into R3
5445 // MOVW R3, #0x0101
5446 bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5447 bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5448 // MOVT R3, #0x0101
5449 bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5450 bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5451
5452 // MUL R4, R4, R3
5453 // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5454 let hw1: u16 = (0xFB00 | 4) as u16;
5455 let hw2: u16 = (0xF000 | (4 << 8) | r3) as u16;
5456 bytes.extend_from_slice(&hw1.to_le_bytes());
5457 bytes.extend_from_slice(&hw2.to_le_bytes());
5458
5459 // LSR.W R4, R4, #24
5460 // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5461 let hw1: u16 = 0xEA4F;
5462 let hw2: u16 = (0x6000 | (4 << 8) | 0x10 | 4) as u16;
5463 bytes.extend_from_slice(&hw1.to_le_bytes());
5464 bytes.extend_from_slice(&hw2.to_le_bytes());
5465
5466 // --- POPCNT for R5 (hi word) - same algorithm ---
5467 // Step 1
5468 let hw1: u16 = 0xEA4F;
5469 let hw2: u16 = ((r12 << 8) | 0x50 | 5) as u16;
5470 bytes.extend_from_slice(&hw1.to_le_bytes());
5471 bytes.extend_from_slice(&hw2.to_le_bytes());
5472
5473 // Load 0x55555555 into R3
5474 bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5475 bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5476 bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5477 bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5478
5479 let hw1: u16 = (0xEA00 | r12) as u16;
5480 let hw2: u16 = ((r12 << 8) | r3) as u16;
5481 bytes.extend_from_slice(&hw1.to_le_bytes());
5482 bytes.extend_from_slice(&hw2.to_le_bytes());
5483
5484 let hw1: u16 = (0xEBA0 | 5) as u16;
5485 let hw2: u16 = ((5 << 8) | r12) as u16;
5486 bytes.extend_from_slice(&hw1.to_le_bytes());
5487 bytes.extend_from_slice(&hw2.to_le_bytes());
5488
5489 // Step 2
5490 bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5491 bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5492 bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5493 bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5494
5495 let hw1: u16 = (0xEA00 | 5) as u16;
5496 let hw2: u16 = ((r12 << 8) | r3) as u16;
5497 bytes.extend_from_slice(&hw1.to_le_bytes());
5498 bytes.extend_from_slice(&hw2.to_le_bytes());
5499
5500 let hw1: u16 = 0xEA4F;
5501 let hw2: u16 = ((5 << 8) | 0x90 | 5) as u16;
5502 bytes.extend_from_slice(&hw1.to_le_bytes());
5503 bytes.extend_from_slice(&hw2.to_le_bytes());
5504
5505 let hw1: u16 = (0xEA00 | 5) as u16;
5506 let hw2: u16 = ((5 << 8) | r3) as u16;
5507 bytes.extend_from_slice(&hw1.to_le_bytes());
5508 bytes.extend_from_slice(&hw2.to_le_bytes());
5509
5510 let hw1: u16 = (0xEB00 | 5) as u16;
5511 let hw2: u16 = ((5 << 8) | r12) as u16;
5512 bytes.extend_from_slice(&hw1.to_le_bytes());
5513 bytes.extend_from_slice(&hw2.to_le_bytes());
5514
5515 // Step 3: LSR.W R12, R5, #4
5516 // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5517 let hw1: u16 = 0xEA4F;
5518 let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 5) as u16;
5519 bytes.extend_from_slice(&hw1.to_le_bytes());
5520 bytes.extend_from_slice(&hw2.to_le_bytes());
5521
5522 let hw1: u16 = (0xEB00 | 5) as u16;
5523 let hw2: u16 = ((5 << 8) | r12) as u16;
5524 bytes.extend_from_slice(&hw1.to_le_bytes());
5525 bytes.extend_from_slice(&hw2.to_le_bytes());
5526
5527 // Load 0x0F0F0F0F into R3 (for hi-word)
5528 bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5529 bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5530 bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5531 bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5532
5533 let hw1: u16 = (0xEA00 | 5) as u16;
5534 let hw2: u16 = ((5 << 8) | r3) as u16;
5535 bytes.extend_from_slice(&hw1.to_le_bytes());
5536 bytes.extend_from_slice(&hw2.to_le_bytes());
5537
5538 // Step 4
5539 bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5540 bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5541 bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5542 bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5543
5544 // MUL R5, R5, R3
5545 // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5546 let hw1: u16 = (0xFB00 | 5) as u16;
5547 let hw2: u16 = (0xF000 | (5 << 8) | r3) as u16;
5548 bytes.extend_from_slice(&hw1.to_le_bytes());
5549 bytes.extend_from_slice(&hw2.to_le_bytes());
5550
5551 // LSR.W R5, R5, #24
5552 // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5553 let hw1: u16 = 0xEA4F;
5554 let hw2: u16 = (0x6000 | (5 << 8) | 0x10 | 5) as u16;
5555 bytes.extend_from_slice(&hw1.to_le_bytes());
5556 bytes.extend_from_slice(&hw2.to_le_bytes());
5557
5558 // #632: the count must be carried ACROSS the scratch restore
5559 // in a register the POP cannot touch. rd is allocator-assigned
5560 // (any of R0-R8) and can land inside the {R3,R4,R5} restore set
5561 // — the old `ADDS rd, R4, R5; POP {R3,R4,R5}` destroyed the
5562 // result one instruction after computing it (0 for every input
5563 // under qemu). R12 is encoder scratch: never allocatable (#212)
5564 // and never in a restore set, so no choice of rd can collide.
5565 // ADD.W R12, R4, R5
5566 bytes.extend_from_slice(&0xEB04u16.to_le_bytes());
5567 bytes.extend_from_slice(&0x0C05u16.to_le_bytes());
5568
5569 // POP {R3, R4, R5}
5570 bytes.extend_from_slice(&0xBC38u16.to_le_bytes());
5571
5572 // MOV rd, R12 — after the restore. The 4-bit Rd (D:rd) form is
5573 // also total over rd = R8, where the old ADDS T1 3-bit field
5574 // silently corrupted the encoding (#178/#180 class).
5575 let mov: u16 =
5576 (0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7)) as u16;
5577 bytes.extend_from_slice(&mov.to_le_bytes());
5578
5579 // #1048: the former trailing `MOV.W rnhi, #0` hi-word clear
5580 // is GONE — it wrote the OPERAND's home high register (see
5581 // the I64Clz comment). Callers that relied on the implicit
5582 // clear emit their own explicit hi-zero op.
5583
5584 Ok(bytes)
5585 }
5586
5587 // I64Extend8S: Sign-extend low 8 bits to 64 bits
5588 // Result: rdlo = sign_extend_8(rnlo), rdhi = rdlo >> 31
5589 ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
5590 let rdlo_bits = reg_to_bits(rdlo);
5591 let rdhi_bits = reg_to_bits(rdhi);
5592 let rnlo_bits = reg_to_bits(rnlo);
5593 let mut bytes = Vec::new();
5594
5595 // SXTB.W rdlo, rnlo (sign-extend byte to 32-bit)
5596 // SXTB T2: hw1 = 0xFA4F, hw2 = 0xF0<Rd><Rm>
5597 let hw1: u16 = 0xFA4F_u16;
5598 let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5599 bytes.extend_from_slice(&hw1.to_le_bytes());
5600 bytes.extend_from_slice(&hw2.to_le_bytes());
5601
5602 // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5603 // ASR (immediate): hw1 = 0xEA4F, hw2 = imm3:Rd:imm2:type:Rm
5604 // For imm5=31: imm3=111, imm2=11, type=10 (ASR)
5605 // hw2 = (7 << 12) | (rdhi << 8) | (3 << 6) | (2 << 4) | rdlo
5606 let hw1: u16 = 0xEA4F;
5607 let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5608 bytes.extend_from_slice(&hw1.to_le_bytes());
5609 bytes.extend_from_slice(&hw2.to_le_bytes());
5610
5611 Ok(bytes)
5612 }
5613
5614 // I64Extend16S: Sign-extend low 16 bits to 64 bits
5615 // Result: rdlo = sign_extend_16(rnlo), rdhi = rdlo >> 31
5616 ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
5617 let rdlo_bits = reg_to_bits(rdlo);
5618 let rdhi_bits = reg_to_bits(rdhi);
5619 let rnlo_bits = reg_to_bits(rnlo);
5620 let mut bytes = Vec::new();
5621
5622 // SXTH.W rdlo, rnlo (sign-extend halfword to 32-bit)
5623 // SXTH T2: hw1 = 0xFA0F, hw2 = 0xF0<Rd><Rm>
5624 let hw1: u16 = 0xFA0F_u16;
5625 let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5626 bytes.extend_from_slice(&hw1.to_le_bytes());
5627 bytes.extend_from_slice(&hw2.to_le_bytes());
5628
5629 // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5630 let hw1: u16 = 0xEA4F;
5631 let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5632 bytes.extend_from_slice(&hw1.to_le_bytes());
5633 bytes.extend_from_slice(&hw2.to_le_bytes());
5634
5635 Ok(bytes)
5636 }
5637
5638 // I64Extend32S: Sign-extend low 32 bits to 64 bits
5639 // Result: rdlo = rnlo, rdhi = rnlo >> 31
5640 ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
5641 let rdlo_bits = reg_to_bits(rdlo);
5642 let rdhi_bits = reg_to_bits(rdhi);
5643 let rnlo_bits = reg_to_bits(rnlo);
5644 let mut bytes = Vec::new();
5645
5646 // MOV rdlo, rnlo (if different)
5647 if rdlo_bits != rnlo_bits {
5648 // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5649 let d_bit = ((rdlo_bits >> 3) & 1) as u16;
5650 let mov: u16 = 0x4600
5651 | (d_bit << 7)
5652 | ((rnlo_bits as u16) << 3)
5653 | ((rdlo_bits & 0x7) as u16);
5654 bytes.extend_from_slice(&mov.to_le_bytes());
5655 }
5656
5657 // ASR.W rdhi, rnlo, #31 (sign-extend to high word)
5658 let hw1: u16 = 0xEA4F;
5659 let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rnlo_bits) as u16;
5660 bytes.extend_from_slice(&hw1.to_le_bytes());
5661 bytes.extend_from_slice(&hw2.to_le_bytes());
5662
5663 Ok(bytes)
5664 }
5665
5666 // SelectMove: IT <cond>; MOV{cond} rd, rm
5667 // Conditional move: only execute MOV if condition is true
5668 ArmOp::SelectMove { rd, rm, cond } => {
5669 let rd_bits = reg_to_bits(rd) as u16;
5670 let rm_bits = reg_to_bits(rm) as u16;
5671
5672 // Condition code encoding for IT block
5673 use synth_synthesis::Condition;
5674 let cond_bits: u16 = match cond {
5675 Condition::EQ => 0x0, // Equal
5676 Condition::NE => 0x1, // Not equal
5677 Condition::HS => 0x2, // Higher or same (unsigned >=)
5678 Condition::LO => 0x3, // Lower (unsigned <)
5679 Condition::HI => 0x8, // Higher (unsigned >)
5680 Condition::LS => 0x9, // Lower or same (unsigned <=)
5681 Condition::GE => 0xA, // Greater or equal (signed)
5682 Condition::LT => 0xB, // Less than (signed)
5683 Condition::GT => 0xC, // Greater than (signed)
5684 Condition::LE => 0xD, // Less or equal (signed)
5685 };
5686
5687 // IT <cond>: single Then block (mask = 0x8 for T only)
5688 // IT instruction: 1011 1111 firstcond mask
5689 let it_instr: u16 = 0xBF00 | (cond_bits << 4) | 0x8;
5690
5691 // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5692 // This MOV will only execute if condition is true due to IT block
5693 let d_bit = (rd_bits >> 3) & 1;
5694 let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5695
5696 // Emit: IT <cond>, MOV rd, rm
5697 let mut bytes = it_instr.to_le_bytes().to_vec();
5698 bytes.extend_from_slice(&mov_instr.to_le_bytes());
5699 Ok(bytes)
5700 }
5701
5702 // Popcnt: Population count (count set bits)
5703 // ARM Cortex-M has no native POPCNT, so we implement the bit manipulation algorithm:
5704 // x = x - ((x >> 1) & 0x55555555);
5705 // x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
5706 // x = (x + (x >> 4)) & 0x0F0F0F0F;
5707 // x = x + (x >> 8);
5708 // x = x + (x >> 16);
5709 // return x & 0x3F;
5710 //
5711 // #1021: R12 (IP, never allocatable) is the ONLY scratch. The
5712 // previous expansion borrowed R11 as a second temp — but R11 is
5713 // the WASM linear-memory base, materialized at entry and read by
5714 // every later LDR/STR, and it is NOT in the pushed set, so the
5715 // clobber leaked to the CALLER too (a live memory-safety
5716 // miscompile: loads through `base = x >> 16`). The second temp is
5717 // eliminated the way the healthy i64.popcnt discipline implies —
5718 // never touch an unsaved register — but without its PUSH/POP
5719 // wrapper: the SWAR masks 0x55555555 / 0x33333333 / 0x0F0F0F0F
5720 // are all `0xXYXYXYXY` ThumbExpandImm modified immediates, so
5721 // each AND takes its mask from the instruction itself and R12
5722 // alone carries every intermediate. Straight-line, no branches,
5723 // no stack traffic — nothing to skip on a trap edge.
5724 ArmOp::Popcnt { rd, rm } => {
5725 let rd_bits = reg_to_bits(rd);
5726 // Defensive (#1021): rd = R11/R12/SP/PC would silently
5727 // corrupt the linear-memory base, the expansion's own
5728 // scratch, or the stack. The selector never assigns them
5729 // (pool R0-R8); refuse loudly if that ever changes.
5730 if rd_bits >= 11 {
5731 return Err(synth_core::Error::synthesis(
5732 "Popcnt destination must be R0-R10: R11 is the linear-memory \
5733 base and R12 is the expansion's scratch (#1021)",
5734 ));
5735 }
5736 let mut bytes = Vec::new();
5737
5738 // First, move rm to rd if they're different
5739 if rd != rm {
5740 let rm_bits = reg_to_bits(rm) as u16;
5741 // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5742 let d_bit = ((rd_bits as u16) >> 3) & 1;
5743 let mov_instr: u16 =
5744 0x4600 | (d_bit << 7) | (rm_bits << 3) | ((rd_bits as u16) & 0x7);
5745 bytes.extend_from_slice(&mov_instr.to_le_bytes());
5746 }
5747
5748 // Step 1: x = x - ((x >> 1) & 0x55555555)
5749 // R12 = rd >> 1
5750 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 1)?);
5751 // R12 = R12 & 0x55555555 (modified immediate, no constant reg)
5752 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(12, 12, 0x5555_5555)?);
5753 // rd = rd - R12
5754 bytes.extend_from_slice(&self.encode_thumb32_sub_reg_raw(rd_bits, rd_bits, 12)?);
5755
5756 // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5757 // R12 = rd & 0x33333333
5758 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5759 12,
5760 rd_bits,
5761 0x3333_3333,
5762 )?);
5763 // rd = rd >> 2
5764 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(rd_bits, rd_bits, 2)?);
5765 // rd = rd & 0x33333333
5766 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5767 rd_bits,
5768 rd_bits,
5769 0x3333_3333,
5770 )?);
5771 // rd = rd + R12
5772 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5773
5774 // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5775 // R12 = rd >> 4
5776 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 4)?);
5777 // rd = rd + R12
5778 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5779 // rd = rd & 0x0F0F0F0F
5780 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5781 rd_bits,
5782 rd_bits,
5783 0x0F0F_0F0F,
5784 )?);
5785
5786 // Step 4: x = x + (x >> 8)
5787 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 8)?);
5788 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5789
5790 // Step 5: x = x + (x >> 16)
5791 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 16)?);
5792 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5793
5794 // Step 6: return x & 0x3F
5795 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(rd_bits, rd_bits, 0x3F)?);
5796
5797 Ok(bytes)
5798 }
5799
5800 // I64DivU: 64-bit unsigned division using binary long division
5801 // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = quotient
5802 // Uses: R4-R7, R12 as loop counter (avoid R8 for Renode compatibility)
5803 //
5804 // #610: the fixed-ABI wrapper marshals the selector-assigned
5805 // operand registers into the core's fixed regs and lands the
5806 // result in rd — pre-#610 this arm IGNORED its register fields,
5807 // so the selector read its rd pair (e.g. R4:R5) after the core's
5808 // own POP restored the stale caller values over it: 0 for every
5809 // input. A zero divisor now traps (UDF #0), per WASM semantics.
5810 ArmOp::I64DivU {
5811 rdlo,
5812 rdhi,
5813 rnlo,
5814 rnhi,
5815 rmlo,
5816 rmhi,
5817 elide_zero_guard,
5818 } => {
5819 let mut bytes = Vec::new();
5820 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5821 // #494 phase 2b: elided only under a certificate-discharged
5822 // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
5823 if !elide_zero_guard {
5824 emit_i64_divisor_zero_trap(&mut bytes);
5825 }
5826
5827 // PUSH {R4-R7} - save scratch registers (NO LR — this is inline code)
5828 // 16-bit PUSH: 1011 010 M rrrrrrrr where M=0 (no LR), r=R4-R7 = 0xF0
5829 // Encoding: 1011 0100 1111 0000 = 0xB4F0
5830 bytes.extend_from_slice(&0xB4F0u16.to_le_bytes());
5831
5832 // Initialize quotient (R4:R5) = 0
5833 bytes.extend_from_slice(&0x2400u16.to_le_bytes()); // MOV R4, #0
5834 bytes.extend_from_slice(&0x2500u16.to_le_bytes()); // MOV R5, #0
5835
5836 // Initialize remainder (R6:R7) = 0
5837 bytes.extend_from_slice(&0x2600u16.to_le_bytes()); // MOV R6, #0
5838 bytes.extend_from_slice(&0x2700u16.to_le_bytes()); // MOV R7, #0
5839
5840 // Initialize loop counter R12 = 64 (use R12 scratch instead of R8)
5841 // MOV.W R12, #64: F04F 0C40
5842 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5843 bytes.extend_from_slice(&0x0C40u16.to_le_bytes());
5844
5845 // Loop start
5846 let loop_start = bytes.len();
5847
5848 // === Loop body: process one bit ===
5849
5850 // 1. Shift quotient R4:R5 left by 1
5851 // LSLS R5, R5, #1 (16-bit: 0000 0010 1010 1101 = 0x006D -> actually 0x002D for LSL R5,R5,#1)
5852 // LSL Rd, Rm, #imm5: 000 00 imm5 Rm Rd = 000 00 00001 101 101 = 0x006D
5853 bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5854 // Get carry from R4 into R5: ORR R5, R5, R4 LSR #31
5855 // Thumb-2 ORR with shifted register: EA45 75D4 = ORR.W R5, R5, R4, LSR #31
5856 // 11101010 010 S Rn | 0 imm3 Rd imm2 type Rm
5857 // type=01 (LSR), imm5=31 (imm3=111, imm2=11)
5858 bytes.extend_from_slice(&0xEA45u16.to_le_bytes());
5859 bytes.extend_from_slice(&0x75D4u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5860 // LSLS R4, R4, #1: 000 00 00001 100 100 = 0x0064
5861 bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5862
5863 // 2. Shift remainder R6:R7 left by 1, OR in MSB of dividend R1
5864 // LSLS R7, R7, #1
5865 bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5866 // ORR.W R7, R7, R6, LSR #31
5867 bytes.extend_from_slice(&0xEA47u16.to_le_bytes());
5868 bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5869 // LSLS R6, R6, #1
5870 bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5871 // ORR.W R6, R6, R1, LSR #31 (bring in MSB of dividend high)
5872 bytes.extend_from_slice(&0xEA46u16.to_le_bytes());
5873 bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5874
5875 // 3. Shift dividend R0:R1 left by 1
5876 // LSLS R1, R1, #1
5877 bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5878 // ORR.W R1, R1, R0, LSR #31
5879 bytes.extend_from_slice(&0xEA41u16.to_le_bytes());
5880 bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5881 // LSLS R0, R0, #1
5882 bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5883
5884 // 4. Compare remainder >= divisor (64-bit unsigned comparison)
5885 // Compare high words first: CMP R7, R3
5886 // CMP Rn, Rm encoding: 0x4280 | (Rm << 3) | Rn
5887 bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3 (16-bit)
5888 // BHI means R7 > R3 (unsigned) - definitely subtract
5889 // BLO means R7 < R3 - definitely don't subtract
5890 // BEQ means need to check low words
5891
5892 // If high > divisor high: branch to subtract (forward +offset)
5893 // BHI.N +6 (skip CMP, skip BLO, do subtract)
5894 // BHI: 1101 1000 offset8 where cond=1000 (HI)
5895 bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4 (to subtract block)
5896
5897 // If high < divisor high: branch past subtract
5898 // BLO.N +10 (skip to decrement)
5899 bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BLO/BCC +12 (past subtract)
5900
5901 // High words equal, compare low: CMP R6, R2
5902 bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2 (16-bit)
5903 // BLO/BCC past subtract (skip SUBS+SBC.W+ORR.W = 10 bytes = 4 halfwords from PC+4)
5904 bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords (past subtract)
5905
5906 // === Subtract block: remainder -= divisor, quotient |= 1 ===
5907 // SUBS R6, R6, R2
5908 bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2 (16-bit)
5909 // SBC R7, R7, R3 (with borrow)
5910 // Thumb-2 SBC.W: EB67 0703 = SBC.W R7, R7, R3
5911 bytes.extend_from_slice(&0xEB67u16.to_le_bytes());
5912 bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5913 // ORR R4, R4, #1 (set bit 0 of quotient low)
5914 bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5915 bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5916
5917 // === Decrement counter and loop ===
5918 // SUBS.W R12, R12, #1 (decrement loop counter)
5919 // SUBS.W R12, R12, #1: F1BC 0C01
5920 bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
5921 bytes.extend_from_slice(&0x0C01u16.to_le_bytes());
5922
5923 // BNE back to loop_start
5924 let branch_offset_bytes = bytes.len() - loop_start + 4; // +4 for pipeline
5925 let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5926 let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5927 bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5928
5929 // === Loop done, move quotient to R0:R1 ===
5930 bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5931 bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5932
5933 // POP {R4-R7} - restore scratch registers (NO PC — inline code continues)
5934 // 16-bit POP: 1011 110 P rrrrrrrr where P=0 (no PC), r=R4-R7 = 0xF0
5935 // Encoding: 1011 1100 1111 0000 = 0xBCF0
5936 bytes.extend_from_slice(&0xBCF0u16.to_le_bytes());
5937
5938 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5939 Ok(bytes)
5940 }
5941
5942 // I64DivS: 64-bit signed division
5943 // Converts to unsigned, divides, then applies sign
5944 // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5945 // -> R0:R1 = quotient (signed)
5946 // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5947 ArmOp::I64DivS {
5948 rdlo,
5949 rdhi,
5950 rnlo,
5951 rnhi,
5952 rmlo,
5953 rmhi,
5954 elide_zero_guard,
5955 elide_overflow_guard,
5956 } => {
5957 let mut bytes = Vec::new();
5958 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5959 // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
5960 // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
5961 // the #633 overflow guard falls ONLY to
5962 // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
5963 // divisor-nonzero fact alone must keep it.
5964 if !elide_zero_guard {
5965 emit_i64_divisor_zero_trap(&mut bytes);
5966 }
5967 if !elide_overflow_guard {
5968 // #633: INT64_MIN / -1 overflows — trap like the i32 path
5969 // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
5970 emit_i64_divs_overflow_trap(&mut bytes);
5971 }
5972
5973 // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5974 bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5975 bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5976
5977 // Save result sign in R9: R9 = R1 XOR R3 (sign bit = MSB)
5978 // EOR.W R9, R1, R3
5979 bytes.extend_from_slice(&0xEA81u16.to_le_bytes());
5980 bytes.extend_from_slice(&0x0903u16.to_le_bytes());
5981
5982 // If dividend negative (R1 MSB set), negate it
5983 // TST R1, R1 (check sign)
5984 bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5985 // BPL skip_neg_dividend (+10 bytes = 5 halfwords)
5986 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5987
5988 // Negate R0:R1 (64-bit): RSBS R0, R0, #0; SBC R1, R1, R1 LSL #1
5989 // Actually: MVN R0, R0; MVN R1, R1; ADDS R0, R0, #1; ADC R1, R1, #0
5990 bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5991 bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5992 bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5993 bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5994 bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5995
5996 // If divisor negative (R3 MSB set), negate it
5997 bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5998 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5999
6000 // Negate R2:R3
6001 bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
6002 bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
6003 bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
6004 bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
6005 bytes.extend_from_slice(&0x0300u16.to_le_bytes());
6006
6007 // === Now do unsigned division (same as I64DivU) ===
6008 // Initialize quotient (R4:R5) = 0
6009 bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6010 bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6011 // Initialize remainder (R6:R7) = 0
6012 bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6013 bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6014 // Initialize loop counter R8 = 64
6015 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6016 bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6017
6018 let loop_start = bytes.len();
6019
6020 // Shift quotient left
6021 bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6022 bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6023 bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6024 bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6025
6026 // Shift remainder left, OR in MSB of dividend
6027 bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6028 bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6029 bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6030 bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6031 bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6032 bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6033
6034 // Shift dividend left
6035 bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6036 bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6037 bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6038 bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6039
6040 // Compare and conditionally subtract
6041 bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6042 bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6043 bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6044 bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6045 bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6046
6047 // Subtract and set quotient bit
6048 bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6049 bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6050 bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6051 bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6052 bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6053
6054 // Decrement and loop
6055 bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6056 bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6057
6058 let branch_offset_bytes = bytes.len() - loop_start + 4;
6059 let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6060 let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6061 bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6062
6063 // Move quotient to R0:R1
6064 bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
6065 bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
6066
6067 // If result should be negative (R9 MSB set), negate R0:R1
6068 bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9 (check MSB)
6069 bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6070 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8 (skip negation)
6071
6072 // Negate result R0:R1
6073 bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6074 bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6075 bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6076 bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6077 bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6078
6079 // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6080 bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6081 bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6082
6083 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6084 Ok(bytes)
6085 }
6086
6087 // I64RemU: 64-bit unsigned remainder using binary long division
6088 // Same algorithm as I64DivU but returns remainder instead of quotient
6089 // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = remainder
6090 // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6091 ArmOp::I64RemU {
6092 rdlo,
6093 rdhi,
6094 rnlo,
6095 rnhi,
6096 rmlo,
6097 rmhi,
6098 elide_zero_guard,
6099 } => {
6100 let mut bytes = Vec::new();
6101 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6102 if !elide_zero_guard {
6103 emit_i64_divisor_zero_trap(&mut bytes);
6104 }
6105
6106 // PUSH {R4-R8} - save scratch registers (NO LR — inline code)
6107 bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6108 bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6109
6110 // Initialize quotient (R4:R5) = 0 (computed but not returned)
6111 bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6112 bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6113 // Initialize remainder (R6:R7) = 0
6114 bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6115 bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6116 // Initialize loop counter R8 = 64
6117 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6118 bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6119
6120 let loop_start = bytes.len();
6121
6122 // Shift quotient left (not needed for result, but keeps algorithm same)
6123 bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6124 bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6125 bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6126 bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6127
6128 // Shift remainder left, OR in MSB of dividend
6129 bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6130 bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6131 bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6132 bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6133 bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6134 bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6135
6136 // Shift dividend left
6137 bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6138 bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6139 bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6140 bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6141
6142 // Compare and conditionally subtract
6143 bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6144 bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6145 bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6146 bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6147 bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6148
6149 // Subtract and set quotient bit
6150 bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6151 bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6152 bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6153 bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6154 bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6155
6156 // Decrement and loop
6157 bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6158 bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6159
6160 let branch_offset_bytes = bytes.len() - loop_start + 4;
6161 let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6162 let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6163 bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6164
6165 // Move REMAINDER to R0:R1 (difference from I64DivU)
6166 bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6167 bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6168
6169 // POP {R4-R8} - restore scratch registers (NO PC — inline code continues)
6170 bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6171 bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6172
6173 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6174 Ok(bytes)
6175 }
6176
6177 // I64RemS: 64-bit signed remainder
6178 // Remainder sign follows dividend sign (not quotient rule)
6179 // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
6180 // -> R0:R1 = remainder (signed, same sign as dividend)
6181 // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6182 ArmOp::I64RemS {
6183 rdlo,
6184 rdhi,
6185 rnlo,
6186 rnhi,
6187 rmlo,
6188 rmhi,
6189 elide_zero_guard,
6190 } => {
6191 let mut bytes = Vec::new();
6192 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6193 if !elide_zero_guard {
6194 emit_i64_divisor_zero_trap(&mut bytes);
6195 }
6196
6197 // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
6198 bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6199 bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6200
6201 // Save dividend sign in R9 (remainder sign = dividend sign)
6202 // MOV R9, R1 (just need the sign bit)
6203 bytes.extend_from_slice(&0x4689u16.to_le_bytes()); // MOV R9, R1
6204
6205 // If dividend negative (R1 MSB set), negate it
6206 bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
6207 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6208
6209 // Negate R0:R1
6210 bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6211 bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6212 bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6213 bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6214 bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6215
6216 // If divisor negative (R3 MSB set), negate it
6217 bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
6218 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6219
6220 // Negate R2:R3
6221 bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
6222 bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
6223 bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
6224 bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
6225 bytes.extend_from_slice(&0x0300u16.to_le_bytes());
6226
6227 // === Unsigned division algorithm ===
6228 // Initialize quotient (R4:R5) = 0
6229 bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6230 bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6231 // Initialize remainder (R6:R7) = 0
6232 bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6233 bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6234 // Initialize loop counter R8 = 64
6235 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6236 bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6237
6238 let loop_start = bytes.len();
6239
6240 // Shift quotient left
6241 bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6242 bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6243 bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6244 bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6245
6246 // Shift remainder left, OR in MSB of dividend
6247 bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6248 bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6249 bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6250 bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6251 bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6252 bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6253
6254 // Shift dividend left
6255 bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6256 bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6257 bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6258 bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6259
6260 // Compare and conditionally subtract
6261 bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6262 bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6263 bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6264 bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6265 bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6266
6267 // Subtract and set quotient bit
6268 bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6269 bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6270 bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6271 bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6272 bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6273
6274 // Decrement and loop
6275 bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6276 bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6277
6278 let branch_offset_bytes = bytes.len() - loop_start + 4;
6279 let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6280 let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6281 bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6282
6283 // Move remainder to R0:R1
6284 bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6285 bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6286
6287 // If original dividend was negative (R9 MSB set), negate remainder
6288 bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9
6289 bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6290 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6291
6292 // Negate result R0:R1
6293 bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6294 bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6295 bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6296 bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6297 bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6298
6299 // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6300 bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6301 bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6302
6303 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6304 Ok(bytes)
6305 }
6306
6307 // === F32 VFP single-precision Thumb-2 encodings ===
6308 // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6309 ArmOp::F32Add { sd, sn, sm } => {
6310 Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A00, sd, sn, sm)?))
6311 }
6312 ArmOp::F32Sub { sd, sn, sm } => {
6313 Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A40, sd, sn, sm)?))
6314 }
6315 ArmOp::F32Mul { sd, sn, sm } => {
6316 Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE200A00, sd, sn, sm)?))
6317 }
6318 ArmOp::F32Div { sd, sn, sm } => {
6319 Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE800A00, sd, sn, sm)?))
6320 }
6321 ArmOp::F32Abs { sd, sm } => {
6322 Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB00AC0, sd, sm)?))
6323 }
6324 ArmOp::F32Neg { sd, sm } => {
6325 Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10A40, sd, sm)?))
6326 }
6327 ArmOp::F32Sqrt { sd, sm } => {
6328 Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10AC0, sd, sm)?))
6329 }
6330
6331 // f32 pseudo-ops — multi-instruction sequences
6332 // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6333 ArmOp::F32Ceil { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b01),
6334 ArmOp::F32Floor { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b10),
6335 ArmOp::F32Trunc { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b11),
6336 ArmOp::F32Nearest { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b00),
6337 ArmOp::F32Min { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, true),
6338 ArmOp::F32Max { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, false),
6339 ArmOp::F32Copysign { sd, sn, sm } => self.encode_thumb_f32_copysign(sd, sn, sm),
6340
6341 // f32 comparisons — VCMP + VMRS + MOV #0 + IT + MOV #1
6342 ArmOp::F32Eq { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x0),
6343 ArmOp::F32Ne { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x1),
6344 ArmOp::F32Lt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x4),
6345 ArmOp::F32Le { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x9),
6346 ArmOp::F32Gt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xC),
6347 ArmOp::F32Ge { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xA),
6348
6349 ArmOp::F32Const { sd, value } => self.encode_thumb_f32_const(sd, *value),
6350
6351 ArmOp::F32Load { sd, addr } => {
6352 Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED900A00, sd, addr)?))
6353 }
6354 ArmOp::F32Store { sd, addr } => {
6355 Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED800A00, sd, addr)?))
6356 }
6357
6358 ArmOp::F32ConvertI32S { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, true),
6359 ArmOp::F32ConvertI32U { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, false),
6360 ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
6361 Err(synth_core::Error::synthesis(
6362 "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6363 ))
6364 }
6365 ArmOp::F32ReinterpretI32 { sd, rm } => {
6366 Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(true, sd, rm)?))
6367 }
6368 ArmOp::I32ReinterpretF32 { rd, sm } => {
6369 Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(false, sm, rd)?))
6370 }
6371 ArmOp::I32TruncF32S { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, true),
6372 ArmOp::I32TruncF32U { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, false),
6373
6374 // === F64 VFP double-precision Thumb-2 encodings ===
6375 // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6376 ArmOp::F64Add { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6377 0xEE300B00, dd, dn, dm,
6378 )?)),
6379 ArmOp::F64Sub { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6380 0xEE300B40, dd, dn, dm,
6381 )?)),
6382 ArmOp::F64Mul { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6383 0xEE200B00, dd, dn, dm,
6384 )?)),
6385 ArmOp::F64Div { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6386 0xEE800B00, dd, dn, dm,
6387 )?)),
6388 ArmOp::F64Abs { dd, dm } => {
6389 Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?))
6390 }
6391 ArmOp::F64Neg { dd, dm } => {
6392 Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?))
6393 }
6394 ArmOp::F64Sqrt { dd, dm } => {
6395 Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?))
6396 }
6397
6398 // f64 pseudo-ops
6399 // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6400 ArmOp::F64Ceil { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b01),
6401 ArmOp::F64Floor { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b10),
6402 ArmOp::F64Trunc { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b11),
6403 ArmOp::F64Nearest { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b00),
6404 ArmOp::F64Min { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, true),
6405 ArmOp::F64Max { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, false),
6406 ArmOp::F64Copysign { dd, dn, dm } => self.encode_thumb_f64_copysign(dd, dn, dm),
6407
6408 // f64 comparisons
6409 ArmOp::F64Eq { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x0),
6410 ArmOp::F64Ne { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x1),
6411 ArmOp::F64Lt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x4),
6412 ArmOp::F64Le { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x9),
6413 ArmOp::F64Gt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xC),
6414 ArmOp::F64Ge { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xA),
6415
6416 ArmOp::F64Const { dd, value } => self.encode_thumb_f64_const(dd, *value),
6417
6418 ArmOp::F64Load { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6419 0xED900B00, dd, addr,
6420 )?)),
6421 ArmOp::F64Store { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6422 0xED800B00, dd, addr,
6423 )?)),
6424
6425 ArmOp::F64ConvertI32S { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, true),
6426 ArmOp::F64ConvertI32U { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, false),
6427 ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
6428 Err(synth_core::Error::synthesis(
6429 "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6430 ))
6431 }
6432 ArmOp::F64PromoteF32 { dd, sm } => self.encode_thumb_f64_promote_f32(dd, sm),
6433 ArmOp::F32DemoteF64 { sd, dm } => self.encode_thumb_f32_demote_f64(sd, dm),
6434 ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => Ok(vfp_to_thumb_bytes(
6435 encode_vmov_core_dreg(true, dd, rmlo, rmhi)?,
6436 )),
6437 ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => Ok(vfp_to_thumb_bytes(
6438 encode_vmov_core_dreg(false, dm, rdlo, rdhi)?,
6439 )),
6440 ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
6441 Err(synth_core::Error::synthesis(
6442 "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
6443 ))
6444 }
6445 ArmOp::I32TruncF64S { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, true),
6446 ArmOp::I32TruncF64U { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, false),
6447
6448 // ===== i64 operations: encode as multi-instruction Thumb-2 sequences =====
6449
6450 // I64Add: ADDS rdlo, rnlo, rmlo; ADC.W rdhi, rnhi, rmhi
6451 ArmOp::I64Add {
6452 rdlo,
6453 rdhi,
6454 rnlo,
6455 rnhi,
6456 rmlo,
6457 rmhi,
6458 } => {
6459 let mut bytes = Vec::new();
6460 // ADDS rdlo, rnlo, rmlo (16-bit)
6461 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adds {
6462 rd: *rdlo,
6463 rn: *rnlo,
6464 op2: Operand2::Reg(*rmlo),
6465 })?);
6466 // ADC.W rdhi, rnhi, rmhi (32-bit)
6467 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adc {
6468 rd: *rdhi,
6469 rn: *rnhi,
6470 op2: Operand2::Reg(*rmhi),
6471 })?);
6472 Ok(bytes)
6473 }
6474
6475 // I64Sub: SUBS rdlo, rnlo, rmlo; SBC.W rdhi, rnhi, rmhi
6476 ArmOp::I64Sub {
6477 rdlo,
6478 rdhi,
6479 rnlo,
6480 rnhi,
6481 rmlo,
6482 rmhi,
6483 } => {
6484 let mut bytes = Vec::new();
6485 // SUBS rdlo, rnlo, rmlo (16-bit)
6486 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Subs {
6487 rd: *rdlo,
6488 rn: *rnlo,
6489 op2: Operand2::Reg(*rmlo),
6490 })?);
6491 // SBC.W rdhi, rnhi, rmhi (32-bit)
6492 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Sbc {
6493 rd: *rdhi,
6494 rn: *rnhi,
6495 op2: Operand2::Reg(*rmhi),
6496 })?);
6497 Ok(bytes)
6498 }
6499
6500 // I64And: AND rdlo, rnlo, rmlo; AND rdhi, rnhi, rmhi
6501 ArmOp::I64And {
6502 rdlo,
6503 rdhi,
6504 rnlo,
6505 rnhi,
6506 rmlo,
6507 rmhi,
6508 } => {
6509 let mut bytes = Vec::new();
6510 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6511 rd: *rdlo,
6512 rn: *rnlo,
6513 op2: Operand2::Reg(*rmlo),
6514 })?);
6515 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6516 rd: *rdhi,
6517 rn: *rnhi,
6518 op2: Operand2::Reg(*rmhi),
6519 })?);
6520 Ok(bytes)
6521 }
6522
6523 // I64Or: ORR rdlo, rnlo, rmlo; ORR rdhi, rnhi, rmhi
6524 ArmOp::I64Or {
6525 rdlo,
6526 rdhi,
6527 rnlo,
6528 rnhi,
6529 rmlo,
6530 rmhi,
6531 } => {
6532 let mut bytes = Vec::new();
6533 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6534 rd: *rdlo,
6535 rn: *rnlo,
6536 op2: Operand2::Reg(*rmlo),
6537 })?);
6538 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6539 rd: *rdhi,
6540 rn: *rnhi,
6541 op2: Operand2::Reg(*rmhi),
6542 })?);
6543 Ok(bytes)
6544 }
6545
6546 // I64Xor: EOR rdlo, rnlo, rmlo; EOR rdhi, rnhi, rmhi
6547 ArmOp::I64Xor {
6548 rdlo,
6549 rdhi,
6550 rnlo,
6551 rnhi,
6552 rmlo,
6553 rmhi,
6554 } => {
6555 let mut bytes = Vec::new();
6556 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6557 rd: *rdlo,
6558 rn: *rnlo,
6559 op2: Operand2::Reg(*rmlo),
6560 })?);
6561 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6562 rd: *rdhi,
6563 rn: *rnhi,
6564 op2: Operand2::Reg(*rmhi),
6565 })?);
6566 Ok(bytes)
6567 }
6568
6569 // I64Eqz: ORR scratch, lo, hi; ITE EQ; MOV rd, #1; MOV rd, #0
6570 ArmOp::I64Eqz { rd, rnlo, rnhi } => self.encode_thumb(&ArmOp::I64SetCondZ {
6571 rd: *rd,
6572 rn_lo: *rnlo,
6573 rn_hi: *rnhi,
6574 }),
6575
6576 // I64 comparisons: delegate to I64SetCond
6577 ArmOp::I64Eq {
6578 rd,
6579 rnlo,
6580 rnhi,
6581 rmlo,
6582 rmhi,
6583 } => self.encode_thumb(&ArmOp::I64SetCond {
6584 rd: *rd,
6585 rn_lo: *rnlo,
6586 rn_hi: *rnhi,
6587 rm_lo: *rmlo,
6588 rm_hi: *rmhi,
6589 cond: synth_synthesis::Condition::EQ,
6590 }),
6591
6592 ArmOp::I64Ne {
6593 rd,
6594 rnlo,
6595 rnhi,
6596 rmlo,
6597 rmhi,
6598 } => self.encode_thumb(&ArmOp::I64SetCond {
6599 rd: *rd,
6600 rn_lo: *rnlo,
6601 rn_hi: *rnhi,
6602 rm_lo: *rmlo,
6603 rm_hi: *rmhi,
6604 cond: synth_synthesis::Condition::NE,
6605 }),
6606
6607 ArmOp::I64LtS {
6608 rd,
6609 rnlo,
6610 rnhi,
6611 rmlo,
6612 rmhi,
6613 } => self.encode_thumb(&ArmOp::I64SetCond {
6614 rd: *rd,
6615 rn_lo: *rnlo,
6616 rn_hi: *rnhi,
6617 rm_lo: *rmlo,
6618 rm_hi: *rmhi,
6619 cond: synth_synthesis::Condition::LT,
6620 }),
6621
6622 ArmOp::I64LtU {
6623 rd,
6624 rnlo,
6625 rnhi,
6626 rmlo,
6627 rmhi,
6628 } => self.encode_thumb(&ArmOp::I64SetCond {
6629 rd: *rd,
6630 rn_lo: *rnlo,
6631 rn_hi: *rnhi,
6632 rm_lo: *rmlo,
6633 rm_hi: *rmhi,
6634 cond: synth_synthesis::Condition::LO,
6635 }),
6636
6637 ArmOp::I64LeS {
6638 rd,
6639 rnlo,
6640 rnhi,
6641 rmlo,
6642 rmhi,
6643 } => self.encode_thumb(&ArmOp::I64SetCond {
6644 rd: *rd,
6645 rn_lo: *rnlo,
6646 rn_hi: *rnhi,
6647 rm_lo: *rmlo,
6648 rm_hi: *rmhi,
6649 cond: synth_synthesis::Condition::LE,
6650 }),
6651
6652 ArmOp::I64LeU {
6653 rd,
6654 rnlo,
6655 rnhi,
6656 rmlo,
6657 rmhi,
6658 } => self.encode_thumb(&ArmOp::I64SetCond {
6659 rd: *rd,
6660 rn_lo: *rnlo,
6661 rn_hi: *rnhi,
6662 rm_lo: *rmlo,
6663 rm_hi: *rmhi,
6664 cond: synth_synthesis::Condition::LS,
6665 }),
6666
6667 ArmOp::I64GtS {
6668 rd,
6669 rnlo,
6670 rnhi,
6671 rmlo,
6672 rmhi,
6673 } => self.encode_thumb(&ArmOp::I64SetCond {
6674 rd: *rd,
6675 rn_lo: *rnlo,
6676 rn_hi: *rnhi,
6677 rm_lo: *rmlo,
6678 rm_hi: *rmhi,
6679 cond: synth_synthesis::Condition::GT,
6680 }),
6681
6682 ArmOp::I64GtU {
6683 rd,
6684 rnlo,
6685 rnhi,
6686 rmlo,
6687 rmhi,
6688 } => self.encode_thumb(&ArmOp::I64SetCond {
6689 rd: *rd,
6690 rn_lo: *rnlo,
6691 rn_hi: *rnhi,
6692 rm_lo: *rmlo,
6693 rm_hi: *rmhi,
6694 cond: synth_synthesis::Condition::HI,
6695 }),
6696
6697 ArmOp::I64GeS {
6698 rd,
6699 rnlo,
6700 rnhi,
6701 rmlo,
6702 rmhi,
6703 } => self.encode_thumb(&ArmOp::I64SetCond {
6704 rd: *rd,
6705 rn_lo: *rnlo,
6706 rn_hi: *rnhi,
6707 rm_lo: *rmlo,
6708 rm_hi: *rmhi,
6709 cond: synth_synthesis::Condition::GE,
6710 }),
6711
6712 ArmOp::I64GeU {
6713 rd,
6714 rnlo,
6715 rnhi,
6716 rmlo,
6717 rmhi,
6718 } => self.encode_thumb(&ArmOp::I64SetCond {
6719 rd: *rd,
6720 rn_lo: *rnlo,
6721 rn_hi: *rnhi,
6722 rm_lo: *rmlo,
6723 rm_hi: *rmhi,
6724 cond: synth_synthesis::Condition::HS,
6725 }),
6726
6727 // I64Const: MOVW rdlo, lo16; MOVT rdlo, hi16; MOVW rdhi, lo16_hi; MOVT rdhi, hi16_hi
6728 ArmOp::I64Const { rdlo, rdhi, value } => {
6729 let lo32 = *value as u32;
6730 let hi32 = (*value >> 32) as u32;
6731 let mut bytes = Vec::new();
6732 // Load low 32 bits into rdlo
6733 bytes.extend_from_slice(
6734 &self.encode_thumb32_movw_raw(reg_to_bits(rdlo), lo32 & 0xFFFF)?,
6735 );
6736 if lo32 > 0xFFFF {
6737 bytes.extend_from_slice(
6738 &self.encode_thumb32_movt_raw(reg_to_bits(rdlo), lo32 >> 16)?,
6739 );
6740 }
6741 // Load high 32 bits into rdhi
6742 bytes.extend_from_slice(
6743 &self.encode_thumb32_movw_raw(reg_to_bits(rdhi), hi32 & 0xFFFF)?,
6744 );
6745 if hi32 > 0xFFFF {
6746 bytes.extend_from_slice(
6747 &self.encode_thumb32_movt_raw(reg_to_bits(rdhi), hi32 >> 16)?,
6748 );
6749 }
6750 Ok(bytes)
6751 }
6752
6753 // I64Ldr: LDR rdlo, [base, offset]; LDR rdhi, [base, offset+4]
6754 ArmOp::I64Ldr { rdlo, rdhi, addr } => {
6755 let mut bytes = Vec::new();
6756 // #372/#382: a memory `i64.load` carries an index register
6757 // (`reg_imm(R11, addr_reg, offset)` = R11 + addr + offset). The
6758 // immediate `encode_thumb32_ldr` below uses only base+offset and
6759 // would SILENTLY DROP `offset_reg` — the #206 defect, here for
6760 // i64. `i64_effective_base` materializes the effective base into
6761 // `ip` (and, when `offset+4 > 0xFFF`, folds the offset in too so
6762 // the function is NOT skipped — #382), returning the residual
6763 // imm12 for the two halves. Frame i64 loads (no `offset_reg`, e.g.
6764 // a spilled local at `[SP, #off]`) keep the plain `[base,#off]`
6765 // form unchanged — so existing output is byte-identical.
6766 let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6767 bytes.extend_from_slice(&self.encode_thumb32_ldr(rdlo, &base, offset)?);
6768 bytes.extend_from_slice(&self.encode_thumb32_ldr(
6769 rdhi,
6770 &base,
6771 offset.wrapping_add(4),
6772 )?);
6773 Ok(bytes)
6774 }
6775
6776 // I64Str: STR rdlo, [base, offset]; STR rdhi, [base, offset+4]
6777 ArmOp::I64Str { rdlo, rdhi, addr } => {
6778 let mut bytes = Vec::new();
6779 // #372/#382: same index-materialization + large-offset fold as
6780 // I64Ldr (see above).
6781 let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6782 bytes.extend_from_slice(&self.encode_thumb32_str(rdlo, &base, offset)?);
6783 bytes.extend_from_slice(&self.encode_thumb32_str(
6784 rdhi,
6785 &base,
6786 offset.wrapping_add(4),
6787 )?);
6788 Ok(bytes)
6789 }
6790
6791 // I64ExtendI32S: MOV rdlo, rn; ASR rdhi, rdlo, #31 (sign-extend)
6792 ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
6793 let mut bytes = Vec::new();
6794 if rdlo != rn {
6795 // MOV rdlo, rn (16-bit)
6796 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6797 rd: *rdlo,
6798 op2: Operand2::Reg(*rn),
6799 })?);
6800 }
6801 // ASR rdhi, rdlo, #31 (sign-extend: fill high word with sign bit)
6802 bytes.extend_from_slice(
6803 &self.encode_thumb32_shift(rdhi, rdlo, 31, 0b10)?, // ASR type
6804 );
6805 Ok(bytes)
6806 }
6807
6808 // I64ExtendI32U: MOV rdlo, rn; MOV rdhi, #0
6809 ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
6810 let mut bytes = Vec::new();
6811 if rdlo != rn {
6812 // MOV rdlo, rn
6813 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6814 rd: *rdlo,
6815 op2: Operand2::Reg(*rn),
6816 })?);
6817 }
6818 // MOV rdhi, #0 (#916: MOV.W for rdhi >= R8). Unconditional
6819 // site with no branches in the expansion — before the fix this
6820 // emitted the literal two-instruction stream [4608, 2800], half
6821 // of which was `CMP r0,#0` rather than the high-word clear, so
6822 // every i64.extend_i32_u into a high pair leaked stale bits.
6823 emit_thumb_zero_fill(&mut bytes, reg_to_bits(rdhi));
6824 Ok(bytes)
6825 }
6826
6827 // I32WrapI64: MOV rd, rnlo (just take low 32 bits)
6828 ArmOp::I32WrapI64 { rd, rnlo } => {
6829 if rd == rnlo {
6830 // No-op: already in the right register
6831 let instr: u16 = 0xBF00; // NOP
6832 Ok(instr.to_le_bytes().to_vec())
6833 } else {
6834 // MOV rd, rnlo
6835 self.encode_thumb(&ArmOp::Mov {
6836 rd: *rd,
6837 op2: Operand2::Reg(*rnlo),
6838 })
6839 }
6840 }
6841
6842 // ===== Helium MVE operations (Thumb-2 encoding) =====
6843 ArmOp::MveLoad { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vldrw(qd, addr))),
6844 ArmOp::MveStore { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vstrw(qd, addr))),
6845 ArmOp::MveConst { qd, bytes } => self.encode_thumb_mve_const(qd, bytes),
6846 ArmOp::MveAnd { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6847 0xEF000150, qd, qn, qm,
6848 ))),
6849 ArmOp::MveOrr { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6850 0xEF200150, qd, qn, qm,
6851 ))),
6852 ArmOp::MveEor { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6853 0xFF000150, qd, qn, qm,
6854 ))),
6855 ArmOp::MveMvn { qd, qm } => {
6856 // VMVN Qd, Qm: 0xFFB005C0 | Qd<<12 | Qm
6857 let qd_enc = qreg_to_num(qd);
6858 let qm_enc = qreg_to_num(qm);
6859 let instr: u32 = 0xFFB005C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6860 Ok(vfp_to_thumb_bytes(instr))
6861 }
6862 ArmOp::MveBic { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6863 0xEF100150, qd, qn, qm,
6864 ))),
6865 ArmOp::MveAddI { qd, qn, qm, size } => {
6866 let sz = mve_size_bits(size);
6867 let base: u32 = 0xEF000840 | (sz << 20);
6868 Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6869 }
6870 ArmOp::MveSubI { qd, qn, qm, size } => {
6871 let sz = mve_size_bits(size);
6872 let base: u32 = 0xFF000840 | (sz << 20);
6873 Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6874 }
6875 ArmOp::MveMulI { qd, qn, qm, size } => {
6876 let sz = mve_size_bits(size);
6877 let base: u32 = 0xEF000950 | (sz << 20);
6878 Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6879 }
6880 ArmOp::MveNegI { qd, qm, size } => {
6881 let sz = mve_size_bits(size);
6882 // VNEG.Sx Qd, Qm
6883 let qd_enc = qreg_to_num(qd);
6884 let qm_enc = qreg_to_num(qm);
6885 let base: u32 = 0xFFB103C0 | (sz << 18);
6886 let instr = base | ((qd_enc * 2) << 12) | (qm_enc * 2);
6887 Ok(vfp_to_thumb_bytes(instr))
6888 }
6889 ArmOp::MveDup { qd, rn, size } => {
6890 let sz = mve_size_bits(size);
6891 let qd_enc = qreg_to_num(qd);
6892 let rn_bits = reg_to_bits(rn);
6893 // VDUP.sz Qd, Rn: EEA0 0B10 variant
6894 // size encoding: 00=32, 01=16, 10=8
6895 let be = match sz {
6896 0 => 0b00u32, // 8-bit
6897 1 => 0b01, // 16-bit
6898 _ => 0b00, // 32-bit (default)
6899 };
6900 let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12) | (be << 5);
6901 Ok(vfp_to_thumb_bytes(instr))
6902 }
6903 ArmOp::MveExtractLane { rd, qn, lane, size } => {
6904 let qn_enc = qreg_to_num(qn);
6905 let rd_bits = reg_to_bits(rd);
6906 // VMOV.sz Rd, Dn[x] — extract from Q-register lane
6907 // For 32-bit: VMOV Rd, Dn — where Dn is the appropriate D-register
6908 let d_reg = qn_enc * 2 + ((*lane as u32) >> 1);
6909 let lane_in_d = (*lane as u32) & 1;
6910 let _sz = mve_size_bits(size);
6911 // VMOV Rd, Dn[x]: EE10 0B10 for 32-bit
6912 let instr: u32 = 0xEE100B10 | (d_reg << 16) | (rd_bits << 12) | (lane_in_d << 21);
6913 Ok(vfp_to_thumb_bytes(instr))
6914 }
6915 ArmOp::MveInsertLane { qd, rn, lane, size } => {
6916 let qd_enc = qreg_to_num(qd);
6917 let rn_bits = reg_to_bits(rn);
6918 let d_reg = qd_enc * 2 + ((*lane as u32) >> 1);
6919 let lane_in_d = (*lane as u32) & 1;
6920 let _sz = mve_size_bits(size);
6921 // VMOV Dn[x], Rn: EE00 0B10 for 32-bit
6922 let instr: u32 = 0xEE000B10 | (d_reg << 16) | (rn_bits << 12) | (lane_in_d << 21);
6923 Ok(vfp_to_thumb_bytes(instr))
6924 }
6925
6926 // MVE float comparisons — emit VCMP + VPSEL sequence (simplified: just VCMP)
6927 ArmOp::MveCmpEqI { qd, qn, qm, size }
6928 | ArmOp::MveCmpNeI { qd, qn, qm, size }
6929 | ArmOp::MveCmpLtS { qd, qn, qm, size }
6930 | ArmOp::MveCmpLtU { qd, qn, qm, size }
6931 | ArmOp::MveCmpGtS { qd, qn, qm, size }
6932 | ArmOp::MveCmpGtU { qd, qn, qm, size }
6933 | ArmOp::MveCmpLeS { qd, qn, qm, size }
6934 | ArmOp::MveCmpLeU { qd, qn, qm, size }
6935 | ArmOp::MveCmpGeS { qd, qn, qm, size }
6936 | ArmOp::MveCmpGeU { qd, qn, qm, size } => {
6937 // Encode as VADD (placeholder encoding — real implementation
6938 // would use VCMP + VPSEL pair)
6939 let sz = mve_size_bits(size);
6940 let base: u32 = 0xEF000840 | (sz << 20);
6941 Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6942 }
6943
6944 // f32x4 MVE arithmetic
6945 ArmOp::MveAddF32 { qd, qn, qm } => {
6946 // VADD.F32 Qd, Qn, Qm (MVE): 0xEF000D40
6947 Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6948 }
6949 ArmOp::MveSubF32 { qd, qn, qm } => {
6950 // VSUB.F32 Qd, Qn, Qm (MVE): 0xEF200D40
6951 Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF200D40, qd, qn, qm)))
6952 }
6953 ArmOp::MveMulF32 { qd, qn, qm } => {
6954 // VMUL.F32 Qd, Qn, Qm (MVE): 0xFF000D50
6955 Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xFF000D50, qd, qn, qm)))
6956 }
6957 ArmOp::MveNegF32 { qd, qm } => {
6958 let qd_enc = qreg_to_num(qd);
6959 let qm_enc = qreg_to_num(qm);
6960 // VNEG.F32 Qd, Qm: FFB907C0
6961 let instr: u32 = 0xFFB907C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6962 Ok(vfp_to_thumb_bytes(instr))
6963 }
6964 ArmOp::MveAbsF32 { qd, qm } => {
6965 let qd_enc = qreg_to_num(qd);
6966 let qm_enc = qreg_to_num(qm);
6967 // VABS.F32 Qd, Qm: FFB90740
6968 let instr: u32 = 0xFFB90740 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6969 Ok(vfp_to_thumb_bytes(instr))
6970 }
6971 ArmOp::MveCmpEqF32 { qd, qn, qm }
6972 | ArmOp::MveCmpNeF32 { qd, qn, qm }
6973 | ArmOp::MveCmpLtF32 { qd, qn, qm }
6974 | ArmOp::MveCmpLeF32 { qd, qn, qm }
6975 | ArmOp::MveCmpGtF32 { qd, qn, qm }
6976 | ArmOp::MveCmpGeF32 { qd, qn, qm } => {
6977 // Placeholder: encode as VADD.F32 (real impl needs VCMP.F32 + VPSEL)
6978 Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6979 }
6980 ArmOp::MveDupF32 { qd, rn } => {
6981 let qd_enc = qreg_to_num(qd);
6982 let rn_bits = reg_to_bits(rn);
6983 // VDUP.32 Qd, Rn (same encoding as integer VDUP.32)
6984 let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12);
6985 Ok(vfp_to_thumb_bytes(instr))
6986 }
6987 ArmOp::MveExtractLaneF32 { rd, qn, lane } => {
6988 let qn_enc = qreg_to_num(qn);
6989 let rd_bits = reg_to_bits(rd);
6990 // VMOV Rd, Sn where Sn = Q*4 + lane
6991 let s_num = qn_enc * 4 + (*lane as u32);
6992 let (vn, n) = encode_sreg(s_num);
6993 let instr: u32 = 0xEE100A10 | (vn << 16) | (rd_bits << 12) | (n << 7);
6994 Ok(vfp_to_thumb_bytes(instr))
6995 }
6996 ArmOp::MveReplaceLaneF32 { qd, rn, lane } => {
6997 let qd_enc = qreg_to_num(qd);
6998 let rn_bits = reg_to_bits(rn);
6999 // VMOV Sn, Rn where Sn = Q*4 + lane
7000 let s_num = qd_enc * 4 + (*lane as u32);
7001 let (vn, n) = encode_sreg(s_num);
7002 let instr: u32 = 0xEE000A10 | (vn << 16) | (rn_bits << 12) | (n << 7);
7003 Ok(vfp_to_thumb_bytes(instr))
7004 }
7005 ArmOp::MveDivF32 { qd, qn, qm } => {
7006 // Lane-wise: extract 4 S-regs, VDIV, insert back
7007 self.encode_thumb_mve_lane_wise_f32_binop(qd, qn, qm, 0xEE800A00)
7008 }
7009 ArmOp::MveSqrtF32 { qd, qm } => {
7010 // Lane-wise: extract 4 S-regs, VSQRT, insert back
7011 self.encode_thumb_mve_lane_wise_f32_sqrt(qd, qm)
7012 }
7013
7014 // RQ-67-VFPREACH (#1267): the AAPCS callee-saved VFP half.
7015 //
7016 // Encodings taken from llvm-mc, not derived by hand:
7017 // llvm-mc -triple=thumbv7em-none-eabi -mattr=+vfp4 -show-encoding
7018 // vpush {d8-d15} -> [0x2d,0xed,0x10,0x8b] (ED2D 8B10)
7019 // vpop {d8-d15} -> [0xbd,0xec,0x10,0x8b] (ECBD 8B10)
7020 // imm8 is 2 x the double count (0x10 = 16 = 2 x 8), and the
7021 // `8B` nibble selects the 64-bit form — `vpush {d8}` is 8B02,
7022 // which is how that field was confirmed rather than assumed.
7023 //
7024 // NOTE FOR ANYONE ADDING AN OP BELOW THIS POINT: the catch-all that
7025 // follows returns a NOP, so an op that reaches it is encoded as
7026 // nothing at all. These two arms exist ABOVE it deliberately.
7027 ArmOp::VPushCalleeSavedVfp => Ok(vec![0x2d, 0xed, 0x10, 0x8b]),
7028 ArmOp::VPopCalleeSavedVfp => Ok(vec![0xbd, 0xec, 0x10, 0x8b]),
7029
7030 // Catch-all for any remaining ops
7031 //
7032 // #615 FIXED THIS CLASS ON THE A32 PATH AND NOT HERE. Thumb-2 is
7033 // the DEFAULT target, and this arm silently encodes any unhandled
7034 // op as `NOP`. RQ-67-VFPREACH hit it directly: the two VFP
7035 // save/restore ops above compiled clean and would have emitted a
7036 // save that does nothing. Filed rather than widened in this lane —
7037 // turning it into a typed Err is a behaviour change across every
7038 // op that currently lands here, which needs its own byte-identity
7039 // evidence.
7040 other => Err(synth_core::Error::synthesis(format!(
7041 "{other:?} reached the Thumb-2 encoder with no Thumb-2 encoding — \
7042 refusing to emit a silent NOP (#1272; #615 closed this class on A32 \
7043 only, and Thumb-2 is the default target)"
7044 ))),
7045 }
7046 }
7047
7048 // === Thumb-2 VFP multi-instruction helpers ===
7049
7050 /// Encode F32 comparison as Thumb-2: VCMP.F32 + VMRS + MOVS rd,#0 + IT + MOV rd,#1
7051 fn encode_thumb_f32_compare(
7052 &self,
7053 rd: &Reg,
7054 sn: &VfpReg,
7055 sm: &VfpReg,
7056 cond_code: u32,
7057 ) -> Result<Vec<u8>> {
7058 let mut bytes = Vec::new();
7059 let rd_bits = reg_to_bits(rd);
7060
7061 // #709 (bug found under #708/#709): the `MOVS Rd,#0` below is a
7062 // FLAG-SETTING 16-bit move. Emitting it AFTER `VMRS APSR_nzcv, FPSCR`
7063 // (as the original code did) clobbered the N/Z/C/V flags the VMRS just
7064 // transferred from the VFP compare, so the following `IT<cond>` read
7065 // stale flags and every f32 comparison silently returned 0 (verified:
7066 // `flt(1.0,2.0)` → 0 on Cortex-M4F). The 619 harness never caught it
7067 // because it deliberately skipped compare EXECUTION on a false premise
7068 // (unicorn DOES model VMRS→APSR). Fix: materialize the `#0` FIRST, then
7069 // VCMP+VMRS set the flags the `IT` consumes. Instruction sizes are
7070 // unchanged (pure reorder), so the estimator↔encoder oracle (#511) is
7071 // untouched — only the byte ORDER differs.
7072
7073 // MOVS Rd, #0 (16-bit): 0010 0 Rd(3) 0000 0000 — its flag side effect
7074 // is immediately overwritten by the VMRS below.
7075 if rd_bits < 8 {
7076 let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
7077 bytes.extend_from_slice(&movs_zero.to_le_bytes());
7078 } else {
7079 // MOV.W Rd, #0 (32-bit Thumb-2)
7080 let hw1: u16 = 0xF04F;
7081 let hw2: u16 = (rd_bits as u16) << 8;
7082 bytes.extend_from_slice(&hw1.to_le_bytes());
7083 bytes.extend_from_slice(&hw2.to_le_bytes());
7084 }
7085
7086 // VCMP.F32 Sn, Sm
7087 let sn_num = vfp_sreg_to_num(sn)?;
7088 let sm_num = vfp_sreg_to_num(sm)?;
7089 let (vd, d) = encode_sreg(sn_num);
7090 let (vm, m) = encode_sreg(sm_num);
7091 let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7092 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7093
7094 // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10 (sets the flags IT consumes)
7095 bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7096
7097 // IT<cond> — If-Then for conditional MOV
7098 // IT encoding: 1011 1111 cond(4) mask(4)
7099 // mask = 0x8 for single "then" (IT)
7100 let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7101 bytes.extend_from_slice(&it.to_le_bytes());
7102
7103 // MOV Rd, #1 (16-bit, conditional due to IT): 0010 0 Rd(3) 0000 0001
7104 if rd_bits < 8 {
7105 let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7106 bytes.extend_from_slice(&mov_one.to_le_bytes());
7107 } else {
7108 // MOV.W Rd, #1 (32-bit)
7109 let hw1: u16 = 0xF04F;
7110 let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7111 bytes.extend_from_slice(&hw1.to_le_bytes());
7112 bytes.extend_from_slice(&hw2.to_le_bytes());
7113 }
7114
7115 Ok(bytes)
7116 }
7117
7118 /// Encode F32 constant load as Thumb-2: MOVW + MOVT + VMOV
7119 fn encode_thumb_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
7120 let mut bytes = Vec::new();
7121 let bits = value.to_bits();
7122 let rt: u32 = 12; // R12/IP as temp
7123
7124 // MOVW R12, #lo16
7125 // Thumb-2 MOVW: 11110 i 10 0100 imm4 | 0 imm3 Rd imm8
7126 let lo16 = bits & 0xFFFF;
7127 let imm4 = (lo16 >> 12) & 0xF;
7128 let i_bit = (lo16 >> 11) & 1;
7129 let imm3 = (lo16 >> 8) & 0x7;
7130 let imm8 = lo16 & 0xFF;
7131 let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7132 let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
7133 bytes.extend_from_slice(&hw1.to_le_bytes());
7134 bytes.extend_from_slice(&hw2.to_le_bytes());
7135
7136 // MOVT R12, #hi16
7137 let hi16 = (bits >> 16) & 0xFFFF;
7138 let imm4 = (hi16 >> 12) & 0xF;
7139 let i_bit = (hi16 >> 11) & 1;
7140 let imm3 = (hi16 >> 8) & 0x7;
7141 let imm8 = hi16 & 0xFF;
7142 let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
7143 let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
7144 bytes.extend_from_slice(&hw1.to_le_bytes());
7145 bytes.extend_from_slice(&hw2.to_le_bytes());
7146
7147 // VMOV Sd, R12
7148 let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
7149 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7150
7151 Ok(bytes)
7152 }
7153
7154 /// Encode VMOV + VCVT.F32.xS32 as Thumb-2
7155 fn encode_thumb_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7156 let mut bytes = Vec::new();
7157
7158 // VMOV Sd, Rm
7159 let vmov = encode_vmov_core_sreg(true, sd, rm)?;
7160 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7161
7162 // VCVT.F32.S32/U32 Sd, Sd. Bit 7 (op) = 1 for signed (S32), 0 for
7163 // unsigned (U32): signed = 0xEEB80AC0, unsigned = 0xEEB80A40
7164 // (GI-FPU-002: previously swapped — see the ARM32 twin).
7165 let sd_num = vfp_sreg_to_num(sd)?;
7166 let (vd, d) = encode_sreg(sd_num);
7167 let (vm, m) = encode_sreg(sd_num);
7168 let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
7169 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7170 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7171
7172 Ok(bytes)
7173 }
7174
7175 /// Encode F32 rounding pseudo-op as Thumb-2 via VCVT to integer and back
7176 /// Encode F32 rounding as Thumb-2.
7177 /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
7178 ///
7179 /// For trunc: uses VCVTR.S32.F32 (always truncates).
7180 /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant),
7181 /// then restores FPSCR.
7182 fn encode_thumb_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7183 let mut bytes = Vec::new();
7184 let sm_num = vfp_sreg_to_num(sm)?;
7185 let sd_num = vfp_sreg_to_num(sd)?;
7186 let (vd_s, d_s) = encode_sreg(sd_num);
7187 let (vm_s, m_s) = encode_sreg(sm_num);
7188
7189 if mode == 0b11 {
7190 // Trunc (toward zero): VCVTR.S32.F32 — bit[7]=1, always truncates
7191 let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7192 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7193 } else {
7194 // ceil/floor/nearest: manipulate FPSCR rounding mode
7195 let rt: u32 = 12; // R12/IP as temp
7196
7197 // VMRS R12, FPSCR
7198 let vmrs = 0xEEF10A10 | (rt << 12);
7199 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7200
7201 // BIC.W R12, R12, #(3 << 22) — clear RMode bits [23:22]
7202 // Thumb-2 modified immediate for 3<<22 = 0x00C00000:
7203 // BIC.W encoding: 11110 i 0 0001 S Rn | 0 imm3 Rd imm8
7204 // 0x00C00000 = 0x03 shifted left by 22 => Thumb mod-imm: i=0, imm3=0b101, imm8=0x03
7205 let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF); // BIC, Rn=R12
7206 let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
7207 bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7208 bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7209
7210 // ORR.W R12, R12, #(mode << 22)
7211 if mode != 0 {
7212 let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF); // ORR, Rn=R12
7213 let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
7214 bytes.extend_from_slice(&orr_hw1.to_le_bytes());
7215 bytes.extend_from_slice(&orr_hw2.to_le_bytes());
7216 }
7217
7218 // VMSR FPSCR, R12
7219 let vmsr = 0xEEE10A10 | (rt << 12);
7220 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7221
7222 // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rmode
7223 let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7224 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7225
7226 // Restore FPSCR: clear rmode bits back to nearest (default)
7227 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7228 bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7229 bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7230 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7231 }
7232
7233 // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
7234 let (vd2, d2) = encode_sreg(sd_num);
7235 let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
7236 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
7237
7238 Ok(bytes)
7239 }
7240
7241 /// Encode F32 min/max as Thumb-2: VMOV + VCMP + VMRS + IT + VMOV
7242 fn encode_thumb_f32_minmax(
7243 &self,
7244 sd: &VfpReg,
7245 sn: &VfpReg,
7246 sm: &VfpReg,
7247 is_min: bool,
7248 ) -> Result<Vec<u8>> {
7249 let mut bytes = Vec::new();
7250 let sn_num = vfp_sreg_to_num(sn)?;
7251 let sm_num = vfp_sreg_to_num(sm)?;
7252 let sd_num = vfp_sreg_to_num(sd)?;
7253
7254 // VMOV.F32 Sd, Sn
7255 let (vd, d) = encode_sreg(sd_num);
7256 let (vn, n) = encode_sreg(sn_num);
7257 let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
7258 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sn));
7259
7260 // VCMP.F32 Sn, Sm
7261 let (vm, m) = encode_sreg(sm_num);
7262 let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7263 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7264
7265 // VMRS APSR_nzcv, FPSCR
7266 bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7267
7268 // IT GT (for min) or IT MI (for max)
7269 let cond: u16 = if is_min { 0xC } else { 0x4 };
7270 let it: u16 = 0xBF00 | (cond << 4) | 0x8;
7271 bytes.extend_from_slice(&it.to_le_bytes());
7272
7273 // VMOV{cond}.F32 Sd, Sm — conditional VMOV in IT block
7274 let vmov_sm = 0xEEB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7275 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sm));
7276
7277 Ok(bytes)
7278 }
7279
7280 /// Encode F32 copysign as Thumb-2
7281 /// Encode F32 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7282 /// encoder scratch, #212), the flags, and Sd:
7283 ///
7284 /// VMOV R12, Sm ; CMP R12, #0 (N flag = the sign bit)
7285 /// VABS.F32 Sd, Sn (magnitude, sign cleared)
7286 /// IT MI ; VNEG.F32(MI) Sd, Sd
7287 ///
7288 /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7289 /// The R12 capture happens BEFORE Sd is written, so Sd aliasing Sn or Sm
7290 /// is safe. (The previous sequence staged the magnitude through R0 —
7291 /// clobbering a live allocator-owned value, the #615 class; caught while
7292 /// composing the F64 twin for #369.)
7293 fn encode_thumb_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7294 let mut bytes = Vec::new();
7295
7296 // VMOV R12, Sm (sign source bits)
7297 bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
7298 false,
7299 sm,
7300 &Reg::R12,
7301 )?));
7302 // CMP.W R12, #0 — N = bit31 (the sign, incl. -0.0 / -NaN).
7303 bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7304 bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7305 // VABS.F32 Sd, Sn
7306 let sd_num = vfp_sreg_to_num(sd)?;
7307 let sn_num = vfp_sreg_to_num(sn)?;
7308 let (vd, d) = encode_sreg(sd_num);
7309 let (vn, n) = encode_sreg(sn_num);
7310 let vabs = 0xEEB00AC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7311 bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7312 // IT MI ; VNEG.F32(MI) Sd, Sd
7313 bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7314 let vneg = 0xEEB10A40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7315 bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7316
7317 Ok(bytes)
7318 }
7319
7320 /// Encode F64 comparison as Thumb-2: VCMP.F64 + VMRS + MOV #0 + IT + MOV #1
7321 fn encode_thumb_f64_compare(
7322 &self,
7323 rd: &Reg,
7324 dn: &VfpReg,
7325 dm: &VfpReg,
7326 cond_code: u32,
7327 ) -> Result<Vec<u8>> {
7328 let mut bytes = Vec::new();
7329 let rd_bits = reg_to_bits(rd);
7330
7331 // #712-class fix (found at f64-phase-2 wiring, #369): the 16-bit
7332 // `MOVS Rd,#0` is FLAG-SETTING. The original order emitted it AFTER
7333 // `VMRS APSR_nzcv, FPSCR`, clobbering the N/Z/C/V flags the VMRS just
7334 // transferred, so the following `IT<cond>` read stale flags and every
7335 // f64 comparison silently returned 0 — the exact bug the f32 compare
7336 // encoder shipped with and #712 fixed. Same fix: materialize the `#0`
7337 // FIRST (its flag side effect is overwritten by the VMRS), then
7338 // VCMP+VMRS set the flags the IT consumes. Pure reorder — sizes
7339 // unchanged.
7340
7341 // MOVS Rd, #0
7342 if rd_bits < 8 {
7343 let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
7344 bytes.extend_from_slice(&movs_zero.to_le_bytes());
7345 } else {
7346 let hw1: u16 = 0xF04F;
7347 let hw2: u16 = (rd_bits as u16) << 8;
7348 bytes.extend_from_slice(&hw1.to_le_bytes());
7349 bytes.extend_from_slice(&hw2.to_le_bytes());
7350 }
7351
7352 // VCMP.F64 Dn, Dm
7353 let dn_num = vfp_dreg_to_num(dn)?;
7354 let dm_num = vfp_dreg_to_num(dm)?;
7355 let (vd, d) = encode_dreg(dn_num);
7356 let (vm, m) = encode_dreg(dm_num);
7357 let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7358 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7359
7360 // VMRS APSR_nzcv, FPSCR (sets the flags the IT consumes)
7361 bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7362
7363 // IT<cond>
7364 let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7365 bytes.extend_from_slice(&it.to_le_bytes());
7366
7367 // MOV Rd, #1
7368 if rd_bits < 8 {
7369 let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7370 bytes.extend_from_slice(&mov_one.to_le_bytes());
7371 } else {
7372 let hw1: u16 = 0xF04F;
7373 let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7374 bytes.extend_from_slice(&hw1.to_le_bytes());
7375 bytes.extend_from_slice(&hw2.to_le_bytes());
7376 }
7377
7378 Ok(bytes)
7379 }
7380
7381 /// Encode F64 constant load as Thumb-2: MOVW+MOVT (lo32 into R0) + MOVW+MOVT (hi32 into R12) + VMOV Dd, R0, R12
7382 fn encode_thumb_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
7383 let mut bytes = Vec::new();
7384 let bits = value.to_bits();
7385 let lo32 = bits as u32;
7386 let hi32 = (bits >> 32) as u32;
7387
7388 // MOVW R0, #lo16(lo32)
7389 let lo16 = lo32 & 0xFFFF;
7390 bytes.extend_from_slice(&self.encode_thumb32_movw_raw(0, lo16)?);
7391
7392 // MOVT R0, #hi16(lo32)
7393 let hi16 = (lo32 >> 16) & 0xFFFF;
7394 bytes.extend_from_slice(&self.encode_thumb32_movt_raw(0, hi16)?);
7395
7396 // MOVW R12, #lo16(hi32)
7397 let lo16 = hi32 & 0xFFFF;
7398 bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
7399
7400 // MOVT R12, #hi16(hi32)
7401 let hi16 = (hi32 >> 16) & 0xFFFF;
7402 bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
7403
7404 // VMOV Dd, R0, R12
7405 let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
7406 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7407
7408 Ok(bytes)
7409 }
7410
7411 /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as Thumb-2
7412 /// Encode i32 → f64 conversion as Thumb-2. The integer stages through the
7413 /// DESTINATION's own low S-alias (`S(2d)`) — allocator-owned by
7414 /// definition — never S0 (which may hold a live value; the previous
7415 /// pseudo-op's S0 staging was the #615 class). Also fixes the SWAPPED
7416 /// signed/unsigned VCVT bases (bit7 = 1 is SIGNED — the same swap the f32
7417 /// twin had; latent here because f64.convert_i32_* was decode-dropped
7418 /// until #369): clang-verified vcvt.f64.s32 d1,s2 = eeb8 1bc1,
7419 /// vcvt.f64.u32 d1,s2 = eeb8 1b41.
7420 fn encode_thumb_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7421 let dd_num = vfp_dreg_to_num(dd)?;
7422 if dd_num > 7 {
7423 return Err(synth_core::Error::synthesis(format!(
7424 "F64ConvertI32: destination {dd:?} has no S-register alias \
7425 (D8..D15) — the selector allocates only D0..D7"
7426 )));
7427 }
7428 let mut bytes = Vec::new();
7429
7430 // VMOV S(2d), Rm — stage the integer in the destination's low word.
7431 let (vn_s, n_s) = encode_sreg(2 * dd_num);
7432 let rt = reg_to_bits(rm);
7433 let vmov = 0xEE000A10 | (vn_s << 16) | (rt << 12) | (n_s << 7);
7434 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7435
7436 // VCVT.F64.S32/U32 Dd, S(2d)
7437 let (vd, d) = encode_dreg(dd_num);
7438 let (vm, m) = encode_sreg(2 * dd_num);
7439 let base = if signed { 0xEEB80BC0 } else { 0xEEB80B40 };
7440 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7441 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7442
7443 Ok(bytes)
7444 }
7445
7446 /// Encode VCVT.F64.F32 Dd, Sm as Thumb-2
7447 fn encode_thumb_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7448 let dd_num = vfp_dreg_to_num(dd)?;
7449 let sm_num = vfp_sreg_to_num(sm)?;
7450 let (vd, d) = encode_dreg(dd_num);
7451 let (vm, m) = encode_sreg(sm_num);
7452
7453 let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7454 Ok(vfp_to_thumb_bytes(vcvt))
7455 }
7456
7457 /// Encode VCVT.F32.F64 Sd, Dm (f32.demote_f64) as Thumb-2 — single
7458 /// instruction, round-to-nearest-even per FPSCR default, exactly WASM
7459 /// §4.3.3 demote (clang-verified: vcvt.f32.f64 s1,d2 = eef7 0bc2).
7460 fn encode_thumb_f32_demote_f64(&self, sd: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7461 let sd_num = vfp_sreg_to_num(sd)?;
7462 let dm_num = vfp_dreg_to_num(dm)?;
7463 let (vd, d) = encode_sreg(sd_num);
7464 let (vm, m) = encode_dreg(dm_num);
7465
7466 let vcvt = 0xEEB70BC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7467 Ok(vfp_to_thumb_bytes(vcvt))
7468 }
7469
7470 /// Encode f64 → i32 truncation as Thumb-2 (round-toward-zero VCVT).
7471 ///
7472 /// The 32-bit result stages through the SOURCE's own low S-alias, `S(2m)`,
7473 /// clobbering half of an operand the selector has already popped. The
7474 /// overlapping write is well-defined: VCVT reads its source operand before
7475 /// writing (compilers emit `vcvt.f32.f64 s0, d0` routinely).
7476 ///
7477 /// # The one precondition, and who actually provides it
7478 ///
7479 /// `dm` must be a DEAD TEMP — never a pinned param/local home. That is the
7480 /// whole safety argument, and it is worth naming the guarantor precisely
7481 /// (#946): **`select_with_stack`** provides it, by copying a home into a
7482 /// fresh D-temp first. Visible in the shipped output for
7483 /// `(func (param f64) (result i32) (i32.trunc_f64_s (local.get 0)))`:
7484 ///
7485 /// ```text
7486 /// vmov r1, r2, d0 ; read the param out of its AAPCS-VFP home D0
7487 /// vmov d1, r1, r2 ; ...into a fresh D-temp
7488 /// vcvt.s32.f64 s2, d1 ; convert from the TEMP, staging into its own S2
7489 /// ```
7490 ///
7491 /// `InstructionSelector::select` / `select_default` do NOT provide it —
7492 /// `alloc_vfp_dreg` is a bare round-robin `(n + 1) % 16` with no liveness
7493 /// or home test. That path is not reachable from `synth compile`
7494 /// (`arm_backend.rs` calls `select_with_stack` exclusively; the only
7495 /// non-test caller of `select` is `examples/compile_add.rs`), so this is
7496 /// not a live miscompile — but a caller reaching that `pub` API directly
7497 /// gets no such guarantee.
7498 ///
7499 /// # What this deliberately does NOT claim
7500 ///
7501 /// An earlier version of this comment said the staging register is "never
7502 /// S0, which may hold an unrelated live value (the #615 class)". **That is
7503 /// false**, and measurably so: for
7504 /// `(func (result i32) (i32.trunc_f64_s (f64.const 3.7)))` the shipped
7505 /// compiler emits `vcvt.s32.f64 s0, d0`.
7506 ///
7507 /// It is also unnecessary. S0 is only dangerous as an *unrelated* scratch;
7508 /// here it is always the low half of `dm` itself, which the precondition
7509 /// above already makes dead. Naming a guard the code does not have — and
7510 /// does not need — invites a future reader to lean on it. The dead-temp
7511 /// precondition is the only thing holding this up.
7512 fn encode_thumb_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7513 let dm_num = vfp_dreg_to_num(dm)?;
7514 if dm_num > 7 {
7515 return Err(synth_core::Error::synthesis(format!(
7516 "I32TruncF64: source {dm:?} has no S-register alias \
7517 (D8..D15) — the selector allocates only D0..D7"
7518 )));
7519 }
7520 let mut bytes = Vec::new();
7521
7522 // VCVT.S32/U32.F64 S(2m), Dm (clang-verified:
7523 // vcvt.s32.f64 s1,d2 = eefd 0bc2 ; vcvt.u32.f64 s1,d2 = eefc 0bc2)
7524 let (vm, m) = encode_dreg(dm_num);
7525 let (vd_s, d_s) = encode_sreg(2 * dm_num);
7526 let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
7527 let vcvt = base | (d_s << 22) | (vd_s << 12) | (m << 5) | vm;
7528 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7529
7530 // VMOV Rd, S(2m)
7531 let rt = reg_to_bits(rd);
7532 let vmov = 0xEE100A10 | (vd_s << 16) | (rt << 12) | (d_s << 7);
7533 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7534
7535 Ok(bytes)
7536 }
7537
7538 /// Encode F64 rounding as a SINGLE Thumb-2 VRINT (FPv5 / cortex-m7dp).
7539 /// `mode` keeps the legacy FPSCR-RMode numbering of the callers —
7540 /// 0b00=nearest(ties-to-even)→VRINTN, 0b01=+inf(ceil)→VRINTP,
7541 /// 0b10=-inf(floor)→VRINTM, 0b11=zero(trunc)→VRINTZ — but the rounding
7542 /// mode is now ENCODED in the instruction, not smuggled through FPSCR.
7543 /// (The previous pseudo-op round-tripped through a 32-bit integer in S0:
7544 /// wrong for |x| >= 2^31, NaN/±inf collapsed to 0, -0.0 lost, and it
7545 /// CLOBBERED S0/R12 behind the allocator's back — the #615 class.)
7546 /// VRINT quietens an sNaN and preserves the sign of ±0.0/NaN per IEEE 754
7547 /// roundToIntegral, which is exactly WASM Core §4.3.3 f64.ceil/floor/
7548 /// trunc/nearest. VRINTN/P/M live in the FE "always-execute" space (never
7549 /// IT-conditional; none of these sequences emits them inside an IT block).
7550 fn encode_thumb_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7551 let dd_num = vfp_dreg_to_num(dd)?;
7552 let dm_num = vfp_dreg_to_num(dm)?;
7553 let (vd, d) = encode_dreg(dd_num);
7554 let (vm, m) = encode_dreg(dm_num);
7555 // clang-verified bases (thumbv7em, fpv5-d16):
7556 // vrintn.f64 d1,d2 = feb9 1b42 ; vrintp = feba 1b42
7557 // vrintm.f64 d1,d2 = febb 1b42 ; vrintz = eeb6 1bc2
7558 let base: u32 = match mode {
7559 0b00 => 0xFEB90B40, // VRINTN.F64 (round to nearest, ties to even)
7560 0b01 => 0xFEBA0B40, // VRINTP.F64 (round toward +inf)
7561 0b10 => 0xFEBB0B40, // VRINTM.F64 (round toward -inf)
7562 _ => 0xEEB60BC0, // VRINTZ.F64 (round toward zero)
7563 };
7564 Ok(vfp_to_thumb_bytes(
7565 base | (d << 22) | (vd << 12) | (m << 5) | vm,
7566 ))
7567 }
7568
7569 /// Encode F64 min/max as Thumb-2 with WASM Core §4.3.3 semantics:
7570 ///
7571 /// VCMP.F64 Dn, Dm ; VMRS APSR_nzcv, FPSCR
7572 /// VMINNM.F64/VMAXNM.F64 Dd, Dn, Dm (FPv5; -0.0 < +0.0 ordered)
7573 /// IT VS ; VADD.F64(VS) Dd, Dn, Dm (unordered ⇒ NaN-propagating)
7574 ///
7575 /// VMINNM/VMAXNM alone are IEEE minNum/maxNum, which return the NUMBER
7576 /// when exactly one operand is NaN — WASM requires NaN. The VS-guarded
7577 /// VADD overwrites the result with a quiet NaN whenever the compare was
7578 /// unordered (either operand NaN); on the ordered path VMINNM/VMAXNM
7579 /// order -0.0 below +0.0, matching WASM's min(+0,-0) = -0 / max = +0.
7580 /// Clobbers ONLY Dd and the flags (the previous pseudo-op's ordered IT
7581 /// GT/MI select returned the WRONG operand for NaN and ±0 mixes).
7582 ///
7583 /// Ok-or-Err: `dd` must not alias `dn`/`dm` — the VS fix-up reads them
7584 /// AFTER VMINNM wrote `dd` (the selector always allocates a fresh
7585 /// destination while both sources are still marked live).
7586 fn encode_thumb_f64_minmax(
7587 &self,
7588 dd: &VfpReg,
7589 dn: &VfpReg,
7590 dm: &VfpReg,
7591 is_min: bool,
7592 ) -> Result<Vec<u8>> {
7593 if dd == dn || dd == dm {
7594 return Err(synth_core::Error::synthesis(format!(
7595 "F64{}: destination {dd:?} aliases a source ({dn:?},{dm:?}) — \
7596 the unordered NaN fix-up would read a clobbered operand \
7597 (compiler bug: the selector must allocate a fresh D-temp)",
7598 if is_min { "Min" } else { "Max" },
7599 )));
7600 }
7601 let mut bytes = Vec::new();
7602 let dd_num = vfp_dreg_to_num(dd)?;
7603 let dn_num = vfp_dreg_to_num(dn)?;
7604 let dm_num = vfp_dreg_to_num(dm)?;
7605 let (vd, d) = encode_dreg(dd_num);
7606 let (vn, n) = encode_dreg(dn_num);
7607 let (vm, m) = encode_dreg(dm_num);
7608
7609 // VCMP.F64 Dn, Dm (clang-verified: vcmp.f64 d2,d3 = eeb4 2b43)
7610 let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7611 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7612 // VMRS APSR_nzcv, FPSCR
7613 bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7614 // VMINNM.F64 / VMAXNM.F64 Dd, Dn, Dm (clang-verified:
7615 // vminnm.f64 d1,d2,d3 = fe82 1b43 ; vmaxnm = fe82 1b03)
7616 let base: u32 = if is_min { 0xFE800B40 } else { 0xFE800B00 };
7617 let vnm = base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7618 bytes.extend_from_slice(&vfp_to_thumb_bytes(vnm));
7619 // IT VS (unordered ⇒ at least one NaN operand)
7620 bytes.extend_from_slice(&0xBF68_u16.to_le_bytes());
7621 // VADD.F64(VS) Dd, Dn, Dm — NaN + x propagates a quiet NaN
7622 let vadd = 0xEE300B00 | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7623 bytes.extend_from_slice(&vfp_to_thumb_bytes(vadd));
7624
7625 Ok(bytes)
7626 }
7627
7628 /// Encode F64 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7629 /// encoder scratch, #212), the flags, and Dd:
7630 ///
7631 /// VMOV R12, S(2m+1) (high word of the SIGN source Dm)
7632 /// CMP R12, #0 (N flag = the sign bit)
7633 /// VABS.F64 Dd, Dn (magnitude, sign cleared)
7634 /// IT MI ; VNEG.F64(MI) Dd, Dd
7635 ///
7636 /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7637 /// The R12 capture happens BEFORE Dd is written, so Dd aliasing Dn or Dm
7638 /// is safe. (The previous pseudo-op clobbered R0/R1/R2 behind the
7639 /// allocator's back — the #615 class.)
7640 fn encode_thumb_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7641 let dm_num = vfp_dreg_to_num(dm)?;
7642 if dm_num > 7 {
7643 return Err(synth_core::Error::synthesis(format!(
7644 "F64Copysign: sign source {dm:?} has no S-register alias \
7645 (D8..D15) — the selector allocates only D0..D7"
7646 )));
7647 }
7648 let mut bytes = Vec::new();
7649 // VMOV R12, S(2m+1) — the sign source's high word.
7650 let (vn_s, n_s) = encode_sreg(2 * dm_num + 1);
7651 let vmov = 0xEE100A10 | (vn_s << 16) | (12 << 12) | (n_s << 7);
7652 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7653 // CMP R12, #0 (T2: CMP.W R12, #0) — N = bit31 of the sign word.
7654 bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7655 bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7656 // VABS.F64 Dd, Dn
7657 let dd_num = vfp_dreg_to_num(dd)?;
7658 let dn_num = vfp_dreg_to_num(dn)?;
7659 let (vd, d) = encode_dreg(dd_num);
7660 let (vn, n) = encode_dreg(dn_num);
7661 let vabs = 0xEEB00BC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7662 bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7663 // IT MI ; VNEG.F64(MI) Dd, Dd
7664 bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7665 let vneg = 0xEEB10B40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7666 bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7667
7668 Ok(bytes)
7669 }
7670
7671 /// Encode VCVT.S32/U32.F32 + VMOV as Thumb-2
7672 fn encode_thumb_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7673 let mut bytes = Vec::new();
7674
7675 let sm_num = vfp_sreg_to_num(sm)?;
7676 let (vd, d) = encode_sreg(sm_num);
7677 let (vm, m) = encode_sreg(sm_num);
7678 let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
7679 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7680 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7681
7682 // VMOV Rd, Sm
7683 let vmov = encode_vmov_core_sreg(false, sm, rd)?;
7684 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7685
7686 Ok(bytes)
7687 }
7688
7689 // === Thumb-2 32-bit encoding helpers ===
7690
7691 /// Encode Thumb-2 32-bit ADD with immediate
7692 fn encode_thumb32_add(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7693 let rd_bits = reg_to_bits(rd);
7694 let rn_bits = reg_to_bits(rn);
7695
7696 // The `i:imm3:imm8` field is split the same way for both forms.
7697 let i_bit = (imm >> 11) & 1;
7698 let imm3 = (imm >> 8) & 0x7;
7699 let imm8 = imm & 0xFF;
7700
7701 let hw1_base = if imm <= 0xFF {
7702 // ADD.W (T3): the field is a ThumbExpandImm modified immediate. For
7703 // imm <= 0xFF (i:imm3 = 0000) it is the zero-extended byte, which is
7704 // correct — keep this form so existing encodings stay bit-identical.
7705 0xF100
7706 } else if imm <= 0xFFF {
7707 // ADDW (T4): a PLAIN 12-bit immediate (0..4095) — no ThumbExpandImm.
7708 // This is what makes `add sp, sp, #frame` correct for frame sizes
7709 // >= 256, which ADD.W (T3) would silently mis-encode (e.g. #256 -> #0).
7710 0xF200
7711 } else {
7712 return Err(synth_core::Error::synthesis(
7713 "ADD immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7714 ));
7715 };
7716
7717 let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7718 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7719
7720 let mut bytes = hw1.to_le_bytes().to_vec();
7721 bytes.extend_from_slice(&hw2.to_le_bytes());
7722 Ok(bytes)
7723 }
7724
7725 /// Encode Thumb-2 32-bit SUB with immediate
7726 fn encode_thumb32_sub(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7727 let rd_bits = reg_to_bits(rd);
7728 let rn_bits = reg_to_bits(rn);
7729
7730 let i_bit = (imm >> 11) & 1;
7731 let imm3 = (imm >> 8) & 0x7;
7732 let imm8 = imm & 0xFF;
7733
7734 let hw1_base = if imm <= 0xFF {
7735 // SUB.W (T3) modified immediate — correct for the zero-extended byte
7736 // (imm <= 0xFF). Kept bit-identical for existing encodings.
7737 0xF1A0
7738 } else if imm <= 0xFFF {
7739 // SUBW (T4): plain 12-bit immediate (0..4095). Makes
7740 // `sub sp, sp, #frame` correct for frame sizes >= 256.
7741 0xF2A0
7742 } else {
7743 return Err(synth_core::Error::synthesis(
7744 "SUB immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7745 ));
7746 };
7747
7748 let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7749 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7750
7751 let mut bytes = hw1.to_le_bytes().to_vec();
7752 bytes.extend_from_slice(&hw2.to_le_bytes());
7753 Ok(bytes)
7754 }
7755
7756 /// Encode Thumb-2 32-bit ADDS with immediate (sets flags)
7757 fn encode_thumb32_adds(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7758 let rd_bits = reg_to_bits(rd);
7759 let rn_bits = reg_to_bits(rn);
7760
7761 // ADDS.W (flag-setting) has only the modified-immediate form — error on
7762 // an un-encodable value rather than silently add the wrong constant.
7763 let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7764 synth_core::Error::synthesis(
7765 "ADDS immediate is not a valid ThumbExpandImm — materialize into a register",
7766 )
7767 })?;
7768 let i_bit = (field >> 11) & 1;
7769 let imm3 = (field >> 8) & 0x7;
7770 let imm8 = field & 0xFF;
7771
7772 // ADDS.W Rd, Rn, #imm (with S=1)
7773 // First halfword: 1111 0 i 0 1000 1 Rn = F110 | i<<10 | Rn
7774 let hw1: u16 = (0xF110 | (i_bit << 10) | rn_bits) as u16;
7775 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7776
7777 let mut bytes = hw1.to_le_bytes().to_vec();
7778 bytes.extend_from_slice(&hw2.to_le_bytes());
7779 Ok(bytes)
7780 }
7781
7782 /// Encode Thumb-2 32-bit SUBS with immediate (sets flags)
7783 fn encode_thumb32_subs(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7784 let rd_bits = reg_to_bits(rd);
7785 let rn_bits = reg_to_bits(rn);
7786
7787 // SUBS.W (flag-setting) has only the modified-immediate form — error on
7788 // an un-encodable value rather than silently subtract the wrong constant.
7789 let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7790 synth_core::Error::synthesis(
7791 "SUBS immediate is not a valid ThumbExpandImm — materialize into a register",
7792 )
7793 })?;
7794 let i_bit = (field >> 11) & 1;
7795 let imm3 = (field >> 8) & 0x7;
7796 let imm8 = field & 0xFF;
7797
7798 // SUBS.W Rd, Rn, #imm (with S=1)
7799 // First halfword: 1111 0 i 0 1101 1 Rn = F1B0 | i<<10 | Rn
7800 let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7801 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7802
7803 let mut bytes = hw1.to_le_bytes().to_vec();
7804 bytes.extend_from_slice(&hw2.to_le_bytes());
7805 Ok(bytes)
7806 }
7807
7808 /// Encode Thumb-2 32-bit MOVW (16-bit immediate)
7809 ///
7810 /// # Contract (Verus-style)
7811 /// ```text
7812 /// requires rd <= R14
7813 /// ensures result.len() == 4
7814 /// ensures (imm & 0xFFFF) can be reconstructed from the encoding
7815 /// ```
7816 fn encode_thumb32_movw(&self, rd: &Reg, imm: u32) -> Result<Vec<u8>> {
7817 let rd_bits = reg_to_bits(rd);
7818 reg_bits_checked(rd_bits)?;
7819 let imm16 = imm & 0xFFFF;
7820
7821 // MOVW Rd, #imm16
7822 // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7823 let imm4 = (imm16 >> 12) & 0xF;
7824 let i_bit = (imm16 >> 11) & 1;
7825 let imm3 = (imm16 >> 8) & 0x7;
7826 let imm8 = imm16 & 0xFF;
7827
7828 let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7829 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7830
7831 let mut bytes = hw1.to_le_bytes().to_vec();
7832 bytes.extend_from_slice(&hw2.to_le_bytes());
7833 encoding_contracts::verify_thumb32(&bytes);
7834 Ok(bytes)
7835 }
7836
7837 /// Encode Thumb-2 32-bit shift with immediate
7838 ///
7839 /// # Contract (Verus-style)
7840 /// ```text
7841 /// requires rd <= R14, rm <= R14
7842 /// ensures result.len() == 4
7843 /// ```
7844 fn encode_thumb32_shift(
7845 &self,
7846 rd: &Reg,
7847 rm: &Reg,
7848 shift: u32,
7849 shift_type: u8,
7850 ) -> Result<Vec<u8>> {
7851 let rd_bits = reg_to_bits(rd);
7852 let rm_bits = reg_to_bits(rm);
7853 reg_bits_checked(rd_bits)?;
7854 reg_bits_checked(rm_bits)?;
7855 let imm5 = shift & 0x1F;
7856 let imm2 = imm5 & 0x3;
7857 let imm3 = (imm5 >> 2) & 0x7;
7858
7859 // MOV.W Rd, Rm, <shift> #imm
7860 // EA4F 0 imm3 Rd imm2 type Rm
7861 let hw1: u16 = 0xEA4F;
7862 let hw2: u16 =
7863 ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | ((shift_type as u32) << 4) | rm_bits)
7864 as u16;
7865
7866 let mut bytes = hw1.to_le_bytes().to_vec();
7867 bytes.extend_from_slice(&hw2.to_le_bytes());
7868 Ok(bytes)
7869 }
7870
7871 /// Encode Thumb-2 32-bit shift by register
7872 /// Encoding: 11111010 0xx0 Rn | 1111 Rd 0000 Rm
7873 /// shift_type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
7874 fn encode_thumb32_shift_reg(
7875 &self,
7876 rd: &Reg,
7877 rn: &Reg,
7878 rm: &Reg,
7879 shift_type: u8,
7880 ) -> Result<Vec<u8>> {
7881 let rd_bits = reg_to_bits(rd);
7882 let rn_bits = reg_to_bits(rn);
7883 let rm_bits = reg_to_bits(rm);
7884
7885 // hw1: 1111 1010 0xx0 Rn
7886 let hw1: u16 = (0xFA00 | ((shift_type as u32) << 5) | rn_bits) as u16;
7887 // hw2: 1111 Rd 0000 Rm
7888 let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
7889
7890 let mut bytes = hw1.to_le_bytes().to_vec();
7891 bytes.extend_from_slice(&hw2.to_le_bytes());
7892 Ok(bytes)
7893 }
7894
7895 /// Encode Thumb-2 32-bit CMP with immediate
7896 fn encode_thumb32_cmp_imm(&self, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7897 let rn_bits = reg_to_bits(rn);
7898
7899 // CMP.W has only the modified-immediate form (no plain-imm12 like ADDW),
7900 // so an un-encodable immediate MUST be materialized into a register by
7901 // the selector. Error rather than silently compare the wrong constant.
7902 let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7903 synth_core::Error::synthesis(
7904 "CMP immediate is not a valid ThumbExpandImm — materialize into a register",
7905 )
7906 })?;
7907 let i_bit = (field >> 11) & 1;
7908 let imm3 = (field >> 8) & 0x7;
7909 let imm8 = field & 0xFF;
7910
7911 // CMP.W Rn, #imm
7912 let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7913 let hw2: u16 = ((imm3 << 12) | 0x0F00 | imm8) as u16;
7914
7915 let mut bytes = hw1.to_le_bytes().to_vec();
7916 bytes.extend_from_slice(&hw2.to_le_bytes());
7917 Ok(bytes)
7918 }
7919
7920 /// #372/#382: resolve the base register AND residual immediate offset for an
7921 /// `I64Ldr`/`I64Str` whose address may carry an index register. Returns
7922 /// `(base, low_offset)`; the caller accesses the halves at `[base,
7923 /// #low_offset]` and `[base, #low_offset + 4]`.
7924 ///
7925 /// - Frame access (no `offset_reg`, e.g. a spilled local at `[SP, #off]`):
7926 /// returns `(addr.base, off)` and emits NOTHING — byte-identical.
7927 /// - Memory access (`reg_imm(R11, addr, offset)` = `R11 + addr + offset`)
7928 /// with `offset + 4 <= 0xFFF`: emits `ADD.W ip, base, index` and returns
7929 /// `(ip, offset)`, folding `offset`/`offset+4` into the halves' imm12.
7930 /// Byte-identical to the pre-#382 (#372) behavior.
7931 /// - Memory access with `offset + 4 > 0xFFF`: the imm12 form cannot hold the
7932 /// high half's offset, so `encode_thumb32_ldr`'s `check_ldst_imm12` (#259)
7933 /// rightly refused it and the WHOLE function was skipped (#382). Instead
7934 /// MATERIALIZE the offset into the base: `ADD ip, index, #offset` (against
7935 /// the read-only INDEX register, so `encode_thumb32_add_imm` never trips its
7936 /// `rd==rn==R12` alias trap), then `ADD.W ip, ip, base` (+ R11), and return
7937 /// `(ip, 0)` so the halves use `[ip, #0]` / `[ip, #4]`.
7938 ///
7939 /// The effective address is fully materialized into `ip` BEFORE the halves
7940 /// are accessed, so an `rdlo` aliasing the index register is safe.
7941 ///
7942 /// RQ-63-ARMI64OFF (#1165): the wasm memarg is unsigned and reaches the
7943 /// encoder through the selector's `as i32` cast, so a memarg `>= 2^31`
7944 /// arrives NEGATIVE. It is read back as `u32` (lossless) and the
7945 /// materialization carries the full 32 bits — `base + index + offset (mod
7946 /// 2^32)`, the arithmetic the Thumb-2 word path (`encode_thumb32_add_imm`)
7947 /// and the software bounds guard already use for the same memarg. This
7948 /// used to CLAMP a negative offset to 0, so `i64.load offset=0xfffffff8`
7949 /// silently read `[R11 + addr]` — a wrong address, and a divergence from
7950 /// the i32 form of the same memarg. A FRAME access (no index) with an
7951 /// out-of-range or negative offset is refused by `check_ldst_imm12` in the
7952 /// halves — loud, never clamped.
7953 fn i64_effective_base(&self, bytes: &mut Vec<u8>, addr: &MemAddr) -> Result<(Reg, u32)> {
7954 let offset = addr.offset as u32;
7955 match addr.offset_reg {
7956 Some(idx) => {
7957 let ip = Reg::R12;
7958 // The high half sits at +4, which must itself fit imm12.
7959 if offset > 0xFFB {
7960 // Large static offset (#382): fold it (and R11) into ip so the
7961 // imm12 halves stay in range instead of skipping the function.
7962 // ADD ip, index, #offset (index != ip → no add_imm alias trap)
7963 bytes.extend_from_slice(&self.encode_thumb32_add_imm(&ip, &idx, offset)?);
7964 // ADD.W ip, ip, base (+ R11)
7965 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
7966 reg_to_bits(&ip),
7967 reg_to_bits(&ip),
7968 reg_to_bits(&addr.base),
7969 )?);
7970 Ok((ip, 0))
7971 } else {
7972 // ADD.W ip, addr.base, idx (Thumb-2, byte-verified vs as)
7973 let hw1: u16 = 0xEB00 | reg_to_bits(&addr.base) as u16;
7974 let hw2: u16 = 0x0C00 | reg_to_bits(&idx) as u16;
7975 bytes.extend_from_slice(&hw1.to_le_bytes());
7976 bytes.extend_from_slice(&hw2.to_le_bytes());
7977 Ok((ip, offset))
7978 }
7979 }
7980 None => Ok((addr.base, offset)),
7981 }
7982 }
7983
7984 /// Encode Thumb-2 32-bit LDR
7985 fn encode_thumb32_ldr(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7986 let rd_bits = reg_to_bits(rd);
7987 let base_bits = reg_to_bits(base);
7988
7989 // LDR.W Rd, [Rn, #imm12]
7990 check_ldst_imm12(offset)?;
7991 let hw1: u16 = (0xF8D0 | base_bits) as u16;
7992 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7993
7994 let mut bytes = hw1.to_le_bytes().to_vec();
7995 bytes.extend_from_slice(&hw2.to_le_bytes());
7996 Ok(bytes)
7997 }
7998
7999 /// Encode Thumb-2 32-bit STR
8000 fn encode_thumb32_str(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8001 let rd_bits = reg_to_bits(rd);
8002 let base_bits = reg_to_bits(base);
8003
8004 // STR.W Rd, [Rn, #imm12]
8005 check_ldst_imm12(offset)?;
8006 let hw1: u16 = (0xF8C0 | base_bits) as u16;
8007 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8008
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 LDR with register offset: LDR.W Rd, [Rn, Rm]
8015 fn encode_thumb32_ldr_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8016 let rd_bits = reg_to_bits(rd);
8017 let base_bits = reg_to_bits(base);
8018 let rm_bits = reg_to_bits(offset_reg);
8019
8020 // LDR.W Rd, [Rn, Rm, LSL #0]
8021 // Encoding: 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
8022 // imm2 = 00 for no shift (LSL #0)
8023 let hw1: u16 = (0xF850 | base_bits) as u16;
8024 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8025
8026 let mut bytes = hw1.to_le_bytes().to_vec();
8027 bytes.extend_from_slice(&hw2.to_le_bytes());
8028 Ok(bytes)
8029 }
8030
8031 /// Encode Thumb-2 32-bit STR with register offset: STR.W Rd, [Rn, Rm]
8032 fn encode_thumb32_str_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8033 let rd_bits = reg_to_bits(rd);
8034 let base_bits = reg_to_bits(base);
8035 let rm_bits = reg_to_bits(offset_reg);
8036
8037 // STR.W Rd, [Rn, Rm, LSL #0]
8038 // Encoding: 1111 1000 0100 Rn | Rt 0000 00 imm2 Rm
8039 // imm2 = 00 for no shift (LSL #0)
8040 let hw1: u16 = (0xF840 | base_bits) as u16;
8041 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8042
8043 let mut bytes = hw1.to_le_bytes().to_vec();
8044 bytes.extend_from_slice(&hw2.to_le_bytes());
8045 Ok(bytes)
8046 }
8047
8048 // === Sub-word load/store Thumb-2 encoding helpers ===
8049
8050 /// Encode Thumb-2 32-bit LDRB with immediate: LDRB.W Rd, [Rn, #imm12]
8051 fn encode_thumb32_ldrb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8052 let rd_bits = reg_to_bits(rd);
8053 let base_bits = reg_to_bits(base);
8054 // LDRB.W Rd, [Rn, #imm12]: 1111 1000 1001 Rn | Rt imm12
8055 check_ldst_imm12(offset)?;
8056 let hw1: u16 = (0xF890 | base_bits) as u16;
8057 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8058 let mut bytes = hw1.to_le_bytes().to_vec();
8059 bytes.extend_from_slice(&hw2.to_le_bytes());
8060 Ok(bytes)
8061 }
8062
8063 /// Encode Thumb-2 32-bit LDRB with register: LDRB.W Rd, [Rn, Rm]
8064 fn encode_thumb32_ldrb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8065 let rd_bits = reg_to_bits(rd);
8066 let base_bits = reg_to_bits(base);
8067 let rm_bits = reg_to_bits(offset_reg);
8068 // LDRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0001 Rn | Rt 0000 00 imm2 Rm
8069 let hw1: u16 = (0xF810 | base_bits) as u16;
8070 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8071 let mut bytes = hw1.to_le_bytes().to_vec();
8072 bytes.extend_from_slice(&hw2.to_le_bytes());
8073 Ok(bytes)
8074 }
8075
8076 /// Encode Thumb-2 32-bit LDRSB with immediate: LDRSB.W Rd, [Rn, #imm12]
8077 fn encode_thumb32_ldrsb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8078 let rd_bits = reg_to_bits(rd);
8079 let base_bits = reg_to_bits(base);
8080 // LDRSB.W Rd, [Rn, #imm12]: 1111 1001 1001 Rn | Rt imm12
8081 check_ldst_imm12(offset)?;
8082 let hw1: u16 = (0xF990 | base_bits) as u16;
8083 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8084 let mut bytes = hw1.to_le_bytes().to_vec();
8085 bytes.extend_from_slice(&hw2.to_le_bytes());
8086 Ok(bytes)
8087 }
8088
8089 /// Encode Thumb-2 32-bit LDRSB with register: LDRSB.W Rd, [Rn, Rm]
8090 fn encode_thumb32_ldrsb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8091 let rd_bits = reg_to_bits(rd);
8092 let base_bits = reg_to_bits(base);
8093 let rm_bits = reg_to_bits(offset_reg);
8094 // LDRSB.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0001 Rn | Rt 0000 00 imm2 Rm
8095 let hw1: u16 = (0xF910 | base_bits) as u16;
8096 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8097 let mut bytes = hw1.to_le_bytes().to_vec();
8098 bytes.extend_from_slice(&hw2.to_le_bytes());
8099 Ok(bytes)
8100 }
8101
8102 /// Encode Thumb-2 32-bit LDRH with immediate: LDRH.W Rd, [Rn, #imm12]
8103 fn encode_thumb32_ldrh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8104 let rd_bits = reg_to_bits(rd);
8105 let base_bits = reg_to_bits(base);
8106 // LDRH.W Rd, [Rn, #imm12]: 1111 1000 1011 Rn | Rt imm12
8107 check_ldst_imm12(offset)?;
8108 let hw1: u16 = (0xF8B0 | base_bits) as u16;
8109 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8110 let mut bytes = hw1.to_le_bytes().to_vec();
8111 bytes.extend_from_slice(&hw2.to_le_bytes());
8112 Ok(bytes)
8113 }
8114
8115 /// Encode Thumb-2 32-bit LDRH with register: LDRH.W Rd, [Rn, Rm]
8116 fn encode_thumb32_ldrh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8117 let rd_bits = reg_to_bits(rd);
8118 let base_bits = reg_to_bits(base);
8119 let rm_bits = reg_to_bits(offset_reg);
8120 // LDRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0011 Rn | Rt 0000 00 imm2 Rm
8121 let hw1: u16 = (0xF830 | base_bits) as u16;
8122 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8123 let mut bytes = hw1.to_le_bytes().to_vec();
8124 bytes.extend_from_slice(&hw2.to_le_bytes());
8125 Ok(bytes)
8126 }
8127
8128 /// Encode Thumb-2 32-bit LDRSH with immediate: LDRSH.W Rd, [Rn, #imm12]
8129 fn encode_thumb32_ldrsh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8130 let rd_bits = reg_to_bits(rd);
8131 let base_bits = reg_to_bits(base);
8132 // LDRSH.W Rd, [Rn, #imm12]: 1111 1001 1011 Rn | Rt imm12
8133 check_ldst_imm12(offset)?;
8134 let hw1: u16 = (0xF9B0 | base_bits) as u16;
8135 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8136 let mut bytes = hw1.to_le_bytes().to_vec();
8137 bytes.extend_from_slice(&hw2.to_le_bytes());
8138 Ok(bytes)
8139 }
8140
8141 /// Encode Thumb-2 32-bit LDRSH with register: LDRSH.W Rd, [Rn, Rm]
8142 fn encode_thumb32_ldrsh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8143 let rd_bits = reg_to_bits(rd);
8144 let base_bits = reg_to_bits(base);
8145 let rm_bits = reg_to_bits(offset_reg);
8146 // LDRSH.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0011 Rn | Rt 0000 00 imm2 Rm
8147 let hw1: u16 = (0xF930 | base_bits) as u16;
8148 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8149 let mut bytes = hw1.to_le_bytes().to_vec();
8150 bytes.extend_from_slice(&hw2.to_le_bytes());
8151 Ok(bytes)
8152 }
8153
8154 /// Encode Thumb-2 32-bit STRB with immediate: STRB.W Rd, [Rn, #imm12]
8155 fn encode_thumb32_strb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8156 let rd_bits = reg_to_bits(rd);
8157 let base_bits = reg_to_bits(base);
8158 // STRB.W Rd, [Rn, #imm12]: 1111 1000 1000 Rn | Rt imm12
8159 check_ldst_imm12(offset)?;
8160 let hw1: u16 = (0xF880 | base_bits) as u16;
8161 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8162 let mut bytes = hw1.to_le_bytes().to_vec();
8163 bytes.extend_from_slice(&hw2.to_le_bytes());
8164 Ok(bytes)
8165 }
8166
8167 /// Encode Thumb-2 32-bit STRB with register: STRB.W Rd, [Rn, Rm]
8168 fn encode_thumb32_strb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8169 let rd_bits = reg_to_bits(rd);
8170 let base_bits = reg_to_bits(base);
8171 let rm_bits = reg_to_bits(offset_reg);
8172 // STRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0000 Rn | Rt 0000 00 imm2 Rm
8173 let hw1: u16 = (0xF800 | base_bits) as u16;
8174 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8175 let mut bytes = hw1.to_le_bytes().to_vec();
8176 bytes.extend_from_slice(&hw2.to_le_bytes());
8177 Ok(bytes)
8178 }
8179
8180 /// Encode Thumb-2 32-bit STRH with immediate: STRH.W Rd, [Rn, #imm12]
8181 fn encode_thumb32_strh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8182 let rd_bits = reg_to_bits(rd);
8183 let base_bits = reg_to_bits(base);
8184 // STRH.W Rd, [Rn, #imm12]: 1111 1000 1010 Rn | Rt imm12
8185 check_ldst_imm12(offset)?;
8186 let hw1: u16 = (0xF8A0 | base_bits) as u16;
8187 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8188 let mut bytes = hw1.to_le_bytes().to_vec();
8189 bytes.extend_from_slice(&hw2.to_le_bytes());
8190 Ok(bytes)
8191 }
8192
8193 /// Encode Thumb-2 32-bit STRH with register: STRH.W Rd, [Rn, Rm]
8194 fn encode_thumb32_strh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8195 let rd_bits = reg_to_bits(rd);
8196 let base_bits = reg_to_bits(base);
8197 let rm_bits = reg_to_bits(offset_reg);
8198 // STRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0010 Rn | Rt 0000 00 imm2 Rm
8199 let hw1: u16 = (0xF820 | base_bits) as u16;
8200 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8201 let mut bytes = hw1.to_le_bytes().to_vec();
8202 bytes.extend_from_slice(&hw2.to_le_bytes());
8203 Ok(bytes)
8204 }
8205
8206 /// Encode Thumb-2 32-bit ADD with immediate: ADD.W Rd, Rn, #imm
8207 fn encode_thumb32_add_imm(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
8208 let rd_bits = reg_to_bits(rd);
8209 let rn_bits = reg_to_bits(rn);
8210
8211 // In-range immediates (<= 0xFFF) delegate to `encode_thumb32_add`,
8212 // which picks the correct form per value:
8213 // - imm <= 0xFF -> ADD.W (T3). Its `i:imm3:imm8` field is a
8214 // ThumbExpandImm MODIFIED immediate — raw == expanded only here.
8215 // - 0x100..=0xFFF -> ADDW (T4, 0xF200): a PLAIN 12-bit immediate.
8216 //
8217 // #681: this function used to pack the raw value into the T3 field for
8218 // ALL imm <= 0xFFF. ThumbExpandImm(0x200) = 0 and ThumbExpandImm(0x400)
8219 // = 0x8000_0000, so every dynamic-address load/store with a static
8220 // offset in 0x100..=0xFFF silently computed a WRONG address — and in
8221 // --safety-bounds software the guard checked the intended address while
8222 // the access used the mis-encoded one (bounds bypass). Same
8223 // ThumbExpandImm raw-packing class as #253/#255, reached via #382.
8224 if imm <= 0xFFF {
8225 self.encode_thumb32_add(rd, rn, imm)
8226 } else {
8227 // Out-of-range immediate (> 0xFFF): materialize it into a scratch
8228 // register, then ADD.W Rd, Rn, scratch. This is the #180/#185
8229 // "encoder must produce a legal sequence, not assert" class — see #350.
8230 //
8231 // Scratch choice (must NEVER equal Rn, or Rn would be clobbered before
8232 // the ADD reads it):
8233 // - rd != rn => use rd itself (rn is untouched, since rd != rn).
8234 // - rd == rn => use R12/IP (the reserved encoder scratch). rd/rn are
8235 // never R12 (R12 is non-allocatable), so it can't alias.
8236 //
8237 // The materialized value is the same whether or not MOVT is emitted, so
8238 // the byte length depends only on `imm` (and rd==rn) — the size probe and
8239 // the final emit therefore agree (mandatory: the function is encoded twice).
8240 let scratch: u32 = if rd_bits == rn_bits {
8241 12 // R12/IP — in-place add, can't use rd because rd == rn
8242 } else {
8243 rd_bits // rn is preserved because rd != rn
8244 };
8245 // Invariant: the scratch must never alias Rn (would clobber it before
8246 // the ADD reads it). Unreachable in real codegen (rd/rn are never R12,
8247 // which is reserved encoder scratch), but the encoder is also driven by
8248 // the `encoder_no_panic` fuzz harness with ARBITRARY registers — incl.
8249 // rd==rn==R12, which makes scratch (R12) alias Rn. The encoder contract
8250 // (#180/#185) is Ok-or-Err, never a panic, so return a typed error
8251 // instead of asserting. #350 follow-up.
8252 if scratch == rn_bits {
8253 return Err(synth_core::Error::synthesis(format!(
8254 "ADD #imm: cannot lower #{imm:#x} for Rd==Rn==R12 — no free scratch \
8255 register (R12 is the reserved encoder scratch and aliases Rn here)"
8256 )));
8257 }
8258
8259 let lo16 = imm & 0xFFFF;
8260 let hi16 = (imm >> 16) & 0xFFFF;
8261
8262 let mut bytes = self.encode_thumb32_movw_raw(scratch, lo16)?;
8263 if hi16 != 0 {
8264 bytes.extend_from_slice(&self.encode_thumb32_movt_raw(scratch, hi16)?);
8265 }
8266 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rn_bits, scratch)?);
8267 Ok(bytes)
8268 }
8269 }
8270
8271 // === Raw encoding helpers for POPCNT (take register numbers directly) ===
8272
8273 /// Encode Thumb-2 32-bit MOVW (16-bit immediate) - raw version
8274 ///
8275 /// # Contract (Verus-style)
8276 /// ```text
8277 /// requires rd <= 14, imm16 <= 0xFFFF
8278 /// ensures result.len() == 4
8279 /// ```
8280 fn encode_thumb32_movw_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8281 reg_bits_checked(rd)?;
8282 encoding_contracts::verify_imm16(imm16);
8283 // MOVW Rd, #imm16
8284 // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
8285 let imm16 = imm16 & 0xFFFF;
8286 let imm4 = (imm16 >> 12) & 0xF;
8287 let i_bit = (imm16 >> 11) & 1;
8288 let imm3 = (imm16 >> 8) & 0x7;
8289 let imm8 = imm16 & 0xFF;
8290
8291 let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
8292 let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8293
8294 let mut bytes = hw1.to_le_bytes().to_vec();
8295 bytes.extend_from_slice(&hw2.to_le_bytes());
8296 encoding_contracts::verify_thumb32(&bytes);
8297 Ok(bytes)
8298 }
8299
8300 /// Encode Thumb-2 32-bit MOVT (move top 16 bits) - raw version
8301 ///
8302 /// # Contract (Verus-style)
8303 /// ```text
8304 /// requires rd <= 14, imm16 <= 0xFFFF
8305 /// ensures result.len() == 4
8306 /// ```
8307 fn encode_thumb32_movt_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8308 reg_bits_checked(rd)?;
8309 encoding_contracts::verify_imm16(imm16);
8310 // MOVT Rd, #imm16
8311 // 1111 0 i 10 1 1 0 0 imm4 | 0 imm3 Rd imm8
8312 let imm16 = imm16 & 0xFFFF;
8313 let imm4 = (imm16 >> 12) & 0xF;
8314 let i_bit = (imm16 >> 11) & 1;
8315 let imm3 = (imm16 >> 8) & 0x7;
8316 let imm8 = imm16 & 0xFF;
8317
8318 let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
8319 let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8320
8321 let mut bytes = hw1.to_le_bytes().to_vec();
8322 bytes.extend_from_slice(&hw2.to_le_bytes());
8323 encoding_contracts::verify_thumb32(&bytes);
8324 Ok(bytes)
8325 }
8326
8327 /// Encode Thumb-2 32-bit LSR (logical shift right) with immediate - raw version
8328 fn encode_thumb32_lsr_raw(&self, rd: u32, rm: u32, shift: u32) -> Result<Vec<u8>> {
8329 // MOV.W Rd, Rm, LSR #imm
8330 // EA4F 0 imm3 Rd imm2 01 Rm
8331 let imm5 = shift & 0x1F;
8332 let imm2 = imm5 & 0x3;
8333 let imm3 = (imm5 >> 2) & 0x7;
8334
8335 let hw1: u16 = 0xEA4F;
8336 let hw2: u16 = ((imm3 << 12) | (rd << 8) | (imm2 << 6) | (0b01 << 4) | rm) as u16;
8337
8338 let mut bytes = hw1.to_le_bytes().to_vec();
8339 bytes.extend_from_slice(&hw2.to_le_bytes());
8340 Ok(bytes)
8341 }
8342
8343 /// Encode Thumb-2 32-bit AND with immediate - raw version
8344 fn encode_thumb32_and_imm_raw(&self, rd: u32, rn: u32, imm: u32) -> Result<Vec<u8>> {
8345 // AND.W Rd, Rn, #<modified_immediate>
8346 // F0 00 Rn | 0 imm3 Rd imm8
8347 //
8348 // #681 class audit: the field is a ThumbExpandImm modified immediate,
8349 // not a raw value. The only current caller (POPCNT final mask) passes
8350 // 0x3F, which expands to itself — the gate is byte-identical today and
8351 // closes the raw-packing landmine for any future caller.
8352 let field = try_thumb_expand_imm(imm).ok_or_else(|| {
8353 synth_core::Error::synthesis(
8354 "AND immediate is not a valid ThumbExpandImm — materialize into a register",
8355 )
8356 })?;
8357 let i_bit = (field >> 11) & 1;
8358 let imm3 = (field >> 8) & 0x7;
8359 let imm8 = field & 0xFF;
8360
8361 let hw1: u16 = (0xF000 | (i_bit << 10) | rn) as u16;
8362 let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8363
8364 let mut bytes = hw1.to_le_bytes().to_vec();
8365 bytes.extend_from_slice(&hw2.to_le_bytes());
8366 Ok(bytes)
8367 }
8368
8369 /// Encode Thumb-2 32-bit SUB (register) - raw version
8370 fn encode_thumb32_sub_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8371 // SUB.W Rd, Rn, Rm
8372 // EBA0 Rn | 0 Rd 00 00 Rm
8373 let hw1: u16 = (0xEBA0 | rn) as u16;
8374 let hw2: u16 = ((rd << 8) | rm) as u16;
8375
8376 let mut bytes = hw1.to_le_bytes().to_vec();
8377 bytes.extend_from_slice(&hw2.to_le_bytes());
8378 Ok(bytes)
8379 }
8380
8381 /// Encode Thumb-2 32-bit ADD (register) - raw version
8382 fn encode_thumb32_add_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8383 // ADD.W Rd, Rn, Rm
8384 // EB00 Rn | 0 Rd 00 00 Rm
8385 let hw1: u16 = (0xEB00 | rn) as u16;
8386 let hw2: u16 = ((rd << 8) | rm) as u16;
8387
8388 let mut bytes = hw1.to_le_bytes().to_vec();
8389 bytes.extend_from_slice(&hw2.to_le_bytes());
8390 Ok(bytes)
8391 }
8392
8393 /// Encode Thumb-2 32-bit ADDS (register, flag-setting) - raw version.
8394 /// Used as the high-register fallback for `ArmOp::Adds` (i64 low-word add)
8395 /// so R8-R11 pair operands don't overflow the 16-bit field — #178/#180.
8396 fn encode_thumb32_adds_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8397 // ADDS.W Rd, Rn, Rm (T3, S=1): EB10 Rn | 0 Rd 00 00 Rm
8398 let hw1: u16 = (0xEB10 | rn) as u16;
8399 let hw2: u16 = ((rd << 8) | rm) as u16;
8400 let mut bytes = hw1.to_le_bytes().to_vec();
8401 bytes.extend_from_slice(&hw2.to_le_bytes());
8402 Ok(bytes)
8403 }
8404
8405 /// Encode Thumb-2 32-bit SUBS (register, flag-setting) - raw version.
8406 /// High-register fallback for `ArmOp::Subs` (i64 low-word subtract) — #178/#180.
8407 fn encode_thumb32_subs_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8408 // SUBS.W Rd, Rn, Rm (T3, S=1): EBB0 Rn | 0 Rd 00 00 Rm
8409 let hw1: u16 = (0xEBB0 | rn) as u16;
8410 let hw2: u16 = ((rd << 8) | rm) as u16;
8411 let mut bytes = hw1.to_le_bytes().to_vec();
8412 bytes.extend_from_slice(&hw2.to_le_bytes());
8413 Ok(bytes)
8414 }
8415
8416 /// Encode a sequence of ARM instructions
8417 pub fn encode_sequence(&self, ops: &[ArmOp]) -> Result<Vec<u8>> {
8418 let mut code = Vec::new();
8419
8420 for op in ops {
8421 let encoded = self.encode(op)?;
8422 code.extend_from_slice(&encoded);
8423 }
8424
8425 Ok(code)
8426 }
8427}
8428
8429/// Convert register to bit encoding (0-15)
8430/// Reverse of the ARMv7-M `ThumbExpandImm`: given a 32-bit immediate, return the
8431/// 12-bit `i:imm3:imm8` field if it is a representable modified immediate, else
8432/// `None` (the caller must materialize the value into a register). This is the
8433/// shared correct path for the data-processing immediate encoders — without it
8434/// they pack raw bits and silently mis-encode any value `> 0xFF` that isn't a
8435/// modified immediate (the silent-miscompile class behind #251/#253/#255).
8436fn try_thumb_expand_imm(value: u32) -> Option<u32> {
8437 // i:imm3 = 0000 → 8-bit value, zero-extended (00000000 00000000 00000000 XY).
8438 if value <= 0xFF {
8439 return Some(value);
8440 }
8441 let b0 = value & 0xFF; // byte 0
8442 let b1 = (value >> 8) & 0xFF; // byte 1
8443 // 0x00XY00XY (i:imm3 = 0001) — XY in bytes 0 and 2
8444 if value == (b0 << 16) | b0 {
8445 return Some(0x100 | b0);
8446 }
8447 // 0xXY00XY00 (i:imm3 = 0010) — XY in bytes 1 and 3
8448 if value == (b1 << 24) | (b1 << 8) {
8449 return Some(0x200 | b1);
8450 }
8451 // 0xXYXYXYXY (i:imm3 = 0011) — XY in all four bytes
8452 if value == (b0 << 24) | (b0 << 16) | (b0 << 8) | b0 {
8453 return Some(0x300 | b0);
8454 }
8455 // An 8-bit value with bit 7 set, rotated right by 8..=31. `rotate_left(rot)`
8456 // undoes the encoded right rotation; if the result is `1bbbbbbb` (0x80..=0xFF)
8457 // the value is representable. imm12[11:7] = rot, imm12[6:0] = low 7 bits.
8458 for rot in 8..=31u32 {
8459 let unrot = value.rotate_left(rot);
8460 if (0x80..=0xFF).contains(&unrot) {
8461 return Some((rot << 7) | (unrot & 0x7F));
8462 }
8463 }
8464 None
8465}
8466
8467/// Guard a Thumb-2 `LDR/STR Rd, [Rn, #imm12]` offset. The imm12 form supports
8468/// `0..=4095`; a larger offset must be materialized into a register by the
8469/// selector (register-offset addressing). Returning `Err` rather than silently
8470/// masking `offset & 0xFFF` closes the wrong-address miscompile class (#259,
8471/// the load/store sibling of #253/#255).
8472fn check_ldst_imm12(offset: u32) -> Result<()> {
8473 if offset > 0xFFF {
8474 Err(synth_core::Error::synthesis(
8475 "load/store immediate offset > 0xFFF (4095) — materialize the offset into a register",
8476 ))
8477 } else {
8478 Ok(())
8479 }
8480}
8481
8482/// #916 — emit `Rd = 0` in Thumb-2, correctly for EVERY destination register.
8483///
8484/// The 16-bit `MOVS Rd, #imm8` (T1) is `0010 0 Rd(3) imm8` — the Rd field is
8485/// **three bits**. For R8-R12 `reg_to_bits` yields 8..12, so `rd_bits << 8`
8486/// overflows into bit 11 and `0x2000 | 0x0800` is `0x2800` = `CMP r0, #0`:
8487/// not a move at all. The destination is never written (it keeps stale data)
8488/// and the flags are clobbered. Same class as #180 / H-CODE-9, and the same
8489/// defect #311 fixed for `I64SetCond`.
8490///
8491/// High registers therefore take the 32-bit `MOV.W Rd, #imm8` (T2,
8492/// `F04F 0000 | Rd<<8 | imm8`), whose Rd field is four bits. `MOV.W` with S=0
8493/// does not set flags, which is what these zero-fill sites want anyway.
8494///
8495/// **Callers with branches must consult [`thumb_zero_fill_halfwords`].** This
8496/// emits 1 halfword for R0-R7 and 2 for R8-R12; any branch whose target lies
8497/// PAST this instruction moves when it widens and its displacement has to be
8498/// derived rather than hard-coded. (A branch targeting this instruction's own
8499/// address is unaffected — an instruction cannot move itself.)
8500fn emit_thumb_zero_fill(bytes: &mut Vec<u8>, rd_bits: u32) {
8501 if rd_bits < 8 {
8502 let movs: u16 = 0x2000 | ((rd_bits as u16) << 8);
8503 bytes.extend_from_slice(&movs.to_le_bytes());
8504 } else {
8505 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
8506 bytes.extend_from_slice(&((rd_bits as u16) << 8).to_le_bytes());
8507 }
8508}
8509
8510/// Halfword length of the encoding [`emit_thumb_zero_fill`] picks for
8511/// `rd_bits`. Branch displacements spanning the zero-fill derive from this so
8512/// the encoder cannot drift from itself (#916; the byte-size estimator mirrors
8513/// it in `synth_synthesis::estimate_arm_byte_size`, pinned by the #498
8514/// `estimator_encoder_agreement` oracle).
8515fn thumb_zero_fill_halfwords(rd_bits: u32) -> u16 {
8516 if rd_bits < 8 { 1 } else { 2 }
8517}
8518
8519fn reg_to_bits(reg: &Reg) -> u32 {
8520 match reg {
8521 Reg::R0 => 0,
8522 Reg::R1 => 1,
8523 Reg::R2 => 2,
8524 Reg::R3 => 3,
8525 Reg::R4 => 4,
8526 Reg::R5 => 5,
8527 Reg::R6 => 6,
8528 Reg::R7 => 7,
8529 Reg::R8 => 8,
8530 Reg::R9 => 9,
8531 Reg::R10 => 10,
8532 Reg::R11 => 11,
8533 Reg::R12 => 12,
8534 Reg::SP => 13,
8535 Reg::LR => 14,
8536 Reg::PC => 15,
8537 }
8538}
8539
8540// ======================================================================
8541// #610 — i64 fixed-ABI expansion wrappers.
8542//
8543// The hand-written multi-instruction i64 cores (rotl/rotr and the div/rem
8544// shift-subtract loops) compute in FIXED low registers. Before #610 the
8545// div/rem arms ignored their operand fields outright (hardcoded R0:R1 /
8546// R2:R3 in, result to R0:R1) and the rot arms used R3/R4 scratch that
8547// collided with selector-assigned registers — then restored the saved
8548// scratch OVER the result (`POP {R4}` with rd_lo == R4), so the op
8549// returned the caller's stale register: 0 for every input under qemu.
8550//
8551// These wrappers make each core honor its register parameters:
8552// 1. save R0-R3,
8553// 2. marshal the operand registers into the core's fixed input regs via
8554// the stack (permutation-safe: every source is read before any fixed
8555// register is written),
8556// 3. run the fixed-reg core (self-preserving for R4+; R12 is encoder
8557// scratch and never allocatable, #212),
8558// 4. MOV the result pair from R0:R1 into the selector's rd pair,
8559// 5. restore R0-R3, skipping any register the result now occupies.
8560//
8561// All emitted lengths are register-independent so the optimized path's
8562// byte-size estimator (`estimate_arm_byte_size`, pinned by the
8563// estimator↔encoder agreement oracle #498/#511) stays a constant per op.
8564// ======================================================================
8565
8566/// Steps 1+2: `PUSH {R0-R3}`, then marshal `srcs` (operand registers, any of
8567/// R0-R12) into `R0..R<n>` via individual stack pushes. Sources are all read
8568/// before any destination register is written, so arbitrary source/target
8569/// permutations (including operands living in R0-R3) are safe.
8570fn emit_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8571 debug_assert!(srcs.len() <= 4);
8572 // PUSH {R0-R3} — save the caller-visible low registers.
8573 bytes.extend_from_slice(&0xB40Fu16.to_le_bytes());
8574 // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8575 for src in srcs.iter().rev() {
8576 let rt = reg_to_bits(src) as u16;
8577 bytes.extend_from_slice(&0xF84Du16.to_le_bytes());
8578 bytes.extend_from_slice(&((rt << 12) | 0x0D04).to_le_bytes());
8579 }
8580 // POP {Ri} — Ri := srcs[i].
8581 for i in 0..srcs.len() as u16 {
8582 bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes());
8583 }
8584}
8585
8586/// Steps 4+5: move the core's R0:R1 result into the selector's rd pair, then
8587/// restore the R0-R3 saved by [`emit_i64_fixed_abi_entry`], skipping any
8588/// register the result now lives in (its saved caller word is discarded).
8589fn emit_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8590 let lo = reg_to_bits(rdlo);
8591 let hi = reg_to_bits(rdhi);
8592 if lo == 1 && hi == 0 {
8593 // A fully swapped pair would clobber one half in either MOV order.
8594 // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8595 return Err(synth_core::Error::synthesis(
8596 "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8597 ));
8598 }
8599 let mov16 = |bytes: &mut Vec<u8>, rd: u32, rm: u32| {
8600 let d = ((rd >> 3) & 1) as u16;
8601 bytes.extend_from_slice(
8602 &(0x4600u16 | (d << 7) | ((rm as u16) << 3) | ((rd & 7) as u16)).to_le_bytes(),
8603 );
8604 };
8605 if hi == 0 {
8606 // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8607 mov16(bytes, lo, 0);
8608 mov16(bytes, hi, 1);
8609 } else {
8610 // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8611 mov16(bytes, hi, 1);
8612 mov16(bytes, lo, 0);
8613 }
8614 for i in 0..4u32 {
8615 if i == lo || i == hi {
8616 // The result lives here — drop the saved caller word.
8617 bytes.extend_from_slice(&0xB001u16.to_le_bytes()); // ADD SP, #4
8618 } else {
8619 bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes()); // POP {Ri}
8620 }
8621 }
8622 Ok(())
8623}
8624
8625/// WASM `i64.div_*` / `i64.rem_*` by zero must trap, matching the i32 path's
8626/// cmp/bne/udf guard. Emitted after marshaling, when the divisor pair is in
8627/// R2:R3: `ORRS R12, R2, R3` — `BNE` over a `UDF #0` when nonzero.
8628fn emit_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8629 bytes.extend_from_slice(&0xEA52u16.to_le_bytes()); // ORRS.W R12, R2, R3
8630 bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8631 bytes.extend_from_slice(&0xD100u16.to_le_bytes()); // BNE.N +0 (skip the UDF)
8632 bytes.extend_from_slice(&0xDE00u16.to_le_bytes()); // UDF #0 — divide by zero
8633}
8634
8635/// WASM `i64.div_s(INT64_MIN, -1)` must trap (Core §4.3.2 `idiv_s`: the
8636/// quotient +2^63 is unrepresentable), matching the i32 path's overflow
8637/// guard — #633: without it the core negated INT64_MIN onto itself and
8638/// silently returned INT64_MIN. Emitted after marshaling, when the dividend
8639/// pair is in R0:R1 and the divisor pair in R2:R3; R12 is encoder scratch.
8640///
8641/// div_s ONLY — `i64.rem_s(INT64_MIN, -1)` is defined as 0 and must NOT
8642/// trap (`irem_s`), so the I64RemS arm never calls this. 22 bytes,
8643/// register-independent (estimator contract, #498/#511).
8644fn emit_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8645 // AND.W R12, R2, R3 — R12 == 0xFFFFFFFF iff divisor == -1
8646 bytes.extend_from_slice(&0xEA02u16.to_le_bytes());
8647 bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8648 // CMN.W R12, #1 — EQ iff both divisor words are all-ones
8649 bytes.extend_from_slice(&0xF11Cu16.to_le_bytes());
8650 bytes.extend_from_slice(&0x0F01u16.to_le_bytes());
8651 // BNE .no_trap
8652 bytes.extend_from_slice(&0xD105u16.to_le_bytes());
8653 // CMP R0, #0 — dividend lo word of INT64_MIN
8654 bytes.extend_from_slice(&0x2800u16.to_le_bytes());
8655 // BNE .no_trap
8656 bytes.extend_from_slice(&0xD103u16.to_le_bytes());
8657 // CMP.W R1, #0x80000000 — dividend hi word of INT64_MIN
8658 bytes.extend_from_slice(&0xF1B1u16.to_le_bytes());
8659 bytes.extend_from_slice(&0x4F00u16.to_le_bytes());
8660 // BNE .no_trap
8661 bytes.extend_from_slice(&0xD100u16.to_le_bytes());
8662 // UDF #0 — signed-division overflow
8663 bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
8664 // .no_trap:
8665}
8666
8667// ======================================================================
8668// #615 — A32 (ARM-mode) twins of the #610 i64 fixed-ABI wrappers above.
8669// Identical register contract, A32 encodings: the multi-instruction i64
8670// cores (rotl/rotr, div/rem) compute in fixed low registers (value/dividend
8671// R0:R1, amount R2 / divisor R2:R3, result to R0:R1); the wrappers marshal
8672// the selector-assigned operand registers in and the result out, saving and
8673// restoring the caller-visible R0-R3 around the core.
8674// ======================================================================
8675
8676/// A32 steps 1+2: `STMDB SP!, {R0-R3}`, then marshal `srcs` into `R0..R<n>`
8677/// via individual stack pushes (`STR src, [SP, #-4]!` in reverse order, then
8678/// `LDR Ri, [SP], #4`). Every source is read before any fixed register is
8679/// written, so arbitrary source/target permutations are safe.
8680fn emit_a32_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8681 debug_assert!(srcs.len() <= 4);
8682 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8683 // PUSH {R0-R3} — save the caller-visible low registers.
8684 w(bytes, 0xE92D_000F);
8685 // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8686 for src in srcs.iter().rev() {
8687 w(bytes, 0xE52D_0004 | (reg_to_bits(src) << 12));
8688 }
8689 // LDR Ri, [SP], #4 — Ri := srcs[i].
8690 for i in 0..srcs.len() as u32 {
8691 w(bytes, 0xE49D_0004 | (i << 12));
8692 }
8693}
8694
8695/// A32 steps 4+5: move the core's R0:R1 result into the selector's rd pair,
8696/// then restore the R0-R3 saved by [`emit_a32_i64_fixed_abi_entry`], skipping
8697/// any register the result now lives in (its saved caller word is discarded).
8698fn emit_a32_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8699 let lo = reg_to_bits(rdlo);
8700 let hi = reg_to_bits(rdhi);
8701 if lo == 1 && hi == 0 {
8702 // A fully swapped pair would clobber one half in either MOV order.
8703 // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8704 return Err(synth_core::Error::synthesis(
8705 "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8706 ));
8707 }
8708 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8709 let mov = |bytes: &mut Vec<u8>, rd: u32, rm: u32| w(bytes, 0xE1A0_0000 | (rd << 12) | rm);
8710 if hi == 0 {
8711 // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8712 mov(bytes, lo, 0);
8713 mov(bytes, hi, 1);
8714 } else {
8715 // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8716 mov(bytes, hi, 1);
8717 mov(bytes, lo, 0);
8718 }
8719 for i in 0..4u32 {
8720 if i == lo || i == hi {
8721 // The result lives here — drop the saved caller word.
8722 w(bytes, 0xE28D_D004); // ADD SP, SP, #4
8723 } else {
8724 w(bytes, 0xE49D_0004 | (i << 12)); // LDR Ri, [SP], #4
8725 }
8726 }
8727 Ok(())
8728}
8729
8730/// A32 zero-divisor trap, emitted after marshaling when the divisor pair is
8731/// in R2:R3: `ORRS R12, R2, R3` sets Z iff the divisor is zero; `BNE` skips a
8732/// `UDF #0` (WASM div/rem-by-zero must trap, matching the Thumb-2 twin).
8733fn emit_a32_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8734 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8735 w(bytes, 0xE192_C003); // ORRS R12, R2, R3
8736 w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8737 w(bytes, 0xE7F0_00F0); // UDF #0 — divide by zero
8738}
8739
8740/// A32 twin of [`emit_i64_divs_overflow_trap`] (#633): trap on
8741/// `i64.div_s(INT64_MIN, -1)`. Conditional execution replaces the Thumb
8742/// branches — the CMPEQ chain leaves EQ set only when divisor == -1 AND
8743/// dividend == INT64_MIN. div_s only; rem_s must keep returning 0.
8744fn emit_a32_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8745 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8746 w(bytes, 0xE002_C003); // AND R12, R2, R3 (== 0xFFFFFFFF iff divisor == -1)
8747 w(bytes, 0xE37C_0001); // CMN R12, #1 (EQ iff divisor == -1)
8748 w(bytes, 0x0350_0000); // CMPEQ R0, #0 (EQ iff also dividend lo == 0)
8749 w(bytes, 0x0351_0102); // CMPEQ R1, #0x80000000 (EQ iff dividend == INT64_MIN)
8750 w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8751 w(bytes, 0xE7F0_00F0); // UDF #0 — signed-division overflow
8752}
8753
8754/// Fallible form of the `verify_reg_bits` contract. PC (R15) is not a valid
8755/// data operand for the Thumb-2 encodings that use this guard (SDIV/UDIV/MLS/…
8756/// are UNPREDICTABLE with PC). Synth's own codegen never emits PC there, but
8757/// the encoder must stay *total* over arbitrary `ArmOp` inputs — the fuzz
8758/// harness (`encoder_no_panic`) requires Ok-or-Err, never a panic. Pre-fix, the
8759/// `debug_assert` in `verify_reg_bits` aborted under `-Cdebug-assertions`.
8760/// Returns a typed Err instead. See #185.
8761fn reg_bits_checked(bits: u32) -> Result<()> {
8762 if bits > 14 {
8763 return Err(synth_core::Error::synthesis(format!(
8764 "register bits {bits} (PC/R15) is not a valid operand for this Thumb-2 encoding"
8765 )));
8766 }
8767 Ok(())
8768}
8769
8770/// Try to encode a 32-bit value as an ARM rotated immediate (imm8 ROR 2*rot4).
8771/// Returns Some((encoded_bits, 1)) if representable, None otherwise.
8772fn try_encode_rotated_imm(val: u32) -> Option<(u32, u32)> {
8773 if val == 0 {
8774 return Some((0, 1));
8775 }
8776 for rot in 0..16u32 {
8777 let shift = rot * 2;
8778 // Rotate left by shift (undo the ROR) to see if result fits in 8 bits
8779 let unrotated = val.rotate_left(shift);
8780 if unrotated <= 0xFF {
8781 // Encoded as: rot4(4 bits) | imm8(8 bits) = rotate_imm << 8 | imm8
8782 return Some(((rot << 8) | unrotated, 1));
8783 }
8784 }
8785 None
8786}
8787
8788/// Encode operand2 field and return (bits, immediate_flag).
8789/// For ARM32 mode, immediates use the rotated-immediate encoding (imm8 ROR 2*rot4).
8790/// Panics if an immediate value cannot be represented. Callers that need large
8791/// immediates should use MOVW/MOVT instead of Operand2::Imm.
8792fn encode_operand2(op2: &Operand2) -> Result<(u32, u32)> {
8793 match op2 {
8794 Operand2::Imm(val) => {
8795 let uval = *val as u32;
8796 // Attempt rotated-immediate encoding (ARM32 Operand2)
8797 if let Some(encoded) = try_encode_rotated_imm(uval) {
8798 Ok(encoded)
8799 } else {
8800 // #378-class honesty: an immediate that can't be expressed as an
8801 // ARM32 rotated immediate is an INTERNAL selector bug — large
8802 // constants must be materialized via MOVW/MOVT, not passed here.
8803 // FAIL HONESTLY with an Err rather than silently masking to
8804 // `uval & 0xFF` and emitting a WRONG immediate. The encoder is
8805 // Ok-or-Err, never corrupt (#180/#185); a loud Err is also why
8806 // this is an Err and not a panic (the `encoder_no_panic` fuzz
8807 // contract — malformed/oversized input must degrade, not crash).
8808 Err(synth_core::Error::synthesis(format!(
8809 "encode_operand2: immediate {uval:#x} ({val}) is not an ARM32 \
8810 rotated immediate — the selector must materialize large \
8811 constants via MOVW/MOVT"
8812 )))
8813 }
8814 }
8815
8816 Operand2::Reg(reg) => {
8817 let reg_bits = reg_to_bits(reg);
8818 Ok((reg_bits, 0)) // I=0 for register
8819 }
8820
8821 Operand2::RegShift {
8822 rm,
8823 shift: _,
8824 amount,
8825 } => {
8826 // Simplified encoding with shift
8827 let rm_bits = reg_to_bits(rm);
8828 let shift_bits = (*amount & 0x1F) << 7;
8829 Ok((shift_bits | rm_bits, 0))
8830 }
8831 }
8832}
8833
8834/// Largest immediate an A32 `LDR`/`STR`/`LDRB`/`STRB` `[Rn, #imm12]` form holds.
8835const A32_LDST_IMM12_MAX: u32 = 0xFFF;
8836/// Largest immediate an A32 `LDRH`/`STRH`/`LDRSB`/`LDRSH` `[Rn, #imm8]`
8837/// (`imm4H:imm4L`) form holds.
8838const A32_LDST_IMM8_MAX: u32 = 0xFF;
8839/// Largest LOW-half immediate an A32 `I64Ldr`/`I64Str` pair can fold: the high
8840/// half sits at `+4`, which must itself fit imm12 (`0xFFB + 4 = 0xFFF`).
8841const A32_I64_PAIR_IMM12_MAX: u32 = 0xFFB;
8842
8843/// RQ-63-ARMI64OFF (#1165): resolve the A32 base register and residual
8844/// immediate for a load/store whose static offset may exceed the form's
8845/// immediate field — the A32 counterpart of the Thumb-2 `i64_effective_base`
8846/// (#372/#382). Returns `(base, residual)`; the caller encodes `[base,
8847/// #residual]` (and `#residual + 4` for the i64 high half).
8848///
8849/// The offset is the wasm memarg round-tripped through the selector's `as i32`
8850/// cast (a memarg `>= 2^31` arrives negative), so it is read back as `u32` and
8851/// the effective address is `base + index + offset (mod 2^32)` — the same
8852/// arithmetic the Thumb-2 word path (`encode_thumb32_add_imm`) and the
8853/// `--safety-bounds software` guard (`software_bounds_guard`, which round-trips
8854/// the same cast) already use. A clamp or a mask here is a WRONG ADDRESS, i.e.
8855/// the silent memory miscompile this artifact exists to refuse: before this
8856/// helper the A32 immediate arms masked `& 0xFFF` (word/byte) and `& 0xFF`
8857/// (halfword/signed), so `i32.load offset=5000` on the `-b arm` default target
8858/// silently read `[ip, #904]`, and the i64 pair arm declined outright (46 of
8859/// the 110 core-module declines in v0.62's ARM census).
8860///
8861/// - `offset <= imm_max`, no index: emits nothing, returns `(addr.base, offset)`
8862/// — byte-identical to the pre-#1165 immediate form.
8863/// - `offset <= imm_max`, index `rm`: emits `ADD ip, base, rm`, returns
8864/// `(ip, offset)` — byte-identical to the #206 register-offset form.
8865/// - `offset > imm_max`: MATERIALIZES the whole effective base into `ip`:
8866/// `MOVW ip, #lo16 ; [MOVT ip, #hi16 — only when non-zero] ; ADD ip, ip, base
8867/// ; [ADD ip, ip, rm]`, returns `(ip, 0)`. `ip` is fully computed BEFORE the
8868/// access, so a destination register aliasing `base` or `rm` is safe.
8869///
8870/// `ip` (R12) is the reserved encoder scratch — never allocated (pool R0–R8;
8871/// R9/R10/R11 are the globals/size/base contract registers). If `base` or `rm`
8872/// IS R12 the materialization would clobber its own input, so that case is a
8873/// typed `Err` — a loud decline, never a wrong address (the alias trap
8874/// `encode_thumb32_add_imm` carries for `rd == rn == R12`).
8875///
8876/// Bytes verified against `arm-none-eabi-as` in the `test_1165_a32_*` tests
8877/// and executed by `scripts/repro/a32_ldst_offset_1165_differential.py`.
8878fn a32_effective_base(bytes: &mut Vec<u8>, addr: &MemAddr, imm_max: u32) -> Result<(Reg, u32)> {
8879 let ip = Reg::R12;
8880 let ip_bits = reg_to_bits(&ip);
8881 let offset = addr.offset as u32;
8882 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8883 // ADD rd, rn, rm (cond=AL, opcode=ADD, S=0, register operand2).
8884 let add_reg = |rd: u32, rn: u32, rm: u32| 0xE080_0000 | (rn << 16) | (rd << 12) | rm;
8885 if offset <= imm_max {
8886 return Ok(match addr.offset_reg {
8887 Some(rm) => {
8888 w(
8889 bytes,
8890 add_reg(ip_bits, reg_to_bits(&addr.base), reg_to_bits(&rm)),
8891 );
8892 (ip, offset)
8893 }
8894 None => (addr.base, offset),
8895 });
8896 }
8897 if addr.base == ip || addr.offset_reg == Some(ip) {
8898 return Err(synth_core::Error::synthesis(format!(
8899 "A32 load/store offset {offset:#x} exceeds the immediate field (max {imm_max:#x}) \
8900 and the address already uses the R12 scratch — no free register to \
8901 materialize it (RQ-63-ARMI64OFF)"
8902 )));
8903 }
8904 // MOVW ip, #lo16 ; MOVT ip, #hi16 (elided when zero, mirroring I64Const).
8905 let lo16 = offset & 0xFFFF;
8906 let hi16 = offset >> 16;
8907 w(
8908 bytes,
8909 0xE300_0000 | ((lo16 >> 12) << 16) | (ip_bits << 12) | (lo16 & 0xFFF),
8910 );
8911 if hi16 != 0 {
8912 w(
8913 bytes,
8914 0xE340_0000 | ((hi16 >> 12) << 16) | (ip_bits << 12) | (hi16 & 0xFFF),
8915 );
8916 }
8917 w(bytes, add_reg(ip_bits, ip_bits, reg_to_bits(&addr.base)));
8918 if let Some(rm) = addr.offset_reg {
8919 w(bytes, add_reg(ip_bits, ip_bits, reg_to_bits(&rm)));
8920 }
8921 Ok((ip, 0))
8922}
8923
8924/// Encode an A32 immediate-form memory address to `(base_reg, imm12)`.
8925///
8926/// RQ-63-ARMI64OFF (#1165): this used to mask `& 0xFFF`, silently re-targeting
8927/// any offset past the field (the #259 class, closed on Thumb-2 by
8928/// `check_ldst_imm12` but never here). The immediate arms are only reached with
8929/// an in-range, index-free address — `encode_arm_reg_offset_mem` materializes
8930/// everything else into IP first — so this is a TRIPWIRE: a typed error, never
8931/// a wrong address, should a future arm bypass that pre-pass.
8932fn encode_mem_addr(addr: &MemAddr) -> Result<(u32, u32)> {
8933 check_a32_imm_addr(addr, A32_LDST_IMM12_MAX)
8934}
8935
8936/// `encode_mem_addr` for the `imm4H:imm4L` (8-bit) forms — `LDRH`/`STRH`/
8937/// `LDRSB`/`LDRSH`. Same tripwire contract; the former `& 0xFF` mask is gone.
8938fn encode_mem_addr_imm8(addr: &MemAddr) -> Result<(u32, u32)> {
8939 check_a32_imm_addr(addr, A32_LDST_IMM8_MAX)
8940}
8941
8942fn check_a32_imm_addr(addr: &MemAddr, imm_max: u32) -> Result<(u32, u32)> {
8943 if addr.offset_reg.is_some() {
8944 return Err(synth_core::Error::synthesis(
8945 "internal: A32 immediate-form load/store reached with a register offset — \
8946 encode_arm_reg_offset_mem must materialize it first (#206)",
8947 ));
8948 }
8949 let offset = addr.offset as u32;
8950 if offset > imm_max {
8951 return Err(synth_core::Error::synthesis(format!(
8952 "internal: A32 load/store immediate offset {offset:#x} exceeds the {imm_max:#x} \
8953 field — encode_arm_reg_offset_mem must materialize it first (RQ-63-ARMI64OFF)"
8954 )));
8955 }
8956 Ok((reg_to_bits(&addr.base), offset))
8957}
8958
8959/// S-register number: S0=0, S1=1, ..., S31=31
8960fn vfp_sreg_to_num(reg: &VfpReg) -> Result<u32> {
8961 match reg {
8962 VfpReg::S0 => Ok(0),
8963 VfpReg::S1 => Ok(1),
8964 VfpReg::S2 => Ok(2),
8965 VfpReg::S3 => Ok(3),
8966 VfpReg::S4 => Ok(4),
8967 VfpReg::S5 => Ok(5),
8968 VfpReg::S6 => Ok(6),
8969 VfpReg::S7 => Ok(7),
8970 VfpReg::S8 => Ok(8),
8971 VfpReg::S9 => Ok(9),
8972 VfpReg::S10 => Ok(10),
8973 VfpReg::S11 => Ok(11),
8974 VfpReg::S12 => Ok(12),
8975 VfpReg::S13 => Ok(13),
8976 VfpReg::S14 => Ok(14),
8977 VfpReg::S15 => Ok(15),
8978 VfpReg::S16 => Ok(16),
8979 VfpReg::S17 => Ok(17),
8980 VfpReg::S18 => Ok(18),
8981 VfpReg::S19 => Ok(19),
8982 VfpReg::S20 => Ok(20),
8983 VfpReg::S21 => Ok(21),
8984 VfpReg::S22 => Ok(22),
8985 VfpReg::S23 => Ok(23),
8986 VfpReg::S24 => Ok(24),
8987 VfpReg::S25 => Ok(25),
8988 VfpReg::S26 => Ok(26),
8989 VfpReg::S27 => Ok(27),
8990 VfpReg::S28 => Ok(28),
8991 VfpReg::S29 => Ok(29),
8992 VfpReg::S30 => Ok(30),
8993 VfpReg::S31 => Ok(31),
8994 // D-registers are not used in F32 single-precision encodings
8995 _ => Err(synth_core::Error::SynthesisError(
8996 "D-register not supported in single-precision VFP encoding".to_string(),
8997 )),
8998 }
8999}
9000
9001/// D-register number: D0=0, D1=1, ..., D15=15
9002fn vfp_dreg_to_num(reg: &VfpReg) -> Result<u32> {
9003 match reg {
9004 VfpReg::D0 => Ok(0),
9005 VfpReg::D1 => Ok(1),
9006 VfpReg::D2 => Ok(2),
9007 VfpReg::D3 => Ok(3),
9008 VfpReg::D4 => Ok(4),
9009 VfpReg::D5 => Ok(5),
9010 VfpReg::D6 => Ok(6),
9011 VfpReg::D7 => Ok(7),
9012 VfpReg::D8 => Ok(8),
9013 VfpReg::D9 => Ok(9),
9014 VfpReg::D10 => Ok(10),
9015 VfpReg::D11 => Ok(11),
9016 VfpReg::D12 => Ok(12),
9017 VfpReg::D13 => Ok(13),
9018 VfpReg::D14 => Ok(14),
9019 VfpReg::D15 => Ok(15),
9020 // S-registers are not used in F64 double-precision encodings
9021 _ => Err(synth_core::Error::SynthesisError(
9022 "S-register not supported in double-precision VFP encoding".to_string(),
9023 )),
9024 }
9025}
9026
9027/// Split S-register into (Vx[3:0], qualifier_bit) for VFP encoding.
9028/// For an S-register number s: Vx = s >> 1, qualifier = s & 1.
9029/// The qualifier bit goes to D (bit 22), N (bit 7), or M (bit 5) depending on role.
9030fn encode_sreg(s: u32) -> (u32, u32) {
9031 (s >> 1, s & 1)
9032}
9033
9034/// Split D-register into (Vx[3:0], qualifier_bit) for VFP double-precision encoding.
9035/// For a D-register number d: Vx = d & 0xF, qualifier = (d >> 4) & 1.
9036/// For D0-D15, qualifier is always 0.
9037fn encode_dreg(d: u32) -> (u32, u32) {
9038 (d & 0xF, (d >> 4) & 1)
9039}
9040
9041/// Encode a VFP 3-register arithmetic instruction (VADD.F32, VSUB.F32, VMUL.F32, VDIV.F32).
9042/// Returns the full 32-bit instruction word.
9043///
9044/// VFP encoding: [cond 1110] [D opc1 Vn] [Vd 101 sz] [N opc2 M 0 Vm]
9045/// For single-precision (sz=0), coprocessor = 0xA (bits[11:8]).
9046fn encode_vfp_3reg(base: u32, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<u32> {
9047 let sd_num = vfp_sreg_to_num(sd)?;
9048 let sn_num = vfp_sreg_to_num(sn)?;
9049 let sm_num = vfp_sreg_to_num(sm)?;
9050 let (vd, d) = encode_sreg(sd_num);
9051 let (vn, n) = encode_sreg(sn_num);
9052 let (vm, m) = encode_sreg(sm_num);
9053
9054 Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
9055}
9056
9057/// Encode a VFP 2-register instruction (VNEG.F32, VABS.F32, VSQRT.F32).
9058/// Returns the full 32-bit instruction word.
9059fn encode_vfp_2reg(base: u32, sd: &VfpReg, sm: &VfpReg) -> Result<u32> {
9060 let sd_num = vfp_sreg_to_num(sd)?;
9061 let sm_num = vfp_sreg_to_num(sm)?;
9062 let (vd, d) = encode_sreg(sd_num);
9063 let (vm, m) = encode_sreg(sm_num);
9064
9065 Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
9066}
9067
9068/// Encode a VFP load/store (VLDR.F32 / VSTR.F32).
9069/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
9070/// U bit (bit 23) controls add/subtract offset.
9071fn encode_vfp_ldst(base: u32, sd: &VfpReg, addr: &MemAddr) -> Result<u32> {
9072 let sd_num = vfp_sreg_to_num(sd)?;
9073 let (vd, d) = encode_sreg(sd_num);
9074 let rn = reg_to_bits(&addr.base);
9075
9076 let offset = addr.offset;
9077 let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9078 let abs_offset = offset.unsigned_abs();
9079 let imm8 = (abs_offset / 4) & 0xFF;
9080
9081 Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
9082}
9083
9084/// Encode VMOV between core register and S-register.
9085/// VMOV Sn, Rt: 0xEE00_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
9086/// VMOV Rt, Sn: 0xEE10_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
9087fn encode_vmov_core_sreg(to_sreg: bool, sreg: &VfpReg, core: &Reg) -> Result<u32> {
9088 let s_num = vfp_sreg_to_num(sreg)?;
9089 let (vn, n) = encode_sreg(s_num);
9090 let rt = reg_to_bits(core);
9091
9092 let base = if to_sreg { 0xEE000A10 } else { 0xEE100A10 };
9093 Ok(base | (vn << 16) | (rt << 12) | (n << 7))
9094}
9095
9096/// Encode a VFP 3-register double-precision instruction (VADD.F64, VSUB.F64, etc.).
9097/// For double-precision (sz=1), coprocessor = 0xB (bits[11:8]).
9098/// The base should have bit 8 = 1 for F64 (0xB suffix instead of 0xA).
9099fn encode_vfp_3reg_f64(base: u32, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<u32> {
9100 let dd_num = vfp_dreg_to_num(dd)?;
9101 let dn_num = vfp_dreg_to_num(dn)?;
9102 let dm_num = vfp_dreg_to_num(dm)?;
9103 let (vd, d) = encode_dreg(dd_num);
9104 let (vn, n) = encode_dreg(dn_num);
9105 let (vm, m) = encode_dreg(dm_num);
9106
9107 Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
9108}
9109
9110/// Encode a VFP 2-register double-precision instruction (VNEG.F64, VABS.F64, VSQRT.F64).
9111fn encode_vfp_2reg_f64(base: u32, dd: &VfpReg, dm: &VfpReg) -> Result<u32> {
9112 let dd_num = vfp_dreg_to_num(dd)?;
9113 let dm_num = vfp_dreg_to_num(dm)?;
9114 let (vd, d) = encode_dreg(dd_num);
9115 let (vm, m) = encode_dreg(dm_num);
9116
9117 Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
9118}
9119
9120/// Encode a VFP load/store for double-precision (VLDR.64 / VSTR.64).
9121/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
9122fn encode_vfp_ldst_f64(base: u32, dd: &VfpReg, addr: &MemAddr) -> Result<u32> {
9123 let dd_num = vfp_dreg_to_num(dd)?;
9124 let (vd, d) = encode_dreg(dd_num);
9125 let rn = reg_to_bits(&addr.base);
9126
9127 let offset = addr.offset;
9128 let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9129 let abs_offset = offset.unsigned_abs();
9130 let imm8 = (abs_offset / 4) & 0xFF;
9131
9132 Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
9133}
9134
9135/// Encode VMOV between two core registers and a D-register.
9136/// VMOV Dm, Rt, Rt2: 0xEC40_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
9137/// VMOV Rt, Rt2, Dm: 0xEC50_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
9138fn encode_vmov_core_dreg(
9139 to_dreg: bool,
9140 dreg: &VfpReg,
9141 core_lo: &Reg,
9142 core_hi: &Reg,
9143) -> Result<u32> {
9144 let d_num = vfp_dreg_to_num(dreg)?;
9145 let (vm, m) = encode_dreg(d_num);
9146 let rt = reg_to_bits(core_lo);
9147 let rt2 = reg_to_bits(core_hi);
9148
9149 let base = if to_dreg { 0xEC400B10 } else { 0xEC500B10 };
9150 Ok(base | (rt2 << 16) | (rt << 12) | (m << 5) | vm)
9151}
9152
9153/// Emit a VFP 32-bit instruction as Thumb-2 bytes (two LE halfwords).
9154fn vfp_to_thumb_bytes(instr: u32) -> Vec<u8> {
9155 let hw1 = ((instr >> 16) & 0xFFFF) as u16;
9156 let hw2 = (instr & 0xFFFF) as u16;
9157 let mut bytes = hw1.to_le_bytes().to_vec();
9158 bytes.extend_from_slice(&hw2.to_le_bytes());
9159 bytes
9160}
9161
9162// ============================================================================
9163// Helium MVE encoding helpers
9164// ============================================================================
9165
9166/// Q-register number: Q0=0, Q1=1, ..., Q7=7
9167fn qreg_to_num(reg: &QReg) -> u32 {
9168 match reg {
9169 QReg::Q0 => 0,
9170 QReg::Q1 => 1,
9171 QReg::Q2 => 2,
9172 QReg::Q3 => 3,
9173 QReg::Q4 => 4,
9174 QReg::Q5 => 5,
9175 QReg::Q6 => 6,
9176 QReg::Q7 => 7,
9177 }
9178}
9179
9180/// MVE element size to encoding bits: S8=0b00, S16=0b01, S32=0b10
9181fn mve_size_bits(size: &MveSize) -> u32 {
9182 match size {
9183 MveSize::S8 => 0b00,
9184 MveSize::S16 => 0b01,
9185 MveSize::S32 => 0b10,
9186 }
9187}
9188
9189/// Encode MVE 3-register instruction.
9190/// Q-registers are encoded as D-register pairs: Q0=D0:D1, Q1=D2:D3, etc.
9191/// In NEON/MVE encoding, the Q-register uses D-register number = Qn * 2.
9192fn encode_mve_3reg(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
9193 let d = qreg_to_num(qd) * 2;
9194 let n = qreg_to_num(qn) * 2;
9195 let m = qreg_to_num(qm) * 2;
9196
9197 // Standard NEON/MVE 3-register encoding:
9198 // D bit (bit 22) = Vd[4], Vd[3:0] = bits [15:12]
9199 // N bit (bit 7) = Vn[4], Vn[3:0] = bits [19:16]
9200 // M bit (bit 5) = Vm[4], Vm[3:0] = bits [3:0]
9201 let vd = d & 0xF;
9202 let d_bit = (d >> 4) & 1;
9203 let vn = n & 0xF;
9204 let n_bit = (n >> 4) & 1;
9205 let vm = m & 0xF;
9206 let m_bit = (m >> 4) & 1;
9207
9208 base | (d_bit << 22) | (vn << 16) | (vd << 12) | (n_bit << 7) | (m_bit << 5) | vm
9209}
9210
9211/// Encode MVE 3-register bitwise instruction (VAND, VORR, VEOR, VBIC).
9212fn encode_mve_3reg_bitwise(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
9213 encode_mve_3reg(base, qd, qn, qm)
9214}
9215
9216/// Encode MVE VLDRW.32 Qd, [Rn, #offset]
9217/// Format: EC9x xxxx - contiguous load, word-sized elements
9218fn encode_mve_vldrw(qd: &QReg, addr: &MemAddr) -> u32 {
9219 let qd_enc = qreg_to_num(qd) * 2;
9220 let rn = reg_to_bits(&addr.base);
9221 let offset = addr.offset;
9222 let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9223 let abs_offset = offset.unsigned_abs();
9224 let imm7 = (abs_offset / 4) & 0x7F; // 7-bit word-aligned offset
9225
9226 // VLDRW.32 Qd, [Rn, #imm]: ED10 xx80 variant
9227 0xED100E80
9228 | (u_bit << 23)
9229 | ((qd_enc >> 4) << 22)
9230 | (rn << 16)
9231 | ((qd_enc & 0xF) << 12)
9232 | (imm7 & 0x7F)
9233}
9234
9235/// Encode MVE VSTRW.32 Qd, [Rn, #offset]
9236fn encode_mve_vstrw(qd: &QReg, addr: &MemAddr) -> u32 {
9237 let qd_enc = qreg_to_num(qd) * 2;
9238 let rn = reg_to_bits(&addr.base);
9239 let offset = addr.offset;
9240 let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9241 let abs_offset = offset.unsigned_abs();
9242 let imm7 = (abs_offset / 4) & 0x7F;
9243
9244 0xED000E80
9245 | (u_bit << 23)
9246 | ((qd_enc >> 4) << 22)
9247 | (rn << 16)
9248 | ((qd_enc & 0xF) << 12)
9249 | (imm7 & 0x7F)
9250}
9251
9252impl ArmEncoder {
9253 /// Encode MVE constant load: MOVW+MOVT+VMOV for each 32-bit word, then assemble Q-register
9254 fn encode_thumb_mve_const(&self, qd: &QReg, bytes: &[u8; 16]) -> Result<Vec<u8>> {
9255 let mut result = Vec::new();
9256 let qd_num = qreg_to_num(qd);
9257
9258 // Load each 32-bit word into R12 (temp) then VMOV into S-register
9259 for i in 0..4 {
9260 let word = u32::from_le_bytes([
9261 bytes[i * 4],
9262 bytes[i * 4 + 1],
9263 bytes[i * 4 + 2],
9264 bytes[i * 4 + 3],
9265 ]);
9266 let lo16 = word & 0xFFFF;
9267 let hi16 = (word >> 16) & 0xFFFF;
9268
9269 // MOVW R12, #lo16
9270 result.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
9271 // MOVT R12, #hi16
9272 if hi16 != 0 {
9273 result.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
9274 }
9275
9276 // VMOV Sn, R12 where Sn = Qd*4 + i
9277 let s_num = qd_num * 4 + i as u32;
9278 let (vn, n) = encode_sreg(s_num);
9279 let vmov: u32 = 0xEE000A10 | (vn << 16) | (12 << 12) | (n << 7);
9280 result.extend_from_slice(&vfp_to_thumb_bytes(vmov));
9281 }
9282
9283 Ok(result)
9284 }
9285
9286 /// Encode lane-wise f32 binary operation (VDIV, etc.) via S-register extraction
9287 fn encode_thumb_mve_lane_wise_f32_binop(
9288 &self,
9289 qd: &QReg,
9290 qn: &QReg,
9291 qm: &QReg,
9292 vfp_base: u32,
9293 ) -> Result<Vec<u8>> {
9294 let mut result = Vec::new();
9295 let qd_num = qreg_to_num(qd);
9296 let qn_num = qreg_to_num(qn);
9297 let qm_num = qreg_to_num(qm);
9298
9299 // For each lane 0..3: use S-registers directly (Q aliasing)
9300 for i in 0..4u32 {
9301 let sd = qd_num * 4 + i;
9302 let sn = qn_num * 4 + i;
9303 let sm = qm_num * 4 + i;
9304
9305 let (vd, d) = encode_sreg(sd);
9306 let (vn, n) = encode_sreg(sn);
9307 let (vm, m) = encode_sreg(sm);
9308
9309 let instr = vfp_base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
9310 result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9311 }
9312
9313 Ok(result)
9314 }
9315
9316 /// Encode lane-wise f32 VSQRT via S-register extraction
9317 fn encode_thumb_mve_lane_wise_f32_sqrt(&self, qd: &QReg, qm: &QReg) -> Result<Vec<u8>> {
9318 let mut result = Vec::new();
9319 let qd_num = qreg_to_num(qd);
9320 let qm_num = qreg_to_num(qm);
9321
9322 // VSQRT.F32 base: 0xEEB10AC0
9323 for i in 0..4u32 {
9324 let sd = qd_num * 4 + i;
9325 let sm = qm_num * 4 + i;
9326
9327 let (vd, d) = encode_sreg(sd);
9328 let (vm, m) = encode_sreg(sm);
9329
9330 let instr: u32 = 0xEEB10AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
9331 result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9332 }
9333
9334 Ok(result)
9335 }
9336}
9337
9338/// VCR-TIER-001 (#1021/#1048) — the SCRATCH CONTRACT of an `ArmOp`'s encoder
9339/// expansion: the registers, beyond the op's declared RESULT registers and the
9340/// globally sanctioned R12/IP encoder scratch, that the expansion may leave
9341/// modified when it completes. Transient-but-restored traffic (push/pop through
9342/// the expansion's own stack red-zone, SP restored on exit) is not "modified".
9343///
9344/// This is the SINGLE declaration site for that contract — the one place a
9345/// future expansion that must borrow a register says so (the repo rule: one
9346/// declaration site, no duplicate copy that can silently drift). It is deliberately NOT derived
9347/// from the expansion's observed behavior: a contract read off the bytes would
9348/// rubber-stamp any clobber. Intent is declared here; the canary gate
9349/// (`scripts/repro/expansion_canary_gate_1021.py`) executes the REAL emitted
9350/// bytes of every variant the shipped rule table emits, on both backends, with
9351/// every non-contract register holding a distinctive canary, and fails on any
9352/// undeclared write — and on any declared register the expansion never
9353/// actually writes, so an over-broad declaration cannot hollow the gate.
9354///
9355/// The default is the STRICTEST reading — result registers only — which is
9356/// exactly the silent claim the atomic `ArmSemantics` pseudo-op model already
9357/// makes (#1021: an atomic model of a multi-instruction expansion is a silent
9358/// claim that the expansion is scratch-free). Today the table is EMPTY: #1039
9359/// reworked `i32.popcnt` off R11 (the linear-memory base) and #1048 reworked
9360/// the i64 shifts and bit-counts off their own operand registers, so every
9361/// expansion of every rule-emitted variant is R12-only on both Thumb-2 and
9362/// A32. Backend-independent for the same reason; if a backend's expansion ever
9363/// diverges, this signature grows a backend parameter in the same PR.
9364pub fn expansion_scratch_contract(op: &ArmOp) -> &'static [Reg] {
9365 // No variant currently borrows any register beyond R12. A new declaration
9366 // is added as a `match op { .. }` arm here — nowhere else.
9367 let _ = op;
9368 &[]
9369}
9370
9371#[cfg(test)]
9372mod tests {
9373 use super::*;
9374
9375 #[test]
9376 fn test_encoder_creation() {
9377 let encoder_arm = ArmEncoder::new_arm32();
9378 assert!(!encoder_arm.thumb_mode);
9379
9380 let encoder_thumb = ArmEncoder::new_thumb2();
9381 assert!(encoder_thumb.thumb_mode);
9382 }
9383
9384 /// #204 WAKE-path regression: `SetCond` materialized 0/1 with the 16-bit
9385 /// `MOVS Rd,#imm` (T1), whose Rd field is 3 bits (R0–R7). For a high Rd
9386 /// (R8–R12) `rd_bits << 8` overflows bit 11, flipping the opcode MOVS→CMP
9387 /// (`0x2c00`), so the boolean was never written — gale's `has_waiter` kept a
9388 /// stale value and the binary-sem WAKE dispatch read garbage. High Rd must
9389 /// use the 32-bit `MOV.W` (T2). Verify the bytes, not the IR.
9390 /// #311: the SAME high-Rd MOVS→CMP transmutation as #204, but in the
9391 /// i64 comparison expansions (I64SetCond / I64SetCondZ) — missed by the
9392 /// #204 hardening. With rd=R8 the boolean died in the flags
9393 /// (`ite eq; cmpeq r0,#1; cmpne r0,#0`), so gale's packed-u64 select
9394 /// read a stale register on silicon. High Rd must take MOV.W / CMP.W.
9395 #[test]
9396 fn test_encode_i64setcond_high_reg_uses_mov_w_311() {
9397 use synth_synthesis::{ArmOp, Condition, Reg};
9398 let enc = ArmEncoder::new_thumb2();
9399 let bytes = enc
9400 .encode(&ArmOp::I64SetCond {
9401 rd: Reg::R8,
9402 rn_lo: Reg::R2,
9403 rn_hi: Reg::R3,
9404 rm_lo: Reg::R6,
9405 rm_hi: Reg::R7,
9406 cond: Condition::EQ,
9407 })
9408 .unwrap();
9409 // The 32-bit MOV.W immediate (T2) first halfword is 0xF04F; the
9410 // 16-bit transmuted forms would contain 0x2801/0x2800 (CMP r0,#1/#0).
9411 let halfwords: Vec<u16> = bytes
9412 .chunks(2)
9413 .map(|c| u16::from_le_bytes([c[0], c[1]]))
9414 .collect();
9415 assert!(
9416 halfwords.iter().filter(|&&h| h == 0xF04F).count() == 2,
9417 "high rd must use two MOV.W (T2) encodings, got {halfwords:04x?}"
9418 );
9419 assert!(
9420 !halfwords.contains(&0x2801) && !halfwords.contains(&0x2800),
9421 "no transmuted 16-bit CMP imm: {halfwords:04x?}"
9422 );
9423
9424 let bytes_z = enc
9425 .encode(&ArmOp::I64SetCondZ {
9426 rd: Reg::R8,
9427 rn_lo: Reg::R2,
9428 rn_hi: Reg::R3,
9429 })
9430 .unwrap();
9431 let hw_z: Vec<u16> = bytes_z
9432 .chunks(2)
9433 .map(|c| u16::from_le_bytes([c[0], c[1]]))
9434 .collect();
9435 assert!(
9436 hw_z.iter().filter(|&&h| h == 0xF04F).count() == 2,
9437 "SetCondZ high rd MOV.W: {hw_z:04x?}"
9438 );
9439 // CMP.W rd,#0 (T2) first halfword: 0xF1B0 | rd
9440 assert!(
9441 hw_z.contains(&(0xF1B0 | 8)),
9442 "SetCondZ high rd must use CMP.W: {hw_z:04x?}"
9443 );
9444 }
9445
9446 #[test]
9447 fn test_encode_setcond_high_reg_uses_mov_w_204() {
9448 use synth_synthesis::{ArmOp, Condition, Reg};
9449 let enc = ArmEncoder::new_thumb2();
9450 // R12 (high): must be ITE + MOV.W #1 + MOV.W #0, never a 16-bit MOVS/CMP.
9451 let hi = enc
9452 .encode(&ArmOp::SetCond {
9453 rd: Reg::R12,
9454 cond: Condition::NE,
9455 })
9456 .unwrap();
9457 assert_eq!(hi.len(), 10, "ITE(2) + MOV.W(4) + MOV.W(4): {hi:02x?}");
9458 // both value halfwords are MOV.W (0xF04F) — NOT the corrupt CMP (0x2c..).
9459 assert_eq!(&hi[2..4], &[0x4F, 0xF0], "then = MOV.W: {hi:02x?}");
9460 assert_eq!(&hi[6..8], &[0x4F, 0xF0], "else = MOV.W: {hi:02x?}");
9461 assert_eq!(hi[4] & 0x0F, 0x01, "then imm = #1");
9462 assert_eq!(hi[8] & 0x0F, 0x00, "else imm = #0");
9463 // Low Rd keeps the compact 16-bit MOVS form.
9464 let lo = enc
9465 .encode(&ArmOp::SetCond {
9466 rd: Reg::R0,
9467 cond: Condition::NE,
9468 })
9469 .unwrap();
9470 assert_eq!(lo.len(), 6, "ITE(2) + MOVS(2) + MOVS(2): {lo:02x?}");
9471 assert_eq!(lo[2..4], [0x01, 0x20], "then = MOVS R0,#1");
9472 assert_eq!(lo[4..6], [0x00, 0x20], "else = MOVS R0,#0");
9473 }
9474
9475 /// #209 Opt 1b: UMULL RdLo, RdHi, Rn, Rm encodes correctly on both ISAs.
9476 /// Thumb-2 T1: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm.
9477 /// A32: cond 0000 1000 RdHi RdLo Rm 1001 Rn.
9478 #[test]
9479 fn test_encode_umull_209b() {
9480 use synth_synthesis::{ArmOp, Reg};
9481 let op = ArmOp::Umull {
9482 rdlo: Reg::R4,
9483 rdhi: Reg::R5,
9484 rn: Reg::R0,
9485 rm: Reg::R3,
9486 };
9487 // Thumb-2: hw1 = 0xFBA0 | 0 = 0xFBA0; hw2 = (4<<12)|(5<<8)|3 = 0x4503.
9488 let t = ArmEncoder::new_thumb2().encode(&op).unwrap();
9489 assert_eq!(
9490 t,
9491 vec![0xA0, 0xFB, 0x03, 0x45],
9492 "umull r4,r5,r0,r3 (T2): {t:02x?}"
9493 );
9494 // A32: 0xE0800090 | (5<<16) | (4<<12) | (3<<8) | 0 = 0xE0854390.
9495 let a = ArmEncoder::new_arm32().encode(&op).unwrap();
9496 assert_eq!(
9497 a,
9498 0xE085_4390u32.to_le_bytes().to_vec(),
9499 "umull (A32): {a:02x?}"
9500 );
9501 }
9502
9503 /// #206 regression: the ARM32 (A32) `Ldr`/`Str` encoders fed `addr` through
9504 /// `encode_mem_addr`, which returns only the 12-bit immediate — so a register
9505 /// offset (`[rn, rm, #off]`) was silently dropped to `[rn, #off]`, sending
9506 /// the access to the wrong runtime address (silent miscompile on the default
9507 /// `--target arm`). A register offset must materialize `ip = rn + rm` and
9508 /// load from `[ip, #off]`. Verify the bytes.
9509 #[test]
9510 fn test_encode_arm32_indexed_load_keeps_index_206() {
9511 use synth_synthesis::{ArmOp, MemAddr, Reg};
9512 let enc = ArmEncoder::new_arm32();
9513 // ldr r0, [r11, r1, #8] must NOT collapse to a single immediate ldr.
9514 let bytes = enc
9515 .encode(&ArmOp::Ldr {
9516 rd: Reg::R0,
9517 addr: MemAddr::reg_imm(Reg::R11, Reg::R1, 8),
9518 })
9519 .unwrap();
9520 assert_eq!(
9521 bytes.len(),
9522 8,
9523 "expected ADD ip + LDR (2 words): {bytes:02x?}"
9524 );
9525 let add = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9526 let ldr = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9527 // ADD ip, r11, r1 = 0xE08BC001
9528 assert_eq!(add, 0xE08B_C001, "ADD ip,r11,r1: {add:#010x}");
9529 // LDR r0, [ip, #8] = 0xE59C0008
9530 assert_eq!(ldr, 0xE59C_0008, "LDR r0,[ip,#8]: {ldr:#010x}");
9531 // A bare immediate ldr (the bug) would be 0xE59B0008 (base=r11) — reject.
9532 assert_ne!(ldr, 0xE59B_0008, "index must not be dropped");
9533 }
9534
9535 /// #594 regression: `call_indirect` on the A32 path (`--target cortex-r5`)
9536 /// was encoded as a literal NOP (0xE1A00000) — the call never happened and
9537 /// the function silently returned the leftover table-index value. The A32
9538 /// encoder must emit a real dispatch expansion, since #642 guarded by an
9539 /// inline bounds check:
9540 /// `MOVW r12, #size; CMP idx, r12; BLO +1; UDF;
9541 /// MOV r12, idx, LSL #2; LDR r12, [r11, r12]; BLX r12`.
9542 #[test]
9543 fn test_encode_arm32_call_indirect_is_real_call_594() {
9544 use synth_synthesis::{ArmOp, Reg};
9545 let enc = ArmEncoder::new_arm32();
9546 let bytes = enc
9547 .encode(&ArmOp::CallIndirect {
9548 rd: Reg::R0,
9549 type_idx: 0,
9550 table_index_reg: Reg::R0,
9551 table_size: 4,
9552 table_byte_offset: 0,
9553 null_check: false,
9554 type_check: None,
9555 })
9556 .unwrap();
9557 assert_eq!(
9558 bytes.len(),
9559 28,
9560 "expected MOVW + CMP + BLO + UDF + MOV + LDR + BLX (7 words): {bytes:02x?}"
9561 );
9562 let words: Vec<u32> = bytes
9563 .as_chunks::<4>()
9564 .0
9565 .iter()
9566 .map(|&w| u32::from_le_bytes(w))
9567 .collect();
9568 // #642 bounds guard: MOVW r12, #4; CMP r0, r12; BLO +1; UDF
9569 assert_eq!(words[0], 0xE300_C004, "MOVW r12,#4: {:#010x}", words[0]);
9570 assert_eq!(words[1], 0xE150_000C, "CMP r0,r12: {:#010x}", words[1]);
9571 assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9572 assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9573 // MOV r12, r0, LSL #2 = 0xE1A0C100
9574 assert_eq!(
9575 words[4], 0xE1A0_C100,
9576 "MOV r12,r0,LSL#2: {:#010x}",
9577 words[4]
9578 );
9579 // LDR r12, [r11, r12] = 0xE79BC00C
9580 assert_eq!(
9581 words[5], 0xE79B_C00C,
9582 "LDR r12,[r11,r12]: {:#010x}",
9583 words[5]
9584 );
9585 // BLX r12 = 0xE12FFF3C
9586 assert_eq!(words[6], 0xE12F_FF3C, "BLX r12: {:#010x}", words[6]);
9587 // The bug: a single NOP word. Must never come back.
9588 assert!(
9589 !bytes
9590 .as_chunks::<4>()
9591 .0
9592 .iter()
9593 .any(|&w| w == 0xE1A0_0000u32.to_le_bytes()),
9594 "call_indirect must not contain a NOP (#594): {bytes:02x?}"
9595 );
9596
9597 // A non-R0 index register lands in the MOV's Rm and CMP's Rn fields.
9598 let bytes = enc
9599 .encode(&ArmOp::CallIndirect {
9600 rd: Reg::R0,
9601 type_idx: 0,
9602 table_index_reg: Reg::R4,
9603 table_size: 4,
9604 table_byte_offset: 0,
9605 null_check: false,
9606 type_check: None,
9607 })
9608 .unwrap();
9609 let cmp = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9610 assert_eq!(cmp, 0xE154_000C, "CMP r4,r12: {cmp:#010x}");
9611 let mov = u32::from_le_bytes(bytes[16..20].try_into().unwrap());
9612 assert_eq!(mov, 0xE1A0_C104, "MOV r12,r4,LSL#2: {mov:#010x}");
9613 }
9614
9615 /// #642: a table size above 16 bits must not be silently truncated by the
9616 /// MOVW — the A32 guard adds a MOVT for the high half.
9617 #[test]
9618 fn test_encode_arm32_call_indirect_wide_table_size_642() {
9619 use synth_synthesis::{ArmOp, Reg};
9620 let enc = ArmEncoder::new_arm32();
9621 let bytes = enc
9622 .encode(&ArmOp::CallIndirect {
9623 rd: Reg::R0,
9624 type_idx: 0,
9625 table_index_reg: Reg::R0,
9626 table_size: 0x0002_0003,
9627 table_byte_offset: 0,
9628 null_check: false,
9629 type_check: None,
9630 })
9631 .unwrap();
9632 assert_eq!(bytes.len(), 32, "MOVT arm adds one word: {bytes:02x?}");
9633 let movw = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9634 let movt = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9635 assert_eq!(movw, 0xE300_C003, "MOVW r12,#3: {movw:#010x}");
9636 assert_eq!(movt, 0xE340_C002, "MOVT r12,#2: {movt:#010x}");
9637 }
9638
9639 /// #597 anchor (justified correctness RE-PIN of the #594-era freeze): the
9640 /// Thumb-2 `CallIndirect` expansion is `mov.w ip, rm, LSL #2; ldr.w ip,
9641 /// [r11, ip]; blx ip`.
9642 ///
9643 /// The #594 PR froze the then-current bytes `4F EA 20 0C ...` whose first
9644 /// word decodes as `mov.w ip, rm, ASR #32` — the intended `LSL #2` had
9645 /// its shift amount in the TYPE field (bits 5:4) instead of imm2 (bits
9646 /// 7:6), so the index was destroyed and every call_indirect dispatched
9647 /// table entry 0 (shipped miscompile, masked by index-0 probes). #597
9648 /// corrects the encoding; new bytes `4F EA 80 0C ...` were
9649 /// execution-validated under unicorn against the wasmtime oracle on a
9650 /// multi-entry table (indexes 0, 1, 3 —
9651 /// scripts/repro/call_indirect_597_differential.py) before this pin was
9652 /// replaced. Old pin: [4F EA 20 0C, 5B F8 0C C0, E0 47] (ASR #32 — must
9653 /// never come back).
9654 #[test]
9655 fn test_encode_thumb_call_indirect_lsl2_597() {
9656 use synth_synthesis::{ArmOp, Reg};
9657 let enc = ArmEncoder::new_thumb2();
9658 let bytes = enc
9659 .encode(&ArmOp::CallIndirect {
9660 rd: Reg::R0,
9661 type_idx: 0,
9662 table_index_reg: Reg::R0,
9663 table_size: 4,
9664 table_byte_offset: 0,
9665 null_check: false,
9666 type_check: None,
9667 })
9668 .unwrap();
9669 assert_eq!(
9670 bytes,
9671 vec![
9672 // #642 bounds guard: movw ip,#4; cmp r0,ip; blo +1; udf #0
9673 0x40, 0xF2, 0x04, 0x0C, // movw ip, #4
9674 0x60, 0x45, // cmp r0, ip
9675 0x00, 0xD3, // blo .+4 (skip the udf)
9676 0x00, 0xDE, // udf #0 — OOB index trap (WASM §4.4.8)
9677 // #597-pinned dispatch
9678 0x4F, 0xEA, 0x80, 0x0C, // mov.w ip, r0, lsl #2
9679 0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9680 0xE0, 0x47, // blx ip
9681 ],
9682 "Thumb-2 CallIndirect: bounds guard + mov.w/ldr.w/blx dispatch: {bytes:02x?}"
9683 );
9684 // The #597 bug bytes (ASR #32 dispatch first word) must never come back.
9685 assert!(
9686 !bytes.windows(4).any(|w| w == [0x4F, 0xEA, 0x20, 0x0C]),
9687 "mov.w ip, rm, ASR #32 — the #597 type-field bug"
9688 );
9689
9690 // A non-R0 index register lands in the mov.w's Rm field (hw2 bits 3:0)
9691 // and the cmp's Rn field.
9692 let bytes = enc
9693 .encode(&ArmOp::CallIndirect {
9694 rd: Reg::R0,
9695 type_idx: 0,
9696 table_index_reg: Reg::R4,
9697 table_size: 4,
9698 table_byte_offset: 0,
9699 null_check: false,
9700 type_check: None,
9701 })
9702 .unwrap();
9703 assert_eq!(&bytes[4..6], &[0x64, 0x45], "cmp r4, ip: {bytes:02x?}");
9704 assert_eq!(
9705 &bytes[10..14],
9706 &[0x4F, 0xEA, 0x84, 0x0C],
9707 "mov.w ip, r4, LSL #2: {bytes:02x?}"
9708 );
9709 }
9710
9711 /// #642: the Thumb-2 bounds guard for a high-register index (R8 — the top
9712 /// of the allocatable pool) uses the high-reg-capable 16-bit CMP (T2) with
9713 /// the N bit set; a table size above 16 bits adds a MOVT.
9714 #[test]
9715 fn test_encode_thumb_call_indirect_guard_shapes_642() {
9716 use synth_synthesis::{ArmOp, Reg};
9717 let enc = ArmEncoder::new_thumb2();
9718 let bytes = enc
9719 .encode(&ArmOp::CallIndirect {
9720 rd: Reg::R0,
9721 type_idx: 0,
9722 table_index_reg: Reg::R8,
9723 table_size: 3,
9724 table_byte_offset: 0,
9725 null_check: false,
9726 type_check: None,
9727 })
9728 .unwrap();
9729 // cmp r8, ip — T2: 0x4500 | N(1)<<7 | Rm(12)<<3 | Rn(0) = 0x45E0
9730 assert_eq!(&bytes[4..6], &[0xE0, 0x45], "cmp r8, ip: {bytes:02x?}");
9731
9732 let bytes = enc
9733 .encode(&ArmOp::CallIndirect {
9734 rd: Reg::R0,
9735 type_idx: 0,
9736 table_index_reg: Reg::R0,
9737 table_size: 0x0002_0003,
9738 table_byte_offset: 0,
9739 null_check: false,
9740 type_check: None,
9741 })
9742 .unwrap();
9743 // movw ip,#3 then movt ip,#2 — the size must not be truncated.
9744 assert_eq!(
9745 &bytes[0..8],
9746 &[0x40, 0xF2, 0x03, 0x0C, 0xC0, 0xF2, 0x02, 0x0C],
9747 "movw ip,#3; movt ip,#2: {bytes:02x?}"
9748 );
9749 }
9750
9751 /// #650: a non-zero table base offset (table N of the contiguous R11
9752 /// region) routes the Thumb-2 pointer load through
9753 /// `add.w ip, r11, ip; ldr.w ip, [ip, #offset]` — and offset 0 keeps the
9754 /// pre-#650 single-load bytes IDENTICAL (the by-construction pin).
9755 #[test]
9756 fn test_encode_thumb_call_indirect_table_offset_650() {
9757 use synth_synthesis::{ArmOp, Reg};
9758 let enc = ArmEncoder::new_thumb2();
9759 // falcon's fused-component shape: table 0 has 7 entries, so table 1
9760 // sits at byte offset 28.
9761 let bytes = enc
9762 .encode(&ArmOp::CallIndirect {
9763 rd: Reg::R0,
9764 type_idx: 0,
9765 table_index_reg: Reg::R1,
9766 table_size: 41,
9767 table_byte_offset: 28,
9768 null_check: false,
9769 type_check: None,
9770 })
9771 .unwrap();
9772 assert_eq!(
9773 bytes,
9774 vec![
9775 // #642 bounds guard against TABLE 1's OWN size (41)
9776 0x40, 0xF2, 0x29, 0x0C, // movw ip, #41
9777 0x61, 0x45, // cmp r1, ip
9778 0x00, 0xD3, // blo .+4 (skip the udf)
9779 0x00, 0xDE, // udf #0 — OOB trap (WASM §4.4.8)
9780 // dispatch through table 1's base (R11 + 28)
9781 0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9782 0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9783 0xDC, 0xF8, 0x1C, 0xC0, // ldr.w ip, [ip, #28]
9784 0xE0, 0x47, // blx ip
9785 ],
9786 "Thumb-2 table-1 dispatch (#650): {bytes:02x?}"
9787 );
9788
9789 // Offset 0 must stay the #597-pinned single-load form (no add.w, no
9790 // imm-form ldr) — single-table byte identity by construction.
9791 let zero = enc
9792 .encode(&ArmOp::CallIndirect {
9793 rd: Reg::R0,
9794 type_idx: 0,
9795 table_index_reg: Reg::R1,
9796 table_size: 41,
9797 table_byte_offset: 0,
9798 null_check: false,
9799 type_check: None,
9800 })
9801 .unwrap();
9802 assert_eq!(
9803 &zero[10..],
9804 &[
9805 0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9806 0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9807 0xE0, 0x47, // blx ip
9808 ],
9809 "offset 0 keeps the pre-#650 dispatch bytes: {zero:02x?}"
9810 );
9811 }
9812
9813 /// #650: the A32 twin — `add r12, r11, r12; ldr r12, [r12, #offset]` for
9814 /// a non-zero table base offset; offset 0 keeps the #594/#642 form.
9815 #[test]
9816 fn test_encode_arm32_call_indirect_table_offset_650() {
9817 use synth_synthesis::{ArmOp, Reg};
9818 let enc = ArmEncoder::new_arm32();
9819 let bytes = enc
9820 .encode(&ArmOp::CallIndirect {
9821 rd: Reg::R0,
9822 type_idx: 0,
9823 table_index_reg: Reg::R1,
9824 table_size: 41,
9825 table_byte_offset: 28,
9826 null_check: false,
9827 type_check: None,
9828 })
9829 .unwrap();
9830 let words: Vec<u32> = bytes
9831 .as_chunks::<4>()
9832 .0
9833 .iter()
9834 .map(|&w| u32::from_le_bytes(w))
9835 .collect();
9836 assert_eq!(words[0], 0xE300_C029, "MOVW r12,#41: {:#010x}", words[0]);
9837 assert_eq!(words[1], 0xE151_000C, "CMP r1,r12: {:#010x}", words[1]);
9838 assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9839 assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9840 assert_eq!(
9841 words[4], 0xE1A0_C101,
9842 "MOV r12,r1,LSL#2: {:#010x}",
9843 words[4]
9844 );
9845 assert_eq!(
9846 words[5], 0xE08B_C00C,
9847 "ADD r12,r11,r12 (#650): {:#010x}",
9848 words[5]
9849 );
9850 assert_eq!(
9851 words[6], 0xE59C_C01C,
9852 "LDR r12,[r12,#28] (#650): {:#010x}",
9853 words[6]
9854 );
9855 assert_eq!(words[7], 0xE12F_FF3C, "BLX r12: {:#010x}", words[7]);
9856 }
9857
9858 /// #664: `null_check` inserts a null-funcref trap between the Thumb-2
9859 /// pointer load and the `BLX` (`cmp.w ip, #0; bne .+4; udf #0`) — a
9860 /// zero-linked (uninitialized) slot must TRAP (WASM §4.4.8), never
9861 /// branch to address 0. `null_check: false` keeps the expansion
9862 /// byte-identical to the pre-#664 form (by-construction pin).
9863 #[test]
9864 fn test_encode_thumb_call_indirect_null_check_664() {
9865 use synth_synthesis::{ArmOp, Reg};
9866 let enc = ArmEncoder::new_thumb2();
9867 let op = |null_check| ArmOp::CallIndirect {
9868 rd: Reg::R0,
9869 type_idx: 0,
9870 table_index_reg: Reg::R1,
9871 table_size: 4,
9872 table_byte_offset: 0,
9873 null_check,
9874 type_check: None,
9875 };
9876 let with = enc.encode(&op(true)).unwrap();
9877 let without = enc.encode(&op(false)).unwrap();
9878 // The checked form = the unchecked form with EXACTLY the three-insn
9879 // null check spliced in before the final BLX (byte identity of the
9880 // shared prefix/suffix — nothing else may move).
9881 assert_eq!(
9882 with.len(),
9883 without.len() + 8,
9884 "cmp.w (4) + bne (2) + udf (2): {with:02x?}"
9885 );
9886 let blx_at = without.len() - 2;
9887 assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9888 assert_eq!(
9889 &with[blx_at..],
9890 &[
9891 0xBC, 0xF1, 0x00, 0x0F, // cmp.w ip, #0
9892 0x00, 0xD1, // bne .+4 (skip the udf)
9893 0x00, 0xDE, // udf #0 — null-funcref trap (#664)
9894 0xE0, 0x47, // blx ip
9895 ],
9896 "null check precedes the BLX: {with:02x?}"
9897 );
9898 assert_eq!(&with[with.len() - 2..], &without[blx_at..], "same BLX");
9899 }
9900
9901 /// #664: the A32 twin — `cmp r12, #0; bne .+8; udf` before the `BLX`;
9902 /// `null_check: false` keeps the #594/#642/#650 bytes identical.
9903 #[test]
9904 fn test_encode_arm32_call_indirect_null_check_664() {
9905 use synth_synthesis::{ArmOp, Reg};
9906 let enc = ArmEncoder::new_arm32();
9907 let op = |null_check| ArmOp::CallIndirect {
9908 rd: Reg::R0,
9909 type_idx: 0,
9910 table_index_reg: Reg::R1,
9911 table_size: 4,
9912 table_byte_offset: 0,
9913 null_check,
9914 type_check: None,
9915 };
9916 let with = enc.encode(&op(true)).unwrap();
9917 let without = enc.encode(&op(false)).unwrap();
9918 assert_eq!(with.len(), without.len() + 12, "3 A32 words: {with:02x?}");
9919 let blx_at = without.len() - 4;
9920 assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9921 let words: Vec<u32> = with[blx_at..]
9922 .as_chunks::<4>()
9923 .0
9924 .iter()
9925 .map(|&w| u32::from_le_bytes(w))
9926 .collect();
9927 assert_eq!(words[0], 0xE35C_0000, "CMP r12,#0: {:#010x}", words[0]);
9928 assert_eq!(words[1], 0x1A00_0000, "BNE +1 insn: {:#010x}", words[1]);
9929 assert_eq!(words[2], 0xE7F0_00F0, "UDF (null trap): {:#010x}", words[2]);
9930 assert_eq!(words[3], 0xE12F_FF3C, "BLX r12: {:#010x}", words[3]);
9931 }
9932
9933 /// #676: `type_check` splices the runtime type check — scale the index,
9934 /// load the slot's structural class id from the type-id sidecar
9935 /// (`ldr.w ip, [ip, #type_off]`), compare against the expected class id
9936 /// and trap on mismatch (WASM §4.4.8) — between the bounds guard and
9937 /// the dispatch tail. `type_check: None` keeps the expansion
9938 /// byte-identical to the pre-#676 form (by-construction pin, the same
9939 /// trick as #650 offset-0 / #664 `null_check: false`).
9940 #[test]
9941 fn test_encode_thumb_call_indirect_type_check_676() {
9942 use synth_synthesis::{ArmOp, Reg};
9943 let enc = ArmEncoder::new_thumb2();
9944 let op = |type_check| ArmOp::CallIndirect {
9945 rd: Reg::R0,
9946 type_idx: 1,
9947 table_index_reg: Reg::R1,
9948 table_size: 5,
9949 table_byte_offset: 0,
9950 null_check: false,
9951 type_check,
9952 };
9953 let with = enc.encode(&op(Some((2, 20)))).unwrap();
9954 let without = enc.encode(&op(None)).unwrap();
9955 // The checked form = the unchecked form with EXACTLY the six-insn
9956 // type check spliced in after the bounds guard (byte identity of
9957 // the shared prefix/suffix — nothing else may move).
9958 assert_eq!(
9959 with.len(),
9960 without.len() + 20,
9961 "lsl.w(4)+add.w(4)+ldr.w(4)+cmp.w(4)+beq(2)+udf(2): {with:02x?}"
9962 );
9963 // Bounds guard: movw(4) + cmp(2) + blo(2) + udf(2) = 10 bytes.
9964 let guard_end = 10;
9965 assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
9966 assert_eq!(
9967 &with[guard_end..guard_end + 20],
9968 &[
9969 0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9970 0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9971 0xDC, 0xF8, 0x14, 0xC0, // ldr.w ip, [ip, #20] — sidecar slot id
9972 0xBC, 0xF1, 0x02, 0x0F, // cmp.w ip, #2 — expected class id
9973 0x00, 0xD0, // beq .+4 (skip the udf on a match)
9974 0x00, 0xDE, // udf #0 — §4.4.8 type-mismatch trap (#676)
9975 ],
9976 "type check follows the bounds guard: {with:02x?}"
9977 );
9978 assert_eq!(
9979 &with[guard_end + 20..],
9980 &without[guard_end..],
9981 "dispatch tail unchanged (idx*4 recomputed)"
9982 );
9983 }
9984
9985 /// #676: the A32 twin — `mov r12, idx, lsl #2; add r12, r11, r12;
9986 /// ldr r12, [r12, #type_off]; cmp r12, #id; beq .+8; udf` after the
9987 /// bounds guard; `type_check: None` keeps the #594/#642/#650/#664
9988 /// bytes identical.
9989 #[test]
9990 fn test_encode_arm32_call_indirect_type_check_676() {
9991 use synth_synthesis::{ArmOp, Reg};
9992 let enc = ArmEncoder::new_arm32();
9993 let op = |type_check| ArmOp::CallIndirect {
9994 rd: Reg::R0,
9995 type_idx: 1,
9996 table_index_reg: Reg::R1,
9997 table_size: 5,
9998 table_byte_offset: 0,
9999 null_check: false,
10000 type_check,
10001 };
10002 let with = enc.encode(&op(Some((2, 20)))).unwrap();
10003 let without = enc.encode(&op(None)).unwrap();
10004 assert_eq!(with.len(), without.len() + 24, "6 A32 words: {with:02x?}");
10005 // Bounds guard: movw + cmp + blo + udf = 4 words = 16 bytes.
10006 let guard_end = 16;
10007 assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
10008 let words: Vec<u32> = with[guard_end..guard_end + 24]
10009 .as_chunks::<4>()
10010 .0
10011 .iter()
10012 .map(|&w| u32::from_le_bytes(w))
10013 .collect();
10014 assert_eq!(
10015 words[0], 0xE1A0_C101,
10016 "MOV r12,r1,LSL#2: {:#010x}",
10017 words[0]
10018 );
10019 assert_eq!(words[1], 0xE08B_C00C, "ADD r12,r11,r12: {:#010x}", words[1]);
10020 assert_eq!(
10021 words[2], 0xE59C_C014,
10022 "LDR r12,[r12,#20] (sidecar): {:#010x}",
10023 words[2]
10024 );
10025 assert_eq!(
10026 words[3], 0xE35C_0002,
10027 "CMP r12,#2 (expected class id): {:#010x}",
10028 words[3]
10029 );
10030 assert_eq!(words[4], 0x0A00_0000, "BEQ +1 insn: {:#010x}", words[4]);
10031 assert_eq!(
10032 words[5], 0xE7F0_00F0,
10033 "UDF (type-mismatch trap): {:#010x}",
10034 words[5]
10035 );
10036 assert_eq!(
10037 &with[guard_end + 24..],
10038 &without[guard_end..],
10039 "dispatch tail unchanged"
10040 );
10041 }
10042
10043 /// #178/#180 regression: the Thumb `Add`/`Adds`/`Subs` reg-forms used the
10044 /// 16-bit encoding unconditionally. For high registers (R12 base scratch,
10045 /// R8-R11 i64 pairs) the 3-bit register fields overflow and corrupt the
10046 /// operands — `add ip,ip,r0` came out as `adds r4,r5,r1` (0x186C), silently
10047 /// dropping the address operand and miscompiling every optimized memory
10048 /// access. High registers must use the 32-bit `.W` forms.
10049 #[test]
10050 fn test_encode_thumb_add_high_reg_uses_add_w_178_180() {
10051 let encoder = ArmEncoder::new_thumb2();
10052
10053 // add ip, ip, r0 — the exact MemLoad/MemStore base+addr op.
10054 let code = encoder
10055 .encode(&ArmOp::Add {
10056 rd: Reg::R12,
10057 rn: Reg::R12,
10058 op2: Operand2::Reg(Reg::R0),
10059 })
10060 .unwrap();
10061 // ADD.W ip, ip, r0 = EB0C 0C00 (little-endian halfwords).
10062 assert_eq!(
10063 code,
10064 vec![0x0C, 0xEB, 0x00, 0x0C],
10065 "high-reg Thumb ADD must be 32-bit ADD.W (EB0C 0C00), not corrupt 16-bit; got {code:02X?}"
10066 );
10067 // Must NOT be the buggy 16-bit 0x186C (`adds r4,r5,r1`).
10068 assert_ne!(code, vec![0x6C, 0x18], "regressed to corrupt 16-bit ADDS");
10069
10070 // Low-register add stays 16-bit (no regression for the common case).
10071 let lo = encoder
10072 .encode(&ArmOp::Add {
10073 rd: Reg::R1,
10074 rn: Reg::R2,
10075 op2: Operand2::Reg(Reg::R3),
10076 })
10077 .unwrap();
10078 assert_eq!(
10079 lo.len(),
10080 2,
10081 "low-reg ADD should remain 16-bit, got {lo:02X?}"
10082 );
10083 }
10084
10085 /// #178/#180 sibling: i64 low-word `Adds`/`Subs` can land in R8-R11 pairs;
10086 /// those must fall back to 32-bit ADDS.W/SUBS.W (flag-setting preserved).
10087 #[test]
10088 fn test_encode_thumb_adds_subs_high_reg_use_32bit_178_180() {
10089 let encoder = ArmEncoder::new_thumb2();
10090
10091 // adds r10, r10, r8 → ADDS.W = EB1A 0A08
10092 let adds = encoder
10093 .encode(&ArmOp::Adds {
10094 rd: Reg::R10,
10095 rn: Reg::R10,
10096 op2: Operand2::Reg(Reg::R8),
10097 })
10098 .unwrap();
10099 assert_eq!(
10100 adds,
10101 vec![0x1A, 0xEB, 0x08, 0x0A],
10102 "high-reg ADDS must be 32-bit ADDS.W (EB1A 0A08); got {adds:02X?}"
10103 );
10104
10105 // subs r10, r10, r8 → SUBS.W = EBBA 0A08
10106 let subs = encoder
10107 .encode(&ArmOp::Subs {
10108 rd: Reg::R10,
10109 rn: Reg::R10,
10110 op2: Operand2::Reg(Reg::R8),
10111 })
10112 .unwrap();
10113 assert_eq!(
10114 subs,
10115 vec![0xBA, 0xEB, 0x08, 0x0A],
10116 "high-reg SUBS must be 32-bit SUBS.W (EBBA 0A08); got {subs:02X?}"
10117 );
10118 }
10119
10120 /// #184 (sibling of #180): 16-bit CMN (T1) only encodes R0-R7. High registers
10121 /// must use 32-bit CMN.W, not the corrupt truncated 16-bit form.
10122 #[test]
10123 fn test_encode_thumb_cmn_high_reg_uses_cmn_w_184() {
10124 let encoder = ArmEncoder::new_thumb2();
10125
10126 // cmn r10, r8 → CMN.W = EB1A 0F08 (ADD.W S=1, Rd=PC discarded).
10127 let cmn = encoder
10128 .encode(&ArmOp::Cmn {
10129 rn: Reg::R10,
10130 op2: Operand2::Reg(Reg::R8),
10131 })
10132 .unwrap();
10133 assert_eq!(
10134 cmn,
10135 vec![0x1A, 0xEB, 0x08, 0x0F],
10136 "high-reg CMN must be 32-bit CMN.W (EB1A 0F08); got {cmn:02X?}"
10137 );
10138
10139 // Low registers stay 16-bit: cmn r1, r2 = 0x42D1.
10140 let lo = encoder
10141 .encode(&ArmOp::Cmn {
10142 rn: Reg::R1,
10143 op2: Operand2::Reg(Reg::R2),
10144 })
10145 .unwrap();
10146 assert_eq!(
10147 lo.len(),
10148 2,
10149 "low-reg CMN should remain 16-bit, got {lo:02X?}"
10150 );
10151 assert_eq!(lo, vec![0xD1, 0x42], "low-reg CMN bytes wrong: {lo:02X?}");
10152 }
10153
10154 /// #185 regression: feeding PC (R15) as a data operand to a Thumb-2 op that
10155 /// guards its registers must return Err, not panic under debug-assertions.
10156 /// (Synth never emits PC here; the fuzz harness requires encode() be total.)
10157 #[test]
10158 fn test_encode_pc_operand_returns_err_not_panic_185() {
10159 let encoder = ArmEncoder::new_thumb2();
10160 for op in [
10161 ArmOp::Sdiv {
10162 rd: Reg::PC,
10163 rn: Reg::R0,
10164 rm: Reg::R1,
10165 },
10166 ArmOp::Udiv {
10167 rd: Reg::R0,
10168 rn: Reg::PC,
10169 rm: Reg::R1,
10170 },
10171 ArmOp::Sdiv {
10172 rd: Reg::R0,
10173 rn: Reg::R1,
10174 rm: Reg::PC,
10175 },
10176 ] {
10177 let r = encoder.encode(&op);
10178 assert!(
10179 r.is_err(),
10180 "encode({op:?}) must return Err for a PC operand, got {r:?}"
10181 );
10182 }
10183 // Valid registers still encode fine (no false rejection).
10184 assert!(
10185 encoder
10186 .encode(&ArmOp::Sdiv {
10187 rd: Reg::R0,
10188 rn: Reg::R1,
10189 rm: Reg::R2
10190 })
10191 .is_ok()
10192 );
10193 }
10194
10195 #[test]
10196 fn test_encode_nop_arm32() {
10197 let encoder = ArmEncoder::new_arm32();
10198 let code = encoder.encode(&ArmOp::Nop).unwrap();
10199
10200 assert_eq!(code.len(), 4); // ARM32 instructions are 4 bytes
10201 assert_eq!(code, vec![0x00, 0x00, 0xA0, 0xE1]); // MOV R0, R0
10202 }
10203
10204 #[test]
10205 fn test_encode_nop_thumb() {
10206 let encoder = ArmEncoder::new_thumb2();
10207 let code = encoder.encode(&ArmOp::Nop).unwrap();
10208
10209 assert_eq!(code.len(), 2); // Thumb instructions are 2 bytes
10210 assert_eq!(code, vec![0x00, 0xBF]); // NOP
10211 }
10212
10213 #[test]
10214 fn test_encode_mov_immediate_arm32() {
10215 let encoder = ArmEncoder::new_arm32();
10216 let op = ArmOp::Mov {
10217 rd: Reg::R0,
10218 op2: Operand2::Imm(42),
10219 };
10220
10221 let code = encoder.encode(&op).unwrap();
10222 assert_eq!(code.len(), 4);
10223
10224 // Verify it's a MOV instruction (bits should have immediate flag set)
10225 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10226 assert_eq!(instr & 0x0E000000, 0x02000000); // Check I bit is set
10227 }
10228
10229 #[test]
10230 fn test_encode_add_registers_arm32() {
10231 let encoder = ArmEncoder::new_arm32();
10232 let op = ArmOp::Add {
10233 rd: Reg::R0,
10234 rn: Reg::R1,
10235 op2: Operand2::Reg(Reg::R2),
10236 };
10237
10238 let code = encoder.encode(&op).unwrap();
10239 assert_eq!(code.len(), 4);
10240
10241 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10242 // Verify it's an ADD instruction with correct opcode
10243 assert_eq!(instr & 0x0FE00000, 0x00800000);
10244 }
10245
10246 /// #350 — `encode_thumb32_add_imm` must lower an out-of-range immediate
10247 /// (> 0xFFF) to a legal MOVW(/MOVT) + ADD.W-register sequence instead of
10248 /// erroring. The small-imm fast path (imm <= 0xFFF) stays byte-identical.
10249 #[test]
10250 fn test_encode_add_imm_large_350() {
10251 let enc = ArmEncoder::new_thumb2();
10252
10253 // --- Fast path: imm <= 0xFFF is a single 4-byte instruction, and the
10254 // VALUE must be right (#681: this test used to assert only the length,
10255 // letting the raw-packed T3 mis-encoding of 0x123 pass CI). 0x123 is
10256 // not ThumbExpandImm-representable, so it must be ADDW (T4, plain
10257 // imm12): clang `addw r0, r1, #0x123` = f201 0023.
10258 let small = enc
10259 .encode_thumb32_add_imm(&Reg::R0, &Reg::R1, 0x123)
10260 .unwrap();
10261 assert_eq!(small, vec![0x01, 0xF2, 0x23, 0x10], "ADDW r0, r1, #0x123");
10262
10263 // helper: decode a Thumb-2 MOVW/MOVT halfword pair back to its imm16
10264 fn movx_imm16(b: &[u8]) -> u32 {
10265 let hw1 = u16::from_le_bytes([b[0], b[1]]) as u32;
10266 let hw2 = u16::from_le_bytes([b[2], b[3]]) as u32;
10267 let imm4 = hw1 & 0xF;
10268 let i = (hw1 >> 10) & 1;
10269 let imm3 = (hw2 >> 12) & 0x7;
10270 let imm8 = hw2 & 0xFF;
10271 (imm4 << 12) | (i << 11) | (imm3 << 8) | imm8
10272 }
10273 fn movx_rd(b: &[u8]) -> u32 {
10274 (u16::from_le_bytes([b[2], b[3]]) as u32 >> 8) & 0xF
10275 }
10276
10277 // --- rd != rn: scratch is rd. imm = 70000 = 0x11170 needs MOVW+MOVT. ---
10278 // 0x11170: lo16 = 0x1170, hi16 = 0x0001
10279 let seq = enc
10280 .encode_thumb32_add_imm(&Reg::R12, &Reg::R0, 70000)
10281 .unwrap();
10282 assert_eq!(seq.len(), 12, "MOVW + MOVT + ADD = 12 bytes");
10283 // MOVW r12, #0x1170
10284 assert_eq!(u16::from_le_bytes([seq[0], seq[1]]) & 0xFBF0, 0xF240);
10285 assert_eq!(movx_rd(&seq[0..4]), 12);
10286 assert_eq!(movx_imm16(&seq[0..4]), 0x1170);
10287 // MOVT r12, #0x0001
10288 assert_eq!(u16::from_le_bytes([seq[4], seq[5]]) & 0xFBF0, 0xF2C0);
10289 assert_eq!(movx_rd(&seq[4..8]), 12);
10290 assert_eq!(movx_imm16(&seq[4..8]), 0x0001);
10291 // ADD.W r12, r0, r12 (EB00 | rn=0 ; rd=12, rm=12)
10292 let add1 = u16::from_le_bytes([seq[8], seq[9]]) as u32;
10293 let add2 = u16::from_le_bytes([seq[10], seq[11]]) as u32;
10294 assert_eq!(add1 & 0xFFF0, 0xEB00);
10295 assert_eq!(add1 & 0xF, 0); // rn = r0
10296 assert_eq!((add2 >> 8) & 0xF, 12); // rd = r12
10297 assert_eq!(add2 & 0xF, 12); // rm = scratch = r12
10298 // The materialized scratch must reconstruct exactly 70000.
10299 assert_eq!(
10300 (movx_imm16(&seq[4..8]) << 16) | movx_imm16(&seq[0..4]),
10301 70000
10302 );
10303
10304 // --- imm <= 0xFFFF: MOVT is skipped (MOVW + ADD = 8 bytes). ---
10305 let seq16 = enc
10306 .encode_thumb32_add_imm(&Reg::R3, &Reg::R0, 0xABCD)
10307 .unwrap();
10308 assert_eq!(seq16.len(), 8, "imm <= 0xFFFF skips MOVT");
10309 assert_eq!(movx_imm16(&seq16[0..4]), 0xABCD);
10310 assert_eq!(movx_rd(&seq16[0..4]), 3); // scratch = rd = r3
10311
10312 // --- rd == rn (in-place add): scratch must be R12, not rd. ---
10313 // imm = 0x12345: lo16 = 0x2345, hi16 = 0x0001
10314 let inplace = enc
10315 .encode_thumb32_add_imm(&Reg::R5, &Reg::R5, 0x12345)
10316 .unwrap();
10317 assert_eq!(inplace.len(), 12);
10318 assert_eq!(movx_rd(&inplace[0..4]), 12, "rd==rn must use R12 scratch");
10319 assert_eq!(
10320 (movx_imm16(&inplace[4..8]) << 16) | movx_imm16(&inplace[0..4]),
10321 0x12345
10322 );
10323 // ADD.W r5, r5, r12 — rm must be the scratch (12), never rn.
10324 let ip_add2 = u16::from_le_bytes([inplace[10], inplace[11]]) as u32;
10325 assert_eq!(ip_add2 & 0xF, 12);
10326 assert_eq!((ip_add2 >> 8) & 0xF, 5);
10327 }
10328
10329 /// #681 — `encode_thumb32_add_imm` packed a RAW immediate into the T3
10330 /// ADD.W `i:imm3:imm8` field, which is a ThumbExpandImm MODIFIED immediate:
10331 /// ThumbExpandImm(0x200) = 0, ThumbExpandImm(0x400) = 0x8000_0000. Every
10332 /// dynamic-address load/store with a static offset in 0x100..=0xFFF
10333 /// computed a wrong address (and bypassed --safety-bounds software: the
10334 /// guard checked the intended address, the access used the mis-encoded
10335 /// one). Fix: imm <= 0xFF keeps T3 (raw == expanded there, bit-identical);
10336 /// 0x100..=0xFFF uses ADDW (T4, plain imm12) — same lowering
10337 /// `encode_thumb32_add` already uses per #253.
10338 ///
10339 /// Every expected byte sequence below is pinned against clang
10340 /// (`-target thumbv7m-none-eabi`) output, bit-for-bit (#544 pattern).
10341 #[test]
10342 fn test_encode_add_imm_thumb_expand_681() {
10343 let enc = ArmEncoder::new_thumb2();
10344 let add = |rd: &Reg, rn: &Reg, imm: u32| enc.encode_thumb32_add_imm(rd, rn, imm).unwrap();
10345
10346 // imm <= 0xFF stays T3 ADD.W (raw == ThumbExpandImm-expanded):
10347 // clang: add.w r12, r0, #0xff = f100 0cff
10348 assert_eq!(add(&Reg::R12, &Reg::R0, 0xFF), vec![0x00, 0xF1, 0xFF, 0x0C]);
10349
10350 // 0x100..=0xFFF must be ADDW (T4, plain imm12). The old T3 raw packing
10351 // decoded as +0 (0x100/0x200), +0x80000000 (0x400), etc.
10352 // clang: addw r12, r0, #0x100 = f200 1c00
10353 assert_eq!(
10354 add(&Reg::R12, &Reg::R0, 0x100),
10355 vec![0x00, 0xF2, 0x00, 0x1C]
10356 );
10357 // clang: addw r12, r0, #0x104 = f200 1c04
10358 assert_eq!(
10359 add(&Reg::R12, &Reg::R0, 0x104),
10360 vec![0x00, 0xF2, 0x04, 0x1C]
10361 );
10362 // clang: addw r12, r0, #0x200 = f200 2c00
10363 assert_eq!(
10364 add(&Reg::R12, &Reg::R0, 0x200),
10365 vec![0x00, 0xF2, 0x00, 0x2C]
10366 );
10367 // clang: addw r12, r0, #0x3fc = f200 3cfc
10368 assert_eq!(
10369 add(&Reg::R12, &Reg::R0, 0x3FC),
10370 vec![0x00, 0xF2, 0xFC, 0x3C]
10371 );
10372 // clang: addw r12, r0, #0x400 = f200 4c00
10373 assert_eq!(
10374 add(&Reg::R12, &Reg::R0, 0x400),
10375 vec![0x00, 0xF2, 0x00, 0x4C]
10376 );
10377 // clang: addw r12, r0, #0xfff = f600 7cff
10378 assert_eq!(
10379 add(&Reg::R12, &Reg::R0, 0xFFF),
10380 vec![0x00, 0xF6, 0xFF, 0x7C]
10381 );
10382 // Non-scratch rd/rn — clang: addw r1, r2, #0x104 = f202 1104
10383 assert_eq!(add(&Reg::R1, &Reg::R2, 0x104), vec![0x02, 0xF2, 0x04, 0x11]);
10384 }
10385
10386 /// #681 class audit — the T2 RSB and AND.W immediate fields are also
10387 /// ThumbExpandImm-coded and were raw-packed. Neither has a plain-imm12
10388 /// (T4-style) form, so a non-representable immediate must Err loudly
10389 /// (#253/#255/#378 class: never silently encode a different constant).
10390 /// Existing emitters only use representable values (RSB #32, AND #0x3F),
10391 /// pinned here bit-for-bit against clang.
10392 #[test]
10393 fn test_rsb_and_imm_thumb_expand_gate_681() {
10394 let enc = ArmEncoder::new_thumb2();
10395
10396 // clang: rsb.w r3, r2, #0x20 = f1c2 0320 — byte-identical to before.
10397 let rsb = enc
10398 .encode(&ArmOp::Rsb {
10399 rd: Reg::R3,
10400 rn: Reg::R2,
10401 imm: 32,
10402 })
10403 .unwrap();
10404 assert_eq!(rsb, vec![0xC2, 0xF1, 0x20, 0x03]);
10405
10406 // 0x101 is not ThumbExpandImm-representable -> must Err, not mis-encode.
10407 assert!(
10408 enc.encode(&ArmOp::Rsb {
10409 rd: Reg::R3,
10410 rn: Reg::R2,
10411 imm: 0x101,
10412 })
10413 .is_err(),
10414 "non-ThumbExpandImm RSB immediate must Err"
10415 );
10416
10417 // clang: and r4, r4, #0x3f = f004 043f — byte-identical to before.
10418 let and = enc.encode_thumb32_and_imm_raw(4, 4, 0x3F).unwrap();
10419 assert_eq!(and, vec![0x04, 0xF0, 0x3F, 0x04]);
10420 assert!(
10421 enc.encode_thumb32_and_imm_raw(4, 4, 0x101).is_err(),
10422 "non-ThumbExpandImm AND immediate must Err"
10423 );
10424
10425 // A32 RSB: imm12 is a rotate:imm8 modified immediate; > 0xFF used to be
10426 // silently masked to `imm & 0xFF` (#378 masking class) -> must Err.
10427 let a32 = ArmEncoder::new_arm32();
10428 assert!(
10429 a32.encode(&ArmOp::Rsb {
10430 rd: Reg::R3,
10431 rn: Reg::R2,
10432 imm: 0x120,
10433 })
10434 .is_err(),
10435 "A32 RSB immediate > 0xFF must Err, not mask"
10436 );
10437 // imm 32 (the only value real codegen emits) still encodes.
10438 assert!(
10439 a32.encode(&ArmOp::Rsb {
10440 rd: Reg::R3,
10441 rn: Reg::R2,
10442 imm: 32,
10443 })
10444 .is_ok()
10445 );
10446 }
10447
10448 /// #350 follow-up — the `encoder_no_panic` fuzz harness drives the encoder
10449 /// with ARBITRARY registers, including the one case the in-place lowering
10450 /// cannot serve: rd==rn==R12. There the scratch (R12, the reserved encoder
10451 /// register) would alias Rn and clobber it before the ADD reads it. The
10452 /// encoder contract (#180/#185) is Ok-or-Err, never a panic — so this must
10453 /// return Err, not assert. (Real codegen never emits rd==rn==R12 because R12
10454 /// is non-allocatable; this guards only the fuzz/adversarial path.)
10455 #[test]
10456 fn test_encode_add_imm_large_rd_rn_r12_errs_not_panics_350() {
10457 let enc = ArmEncoder::new_thumb2();
10458 // Out-of-range imm with rd==rn==R12: no free scratch -> Err.
10459 let r = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 70000);
10460 assert!(
10461 r.is_err(),
10462 "rd==rn==R12 with out-of-range imm must Err (no free scratch), got {r:?}"
10463 );
10464 // Small imm with rd==rn==R12 still takes the single-instruction fast path
10465 // (no scratch needed) and must succeed — the guard is scoped to the
10466 // out-of-range lowering only.
10467 let small = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 0x10);
10468 assert!(small.is_ok(), "small imm needs no scratch, must stay Ok");
10469 }
10470
10471 /// #378 — `encode_operand2` (ARM32 data-processing operand) must FAIL
10472 /// HONESTLY on an immediate that is not a valid rotated immediate, rather
10473 /// than silently masking it to `imm & 0xFF` and emitting a WRONG
10474 /// instruction. `0x1FF` has 9 set bits, so it cannot come from rotating an
10475 /// 8-bit imm8 — non-encodable. Real codegen materializes large constants via
10476 /// MOVW/MOVT; this guards the encoder's Ok-or-Err contract (#180/#185)
10477 /// directly. It is an Err (not a panic) so the `encoder_no_panic` fuzz
10478 /// harness — which drives arbitrary operands — still passes.
10479 #[test]
10480 fn test_encode_operand2_non_rotatable_imm_errs_not_masks_378() {
10481 let enc = ArmEncoder::new_arm32();
10482 let bad = enc.encode(&ArmOp::Add {
10483 rd: Reg::R0,
10484 rn: Reg::R1,
10485 op2: Operand2::Imm(0x1FF),
10486 });
10487 assert!(
10488 bad.is_err(),
10489 "non-rotatable ARM32 immediate 0x1FF must Err (was silently masked \
10490 to 0xFF), got {bad:?}"
10491 );
10492 // A representable rotated immediate still encodes fine (regression guard).
10493 let ok = enc.encode(&ArmOp::Add {
10494 rd: Reg::R0,
10495 rn: Reg::R1,
10496 op2: Operand2::Imm(0xFF),
10497 });
10498 assert!(
10499 ok.is_ok(),
10500 "0xFF is a valid rotated immediate, must stay Ok"
10501 );
10502 }
10503
10504 #[test]
10505 fn test_encode_ldr_arm32() {
10506 let encoder = ArmEncoder::new_arm32();
10507 let op = ArmOp::Ldr {
10508 rd: Reg::R0,
10509 addr: MemAddr::imm(Reg::R1, 4),
10510 };
10511
10512 let code = encoder.encode(&op).unwrap();
10513 assert_eq!(code.len(), 4);
10514
10515 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10516 // Verify load bit is set
10517 assert_eq!(instr & 0x00100000, 0x00100000);
10518 }
10519
10520 #[test]
10521 fn test_encode_str_arm32() {
10522 let encoder = ArmEncoder::new_arm32();
10523 let op = ArmOp::Str {
10524 rd: Reg::R0,
10525 addr: MemAddr::imm(Reg::SP, 0),
10526 };
10527
10528 let code = encoder.encode(&op).unwrap();
10529 assert_eq!(code.len(), 4);
10530 }
10531
10532 #[test]
10533 fn test_encode_branch_arm32() {
10534 let encoder = ArmEncoder::new_arm32();
10535 let op = ArmOp::Bl {
10536 label: "main".to_string(),
10537 };
10538
10539 let code = encoder.encode(&op).unwrap();
10540 assert_eq!(code.len(), 4);
10541
10542 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10543 // Verify BL opcode
10544 assert_eq!(instr & 0x0F000000, 0x0B000000);
10545 }
10546
10547 /// #1040: the A32 BL relocatable placeholder must carry an embedded addend
10548 /// of -8 so an R_ARM_CALL nets to exactly the symbol S — the A32 twin of
10549 /// the Thumb #167/#174 test above. A32 `BL` computes
10550 /// `target = P + 8 + (imm24 << 2)`, so:
10551 /// - `eb000000` (imm24 = 0, addend 0) lands at S+8, two instructions
10552 /// past the callee entry. This is what synth emitted before #1040, and
10553 /// combined with the mislabelled R_ARM_THM_CALL a real linker
10554 /// (`arm-none-eabi-ld`) corrupted the word to `eaca0000` — opcode
10555 /// `eb` (BL) flipped to `ea` (B), so LR was never set at all.
10556 /// - `ebfffffe` (imm24 = -2, offset -8) is `bl <self>` and nets to S.
10557 /// This is exactly what `arm-none-eabi-as -march=armv7-r` emits for
10558 /// `bl <extern>`, verified directly rather than derived from the ABI.
10559 #[test]
10560 fn test_encode_arm32_bl_placeholder_addend_1040() {
10561 let encoder = ArmEncoder::new_arm32();
10562 let code = encoder
10563 .encode(&ArmOp::Bl {
10564 label: "callee".to_string(),
10565 })
10566 .unwrap();
10567 assert_eq!(code.len(), 4, "A32 BL is one 32-bit word");
10568 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10569 assert_eq!(
10570 instr, 0xEBFF_FFFE,
10571 "A32 BL placeholder must be `ebfffffe` (gas's `bl <extern>`), not `eb000000` — a 0 addend resolves two instructions past the callee entry (#1040)"
10572 );
10573 // Spell the addend out so a future edit cannot satisfy the literal by
10574 // accident: sign-extended imm24, word-scaled, plus the +8 pipeline
10575 // bias, must be exactly 0 (branch-to-self).
10576 let imm24 = instr & 0x00FF_FFFF;
10577 let signed = ((imm24 as i32) << 8) >> 8;
10578 assert_eq!(
10579 8 + (signed << 2),
10580 0,
10581 "placeholder must branch to itself so the REL addend is -8"
10582 );
10583 }
10584
10585 /// Regression test for #167 + #174: the Thumb-2 BL relocatable placeholder
10586 /// must carry a -4 addend so an R_ARM_THM_CALL nets to exactly the symbol S.
10587 /// The correct encoding is what `gas` emits for `bl <extern>`: f7ff fffe
10588 /// (hw1=0xF7FF, hw2=0xFFFE), little-endian bytes FF F7 FE FF.
10589 /// - 0xD000 (J1=J2=0) → ~+0x600000 garbage addend: `bl c0000c` / truncated
10590 /// to fit (#167).
10591 /// - 0xF800 (addend 0) → lands at S+4, one instruction past the callee
10592 /// entry (#174).
10593 /// - 0xFFFE (addend -4) → lands at S. Correct.
10594 #[test]
10595 fn test_encode_thumb_bl_placeholder_addend_167_174() {
10596 let encoder = ArmEncoder::new_thumb2();
10597 let op = ArmOp::Bl {
10598 label: "callee".to_string(),
10599 };
10600
10601 let code = encoder.encode(&op).unwrap();
10602 assert_eq!(code.len(), 4, "Thumb-2 BL is 32-bit");
10603
10604 let hw1 = u16::from_le_bytes([code[0], code[1]]);
10605 let hw2 = u16::from_le_bytes([code[2], code[3]]);
10606 assert_eq!(hw1, 0xF7FF, "BL first halfword (matches gas `bl <extern>`)");
10607 assert_eq!(
10608 hw2, 0xFFFE,
10609 "BL second halfword must be 0xFFFE (-4 addend → nets to S), not 0xF800 (→ S+4, #174) or 0xD000 (#167)"
10610 );
10611 assert_ne!(hw2, 0xF800, "0xF800 (addend 0) lands at S+4 (#174)");
10612 assert_ne!(hw2, 0xD000, "0xD000 bakes in a ~+0x600000 addend (#167)");
10613 }
10614
10615 /// #740: the Thumb-2 32-bit B<cond>.W (encoding T3) must pack the
10616 /// HALFWORD offset directly into S:J2:J1:imm6:imm11 — the byte offset is
10617 /// SignExtend(S:J2:J1:imm6:imm11:'0'). The old arm packed
10618 /// `halfword_offset >> 1`, HALVING every wide conditional branch's
10619 /// displacement: gust_poll's loop-head `br_if` to an outer block end
10620 /// landed mid-shape (a spurious state write + spurious calls on the
10621 /// empty-budget path). Narrow (16-bit) B<cond> was unaffected — only
10622 /// spans > 254 bytes hit the bug. Bytes cross-checked against the llvm
10623 /// disassembler (`bne.w #0x224` = f040 8112).
10624 #[test]
10625 fn test_encode_thumb_bcond_wide_t3_halfword_offset_740() {
10626 use synth_synthesis::Condition;
10627 let encoder = ArmEncoder::new_thumb2();
10628
10629 // gust_poll's loop-head edge: NE, +0x112 halfwords (+0x224 bytes).
10630 let code = encoder
10631 .encode(&ArmOp::BCondOffset {
10632 cond: Condition::NE,
10633 offset: 0x112,
10634 })
10635 .unwrap();
10636 assert_eq!(code.len(), 4, "offset beyond ±127 halfwords must be wide");
10637 let hw1 = u16::from_le_bytes([code[0], code[1]]);
10638 let hw2 = u16::from_le_bytes([code[2], code[3]]);
10639 assert_eq!(hw1, 0xF040, "T3 hw1: 1111 0 S=0 cond=NE imm6=0");
10640 assert_eq!(
10641 hw2, 0x8112,
10642 "T3 hw2 imm11 must carry halfword offset bits [10:0] directly — \
10643 0x8089 (offset>>1) is the halved #740 miscompile"
10644 );
10645
10646 // Backward wide branch: EQ, -0x100 halfwords. S=1, J2=J1=1,
10647 // imm6=0b111111, imm11=0x700 → f43f af00.
10648 let code = encoder
10649 .encode(&ArmOp::BCondOffset {
10650 cond: Condition::EQ,
10651 offset: -0x100,
10652 })
10653 .unwrap();
10654 assert_eq!(code.len(), 4);
10655 let hw1 = u16::from_le_bytes([code[0], code[1]]);
10656 let hw2 = u16::from_le_bytes([code[2], code[3]]);
10657 assert_eq!(hw1, 0xF43F, "T3 hw1: S=1, cond=EQ, imm6=0x3F");
10658 assert_eq!(hw2, 0xAF00, "T3 hw2: J1=1 J2=1 imm11=0x700");
10659
10660 // Narrow encoding stays byte-identical (in-range offsets untouched).
10661 let code = encoder
10662 .encode(&ArmOp::BCondOffset {
10663 cond: Condition::EQ,
10664 offset: 5,
10665 })
10666 .unwrap();
10667 assert_eq!(code, vec![0x05, 0xD0], "narrow B<cond> unchanged");
10668
10669 // Out of the signed 20-bit T3 range: loud Err, never a truncated jump.
10670 assert!(
10671 encoder
10672 .encode(&ArmOp::BCondOffset {
10673 cond: Condition::NE,
10674 offset: 1 << 19,
10675 })
10676 .is_err(),
10677 "out-of-range T3 offset must be a loud decline"
10678 );
10679 }
10680
10681 #[test]
10682 fn test_encode_sequence() {
10683 let encoder = ArmEncoder::new_arm32();
10684 let ops = vec![
10685 ArmOp::Mov {
10686 rd: Reg::R0,
10687 op2: Operand2::Imm(42),
10688 },
10689 ArmOp::Mov {
10690 rd: Reg::R1,
10691 op2: Operand2::Imm(10),
10692 },
10693 ArmOp::Add {
10694 rd: Reg::R2,
10695 rn: Reg::R0,
10696 op2: Operand2::Reg(Reg::R1),
10697 },
10698 ];
10699
10700 let code = encoder.encode_sequence(&ops).unwrap();
10701 assert_eq!(code.len(), 12); // 3 instructions * 4 bytes
10702 }
10703
10704 #[test]
10705 fn test_reg_to_bits() {
10706 assert_eq!(reg_to_bits(&Reg::R0), 0);
10707 assert_eq!(reg_to_bits(&Reg::R7), 7);
10708 assert_eq!(reg_to_bits(&Reg::SP), 13);
10709 assert_eq!(reg_to_bits(&Reg::LR), 14);
10710 assert_eq!(reg_to_bits(&Reg::PC), 15);
10711 }
10712
10713 #[test]
10714 fn test_encode_bitwise_operations() {
10715 let encoder = ArmEncoder::new_arm32();
10716
10717 let and_op = ArmOp::And {
10718 rd: Reg::R0,
10719 rn: Reg::R1,
10720 op2: Operand2::Reg(Reg::R2),
10721 };
10722 let and_code = encoder.encode(&and_op).unwrap();
10723 assert_eq!(and_code.len(), 4);
10724
10725 let orr_op = ArmOp::Orr {
10726 rd: Reg::R0,
10727 rn: Reg::R1,
10728 op2: Operand2::Reg(Reg::R2),
10729 };
10730 let orr_code = encoder.encode(&orr_op).unwrap();
10731 assert_eq!(orr_code.len(), 4);
10732
10733 let eor_op = ArmOp::Eor {
10734 rd: Reg::R0,
10735 rn: Reg::R1,
10736 op2: Operand2::Reg(Reg::R2),
10737 };
10738 let eor_code = encoder.encode(&eor_op).unwrap();
10739 assert_eq!(eor_code.len(), 4);
10740 }
10741
10742 // === Thumb-2 32-bit encoding tests ===
10743
10744 #[test]
10745 fn test_encode_sdiv_thumb2() {
10746 let encoder = ArmEncoder::new_thumb2();
10747 let op = ArmOp::Sdiv {
10748 rd: Reg::R0,
10749 rn: Reg::R1,
10750 rm: Reg::R2,
10751 };
10752
10753 let code = encoder.encode(&op).unwrap();
10754 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10755
10756 // SDIV R0, R1, R2: 0xFB91 0xF0F2
10757 // First halfword: 0xFB90 | Rn(1) = 0xFB91
10758 // Second halfword: 0xF0F0 | Rd(0)<<8 | Rm(2) = 0xF0F2
10759 // Little-endian: [0x91, 0xFB, 0xF2, 0xF0]
10760 assert_eq!(code[0], 0x91);
10761 assert_eq!(code[1], 0xFB);
10762 assert_eq!(code[2], 0xF2);
10763 assert_eq!(code[3], 0xF0);
10764 }
10765
10766 #[test]
10767 fn test_encode_udiv_thumb2() {
10768 let encoder = ArmEncoder::new_thumb2();
10769 let op = ArmOp::Udiv {
10770 rd: Reg::R0,
10771 rn: Reg::R1,
10772 rm: Reg::R2,
10773 };
10774
10775 let code = encoder.encode(&op).unwrap();
10776 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10777
10778 // UDIV R0, R1, R2: 0xFBB1 0xF0F2
10779 // Little-endian: [0xB1, 0xFB, 0xF2, 0xF0]
10780 assert_eq!(code[0], 0xB1);
10781 assert_eq!(code[1], 0xFB);
10782 assert_eq!(code[2], 0xF2);
10783 assert_eq!(code[3], 0xF0);
10784 }
10785
10786 #[test]
10787 fn test_encode_mul_thumb2() {
10788 let encoder = ArmEncoder::new_thumb2();
10789 let op = ArmOp::Mul {
10790 rd: Reg::R0,
10791 rn: Reg::R1,
10792 rm: Reg::R2,
10793 };
10794
10795 let code = encoder.encode(&op).unwrap();
10796 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10797 }
10798
10799 #[test]
10800 fn test_encode_and_thumb2() {
10801 let encoder = ArmEncoder::new_thumb2();
10802 let op = ArmOp::And {
10803 rd: Reg::R0,
10804 rn: Reg::R1,
10805 op2: Operand2::Reg(Reg::R2),
10806 };
10807
10808 let code = encoder.encode(&op).unwrap();
10809 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10810 }
10811
10812 #[test]
10813 fn test_encode_lsl_thumb2_low_regs() {
10814 let encoder = ArmEncoder::new_thumb2();
10815 let op = ArmOp::Lsl {
10816 rd: Reg::R0,
10817 rn: Reg::R1,
10818 shift: 5,
10819 };
10820
10821 let code = encoder.encode(&op).unwrap();
10822 assert_eq!(code.len(), 2); // 16-bit for low registers
10823 }
10824
10825 #[test]
10826 fn test_encode_clz_thumb2() {
10827 let encoder = ArmEncoder::new_thumb2();
10828 let op = ArmOp::Clz {
10829 rd: Reg::R0,
10830 rm: Reg::R1,
10831 };
10832
10833 let code = encoder.encode(&op).unwrap();
10834 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10835 }
10836
10837 #[test]
10838 fn test_encode_bx_thumb2() {
10839 let encoder = ArmEncoder::new_thumb2();
10840 let op = ArmOp::Bx { rm: Reg::LR };
10841
10842 let code = encoder.encode(&op).unwrap();
10843 assert_eq!(code.len(), 2); // 16-bit instruction
10844
10845 // BX LR: 0x4770
10846 assert_eq!(code, vec![0x70, 0x47]);
10847 }
10848
10849 // ========================================================================
10850 // f32 pseudo-op encoding tests
10851 // ========================================================================
10852
10853 #[test]
10854 fn test_encode_f32_abs_arm32() {
10855 let encoder = ArmEncoder::new_arm32();
10856 let op = ArmOp::F32Abs {
10857 sd: VfpReg::S0,
10858 sm: VfpReg::S2,
10859 };
10860 let code = encoder.encode(&op).unwrap();
10861 assert_eq!(code.len(), 4); // Single VFP instruction
10862 }
10863
10864 #[test]
10865 fn test_encode_f32_neg_arm32() {
10866 let encoder = ArmEncoder::new_arm32();
10867 let op = ArmOp::F32Neg {
10868 sd: VfpReg::S0,
10869 sm: VfpReg::S2,
10870 };
10871 let code = encoder.encode(&op).unwrap();
10872 assert_eq!(code.len(), 4);
10873 }
10874
10875 #[test]
10876 fn test_encode_f32_sqrt_arm32() {
10877 let encoder = ArmEncoder::new_arm32();
10878 let op = ArmOp::F32Sqrt {
10879 sd: VfpReg::S0,
10880 sm: VfpReg::S2,
10881 };
10882 let code = encoder.encode(&op).unwrap();
10883 assert_eq!(code.len(), 4);
10884 }
10885
10886 #[test]
10887 fn test_encode_f32_ceil_arm32() {
10888 let encoder = ArmEncoder::new_arm32();
10889 let op = ArmOp::F32Ceil {
10890 sd: VfpReg::S0,
10891 sm: VfpReg::S2,
10892 };
10893 let code = encoder.encode(&op).unwrap();
10894 // VMRS + BIC + ORR + VMSR + VCVT.S32.F32 + VMRS + BIC + VMSR + VCVT.F32.S32
10895 assert_eq!(code.len(), 36);
10896 }
10897
10898 #[test]
10899 fn test_encode_f32_floor_thumb2() {
10900 let encoder = ArmEncoder::new_thumb2();
10901 let op = ArmOp::F32Floor {
10902 sd: VfpReg::S0,
10903 sm: VfpReg::S2,
10904 };
10905 let code = encoder.encode(&op).unwrap();
10906 // VMRS + BIC.W + ORR.W + VMSR + VCVT + VMRS + BIC.W + VMSR + VCVT.F32.S32
10907 assert_eq!(code.len(), 36);
10908 }
10909
10910 #[test]
10911 fn test_encode_f32_min_arm32() {
10912 let encoder = ArmEncoder::new_arm32();
10913 let op = ArmOp::F32Min {
10914 sd: VfpReg::S0,
10915 sn: VfpReg::S2,
10916 sm: VfpReg::S4,
10917 };
10918 let code = encoder.encode(&op).unwrap();
10919 assert_eq!(code.len(), 16); // VMOV + VCMP + VMRS + conditional VMOV
10920 }
10921
10922 #[test]
10923 fn test_encode_f32_max_thumb2() {
10924 let encoder = ArmEncoder::new_thumb2();
10925 let op = ArmOp::F32Max {
10926 sd: VfpReg::S0,
10927 sn: VfpReg::S2,
10928 sm: VfpReg::S4,
10929 };
10930 let code = encoder.encode(&op).unwrap();
10931 // VMOV(4) + VCMP(4) + VMRS(4) + IT(2) + VMOV(4) = 18
10932 assert_eq!(code.len(), 18);
10933 }
10934
10935 #[test]
10936 fn test_encode_f32_copysign_arm32() {
10937 let encoder = ArmEncoder::new_arm32();
10938 let op = ArmOp::F32Copysign {
10939 sd: VfpReg::S0,
10940 sn: VfpReg::S2,
10941 sm: VfpReg::S4,
10942 };
10943 let code = encoder.encode(&op).unwrap();
10944 // VMOV + VMOV + AND + BIC + ORR + VMOV = 6 * 4 = 24
10945 assert_eq!(code.len(), 24);
10946 }
10947
10948 // ========================================================================
10949 // f64 encoding tests
10950 // ========================================================================
10951
10952 #[test]
10953 fn test_encode_f64_add_arm32() {
10954 let encoder = ArmEncoder::new_arm32();
10955 let op = ArmOp::F64Add {
10956 dd: VfpReg::D0,
10957 dn: VfpReg::D1,
10958 dm: VfpReg::D2,
10959 };
10960 let code = encoder.encode(&op).unwrap();
10961 assert_eq!(code.len(), 4);
10962 // VADD.F64 D0, D1, D2: check coprocessor is cp11 (0xB)
10963 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10964 assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
10965 }
10966
10967 #[test]
10968 fn test_encode_f64_sub_thumb2() {
10969 let encoder = ArmEncoder::new_thumb2();
10970 let op = ArmOp::F64Sub {
10971 dd: VfpReg::D0,
10972 dn: VfpReg::D1,
10973 dm: VfpReg::D2,
10974 };
10975 let code = encoder.encode(&op).unwrap();
10976 assert_eq!(code.len(), 4); // 32-bit VFP as two Thumb halfwords
10977 }
10978
10979 #[test]
10980 fn test_encode_f64_mul_arm32() {
10981 let encoder = ArmEncoder::new_arm32();
10982 let op = ArmOp::F64Mul {
10983 dd: VfpReg::D0,
10984 dn: VfpReg::D1,
10985 dm: VfpReg::D2,
10986 };
10987 let code = encoder.encode(&op).unwrap();
10988 assert_eq!(code.len(), 4);
10989 }
10990
10991 #[test]
10992 fn test_encode_f64_div_arm32() {
10993 let encoder = ArmEncoder::new_arm32();
10994 let op = ArmOp::F64Div {
10995 dd: VfpReg::D0,
10996 dn: VfpReg::D1,
10997 dm: VfpReg::D2,
10998 };
10999 let code = encoder.encode(&op).unwrap();
11000 assert_eq!(code.len(), 4);
11001 }
11002
11003 #[test]
11004 fn test_encode_f64_abs_arm32() {
11005 let encoder = ArmEncoder::new_arm32();
11006 let op = ArmOp::F64Abs {
11007 dd: VfpReg::D0,
11008 dm: VfpReg::D2,
11009 };
11010 let code = encoder.encode(&op).unwrap();
11011 assert_eq!(code.len(), 4);
11012 }
11013
11014 #[test]
11015 fn test_encode_f64_neg_arm32() {
11016 let encoder = ArmEncoder::new_arm32();
11017 let op = ArmOp::F64Neg {
11018 dd: VfpReg::D0,
11019 dm: VfpReg::D2,
11020 };
11021 let code = encoder.encode(&op).unwrap();
11022 assert_eq!(code.len(), 4);
11023 }
11024
11025 #[test]
11026 fn test_encode_f64_sqrt_arm32() {
11027 let encoder = ArmEncoder::new_arm32();
11028 let op = ArmOp::F64Sqrt {
11029 dd: VfpReg::D0,
11030 dm: VfpReg::D2,
11031 };
11032 let code = encoder.encode(&op).unwrap();
11033 assert_eq!(code.len(), 4);
11034 }
11035
11036 #[test]
11037 fn test_encode_f64_load_arm32() {
11038 let encoder = ArmEncoder::new_arm32();
11039 let op = ArmOp::F64Load {
11040 dd: VfpReg::D0,
11041 addr: MemAddr::imm(Reg::R0, 8),
11042 };
11043 let code = encoder.encode(&op).unwrap();
11044 assert_eq!(code.len(), 4);
11045 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11046 assert_eq!((instr >> 8) & 0xF, 0xB); // cp11 for F64
11047 assert_eq!(instr & 0xFF, 2); // offset 8 / 4 = 2
11048 }
11049
11050 #[test]
11051 fn test_encode_f64_store_thumb2() {
11052 let encoder = ArmEncoder::new_thumb2();
11053 let op = ArmOp::F64Store {
11054 dd: VfpReg::D0,
11055 addr: MemAddr::imm(Reg::SP, 0),
11056 };
11057 let code = encoder.encode(&op).unwrap();
11058 assert_eq!(code.len(), 4);
11059 }
11060
11061 #[test]
11062 fn test_encode_f64_compare_arm32() {
11063 let encoder = ArmEncoder::new_arm32();
11064 let op = ArmOp::F64Eq {
11065 rd: Reg::R0,
11066 dn: VfpReg::D0,
11067 dm: VfpReg::D1,
11068 };
11069 let code = encoder.encode(&op).unwrap();
11070 assert_eq!(code.len(), 16); // VCMP + VMRS + MOV #0 + MOVcond #1
11071 }
11072
11073 #[test]
11074 fn test_encode_f64_compare_thumb2() {
11075 let encoder = ArmEncoder::new_thumb2();
11076 let op = ArmOp::F64Lt {
11077 rd: Reg::R0,
11078 dn: VfpReg::D0,
11079 dm: VfpReg::D1,
11080 };
11081 let code = encoder.encode(&op).unwrap();
11082 // VCMP(4) + VMRS(4) + MOVS(2) + IT(2) + MOV(2) = 14
11083 assert_eq!(code.len(), 14);
11084 }
11085
11086 #[test]
11087 fn test_encode_f64_const_arm32() {
11088 let encoder = ArmEncoder::new_arm32();
11089 let op = ArmOp::F64Const {
11090 dd: VfpReg::D0,
11091 value: 3.125,
11092 };
11093 let code = encoder.encode(&op).unwrap();
11094 // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
11095 assert_eq!(code.len(), 20);
11096 }
11097
11098 #[test]
11099 fn test_encode_f64_const_thumb2() {
11100 let encoder = ArmEncoder::new_thumb2();
11101 let op = ArmOp::F64Const {
11102 dd: VfpReg::D0,
11103 value: 2.5,
11104 };
11105 let code = encoder.encode(&op).unwrap();
11106 // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
11107 assert_eq!(code.len(), 20);
11108 }
11109
11110 #[test]
11111 fn test_encode_f64_convert_i32s_arm32() {
11112 let encoder = ArmEncoder::new_arm32();
11113 let op = ArmOp::F64ConvertI32S {
11114 dd: VfpReg::D0,
11115 rm: Reg::R0,
11116 };
11117 let code = encoder.encode(&op).unwrap();
11118 // VMOV(4) + VCVT(4) = 8
11119 assert_eq!(code.len(), 8);
11120 }
11121
11122 #[test]
11123 fn test_encode_f64_promote_f32_arm32() {
11124 let encoder = ArmEncoder::new_arm32();
11125 let op = ArmOp::F64PromoteF32 {
11126 dd: VfpReg::D0,
11127 sm: VfpReg::S0,
11128 };
11129 let code = encoder.encode(&op).unwrap();
11130 assert_eq!(code.len(), 4); // Single VCVT.F64.F32 instruction
11131 }
11132
11133 #[test]
11134 fn test_encode_f64_promote_f32_thumb2() {
11135 let encoder = ArmEncoder::new_thumb2();
11136 let op = ArmOp::F64PromoteF32 {
11137 dd: VfpReg::D0,
11138 sm: VfpReg::S0,
11139 };
11140 let code = encoder.encode(&op).unwrap();
11141 assert_eq!(code.len(), 4);
11142 }
11143
11144 #[test]
11145 fn test_encode_i32_trunc_f64s_arm32() {
11146 let encoder = ArmEncoder::new_arm32();
11147 let op = ArmOp::I32TruncF64S {
11148 rd: Reg::R0,
11149 dm: VfpReg::D0,
11150 };
11151 let code = encoder.encode(&op).unwrap();
11152 // VCVT(4) + VMOV(4) = 8
11153 assert_eq!(code.len(), 8);
11154 }
11155
11156 #[test]
11157 fn test_encode_f64_reinterpret_i64_arm32() {
11158 let encoder = ArmEncoder::new_arm32();
11159 let op = ArmOp::F64ReinterpretI64 {
11160 dd: VfpReg::D0,
11161 rmlo: Reg::R0,
11162 rmhi: Reg::R1,
11163 };
11164 let code = encoder.encode(&op).unwrap();
11165 assert_eq!(code.len(), 4); // Single VMOV instruction
11166 }
11167
11168 #[test]
11169 fn test_encode_i64_reinterpret_f64_thumb2() {
11170 let encoder = ArmEncoder::new_thumb2();
11171 let op = ArmOp::I64ReinterpretF64 {
11172 rdlo: Reg::R0,
11173 rdhi: Reg::R1,
11174 dm: VfpReg::D0,
11175 };
11176 let code = encoder.encode(&op).unwrap();
11177 assert_eq!(code.len(), 4);
11178 }
11179
11180 #[test]
11181 fn test_encode_f64_trunc_thumb2() {
11182 let encoder = ArmEncoder::new_thumb2();
11183 let op = ArmOp::F64Trunc {
11184 dd: VfpReg::D0,
11185 dm: VfpReg::D1,
11186 };
11187 let code = encoder.encode(&op).unwrap();
11188 // GI-FPU-002 phase 3 (#369): a single VRINTZ.F64 (clang-verified
11189 // vrintz.f64 d0,d1 base) — no more FPSCR dance / S0 clobber.
11190 assert_eq!(code.len(), 4);
11191 assert_eq!(code, vec![0xb6, 0xee, 0xc1, 0x0b]);
11192 }
11193
11194 /// GI-FPU-002 phase 3 (#369): the rewritten f64 tail sequences, byte-exact
11195 /// against clang (`-target thumbv7em-none-eabi -mfpu=fpv5-d16`). Each
11196 /// clobbers ONLY its destination (+R12/flags where noted) — the previous
11197 /// pseudo-ops staged through live S0/R0-R2 (the #615 class) and the
11198 /// min/max/rounding semantics were wrong (ordered IT select returned the
11199 /// wrong operand on NaN/±0; rounding round-tripped through a 32-bit int).
11200 #[test]
11201 fn test_369_f64_tail_thumb2_encodings_match_clang() {
11202 let enc = ArmEncoder::new_thumb2();
11203 // vrintn/vrintp/vrintm.f64 d1, d2 (FE space, never IT'd).
11204 for (op, want) in [
11205 (
11206 ArmOp::F64Nearest {
11207 dd: VfpReg::D1,
11208 dm: VfpReg::D2,
11209 },
11210 vec![0xb9, 0xfe, 0x42, 0x1b],
11211 ),
11212 (
11213 ArmOp::F64Ceil {
11214 dd: VfpReg::D1,
11215 dm: VfpReg::D2,
11216 },
11217 vec![0xba, 0xfe, 0x42, 0x1b],
11218 ),
11219 (
11220 ArmOp::F64Floor {
11221 dd: VfpReg::D1,
11222 dm: VfpReg::D2,
11223 },
11224 vec![0xbb, 0xfe, 0x42, 0x1b],
11225 ),
11226 ] {
11227 assert_eq!(enc.encode(&op).unwrap(), want, "{op:?}");
11228 }
11229 // vcmp.f64 d1,d2 ; vmrs ; vminnm.f64 d0,d1,d2 ; it vs ; vaddvs.f64
11230 let min = enc
11231 .encode(&ArmOp::F64Min {
11232 dd: VfpReg::D0,
11233 dn: VfpReg::D1,
11234 dm: VfpReg::D2,
11235 })
11236 .unwrap();
11237 assert_eq!(
11238 min,
11239 vec![
11240 0xb4, 0xee, 0x42, 0x1b, // vcmp.f64 d1, d2
11241 0xf1, 0xee, 0x10, 0xfa, // vmrs APSR_nzcv, fpscr
11242 0x81, 0xfe, 0x42, 0x0b, // vminnm.f64 d0, d1, d2
11243 0x68, 0xbf, // it vs
11244 0x31, 0xee, 0x02, 0x0b, // vaddvs.f64 d0, d1, d2
11245 ]
11246 );
11247 // vmaxnm variant flips only bit6 of the VMINNM word.
11248 let max = enc
11249 .encode(&ArmOp::F64Max {
11250 dd: VfpReg::D0,
11251 dn: VfpReg::D1,
11252 dm: VfpReg::D2,
11253 })
11254 .unwrap();
11255 assert_eq!(&max[8..12], &[0x81, 0xfe, 0x02, 0x0b]);
11256 // Destination aliasing a source must ERR (the NaN fix-up would read
11257 // a clobbered operand), never encode.
11258 assert!(
11259 enc.encode(&ArmOp::F64Min {
11260 dd: VfpReg::D1,
11261 dn: VfpReg::D1,
11262 dm: VfpReg::D2,
11263 })
11264 .is_err()
11265 );
11266 // copysign d0,(mag)d1,(sign)d2:
11267 // vmov r12,s5 ; cmp.w r12,#0 ; vabs.f64 d0,d1 ; it mi ; vnegmi.f64 d0,d0
11268 let cs = enc
11269 .encode(&ArmOp::F64Copysign {
11270 dd: VfpReg::D0,
11271 dn: VfpReg::D1,
11272 dm: VfpReg::D2,
11273 })
11274 .unwrap();
11275 assert_eq!(
11276 cs,
11277 vec![
11278 0x12, 0xee, 0x90, 0xca, // vmov r12, s5
11279 0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
11280 0xb0, 0xee, 0xc1, 0x0b, // vabs.f64 d0, d1
11281 0x48, 0xbf, // it mi
11282 0xb1, 0xee, 0x40, 0x0b, // vnegmi.f64 d0, d0
11283 ]
11284 );
11285 // f32 copysign s0,(mag)s1,(sign)s2 — the R0-clobber-free rewrite:
11286 // vmov r12,s2 ; cmp.w r12,#0 ; vabs.f32 s0,s1 ; it mi ; vnegmi.f32
11287 let cs32 = enc
11288 .encode(&ArmOp::F32Copysign {
11289 sd: VfpReg::S0,
11290 sn: VfpReg::S1,
11291 sm: VfpReg::S2,
11292 })
11293 .unwrap();
11294 assert_eq!(
11295 cs32,
11296 vec![
11297 0x11, 0xee, 0x10, 0xca, // vmov r12, s2
11298 0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
11299 0xb0, 0xee, 0xe0, 0x0a, // vabs.f32 s0, s1
11300 0x48, 0xbf, // it mi
11301 0xb1, 0xee, 0x40, 0x0a, // vnegmi.f32 s0, s0
11302 ]
11303 );
11304 // i32 -> f64 stages through the DESTINATION's S-alias (never S0) and
11305 // uses the CORRECT signed/unsigned VCVT bases (previously swapped):
11306 // vmov s0,r3 ; vcvt.f64.s32 d0,s0
11307 let conv_s = enc
11308 .encode(&ArmOp::F64ConvertI32S {
11309 dd: VfpReg::D0,
11310 rm: Reg::R3,
11311 })
11312 .unwrap();
11313 assert_eq!(
11314 conv_s,
11315 vec![
11316 0x00, 0xee, 0x10, 0x3a, // vmov s0, r3
11317 0xb8, 0xee, 0xc0, 0x0b, // vcvt.f64.s32 d0, s0
11318 ]
11319 );
11320 let conv_u = enc
11321 .encode(&ArmOp::F64ConvertI32U {
11322 dd: VfpReg::D0,
11323 rm: Reg::R3,
11324 })
11325 .unwrap();
11326 assert_eq!(&conv_u[4..8], &[0xb8, 0xee, 0x40, 0x0b]); // vcvt.f64.u32
11327 // f64 -> i32 stages through the SOURCE's S-alias (never S0):
11328 // vcvt.s32.f64 s2,d1 ; vmov r3,s2
11329 let trunc_s = enc
11330 .encode(&ArmOp::I32TruncF64S {
11331 rd: Reg::R3,
11332 dm: VfpReg::D1,
11333 })
11334 .unwrap();
11335 assert_eq!(
11336 trunc_s,
11337 vec![
11338 0xbd, 0xee, 0xc1, 0x1b, // vcvt.s32.f64 s2, d1
11339 0x11, 0xee, 0x10, 0x3a, // vmov r3, s2
11340 ]
11341 );
11342 let trunc_u = enc
11343 .encode(&ArmOp::I32TruncF64U {
11344 rd: Reg::R3,
11345 dm: VfpReg::D1,
11346 })
11347 .unwrap();
11348 assert_eq!(&trunc_u[0..4], &[0xbc, 0xee, 0xc1, 0x1b]); // vcvt.u32.f64
11349 // f32.demote_f64: vcvt.f32.f64 s1, d2
11350 let demote = enc
11351 .encode(&ArmOp::F32DemoteF64 {
11352 sd: VfpReg::S1,
11353 dm: VfpReg::D2,
11354 })
11355 .unwrap();
11356 assert_eq!(demote, vec![0xf7, 0xee, 0xc2, 0x0b]);
11357 }
11358
11359 #[test]
11360 fn test_encode_f64_min_arm32() {
11361 let encoder = ArmEncoder::new_arm32();
11362 let op = ArmOp::F64Min {
11363 dd: VfpReg::D0,
11364 dn: VfpReg::D1,
11365 dm: VfpReg::D2,
11366 };
11367 let code = encoder.encode(&op).unwrap();
11368 // VMOV + VCMP + VMRS + conditional VMOV = 16
11369 assert_eq!(code.len(), 16);
11370 }
11371
11372 #[test]
11373 fn test_f64_cp11_encoding() {
11374 // Verify that F64 instructions use coprocessor 11 (0xB), not 10 (0xA)
11375 let encoder = ArmEncoder::new_arm32();
11376
11377 // F64Add
11378 let code = encoder
11379 .encode(&ArmOp::F64Add {
11380 dd: VfpReg::D0,
11381 dn: VfpReg::D0,
11382 dm: VfpReg::D0,
11383 })
11384 .unwrap();
11385 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11386 assert_eq!((instr >> 8) & 0xF, 0xB, "F64 should use cp11");
11387
11388 // F32Add for comparison
11389 let code = encoder
11390 .encode(&ArmOp::F32Add {
11391 sd: VfpReg::S0,
11392 sn: VfpReg::S0,
11393 sm: VfpReg::S0,
11394 })
11395 .unwrap();
11396 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11397 assert_eq!((instr >> 8) & 0xF, 0xA, "F32 should use cp10");
11398 }
11399
11400 #[test]
11401 fn test_dreg_encoding_higher_registers() {
11402 let encoder = ArmEncoder::new_arm32();
11403
11404 // Test with D15 (highest register)
11405 let op = ArmOp::F64Add {
11406 dd: VfpReg::D15,
11407 dn: VfpReg::D14,
11408 dm: VfpReg::D13,
11409 };
11410 let code = encoder.encode(&op).unwrap();
11411 assert_eq!(code.len(), 4);
11412
11413 // Verify the register encoding worked (instruction is valid)
11414 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11415 assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
11416 }
11417
11418 // ========================================================================
11419 // Control flow encoding tests
11420 // ========================================================================
11421
11422 #[test]
11423 fn test_encode_label_emits_no_bytes() {
11424 let encoder = ArmEncoder::new_thumb2();
11425 let op = ArmOp::Label {
11426 name: ".Lblock_end_0".to_string(),
11427 };
11428 let code = encoder.encode(&op).unwrap();
11429 assert!(code.is_empty(), "Label should emit zero bytes");
11430
11431 let encoder32 = ArmEncoder::new_arm32();
11432 let code32 = encoder32.encode(&op).unwrap();
11433 assert!(
11434 code32.is_empty(),
11435 "Label should emit zero bytes in ARM32 too"
11436 );
11437 }
11438
11439 #[test]
11440 fn test_encode_bcc_eq_thumb2() {
11441 use synth_synthesis::Condition;
11442 let encoder = ArmEncoder::new_thumb2();
11443 let op = ArmOp::Bcc {
11444 cond: Condition::EQ,
11445 label: "target".to_string(),
11446 };
11447 let code = encoder.encode(&op).unwrap();
11448 assert_eq!(code.len(), 2); // 16-bit conditional branch
11449
11450 // BEQ with offset 0: 0xD000 in little-endian
11451 assert_eq!(code, vec![0x00, 0xD0]);
11452 }
11453
11454 #[test]
11455 fn test_encode_bcc_ne_thumb2() {
11456 use synth_synthesis::Condition;
11457 let encoder = ArmEncoder::new_thumb2();
11458 let op = ArmOp::Bcc {
11459 cond: Condition::NE,
11460 label: "target".to_string(),
11461 };
11462 let code = encoder.encode(&op).unwrap();
11463 assert_eq!(code.len(), 2);
11464
11465 // BNE with offset 0: 0xD100 in little-endian
11466 assert_eq!(code, vec![0x00, 0xD1]);
11467 }
11468
11469 #[test]
11470 fn test_encode_bcc_arm32() {
11471 use synth_synthesis::Condition;
11472 let encoder = ArmEncoder::new_arm32();
11473 let op = ArmOp::Bcc {
11474 cond: Condition::EQ,
11475 label: "target".to_string(),
11476 };
11477 let code = encoder.encode(&op).unwrap();
11478 assert_eq!(code.len(), 4); // 32-bit ARM instruction
11479
11480 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11481 // BEQ: cond=0x0, opcode=0xA, offset=0
11482 assert_eq!(instr & 0xF0000000, 0x00000000); // EQ condition
11483 assert_eq!(instr & 0x0F000000, 0x0A000000); // Branch opcode
11484 }
11485
11486 #[test]
11487 fn test_encode_udf_thumb2() {
11488 let encoder = ArmEncoder::new_thumb2();
11489 let op = ArmOp::Udf { imm: 0 };
11490 let code = encoder.encode(&op).unwrap();
11491 assert_eq!(code.len(), 2); // 16-bit
11492
11493 // UDF #0: 0xDE00 in little-endian
11494 assert_eq!(code, vec![0x00, 0xDE]);
11495 }
11496
11497 /// #610: the i64 rot/div/rem expansions must land the result in the
11498 /// selector-assigned rd pair and leave R0-R3 preserved (restored from the
11499 /// fixed-ABI wrapper's save area) — pre-#610 the rot expansion's own
11500 /// `POP {R4}` restored stale scratch OVER the result (rd_lo == R4) and
11501 /// the div/rem expansions ignored their register fields outright.
11502 #[test]
11503 fn test_610_i64_rot_expansion_ends_with_rd_movs_and_restore() {
11504 let encoder = ArmEncoder::new_thumb2();
11505 for op in [
11506 ArmOp::I64Rotl {
11507 rdlo: Reg::R4,
11508 rdhi: Reg::R5,
11509 rnlo: Reg::R0,
11510 rnhi: Reg::R1,
11511 shift: Reg::R2,
11512 },
11513 ArmOp::I64Rotr {
11514 rdlo: Reg::R4,
11515 rdhi: Reg::R5,
11516 rnlo: Reg::R0,
11517 rnhi: Reg::R1,
11518 shift: Reg::R2,
11519 },
11520 ] {
11521 let code = encoder.encode(&op).unwrap();
11522 assert_eq!(code.len(), 102, "register-independent size (estimator pin)");
11523 // Tail: MOV r5, r1 (0x460D); MOV r4, r0 (0x4604); POP {r0..r3}
11524 // (rd pair r4:r5 does not overlap the save area — all 4 restored).
11525 let tail: Vec<u16> = code[code.len() - 12..]
11526 .chunks(2)
11527 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11528 .collect();
11529 assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11530 }
11531 }
11532
11533 /// #610: div/rem expansions honor rd and carry the divide-by-zero trap
11534 /// guard (`ORRS R12, R2, R3; BNE +0; UDF #0`) after operand marshaling.
11535 #[test]
11536 fn test_610_i64_div_rem_expansion_guard_and_rd() {
11537 let encoder = ArmEncoder::new_thumb2();
11538 let mk = |which: u8| {
11539 let (rdlo, rdhi, rnlo, rnhi, rmlo, rmhi) =
11540 (Reg::R4, Reg::R5, Reg::R0, Reg::R1, Reg::R2, Reg::R3);
11541 match which {
11542 0 => ArmOp::I64DivU {
11543 rdlo,
11544 rdhi,
11545 rnlo,
11546 rnhi,
11547 rmlo,
11548 rmhi,
11549 elide_zero_guard: false,
11550 },
11551 1 => ArmOp::I64RemU {
11552 rdlo,
11553 rdhi,
11554 rnlo,
11555 rnhi,
11556 rmlo,
11557 rmhi,
11558 elide_zero_guard: false,
11559 },
11560 2 => ArmOp::I64DivS {
11561 rdlo,
11562 rdhi,
11563 rnlo,
11564 rnhi,
11565 rmlo,
11566 rmhi,
11567 elide_zero_guard: false,
11568 elide_overflow_guard: false,
11569 },
11570 _ => ArmOp::I64RemS {
11571 rdlo,
11572 rdhi,
11573 rnlo,
11574 rnhi,
11575 rmlo,
11576 rmhi,
11577 elide_zero_guard: false,
11578 },
11579 }
11580 };
11581 for which in 0..4u8 {
11582 let code = encoder.encode(&mk(which)).unwrap();
11583 // Zero-divisor trap guard right after the 26-byte marshal prologue.
11584 let guard: Vec<u16> = code[26..34]
11585 .chunks(2)
11586 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11587 .collect();
11588 assert_eq!(
11589 guard,
11590 vec![0xEA52, 0x0C03, 0xD100, 0xDE00],
11591 "ORRS R12,R2,R3; BNE +0; UDF #0"
11592 );
11593 // Tail: result into rd pair (r5:r4), then restore all of R0-R3.
11594 let tail: Vec<u16> = code[code.len() - 12..]
11595 .chunks(2)
11596 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11597 .collect();
11598 assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11599 }
11600 }
11601
11602 /// #610: when rd overlaps R0-R3 the restore must SKIP the result
11603 /// registers (drop the saved caller word) instead of popping over them.
11604 #[test]
11605 fn test_610_i64_divu_rd_in_r0_r1_skips_restore() {
11606 let encoder = ArmEncoder::new_thumb2();
11607 let code = encoder
11608 .encode(&ArmOp::I64DivU {
11609 rdlo: Reg::R0,
11610 rdhi: Reg::R1,
11611 rnlo: Reg::R0,
11612 rnhi: Reg::R1,
11613 rmlo: Reg::R2,
11614 rmhi: Reg::R3,
11615 elide_zero_guard: false,
11616 })
11617 .unwrap();
11618 let tail: Vec<u16> = code[code.len() - 12..]
11619 .chunks(2)
11620 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11621 .collect();
11622 // MOV r1,r1 / MOV r0,r0 (no-ops, size-stable), ADD SP,#4 twice
11623 // (discard saved r0/r1 — the result lives there), POP {r2}, POP {r3}.
11624 assert_eq!(tail, vec![0x4609, 0x4600, 0xB001, 0xB001, 0xBC04, 0xBC08]);
11625 }
11626
11627 /// #610: a fully swapped rd pair (rd_lo=R1, rd_hi=R0) cannot be
11628 /// materialized by two MOVs in either order — must be a loud Err, never
11629 /// silent corruption. (Selector pairs are consecutive, so unreachable.)
11630 #[test]
11631 fn test_610_i64_swapped_rd_pair_rejected() {
11632 let encoder = ArmEncoder::new_thumb2();
11633 let result = encoder.encode(&ArmOp::I64RemU {
11634 rdlo: Reg::R1,
11635 rdhi: Reg::R0,
11636 rnlo: Reg::R2,
11637 rnhi: Reg::R3,
11638 rmlo: Reg::R4,
11639 rmhi: Reg::R5,
11640 elide_zero_guard: false,
11641 });
11642 assert!(result.is_err(), "swapped rd pair must be rejected loudly");
11643 }
11644
11645 /// #632: the I64Popcnt expansion's own scratch restore (`POP {R3,R4,R5}`)
11646 /// must not clobber the result. Pre-fix the total was materialized with
11647 /// `ADDS rd, R4, R5` BEFORE the pop, so any allocator-assigned
11648 /// rd ∈ {R3,R4,R5} received stale stack garbage. Post-fix the count is
11649 /// carried across the restore in R12 (never allocatable, never restored)
11650 /// and moved into rd only after the pop — structurally rd-independent.
11651 #[test]
11652 fn test_632_i64_popcnt_result_survives_scratch_restore() {
11653 let encoder = ArmEncoder::new_thumb2();
11654 // Every allocatable rd, including the restore set {R3,R4,R5} and R8.
11655 for rd in [
11656 Reg::R0,
11657 Reg::R2,
11658 Reg::R3,
11659 Reg::R4,
11660 Reg::R5,
11661 Reg::R6,
11662 Reg::R8,
11663 ] {
11664 let code = encoder
11665 .encode(&ArmOp::I64Popcnt {
11666 rd,
11667 rnlo: Reg::R6,
11668 rnhi: Reg::R7,
11669 })
11670 .unwrap();
11671 assert_eq!(code.len(), 176, "register-independent size (estimator pin)");
11672 let hw: Vec<u16> = code
11673 .chunks(2)
11674 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11675 .collect();
11676 let pop = hw
11677 .iter()
11678 .position(|&h| h == 0xBC38)
11679 .expect("POP {R3,R4,R5} present");
11680 // Immediately before the POP: ADD.W R12, R4, R5 (the total lives
11681 // in R12, which the POP cannot touch).
11682 assert_eq!(
11683 &hw[pop - 2..pop],
11684 &[0xEB04, 0x0C05],
11685 "total must be carried in R12 across the restore"
11686 );
11687 // Immediately after the POP: MOV rd, R12.
11688 let rd_bits = match rd {
11689 Reg::R8 => 8u16,
11690 Reg::R6 => 6,
11691 Reg::R5 => 5,
11692 Reg::R4 => 4,
11693 Reg::R3 => 3,
11694 Reg::R2 => 2,
11695 _ => 0,
11696 };
11697 let expect_mov = 0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7);
11698 assert_eq!(hw[pop + 1], expect_mov, "MOV rd, R12 after the restore");
11699 // No write into rd between the PUSH and the POP (the old
11700 // pre-restore ADDS is gone).
11701 assert!(
11702 !hw[..pop].contains(&(0x1800 | (5 << 6) | (4 << 3) | rd_bits)),
11703 "no ADDS rd, R4, R5 before the restore pop"
11704 );
11705 }
11706 }
11707
11708 /// #632 audit: the entry marshal must be permutation-safe. Pre-fix
11709 /// `MOV R4, rnlo; MOV R5, rnhi` read a clobbered R4 when the operand
11710 /// pair lived at (R3, R4). Post-fix rnlo routes through R12.
11711 #[test]
11712 fn test_632_i64_popcnt_marshal_pair_at_r3_r4() {
11713 let encoder = ArmEncoder::new_thumb2();
11714 let code = encoder
11715 .encode(&ArmOp::I64Popcnt {
11716 rd: Reg::R0,
11717 rnlo: Reg::R3,
11718 rnhi: Reg::R4,
11719 })
11720 .unwrap();
11721 let hw: Vec<u16> = code
11722 .chunks(2)
11723 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11724 .collect();
11725 // PUSH {R3,R4,R5}; MOV R12, R3; MOV R5, R4 (rnhi read BEFORE any
11726 // write to R4); MOV R4, R12.
11727 assert_eq!(hw[0], 0xB438);
11728 assert_eq!(hw[1], 0x4600 | (1 << 7) | (3 << 3) | 4, "MOV R12, rnlo");
11729 assert_eq!(hw[2], 0x4600 | (4 << 3) | 5, "MOV R5, rnhi");
11730 assert_eq!(hw[3], 0x4664, "MOV R4, R12");
11731 }
11732
11733 /// #632: A32 twin — same structural fix on the ARM-mode path
11734 /// (`--target cortex-r5`): total carried in R12 across the restore.
11735 #[test]
11736 fn test_632_a32_i64_popcnt_result_survives_scratch_restore() {
11737 let encoder = ArmEncoder::new_arm32();
11738 for rd in [Reg::R0, Reg::R3, Reg::R4, Reg::R5, Reg::R8] {
11739 let code = encoder
11740 .encode(&ArmOp::I64Popcnt {
11741 rd,
11742 rnlo: Reg::R6,
11743 rnhi: Reg::R7,
11744 })
11745 .unwrap();
11746 let words: Vec<u32> = code
11747 .chunks(4)
11748 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11749 .collect();
11750 let pop = words
11751 .iter()
11752 .position(|&w| w == 0xE8BD_0038)
11753 .expect("POP {R3,R4,R5} present");
11754 assert_eq!(words[pop - 1], 0xE084_C005, "ADD R12, R4, R5 before POP");
11755 let rd_bits = match rd {
11756 Reg::R8 => 8u32,
11757 Reg::R5 => 5,
11758 Reg::R4 => 4,
11759 Reg::R3 => 3,
11760 _ => 0,
11761 };
11762 assert_eq!(
11763 words[pop + 1],
11764 0xE1A0_0000 | (rd_bits << 12) | 12,
11765 "MOV rd, R12 after the restore"
11766 );
11767 }
11768 }
11769
11770 /// #633: I64DivS must carry the INT64_MIN/-1 overflow guard (mirroring
11771 /// the i32 path) right after the zero-divisor guard — dividend in R0:R1,
11772 /// divisor in R2:R3 on the #610/#613 fixed-ABI wrapper path.
11773 #[test]
11774 fn test_633_i64_divs_overflow_guard_emitted() {
11775 let encoder = ArmEncoder::new_thumb2();
11776 let code = encoder
11777 .encode(&ArmOp::I64DivS {
11778 rdlo: Reg::R4,
11779 rdhi: Reg::R5,
11780 rnlo: Reg::R0,
11781 rnhi: Reg::R1,
11782 rmlo: Reg::R2,
11783 rmhi: Reg::R3,
11784 elide_zero_guard: false,
11785 elide_overflow_guard: false,
11786 })
11787 .unwrap();
11788 // 26-byte marshal + 8-byte zero-trap, then the 22-byte overflow guard.
11789 let guard: Vec<u16> = code[34..56]
11790 .chunks(2)
11791 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11792 .collect();
11793 assert_eq!(
11794 guard,
11795 vec![
11796 0xEA02, 0x0C03, // AND.W R12, R2, R3
11797 0xF11C, 0x0F01, // CMN.W R12, #1
11798 0xD105, // BNE .no_trap
11799 0x2800, // CMP R0, #0
11800 0xD103, // BNE .no_trap
11801 0xF1B1, 0x4F00, // CMP.W R1, #0x80000000
11802 0xD100, // BNE .no_trap
11803 0xDE00, // UDF #0 — signed-division overflow
11804 ],
11805 "INT64_MIN/-1 overflow guard after the zero-divisor guard"
11806 );
11807 }
11808
11809 /// #633 fix-guard twin: I64RemS must NOT carry the overflow guard —
11810 /// rem_s(INT64_MIN, -1) is defined as 0 and must not trap. Exactly one
11811 /// UDF (the zero-divisor trap) in the whole expansion.
11812 #[test]
11813 fn test_633_i64_rems_has_no_overflow_guard() {
11814 let encoder = ArmEncoder::new_thumb2();
11815 for (is_rem_s, op) in [
11816 (
11817 true,
11818 ArmOp::I64RemS {
11819 rdlo: Reg::R4,
11820 rdhi: Reg::R5,
11821 rnlo: Reg::R0,
11822 rnhi: Reg::R1,
11823 rmlo: Reg::R2,
11824 rmhi: Reg::R3,
11825 elide_zero_guard: false,
11826 },
11827 ),
11828 (
11829 false,
11830 ArmOp::I64DivS {
11831 rdlo: Reg::R4,
11832 rdhi: Reg::R5,
11833 rnlo: Reg::R0,
11834 rnhi: Reg::R1,
11835 rmlo: Reg::R2,
11836 rmhi: Reg::R3,
11837 elide_zero_guard: false,
11838 elide_overflow_guard: false,
11839 },
11840 ),
11841 ] {
11842 let code = encoder.encode(&op).unwrap();
11843 let udfs = code
11844 .chunks(2)
11845 .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11846 .count();
11847 let want = if is_rem_s { 1 } else { 2 };
11848 assert_eq!(
11849 udfs, want,
11850 "rem_s: zero-trap only; div_s: zero-trap + overflow trap"
11851 );
11852 }
11853 }
11854
11855 /// #494 phase 2b: `elide_zero_guard` drops EXACTLY the 8-byte fused
11856 /// zero-trap (`ORRS.W R12,R2,R3; BNE; UDF #0`) and nothing else — the
11857 /// rest of the expansion is byte-identical (splice check).
11858 #[test]
11859 fn test_494_i64_zero_guard_elision_is_exact_splice() {
11860 let encoder = ArmEncoder::new_thumb2();
11861 let mk = |elide_zero_guard: bool| {
11862 encoder
11863 .encode(&ArmOp::I64DivU {
11864 rdlo: Reg::R4,
11865 rdhi: Reg::R5,
11866 rnlo: Reg::R0,
11867 rnhi: Reg::R1,
11868 rmlo: Reg::R2,
11869 rmhi: Reg::R3,
11870 elide_zero_guard,
11871 })
11872 .unwrap()
11873 };
11874 let full = mk(false);
11875 let elided = mk(true);
11876 assert_eq!(full.len(), elided.len() + 8, "zero guard is 8 bytes");
11877 // Marshal prologue (26 B) unchanged, guard (8 B) gone, tail identical.
11878 assert_eq!(&full[..26], &elided[..26]);
11879 assert_eq!(
11880 &full[26..34],
11881 &[0x52, 0xEA, 0x03, 0x0C, 0x00, 0xD1, 0x00, 0xDE],
11882 "the spliced-out bytes are exactly ORRS.W; BNE; UDF #0"
11883 );
11884 assert_eq!(&full[34..], &elided[26..]);
11885 }
11886
11887 /// #494 phase 2b two-guard distinction (the #633/#634 synergy): a
11888 /// divisor-nonzero fact elides ONLY the zero guard — the INT64_MIN/-1
11889 /// OVERFLOW guard is a separate obligation and must survive
11890 /// `elide_zero_guard: true`. Pinned on div_s in all flag states.
11891 #[test]
11892 fn test_494_i64_divs_overflow_guard_retained_when_only_zero_elided() {
11893 let encoder = ArmEncoder::new_thumb2();
11894 let mk = |zero: bool, ovf: bool| {
11895 encoder
11896 .encode(&ArmOp::I64DivS {
11897 rdlo: Reg::R4,
11898 rdhi: Reg::R5,
11899 rnlo: Reg::R0,
11900 rnhi: Reg::R1,
11901 rmlo: Reg::R2,
11902 rmhi: Reg::R3,
11903 elide_zero_guard: zero,
11904 elide_overflow_guard: ovf,
11905 })
11906 .unwrap()
11907 };
11908 let udf_count = |code: &[u8]| {
11909 code.chunks(2)
11910 .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11911 .count()
11912 };
11913 let full = mk(false, false);
11914 let zero_only = mk(true, false);
11915 let both = mk(true, true);
11916 assert_eq!(udf_count(&full), 2, "baseline: zero trap + overflow trap");
11917 assert_eq!(
11918 udf_count(&zero_only),
11919 1,
11920 "divisor-nonzero elides the zero trap ONLY — the #633 overflow \
11921 guard must be retained"
11922 );
11923 // The retained guard is the 22-byte overflow sequence, now right
11924 // after the 26-byte marshal prologue.
11925 let guard: Vec<u16> = zero_only[26..48]
11926 .chunks(2)
11927 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11928 .collect();
11929 assert_eq!(
11930 guard,
11931 vec![
11932 0xEA02, 0x0C03, 0xF11C, 0x0F01, 0xD105, 0x2800, 0xD103, 0xF1B1, 0x4F00, 0xD100,
11933 0xDE00,
11934 ],
11935 "the surviving guard is the INT64_MIN/-1 overflow trap"
11936 );
11937 assert_eq!(full.len(), zero_only.len() + 8);
11938 assert_eq!(zero_only.len(), both.len() + 22);
11939 assert_eq!(udf_count(&both), 0, "both obligations discharged ⇒ no UDF");
11940 }
11941
11942 /// #494 phase 2b A32 twin: zero-guard elision is an exact 12-byte splice
11943 /// and the A32 overflow guard survives a zero-only elision.
11944 #[test]
11945 fn test_494_a32_i64_guard_elision() {
11946 let encoder = ArmEncoder::new_arm32();
11947 let mk = |zero: bool, ovf: bool| {
11948 encoder
11949 .encode(&ArmOp::I64DivS {
11950 rdlo: Reg::R4,
11951 rdhi: Reg::R5,
11952 rnlo: Reg::R0,
11953 rnhi: Reg::R1,
11954 rmlo: Reg::R2,
11955 rmhi: Reg::R3,
11956 elide_zero_guard: zero,
11957 elide_overflow_guard: ovf,
11958 })
11959 .unwrap()
11960 };
11961 let full = mk(false, false);
11962 let zero_only = mk(true, false);
11963 let both = mk(true, true);
11964 // A32 zero guard = 3 words (ORRS/BNE/UDF), overflow guard = 6 words.
11965 assert_eq!(full.len(), zero_only.len() + 12);
11966 assert_eq!(zero_only.len(), both.len() + 24);
11967 let udf_count = |code: &[u8]| {
11968 code.chunks(4)
11969 .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
11970 .count()
11971 };
11972 assert_eq!(udf_count(&full), 2);
11973 assert_eq!(
11974 udf_count(&zero_only),
11975 1,
11976 "A32: overflow guard retained under zero-only elision"
11977 );
11978 assert_eq!(udf_count(&both), 0);
11979 }
11980
11981 /// #633: A32 twin — the conditional-execution overflow guard on the
11982 /// ARM-mode I64DivS, and its absence from I64RemS.
11983 #[test]
11984 fn test_633_a32_i64_divs_overflow_guard() {
11985 let encoder = ArmEncoder::new_arm32();
11986 let mk_divs = ArmOp::I64DivS {
11987 rdlo: Reg::R4,
11988 rdhi: Reg::R5,
11989 rnlo: Reg::R0,
11990 rnhi: Reg::R1,
11991 rmlo: Reg::R2,
11992 rmhi: Reg::R3,
11993 elide_zero_guard: false,
11994 elide_overflow_guard: false,
11995 };
11996 let code = encoder.encode(&mk_divs).unwrap();
11997 let words: Vec<u32> = code
11998 .chunks(4)
11999 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
12000 .collect();
12001 let guard = [
12002 0xE002_C003u32, // AND R12, R2, R3
12003 0xE37C_0001, // CMN R12, #1
12004 0x0350_0000, // CMPEQ R0, #0
12005 0x0351_0102, // CMPEQ R1, #0x80000000
12006 0x1A00_0000, // BNE +1 insn
12007 0xE7F0_00F0, // UDF #0
12008 ];
12009 assert!(
12010 words.windows(6).any(|w| w == guard),
12011 "A32 I64DivS carries the INT64_MIN/-1 overflow guard"
12012 );
12013 let rems = encoder
12014 .encode(&ArmOp::I64RemS {
12015 rdlo: Reg::R4,
12016 rdhi: Reg::R5,
12017 rnlo: Reg::R0,
12018 rnhi: Reg::R1,
12019 rmlo: Reg::R2,
12020 rmhi: Reg::R3,
12021 elide_zero_guard: false,
12022 })
12023 .unwrap();
12024 let rems_udfs = rems
12025 .chunks(4)
12026 .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
12027 .count();
12028 assert_eq!(rems_udfs, 1, "A32 I64RemS keeps only the zero-divisor trap");
12029 }
12030
12031 #[test]
12032 fn test_encode_nop_thumb2() {
12033 let encoder = ArmEncoder::new_thumb2();
12034 let op = ArmOp::Nop;
12035 let code = encoder.encode(&op).unwrap();
12036 assert_eq!(code.len(), 2); // 16-bit
12037
12038 // NOP: 0xBF00 in little-endian
12039 assert_eq!(code, vec![0x00, 0xBF]);
12040 }
12041
12042 // =========================================================================
12043 // i64 Thumb-2 encoding tests
12044 // =========================================================================
12045
12046 #[test]
12047 fn test_encode_i64_add_thumb2() {
12048 let encoder = ArmEncoder::new_thumb2();
12049 let op = ArmOp::I64Add {
12050 rdlo: Reg::R0,
12051 rdhi: Reg::R1,
12052 rnlo: Reg::R0,
12053 rnhi: Reg::R1,
12054 rmlo: Reg::R2,
12055 rmhi: Reg::R3,
12056 };
12057 let code = encoder.encode(&op).unwrap();
12058 // Should emit ADDS (2 bytes) + ADC.W (4 bytes) = 6 bytes
12059 assert_eq!(code.len(), 6, "I64Add should be 6 bytes (ADDS + ADC.W)");
12060 }
12061
12062 #[test]
12063 fn test_encode_i64_sub_thumb2() {
12064 let encoder = ArmEncoder::new_thumb2();
12065 let op = ArmOp::I64Sub {
12066 rdlo: Reg::R0,
12067 rdhi: Reg::R1,
12068 rnlo: Reg::R0,
12069 rnhi: Reg::R1,
12070 rmlo: Reg::R2,
12071 rmhi: Reg::R3,
12072 };
12073 let code = encoder.encode(&op).unwrap();
12074 // Should emit SUBS (2 bytes) + SBC.W (4 bytes) = 6 bytes
12075 assert_eq!(code.len(), 6, "I64Sub should be 6 bytes (SUBS + SBC.W)");
12076 }
12077
12078 #[test]
12079 fn test_encode_i64_and_thumb2() {
12080 let encoder = ArmEncoder::new_thumb2();
12081 let op = ArmOp::I64And {
12082 rdlo: Reg::R0,
12083 rdhi: Reg::R1,
12084 rnlo: Reg::R0,
12085 rnhi: Reg::R1,
12086 rmlo: Reg::R2,
12087 rmhi: Reg::R3,
12088 };
12089 let code = encoder.encode(&op).unwrap();
12090 // AND.W (4 bytes) + AND.W (4 bytes) = 8 bytes
12091 assert!(code.len() >= 4, "I64And should emit at least 4 bytes");
12092 }
12093
12094 #[test]
12095 fn test_encode_i64_or_thumb2() {
12096 let encoder = ArmEncoder::new_thumb2();
12097 let op = ArmOp::I64Or {
12098 rdlo: Reg::R0,
12099 rdhi: Reg::R1,
12100 rnlo: Reg::R0,
12101 rnhi: Reg::R1,
12102 rmlo: Reg::R2,
12103 rmhi: Reg::R3,
12104 };
12105 let code = encoder.encode(&op).unwrap();
12106 assert!(code.len() >= 4, "I64Or should emit at least 4 bytes");
12107 }
12108
12109 #[test]
12110 fn test_encode_i64_xor_thumb2() {
12111 let encoder = ArmEncoder::new_thumb2();
12112 let op = ArmOp::I64Xor {
12113 rdlo: Reg::R0,
12114 rdhi: Reg::R1,
12115 rnlo: Reg::R0,
12116 rnhi: Reg::R1,
12117 rmlo: Reg::R2,
12118 rmhi: Reg::R3,
12119 };
12120 let code = encoder.encode(&op).unwrap();
12121 assert!(code.len() >= 4, "I64Xor should emit at least 4 bytes");
12122 }
12123
12124 #[test]
12125 fn test_encode_i64_const_small_thumb2() {
12126 let encoder = ArmEncoder::new_thumb2();
12127 // Small constant: only needs MOVW for each half
12128 let op = ArmOp::I64Const {
12129 rdlo: Reg::R0,
12130 rdhi: Reg::R1,
12131 value: 42,
12132 };
12133 let code = encoder.encode(&op).unwrap();
12134 // MOVW R0, #42 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes minimum
12135 assert!(code.len() >= 8, "I64Const should emit at least 8 bytes");
12136 }
12137
12138 #[test]
12139 fn test_encode_i64_const_large_thumb2() {
12140 let encoder = ArmEncoder::new_thumb2();
12141 // Large constant: needs MOVW+MOVT for each half
12142 let op = ArmOp::I64Const {
12143 rdlo: Reg::R0,
12144 rdhi: Reg::R1,
12145 value: 0x1234_5678_9ABC_DEF0_u64 as i64,
12146 };
12147 let code = encoder.encode(&op).unwrap();
12148 // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
12149 assert_eq!(
12150 code.len(),
12151 16,
12152 "I64Const with large value should be 16 bytes"
12153 );
12154 }
12155
12156 #[test]
12157 fn test_encode_i64_extend_i32_s_thumb2() {
12158 let encoder = ArmEncoder::new_thumb2();
12159 let op = ArmOp::I64ExtendI32S {
12160 rdlo: Reg::R0,
12161 rdhi: Reg::R1,
12162 rn: Reg::R0,
12163 };
12164 let code = encoder.encode(&op).unwrap();
12165 // When rdlo == rn, only ASR (4 bytes) is emitted
12166 assert_eq!(
12167 code.len(),
12168 4,
12169 "I64ExtendI32S (same reg) should be 4 bytes (ASR only)"
12170 );
12171 }
12172
12173 #[test]
12174 fn test_encode_i64_extend_i32_s_diff_reg_thumb2() {
12175 let encoder = ArmEncoder::new_thumb2();
12176 let op = ArmOp::I64ExtendI32S {
12177 rdlo: Reg::R0,
12178 rdhi: Reg::R1,
12179 rn: Reg::R2,
12180 };
12181 let code = encoder.encode(&op).unwrap();
12182 // MOV rdlo, rn (2 bytes for low regs) + ASR rdhi, rdlo, #31 (4 bytes) = 6 bytes
12183 assert!(
12184 code.len() >= 6,
12185 "I64ExtendI32S (diff reg) should be at least 6 bytes"
12186 );
12187 }
12188
12189 #[test]
12190 fn test_encode_i64_extend_i32_u_thumb2() {
12191 let encoder = ArmEncoder::new_thumb2();
12192 let op = ArmOp::I64ExtendI32U {
12193 rdlo: Reg::R0,
12194 rdhi: Reg::R1,
12195 rn: Reg::R0,
12196 };
12197 let code = encoder.encode(&op).unwrap();
12198 // When rdlo == rn, only MOV rdhi, #0 (2 bytes) is emitted
12199 assert_eq!(
12200 code.len(),
12201 2,
12202 "I64ExtendI32U (same reg) should be 2 bytes (MOV #0 only)"
12203 );
12204 }
12205
12206 #[test]
12207 fn test_encode_i32_wrap_i64_nop_thumb2() {
12208 let encoder = ArmEncoder::new_thumb2();
12209 // When rd == rnlo, should be a NOP
12210 let op = ArmOp::I32WrapI64 {
12211 rd: Reg::R0,
12212 rnlo: Reg::R0,
12213 };
12214 let code = encoder.encode(&op).unwrap();
12215 assert_eq!(code.len(), 2, "I32WrapI64 same reg should be NOP (2 bytes)");
12216 assert_eq!(code, vec![0x00, 0xBF]); // NOP
12217 }
12218
12219 #[test]
12220 fn test_encode_i32_wrap_i64_diff_reg_thumb2() {
12221 let encoder = ArmEncoder::new_thumb2();
12222 let op = ArmOp::I32WrapI64 {
12223 rd: Reg::R2,
12224 rnlo: Reg::R0,
12225 };
12226 let code = encoder.encode(&op).unwrap();
12227 // MOV R2, R0 (2 or 4 bytes)
12228 assert!(
12229 code.len() >= 2,
12230 "I32WrapI64 diff reg should emit at least 2 bytes"
12231 );
12232 }
12233
12234 #[test]
12235 fn test_encode_i64_eqz_thumb2() {
12236 let encoder = ArmEncoder::new_thumb2();
12237 let op = ArmOp::I64Eqz {
12238 rd: Reg::R0,
12239 rnlo: Reg::R0,
12240 rnhi: Reg::R1,
12241 };
12242 let code = encoder.encode(&op).unwrap();
12243 // Delegates to I64SetCondZ which is already encoded
12244 assert!(
12245 code.len() >= 6,
12246 "I64Eqz should emit at least 6 bytes for ORR+ITE+MOV+MOV"
12247 );
12248 }
12249
12250 #[test]
12251 fn test_encode_i64_eq_thumb2() {
12252 let encoder = ArmEncoder::new_thumb2();
12253 let op = ArmOp::I64Eq {
12254 rd: Reg::R0,
12255 rnlo: Reg::R0,
12256 rnhi: Reg::R1,
12257 rmlo: Reg::R2,
12258 rmhi: Reg::R3,
12259 };
12260 let code = encoder.encode(&op).unwrap();
12261 // Delegates to I64SetCond EQ: CMP lo + IT EQ + CMPEQ hi + ITE EQ + MOV 1 + MOV 0
12262 assert!(code.len() >= 10, "I64Eq should emit at least 10 bytes");
12263 }
12264
12265 #[test]
12266 fn test_encode_i64_ldr_thumb2() {
12267 let encoder = ArmEncoder::new_thumb2();
12268 let op = ArmOp::I64Ldr {
12269 rdlo: Reg::R0,
12270 rdhi: Reg::R1,
12271 addr: MemAddr::imm(Reg::SP, 0),
12272 };
12273 let code = encoder.encode(&op).unwrap();
12274 // Two LDR instructions (lo at offset, hi at offset+4)
12275 assert!(code.len() >= 4, "I64Ldr should emit at least 4 bytes");
12276 }
12277
12278 #[test]
12279 fn test_372_i64_ldr_indexed_materializes_address() {
12280 // #372: a memory i64.load carries an index register (R11 + addr + off).
12281 // The encoder must materialize `ip = base + index` (ADD.W) and load via
12282 // `[ip,#off]` — NOT drop the index. A frame (non-indexed) i64.load must
12283 // stay byte-identical (plain `[base,#off]`, no ADD).
12284 let encoder = ArmEncoder::new_thumb2();
12285 let indexed = encoder
12286 .encode(&ArmOp::I64Ldr {
12287 rdlo: Reg::R0,
12288 rdhi: Reg::R1,
12289 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
12290 })
12291 .unwrap();
12292 // ADD.W ip, fp, r0 = eb0b 0c00 (byte-verified vs arm-none-eabi-as).
12293 assert_eq!(
12294 &indexed[0..4],
12295 &[0x0b, 0xeb, 0x00, 0x0c],
12296 "indexed I64Ldr must start with ADD.W ip, base, index"
12297 );
12298 let frame = encoder
12299 .encode(&ArmOp::I64Ldr {
12300 rdlo: Reg::R0,
12301 rdhi: Reg::R1,
12302 addr: MemAddr::imm(Reg::SP, 8),
12303 })
12304 .unwrap();
12305 // No index -> no ADD.W prefix (byte-identical frame access).
12306 assert_ne!(
12307 &frame[0..2],
12308 &[0x0b, 0xeb],
12309 "frame (non-indexed) I64Ldr must NOT emit an ADD.W"
12310 );
12311 }
12312
12313 #[test]
12314 fn test_382_i64_ldst_large_offset_materializes_not_skips() {
12315 // #382: an indexed i64.load/store whose static offset > 0xFFF must
12316 // MATERIALIZE the offset into the base — NOT return Err (skip the fn).
12317 // Sequence for reg_imm(R11, R0, 5000): MOVW ip,#5000 ; ADD ip,r0,ip ;
12318 // ADD ip,ip,fp ; LDR/STR halves at [ip,#0] / [ip,#4]. Byte-verified tail
12319 // vs arm-none-eabi-as.
12320 let encoder = ArmEncoder::new_thumb2();
12321 // 0x1388 > 0xFFF (MemAddr is not Copy, so build it per use).
12322
12323 let ld = encoder
12324 .encode(&ArmOp::I64Ldr {
12325 rdlo: Reg::R0,
12326 rdhi: Reg::R1,
12327 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
12328 })
12329 .expect("large-offset i64.load must lower, not skip");
12330 // MOVW ip,#0x1388 (4) + ADD ip,r0,ip (4) + ADD ip,ip,fp (4) + 2 LDR (8).
12331 assert_eq!(ld.len(), 20, "expected MOVW + 2×ADD + 2×LDR");
12332 // Must NOT be the small-offset `ADD.W ip, fp, r0` (0x0b 0xeb) prefix —
12333 // that path can only reach imm12 offsets.
12334 assert_ne!(
12335 &ld[0..2],
12336 &[0x0b, 0xeb],
12337 "must materialize the large offset"
12338 );
12339 // Effective base built in ip, then halves at [ip,#0] / [ip,#4].
12340 assert_eq!(
12341 &ld[4..20],
12342 &[
12343 0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
12344 0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
12345 0xdc, 0xf8, 0x00, 0x00, // LDR.W r0, [ip, #0]
12346 0xdc, 0xf8, 0x04, 0x10, // LDR.W r1, [ip, #4]
12347 ],
12348 "large-offset i64.load must fold offset into ip and access [ip,#0]/[ip,#4]"
12349 );
12350
12351 // Store: same base materialization, STR halves.
12352 let st = encoder
12353 .encode(&ArmOp::I64Str {
12354 rdlo: Reg::R2,
12355 rdhi: Reg::R3,
12356 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
12357 })
12358 .expect("large-offset i64.store must lower, not skip");
12359 assert_eq!(st.len(), 20);
12360 assert_eq!(
12361 &st[4..20],
12362 &[
12363 0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
12364 0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
12365 0xcc, 0xf8, 0x00, 0x20, // STR.W r2, [ip, #0]
12366 0xcc, 0xf8, 0x04, 0x30, // STR.W r3, [ip, #4]
12367 ],
12368 "large-offset i64.store must fold offset into ip and access [ip,#0]/[ip,#4]"
12369 );
12370
12371 // Small-offset (imm12) indexed access stays byte-identical (#372): the
12372 // effective base is a single `ADD.W ip, fp, r0` and the halves keep the
12373 // folded immediates — NO extra MOVW/ADD.
12374 let small = encoder
12375 .encode(&ArmOp::I64Ldr {
12376 rdlo: Reg::R0,
12377 rdhi: Reg::R1,
12378 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 8),
12379 })
12380 .unwrap();
12381 assert_eq!(
12382 &small[0..4],
12383 &[0x0b, 0xeb, 0x00, 0x0c],
12384 "small-offset indexed i64 must keep the single ADD.W ip, fp, r0"
12385 );
12386 assert_eq!(small.len(), 12, "ADD.W + 2×LDR.W (offset folded in imm12)");
12387 }
12388
12389 /// RQ-63-ARMI64OFF (#1165): A32 `I64Ldr`/`I64Str` at and past the pair
12390 /// form's immediate boundary. Bytes verified against `arm-none-eabi-as`
12391 /// (`.arch armv7-r`, `.arm`). 4091 is the last offset the pair folds (the
12392 /// high half sits at 4095 = imm12 max); 4092 is the straddle the v0.62
12393 /// census declined on (low half fits, high half does not).
12394 #[test]
12395 fn test_1165_a32_i64_ldst_offset_boundary_materializes() {
12396 let enc = ArmEncoder::new_arm32();
12397 let le = |ws: &[u32]| ws.iter().flat_map(|w| w.to_le_bytes()).collect::<Vec<u8>>();
12398 let ld = |off: i32| {
12399 enc.encode(&ArmOp::I64Ldr {
12400 rdlo: Reg::R0,
12401 rdhi: Reg::R1,
12402 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, off),
12403 })
12404 };
12405 // AT the boundary (4091): folded, byte-identical to the pre-#1165 form.
12406 assert_eq!(
12407 ld(4091).unwrap(),
12408 le(&[0xE08BC000, 0xE59C0FFB, 0xE59C1FFF]),
12409 "4091: add ip,fp,r0 ; ldr r0,[ip,#4091] ; ldr r1,[ip,#4095]"
12410 );
12411 // ONE ABOVE (4092): the high half would need #4096 → materialize.
12412 assert_eq!(
12413 ld(4092).unwrap(),
12414 le(&[0xE300CFFC, 0xE08CC00B, 0xE08CC000, 0xE59C0000, 0xE59C1004]),
12415 "4092: movw ip,#4092 ; add ip,ip,fp ; add ip,ip,r0 ; ldr r0,[ip] ; ldr r1,[ip,#4]"
12416 );
12417 // 4095 (low half at the imm12 max), 4096 (both out), 5000.
12418 assert_eq!(ld(4095).unwrap()[..4], le(&[0xE300CFFF])[..]);
12419 assert_eq!(ld(4096).unwrap()[..4], le(&[0xE301C000])[..]);
12420 assert_eq!(
12421 ld(5000).unwrap(),
12422 le(&[0xE301C388, 0xE08CC00B, 0xE08CC000, 0xE59C0000, 0xE59C1004])
12423 );
12424 // 70000 needs MOVT; store form.
12425 assert_eq!(
12426 enc.encode(&ArmOp::I64Str {
12427 rdlo: Reg::R2,
12428 rdhi: Reg::R3,
12429 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 70000),
12430 })
12431 .unwrap(),
12432 le(&[
12433 0xE301C170, 0xE340C001, 0xE08CC00B, 0xE08CC000, 0xE58C2000, 0xE58C3004
12434 ]),
12435 "70000: movw ip,#0x1170 ; movt ip,#1 ; add ip,ip,fp ; add ip,ip,r0 ; str r2,[ip] ; str r3,[ip,#4]"
12436 );
12437 // A memarg >= 2^31 arrives negative (0xFFFFFFF8 as i32 = -8): the full
12438 // 32 bits are materialized, mod 2^32 — never clamped, never masked.
12439 assert_eq!(
12440 ld(-8).unwrap(),
12441 le(&[
12442 0xE30FCFF8, 0xE34FCFFF, 0xE08CC00B, 0xE08CC000, 0xE59C0000, 0xE59C1004
12443 ]),
12444 "0xfffffff8: movw ip,#0xfff8 ; movt ip,#0xffff ; add ; add ; ldr ; ldr"
12445 );
12446 // No index register (frame-style base), out of range: MOVW ; ADD ip,ip,base.
12447 assert_eq!(
12448 enc.encode(&ArmOp::I64Ldr {
12449 rdlo: Reg::R0,
12450 rdhi: Reg::R1,
12451 addr: MemAddr::imm(Reg::SP, 5000),
12452 })
12453 .unwrap(),
12454 le(&[0xE301C388, 0xE08CC00D, 0xE59C0000, 0xE59C1004])
12455 );
12456 // The base IS the scratch: no free register → typed error, not a clobber.
12457 assert!(
12458 enc.encode(&ArmOp::I64Ldr {
12459 rdlo: Reg::R0,
12460 rdhi: Reg::R1,
12461 addr: MemAddr::imm(Reg::R12, 5000),
12462 })
12463 .is_err(),
12464 "materializing over an R12 base must be refused, not clobber it"
12465 );
12466 }
12467
12468 /// RQ-63-ARMI64OFF (#1165): the A32 WORD and SUB-WORD immediate arms used
12469 /// to MASK an out-of-range offset (`& 0xFFF` / `& 0xFF`) — a silent wrong
12470 /// address (`i32.load offset=5000` → `ldr r0,[ip,#904]`, `i32.load16_u
12471 /// offset=256` → `ldrh r0,[ip]`). At the field maximum the bytes are
12472 /// unchanged; one above, the offset is materialized. Bytes verified
12473 /// against `arm-none-eabi-as`.
12474 #[test]
12475 fn test_1165_a32_word_subword_offset_boundary_materializes_not_masks() {
12476 let enc = ArmEncoder::new_arm32();
12477 let le = |ws: &[u32]| ws.iter().flat_map(|w| w.to_le_bytes()).collect::<Vec<u8>>();
12478 let idx = |off: i32| MemAddr::reg_imm(Reg::R11, Reg::R0, off);
12479 let ldr = |addr| enc.encode(&ArmOp::Ldr { rd: Reg::R0, addr }).unwrap();
12480 let ldrh = |addr| enc.encode(&ArmOp::Ldrh { rd: Reg::R0, addr }).unwrap();
12481 let ldrsb = |addr| enc.encode(&ArmOp::Ldrsb { rd: Reg::R0, addr }).unwrap();
12482 let strh = |addr| enc.encode(&ArmOp::Strh { rd: Reg::R1, addr }).unwrap();
12483 let ldrb = |addr| enc.encode(&ArmOp::Ldrb { rd: Reg::R0, addr }).unwrap();
12484 // LDR imm12: 4095 at, 4096 above.
12485 assert_eq!(ldr(idx(4095)), le(&[0xE08BC000, 0xE59C0FFF]));
12486 assert_eq!(
12487 ldr(idx(4096)),
12488 le(&[0xE301C000, 0xE08CC00B, 0xE08CC000, 0xE59C0000]),
12489 "ldr 4096 must materialize, not mask to #0"
12490 );
12491 // LDRH imm8: 255 at, 256 above (the pre-fix bytes were `ldrh r0,[ip]`).
12492 assert_eq!(ldrh(idx(255)), le(&[0xE08BC000, 0xE1DC0FBF]));
12493 assert_eq!(
12494 ldrh(idx(256)),
12495 le(&[0xE300C100, 0xE08CC00B, 0xE08CC000, 0xE1DC00B0]),
12496 "ldrh 256 must materialize, not mask to #0"
12497 );
12498 // LDRSB imm8 (256 above), STRH imm8 (300 above).
12499 assert_eq!(
12500 ldrsb(idx(256)),
12501 le(&[0xE300C100, 0xE08CC00B, 0xE08CC000, 0xE1DC00D0])
12502 );
12503 assert_eq!(
12504 strh(idx(300)),
12505 le(&[0xE300C12C, 0xE08CC00B, 0xE08CC000, 0xE1CC10B0])
12506 );
12507 // LDRB is an imm12 form: 4095 at, 4096 above.
12508 assert_eq!(ldrb(idx(4095)), le(&[0xE08BC000, 0xE5DC0FFF]));
12509 assert_eq!(
12510 ldrb(idx(4096)),
12511 le(&[0xE301C000, 0xE08CC00B, 0xE08CC000, 0xE5DC0000])
12512 );
12513 // No index, out of range: MOVW ; ADD ip,ip,base ; access [ip].
12514 assert_eq!(
12515 ldr(MemAddr::imm(Reg::R0, 5000)),
12516 le(&[0xE301C388, 0xE08CC000, 0xE59C0000])
12517 );
12518 // The immediate arms are now a TRIPWIRE, never a mask.
12519 assert!(encode_mem_addr(&MemAddr::imm(Reg::R1, 0xFFF)).is_ok());
12520 assert!(
12521 encode_mem_addr(&MemAddr::imm(Reg::R1, 0x1000)).is_err(),
12522 "imm12 arm must refuse 0x1000, not mask it to #0"
12523 );
12524 assert!(encode_mem_addr_imm8(&MemAddr::imm(Reg::R1, 0xFF)).is_ok());
12525 assert!(
12526 encode_mem_addr_imm8(&MemAddr::imm(Reg::R1, 0x100)).is_err(),
12527 "imm8 arm must refuse 0x100, not mask it to #0"
12528 );
12529 // Base is the scratch: refused, not clobbered.
12530 assert!(
12531 enc.encode(&ArmOp::Ldr {
12532 rd: Reg::R0,
12533 addr: MemAddr::imm(Reg::R12, 5000),
12534 })
12535 .is_err()
12536 );
12537 }
12538
12539 /// RQ-63-ARMI64OFF (#1165): Thumb-2 `i64_effective_base` used to CLAMP a
12540 /// negative offset (a memarg >= 2^31 after the selector's `as i32` cast)
12541 /// to 0 — `i64.load offset=0xfffffff8` read `[R11+addr]`. It now
12542 /// materializes the full 32 bits (MOVW+MOVT), the arithmetic the i32 word
12543 /// path uses for the same memarg. Bytes verified against
12544 /// `arm-none-eabi-as` (`.arch armv7e-m`, `.thumb`). A FRAME access with a
12545 /// negative offset (no index register) is refused loudly by
12546 /// `check_ldst_imm12`, never clamped.
12547 #[test]
12548 fn test_1165_thumb2_i64_negative_offset_materializes_not_clamps() {
12549 let enc = ArmEncoder::new_thumb2();
12550 let ld = enc
12551 .encode(&ArmOp::I64Ldr {
12552 rdlo: Reg::R0,
12553 rdhi: Reg::R1,
12554 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, -8),
12555 })
12556 .unwrap();
12557 assert_eq!(
12558 ld,
12559 vec![
12560 0x4f, 0xf6, 0xf8, 0x7c, // movw ip, #0xfff8
12561 0xcf, 0xf6, 0xff, 0x7c, // movt ip, #0xffff
12562 0x00, 0xeb, 0x0c, 0x0c, // add.w ip, r0, ip
12563 0x0c, 0xeb, 0x0b, 0x0c, // add.w ip, ip, fp
12564 0xdc, 0xf8, 0x00, 0x00, // ldr.w r0, [ip]
12565 0xdc, 0xf8, 0x04, 0x10, // ldr.w r1, [ip, #4]
12566 ],
12567 "0xfffffff8 must be materialized in full, not clamped to [ip,#0]"
12568 );
12569 let st = enc
12570 .encode(&ArmOp::I64Str {
12571 rdlo: Reg::R2,
12572 rdhi: Reg::R3,
12573 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, i32::MIN),
12574 })
12575 .unwrap();
12576 assert_eq!(
12577 st,
12578 vec![
12579 0x40, 0xf2, 0x00, 0x0c, // movw ip, #0
12580 0xc8, 0xf2, 0x00, 0x0c, // movt ip, #0x8000
12581 0x00, 0xeb, 0x0c, 0x0c, // add.w ip, r0, ip
12582 0x0c, 0xeb, 0x0b, 0x0c, // add.w ip, ip, fp
12583 0xcc, 0xf8, 0x00, 0x20, // str.w r2, [ip]
12584 0xcc, 0xf8, 0x04, 0x30, // str.w r3, [ip, #4]
12585 ]
12586 );
12587 assert!(
12588 enc.encode(&ArmOp::I64Ldr {
12589 rdlo: Reg::R0,
12590 rdhi: Reg::R1,
12591 addr: MemAddr::imm(Reg::SP, -8),
12592 })
12593 .is_err(),
12594 "a negative frame offset must be refused, not clamped to [sp,#0]"
12595 );
12596 }
12597
12598 #[test]
12599 fn test_encode_i64_str_thumb2() {
12600 let encoder = ArmEncoder::new_thumb2();
12601 let op = ArmOp::I64Str {
12602 rdlo: Reg::R0,
12603 rdhi: Reg::R1,
12604 addr: MemAddr::imm(Reg::SP, 0),
12605 };
12606 let code = encoder.encode(&op).unwrap();
12607 // Two STR instructions (lo at offset, hi at offset+4)
12608 assert!(code.len() >= 4, "I64Str should emit at least 4 bytes");
12609 }
12610
12611 #[test]
12612 fn test_encode_i64_all_comparisons_thumb2() {
12613 let encoder = ArmEncoder::new_thumb2();
12614
12615 let ops = vec![
12616 ArmOp::I64Ne {
12617 rd: Reg::R0,
12618 rnlo: Reg::R0,
12619 rnhi: Reg::R1,
12620 rmlo: Reg::R2,
12621 rmhi: Reg::R3,
12622 },
12623 ArmOp::I64LtS {
12624 rd: Reg::R0,
12625 rnlo: Reg::R0,
12626 rnhi: Reg::R1,
12627 rmlo: Reg::R2,
12628 rmhi: Reg::R3,
12629 },
12630 ArmOp::I64LtU {
12631 rd: Reg::R0,
12632 rnlo: Reg::R0,
12633 rnhi: Reg::R1,
12634 rmlo: Reg::R2,
12635 rmhi: Reg::R3,
12636 },
12637 ArmOp::I64LeS {
12638 rd: Reg::R0,
12639 rnlo: Reg::R0,
12640 rnhi: Reg::R1,
12641 rmlo: Reg::R2,
12642 rmhi: Reg::R3,
12643 },
12644 ArmOp::I64LeU {
12645 rd: Reg::R0,
12646 rnlo: Reg::R0,
12647 rnhi: Reg::R1,
12648 rmlo: Reg::R2,
12649 rmhi: Reg::R3,
12650 },
12651 ArmOp::I64GtS {
12652 rd: Reg::R0,
12653 rnlo: Reg::R0,
12654 rnhi: Reg::R1,
12655 rmlo: Reg::R2,
12656 rmhi: Reg::R3,
12657 },
12658 ArmOp::I64GtU {
12659 rd: Reg::R0,
12660 rnlo: Reg::R0,
12661 rnhi: Reg::R1,
12662 rmlo: Reg::R2,
12663 rmhi: Reg::R3,
12664 },
12665 ArmOp::I64GeS {
12666 rd: Reg::R0,
12667 rnlo: Reg::R0,
12668 rnhi: Reg::R1,
12669 rmlo: Reg::R2,
12670 rmhi: Reg::R3,
12671 },
12672 ArmOp::I64GeU {
12673 rd: Reg::R0,
12674 rnlo: Reg::R0,
12675 rnhi: Reg::R1,
12676 rmlo: Reg::R2,
12677 rmhi: Reg::R3,
12678 },
12679 ];
12680
12681 for op in &ops {
12682 let code = encoder.encode(op).unwrap();
12683 assert!(
12684 code.len() >= 8,
12685 "i64 comparison {:?} should emit at least 8 bytes, got {}",
12686 op,
12687 code.len()
12688 );
12689 }
12690 }
12691
12692 #[test]
12693 fn test_encode_i64_const_zero_thumb2() {
12694 let encoder = ArmEncoder::new_thumb2();
12695 let op = ArmOp::I64Const {
12696 rdlo: Reg::R0,
12697 rdhi: Reg::R1,
12698 value: 0,
12699 };
12700 let code = encoder.encode(&op).unwrap();
12701 // MOVW R0, #0 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes
12702 assert_eq!(code.len(), 8, "I64Const(0) should be 8 bytes");
12703 }
12704
12705 #[test]
12706 fn test_encode_i64_const_negative_one_thumb2() {
12707 let encoder = ArmEncoder::new_thumb2();
12708 let op = ArmOp::I64Const {
12709 rdlo: Reg::R0,
12710 rdhi: Reg::R1,
12711 value: -1, // 0xFFFF_FFFF_FFFF_FFFF
12712 };
12713 let code = encoder.encode(&op).unwrap();
12714 // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
12715 assert_eq!(code.len(), 16, "I64Const(-1) should be 16 bytes");
12716 }
12717
12718 // =========================================================================
12719 // Sub-word load/store encoding tests
12720 // =========================================================================
12721
12722 #[test]
12723 fn test_encode_ldrb_arm32() {
12724 let encoder = ArmEncoder::new_arm32();
12725 let op = ArmOp::Ldrb {
12726 rd: Reg::R0,
12727 addr: MemAddr::imm(Reg::R1, 4),
12728 };
12729 let code = encoder.encode(&op).unwrap();
12730 assert_eq!(code.len(), 4, "ARM32 LDRB should be 4 bytes");
12731 // LDRB R0, [R1, #4] = 0xE5D10004
12732 let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12733 assert_eq!(encoded, 0xE5D10004, "Should encode LDRB R0, [R1, #4]");
12734 }
12735
12736 #[test]
12737 fn test_encode_strb_arm32() {
12738 let encoder = ArmEncoder::new_arm32();
12739 let op = ArmOp::Strb {
12740 rd: Reg::R0,
12741 addr: MemAddr::imm(Reg::R1, 0),
12742 };
12743 let code = encoder.encode(&op).unwrap();
12744 assert_eq!(code.len(), 4, "ARM32 STRB should be 4 bytes");
12745 // STRB R0, [R1, #0] = 0xE5C10000
12746 let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12747 assert_eq!(encoded, 0xE5C10000, "Should encode STRB R0, [R1, #0]");
12748 }
12749
12750 #[test]
12751 fn test_encode_ldrh_arm32() {
12752 let encoder = ArmEncoder::new_arm32();
12753 let op = ArmOp::Ldrh {
12754 rd: Reg::R0,
12755 addr: MemAddr::imm(Reg::R1, 2),
12756 };
12757 let code = encoder.encode(&op).unwrap();
12758 assert_eq!(code.len(), 4, "ARM32 LDRH should be 4 bytes");
12759 }
12760
12761 #[test]
12762 fn test_encode_strh_arm32() {
12763 let encoder = ArmEncoder::new_arm32();
12764 let op = ArmOp::Strh {
12765 rd: Reg::R0,
12766 addr: MemAddr::imm(Reg::R1, 0),
12767 };
12768 let code = encoder.encode(&op).unwrap();
12769 assert_eq!(code.len(), 4, "ARM32 STRH should be 4 bytes");
12770 }
12771
12772 #[test]
12773 fn test_encode_ldrsb_arm32() {
12774 let encoder = ArmEncoder::new_arm32();
12775 let op = ArmOp::Ldrsb {
12776 rd: Reg::R0,
12777 addr: MemAddr::imm(Reg::R1, 0),
12778 };
12779 let code = encoder.encode(&op).unwrap();
12780 assert_eq!(code.len(), 4, "ARM32 LDRSB should be 4 bytes");
12781 }
12782
12783 #[test]
12784 fn test_encode_ldrsh_arm32() {
12785 let encoder = ArmEncoder::new_arm32();
12786 let op = ArmOp::Ldrsh {
12787 rd: Reg::R0,
12788 addr: MemAddr::imm(Reg::R1, 0),
12789 };
12790 let code = encoder.encode(&op).unwrap();
12791 assert_eq!(code.len(), 4, "ARM32 LDRSH should be 4 bytes");
12792 }
12793
12794 #[test]
12795 fn test_encode_ldrb_thumb2_16bit() {
12796 let encoder = ArmEncoder::new_thumb2();
12797 let op = ArmOp::Ldrb {
12798 rd: Reg::R0,
12799 addr: MemAddr::imm(Reg::R1, 4),
12800 };
12801 let code = encoder.encode(&op).unwrap();
12802 // Low registers + small offset -> 16-bit encoding
12803 assert_eq!(
12804 code.len(),
12805 2,
12806 "Thumb-2 LDRB with small offset should be 16-bit"
12807 );
12808 }
12809
12810 #[test]
12811 fn test_encode_ldrb_thumb2_32bit() {
12812 let encoder = ArmEncoder::new_thumb2();
12813 let op = ArmOp::Ldrb {
12814 rd: Reg::R0,
12815 addr: MemAddr::imm(Reg::R1, 100), // offset > 31 needs 32-bit
12816 };
12817 let code = encoder.encode(&op).unwrap();
12818 assert_eq!(
12819 code.len(),
12820 4,
12821 "Thumb-2 LDRB with large offset should be 32-bit"
12822 );
12823 }
12824
12825 #[test]
12826 fn test_encode_strb_thumb2_16bit() {
12827 let encoder = ArmEncoder::new_thumb2();
12828 let op = ArmOp::Strb {
12829 rd: Reg::R0,
12830 addr: MemAddr::imm(Reg::R1, 10),
12831 };
12832 let code = encoder.encode(&op).unwrap();
12833 assert_eq!(
12834 code.len(),
12835 2,
12836 "Thumb-2 STRB with small offset should be 16-bit"
12837 );
12838 }
12839
12840 #[test]
12841 fn test_encode_ldrh_thumb2_16bit() {
12842 let encoder = ArmEncoder::new_thumb2();
12843 let op = ArmOp::Ldrh {
12844 rd: Reg::R0,
12845 addr: MemAddr::imm(Reg::R1, 4), // offset aligned to 2, <= 62
12846 };
12847 let code = encoder.encode(&op).unwrap();
12848 assert_eq!(
12849 code.len(),
12850 2,
12851 "Thumb-2 LDRH with small aligned offset should be 16-bit"
12852 );
12853 }
12854
12855 #[test]
12856 fn test_encode_strh_thumb2_16bit() {
12857 let encoder = ArmEncoder::new_thumb2();
12858 let op = ArmOp::Strh {
12859 rd: Reg::R0,
12860 addr: MemAddr::imm(Reg::R1, 4),
12861 };
12862 let code = encoder.encode(&op).unwrap();
12863 assert_eq!(
12864 code.len(),
12865 2,
12866 "Thumb-2 STRH with small aligned offset should be 16-bit"
12867 );
12868 }
12869
12870 #[test]
12871 fn test_encode_ldrsb_thumb2() {
12872 let encoder = ArmEncoder::new_thumb2();
12873 let op = ArmOp::Ldrsb {
12874 rd: Reg::R0,
12875 addr: MemAddr::imm(Reg::R1, 0),
12876 };
12877 let code = encoder.encode(&op).unwrap();
12878 // LDRSB has no 16-bit immediate form, always 32-bit
12879 assert_eq!(code.len(), 4, "Thumb-2 LDRSB should be 32-bit");
12880 }
12881
12882 #[test]
12883 fn test_encode_ldrsh_thumb2() {
12884 let encoder = ArmEncoder::new_thumb2();
12885 let op = ArmOp::Ldrsh {
12886 rd: Reg::R0,
12887 addr: MemAddr::imm(Reg::R1, 0),
12888 };
12889 let code = encoder.encode(&op).unwrap();
12890 assert_eq!(code.len(), 4, "Thumb-2 LDRSH should be 32-bit");
12891 }
12892
12893 #[test]
12894 fn test_encode_memory_size_thumb2() {
12895 let encoder = ArmEncoder::new_thumb2();
12896 let op = ArmOp::MemorySize { rd: Reg::R0 };
12897 let code = encoder.encode(&op).unwrap();
12898 // R0 and R10 are not both low registers, so this needs careful handling
12899 assert!(!code.is_empty(), "MemorySize should produce code");
12900 }
12901
12902 #[test]
12903 fn test_encode_memory_grow_thumb2() {
12904 let encoder = ArmEncoder::new_thumb2();
12905 let op = ArmOp::MemoryGrow {
12906 rd: Reg::R0,
12907 rn: Reg::R0,
12908 };
12909 let code = encoder.encode(&op).unwrap();
12910 assert_eq!(code.len(), 4, "MemoryGrow (MVN) should be 32-bit Thumb-2");
12911 }
12912
12913 #[test]
12914 fn test_encode_subword_reg_offset_thumb2() {
12915 let encoder = ArmEncoder::new_thumb2();
12916
12917 // LDRB with register offset
12918 let op = ArmOp::Ldrb {
12919 rd: Reg::R0,
12920 addr: MemAddr::reg(Reg::R1, Reg::R2),
12921 };
12922 let code = encoder.encode(&op).unwrap();
12923 assert_eq!(
12924 code.len(),
12925 4,
12926 "Thumb-2 LDRB with reg offset should be 32-bit"
12927 );
12928
12929 // STRB with register offset
12930 let op = ArmOp::Strb {
12931 rd: Reg::R0,
12932 addr: MemAddr::reg(Reg::R1, Reg::R2),
12933 };
12934 let code = encoder.encode(&op).unwrap();
12935 assert_eq!(
12936 code.len(),
12937 4,
12938 "Thumb-2 STRB with reg offset should be 32-bit"
12939 );
12940
12941 // LDRH with register offset
12942 let op = ArmOp::Ldrh {
12943 rd: Reg::R0,
12944 addr: MemAddr::reg(Reg::R1, Reg::R2),
12945 };
12946 let code = encoder.encode(&op).unwrap();
12947 assert_eq!(
12948 code.len(),
12949 4,
12950 "Thumb-2 LDRH with reg offset should be 32-bit"
12951 );
12952
12953 // STRH with register offset
12954 let op = ArmOp::Strh {
12955 rd: Reg::R0,
12956 addr: MemAddr::reg(Reg::R1, Reg::R2),
12957 };
12958 let code = encoder.encode(&op).unwrap();
12959 assert_eq!(
12960 code.len(),
12961 4,
12962 "Thumb-2 STRH with reg offset should be 32-bit"
12963 );
12964 }
12965
12966 #[test]
12967 fn test_encode_subword_reg_imm_offset_thumb2() {
12968 let encoder = ArmEncoder::new_thumb2();
12969
12970 // LDRB with both register and immediate offset
12971 let op = ArmOp::Ldrb {
12972 rd: Reg::R0,
12973 addr: MemAddr::reg_imm(Reg::R1, Reg::R2, 4),
12974 };
12975 let code = encoder.encode(&op).unwrap();
12976 // ADD R12, R2, #4 (4 bytes) + LDRB R0, [R1, R12] (4 bytes) = 8 bytes
12977 assert_eq!(
12978 code.len(),
12979 8,
12980 "Thumb-2 LDRB with reg+imm offset should be 8 bytes"
12981 );
12982 }
12983
12984 // ========================================================================
12985 // Helium MVE encoding tests
12986 // ========================================================================
12987
12988 #[test]
12989 fn test_encode_mve_addi32_thumb2() {
12990 let encoder = ArmEncoder::new_thumb2();
12991 let op = ArmOp::MveAddI {
12992 qd: QReg::Q0,
12993 qn: QReg::Q1,
12994 qm: QReg::Q2,
12995 size: MveSize::S32,
12996 };
12997 let code = encoder.encode(&op).unwrap();
12998 assert_eq!(
12999 code.len(),
13000 4,
13001 "MVE VADD.I32 should be 4 bytes (Thumb-2 32-bit)"
13002 );
13003 }
13004
13005 #[test]
13006 fn test_encode_mve_subi16_thumb2() {
13007 let encoder = ArmEncoder::new_thumb2();
13008 let op = ArmOp::MveSubI {
13009 qd: QReg::Q0,
13010 qn: QReg::Q1,
13011 qm: QReg::Q2,
13012 size: MveSize::S16,
13013 };
13014 let code = encoder.encode(&op).unwrap();
13015 assert_eq!(code.len(), 4, "MVE VSUB.I16 should be 4 bytes");
13016 }
13017
13018 #[test]
13019 fn test_encode_mve_muli8_thumb2() {
13020 let encoder = ArmEncoder::new_thumb2();
13021 let op = ArmOp::MveMulI {
13022 qd: QReg::Q0,
13023 qn: QReg::Q1,
13024 qm: QReg::Q2,
13025 size: MveSize::S8,
13026 };
13027 let code = encoder.encode(&op).unwrap();
13028 assert_eq!(code.len(), 4, "MVE VMUL.I8 should be 4 bytes");
13029 }
13030
13031 #[test]
13032 fn test_encode_mve_bitwise_thumb2() {
13033 let encoder = ArmEncoder::new_thumb2();
13034
13035 let ops = vec![
13036 ArmOp::MveAnd {
13037 qd: QReg::Q0,
13038 qn: QReg::Q1,
13039 qm: QReg::Q2,
13040 },
13041 ArmOp::MveOrr {
13042 qd: QReg::Q0,
13043 qn: QReg::Q1,
13044 qm: QReg::Q2,
13045 },
13046 ArmOp::MveEor {
13047 qd: QReg::Q0,
13048 qn: QReg::Q1,
13049 qm: QReg::Q2,
13050 },
13051 ArmOp::MveBic {
13052 qd: QReg::Q0,
13053 qn: QReg::Q1,
13054 qm: QReg::Q2,
13055 },
13056 ];
13057 for op in ops {
13058 let code = encoder.encode(&op).unwrap();
13059 assert_eq!(code.len(), 4, "MVE bitwise op should be 4 bytes");
13060 }
13061 }
13062
13063 #[test]
13064 fn test_encode_mve_mvn_thumb2() {
13065 let encoder = ArmEncoder::new_thumb2();
13066 let op = ArmOp::MveMvn {
13067 qd: QReg::Q0,
13068 qm: QReg::Q1,
13069 };
13070 let code = encoder.encode(&op).unwrap();
13071 assert_eq!(code.len(), 4, "MVE VMVN should be 4 bytes");
13072 }
13073
13074 #[test]
13075 fn test_encode_mve_load_store_thumb2() {
13076 let encoder = ArmEncoder::new_thumb2();
13077
13078 let load = ArmOp::MveLoad {
13079 qd: QReg::Q0,
13080 addr: MemAddr::imm(Reg::R0, 16),
13081 };
13082 let code = encoder.encode(&load).unwrap();
13083 assert_eq!(code.len(), 4, "MVE VLDRW.32 should be 4 bytes");
13084
13085 let store = ArmOp::MveStore {
13086 qd: QReg::Q1,
13087 addr: MemAddr::imm(Reg::R1, 0),
13088 };
13089 let code = encoder.encode(&store).unwrap();
13090 assert_eq!(code.len(), 4, "MVE VSTRW.32 should be 4 bytes");
13091 }
13092
13093 #[test]
13094 fn test_encode_mve_const_thumb2() {
13095 let encoder = ArmEncoder::new_thumb2();
13096 let op = ArmOp::MveConst {
13097 qd: QReg::Q0,
13098 bytes: [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0],
13099 };
13100 let code = encoder.encode(&op).unwrap();
13101 // Should be 4 words of (MOVW R12 + VMOV Sn) = 4 * (4+4) = 32 bytes min
13102 // Some words with hi16=0 skip MOVT, so length varies
13103 assert!(
13104 code.len() >= 24,
13105 "MVE const should produce multiple instructions"
13106 );
13107 }
13108
13109 #[test]
13110 fn test_encode_mve_dup_thumb2() {
13111 let encoder = ArmEncoder::new_thumb2();
13112 let op = ArmOp::MveDup {
13113 qd: QReg::Q0,
13114 rn: Reg::R0,
13115 size: MveSize::S32,
13116 };
13117 let code = encoder.encode(&op).unwrap();
13118 assert_eq!(code.len(), 4, "MVE VDUP.32 should be 4 bytes");
13119 }
13120
13121 #[test]
13122 fn test_encode_mve_extract_lane_thumb2() {
13123 let encoder = ArmEncoder::new_thumb2();
13124 let op = ArmOp::MveExtractLane {
13125 rd: Reg::R0,
13126 qn: QReg::Q1,
13127 lane: 2,
13128 size: MveSize::S32,
13129 };
13130 let code = encoder.encode(&op).unwrap();
13131 assert_eq!(code.len(), 4, "MVE extract lane should be 4 bytes");
13132 }
13133
13134 #[test]
13135 fn test_encode_mve_insert_lane_thumb2() {
13136 let encoder = ArmEncoder::new_thumb2();
13137 let op = ArmOp::MveInsertLane {
13138 qd: QReg::Q0,
13139 rn: Reg::R1,
13140 lane: 3,
13141 size: MveSize::S32,
13142 };
13143 let code = encoder.encode(&op).unwrap();
13144 assert_eq!(code.len(), 4, "MVE insert lane should be 4 bytes");
13145 }
13146
13147 #[test]
13148 fn test_encode_mve_addf32_thumb2() {
13149 let encoder = ArmEncoder::new_thumb2();
13150 let op = ArmOp::MveAddF32 {
13151 qd: QReg::Q0,
13152 qn: QReg::Q1,
13153 qm: QReg::Q2,
13154 };
13155 let code = encoder.encode(&op).unwrap();
13156 assert_eq!(code.len(), 4, "MVE VADD.F32 should be 4 bytes");
13157 }
13158
13159 #[test]
13160 fn test_encode_mve_divf32_thumb2() {
13161 let encoder = ArmEncoder::new_thumb2();
13162 let op = ArmOp::MveDivF32 {
13163 qd: QReg::Q0,
13164 qn: QReg::Q1,
13165 qm: QReg::Q2,
13166 };
13167 let code = encoder.encode(&op).unwrap();
13168 // Lane-wise: 4 x VDIV.F32 = 4 x 4 = 16 bytes
13169 assert_eq!(
13170 code.len(),
13171 16,
13172 "MVE VDIV.F32 (lane-wise) should be 16 bytes"
13173 );
13174 }
13175
13176 #[test]
13177 fn test_encode_mve_sqrtf32_thumb2() {
13178 let encoder = ArmEncoder::new_thumb2();
13179 let op = ArmOp::MveSqrtF32 {
13180 qd: QReg::Q0,
13181 qm: QReg::Q1,
13182 };
13183 let code = encoder.encode(&op).unwrap();
13184 // Lane-wise: 4 x VSQRT.F32 = 4 x 4 = 16 bytes
13185 assert_eq!(
13186 code.len(),
13187 16,
13188 "MVE VSQRT.F32 (lane-wise) should be 16 bytes"
13189 );
13190 }
13191
13192 #[test]
13193 fn test_encode_mve_negf32_thumb2() {
13194 let encoder = ArmEncoder::new_thumb2();
13195 let op = ArmOp::MveNegF32 {
13196 qd: QReg::Q0,
13197 qm: QReg::Q1,
13198 };
13199 let code = encoder.encode(&op).unwrap();
13200 assert_eq!(code.len(), 4, "MVE VNEG.F32 should be 4 bytes");
13201 }
13202
13203 #[test]
13204 fn test_encode_mve_absf32_thumb2() {
13205 let encoder = ArmEncoder::new_thumb2();
13206 let op = ArmOp::MveAbsF32 {
13207 qd: QReg::Q0,
13208 qm: QReg::Q1,
13209 };
13210 let code = encoder.encode(&op).unwrap();
13211 assert_eq!(code.len(), 4, "MVE VABS.F32 should be 4 bytes");
13212 }
13213
13214 /// VCR-RA-001 / immediate-folding precondition: pins the Thumb-2 `AND`
13215 /// immediate encoding for the byte range and documents its bound.
13216 ///
13217 /// The `And { Operand2::Imm }` encoder packs the low 12 bits straight into
13218 /// the `i:imm3:imm8` field WITHOUT applying ThumbExpandImm (the modified-
13219 /// immediate expansion). For `imm <= 0xFF` (e.g. gale's int8 clamps
13220 /// `#0x7e` / `#0x7f`) that is correct — `i:imm3 = 0000` means "imm8
13221 /// zero-extended". So `and r2, r0, #0x7e` encodes to the canonical
13222 /// `00 f0 7e 02`. For `imm >= 0x100` the field would need a true
13223 /// ThumbExpandImm pattern (rotation / replication), which is NOT
13224 /// implemented here — so **immediate folding must gate on `imm <= 0xFF`**
13225 /// until the encoder is hardened to ThumbExpandImm/Ok-or-Err (the
13226 /// "encoder must be Ok-or-Err, never silently wrong" principle, #180/#185).
13227 /// This bound covers the measured `flat_flight` waste (#209).
13228 #[test]
13229 fn and_immediate_encodes_correctly_in_byte_range_documents_fold_bound() {
13230 let encoder = ArmEncoder::new_thumb2();
13231 let op = ArmOp::And {
13232 rd: Reg::R2,
13233 rn: Reg::R0,
13234 op2: Operand2::Imm(0x7e),
13235 };
13236 let code = encoder.encode(&op).unwrap();
13237 assert_eq!(
13238 code,
13239 vec![0x00, 0xf0, 0x7e, 0x02],
13240 "and r2, r0, #0x7e must encode to the canonical AND.W T1 (imm8=0x7e)"
13241 );
13242 }
13243
13244 /// #255: the shared ThumbExpandImm reverse-encoder underpinning the
13245 /// data-processing immediate fix. Encodable modified immediates round-trip to
13246 /// the expected `i:imm3:imm8` field; a genuinely non-modified value is `None`
13247 /// (caller must materialize into a register). Note `1000 = 0xFA ror 30` *is*
13248 /// representable (field 0xF7A) — the old encoder mis-encoded it (raw 0x3E8);
13249 /// this encodes it correctly.
13250 #[test]
13251 fn try_thumb_expand_imm_encodes_modified_immediates() {
13252 assert_eq!(try_thumb_expand_imm(0x7e), Some(0x07e)); // zero-extended byte
13253 assert_eq!(try_thumb_expand_imm(0xff), Some(0x0ff));
13254 assert_eq!(try_thumb_expand_imm(0x0001_0001), Some(0x101)); // 0x00XY00XY
13255 assert_eq!(try_thumb_expand_imm(0xff00_ff00), Some(0x2ff)); // 0xXY00XY00
13256 assert_eq!(try_thumb_expand_imm(0xffff_ffff), Some(0x3ff)); // 0xXYXYXYXY
13257 assert_eq!(try_thumb_expand_imm(0x100), Some(0xf80)); // 0x80 ror 31
13258 assert_eq!(try_thumb_expand_imm(0x8000_0000), Some(0x400)); // 0x80 ror 8
13259 assert_eq!(try_thumb_expand_imm(1000), Some(0xf7a)); // 0xFA ror 30
13260 // Genuinely unrepresentable (bits too far apart for an 8-bit window).
13261 assert_eq!(try_thumb_expand_imm(0x101), None);
13262 assert_eq!(try_thumb_expand_imm(0x12345), None);
13263 }
13264
13265 /// #255: CMP/ADDS/SUBS encode any valid modified immediate correctly, and
13266 /// ERROR (not silently mis-encode) on a genuinely unrepresentable one,
13267 /// forcing the selector to materialize into a register — closing the
13268 /// silent-miscompile class of #251/#253.
13269 #[test]
13270 fn cmp_adds_subs_immediate_error_on_non_modified_imm() {
13271 let encoder = ArmEncoder::new_thumb2();
13272 // cmp r0, #0xff → valid → Ok; cmp r0, #1000 → valid (0xFA ror 30) → Ok.
13273 assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 0xff).is_ok());
13274 assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 1000).is_ok());
13275 // cmp r0, #0x101 → NOT a modified immediate → Err (materialize-reg).
13276 assert!(
13277 encoder.encode_thumb32_cmp_imm(&Reg::R0, 0x101).is_err(),
13278 "cmp #0x101 must error, not compare the wrong constant"
13279 );
13280 assert!(
13281 encoder
13282 .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x101)
13283 .is_err()
13284 );
13285 assert!(
13286 encoder
13287 .encode_thumb32_subs(&Reg::R0, &Reg::R0, 0x101)
13288 .is_err()
13289 );
13290 // ...but a valid modified immediate still encodes.
13291 assert!(
13292 encoder
13293 .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x80)
13294 .is_ok()
13295 );
13296 }
13297
13298 /// #257: MLA (multiply-accumulate) encodes as MLS without the bit-4 op flag.
13299 /// `mla r2, r3, r4, r8` (rd=r2, rn=r3, rm=r4, ra=r8) → Thumb-2 `03 fb 04 82`.
13300 #[test]
13301 fn mla_thumb2_encodes_correctly() {
13302 let encoder = ArmEncoder::new_thumb2();
13303 let code = encoder
13304 .encode(&ArmOp::Mla {
13305 rd: Reg::R2,
13306 rn: Reg::R3,
13307 rm: Reg::R4,
13308 ra: Reg::R8,
13309 })
13310 .unwrap();
13311 // hw1 = 0xFB03, hw2 = (8<<12)|(2<<8)|4 = 0x8204
13312 assert_eq!(code, vec![0x03, 0xfb, 0x04, 0x82]);
13313 }
13314
13315 /// #259: LDR/STR (and sub-word) immediate-offset encoders truncated
13316 /// `offset & 0xFFF`, silently targeting the wrong address for offset >= 4096.
13317 /// They now error (the selector must use register-offset addressing) — the
13318 /// load/store sibling of the #253/#255 class. Offsets <= 4095 still encode.
13319 #[test]
13320 fn ldst_imm12_offset_errors_when_out_of_range() {
13321 let encoder = ArmEncoder::new_thumb2();
13322 // offset 0xFFF (4095): valid → Ok; ldr r0, [r1, #4095].
13323 assert!(
13324 encoder
13325 .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0xFFF)
13326 .is_ok()
13327 );
13328 // offset 0x1000 (4096): out of imm12 range → Err (not & 0xFFF → #0).
13329 assert!(
13330 encoder
13331 .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0x1000)
13332 .is_err(),
13333 "ldr offset 4096 must error, not wrap to 0"
13334 );
13335 assert!(
13336 encoder
13337 .encode_thumb32_str(&Reg::R0, &Reg::R1, 0x1000)
13338 .is_err()
13339 );
13340 assert!(
13341 encoder
13342 .encode_thumb32_ldrb_imm(&Reg::R0, &Reg::R1, 5000)
13343 .is_err()
13344 );
13345 assert!(
13346 encoder
13347 .encode_thumb32_strh_imm(&Reg::R0, &Reg::R1, 5000)
13348 .is_err()
13349 );
13350 }
13351
13352 /// Latent miscompile fix: ADD/SUB with a >0xFF immediate (e.g.
13353 /// `add sp, sp, #frame` for a >=256-byte frame) used ADD.W (T3), whose
13354 /// `i:imm3:imm8` is a ThumbExpandImm modified immediate — so `#256` silently
13355 /// encoded as `#0` (stack corruption). Use ADDW/SUBW (T4), a PLAIN 12-bit
13356 /// immediate, for 0x100..=0xFFF; keep T3 for <=0xFF (bit-identical); error
13357 /// beyond 4095.
13358 #[test]
13359 fn add_sub_large_immediate_use_addw_subw_not_misencoded() {
13360 let encoder = ArmEncoder::new_thumb2();
13361 // add sp, sp, #256 → ADDW (T4) SP, SP, #256 = 0d f2 00 1d
13362 assert_eq!(
13363 encoder
13364 .encode(&ArmOp::Add {
13365 rd: Reg::SP,
13366 rn: Reg::SP,
13367 op2: Operand2::Imm(256),
13368 })
13369 .unwrap(),
13370 vec![0x0d, 0xf2, 0x00, 0x1d],
13371 "add sp,sp,#256 must be ADDW (plain imm12), not a mis-encoded ADD.W"
13372 );
13373 // sub sp, sp, #256 → SUBW (T4) SP, SP, #256 = ad f2 00 1d
13374 assert_eq!(
13375 encoder
13376 .encode(&ArmOp::Sub {
13377 rd: Reg::SP,
13378 rn: Reg::SP,
13379 op2: Operand2::Imm(256),
13380 })
13381 .unwrap(),
13382 vec![0xad, 0xf2, 0x00, 0x1d],
13383 );
13384 // > 4095 has no single-instruction encoding → error, not silent wrong.
13385 assert!(
13386 encoder
13387 .encode(&ArmOp::Add {
13388 rd: Reg::SP,
13389 rn: Reg::SP,
13390 op2: Operand2::Imm(5000),
13391 })
13392 .is_err(),
13393 "add #5000 must error (no single ADDW), not mis-encode"
13394 );
13395 }
13396
13397 /// Closes the data-proc immediate class: AND and CMN now go through
13398 /// `try_thumb_expand_imm` like ORR/EOR/CMP — correct for any modified
13399 /// immediate, `Err` (not raw-pack / NOP) on an un-encodable one. The byte
13400 /// range stays bit-identical (`and r2,r0,#0x7e` is unchanged).
13401 #[test]
13402 fn and_cmn_immediate_thumb_expand_else_error() {
13403 let encoder = ArmEncoder::new_thumb2();
13404 // byte range unchanged (bit-identical with the pre-retrofit encoding)
13405 assert_eq!(
13406 encoder
13407 .encode(&ArmOp::And {
13408 rd: Reg::R2,
13409 rn: Reg::R0,
13410 op2: Operand2::Imm(0x7e),
13411 })
13412 .unwrap(),
13413 vec![0x00, 0xf0, 0x7e, 0x02],
13414 );
13415 // a valid replicated modified immediate now encodes (was silently wrong)
13416 assert!(
13417 encoder
13418 .encode(&ArmOp::And {
13419 rd: Reg::R2,
13420 rn: Reg::R0,
13421 op2: Operand2::Imm(0xff00ff00u32 as i32),
13422 })
13423 .is_ok()
13424 );
13425 // a genuinely un-encodable immediate errors (AND was raw-pack; CMN NOP)
13426 assert!(
13427 encoder
13428 .encode(&ArmOp::And {
13429 rd: Reg::R2,
13430 rn: Reg::R0,
13431 op2: Operand2::Imm(0x101),
13432 })
13433 .is_err()
13434 );
13435 assert!(
13436 encoder
13437 .encode(&ArmOp::Cmn {
13438 rn: Reg::R0,
13439 op2: Operand2::Imm(0x101),
13440 })
13441 .is_err(),
13442 "CMN #0x101 must error, not emit a NOP"
13443 );
13444 }
13445
13446 /// VCR-RA-001: ORR/EOR with a small immediate must encode the real
13447 /// instruction (not a silent `0xBF00` NOP). Pins the byte range and the
13448 /// Ok-or-Err bound that makes future Or/Eor immediate folding safe.
13449 #[test]
13450 fn orr_eor_immediate_encode_in_byte_range_else_error() {
13451 let encoder = ArmEncoder::new_thumb2();
13452 // orr r2, r0, #0x7e → ORR.W T1, imm8=0x7e
13453 assert_eq!(
13454 encoder
13455 .encode(&ArmOp::Orr {
13456 rd: Reg::R2,
13457 rn: Reg::R0,
13458 op2: Operand2::Imm(0x7e),
13459 })
13460 .unwrap(),
13461 vec![0x40, 0xf0, 0x7e, 0x02],
13462 );
13463 // eor r2, r0, #0x7e → EOR.W T1, imm8=0x7e
13464 assert_eq!(
13465 encoder
13466 .encode(&ArmOp::Eor {
13467 rd: Reg::R2,
13468 rn: Reg::R0,
13469 op2: Operand2::Imm(0x7e),
13470 })
13471 .unwrap(),
13472 vec![0x80, 0xf0, 0x7e, 0x02],
13473 );
13474 // Out-of-range immediates error rather than silently mis-encode / NOP.
13475 assert!(
13476 encoder
13477 .encode(&ArmOp::Orr {
13478 rd: Reg::R2,
13479 rn: Reg::R0,
13480 op2: Operand2::Imm(0x140),
13481 })
13482 .is_err(),
13483 "ORR #0x140 must error, not emit a NOP"
13484 );
13485 }
13486
13487 #[test]
13488 fn test_encode_mve_different_qregs() {
13489 let encoder = ArmEncoder::new_thumb2();
13490
13491 // Test that different Q-register numbers produce different encodings
13492 let op1 = ArmOp::MveAddI {
13493 qd: QReg::Q0,
13494 qn: QReg::Q0,
13495 qm: QReg::Q0,
13496 size: MveSize::S32,
13497 };
13498 let op2 = ArmOp::MveAddI {
13499 qd: QReg::Q3,
13500 qn: QReg::Q5,
13501 qm: QReg::Q7,
13502 size: MveSize::S32,
13503 };
13504 let code1 = encoder.encode(&op1).unwrap();
13505 let code2 = encoder.encode(&op2).unwrap();
13506 assert_ne!(
13507 code1, code2,
13508 "Different Q-registers should produce different encodings"
13509 );
13510 }
13511
13512 #[test]
13513 fn test_encode_mve_arm32_loud_err() {
13514 // #615: MVE (Helium) is Thumb-2-only. The ARM32 encoder used to emit
13515 // a silent NOP here (dropping the vector op); it must now be a typed
13516 // Err so a broken "MVE implies Thumb" invariant fails loudly.
13517 let encoder = ArmEncoder::new_arm32();
13518 let op = ArmOp::MveAddI {
13519 qd: QReg::Q0,
13520 qn: QReg::Q1,
13521 qm: QReg::Q2,
13522 size: MveSize::S32,
13523 };
13524 let err = encoder
13525 .encode(&op)
13526 .expect_err("ARM32 MVE must be a loud Err, not a silent NOP (#615)");
13527 assert!(
13528 err.to_string().contains("Thumb-2 only"),
13529 "unexpected error message: {err}"
13530 );
13531 }
13532}