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, page_log2 } => {
1757 let rd_bits = reg_to_bits(rd);
1758 let r10_bits = reg_to_bits(&Reg::R10);
1759 // R10 holds the size in BYTES; `memory.size` answers in DECLARED
1760 // pages, so shift right by log2(page size) (#1441).
1761 //
1762 // SHIFT 0 IS NOT AN LSR, and this is the trap the fix had to
1763 // avoid: in `cond|000|1101|S|0000|Rd|shift5|type|0|Rm` an
1764 // `shift5` of 0 with `type=01` (LSR) means SHIFT BY 32, not by
1765 // zero. `(pagesize 1)` therefore encodes as LSL #0 — the
1766 // canonical `MOV rd, R10` — and a naive "shift by page_log2"
1767 // would have emitted a shift of 32 and returned 0. The #180/#185
1768 // encoder rule applies: Ok-or-Err, never a silent wrong word.
1769 if *page_log2 == 0 {
1770 // LSL #0 == MOV rd, R10
1771 0xE1A00000 | (rd_bits << 12) | r10_bits
1772 } else if *page_log2 < 32 {
1773 // LSR #page_log2: shift5=page_log2, type=01
1774 0xE1A00000 | (rd_bits << 12) | (page_log2 << 7) | (0b01 << 5) | r10_bits
1775 } else {
1776 return Err(synth_core::Error::synthesis(format!(
1777 "#1441 internal: memory.size page_log2 = {page_log2} is \
1778 not encodable as an ARM32 shift (0..=31) — refusing \
1779 rather than emitting a shift of 32"
1780 )));
1781 }
1782 }
1783
1784 ArmOp::MemoryGrow { rd, .. } => {
1785 let rd_bits = reg_to_bits(rd);
1786 // On embedded, always fail: MOV rd, #-1
1787 0xE3E00000 | (rd_bits << 12) // MVN rd, #0 = MOV rd, #-1
1788 }
1789
1790 // Label pseudo-instruction: emits no machine code
1791 ArmOp::Label { .. } => {
1792 return Ok(Vec::new());
1793 }
1794
1795 // Branch instructions
1796 ArmOp::B { label: _ } => {
1797 // B encoding: cond(4) | 1010 | offset(24)
1798 // Simplified: branch to offset 0 (will be patched by linker/resolver)
1799 0xEA000000
1800 }
1801
1802 // Conditional branch to label (generic)
1803 ArmOp::Bcc { cond, label: _ } => {
1804 use synth_synthesis::Condition;
1805 let cond_bits: u32 = match cond {
1806 Condition::EQ => 0x0,
1807 Condition::NE => 0x1,
1808 Condition::HS => 0x2,
1809 Condition::LO => 0x3,
1810 Condition::HI => 0x8,
1811 Condition::LS => 0x9,
1812 Condition::GE => 0xA,
1813 Condition::LT => 0xB,
1814 Condition::GT => 0xC,
1815 Condition::LE => 0xD,
1816 };
1817 // B<cond> with offset 0 (will be patched)
1818 (cond_bits << 28) | 0x0A000000
1819 }
1820
1821 // BHS (Branch if Higher or Same) - used for bounds checking
1822 ArmOp::Bhs { label: _ } => {
1823 // BHS encoding: cond(2=HS) | 1010 | offset(24)
1824 0x2A000000 // BHS with offset 0
1825 }
1826
1827 // BLO (Branch if Lower) - complementary to BHS
1828 ArmOp::Blo { label: _ } => {
1829 // BLO encoding: cond(3=LO) | 1010 | offset(24)
1830 0x3A000000 // BLO with offset 0
1831 }
1832
1833 // Branch with numeric offset (in instructions)
1834 // ARM32 B instruction: offset is in instructions, stored as words
1835 // The offset is relative to PC+8 (due to ARM pipeline)
1836 ArmOp::BOffset { offset } => {
1837 // B encoding: cond(4) | 1010 | offset(24)
1838 // Offset is signed, in words (4-byte units)
1839 // ARM adds PC+8 to the offset, so we need to adjust:
1840 // target = PC + 8 + (offset * 4)
1841 // For backward branch of N instructions: offset = -(N + 2)
1842 // wrapping_sub keeps the encoder total under fuzzing (#186): an
1843 // extreme i32::MIN offset would otherwise overflow-panic; for any
1844 // real branch offset this is identical to `- 2`.
1845 let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1846 let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1847 0xEA000000 | offset_bits
1848 }
1849
1850 // Conditional branch with numeric offset
1851 ArmOp::BCondOffset { cond, offset } => {
1852 use synth_synthesis::Condition;
1853 let cond_bits: u32 = match cond {
1854 Condition::EQ => 0x0,
1855 Condition::NE => 0x1,
1856 Condition::HS => 0x2,
1857 Condition::LO => 0x3,
1858 Condition::HI => 0x8,
1859 Condition::LS => 0x9,
1860 Condition::GE => 0xA,
1861 Condition::LT => 0xB,
1862 Condition::GT => 0xC,
1863 Condition::LE => 0xD,
1864 };
1865 // B<cond> encoding: cond(4) | 1010 | offset(24)
1866 // wrapping_sub: total under fuzzing (#186), identical for real offsets.
1867 let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1868 let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1869 (cond_bits << 28) | 0x0A000000 | offset_bits
1870 }
1871
1872 ArmOp::Bl { label: _ } => {
1873 // BL encoding: cond(4) | 1011 | offset(24). Relocatable
1874 // placeholder; an R_ARM_CALL relocation patches the target.
1875 //
1876 // #1040: the placeholder must carry an embedded addend of -8,
1877 // not 0. A32 `BL` computes `target = P + 8 + (imm24 << 2)`, so
1878 // under REL semantics a 0 addend (`eb000000`) resolves two
1879 // instructions PAST the callee entry — the A32 twin of the
1880 // Thumb #174 bug. The correct word is what `gas` emits for
1881 // `bl <extern>` in ARM mode:
1882 // ebfffffe -> `bl <self>` (imm24 = -2, offset = -8),
1883 // which nets to exactly S. Verified against
1884 // `arm-none-eabi-as -march=armv7-r`, which emits `ebfffffe`
1885 // with an R_ARM_CALL relocation.
1886 0xEBFFFFFE
1887 }
1888
1889 ArmOp::Bx { rm } => {
1890 let rm_bits = reg_to_bits(rm);
1891
1892 // BX encoding: cond(4) | 000100101111111111110001 | Rm(4)
1893 0xE12FFF10 | rm_bits
1894 }
1895
1896 ArmOp::Blx { rm } => {
1897 let rm_bits = reg_to_bits(rm);
1898
1899 // BLX (register) encoding: cond(4) | 000100101111111111110011 | Rm(4)
1900 0xE12FFF30 | rm_bits
1901 }
1902
1903 ArmOp::Push { regs } => {
1904 // STMDB SP!, {regs} encoding: cond(4) | 100100 | 10 | 1101 | register_list(16)
1905 let mut reg_list: u32 = 0;
1906 for r in regs {
1907 reg_list |= 1 << reg_to_bits(r);
1908 }
1909 0xE92D0000 | reg_list
1910 }
1911
1912 ArmOp::Pop { regs } => {
1913 // LDMIA SP!, {regs} encoding: cond(4) | 100010 | 11 | 1101 | register_list(16)
1914 let mut reg_list: u32 = 0;
1915 for r in regs {
1916 reg_list |= 1 << reg_to_bits(r);
1917 }
1918 0xE8BD0000 | reg_list
1919 }
1920
1921 ArmOp::Nop => {
1922 // NOP encoding: MOV R0, R0
1923 0xE1A00000
1924 }
1925
1926 ArmOp::Udf { imm } => {
1927 // UDF (Undefined) encoding in ARM: 0xE7F000F0 | (imm12_hi << 8) | imm4_lo
1928 // We only use imm8, so split into imm4_hi and imm4_lo
1929 let imm8 = *imm as u32;
1930 0xE7F000F0 | ((imm8 & 0xF0) << 4) | (imm8 & 0x0F)
1931 }
1932
1933 // #615: handled by the `encode_arm_expanded` early return at the
1934 // top of this function — a real MOV{cond}/MOV pair now, never a
1935 // silent NOP again.
1936 ArmOp::Popcnt { .. } | ArmOp::SetCond { .. } | ArmOp::SelectMove { .. } => {
1937 unreachable!("handled by encode_arm_expanded (#615)")
1938 }
1939
1940 // Verification-only pseudo-ops: `synth-verify`'s ArmSemantics
1941 // models these, but NO codegen path constructs them (the selector
1942 // lowers select/locals/globals/br_table/call to real instruction
1943 // sequences before the encoder). Encoding one as a NOP silently
1944 // dropped the operation (#615 class); a typed Err keeps the
1945 // encoder total (Ok-or-Err, the `encoder_no_panic` contract)
1946 // while making any future reachability LOUD.
1947 ArmOp::Select { .. }
1948 | ArmOp::LocalGet { .. }
1949 | ArmOp::LocalSet { .. }
1950 | ArmOp::LocalTee { .. }
1951 | ArmOp::GlobalGet { .. }
1952 | ArmOp::GlobalSet { .. }
1953 | ArmOp::BrTable { .. }
1954 | ArmOp::Call { .. } => {
1955 return Err(synth_core::Error::synthesis(format!(
1956 "verification-only pseudo-op {op:?} reached the A32 encoder — \
1957 codegen lowers it before encoding; refusing to emit a silent NOP (#615)"
1958 )));
1959 }
1960
1961 // #594: CallIndirect is expanded to a real multi-instruction
1962 // sequence by the early return at the top of this function —
1963 // it must NEVER fall through to a silent NOP again.
1964 ArmOp::CallIndirect { .. } => {
1965 unreachable!("CallIndirect handled by encode_arm_call_indirect (#594)")
1966 }
1967
1968 // #615: every i64 op (and I32WrapI64) is expanded to a real A32
1969 // multi-instruction sequence by `encode_arm_expanded` — the
1970 // "encode as NOP for now" era ended with the value silently
1971 // vanishing on `--target cortex-r5`.
1972 ArmOp::I64Add { .. }
1973 | ArmOp::I64Sub { .. }
1974 | ArmOp::I64DivS { .. }
1975 | ArmOp::I64DivU { .. }
1976 | ArmOp::I64RemS { .. }
1977 | ArmOp::I64RemU { .. }
1978 | ArmOp::I64Clz { .. }
1979 | ArmOp::I64Ctz { .. }
1980 | ArmOp::I64Popcnt { .. }
1981 | ArmOp::I64And { .. }
1982 | ArmOp::I64Or { .. }
1983 | ArmOp::I64Xor { .. }
1984 | ArmOp::I64Eqz { .. }
1985 | ArmOp::I64Eq { .. }
1986 | ArmOp::I64Ne { .. }
1987 | ArmOp::I64LtS { .. }
1988 | ArmOp::I64LtU { .. }
1989 | ArmOp::I64LeS { .. }
1990 | ArmOp::I64LeU { .. }
1991 | ArmOp::I64GtS { .. }
1992 | ArmOp::I64GtU { .. }
1993 | ArmOp::I64GeS { .. }
1994 | ArmOp::I64GeU { .. }
1995 | ArmOp::I64Const { .. }
1996 | ArmOp::I64Ldr { .. }
1997 | ArmOp::I64Str { .. }
1998 | ArmOp::I64ExtendI32S { .. }
1999 | ArmOp::I64ExtendI32U { .. }
2000 | ArmOp::I64Extend8S { .. }
2001 | ArmOp::I64Extend16S { .. }
2002 | ArmOp::I64Extend32S { .. }
2003 | ArmOp::I32WrapI64 { .. } => {
2004 unreachable!("handled by encode_arm_expanded (#615)")
2005 }
2006
2007 // f32 VFP single-precision instructions
2008 ArmOp::F32Add { sd, sn, sm } => encode_vfp_3reg(0xEE300A00, sd, sn, sm)?,
2009 ArmOp::F32Sub { sd, sn, sm } => encode_vfp_3reg(0xEE300A40, sd, sn, sm)?,
2010 ArmOp::F32Mul { sd, sn, sm } => encode_vfp_3reg(0xEE200A00, sd, sn, sm)?,
2011 ArmOp::F32Div { sd, sn, sm } => encode_vfp_3reg(0xEE800A00, sd, sn, sm)?,
2012 ArmOp::F32Abs { sd, sm } => encode_vfp_2reg(0xEEB00AC0, sd, sm)?,
2013 ArmOp::F32Neg { sd, sm } => encode_vfp_2reg(0xEEB10A40, sd, sm)?,
2014 ArmOp::F32Sqrt { sd, sm } => encode_vfp_2reg(0xEEB10AC0, sd, sm)?,
2015
2016 // f32 pseudo-ops — multi-instruction sequences
2017 // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
2018 ArmOp::F32Ceil { sd, sm } => {
2019 return self.encode_arm_f32_rounding(sd, sm, 0b01); // Round toward +Inf
2020 }
2021 ArmOp::F32Floor { sd, sm } => {
2022 return self.encode_arm_f32_rounding(sd, sm, 0b10); // Round toward -Inf
2023 }
2024 ArmOp::F32Trunc { sd, sm } => {
2025 return self.encode_arm_f32_rounding(sd, sm, 0b11); // VCVT toward zero
2026 }
2027 ArmOp::F32Nearest { sd, sm } => {
2028 return self.encode_arm_f32_rounding(sd, sm, 0b00); // VCVT to nearest
2029 }
2030 ArmOp::F32Min { sd, sn, sm } => {
2031 return self.encode_arm_f32_minmax(sd, sn, sm, true);
2032 }
2033 ArmOp::F32Max { sd, sn, sm } => {
2034 return self.encode_arm_f32_minmax(sd, sn, sm, false);
2035 }
2036 ArmOp::F32Copysign { sd, sn, sm } => {
2037 return self.encode_arm_f32_copysign(sd, sn, sm);
2038 }
2039
2040 // f32 comparisons — multi-instruction: VCMP + VMRS + conditional MOV
2041 ArmOp::F32Eq { rd, sn, sm } => {
2042 return self.encode_arm_f32_compare(rd, sn, sm, 0x0); // EQ
2043 }
2044 ArmOp::F32Ne { rd, sn, sm } => {
2045 return self.encode_arm_f32_compare(rd, sn, sm, 0x1); // NE
2046 }
2047 ArmOp::F32Lt { rd, sn, sm } => {
2048 return self.encode_arm_f32_compare(rd, sn, sm, 0x4); // MI (less than)
2049 }
2050 ArmOp::F32Le { rd, sn, sm } => {
2051 return self.encode_arm_f32_compare(rd, sn, sm, 0x9); // LS (less or same)
2052 }
2053 ArmOp::F32Gt { rd, sn, sm } => {
2054 return self.encode_arm_f32_compare(rd, sn, sm, 0xC); // GT
2055 }
2056 ArmOp::F32Ge { rd, sn, sm } => {
2057 return self.encode_arm_f32_compare(rd, sn, sm, 0xA); // GE
2058 }
2059
2060 // f32 const — multi-instruction: MOVW + MOVT + VMOV
2061 ArmOp::F32Const { sd, value } => {
2062 return self.encode_arm_f32_const(sd, *value);
2063 }
2064
2065 ArmOp::F32Load { sd, addr } => encode_vfp_ldst(0xED900A00, sd, addr)?,
2066 ArmOp::F32Store { sd, addr } => encode_vfp_ldst(0xED800A00, sd, addr)?,
2067
2068 // f32 conversions — multi-instruction sequences
2069 ArmOp::F32ConvertI32S { sd, rm } => {
2070 return self.encode_arm_f32_convert_i32(sd, rm, true);
2071 }
2072 ArmOp::F32ConvertI32U { sd, rm } => {
2073 return self.encode_arm_f32_convert_i32(sd, rm, false);
2074 }
2075 ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
2076 return Err(synth_core::Error::synthesis(
2077 "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2078 ));
2079 }
2080 // RQ-67-VFPREACH (#1267): the callee-saved VFP save/restore is
2081 // emitted only by the Cortex-M (Thumb-2) VFP-pressure rung. The A32
2082 // path has no such rung, so reaching here means a caller built the
2083 // op for the wrong profile — refuse LOUDLY rather than encode
2084 // something plausible (#615: expand-or-loud-reject, never a silent
2085 // NOP).
2086 ArmOp::VPushCalleeSavedVfp | ArmOp::VPopCalleeSavedVfp => {
2087 return Err(synth_core::Error::synthesis(
2088 "VPUSH/VPOP {d8-d15} is Thumb-2 only in this backend (RQ-67-VFPREACH, #1267)",
2089 ));
2090 }
2091 ArmOp::F32ReinterpretI32 { sd, rm } => encode_vmov_core_sreg(true, sd, rm)?,
2092 ArmOp::I32ReinterpretF32 { rd, sm } => encode_vmov_core_sreg(false, sm, rd)?,
2093 ArmOp::I32TruncF32S { rd, sm } => {
2094 return self.encode_arm_i32_trunc_f32(rd, sm, true);
2095 }
2096 ArmOp::I32TruncF32U { rd, sm } => {
2097 return self.encode_arm_i32_trunc_f32(rd, sm, false);
2098 }
2099
2100 // f64 VFP double-precision instructions (ARM32)
2101 // F64 arithmetic: same as F32 but with sz=1 (bit 8 = 1, cp11 = 0xB)
2102 ArmOp::F64Add { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B00, dd, dn, dm)?,
2103 ArmOp::F64Sub { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B40, dd, dn, dm)?,
2104 ArmOp::F64Mul { dd, dn, dm } => encode_vfp_3reg_f64(0xEE200B00, dd, dn, dm)?,
2105 ArmOp::F64Div { dd, dn, dm } => encode_vfp_3reg_f64(0xEE800B00, dd, dn, dm)?,
2106 ArmOp::F64Abs { dd, dm } => encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?,
2107 ArmOp::F64Neg { dd, dm } => encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?,
2108 ArmOp::F64Sqrt { dd, dm } => encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?,
2109
2110 // f64 pseudo-ops
2111 // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
2112 ArmOp::F64Ceil { dd, dm } => {
2113 return self.encode_arm_f64_rounding(dd, dm, 0b01);
2114 }
2115 ArmOp::F64Floor { dd, dm } => {
2116 return self.encode_arm_f64_rounding(dd, dm, 0b10);
2117 }
2118 ArmOp::F64Trunc { dd, dm } => {
2119 return self.encode_arm_f64_rounding(dd, dm, 0b11);
2120 }
2121 ArmOp::F64Nearest { dd, dm } => {
2122 return self.encode_arm_f64_rounding(dd, dm, 0b00);
2123 }
2124 ArmOp::F64Min { dd, dn, dm } => {
2125 return self.encode_arm_f64_minmax(dd, dn, dm, true);
2126 }
2127 ArmOp::F64Max { dd, dn, dm } => {
2128 return self.encode_arm_f64_minmax(dd, dn, dm, false);
2129 }
2130 ArmOp::F64Copysign { dd, dn, dm } => {
2131 return self.encode_arm_f64_copysign(dd, dn, dm);
2132 }
2133
2134 // f64 comparisons
2135 ArmOp::F64Eq { rd, dn, dm } => {
2136 return self.encode_arm_f64_compare(rd, dn, dm, 0x0);
2137 }
2138 ArmOp::F64Ne { rd, dn, dm } => {
2139 return self.encode_arm_f64_compare(rd, dn, dm, 0x1);
2140 }
2141 ArmOp::F64Lt { rd, dn, dm } => {
2142 return self.encode_arm_f64_compare(rd, dn, dm, 0x4);
2143 }
2144 ArmOp::F64Le { rd, dn, dm } => {
2145 return self.encode_arm_f64_compare(rd, dn, dm, 0x9);
2146 }
2147 ArmOp::F64Gt { rd, dn, dm } => {
2148 return self.encode_arm_f64_compare(rd, dn, dm, 0xC);
2149 }
2150 ArmOp::F64Ge { rd, dn, dm } => {
2151 return self.encode_arm_f64_compare(rd, dn, dm, 0xA);
2152 }
2153
2154 ArmOp::F64Const { dd, value } => {
2155 return self.encode_arm_f64_const(dd, *value);
2156 }
2157
2158 ArmOp::F64Load { dd, addr } => encode_vfp_ldst_f64(0xED900B00, dd, addr)?,
2159 ArmOp::F64Store { dd, addr } => encode_vfp_ldst_f64(0xED800B00, dd, addr)?,
2160
2161 ArmOp::F64ConvertI32S { dd, rm } => {
2162 return self.encode_arm_f64_convert_i32(dd, rm, true);
2163 }
2164 ArmOp::F64ConvertI32U { dd, rm } => {
2165 return self.encode_arm_f64_convert_i32(dd, rm, false);
2166 }
2167 ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
2168 return Err(synth_core::Error::synthesis(
2169 "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2170 ));
2171 }
2172 ArmOp::F64PromoteF32 { dd, sm } => {
2173 return self.encode_arm_f64_promote_f32(dd, sm);
2174 }
2175 // GI-FPU-002 (#369): no synth A32 target carries an FPU (cortex-r5
2176 // has none — the selector declines every float op there), so the
2177 // A32 encoder refuses loudly instead of shipping an untested
2178 // encoding (#615: never a silent wrong byte).
2179 ArmOp::F32DemoteF64 { .. } => {
2180 return Err(synth_core::Error::synthesis(
2181 "F32DemoteF64 has no A32 encoding (no A32 target has an FPU)",
2182 ));
2183 }
2184 ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => {
2185 encode_vmov_core_dreg(true, dd, rmlo, rmhi)?
2186 }
2187 ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => {
2188 encode_vmov_core_dreg(false, dm, rdlo, rdhi)?
2189 }
2190 ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
2191 return Err(synth_core::Error::synthesis(
2192 "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
2193 ));
2194 }
2195 ArmOp::I32TruncF64S { rd, dm } => {
2196 return self.encode_arm_i32_trunc_f64(rd, dm, true);
2197 }
2198 ArmOp::I32TruncF64U { rd, dm } => {
2199 return self.encode_arm_i32_trunc_f64(rd, dm, false);
2200 }
2201 // #615: multi-instruction i64 sequences — expanded to real A32 by
2202 // `encode_arm_expanded`, no longer "Thumb-2 only" NOPs.
2203 ArmOp::I64SetCond { .. }
2204 | ArmOp::I64SetCondZ { .. }
2205 | ArmOp::I64Mul { .. }
2206 | ArmOp::I64Shl { .. }
2207 | ArmOp::I64ShrS { .. }
2208 | ArmOp::I64ShrU { .. }
2209 | ArmOp::I64Rotl { .. }
2210 | ArmOp::I64Rotr { .. } => {
2211 unreachable!("handled by encode_arm_expanded (#615)")
2212 }
2213
2214 // MVE instructions — Thumb-2 only (Cortex-M55 is always Thumb-2)
2215 ArmOp::MveLoad { .. }
2216 | ArmOp::MveStore { .. }
2217 | ArmOp::MveConst { .. }
2218 | ArmOp::MveAnd { .. }
2219 | ArmOp::MveOrr { .. }
2220 | ArmOp::MveEor { .. }
2221 | ArmOp::MveMvn { .. }
2222 | ArmOp::MveBic { .. }
2223 | ArmOp::MveAddI { .. }
2224 | ArmOp::MveSubI { .. }
2225 | ArmOp::MveMulI { .. }
2226 | ArmOp::MveNegI { .. }
2227 | ArmOp::MveCmpEqI { .. }
2228 | ArmOp::MveCmpNeI { .. }
2229 | ArmOp::MveCmpLtS { .. }
2230 | ArmOp::MveCmpLtU { .. }
2231 | ArmOp::MveCmpGtS { .. }
2232 | ArmOp::MveCmpGtU { .. }
2233 | ArmOp::MveCmpLeS { .. }
2234 | ArmOp::MveCmpLeU { .. }
2235 | ArmOp::MveCmpGeS { .. }
2236 | ArmOp::MveCmpGeU { .. }
2237 | ArmOp::MveDup { .. }
2238 | ArmOp::MveExtractLane { .. }
2239 | ArmOp::MveInsertLane { .. }
2240 | ArmOp::MveAddF32 { .. }
2241 | ArmOp::MveSubF32 { .. }
2242 | ArmOp::MveMulF32 { .. }
2243 | ArmOp::MveNegF32 { .. }
2244 | ArmOp::MveAbsF32 { .. }
2245 | ArmOp::MveCmpEqF32 { .. }
2246 | ArmOp::MveCmpNeF32 { .. }
2247 | ArmOp::MveCmpLtF32 { .. }
2248 | ArmOp::MveCmpLeF32 { .. }
2249 | ArmOp::MveCmpGtF32 { .. }
2250 | ArmOp::MveCmpGeF32 { .. }
2251 | ArmOp::MveDupF32 { .. }
2252 | ArmOp::MveExtractLaneF32 { .. }
2253 | ArmOp::MveReplaceLaneF32 { .. }
2254 | ArmOp::MveDivF32 { .. }
2255 | ArmOp::MveSqrtF32 { .. } => {
2256 // MVE (Helium) is a Thumb-2-only extension (Cortex-M55); there
2257 // is no A32 encoding. The selector only emits MVE ops for
2258 // Thumb targets — a NOP here silently dropped the vector op
2259 // if that invariant ever broke (#615 class). Err keeps the
2260 // encoder total and the failure loud.
2261 return Err(synth_core::Error::synthesis(format!(
2262 "MVE op {op:?} has no A32 (ARM-mode) encoding — MVE is Thumb-2 only (#615)"
2263 )));
2264 }
2265 };
2266
2267 // ARM32 instructions are little-endian
2268 Ok(instr.to_le_bytes().to_vec())
2269 }
2270
2271 // === ARM32 VFP multi-instruction helpers ===
2272
2273 /// Encode F32 comparison as ARM32: VCMP.F32 + VMRS + MOV rd,#0 + MOVcond rd,#1
2274 fn encode_arm_f32_compare(
2275 &self,
2276 rd: &Reg,
2277 sn: &VfpReg,
2278 sm: &VfpReg,
2279 cond_code: u32,
2280 ) -> Result<Vec<u8>> {
2281 let mut bytes = Vec::new();
2282
2283 // VCMP.F32 Sn, Sm: 0xEEB40A40 with Sn in Vd position, Sm in Vm position
2284 let sn_num = vfp_sreg_to_num(sn)?;
2285 let sm_num = vfp_sreg_to_num(sm)?;
2286 let (vd, d) = encode_sreg(sn_num);
2287 let (vm, m) = encode_sreg(sm_num);
2288 let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2289 bytes.extend_from_slice(&vcmp.to_le_bytes());
2290
2291 // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
2292 bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2293
2294 // MOV rd, #0: 0xE3A0_0000 | (rd << 12)
2295 let rd_bits = reg_to_bits(rd);
2296 let mov_zero = 0xE3A00000 | (rd_bits << 12);
2297 bytes.extend_from_slice(&mov_zero.to_le_bytes());
2298
2299 // MOVcond rd, #1: cond(4) | 0011 1010 0000 rd(4) 0000 0000 0001
2300 let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2301 bytes.extend_from_slice(&mov_one.to_le_bytes());
2302
2303 Ok(bytes)
2304 }
2305
2306 /// Encode F32 constant load as ARM32: MOVW Rt,#lo16 + MOVT Rt,#hi16 + VMOV Sd,Rt
2307 fn encode_arm_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
2308 let mut bytes = Vec::new();
2309 let bits = value.to_bits();
2310
2311 // Use R12 as temp register for constant loading
2312 let rt: u32 = 12; // R12/IP
2313
2314 // MOVW R12, #lo16: 0xE300_C000 | (imm4 << 16) | imm12
2315 let lo16 = bits & 0xFFFF;
2316 let movw = 0xE3000000 | (rt << 12) | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2317 bytes.extend_from_slice(&movw.to_le_bytes());
2318
2319 // MOVT R12, #hi16: 0xE340_C000 | (imm4 << 16) | imm12
2320 let hi16 = (bits >> 16) & 0xFFFF;
2321 let movt = 0xE3400000 | (rt << 12) | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2322 bytes.extend_from_slice(&movt.to_le_bytes());
2323
2324 // VMOV Sd, R12
2325 let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
2326 bytes.extend_from_slice(&vmov.to_le_bytes());
2327
2328 Ok(bytes)
2329 }
2330
2331 /// Encode VMOV + VCVT.F32.S32/U32 as ARM32
2332 fn encode_arm_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2333 let mut bytes = Vec::new();
2334
2335 // VMOV Sd, Rm — move integer to VFP register
2336 let vmov = encode_vmov_core_sreg(true, sd, rm)?;
2337 bytes.extend_from_slice(&vmov.to_le_bytes());
2338
2339 // VCVT.F32.S32 Sd, Sd (signed) or VCVT.F32.U32 Sd, Sd (unsigned).
2340 // The "op" bit (bit 7) selects signedness: 1 = signed (S32), 0 =
2341 // unsigned (U32). So signed = 0xEEB80AC0, unsigned = 0xEEB80A40 —
2342 // objdump confirms 0xEEB80A40 decodes to `vcvt.f32.u32` (GI-FPU-002:
2343 // the two were previously swapped, silently making `convert_i32_s`
2344 // an unsigned conversion).
2345 let sd_num = vfp_sreg_to_num(sd)?;
2346 let (vd, d) = encode_sreg(sd_num);
2347 let (vm, m) = encode_sreg(sd_num); // same register as source
2348 let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
2349 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2350 bytes.extend_from_slice(&vcvt.to_le_bytes());
2351
2352 Ok(bytes)
2353 }
2354
2355 /// Encode F32 rounding pseudo-op as ARM32 via VCVT to integer and back.
2356 /// mode: 0b00=nearest, 0b01=floor(-Inf), 0b10=ceil(+Inf), 0b11=trunc(zero)
2357 /// Strategy: VCVT.S32.F32 Sd, Sm (toward zero), then VCVT.F32.S32 Sd, Sd
2358 /// For ceil/floor/nearest, we use VCVTR (round toward mode) + convert back.
2359 /// Simplified: convert to int (toward zero for trunc) then back to float.
2360 /// Encode F32 rounding as ARM32.
2361 /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2362 ///
2363 /// For trunc (mode=0b11): uses VCVTR.S32.F32 (always rounds toward zero).
2364 /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant
2365 /// which honours FPSCR rmode), then restores FPSCR.
2366 fn encode_arm_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2367 let mut bytes = Vec::new();
2368 let sm_num = vfp_sreg_to_num(sm)?;
2369 let sd_num = vfp_sreg_to_num(sd)?;
2370 let (vd_s, d_s) = encode_sreg(sd_num);
2371 let (vm_s, m_s) = encode_sreg(sm_num);
2372
2373 if mode == 0b11 {
2374 // Trunc (toward zero): VCVTR.S32.F32 — the "R" variant always truncates.
2375 // 0xEEBD0AC0: bit[7]=1 => round toward zero regardless of FPSCR
2376 let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2377 bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2378 } else {
2379 // ceil/floor/nearest: manipulate FPSCR rounding mode
2380 let rt: u32 = 12; // R12/IP as temp
2381
2382 // VMRS R12, FPSCR
2383 let vmrs = 0xEEF10A10 | (rt << 12);
2384 bytes.extend_from_slice(&vmrs.to_le_bytes());
2385
2386 // BIC R12, R12, #(3 << 22) — clear RMode bits [23:22]
2387 // 3<<22 = 0x00C00000. ARM rotated imm: 0x03 ror 10 (rotation=5, imm8=0x03)
2388 let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2389 bytes.extend_from_slice(&bic.to_le_bytes());
2390
2391 // ORR R12, R12, #(mode << 22) — set desired rounding mode
2392 if mode != 0 {
2393 // mode<<22: rotation=5, imm8=mode
2394 let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2395 bytes.extend_from_slice(&orr.to_le_bytes());
2396 }
2397
2398 // VMSR FPSCR, R12
2399 let vmsr = 0xEEE10A10 | (rt << 12);
2400 bytes.extend_from_slice(&vmsr.to_le_bytes());
2401
2402 // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rounding mode
2403 let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2404 bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2405
2406 // Restore FPSCR: clear rmode bits back to nearest (default)
2407 bytes.extend_from_slice(&vmrs.to_le_bytes());
2408 bytes.extend_from_slice(&bic.to_le_bytes());
2409 bytes.extend_from_slice(&vmsr.to_le_bytes());
2410 }
2411
2412 // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
2413 let (vd2, d2) = encode_sreg(sd_num);
2414 let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
2415 bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2416
2417 Ok(bytes)
2418 }
2419
2420 /// Encode F32 min/max as ARM32: VCMP + VMRS + conditional VMOV
2421 fn encode_arm_f32_minmax(
2422 &self,
2423 sd: &VfpReg,
2424 sn: &VfpReg,
2425 sm: &VfpReg,
2426 is_min: bool,
2427 ) -> Result<Vec<u8>> {
2428 let mut bytes = Vec::new();
2429 let sn_num = vfp_sreg_to_num(sn)?;
2430 let sm_num = vfp_sreg_to_num(sm)?;
2431 let sd_num = vfp_sreg_to_num(sd)?;
2432
2433 // VMOV Sd, Sn (start with first operand)
2434 let (vd, d) = encode_sreg(sd_num);
2435 let (vn, n) = encode_sreg(sn_num);
2436 let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2437 bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2438
2439 // VCMP.F32 Sn, Sm
2440 let (vm, m) = encode_sreg(sm_num);
2441 let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2442 bytes.extend_from_slice(&vcmp.to_le_bytes());
2443
2444 // VMRS APSR_nzcv, FPSCR
2445 bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2446
2447 // For min: if Sn > Sm (GT), use Sm. Condition = GT (0xC)
2448 // For max: if Sn < Sm (MI/LT), use Sm. Condition = MI (0x4)
2449 let cond = if is_min { 0xCu32 } else { 0x4u32 };
2450
2451 // VMOV{cond} Sd, Sm — conditional VMOV
2452 let vmov_cond = (cond << 28) | 0x0EB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2453 bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2454
2455 Ok(bytes)
2456 }
2457
2458 /// Encode F32 copysign as ARM32: extract sign from Sm, magnitude from Sn
2459 fn encode_arm_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2460 let mut bytes = Vec::new();
2461
2462 // VMOV R12, Sm (get sign source bits)
2463 let vmov_sm = encode_vmov_core_sreg(false, sm, &Reg::R12)?;
2464 bytes.extend_from_slice(&vmov_sm.to_le_bytes());
2465
2466 // VMOV R0, Sn (get magnitude source bits) — use R0 as temp
2467 let vmov_sn = encode_vmov_core_sreg(false, sn, &Reg::R0)?;
2468 bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2469
2470 // AND R12, R12, #0x80000000 (keep only sign bit)
2471 // Thumb-2 constant 0x80000000 needs special encoding; in ARM32 use rotated imm
2472 // 0x80000000 = 0x02 rotated right by 2 (rotation=1, imm8=0x02)
2473 let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2474 bytes.extend_from_slice(&and_sign.to_le_bytes());
2475
2476 // BIC R0, R0, #0x80000000 (clear sign bit from magnitude)
2477 // R0 = register 0, so Rn and Rd fields are 0
2478 let bic_sign = 0xE3C00000u32 | (1 << 8) | 0x02;
2479 bytes.extend_from_slice(&bic_sign.to_le_bytes());
2480
2481 // ORR R0, R0, R12 (combine sign + magnitude)
2482 // R0 = register 0, so Rn and Rd fields are 0
2483 let orr = 0xE1800000u32 | 12;
2484 bytes.extend_from_slice(&orr.to_le_bytes());
2485
2486 // VMOV Sd, R0
2487 let vmov_result = encode_vmov_core_sreg(true, sd, &Reg::R0)?;
2488 bytes.extend_from_slice(&vmov_result.to_le_bytes());
2489
2490 Ok(bytes)
2491 }
2492
2493 /// Encode F64 comparison as ARM32: VCMP.F64 + VMRS + MOV rd,#0 + MOVcond rd,#1
2494 fn encode_arm_f64_compare(
2495 &self,
2496 rd: &Reg,
2497 dn: &VfpReg,
2498 dm: &VfpReg,
2499 cond_code: u32,
2500 ) -> Result<Vec<u8>> {
2501 let mut bytes = Vec::new();
2502
2503 // VCMP.F64 Dn, Dm: 0xEEB40B40 with Dn in Vd position, Dm in Vm position
2504 let dn_num = vfp_dreg_to_num(dn)?;
2505 let dm_num = vfp_dreg_to_num(dm)?;
2506 let (vd, d) = encode_dreg(dn_num);
2507 let (vm, m) = encode_dreg(dm_num);
2508 let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2509 bytes.extend_from_slice(&vcmp.to_le_bytes());
2510
2511 // VMRS APSR_nzcv, FPSCR
2512 bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2513
2514 // MOV rd, #0
2515 let rd_bits = reg_to_bits(rd);
2516 let mov_zero = 0xE3A00000 | (rd_bits << 12);
2517 bytes.extend_from_slice(&mov_zero.to_le_bytes());
2518
2519 // MOVcond rd, #1
2520 let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2521 bytes.extend_from_slice(&mov_one.to_le_bytes());
2522
2523 Ok(bytes)
2524 }
2525
2526 /// Encode F64 constant load as ARM32: MOVW + MOVT + MOVW + MOVT + VMOV
2527 fn encode_arm_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
2528 let mut bytes = Vec::new();
2529 let bits = value.to_bits();
2530 let lo32 = bits as u32;
2531 let hi32 = (bits >> 32) as u32;
2532
2533 // Load low 32 bits into R0 (Rd field = 0 for R0)
2534 let lo16 = lo32 & 0xFFFF;
2535 let movw_r0 = 0xE3000000 | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2536 bytes.extend_from_slice(&movw_r0.to_le_bytes());
2537 let hi16 = (lo32 >> 16) & 0xFFFF;
2538 let movt_r0 = 0xE3400000 | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2539 bytes.extend_from_slice(&movt_r0.to_le_bytes());
2540
2541 // Load high 32 bits into R12
2542 let lo16 = hi32 & 0xFFFF;
2543 let movw_r12 = 0xE3000000 | ((lo16 >> 12) << 16) | (12 << 12) | (lo16 & 0xFFF);
2544 bytes.extend_from_slice(&movw_r12.to_le_bytes());
2545 let hi16 = (hi32 >> 16) & 0xFFFF;
2546 let movt_r12 = 0xE3400000 | ((hi16 >> 12) << 16) | (12 << 12) | (hi16 & 0xFFF);
2547 bytes.extend_from_slice(&movt_r12.to_le_bytes());
2548
2549 // VMOV Dd, R0, R12
2550 let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
2551 bytes.extend_from_slice(&vmov.to_le_bytes());
2552
2553 Ok(bytes)
2554 }
2555
2556 /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as ARM32
2557 fn encode_arm_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2558 let mut bytes = Vec::new();
2559
2560 // Use S0 as intermediate: VMOV S0, Rm
2561 let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
2562 bytes.extend_from_slice(&vmov.to_le_bytes());
2563
2564 // VCVT.F64.S32 Dd, S0 (signed) or VCVT.F64.U32 Dd, S0 (unsigned)
2565 // Base: 0xEEB80B40 (signed) or 0xEEB80BC0 (unsigned)
2566 let dd_num = vfp_dreg_to_num(dd)?;
2567 let (vd, d) = encode_dreg(dd_num);
2568 let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
2569 // S0 is register 0: Vm=0, M=0
2570 let vcvt = base | (d << 22) | (vd << 12);
2571 bytes.extend_from_slice(&vcvt.to_le_bytes());
2572
2573 Ok(bytes)
2574 }
2575
2576 /// Encode VCVT.F64.F32 Dd, Sm as ARM32 (f32 to f64 promotion)
2577 fn encode_arm_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2578 let dd_num = vfp_dreg_to_num(dd)?;
2579 let sm_num = vfp_sreg_to_num(sm)?;
2580 let (vd, d) = encode_dreg(dd_num);
2581 let (vm, m) = encode_sreg(sm_num);
2582
2583 // VCVT.F64.F32 Dd, Sm: 0xEEB70AC0
2584 let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
2585 Ok(vcvt.to_le_bytes().to_vec())
2586 }
2587
2588 /// Encode VCVT.S32/U32.F64 Sd, Dm + VMOV Rd, Sd as ARM32
2589 fn encode_arm_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2590 let mut bytes = Vec::new();
2591 let dm_num = vfp_dreg_to_num(dm)?;
2592 let (vm, m) = encode_dreg(dm_num);
2593
2594 // VCVT.S32.F64 S0, Dm (toward zero) or VCVT.U32.F64 S0, Dm
2595 // S0: Vd=0, D=0
2596 let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
2597 let vcvt = base | (m << 5) | vm;
2598 bytes.extend_from_slice(&vcvt.to_le_bytes());
2599
2600 // VMOV Rd, S0
2601 let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
2602 bytes.extend_from_slice(&vmov.to_le_bytes());
2603
2604 Ok(bytes)
2605 }
2606
2607 /// Encode F64 rounding pseudo-op as ARM32 via VCVT to integer and back.
2608 /// Encode F64 rounding as ARM32.
2609 /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2610 ///
2611 /// For trunc: uses VCVTR.S32.F64 (always truncates).
2612 /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F64 (non-R variant),
2613 /// then restores FPSCR.
2614 fn encode_arm_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2615 let mut bytes = Vec::new();
2616 let dm_num = vfp_dreg_to_num(dm)?;
2617 let dd_num = vfp_dreg_to_num(dd)?;
2618 let (vm, m) = encode_dreg(dm_num);
2619 let (vd, d) = encode_dreg(dd_num);
2620
2621 if mode == 0b11 {
2622 // Trunc (toward zero): VCVTR.S32.F64 — bit[7]=1, always truncates
2623 let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
2624 bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2625 } else {
2626 // ceil/floor/nearest: manipulate FPSCR rounding mode
2627 let rt: u32 = 12;
2628
2629 // VMRS R12, FPSCR
2630 let vmrs = 0xEEF10A10 | (rt << 12);
2631 bytes.extend_from_slice(&vmrs.to_le_bytes());
2632
2633 // BIC R12, R12, #(3 << 22)
2634 let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2635 bytes.extend_from_slice(&bic.to_le_bytes());
2636
2637 // ORR R12, R12, #(mode << 22)
2638 if mode != 0 {
2639 let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2640 bytes.extend_from_slice(&orr.to_le_bytes());
2641 }
2642
2643 // VMSR FPSCR, R12
2644 let vmsr = 0xEEE10A10 | (rt << 12);
2645 bytes.extend_from_slice(&vmsr.to_le_bytes());
2646
2647 // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0), uses FPSCR rmode
2648 let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
2649 bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2650
2651 // Restore FPSCR
2652 bytes.extend_from_slice(&vmrs.to_le_bytes());
2653 bytes.extend_from_slice(&bic.to_le_bytes());
2654 bytes.extend_from_slice(&vmsr.to_le_bytes());
2655 }
2656
2657 // VCVT.F64.S32 Dd, S0 (convert back to double)
2658 let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
2659 bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2660
2661 Ok(bytes)
2662 }
2663
2664 /// Encode F64 min/max as ARM32: VMOV + VCMP + VMRS + conditional VMOV
2665 fn encode_arm_f64_minmax(
2666 &self,
2667 dd: &VfpReg,
2668 dn: &VfpReg,
2669 dm: &VfpReg,
2670 is_min: bool,
2671 ) -> Result<Vec<u8>> {
2672 let mut bytes = Vec::new();
2673 let dn_num = vfp_dreg_to_num(dn)?;
2674 let dm_num = vfp_dreg_to_num(dm)?;
2675 let dd_num = vfp_dreg_to_num(dd)?;
2676
2677 // VMOV.F64 Dd, Dn (start with first operand)
2678 let (vd, d) = encode_dreg(dd_num);
2679 let (vn, n) = encode_dreg(dn_num);
2680 let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2681 bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2682
2683 // VCMP.F64 Dn, Dm
2684 let (vm, m) = encode_dreg(dm_num);
2685 let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2686 bytes.extend_from_slice(&vcmp.to_le_bytes());
2687
2688 // VMRS APSR_nzcv, FPSCR
2689 bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2690
2691 let cond = if is_min { 0xCu32 } else { 0x4u32 };
2692 let vmov_cond = (cond << 28) | 0x0EB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2693 bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2694
2695 Ok(bytes)
2696 }
2697
2698 /// Encode F64 copysign as ARM32
2699 fn encode_arm_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
2700 let mut bytes = Vec::new();
2701
2702 // VMOV R0, R12, Dm (get sign source bits)
2703 let vmov_dm = encode_vmov_core_dreg(false, dm, &Reg::R0, &Reg::R12)?;
2704 bytes.extend_from_slice(&vmov_dm.to_le_bytes());
2705
2706 // VMOV R1, R2, Dn (get magnitude source bits)
2707 // We use R1 (lo) and R2 (hi) for the magnitude
2708 let vmov_dn = encode_vmov_core_dreg(false, dn, &Reg::R1, &Reg::R2)?;
2709 bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2710
2711 // AND R12, R12, #0x80000000 (keep only sign bit from hi word)
2712 let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2713 bytes.extend_from_slice(&and_sign.to_le_bytes());
2714
2715 // BIC R2, R2, #0x80000000 (clear sign bit from magnitude hi word)
2716 let bic_sign = 0xE3C00000u32 | (2 << 16) | (2 << 12) | (1 << 8) | 0x02;
2717 bytes.extend_from_slice(&bic_sign.to_le_bytes());
2718
2719 // ORR R2, R2, R12 (combine sign + magnitude)
2720 let orr = 0xE1800000u32 | (2 << 16) | (2 << 12) | 12;
2721 bytes.extend_from_slice(&orr.to_le_bytes());
2722
2723 // VMOV Dd, R1, R2
2724 let vmov_result = encode_vmov_core_dreg(true, dd, &Reg::R1, &Reg::R2)?;
2725 bytes.extend_from_slice(&vmov_result.to_le_bytes());
2726
2727 Ok(bytes)
2728 }
2729
2730 /// Encode VCVT.S32/U32.F32 + VMOV as ARM32
2731 fn encode_arm_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2732 let mut bytes = Vec::new();
2733
2734 // VCVT.S32.F32 Sd, Sm (toward zero) or VCVT.U32.F32 Sd, Sm
2735 // We use Sm as both source and destination for the intermediate result
2736 let sm_num = vfp_sreg_to_num(sm)?;
2737 let (vd, d) = encode_sreg(sm_num);
2738 let (vm, m) = encode_sreg(sm_num);
2739 let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
2740 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2741 bytes.extend_from_slice(&vcvt.to_le_bytes());
2742
2743 // VMOV Rd, Sm — move result back to core register
2744 let vmov = encode_vmov_core_sreg(false, sm, rd)?;
2745 bytes.extend_from_slice(&vmov.to_le_bytes());
2746
2747 Ok(bytes)
2748 }
2749
2750 /// Encode an ARM instruction in Thumb-2 mode (16-bit or 32-bit instructions)
2751 fn encode_thumb(&self, op: &ArmOp) -> Result<Vec<u8>> {
2752 // Thumb-2 supports both 16-bit and 32-bit instructions
2753 // 32-bit instructions are encoded as two 16-bit halfwords (big-endian order)
2754 match op {
2755 // === 16-bit Thumb encodings ===
2756 ArmOp::Add { rd, rn, op2 } => {
2757 let rd_bits = reg_to_bits(rd) as u16;
2758 let rn_bits = reg_to_bits(rn) as u16;
2759
2760 if let Operand2::Reg(rm) = op2 {
2761 let rm_bits = reg_to_bits(rm) as u16;
2762 // 16-bit ADDS only has 3-bit register fields (R0-R7). For
2763 // high registers (e.g. R12, the MemLoad/MemStore base
2764 // scratch) the bits overflow into adjacent fields, silently
2765 // corrupting the operands — issue #178/#180: `add ip,ip,r0`
2766 // was emitted as `adds r4,r5,r1`. Guard on all three regs
2767 // being low and fall back to 32-bit ADD.W otherwise, exactly
2768 // as the Sub handler below does.
2769 if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2770 // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2771 let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2772 Ok(instr.to_le_bytes().to_vec())
2773 } else {
2774 // ADD.W Rd, Rn, Rm (32-bit) for high registers
2775 self.encode_thumb32_add_reg_raw(
2776 rd_bits as u32,
2777 rn_bits as u32,
2778 rm_bits as u32,
2779 )
2780 }
2781 } else if let Operand2::Imm(imm) = op2 {
2782 if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2783 // ADDS Rd, Rn, #imm3 (16-bit): 0001 110 imm3 Rn Rd
2784 let instr: u16 = 0x1C00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2785 Ok(instr.to_le_bytes().to_vec())
2786 } else {
2787 // Use 32-bit ADD for larger immediates
2788 self.encode_thumb32_add(rd, rn, *imm as u32)
2789 }
2790 } else {
2791 // Fallback to 32-bit encoding
2792 self.encode_thumb32_add(rd, rn, 0)
2793 }
2794 }
2795
2796 ArmOp::Sub { rd, rn, op2 } => {
2797 let rd_bits = reg_to_bits(rd) as u16;
2798 let rn_bits = reg_to_bits(rn) as u16;
2799
2800 if let Operand2::Reg(rm) = op2 {
2801 let rm_bits = reg_to_bits(rm) as u16;
2802 // 16-bit SUBS can only use low registers (R0-R7)
2803 if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2804 // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2805 let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2806 Ok(instr.to_le_bytes().to_vec())
2807 } else {
2808 // Use 32-bit SUB.W for high registers
2809 self.encode_thumb32_sub_reg_raw(
2810 rd_bits as u32,
2811 rn_bits as u32,
2812 rm_bits as u32,
2813 )
2814 }
2815 } else if let Operand2::Imm(imm) = op2 {
2816 if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2817 // SUBS Rd, Rn, #imm3 (16-bit): 0001 111 imm3 Rn Rd
2818 let instr: u16 = 0x1E00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2819 Ok(instr.to_le_bytes().to_vec())
2820 } else {
2821 self.encode_thumb32_sub(rd, rn, *imm as u32)
2822 }
2823 } else {
2824 self.encode_thumb32_sub(rd, rn, 0)
2825 }
2826 }
2827
2828 ArmOp::Mov { rd, op2 } => {
2829 let rd_bits = reg_to_bits(rd) as u16;
2830
2831 if let Operand2::Imm(imm) = op2 {
2832 // #498: the old test here was the SIGNED `*imm <= 255`,
2833 // so a negative immediate (e.g. -1) fell into the 16-bit
2834 // MOVS arm and encoded the wrong VALUE (#(imm & 0xFF) =
2835 // #0xFF). A positive imm above 0xFFFF was equally wrong:
2836 // MOVW truncates to 16 bits. Split on the UNSIGNED value:
2837 // imm8 → MOVS, imm16 → MOVW, anything wider (negative or
2838 // >0xFFFF) → the full-value MOVW+MOVT pair. No emitter
2839 // produces the wide shape today (both selectors
2840 // materialize wide constants as explicit Movw/Movt or
2841 // Movw+Mvn), so this is byte-identical on shipped paths —
2842 // it retires the latent wrong-value encodings the
2843 // `estimator_encoder_agreement` oracle had pinned.
2844 let uimm = *imm as u32;
2845 if uimm <= 255 && rd_bits < 8 {
2846 // MOVS Rd, #imm8 (16-bit): 0010 0 Rd imm8
2847 let imm_bits = (*imm as u16) & 0xFF;
2848 let instr: u16 = 0x2000 | (rd_bits << 8) | imm_bits;
2849 Ok(instr.to_le_bytes().to_vec())
2850 } else if uimm <= 0xFFFF {
2851 // Use 32-bit MOVW for 16-bit immediates
2852 self.encode_thumb32_movw(rd, uimm)
2853 } else {
2854 // Full 32-bit value: MOVW low16 + MOVT high16
2855 let mut bytes = self.encode_thumb32_movw(rd, uimm & 0xFFFF)?;
2856 bytes.extend(self.encode_thumb32_movt_raw(reg_to_bits(rd), uimm >> 16)?);
2857 Ok(bytes)
2858 }
2859 } else if let Operand2::Reg(rm) = op2 {
2860 let rm_bits = reg_to_bits(rm) as u16;
2861 // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
2862 // D = Rd[3], Rd[2:0] in lower bits
2863 let d_bit = (rd_bits >> 3) & 1;
2864 let instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
2865 Ok(instr.to_le_bytes().to_vec())
2866 } else {
2867 // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
2868 // operand form the arm does not encode (Imm and Reg only) -- #615's
2869 // silent-miscompile class, on the DEFAULT target. Refuse instead.
2870 Err(synth_core::Error::synthesis(format!(
2871 "Thumb-2 MOV with operand {op2:?} has no Thumb-2 encoding here (Imm and Reg \
2872 are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
2873 )))
2874 }
2875 }
2876
2877 ArmOp::Push { regs } => {
2878 // Thumb-2 PUSH encoding:
2879 // If all regs in R0-R7 + LR, use 16-bit: 1011 010 M rrrrrrrr
2880 // Otherwise use 32-bit: STMDB SP!, {regs} = 1110 1001 0010 1101 | 0M0 reglist(13)
2881 let mut reg_list: u16 = 0;
2882 let mut need_32bit = false;
2883 for r in regs {
2884 let bit = reg_to_bits(r);
2885 if bit >= 8 && *r != Reg::LR {
2886 need_32bit = true;
2887 }
2888 reg_list |= 1 << bit;
2889 }
2890 if !need_32bit {
2891 // 16-bit PUSH: 1011 010 M rrrrrrrr
2892 let m_bit = if reg_list & (1 << 14) != 0 {
2893 1u16
2894 } else {
2895 0u16
2896 };
2897 let low_regs = reg_list & 0xFF;
2898 let instr: u16 = 0xB400 | (m_bit << 8) | low_regs;
2899 Ok(instr.to_le_bytes().to_vec())
2900 } else {
2901 // 32-bit STMDB SP!, {regs}: E92D | reglist(16)
2902 let hw1: u16 = 0xE92D;
2903 let hw2: u16 = reg_list;
2904 let mut bytes = hw1.to_le_bytes().to_vec();
2905 bytes.extend_from_slice(&hw2.to_le_bytes());
2906 Ok(bytes)
2907 }
2908 }
2909
2910 ArmOp::Pop { regs } => {
2911 // Thumb-2 POP encoding:
2912 // If all regs in R0-R7 + PC, use 16-bit: 1011 110 P rrrrrrrr
2913 // Otherwise use 32-bit: LDMIA SP!, {regs} = 1110 1000 1011 1101 | PM0 reglist(13)
2914 let mut reg_list: u16 = 0;
2915 let mut need_32bit = false;
2916 for r in regs {
2917 let bit = reg_to_bits(r);
2918 if bit >= 8 && *r != Reg::PC {
2919 need_32bit = true;
2920 }
2921 reg_list |= 1 << bit;
2922 }
2923 if !need_32bit {
2924 // 16-bit POP: 1011 110 P rrrrrrrr
2925 let p_bit = if reg_list & (1 << 15) != 0 {
2926 1u16
2927 } else {
2928 0u16
2929 };
2930 let low_regs = reg_list & 0xFF;
2931 let instr: u16 = 0xBC00 | (p_bit << 8) | low_regs;
2932 Ok(instr.to_le_bytes().to_vec())
2933 } else {
2934 // 32-bit LDMIA SP!, {regs}: E8BD | reglist(16)
2935 let hw1: u16 = 0xE8BD;
2936 let hw2: u16 = reg_list;
2937 let mut bytes = hw1.to_le_bytes().to_vec();
2938 bytes.extend_from_slice(&hw2.to_le_bytes());
2939 Ok(bytes)
2940 }
2941 }
2942
2943 ArmOp::Nop => {
2944 let instr: u16 = 0xBF00; // NOP in Thumb-2
2945 Ok(instr.to_le_bytes().to_vec())
2946 }
2947
2948 ArmOp::Udf { imm } => {
2949 // UDF (Undefined) in Thumb-2: 16-bit encoding is 0xDE00 | imm8
2950 // This triggers UsageFault/HardFault, used for WASM traps
2951 let instr: u16 = 0xDE00 | (*imm as u16);
2952 let bytes = instr.to_le_bytes().to_vec();
2953 encoding_contracts::verify_thumb16(&bytes);
2954 Ok(bytes)
2955 }
2956
2957 // i64 support: ADDS, ADC, SUBS, SBC for register pair arithmetic
2958 // ADDS sets flags (carry), ADC uses carry from previous ADDS
2959 ArmOp::Adds { rd, rn, op2 } => {
2960 let rd_bits = reg_to_bits(rd) as u16;
2961 let rn_bits = reg_to_bits(rn) as u16;
2962
2963 if let Operand2::Reg(rm) = op2 {
2964 let rm_bits = reg_to_bits(rm) as u16;
2965 // 16-bit ADDS is R0-R7 only; i64 pair allocation can place
2966 // operands in R8-R11, which would overflow the 3-bit fields
2967 // and corrupt the operands (#178/#180 class). Guard and fall
2968 // back to 32-bit ADDS.W for high registers.
2969 if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2970 // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2971 let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2972 Ok(instr.to_le_bytes().to_vec())
2973 } else {
2974 self.encode_thumb32_adds_reg_raw(
2975 rd_bits as u32,
2976 rn_bits as u32,
2977 rm_bits as u32,
2978 )
2979 }
2980 } else {
2981 // 32-bit Thumb-2 ADDS with immediate
2982 self.encode_thumb32_adds(rd, rn, 0)
2983 }
2984 }
2985
2986 // ADC: Add with Carry (Thumb-2 32-bit)
2987 // ADC.W Rd, Rn, Rm: EB40 Rn | 00 Rd 00 Rm
2988 ArmOp::Adc { rd, rn, op2 } => {
2989 let rd_bits = reg_to_bits(rd);
2990 let rn_bits = reg_to_bits(rn);
2991
2992 if let Operand2::Reg(rm) = op2 {
2993 let rm_bits = reg_to_bits(rm);
2994 // ADC.W Rd, Rn, Rm (T2): 1110 1011 0100 Rn | 0 000 Rd 00 00 Rm
2995 let hw1: u16 = (0xEB40 | rn_bits) as u16;
2996 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2997
2998 let mut bytes = hw1.to_le_bytes().to_vec();
2999 bytes.extend_from_slice(&hw2.to_le_bytes());
3000 Ok(bytes)
3001 } else {
3002 // ADC with immediate - use 32-bit encoding
3003 let hw1: u16 = (0xF140 | rn_bits) as u16;
3004 let hw2: u16 = (rd_bits << 8) as u16;
3005 let mut bytes = hw1.to_le_bytes().to_vec();
3006 bytes.extend_from_slice(&hw2.to_le_bytes());
3007 Ok(bytes)
3008 }
3009 }
3010
3011 // SUBS sets flags (borrow), SBC uses borrow from previous SUBS
3012 ArmOp::Subs { rd, rn, op2 } => {
3013 let rd_bits = reg_to_bits(rd) as u16;
3014 let rn_bits = reg_to_bits(rn) as u16;
3015
3016 if let Operand2::Reg(rm) = op2 {
3017 let rm_bits = reg_to_bits(rm) as u16;
3018 // 16-bit SUBS is R0-R7 only; high-register i64 pair operands
3019 // would overflow the 3-bit fields (#178/#180 class). Guard
3020 // and fall back to 32-bit SUBS.W for high registers.
3021 if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
3022 // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
3023 let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
3024 Ok(instr.to_le_bytes().to_vec())
3025 } else {
3026 self.encode_thumb32_subs_reg_raw(
3027 rd_bits as u32,
3028 rn_bits as u32,
3029 rm_bits as u32,
3030 )
3031 }
3032 } else {
3033 // 32-bit Thumb-2 SUBS with immediate
3034 self.encode_thumb32_subs(rd, rn, 0)
3035 }
3036 }
3037
3038 // SBC: Subtract with Carry (Thumb-2 32-bit)
3039 // SBC.W Rd, Rn, Rm: EB60 Rn | 00 Rd 00 Rm
3040 ArmOp::Sbc { rd, rn, op2 } => {
3041 let rd_bits = reg_to_bits(rd);
3042 let rn_bits = reg_to_bits(rn);
3043
3044 if let Operand2::Reg(rm) = op2 {
3045 let rm_bits = reg_to_bits(rm);
3046 // SBC.W Rd, Rn, Rm (T2): 1110 1011 0110 Rn | 0 000 Rd 00 00 Rm
3047 let hw1: u16 = (0xEB60 | rn_bits) as u16;
3048 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3049
3050 let mut bytes = hw1.to_le_bytes().to_vec();
3051 bytes.extend_from_slice(&hw2.to_le_bytes());
3052 Ok(bytes)
3053 } else {
3054 // SBC with immediate - use 32-bit encoding
3055 let hw1: u16 = (0xF160 | rn_bits) as u16;
3056 let hw2: u16 = (rd_bits << 8) as u16;
3057 let mut bytes = hw1.to_le_bytes().to_vec();
3058 bytes.extend_from_slice(&hw2.to_le_bytes());
3059 Ok(bytes)
3060 }
3061 }
3062
3063 // === 32-bit Thumb-2 encodings ===
3064
3065 // SDIV: 11111011 1001 Rn 1111 Rd 1111 Rm
3066 ArmOp::Sdiv { rd, rn, rm } => {
3067 let rd_bits = reg_to_bits(rd);
3068 let rn_bits = reg_to_bits(rn);
3069 let rm_bits = reg_to_bits(rm);
3070 reg_bits_checked(rd_bits)?;
3071 reg_bits_checked(rn_bits)?;
3072 reg_bits_checked(rm_bits)?;
3073
3074 // Thumb-2 SDIV: FB90 F0F0 | Rn<<16 | Rd<<8 | Rm
3075 // First halfword: 1111 1011 1001 Rn = 0xFB90 | Rn
3076 // Second halfword: 1111 Rd 1111 Rm = 0xF0F0 | Rd<<8 | Rm
3077 let hw1: u16 = (0xFB90 | rn_bits) as u16;
3078 let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
3079
3080 // Thumb-2 32-bit instructions: first halfword, then second halfword (little-endian each)
3081 let mut bytes = hw1.to_le_bytes().to_vec();
3082 bytes.extend_from_slice(&hw2.to_le_bytes());
3083 encoding_contracts::verify_thumb32(&bytes);
3084 Ok(bytes)
3085 }
3086
3087 // UDIV: 11111011 1011 Rn 1111 Rd 1111 Rm
3088 ArmOp::Udiv { rd, rn, rm } => {
3089 let rd_bits = reg_to_bits(rd);
3090 let rn_bits = reg_to_bits(rn);
3091 let rm_bits = reg_to_bits(rm);
3092 reg_bits_checked(rd_bits)?;
3093 reg_bits_checked(rn_bits)?;
3094 reg_bits_checked(rm_bits)?;
3095
3096 // Thumb-2 UDIV: FBB0 F0F0 | Rn<<16 | Rd<<8 | Rm
3097 let hw1: u16 = (0xFBB0 | rn_bits) as u16;
3098 let hw2: u16 = (0xF0F0 | (rd_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 ArmOp::Umull { rdlo, rdhi, rn, rm } => {
3107 let rdlo_bits = reg_to_bits(rdlo);
3108 let rdhi_bits = reg_to_bits(rdhi);
3109 let rn_bits = reg_to_bits(rn);
3110 let rm_bits = reg_to_bits(rm);
3111 reg_bits_checked(rdlo_bits)?;
3112 reg_bits_checked(rdhi_bits)?;
3113 reg_bits_checked(rn_bits)?;
3114 reg_bits_checked(rm_bits)?;
3115
3116 // Thumb-2 UMULL: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm
3117 let hw1: u16 = (0xFBA0 | rn_bits) as u16;
3118 let hw2: u16 = ((rdlo_bits << 12) | (rdhi_bits << 8) | rm_bits) as u16;
3119
3120 let mut bytes = hw1.to_le_bytes().to_vec();
3121 bytes.extend_from_slice(&hw2.to_le_bytes());
3122 encoding_contracts::verify_thumb32(&bytes);
3123 Ok(bytes)
3124 }
3125
3126 // MUL (Thumb-2 32-bit): MUL Rd, Rn, Rm
3127 ArmOp::Mul { rd, rn, rm } => {
3128 let rd_bits = reg_to_bits(rd);
3129 let rn_bits = reg_to_bits(rn);
3130 let rm_bits = reg_to_bits(rm);
3131
3132 // Thumb-2 MUL: FB00 F000 | Rn | Rd<<8 | Rm
3133 // 11111011 0000 Rn | 1111 Rd 0000 Rm
3134 let hw1: u16 = (0xFB00 | rn_bits) as u16;
3135 let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
3136
3137 let mut bytes = hw1.to_le_bytes().to_vec();
3138 bytes.extend_from_slice(&hw2.to_le_bytes());
3139 Ok(bytes)
3140 }
3141
3142 // MLS: Rd = Ra - Rn * Rm
3143 ArmOp::Mls { rd, rn, rm, ra } => {
3144 let rd_bits = reg_to_bits(rd);
3145 let rn_bits = reg_to_bits(rn);
3146 let rm_bits = reg_to_bits(rm);
3147 let ra_bits = reg_to_bits(ra);
3148
3149 // Thumb-2 MLS: FB00 Rn | Ra Rd 0001 Rm
3150 // 11111011 0000 Rn | Ra Rd 0001 Rm
3151 let hw1: u16 = (0xFB00 | rn_bits) as u16;
3152 let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | 0x10 | rm_bits) as u16;
3153
3154 let mut bytes = hw1.to_le_bytes().to_vec();
3155 bytes.extend_from_slice(&hw2.to_le_bytes());
3156 Ok(bytes)
3157 }
3158
3159 ArmOp::Mla { rd, rn, rm, ra } => {
3160 let rd_bits = reg_to_bits(rd);
3161 let rn_bits = reg_to_bits(rn);
3162 let rm_bits = reg_to_bits(rm);
3163 let ra_bits = reg_to_bits(ra);
3164
3165 // Thumb-2 MLA: FB00 Rn | Ra Rd 0000 Rm — same as MLS without the
3166 // bit-4 (0x10) op flag. rd = ra + rn*rm.
3167 let hw1: u16 = (0xFB00 | rn_bits) as u16;
3168 let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | rm_bits) as u16;
3169
3170 let mut bytes = hw1.to_le_bytes().to_vec();
3171 bytes.extend_from_slice(&hw2.to_le_bytes());
3172 Ok(bytes)
3173 }
3174
3175 // AND (Thumb-2 32-bit)
3176 ArmOp::And { rd, rn, op2 } => {
3177 if let Operand2::Reg(rm) = op2 {
3178 let rd_bits = reg_to_bits(rd);
3179 let rn_bits = reg_to_bits(rn);
3180 let rm_bits = reg_to_bits(rm);
3181
3182 // Thumb-2 AND register: EA00 Rn | 0 Rd 00 00 Rm
3183 let hw1: u16 = (0xEA00 | rn_bits) as u16;
3184 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3185
3186 let mut bytes = hw1.to_le_bytes().to_vec();
3187 bytes.extend_from_slice(&hw2.to_le_bytes());
3188 Ok(bytes)
3189 } else if let Operand2::Imm(imm) = op2 {
3190 let rd_bits = reg_to_bits(rd);
3191 let rn_bits = reg_to_bits(rn);
3192
3193 // Thumb-2 AND.W immediate T1: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8.
3194 // The i:imm3:imm8 field is a ThumbExpandImm modified immediate —
3195 // encode it correctly (or error on an un-encodable value)
3196 // rather than packing raw bits, closing the silent-miscompile
3197 // class for AND alongside ORR/EOR (#251) / ADD/SUB (#253) /
3198 // CMP (#255).
3199 let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3200 synth_core::Error::synthesis(
3201 "AND immediate is not a valid ThumbExpandImm — materialize into a register",
3202 )
3203 })?;
3204 let i_bit = (field >> 11) & 1;
3205 let imm3 = (field >> 8) & 0x7;
3206 let imm8 = field & 0xFF;
3207
3208 let hw1: u16 = (0xF000 | (i_bit << 10) | rn_bits) as u16;
3209 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3210
3211 let mut bytes = hw1.to_le_bytes().to_vec();
3212 bytes.extend_from_slice(&hw2.to_le_bytes());
3213 Ok(bytes)
3214 } else {
3215 // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
3216 // operand form the arm does not encode (Reg and Imm only) -- #615's
3217 // silent-miscompile class, on the DEFAULT target. Refuse instead.
3218 Err(synth_core::Error::synthesis(format!(
3219 "Thumb-2 AND with operand {op2:?} has no Thumb-2 encoding here (Reg and Imm \
3220 are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
3221 )))
3222 }
3223 }
3224
3225 // ORR (Thumb-2 32-bit)
3226 ArmOp::Orr { rd, rn, op2 } => {
3227 if let Operand2::Reg(rm) = op2 {
3228 let rd_bits = reg_to_bits(rd);
3229 let rn_bits = reg_to_bits(rn);
3230 let rm_bits = reg_to_bits(rm);
3231
3232 // Thumb-2 ORR: EA40 Rn | 0 Rd 00 00 Rm
3233 let hw1: u16 = (0xEA40 | rn_bits) as u16;
3234 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3235
3236 let mut bytes = hw1.to_le_bytes().to_vec();
3237 bytes.extend_from_slice(&hw2.to_le_bytes());
3238 Ok(bytes)
3239 } else if let Operand2::Imm(imm) = op2 {
3240 // ORR.W immediate T1: 11110 i 0 0010 S Rn | 0 imm3 Rd imm8.
3241 // Only the zero-extended byte form (imm <= 0xFF) is encoded;
3242 // larger modified immediates need ThumbExpandImm — return an
3243 // error rather than silently emit a NOP (Ok-or-Err, #180/#185).
3244 let imm_val = *imm as u32;
3245 if imm_val > 0xFF {
3246 return Err(synth_core::Error::synthesis(
3247 "ORR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3248 ));
3249 }
3250 let rd_bits = reg_to_bits(rd);
3251 let rn_bits = reg_to_bits(rn);
3252 let hw1: u16 = (0xF040 | rn_bits) as u16;
3253 let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3254 let mut bytes = hw1.to_le_bytes().to_vec();
3255 bytes.extend_from_slice(&hw2.to_le_bytes());
3256 Ok(bytes)
3257 } else {
3258 // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
3259 // operand form the arm does not encode (Reg and Imm only) -- #615's
3260 // silent-miscompile class, on the DEFAULT target. Refuse instead.
3261 Err(synth_core::Error::synthesis(format!(
3262 "Thumb-2 ORR with operand {op2:?} has no Thumb-2 encoding here (Reg and Imm \
3263 are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
3264 )))
3265 }
3266 }
3267
3268 // EOR (Thumb-2 32-bit)
3269 ArmOp::Eor { rd, rn, op2 } => {
3270 if let Operand2::Reg(rm) = op2 {
3271 let rd_bits = reg_to_bits(rd);
3272 let rn_bits = reg_to_bits(rn);
3273 let rm_bits = reg_to_bits(rm);
3274
3275 // Thumb-2 EOR: EA80 Rn | 0 Rd 00 00 Rm
3276 let hw1: u16 = (0xEA80 | rn_bits) as u16;
3277 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3278
3279 let mut bytes = hw1.to_le_bytes().to_vec();
3280 bytes.extend_from_slice(&hw2.to_le_bytes());
3281 Ok(bytes)
3282 } else if let Operand2::Imm(imm) = op2 {
3283 // EOR.W immediate T1: 11110 i 0 0100 S Rn | 0 imm3 Rd imm8.
3284 // Byte form only (imm <= 0xFF); larger needs ThumbExpandImm —
3285 // error, not a silent NOP (Ok-or-Err, #180/#185).
3286 let imm_val = *imm as u32;
3287 if imm_val > 0xFF {
3288 return Err(synth_core::Error::synthesis(
3289 "EOR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3290 ));
3291 }
3292 let rd_bits = reg_to_bits(rd);
3293 let rn_bits = reg_to_bits(rn);
3294 let hw1: u16 = (0xF080 | rn_bits) as u16;
3295 let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3296 let mut bytes = hw1.to_le_bytes().to_vec();
3297 bytes.extend_from_slice(&hw2.to_le_bytes());
3298 Ok(bytes)
3299 } else {
3300 // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
3301 // operand form the arm does not encode (Reg and Imm only) -- #615's
3302 // silent-miscompile class, on the DEFAULT target. Refuse instead.
3303 Err(synth_core::Error::synthesis(format!(
3304 "Thumb-2 EOR with operand {op2:?} has no Thumb-2 encoding here (Reg and Imm \
3305 are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
3306 )))
3307 }
3308 }
3309
3310 // Shift operations (16-bit for low registers)
3311 ArmOp::Lsl { rd, rn, shift } => {
3312 let rd_bits = reg_to_bits(rd) as u16;
3313 let rn_bits = reg_to_bits(rn) as u16;
3314 let shift_bits = (*shift as u16) & 0x1F;
3315
3316 if rd_bits < 8 && rn_bits < 8 {
3317 // LSLS Rd, Rm, #imm5 (16-bit): 0000 0 imm5 Rm Rd
3318 let instr: u16 = (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3319 Ok(instr.to_le_bytes().to_vec())
3320 } else {
3321 // Use 32-bit encoding for high registers
3322 self.encode_thumb32_shift(rd, rn, *shift, 0b00) // LSL type
3323 }
3324 }
3325
3326 ArmOp::Lsr { rd, rn, shift } => {
3327 let rd_bits = reg_to_bits(rd) as u16;
3328 let rn_bits = reg_to_bits(rn) as u16;
3329 let shift_bits = (*shift as u16) & 0x1F;
3330
3331 if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3332 // LSRS Rd, Rm, #imm5 (16-bit): 0000 1 imm5 Rm Rd
3333 let instr: u16 = 0x0800 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3334 Ok(instr.to_le_bytes().to_vec())
3335 } else {
3336 self.encode_thumb32_shift(rd, rn, *shift, 0b01) // LSR type
3337 }
3338 }
3339
3340 ArmOp::Asr { rd, rn, shift } => {
3341 let rd_bits = reg_to_bits(rd) as u16;
3342 let rn_bits = reg_to_bits(rn) as u16;
3343 let shift_bits = (*shift as u16) & 0x1F;
3344
3345 if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3346 // ASRS Rd, Rm, #imm5 (16-bit): 0001 0 imm5 Rm Rd
3347 let instr: u16 = 0x1000 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3348 Ok(instr.to_le_bytes().to_vec())
3349 } else {
3350 self.encode_thumb32_shift(rd, rn, *shift, 0b10) // ASR type
3351 }
3352 }
3353
3354 ArmOp::Ror { rd, rn, shift } => {
3355 // ROR doesn't have a 16-bit immediate form, use 32-bit
3356 self.encode_thumb32_shift(rd, rn, *shift, 0b11) // ROR type
3357 }
3358
3359 // Register-based shifts (Thumb-2 32-bit)
3360 // Encoding: 11111010 0xxS Rn 1111 Rd 0000 Rm
3361 // xx = shift type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
3362 ArmOp::LslReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b00),
3363 ArmOp::LsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b01),
3364 ArmOp::AsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b10),
3365 ArmOp::RorReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b11),
3366
3367 // RSB (Reverse Subtract): Rd = imm - Rn
3368 // Thumb-2 T2 encoding: 11110 i 0 1110 S Rn | 0 imm3 Rd imm8
3369 ArmOp::Rsb { rd, rn, imm } => {
3370 let rd_bits = reg_to_bits(rd);
3371 let rn_bits = reg_to_bits(rn);
3372
3373 // #681 class audit: the T2 `i:imm3:imm8` field is a
3374 // ThumbExpandImm modified immediate and RSB has NO plain-imm12
3375 // (T4-style) form — packing a raw value > 0xFF silently encodes
3376 // a different constant (#253/#255 class). All current emitters
3377 // use imm 32 (shift complement), which expands to itself, so
3378 // this gate is byte-identical for existing codegen.
3379 let field = try_thumb_expand_imm(*imm).ok_or_else(|| {
3380 synth_core::Error::synthesis(
3381 "RSB immediate is not a valid ThumbExpandImm — materialize into a register",
3382 )
3383 })?;
3384 let i_bit = (field >> 11) & 1;
3385 let imm3 = (field >> 8) & 0x7;
3386 let imm8 = field & 0xFF;
3387
3388 // hw1: 11110 i 01110 0 Rn (S=0)
3389 let hw1: u16 = (0xF1C0 | (i_bit << 10) | rn_bits) as u16;
3390 // hw2: 0 imm3 Rd imm8
3391 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3392
3393 let mut bytes = hw1.to_le_bytes().to_vec();
3394 bytes.extend_from_slice(&hw2.to_le_bytes());
3395 Ok(bytes)
3396 }
3397
3398 // CLZ (Thumb-2 32-bit)
3399 ArmOp::Clz { rd, rm } => {
3400 let rd_bits = reg_to_bits(rd);
3401 let rm_bits = reg_to_bits(rm);
3402
3403 // Thumb-2 CLZ: FAB0 Rm | F8 Rd Rm
3404 // 11111010 1011 Rm | 1111 1000 Rd Rm
3405 let hw1: u16 = (0xFAB0 | rm_bits) as u16;
3406 let hw2: u16 = (0xF080 | (rd_bits << 8) | rm_bits) as u16;
3407
3408 let mut bytes = hw1.to_le_bytes().to_vec();
3409 bytes.extend_from_slice(&hw2.to_le_bytes());
3410 Ok(bytes)
3411 }
3412
3413 // RBIT (Thumb-2 32-bit)
3414 ArmOp::Rbit { rd, rm } => {
3415 let rd_bits = reg_to_bits(rd);
3416 let rm_bits = reg_to_bits(rm);
3417
3418 // Thumb-2 RBIT: FA90 Rm | F0 Rd A0 Rm
3419 // 11111010 1001 Rm | 1111 Rd 1010 Rm
3420 let hw1: u16 = (0xFA90 | rm_bits) as u16;
3421 let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rm_bits) as u16;
3422
3423 let mut bytes = hw1.to_le_bytes().to_vec();
3424 bytes.extend_from_slice(&hw2.to_le_bytes());
3425 Ok(bytes)
3426 }
3427
3428 // SXTB (16-bit for low registers)
3429 ArmOp::Sxtb { rd, rm } => {
3430 let rd_bits = reg_to_bits(rd) as u16;
3431 let rm_bits = reg_to_bits(rm) as u16;
3432
3433 if rd_bits < 8 && rm_bits < 8 {
3434 // SXTB Rd, Rm (16-bit): 1011 0010 01 Rm Rd
3435 let instr: u16 = 0xB240 | (rm_bits << 3) | rd_bits;
3436 Ok(instr.to_le_bytes().to_vec())
3437 } else {
3438 // Thumb-2 SXTB.W: FA4F F(rd)80 (rm)
3439 // 11111010 0100 1111 | 1111 Rd 10 rotate Rm
3440 let rd_bits32 = rd_bits as u32;
3441 let rm_bits32 = rm_bits as u32;
3442 let hw1: u16 = 0xFA4F;
3443 let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3444 let mut bytes = hw1.to_le_bytes().to_vec();
3445 bytes.extend_from_slice(&hw2.to_le_bytes());
3446 Ok(bytes)
3447 }
3448 }
3449
3450 // SXTH (16-bit for low registers)
3451 ArmOp::Sxth { rd, rm } => {
3452 let rd_bits = reg_to_bits(rd) as u16;
3453 let rm_bits = reg_to_bits(rm) as u16;
3454
3455 if rd_bits < 8 && rm_bits < 8 {
3456 // SXTH Rd, Rm (16-bit): 1011 0010 00 Rm Rd
3457 let instr: u16 = 0xB200 | (rm_bits << 3) | rd_bits;
3458 Ok(instr.to_le_bytes().to_vec())
3459 } else {
3460 // Thumb-2 SXTH.W: FA0F F(rd)80 (rm)
3461 // 11111010 0000 1111 | 1111 Rd 10 rotate Rm
3462 let rd_bits32 = rd_bits as u32;
3463 let rm_bits32 = rm_bits as u32;
3464 let hw1: u16 = 0xFA0F;
3465 let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3466 let mut bytes = hw1.to_le_bytes().to_vec();
3467 bytes.extend_from_slice(&hw2.to_le_bytes());
3468 Ok(bytes)
3469 }
3470 }
3471
3472 // UXTB Rd,Rm — zero-extend byte (rd = rm & 0xff)
3473 ArmOp::Uxtb { rd, rm } => {
3474 let rd_bits = reg_to_bits(rd) as u16;
3475 let rm_bits = reg_to_bits(rm) as u16;
3476 if rd_bits < 8 && rm_bits < 8 {
3477 // UXTB Rd, Rm (16-bit): 1011 0010 11 Rm Rd
3478 let instr: u16 = 0xB2C0 | (rm_bits << 3) | rd_bits;
3479 Ok(instr.to_le_bytes().to_vec())
3480 } else {
3481 // Thumb-2 UXTB.W: FA5F F(rd)80 (rm)
3482 let hw1: u16 = 0xFA5F;
3483 let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3484 let mut bytes = hw1.to_le_bytes().to_vec();
3485 bytes.extend_from_slice(&hw2.to_le_bytes());
3486 Ok(bytes)
3487 }
3488 }
3489
3490 // UXTH Rd,Rm — zero-extend halfword (rd = rm & 0xffff)
3491 ArmOp::Uxth { rd, rm } => {
3492 let rd_bits = reg_to_bits(rd) as u16;
3493 let rm_bits = reg_to_bits(rm) as u16;
3494 if rd_bits < 8 && rm_bits < 8 {
3495 // UXTH Rd, Rm (16-bit): 1011 0010 10 Rm Rd
3496 let instr: u16 = 0xB280 | (rm_bits << 3) | rd_bits;
3497 Ok(instr.to_le_bytes().to_vec())
3498 } else {
3499 // Thumb-2 UXTH.W: FA1F F(rd)80 (rm)
3500 let hw1: u16 = 0xFA1F;
3501 let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3502 let mut bytes = hw1.to_le_bytes().to_vec();
3503 bytes.extend_from_slice(&hw2.to_le_bytes());
3504 Ok(bytes)
3505 }
3506 }
3507
3508 // CMP (can be 16-bit for low registers)
3509 ArmOp::Cmp { rn, op2 } => {
3510 let rn_bits = reg_to_bits(rn) as u16;
3511
3512 if let Operand2::Imm(imm) = op2 {
3513 // Only use 16-bit encoding for non-negative immediates 0-255
3514 // Negative immediates must use 32-bit encoding
3515 if *imm >= 0 && *imm <= 255 && rn_bits < 8 {
3516 // CMP Rn, #imm8 (16-bit): 0010 1 Rn imm8
3517 let instr: u16 = 0x2800 | (rn_bits << 8) | (*imm as u16 & 0xFF);
3518 Ok(instr.to_le_bytes().to_vec())
3519 } else {
3520 self.encode_thumb32_cmp_imm(rn, *imm as u32)
3521 }
3522 } else if let Operand2::Reg(rm) = op2 {
3523 let rm_bits = reg_to_bits(rm) as u16;
3524 if rn_bits < 8 && rm_bits < 8 {
3525 // CMP Rn, Rm (16-bit low): 0100 0010 10 Rm Rn
3526 let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
3527 Ok(instr.to_le_bytes().to_vec())
3528 } else {
3529 // CMP Rn, Rm (16-bit high): 0100 0101 N Rm Rn[2:0]
3530 let n_bit = (rn_bits >> 3) & 1;
3531 let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
3532 Ok(instr.to_le_bytes().to_vec())
3533 }
3534 } else {
3535 // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
3536 // operand form the arm does not encode (Imm and Reg only) -- #615's
3537 // silent-miscompile class, on the DEFAULT target. Refuse instead.
3538 Err(synth_core::Error::synthesis(format!(
3539 "Thumb-2 CMP with operand {op2:?} has no Thumb-2 encoding here (Imm and Reg \
3540 are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
3541 )))
3542 }
3543 }
3544
3545 // CMN (Compare Negative) - computes Rn + op2 and sets flags
3546 // CMN Rn, #1 sets Z flag if Rn == -1 (since -1 + 1 = 0)
3547 ArmOp::Cmn { rn, op2 } => {
3548 let rn_bits = reg_to_bits(rn) as u16;
3549
3550 if let Operand2::Imm(imm) = op2 {
3551 // CMN.W Rn, #imm (32-bit): i:imm3:imm8 is a ThumbExpandImm
3552 // modified immediate (the field sits in imm3=hw2[14:12],
3553 // imm8=hw2[7:0], i=hw1[10]). Encode it correctly, or error on
3554 // an un-encodable value — replacing the old silent `0xBF00`
3555 // NOP (the last of the silent-miscompile data-proc encoders).
3556 let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3557 synth_core::Error::synthesis(
3558 "CMN immediate is not a valid ThumbExpandImm — materialize into a register",
3559 )
3560 })?;
3561 let i_bit = (field >> 11) & 1;
3562 let imm3 = (field >> 8) & 0x7;
3563 let imm8 = field & 0xFF;
3564 let hw1: u16 = (0xF110 | (i_bit << 10) as u16) | rn_bits;
3565 let hw2: u16 = (imm3 << 12) as u16 | 0x0F00 | imm8 as u16;
3566 let mut bytes = hw1.to_le_bytes().to_vec();
3567 bytes.extend_from_slice(&hw2.to_le_bytes());
3568 Ok(bytes)
3569 } else if let Operand2::Reg(rm) = op2 {
3570 let rm_bits = reg_to_bits(rm) as u16;
3571 // 16-bit CMN (T1) only encodes R0-R7; high registers overflow
3572 // the 3-bit fields and corrupt the operands (#184, the #180
3573 // class). CMN has no high-register 16-bit form, so fall back
3574 // to 32-bit CMN.W (T2): EB10 Rn | 0F00 Rm (ADD.W with S=1 and
3575 // Rd discarded as PC/1111).
3576 if rn_bits < 8 && rm_bits < 8 {
3577 // CMN Rn, Rm (16-bit): 0100 0010 11 Rm Rn
3578 let instr: u16 = 0x42C0 | (rm_bits << 3) | rn_bits;
3579 Ok(instr.to_le_bytes().to_vec())
3580 } else {
3581 let hw1: u16 = 0xEB10 | rn_bits;
3582 let hw2: u16 = 0x0F00 | rm_bits;
3583 let mut bytes = hw1.to_le_bytes().to_vec();
3584 bytes.extend_from_slice(&hw2.to_le_bytes());
3585 Ok(bytes)
3586 }
3587 } else {
3588 Ok(vec![0xBF, 0x00])
3589 }
3590 }
3591
3592 // LDR (can be 16-bit for simple cases)
3593 ArmOp::Ldr { rd, addr } => {
3594 let rd_bits = reg_to_bits(rd);
3595 let base_bits = reg_to_bits(&addr.base);
3596
3597 // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3598 if let Some(offset_reg) = &addr.offset_reg {
3599 let rm_bits = reg_to_bits(offset_reg);
3600
3601 // If there's also an immediate offset, we need to ADD it first
3602 if addr.offset != 0 {
3603 // Use R12 (IP) as scratch to avoid clobbering the address register
3604 // ADD R12, Rm, #offset; LDR Rd, [base, R12]
3605 let scratch = Reg::R12;
3606 let mut bytes =
3607 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3608 bytes.extend(self.encode_thumb32_ldr_reg(rd, &addr.base, &scratch)?);
3609 return Ok(bytes);
3610 }
3611
3612 // Simple register offset: LDR Rd, [Rn, Rm]
3613 // 16-bit: only if Rd, Rn, Rm < R8
3614 if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3615 // LDR Rd, [Rn, Rm] (16-bit): 0101 100 Rm Rn Rd
3616 let instr: u16 = 0x5800
3617 | ((rm_bits as u16) << 6)
3618 | ((base_bits as u16) << 3)
3619 | (rd_bits as u16);
3620 return Ok(instr.to_le_bytes().to_vec());
3621 }
3622
3623 // 32-bit register offset
3624 return self.encode_thumb32_ldr_reg(rd, &addr.base, offset_reg);
3625 }
3626
3627 // Immediate offset mode [base, #imm]
3628 let offset = addr.offset as u32;
3629
3630 if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3631 // LDR Rd, [Rn, #imm5*4] (16-bit): 0110 1 imm5 Rn Rd
3632 let imm5 = (offset >> 2) as u16;
3633 let instr: u16 =
3634 0x6800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3635 Ok(instr.to_le_bytes().to_vec())
3636 } else {
3637 self.encode_thumb32_ldr(rd, &addr.base, offset)
3638 }
3639 }
3640
3641 // STR (can be 16-bit for simple cases)
3642 ArmOp::Str { rd, addr } => {
3643 let rd_bits = reg_to_bits(rd);
3644 let base_bits = reg_to_bits(&addr.base);
3645
3646 // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3647 if let Some(offset_reg) = &addr.offset_reg {
3648 let rm_bits = reg_to_bits(offset_reg);
3649
3650 // If there's also an immediate offset, we need to ADD it first
3651 if addr.offset != 0 {
3652 // Use R12 (IP) as scratch to avoid clobbering the address register
3653 // ADD R12, Rm, #offset; STR Rd, [base, R12]
3654 let scratch = Reg::R12;
3655 let mut bytes =
3656 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3657 bytes.extend(self.encode_thumb32_str_reg(rd, &addr.base, &scratch)?);
3658 return Ok(bytes);
3659 }
3660
3661 // Simple register offset: STR Rd, [Rn, Rm]
3662 // 16-bit: only if Rd, Rn, Rm < R8
3663 if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3664 // STR Rd, [Rn, Rm] (16-bit): 0101 000 Rm Rn Rd
3665 let instr: u16 = 0x5000
3666 | ((rm_bits as u16) << 6)
3667 | ((base_bits as u16) << 3)
3668 | (rd_bits as u16);
3669 return Ok(instr.to_le_bytes().to_vec());
3670 }
3671
3672 // 32-bit register offset
3673 return self.encode_thumb32_str_reg(rd, &addr.base, offset_reg);
3674 }
3675
3676 // Immediate offset mode [base, #imm]
3677 let offset = addr.offset as u32;
3678
3679 if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3680 // STR Rd, [Rn, #imm5*4] (16-bit): 0110 0 imm5 Rn Rd
3681 let imm5 = (offset >> 2) as u16;
3682 let instr: u16 =
3683 0x6000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3684 Ok(instr.to_le_bytes().to_vec())
3685 } else {
3686 self.encode_thumb32_str(rd, &addr.base, offset)
3687 }
3688 }
3689
3690 // LDRB (Thumb-2)
3691 ArmOp::Ldrb { rd, addr } => {
3692 let rd_bits = reg_to_bits(rd);
3693 let base_bits = reg_to_bits(&addr.base);
3694
3695 if let Some(offset_reg) = &addr.offset_reg {
3696 if addr.offset != 0 {
3697 let scratch = Reg::R12;
3698 let mut bytes =
3699 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3700 bytes.extend(self.encode_thumb32_ldrb_reg(rd, &addr.base, &scratch)?);
3701 return Ok(bytes);
3702 }
3703 return self.encode_thumb32_ldrb_reg(rd, &addr.base, offset_reg);
3704 }
3705
3706 let offset = addr.offset as u32;
3707 if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3708 // LDRB Rd, [Rn, #imm5] (16-bit): 0111 1 imm5 Rn Rd
3709 let instr: u16 = 0x7800
3710 | ((offset as u16) << 6)
3711 | ((base_bits as u16) << 3)
3712 | (rd_bits as u16);
3713 Ok(instr.to_le_bytes().to_vec())
3714 } else {
3715 self.encode_thumb32_ldrb_imm(rd, &addr.base, offset)
3716 }
3717 }
3718
3719 // LDRSB (Thumb-2)
3720 ArmOp::Ldrsb { rd, addr } => {
3721 let rd_bits = reg_to_bits(rd);
3722 let base_bits = reg_to_bits(&addr.base);
3723
3724 if let Some(offset_reg) = &addr.offset_reg {
3725 if addr.offset != 0 {
3726 let scratch = Reg::R12;
3727 let mut bytes =
3728 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3729 bytes.extend(self.encode_thumb32_ldrsb_reg(rd, &addr.base, &scratch)?);
3730 return Ok(bytes);
3731 }
3732 return self.encode_thumb32_ldrsb_reg(rd, &addr.base, offset_reg);
3733 }
3734
3735 let offset = addr.offset as u32;
3736 // LDRSB has no 16-bit immediate form (only register)
3737 // For 16-bit reg form: only if Rd, Rn, Rm < R8
3738 if rd_bits < 8 && base_bits < 8 && offset == 0 {
3739 // No immediate 16-bit encoding for LDRSB; use 32-bit
3740 self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3741 } else {
3742 self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3743 }
3744 }
3745
3746 // LDRH (Thumb-2)
3747 ArmOp::Ldrh { rd, addr } => {
3748 let rd_bits = reg_to_bits(rd);
3749 let base_bits = reg_to_bits(&addr.base);
3750
3751 if let Some(offset_reg) = &addr.offset_reg {
3752 if addr.offset != 0 {
3753 let scratch = Reg::R12;
3754 let mut bytes =
3755 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3756 bytes.extend(self.encode_thumb32_ldrh_reg(rd, &addr.base, &scratch)?);
3757 return Ok(bytes);
3758 }
3759 return self.encode_thumb32_ldrh_reg(rd, &addr.base, offset_reg);
3760 }
3761
3762 let offset = addr.offset as u32;
3763 if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3764 // LDRH Rd, [Rn, #imm5*2] (16-bit): 1000 1 imm5 Rn Rd
3765 let imm5 = (offset >> 1) as u16;
3766 let instr: u16 =
3767 0x8800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3768 Ok(instr.to_le_bytes().to_vec())
3769 } else {
3770 self.encode_thumb32_ldrh_imm(rd, &addr.base, offset)
3771 }
3772 }
3773
3774 // LDRSH (Thumb-2)
3775 ArmOp::Ldrsh { rd, addr } => {
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_ldrsh_reg(rd, &addr.base, &scratch)?);
3782 return Ok(bytes);
3783 }
3784 return self.encode_thumb32_ldrsh_reg(rd, &addr.base, offset_reg);
3785 }
3786
3787 let offset = addr.offset as u32;
3788 self.encode_thumb32_ldrsh_imm(rd, &addr.base, offset)
3789 }
3790
3791 // STRB (Thumb-2)
3792 ArmOp::Strb { rd, addr } => {
3793 let rd_bits = reg_to_bits(rd);
3794 let base_bits = reg_to_bits(&addr.base);
3795
3796 if let Some(offset_reg) = &addr.offset_reg {
3797 if addr.offset != 0 {
3798 let scratch = Reg::R12;
3799 let mut bytes =
3800 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3801 bytes.extend(self.encode_thumb32_strb_reg(rd, &addr.base, &scratch)?);
3802 return Ok(bytes);
3803 }
3804 return self.encode_thumb32_strb_reg(rd, &addr.base, offset_reg);
3805 }
3806
3807 let offset = addr.offset as u32;
3808 if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3809 // STRB Rd, [Rn, #imm5] (16-bit): 0111 0 imm5 Rn Rd
3810 let instr: u16 = 0x7000
3811 | ((offset as u16) << 6)
3812 | ((base_bits as u16) << 3)
3813 | (rd_bits as u16);
3814 Ok(instr.to_le_bytes().to_vec())
3815 } else {
3816 self.encode_thumb32_strb_imm(rd, &addr.base, offset)
3817 }
3818 }
3819
3820 // STRH (Thumb-2)
3821 ArmOp::Strh { rd, addr } => {
3822 let rd_bits = reg_to_bits(rd);
3823 let base_bits = reg_to_bits(&addr.base);
3824
3825 if let Some(offset_reg) = &addr.offset_reg {
3826 if addr.offset != 0 {
3827 let scratch = Reg::R12;
3828 let mut bytes =
3829 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3830 bytes.extend(self.encode_thumb32_strh_reg(rd, &addr.base, &scratch)?);
3831 return Ok(bytes);
3832 }
3833 return self.encode_thumb32_strh_reg(rd, &addr.base, offset_reg);
3834 }
3835
3836 let offset = addr.offset as u32;
3837 if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3838 // STRH Rd, [Rn, #imm5*2] (16-bit): 1000 0 imm5 Rn Rd
3839 let imm5 = (offset >> 1) as u16;
3840 let instr: u16 =
3841 0x8000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3842 Ok(instr.to_le_bytes().to_vec())
3843 } else {
3844 self.encode_thumb32_strh_imm(rd, &addr.base, offset)
3845 }
3846 }
3847
3848 // MemorySize (Thumb-2)
3849 ArmOp::MemorySize { rd, page_log2 } => {
3850 // R10 holds the size in BYTES; `memory.size` answers in DECLARED
3851 // pages, so shift right by log2(page size) (#1441). 16 for the
3852 // default 64 KiB page, 0 for `(pagesize 1)`.
3853 let rd_bits = reg_to_bits(rd);
3854 let r10_bits = reg_to_bits(&Reg::R10);
3855 if *page_log2 >= 32 {
3856 return Err(synth_core::Error::synthesis(format!(
3857 "#1441 internal: memory.size page_log2 = {page_log2} is \
3858 not encodable as a Thumb-2 shift (0..=31) — refusing \
3859 rather than emitting a shift of 32"
3860 )));
3861 }
3862 // SHIFT 0 IS NOT AN LSR. In the 16-bit form `0000 1 imm5 Rm Rd`
3863 // an `imm5` of 0 is not "LSR #0" at all, and in the 32-bit form
3864 // `type=01` with a zero immediate means SHIFT BY 32. So
3865 // `(pagesize 1)` must encode as a MOV. Emitting the shift
3866 // unconditionally would have returned 0 for every
3867 // `memory.size` on a byte-paged memory — a silent wrong answer
3868 // produced by the fix itself.
3869 if rd_bits < 8 && r10_bits < 8 && *page_log2 != 0 {
3870 // 16-bit LSRS Rd, Rm, #imm5 (unreachable for R10, which is
3871 // a high register — kept because the guard, not the
3872 // register allocation, is what makes it safe).
3873 let instr: u16 = 0x0800
3874 | ((*page_log2 as u16) << 6)
3875 | ((r10_bits as u16) << 3)
3876 | (rd_bits as u16);
3877 Ok(instr.to_le_bytes().to_vec())
3878 } else {
3879 // Thumb-2 32-bit: 1110 1010 010 0 1111 | 0 imm3 Rd imm2 tt Rm
3880 // `tt` = 01 for LSR, 00 for LSL. LSL #0 is the canonical
3881 // MOV.W Rd, Rm.
3882 let imm5: u32 = *page_log2;
3883 let imm3 = (imm5 >> 2) & 0x7;
3884 let imm2 = imm5 & 0x3;
3885 let shift_type: u32 = if imm5 == 0 { 0x00 } else { 0x10 };
3886 let hw1: u16 = 0xEA4F;
3887 let hw2: u16 =
3888 ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | shift_type | r10_bits)
3889 as u16;
3890 let mut bytes = hw1.to_le_bytes().to_vec();
3891 bytes.extend_from_slice(&hw2.to_le_bytes());
3892 Ok(bytes)
3893 }
3894 }
3895
3896 // MemoryGrow (Thumb-2)
3897 ArmOp::MemoryGrow { rd, .. } => {
3898 // On embedded with fixed memory, always return -1 (failure)
3899 // MVN rd, #0 → MOV rd, #-1
3900 // Thumb-2 32-bit: MVN: 1111 0 i 0 0 0 1 1 0 1111 | 0 imm3 Rd imm8
3901 let rd_bits = reg_to_bits(rd);
3902 let hw1: u16 = 0xF06F; // MVN with i=0
3903 let hw2: u16 = (rd_bits << 8) as u16; // imm8=0 → ~0 = 0xFFFFFFFF = -1
3904 let mut bytes = hw1.to_le_bytes().to_vec();
3905 bytes.extend_from_slice(&hw2.to_le_bytes());
3906 Ok(bytes)
3907 }
3908
3909 // BX (16-bit)
3910 ArmOp::Bx { rm } => {
3911 let rm_bits = reg_to_bits(rm) as u16;
3912 // BX Rm (16-bit): 0100 0111 0 Rm 000
3913 let instr: u16 = 0x4700 | (rm_bits << 3);
3914 Ok(instr.to_le_bytes().to_vec())
3915 }
3916
3917 // BLX (16-bit) - Branch with Link and Exchange
3918 // BLX Rm: 0100 0111 1 Rm 000
3919 ArmOp::Blx { rm } => {
3920 let rm_bits = reg_to_bits(rm) as u16;
3921 let instr: u16 = 0x4780 | (rm_bits << 3);
3922 Ok(instr.to_le_bytes().to_vec())
3923 }
3924
3925 // CallIndirect - indirect function call via table lookup
3926 // table_index_reg contains the table index
3927 // Generates (#642): MOVW ip,#size [; MOVT]; CMP idx,ip; BLO +1;
3928 // UDF #0; LSL R12,idx,#2; LDR R12,[R11,R12]; BLX R12
3929 // #650, table_byte_offset != 0 (a non-zero table in the contiguous
3930 // R11 region): the pointer load becomes
3931 // ADD R12,R11,R12; LDR R12,[R12,#offset]
3932 // #664, null_check (the table has null slots, linked as ZERO
3933 // words): the loaded pointer is null-checked before the BLX —
3934 // CMP.W R12,#0; BNE +1; UDF #0
3935 // #676, type_check (heterogeneous table — runtime §4.4.8 type
3936 // check against the type-id sidecar at R11+off): after the
3937 // bounds guard —
3938 // LSL R12,idx,#2; ADD R12,R11,R12;
3939 // LDR R12,[R12,#type_off]; CMP.W R12,#id;
3940 // BEQ +1; UDF #0
3941 // (the dispatch tail then recomputes idx*4 — idx stays live).
3942 ArmOp::CallIndirect {
3943 rd: _,
3944 type_idx: _,
3945 table_index_reg,
3946 table_size,
3947 table_byte_offset,
3948 null_check,
3949 type_check,
3950 } => {
3951 let idx_reg = reg_to_bits(table_index_reg);
3952 let mut bytes = Vec::new();
3953
3954 // The expansion:
3955 // 1. Bounds guard (#642): trap (UDF #0, WASM Core §4.4.8) when
3956 // index >= table size. Without it an out-of-bounds index
3957 // reads past the table and BLXes whatever word lies there —
3958 // an uncontrolled indirect branch instead of a trap.
3959 // 2. Multiplies index by 4 (function pointer size)
3960 // 3. Loads function pointer from table (table base in R11)
3961 // 4. Calls the function via BLX
3962 //
3963 // Table base setup must be done by caller/runtime. The type
3964 // check §4.4.8 also requires is discharged at COMPILE time:
3965 // the selector only emits this op after verifying the closed-
3966 // world property that every table entry's signature equals the
3967 // expected type (the raw code-pointer table carries no runtime
3968 // type ids to compare) — see the #642 selector guard.
3969
3970 // MOVW R12, #(size & 0xFFFF) — Thumb-2 T3:
3971 // 11110 i 100100 imm4 | 0 imm3 Rd imm8 (Rd=R12).
3972 let size_lo = *table_size & 0xFFFF;
3973 let hw1: u16 =
3974 (0xF240 | (((size_lo >> 11) & 1) << 10) | ((size_lo >> 12) & 0xF)) as u16;
3975 let hw2: u16 =
3976 ((((size_lo >> 8) & 0x7) << 12) | (12 << 8) | (size_lo & 0xFF)) as u16;
3977 bytes.extend_from_slice(&hw1.to_le_bytes());
3978 bytes.extend_from_slice(&hw2.to_le_bytes());
3979 // MOVT R12, #(size >> 16) — only when the table size exceeds
3980 // 16 bits (never in practice, but the guard must not compare
3981 // against a truncated size).
3982 let size_hi = *table_size >> 16;
3983 if size_hi != 0 {
3984 let hw1: u16 =
3985 (0xF2C0 | (((size_hi >> 11) & 1) << 10) | ((size_hi >> 12) & 0xF)) as u16;
3986 let hw2: u16 =
3987 ((((size_hi >> 8) & 0x7) << 12) | (12 << 8) | (size_hi & 0xFF)) as u16;
3988 bytes.extend_from_slice(&hw1.to_le_bytes());
3989 bytes.extend_from_slice(&hw2.to_le_bytes());
3990 }
3991 // CMP idx, R12 — 16-bit T2 (high-register capable):
3992 // 010001 01 N Rm(4) Rn(3), Rn full = N:Rn3.
3993 let cmp: u16 = (0x4500 | ((idx_reg & 8) << 4) | (12 << 3) | (idx_reg & 7)) as u16;
3994 bytes.extend_from_slice(&cmp.to_le_bytes());
3995 // BLO +1 insn (skip the UDF when index < size) — B<cond>.N
3996 // imm8=0: target = branch + 4. LO = unsigned lower.
3997 bytes.extend_from_slice(&0xD300u16.to_le_bytes());
3998 // UDF #0 — call_indirect out-of-bounds trap (same trap idiom as
3999 // the div-by-zero guards).
4000 bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
4001
4002 // #676: runtime type check — ONLY for a heterogeneous table
4003 // (mixed signatures, closed-world verdict impossible). Load
4004 // the indexed slot's structural class id from the type-id
4005 // sidecar (`R11 + type_off + idx*4`; `type_off` = sidecar
4006 // base + this table's base offset, a compile-time constant)
4007 // and compare it against the expected type's class id — a
4008 // mismatch is the WASM Core §4.4.8 type trap. Null slots
4009 // carry the reserved id 0, so this compare subsumes the
4010 // #664 null trap (the selector passes `null_check: false`).
4011 // `None` emits NOTHING: every homogeneous table keeps the
4012 // pre-#676 bytes identical BY CONSTRUCTION. R12 stays the
4013 // only scratch (#212); the dispatch tail below recomputes
4014 // idx*4 — the index register is never clobbered here.
4015 if let Some((expected_id, type_off)) = type_check {
4016 // RQ-61-IMMRANGE (#1072): compiled out in release, where
4017 // the masks below silently TRUNCATE (id 256 compares as
4018 // 0 — a NULL slot would pass the §4.4.8 check). The
4019 // enforcement claim is DEMONSTRATED: the sole `Some`
4020 // producer, `resolve_runtime_type_check`, loud-declines
4021 // id > 255 / offset > 4095, tripped by
4022 // `test_676_call_indirect_runtime_check_range_declines`
4023 // (mutation-checked — see the A32 twin above).
4024 debug_assert!(*expected_id <= 255, "selector enforces the CMP imm8 range");
4025 debug_assert!(*type_off <= 4095, "selector enforces the LDR imm12 range");
4026 // MOV.W R12, idx, LSL #2 (same encoding as the dispatch
4027 // tail's index scale below).
4028 bytes.extend_from_slice(&0xEA4Fu16.to_le_bytes());
4029 bytes.extend_from_slice(
4030 &(((0x0C00 | (0b10 << 6)) | idx_reg) as u16).to_le_bytes(),
4031 );
4032 // ADD.W R12, R11, R12 (the #650 base-add form).
4033 bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
4034 bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
4035 // LDR.W R12, [R12, #type_off] — T3 LDR (immediate):
4036 // 1111 1000 1101 Rn=1100 | Rt=1100 imm12.
4037 bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
4038 bytes.extend_from_slice(
4039 &(0xC000u16 | (*type_off as u16 & 0x0FFF)).to_le_bytes(),
4040 );
4041 // CMP.W R12, #expected_id — T2 CMP (immediate), imm8
4042 // (same form as the #664 null check's CMP.W R12, #0).
4043 bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
4044 bytes.extend_from_slice(
4045 &(0x0F00u16 | (*expected_id as u16 & 0xFF)).to_le_bytes(),
4046 );
4047 // BEQ +1 insn (skip the UDF when the class id matches) —
4048 // B<cond>.N imm8=0: target = branch + 4. EQ.
4049 bytes.extend_from_slice(&0xD000u16.to_le_bytes());
4050 // UDF #0 — the §4.4.8 type-mismatch trap (same idiom as
4051 // the bounds guard above).
4052 bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
4053 }
4054
4055 // LSL R12, idx_reg, #2 (multiply index by 4)
4056 // Thumb-2 MOV with shift: 11101010 010 S 1111 | 0 imm3 Rd imm2 type Rm
4057 // LSL: type=00 (bits 5:4), imm5=2 -> imm3=000, imm2=10 (bits 7:6)
4058 // #597: the shift amount was previously shifted into bits 5:4 —
4059 // the TYPE field — encoding `mov.w ip, rm, ASR #32`, which
4060 // destroyed the index and dispatched table entry 0 for every
4061 // call. imm2 lives at bits 7:6.
4062 let hw1: u16 = 0xEA4F_u16; // MOV.W R12, Rm, LSL #2
4063 let hw2: u16 = ((0x0C00 | (0b10 << 6)) | idx_reg) as u16;
4064 bytes.extend_from_slice(&hw1.to_le_bytes());
4065 bytes.extend_from_slice(&hw2.to_le_bytes());
4066
4067 if *table_byte_offset == 0 {
4068 // Table 0 (base = R11 itself): the pre-#650 single-load
4069 // form — a single-table module's bytes stay identical BY
4070 // CONSTRUCTION.
4071 // LDR R12, [R11, R12] - load function pointer
4072 // Thumb-2 LDR (register): 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
4073 // Rn=R11, Rt=R12, Rm=R12, imm2=00 (no shift)
4074 let ldr_hw1: u16 = 0xF85B; // LDR.W Rt, [R11, Rm]
4075 let ldr_hw2: u16 = 0xC00C; // Rt=R12, imm2=00, Rm=R12
4076 bytes.extend_from_slice(&ldr_hw1.to_le_bytes());
4077 bytes.extend_from_slice(&ldr_hw2.to_le_bytes());
4078 } else {
4079 // #650: table N of the contiguous R11 region — fold the
4080 // compile-time base offset into the pointer load via the
4081 // LDR imm12 form (R12 stays the only scratch, per the
4082 // #212 convention).
4083 assert!(
4084 *table_byte_offset <= 4095,
4085 "call_indirect table base offset {table_byte_offset} exceeds \
4086 LDR imm12 — the selector must have declined this (#650)"
4087 );
4088 // ADD.W R12, R11, R12 — T3 ADD (register):
4089 // 11101011000 S=0 Rn=1011 | 0 imm3=000 Rd=1100 imm2=00 type=00 Rm=1100
4090 bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
4091 bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
4092 // LDR.W R12, [R12, #offset] — T3 LDR (immediate):
4093 // 1111 1000 1101 Rn=1100 | Rt=1100 imm12
4094 bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
4095 bytes.extend_from_slice(
4096 &((0xC000u16) | (*table_byte_offset as u16 & 0x0FFF)).to_le_bytes(),
4097 );
4098 }
4099
4100 // #664: null-slot trap — ONLY when the table image carries
4101 // null (uninitialized) slots, which the layout contract
4102 // requires to be linked as ZERO words. A fully-initialized
4103 // table skips this branch entirely, keeping the pre-#664
4104 // expansion byte-identical BY CONSTRUCTION (the #650
4105 // offset-0 trick).
4106 if *null_check {
4107 // CMP.W R12, #0 — T2 CMP (immediate): 11110 i 0 1101 1
4108 // Rn(4) | 0 imm3 1111 imm8, Rn=R12, imm=0.
4109 bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
4110 bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
4111 // BNE +1 insn (skip the UDF when the pointer is non-null)
4112 // — B<cond>.N imm8=0: target = branch + 4. NE.
4113 bytes.extend_from_slice(&0xD100u16.to_le_bytes());
4114 // UDF #0 — call_indirect null-funcref trap (WASM Core
4115 // §4.4.8: calling an uninitialized element traps; same
4116 // trap idiom as the bounds guard above).
4117 bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
4118 }
4119
4120 // BLX R12 (call function indirectly)
4121 // BLX Rm (16-bit): 0100 0111 1 Rm 000
4122 let blx: u16 = 0x47E0; // BLX R12
4123 bytes.extend_from_slice(&blx.to_le_bytes());
4124
4125 Ok(bytes)
4126 }
4127
4128 // Label pseudo-instruction: emits no machine code
4129 ArmOp::Label { .. } => Ok(Vec::new()),
4130
4131 // Conditional branch to label (generic) - offset 0, will be patched
4132 ArmOp::Bcc { cond, label: _ } => {
4133 use synth_synthesis::Condition;
4134 let cond_bits: u16 = match cond {
4135 Condition::EQ => 0x0,
4136 Condition::NE => 0x1,
4137 Condition::HS => 0x2,
4138 Condition::LO => 0x3,
4139 Condition::HI => 0x8,
4140 Condition::LS => 0x9,
4141 Condition::GE => 0xA,
4142 Condition::LT => 0xB,
4143 Condition::GT => 0xC,
4144 Condition::LE => 0xD,
4145 };
4146 // 16-bit B<cond> with offset 0: 1101 cond imm8
4147 let instr: u16 = 0xD000 | (cond_bits << 8);
4148 Ok(instr.to_le_bytes().to_vec())
4149 }
4150
4151 // Branch instructions
4152 ArmOp::B { label: _ } => {
4153 // Simplified: B.N with offset 0
4154 // For real usage, would need label resolution
4155 let instr: u16 = 0xE000; // B.N #0
4156 Ok(instr.to_le_bytes().to_vec())
4157 }
4158
4159 // BHS (Branch if Higher or Same) - used for bounds checking
4160 // Condition code: 0x2 (C set)
4161 ArmOp::Bhs { label: _ } => {
4162 // 16-bit B<cond> with offset 0: 1101 cond imm8
4163 // cond = 0x2 (HS)
4164 let instr: u16 = 0xD200; // BHS.N #0
4165 Ok(instr.to_le_bytes().to_vec())
4166 }
4167
4168 // BLO (Branch if Lower) - complementary to BHS
4169 // Condition code: 0x3 (C clear)
4170 ArmOp::Blo { label: _ } => {
4171 // 16-bit B<cond> with offset 0: 1101 cond imm8
4172 // cond = 0x3 (LO)
4173 let instr: u16 = 0xD300; // BLO.N #0
4174 Ok(instr.to_le_bytes().to_vec())
4175 }
4176
4177 // Branch with numeric offset (Thumb-2)
4178 // Thumb-2 B.W instruction: 32-bit with +-16MB range
4179 ArmOp::BOffset { offset } => {
4180 // offset is already the halfword displacement: (target - branch - 4) / 2
4181 // This is the raw encoded value, accounting for variable-length instructions
4182 let halfword_offset = *offset;
4183
4184 // 16-bit B.N encoding: 1110 0 imm11 (11-bit signed halfword offset)
4185 // Range: -1024 to +1022 halfwords
4186 if (-1024..=1022).contains(&halfword_offset) {
4187 // 16-bit B.N encoding: 1110 0 imm11
4188 let imm11 = (halfword_offset as u16) & 0x7FF;
4189 let instr: u16 = 0xE000 | imm11;
4190 Ok(instr.to_le_bytes().to_vec())
4191 } else {
4192 // 32-bit B.W encoding for larger offsets
4193 // First halfword: 1111 0 S imm10
4194 // Second halfword: 10 J1 0 J2 imm11
4195 // Total offset = SignExtend(S:I1:I2:imm10:imm11:0)
4196 // where I1 = NOT(J1 XOR S), I2 = NOT(J2 XOR S)
4197
4198 // The B.W (T4) encoding packs the signed offset as:
4199 // S:I1:I2:imm10:imm11:0 (25-bit signed, halfword-aligned)
4200 // where J1 = NOT(I1 XOR S), J2 = NOT(I2 XOR S)
4201 // Input halfword_offset already equals (target - PC - 4) / 2,
4202 // so the full byte offset = halfword_offset << 1.
4203 // The encoding fields split that 25-bit signed value (including the
4204 // implicit trailing zero) as: S | imm10 | imm11
4205 // with I1 = bit 23 and I2 = bit 22 of the signed offset.
4206 let signed_offset = halfword_offset << 1; // byte offset
4207 let s = if signed_offset < 0 { 1u32 } else { 0u32 };
4208 let uoffset = signed_offset as u32;
4209 let imm10 = (uoffset >> 12) & 0x3FF; // bits [21:12]
4210 let imm11 = (uoffset >> 1) & 0x7FF; // bits [11:1]
4211 let i1 = (uoffset >> 23) & 1; // bit 23
4212 let i2 = (uoffset >> 22) & 1; // bit 22
4213 let j1 = (!(i1 ^ s)) & 1; // J1 = NOT(I1 XOR S)
4214 let j2 = (!(i2 ^ s)) & 1; // J2 = NOT(I2 XOR S)
4215
4216 let hw1: u16 = (0xF000 | (s << 10) | imm10) as u16;
4217 let hw2: u16 = (0x9000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4218
4219 let mut bytes = hw1.to_le_bytes().to_vec();
4220 bytes.extend_from_slice(&hw2.to_le_bytes());
4221 Ok(bytes)
4222 }
4223 }
4224
4225 // Conditional branch with numeric offset (Thumb-2)
4226 ArmOp::BCondOffset { cond, offset } => {
4227 use synth_synthesis::Condition;
4228 let cond_bits: u16 = match cond {
4229 Condition::EQ => 0x0,
4230 Condition::NE => 0x1,
4231 Condition::HS => 0x2,
4232 Condition::LO => 0x3,
4233 Condition::HI => 0x8,
4234 Condition::LS => 0x9,
4235 Condition::GE => 0xA,
4236 Condition::LT => 0xB,
4237 Condition::GT => 0xC,
4238 Condition::LE => 0xD,
4239 };
4240
4241 // offset is already the halfword displacement: (target - branch - 4) / 2
4242 // This is the raw imm8 value for 16-bit B<cond> encoding
4243 let halfword_offset = *offset;
4244
4245 // 16-bit B<cond> encoding: 1101 cond imm8
4246 // Range: -256 to +254 halfwords (imm8 is sign-extended and shifted left 1)
4247 if (-128..=127).contains(&halfword_offset) {
4248 let imm8 = (halfword_offset as u16) & 0xFF;
4249 let instr: u16 = 0xD000 | (cond_bits << 8) | imm8;
4250 Ok(instr.to_le_bytes().to_vec())
4251 } else {
4252 // 32-bit B<cond>.W (encoding T3) for larger offsets
4253 // First halfword: 1111 0 S cond(4) imm6
4254 // Second halfword: 10 J1 0 J2 imm11
4255 //
4256 // Per ARMv7-M, the branch BYTE offset is
4257 // SignExtend(S:J2:J1:imm6:imm11:'0'), i.e. the field value
4258 // S:J2:J1:imm6:imm11 IS the signed 20-bit HALFWORD offset —
4259 // imm11/imm6/J1/J2/S take `halfword_offset` bits [10:0],
4260 // [16:11], 17, 18 and 19 directly (mirroring the T4
4261 // unconditional arm above).
4262 //
4263 // #740: this arm previously packed `halfword_offset >> 1`
4264 // into imm6:imm11 — HALVING the displacement — so every
4265 // wide conditional branch (span > 254 bytes) landed at half
4266 // its intended offset: gust_poll's loop-head `br_if` to an
4267 // outer block end jumped mid-shape. Narrow (16-bit) B<cond>
4268 // encodings were unaffected, which is why short-range CF
4269 // fixtures never caught it.
4270 if !(-(1 << 19)..(1 << 19)).contains(&halfword_offset) {
4271 return Err(synth_core::Error::synthesis(format!(
4272 "B<cond>.W (T3) halfword offset {halfword_offset} exceeds \
4273 the signed 20-bit encoding range (±1 MB) — refusing to \
4274 emit a truncated branch"
4275 )));
4276 }
4277 let u = halfword_offset as u32;
4278 let imm11 = u & 0x7FF; // halfword offset bits [10:0]
4279 let imm6 = (u >> 11) & 0x3F; // bits [16:11]
4280 let j1 = (u >> 17) & 1; // bit 17
4281 let j2 = (u >> 18) & 1; // bit 18
4282 let s = (u >> 19) & 1; // sign (range-checked above)
4283
4284 let hw1: u16 = (0xF000 | (s << 10) | ((cond_bits as u32) << 6) | imm6) as u16;
4285 let hw2: u16 = (0x8000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4286
4287 let mut bytes = hw1.to_le_bytes().to_vec();
4288 bytes.extend_from_slice(&hw2.to_le_bytes());
4289 Ok(bytes)
4290 }
4291 }
4292
4293 ArmOp::Bl { label: _ } => {
4294 // BL is always 32-bit in Thumb-2, encoded here as a relocatable
4295 // placeholder; an R_ARM_THM_CALL relocation patches the target
4296 // (see arm_backend.rs). The placeholder must carry an embedded
4297 // addend of -4 so the relocation nets to exactly the symbol S.
4298 //
4299 // Thumb BL computes `target = (P + 4) + signed_offset`. Under
4300 // R_ARM_THM_CALL the linker resolves using the in-place addend;
4301 // a 0xF800 placeholder (addend 0) lands at S+4 — every call one
4302 // instruction past the callee entry (#174). The correct
4303 // placeholder is what `gas` emits for `bl <extern>`:
4304 // f7ff fffe -> `bl <self>` (S=1, J1=J2=1, imm = -4 addend),
4305 // i.e. hw1=0xF7FF, hw2=0xFFFE. This nets to S, not S+4.
4306 // (The earlier 0xD000 was worse still — a ~+0x600000 addend,
4307 // the garbage `bl c0000c` and "truncated to fit" of #167.)
4308 let hw1: u16 = 0xF7FF;
4309 let hw2: u16 = 0xFFFE;
4310 let mut bytes = hw1.to_le_bytes().to_vec();
4311 bytes.extend_from_slice(&hw2.to_le_bytes());
4312 Ok(bytes)
4313 }
4314
4315 // MVN
4316 ArmOp::Mvn { rd, op2 } => {
4317 if let Operand2::Reg(rm) = op2 {
4318 let rd_bits = reg_to_bits(rd) as u16;
4319 let rm_bits = reg_to_bits(rm) as u16;
4320
4321 if rd_bits < 8 && rm_bits < 8 {
4322 // MVNS Rd, Rm (16-bit): 0100 0011 11 Rm Rd
4323 let instr: u16 = 0x43C0 | (rm_bits << 3) | rd_bits;
4324 Ok(instr.to_le_bytes().to_vec())
4325 } else {
4326 // 32-bit MVN
4327 let hw1: u16 = 0xEA6F_u16;
4328 let hw2: u16 = ((reg_to_bits(rd) << 8) | reg_to_bits(rm)) as u16;
4329 let mut bytes = hw1.to_le_bytes().to_vec();
4330 bytes.extend_from_slice(&hw2.to_le_bytes());
4331 Ok(bytes)
4332 }
4333 } else {
4334 // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
4335 // operand form the arm does not encode (Reg only) -- #615's
4336 // silent-miscompile class, on the DEFAULT target. Refuse instead.
4337 Err(synth_core::Error::synthesis(format!(
4338 "Thumb-2 MVN with operand {op2:?} has no Thumb-2 encoding here (Reg \
4339 are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
4340 )))
4341 }
4342 }
4343
4344 // MOVW - Move Wide (Thumb-2 32-bit)
4345 ArmOp::Movw { rd, imm16 } => {
4346 self.encode_thumb32_movw_raw(reg_to_bits(rd), *imm16 as u32)
4347 }
4348
4349 // MOVT - Move Top (Thumb-2 32-bit)
4350 ArmOp::Movt { rd, imm16 } => {
4351 self.encode_thumb32_movt_raw(reg_to_bits(rd), *imm16 as u32)
4352 }
4353
4354 // #237: symbol-relative MOVW/MOVT. Encode the addend's low/high 16
4355 // bits in place; the backend records an R_ARM_MOVW_ABS_NC /
4356 // R_ARM_MOVT_ABS relocation against `symbol`, so the linker adds the
4357 // symbol's final address to the in-place addend (REL semantics).
4358 ArmOp::MovwSym { rd, addend, .. } => {
4359 self.encode_thumb32_movw_raw(reg_to_bits(rd), (*addend as u32) & 0xffff)
4360 }
4361 ArmOp::MovtSym { rd, addend, .. } => {
4362 self.encode_thumb32_movt_raw(reg_to_bits(rd), ((*addend as u32) >> 16) & 0xffff)
4363 }
4364
4365 // #345: literal-pool address load — emit a PLACEHOLDER `LDR.W rd,
4366 // [pc, #0]` (U=1, imm12=0). The backend (arm_backend.rs) places the
4367 // 4-byte pool word at the end of the function, records the R_ARM_ABS32
4368 // relocation against `symbol+addend`, and patches the imm12 with the
4369 // real PC-relative distance once the pool offset is known.
4370 // Encoding T2: 1111 1000 1101 1111 | Rt(4) imm12(12), with the literal
4371 // base = Align(PC,4) and PC = address of this instruction + 4.
4372 ArmOp::LdrSym { rd, .. } => {
4373 let rt = reg_to_bits(rd) as u16;
4374 let hw1: u16 = 0xF8DF; // LDR.W (literal), U=1
4375 let hw2: u16 = rt << 12; // imm12 = 0 placeholder
4376 let mut bytes = Vec::with_capacity(4);
4377 bytes.extend_from_slice(&hw1.to_le_bytes());
4378 bytes.extend_from_slice(&hw2.to_le_bytes());
4379 Ok(bytes)
4380 }
4381
4382 // SetCond: Materialize condition flag into register (0 or 1)
4383 // Strategy: ITE <cond>; MOV Rd, #1; MOV Rd, #0
4384 // IMPORTANT: Must use ITE (If-Then-Else) because 16-bit Thumb MOV
4385 // always sets flags (MOVS). We need to evaluate the condition BEFORE
4386 // any MOV instruction clobbers the flags from CMP.
4387 ArmOp::SetCond { rd, cond } => {
4388 let rd_bits = reg_to_bits(rd) as u16;
4389
4390 // Condition code encoding for IT block
4391 use synth_synthesis::Condition;
4392 let cond_bits: u16 = match cond {
4393 Condition::EQ => 0x0,
4394 Condition::NE => 0x1,
4395 Condition::LT => 0xB,
4396 Condition::LE => 0xD,
4397 Condition::GT => 0xC,
4398 Condition::GE => 0xA,
4399 Condition::LO => 0x3, // CC/LO (unsigned <)
4400 Condition::LS => 0x9, // LS (unsigned <=)
4401 Condition::HI => 0x8, // HI (unsigned >)
4402 Condition::HS => 0x2, // CS/HS (unsigned >=)
4403 };
4404
4405 // ITE <cond>: encodes If-Then-Else block
4406 // The mask field depends on firstcond[0]:
4407 // - If firstcond[0] = 0: mask = 0xC for TE pattern (ITE EQ = BF0C)
4408 // - If firstcond[0] = 1: mask = 0x4 for TE pattern (ITE NE = BF14)
4409 let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4410 let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4411
4412 // Materialize 0/1 into Rd. The 16-bit MOVS (T1) encodes Rd in a
4413 // 3-bit field (bits[10:8]) — only R0–R7. For a high register
4414 // (R8–R12) `rd_bits << 8` overflows into bit 11 and silently
4415 // turns MOVS into CMP (00100 → 00101), corrupting the result
4416 // (this mis-materialized gale's `has_waiter`, so its `local.set`
4417 // stored a stale register → the binary-sem WAKE dispatch read
4418 // garbage). Use the 32-bit MOV.W (T2) for high registers, which
4419 // has a 4-bit Rd field. MOV.W with S=0 doesn't set flags, which
4420 // is fine inside the ITE (the materialized value is the result;
4421 // the flags are not consumed afterwards).
4422 let mut bytes = ite_instr.to_le_bytes().to_vec();
4423 let push_mov = |bytes: &mut Vec<u8>, imm: u16| {
4424 if rd_bits <= 7 {
4425 let m: u16 = 0x2000 | (rd_bits << 8) | imm; // 16-bit MOVS Rd,#imm
4426 bytes.extend_from_slice(&m.to_le_bytes());
4427 } else {
4428 // 32-bit MOV.W Rd, #imm (T2): F04F | (Rd<<8) | imm8
4429 let hw1: u16 = 0xF04F;
4430 let hw2: u16 = (rd_bits << 8) | imm;
4431 bytes.extend_from_slice(&hw1.to_le_bytes());
4432 bytes.extend_from_slice(&hw2.to_le_bytes());
4433 }
4434 };
4435 push_mov(&mut bytes, 1); // Then branch (condition true) → 1
4436 push_mov(&mut bytes, 0); // Else branch (condition false) → 0
4437 Ok(bytes)
4438 }
4439
4440 // I64SetCond: Compare two i64 register pairs, result 0/1 in rd
4441 // EQ/NE: CMP lo,lo; IT EQ; CMPEQ hi,hi; ITE <cond>; MOV 1; MOV 0
4442 // LT: CMP lo,lo; SBCS rd,hi,hi; ITE LT; MOV 1; MOV 0
4443 // GT: CMP lo,lo (swapped); SBCS rd,hi,hi (swapped); ITE LT; MOV 1; MOV 0
4444 ArmOp::I64SetCond {
4445 rd,
4446 rn_lo,
4447 rn_hi,
4448 rm_lo,
4449 rm_hi,
4450 cond,
4451 } => {
4452 use synth_synthesis::Condition;
4453 let rd_bits = reg_to_bits(rd) as u16;
4454 let mut bytes = Vec::new();
4455
4456 // Helper: encode CMP Rn, Rm (16-bit)
4457 let encode_cmp_reg = |rn: &synth_synthesis::Reg,
4458 rm: &synth_synthesis::Reg|
4459 -> Vec<u8> {
4460 let rn_bits = reg_to_bits(rn) as u16;
4461 let rm_bits = reg_to_bits(rm) as u16;
4462 if rn_bits < 8 && rm_bits < 8 {
4463 let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
4464 instr.to_le_bytes().to_vec()
4465 } else {
4466 let n_bit = (rn_bits >> 3) & 1;
4467 let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
4468 instr.to_le_bytes().to_vec()
4469 }
4470 };
4471
4472 // Helper: encode ITE <cond> (2 bytes)
4473 let encode_ite = |cond_bits: u16| -> Vec<u8> {
4474 let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4475 let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4476 ite_instr.to_le_bytes().to_vec()
4477 };
4478
4479 // Helper: encode SetCond (ITE + MOV #1 + MOV #0) for given condition
4480 let encode_setcond = |cond_bits: u16, rd_bits: u16| -> Vec<u8> {
4481 let mut b = encode_ite(cond_bits);
4482 if rd_bits < 8 {
4483 let mov_one: u16 = 0x2001 | (rd_bits << 8);
4484 let mov_zero: u16 = 0x2000 | (rd_bits << 8);
4485 b.extend_from_slice(&mov_one.to_le_bytes());
4486 b.extend_from_slice(&mov_zero.to_le_bytes());
4487 } else {
4488 // #311: rd >= R8 — the 16-bit MOV imm8 form has a 3-bit
4489 // rd field; rd_bits<<8 overflows into bit 11 and
4490 // TRANSMUTES the MOV into CMP (0x2001|0x0800 = 0x2801 =
4491 // CMP r0,#1): the boolean dies in the flags and the
4492 // consumer reads a stale register. Use the 32-bit
4493 // MOV.W (T2: F04F 0000|rd<<8|imm8) — IT-legal,
4494 // flag-preserving. Same class as H-CODE-9 / #180.
4495 for imm in [1u16, 0u16] {
4496 let hw1: u16 = 0xF04F;
4497 let hw2: u16 = (rd_bits << 8) | imm;
4498 b.extend_from_slice(&hw1.to_le_bytes());
4499 b.extend_from_slice(&hw2.to_le_bytes());
4500 }
4501 }
4502 b
4503 };
4504
4505 match cond {
4506 Condition::EQ | Condition::NE => {
4507 // CMP rn_lo, rm_lo (compare low words)
4508 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4509
4510 // IT EQ (execute next instruction only if Z=1)
4511 let it_eq: u16 = 0xBF08; // IT EQ: cond=0000, mask=1000
4512 bytes.extend_from_slice(&it_eq.to_le_bytes());
4513
4514 // CMPEQ rn_hi, rm_hi (compare high words, only if low equal)
4515 bytes.extend_from_slice(&encode_cmp_reg(rn_hi, rm_hi));
4516
4517 // ITE <cond>; MOV rd, #1; MOV rd, #0
4518 let cond_bits: u16 = match cond {
4519 Condition::EQ => 0x0,
4520 Condition::NE => 0x1,
4521 _ => unreachable!(),
4522 };
4523 bytes.extend_from_slice(&encode_setcond(cond_bits, rd_bits));
4524 }
4525
4526 Condition::LT => {
4527 // CMP rn_lo, rm_lo (sets C flag for borrow)
4528 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4529
4530 // SBCS rd, rn_hi, rm_hi (subtract with carry, sets N,V flags)
4531 // SBCS.W Rd, Rn, Rm: EB70 Rn | 0000 Rd 0000 Rm
4532 let rn_hi_bits = reg_to_bits(rn_hi);
4533 let rm_hi_bits = reg_to_bits(rm_hi);
4534 let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4535 let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4536 bytes.extend_from_slice(&hw1.to_le_bytes());
4537 bytes.extend_from_slice(&hw2.to_le_bytes());
4538
4539 // ITE LT; MOV rd, #1; MOV rd, #0
4540 bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4541 }
4542
4543 Condition::GT => {
4544 // GT(a,b) = LT(b,a): swap operands
4545 // CMP rm_lo, rn_lo (swapped)
4546 bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4547
4548 // SBCS rd, rm_hi, rn_hi (swapped)
4549 let rm_hi_bits = reg_to_bits(rm_hi);
4550 let rn_hi_bits = reg_to_bits(rn_hi);
4551 let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4552 let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4553 bytes.extend_from_slice(&hw1.to_le_bytes());
4554 bytes.extend_from_slice(&hw2.to_le_bytes());
4555
4556 // ITE LT; MOV rd, #1; MOV rd, #0
4557 bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4558 }
4559
4560 Condition::LE => {
4561 // LE(a,b) = !GT(a,b): use GT logic but invert result
4562 // GT(a,b) = LT(b,a): so we do CMP(b,a) and check LT, then invert
4563 // CMP rm_lo, rn_lo (swapped, same as GT)
4564 bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4565
4566 // SBCS rd, rm_hi, rn_hi (swapped)
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
4574 // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT, so inverting GT result)
4575 bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4576 }
4577
4578 Condition::GE => {
4579 // GE(a,b) = !LT(a,b): use LT logic but invert result
4580 // CMP rn_lo, rm_lo (same as LT)
4581 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4582
4583 // SBCS rd, rn_hi, rm_hi (same as LT)
4584 let rn_hi_bits = reg_to_bits(rn_hi);
4585 let rm_hi_bits = reg_to_bits(rm_hi);
4586 let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4587 let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4588 bytes.extend_from_slice(&hw1.to_le_bytes());
4589 bytes.extend_from_slice(&hw2.to_le_bytes());
4590
4591 // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT)
4592 bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4593 }
4594
4595 // Unsigned comparisons - same instruction sequence, different conditions
4596 Condition::LO => {
4597 // LO (unsigned LT): CMP lo, SBCS hi, check C=0
4598 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4599 let rn_hi_bits = reg_to_bits(rn_hi);
4600 let rm_hi_bits = reg_to_bits(rm_hi);
4601 let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4602 let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4603 bytes.extend_from_slice(&hw1.to_le_bytes());
4604 bytes.extend_from_slice(&hw2.to_le_bytes());
4605 bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4606 }
4607
4608 Condition::HI => {
4609 // HI (unsigned GT): swap operands and check LO
4610 bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4611 let rm_hi_bits = reg_to_bits(rm_hi);
4612 let rn_hi_bits = reg_to_bits(rn_hi);
4613 let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4614 let hw2: u16 = ((rd_bits as u32) << 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 bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4618 }
4619
4620 Condition::LS => {
4621 // LS (unsigned LE): !(a > b) = !(HI), so do HI and invert
4622 bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4623 let rm_hi_bits = reg_to_bits(rm_hi);
4624 let rn_hi_bits = reg_to_bits(rn_hi);
4625 let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4626 let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4627 bytes.extend_from_slice(&hw1.to_le_bytes());
4628 bytes.extend_from_slice(&hw2.to_le_bytes());
4629 bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4630 }
4631
4632 Condition::HS => {
4633 // HS (unsigned GE): !(a < b) = !(LO)
4634 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4635 let rn_hi_bits = reg_to_bits(rn_hi);
4636 let rm_hi_bits = reg_to_bits(rm_hi);
4637 let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4638 let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4639 bytes.extend_from_slice(&hw1.to_le_bytes());
4640 bytes.extend_from_slice(&hw2.to_le_bytes());
4641 bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4642 }
4643 }
4644
4645 Ok(bytes)
4646 }
4647
4648 // I64SetCondZ: Test if i64 register pair is zero, result 0/1 in rd
4649 // ORR.W rd, rn_lo, rn_hi; CMP rd, #0; ITE EQ; MOV 1; MOV 0
4650 ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
4651 let rd_bits = reg_to_bits(rd);
4652 let rn_lo_bits = reg_to_bits(rn_lo);
4653 let rn_hi_bits = reg_to_bits(rn_hi);
4654 let mut bytes = Vec::new();
4655
4656 // ORR.W rd, rn_lo, rn_hi: EA40 rn_lo | 0000 rd 0000 rn_hi
4657 let hw1: u16 = (0xEA40 | rn_lo_bits) as u16;
4658 let hw2: u16 = ((rd_bits << 8) | rn_hi_bits) as u16;
4659 bytes.extend_from_slice(&hw1.to_le_bytes());
4660 bytes.extend_from_slice(&hw2.to_le_bytes());
4661
4662 // CMP rd, #0 — 16-bit form only for r0-r7 (3-bit rd field);
4663 // high registers take CMP.W (T2: F1B0|rn 0F00|imm8). This was
4664 // H-CODE-9: rd_bits<<8 overflowing the field compared the
4665 // WRONG register. Same hardening as the #311 SetCond fix.
4666 if rd_bits < 8 {
4667 let cmp_instr: u16 = 0x2800 | ((rd_bits as u16) << 8);
4668 bytes.extend_from_slice(&cmp_instr.to_le_bytes());
4669 } else {
4670 let hw1: u16 = 0xF1B0 | (rd_bits as u16);
4671 let hw2: u16 = 0x0F00;
4672 bytes.extend_from_slice(&hw1.to_le_bytes());
4673 bytes.extend_from_slice(&hw2.to_le_bytes());
4674 }
4675
4676 // ITE EQ; MOV rd, #1; MOV rd, #0 (32-bit MOV.W for rd >= R8,
4677 // #311 — see I64SetCond)
4678 let mask = 0xC_u16; // ITE EQ mask: firstcond[0]=0, mask=0xC
4679 let ite_instr: u16 = 0xBF00 | mask;
4680 bytes.extend_from_slice(&ite_instr.to_le_bytes());
4681 if rd_bits < 8 {
4682 let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
4683 let mov_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
4684 bytes.extend_from_slice(&mov_one.to_le_bytes());
4685 bytes.extend_from_slice(&mov_zero.to_le_bytes());
4686 } else {
4687 for imm in [1u16, 0u16] {
4688 let hw1: u16 = 0xF04F;
4689 let hw2: u16 = ((rd_bits as u16) << 8) | imm;
4690 bytes.extend_from_slice(&hw1.to_le_bytes());
4691 bytes.extend_from_slice(&hw2.to_le_bytes());
4692 }
4693 }
4694
4695 Ok(bytes)
4696 }
4697
4698 // I64Mul: 64-bit multiply using UMULL + MLA cross products
4699 // Formula: result = (a_lo * b_lo) + ((a_lo * b_hi + a_hi * b_lo) << 32)
4700 // Uses R12 as scratch register
4701 ArmOp::I64Mul {
4702 rd_lo,
4703 rd_hi,
4704 rn_lo,
4705 rn_hi,
4706 rm_lo,
4707 rm_hi,
4708 } => {
4709 let rd_lo_bits = reg_to_bits(rd_lo);
4710 let rd_hi_bits = reg_to_bits(rd_hi);
4711 let rn_lo_bits = reg_to_bits(rn_lo);
4712 let rn_hi_bits = reg_to_bits(rn_hi);
4713 let rm_lo_bits = reg_to_bits(rm_lo);
4714 let rm_hi_bits = reg_to_bits(rm_hi);
4715 let r12: u32 = 12; // IP scratch register
4716 let mut bytes = Vec::new();
4717
4718 // 1. MUL R12, rn_lo, rm_hi (R12 = a_lo * b_hi)
4719 // Thumb-2 MUL: hw1=0xFB00|Rn, hw2=0xF000|(Rd<<8)|Rm
4720 let hw1: u16 = (0xFB00 | rn_lo_bits) as u16;
4721 let hw2: u16 = (0xF000 | (r12 << 8) | rm_hi_bits) as u16;
4722 bytes.extend_from_slice(&hw1.to_le_bytes());
4723 bytes.extend_from_slice(&hw2.to_le_bytes());
4724
4725 // 2. MLA R12, rn_hi, rm_lo, R12 (R12 += a_hi * b_lo)
4726 // Thumb-2 MLA: hw1=0xFB00|Rn, hw2=(Ra<<12)|(Rd<<8)|Rm
4727 let hw1: u16 = (0xFB00 | rn_hi_bits) as u16;
4728 let hw2: u16 = ((r12 << 12) | (r12 << 8) | rm_lo_bits) as u16;
4729 bytes.extend_from_slice(&hw1.to_le_bytes());
4730 bytes.extend_from_slice(&hw2.to_le_bytes());
4731
4732 // 3. UMULL rd_lo, rd_hi, rn_lo, rm_lo (rd_lo:rd_hi = a_lo * b_lo)
4733 // Thumb-2 UMULL: hw1=0xFBA0|Rn, hw2=(RdLo<<12)|(RdHi<<8)|Rm
4734 let hw1: u16 = (0xFBA0 | rn_lo_bits) as u16;
4735 let hw2: u16 = ((rd_lo_bits << 12) | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4736 bytes.extend_from_slice(&hw1.to_le_bytes());
4737 bytes.extend_from_slice(&hw2.to_le_bytes());
4738
4739 // 4. ADD rd_hi, R12 (rd_hi += cross products)
4740 // 16-bit high reg ADD: 01000100 D Rm Rdn[2:0]
4741 let d_bit = (rd_hi_bits >> 3) & 1;
4742 let add_instr: u16 =
4743 (0x4400 | (d_bit << 7) | (r12 << 3) | (rd_hi_bits & 0x7)) as u16;
4744 bytes.extend_from_slice(&add_instr.to_le_bytes());
4745
4746 Ok(bytes)
4747 }
4748
4749 // I64Shl: 64-bit shift left with branch for n<32 vs n>=32.
4750 //
4751 // #1048: the expansion must NEVER write its own input operands.
4752 // The pre-#1048 sequence masked the amount IN PLACE
4753 // (`AND.W rm_lo, rm_lo, #63`) and used the amount's home high
4754 // register `rm_hi` as scratch (`SUBS.W rm_hi, rm_lo, #32`, RSB,
4755 // LSR) — so re-reading the amount after the shift returned a
4756 // mangled value (amt=64 read back 0, amt=67 read back 3). The
4757 // rewrite uses R12 — encoder scratch, never allocatable (#212) —
4758 // as the ONLY temporary, re-deriving the masked amount from the
4759 // untouched rm_lo whenever a second live temp would otherwise be
4760 // needed. This matches the Rocq/SMT pseudo-op models
4761 // (I64ShlPseudo writes rd_lo/rd_hi ONLY), which were proven over
4762 // exactly this non-clobbering contract all along.
4763 ArmOp::I64Shl {
4764 rd_lo,
4765 rd_hi,
4766 rn_lo,
4767 rn_hi,
4768 rm_lo,
4769 rm_hi: _,
4770 } => {
4771 let rd_lo_bits = reg_to_bits(rd_lo);
4772 let rd_hi_bits = reg_to_bits(rd_hi);
4773 let rn_lo_bits = reg_to_bits(rn_lo);
4774 let rn_hi_bits = reg_to_bits(rn_hi);
4775 let rm_lo_bits = reg_to_bits(rm_lo);
4776 let r12: u32 = 12; // the only scratch — never allocatable
4777 let mut bytes = Vec::new();
4778
4779 // #1039 house style: refuse a destination that would collide
4780 // with an input still needed after the destination is first
4781 // written, loudly — never misassemble. rd_hi is written before
4782 // rn_lo and rm_lo are last read; the in-place form
4783 // rd == rn (select_default) has rd_hi == rn_hi and stays legal.
4784 if rd_hi_bits == rn_lo_bits || rd_hi_bits == rm_lo_bits {
4785 return Err(synth_core::Error::synthesis(format!(
4786 "I64Shl: rd_hi {rd_hi:?} aliases an input ({rn_lo:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4787 )));
4788 }
4789
4790 // AND.W R12, rm_lo, #63 (n — the amount register is only READ)
4791 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4792 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4793 bytes.extend_from_slice(&hw1.to_le_bytes());
4794 bytes.extend_from_slice(&hw2.to_le_bytes());
4795
4796 // SUBS.W R12, R12, #32 (R12 = n-32, sets flags)
4797 let hw1: u16 = (0xF1B0 | r12) as u16;
4798 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4799 bytes.extend_from_slice(&hw1.to_le_bytes());
4800 bytes.extend_from_slice(&hw2.to_le_bytes());
4801
4802 // BPL .large (branch if n >= 32, offset = +14 halfwords)
4803 let bpl: u16 = 0xD50E;
4804 bytes.extend_from_slice(&bpl.to_le_bytes());
4805
4806 // --- Small shift (n < 32) ---
4807 // AND.W R12, rm_lo, #63 (n again — R12 held n-32)
4808 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4809 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4810 bytes.extend_from_slice(&hw1.to_le_bytes());
4811 bytes.extend_from_slice(&hw2.to_le_bytes());
4812
4813 // LSL.W rd_hi, rn_hi, R12 (hi << n; rn_hi's last read)
4814 let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4815 let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4816 bytes.extend_from_slice(&hw1.to_le_bytes());
4817 bytes.extend_from_slice(&hw2.to_le_bytes());
4818
4819 // RSB.W R12, R12, #32 (R12 = 32-n)
4820 let hw1: u16 = (0xF1C0 | r12) as u16;
4821 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4822 bytes.extend_from_slice(&hw1.to_le_bytes());
4823 bytes.extend_from_slice(&hw2.to_le_bytes());
4824
4825 // LSR.W R12, rn_lo, R12 (overflow = lo >> (32-n); n=0 gives
4826 // a register shift by 32 which yields 0 — exact)
4827 let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4828 let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4829 bytes.extend_from_slice(&hw1.to_le_bytes());
4830 bytes.extend_from_slice(&hw2.to_le_bytes());
4831
4832 // ORR.W rd_hi, rd_hi, R12 (hi |= overflow bits from lo)
4833 let hw1: u16 = (0xEA40 | rd_hi_bits) as u16;
4834 let hw2: u16 = ((rd_hi_bits << 8) | r12) as u16;
4835 bytes.extend_from_slice(&hw1.to_le_bytes());
4836 bytes.extend_from_slice(&hw2.to_le_bytes());
4837
4838 // AND.W R12, rm_lo, #63 (n once more for the low half)
4839 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4840 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4841 bytes.extend_from_slice(&hw1.to_le_bytes());
4842 bytes.extend_from_slice(&hw2.to_le_bytes());
4843
4844 // LSL.W rd_lo, rn_lo, R12 (lo << n)
4845 let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4846 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4847 bytes.extend_from_slice(&hw1.to_le_bytes());
4848 bytes.extend_from_slice(&hw2.to_le_bytes());
4849
4850 // B .done — `.done` is the END of the expansion, i.e. PAST the
4851 // large-shift arm's trailing zero-fill. #916: that zero-fill is
4852 // 1 halfword for a low rd_lo but 2 for R8-R12 (MOV.W), so the
4853 // displacement is DERIVED from its real width instead of the
4854 // hard-coded 0xE002 — widening the MOV without this would
4855 // overshoot `.done` and turn a data miscompile into a
4856 // control-flow one. Thumb `B` reads PC as its own address + 4
4857 // (= +2 halfwords), so imm11 = (large-arm halfwords) - 1.
4858 let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_lo_bits);
4859 let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4860 bytes.extend_from_slice(&b_done.to_le_bytes());
4861
4862 // --- Large shift (n >= 32) --- (R12 still holds n-32)
4863 // LSL.W rd_hi, rn_lo, R12 (hi = lo << (n-32))
4864 let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4865 let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4866 bytes.extend_from_slice(&hw1.to_le_bytes());
4867 bytes.extend_from_slice(&hw2.to_le_bytes());
4868
4869 // MOV rd_lo, #0 (#916: MOV.W for rd_lo >= R8). NOTE the order
4870 // is load-bearing — zeroing rd_lo BEFORE the LSL.W would
4871 // destroy rn_lo in the in-place case rd_lo == rn_lo, so this
4872 // cannot be reordered to dodge the displacement change.
4873 emit_thumb_zero_fill(&mut bytes, rd_lo_bits);
4874
4875 Ok(bytes) // 46 bytes (48 when rd_lo >= R8 takes MOV.W)
4876 }
4877
4878 // I64ShrU: 64-bit logical shift right with branch for n<32 vs
4879 // n>=32. #1048: R12-only scratch, operands never written — see
4880 // the I64Shl comment for the full rationale.
4881 ArmOp::I64ShrU {
4882 rd_lo,
4883 rd_hi,
4884 rn_lo,
4885 rn_hi,
4886 rm_lo,
4887 rm_hi: _,
4888 } => {
4889 let rd_lo_bits = reg_to_bits(rd_lo);
4890 let rd_hi_bits = reg_to_bits(rd_hi);
4891 let rn_lo_bits = reg_to_bits(rn_lo);
4892 let rn_hi_bits = reg_to_bits(rn_hi);
4893 let rm_lo_bits = reg_to_bits(rm_lo);
4894 let r12: u32 = 12; // the only scratch — never allocatable
4895 let mut bytes = Vec::new();
4896
4897 // #1039 house style: rd_lo is written before rn_hi and rm_lo
4898 // are last read — refuse the collision loudly. The in-place
4899 // form rd == rn (select_default) has rd_lo == rn_lo and stays
4900 // legal.
4901 if rd_lo_bits == rn_hi_bits || rd_lo_bits == rm_lo_bits {
4902 return Err(synth_core::Error::synthesis(format!(
4903 "I64ShrU: rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4904 )));
4905 }
4906
4907 // AND.W R12, rm_lo, #63 (n — the amount register is only READ)
4908 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4909 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4910 bytes.extend_from_slice(&hw1.to_le_bytes());
4911 bytes.extend_from_slice(&hw2.to_le_bytes());
4912
4913 // SUBS.W R12, R12, #32 (R12 = n-32, sets flags)
4914 let hw1: u16 = (0xF1B0 | r12) as u16;
4915 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4916 bytes.extend_from_slice(&hw1.to_le_bytes());
4917 bytes.extend_from_slice(&hw2.to_le_bytes());
4918
4919 // BPL .large (+14 halfwords)
4920 let bpl: u16 = 0xD50E;
4921 bytes.extend_from_slice(&bpl.to_le_bytes());
4922
4923 // --- Small shift (n < 32) ---
4924 // AND.W R12, rm_lo, #63 (n again — R12 held n-32)
4925 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4926 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4927 bytes.extend_from_slice(&hw1.to_le_bytes());
4928 bytes.extend_from_slice(&hw2.to_le_bytes());
4929
4930 // LSR.W rd_lo, rn_lo, R12 (lo >> n; rn_lo's last read)
4931 let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4932 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4933 bytes.extend_from_slice(&hw1.to_le_bytes());
4934 bytes.extend_from_slice(&hw2.to_le_bytes());
4935
4936 // RSB.W R12, R12, #32 (R12 = 32-n)
4937 let hw1: u16 = (0xF1C0 | r12) as u16;
4938 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4939 bytes.extend_from_slice(&hw1.to_le_bytes());
4940 bytes.extend_from_slice(&hw2.to_le_bytes());
4941
4942 // LSL.W R12, rn_hi, R12 (overflow = hi << (32-n); n=0 gives
4943 // a register shift by 32 which yields 0 — exact)
4944 let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4945 let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4946 bytes.extend_from_slice(&hw1.to_le_bytes());
4947 bytes.extend_from_slice(&hw2.to_le_bytes());
4948
4949 // ORR.W rd_lo, rd_lo, R12 (lo |= overflow from hi)
4950 let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4951 let hw2: u16 = ((rd_lo_bits << 8) | r12) as u16;
4952 bytes.extend_from_slice(&hw1.to_le_bytes());
4953 bytes.extend_from_slice(&hw2.to_le_bytes());
4954
4955 // AND.W R12, rm_lo, #63 (n once more for the high half)
4956 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4957 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4958 bytes.extend_from_slice(&hw1.to_le_bytes());
4959 bytes.extend_from_slice(&hw2.to_le_bytes());
4960
4961 // LSR.W rd_hi, rn_hi, R12 (hi >> n, logical)
4962 let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4963 let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4964 bytes.extend_from_slice(&hw1.to_le_bytes());
4965 bytes.extend_from_slice(&hw2.to_le_bytes());
4966
4967 // B .done — see I64Shl: `.done` is the END of the expansion,
4968 // past the trailing zero-fill, so the displacement is derived
4969 // from that zero-fill's real width (#916).
4970 let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_hi_bits);
4971 let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4972 bytes.extend_from_slice(&b_done.to_le_bytes());
4973
4974 // --- Large shift (n >= 32) --- (R12 still holds n-32)
4975 // LSR.W rd_lo, rn_hi, R12 (lo = hi >> (n-32))
4976 let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4977 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4978 bytes.extend_from_slice(&hw1.to_le_bytes());
4979 bytes.extend_from_slice(&hw2.to_le_bytes());
4980
4981 // MOV rd_hi, #0 (#916: MOV.W for rd_hi >= R8). Order is
4982 // load-bearing: the LSR.W above reads rn_hi, which may BE
4983 // rd_hi in the in-place case.
4984 emit_thumb_zero_fill(&mut bytes, rd_hi_bits);
4985
4986 Ok(bytes) // 46 bytes (48 when rd_hi >= R8 takes MOV.W)
4987 }
4988
4989 // I64ShrS: 64-bit arithmetic shift right with branch for n<32 vs
4990 // n>=32. #1048: R12-only scratch, operands never written — see
4991 // the I64Shl comment for the full rationale.
4992 ArmOp::I64ShrS {
4993 rd_lo,
4994 rd_hi,
4995 rn_lo,
4996 rn_hi,
4997 rm_lo,
4998 rm_hi: _,
4999 } => {
5000 let rd_lo_bits = reg_to_bits(rd_lo);
5001 let rd_hi_bits = reg_to_bits(rd_hi);
5002 let rn_lo_bits = reg_to_bits(rn_lo);
5003 let rn_hi_bits = reg_to_bits(rn_hi);
5004 let rm_lo_bits = reg_to_bits(rm_lo);
5005 let r12: u32 = 12; // the only scratch — never allocatable
5006 let mut bytes = Vec::new();
5007
5008 // #1039 house style: rd_lo is written before rn_hi and rm_lo
5009 // are last read (on BOTH arms of the diamond — the large arm's
5010 // trailing `ASR rd_hi, rn_hi, #31` also reads rn_hi after
5011 // rd_lo is written). The in-place form rd == rn stays legal.
5012 if rd_lo_bits == rn_hi_bits || rd_lo_bits == rm_lo_bits {
5013 return Err(synth_core::Error::synthesis(format!(
5014 "I64ShrS: rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
5015 )));
5016 }
5017
5018 // AND.W R12, rm_lo, #63 (n — the amount register is only READ)
5019 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
5020 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
5021 bytes.extend_from_slice(&hw1.to_le_bytes());
5022 bytes.extend_from_slice(&hw2.to_le_bytes());
5023
5024 // SUBS.W R12, R12, #32 (R12 = n-32, sets flags)
5025 let hw1: u16 = (0xF1B0 | r12) as u16;
5026 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
5027 bytes.extend_from_slice(&hw1.to_le_bytes());
5028 bytes.extend_from_slice(&hw2.to_le_bytes());
5029
5030 // BPL .large (+14 halfwords)
5031 let bpl: u16 = 0xD50E;
5032 bytes.extend_from_slice(&bpl.to_le_bytes());
5033
5034 // --- Small shift (n < 32) ---
5035 // AND.W R12, rm_lo, #63 (n again — R12 held n-32)
5036 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
5037 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
5038 bytes.extend_from_slice(&hw1.to_le_bytes());
5039 bytes.extend_from_slice(&hw2.to_le_bytes());
5040
5041 // LSR.W rd_lo, rn_lo, R12 (lo >> n, logical for lo word)
5042 let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
5043 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
5044 bytes.extend_from_slice(&hw1.to_le_bytes());
5045 bytes.extend_from_slice(&hw2.to_le_bytes());
5046
5047 // RSB.W R12, R12, #32 (R12 = 32-n)
5048 let hw1: u16 = (0xF1C0 | r12) as u16;
5049 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
5050 bytes.extend_from_slice(&hw1.to_le_bytes());
5051 bytes.extend_from_slice(&hw2.to_le_bytes());
5052
5053 // LSL.W R12, rn_hi, R12 (overflow = hi << (32-n); n=0 gives
5054 // a register shift by 32 which yields 0 — exact)
5055 let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
5056 let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
5057 bytes.extend_from_slice(&hw1.to_le_bytes());
5058 bytes.extend_from_slice(&hw2.to_le_bytes());
5059
5060 // ORR.W rd_lo, rd_lo, R12 (lo |= overflow from hi)
5061 let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
5062 let hw2: u16 = ((rd_lo_bits << 8) | r12) as u16;
5063 bytes.extend_from_slice(&hw1.to_le_bytes());
5064 bytes.extend_from_slice(&hw2.to_le_bytes());
5065
5066 // AND.W R12, rm_lo, #63 (n once more for the high half)
5067 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
5068 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
5069 bytes.extend_from_slice(&hw1.to_le_bytes());
5070 bytes.extend_from_slice(&hw2.to_le_bytes());
5071
5072 // ASR.W rd_hi, rn_hi, R12 (hi >> n, arithmetic/sign-extending)
5073 let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
5074 let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
5075 bytes.extend_from_slice(&hw1.to_le_bytes());
5076 bytes.extend_from_slice(&hw2.to_le_bytes());
5077
5078 // B .done (+3 halfwords, large shift is 8 bytes)
5079 let b_done: u16 = 0xE003;
5080 bytes.extend_from_slice(&b_done.to_le_bytes());
5081
5082 // --- Large shift (n >= 32) --- (R12 still holds n-32)
5083 // ASR.W rd_lo, rn_hi, R12 (lo = hi >>> (n-32))
5084 let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
5085 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
5086 bytes.extend_from_slice(&hw1.to_le_bytes());
5087 bytes.extend_from_slice(&hw2.to_le_bytes());
5088
5089 // ASR.W rd_hi, rn_hi, #31 (hi = sign extension, all 0s or all 1s)
5090 // Thumb-2 ASR immediate: hw1=0xEA4F, hw2=imm3:Rd:imm2:10:Rm
5091 // imm5=31=11111 → imm3=111, imm2=11
5092 let hw1: u16 = 0xEA4F;
5093 let hw2: u16 = (0x7000 | (rd_hi_bits << 8) | 0x00E0 | rn_hi_bits) as u16;
5094 bytes.extend_from_slice(&hw1.to_le_bytes());
5095 bytes.extend_from_slice(&hw2.to_le_bytes());
5096
5097 Ok(bytes) // Total: 48 bytes
5098 }
5099
5100 // I64Rotl: 64-bit rotate left (#610 rewrite).
5101 // For n < 32: new_hi = (hi << n) | (lo >> (32-n)), new_lo = (lo << n) | (hi >> (32-n))
5102 // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
5103 //
5104 // Fixed-reg core: value in R0:R1, amount in R2, scratch R3 + R12
5105 // (all four saved/marshaled by the #610 fixed-ABI wrapper; the
5106 // pre-#610 expansion wrote through the selector's registers with
5107 // colliding R3/R4 scratch and restored the saved R4 OVER the
5108 // result). Relies on ARM register-shift semantics: amounts >= 32
5109 // yield 0 for LSL/LSR, which makes n = 0 and n = 32 exact.
5110 ArmOp::I64Rotl {
5111 rdlo,
5112 rdhi,
5113 rnlo,
5114 rnhi,
5115 shift,
5116 } => {
5117 let mut bytes = Vec::new();
5118 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
5119
5120 let core: [u16; 35] = [
5121 0xF002, 0x023F, // AND.W R2, R2, #63 (mask amount mod 64)
5122 0xF1B2, 0x0320, // SUBS.W R3, R2, #32 (R3 = n-32, sets N)
5123 0xD50E, // BPL .large (n >= 32)
5124 // --- small rotation (n < 32) ---
5125 0xF1C2, 0x0320, // RSB.W R3, R2, #32 (R3 = 32-n)
5126 0xFA20, 0xFC03, // LSR.W R12, R0, R3 (lo >> (32-n))
5127 0xFA21, 0xF303, // LSR.W R3, R1, R3 (hi >> (32-n))
5128 0xFA01, 0xF102, // LSL.W R1, R1, R2 (hi << n)
5129 0xEA41, 0x010C, // ORR.W R1, R1, R12 (new_hi)
5130 0xFA00, 0xF002, // LSL.W R0, R0, R2 (lo << n)
5131 0xEA40, 0x0003, // ORR.W R0, R0, R3 (new_lo)
5132 0xE00E, // B .done
5133 // --- large rotation (n >= 32), R3 = m = n-32 ---
5134 0xF1C3, 0x0220, // RSB.W R2, R3, #32 (R2 = 32-m = 64-n)
5135 0xFA21, 0xFC02, // LSR.W R12, R1, R2 (hi >> (64-n))
5136 0xFA20, 0xF202, // LSR.W R2, R0, R2 (lo >> (64-n))
5137 0xFA00, 0xF003, // LSL.W R0, R0, R3 (lo << m)
5138 0xFA01, 0xF103, // LSL.W R1, R1, R3 (hi << m)
5139 0xEA40, 0x0C0C, // ORR.W R12, R0, R12 (new_hi = (lo<<m)|(hi>>(64-n)))
5140 0xEA41, 0x0002, // ORR.W R0, R1, R2 (new_lo = (hi<<m)|(lo>>(64-n)))
5141 0x4661, // MOV R1, R12 (new_hi into place)
5142 // .done: result in R0:R1
5143 ];
5144 for hw in core {
5145 bytes.extend_from_slice(&hw.to_le_bytes());
5146 }
5147
5148 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5149 Ok(bytes) // Total: 102 bytes
5150 }
5151
5152 // I64Rotr: 64-bit rotate right (#610 rewrite).
5153 // For n < 32: new_lo = (lo >> n) | (hi << (32-n)), new_hi = (hi >> n) | (lo << (32-n))
5154 // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
5155 //
5156 // Same fixed-reg core contract as I64Rotl: value in R0:R1, amount
5157 // in R2, scratch R3 + R12, all covered by the fixed-ABI wrapper.
5158 ArmOp::I64Rotr {
5159 rdlo,
5160 rdhi,
5161 rnlo,
5162 rnhi,
5163 shift,
5164 } => {
5165 let mut bytes = Vec::new();
5166 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
5167
5168 let core: [u16; 35] = [
5169 0xF002, 0x023F, // AND.W R2, R2, #63 (mask amount mod 64)
5170 0xF1B2, 0x0320, // SUBS.W R3, R2, #32 (R3 = n-32, sets N)
5171 0xD50E, // BPL .large (n >= 32)
5172 // --- small rotation (n < 32) ---
5173 0xF1C2, 0x0320, // RSB.W R3, R2, #32 (R3 = 32-n)
5174 0xFA01, 0xFC03, // LSL.W R12, R1, R3 (hi << (32-n))
5175 0xFA00, 0xF303, // LSL.W R3, R0, R3 (lo << (32-n))
5176 0xFA20, 0xF002, // LSR.W R0, R0, R2 (lo >> n)
5177 0xEA40, 0x000C, // ORR.W R0, R0, R12 (new_lo)
5178 0xFA21, 0xF102, // LSR.W R1, R1, R2 (hi >> n)
5179 0xEA41, 0x0103, // ORR.W R1, R1, R3 (new_hi)
5180 0xE00E, // B .done
5181 // --- large rotation (n >= 32), R3 = m = n-32 ---
5182 0xF1C3, 0x0220, // RSB.W R2, R3, #32 (R2 = 32-m = 64-n)
5183 0xFA00, 0xFC02, // LSL.W R12, R0, R2 (lo << (64-n))
5184 0xFA01, 0xF202, // LSL.W R2, R1, R2 (hi << (64-n))
5185 0xFA21, 0xF103, // LSR.W R1, R1, R3 (hi >> m)
5186 0xEA41, 0x0C0C, // ORR.W R12, R1, R12 (new_lo = (hi>>m)|(lo<<(64-n)))
5187 0xFA20, 0xF103, // LSR.W R1, R0, R3 (lo >> m)
5188 0xEA41, 0x0102, // ORR.W R1, R1, R2 (new_hi = (lo>>m)|(hi<<(64-n)))
5189 0x4660, // MOV R0, R12 (new_lo into place)
5190 // .done: result in R0:R1
5191 ];
5192 for hw in core {
5193 bytes.extend_from_slice(&hw.to_le_bytes());
5194 }
5195
5196 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5197 Ok(bytes) // Total: 102 bytes
5198 }
5199
5200 // I64Clz: Count leading zeros in 64-bit value
5201 // If hi != 0: result = CLZ(hi)
5202 // If hi == 0: result = 32 + CLZ(lo)
5203 //
5204 // Layout (using CMP+BNE approach for consistency):
5205 // 0: CMP.W rnhi, #0 (4 bytes)
5206 // 4: BEQ .hi_zero (2 bytes) - branch forward to offset 14
5207 // 6: CLZ.W rd, rnhi (4 bytes)
5208 // 10: B .done (2 bytes) - branch forward to offset 22
5209 // 12: NOP (2 bytes) - padding for alignment
5210 // 14: .hi_zero: CLZ.W rd, rnlo (4 bytes)
5211 // 18: ADD.W rd, rd, #32 (4 bytes)
5212 // 22: .done
5213 ArmOp::I64Clz { rd, rnlo, rnhi } => {
5214 let rd_bits = reg_to_bits(rd);
5215 let rn_lo_bits = reg_to_bits(rnlo);
5216 let rn_hi_bits = reg_to_bits(rnhi);
5217 let mut bytes = Vec::new();
5218
5219 // CMP.W rnhi, #0 (4 bytes at offset 0)
5220 let hw1: u16 = (0xF1B0 | rn_hi_bits) as u16;
5221 let hw2: u16 = 0x0F00;
5222 bytes.extend_from_slice(&hw1.to_le_bytes());
5223 bytes.extend_from_slice(&hw2.to_le_bytes());
5224
5225 // BEQ .hi_zero (2 bytes at offset 4)
5226 // PC = 4 + 4 = 8, target = 14, offset = 6, imm8 = 3
5227 let beq: u16 = 0xD003;
5228 bytes.extend_from_slice(&beq.to_le_bytes());
5229
5230 // CLZ.W rd, rnhi (4 bytes at offset 6)
5231 // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5232 let hw1: u16 = (0xFAB0 | rn_hi_bits) as u16;
5233 let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_hi_bits) as u16;
5234 bytes.extend_from_slice(&hw1.to_le_bytes());
5235 bytes.extend_from_slice(&hw2.to_le_bytes());
5236
5237 // B .done (2 bytes at offset 10)
5238 // PC = 10 + 4 = 14, target = 22, offset = 8, imm11 = 4
5239 let b_done: u16 = 0xE004;
5240 bytes.extend_from_slice(&b_done.to_le_bytes());
5241
5242 // NOP (2 bytes at offset 12) - padding
5243 bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5244
5245 // .hi_zero: (offset 14)
5246 // CLZ.W rd, rnlo (4 bytes)
5247 // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5248 let hw1: u16 = (0xFAB0 | rn_lo_bits) as u16;
5249 let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_lo_bits) as u16;
5250 bytes.extend_from_slice(&hw1.to_le_bytes());
5251 bytes.extend_from_slice(&hw2.to_le_bytes());
5252
5253 // ADD.W rd, rd, #32 (4 bytes at offset 18)
5254 let hw1: u16 = (0xF100 | rd_bits) as u16;
5255 let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5256 bytes.extend_from_slice(&hw1.to_le_bytes());
5257 bytes.extend_from_slice(&hw2.to_le_bytes());
5258
5259 // .done: (offset 22 — the end of the expansion)
5260 //
5261 // #1048: the former trailing hi-word clear (`MOV rnhi, #0`)
5262 // is GONE. It was aimed at the RESULT's high half but wrote
5263 // the OPERAND's home high register — a real executed
5264 // miscompile on the direct selector, which allocates a fresh
5265 // destination pair and zeroes its own dst_hi, leaving the
5266 // operand's hi limb destroyed for any later re-read. The
5267 // Rocq/SMT models of I64ClzPseudo always said "writes rd
5268 // ONLY"; the callers that relied on the implicit clear
5269 // (select_default, optimizer_bridge) now emit their own
5270 // explicit hi-zero op. `B .done` above targets offset 22 =
5271 // past-the-end, `BEQ` targets offset 14 — no displacement
5272 // moves.
5273
5274 Ok(bytes) // 22 bytes, register-independent
5275 }
5276
5277 // I64Ctz: Count trailing zeros in 64-bit value
5278 // If lo != 0: result = CTZ(lo) = CLZ(RBIT(lo))
5279 // If lo == 0: result = 32 + CTZ(hi) = 32 + CLZ(RBIT(hi))
5280 //
5281 // Layout:
5282 // 0: CMP.W rnlo, #0 (4 bytes)
5283 // 4: BEQ .lo_zero (2 bytes) - branch to offset 18
5284 // 6: RBIT.W rd, rnlo (4 bytes)
5285 // 10: CLZ.W rd, rd (4 bytes)
5286 // 14: B .done (2 bytes) - branch to offset 30
5287 // 16: NOP (2 bytes) - padding
5288 // 18: .lo_zero: RBIT.W rd, rnhi (4 bytes)
5289 // 22: CLZ.W rd, rd (4 bytes)
5290 // 26: ADD.W rd, rd, #32 (4 bytes)
5291 // 30: .done
5292 ArmOp::I64Ctz { rd, rnlo, rnhi } => {
5293 let rd_bits = reg_to_bits(rd);
5294 let rn_lo_bits = reg_to_bits(rnlo);
5295 let rn_hi_bits = reg_to_bits(rnhi);
5296 let mut bytes = Vec::new();
5297
5298 // CMP.W rnlo, #0 (4 bytes at offset 0)
5299 let hw1: u16 = (0xF1B0 | rn_lo_bits) as u16;
5300 let hw2: u16 = 0x0F00;
5301 bytes.extend_from_slice(&hw1.to_le_bytes());
5302 bytes.extend_from_slice(&hw2.to_le_bytes());
5303
5304 // BEQ .lo_zero (2 bytes at offset 4)
5305 // PC = 4 + 4 = 8, target = 18, offset = 10, imm8 = 5
5306 let beq: u16 = 0xD005;
5307 bytes.extend_from_slice(&beq.to_le_bytes());
5308
5309 // RBIT.W rd, rnlo (4 bytes at offset 6)
5310 // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5311 let hw1: u16 = (0xFA90 | rn_lo_bits) as u16;
5312 let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_lo_bits) as u16;
5313 bytes.extend_from_slice(&hw1.to_le_bytes());
5314 bytes.extend_from_slice(&hw2.to_le_bytes());
5315
5316 // CLZ.W rd, rd (4 bytes at offset 10)
5317 // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5318 let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5319 let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5320 bytes.extend_from_slice(&hw1.to_le_bytes());
5321 bytes.extend_from_slice(&hw2.to_le_bytes());
5322
5323 // B .done (2 bytes at offset 14)
5324 // PC = 14 + 4 = 18, target = 30, offset = 12, imm11 = 6
5325 let b_done: u16 = 0xE006;
5326 bytes.extend_from_slice(&b_done.to_le_bytes());
5327
5328 // NOP (2 bytes at offset 16) - padding
5329 bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5330
5331 // .lo_zero: (offset 18)
5332 // RBIT.W rd, rnhi (4 bytes)
5333 // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5334 let hw1: u16 = (0xFA90 | rn_hi_bits) as u16;
5335 let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_hi_bits) as u16;
5336 bytes.extend_from_slice(&hw1.to_le_bytes());
5337 bytes.extend_from_slice(&hw2.to_le_bytes());
5338
5339 // CLZ.W rd, rd (4 bytes at offset 22)
5340 // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5341 let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5342 let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5343 bytes.extend_from_slice(&hw1.to_le_bytes());
5344 bytes.extend_from_slice(&hw2.to_le_bytes());
5345
5346 // ADD.W rd, rd, #32 (4 bytes at offset 26)
5347 let hw1: u16 = (0xF100 | rd_bits) as u16;
5348 let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5349 bytes.extend_from_slice(&hw1.to_le_bytes());
5350 bytes.extend_from_slice(&hw2.to_le_bytes());
5351
5352 // .done: (offset 30 — the end of the expansion)
5353 // #1048: the former trailing `MOV rnhi, #0` is GONE — it
5354 // wrote the OPERAND's home high register (see the I64Clz
5355 // comment above). `B .done` targets offset 30 = past-the-end,
5356 // `BEQ` targets offset 18 — no displacement moves.
5357
5358 Ok(bytes) // 30 bytes, register-independent
5359 }
5360
5361 // I64Popcnt: Population count of 64-bit value
5362 // result = POPCNT(lo) + POPCNT(hi)
5363 // Using SIMD-style parallel bit counting algorithm
5364 ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
5365 let rd_bits = reg_to_bits(rd);
5366 let rn_lo_bits = reg_to_bits(rnlo);
5367 let rn_hi_bits = reg_to_bits(rnhi);
5368 let r12: u32 = 12; // IP scratch
5369 let r3: u32 = 3; // Scratch for hi popcnt result
5370 let mut bytes = Vec::new();
5371
5372 // PUSH {R3, R4, R5} - save scratch registers
5373 bytes.extend_from_slice(&0xB438u16.to_le_bytes());
5374
5375 // Strategy: compute popcnt(lo) -> R4, popcnt(hi) -> R5, add them -> rd
5376 // Using lookup table approach for each byte would be too large
5377 // Using shift-and-add approach instead
5378
5379 // For simplicity and correctness, use the efficient parallel algorithm
5380 // but implement it as a series of inline operations
5381
5382 // Marshal the operand pair into the fixed scratch regs, routing
5383 // rnlo through R12 (#632 audit): writing R4 first corrupted the
5384 // rnhi read for a pair living at (R3,R4) — every source is read
5385 // before any scratch register it could occupy is written.
5386 // MOV R12, rnlo
5387 let mov: u16 = (0x4600 | (1 << 7) | (rn_lo_bits << 3) | 4) as u16;
5388 bytes.extend_from_slice(&mov.to_le_bytes());
5389 // MOV R5, rnhi (R4 untouched so far; rnhi == R5 is a no-op)
5390 let mov: u16 = (0x4600 | (rn_hi_bits << 3) | 5) as u16;
5391 bytes.extend_from_slice(&mov.to_le_bytes());
5392 // MOV R4, R12
5393 bytes.extend_from_slice(&0x4664u16.to_le_bytes());
5394
5395 // --- POPCNT for R4 (lo word) ---
5396 // Step 1: x = x - ((x >> 1) & 0x55555555)
5397 // LSR.W R12, R4, #1
5398 let hw1: u16 = 0xEA4F;
5399 let hw2: u16 = ((r12 << 8) | 0x50 | 4) as u16;
5400 bytes.extend_from_slice(&hw1.to_le_bytes());
5401 bytes.extend_from_slice(&hw2.to_le_bytes());
5402
5403 // Load 0x55555555 into R3 using MOVW/MOVT
5404 // MOVW R3, #0x5555
5405 bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5406 bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5407 // MOVT R3, #0x5555
5408 bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5409 bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5410
5411 // AND.W R12, R12, R3
5412 let hw1: u16 = (0xEA00 | r12) as u16;
5413 let hw2: u16 = ((r12 << 8) | r3) as u16;
5414 bytes.extend_from_slice(&hw1.to_le_bytes());
5415 bytes.extend_from_slice(&hw2.to_le_bytes());
5416
5417 // SUB.W R4, R4, R12
5418 let hw1: u16 = (0xEBA0 | 4) as u16;
5419 let hw2: u16 = ((4 << 8) | r12) as u16;
5420 bytes.extend_from_slice(&hw1.to_le_bytes());
5421 bytes.extend_from_slice(&hw2.to_le_bytes());
5422
5423 // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5424 // Load 0x33333333 into R3
5425 // MOVW R3, #0x3333
5426 bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5427 bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5428 // MOVT R3, #0x3333
5429 bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5430 bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5431
5432 // AND.W R12, R4, R3
5433 let hw1: u16 = (0xEA00 | 4) as u16;
5434 let hw2: u16 = ((r12 << 8) | r3) as u16;
5435 bytes.extend_from_slice(&hw1.to_le_bytes());
5436 bytes.extend_from_slice(&hw2.to_le_bytes());
5437
5438 // LSR.W R4, R4, #2
5439 let hw1: u16 = 0xEA4F;
5440 let hw2: u16 = ((4 << 8) | 0x90 | 4) as u16;
5441 bytes.extend_from_slice(&hw1.to_le_bytes());
5442 bytes.extend_from_slice(&hw2.to_le_bytes());
5443
5444 // AND.W R4, R4, R3
5445 let hw1: u16 = (0xEA00 | 4) as u16;
5446 let hw2: u16 = ((4 << 8) | r3) as u16;
5447 bytes.extend_from_slice(&hw1.to_le_bytes());
5448 bytes.extend_from_slice(&hw2.to_le_bytes());
5449
5450 // ADD.W R4, R4, R12
5451 let hw1: u16 = (0xEB00 | 4) as u16;
5452 let hw2: u16 = ((4 << 8) | r12) as u16;
5453 bytes.extend_from_slice(&hw1.to_le_bytes());
5454 bytes.extend_from_slice(&hw2.to_le_bytes());
5455
5456 // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5457 // LSR.W R12, R4, #4
5458 // hw2 = (imm3 << 12) | (Rd << 8) | (imm2 << 6) | (type << 4) | Rm
5459 // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5460 let hw1: u16 = 0xEA4F;
5461 let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 4) as u16;
5462 bytes.extend_from_slice(&hw1.to_le_bytes());
5463 bytes.extend_from_slice(&hw2.to_le_bytes());
5464
5465 // ADD.W R4, R4, R12
5466 let hw1: u16 = (0xEB00 | 4) as u16;
5467 let hw2: u16 = ((4 << 8) | r12) as u16;
5468 bytes.extend_from_slice(&hw1.to_le_bytes());
5469 bytes.extend_from_slice(&hw2.to_le_bytes());
5470
5471 // Load 0x0F0F0F0F into R3
5472 // MOVW R3, #0x0F0F (imm4=0, i=1, imm3=7, imm8=0x0F)
5473 // hw1 = 11110 1 10 0100 0000 = 0xF640
5474 // hw2 = 0 111 0011 00001111 = 0x730F
5475 bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5476 bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5477 // MOVT R3, #0x0F0F
5478 bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5479 bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5480
5481 // AND.W R4, R4, R3
5482 let hw1: u16 = (0xEA00 | 4) as u16;
5483 let hw2: u16 = ((4 << 8) | r3) as u16;
5484 bytes.extend_from_slice(&hw1.to_le_bytes());
5485 bytes.extend_from_slice(&hw2.to_le_bytes());
5486
5487 // Step 4: x = x * 0x01010101 >> 24
5488 // Load 0x01010101 into R3
5489 // MOVW R3, #0x0101
5490 bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5491 bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5492 // MOVT R3, #0x0101
5493 bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5494 bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5495
5496 // MUL R4, R4, R3
5497 // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5498 let hw1: u16 = (0xFB00 | 4) as u16;
5499 let hw2: u16 = (0xF000 | (4 << 8) | r3) as u16;
5500 bytes.extend_from_slice(&hw1.to_le_bytes());
5501 bytes.extend_from_slice(&hw2.to_le_bytes());
5502
5503 // LSR.W R4, R4, #24
5504 // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5505 let hw1: u16 = 0xEA4F;
5506 let hw2: u16 = (0x6000 | (4 << 8) | 0x10 | 4) as u16;
5507 bytes.extend_from_slice(&hw1.to_le_bytes());
5508 bytes.extend_from_slice(&hw2.to_le_bytes());
5509
5510 // --- POPCNT for R5 (hi word) - same algorithm ---
5511 // Step 1
5512 let hw1: u16 = 0xEA4F;
5513 let hw2: u16 = ((r12 << 8) | 0x50 | 5) as u16;
5514 bytes.extend_from_slice(&hw1.to_le_bytes());
5515 bytes.extend_from_slice(&hw2.to_le_bytes());
5516
5517 // Load 0x55555555 into R3
5518 bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5519 bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5520 bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5521 bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5522
5523 let hw1: u16 = (0xEA00 | r12) as u16;
5524 let hw2: u16 = ((r12 << 8) | r3) as u16;
5525 bytes.extend_from_slice(&hw1.to_le_bytes());
5526 bytes.extend_from_slice(&hw2.to_le_bytes());
5527
5528 let hw1: u16 = (0xEBA0 | 5) as u16;
5529 let hw2: u16 = ((5 << 8) | r12) as u16;
5530 bytes.extend_from_slice(&hw1.to_le_bytes());
5531 bytes.extend_from_slice(&hw2.to_le_bytes());
5532
5533 // Step 2
5534 bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5535 bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5536 bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5537 bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5538
5539 let hw1: u16 = (0xEA00 | 5) as u16;
5540 let hw2: u16 = ((r12 << 8) | r3) as u16;
5541 bytes.extend_from_slice(&hw1.to_le_bytes());
5542 bytes.extend_from_slice(&hw2.to_le_bytes());
5543
5544 let hw1: u16 = 0xEA4F;
5545 let hw2: u16 = ((5 << 8) | 0x90 | 5) as u16;
5546 bytes.extend_from_slice(&hw1.to_le_bytes());
5547 bytes.extend_from_slice(&hw2.to_le_bytes());
5548
5549 let hw1: u16 = (0xEA00 | 5) as u16;
5550 let hw2: u16 = ((5 << 8) | r3) as u16;
5551 bytes.extend_from_slice(&hw1.to_le_bytes());
5552 bytes.extend_from_slice(&hw2.to_le_bytes());
5553
5554 let hw1: u16 = (0xEB00 | 5) as u16;
5555 let hw2: u16 = ((5 << 8) | r12) as u16;
5556 bytes.extend_from_slice(&hw1.to_le_bytes());
5557 bytes.extend_from_slice(&hw2.to_le_bytes());
5558
5559 // Step 3: LSR.W R12, R5, #4
5560 // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5561 let hw1: u16 = 0xEA4F;
5562 let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 5) as u16;
5563 bytes.extend_from_slice(&hw1.to_le_bytes());
5564 bytes.extend_from_slice(&hw2.to_le_bytes());
5565
5566 let hw1: u16 = (0xEB00 | 5) as u16;
5567 let hw2: u16 = ((5 << 8) | r12) as u16;
5568 bytes.extend_from_slice(&hw1.to_le_bytes());
5569 bytes.extend_from_slice(&hw2.to_le_bytes());
5570
5571 // Load 0x0F0F0F0F into R3 (for hi-word)
5572 bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5573 bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5574 bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5575 bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5576
5577 let hw1: u16 = (0xEA00 | 5) as u16;
5578 let hw2: u16 = ((5 << 8) | r3) as u16;
5579 bytes.extend_from_slice(&hw1.to_le_bytes());
5580 bytes.extend_from_slice(&hw2.to_le_bytes());
5581
5582 // Step 4
5583 bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5584 bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5585 bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5586 bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5587
5588 // MUL R5, R5, R3
5589 // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5590 let hw1: u16 = (0xFB00 | 5) as u16;
5591 let hw2: u16 = (0xF000 | (5 << 8) | r3) as u16;
5592 bytes.extend_from_slice(&hw1.to_le_bytes());
5593 bytes.extend_from_slice(&hw2.to_le_bytes());
5594
5595 // LSR.W R5, R5, #24
5596 // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5597 let hw1: u16 = 0xEA4F;
5598 let hw2: u16 = (0x6000 | (5 << 8) | 0x10 | 5) as u16;
5599 bytes.extend_from_slice(&hw1.to_le_bytes());
5600 bytes.extend_from_slice(&hw2.to_le_bytes());
5601
5602 // #632: the count must be carried ACROSS the scratch restore
5603 // in a register the POP cannot touch. rd is allocator-assigned
5604 // (any of R0-R8) and can land inside the {R3,R4,R5} restore set
5605 // — the old `ADDS rd, R4, R5; POP {R3,R4,R5}` destroyed the
5606 // result one instruction after computing it (0 for every input
5607 // under qemu). R12 is encoder scratch: never allocatable (#212)
5608 // and never in a restore set, so no choice of rd can collide.
5609 // ADD.W R12, R4, R5
5610 bytes.extend_from_slice(&0xEB04u16.to_le_bytes());
5611 bytes.extend_from_slice(&0x0C05u16.to_le_bytes());
5612
5613 // POP {R3, R4, R5}
5614 bytes.extend_from_slice(&0xBC38u16.to_le_bytes());
5615
5616 // MOV rd, R12 — after the restore. The 4-bit Rd (D:rd) form is
5617 // also total over rd = R8, where the old ADDS T1 3-bit field
5618 // silently corrupted the encoding (#178/#180 class).
5619 let mov: u16 =
5620 (0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7)) as u16;
5621 bytes.extend_from_slice(&mov.to_le_bytes());
5622
5623 // #1048: the former trailing `MOV.W rnhi, #0` hi-word clear
5624 // is GONE — it wrote the OPERAND's home high register (see
5625 // the I64Clz comment). Callers that relied on the implicit
5626 // clear emit their own explicit hi-zero op.
5627
5628 Ok(bytes)
5629 }
5630
5631 // I64Extend8S: Sign-extend low 8 bits to 64 bits
5632 // Result: rdlo = sign_extend_8(rnlo), rdhi = rdlo >> 31
5633 ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
5634 let rdlo_bits = reg_to_bits(rdlo);
5635 let rdhi_bits = reg_to_bits(rdhi);
5636 let rnlo_bits = reg_to_bits(rnlo);
5637 let mut bytes = Vec::new();
5638
5639 // SXTB.W rdlo, rnlo (sign-extend byte to 32-bit)
5640 // SXTB T2: hw1 = 0xFA4F, hw2 = 0xF0<Rd><Rm>
5641 let hw1: u16 = 0xFA4F_u16;
5642 let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5643 bytes.extend_from_slice(&hw1.to_le_bytes());
5644 bytes.extend_from_slice(&hw2.to_le_bytes());
5645
5646 // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5647 // ASR (immediate): hw1 = 0xEA4F, hw2 = imm3:Rd:imm2:type:Rm
5648 // For imm5=31: imm3=111, imm2=11, type=10 (ASR)
5649 // hw2 = (7 << 12) | (rdhi << 8) | (3 << 6) | (2 << 4) | rdlo
5650 let hw1: u16 = 0xEA4F;
5651 let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5652 bytes.extend_from_slice(&hw1.to_le_bytes());
5653 bytes.extend_from_slice(&hw2.to_le_bytes());
5654
5655 Ok(bytes)
5656 }
5657
5658 // I64Extend16S: Sign-extend low 16 bits to 64 bits
5659 // Result: rdlo = sign_extend_16(rnlo), rdhi = rdlo >> 31
5660 ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
5661 let rdlo_bits = reg_to_bits(rdlo);
5662 let rdhi_bits = reg_to_bits(rdhi);
5663 let rnlo_bits = reg_to_bits(rnlo);
5664 let mut bytes = Vec::new();
5665
5666 // SXTH.W rdlo, rnlo (sign-extend halfword to 32-bit)
5667 // SXTH T2: hw1 = 0xFA0F, hw2 = 0xF0<Rd><Rm>
5668 let hw1: u16 = 0xFA0F_u16;
5669 let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5670 bytes.extend_from_slice(&hw1.to_le_bytes());
5671 bytes.extend_from_slice(&hw2.to_le_bytes());
5672
5673 // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5674 let hw1: u16 = 0xEA4F;
5675 let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5676 bytes.extend_from_slice(&hw1.to_le_bytes());
5677 bytes.extend_from_slice(&hw2.to_le_bytes());
5678
5679 Ok(bytes)
5680 }
5681
5682 // I64Extend32S: Sign-extend low 32 bits to 64 bits
5683 // Result: rdlo = rnlo, rdhi = rnlo >> 31
5684 ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
5685 let rdlo_bits = reg_to_bits(rdlo);
5686 let rdhi_bits = reg_to_bits(rdhi);
5687 let rnlo_bits = reg_to_bits(rnlo);
5688 let mut bytes = Vec::new();
5689
5690 // MOV rdlo, rnlo (if different)
5691 if rdlo_bits != rnlo_bits {
5692 // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5693 let d_bit = ((rdlo_bits >> 3) & 1) as u16;
5694 let mov: u16 = 0x4600
5695 | (d_bit << 7)
5696 | ((rnlo_bits as u16) << 3)
5697 | ((rdlo_bits & 0x7) as u16);
5698 bytes.extend_from_slice(&mov.to_le_bytes());
5699 }
5700
5701 // ASR.W rdhi, rnlo, #31 (sign-extend to high word)
5702 let hw1: u16 = 0xEA4F;
5703 let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rnlo_bits) as u16;
5704 bytes.extend_from_slice(&hw1.to_le_bytes());
5705 bytes.extend_from_slice(&hw2.to_le_bytes());
5706
5707 Ok(bytes)
5708 }
5709
5710 // SelectMove: IT <cond>; MOV{cond} rd, rm
5711 // Conditional move: only execute MOV if condition is true
5712 ArmOp::SelectMove { rd, rm, cond } => {
5713 let rd_bits = reg_to_bits(rd) as u16;
5714 let rm_bits = reg_to_bits(rm) as u16;
5715
5716 // Condition code encoding for IT block
5717 use synth_synthesis::Condition;
5718 let cond_bits: u16 = match cond {
5719 Condition::EQ => 0x0, // Equal
5720 Condition::NE => 0x1, // Not equal
5721 Condition::HS => 0x2, // Higher or same (unsigned >=)
5722 Condition::LO => 0x3, // Lower (unsigned <)
5723 Condition::HI => 0x8, // Higher (unsigned >)
5724 Condition::LS => 0x9, // Lower or same (unsigned <=)
5725 Condition::GE => 0xA, // Greater or equal (signed)
5726 Condition::LT => 0xB, // Less than (signed)
5727 Condition::GT => 0xC, // Greater than (signed)
5728 Condition::LE => 0xD, // Less or equal (signed)
5729 };
5730
5731 // IT <cond>: single Then block (mask = 0x8 for T only)
5732 // IT instruction: 1011 1111 firstcond mask
5733 let it_instr: u16 = 0xBF00 | (cond_bits << 4) | 0x8;
5734
5735 // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5736 // This MOV will only execute if condition is true due to IT block
5737 let d_bit = (rd_bits >> 3) & 1;
5738 let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5739
5740 // Emit: IT <cond>, MOV rd, rm
5741 let mut bytes = it_instr.to_le_bytes().to_vec();
5742 bytes.extend_from_slice(&mov_instr.to_le_bytes());
5743 Ok(bytes)
5744 }
5745
5746 // Popcnt: Population count (count set bits)
5747 // ARM Cortex-M has no native POPCNT, so we implement the bit manipulation algorithm:
5748 // x = x - ((x >> 1) & 0x55555555);
5749 // x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
5750 // x = (x + (x >> 4)) & 0x0F0F0F0F;
5751 // x = x + (x >> 8);
5752 // x = x + (x >> 16);
5753 // return x & 0x3F;
5754 //
5755 // #1021: R12 (IP, never allocatable) is the ONLY scratch. The
5756 // previous expansion borrowed R11 as a second temp — but R11 is
5757 // the WASM linear-memory base, materialized at entry and read by
5758 // every later LDR/STR, and it is NOT in the pushed set, so the
5759 // clobber leaked to the CALLER too (a live memory-safety
5760 // miscompile: loads through `base = x >> 16`). The second temp is
5761 // eliminated the way the healthy i64.popcnt discipline implies —
5762 // never touch an unsaved register — but without its PUSH/POP
5763 // wrapper: the SWAR masks 0x55555555 / 0x33333333 / 0x0F0F0F0F
5764 // are all `0xXYXYXYXY` ThumbExpandImm modified immediates, so
5765 // each AND takes its mask from the instruction itself and R12
5766 // alone carries every intermediate. Straight-line, no branches,
5767 // no stack traffic — nothing to skip on a trap edge.
5768 ArmOp::Popcnt { rd, rm } => {
5769 let rd_bits = reg_to_bits(rd);
5770 // Defensive (#1021): rd = R11/R12/SP/PC would silently
5771 // corrupt the linear-memory base, the expansion's own
5772 // scratch, or the stack. The selector never assigns them
5773 // (pool R0-R8); refuse loudly if that ever changes.
5774 if rd_bits >= 11 {
5775 return Err(synth_core::Error::synthesis(
5776 "Popcnt destination must be R0-R10: R11 is the linear-memory \
5777 base and R12 is the expansion's scratch (#1021)",
5778 ));
5779 }
5780 let mut bytes = Vec::new();
5781
5782 // First, move rm to rd if they're different
5783 if rd != rm {
5784 let rm_bits = reg_to_bits(rm) as u16;
5785 // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5786 let d_bit = ((rd_bits as u16) >> 3) & 1;
5787 let mov_instr: u16 =
5788 0x4600 | (d_bit << 7) | (rm_bits << 3) | ((rd_bits as u16) & 0x7);
5789 bytes.extend_from_slice(&mov_instr.to_le_bytes());
5790 }
5791
5792 // Step 1: x = x - ((x >> 1) & 0x55555555)
5793 // R12 = rd >> 1
5794 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 1)?);
5795 // R12 = R12 & 0x55555555 (modified immediate, no constant reg)
5796 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(12, 12, 0x5555_5555)?);
5797 // rd = rd - R12
5798 bytes.extend_from_slice(&self.encode_thumb32_sub_reg_raw(rd_bits, rd_bits, 12)?);
5799
5800 // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5801 // R12 = rd & 0x33333333
5802 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5803 12,
5804 rd_bits,
5805 0x3333_3333,
5806 )?);
5807 // rd = rd >> 2
5808 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(rd_bits, rd_bits, 2)?);
5809 // rd = rd & 0x33333333
5810 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5811 rd_bits,
5812 rd_bits,
5813 0x3333_3333,
5814 )?);
5815 // rd = rd + R12
5816 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5817
5818 // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5819 // R12 = rd >> 4
5820 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 4)?);
5821 // rd = rd + R12
5822 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5823 // rd = rd & 0x0F0F0F0F
5824 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5825 rd_bits,
5826 rd_bits,
5827 0x0F0F_0F0F,
5828 )?);
5829
5830 // Step 4: x = x + (x >> 8)
5831 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 8)?);
5832 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5833
5834 // Step 5: x = x + (x >> 16)
5835 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 16)?);
5836 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5837
5838 // Step 6: return x & 0x3F
5839 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(rd_bits, rd_bits, 0x3F)?);
5840
5841 Ok(bytes)
5842 }
5843
5844 // I64DivU: 64-bit unsigned division using binary long division
5845 // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = quotient
5846 // Uses: R4-R7, R12 as loop counter (avoid R8 for Renode compatibility)
5847 //
5848 // #610: the fixed-ABI wrapper marshals the selector-assigned
5849 // operand registers into the core's fixed regs and lands the
5850 // result in rd — pre-#610 this arm IGNORED its register fields,
5851 // so the selector read its rd pair (e.g. R4:R5) after the core's
5852 // own POP restored the stale caller values over it: 0 for every
5853 // input. A zero divisor now traps (UDF #0), per WASM semantics.
5854 ArmOp::I64DivU {
5855 rdlo,
5856 rdhi,
5857 rnlo,
5858 rnhi,
5859 rmlo,
5860 rmhi,
5861 elide_zero_guard,
5862 } => {
5863 let mut bytes = Vec::new();
5864 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5865 // #494 phase 2b: elided only under a certificate-discharged
5866 // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
5867 if !elide_zero_guard {
5868 emit_i64_divisor_zero_trap(&mut bytes);
5869 }
5870
5871 // PUSH {R4-R7} - save scratch registers (NO LR — this is inline code)
5872 // 16-bit PUSH: 1011 010 M rrrrrrrr where M=0 (no LR), r=R4-R7 = 0xF0
5873 // Encoding: 1011 0100 1111 0000 = 0xB4F0
5874 bytes.extend_from_slice(&0xB4F0u16.to_le_bytes());
5875
5876 // Initialize quotient (R4:R5) = 0
5877 bytes.extend_from_slice(&0x2400u16.to_le_bytes()); // MOV R4, #0
5878 bytes.extend_from_slice(&0x2500u16.to_le_bytes()); // MOV R5, #0
5879
5880 // Initialize remainder (R6:R7) = 0
5881 bytes.extend_from_slice(&0x2600u16.to_le_bytes()); // MOV R6, #0
5882 bytes.extend_from_slice(&0x2700u16.to_le_bytes()); // MOV R7, #0
5883
5884 // Initialize loop counter R12 = 64 (use R12 scratch instead of R8)
5885 // MOV.W R12, #64: F04F 0C40
5886 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5887 bytes.extend_from_slice(&0x0C40u16.to_le_bytes());
5888
5889 // Loop start
5890 let loop_start = bytes.len();
5891
5892 // === Loop body: process one bit ===
5893
5894 // 1. Shift quotient R4:R5 left by 1
5895 // LSLS R5, R5, #1 (16-bit: 0000 0010 1010 1101 = 0x006D -> actually 0x002D for LSL R5,R5,#1)
5896 // LSL Rd, Rm, #imm5: 000 00 imm5 Rm Rd = 000 00 00001 101 101 = 0x006D
5897 bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5898 // Get carry from R4 into R5: ORR R5, R5, R4 LSR #31
5899 // Thumb-2 ORR with shifted register: EA45 75D4 = ORR.W R5, R5, R4, LSR #31
5900 // 11101010 010 S Rn | 0 imm3 Rd imm2 type Rm
5901 // type=01 (LSR), imm5=31 (imm3=111, imm2=11)
5902 bytes.extend_from_slice(&0xEA45u16.to_le_bytes());
5903 bytes.extend_from_slice(&0x75D4u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5904 // LSLS R4, R4, #1: 000 00 00001 100 100 = 0x0064
5905 bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5906
5907 // 2. Shift remainder R6:R7 left by 1, OR in MSB of dividend R1
5908 // LSLS R7, R7, #1
5909 bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5910 // ORR.W R7, R7, R6, LSR #31
5911 bytes.extend_from_slice(&0xEA47u16.to_le_bytes());
5912 bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5913 // LSLS R6, R6, #1
5914 bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5915 // ORR.W R6, R6, R1, LSR #31 (bring in MSB of dividend high)
5916 bytes.extend_from_slice(&0xEA46u16.to_le_bytes());
5917 bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5918
5919 // 3. Shift dividend R0:R1 left by 1
5920 // LSLS R1, R1, #1
5921 bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5922 // ORR.W R1, R1, R0, LSR #31
5923 bytes.extend_from_slice(&0xEA41u16.to_le_bytes());
5924 bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5925 // LSLS R0, R0, #1
5926 bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5927
5928 // 4. Compare remainder >= divisor (64-bit unsigned comparison)
5929 // Compare high words first: CMP R7, R3
5930 // CMP Rn, Rm encoding: 0x4280 | (Rm << 3) | Rn
5931 bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3 (16-bit)
5932 // BHI means R7 > R3 (unsigned) - definitely subtract
5933 // BLO means R7 < R3 - definitely don't subtract
5934 // BEQ means need to check low words
5935
5936 // If high > divisor high: branch to subtract (forward +offset)
5937 // BHI.N +6 (skip CMP, skip BLO, do subtract)
5938 // BHI: 1101 1000 offset8 where cond=1000 (HI)
5939 bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4 (to subtract block)
5940
5941 // If high < divisor high: branch past subtract
5942 // BLO.N +10 (skip to decrement)
5943 bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BLO/BCC +12 (past subtract)
5944
5945 // High words equal, compare low: CMP R6, R2
5946 bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2 (16-bit)
5947 // BLO/BCC past subtract (skip SUBS+SBC.W+ORR.W = 10 bytes = 4 halfwords from PC+4)
5948 bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords (past subtract)
5949
5950 // === Subtract block: remainder -= divisor, quotient |= 1 ===
5951 // SUBS R6, R6, R2
5952 bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2 (16-bit)
5953 // SBC R7, R7, R3 (with borrow)
5954 // Thumb-2 SBC.W: EB67 0703 = SBC.W R7, R7, R3
5955 bytes.extend_from_slice(&0xEB67u16.to_le_bytes());
5956 bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5957 // ORR R4, R4, #1 (set bit 0 of quotient low)
5958 bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5959 bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5960
5961 // === Decrement counter and loop ===
5962 // SUBS.W R12, R12, #1 (decrement loop counter)
5963 // SUBS.W R12, R12, #1: F1BC 0C01
5964 bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
5965 bytes.extend_from_slice(&0x0C01u16.to_le_bytes());
5966
5967 // BNE back to loop_start
5968 let branch_offset_bytes = bytes.len() - loop_start + 4; // +4 for pipeline
5969 let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5970 let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5971 bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5972
5973 // === Loop done, move quotient to R0:R1 ===
5974 bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5975 bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5976
5977 // POP {R4-R7} - restore scratch registers (NO PC — inline code continues)
5978 // 16-bit POP: 1011 110 P rrrrrrrr where P=0 (no PC), r=R4-R7 = 0xF0
5979 // Encoding: 1011 1100 1111 0000 = 0xBCF0
5980 bytes.extend_from_slice(&0xBCF0u16.to_le_bytes());
5981
5982 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5983 Ok(bytes)
5984 }
5985
5986 // I64DivS: 64-bit signed division
5987 // Converts to unsigned, divides, then applies sign
5988 // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5989 // -> R0:R1 = quotient (signed)
5990 // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5991 ArmOp::I64DivS {
5992 rdlo,
5993 rdhi,
5994 rnlo,
5995 rnhi,
5996 rmlo,
5997 rmhi,
5998 elide_zero_guard,
5999 elide_overflow_guard,
6000 } => {
6001 let mut bytes = Vec::new();
6002 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6003 // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
6004 // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
6005 // the #633 overflow guard falls ONLY to
6006 // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
6007 // divisor-nonzero fact alone must keep it.
6008 if !elide_zero_guard {
6009 emit_i64_divisor_zero_trap(&mut bytes);
6010 }
6011 if !elide_overflow_guard {
6012 // #633: INT64_MIN / -1 overflows — trap like the i32 path
6013 // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
6014 emit_i64_divs_overflow_trap(&mut bytes);
6015 }
6016
6017 // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
6018 bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6019 bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6020
6021 // Save result sign in R9: R9 = R1 XOR R3 (sign bit = MSB)
6022 // EOR.W R9, R1, R3
6023 bytes.extend_from_slice(&0xEA81u16.to_le_bytes());
6024 bytes.extend_from_slice(&0x0903u16.to_le_bytes());
6025
6026 // If dividend negative (R1 MSB set), negate it
6027 // TST R1, R1 (check sign)
6028 bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
6029 // BPL skip_neg_dividend (+10 bytes = 5 halfwords)
6030 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6031
6032 // Negate R0:R1 (64-bit): RSBS R0, R0, #0; SBC R1, R1, R1 LSL #1
6033 // Actually: MVN R0, R0; MVN R1, R1; ADDS R0, R0, #1; ADC R1, R1, #0
6034 bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6035 bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6036 bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6037 bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6038 bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6039
6040 // If divisor negative (R3 MSB set), negate it
6041 bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
6042 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6043
6044 // Negate R2:R3
6045 bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
6046 bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
6047 bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
6048 bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
6049 bytes.extend_from_slice(&0x0300u16.to_le_bytes());
6050
6051 // === Now do unsigned division (same as I64DivU) ===
6052 // Initialize quotient (R4:R5) = 0
6053 bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6054 bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6055 // Initialize remainder (R6:R7) = 0
6056 bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6057 bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6058 // Initialize loop counter R8 = 64
6059 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6060 bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6061
6062 let loop_start = bytes.len();
6063
6064 // Shift quotient left
6065 bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6066 bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6067 bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6068 bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6069
6070 // Shift remainder left, OR in MSB of dividend
6071 bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6072 bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6073 bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6074 bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6075 bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6076 bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6077
6078 // Shift dividend left
6079 bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6080 bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6081 bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6082 bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6083
6084 // Compare and conditionally subtract
6085 bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6086 bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6087 bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6088 bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6089 bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6090
6091 // Subtract and set quotient bit
6092 bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6093 bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6094 bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6095 bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6096 bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6097
6098 // Decrement and loop
6099 bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6100 bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6101
6102 let branch_offset_bytes = bytes.len() - loop_start + 4;
6103 let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6104 let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6105 bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6106
6107 // Move quotient to R0:R1
6108 bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
6109 bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
6110
6111 // If result should be negative (R9 MSB set), negate R0:R1
6112 bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9 (check MSB)
6113 bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6114 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8 (skip negation)
6115
6116 // Negate result R0:R1
6117 bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6118 bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6119 bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6120 bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6121 bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6122
6123 // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6124 bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6125 bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6126
6127 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6128 Ok(bytes)
6129 }
6130
6131 // I64RemU: 64-bit unsigned remainder using binary long division
6132 // Same algorithm as I64DivU but returns remainder instead of quotient
6133 // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = remainder
6134 // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6135 ArmOp::I64RemU {
6136 rdlo,
6137 rdhi,
6138 rnlo,
6139 rnhi,
6140 rmlo,
6141 rmhi,
6142 elide_zero_guard,
6143 } => {
6144 let mut bytes = Vec::new();
6145 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6146 if !elide_zero_guard {
6147 emit_i64_divisor_zero_trap(&mut bytes);
6148 }
6149
6150 // PUSH {R4-R8} - save scratch registers (NO LR — inline code)
6151 bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6152 bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6153
6154 // Initialize quotient (R4:R5) = 0 (computed but not returned)
6155 bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6156 bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6157 // Initialize remainder (R6:R7) = 0
6158 bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6159 bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6160 // Initialize loop counter R8 = 64
6161 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6162 bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6163
6164 let loop_start = bytes.len();
6165
6166 // Shift quotient left (not needed for result, but keeps algorithm same)
6167 bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6168 bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6169 bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6170 bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6171
6172 // Shift remainder left, OR in MSB of dividend
6173 bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6174 bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6175 bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6176 bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6177 bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6178 bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6179
6180 // Shift dividend left
6181 bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6182 bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6183 bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6184 bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6185
6186 // Compare and conditionally subtract
6187 bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6188 bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6189 bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6190 bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6191 bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6192
6193 // Subtract and set quotient bit
6194 bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6195 bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6196 bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6197 bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6198 bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6199
6200 // Decrement and loop
6201 bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6202 bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6203
6204 let branch_offset_bytes = bytes.len() - loop_start + 4;
6205 let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6206 let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6207 bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6208
6209 // Move REMAINDER to R0:R1 (difference from I64DivU)
6210 bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6211 bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6212
6213 // POP {R4-R8} - restore scratch registers (NO PC — inline code continues)
6214 bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6215 bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6216
6217 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6218 Ok(bytes)
6219 }
6220
6221 // I64RemS: 64-bit signed remainder
6222 // Remainder sign follows dividend sign (not quotient rule)
6223 // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
6224 // -> R0:R1 = remainder (signed, same sign as dividend)
6225 // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6226 ArmOp::I64RemS {
6227 rdlo,
6228 rdhi,
6229 rnlo,
6230 rnhi,
6231 rmlo,
6232 rmhi,
6233 elide_zero_guard,
6234 } => {
6235 let mut bytes = Vec::new();
6236 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6237 if !elide_zero_guard {
6238 emit_i64_divisor_zero_trap(&mut bytes);
6239 }
6240
6241 // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
6242 bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6243 bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6244
6245 // Save dividend sign in R9 (remainder sign = dividend sign)
6246 // MOV R9, R1 (just need the sign bit)
6247 bytes.extend_from_slice(&0x4689u16.to_le_bytes()); // MOV R9, R1
6248
6249 // If dividend negative (R1 MSB set), negate it
6250 bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
6251 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6252
6253 // Negate R0:R1
6254 bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6255 bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6256 bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6257 bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6258 bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6259
6260 // If divisor negative (R3 MSB set), negate it
6261 bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
6262 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6263
6264 // Negate R2:R3
6265 bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
6266 bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
6267 bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
6268 bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
6269 bytes.extend_from_slice(&0x0300u16.to_le_bytes());
6270
6271 // === Unsigned division algorithm ===
6272 // Initialize quotient (R4:R5) = 0
6273 bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6274 bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6275 // Initialize remainder (R6:R7) = 0
6276 bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6277 bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6278 // Initialize loop counter R8 = 64
6279 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6280 bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6281
6282 let loop_start = bytes.len();
6283
6284 // Shift quotient left
6285 bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6286 bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6287 bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6288 bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6289
6290 // Shift remainder left, OR in MSB of dividend
6291 bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6292 bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6293 bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6294 bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6295 bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6296 bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6297
6298 // Shift dividend left
6299 bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6300 bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6301 bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6302 bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6303
6304 // Compare and conditionally subtract
6305 bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6306 bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6307 bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6308 bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6309 bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6310
6311 // Subtract and set quotient bit
6312 bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6313 bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6314 bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6315 bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6316 bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6317
6318 // Decrement and loop
6319 bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6320 bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6321
6322 let branch_offset_bytes = bytes.len() - loop_start + 4;
6323 let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6324 let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6325 bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6326
6327 // Move remainder to R0:R1
6328 bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6329 bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6330
6331 // If original dividend was negative (R9 MSB set), negate remainder
6332 bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9
6333 bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6334 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6335
6336 // Negate result R0:R1
6337 bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6338 bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6339 bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6340 bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6341 bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6342
6343 // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6344 bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6345 bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6346
6347 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6348 Ok(bytes)
6349 }
6350
6351 // === F32 VFP single-precision Thumb-2 encodings ===
6352 // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6353 ArmOp::F32Add { sd, sn, sm } => {
6354 Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A00, sd, sn, sm)?))
6355 }
6356 ArmOp::F32Sub { sd, sn, sm } => {
6357 Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A40, sd, sn, sm)?))
6358 }
6359 ArmOp::F32Mul { sd, sn, sm } => {
6360 Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE200A00, sd, sn, sm)?))
6361 }
6362 ArmOp::F32Div { sd, sn, sm } => {
6363 Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE800A00, sd, sn, sm)?))
6364 }
6365 ArmOp::F32Abs { sd, sm } => {
6366 Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB00AC0, sd, sm)?))
6367 }
6368 ArmOp::F32Neg { sd, sm } => {
6369 Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10A40, sd, sm)?))
6370 }
6371 ArmOp::F32Sqrt { sd, sm } => {
6372 Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10AC0, sd, sm)?))
6373 }
6374
6375 // f32 pseudo-ops — multi-instruction sequences
6376 // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6377 ArmOp::F32Ceil { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b01),
6378 ArmOp::F32Floor { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b10),
6379 ArmOp::F32Trunc { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b11),
6380 ArmOp::F32Nearest { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b00),
6381 ArmOp::F32Min { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, true),
6382 ArmOp::F32Max { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, false),
6383 ArmOp::F32Copysign { sd, sn, sm } => self.encode_thumb_f32_copysign(sd, sn, sm),
6384
6385 // f32 comparisons — VCMP + VMRS + MOV #0 + IT + MOV #1
6386 ArmOp::F32Eq { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x0),
6387 ArmOp::F32Ne { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x1),
6388 ArmOp::F32Lt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x4),
6389 ArmOp::F32Le { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x9),
6390 ArmOp::F32Gt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xC),
6391 ArmOp::F32Ge { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xA),
6392
6393 ArmOp::F32Const { sd, value } => self.encode_thumb_f32_const(sd, *value),
6394
6395 ArmOp::F32Load { sd, addr } => {
6396 Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED900A00, sd, addr)?))
6397 }
6398 ArmOp::F32Store { sd, addr } => {
6399 Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED800A00, sd, addr)?))
6400 }
6401
6402 ArmOp::F32ConvertI32S { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, true),
6403 ArmOp::F32ConvertI32U { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, false),
6404 ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
6405 Err(synth_core::Error::synthesis(
6406 "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6407 ))
6408 }
6409 ArmOp::F32ReinterpretI32 { sd, rm } => {
6410 Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(true, sd, rm)?))
6411 }
6412 ArmOp::I32ReinterpretF32 { rd, sm } => {
6413 Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(false, sm, rd)?))
6414 }
6415 ArmOp::I32TruncF32S { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, true),
6416 ArmOp::I32TruncF32U { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, false),
6417
6418 // === F64 VFP double-precision Thumb-2 encodings ===
6419 // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6420 ArmOp::F64Add { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6421 0xEE300B00, dd, dn, dm,
6422 )?)),
6423 ArmOp::F64Sub { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6424 0xEE300B40, dd, dn, dm,
6425 )?)),
6426 ArmOp::F64Mul { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6427 0xEE200B00, dd, dn, dm,
6428 )?)),
6429 ArmOp::F64Div { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6430 0xEE800B00, dd, dn, dm,
6431 )?)),
6432 ArmOp::F64Abs { dd, dm } => {
6433 Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?))
6434 }
6435 ArmOp::F64Neg { dd, dm } => {
6436 Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?))
6437 }
6438 ArmOp::F64Sqrt { dd, dm } => {
6439 Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?))
6440 }
6441
6442 // f64 pseudo-ops
6443 // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6444 ArmOp::F64Ceil { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b01),
6445 ArmOp::F64Floor { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b10),
6446 ArmOp::F64Trunc { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b11),
6447 ArmOp::F64Nearest { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b00),
6448 ArmOp::F64Min { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, true),
6449 ArmOp::F64Max { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, false),
6450 ArmOp::F64Copysign { dd, dn, dm } => self.encode_thumb_f64_copysign(dd, dn, dm),
6451
6452 // f64 comparisons
6453 ArmOp::F64Eq { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x0),
6454 ArmOp::F64Ne { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x1),
6455 ArmOp::F64Lt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x4),
6456 ArmOp::F64Le { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x9),
6457 ArmOp::F64Gt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xC),
6458 ArmOp::F64Ge { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xA),
6459
6460 ArmOp::F64Const { dd, value } => self.encode_thumb_f64_const(dd, *value),
6461
6462 ArmOp::F64Load { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6463 0xED900B00, dd, addr,
6464 )?)),
6465 ArmOp::F64Store { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6466 0xED800B00, dd, addr,
6467 )?)),
6468
6469 ArmOp::F64ConvertI32S { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, true),
6470 ArmOp::F64ConvertI32U { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, false),
6471 ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
6472 Err(synth_core::Error::synthesis(
6473 "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6474 ))
6475 }
6476 ArmOp::F64PromoteF32 { dd, sm } => self.encode_thumb_f64_promote_f32(dd, sm),
6477 ArmOp::F32DemoteF64 { sd, dm } => self.encode_thumb_f32_demote_f64(sd, dm),
6478 ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => Ok(vfp_to_thumb_bytes(
6479 encode_vmov_core_dreg(true, dd, rmlo, rmhi)?,
6480 )),
6481 ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => Ok(vfp_to_thumb_bytes(
6482 encode_vmov_core_dreg(false, dm, rdlo, rdhi)?,
6483 )),
6484 ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
6485 Err(synth_core::Error::synthesis(
6486 "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
6487 ))
6488 }
6489 ArmOp::I32TruncF64S { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, true),
6490 ArmOp::I32TruncF64U { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, false),
6491
6492 // ===== i64 operations: encode as multi-instruction Thumb-2 sequences =====
6493
6494 // I64Add: ADDS rdlo, rnlo, rmlo; ADC.W rdhi, rnhi, rmhi
6495 ArmOp::I64Add {
6496 rdlo,
6497 rdhi,
6498 rnlo,
6499 rnhi,
6500 rmlo,
6501 rmhi,
6502 } => {
6503 let mut bytes = Vec::new();
6504 // ADDS rdlo, rnlo, rmlo (16-bit)
6505 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adds {
6506 rd: *rdlo,
6507 rn: *rnlo,
6508 op2: Operand2::Reg(*rmlo),
6509 })?);
6510 // ADC.W rdhi, rnhi, rmhi (32-bit)
6511 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adc {
6512 rd: *rdhi,
6513 rn: *rnhi,
6514 op2: Operand2::Reg(*rmhi),
6515 })?);
6516 Ok(bytes)
6517 }
6518
6519 // I64Sub: SUBS rdlo, rnlo, rmlo; SBC.W rdhi, rnhi, rmhi
6520 ArmOp::I64Sub {
6521 rdlo,
6522 rdhi,
6523 rnlo,
6524 rnhi,
6525 rmlo,
6526 rmhi,
6527 } => {
6528 let mut bytes = Vec::new();
6529 // SUBS rdlo, rnlo, rmlo (16-bit)
6530 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Subs {
6531 rd: *rdlo,
6532 rn: *rnlo,
6533 op2: Operand2::Reg(*rmlo),
6534 })?);
6535 // SBC.W rdhi, rnhi, rmhi (32-bit)
6536 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Sbc {
6537 rd: *rdhi,
6538 rn: *rnhi,
6539 op2: Operand2::Reg(*rmhi),
6540 })?);
6541 Ok(bytes)
6542 }
6543
6544 // I64And: AND rdlo, rnlo, rmlo; AND rdhi, rnhi, rmhi
6545 ArmOp::I64And {
6546 rdlo,
6547 rdhi,
6548 rnlo,
6549 rnhi,
6550 rmlo,
6551 rmhi,
6552 } => {
6553 let mut bytes = Vec::new();
6554 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6555 rd: *rdlo,
6556 rn: *rnlo,
6557 op2: Operand2::Reg(*rmlo),
6558 })?);
6559 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6560 rd: *rdhi,
6561 rn: *rnhi,
6562 op2: Operand2::Reg(*rmhi),
6563 })?);
6564 Ok(bytes)
6565 }
6566
6567 // I64Or: ORR rdlo, rnlo, rmlo; ORR rdhi, rnhi, rmhi
6568 ArmOp::I64Or {
6569 rdlo,
6570 rdhi,
6571 rnlo,
6572 rnhi,
6573 rmlo,
6574 rmhi,
6575 } => {
6576 let mut bytes = Vec::new();
6577 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6578 rd: *rdlo,
6579 rn: *rnlo,
6580 op2: Operand2::Reg(*rmlo),
6581 })?);
6582 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6583 rd: *rdhi,
6584 rn: *rnhi,
6585 op2: Operand2::Reg(*rmhi),
6586 })?);
6587 Ok(bytes)
6588 }
6589
6590 // I64Xor: EOR rdlo, rnlo, rmlo; EOR rdhi, rnhi, rmhi
6591 ArmOp::I64Xor {
6592 rdlo,
6593 rdhi,
6594 rnlo,
6595 rnhi,
6596 rmlo,
6597 rmhi,
6598 } => {
6599 let mut bytes = Vec::new();
6600 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6601 rd: *rdlo,
6602 rn: *rnlo,
6603 op2: Operand2::Reg(*rmlo),
6604 })?);
6605 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6606 rd: *rdhi,
6607 rn: *rnhi,
6608 op2: Operand2::Reg(*rmhi),
6609 })?);
6610 Ok(bytes)
6611 }
6612
6613 // I64Eqz: ORR scratch, lo, hi; ITE EQ; MOV rd, #1; MOV rd, #0
6614 ArmOp::I64Eqz { rd, rnlo, rnhi } => self.encode_thumb(&ArmOp::I64SetCondZ {
6615 rd: *rd,
6616 rn_lo: *rnlo,
6617 rn_hi: *rnhi,
6618 }),
6619
6620 // I64 comparisons: delegate to I64SetCond
6621 ArmOp::I64Eq {
6622 rd,
6623 rnlo,
6624 rnhi,
6625 rmlo,
6626 rmhi,
6627 } => self.encode_thumb(&ArmOp::I64SetCond {
6628 rd: *rd,
6629 rn_lo: *rnlo,
6630 rn_hi: *rnhi,
6631 rm_lo: *rmlo,
6632 rm_hi: *rmhi,
6633 cond: synth_synthesis::Condition::EQ,
6634 }),
6635
6636 ArmOp::I64Ne {
6637 rd,
6638 rnlo,
6639 rnhi,
6640 rmlo,
6641 rmhi,
6642 } => self.encode_thumb(&ArmOp::I64SetCond {
6643 rd: *rd,
6644 rn_lo: *rnlo,
6645 rn_hi: *rnhi,
6646 rm_lo: *rmlo,
6647 rm_hi: *rmhi,
6648 cond: synth_synthesis::Condition::NE,
6649 }),
6650
6651 ArmOp::I64LtS {
6652 rd,
6653 rnlo,
6654 rnhi,
6655 rmlo,
6656 rmhi,
6657 } => self.encode_thumb(&ArmOp::I64SetCond {
6658 rd: *rd,
6659 rn_lo: *rnlo,
6660 rn_hi: *rnhi,
6661 rm_lo: *rmlo,
6662 rm_hi: *rmhi,
6663 cond: synth_synthesis::Condition::LT,
6664 }),
6665
6666 ArmOp::I64LtU {
6667 rd,
6668 rnlo,
6669 rnhi,
6670 rmlo,
6671 rmhi,
6672 } => self.encode_thumb(&ArmOp::I64SetCond {
6673 rd: *rd,
6674 rn_lo: *rnlo,
6675 rn_hi: *rnhi,
6676 rm_lo: *rmlo,
6677 rm_hi: *rmhi,
6678 cond: synth_synthesis::Condition::LO,
6679 }),
6680
6681 ArmOp::I64LeS {
6682 rd,
6683 rnlo,
6684 rnhi,
6685 rmlo,
6686 rmhi,
6687 } => self.encode_thumb(&ArmOp::I64SetCond {
6688 rd: *rd,
6689 rn_lo: *rnlo,
6690 rn_hi: *rnhi,
6691 rm_lo: *rmlo,
6692 rm_hi: *rmhi,
6693 cond: synth_synthesis::Condition::LE,
6694 }),
6695
6696 ArmOp::I64LeU {
6697 rd,
6698 rnlo,
6699 rnhi,
6700 rmlo,
6701 rmhi,
6702 } => self.encode_thumb(&ArmOp::I64SetCond {
6703 rd: *rd,
6704 rn_lo: *rnlo,
6705 rn_hi: *rnhi,
6706 rm_lo: *rmlo,
6707 rm_hi: *rmhi,
6708 cond: synth_synthesis::Condition::LS,
6709 }),
6710
6711 ArmOp::I64GtS {
6712 rd,
6713 rnlo,
6714 rnhi,
6715 rmlo,
6716 rmhi,
6717 } => self.encode_thumb(&ArmOp::I64SetCond {
6718 rd: *rd,
6719 rn_lo: *rnlo,
6720 rn_hi: *rnhi,
6721 rm_lo: *rmlo,
6722 rm_hi: *rmhi,
6723 cond: synth_synthesis::Condition::GT,
6724 }),
6725
6726 ArmOp::I64GtU {
6727 rd,
6728 rnlo,
6729 rnhi,
6730 rmlo,
6731 rmhi,
6732 } => self.encode_thumb(&ArmOp::I64SetCond {
6733 rd: *rd,
6734 rn_lo: *rnlo,
6735 rn_hi: *rnhi,
6736 rm_lo: *rmlo,
6737 rm_hi: *rmhi,
6738 cond: synth_synthesis::Condition::HI,
6739 }),
6740
6741 ArmOp::I64GeS {
6742 rd,
6743 rnlo,
6744 rnhi,
6745 rmlo,
6746 rmhi,
6747 } => self.encode_thumb(&ArmOp::I64SetCond {
6748 rd: *rd,
6749 rn_lo: *rnlo,
6750 rn_hi: *rnhi,
6751 rm_lo: *rmlo,
6752 rm_hi: *rmhi,
6753 cond: synth_synthesis::Condition::GE,
6754 }),
6755
6756 ArmOp::I64GeU {
6757 rd,
6758 rnlo,
6759 rnhi,
6760 rmlo,
6761 rmhi,
6762 } => self.encode_thumb(&ArmOp::I64SetCond {
6763 rd: *rd,
6764 rn_lo: *rnlo,
6765 rn_hi: *rnhi,
6766 rm_lo: *rmlo,
6767 rm_hi: *rmhi,
6768 cond: synth_synthesis::Condition::HS,
6769 }),
6770
6771 // I64Const: MOVW rdlo, lo16; MOVT rdlo, hi16; MOVW rdhi, lo16_hi; MOVT rdhi, hi16_hi
6772 ArmOp::I64Const { rdlo, rdhi, value } => {
6773 let lo32 = *value as u32;
6774 let hi32 = (*value >> 32) as u32;
6775 let mut bytes = Vec::new();
6776 // Load low 32 bits into rdlo
6777 bytes.extend_from_slice(
6778 &self.encode_thumb32_movw_raw(reg_to_bits(rdlo), lo32 & 0xFFFF)?,
6779 );
6780 if lo32 > 0xFFFF {
6781 bytes.extend_from_slice(
6782 &self.encode_thumb32_movt_raw(reg_to_bits(rdlo), lo32 >> 16)?,
6783 );
6784 }
6785 // Load high 32 bits into rdhi
6786 bytes.extend_from_slice(
6787 &self.encode_thumb32_movw_raw(reg_to_bits(rdhi), hi32 & 0xFFFF)?,
6788 );
6789 if hi32 > 0xFFFF {
6790 bytes.extend_from_slice(
6791 &self.encode_thumb32_movt_raw(reg_to_bits(rdhi), hi32 >> 16)?,
6792 );
6793 }
6794 Ok(bytes)
6795 }
6796
6797 // I64Ldr: LDR rdlo, [base, offset]; LDR rdhi, [base, offset+4]
6798 ArmOp::I64Ldr { rdlo, rdhi, addr } => {
6799 let mut bytes = Vec::new();
6800 // #372/#382: a memory `i64.load` carries an index register
6801 // (`reg_imm(R11, addr_reg, offset)` = R11 + addr + offset). The
6802 // immediate `encode_thumb32_ldr` below uses only base+offset and
6803 // would SILENTLY DROP `offset_reg` — the #206 defect, here for
6804 // i64. `i64_effective_base` materializes the effective base into
6805 // `ip` (and, when `offset+4 > 0xFFF`, folds the offset in too so
6806 // the function is NOT skipped — #382), returning the residual
6807 // imm12 for the two halves. Frame i64 loads (no `offset_reg`, e.g.
6808 // a spilled local at `[SP, #off]`) keep the plain `[base,#off]`
6809 // form unchanged — so existing output is byte-identical.
6810 let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6811 bytes.extend_from_slice(&self.encode_thumb32_ldr(rdlo, &base, offset)?);
6812 bytes.extend_from_slice(&self.encode_thumb32_ldr(
6813 rdhi,
6814 &base,
6815 offset.wrapping_add(4),
6816 )?);
6817 Ok(bytes)
6818 }
6819
6820 // I64Str: STR rdlo, [base, offset]; STR rdhi, [base, offset+4]
6821 ArmOp::I64Str { rdlo, rdhi, addr } => {
6822 let mut bytes = Vec::new();
6823 // #372/#382: same index-materialization + large-offset fold as
6824 // I64Ldr (see above).
6825 let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6826 bytes.extend_from_slice(&self.encode_thumb32_str(rdlo, &base, offset)?);
6827 bytes.extend_from_slice(&self.encode_thumb32_str(
6828 rdhi,
6829 &base,
6830 offset.wrapping_add(4),
6831 )?);
6832 Ok(bytes)
6833 }
6834
6835 // I64ExtendI32S: MOV rdlo, rn; ASR rdhi, rdlo, #31 (sign-extend)
6836 ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
6837 let mut bytes = Vec::new();
6838 if rdlo != rn {
6839 // MOV rdlo, rn (16-bit)
6840 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6841 rd: *rdlo,
6842 op2: Operand2::Reg(*rn),
6843 })?);
6844 }
6845 // ASR rdhi, rdlo, #31 (sign-extend: fill high word with sign bit)
6846 bytes.extend_from_slice(
6847 &self.encode_thumb32_shift(rdhi, rdlo, 31, 0b10)?, // ASR type
6848 );
6849 Ok(bytes)
6850 }
6851
6852 // I64ExtendI32U: MOV rdlo, rn; MOV rdhi, #0
6853 ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
6854 let mut bytes = Vec::new();
6855 if rdlo != rn {
6856 // MOV rdlo, rn
6857 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6858 rd: *rdlo,
6859 op2: Operand2::Reg(*rn),
6860 })?);
6861 }
6862 // MOV rdhi, #0 (#916: MOV.W for rdhi >= R8). Unconditional
6863 // site with no branches in the expansion — before the fix this
6864 // emitted the literal two-instruction stream [4608, 2800], half
6865 // of which was `CMP r0,#0` rather than the high-word clear, so
6866 // every i64.extend_i32_u into a high pair leaked stale bits.
6867 emit_thumb_zero_fill(&mut bytes, reg_to_bits(rdhi));
6868 Ok(bytes)
6869 }
6870
6871 // I32WrapI64: MOV rd, rnlo (just take low 32 bits)
6872 ArmOp::I32WrapI64 { rd, rnlo } => {
6873 if rd == rnlo {
6874 // No-op: already in the right register
6875 let instr: u16 = 0xBF00; // NOP
6876 Ok(instr.to_le_bytes().to_vec())
6877 } else {
6878 // MOV rd, rnlo
6879 self.encode_thumb(&ArmOp::Mov {
6880 rd: *rd,
6881 op2: Operand2::Reg(*rnlo),
6882 })
6883 }
6884 }
6885
6886 // ===== Helium MVE operations (Thumb-2 encoding) =====
6887 ArmOp::MveLoad { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vldrw(qd, addr))),
6888 ArmOp::MveStore { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vstrw(qd, addr))),
6889 ArmOp::MveConst { qd, bytes } => self.encode_thumb_mve_const(qd, bytes),
6890 ArmOp::MveAnd { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6891 0xEF000150, qd, qn, qm,
6892 ))),
6893 ArmOp::MveOrr { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6894 0xEF200150, qd, qn, qm,
6895 ))),
6896 ArmOp::MveEor { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6897 0xFF000150, qd, qn, qm,
6898 ))),
6899 ArmOp::MveMvn { qd, qm } => {
6900 // VMVN Qd, Qm: 0xFFB005C0 | Qd<<12 | Qm
6901 let qd_enc = qreg_to_num(qd);
6902 let qm_enc = qreg_to_num(qm);
6903 let instr: u32 = 0xFFB005C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6904 Ok(vfp_to_thumb_bytes(instr))
6905 }
6906 ArmOp::MveBic { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6907 0xEF100150, qd, qn, qm,
6908 ))),
6909 ArmOp::MveAddI { qd, qn, qm, size } => {
6910 let sz = mve_size_bits(size);
6911 let base: u32 = 0xEF000840 | (sz << 20);
6912 Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6913 }
6914 ArmOp::MveSubI { qd, qn, qm, size } => {
6915 let sz = mve_size_bits(size);
6916 let base: u32 = 0xFF000840 | (sz << 20);
6917 Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6918 }
6919 ArmOp::MveMulI { qd, qn, qm, size } => {
6920 let sz = mve_size_bits(size);
6921 let base: u32 = 0xEF000950 | (sz << 20);
6922 Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6923 }
6924 ArmOp::MveNegI { qd, qm, size } => {
6925 let sz = mve_size_bits(size);
6926 // VNEG.Sx Qd, Qm
6927 let qd_enc = qreg_to_num(qd);
6928 let qm_enc = qreg_to_num(qm);
6929 let base: u32 = 0xFFB103C0 | (sz << 18);
6930 let instr = base | ((qd_enc * 2) << 12) | (qm_enc * 2);
6931 Ok(vfp_to_thumb_bytes(instr))
6932 }
6933 ArmOp::MveDup { qd, rn, size } => {
6934 let sz = mve_size_bits(size);
6935 let qd_enc = qreg_to_num(qd);
6936 let rn_bits = reg_to_bits(rn);
6937 // VDUP.sz Qd, Rn: EEA0 0B10 variant
6938 // size encoding: 00=32, 01=16, 10=8
6939 let be = match sz {
6940 0 => 0b00u32, // 8-bit
6941 1 => 0b01, // 16-bit
6942 _ => 0b00, // 32-bit (default)
6943 };
6944 let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12) | (be << 5);
6945 Ok(vfp_to_thumb_bytes(instr))
6946 }
6947 ArmOp::MveExtractLane { rd, qn, lane, size } => {
6948 let qn_enc = qreg_to_num(qn);
6949 let rd_bits = reg_to_bits(rd);
6950 // VMOV.sz Rd, Dn[x] — extract from Q-register lane
6951 // For 32-bit: VMOV Rd, Dn — where Dn is the appropriate D-register
6952 let d_reg = qn_enc * 2 + ((*lane as u32) >> 1);
6953 let lane_in_d = (*lane as u32) & 1;
6954 let _sz = mve_size_bits(size);
6955 // VMOV Rd, Dn[x]: EE10 0B10 for 32-bit
6956 let instr: u32 = 0xEE100B10 | (d_reg << 16) | (rd_bits << 12) | (lane_in_d << 21);
6957 Ok(vfp_to_thumb_bytes(instr))
6958 }
6959 ArmOp::MveInsertLane { qd, rn, lane, size } => {
6960 let qd_enc = qreg_to_num(qd);
6961 let rn_bits = reg_to_bits(rn);
6962 let d_reg = qd_enc * 2 + ((*lane as u32) >> 1);
6963 let lane_in_d = (*lane as u32) & 1;
6964 let _sz = mve_size_bits(size);
6965 // VMOV Dn[x], Rn: EE00 0B10 for 32-bit
6966 let instr: u32 = 0xEE000B10 | (d_reg << 16) | (rn_bits << 12) | (lane_in_d << 21);
6967 Ok(vfp_to_thumb_bytes(instr))
6968 }
6969
6970 // MVE float comparisons — emit VCMP + VPSEL sequence (simplified: just VCMP)
6971 ArmOp::MveCmpEqI { qd, qn, qm, size }
6972 | ArmOp::MveCmpNeI { qd, qn, qm, size }
6973 | ArmOp::MveCmpLtS { qd, qn, qm, size }
6974 | ArmOp::MveCmpLtU { qd, qn, qm, size }
6975 | ArmOp::MveCmpGtS { qd, qn, qm, size }
6976 | ArmOp::MveCmpGtU { qd, qn, qm, size }
6977 | ArmOp::MveCmpLeS { qd, qn, qm, size }
6978 | ArmOp::MveCmpLeU { qd, qn, qm, size }
6979 | ArmOp::MveCmpGeS { qd, qn, qm, size }
6980 | ArmOp::MveCmpGeU { qd, qn, qm, size } => {
6981 // Encode as VADD (placeholder encoding — real implementation
6982 // would use VCMP + VPSEL pair)
6983 let sz = mve_size_bits(size);
6984 let base: u32 = 0xEF000840 | (sz << 20);
6985 Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6986 }
6987
6988 // f32x4 MVE arithmetic
6989 ArmOp::MveAddF32 { qd, qn, qm } => {
6990 // VADD.F32 Qd, Qn, Qm (MVE): 0xEF000D40
6991 Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6992 }
6993 ArmOp::MveSubF32 { qd, qn, qm } => {
6994 // VSUB.F32 Qd, Qn, Qm (MVE): 0xEF200D40
6995 Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF200D40, qd, qn, qm)))
6996 }
6997 ArmOp::MveMulF32 { qd, qn, qm } => {
6998 // VMUL.F32 Qd, Qn, Qm (MVE): 0xFF000D50
6999 Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xFF000D50, qd, qn, qm)))
7000 }
7001 ArmOp::MveNegF32 { qd, qm } => {
7002 let qd_enc = qreg_to_num(qd);
7003 let qm_enc = qreg_to_num(qm);
7004 // VNEG.F32 Qd, Qm: FFB907C0
7005 let instr: u32 = 0xFFB907C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
7006 Ok(vfp_to_thumb_bytes(instr))
7007 }
7008 ArmOp::MveAbsF32 { qd, qm } => {
7009 let qd_enc = qreg_to_num(qd);
7010 let qm_enc = qreg_to_num(qm);
7011 // VABS.F32 Qd, Qm: FFB90740
7012 let instr: u32 = 0xFFB90740 | ((qd_enc * 2) << 12) | (qm_enc * 2);
7013 Ok(vfp_to_thumb_bytes(instr))
7014 }
7015 ArmOp::MveCmpEqF32 { qd, qn, qm }
7016 | ArmOp::MveCmpNeF32 { qd, qn, qm }
7017 | ArmOp::MveCmpLtF32 { qd, qn, qm }
7018 | ArmOp::MveCmpLeF32 { qd, qn, qm }
7019 | ArmOp::MveCmpGtF32 { qd, qn, qm }
7020 | ArmOp::MveCmpGeF32 { qd, qn, qm } => {
7021 // Placeholder: encode as VADD.F32 (real impl needs VCMP.F32 + VPSEL)
7022 Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
7023 }
7024 ArmOp::MveDupF32 { qd, rn } => {
7025 let qd_enc = qreg_to_num(qd);
7026 let rn_bits = reg_to_bits(rn);
7027 // VDUP.32 Qd, Rn (same encoding as integer VDUP.32)
7028 let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12);
7029 Ok(vfp_to_thumb_bytes(instr))
7030 }
7031 ArmOp::MveExtractLaneF32 { rd, qn, lane } => {
7032 let qn_enc = qreg_to_num(qn);
7033 let rd_bits = reg_to_bits(rd);
7034 // VMOV Rd, Sn where Sn = Q*4 + lane
7035 let s_num = qn_enc * 4 + (*lane as u32);
7036 let (vn, n) = encode_sreg(s_num);
7037 let instr: u32 = 0xEE100A10 | (vn << 16) | (rd_bits << 12) | (n << 7);
7038 Ok(vfp_to_thumb_bytes(instr))
7039 }
7040 ArmOp::MveReplaceLaneF32 { qd, rn, lane } => {
7041 let qd_enc = qreg_to_num(qd);
7042 let rn_bits = reg_to_bits(rn);
7043 // VMOV Sn, Rn where Sn = Q*4 + lane
7044 let s_num = qd_enc * 4 + (*lane as u32);
7045 let (vn, n) = encode_sreg(s_num);
7046 let instr: u32 = 0xEE000A10 | (vn << 16) | (rn_bits << 12) | (n << 7);
7047 Ok(vfp_to_thumb_bytes(instr))
7048 }
7049 ArmOp::MveDivF32 { qd, qn, qm } => {
7050 // Lane-wise: extract 4 S-regs, VDIV, insert back
7051 self.encode_thumb_mve_lane_wise_f32_binop(qd, qn, qm, 0xEE800A00)
7052 }
7053 ArmOp::MveSqrtF32 { qd, qm } => {
7054 // Lane-wise: extract 4 S-regs, VSQRT, insert back
7055 self.encode_thumb_mve_lane_wise_f32_sqrt(qd, qm)
7056 }
7057
7058 // RQ-67-VFPREACH (#1267): the AAPCS callee-saved VFP half.
7059 //
7060 // Encodings taken from llvm-mc, not derived by hand:
7061 // llvm-mc -triple=thumbv7em-none-eabi -mattr=+vfp4 -show-encoding
7062 // vpush {d8-d15} -> [0x2d,0xed,0x10,0x8b] (ED2D 8B10)
7063 // vpop {d8-d15} -> [0xbd,0xec,0x10,0x8b] (ECBD 8B10)
7064 // imm8 is 2 x the double count (0x10 = 16 = 2 x 8), and the
7065 // `8B` nibble selects the 64-bit form — `vpush {d8}` is 8B02,
7066 // which is how that field was confirmed rather than assumed.
7067 //
7068 // NOTE FOR ANYONE ADDING AN OP BELOW THIS POINT: the catch-all that
7069 // follows returns a NOP, so an op that reaches it is encoded as
7070 // nothing at all. These two arms exist ABOVE it deliberately.
7071 ArmOp::VPushCalleeSavedVfp => Ok(vec![0x2d, 0xed, 0x10, 0x8b]),
7072 ArmOp::VPopCalleeSavedVfp => Ok(vec![0xbd, 0xec, 0x10, 0x8b]),
7073
7074 // Catch-all for any remaining ops
7075 //
7076 // #615 FIXED THIS CLASS ON THE A32 PATH AND NOT HERE. Thumb-2 is
7077 // the DEFAULT target, and this arm silently encodes any unhandled
7078 // op as `NOP`. RQ-67-VFPREACH hit it directly: the two VFP
7079 // save/restore ops above compiled clean and would have emitted a
7080 // save that does nothing. Filed rather than widened in this lane —
7081 // turning it into a typed Err is a behaviour change across every
7082 // op that currently lands here, which needs its own byte-identity
7083 // evidence.
7084 other => Err(synth_core::Error::synthesis(format!(
7085 "{other:?} reached the Thumb-2 encoder with no Thumb-2 encoding — \
7086 refusing to emit a silent NOP (#1272; #615 closed this class on A32 \
7087 only, and Thumb-2 is the default target)"
7088 ))),
7089 }
7090 }
7091
7092 // === Thumb-2 VFP multi-instruction helpers ===
7093
7094 /// Encode F32 comparison as Thumb-2: VCMP.F32 + VMRS + MOVS rd,#0 + IT + MOV rd,#1
7095 fn encode_thumb_f32_compare(
7096 &self,
7097 rd: &Reg,
7098 sn: &VfpReg,
7099 sm: &VfpReg,
7100 cond_code: u32,
7101 ) -> Result<Vec<u8>> {
7102 let mut bytes = Vec::new();
7103 let rd_bits = reg_to_bits(rd);
7104
7105 // #709 (bug found under #708/#709): the `MOVS Rd,#0` below is a
7106 // FLAG-SETTING 16-bit move. Emitting it AFTER `VMRS APSR_nzcv, FPSCR`
7107 // (as the original code did) clobbered the N/Z/C/V flags the VMRS just
7108 // transferred from the VFP compare, so the following `IT<cond>` read
7109 // stale flags and every f32 comparison silently returned 0 (verified:
7110 // `flt(1.0,2.0)` → 0 on Cortex-M4F). The 619 harness never caught it
7111 // because it deliberately skipped compare EXECUTION on a false premise
7112 // (unicorn DOES model VMRS→APSR). Fix: materialize the `#0` FIRST, then
7113 // VCMP+VMRS set the flags the `IT` consumes. Instruction sizes are
7114 // unchanged (pure reorder), so the estimator↔encoder oracle (#511) is
7115 // untouched — only the byte ORDER differs.
7116
7117 // MOVS Rd, #0 (16-bit): 0010 0 Rd(3) 0000 0000 — its flag side effect
7118 // is immediately overwritten by the VMRS below.
7119 if rd_bits < 8 {
7120 let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
7121 bytes.extend_from_slice(&movs_zero.to_le_bytes());
7122 } else {
7123 // MOV.W Rd, #0 (32-bit Thumb-2)
7124 let hw1: u16 = 0xF04F;
7125 let hw2: u16 = (rd_bits as u16) << 8;
7126 bytes.extend_from_slice(&hw1.to_le_bytes());
7127 bytes.extend_from_slice(&hw2.to_le_bytes());
7128 }
7129
7130 // VCMP.F32 Sn, Sm
7131 let sn_num = vfp_sreg_to_num(sn)?;
7132 let sm_num = vfp_sreg_to_num(sm)?;
7133 let (vd, d) = encode_sreg(sn_num);
7134 let (vm, m) = encode_sreg(sm_num);
7135 let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7136 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7137
7138 // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10 (sets the flags IT consumes)
7139 bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7140
7141 // IT<cond> — If-Then for conditional MOV
7142 // IT encoding: 1011 1111 cond(4) mask(4)
7143 // mask = 0x8 for single "then" (IT)
7144 let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7145 bytes.extend_from_slice(&it.to_le_bytes());
7146
7147 // MOV Rd, #1 (16-bit, conditional due to IT): 0010 0 Rd(3) 0000 0001
7148 if rd_bits < 8 {
7149 let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7150 bytes.extend_from_slice(&mov_one.to_le_bytes());
7151 } else {
7152 // MOV.W Rd, #1 (32-bit)
7153 let hw1: u16 = 0xF04F;
7154 let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7155 bytes.extend_from_slice(&hw1.to_le_bytes());
7156 bytes.extend_from_slice(&hw2.to_le_bytes());
7157 }
7158
7159 Ok(bytes)
7160 }
7161
7162 /// Encode F32 constant load as Thumb-2: MOVW + MOVT + VMOV
7163 fn encode_thumb_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
7164 let mut bytes = Vec::new();
7165 let bits = value.to_bits();
7166 let rt: u32 = 12; // R12/IP as temp
7167
7168 // MOVW R12, #lo16
7169 // Thumb-2 MOVW: 11110 i 10 0100 imm4 | 0 imm3 Rd imm8
7170 let lo16 = bits & 0xFFFF;
7171 let imm4 = (lo16 >> 12) & 0xF;
7172 let i_bit = (lo16 >> 11) & 1;
7173 let imm3 = (lo16 >> 8) & 0x7;
7174 let imm8 = lo16 & 0xFF;
7175 let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7176 let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
7177 bytes.extend_from_slice(&hw1.to_le_bytes());
7178 bytes.extend_from_slice(&hw2.to_le_bytes());
7179
7180 // MOVT R12, #hi16
7181 let hi16 = (bits >> 16) & 0xFFFF;
7182 let imm4 = (hi16 >> 12) & 0xF;
7183 let i_bit = (hi16 >> 11) & 1;
7184 let imm3 = (hi16 >> 8) & 0x7;
7185 let imm8 = hi16 & 0xFF;
7186 let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
7187 let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
7188 bytes.extend_from_slice(&hw1.to_le_bytes());
7189 bytes.extend_from_slice(&hw2.to_le_bytes());
7190
7191 // VMOV Sd, R12
7192 let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
7193 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7194
7195 Ok(bytes)
7196 }
7197
7198 /// Encode VMOV + VCVT.F32.xS32 as Thumb-2
7199 fn encode_thumb_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7200 let mut bytes = Vec::new();
7201
7202 // VMOV Sd, Rm
7203 let vmov = encode_vmov_core_sreg(true, sd, rm)?;
7204 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7205
7206 // VCVT.F32.S32/U32 Sd, Sd. Bit 7 (op) = 1 for signed (S32), 0 for
7207 // unsigned (U32): signed = 0xEEB80AC0, unsigned = 0xEEB80A40
7208 // (GI-FPU-002: previously swapped — see the ARM32 twin).
7209 let sd_num = vfp_sreg_to_num(sd)?;
7210 let (vd, d) = encode_sreg(sd_num);
7211 let (vm, m) = encode_sreg(sd_num);
7212 let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
7213 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7214 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7215
7216 Ok(bytes)
7217 }
7218
7219 /// Encode F32 rounding pseudo-op as Thumb-2 via VCVT to integer and back
7220 /// Encode F32 rounding as Thumb-2.
7221 /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
7222 ///
7223 /// For trunc: uses VCVTR.S32.F32 (always truncates).
7224 /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant),
7225 /// then restores FPSCR.
7226 fn encode_thumb_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7227 let mut bytes = Vec::new();
7228 let sm_num = vfp_sreg_to_num(sm)?;
7229 let sd_num = vfp_sreg_to_num(sd)?;
7230 let (vd_s, d_s) = encode_sreg(sd_num);
7231 let (vm_s, m_s) = encode_sreg(sm_num);
7232
7233 if mode == 0b11 {
7234 // Trunc (toward zero): VCVTR.S32.F32 — bit[7]=1, always truncates
7235 let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7236 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7237 } else {
7238 // ceil/floor/nearest: manipulate FPSCR rounding mode
7239 let rt: u32 = 12; // R12/IP as temp
7240
7241 // VMRS R12, FPSCR
7242 let vmrs = 0xEEF10A10 | (rt << 12);
7243 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7244
7245 // BIC.W R12, R12, #(3 << 22) — clear RMode bits [23:22]
7246 // Thumb-2 modified immediate for 3<<22 = 0x00C00000:
7247 // BIC.W encoding: 11110 i 0 0001 S Rn | 0 imm3 Rd imm8
7248 // 0x00C00000 = 0x03 shifted left by 22 => Thumb mod-imm: i=0, imm3=0b101, imm8=0x03
7249 let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF); // BIC, Rn=R12
7250 let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
7251 bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7252 bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7253
7254 // ORR.W R12, R12, #(mode << 22)
7255 if mode != 0 {
7256 let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF); // ORR, Rn=R12
7257 let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
7258 bytes.extend_from_slice(&orr_hw1.to_le_bytes());
7259 bytes.extend_from_slice(&orr_hw2.to_le_bytes());
7260 }
7261
7262 // VMSR FPSCR, R12
7263 let vmsr = 0xEEE10A10 | (rt << 12);
7264 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7265
7266 // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rmode
7267 let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7268 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7269
7270 // Restore FPSCR: clear rmode bits back to nearest (default)
7271 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7272 bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7273 bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7274 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7275 }
7276
7277 // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
7278 let (vd2, d2) = encode_sreg(sd_num);
7279 let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
7280 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
7281
7282 Ok(bytes)
7283 }
7284
7285 /// Encode F32 min/max as Thumb-2: VMOV + VCMP + VMRS + IT + VMOV
7286 fn encode_thumb_f32_minmax(
7287 &self,
7288 sd: &VfpReg,
7289 sn: &VfpReg,
7290 sm: &VfpReg,
7291 is_min: bool,
7292 ) -> Result<Vec<u8>> {
7293 let mut bytes = Vec::new();
7294 let sn_num = vfp_sreg_to_num(sn)?;
7295 let sm_num = vfp_sreg_to_num(sm)?;
7296 let sd_num = vfp_sreg_to_num(sd)?;
7297
7298 // VMOV.F32 Sd, Sn
7299 let (vd, d) = encode_sreg(sd_num);
7300 let (vn, n) = encode_sreg(sn_num);
7301 let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
7302 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sn));
7303
7304 // VCMP.F32 Sn, Sm
7305 let (vm, m) = encode_sreg(sm_num);
7306 let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7307 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7308
7309 // VMRS APSR_nzcv, FPSCR
7310 bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7311
7312 // IT GT (for min) or IT MI (for max)
7313 let cond: u16 = if is_min { 0xC } else { 0x4 };
7314 let it: u16 = 0xBF00 | (cond << 4) | 0x8;
7315 bytes.extend_from_slice(&it.to_le_bytes());
7316
7317 // VMOV{cond}.F32 Sd, Sm — conditional VMOV in IT block
7318 let vmov_sm = 0xEEB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7319 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sm));
7320
7321 Ok(bytes)
7322 }
7323
7324 /// Encode F32 copysign as Thumb-2
7325 /// Encode F32 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7326 /// encoder scratch, #212), the flags, and Sd:
7327 ///
7328 /// VMOV R12, Sm ; CMP R12, #0 (N flag = the sign bit)
7329 /// VABS.F32 Sd, Sn (magnitude, sign cleared)
7330 /// IT MI ; VNEG.F32(MI) Sd, Sd
7331 ///
7332 /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7333 /// The R12 capture happens BEFORE Sd is written, so Sd aliasing Sn or Sm
7334 /// is safe. (The previous sequence staged the magnitude through R0 —
7335 /// clobbering a live allocator-owned value, the #615 class; caught while
7336 /// composing the F64 twin for #369.)
7337 fn encode_thumb_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7338 let mut bytes = Vec::new();
7339
7340 // VMOV R12, Sm (sign source bits)
7341 bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
7342 false,
7343 sm,
7344 &Reg::R12,
7345 )?));
7346 // CMP.W R12, #0 — N = bit31 (the sign, incl. -0.0 / -NaN).
7347 bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7348 bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7349 // VABS.F32 Sd, Sn
7350 let sd_num = vfp_sreg_to_num(sd)?;
7351 let sn_num = vfp_sreg_to_num(sn)?;
7352 let (vd, d) = encode_sreg(sd_num);
7353 let (vn, n) = encode_sreg(sn_num);
7354 let vabs = 0xEEB00AC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7355 bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7356 // IT MI ; VNEG.F32(MI) Sd, Sd
7357 bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7358 let vneg = 0xEEB10A40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7359 bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7360
7361 Ok(bytes)
7362 }
7363
7364 /// Encode F64 comparison as Thumb-2: VCMP.F64 + VMRS + MOV #0 + IT + MOV #1
7365 fn encode_thumb_f64_compare(
7366 &self,
7367 rd: &Reg,
7368 dn: &VfpReg,
7369 dm: &VfpReg,
7370 cond_code: u32,
7371 ) -> Result<Vec<u8>> {
7372 let mut bytes = Vec::new();
7373 let rd_bits = reg_to_bits(rd);
7374
7375 // #712-class fix (found at f64-phase-2 wiring, #369): the 16-bit
7376 // `MOVS Rd,#0` is FLAG-SETTING. The original order emitted it AFTER
7377 // `VMRS APSR_nzcv, FPSCR`, clobbering the N/Z/C/V flags the VMRS just
7378 // transferred, so the following `IT<cond>` read stale flags and every
7379 // f64 comparison silently returned 0 — the exact bug the f32 compare
7380 // encoder shipped with and #712 fixed. Same fix: materialize the `#0`
7381 // FIRST (its flag side effect is overwritten by the VMRS), then
7382 // VCMP+VMRS set the flags the IT consumes. Pure reorder — sizes
7383 // unchanged.
7384
7385 // MOVS Rd, #0
7386 if rd_bits < 8 {
7387 let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
7388 bytes.extend_from_slice(&movs_zero.to_le_bytes());
7389 } else {
7390 let hw1: u16 = 0xF04F;
7391 let hw2: u16 = (rd_bits as u16) << 8;
7392 bytes.extend_from_slice(&hw1.to_le_bytes());
7393 bytes.extend_from_slice(&hw2.to_le_bytes());
7394 }
7395
7396 // VCMP.F64 Dn, Dm
7397 let dn_num = vfp_dreg_to_num(dn)?;
7398 let dm_num = vfp_dreg_to_num(dm)?;
7399 let (vd, d) = encode_dreg(dn_num);
7400 let (vm, m) = encode_dreg(dm_num);
7401 let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7402 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7403
7404 // VMRS APSR_nzcv, FPSCR (sets the flags the IT consumes)
7405 bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7406
7407 // IT<cond>
7408 let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7409 bytes.extend_from_slice(&it.to_le_bytes());
7410
7411 // MOV Rd, #1
7412 if rd_bits < 8 {
7413 let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7414 bytes.extend_from_slice(&mov_one.to_le_bytes());
7415 } else {
7416 let hw1: u16 = 0xF04F;
7417 let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7418 bytes.extend_from_slice(&hw1.to_le_bytes());
7419 bytes.extend_from_slice(&hw2.to_le_bytes());
7420 }
7421
7422 Ok(bytes)
7423 }
7424
7425 /// Encode F64 constant load as Thumb-2: MOVW+MOVT (lo32 into R0) + MOVW+MOVT (hi32 into R12) + VMOV Dd, R0, R12
7426 fn encode_thumb_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
7427 let mut bytes = Vec::new();
7428 let bits = value.to_bits();
7429 let lo32 = bits as u32;
7430 let hi32 = (bits >> 32) as u32;
7431
7432 // MOVW R0, #lo16(lo32)
7433 let lo16 = lo32 & 0xFFFF;
7434 bytes.extend_from_slice(&self.encode_thumb32_movw_raw(0, lo16)?);
7435
7436 // MOVT R0, #hi16(lo32)
7437 let hi16 = (lo32 >> 16) & 0xFFFF;
7438 bytes.extend_from_slice(&self.encode_thumb32_movt_raw(0, hi16)?);
7439
7440 // MOVW R12, #lo16(hi32)
7441 let lo16 = hi32 & 0xFFFF;
7442 bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
7443
7444 // MOVT R12, #hi16(hi32)
7445 let hi16 = (hi32 >> 16) & 0xFFFF;
7446 bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
7447
7448 // VMOV Dd, R0, R12
7449 let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
7450 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7451
7452 Ok(bytes)
7453 }
7454
7455 /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as Thumb-2
7456 /// Encode i32 → f64 conversion as Thumb-2. The integer stages through the
7457 /// DESTINATION's own low S-alias (`S(2d)`) — allocator-owned by
7458 /// definition — never S0 (which may hold a live value; the previous
7459 /// pseudo-op's S0 staging was the #615 class). Also fixes the SWAPPED
7460 /// signed/unsigned VCVT bases (bit7 = 1 is SIGNED — the same swap the f32
7461 /// twin had; latent here because f64.convert_i32_* was decode-dropped
7462 /// until #369): clang-verified vcvt.f64.s32 d1,s2 = eeb8 1bc1,
7463 /// vcvt.f64.u32 d1,s2 = eeb8 1b41.
7464 fn encode_thumb_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7465 let dd_num = vfp_dreg_to_num(dd)?;
7466 if dd_num > 7 {
7467 return Err(synth_core::Error::synthesis(format!(
7468 "F64ConvertI32: destination {dd:?} has no S-register alias \
7469 (D8..D15) — the selector allocates only D0..D7"
7470 )));
7471 }
7472 let mut bytes = Vec::new();
7473
7474 // VMOV S(2d), Rm — stage the integer in the destination's low word.
7475 let (vn_s, n_s) = encode_sreg(2 * dd_num);
7476 let rt = reg_to_bits(rm);
7477 let vmov = 0xEE000A10 | (vn_s << 16) | (rt << 12) | (n_s << 7);
7478 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7479
7480 // VCVT.F64.S32/U32 Dd, S(2d)
7481 let (vd, d) = encode_dreg(dd_num);
7482 let (vm, m) = encode_sreg(2 * dd_num);
7483 let base = if signed { 0xEEB80BC0 } else { 0xEEB80B40 };
7484 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7485 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7486
7487 Ok(bytes)
7488 }
7489
7490 /// Encode VCVT.F64.F32 Dd, Sm as Thumb-2
7491 fn encode_thumb_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7492 let dd_num = vfp_dreg_to_num(dd)?;
7493 let sm_num = vfp_sreg_to_num(sm)?;
7494 let (vd, d) = encode_dreg(dd_num);
7495 let (vm, m) = encode_sreg(sm_num);
7496
7497 let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7498 Ok(vfp_to_thumb_bytes(vcvt))
7499 }
7500
7501 /// Encode VCVT.F32.F64 Sd, Dm (f32.demote_f64) as Thumb-2 — single
7502 /// instruction, round-to-nearest-even per FPSCR default, exactly WASM
7503 /// §4.3.3 demote (clang-verified: vcvt.f32.f64 s1,d2 = eef7 0bc2).
7504 fn encode_thumb_f32_demote_f64(&self, sd: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7505 let sd_num = vfp_sreg_to_num(sd)?;
7506 let dm_num = vfp_dreg_to_num(dm)?;
7507 let (vd, d) = encode_sreg(sd_num);
7508 let (vm, m) = encode_dreg(dm_num);
7509
7510 let vcvt = 0xEEB70BC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7511 Ok(vfp_to_thumb_bytes(vcvt))
7512 }
7513
7514 /// Encode f64 → i32 truncation as Thumb-2 (round-toward-zero VCVT).
7515 ///
7516 /// The 32-bit result stages through the SOURCE's own low S-alias, `S(2m)`,
7517 /// clobbering half of an operand the selector has already popped. The
7518 /// overlapping write is well-defined: VCVT reads its source operand before
7519 /// writing (compilers emit `vcvt.f32.f64 s0, d0` routinely).
7520 ///
7521 /// # The one precondition, and who actually provides it
7522 ///
7523 /// `dm` must be a DEAD TEMP — never a pinned param/local home. That is the
7524 /// whole safety argument, and it is worth naming the guarantor precisely
7525 /// (#946): **`select_with_stack`** provides it, by copying a home into a
7526 /// fresh D-temp first. Visible in the shipped output for
7527 /// `(func (param f64) (result i32) (i32.trunc_f64_s (local.get 0)))`:
7528 ///
7529 /// ```text
7530 /// vmov r1, r2, d0 ; read the param out of its AAPCS-VFP home D0
7531 /// vmov d1, r1, r2 ; ...into a fresh D-temp
7532 /// vcvt.s32.f64 s2, d1 ; convert from the TEMP, staging into its own S2
7533 /// ```
7534 ///
7535 /// `InstructionSelector::select` / `select_default` do NOT provide it —
7536 /// `alloc_vfp_dreg` is a bare round-robin `(n + 1) % 16` with no liveness
7537 /// or home test. That path is not reachable from `synth compile`
7538 /// (`arm_backend.rs` calls `select_with_stack` exclusively; the only
7539 /// non-test caller of `select` is `examples/compile_add.rs`), so this is
7540 /// not a live miscompile — but a caller reaching that `pub` API directly
7541 /// gets no such guarantee.
7542 ///
7543 /// # What this deliberately does NOT claim
7544 ///
7545 /// An earlier version of this comment said the staging register is "never
7546 /// S0, which may hold an unrelated live value (the #615 class)". **That is
7547 /// false**, and measurably so: for
7548 /// `(func (result i32) (i32.trunc_f64_s (f64.const 3.7)))` the shipped
7549 /// compiler emits `vcvt.s32.f64 s0, d0`.
7550 ///
7551 /// It is also unnecessary. S0 is only dangerous as an *unrelated* scratch;
7552 /// here it is always the low half of `dm` itself, which the precondition
7553 /// above already makes dead. Naming a guard the code does not have — and
7554 /// does not need — invites a future reader to lean on it. The dead-temp
7555 /// precondition is the only thing holding this up.
7556 fn encode_thumb_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7557 let dm_num = vfp_dreg_to_num(dm)?;
7558 if dm_num > 7 {
7559 return Err(synth_core::Error::synthesis(format!(
7560 "I32TruncF64: source {dm:?} has no S-register alias \
7561 (D8..D15) — the selector allocates only D0..D7"
7562 )));
7563 }
7564 let mut bytes = Vec::new();
7565
7566 // VCVT.S32/U32.F64 S(2m), Dm (clang-verified:
7567 // vcvt.s32.f64 s1,d2 = eefd 0bc2 ; vcvt.u32.f64 s1,d2 = eefc 0bc2)
7568 let (vm, m) = encode_dreg(dm_num);
7569 let (vd_s, d_s) = encode_sreg(2 * dm_num);
7570 let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
7571 let vcvt = base | (d_s << 22) | (vd_s << 12) | (m << 5) | vm;
7572 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7573
7574 // VMOV Rd, S(2m)
7575 let rt = reg_to_bits(rd);
7576 let vmov = 0xEE100A10 | (vd_s << 16) | (rt << 12) | (d_s << 7);
7577 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7578
7579 Ok(bytes)
7580 }
7581
7582 /// Encode F64 rounding as a SINGLE Thumb-2 VRINT (FPv5 / cortex-m7dp).
7583 /// `mode` keeps the legacy FPSCR-RMode numbering of the callers —
7584 /// 0b00=nearest(ties-to-even)→VRINTN, 0b01=+inf(ceil)→VRINTP,
7585 /// 0b10=-inf(floor)→VRINTM, 0b11=zero(trunc)→VRINTZ — but the rounding
7586 /// mode is now ENCODED in the instruction, not smuggled through FPSCR.
7587 /// (The previous pseudo-op round-tripped through a 32-bit integer in S0:
7588 /// wrong for |x| >= 2^31, NaN/±inf collapsed to 0, -0.0 lost, and it
7589 /// CLOBBERED S0/R12 behind the allocator's back — the #615 class.)
7590 /// VRINT quietens an sNaN and preserves the sign of ±0.0/NaN per IEEE 754
7591 /// roundToIntegral, which is exactly WASM Core §4.3.3 f64.ceil/floor/
7592 /// trunc/nearest. VRINTN/P/M live in the FE "always-execute" space (never
7593 /// IT-conditional; none of these sequences emits them inside an IT block).
7594 fn encode_thumb_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7595 let dd_num = vfp_dreg_to_num(dd)?;
7596 let dm_num = vfp_dreg_to_num(dm)?;
7597 let (vd, d) = encode_dreg(dd_num);
7598 let (vm, m) = encode_dreg(dm_num);
7599 // clang-verified bases (thumbv7em, fpv5-d16):
7600 // vrintn.f64 d1,d2 = feb9 1b42 ; vrintp = feba 1b42
7601 // vrintm.f64 d1,d2 = febb 1b42 ; vrintz = eeb6 1bc2
7602 let base: u32 = match mode {
7603 0b00 => 0xFEB90B40, // VRINTN.F64 (round to nearest, ties to even)
7604 0b01 => 0xFEBA0B40, // VRINTP.F64 (round toward +inf)
7605 0b10 => 0xFEBB0B40, // VRINTM.F64 (round toward -inf)
7606 _ => 0xEEB60BC0, // VRINTZ.F64 (round toward zero)
7607 };
7608 Ok(vfp_to_thumb_bytes(
7609 base | (d << 22) | (vd << 12) | (m << 5) | vm,
7610 ))
7611 }
7612
7613 /// Encode F64 min/max as Thumb-2 with WASM Core §4.3.3 semantics:
7614 ///
7615 /// VCMP.F64 Dn, Dm ; VMRS APSR_nzcv, FPSCR
7616 /// VMINNM.F64/VMAXNM.F64 Dd, Dn, Dm (FPv5; -0.0 < +0.0 ordered)
7617 /// IT VS ; VADD.F64(VS) Dd, Dn, Dm (unordered ⇒ NaN-propagating)
7618 ///
7619 /// VMINNM/VMAXNM alone are IEEE minNum/maxNum, which return the NUMBER
7620 /// when exactly one operand is NaN — WASM requires NaN. The VS-guarded
7621 /// VADD overwrites the result with a quiet NaN whenever the compare was
7622 /// unordered (either operand NaN); on the ordered path VMINNM/VMAXNM
7623 /// order -0.0 below +0.0, matching WASM's min(+0,-0) = -0 / max = +0.
7624 /// Clobbers ONLY Dd and the flags (the previous pseudo-op's ordered IT
7625 /// GT/MI select returned the WRONG operand for NaN and ±0 mixes).
7626 ///
7627 /// Ok-or-Err: `dd` must not alias `dn`/`dm` — the VS fix-up reads them
7628 /// AFTER VMINNM wrote `dd` (the selector always allocates a fresh
7629 /// destination while both sources are still marked live).
7630 fn encode_thumb_f64_minmax(
7631 &self,
7632 dd: &VfpReg,
7633 dn: &VfpReg,
7634 dm: &VfpReg,
7635 is_min: bool,
7636 ) -> Result<Vec<u8>> {
7637 if dd == dn || dd == dm {
7638 return Err(synth_core::Error::synthesis(format!(
7639 "F64{}: destination {dd:?} aliases a source ({dn:?},{dm:?}) — \
7640 the unordered NaN fix-up would read a clobbered operand \
7641 (compiler bug: the selector must allocate a fresh D-temp)",
7642 if is_min { "Min" } else { "Max" },
7643 )));
7644 }
7645 let mut bytes = Vec::new();
7646 let dd_num = vfp_dreg_to_num(dd)?;
7647 let dn_num = vfp_dreg_to_num(dn)?;
7648 let dm_num = vfp_dreg_to_num(dm)?;
7649 let (vd, d) = encode_dreg(dd_num);
7650 let (vn, n) = encode_dreg(dn_num);
7651 let (vm, m) = encode_dreg(dm_num);
7652
7653 // VCMP.F64 Dn, Dm (clang-verified: vcmp.f64 d2,d3 = eeb4 2b43)
7654 let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7655 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7656 // VMRS APSR_nzcv, FPSCR
7657 bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7658 // VMINNM.F64 / VMAXNM.F64 Dd, Dn, Dm (clang-verified:
7659 // vminnm.f64 d1,d2,d3 = fe82 1b43 ; vmaxnm = fe82 1b03)
7660 let base: u32 = if is_min { 0xFE800B40 } else { 0xFE800B00 };
7661 let vnm = base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7662 bytes.extend_from_slice(&vfp_to_thumb_bytes(vnm));
7663 // IT VS (unordered ⇒ at least one NaN operand)
7664 bytes.extend_from_slice(&0xBF68_u16.to_le_bytes());
7665 // VADD.F64(VS) Dd, Dn, Dm — NaN + x propagates a quiet NaN
7666 let vadd = 0xEE300B00 | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7667 bytes.extend_from_slice(&vfp_to_thumb_bytes(vadd));
7668
7669 Ok(bytes)
7670 }
7671
7672 /// Encode F64 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7673 /// encoder scratch, #212), the flags, and Dd:
7674 ///
7675 /// VMOV R12, S(2m+1) (high word of the SIGN source Dm)
7676 /// CMP R12, #0 (N flag = the sign bit)
7677 /// VABS.F64 Dd, Dn (magnitude, sign cleared)
7678 /// IT MI ; VNEG.F64(MI) Dd, Dd
7679 ///
7680 /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7681 /// The R12 capture happens BEFORE Dd is written, so Dd aliasing Dn or Dm
7682 /// is safe. (The previous pseudo-op clobbered R0/R1/R2 behind the
7683 /// allocator's back — the #615 class.)
7684 fn encode_thumb_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7685 let dm_num = vfp_dreg_to_num(dm)?;
7686 if dm_num > 7 {
7687 return Err(synth_core::Error::synthesis(format!(
7688 "F64Copysign: sign source {dm:?} has no S-register alias \
7689 (D8..D15) — the selector allocates only D0..D7"
7690 )));
7691 }
7692 let mut bytes = Vec::new();
7693 // VMOV R12, S(2m+1) — the sign source's high word.
7694 let (vn_s, n_s) = encode_sreg(2 * dm_num + 1);
7695 let vmov = 0xEE100A10 | (vn_s << 16) | (12 << 12) | (n_s << 7);
7696 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7697 // CMP R12, #0 (T2: CMP.W R12, #0) — N = bit31 of the sign word.
7698 bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7699 bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7700 // VABS.F64 Dd, Dn
7701 let dd_num = vfp_dreg_to_num(dd)?;
7702 let dn_num = vfp_dreg_to_num(dn)?;
7703 let (vd, d) = encode_dreg(dd_num);
7704 let (vn, n) = encode_dreg(dn_num);
7705 let vabs = 0xEEB00BC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7706 bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7707 // IT MI ; VNEG.F64(MI) Dd, Dd
7708 bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7709 let vneg = 0xEEB10B40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7710 bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7711
7712 Ok(bytes)
7713 }
7714
7715 /// Encode VCVT.S32/U32.F32 + VMOV as Thumb-2
7716 fn encode_thumb_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7717 let mut bytes = Vec::new();
7718
7719 let sm_num = vfp_sreg_to_num(sm)?;
7720 let (vd, d) = encode_sreg(sm_num);
7721 let (vm, m) = encode_sreg(sm_num);
7722 let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
7723 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7724 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7725
7726 // VMOV Rd, Sm
7727 let vmov = encode_vmov_core_sreg(false, sm, rd)?;
7728 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7729
7730 Ok(bytes)
7731 }
7732
7733 // === Thumb-2 32-bit encoding helpers ===
7734
7735 /// Encode Thumb-2 32-bit ADD with immediate
7736 fn encode_thumb32_add(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7737 let rd_bits = reg_to_bits(rd);
7738 let rn_bits = reg_to_bits(rn);
7739
7740 // The `i:imm3:imm8` field is split the same way for both forms.
7741 let i_bit = (imm >> 11) & 1;
7742 let imm3 = (imm >> 8) & 0x7;
7743 let imm8 = imm & 0xFF;
7744
7745 let hw1_base = if imm <= 0xFF {
7746 // ADD.W (T3): the field is a ThumbExpandImm modified immediate. For
7747 // imm <= 0xFF (i:imm3 = 0000) it is the zero-extended byte, which is
7748 // correct — keep this form so existing encodings stay bit-identical.
7749 0xF100
7750 } else if imm <= 0xFFF {
7751 // ADDW (T4): a PLAIN 12-bit immediate (0..4095) — no ThumbExpandImm.
7752 // This is what makes `add sp, sp, #frame` correct for frame sizes
7753 // >= 256, which ADD.W (T3) would silently mis-encode (e.g. #256 -> #0).
7754 0xF200
7755 } else {
7756 return Err(synth_core::Error::synthesis(
7757 "ADD immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7758 ));
7759 };
7760
7761 let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7762 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7763
7764 let mut bytes = hw1.to_le_bytes().to_vec();
7765 bytes.extend_from_slice(&hw2.to_le_bytes());
7766 Ok(bytes)
7767 }
7768
7769 /// Encode Thumb-2 32-bit SUB with immediate
7770 fn encode_thumb32_sub(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7771 let rd_bits = reg_to_bits(rd);
7772 let rn_bits = reg_to_bits(rn);
7773
7774 let i_bit = (imm >> 11) & 1;
7775 let imm3 = (imm >> 8) & 0x7;
7776 let imm8 = imm & 0xFF;
7777
7778 let hw1_base = if imm <= 0xFF {
7779 // SUB.W (T3) modified immediate — correct for the zero-extended byte
7780 // (imm <= 0xFF). Kept bit-identical for existing encodings.
7781 0xF1A0
7782 } else if imm <= 0xFFF {
7783 // SUBW (T4): plain 12-bit immediate (0..4095). Makes
7784 // `sub sp, sp, #frame` correct for frame sizes >= 256.
7785 0xF2A0
7786 } else {
7787 return Err(synth_core::Error::synthesis(
7788 "SUB immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7789 ));
7790 };
7791
7792 let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7793 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7794
7795 let mut bytes = hw1.to_le_bytes().to_vec();
7796 bytes.extend_from_slice(&hw2.to_le_bytes());
7797 Ok(bytes)
7798 }
7799
7800 /// Encode Thumb-2 32-bit ADDS with immediate (sets flags)
7801 fn encode_thumb32_adds(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7802 let rd_bits = reg_to_bits(rd);
7803 let rn_bits = reg_to_bits(rn);
7804
7805 // ADDS.W (flag-setting) has only the modified-immediate form — error on
7806 // an un-encodable value rather than silently add the wrong constant.
7807 let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7808 synth_core::Error::synthesis(
7809 "ADDS immediate is not a valid ThumbExpandImm — materialize into a register",
7810 )
7811 })?;
7812 let i_bit = (field >> 11) & 1;
7813 let imm3 = (field >> 8) & 0x7;
7814 let imm8 = field & 0xFF;
7815
7816 // ADDS.W Rd, Rn, #imm (with S=1)
7817 // First halfword: 1111 0 i 0 1000 1 Rn = F110 | i<<10 | Rn
7818 let hw1: u16 = (0xF110 | (i_bit << 10) | rn_bits) as u16;
7819 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7820
7821 let mut bytes = hw1.to_le_bytes().to_vec();
7822 bytes.extend_from_slice(&hw2.to_le_bytes());
7823 Ok(bytes)
7824 }
7825
7826 /// Encode Thumb-2 32-bit SUBS with immediate (sets flags)
7827 fn encode_thumb32_subs(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7828 let rd_bits = reg_to_bits(rd);
7829 let rn_bits = reg_to_bits(rn);
7830
7831 // SUBS.W (flag-setting) has only the modified-immediate form — error on
7832 // an un-encodable value rather than silently subtract the wrong constant.
7833 let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7834 synth_core::Error::synthesis(
7835 "SUBS immediate is not a valid ThumbExpandImm — materialize into a register",
7836 )
7837 })?;
7838 let i_bit = (field >> 11) & 1;
7839 let imm3 = (field >> 8) & 0x7;
7840 let imm8 = field & 0xFF;
7841
7842 // SUBS.W Rd, Rn, #imm (with S=1)
7843 // First halfword: 1111 0 i 0 1101 1 Rn = F1B0 | i<<10 | Rn
7844 let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7845 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7846
7847 let mut bytes = hw1.to_le_bytes().to_vec();
7848 bytes.extend_from_slice(&hw2.to_le_bytes());
7849 Ok(bytes)
7850 }
7851
7852 /// Encode Thumb-2 32-bit MOVW (16-bit immediate)
7853 ///
7854 /// # Contract (Verus-style)
7855 /// ```text
7856 /// requires rd <= R14
7857 /// ensures result.len() == 4
7858 /// ensures (imm & 0xFFFF) can be reconstructed from the encoding
7859 /// ```
7860 fn encode_thumb32_movw(&self, rd: &Reg, imm: u32) -> Result<Vec<u8>> {
7861 let rd_bits = reg_to_bits(rd);
7862 reg_bits_checked(rd_bits)?;
7863 let imm16 = imm & 0xFFFF;
7864
7865 // MOVW Rd, #imm16
7866 // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7867 let imm4 = (imm16 >> 12) & 0xF;
7868 let i_bit = (imm16 >> 11) & 1;
7869 let imm3 = (imm16 >> 8) & 0x7;
7870 let imm8 = imm16 & 0xFF;
7871
7872 let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7873 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7874
7875 let mut bytes = hw1.to_le_bytes().to_vec();
7876 bytes.extend_from_slice(&hw2.to_le_bytes());
7877 encoding_contracts::verify_thumb32(&bytes);
7878 Ok(bytes)
7879 }
7880
7881 /// Encode Thumb-2 32-bit shift with immediate
7882 ///
7883 /// # Contract (Verus-style)
7884 /// ```text
7885 /// requires rd <= R14, rm <= R14
7886 /// ensures result.len() == 4
7887 /// ```
7888 fn encode_thumb32_shift(
7889 &self,
7890 rd: &Reg,
7891 rm: &Reg,
7892 shift: u32,
7893 shift_type: u8,
7894 ) -> Result<Vec<u8>> {
7895 let rd_bits = reg_to_bits(rd);
7896 let rm_bits = reg_to_bits(rm);
7897 reg_bits_checked(rd_bits)?;
7898 reg_bits_checked(rm_bits)?;
7899 let imm5 = shift & 0x1F;
7900 let imm2 = imm5 & 0x3;
7901 let imm3 = (imm5 >> 2) & 0x7;
7902
7903 // MOV.W Rd, Rm, <shift> #imm
7904 // EA4F 0 imm3 Rd imm2 type Rm
7905 let hw1: u16 = 0xEA4F;
7906 let hw2: u16 =
7907 ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | ((shift_type as u32) << 4) | rm_bits)
7908 as u16;
7909
7910 let mut bytes = hw1.to_le_bytes().to_vec();
7911 bytes.extend_from_slice(&hw2.to_le_bytes());
7912 Ok(bytes)
7913 }
7914
7915 /// Encode Thumb-2 32-bit shift by register
7916 /// Encoding: 11111010 0xx0 Rn | 1111 Rd 0000 Rm
7917 /// shift_type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
7918 fn encode_thumb32_shift_reg(
7919 &self,
7920 rd: &Reg,
7921 rn: &Reg,
7922 rm: &Reg,
7923 shift_type: u8,
7924 ) -> Result<Vec<u8>> {
7925 let rd_bits = reg_to_bits(rd);
7926 let rn_bits = reg_to_bits(rn);
7927 let rm_bits = reg_to_bits(rm);
7928
7929 // hw1: 1111 1010 0xx0 Rn
7930 let hw1: u16 = (0xFA00 | ((shift_type as u32) << 5) | rn_bits) as u16;
7931 // hw2: 1111 Rd 0000 Rm
7932 let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
7933
7934 let mut bytes = hw1.to_le_bytes().to_vec();
7935 bytes.extend_from_slice(&hw2.to_le_bytes());
7936 Ok(bytes)
7937 }
7938
7939 /// Encode Thumb-2 32-bit CMP with immediate
7940 fn encode_thumb32_cmp_imm(&self, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7941 let rn_bits = reg_to_bits(rn);
7942
7943 // CMP.W has only the modified-immediate form (no plain-imm12 like ADDW),
7944 // so an un-encodable immediate MUST be materialized into a register by
7945 // the selector. Error rather than silently compare the wrong constant.
7946 let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7947 synth_core::Error::synthesis(
7948 "CMP immediate is not a valid ThumbExpandImm — materialize into a register",
7949 )
7950 })?;
7951 let i_bit = (field >> 11) & 1;
7952 let imm3 = (field >> 8) & 0x7;
7953 let imm8 = field & 0xFF;
7954
7955 // CMP.W Rn, #imm
7956 let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7957 let hw2: u16 = ((imm3 << 12) | 0x0F00 | imm8) as u16;
7958
7959 let mut bytes = hw1.to_le_bytes().to_vec();
7960 bytes.extend_from_slice(&hw2.to_le_bytes());
7961 Ok(bytes)
7962 }
7963
7964 /// #372/#382: resolve the base register AND residual immediate offset for an
7965 /// `I64Ldr`/`I64Str` whose address may carry an index register. Returns
7966 /// `(base, low_offset)`; the caller accesses the halves at `[base,
7967 /// #low_offset]` and `[base, #low_offset + 4]`.
7968 ///
7969 /// - Frame access (no `offset_reg`, e.g. a spilled local at `[SP, #off]`):
7970 /// returns `(addr.base, off)` and emits NOTHING — byte-identical.
7971 /// - Memory access (`reg_imm(R11, addr, offset)` = `R11 + addr + offset`)
7972 /// with `offset + 4 <= 0xFFF`: emits `ADD.W ip, base, index` and returns
7973 /// `(ip, offset)`, folding `offset`/`offset+4` into the halves' imm12.
7974 /// Byte-identical to the pre-#382 (#372) behavior.
7975 /// - Memory access with `offset + 4 > 0xFFF`: the imm12 form cannot hold the
7976 /// high half's offset, so `encode_thumb32_ldr`'s `check_ldst_imm12` (#259)
7977 /// rightly refused it and the WHOLE function was skipped (#382). Instead
7978 /// MATERIALIZE the offset into the base: `ADD ip, index, #offset` (against
7979 /// the read-only INDEX register, so `encode_thumb32_add_imm` never trips its
7980 /// `rd==rn==R12` alias trap), then `ADD.W ip, ip, base` (+ R11), and return
7981 /// `(ip, 0)` so the halves use `[ip, #0]` / `[ip, #4]`.
7982 ///
7983 /// The effective address is fully materialized into `ip` BEFORE the halves
7984 /// are accessed, so an `rdlo` aliasing the index register is safe.
7985 ///
7986 /// RQ-63-ARMI64OFF (#1165): the wasm memarg is unsigned and reaches the
7987 /// encoder through the selector's `as i32` cast, so a memarg `>= 2^31`
7988 /// arrives NEGATIVE. It is read back as `u32` (lossless) and the
7989 /// materialization carries the full 32 bits — `base + index + offset (mod
7990 /// 2^32)`, the arithmetic the Thumb-2 word path (`encode_thumb32_add_imm`)
7991 /// and the software bounds guard already use for the same memarg. This
7992 /// used to CLAMP a negative offset to 0, so `i64.load offset=0xfffffff8`
7993 /// silently read `[R11 + addr]` — a wrong address, and a divergence from
7994 /// the i32 form of the same memarg. A FRAME access (no index) with an
7995 /// out-of-range or negative offset is refused by `check_ldst_imm12` in the
7996 /// halves — loud, never clamped.
7997 fn i64_effective_base(&self, bytes: &mut Vec<u8>, addr: &MemAddr) -> Result<(Reg, u32)> {
7998 let offset = addr.offset as u32;
7999 match addr.offset_reg {
8000 Some(idx) => {
8001 let ip = Reg::R12;
8002 // The high half sits at +4, which must itself fit imm12.
8003 if offset > 0xFFB {
8004 // Large static offset (#382): fold it (and R11) into ip so the
8005 // imm12 halves stay in range instead of skipping the function.
8006 // ADD ip, index, #offset (index != ip → no add_imm alias trap)
8007 bytes.extend_from_slice(&self.encode_thumb32_add_imm(&ip, &idx, offset)?);
8008 // ADD.W ip, ip, base (+ R11)
8009 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
8010 reg_to_bits(&ip),
8011 reg_to_bits(&ip),
8012 reg_to_bits(&addr.base),
8013 )?);
8014 Ok((ip, 0))
8015 } else {
8016 // ADD.W ip, addr.base, idx (Thumb-2, byte-verified vs as)
8017 let hw1: u16 = 0xEB00 | reg_to_bits(&addr.base) as u16;
8018 let hw2: u16 = 0x0C00 | reg_to_bits(&idx) as u16;
8019 bytes.extend_from_slice(&hw1.to_le_bytes());
8020 bytes.extend_from_slice(&hw2.to_le_bytes());
8021 Ok((ip, offset))
8022 }
8023 }
8024 None => Ok((addr.base, offset)),
8025 }
8026 }
8027
8028 /// Encode Thumb-2 32-bit LDR
8029 fn encode_thumb32_ldr(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8030 let rd_bits = reg_to_bits(rd);
8031 let base_bits = reg_to_bits(base);
8032
8033 // LDR.W Rd, [Rn, #imm12]
8034 check_ldst_imm12(offset)?;
8035 let hw1: u16 = (0xF8D0 | base_bits) as u16;
8036 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8037
8038 let mut bytes = hw1.to_le_bytes().to_vec();
8039 bytes.extend_from_slice(&hw2.to_le_bytes());
8040 Ok(bytes)
8041 }
8042
8043 /// Encode Thumb-2 32-bit STR
8044 fn encode_thumb32_str(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8045 let rd_bits = reg_to_bits(rd);
8046 let base_bits = reg_to_bits(base);
8047
8048 // STR.W Rd, [Rn, #imm12]
8049 check_ldst_imm12(offset)?;
8050 let hw1: u16 = (0xF8C0 | base_bits) as u16;
8051 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8052
8053 let mut bytes = hw1.to_le_bytes().to_vec();
8054 bytes.extend_from_slice(&hw2.to_le_bytes());
8055 Ok(bytes)
8056 }
8057
8058 /// Encode Thumb-2 32-bit LDR with register offset: LDR.W Rd, [Rn, Rm]
8059 fn encode_thumb32_ldr_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8060 let rd_bits = reg_to_bits(rd);
8061 let base_bits = reg_to_bits(base);
8062 let rm_bits = reg_to_bits(offset_reg);
8063
8064 // LDR.W Rd, [Rn, Rm, LSL #0]
8065 // Encoding: 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
8066 // imm2 = 00 for no shift (LSL #0)
8067 let hw1: u16 = (0xF850 | base_bits) as u16;
8068 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8069
8070 let mut bytes = hw1.to_le_bytes().to_vec();
8071 bytes.extend_from_slice(&hw2.to_le_bytes());
8072 Ok(bytes)
8073 }
8074
8075 /// Encode Thumb-2 32-bit STR with register offset: STR.W Rd, [Rn, Rm]
8076 fn encode_thumb32_str_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8077 let rd_bits = reg_to_bits(rd);
8078 let base_bits = reg_to_bits(base);
8079 let rm_bits = reg_to_bits(offset_reg);
8080
8081 // STR.W Rd, [Rn, Rm, LSL #0]
8082 // Encoding: 1111 1000 0100 Rn | Rt 0000 00 imm2 Rm
8083 // imm2 = 00 for no shift (LSL #0)
8084 let hw1: u16 = (0xF840 | base_bits) as u16;
8085 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8086
8087 let mut bytes = hw1.to_le_bytes().to_vec();
8088 bytes.extend_from_slice(&hw2.to_le_bytes());
8089 Ok(bytes)
8090 }
8091
8092 // === Sub-word load/store Thumb-2 encoding helpers ===
8093
8094 /// Encode Thumb-2 32-bit LDRB with immediate: LDRB.W Rd, [Rn, #imm12]
8095 fn encode_thumb32_ldrb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8096 let rd_bits = reg_to_bits(rd);
8097 let base_bits = reg_to_bits(base);
8098 // LDRB.W Rd, [Rn, #imm12]: 1111 1000 1001 Rn | Rt imm12
8099 check_ldst_imm12(offset)?;
8100 let hw1: u16 = (0xF890 | base_bits) as u16;
8101 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8102 let mut bytes = hw1.to_le_bytes().to_vec();
8103 bytes.extend_from_slice(&hw2.to_le_bytes());
8104 Ok(bytes)
8105 }
8106
8107 /// Encode Thumb-2 32-bit LDRB with register: LDRB.W Rd, [Rn, Rm]
8108 fn encode_thumb32_ldrb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8109 let rd_bits = reg_to_bits(rd);
8110 let base_bits = reg_to_bits(base);
8111 let rm_bits = reg_to_bits(offset_reg);
8112 // LDRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0001 Rn | Rt 0000 00 imm2 Rm
8113 let hw1: u16 = (0xF810 | base_bits) as u16;
8114 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8115 let mut bytes = hw1.to_le_bytes().to_vec();
8116 bytes.extend_from_slice(&hw2.to_le_bytes());
8117 Ok(bytes)
8118 }
8119
8120 /// Encode Thumb-2 32-bit LDRSB with immediate: LDRSB.W Rd, [Rn, #imm12]
8121 fn encode_thumb32_ldrsb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8122 let rd_bits = reg_to_bits(rd);
8123 let base_bits = reg_to_bits(base);
8124 // LDRSB.W Rd, [Rn, #imm12]: 1111 1001 1001 Rn | Rt imm12
8125 check_ldst_imm12(offset)?;
8126 let hw1: u16 = (0xF990 | base_bits) as u16;
8127 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8128 let mut bytes = hw1.to_le_bytes().to_vec();
8129 bytes.extend_from_slice(&hw2.to_le_bytes());
8130 Ok(bytes)
8131 }
8132
8133 /// Encode Thumb-2 32-bit LDRSB with register: LDRSB.W Rd, [Rn, Rm]
8134 fn encode_thumb32_ldrsb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8135 let rd_bits = reg_to_bits(rd);
8136 let base_bits = reg_to_bits(base);
8137 let rm_bits = reg_to_bits(offset_reg);
8138 // LDRSB.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0001 Rn | Rt 0000 00 imm2 Rm
8139 let hw1: u16 = (0xF910 | base_bits) as u16;
8140 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8141 let mut bytes = hw1.to_le_bytes().to_vec();
8142 bytes.extend_from_slice(&hw2.to_le_bytes());
8143 Ok(bytes)
8144 }
8145
8146 /// Encode Thumb-2 32-bit LDRH with immediate: LDRH.W Rd, [Rn, #imm12]
8147 fn encode_thumb32_ldrh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8148 let rd_bits = reg_to_bits(rd);
8149 let base_bits = reg_to_bits(base);
8150 // LDRH.W Rd, [Rn, #imm12]: 1111 1000 1011 Rn | Rt imm12
8151 check_ldst_imm12(offset)?;
8152 let hw1: u16 = (0xF8B0 | base_bits) as u16;
8153 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8154 let mut bytes = hw1.to_le_bytes().to_vec();
8155 bytes.extend_from_slice(&hw2.to_le_bytes());
8156 Ok(bytes)
8157 }
8158
8159 /// Encode Thumb-2 32-bit LDRH with register: LDRH.W Rd, [Rn, Rm]
8160 fn encode_thumb32_ldrh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8161 let rd_bits = reg_to_bits(rd);
8162 let base_bits = reg_to_bits(base);
8163 let rm_bits = reg_to_bits(offset_reg);
8164 // LDRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0011 Rn | Rt 0000 00 imm2 Rm
8165 let hw1: u16 = (0xF830 | base_bits) as u16;
8166 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8167 let mut bytes = hw1.to_le_bytes().to_vec();
8168 bytes.extend_from_slice(&hw2.to_le_bytes());
8169 Ok(bytes)
8170 }
8171
8172 /// Encode Thumb-2 32-bit LDRSH with immediate: LDRSH.W Rd, [Rn, #imm12]
8173 fn encode_thumb32_ldrsh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8174 let rd_bits = reg_to_bits(rd);
8175 let base_bits = reg_to_bits(base);
8176 // LDRSH.W Rd, [Rn, #imm12]: 1111 1001 1011 Rn | Rt imm12
8177 check_ldst_imm12(offset)?;
8178 let hw1: u16 = (0xF9B0 | base_bits) as u16;
8179 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8180 let mut bytes = hw1.to_le_bytes().to_vec();
8181 bytes.extend_from_slice(&hw2.to_le_bytes());
8182 Ok(bytes)
8183 }
8184
8185 /// Encode Thumb-2 32-bit LDRSH with register: LDRSH.W Rd, [Rn, Rm]
8186 fn encode_thumb32_ldrsh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8187 let rd_bits = reg_to_bits(rd);
8188 let base_bits = reg_to_bits(base);
8189 let rm_bits = reg_to_bits(offset_reg);
8190 // LDRSH.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0011 Rn | Rt 0000 00 imm2 Rm
8191 let hw1: u16 = (0xF930 | base_bits) as u16;
8192 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8193 let mut bytes = hw1.to_le_bytes().to_vec();
8194 bytes.extend_from_slice(&hw2.to_le_bytes());
8195 Ok(bytes)
8196 }
8197
8198 /// Encode Thumb-2 32-bit STRB with immediate: STRB.W Rd, [Rn, #imm12]
8199 fn encode_thumb32_strb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8200 let rd_bits = reg_to_bits(rd);
8201 let base_bits = reg_to_bits(base);
8202 // STRB.W Rd, [Rn, #imm12]: 1111 1000 1000 Rn | Rt imm12
8203 check_ldst_imm12(offset)?;
8204 let hw1: u16 = (0xF880 | base_bits) as u16;
8205 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8206 let mut bytes = hw1.to_le_bytes().to_vec();
8207 bytes.extend_from_slice(&hw2.to_le_bytes());
8208 Ok(bytes)
8209 }
8210
8211 /// Encode Thumb-2 32-bit STRB with register: STRB.W Rd, [Rn, Rm]
8212 fn encode_thumb32_strb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8213 let rd_bits = reg_to_bits(rd);
8214 let base_bits = reg_to_bits(base);
8215 let rm_bits = reg_to_bits(offset_reg);
8216 // STRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0000 Rn | Rt 0000 00 imm2 Rm
8217 let hw1: u16 = (0xF800 | base_bits) as u16;
8218 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8219 let mut bytes = hw1.to_le_bytes().to_vec();
8220 bytes.extend_from_slice(&hw2.to_le_bytes());
8221 Ok(bytes)
8222 }
8223
8224 /// Encode Thumb-2 32-bit STRH with immediate: STRH.W Rd, [Rn, #imm12]
8225 fn encode_thumb32_strh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8226 let rd_bits = reg_to_bits(rd);
8227 let base_bits = reg_to_bits(base);
8228 // STRH.W Rd, [Rn, #imm12]: 1111 1000 1010 Rn | Rt imm12
8229 check_ldst_imm12(offset)?;
8230 let hw1: u16 = (0xF8A0 | base_bits) as u16;
8231 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8232 let mut bytes = hw1.to_le_bytes().to_vec();
8233 bytes.extend_from_slice(&hw2.to_le_bytes());
8234 Ok(bytes)
8235 }
8236
8237 /// Encode Thumb-2 32-bit STRH with register: STRH.W Rd, [Rn, Rm]
8238 fn encode_thumb32_strh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8239 let rd_bits = reg_to_bits(rd);
8240 let base_bits = reg_to_bits(base);
8241 let rm_bits = reg_to_bits(offset_reg);
8242 // STRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0010 Rn | Rt 0000 00 imm2 Rm
8243 let hw1: u16 = (0xF820 | base_bits) as u16;
8244 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8245 let mut bytes = hw1.to_le_bytes().to_vec();
8246 bytes.extend_from_slice(&hw2.to_le_bytes());
8247 Ok(bytes)
8248 }
8249
8250 /// Encode Thumb-2 32-bit ADD with immediate: ADD.W Rd, Rn, #imm
8251 fn encode_thumb32_add_imm(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
8252 let rd_bits = reg_to_bits(rd);
8253 let rn_bits = reg_to_bits(rn);
8254
8255 // In-range immediates (<= 0xFFF) delegate to `encode_thumb32_add`,
8256 // which picks the correct form per value:
8257 // - imm <= 0xFF -> ADD.W (T3). Its `i:imm3:imm8` field is a
8258 // ThumbExpandImm MODIFIED immediate — raw == expanded only here.
8259 // - 0x100..=0xFFF -> ADDW (T4, 0xF200): a PLAIN 12-bit immediate.
8260 //
8261 // #681: this function used to pack the raw value into the T3 field for
8262 // ALL imm <= 0xFFF. ThumbExpandImm(0x200) = 0 and ThumbExpandImm(0x400)
8263 // = 0x8000_0000, so every dynamic-address load/store with a static
8264 // offset in 0x100..=0xFFF silently computed a WRONG address — and in
8265 // --safety-bounds software the guard checked the intended address while
8266 // the access used the mis-encoded one (bounds bypass). Same
8267 // ThumbExpandImm raw-packing class as #253/#255, reached via #382.
8268 if imm <= 0xFFF {
8269 self.encode_thumb32_add(rd, rn, imm)
8270 } else {
8271 // Out-of-range immediate (> 0xFFF): materialize it into a scratch
8272 // register, then ADD.W Rd, Rn, scratch. This is the #180/#185
8273 // "encoder must produce a legal sequence, not assert" class — see #350.
8274 //
8275 // Scratch choice (must NEVER equal Rn, or Rn would be clobbered before
8276 // the ADD reads it):
8277 // - rd != rn => use rd itself (rn is untouched, since rd != rn).
8278 // - rd == rn => use R12/IP (the reserved encoder scratch). rd/rn are
8279 // never R12 (R12 is non-allocatable), so it can't alias.
8280 //
8281 // The materialized value is the same whether or not MOVT is emitted, so
8282 // the byte length depends only on `imm` (and rd==rn) — the size probe and
8283 // the final emit therefore agree (mandatory: the function is encoded twice).
8284 let scratch: u32 = if rd_bits == rn_bits {
8285 12 // R12/IP — in-place add, can't use rd because rd == rn
8286 } else {
8287 rd_bits // rn is preserved because rd != rn
8288 };
8289 // Invariant: the scratch must never alias Rn (would clobber it before
8290 // the ADD reads it). Unreachable in real codegen (rd/rn are never R12,
8291 // which is reserved encoder scratch), but the encoder is also driven by
8292 // the `encoder_no_panic` fuzz harness with ARBITRARY registers — incl.
8293 // rd==rn==R12, which makes scratch (R12) alias Rn. The encoder contract
8294 // (#180/#185) is Ok-or-Err, never a panic, so return a typed error
8295 // instead of asserting. #350 follow-up.
8296 if scratch == rn_bits {
8297 return Err(synth_core::Error::synthesis(format!(
8298 "ADD #imm: cannot lower #{imm:#x} for Rd==Rn==R12 — no free scratch \
8299 register (R12 is the reserved encoder scratch and aliases Rn here)"
8300 )));
8301 }
8302
8303 let lo16 = imm & 0xFFFF;
8304 let hi16 = (imm >> 16) & 0xFFFF;
8305
8306 let mut bytes = self.encode_thumb32_movw_raw(scratch, lo16)?;
8307 if hi16 != 0 {
8308 bytes.extend_from_slice(&self.encode_thumb32_movt_raw(scratch, hi16)?);
8309 }
8310 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rn_bits, scratch)?);
8311 Ok(bytes)
8312 }
8313 }
8314
8315 // === Raw encoding helpers for POPCNT (take register numbers directly) ===
8316
8317 /// Encode Thumb-2 32-bit MOVW (16-bit immediate) - raw version
8318 ///
8319 /// # Contract (Verus-style)
8320 /// ```text
8321 /// requires rd <= 14, imm16 <= 0xFFFF
8322 /// ensures result.len() == 4
8323 /// ```
8324 fn encode_thumb32_movw_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8325 reg_bits_checked(rd)?;
8326 encoding_contracts::verify_imm16(imm16);
8327 // MOVW Rd, #imm16
8328 // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
8329 let imm16 = imm16 & 0xFFFF;
8330 let imm4 = (imm16 >> 12) & 0xF;
8331 let i_bit = (imm16 >> 11) & 1;
8332 let imm3 = (imm16 >> 8) & 0x7;
8333 let imm8 = imm16 & 0xFF;
8334
8335 let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
8336 let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8337
8338 let mut bytes = hw1.to_le_bytes().to_vec();
8339 bytes.extend_from_slice(&hw2.to_le_bytes());
8340 encoding_contracts::verify_thumb32(&bytes);
8341 Ok(bytes)
8342 }
8343
8344 /// Encode Thumb-2 32-bit MOVT (move top 16 bits) - raw version
8345 ///
8346 /// # Contract (Verus-style)
8347 /// ```text
8348 /// requires rd <= 14, imm16 <= 0xFFFF
8349 /// ensures result.len() == 4
8350 /// ```
8351 fn encode_thumb32_movt_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8352 reg_bits_checked(rd)?;
8353 encoding_contracts::verify_imm16(imm16);
8354 // MOVT Rd, #imm16
8355 // 1111 0 i 10 1 1 0 0 imm4 | 0 imm3 Rd imm8
8356 let imm16 = imm16 & 0xFFFF;
8357 let imm4 = (imm16 >> 12) & 0xF;
8358 let i_bit = (imm16 >> 11) & 1;
8359 let imm3 = (imm16 >> 8) & 0x7;
8360 let imm8 = imm16 & 0xFF;
8361
8362 let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
8363 let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8364
8365 let mut bytes = hw1.to_le_bytes().to_vec();
8366 bytes.extend_from_slice(&hw2.to_le_bytes());
8367 encoding_contracts::verify_thumb32(&bytes);
8368 Ok(bytes)
8369 }
8370
8371 /// Encode Thumb-2 32-bit LSR (logical shift right) with immediate - raw version
8372 fn encode_thumb32_lsr_raw(&self, rd: u32, rm: u32, shift: u32) -> Result<Vec<u8>> {
8373 // MOV.W Rd, Rm, LSR #imm
8374 // EA4F 0 imm3 Rd imm2 01 Rm
8375 let imm5 = shift & 0x1F;
8376 let imm2 = imm5 & 0x3;
8377 let imm3 = (imm5 >> 2) & 0x7;
8378
8379 let hw1: u16 = 0xEA4F;
8380 let hw2: u16 = ((imm3 << 12) | (rd << 8) | (imm2 << 6) | (0b01 << 4) | rm) as u16;
8381
8382 let mut bytes = hw1.to_le_bytes().to_vec();
8383 bytes.extend_from_slice(&hw2.to_le_bytes());
8384 Ok(bytes)
8385 }
8386
8387 /// Encode Thumb-2 32-bit AND with immediate - raw version
8388 fn encode_thumb32_and_imm_raw(&self, rd: u32, rn: u32, imm: u32) -> Result<Vec<u8>> {
8389 // AND.W Rd, Rn, #<modified_immediate>
8390 // F0 00 Rn | 0 imm3 Rd imm8
8391 //
8392 // #681 class audit: the field is a ThumbExpandImm modified immediate,
8393 // not a raw value. The only current caller (POPCNT final mask) passes
8394 // 0x3F, which expands to itself — the gate is byte-identical today and
8395 // closes the raw-packing landmine for any future caller.
8396 let field = try_thumb_expand_imm(imm).ok_or_else(|| {
8397 synth_core::Error::synthesis(
8398 "AND immediate is not a valid ThumbExpandImm — materialize into a register",
8399 )
8400 })?;
8401 let i_bit = (field >> 11) & 1;
8402 let imm3 = (field >> 8) & 0x7;
8403 let imm8 = field & 0xFF;
8404
8405 let hw1: u16 = (0xF000 | (i_bit << 10) | rn) as u16;
8406 let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8407
8408 let mut bytes = hw1.to_le_bytes().to_vec();
8409 bytes.extend_from_slice(&hw2.to_le_bytes());
8410 Ok(bytes)
8411 }
8412
8413 /// Encode Thumb-2 32-bit SUB (register) - raw version
8414 fn encode_thumb32_sub_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8415 // SUB.W Rd, Rn, Rm
8416 // EBA0 Rn | 0 Rd 00 00 Rm
8417 let hw1: u16 = (0xEBA0 | rn) as u16;
8418 let hw2: u16 = ((rd << 8) | rm) as u16;
8419
8420 let mut bytes = hw1.to_le_bytes().to_vec();
8421 bytes.extend_from_slice(&hw2.to_le_bytes());
8422 Ok(bytes)
8423 }
8424
8425 /// Encode Thumb-2 32-bit ADD (register) - raw version
8426 fn encode_thumb32_add_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8427 // ADD.W Rd, Rn, Rm
8428 // EB00 Rn | 0 Rd 00 00 Rm
8429 let hw1: u16 = (0xEB00 | rn) as u16;
8430 let hw2: u16 = ((rd << 8) | rm) as u16;
8431
8432 let mut bytes = hw1.to_le_bytes().to_vec();
8433 bytes.extend_from_slice(&hw2.to_le_bytes());
8434 Ok(bytes)
8435 }
8436
8437 /// Encode Thumb-2 32-bit ADDS (register, flag-setting) - raw version.
8438 /// Used as the high-register fallback for `ArmOp::Adds` (i64 low-word add)
8439 /// so R8-R11 pair operands don't overflow the 16-bit field — #178/#180.
8440 fn encode_thumb32_adds_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8441 // ADDS.W Rd, Rn, Rm (T3, S=1): EB10 Rn | 0 Rd 00 00 Rm
8442 let hw1: u16 = (0xEB10 | rn) as u16;
8443 let hw2: u16 = ((rd << 8) | rm) as u16;
8444 let mut bytes = hw1.to_le_bytes().to_vec();
8445 bytes.extend_from_slice(&hw2.to_le_bytes());
8446 Ok(bytes)
8447 }
8448
8449 /// Encode Thumb-2 32-bit SUBS (register, flag-setting) - raw version.
8450 /// High-register fallback for `ArmOp::Subs` (i64 low-word subtract) — #178/#180.
8451 fn encode_thumb32_subs_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8452 // SUBS.W Rd, Rn, Rm (T3, S=1): EBB0 Rn | 0 Rd 00 00 Rm
8453 let hw1: u16 = (0xEBB0 | rn) as u16;
8454 let hw2: u16 = ((rd << 8) | rm) as u16;
8455 let mut bytes = hw1.to_le_bytes().to_vec();
8456 bytes.extend_from_slice(&hw2.to_le_bytes());
8457 Ok(bytes)
8458 }
8459
8460 /// Encode a sequence of ARM instructions
8461 pub fn encode_sequence(&self, ops: &[ArmOp]) -> Result<Vec<u8>> {
8462 let mut code = Vec::new();
8463
8464 for op in ops {
8465 let encoded = self.encode(op)?;
8466 code.extend_from_slice(&encoded);
8467 }
8468
8469 Ok(code)
8470 }
8471}
8472
8473/// Convert register to bit encoding (0-15)
8474/// Reverse of the ARMv7-M `ThumbExpandImm`: given a 32-bit immediate, return the
8475/// 12-bit `i:imm3:imm8` field if it is a representable modified immediate, else
8476/// `None` (the caller must materialize the value into a register). This is the
8477/// shared correct path for the data-processing immediate encoders — without it
8478/// they pack raw bits and silently mis-encode any value `> 0xFF` that isn't a
8479/// modified immediate (the silent-miscompile class behind #251/#253/#255).
8480fn try_thumb_expand_imm(value: u32) -> Option<u32> {
8481 // i:imm3 = 0000 → 8-bit value, zero-extended (00000000 00000000 00000000 XY).
8482 if value <= 0xFF {
8483 return Some(value);
8484 }
8485 let b0 = value & 0xFF; // byte 0
8486 let b1 = (value >> 8) & 0xFF; // byte 1
8487 // 0x00XY00XY (i:imm3 = 0001) — XY in bytes 0 and 2
8488 if value == (b0 << 16) | b0 {
8489 return Some(0x100 | b0);
8490 }
8491 // 0xXY00XY00 (i:imm3 = 0010) — XY in bytes 1 and 3
8492 if value == (b1 << 24) | (b1 << 8) {
8493 return Some(0x200 | b1);
8494 }
8495 // 0xXYXYXYXY (i:imm3 = 0011) — XY in all four bytes
8496 if value == (b0 << 24) | (b0 << 16) | (b0 << 8) | b0 {
8497 return Some(0x300 | b0);
8498 }
8499 // An 8-bit value with bit 7 set, rotated right by 8..=31. `rotate_left(rot)`
8500 // undoes the encoded right rotation; if the result is `1bbbbbbb` (0x80..=0xFF)
8501 // the value is representable. imm12[11:7] = rot, imm12[6:0] = low 7 bits.
8502 for rot in 8..=31u32 {
8503 let unrot = value.rotate_left(rot);
8504 if (0x80..=0xFF).contains(&unrot) {
8505 return Some((rot << 7) | (unrot & 0x7F));
8506 }
8507 }
8508 None
8509}
8510
8511/// Guard a Thumb-2 `LDR/STR Rd, [Rn, #imm12]` offset. The imm12 form supports
8512/// `0..=4095`; a larger offset must be materialized into a register by the
8513/// selector (register-offset addressing). Returning `Err` rather than silently
8514/// masking `offset & 0xFFF` closes the wrong-address miscompile class (#259,
8515/// the load/store sibling of #253/#255).
8516fn check_ldst_imm12(offset: u32) -> Result<()> {
8517 if offset > 0xFFF {
8518 Err(synth_core::Error::synthesis(
8519 "load/store immediate offset > 0xFFF (4095) — materialize the offset into a register",
8520 ))
8521 } else {
8522 Ok(())
8523 }
8524}
8525
8526/// #916 — emit `Rd = 0` in Thumb-2, correctly for EVERY destination register.
8527///
8528/// The 16-bit `MOVS Rd, #imm8` (T1) is `0010 0 Rd(3) imm8` — the Rd field is
8529/// **three bits**. For R8-R12 `reg_to_bits` yields 8..12, so `rd_bits << 8`
8530/// overflows into bit 11 and `0x2000 | 0x0800` is `0x2800` = `CMP r0, #0`:
8531/// not a move at all. The destination is never written (it keeps stale data)
8532/// and the flags are clobbered. Same class as #180 / H-CODE-9, and the same
8533/// defect #311 fixed for `I64SetCond`.
8534///
8535/// High registers therefore take the 32-bit `MOV.W Rd, #imm8` (T2,
8536/// `F04F 0000 | Rd<<8 | imm8`), whose Rd field is four bits. `MOV.W` with S=0
8537/// does not set flags, which is what these zero-fill sites want anyway.
8538///
8539/// **Callers with branches must consult [`thumb_zero_fill_halfwords`].** This
8540/// emits 1 halfword for R0-R7 and 2 for R8-R12; any branch whose target lies
8541/// PAST this instruction moves when it widens and its displacement has to be
8542/// derived rather than hard-coded. (A branch targeting this instruction's own
8543/// address is unaffected — an instruction cannot move itself.)
8544fn emit_thumb_zero_fill(bytes: &mut Vec<u8>, rd_bits: u32) {
8545 if rd_bits < 8 {
8546 let movs: u16 = 0x2000 | ((rd_bits as u16) << 8);
8547 bytes.extend_from_slice(&movs.to_le_bytes());
8548 } else {
8549 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
8550 bytes.extend_from_slice(&((rd_bits as u16) << 8).to_le_bytes());
8551 }
8552}
8553
8554/// Halfword length of the encoding [`emit_thumb_zero_fill`] picks for
8555/// `rd_bits`. Branch displacements spanning the zero-fill derive from this so
8556/// the encoder cannot drift from itself (#916; the byte-size estimator mirrors
8557/// it in `synth_synthesis::estimate_arm_byte_size`, pinned by the #498
8558/// `estimator_encoder_agreement` oracle).
8559fn thumb_zero_fill_halfwords(rd_bits: u32) -> u16 {
8560 if rd_bits < 8 { 1 } else { 2 }
8561}
8562
8563fn reg_to_bits(reg: &Reg) -> u32 {
8564 match reg {
8565 Reg::R0 => 0,
8566 Reg::R1 => 1,
8567 Reg::R2 => 2,
8568 Reg::R3 => 3,
8569 Reg::R4 => 4,
8570 Reg::R5 => 5,
8571 Reg::R6 => 6,
8572 Reg::R7 => 7,
8573 Reg::R8 => 8,
8574 Reg::R9 => 9,
8575 Reg::R10 => 10,
8576 Reg::R11 => 11,
8577 Reg::R12 => 12,
8578 Reg::SP => 13,
8579 Reg::LR => 14,
8580 Reg::PC => 15,
8581 }
8582}
8583
8584// ======================================================================
8585// #610 — i64 fixed-ABI expansion wrappers.
8586//
8587// The hand-written multi-instruction i64 cores (rotl/rotr and the div/rem
8588// shift-subtract loops) compute in FIXED low registers. Before #610 the
8589// div/rem arms ignored their operand fields outright (hardcoded R0:R1 /
8590// R2:R3 in, result to R0:R1) and the rot arms used R3/R4 scratch that
8591// collided with selector-assigned registers — then restored the saved
8592// scratch OVER the result (`POP {R4}` with rd_lo == R4), so the op
8593// returned the caller's stale register: 0 for every input under qemu.
8594//
8595// These wrappers make each core honor its register parameters:
8596// 1. save R0-R3,
8597// 2. marshal the operand registers into the core's fixed input regs via
8598// the stack (permutation-safe: every source is read before any fixed
8599// register is written),
8600// 3. run the fixed-reg core (self-preserving for R4+; R12 is encoder
8601// scratch and never allocatable, #212),
8602// 4. MOV the result pair from R0:R1 into the selector's rd pair,
8603// 5. restore R0-R3, skipping any register the result now occupies.
8604//
8605// All emitted lengths are register-independent so the optimized path's
8606// byte-size estimator (`estimate_arm_byte_size`, pinned by the
8607// estimator↔encoder agreement oracle #498/#511) stays a constant per op.
8608// ======================================================================
8609
8610/// Steps 1+2: `PUSH {R0-R3}`, then marshal `srcs` (operand registers, any of
8611/// R0-R12) into `R0..R<n>` via individual stack pushes. Sources are all read
8612/// before any destination register is written, so arbitrary source/target
8613/// permutations (including operands living in R0-R3) are safe.
8614fn emit_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8615 debug_assert!(srcs.len() <= 4);
8616 // PUSH {R0-R3} — save the caller-visible low registers.
8617 bytes.extend_from_slice(&0xB40Fu16.to_le_bytes());
8618 // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8619 for src in srcs.iter().rev() {
8620 let rt = reg_to_bits(src) as u16;
8621 bytes.extend_from_slice(&0xF84Du16.to_le_bytes());
8622 bytes.extend_from_slice(&((rt << 12) | 0x0D04).to_le_bytes());
8623 }
8624 // POP {Ri} — Ri := srcs[i].
8625 for i in 0..srcs.len() as u16 {
8626 bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes());
8627 }
8628}
8629
8630/// Steps 4+5: move the core's R0:R1 result into the selector's rd pair, then
8631/// restore the R0-R3 saved by [`emit_i64_fixed_abi_entry`], skipping any
8632/// register the result now lives in (its saved caller word is discarded).
8633fn emit_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8634 let lo = reg_to_bits(rdlo);
8635 let hi = reg_to_bits(rdhi);
8636 if lo == 1 && hi == 0 {
8637 // A fully swapped pair would clobber one half in either MOV order.
8638 // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8639 return Err(synth_core::Error::synthesis(
8640 "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8641 ));
8642 }
8643 let mov16 = |bytes: &mut Vec<u8>, rd: u32, rm: u32| {
8644 let d = ((rd >> 3) & 1) as u16;
8645 bytes.extend_from_slice(
8646 &(0x4600u16 | (d << 7) | ((rm as u16) << 3) | ((rd & 7) as u16)).to_le_bytes(),
8647 );
8648 };
8649 if hi == 0 {
8650 // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8651 mov16(bytes, lo, 0);
8652 mov16(bytes, hi, 1);
8653 } else {
8654 // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8655 mov16(bytes, hi, 1);
8656 mov16(bytes, lo, 0);
8657 }
8658 for i in 0..4u32 {
8659 if i == lo || i == hi {
8660 // The result lives here — drop the saved caller word.
8661 bytes.extend_from_slice(&0xB001u16.to_le_bytes()); // ADD SP, #4
8662 } else {
8663 bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes()); // POP {Ri}
8664 }
8665 }
8666 Ok(())
8667}
8668
8669/// WASM `i64.div_*` / `i64.rem_*` by zero must trap, matching the i32 path's
8670/// cmp/bne/udf guard. Emitted after marshaling, when the divisor pair is in
8671/// R2:R3: `ORRS R12, R2, R3` — `BNE` over a `UDF #0` when nonzero.
8672fn emit_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8673 bytes.extend_from_slice(&0xEA52u16.to_le_bytes()); // ORRS.W R12, R2, R3
8674 bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8675 bytes.extend_from_slice(&0xD100u16.to_le_bytes()); // BNE.N +0 (skip the UDF)
8676 bytes.extend_from_slice(&0xDE00u16.to_le_bytes()); // UDF #0 — divide by zero
8677}
8678
8679/// WASM `i64.div_s(INT64_MIN, -1)` must trap (Core §4.3.2 `idiv_s`: the
8680/// quotient +2^63 is unrepresentable), matching the i32 path's overflow
8681/// guard — #633: without it the core negated INT64_MIN onto itself and
8682/// silently returned INT64_MIN. Emitted after marshaling, when the dividend
8683/// pair is in R0:R1 and the divisor pair in R2:R3; R12 is encoder scratch.
8684///
8685/// div_s ONLY — `i64.rem_s(INT64_MIN, -1)` is defined as 0 and must NOT
8686/// trap (`irem_s`), so the I64RemS arm never calls this. 22 bytes,
8687/// register-independent (estimator contract, #498/#511).
8688fn emit_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8689 // AND.W R12, R2, R3 — R12 == 0xFFFFFFFF iff divisor == -1
8690 bytes.extend_from_slice(&0xEA02u16.to_le_bytes());
8691 bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8692 // CMN.W R12, #1 — EQ iff both divisor words are all-ones
8693 bytes.extend_from_slice(&0xF11Cu16.to_le_bytes());
8694 bytes.extend_from_slice(&0x0F01u16.to_le_bytes());
8695 // BNE .no_trap
8696 bytes.extend_from_slice(&0xD105u16.to_le_bytes());
8697 // CMP R0, #0 — dividend lo word of INT64_MIN
8698 bytes.extend_from_slice(&0x2800u16.to_le_bytes());
8699 // BNE .no_trap
8700 bytes.extend_from_slice(&0xD103u16.to_le_bytes());
8701 // CMP.W R1, #0x80000000 — dividend hi word of INT64_MIN
8702 bytes.extend_from_slice(&0xF1B1u16.to_le_bytes());
8703 bytes.extend_from_slice(&0x4F00u16.to_le_bytes());
8704 // BNE .no_trap
8705 bytes.extend_from_slice(&0xD100u16.to_le_bytes());
8706 // UDF #0 — signed-division overflow
8707 bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
8708 // .no_trap:
8709}
8710
8711// ======================================================================
8712// #615 — A32 (ARM-mode) twins of the #610 i64 fixed-ABI wrappers above.
8713// Identical register contract, A32 encodings: the multi-instruction i64
8714// cores (rotl/rotr, div/rem) compute in fixed low registers (value/dividend
8715// R0:R1, amount R2 / divisor R2:R3, result to R0:R1); the wrappers marshal
8716// the selector-assigned operand registers in and the result out, saving and
8717// restoring the caller-visible R0-R3 around the core.
8718// ======================================================================
8719
8720/// A32 steps 1+2: `STMDB SP!, {R0-R3}`, then marshal `srcs` into `R0..R<n>`
8721/// via individual stack pushes (`STR src, [SP, #-4]!` in reverse order, then
8722/// `LDR Ri, [SP], #4`). Every source is read before any fixed register is
8723/// written, so arbitrary source/target permutations are safe.
8724fn emit_a32_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8725 debug_assert!(srcs.len() <= 4);
8726 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8727 // PUSH {R0-R3} — save the caller-visible low registers.
8728 w(bytes, 0xE92D_000F);
8729 // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8730 for src in srcs.iter().rev() {
8731 w(bytes, 0xE52D_0004 | (reg_to_bits(src) << 12));
8732 }
8733 // LDR Ri, [SP], #4 — Ri := srcs[i].
8734 for i in 0..srcs.len() as u32 {
8735 w(bytes, 0xE49D_0004 | (i << 12));
8736 }
8737}
8738
8739/// A32 steps 4+5: move the core's R0:R1 result into the selector's rd pair,
8740/// then restore the R0-R3 saved by [`emit_a32_i64_fixed_abi_entry`], skipping
8741/// any register the result now lives in (its saved caller word is discarded).
8742fn emit_a32_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8743 let lo = reg_to_bits(rdlo);
8744 let hi = reg_to_bits(rdhi);
8745 if lo == 1 && hi == 0 {
8746 // A fully swapped pair would clobber one half in either MOV order.
8747 // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8748 return Err(synth_core::Error::synthesis(
8749 "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8750 ));
8751 }
8752 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8753 let mov = |bytes: &mut Vec<u8>, rd: u32, rm: u32| w(bytes, 0xE1A0_0000 | (rd << 12) | rm);
8754 if hi == 0 {
8755 // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8756 mov(bytes, lo, 0);
8757 mov(bytes, hi, 1);
8758 } else {
8759 // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8760 mov(bytes, hi, 1);
8761 mov(bytes, lo, 0);
8762 }
8763 for i in 0..4u32 {
8764 if i == lo || i == hi {
8765 // The result lives here — drop the saved caller word.
8766 w(bytes, 0xE28D_D004); // ADD SP, SP, #4
8767 } else {
8768 w(bytes, 0xE49D_0004 | (i << 12)); // LDR Ri, [SP], #4
8769 }
8770 }
8771 Ok(())
8772}
8773
8774/// A32 zero-divisor trap, emitted after marshaling when the divisor pair is
8775/// in R2:R3: `ORRS R12, R2, R3` sets Z iff the divisor is zero; `BNE` skips a
8776/// `UDF #0` (WASM div/rem-by-zero must trap, matching the Thumb-2 twin).
8777fn emit_a32_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8778 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8779 w(bytes, 0xE192_C003); // ORRS R12, R2, R3
8780 w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8781 w(bytes, 0xE7F0_00F0); // UDF #0 — divide by zero
8782}
8783
8784/// A32 twin of [`emit_i64_divs_overflow_trap`] (#633): trap on
8785/// `i64.div_s(INT64_MIN, -1)`. Conditional execution replaces the Thumb
8786/// branches — the CMPEQ chain leaves EQ set only when divisor == -1 AND
8787/// dividend == INT64_MIN. div_s only; rem_s must keep returning 0.
8788fn emit_a32_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8789 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8790 w(bytes, 0xE002_C003); // AND R12, R2, R3 (== 0xFFFFFFFF iff divisor == -1)
8791 w(bytes, 0xE37C_0001); // CMN R12, #1 (EQ iff divisor == -1)
8792 w(bytes, 0x0350_0000); // CMPEQ R0, #0 (EQ iff also dividend lo == 0)
8793 w(bytes, 0x0351_0102); // CMPEQ R1, #0x80000000 (EQ iff dividend == INT64_MIN)
8794 w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8795 w(bytes, 0xE7F0_00F0); // UDF #0 — signed-division overflow
8796}
8797
8798/// Fallible form of the `verify_reg_bits` contract. PC (R15) is not a valid
8799/// data operand for the Thumb-2 encodings that use this guard (SDIV/UDIV/MLS/…
8800/// are UNPREDICTABLE with PC). Synth's own codegen never emits PC there, but
8801/// the encoder must stay *total* over arbitrary `ArmOp` inputs — the fuzz
8802/// harness (`encoder_no_panic`) requires Ok-or-Err, never a panic. Pre-fix, the
8803/// `debug_assert` in `verify_reg_bits` aborted under `-Cdebug-assertions`.
8804/// Returns a typed Err instead. See #185.
8805fn reg_bits_checked(bits: u32) -> Result<()> {
8806 if bits > 14 {
8807 return Err(synth_core::Error::synthesis(format!(
8808 "register bits {bits} (PC/R15) is not a valid operand for this Thumb-2 encoding"
8809 )));
8810 }
8811 Ok(())
8812}
8813
8814/// Try to encode a 32-bit value as an ARM rotated immediate (imm8 ROR 2*rot4).
8815/// Returns Some((encoded_bits, 1)) if representable, None otherwise.
8816fn try_encode_rotated_imm(val: u32) -> Option<(u32, u32)> {
8817 if val == 0 {
8818 return Some((0, 1));
8819 }
8820 for rot in 0..16u32 {
8821 let shift = rot * 2;
8822 // Rotate left by shift (undo the ROR) to see if result fits in 8 bits
8823 let unrotated = val.rotate_left(shift);
8824 if unrotated <= 0xFF {
8825 // Encoded as: rot4(4 bits) | imm8(8 bits) = rotate_imm << 8 | imm8
8826 return Some(((rot << 8) | unrotated, 1));
8827 }
8828 }
8829 None
8830}
8831
8832/// Encode operand2 field and return (bits, immediate_flag).
8833/// For ARM32 mode, immediates use the rotated-immediate encoding (imm8 ROR 2*rot4).
8834/// Panics if an immediate value cannot be represented. Callers that need large
8835/// immediates should use MOVW/MOVT instead of Operand2::Imm.
8836fn encode_operand2(op2: &Operand2) -> Result<(u32, u32)> {
8837 match op2 {
8838 Operand2::Imm(val) => {
8839 let uval = *val as u32;
8840 // Attempt rotated-immediate encoding (ARM32 Operand2)
8841 if let Some(encoded) = try_encode_rotated_imm(uval) {
8842 Ok(encoded)
8843 } else {
8844 // #378-class honesty: an immediate that can't be expressed as an
8845 // ARM32 rotated immediate is an INTERNAL selector bug — large
8846 // constants must be materialized via MOVW/MOVT, not passed here.
8847 // FAIL HONESTLY with an Err rather than silently masking to
8848 // `uval & 0xFF` and emitting a WRONG immediate. The encoder is
8849 // Ok-or-Err, never corrupt (#180/#185); a loud Err is also why
8850 // this is an Err and not a panic (the `encoder_no_panic` fuzz
8851 // contract — malformed/oversized input must degrade, not crash).
8852 Err(synth_core::Error::synthesis(format!(
8853 "encode_operand2: immediate {uval:#x} ({val}) is not an ARM32 \
8854 rotated immediate — the selector must materialize large \
8855 constants via MOVW/MOVT"
8856 )))
8857 }
8858 }
8859
8860 Operand2::Reg(reg) => {
8861 let reg_bits = reg_to_bits(reg);
8862 Ok((reg_bits, 0)) // I=0 for register
8863 }
8864
8865 Operand2::RegShift {
8866 rm,
8867 shift: _,
8868 amount,
8869 } => {
8870 // Simplified encoding with shift
8871 let rm_bits = reg_to_bits(rm);
8872 let shift_bits = (*amount & 0x1F) << 7;
8873 Ok((shift_bits | rm_bits, 0))
8874 }
8875 }
8876}
8877
8878/// Largest immediate an A32 `LDR`/`STR`/`LDRB`/`STRB` `[Rn, #imm12]` form holds.
8879const A32_LDST_IMM12_MAX: u32 = 0xFFF;
8880/// Largest immediate an A32 `LDRH`/`STRH`/`LDRSB`/`LDRSH` `[Rn, #imm8]`
8881/// (`imm4H:imm4L`) form holds.
8882const A32_LDST_IMM8_MAX: u32 = 0xFF;
8883/// Largest LOW-half immediate an A32 `I64Ldr`/`I64Str` pair can fold: the high
8884/// half sits at `+4`, which must itself fit imm12 (`0xFFB + 4 = 0xFFF`).
8885const A32_I64_PAIR_IMM12_MAX: u32 = 0xFFB;
8886
8887/// RQ-63-ARMI64OFF (#1165): resolve the A32 base register and residual
8888/// immediate for a load/store whose static offset may exceed the form's
8889/// immediate field — the A32 counterpart of the Thumb-2 `i64_effective_base`
8890/// (#372/#382). Returns `(base, residual)`; the caller encodes `[base,
8891/// #residual]` (and `#residual + 4` for the i64 high half).
8892///
8893/// The offset is the wasm memarg round-tripped through the selector's `as i32`
8894/// cast (a memarg `>= 2^31` arrives negative), so it is read back as `u32` and
8895/// the effective address is `base + index + offset (mod 2^32)` — the same
8896/// arithmetic the Thumb-2 word path (`encode_thumb32_add_imm`) and the
8897/// `--safety-bounds software` guard (`software_bounds_guard`, which round-trips
8898/// the same cast) already use. A clamp or a mask here is a WRONG ADDRESS, i.e.
8899/// the silent memory miscompile this artifact exists to refuse: before this
8900/// helper the A32 immediate arms masked `& 0xFFF` (word/byte) and `& 0xFF`
8901/// (halfword/signed), so `i32.load offset=5000` on the `-b arm` default target
8902/// silently read `[ip, #904]`, and the i64 pair arm declined outright (46 of
8903/// the 110 core-module declines in v0.62's ARM census).
8904///
8905/// - `offset <= imm_max`, no index: emits nothing, returns `(addr.base, offset)`
8906/// — byte-identical to the pre-#1165 immediate form.
8907/// - `offset <= imm_max`, index `rm`: emits `ADD ip, base, rm`, returns
8908/// `(ip, offset)` — byte-identical to the #206 register-offset form.
8909/// - `offset > imm_max`: MATERIALIZES the whole effective base into `ip`:
8910/// `MOVW ip, #lo16 ; [MOVT ip, #hi16 — only when non-zero] ; ADD ip, ip, base
8911/// ; [ADD ip, ip, rm]`, returns `(ip, 0)`. `ip` is fully computed BEFORE the
8912/// access, so a destination register aliasing `base` or `rm` is safe.
8913///
8914/// `ip` (R12) is the reserved encoder scratch — never allocated (pool R0–R8;
8915/// R9/R10/R11 are the globals/size/base contract registers). If `base` or `rm`
8916/// IS R12 the materialization would clobber its own input, so that case is a
8917/// typed `Err` — a loud decline, never a wrong address (the alias trap
8918/// `encode_thumb32_add_imm` carries for `rd == rn == R12`).
8919///
8920/// Bytes verified against `arm-none-eabi-as` in the `test_1165_a32_*` tests
8921/// and executed by `scripts/repro/a32_ldst_offset_1165_differential.py`.
8922fn a32_effective_base(bytes: &mut Vec<u8>, addr: &MemAddr, imm_max: u32) -> Result<(Reg, u32)> {
8923 let ip = Reg::R12;
8924 let ip_bits = reg_to_bits(&ip);
8925 let offset = addr.offset as u32;
8926 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8927 // ADD rd, rn, rm (cond=AL, opcode=ADD, S=0, register operand2).
8928 let add_reg = |rd: u32, rn: u32, rm: u32| 0xE080_0000 | (rn << 16) | (rd << 12) | rm;
8929 if offset <= imm_max {
8930 return Ok(match addr.offset_reg {
8931 Some(rm) => {
8932 w(
8933 bytes,
8934 add_reg(ip_bits, reg_to_bits(&addr.base), reg_to_bits(&rm)),
8935 );
8936 (ip, offset)
8937 }
8938 None => (addr.base, offset),
8939 });
8940 }
8941 if addr.base == ip || addr.offset_reg == Some(ip) {
8942 return Err(synth_core::Error::synthesis(format!(
8943 "A32 load/store offset {offset:#x} exceeds the immediate field (max {imm_max:#x}) \
8944 and the address already uses the R12 scratch — no free register to \
8945 materialize it (RQ-63-ARMI64OFF)"
8946 )));
8947 }
8948 // MOVW ip, #lo16 ; MOVT ip, #hi16 (elided when zero, mirroring I64Const).
8949 let lo16 = offset & 0xFFFF;
8950 let hi16 = offset >> 16;
8951 w(
8952 bytes,
8953 0xE300_0000 | ((lo16 >> 12) << 16) | (ip_bits << 12) | (lo16 & 0xFFF),
8954 );
8955 if hi16 != 0 {
8956 w(
8957 bytes,
8958 0xE340_0000 | ((hi16 >> 12) << 16) | (ip_bits << 12) | (hi16 & 0xFFF),
8959 );
8960 }
8961 w(bytes, add_reg(ip_bits, ip_bits, reg_to_bits(&addr.base)));
8962 if let Some(rm) = addr.offset_reg {
8963 w(bytes, add_reg(ip_bits, ip_bits, reg_to_bits(&rm)));
8964 }
8965 Ok((ip, 0))
8966}
8967
8968/// Encode an A32 immediate-form memory address to `(base_reg, imm12)`.
8969///
8970/// RQ-63-ARMI64OFF (#1165): this used to mask `& 0xFFF`, silently re-targeting
8971/// any offset past the field (the #259 class, closed on Thumb-2 by
8972/// `check_ldst_imm12` but never here). The immediate arms are only reached with
8973/// an in-range, index-free address — `encode_arm_reg_offset_mem` materializes
8974/// everything else into IP first — so this is a TRIPWIRE: a typed error, never
8975/// a wrong address, should a future arm bypass that pre-pass.
8976fn encode_mem_addr(addr: &MemAddr) -> Result<(u32, u32)> {
8977 check_a32_imm_addr(addr, A32_LDST_IMM12_MAX)
8978}
8979
8980/// `encode_mem_addr` for the `imm4H:imm4L` (8-bit) forms — `LDRH`/`STRH`/
8981/// `LDRSB`/`LDRSH`. Same tripwire contract; the former `& 0xFF` mask is gone.
8982fn encode_mem_addr_imm8(addr: &MemAddr) -> Result<(u32, u32)> {
8983 check_a32_imm_addr(addr, A32_LDST_IMM8_MAX)
8984}
8985
8986fn check_a32_imm_addr(addr: &MemAddr, imm_max: u32) -> Result<(u32, u32)> {
8987 if addr.offset_reg.is_some() {
8988 return Err(synth_core::Error::synthesis(
8989 "internal: A32 immediate-form load/store reached with a register offset — \
8990 encode_arm_reg_offset_mem must materialize it first (#206)",
8991 ));
8992 }
8993 let offset = addr.offset as u32;
8994 if offset > imm_max {
8995 return Err(synth_core::Error::synthesis(format!(
8996 "internal: A32 load/store immediate offset {offset:#x} exceeds the {imm_max:#x} \
8997 field — encode_arm_reg_offset_mem must materialize it first (RQ-63-ARMI64OFF)"
8998 )));
8999 }
9000 Ok((reg_to_bits(&addr.base), offset))
9001}
9002
9003/// S-register number: S0=0, S1=1, ..., S31=31
9004fn vfp_sreg_to_num(reg: &VfpReg) -> Result<u32> {
9005 match reg {
9006 VfpReg::S0 => Ok(0),
9007 VfpReg::S1 => Ok(1),
9008 VfpReg::S2 => Ok(2),
9009 VfpReg::S3 => Ok(3),
9010 VfpReg::S4 => Ok(4),
9011 VfpReg::S5 => Ok(5),
9012 VfpReg::S6 => Ok(6),
9013 VfpReg::S7 => Ok(7),
9014 VfpReg::S8 => Ok(8),
9015 VfpReg::S9 => Ok(9),
9016 VfpReg::S10 => Ok(10),
9017 VfpReg::S11 => Ok(11),
9018 VfpReg::S12 => Ok(12),
9019 VfpReg::S13 => Ok(13),
9020 VfpReg::S14 => Ok(14),
9021 VfpReg::S15 => Ok(15),
9022 VfpReg::S16 => Ok(16),
9023 VfpReg::S17 => Ok(17),
9024 VfpReg::S18 => Ok(18),
9025 VfpReg::S19 => Ok(19),
9026 VfpReg::S20 => Ok(20),
9027 VfpReg::S21 => Ok(21),
9028 VfpReg::S22 => Ok(22),
9029 VfpReg::S23 => Ok(23),
9030 VfpReg::S24 => Ok(24),
9031 VfpReg::S25 => Ok(25),
9032 VfpReg::S26 => Ok(26),
9033 VfpReg::S27 => Ok(27),
9034 VfpReg::S28 => Ok(28),
9035 VfpReg::S29 => Ok(29),
9036 VfpReg::S30 => Ok(30),
9037 VfpReg::S31 => Ok(31),
9038 // D-registers are not used in F32 single-precision encodings
9039 _ => Err(synth_core::Error::SynthesisError(
9040 "D-register not supported in single-precision VFP encoding".to_string(),
9041 )),
9042 }
9043}
9044
9045/// D-register number: D0=0, D1=1, ..., D15=15
9046fn vfp_dreg_to_num(reg: &VfpReg) -> Result<u32> {
9047 match reg {
9048 VfpReg::D0 => Ok(0),
9049 VfpReg::D1 => Ok(1),
9050 VfpReg::D2 => Ok(2),
9051 VfpReg::D3 => Ok(3),
9052 VfpReg::D4 => Ok(4),
9053 VfpReg::D5 => Ok(5),
9054 VfpReg::D6 => Ok(6),
9055 VfpReg::D7 => Ok(7),
9056 VfpReg::D8 => Ok(8),
9057 VfpReg::D9 => Ok(9),
9058 VfpReg::D10 => Ok(10),
9059 VfpReg::D11 => Ok(11),
9060 VfpReg::D12 => Ok(12),
9061 VfpReg::D13 => Ok(13),
9062 VfpReg::D14 => Ok(14),
9063 VfpReg::D15 => Ok(15),
9064 // S-registers are not used in F64 double-precision encodings
9065 _ => Err(synth_core::Error::SynthesisError(
9066 "S-register not supported in double-precision VFP encoding".to_string(),
9067 )),
9068 }
9069}
9070
9071/// Split S-register into (Vx[3:0], qualifier_bit) for VFP encoding.
9072/// For an S-register number s: Vx = s >> 1, qualifier = s & 1.
9073/// The qualifier bit goes to D (bit 22), N (bit 7), or M (bit 5) depending on role.
9074fn encode_sreg(s: u32) -> (u32, u32) {
9075 (s >> 1, s & 1)
9076}
9077
9078/// Split D-register into (Vx[3:0], qualifier_bit) for VFP double-precision encoding.
9079/// For a D-register number d: Vx = d & 0xF, qualifier = (d >> 4) & 1.
9080/// For D0-D15, qualifier is always 0.
9081fn encode_dreg(d: u32) -> (u32, u32) {
9082 (d & 0xF, (d >> 4) & 1)
9083}
9084
9085/// Encode a VFP 3-register arithmetic instruction (VADD.F32, VSUB.F32, VMUL.F32, VDIV.F32).
9086/// Returns the full 32-bit instruction word.
9087///
9088/// VFP encoding: [cond 1110] [D opc1 Vn] [Vd 101 sz] [N opc2 M 0 Vm]
9089/// For single-precision (sz=0), coprocessor = 0xA (bits[11:8]).
9090fn encode_vfp_3reg(base: u32, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<u32> {
9091 let sd_num = vfp_sreg_to_num(sd)?;
9092 let sn_num = vfp_sreg_to_num(sn)?;
9093 let sm_num = vfp_sreg_to_num(sm)?;
9094 let (vd, d) = encode_sreg(sd_num);
9095 let (vn, n) = encode_sreg(sn_num);
9096 let (vm, m) = encode_sreg(sm_num);
9097
9098 Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
9099}
9100
9101/// Encode a VFP 2-register instruction (VNEG.F32, VABS.F32, VSQRT.F32).
9102/// Returns the full 32-bit instruction word.
9103fn encode_vfp_2reg(base: u32, sd: &VfpReg, sm: &VfpReg) -> Result<u32> {
9104 let sd_num = vfp_sreg_to_num(sd)?;
9105 let sm_num = vfp_sreg_to_num(sm)?;
9106 let (vd, d) = encode_sreg(sd_num);
9107 let (vm, m) = encode_sreg(sm_num);
9108
9109 Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
9110}
9111
9112/// Encode a VFP load/store (VLDR.F32 / VSTR.F32).
9113/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
9114/// U bit (bit 23) controls add/subtract offset.
9115fn encode_vfp_ldst(base: u32, sd: &VfpReg, addr: &MemAddr) -> Result<u32> {
9116 let sd_num = vfp_sreg_to_num(sd)?;
9117 let (vd, d) = encode_sreg(sd_num);
9118 let rn = reg_to_bits(&addr.base);
9119
9120 let offset = addr.offset;
9121 let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9122 let abs_offset = offset.unsigned_abs();
9123 let imm8 = (abs_offset / 4) & 0xFF;
9124
9125 Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
9126}
9127
9128/// Encode VMOV between core register and S-register.
9129/// VMOV Sn, Rt: 0xEE00_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
9130/// VMOV Rt, Sn: 0xEE10_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
9131fn encode_vmov_core_sreg(to_sreg: bool, sreg: &VfpReg, core: &Reg) -> Result<u32> {
9132 let s_num = vfp_sreg_to_num(sreg)?;
9133 let (vn, n) = encode_sreg(s_num);
9134 let rt = reg_to_bits(core);
9135
9136 let base = if to_sreg { 0xEE000A10 } else { 0xEE100A10 };
9137 Ok(base | (vn << 16) | (rt << 12) | (n << 7))
9138}
9139
9140/// Encode a VFP 3-register double-precision instruction (VADD.F64, VSUB.F64, etc.).
9141/// For double-precision (sz=1), coprocessor = 0xB (bits[11:8]).
9142/// The base should have bit 8 = 1 for F64 (0xB suffix instead of 0xA).
9143fn encode_vfp_3reg_f64(base: u32, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<u32> {
9144 let dd_num = vfp_dreg_to_num(dd)?;
9145 let dn_num = vfp_dreg_to_num(dn)?;
9146 let dm_num = vfp_dreg_to_num(dm)?;
9147 let (vd, d) = encode_dreg(dd_num);
9148 let (vn, n) = encode_dreg(dn_num);
9149 let (vm, m) = encode_dreg(dm_num);
9150
9151 Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
9152}
9153
9154/// Encode a VFP 2-register double-precision instruction (VNEG.F64, VABS.F64, VSQRT.F64).
9155fn encode_vfp_2reg_f64(base: u32, dd: &VfpReg, dm: &VfpReg) -> Result<u32> {
9156 let dd_num = vfp_dreg_to_num(dd)?;
9157 let dm_num = vfp_dreg_to_num(dm)?;
9158 let (vd, d) = encode_dreg(dd_num);
9159 let (vm, m) = encode_dreg(dm_num);
9160
9161 Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
9162}
9163
9164/// Encode a VFP load/store for double-precision (VLDR.64 / VSTR.64).
9165/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
9166fn encode_vfp_ldst_f64(base: u32, dd: &VfpReg, addr: &MemAddr) -> Result<u32> {
9167 let dd_num = vfp_dreg_to_num(dd)?;
9168 let (vd, d) = encode_dreg(dd_num);
9169 let rn = reg_to_bits(&addr.base);
9170
9171 let offset = addr.offset;
9172 let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9173 let abs_offset = offset.unsigned_abs();
9174 let imm8 = (abs_offset / 4) & 0xFF;
9175
9176 Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
9177}
9178
9179/// Encode VMOV between two core registers and a D-register.
9180/// VMOV Dm, Rt, Rt2: 0xEC40_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
9181/// VMOV Rt, Rt2, Dm: 0xEC50_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
9182fn encode_vmov_core_dreg(
9183 to_dreg: bool,
9184 dreg: &VfpReg,
9185 core_lo: &Reg,
9186 core_hi: &Reg,
9187) -> Result<u32> {
9188 let d_num = vfp_dreg_to_num(dreg)?;
9189 let (vm, m) = encode_dreg(d_num);
9190 let rt = reg_to_bits(core_lo);
9191 let rt2 = reg_to_bits(core_hi);
9192
9193 let base = if to_dreg { 0xEC400B10 } else { 0xEC500B10 };
9194 Ok(base | (rt2 << 16) | (rt << 12) | (m << 5) | vm)
9195}
9196
9197/// Emit a VFP 32-bit instruction as Thumb-2 bytes (two LE halfwords).
9198fn vfp_to_thumb_bytes(instr: u32) -> Vec<u8> {
9199 let hw1 = ((instr >> 16) & 0xFFFF) as u16;
9200 let hw2 = (instr & 0xFFFF) as u16;
9201 let mut bytes = hw1.to_le_bytes().to_vec();
9202 bytes.extend_from_slice(&hw2.to_le_bytes());
9203 bytes
9204}
9205
9206// ============================================================================
9207// Helium MVE encoding helpers
9208// ============================================================================
9209
9210/// Q-register number: Q0=0, Q1=1, ..., Q7=7
9211fn qreg_to_num(reg: &QReg) -> u32 {
9212 match reg {
9213 QReg::Q0 => 0,
9214 QReg::Q1 => 1,
9215 QReg::Q2 => 2,
9216 QReg::Q3 => 3,
9217 QReg::Q4 => 4,
9218 QReg::Q5 => 5,
9219 QReg::Q6 => 6,
9220 QReg::Q7 => 7,
9221 }
9222}
9223
9224/// MVE element size to encoding bits: S8=0b00, S16=0b01, S32=0b10
9225fn mve_size_bits(size: &MveSize) -> u32 {
9226 match size {
9227 MveSize::S8 => 0b00,
9228 MveSize::S16 => 0b01,
9229 MveSize::S32 => 0b10,
9230 }
9231}
9232
9233/// Encode MVE 3-register instruction.
9234/// Q-registers are encoded as D-register pairs: Q0=D0:D1, Q1=D2:D3, etc.
9235/// In NEON/MVE encoding, the Q-register uses D-register number = Qn * 2.
9236fn encode_mve_3reg(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
9237 let d = qreg_to_num(qd) * 2;
9238 let n = qreg_to_num(qn) * 2;
9239 let m = qreg_to_num(qm) * 2;
9240
9241 // Standard NEON/MVE 3-register encoding:
9242 // D bit (bit 22) = Vd[4], Vd[3:0] = bits [15:12]
9243 // N bit (bit 7) = Vn[4], Vn[3:0] = bits [19:16]
9244 // M bit (bit 5) = Vm[4], Vm[3:0] = bits [3:0]
9245 let vd = d & 0xF;
9246 let d_bit = (d >> 4) & 1;
9247 let vn = n & 0xF;
9248 let n_bit = (n >> 4) & 1;
9249 let vm = m & 0xF;
9250 let m_bit = (m >> 4) & 1;
9251
9252 base | (d_bit << 22) | (vn << 16) | (vd << 12) | (n_bit << 7) | (m_bit << 5) | vm
9253}
9254
9255/// Encode MVE 3-register bitwise instruction (VAND, VORR, VEOR, VBIC).
9256fn encode_mve_3reg_bitwise(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
9257 encode_mve_3reg(base, qd, qn, qm)
9258}
9259
9260/// Encode MVE VLDRW.32 Qd, [Rn, #offset]
9261/// Format: EC9x xxxx - contiguous load, word-sized elements
9262fn encode_mve_vldrw(qd: &QReg, addr: &MemAddr) -> u32 {
9263 let qd_enc = qreg_to_num(qd) * 2;
9264 let rn = reg_to_bits(&addr.base);
9265 let offset = addr.offset;
9266 let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9267 let abs_offset = offset.unsigned_abs();
9268 let imm7 = (abs_offset / 4) & 0x7F; // 7-bit word-aligned offset
9269
9270 // VLDRW.32 Qd, [Rn, #imm]: ED10 xx80 variant
9271 0xED100E80
9272 | (u_bit << 23)
9273 | ((qd_enc >> 4) << 22)
9274 | (rn << 16)
9275 | ((qd_enc & 0xF) << 12)
9276 | (imm7 & 0x7F)
9277}
9278
9279/// Encode MVE VSTRW.32 Qd, [Rn, #offset]
9280fn encode_mve_vstrw(qd: &QReg, addr: &MemAddr) -> u32 {
9281 let qd_enc = qreg_to_num(qd) * 2;
9282 let rn = reg_to_bits(&addr.base);
9283 let offset = addr.offset;
9284 let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9285 let abs_offset = offset.unsigned_abs();
9286 let imm7 = (abs_offset / 4) & 0x7F;
9287
9288 0xED000E80
9289 | (u_bit << 23)
9290 | ((qd_enc >> 4) << 22)
9291 | (rn << 16)
9292 | ((qd_enc & 0xF) << 12)
9293 | (imm7 & 0x7F)
9294}
9295
9296impl ArmEncoder {
9297 /// Encode MVE constant load: MOVW+MOVT+VMOV for each 32-bit word, then assemble Q-register
9298 fn encode_thumb_mve_const(&self, qd: &QReg, bytes: &[u8; 16]) -> Result<Vec<u8>> {
9299 let mut result = Vec::new();
9300 let qd_num = qreg_to_num(qd);
9301
9302 // Load each 32-bit word into R12 (temp) then VMOV into S-register
9303 for i in 0..4 {
9304 let word = u32::from_le_bytes([
9305 bytes[i * 4],
9306 bytes[i * 4 + 1],
9307 bytes[i * 4 + 2],
9308 bytes[i * 4 + 3],
9309 ]);
9310 let lo16 = word & 0xFFFF;
9311 let hi16 = (word >> 16) & 0xFFFF;
9312
9313 // MOVW R12, #lo16
9314 result.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
9315 // MOVT R12, #hi16
9316 if hi16 != 0 {
9317 result.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
9318 }
9319
9320 // VMOV Sn, R12 where Sn = Qd*4 + i
9321 let s_num = qd_num * 4 + i as u32;
9322 let (vn, n) = encode_sreg(s_num);
9323 let vmov: u32 = 0xEE000A10 | (vn << 16) | (12 << 12) | (n << 7);
9324 result.extend_from_slice(&vfp_to_thumb_bytes(vmov));
9325 }
9326
9327 Ok(result)
9328 }
9329
9330 /// Encode lane-wise f32 binary operation (VDIV, etc.) via S-register extraction
9331 fn encode_thumb_mve_lane_wise_f32_binop(
9332 &self,
9333 qd: &QReg,
9334 qn: &QReg,
9335 qm: &QReg,
9336 vfp_base: u32,
9337 ) -> Result<Vec<u8>> {
9338 let mut result = Vec::new();
9339 let qd_num = qreg_to_num(qd);
9340 let qn_num = qreg_to_num(qn);
9341 let qm_num = qreg_to_num(qm);
9342
9343 // For each lane 0..3: use S-registers directly (Q aliasing)
9344 for i in 0..4u32 {
9345 let sd = qd_num * 4 + i;
9346 let sn = qn_num * 4 + i;
9347 let sm = qm_num * 4 + i;
9348
9349 let (vd, d) = encode_sreg(sd);
9350 let (vn, n) = encode_sreg(sn);
9351 let (vm, m) = encode_sreg(sm);
9352
9353 let instr = vfp_base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
9354 result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9355 }
9356
9357 Ok(result)
9358 }
9359
9360 /// Encode lane-wise f32 VSQRT via S-register extraction
9361 fn encode_thumb_mve_lane_wise_f32_sqrt(&self, qd: &QReg, qm: &QReg) -> Result<Vec<u8>> {
9362 let mut result = Vec::new();
9363 let qd_num = qreg_to_num(qd);
9364 let qm_num = qreg_to_num(qm);
9365
9366 // VSQRT.F32 base: 0xEEB10AC0
9367 for i in 0..4u32 {
9368 let sd = qd_num * 4 + i;
9369 let sm = qm_num * 4 + i;
9370
9371 let (vd, d) = encode_sreg(sd);
9372 let (vm, m) = encode_sreg(sm);
9373
9374 let instr: u32 = 0xEEB10AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
9375 result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9376 }
9377
9378 Ok(result)
9379 }
9380}
9381
9382/// VCR-TIER-001 (#1021/#1048) — the SCRATCH CONTRACT of an `ArmOp`'s encoder
9383/// expansion: the registers, beyond the op's declared RESULT registers and the
9384/// globally sanctioned R12/IP encoder scratch, that the expansion may leave
9385/// modified when it completes. Transient-but-restored traffic (push/pop through
9386/// the expansion's own stack red-zone, SP restored on exit) is not "modified".
9387///
9388/// This is the SINGLE declaration site for that contract — the one place a
9389/// future expansion that must borrow a register says so (the repo rule: one
9390/// declaration site, no duplicate copy that can silently drift). It is deliberately NOT derived
9391/// from the expansion's observed behavior: a contract read off the bytes would
9392/// rubber-stamp any clobber. Intent is declared here; the canary gate
9393/// (`scripts/repro/expansion_canary_gate_1021.py`) executes the REAL emitted
9394/// bytes of every variant the shipped rule table emits, on both backends, with
9395/// every non-contract register holding a distinctive canary, and fails on any
9396/// undeclared write — and on any declared register the expansion never
9397/// actually writes, so an over-broad declaration cannot hollow the gate.
9398///
9399/// The default is the STRICTEST reading — result registers only — which is
9400/// exactly the silent claim the atomic `ArmSemantics` pseudo-op model already
9401/// makes (#1021: an atomic model of a multi-instruction expansion is a silent
9402/// claim that the expansion is scratch-free). Today the table is EMPTY: #1039
9403/// reworked `i32.popcnt` off R11 (the linear-memory base) and #1048 reworked
9404/// the i64 shifts and bit-counts off their own operand registers, so every
9405/// expansion of every rule-emitted variant is R12-only on both Thumb-2 and
9406/// A32. Backend-independent for the same reason; if a backend's expansion ever
9407/// diverges, this signature grows a backend parameter in the same PR.
9408pub fn expansion_scratch_contract(op: &ArmOp) -> &'static [Reg] {
9409 // No variant currently borrows any register beyond R12. A new declaration
9410 // is added as a `match op { .. }` arm here — nowhere else.
9411 let _ = op;
9412 &[]
9413}
9414
9415#[cfg(test)]
9416mod tests {
9417 use super::*;
9418
9419 #[test]
9420 fn test_encoder_creation() {
9421 let encoder_arm = ArmEncoder::new_arm32();
9422 assert!(!encoder_arm.thumb_mode);
9423
9424 let encoder_thumb = ArmEncoder::new_thumb2();
9425 assert!(encoder_thumb.thumb_mode);
9426 }
9427
9428 /// #204 WAKE-path regression: `SetCond` materialized 0/1 with the 16-bit
9429 /// `MOVS Rd,#imm` (T1), whose Rd field is 3 bits (R0–R7). For a high Rd
9430 /// (R8–R12) `rd_bits << 8` overflows bit 11, flipping the opcode MOVS→CMP
9431 /// (`0x2c00`), so the boolean was never written — gale's `has_waiter` kept a
9432 /// stale value and the binary-sem WAKE dispatch read garbage. High Rd must
9433 /// use the 32-bit `MOV.W` (T2). Verify the bytes, not the IR.
9434 /// #311: the SAME high-Rd MOVS→CMP transmutation as #204, but in the
9435 /// i64 comparison expansions (I64SetCond / I64SetCondZ) — missed by the
9436 /// #204 hardening. With rd=R8 the boolean died in the flags
9437 /// (`ite eq; cmpeq r0,#1; cmpne r0,#0`), so gale's packed-u64 select
9438 /// read a stale register on silicon. High Rd must take MOV.W / CMP.W.
9439 #[test]
9440 fn test_encode_i64setcond_high_reg_uses_mov_w_311() {
9441 use synth_synthesis::{ArmOp, Condition, Reg};
9442 let enc = ArmEncoder::new_thumb2();
9443 let bytes = enc
9444 .encode(&ArmOp::I64SetCond {
9445 rd: Reg::R8,
9446 rn_lo: Reg::R2,
9447 rn_hi: Reg::R3,
9448 rm_lo: Reg::R6,
9449 rm_hi: Reg::R7,
9450 cond: Condition::EQ,
9451 })
9452 .unwrap();
9453 // The 32-bit MOV.W immediate (T2) first halfword is 0xF04F; the
9454 // 16-bit transmuted forms would contain 0x2801/0x2800 (CMP r0,#1/#0).
9455 let halfwords: Vec<u16> = bytes
9456 .chunks(2)
9457 .map(|c| u16::from_le_bytes([c[0], c[1]]))
9458 .collect();
9459 assert!(
9460 halfwords.iter().filter(|&&h| h == 0xF04F).count() == 2,
9461 "high rd must use two MOV.W (T2) encodings, got {halfwords:04x?}"
9462 );
9463 assert!(
9464 !halfwords.contains(&0x2801) && !halfwords.contains(&0x2800),
9465 "no transmuted 16-bit CMP imm: {halfwords:04x?}"
9466 );
9467
9468 let bytes_z = enc
9469 .encode(&ArmOp::I64SetCondZ {
9470 rd: Reg::R8,
9471 rn_lo: Reg::R2,
9472 rn_hi: Reg::R3,
9473 })
9474 .unwrap();
9475 let hw_z: Vec<u16> = bytes_z
9476 .chunks(2)
9477 .map(|c| u16::from_le_bytes([c[0], c[1]]))
9478 .collect();
9479 assert!(
9480 hw_z.iter().filter(|&&h| h == 0xF04F).count() == 2,
9481 "SetCondZ high rd MOV.W: {hw_z:04x?}"
9482 );
9483 // CMP.W rd,#0 (T2) first halfword: 0xF1B0 | rd
9484 assert!(
9485 hw_z.contains(&(0xF1B0 | 8)),
9486 "SetCondZ high rd must use CMP.W: {hw_z:04x?}"
9487 );
9488 }
9489
9490 #[test]
9491 fn test_encode_setcond_high_reg_uses_mov_w_204() {
9492 use synth_synthesis::{ArmOp, Condition, Reg};
9493 let enc = ArmEncoder::new_thumb2();
9494 // R12 (high): must be ITE + MOV.W #1 + MOV.W #0, never a 16-bit MOVS/CMP.
9495 let hi = enc
9496 .encode(&ArmOp::SetCond {
9497 rd: Reg::R12,
9498 cond: Condition::NE,
9499 })
9500 .unwrap();
9501 assert_eq!(hi.len(), 10, "ITE(2) + MOV.W(4) + MOV.W(4): {hi:02x?}");
9502 // both value halfwords are MOV.W (0xF04F) — NOT the corrupt CMP (0x2c..).
9503 assert_eq!(&hi[2..4], &[0x4F, 0xF0], "then = MOV.W: {hi:02x?}");
9504 assert_eq!(&hi[6..8], &[0x4F, 0xF0], "else = MOV.W: {hi:02x?}");
9505 assert_eq!(hi[4] & 0x0F, 0x01, "then imm = #1");
9506 assert_eq!(hi[8] & 0x0F, 0x00, "else imm = #0");
9507 // Low Rd keeps the compact 16-bit MOVS form.
9508 let lo = enc
9509 .encode(&ArmOp::SetCond {
9510 rd: Reg::R0,
9511 cond: Condition::NE,
9512 })
9513 .unwrap();
9514 assert_eq!(lo.len(), 6, "ITE(2) + MOVS(2) + MOVS(2): {lo:02x?}");
9515 assert_eq!(lo[2..4], [0x01, 0x20], "then = MOVS R0,#1");
9516 assert_eq!(lo[4..6], [0x00, 0x20], "else = MOVS R0,#0");
9517 }
9518
9519 /// #209 Opt 1b: UMULL RdLo, RdHi, Rn, Rm encodes correctly on both ISAs.
9520 /// Thumb-2 T1: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm.
9521 /// A32: cond 0000 1000 RdHi RdLo Rm 1001 Rn.
9522 #[test]
9523 fn test_encode_umull_209b() {
9524 use synth_synthesis::{ArmOp, Reg};
9525 let op = ArmOp::Umull {
9526 rdlo: Reg::R4,
9527 rdhi: Reg::R5,
9528 rn: Reg::R0,
9529 rm: Reg::R3,
9530 };
9531 // Thumb-2: hw1 = 0xFBA0 | 0 = 0xFBA0; hw2 = (4<<12)|(5<<8)|3 = 0x4503.
9532 let t = ArmEncoder::new_thumb2().encode(&op).unwrap();
9533 assert_eq!(
9534 t,
9535 vec![0xA0, 0xFB, 0x03, 0x45],
9536 "umull r4,r5,r0,r3 (T2): {t:02x?}"
9537 );
9538 // A32: 0xE0800090 | (5<<16) | (4<<12) | (3<<8) | 0 = 0xE0854390.
9539 let a = ArmEncoder::new_arm32().encode(&op).unwrap();
9540 assert_eq!(
9541 a,
9542 0xE085_4390u32.to_le_bytes().to_vec(),
9543 "umull (A32): {a:02x?}"
9544 );
9545 }
9546
9547 /// #206 regression: the ARM32 (A32) `Ldr`/`Str` encoders fed `addr` through
9548 /// `encode_mem_addr`, which returns only the 12-bit immediate — so a register
9549 /// offset (`[rn, rm, #off]`) was silently dropped to `[rn, #off]`, sending
9550 /// the access to the wrong runtime address (silent miscompile on the default
9551 /// `--target arm`). A register offset must materialize `ip = rn + rm` and
9552 /// load from `[ip, #off]`. Verify the bytes.
9553 #[test]
9554 fn test_encode_arm32_indexed_load_keeps_index_206() {
9555 use synth_synthesis::{ArmOp, MemAddr, Reg};
9556 let enc = ArmEncoder::new_arm32();
9557 // ldr r0, [r11, r1, #8] must NOT collapse to a single immediate ldr.
9558 let bytes = enc
9559 .encode(&ArmOp::Ldr {
9560 rd: Reg::R0,
9561 addr: MemAddr::reg_imm(Reg::R11, Reg::R1, 8),
9562 })
9563 .unwrap();
9564 assert_eq!(
9565 bytes.len(),
9566 8,
9567 "expected ADD ip + LDR (2 words): {bytes:02x?}"
9568 );
9569 let add = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9570 let ldr = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9571 // ADD ip, r11, r1 = 0xE08BC001
9572 assert_eq!(add, 0xE08B_C001, "ADD ip,r11,r1: {add:#010x}");
9573 // LDR r0, [ip, #8] = 0xE59C0008
9574 assert_eq!(ldr, 0xE59C_0008, "LDR r0,[ip,#8]: {ldr:#010x}");
9575 // A bare immediate ldr (the bug) would be 0xE59B0008 (base=r11) — reject.
9576 assert_ne!(ldr, 0xE59B_0008, "index must not be dropped");
9577 }
9578
9579 /// #594 regression: `call_indirect` on the A32 path (`--target cortex-r5`)
9580 /// was encoded as a literal NOP (0xE1A00000) — the call never happened and
9581 /// the function silently returned the leftover table-index value. The A32
9582 /// encoder must emit a real dispatch expansion, since #642 guarded by an
9583 /// inline bounds check:
9584 /// `MOVW r12, #size; CMP idx, r12; BLO +1; UDF;
9585 /// MOV r12, idx, LSL #2; LDR r12, [r11, r12]; BLX r12`.
9586 #[test]
9587 fn test_encode_arm32_call_indirect_is_real_call_594() {
9588 use synth_synthesis::{ArmOp, Reg};
9589 let enc = ArmEncoder::new_arm32();
9590 let bytes = enc
9591 .encode(&ArmOp::CallIndirect {
9592 rd: Reg::R0,
9593 type_idx: 0,
9594 table_index_reg: Reg::R0,
9595 table_size: 4,
9596 table_byte_offset: 0,
9597 null_check: false,
9598 type_check: None,
9599 })
9600 .unwrap();
9601 assert_eq!(
9602 bytes.len(),
9603 28,
9604 "expected MOVW + CMP + BLO + UDF + MOV + LDR + BLX (7 words): {bytes:02x?}"
9605 );
9606 let words: Vec<u32> = bytes
9607 .as_chunks::<4>()
9608 .0
9609 .iter()
9610 .map(|&w| u32::from_le_bytes(w))
9611 .collect();
9612 // #642 bounds guard: MOVW r12, #4; CMP r0, r12; BLO +1; UDF
9613 assert_eq!(words[0], 0xE300_C004, "MOVW r12,#4: {:#010x}", words[0]);
9614 assert_eq!(words[1], 0xE150_000C, "CMP r0,r12: {:#010x}", words[1]);
9615 assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9616 assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9617 // MOV r12, r0, LSL #2 = 0xE1A0C100
9618 assert_eq!(
9619 words[4], 0xE1A0_C100,
9620 "MOV r12,r0,LSL#2: {:#010x}",
9621 words[4]
9622 );
9623 // LDR r12, [r11, r12] = 0xE79BC00C
9624 assert_eq!(
9625 words[5], 0xE79B_C00C,
9626 "LDR r12,[r11,r12]: {:#010x}",
9627 words[5]
9628 );
9629 // BLX r12 = 0xE12FFF3C
9630 assert_eq!(words[6], 0xE12F_FF3C, "BLX r12: {:#010x}", words[6]);
9631 // The bug: a single NOP word. Must never come back.
9632 assert!(
9633 !bytes
9634 .as_chunks::<4>()
9635 .0
9636 .iter()
9637 .any(|&w| w == 0xE1A0_0000u32.to_le_bytes()),
9638 "call_indirect must not contain a NOP (#594): {bytes:02x?}"
9639 );
9640
9641 // A non-R0 index register lands in the MOV's Rm and CMP's Rn fields.
9642 let bytes = enc
9643 .encode(&ArmOp::CallIndirect {
9644 rd: Reg::R0,
9645 type_idx: 0,
9646 table_index_reg: Reg::R4,
9647 table_size: 4,
9648 table_byte_offset: 0,
9649 null_check: false,
9650 type_check: None,
9651 })
9652 .unwrap();
9653 let cmp = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9654 assert_eq!(cmp, 0xE154_000C, "CMP r4,r12: {cmp:#010x}");
9655 let mov = u32::from_le_bytes(bytes[16..20].try_into().unwrap());
9656 assert_eq!(mov, 0xE1A0_C104, "MOV r12,r4,LSL#2: {mov:#010x}");
9657 }
9658
9659 /// #642: a table size above 16 bits must not be silently truncated by the
9660 /// MOVW — the A32 guard adds a MOVT for the high half.
9661 #[test]
9662 fn test_encode_arm32_call_indirect_wide_table_size_642() {
9663 use synth_synthesis::{ArmOp, Reg};
9664 let enc = ArmEncoder::new_arm32();
9665 let bytes = enc
9666 .encode(&ArmOp::CallIndirect {
9667 rd: Reg::R0,
9668 type_idx: 0,
9669 table_index_reg: Reg::R0,
9670 table_size: 0x0002_0003,
9671 table_byte_offset: 0,
9672 null_check: false,
9673 type_check: None,
9674 })
9675 .unwrap();
9676 assert_eq!(bytes.len(), 32, "MOVT arm adds one word: {bytes:02x?}");
9677 let movw = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9678 let movt = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9679 assert_eq!(movw, 0xE300_C003, "MOVW r12,#3: {movw:#010x}");
9680 assert_eq!(movt, 0xE340_C002, "MOVT r12,#2: {movt:#010x}");
9681 }
9682
9683 /// #597 anchor (justified correctness RE-PIN of the #594-era freeze): the
9684 /// Thumb-2 `CallIndirect` expansion is `mov.w ip, rm, LSL #2; ldr.w ip,
9685 /// [r11, ip]; blx ip`.
9686 ///
9687 /// The #594 PR froze the then-current bytes `4F EA 20 0C ...` whose first
9688 /// word decodes as `mov.w ip, rm, ASR #32` — the intended `LSL #2` had
9689 /// its shift amount in the TYPE field (bits 5:4) instead of imm2 (bits
9690 /// 7:6), so the index was destroyed and every call_indirect dispatched
9691 /// table entry 0 (shipped miscompile, masked by index-0 probes). #597
9692 /// corrects the encoding; new bytes `4F EA 80 0C ...` were
9693 /// execution-validated under unicorn against the wasmtime oracle on a
9694 /// multi-entry table (indexes 0, 1, 3 —
9695 /// scripts/repro/call_indirect_597_differential.py) before this pin was
9696 /// replaced. Old pin: [4F EA 20 0C, 5B F8 0C C0, E0 47] (ASR #32 — must
9697 /// never come back).
9698 #[test]
9699 fn test_encode_thumb_call_indirect_lsl2_597() {
9700 use synth_synthesis::{ArmOp, Reg};
9701 let enc = ArmEncoder::new_thumb2();
9702 let bytes = enc
9703 .encode(&ArmOp::CallIndirect {
9704 rd: Reg::R0,
9705 type_idx: 0,
9706 table_index_reg: Reg::R0,
9707 table_size: 4,
9708 table_byte_offset: 0,
9709 null_check: false,
9710 type_check: None,
9711 })
9712 .unwrap();
9713 assert_eq!(
9714 bytes,
9715 vec![
9716 // #642 bounds guard: movw ip,#4; cmp r0,ip; blo +1; udf #0
9717 0x40, 0xF2, 0x04, 0x0C, // movw ip, #4
9718 0x60, 0x45, // cmp r0, ip
9719 0x00, 0xD3, // blo .+4 (skip the udf)
9720 0x00, 0xDE, // udf #0 — OOB index trap (WASM §4.4.8)
9721 // #597-pinned dispatch
9722 0x4F, 0xEA, 0x80, 0x0C, // mov.w ip, r0, lsl #2
9723 0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9724 0xE0, 0x47, // blx ip
9725 ],
9726 "Thumb-2 CallIndirect: bounds guard + mov.w/ldr.w/blx dispatch: {bytes:02x?}"
9727 );
9728 // The #597 bug bytes (ASR #32 dispatch first word) must never come back.
9729 assert!(
9730 !bytes.windows(4).any(|w| w == [0x4F, 0xEA, 0x20, 0x0C]),
9731 "mov.w ip, rm, ASR #32 — the #597 type-field bug"
9732 );
9733
9734 // A non-R0 index register lands in the mov.w's Rm field (hw2 bits 3:0)
9735 // and the cmp's Rn field.
9736 let bytes = enc
9737 .encode(&ArmOp::CallIndirect {
9738 rd: Reg::R0,
9739 type_idx: 0,
9740 table_index_reg: Reg::R4,
9741 table_size: 4,
9742 table_byte_offset: 0,
9743 null_check: false,
9744 type_check: None,
9745 })
9746 .unwrap();
9747 assert_eq!(&bytes[4..6], &[0x64, 0x45], "cmp r4, ip: {bytes:02x?}");
9748 assert_eq!(
9749 &bytes[10..14],
9750 &[0x4F, 0xEA, 0x84, 0x0C],
9751 "mov.w ip, r4, LSL #2: {bytes:02x?}"
9752 );
9753 }
9754
9755 /// #642: the Thumb-2 bounds guard for a high-register index (R8 — the top
9756 /// of the allocatable pool) uses the high-reg-capable 16-bit CMP (T2) with
9757 /// the N bit set; a table size above 16 bits adds a MOVT.
9758 #[test]
9759 fn test_encode_thumb_call_indirect_guard_shapes_642() {
9760 use synth_synthesis::{ArmOp, Reg};
9761 let enc = ArmEncoder::new_thumb2();
9762 let bytes = enc
9763 .encode(&ArmOp::CallIndirect {
9764 rd: Reg::R0,
9765 type_idx: 0,
9766 table_index_reg: Reg::R8,
9767 table_size: 3,
9768 table_byte_offset: 0,
9769 null_check: false,
9770 type_check: None,
9771 })
9772 .unwrap();
9773 // cmp r8, ip — T2: 0x4500 | N(1)<<7 | Rm(12)<<3 | Rn(0) = 0x45E0
9774 assert_eq!(&bytes[4..6], &[0xE0, 0x45], "cmp r8, ip: {bytes:02x?}");
9775
9776 let bytes = enc
9777 .encode(&ArmOp::CallIndirect {
9778 rd: Reg::R0,
9779 type_idx: 0,
9780 table_index_reg: Reg::R0,
9781 table_size: 0x0002_0003,
9782 table_byte_offset: 0,
9783 null_check: false,
9784 type_check: None,
9785 })
9786 .unwrap();
9787 // movw ip,#3 then movt ip,#2 — the size must not be truncated.
9788 assert_eq!(
9789 &bytes[0..8],
9790 &[0x40, 0xF2, 0x03, 0x0C, 0xC0, 0xF2, 0x02, 0x0C],
9791 "movw ip,#3; movt ip,#2: {bytes:02x?}"
9792 );
9793 }
9794
9795 /// #650: a non-zero table base offset (table N of the contiguous R11
9796 /// region) routes the Thumb-2 pointer load through
9797 /// `add.w ip, r11, ip; ldr.w ip, [ip, #offset]` — and offset 0 keeps the
9798 /// pre-#650 single-load bytes IDENTICAL (the by-construction pin).
9799 #[test]
9800 fn test_encode_thumb_call_indirect_table_offset_650() {
9801 use synth_synthesis::{ArmOp, Reg};
9802 let enc = ArmEncoder::new_thumb2();
9803 // falcon's fused-component shape: table 0 has 7 entries, so table 1
9804 // sits at byte offset 28.
9805 let bytes = enc
9806 .encode(&ArmOp::CallIndirect {
9807 rd: Reg::R0,
9808 type_idx: 0,
9809 table_index_reg: Reg::R1,
9810 table_size: 41,
9811 table_byte_offset: 28,
9812 null_check: false,
9813 type_check: None,
9814 })
9815 .unwrap();
9816 assert_eq!(
9817 bytes,
9818 vec![
9819 // #642 bounds guard against TABLE 1's OWN size (41)
9820 0x40, 0xF2, 0x29, 0x0C, // movw ip, #41
9821 0x61, 0x45, // cmp r1, ip
9822 0x00, 0xD3, // blo .+4 (skip the udf)
9823 0x00, 0xDE, // udf #0 — OOB trap (WASM §4.4.8)
9824 // dispatch through table 1's base (R11 + 28)
9825 0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9826 0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9827 0xDC, 0xF8, 0x1C, 0xC0, // ldr.w ip, [ip, #28]
9828 0xE0, 0x47, // blx ip
9829 ],
9830 "Thumb-2 table-1 dispatch (#650): {bytes:02x?}"
9831 );
9832
9833 // Offset 0 must stay the #597-pinned single-load form (no add.w, no
9834 // imm-form ldr) — single-table byte identity by construction.
9835 let zero = enc
9836 .encode(&ArmOp::CallIndirect {
9837 rd: Reg::R0,
9838 type_idx: 0,
9839 table_index_reg: Reg::R1,
9840 table_size: 41,
9841 table_byte_offset: 0,
9842 null_check: false,
9843 type_check: None,
9844 })
9845 .unwrap();
9846 assert_eq!(
9847 &zero[10..],
9848 &[
9849 0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9850 0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9851 0xE0, 0x47, // blx ip
9852 ],
9853 "offset 0 keeps the pre-#650 dispatch bytes: {zero:02x?}"
9854 );
9855 }
9856
9857 /// #650: the A32 twin — `add r12, r11, r12; ldr r12, [r12, #offset]` for
9858 /// a non-zero table base offset; offset 0 keeps the #594/#642 form.
9859 #[test]
9860 fn test_encode_arm32_call_indirect_table_offset_650() {
9861 use synth_synthesis::{ArmOp, Reg};
9862 let enc = ArmEncoder::new_arm32();
9863 let bytes = enc
9864 .encode(&ArmOp::CallIndirect {
9865 rd: Reg::R0,
9866 type_idx: 0,
9867 table_index_reg: Reg::R1,
9868 table_size: 41,
9869 table_byte_offset: 28,
9870 null_check: false,
9871 type_check: None,
9872 })
9873 .unwrap();
9874 let words: Vec<u32> = bytes
9875 .as_chunks::<4>()
9876 .0
9877 .iter()
9878 .map(|&w| u32::from_le_bytes(w))
9879 .collect();
9880 assert_eq!(words[0], 0xE300_C029, "MOVW r12,#41: {:#010x}", words[0]);
9881 assert_eq!(words[1], 0xE151_000C, "CMP r1,r12: {:#010x}", words[1]);
9882 assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9883 assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9884 assert_eq!(
9885 words[4], 0xE1A0_C101,
9886 "MOV r12,r1,LSL#2: {:#010x}",
9887 words[4]
9888 );
9889 assert_eq!(
9890 words[5], 0xE08B_C00C,
9891 "ADD r12,r11,r12 (#650): {:#010x}",
9892 words[5]
9893 );
9894 assert_eq!(
9895 words[6], 0xE59C_C01C,
9896 "LDR r12,[r12,#28] (#650): {:#010x}",
9897 words[6]
9898 );
9899 assert_eq!(words[7], 0xE12F_FF3C, "BLX r12: {:#010x}", words[7]);
9900 }
9901
9902 /// #664: `null_check` inserts a null-funcref trap between the Thumb-2
9903 /// pointer load and the `BLX` (`cmp.w ip, #0; bne .+4; udf #0`) — a
9904 /// zero-linked (uninitialized) slot must TRAP (WASM §4.4.8), never
9905 /// branch to address 0. `null_check: false` keeps the expansion
9906 /// byte-identical to the pre-#664 form (by-construction pin).
9907 #[test]
9908 fn test_encode_thumb_call_indirect_null_check_664() {
9909 use synth_synthesis::{ArmOp, Reg};
9910 let enc = ArmEncoder::new_thumb2();
9911 let op = |null_check| ArmOp::CallIndirect {
9912 rd: Reg::R0,
9913 type_idx: 0,
9914 table_index_reg: Reg::R1,
9915 table_size: 4,
9916 table_byte_offset: 0,
9917 null_check,
9918 type_check: None,
9919 };
9920 let with = enc.encode(&op(true)).unwrap();
9921 let without = enc.encode(&op(false)).unwrap();
9922 // The checked form = the unchecked form with EXACTLY the three-insn
9923 // null check spliced in before the final BLX (byte identity of the
9924 // shared prefix/suffix — nothing else may move).
9925 assert_eq!(
9926 with.len(),
9927 without.len() + 8,
9928 "cmp.w (4) + bne (2) + udf (2): {with:02x?}"
9929 );
9930 let blx_at = without.len() - 2;
9931 assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9932 assert_eq!(
9933 &with[blx_at..],
9934 &[
9935 0xBC, 0xF1, 0x00, 0x0F, // cmp.w ip, #0
9936 0x00, 0xD1, // bne .+4 (skip the udf)
9937 0x00, 0xDE, // udf #0 — null-funcref trap (#664)
9938 0xE0, 0x47, // blx ip
9939 ],
9940 "null check precedes the BLX: {with:02x?}"
9941 );
9942 assert_eq!(&with[with.len() - 2..], &without[blx_at..], "same BLX");
9943 }
9944
9945 /// #664: the A32 twin — `cmp r12, #0; bne .+8; udf` before the `BLX`;
9946 /// `null_check: false` keeps the #594/#642/#650 bytes identical.
9947 #[test]
9948 fn test_encode_arm32_call_indirect_null_check_664() {
9949 use synth_synthesis::{ArmOp, Reg};
9950 let enc = ArmEncoder::new_arm32();
9951 let op = |null_check| ArmOp::CallIndirect {
9952 rd: Reg::R0,
9953 type_idx: 0,
9954 table_index_reg: Reg::R1,
9955 table_size: 4,
9956 table_byte_offset: 0,
9957 null_check,
9958 type_check: None,
9959 };
9960 let with = enc.encode(&op(true)).unwrap();
9961 let without = enc.encode(&op(false)).unwrap();
9962 assert_eq!(with.len(), without.len() + 12, "3 A32 words: {with:02x?}");
9963 let blx_at = without.len() - 4;
9964 assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9965 let words: Vec<u32> = with[blx_at..]
9966 .as_chunks::<4>()
9967 .0
9968 .iter()
9969 .map(|&w| u32::from_le_bytes(w))
9970 .collect();
9971 assert_eq!(words[0], 0xE35C_0000, "CMP r12,#0: {:#010x}", words[0]);
9972 assert_eq!(words[1], 0x1A00_0000, "BNE +1 insn: {:#010x}", words[1]);
9973 assert_eq!(words[2], 0xE7F0_00F0, "UDF (null trap): {:#010x}", words[2]);
9974 assert_eq!(words[3], 0xE12F_FF3C, "BLX r12: {:#010x}", words[3]);
9975 }
9976
9977 /// #676: `type_check` splices the runtime type check — scale the index,
9978 /// load the slot's structural class id from the type-id sidecar
9979 /// (`ldr.w ip, [ip, #type_off]`), compare against the expected class id
9980 /// and trap on mismatch (WASM §4.4.8) — between the bounds guard and
9981 /// the dispatch tail. `type_check: None` keeps the expansion
9982 /// byte-identical to the pre-#676 form (by-construction pin, the same
9983 /// trick as #650 offset-0 / #664 `null_check: false`).
9984 #[test]
9985 fn test_encode_thumb_call_indirect_type_check_676() {
9986 use synth_synthesis::{ArmOp, Reg};
9987 let enc = ArmEncoder::new_thumb2();
9988 let op = |type_check| ArmOp::CallIndirect {
9989 rd: Reg::R0,
9990 type_idx: 1,
9991 table_index_reg: Reg::R1,
9992 table_size: 5,
9993 table_byte_offset: 0,
9994 null_check: false,
9995 type_check,
9996 };
9997 let with = enc.encode(&op(Some((2, 20)))).unwrap();
9998 let without = enc.encode(&op(None)).unwrap();
9999 // The checked form = the unchecked form with EXACTLY the six-insn
10000 // type check spliced in after the bounds guard (byte identity of
10001 // the shared prefix/suffix — nothing else may move).
10002 assert_eq!(
10003 with.len(),
10004 without.len() + 20,
10005 "lsl.w(4)+add.w(4)+ldr.w(4)+cmp.w(4)+beq(2)+udf(2): {with:02x?}"
10006 );
10007 // Bounds guard: movw(4) + cmp(2) + blo(2) + udf(2) = 10 bytes.
10008 let guard_end = 10;
10009 assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
10010 assert_eq!(
10011 &with[guard_end..guard_end + 20],
10012 &[
10013 0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
10014 0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
10015 0xDC, 0xF8, 0x14, 0xC0, // ldr.w ip, [ip, #20] — sidecar slot id
10016 0xBC, 0xF1, 0x02, 0x0F, // cmp.w ip, #2 — expected class id
10017 0x00, 0xD0, // beq .+4 (skip the udf on a match)
10018 0x00, 0xDE, // udf #0 — §4.4.8 type-mismatch trap (#676)
10019 ],
10020 "type check follows the bounds guard: {with:02x?}"
10021 );
10022 assert_eq!(
10023 &with[guard_end + 20..],
10024 &without[guard_end..],
10025 "dispatch tail unchanged (idx*4 recomputed)"
10026 );
10027 }
10028
10029 /// #676: the A32 twin — `mov r12, idx, lsl #2; add r12, r11, r12;
10030 /// ldr r12, [r12, #type_off]; cmp r12, #id; beq .+8; udf` after the
10031 /// bounds guard; `type_check: None` keeps the #594/#642/#650/#664
10032 /// bytes identical.
10033 #[test]
10034 fn test_encode_arm32_call_indirect_type_check_676() {
10035 use synth_synthesis::{ArmOp, Reg};
10036 let enc = ArmEncoder::new_arm32();
10037 let op = |type_check| ArmOp::CallIndirect {
10038 rd: Reg::R0,
10039 type_idx: 1,
10040 table_index_reg: Reg::R1,
10041 table_size: 5,
10042 table_byte_offset: 0,
10043 null_check: false,
10044 type_check,
10045 };
10046 let with = enc.encode(&op(Some((2, 20)))).unwrap();
10047 let without = enc.encode(&op(None)).unwrap();
10048 assert_eq!(with.len(), without.len() + 24, "6 A32 words: {with:02x?}");
10049 // Bounds guard: movw + cmp + blo + udf = 4 words = 16 bytes.
10050 let guard_end = 16;
10051 assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
10052 let words: Vec<u32> = with[guard_end..guard_end + 24]
10053 .as_chunks::<4>()
10054 .0
10055 .iter()
10056 .map(|&w| u32::from_le_bytes(w))
10057 .collect();
10058 assert_eq!(
10059 words[0], 0xE1A0_C101,
10060 "MOV r12,r1,LSL#2: {:#010x}",
10061 words[0]
10062 );
10063 assert_eq!(words[1], 0xE08B_C00C, "ADD r12,r11,r12: {:#010x}", words[1]);
10064 assert_eq!(
10065 words[2], 0xE59C_C014,
10066 "LDR r12,[r12,#20] (sidecar): {:#010x}",
10067 words[2]
10068 );
10069 assert_eq!(
10070 words[3], 0xE35C_0002,
10071 "CMP r12,#2 (expected class id): {:#010x}",
10072 words[3]
10073 );
10074 assert_eq!(words[4], 0x0A00_0000, "BEQ +1 insn: {:#010x}", words[4]);
10075 assert_eq!(
10076 words[5], 0xE7F0_00F0,
10077 "UDF (type-mismatch trap): {:#010x}",
10078 words[5]
10079 );
10080 assert_eq!(
10081 &with[guard_end + 24..],
10082 &without[guard_end..],
10083 "dispatch tail unchanged"
10084 );
10085 }
10086
10087 /// #178/#180 regression: the Thumb `Add`/`Adds`/`Subs` reg-forms used the
10088 /// 16-bit encoding unconditionally. For high registers (R12 base scratch,
10089 /// R8-R11 i64 pairs) the 3-bit register fields overflow and corrupt the
10090 /// operands — `add ip,ip,r0` came out as `adds r4,r5,r1` (0x186C), silently
10091 /// dropping the address operand and miscompiling every optimized memory
10092 /// access. High registers must use the 32-bit `.W` forms.
10093 #[test]
10094 fn test_encode_thumb_add_high_reg_uses_add_w_178_180() {
10095 let encoder = ArmEncoder::new_thumb2();
10096
10097 // add ip, ip, r0 — the exact MemLoad/MemStore base+addr op.
10098 let code = encoder
10099 .encode(&ArmOp::Add {
10100 rd: Reg::R12,
10101 rn: Reg::R12,
10102 op2: Operand2::Reg(Reg::R0),
10103 })
10104 .unwrap();
10105 // ADD.W ip, ip, r0 = EB0C 0C00 (little-endian halfwords).
10106 assert_eq!(
10107 code,
10108 vec![0x0C, 0xEB, 0x00, 0x0C],
10109 "high-reg Thumb ADD must be 32-bit ADD.W (EB0C 0C00), not corrupt 16-bit; got {code:02X?}"
10110 );
10111 // Must NOT be the buggy 16-bit 0x186C (`adds r4,r5,r1`).
10112 assert_ne!(code, vec![0x6C, 0x18], "regressed to corrupt 16-bit ADDS");
10113
10114 // Low-register add stays 16-bit (no regression for the common case).
10115 let lo = encoder
10116 .encode(&ArmOp::Add {
10117 rd: Reg::R1,
10118 rn: Reg::R2,
10119 op2: Operand2::Reg(Reg::R3),
10120 })
10121 .unwrap();
10122 assert_eq!(
10123 lo.len(),
10124 2,
10125 "low-reg ADD should remain 16-bit, got {lo:02X?}"
10126 );
10127 }
10128
10129 /// #178/#180 sibling: i64 low-word `Adds`/`Subs` can land in R8-R11 pairs;
10130 /// those must fall back to 32-bit ADDS.W/SUBS.W (flag-setting preserved).
10131 #[test]
10132 fn test_encode_thumb_adds_subs_high_reg_use_32bit_178_180() {
10133 let encoder = ArmEncoder::new_thumb2();
10134
10135 // adds r10, r10, r8 → ADDS.W = EB1A 0A08
10136 let adds = encoder
10137 .encode(&ArmOp::Adds {
10138 rd: Reg::R10,
10139 rn: Reg::R10,
10140 op2: Operand2::Reg(Reg::R8),
10141 })
10142 .unwrap();
10143 assert_eq!(
10144 adds,
10145 vec![0x1A, 0xEB, 0x08, 0x0A],
10146 "high-reg ADDS must be 32-bit ADDS.W (EB1A 0A08); got {adds:02X?}"
10147 );
10148
10149 // subs r10, r10, r8 → SUBS.W = EBBA 0A08
10150 let subs = encoder
10151 .encode(&ArmOp::Subs {
10152 rd: Reg::R10,
10153 rn: Reg::R10,
10154 op2: Operand2::Reg(Reg::R8),
10155 })
10156 .unwrap();
10157 assert_eq!(
10158 subs,
10159 vec![0xBA, 0xEB, 0x08, 0x0A],
10160 "high-reg SUBS must be 32-bit SUBS.W (EBBA 0A08); got {subs:02X?}"
10161 );
10162 }
10163
10164 /// #184 (sibling of #180): 16-bit CMN (T1) only encodes R0-R7. High registers
10165 /// must use 32-bit CMN.W, not the corrupt truncated 16-bit form.
10166 #[test]
10167 fn test_encode_thumb_cmn_high_reg_uses_cmn_w_184() {
10168 let encoder = ArmEncoder::new_thumb2();
10169
10170 // cmn r10, r8 → CMN.W = EB1A 0F08 (ADD.W S=1, Rd=PC discarded).
10171 let cmn = encoder
10172 .encode(&ArmOp::Cmn {
10173 rn: Reg::R10,
10174 op2: Operand2::Reg(Reg::R8),
10175 })
10176 .unwrap();
10177 assert_eq!(
10178 cmn,
10179 vec![0x1A, 0xEB, 0x08, 0x0F],
10180 "high-reg CMN must be 32-bit CMN.W (EB1A 0F08); got {cmn:02X?}"
10181 );
10182
10183 // Low registers stay 16-bit: cmn r1, r2 = 0x42D1.
10184 let lo = encoder
10185 .encode(&ArmOp::Cmn {
10186 rn: Reg::R1,
10187 op2: Operand2::Reg(Reg::R2),
10188 })
10189 .unwrap();
10190 assert_eq!(
10191 lo.len(),
10192 2,
10193 "low-reg CMN should remain 16-bit, got {lo:02X?}"
10194 );
10195 assert_eq!(lo, vec![0xD1, 0x42], "low-reg CMN bytes wrong: {lo:02X?}");
10196 }
10197
10198 /// #185 regression: feeding PC (R15) as a data operand to a Thumb-2 op that
10199 /// guards its registers must return Err, not panic under debug-assertions.
10200 /// (Synth never emits PC here; the fuzz harness requires encode() be total.)
10201 #[test]
10202 fn test_encode_pc_operand_returns_err_not_panic_185() {
10203 let encoder = ArmEncoder::new_thumb2();
10204 for op in [
10205 ArmOp::Sdiv {
10206 rd: Reg::PC,
10207 rn: Reg::R0,
10208 rm: Reg::R1,
10209 },
10210 ArmOp::Udiv {
10211 rd: Reg::R0,
10212 rn: Reg::PC,
10213 rm: Reg::R1,
10214 },
10215 ArmOp::Sdiv {
10216 rd: Reg::R0,
10217 rn: Reg::R1,
10218 rm: Reg::PC,
10219 },
10220 ] {
10221 let r = encoder.encode(&op);
10222 assert!(
10223 r.is_err(),
10224 "encode({op:?}) must return Err for a PC operand, got {r:?}"
10225 );
10226 }
10227 // Valid registers still encode fine (no false rejection).
10228 assert!(
10229 encoder
10230 .encode(&ArmOp::Sdiv {
10231 rd: Reg::R0,
10232 rn: Reg::R1,
10233 rm: Reg::R2
10234 })
10235 .is_ok()
10236 );
10237 }
10238
10239 #[test]
10240 fn test_encode_nop_arm32() {
10241 let encoder = ArmEncoder::new_arm32();
10242 let code = encoder.encode(&ArmOp::Nop).unwrap();
10243
10244 assert_eq!(code.len(), 4); // ARM32 instructions are 4 bytes
10245 assert_eq!(code, vec![0x00, 0x00, 0xA0, 0xE1]); // MOV R0, R0
10246 }
10247
10248 #[test]
10249 fn test_encode_nop_thumb() {
10250 let encoder = ArmEncoder::new_thumb2();
10251 let code = encoder.encode(&ArmOp::Nop).unwrap();
10252
10253 assert_eq!(code.len(), 2); // Thumb instructions are 2 bytes
10254 assert_eq!(code, vec![0x00, 0xBF]); // NOP
10255 }
10256
10257 #[test]
10258 fn test_encode_mov_immediate_arm32() {
10259 let encoder = ArmEncoder::new_arm32();
10260 let op = ArmOp::Mov {
10261 rd: Reg::R0,
10262 op2: Operand2::Imm(42),
10263 };
10264
10265 let code = encoder.encode(&op).unwrap();
10266 assert_eq!(code.len(), 4);
10267
10268 // Verify it's a MOV instruction (bits should have immediate flag set)
10269 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10270 assert_eq!(instr & 0x0E000000, 0x02000000); // Check I bit is set
10271 }
10272
10273 #[test]
10274 fn test_encode_add_registers_arm32() {
10275 let encoder = ArmEncoder::new_arm32();
10276 let op = ArmOp::Add {
10277 rd: Reg::R0,
10278 rn: Reg::R1,
10279 op2: Operand2::Reg(Reg::R2),
10280 };
10281
10282 let code = encoder.encode(&op).unwrap();
10283 assert_eq!(code.len(), 4);
10284
10285 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10286 // Verify it's an ADD instruction with correct opcode
10287 assert_eq!(instr & 0x0FE00000, 0x00800000);
10288 }
10289
10290 /// #350 — `encode_thumb32_add_imm` must lower an out-of-range immediate
10291 /// (> 0xFFF) to a legal MOVW(/MOVT) + ADD.W-register sequence instead of
10292 /// erroring. The small-imm fast path (imm <= 0xFFF) stays byte-identical.
10293 #[test]
10294 fn test_encode_add_imm_large_350() {
10295 let enc = ArmEncoder::new_thumb2();
10296
10297 // --- Fast path: imm <= 0xFFF is a single 4-byte instruction, and the
10298 // VALUE must be right (#681: this test used to assert only the length,
10299 // letting the raw-packed T3 mis-encoding of 0x123 pass CI). 0x123 is
10300 // not ThumbExpandImm-representable, so it must be ADDW (T4, plain
10301 // imm12): clang `addw r0, r1, #0x123` = f201 0023.
10302 let small = enc
10303 .encode_thumb32_add_imm(&Reg::R0, &Reg::R1, 0x123)
10304 .unwrap();
10305 assert_eq!(small, vec![0x01, 0xF2, 0x23, 0x10], "ADDW r0, r1, #0x123");
10306
10307 // helper: decode a Thumb-2 MOVW/MOVT halfword pair back to its imm16
10308 fn movx_imm16(b: &[u8]) -> u32 {
10309 let hw1 = u16::from_le_bytes([b[0], b[1]]) as u32;
10310 let hw2 = u16::from_le_bytes([b[2], b[3]]) as u32;
10311 let imm4 = hw1 & 0xF;
10312 let i = (hw1 >> 10) & 1;
10313 let imm3 = (hw2 >> 12) & 0x7;
10314 let imm8 = hw2 & 0xFF;
10315 (imm4 << 12) | (i << 11) | (imm3 << 8) | imm8
10316 }
10317 fn movx_rd(b: &[u8]) -> u32 {
10318 (u16::from_le_bytes([b[2], b[3]]) as u32 >> 8) & 0xF
10319 }
10320
10321 // --- rd != rn: scratch is rd. imm = 70000 = 0x11170 needs MOVW+MOVT. ---
10322 // 0x11170: lo16 = 0x1170, hi16 = 0x0001
10323 let seq = enc
10324 .encode_thumb32_add_imm(&Reg::R12, &Reg::R0, 70000)
10325 .unwrap();
10326 assert_eq!(seq.len(), 12, "MOVW + MOVT + ADD = 12 bytes");
10327 // MOVW r12, #0x1170
10328 assert_eq!(u16::from_le_bytes([seq[0], seq[1]]) & 0xFBF0, 0xF240);
10329 assert_eq!(movx_rd(&seq[0..4]), 12);
10330 assert_eq!(movx_imm16(&seq[0..4]), 0x1170);
10331 // MOVT r12, #0x0001
10332 assert_eq!(u16::from_le_bytes([seq[4], seq[5]]) & 0xFBF0, 0xF2C0);
10333 assert_eq!(movx_rd(&seq[4..8]), 12);
10334 assert_eq!(movx_imm16(&seq[4..8]), 0x0001);
10335 // ADD.W r12, r0, r12 (EB00 | rn=0 ; rd=12, rm=12)
10336 let add1 = u16::from_le_bytes([seq[8], seq[9]]) as u32;
10337 let add2 = u16::from_le_bytes([seq[10], seq[11]]) as u32;
10338 assert_eq!(add1 & 0xFFF0, 0xEB00);
10339 assert_eq!(add1 & 0xF, 0); // rn = r0
10340 assert_eq!((add2 >> 8) & 0xF, 12); // rd = r12
10341 assert_eq!(add2 & 0xF, 12); // rm = scratch = r12
10342 // The materialized scratch must reconstruct exactly 70000.
10343 assert_eq!(
10344 (movx_imm16(&seq[4..8]) << 16) | movx_imm16(&seq[0..4]),
10345 70000
10346 );
10347
10348 // --- imm <= 0xFFFF: MOVT is skipped (MOVW + ADD = 8 bytes). ---
10349 let seq16 = enc
10350 .encode_thumb32_add_imm(&Reg::R3, &Reg::R0, 0xABCD)
10351 .unwrap();
10352 assert_eq!(seq16.len(), 8, "imm <= 0xFFFF skips MOVT");
10353 assert_eq!(movx_imm16(&seq16[0..4]), 0xABCD);
10354 assert_eq!(movx_rd(&seq16[0..4]), 3); // scratch = rd = r3
10355
10356 // --- rd == rn (in-place add): scratch must be R12, not rd. ---
10357 // imm = 0x12345: lo16 = 0x2345, hi16 = 0x0001
10358 let inplace = enc
10359 .encode_thumb32_add_imm(&Reg::R5, &Reg::R5, 0x12345)
10360 .unwrap();
10361 assert_eq!(inplace.len(), 12);
10362 assert_eq!(movx_rd(&inplace[0..4]), 12, "rd==rn must use R12 scratch");
10363 assert_eq!(
10364 (movx_imm16(&inplace[4..8]) << 16) | movx_imm16(&inplace[0..4]),
10365 0x12345
10366 );
10367 // ADD.W r5, r5, r12 — rm must be the scratch (12), never rn.
10368 let ip_add2 = u16::from_le_bytes([inplace[10], inplace[11]]) as u32;
10369 assert_eq!(ip_add2 & 0xF, 12);
10370 assert_eq!((ip_add2 >> 8) & 0xF, 5);
10371 }
10372
10373 /// #681 — `encode_thumb32_add_imm` packed a RAW immediate into the T3
10374 /// ADD.W `i:imm3:imm8` field, which is a ThumbExpandImm MODIFIED immediate:
10375 /// ThumbExpandImm(0x200) = 0, ThumbExpandImm(0x400) = 0x8000_0000. Every
10376 /// dynamic-address load/store with a static offset in 0x100..=0xFFF
10377 /// computed a wrong address (and bypassed --safety-bounds software: the
10378 /// guard checked the intended address, the access used the mis-encoded
10379 /// one). Fix: imm <= 0xFF keeps T3 (raw == expanded there, bit-identical);
10380 /// 0x100..=0xFFF uses ADDW (T4, plain imm12) — same lowering
10381 /// `encode_thumb32_add` already uses per #253.
10382 ///
10383 /// Every expected byte sequence below is pinned against clang
10384 /// (`-target thumbv7m-none-eabi`) output, bit-for-bit (#544 pattern).
10385 #[test]
10386 fn test_encode_add_imm_thumb_expand_681() {
10387 let enc = ArmEncoder::new_thumb2();
10388 let add = |rd: &Reg, rn: &Reg, imm: u32| enc.encode_thumb32_add_imm(rd, rn, imm).unwrap();
10389
10390 // imm <= 0xFF stays T3 ADD.W (raw == ThumbExpandImm-expanded):
10391 // clang: add.w r12, r0, #0xff = f100 0cff
10392 assert_eq!(add(&Reg::R12, &Reg::R0, 0xFF), vec![0x00, 0xF1, 0xFF, 0x0C]);
10393
10394 // 0x100..=0xFFF must be ADDW (T4, plain imm12). The old T3 raw packing
10395 // decoded as +0 (0x100/0x200), +0x80000000 (0x400), etc.
10396 // clang: addw r12, r0, #0x100 = f200 1c00
10397 assert_eq!(
10398 add(&Reg::R12, &Reg::R0, 0x100),
10399 vec![0x00, 0xF2, 0x00, 0x1C]
10400 );
10401 // clang: addw r12, r0, #0x104 = f200 1c04
10402 assert_eq!(
10403 add(&Reg::R12, &Reg::R0, 0x104),
10404 vec![0x00, 0xF2, 0x04, 0x1C]
10405 );
10406 // clang: addw r12, r0, #0x200 = f200 2c00
10407 assert_eq!(
10408 add(&Reg::R12, &Reg::R0, 0x200),
10409 vec![0x00, 0xF2, 0x00, 0x2C]
10410 );
10411 // clang: addw r12, r0, #0x3fc = f200 3cfc
10412 assert_eq!(
10413 add(&Reg::R12, &Reg::R0, 0x3FC),
10414 vec![0x00, 0xF2, 0xFC, 0x3C]
10415 );
10416 // clang: addw r12, r0, #0x400 = f200 4c00
10417 assert_eq!(
10418 add(&Reg::R12, &Reg::R0, 0x400),
10419 vec![0x00, 0xF2, 0x00, 0x4C]
10420 );
10421 // clang: addw r12, r0, #0xfff = f600 7cff
10422 assert_eq!(
10423 add(&Reg::R12, &Reg::R0, 0xFFF),
10424 vec![0x00, 0xF6, 0xFF, 0x7C]
10425 );
10426 // Non-scratch rd/rn — clang: addw r1, r2, #0x104 = f202 1104
10427 assert_eq!(add(&Reg::R1, &Reg::R2, 0x104), vec![0x02, 0xF2, 0x04, 0x11]);
10428 }
10429
10430 /// #681 class audit — the T2 RSB and AND.W immediate fields are also
10431 /// ThumbExpandImm-coded and were raw-packed. Neither has a plain-imm12
10432 /// (T4-style) form, so a non-representable immediate must Err loudly
10433 /// (#253/#255/#378 class: never silently encode a different constant).
10434 /// Existing emitters only use representable values (RSB #32, AND #0x3F),
10435 /// pinned here bit-for-bit against clang.
10436 #[test]
10437 fn test_rsb_and_imm_thumb_expand_gate_681() {
10438 let enc = ArmEncoder::new_thumb2();
10439
10440 // clang: rsb.w r3, r2, #0x20 = f1c2 0320 — byte-identical to before.
10441 let rsb = enc
10442 .encode(&ArmOp::Rsb {
10443 rd: Reg::R3,
10444 rn: Reg::R2,
10445 imm: 32,
10446 })
10447 .unwrap();
10448 assert_eq!(rsb, vec![0xC2, 0xF1, 0x20, 0x03]);
10449
10450 // 0x101 is not ThumbExpandImm-representable -> must Err, not mis-encode.
10451 assert!(
10452 enc.encode(&ArmOp::Rsb {
10453 rd: Reg::R3,
10454 rn: Reg::R2,
10455 imm: 0x101,
10456 })
10457 .is_err(),
10458 "non-ThumbExpandImm RSB immediate must Err"
10459 );
10460
10461 // clang: and r4, r4, #0x3f = f004 043f — byte-identical to before.
10462 let and = enc.encode_thumb32_and_imm_raw(4, 4, 0x3F).unwrap();
10463 assert_eq!(and, vec![0x04, 0xF0, 0x3F, 0x04]);
10464 assert!(
10465 enc.encode_thumb32_and_imm_raw(4, 4, 0x101).is_err(),
10466 "non-ThumbExpandImm AND immediate must Err"
10467 );
10468
10469 // A32 RSB: imm12 is a rotate:imm8 modified immediate; > 0xFF used to be
10470 // silently masked to `imm & 0xFF` (#378 masking class) -> must Err.
10471 let a32 = ArmEncoder::new_arm32();
10472 assert!(
10473 a32.encode(&ArmOp::Rsb {
10474 rd: Reg::R3,
10475 rn: Reg::R2,
10476 imm: 0x120,
10477 })
10478 .is_err(),
10479 "A32 RSB immediate > 0xFF must Err, not mask"
10480 );
10481 // imm 32 (the only value real codegen emits) still encodes.
10482 assert!(
10483 a32.encode(&ArmOp::Rsb {
10484 rd: Reg::R3,
10485 rn: Reg::R2,
10486 imm: 32,
10487 })
10488 .is_ok()
10489 );
10490 }
10491
10492 /// #350 follow-up — the `encoder_no_panic` fuzz harness drives the encoder
10493 /// with ARBITRARY registers, including the one case the in-place lowering
10494 /// cannot serve: rd==rn==R12. There the scratch (R12, the reserved encoder
10495 /// register) would alias Rn and clobber it before the ADD reads it. The
10496 /// encoder contract (#180/#185) is Ok-or-Err, never a panic — so this must
10497 /// return Err, not assert. (Real codegen never emits rd==rn==R12 because R12
10498 /// is non-allocatable; this guards only the fuzz/adversarial path.)
10499 #[test]
10500 fn test_encode_add_imm_large_rd_rn_r12_errs_not_panics_350() {
10501 let enc = ArmEncoder::new_thumb2();
10502 // Out-of-range imm with rd==rn==R12: no free scratch -> Err.
10503 let r = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 70000);
10504 assert!(
10505 r.is_err(),
10506 "rd==rn==R12 with out-of-range imm must Err (no free scratch), got {r:?}"
10507 );
10508 // Small imm with rd==rn==R12 still takes the single-instruction fast path
10509 // (no scratch needed) and must succeed — the guard is scoped to the
10510 // out-of-range lowering only.
10511 let small = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 0x10);
10512 assert!(small.is_ok(), "small imm needs no scratch, must stay Ok");
10513 }
10514
10515 /// #378 — `encode_operand2` (ARM32 data-processing operand) must FAIL
10516 /// HONESTLY on an immediate that is not a valid rotated immediate, rather
10517 /// than silently masking it to `imm & 0xFF` and emitting a WRONG
10518 /// instruction. `0x1FF` has 9 set bits, so it cannot come from rotating an
10519 /// 8-bit imm8 — non-encodable. Real codegen materializes large constants via
10520 /// MOVW/MOVT; this guards the encoder's Ok-or-Err contract (#180/#185)
10521 /// directly. It is an Err (not a panic) so the `encoder_no_panic` fuzz
10522 /// harness — which drives arbitrary operands — still passes.
10523 #[test]
10524 fn test_encode_operand2_non_rotatable_imm_errs_not_masks_378() {
10525 let enc = ArmEncoder::new_arm32();
10526 let bad = enc.encode(&ArmOp::Add {
10527 rd: Reg::R0,
10528 rn: Reg::R1,
10529 op2: Operand2::Imm(0x1FF),
10530 });
10531 assert!(
10532 bad.is_err(),
10533 "non-rotatable ARM32 immediate 0x1FF must Err (was silently masked \
10534 to 0xFF), got {bad:?}"
10535 );
10536 // A representable rotated immediate still encodes fine (regression guard).
10537 let ok = enc.encode(&ArmOp::Add {
10538 rd: Reg::R0,
10539 rn: Reg::R1,
10540 op2: Operand2::Imm(0xFF),
10541 });
10542 assert!(
10543 ok.is_ok(),
10544 "0xFF is a valid rotated immediate, must stay Ok"
10545 );
10546 }
10547
10548 #[test]
10549 fn test_encode_ldr_arm32() {
10550 let encoder = ArmEncoder::new_arm32();
10551 let op = ArmOp::Ldr {
10552 rd: Reg::R0,
10553 addr: MemAddr::imm(Reg::R1, 4),
10554 };
10555
10556 let code = encoder.encode(&op).unwrap();
10557 assert_eq!(code.len(), 4);
10558
10559 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10560 // Verify load bit is set
10561 assert_eq!(instr & 0x00100000, 0x00100000);
10562 }
10563
10564 #[test]
10565 fn test_encode_str_arm32() {
10566 let encoder = ArmEncoder::new_arm32();
10567 let op = ArmOp::Str {
10568 rd: Reg::R0,
10569 addr: MemAddr::imm(Reg::SP, 0),
10570 };
10571
10572 let code = encoder.encode(&op).unwrap();
10573 assert_eq!(code.len(), 4);
10574 }
10575
10576 #[test]
10577 fn test_encode_branch_arm32() {
10578 let encoder = ArmEncoder::new_arm32();
10579 let op = ArmOp::Bl {
10580 label: "main".to_string(),
10581 };
10582
10583 let code = encoder.encode(&op).unwrap();
10584 assert_eq!(code.len(), 4);
10585
10586 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10587 // Verify BL opcode
10588 assert_eq!(instr & 0x0F000000, 0x0B000000);
10589 }
10590
10591 /// #1040: the A32 BL relocatable placeholder must carry an embedded addend
10592 /// of -8 so an R_ARM_CALL nets to exactly the symbol S — the A32 twin of
10593 /// the Thumb #167/#174 test above. A32 `BL` computes
10594 /// `target = P + 8 + (imm24 << 2)`, so:
10595 /// - `eb000000` (imm24 = 0, addend 0) lands at S+8, two instructions
10596 /// past the callee entry. This is what synth emitted before #1040, and
10597 /// combined with the mislabelled R_ARM_THM_CALL a real linker
10598 /// (`arm-none-eabi-ld`) corrupted the word to `eaca0000` — opcode
10599 /// `eb` (BL) flipped to `ea` (B), so LR was never set at all.
10600 /// - `ebfffffe` (imm24 = -2, offset -8) is `bl <self>` and nets to S.
10601 /// This is exactly what `arm-none-eabi-as -march=armv7-r` emits for
10602 /// `bl <extern>`, verified directly rather than derived from the ABI.
10603 #[test]
10604 fn test_encode_arm32_bl_placeholder_addend_1040() {
10605 let encoder = ArmEncoder::new_arm32();
10606 let code = encoder
10607 .encode(&ArmOp::Bl {
10608 label: "callee".to_string(),
10609 })
10610 .unwrap();
10611 assert_eq!(code.len(), 4, "A32 BL is one 32-bit word");
10612 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10613 assert_eq!(
10614 instr, 0xEBFF_FFFE,
10615 "A32 BL placeholder must be `ebfffffe` (gas's `bl <extern>`), not `eb000000` — a 0 addend resolves two instructions past the callee entry (#1040)"
10616 );
10617 // Spell the addend out so a future edit cannot satisfy the literal by
10618 // accident: sign-extended imm24, word-scaled, plus the +8 pipeline
10619 // bias, must be exactly 0 (branch-to-self).
10620 let imm24 = instr & 0x00FF_FFFF;
10621 let signed = ((imm24 as i32) << 8) >> 8;
10622 assert_eq!(
10623 8 + (signed << 2),
10624 0,
10625 "placeholder must branch to itself so the REL addend is -8"
10626 );
10627 }
10628
10629 /// Regression test for #167 + #174: the Thumb-2 BL relocatable placeholder
10630 /// must carry a -4 addend so an R_ARM_THM_CALL nets to exactly the symbol S.
10631 /// The correct encoding is what `gas` emits for `bl <extern>`: f7ff fffe
10632 /// (hw1=0xF7FF, hw2=0xFFFE), little-endian bytes FF F7 FE FF.
10633 /// - 0xD000 (J1=J2=0) → ~+0x600000 garbage addend: `bl c0000c` / truncated
10634 /// to fit (#167).
10635 /// - 0xF800 (addend 0) → lands at S+4, one instruction past the callee
10636 /// entry (#174).
10637 /// - 0xFFFE (addend -4) → lands at S. Correct.
10638 #[test]
10639 fn test_encode_thumb_bl_placeholder_addend_167_174() {
10640 let encoder = ArmEncoder::new_thumb2();
10641 let op = ArmOp::Bl {
10642 label: "callee".to_string(),
10643 };
10644
10645 let code = encoder.encode(&op).unwrap();
10646 assert_eq!(code.len(), 4, "Thumb-2 BL is 32-bit");
10647
10648 let hw1 = u16::from_le_bytes([code[0], code[1]]);
10649 let hw2 = u16::from_le_bytes([code[2], code[3]]);
10650 assert_eq!(hw1, 0xF7FF, "BL first halfword (matches gas `bl <extern>`)");
10651 assert_eq!(
10652 hw2, 0xFFFE,
10653 "BL second halfword must be 0xFFFE (-4 addend → nets to S), not 0xF800 (→ S+4, #174) or 0xD000 (#167)"
10654 );
10655 assert_ne!(hw2, 0xF800, "0xF800 (addend 0) lands at S+4 (#174)");
10656 assert_ne!(hw2, 0xD000, "0xD000 bakes in a ~+0x600000 addend (#167)");
10657 }
10658
10659 /// #740: the Thumb-2 32-bit B<cond>.W (encoding T3) must pack the
10660 /// HALFWORD offset directly into S:J2:J1:imm6:imm11 — the byte offset is
10661 /// SignExtend(S:J2:J1:imm6:imm11:'0'). The old arm packed
10662 /// `halfword_offset >> 1`, HALVING every wide conditional branch's
10663 /// displacement: gust_poll's loop-head `br_if` to an outer block end
10664 /// landed mid-shape (a spurious state write + spurious calls on the
10665 /// empty-budget path). Narrow (16-bit) B<cond> was unaffected — only
10666 /// spans > 254 bytes hit the bug. Bytes cross-checked against the llvm
10667 /// disassembler (`bne.w #0x224` = f040 8112).
10668 #[test]
10669 fn test_encode_thumb_bcond_wide_t3_halfword_offset_740() {
10670 use synth_synthesis::Condition;
10671 let encoder = ArmEncoder::new_thumb2();
10672
10673 // gust_poll's loop-head edge: NE, +0x112 halfwords (+0x224 bytes).
10674 let code = encoder
10675 .encode(&ArmOp::BCondOffset {
10676 cond: Condition::NE,
10677 offset: 0x112,
10678 })
10679 .unwrap();
10680 assert_eq!(code.len(), 4, "offset beyond ±127 halfwords must be wide");
10681 let hw1 = u16::from_le_bytes([code[0], code[1]]);
10682 let hw2 = u16::from_le_bytes([code[2], code[3]]);
10683 assert_eq!(hw1, 0xF040, "T3 hw1: 1111 0 S=0 cond=NE imm6=0");
10684 assert_eq!(
10685 hw2, 0x8112,
10686 "T3 hw2 imm11 must carry halfword offset bits [10:0] directly — \
10687 0x8089 (offset>>1) is the halved #740 miscompile"
10688 );
10689
10690 // Backward wide branch: EQ, -0x100 halfwords. S=1, J2=J1=1,
10691 // imm6=0b111111, imm11=0x700 → f43f af00.
10692 let code = encoder
10693 .encode(&ArmOp::BCondOffset {
10694 cond: Condition::EQ,
10695 offset: -0x100,
10696 })
10697 .unwrap();
10698 assert_eq!(code.len(), 4);
10699 let hw1 = u16::from_le_bytes([code[0], code[1]]);
10700 let hw2 = u16::from_le_bytes([code[2], code[3]]);
10701 assert_eq!(hw1, 0xF43F, "T3 hw1: S=1, cond=EQ, imm6=0x3F");
10702 assert_eq!(hw2, 0xAF00, "T3 hw2: J1=1 J2=1 imm11=0x700");
10703
10704 // Narrow encoding stays byte-identical (in-range offsets untouched).
10705 let code = encoder
10706 .encode(&ArmOp::BCondOffset {
10707 cond: Condition::EQ,
10708 offset: 5,
10709 })
10710 .unwrap();
10711 assert_eq!(code, vec![0x05, 0xD0], "narrow B<cond> unchanged");
10712
10713 // Out of the signed 20-bit T3 range: loud Err, never a truncated jump.
10714 assert!(
10715 encoder
10716 .encode(&ArmOp::BCondOffset {
10717 cond: Condition::NE,
10718 offset: 1 << 19,
10719 })
10720 .is_err(),
10721 "out-of-range T3 offset must be a loud decline"
10722 );
10723 }
10724
10725 #[test]
10726 fn test_encode_sequence() {
10727 let encoder = ArmEncoder::new_arm32();
10728 let ops = vec![
10729 ArmOp::Mov {
10730 rd: Reg::R0,
10731 op2: Operand2::Imm(42),
10732 },
10733 ArmOp::Mov {
10734 rd: Reg::R1,
10735 op2: Operand2::Imm(10),
10736 },
10737 ArmOp::Add {
10738 rd: Reg::R2,
10739 rn: Reg::R0,
10740 op2: Operand2::Reg(Reg::R1),
10741 },
10742 ];
10743
10744 let code = encoder.encode_sequence(&ops).unwrap();
10745 assert_eq!(code.len(), 12); // 3 instructions * 4 bytes
10746 }
10747
10748 #[test]
10749 fn test_reg_to_bits() {
10750 assert_eq!(reg_to_bits(&Reg::R0), 0);
10751 assert_eq!(reg_to_bits(&Reg::R7), 7);
10752 assert_eq!(reg_to_bits(&Reg::SP), 13);
10753 assert_eq!(reg_to_bits(&Reg::LR), 14);
10754 assert_eq!(reg_to_bits(&Reg::PC), 15);
10755 }
10756
10757 #[test]
10758 fn test_encode_bitwise_operations() {
10759 let encoder = ArmEncoder::new_arm32();
10760
10761 let and_op = ArmOp::And {
10762 rd: Reg::R0,
10763 rn: Reg::R1,
10764 op2: Operand2::Reg(Reg::R2),
10765 };
10766 let and_code = encoder.encode(&and_op).unwrap();
10767 assert_eq!(and_code.len(), 4);
10768
10769 let orr_op = ArmOp::Orr {
10770 rd: Reg::R0,
10771 rn: Reg::R1,
10772 op2: Operand2::Reg(Reg::R2),
10773 };
10774 let orr_code = encoder.encode(&orr_op).unwrap();
10775 assert_eq!(orr_code.len(), 4);
10776
10777 let eor_op = ArmOp::Eor {
10778 rd: Reg::R0,
10779 rn: Reg::R1,
10780 op2: Operand2::Reg(Reg::R2),
10781 };
10782 let eor_code = encoder.encode(&eor_op).unwrap();
10783 assert_eq!(eor_code.len(), 4);
10784 }
10785
10786 // === Thumb-2 32-bit encoding tests ===
10787
10788 #[test]
10789 fn test_encode_sdiv_thumb2() {
10790 let encoder = ArmEncoder::new_thumb2();
10791 let op = ArmOp::Sdiv {
10792 rd: Reg::R0,
10793 rn: Reg::R1,
10794 rm: Reg::R2,
10795 };
10796
10797 let code = encoder.encode(&op).unwrap();
10798 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10799
10800 // SDIV R0, R1, R2: 0xFB91 0xF0F2
10801 // First halfword: 0xFB90 | Rn(1) = 0xFB91
10802 // Second halfword: 0xF0F0 | Rd(0)<<8 | Rm(2) = 0xF0F2
10803 // Little-endian: [0x91, 0xFB, 0xF2, 0xF0]
10804 assert_eq!(code[0], 0x91);
10805 assert_eq!(code[1], 0xFB);
10806 assert_eq!(code[2], 0xF2);
10807 assert_eq!(code[3], 0xF0);
10808 }
10809
10810 #[test]
10811 fn test_encode_udiv_thumb2() {
10812 let encoder = ArmEncoder::new_thumb2();
10813 let op = ArmOp::Udiv {
10814 rd: Reg::R0,
10815 rn: Reg::R1,
10816 rm: Reg::R2,
10817 };
10818
10819 let code = encoder.encode(&op).unwrap();
10820 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10821
10822 // UDIV R0, R1, R2: 0xFBB1 0xF0F2
10823 // Little-endian: [0xB1, 0xFB, 0xF2, 0xF0]
10824 assert_eq!(code[0], 0xB1);
10825 assert_eq!(code[1], 0xFB);
10826 assert_eq!(code[2], 0xF2);
10827 assert_eq!(code[3], 0xF0);
10828 }
10829
10830 #[test]
10831 fn test_encode_mul_thumb2() {
10832 let encoder = ArmEncoder::new_thumb2();
10833 let op = ArmOp::Mul {
10834 rd: Reg::R0,
10835 rn: Reg::R1,
10836 rm: Reg::R2,
10837 };
10838
10839 let code = encoder.encode(&op).unwrap();
10840 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10841 }
10842
10843 #[test]
10844 fn test_encode_and_thumb2() {
10845 let encoder = ArmEncoder::new_thumb2();
10846 let op = ArmOp::And {
10847 rd: Reg::R0,
10848 rn: Reg::R1,
10849 op2: Operand2::Reg(Reg::R2),
10850 };
10851
10852 let code = encoder.encode(&op).unwrap();
10853 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10854 }
10855
10856 #[test]
10857 fn test_encode_lsl_thumb2_low_regs() {
10858 let encoder = ArmEncoder::new_thumb2();
10859 let op = ArmOp::Lsl {
10860 rd: Reg::R0,
10861 rn: Reg::R1,
10862 shift: 5,
10863 };
10864
10865 let code = encoder.encode(&op).unwrap();
10866 assert_eq!(code.len(), 2); // 16-bit for low registers
10867 }
10868
10869 #[test]
10870 fn test_encode_clz_thumb2() {
10871 let encoder = ArmEncoder::new_thumb2();
10872 let op = ArmOp::Clz {
10873 rd: Reg::R0,
10874 rm: Reg::R1,
10875 };
10876
10877 let code = encoder.encode(&op).unwrap();
10878 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10879 }
10880
10881 #[test]
10882 fn test_encode_bx_thumb2() {
10883 let encoder = ArmEncoder::new_thumb2();
10884 let op = ArmOp::Bx { rm: Reg::LR };
10885
10886 let code = encoder.encode(&op).unwrap();
10887 assert_eq!(code.len(), 2); // 16-bit instruction
10888
10889 // BX LR: 0x4770
10890 assert_eq!(code, vec![0x70, 0x47]);
10891 }
10892
10893 // ========================================================================
10894 // f32 pseudo-op encoding tests
10895 // ========================================================================
10896
10897 #[test]
10898 fn test_encode_f32_abs_arm32() {
10899 let encoder = ArmEncoder::new_arm32();
10900 let op = ArmOp::F32Abs {
10901 sd: VfpReg::S0,
10902 sm: VfpReg::S2,
10903 };
10904 let code = encoder.encode(&op).unwrap();
10905 assert_eq!(code.len(), 4); // Single VFP instruction
10906 }
10907
10908 #[test]
10909 fn test_encode_f32_neg_arm32() {
10910 let encoder = ArmEncoder::new_arm32();
10911 let op = ArmOp::F32Neg {
10912 sd: VfpReg::S0,
10913 sm: VfpReg::S2,
10914 };
10915 let code = encoder.encode(&op).unwrap();
10916 assert_eq!(code.len(), 4);
10917 }
10918
10919 #[test]
10920 fn test_encode_f32_sqrt_arm32() {
10921 let encoder = ArmEncoder::new_arm32();
10922 let op = ArmOp::F32Sqrt {
10923 sd: VfpReg::S0,
10924 sm: VfpReg::S2,
10925 };
10926 let code = encoder.encode(&op).unwrap();
10927 assert_eq!(code.len(), 4);
10928 }
10929
10930 #[test]
10931 fn test_encode_f32_ceil_arm32() {
10932 let encoder = ArmEncoder::new_arm32();
10933 let op = ArmOp::F32Ceil {
10934 sd: VfpReg::S0,
10935 sm: VfpReg::S2,
10936 };
10937 let code = encoder.encode(&op).unwrap();
10938 // VMRS + BIC + ORR + VMSR + VCVT.S32.F32 + VMRS + BIC + VMSR + VCVT.F32.S32
10939 assert_eq!(code.len(), 36);
10940 }
10941
10942 #[test]
10943 fn test_encode_f32_floor_thumb2() {
10944 let encoder = ArmEncoder::new_thumb2();
10945 let op = ArmOp::F32Floor {
10946 sd: VfpReg::S0,
10947 sm: VfpReg::S2,
10948 };
10949 let code = encoder.encode(&op).unwrap();
10950 // VMRS + BIC.W + ORR.W + VMSR + VCVT + VMRS + BIC.W + VMSR + VCVT.F32.S32
10951 assert_eq!(code.len(), 36);
10952 }
10953
10954 #[test]
10955 fn test_encode_f32_min_arm32() {
10956 let encoder = ArmEncoder::new_arm32();
10957 let op = ArmOp::F32Min {
10958 sd: VfpReg::S0,
10959 sn: VfpReg::S2,
10960 sm: VfpReg::S4,
10961 };
10962 let code = encoder.encode(&op).unwrap();
10963 assert_eq!(code.len(), 16); // VMOV + VCMP + VMRS + conditional VMOV
10964 }
10965
10966 #[test]
10967 fn test_encode_f32_max_thumb2() {
10968 let encoder = ArmEncoder::new_thumb2();
10969 let op = ArmOp::F32Max {
10970 sd: VfpReg::S0,
10971 sn: VfpReg::S2,
10972 sm: VfpReg::S4,
10973 };
10974 let code = encoder.encode(&op).unwrap();
10975 // VMOV(4) + VCMP(4) + VMRS(4) + IT(2) + VMOV(4) = 18
10976 assert_eq!(code.len(), 18);
10977 }
10978
10979 #[test]
10980 fn test_encode_f32_copysign_arm32() {
10981 let encoder = ArmEncoder::new_arm32();
10982 let op = ArmOp::F32Copysign {
10983 sd: VfpReg::S0,
10984 sn: VfpReg::S2,
10985 sm: VfpReg::S4,
10986 };
10987 let code = encoder.encode(&op).unwrap();
10988 // VMOV + VMOV + AND + BIC + ORR + VMOV = 6 * 4 = 24
10989 assert_eq!(code.len(), 24);
10990 }
10991
10992 // ========================================================================
10993 // f64 encoding tests
10994 // ========================================================================
10995
10996 #[test]
10997 fn test_encode_f64_add_arm32() {
10998 let encoder = ArmEncoder::new_arm32();
10999 let op = ArmOp::F64Add {
11000 dd: VfpReg::D0,
11001 dn: VfpReg::D1,
11002 dm: VfpReg::D2,
11003 };
11004 let code = encoder.encode(&op).unwrap();
11005 assert_eq!(code.len(), 4);
11006 // VADD.F64 D0, D1, D2: check coprocessor is cp11 (0xB)
11007 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11008 assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
11009 }
11010
11011 #[test]
11012 fn test_encode_f64_sub_thumb2() {
11013 let encoder = ArmEncoder::new_thumb2();
11014 let op = ArmOp::F64Sub {
11015 dd: VfpReg::D0,
11016 dn: VfpReg::D1,
11017 dm: VfpReg::D2,
11018 };
11019 let code = encoder.encode(&op).unwrap();
11020 assert_eq!(code.len(), 4); // 32-bit VFP as two Thumb halfwords
11021 }
11022
11023 #[test]
11024 fn test_encode_f64_mul_arm32() {
11025 let encoder = ArmEncoder::new_arm32();
11026 let op = ArmOp::F64Mul {
11027 dd: VfpReg::D0,
11028 dn: VfpReg::D1,
11029 dm: VfpReg::D2,
11030 };
11031 let code = encoder.encode(&op).unwrap();
11032 assert_eq!(code.len(), 4);
11033 }
11034
11035 #[test]
11036 fn test_encode_f64_div_arm32() {
11037 let encoder = ArmEncoder::new_arm32();
11038 let op = ArmOp::F64Div {
11039 dd: VfpReg::D0,
11040 dn: VfpReg::D1,
11041 dm: VfpReg::D2,
11042 };
11043 let code = encoder.encode(&op).unwrap();
11044 assert_eq!(code.len(), 4);
11045 }
11046
11047 #[test]
11048 fn test_encode_f64_abs_arm32() {
11049 let encoder = ArmEncoder::new_arm32();
11050 let op = ArmOp::F64Abs {
11051 dd: VfpReg::D0,
11052 dm: VfpReg::D2,
11053 };
11054 let code = encoder.encode(&op).unwrap();
11055 assert_eq!(code.len(), 4);
11056 }
11057
11058 #[test]
11059 fn test_encode_f64_neg_arm32() {
11060 let encoder = ArmEncoder::new_arm32();
11061 let op = ArmOp::F64Neg {
11062 dd: VfpReg::D0,
11063 dm: VfpReg::D2,
11064 };
11065 let code = encoder.encode(&op).unwrap();
11066 assert_eq!(code.len(), 4);
11067 }
11068
11069 #[test]
11070 fn test_encode_f64_sqrt_arm32() {
11071 let encoder = ArmEncoder::new_arm32();
11072 let op = ArmOp::F64Sqrt {
11073 dd: VfpReg::D0,
11074 dm: VfpReg::D2,
11075 };
11076 let code = encoder.encode(&op).unwrap();
11077 assert_eq!(code.len(), 4);
11078 }
11079
11080 #[test]
11081 fn test_encode_f64_load_arm32() {
11082 let encoder = ArmEncoder::new_arm32();
11083 let op = ArmOp::F64Load {
11084 dd: VfpReg::D0,
11085 addr: MemAddr::imm(Reg::R0, 8),
11086 };
11087 let code = encoder.encode(&op).unwrap();
11088 assert_eq!(code.len(), 4);
11089 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11090 assert_eq!((instr >> 8) & 0xF, 0xB); // cp11 for F64
11091 assert_eq!(instr & 0xFF, 2); // offset 8 / 4 = 2
11092 }
11093
11094 #[test]
11095 fn test_encode_f64_store_thumb2() {
11096 let encoder = ArmEncoder::new_thumb2();
11097 let op = ArmOp::F64Store {
11098 dd: VfpReg::D0,
11099 addr: MemAddr::imm(Reg::SP, 0),
11100 };
11101 let code = encoder.encode(&op).unwrap();
11102 assert_eq!(code.len(), 4);
11103 }
11104
11105 #[test]
11106 fn test_encode_f64_compare_arm32() {
11107 let encoder = ArmEncoder::new_arm32();
11108 let op = ArmOp::F64Eq {
11109 rd: Reg::R0,
11110 dn: VfpReg::D0,
11111 dm: VfpReg::D1,
11112 };
11113 let code = encoder.encode(&op).unwrap();
11114 assert_eq!(code.len(), 16); // VCMP + VMRS + MOV #0 + MOVcond #1
11115 }
11116
11117 #[test]
11118 fn test_encode_f64_compare_thumb2() {
11119 let encoder = ArmEncoder::new_thumb2();
11120 let op = ArmOp::F64Lt {
11121 rd: Reg::R0,
11122 dn: VfpReg::D0,
11123 dm: VfpReg::D1,
11124 };
11125 let code = encoder.encode(&op).unwrap();
11126 // VCMP(4) + VMRS(4) + MOVS(2) + IT(2) + MOV(2) = 14
11127 assert_eq!(code.len(), 14);
11128 }
11129
11130 #[test]
11131 fn test_encode_f64_const_arm32() {
11132 let encoder = ArmEncoder::new_arm32();
11133 let op = ArmOp::F64Const {
11134 dd: VfpReg::D0,
11135 value: 3.125,
11136 };
11137 let code = encoder.encode(&op).unwrap();
11138 // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
11139 assert_eq!(code.len(), 20);
11140 }
11141
11142 #[test]
11143 fn test_encode_f64_const_thumb2() {
11144 let encoder = ArmEncoder::new_thumb2();
11145 let op = ArmOp::F64Const {
11146 dd: VfpReg::D0,
11147 value: 2.5,
11148 };
11149 let code = encoder.encode(&op).unwrap();
11150 // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
11151 assert_eq!(code.len(), 20);
11152 }
11153
11154 #[test]
11155 fn test_encode_f64_convert_i32s_arm32() {
11156 let encoder = ArmEncoder::new_arm32();
11157 let op = ArmOp::F64ConvertI32S {
11158 dd: VfpReg::D0,
11159 rm: Reg::R0,
11160 };
11161 let code = encoder.encode(&op).unwrap();
11162 // VMOV(4) + VCVT(4) = 8
11163 assert_eq!(code.len(), 8);
11164 }
11165
11166 #[test]
11167 fn test_encode_f64_promote_f32_arm32() {
11168 let encoder = ArmEncoder::new_arm32();
11169 let op = ArmOp::F64PromoteF32 {
11170 dd: VfpReg::D0,
11171 sm: VfpReg::S0,
11172 };
11173 let code = encoder.encode(&op).unwrap();
11174 assert_eq!(code.len(), 4); // Single VCVT.F64.F32 instruction
11175 }
11176
11177 #[test]
11178 fn test_encode_f64_promote_f32_thumb2() {
11179 let encoder = ArmEncoder::new_thumb2();
11180 let op = ArmOp::F64PromoteF32 {
11181 dd: VfpReg::D0,
11182 sm: VfpReg::S0,
11183 };
11184 let code = encoder.encode(&op).unwrap();
11185 assert_eq!(code.len(), 4);
11186 }
11187
11188 #[test]
11189 fn test_encode_i32_trunc_f64s_arm32() {
11190 let encoder = ArmEncoder::new_arm32();
11191 let op = ArmOp::I32TruncF64S {
11192 rd: Reg::R0,
11193 dm: VfpReg::D0,
11194 };
11195 let code = encoder.encode(&op).unwrap();
11196 // VCVT(4) + VMOV(4) = 8
11197 assert_eq!(code.len(), 8);
11198 }
11199
11200 #[test]
11201 fn test_encode_f64_reinterpret_i64_arm32() {
11202 let encoder = ArmEncoder::new_arm32();
11203 let op = ArmOp::F64ReinterpretI64 {
11204 dd: VfpReg::D0,
11205 rmlo: Reg::R0,
11206 rmhi: Reg::R1,
11207 };
11208 let code = encoder.encode(&op).unwrap();
11209 assert_eq!(code.len(), 4); // Single VMOV instruction
11210 }
11211
11212 #[test]
11213 fn test_encode_i64_reinterpret_f64_thumb2() {
11214 let encoder = ArmEncoder::new_thumb2();
11215 let op = ArmOp::I64ReinterpretF64 {
11216 rdlo: Reg::R0,
11217 rdhi: Reg::R1,
11218 dm: VfpReg::D0,
11219 };
11220 let code = encoder.encode(&op).unwrap();
11221 assert_eq!(code.len(), 4);
11222 }
11223
11224 #[test]
11225 fn test_encode_f64_trunc_thumb2() {
11226 let encoder = ArmEncoder::new_thumb2();
11227 let op = ArmOp::F64Trunc {
11228 dd: VfpReg::D0,
11229 dm: VfpReg::D1,
11230 };
11231 let code = encoder.encode(&op).unwrap();
11232 // GI-FPU-002 phase 3 (#369): a single VRINTZ.F64 (clang-verified
11233 // vrintz.f64 d0,d1 base) — no more FPSCR dance / S0 clobber.
11234 assert_eq!(code.len(), 4);
11235 assert_eq!(code, vec![0xb6, 0xee, 0xc1, 0x0b]);
11236 }
11237
11238 /// GI-FPU-002 phase 3 (#369): the rewritten f64 tail sequences, byte-exact
11239 /// against clang (`-target thumbv7em-none-eabi -mfpu=fpv5-d16`). Each
11240 /// clobbers ONLY its destination (+R12/flags where noted) — the previous
11241 /// pseudo-ops staged through live S0/R0-R2 (the #615 class) and the
11242 /// min/max/rounding semantics were wrong (ordered IT select returned the
11243 /// wrong operand on NaN/±0; rounding round-tripped through a 32-bit int).
11244 #[test]
11245 fn test_369_f64_tail_thumb2_encodings_match_clang() {
11246 let enc = ArmEncoder::new_thumb2();
11247 // vrintn/vrintp/vrintm.f64 d1, d2 (FE space, never IT'd).
11248 for (op, want) in [
11249 (
11250 ArmOp::F64Nearest {
11251 dd: VfpReg::D1,
11252 dm: VfpReg::D2,
11253 },
11254 vec![0xb9, 0xfe, 0x42, 0x1b],
11255 ),
11256 (
11257 ArmOp::F64Ceil {
11258 dd: VfpReg::D1,
11259 dm: VfpReg::D2,
11260 },
11261 vec![0xba, 0xfe, 0x42, 0x1b],
11262 ),
11263 (
11264 ArmOp::F64Floor {
11265 dd: VfpReg::D1,
11266 dm: VfpReg::D2,
11267 },
11268 vec![0xbb, 0xfe, 0x42, 0x1b],
11269 ),
11270 ] {
11271 assert_eq!(enc.encode(&op).unwrap(), want, "{op:?}");
11272 }
11273 // vcmp.f64 d1,d2 ; vmrs ; vminnm.f64 d0,d1,d2 ; it vs ; vaddvs.f64
11274 let min = enc
11275 .encode(&ArmOp::F64Min {
11276 dd: VfpReg::D0,
11277 dn: VfpReg::D1,
11278 dm: VfpReg::D2,
11279 })
11280 .unwrap();
11281 assert_eq!(
11282 min,
11283 vec![
11284 0xb4, 0xee, 0x42, 0x1b, // vcmp.f64 d1, d2
11285 0xf1, 0xee, 0x10, 0xfa, // vmrs APSR_nzcv, fpscr
11286 0x81, 0xfe, 0x42, 0x0b, // vminnm.f64 d0, d1, d2
11287 0x68, 0xbf, // it vs
11288 0x31, 0xee, 0x02, 0x0b, // vaddvs.f64 d0, d1, d2
11289 ]
11290 );
11291 // vmaxnm variant flips only bit6 of the VMINNM word.
11292 let max = enc
11293 .encode(&ArmOp::F64Max {
11294 dd: VfpReg::D0,
11295 dn: VfpReg::D1,
11296 dm: VfpReg::D2,
11297 })
11298 .unwrap();
11299 assert_eq!(&max[8..12], &[0x81, 0xfe, 0x02, 0x0b]);
11300 // Destination aliasing a source must ERR (the NaN fix-up would read
11301 // a clobbered operand), never encode.
11302 assert!(
11303 enc.encode(&ArmOp::F64Min {
11304 dd: VfpReg::D1,
11305 dn: VfpReg::D1,
11306 dm: VfpReg::D2,
11307 })
11308 .is_err()
11309 );
11310 // copysign d0,(mag)d1,(sign)d2:
11311 // vmov r12,s5 ; cmp.w r12,#0 ; vabs.f64 d0,d1 ; it mi ; vnegmi.f64 d0,d0
11312 let cs = enc
11313 .encode(&ArmOp::F64Copysign {
11314 dd: VfpReg::D0,
11315 dn: VfpReg::D1,
11316 dm: VfpReg::D2,
11317 })
11318 .unwrap();
11319 assert_eq!(
11320 cs,
11321 vec![
11322 0x12, 0xee, 0x90, 0xca, // vmov r12, s5
11323 0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
11324 0xb0, 0xee, 0xc1, 0x0b, // vabs.f64 d0, d1
11325 0x48, 0xbf, // it mi
11326 0xb1, 0xee, 0x40, 0x0b, // vnegmi.f64 d0, d0
11327 ]
11328 );
11329 // f32 copysign s0,(mag)s1,(sign)s2 — the R0-clobber-free rewrite:
11330 // vmov r12,s2 ; cmp.w r12,#0 ; vabs.f32 s0,s1 ; it mi ; vnegmi.f32
11331 let cs32 = enc
11332 .encode(&ArmOp::F32Copysign {
11333 sd: VfpReg::S0,
11334 sn: VfpReg::S1,
11335 sm: VfpReg::S2,
11336 })
11337 .unwrap();
11338 assert_eq!(
11339 cs32,
11340 vec![
11341 0x11, 0xee, 0x10, 0xca, // vmov r12, s2
11342 0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
11343 0xb0, 0xee, 0xe0, 0x0a, // vabs.f32 s0, s1
11344 0x48, 0xbf, // it mi
11345 0xb1, 0xee, 0x40, 0x0a, // vnegmi.f32 s0, s0
11346 ]
11347 );
11348 // i32 -> f64 stages through the DESTINATION's S-alias (never S0) and
11349 // uses the CORRECT signed/unsigned VCVT bases (previously swapped):
11350 // vmov s0,r3 ; vcvt.f64.s32 d0,s0
11351 let conv_s = enc
11352 .encode(&ArmOp::F64ConvertI32S {
11353 dd: VfpReg::D0,
11354 rm: Reg::R3,
11355 })
11356 .unwrap();
11357 assert_eq!(
11358 conv_s,
11359 vec![
11360 0x00, 0xee, 0x10, 0x3a, // vmov s0, r3
11361 0xb8, 0xee, 0xc0, 0x0b, // vcvt.f64.s32 d0, s0
11362 ]
11363 );
11364 let conv_u = enc
11365 .encode(&ArmOp::F64ConvertI32U {
11366 dd: VfpReg::D0,
11367 rm: Reg::R3,
11368 })
11369 .unwrap();
11370 assert_eq!(&conv_u[4..8], &[0xb8, 0xee, 0x40, 0x0b]); // vcvt.f64.u32
11371 // f64 -> i32 stages through the SOURCE's S-alias (never S0):
11372 // vcvt.s32.f64 s2,d1 ; vmov r3,s2
11373 let trunc_s = enc
11374 .encode(&ArmOp::I32TruncF64S {
11375 rd: Reg::R3,
11376 dm: VfpReg::D1,
11377 })
11378 .unwrap();
11379 assert_eq!(
11380 trunc_s,
11381 vec![
11382 0xbd, 0xee, 0xc1, 0x1b, // vcvt.s32.f64 s2, d1
11383 0x11, 0xee, 0x10, 0x3a, // vmov r3, s2
11384 ]
11385 );
11386 let trunc_u = enc
11387 .encode(&ArmOp::I32TruncF64U {
11388 rd: Reg::R3,
11389 dm: VfpReg::D1,
11390 })
11391 .unwrap();
11392 assert_eq!(&trunc_u[0..4], &[0xbc, 0xee, 0xc1, 0x1b]); // vcvt.u32.f64
11393 // f32.demote_f64: vcvt.f32.f64 s1, d2
11394 let demote = enc
11395 .encode(&ArmOp::F32DemoteF64 {
11396 sd: VfpReg::S1,
11397 dm: VfpReg::D2,
11398 })
11399 .unwrap();
11400 assert_eq!(demote, vec![0xf7, 0xee, 0xc2, 0x0b]);
11401 }
11402
11403 #[test]
11404 fn test_encode_f64_min_arm32() {
11405 let encoder = ArmEncoder::new_arm32();
11406 let op = ArmOp::F64Min {
11407 dd: VfpReg::D0,
11408 dn: VfpReg::D1,
11409 dm: VfpReg::D2,
11410 };
11411 let code = encoder.encode(&op).unwrap();
11412 // VMOV + VCMP + VMRS + conditional VMOV = 16
11413 assert_eq!(code.len(), 16);
11414 }
11415
11416 #[test]
11417 fn test_f64_cp11_encoding() {
11418 // Verify that F64 instructions use coprocessor 11 (0xB), not 10 (0xA)
11419 let encoder = ArmEncoder::new_arm32();
11420
11421 // F64Add
11422 let code = encoder
11423 .encode(&ArmOp::F64Add {
11424 dd: VfpReg::D0,
11425 dn: VfpReg::D0,
11426 dm: VfpReg::D0,
11427 })
11428 .unwrap();
11429 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11430 assert_eq!((instr >> 8) & 0xF, 0xB, "F64 should use cp11");
11431
11432 // F32Add for comparison
11433 let code = encoder
11434 .encode(&ArmOp::F32Add {
11435 sd: VfpReg::S0,
11436 sn: VfpReg::S0,
11437 sm: VfpReg::S0,
11438 })
11439 .unwrap();
11440 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11441 assert_eq!((instr >> 8) & 0xF, 0xA, "F32 should use cp10");
11442 }
11443
11444 #[test]
11445 fn test_dreg_encoding_higher_registers() {
11446 let encoder = ArmEncoder::new_arm32();
11447
11448 // Test with D15 (highest register)
11449 let op = ArmOp::F64Add {
11450 dd: VfpReg::D15,
11451 dn: VfpReg::D14,
11452 dm: VfpReg::D13,
11453 };
11454 let code = encoder.encode(&op).unwrap();
11455 assert_eq!(code.len(), 4);
11456
11457 // Verify the register encoding worked (instruction is valid)
11458 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11459 assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
11460 }
11461
11462 // ========================================================================
11463 // Control flow encoding tests
11464 // ========================================================================
11465
11466 #[test]
11467 fn test_encode_label_emits_no_bytes() {
11468 let encoder = ArmEncoder::new_thumb2();
11469 let op = ArmOp::Label {
11470 name: ".Lblock_end_0".to_string(),
11471 };
11472 let code = encoder.encode(&op).unwrap();
11473 assert!(code.is_empty(), "Label should emit zero bytes");
11474
11475 let encoder32 = ArmEncoder::new_arm32();
11476 let code32 = encoder32.encode(&op).unwrap();
11477 assert!(
11478 code32.is_empty(),
11479 "Label should emit zero bytes in ARM32 too"
11480 );
11481 }
11482
11483 #[test]
11484 fn test_encode_bcc_eq_thumb2() {
11485 use synth_synthesis::Condition;
11486 let encoder = ArmEncoder::new_thumb2();
11487 let op = ArmOp::Bcc {
11488 cond: Condition::EQ,
11489 label: "target".to_string(),
11490 };
11491 let code = encoder.encode(&op).unwrap();
11492 assert_eq!(code.len(), 2); // 16-bit conditional branch
11493
11494 // BEQ with offset 0: 0xD000 in little-endian
11495 assert_eq!(code, vec![0x00, 0xD0]);
11496 }
11497
11498 #[test]
11499 fn test_encode_bcc_ne_thumb2() {
11500 use synth_synthesis::Condition;
11501 let encoder = ArmEncoder::new_thumb2();
11502 let op = ArmOp::Bcc {
11503 cond: Condition::NE,
11504 label: "target".to_string(),
11505 };
11506 let code = encoder.encode(&op).unwrap();
11507 assert_eq!(code.len(), 2);
11508
11509 // BNE with offset 0: 0xD100 in little-endian
11510 assert_eq!(code, vec![0x00, 0xD1]);
11511 }
11512
11513 #[test]
11514 fn test_encode_bcc_arm32() {
11515 use synth_synthesis::Condition;
11516 let encoder = ArmEncoder::new_arm32();
11517 let op = ArmOp::Bcc {
11518 cond: Condition::EQ,
11519 label: "target".to_string(),
11520 };
11521 let code = encoder.encode(&op).unwrap();
11522 assert_eq!(code.len(), 4); // 32-bit ARM instruction
11523
11524 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11525 // BEQ: cond=0x0, opcode=0xA, offset=0
11526 assert_eq!(instr & 0xF0000000, 0x00000000); // EQ condition
11527 assert_eq!(instr & 0x0F000000, 0x0A000000); // Branch opcode
11528 }
11529
11530 #[test]
11531 fn test_encode_udf_thumb2() {
11532 let encoder = ArmEncoder::new_thumb2();
11533 let op = ArmOp::Udf { imm: 0 };
11534 let code = encoder.encode(&op).unwrap();
11535 assert_eq!(code.len(), 2); // 16-bit
11536
11537 // UDF #0: 0xDE00 in little-endian
11538 assert_eq!(code, vec![0x00, 0xDE]);
11539 }
11540
11541 /// #610: the i64 rot/div/rem expansions must land the result in the
11542 /// selector-assigned rd pair and leave R0-R3 preserved (restored from the
11543 /// fixed-ABI wrapper's save area) — pre-#610 the rot expansion's own
11544 /// `POP {R4}` restored stale scratch OVER the result (rd_lo == R4) and
11545 /// the div/rem expansions ignored their register fields outright.
11546 #[test]
11547 fn test_610_i64_rot_expansion_ends_with_rd_movs_and_restore() {
11548 let encoder = ArmEncoder::new_thumb2();
11549 for op in [
11550 ArmOp::I64Rotl {
11551 rdlo: Reg::R4,
11552 rdhi: Reg::R5,
11553 rnlo: Reg::R0,
11554 rnhi: Reg::R1,
11555 shift: Reg::R2,
11556 },
11557 ArmOp::I64Rotr {
11558 rdlo: Reg::R4,
11559 rdhi: Reg::R5,
11560 rnlo: Reg::R0,
11561 rnhi: Reg::R1,
11562 shift: Reg::R2,
11563 },
11564 ] {
11565 let code = encoder.encode(&op).unwrap();
11566 assert_eq!(code.len(), 102, "register-independent size (estimator pin)");
11567 // Tail: MOV r5, r1 (0x460D); MOV r4, r0 (0x4604); POP {r0..r3}
11568 // (rd pair r4:r5 does not overlap the save area — all 4 restored).
11569 let tail: Vec<u16> = code[code.len() - 12..]
11570 .chunks(2)
11571 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11572 .collect();
11573 assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11574 }
11575 }
11576
11577 /// #610: div/rem expansions honor rd and carry the divide-by-zero trap
11578 /// guard (`ORRS R12, R2, R3; BNE +0; UDF #0`) after operand marshaling.
11579 #[test]
11580 fn test_610_i64_div_rem_expansion_guard_and_rd() {
11581 let encoder = ArmEncoder::new_thumb2();
11582 let mk = |which: u8| {
11583 let (rdlo, rdhi, rnlo, rnhi, rmlo, rmhi) =
11584 (Reg::R4, Reg::R5, Reg::R0, Reg::R1, Reg::R2, Reg::R3);
11585 match which {
11586 0 => ArmOp::I64DivU {
11587 rdlo,
11588 rdhi,
11589 rnlo,
11590 rnhi,
11591 rmlo,
11592 rmhi,
11593 elide_zero_guard: false,
11594 },
11595 1 => ArmOp::I64RemU {
11596 rdlo,
11597 rdhi,
11598 rnlo,
11599 rnhi,
11600 rmlo,
11601 rmhi,
11602 elide_zero_guard: false,
11603 },
11604 2 => ArmOp::I64DivS {
11605 rdlo,
11606 rdhi,
11607 rnlo,
11608 rnhi,
11609 rmlo,
11610 rmhi,
11611 elide_zero_guard: false,
11612 elide_overflow_guard: false,
11613 },
11614 _ => ArmOp::I64RemS {
11615 rdlo,
11616 rdhi,
11617 rnlo,
11618 rnhi,
11619 rmlo,
11620 rmhi,
11621 elide_zero_guard: false,
11622 },
11623 }
11624 };
11625 for which in 0..4u8 {
11626 let code = encoder.encode(&mk(which)).unwrap();
11627 // Zero-divisor trap guard right after the 26-byte marshal prologue.
11628 let guard: Vec<u16> = code[26..34]
11629 .chunks(2)
11630 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11631 .collect();
11632 assert_eq!(
11633 guard,
11634 vec![0xEA52, 0x0C03, 0xD100, 0xDE00],
11635 "ORRS R12,R2,R3; BNE +0; UDF #0"
11636 );
11637 // Tail: result into rd pair (r5:r4), then restore all of R0-R3.
11638 let tail: Vec<u16> = code[code.len() - 12..]
11639 .chunks(2)
11640 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11641 .collect();
11642 assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11643 }
11644 }
11645
11646 /// #610: when rd overlaps R0-R3 the restore must SKIP the result
11647 /// registers (drop the saved caller word) instead of popping over them.
11648 #[test]
11649 fn test_610_i64_divu_rd_in_r0_r1_skips_restore() {
11650 let encoder = ArmEncoder::new_thumb2();
11651 let code = encoder
11652 .encode(&ArmOp::I64DivU {
11653 rdlo: Reg::R0,
11654 rdhi: Reg::R1,
11655 rnlo: Reg::R0,
11656 rnhi: Reg::R1,
11657 rmlo: Reg::R2,
11658 rmhi: Reg::R3,
11659 elide_zero_guard: false,
11660 })
11661 .unwrap();
11662 let tail: Vec<u16> = code[code.len() - 12..]
11663 .chunks(2)
11664 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11665 .collect();
11666 // MOV r1,r1 / MOV r0,r0 (no-ops, size-stable), ADD SP,#4 twice
11667 // (discard saved r0/r1 — the result lives there), POP {r2}, POP {r3}.
11668 assert_eq!(tail, vec![0x4609, 0x4600, 0xB001, 0xB001, 0xBC04, 0xBC08]);
11669 }
11670
11671 /// #610: a fully swapped rd pair (rd_lo=R1, rd_hi=R0) cannot be
11672 /// materialized by two MOVs in either order — must be a loud Err, never
11673 /// silent corruption. (Selector pairs are consecutive, so unreachable.)
11674 #[test]
11675 fn test_610_i64_swapped_rd_pair_rejected() {
11676 let encoder = ArmEncoder::new_thumb2();
11677 let result = encoder.encode(&ArmOp::I64RemU {
11678 rdlo: Reg::R1,
11679 rdhi: Reg::R0,
11680 rnlo: Reg::R2,
11681 rnhi: Reg::R3,
11682 rmlo: Reg::R4,
11683 rmhi: Reg::R5,
11684 elide_zero_guard: false,
11685 });
11686 assert!(result.is_err(), "swapped rd pair must be rejected loudly");
11687 }
11688
11689 /// #632: the I64Popcnt expansion's own scratch restore (`POP {R3,R4,R5}`)
11690 /// must not clobber the result. Pre-fix the total was materialized with
11691 /// `ADDS rd, R4, R5` BEFORE the pop, so any allocator-assigned
11692 /// rd ∈ {R3,R4,R5} received stale stack garbage. Post-fix the count is
11693 /// carried across the restore in R12 (never allocatable, never restored)
11694 /// and moved into rd only after the pop — structurally rd-independent.
11695 #[test]
11696 fn test_632_i64_popcnt_result_survives_scratch_restore() {
11697 let encoder = ArmEncoder::new_thumb2();
11698 // Every allocatable rd, including the restore set {R3,R4,R5} and R8.
11699 for rd in [
11700 Reg::R0,
11701 Reg::R2,
11702 Reg::R3,
11703 Reg::R4,
11704 Reg::R5,
11705 Reg::R6,
11706 Reg::R8,
11707 ] {
11708 let code = encoder
11709 .encode(&ArmOp::I64Popcnt {
11710 rd,
11711 rnlo: Reg::R6,
11712 rnhi: Reg::R7,
11713 })
11714 .unwrap();
11715 assert_eq!(code.len(), 176, "register-independent size (estimator pin)");
11716 let hw: Vec<u16> = code
11717 .chunks(2)
11718 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11719 .collect();
11720 let pop = hw
11721 .iter()
11722 .position(|&h| h == 0xBC38)
11723 .expect("POP {R3,R4,R5} present");
11724 // Immediately before the POP: ADD.W R12, R4, R5 (the total lives
11725 // in R12, which the POP cannot touch).
11726 assert_eq!(
11727 &hw[pop - 2..pop],
11728 &[0xEB04, 0x0C05],
11729 "total must be carried in R12 across the restore"
11730 );
11731 // Immediately after the POP: MOV rd, R12.
11732 let rd_bits = match rd {
11733 Reg::R8 => 8u16,
11734 Reg::R6 => 6,
11735 Reg::R5 => 5,
11736 Reg::R4 => 4,
11737 Reg::R3 => 3,
11738 Reg::R2 => 2,
11739 _ => 0,
11740 };
11741 let expect_mov = 0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7);
11742 assert_eq!(hw[pop + 1], expect_mov, "MOV rd, R12 after the restore");
11743 // No write into rd between the PUSH and the POP (the old
11744 // pre-restore ADDS is gone).
11745 assert!(
11746 !hw[..pop].contains(&(0x1800 | (5 << 6) | (4 << 3) | rd_bits)),
11747 "no ADDS rd, R4, R5 before the restore pop"
11748 );
11749 }
11750 }
11751
11752 /// #632 audit: the entry marshal must be permutation-safe. Pre-fix
11753 /// `MOV R4, rnlo; MOV R5, rnhi` read a clobbered R4 when the operand
11754 /// pair lived at (R3, R4). Post-fix rnlo routes through R12.
11755 #[test]
11756 fn test_632_i64_popcnt_marshal_pair_at_r3_r4() {
11757 let encoder = ArmEncoder::new_thumb2();
11758 let code = encoder
11759 .encode(&ArmOp::I64Popcnt {
11760 rd: Reg::R0,
11761 rnlo: Reg::R3,
11762 rnhi: Reg::R4,
11763 })
11764 .unwrap();
11765 let hw: Vec<u16> = code
11766 .chunks(2)
11767 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11768 .collect();
11769 // PUSH {R3,R4,R5}; MOV R12, R3; MOV R5, R4 (rnhi read BEFORE any
11770 // write to R4); MOV R4, R12.
11771 assert_eq!(hw[0], 0xB438);
11772 assert_eq!(hw[1], 0x4600 | (1 << 7) | (3 << 3) | 4, "MOV R12, rnlo");
11773 assert_eq!(hw[2], 0x4600 | (4 << 3) | 5, "MOV R5, rnhi");
11774 assert_eq!(hw[3], 0x4664, "MOV R4, R12");
11775 }
11776
11777 /// #632: A32 twin — same structural fix on the ARM-mode path
11778 /// (`--target cortex-r5`): total carried in R12 across the restore.
11779 #[test]
11780 fn test_632_a32_i64_popcnt_result_survives_scratch_restore() {
11781 let encoder = ArmEncoder::new_arm32();
11782 for rd in [Reg::R0, Reg::R3, Reg::R4, Reg::R5, Reg::R8] {
11783 let code = encoder
11784 .encode(&ArmOp::I64Popcnt {
11785 rd,
11786 rnlo: Reg::R6,
11787 rnhi: Reg::R7,
11788 })
11789 .unwrap();
11790 let words: Vec<u32> = code
11791 .chunks(4)
11792 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11793 .collect();
11794 let pop = words
11795 .iter()
11796 .position(|&w| w == 0xE8BD_0038)
11797 .expect("POP {R3,R4,R5} present");
11798 assert_eq!(words[pop - 1], 0xE084_C005, "ADD R12, R4, R5 before POP");
11799 let rd_bits = match rd {
11800 Reg::R8 => 8u32,
11801 Reg::R5 => 5,
11802 Reg::R4 => 4,
11803 Reg::R3 => 3,
11804 _ => 0,
11805 };
11806 assert_eq!(
11807 words[pop + 1],
11808 0xE1A0_0000 | (rd_bits << 12) | 12,
11809 "MOV rd, R12 after the restore"
11810 );
11811 }
11812 }
11813
11814 /// #633: I64DivS must carry the INT64_MIN/-1 overflow guard (mirroring
11815 /// the i32 path) right after the zero-divisor guard — dividend in R0:R1,
11816 /// divisor in R2:R3 on the #610/#613 fixed-ABI wrapper path.
11817 #[test]
11818 fn test_633_i64_divs_overflow_guard_emitted() {
11819 let encoder = ArmEncoder::new_thumb2();
11820 let code = encoder
11821 .encode(&ArmOp::I64DivS {
11822 rdlo: Reg::R4,
11823 rdhi: Reg::R5,
11824 rnlo: Reg::R0,
11825 rnhi: Reg::R1,
11826 rmlo: Reg::R2,
11827 rmhi: Reg::R3,
11828 elide_zero_guard: false,
11829 elide_overflow_guard: false,
11830 })
11831 .unwrap();
11832 // 26-byte marshal + 8-byte zero-trap, then the 22-byte overflow guard.
11833 let guard: Vec<u16> = code[34..56]
11834 .chunks(2)
11835 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11836 .collect();
11837 assert_eq!(
11838 guard,
11839 vec![
11840 0xEA02, 0x0C03, // AND.W R12, R2, R3
11841 0xF11C, 0x0F01, // CMN.W R12, #1
11842 0xD105, // BNE .no_trap
11843 0x2800, // CMP R0, #0
11844 0xD103, // BNE .no_trap
11845 0xF1B1, 0x4F00, // CMP.W R1, #0x80000000
11846 0xD100, // BNE .no_trap
11847 0xDE00, // UDF #0 — signed-division overflow
11848 ],
11849 "INT64_MIN/-1 overflow guard after the zero-divisor guard"
11850 );
11851 }
11852
11853 /// #633 fix-guard twin: I64RemS must NOT carry the overflow guard —
11854 /// rem_s(INT64_MIN, -1) is defined as 0 and must not trap. Exactly one
11855 /// UDF (the zero-divisor trap) in the whole expansion.
11856 #[test]
11857 fn test_633_i64_rems_has_no_overflow_guard() {
11858 let encoder = ArmEncoder::new_thumb2();
11859 for (is_rem_s, op) in [
11860 (
11861 true,
11862 ArmOp::I64RemS {
11863 rdlo: Reg::R4,
11864 rdhi: Reg::R5,
11865 rnlo: Reg::R0,
11866 rnhi: Reg::R1,
11867 rmlo: Reg::R2,
11868 rmhi: Reg::R3,
11869 elide_zero_guard: false,
11870 },
11871 ),
11872 (
11873 false,
11874 ArmOp::I64DivS {
11875 rdlo: Reg::R4,
11876 rdhi: Reg::R5,
11877 rnlo: Reg::R0,
11878 rnhi: Reg::R1,
11879 rmlo: Reg::R2,
11880 rmhi: Reg::R3,
11881 elide_zero_guard: false,
11882 elide_overflow_guard: false,
11883 },
11884 ),
11885 ] {
11886 let code = encoder.encode(&op).unwrap();
11887 let udfs = code
11888 .chunks(2)
11889 .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11890 .count();
11891 let want = if is_rem_s { 1 } else { 2 };
11892 assert_eq!(
11893 udfs, want,
11894 "rem_s: zero-trap only; div_s: zero-trap + overflow trap"
11895 );
11896 }
11897 }
11898
11899 /// #494 phase 2b: `elide_zero_guard` drops EXACTLY the 8-byte fused
11900 /// zero-trap (`ORRS.W R12,R2,R3; BNE; UDF #0`) and nothing else — the
11901 /// rest of the expansion is byte-identical (splice check).
11902 #[test]
11903 fn test_494_i64_zero_guard_elision_is_exact_splice() {
11904 let encoder = ArmEncoder::new_thumb2();
11905 let mk = |elide_zero_guard: bool| {
11906 encoder
11907 .encode(&ArmOp::I64DivU {
11908 rdlo: Reg::R4,
11909 rdhi: Reg::R5,
11910 rnlo: Reg::R0,
11911 rnhi: Reg::R1,
11912 rmlo: Reg::R2,
11913 rmhi: Reg::R3,
11914 elide_zero_guard,
11915 })
11916 .unwrap()
11917 };
11918 let full = mk(false);
11919 let elided = mk(true);
11920 assert_eq!(full.len(), elided.len() + 8, "zero guard is 8 bytes");
11921 // Marshal prologue (26 B) unchanged, guard (8 B) gone, tail identical.
11922 assert_eq!(&full[..26], &elided[..26]);
11923 assert_eq!(
11924 &full[26..34],
11925 &[0x52, 0xEA, 0x03, 0x0C, 0x00, 0xD1, 0x00, 0xDE],
11926 "the spliced-out bytes are exactly ORRS.W; BNE; UDF #0"
11927 );
11928 assert_eq!(&full[34..], &elided[26..]);
11929 }
11930
11931 /// #494 phase 2b two-guard distinction (the #633/#634 synergy): a
11932 /// divisor-nonzero fact elides ONLY the zero guard — the INT64_MIN/-1
11933 /// OVERFLOW guard is a separate obligation and must survive
11934 /// `elide_zero_guard: true`. Pinned on div_s in all flag states.
11935 #[test]
11936 fn test_494_i64_divs_overflow_guard_retained_when_only_zero_elided() {
11937 let encoder = ArmEncoder::new_thumb2();
11938 let mk = |zero: bool, ovf: bool| {
11939 encoder
11940 .encode(&ArmOp::I64DivS {
11941 rdlo: Reg::R4,
11942 rdhi: Reg::R5,
11943 rnlo: Reg::R0,
11944 rnhi: Reg::R1,
11945 rmlo: Reg::R2,
11946 rmhi: Reg::R3,
11947 elide_zero_guard: zero,
11948 elide_overflow_guard: ovf,
11949 })
11950 .unwrap()
11951 };
11952 let udf_count = |code: &[u8]| {
11953 code.chunks(2)
11954 .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11955 .count()
11956 };
11957 let full = mk(false, false);
11958 let zero_only = mk(true, false);
11959 let both = mk(true, true);
11960 assert_eq!(udf_count(&full), 2, "baseline: zero trap + overflow trap");
11961 assert_eq!(
11962 udf_count(&zero_only),
11963 1,
11964 "divisor-nonzero elides the zero trap ONLY — the #633 overflow \
11965 guard must be retained"
11966 );
11967 // The retained guard is the 22-byte overflow sequence, now right
11968 // after the 26-byte marshal prologue.
11969 let guard: Vec<u16> = zero_only[26..48]
11970 .chunks(2)
11971 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11972 .collect();
11973 assert_eq!(
11974 guard,
11975 vec![
11976 0xEA02, 0x0C03, 0xF11C, 0x0F01, 0xD105, 0x2800, 0xD103, 0xF1B1, 0x4F00, 0xD100,
11977 0xDE00,
11978 ],
11979 "the surviving guard is the INT64_MIN/-1 overflow trap"
11980 );
11981 assert_eq!(full.len(), zero_only.len() + 8);
11982 assert_eq!(zero_only.len(), both.len() + 22);
11983 assert_eq!(udf_count(&both), 0, "both obligations discharged ⇒ no UDF");
11984 }
11985
11986 /// #494 phase 2b A32 twin: zero-guard elision is an exact 12-byte splice
11987 /// and the A32 overflow guard survives a zero-only elision.
11988 #[test]
11989 fn test_494_a32_i64_guard_elision() {
11990 let encoder = ArmEncoder::new_arm32();
11991 let mk = |zero: bool, ovf: bool| {
11992 encoder
11993 .encode(&ArmOp::I64DivS {
11994 rdlo: Reg::R4,
11995 rdhi: Reg::R5,
11996 rnlo: Reg::R0,
11997 rnhi: Reg::R1,
11998 rmlo: Reg::R2,
11999 rmhi: Reg::R3,
12000 elide_zero_guard: zero,
12001 elide_overflow_guard: ovf,
12002 })
12003 .unwrap()
12004 };
12005 let full = mk(false, false);
12006 let zero_only = mk(true, false);
12007 let both = mk(true, true);
12008 // A32 zero guard = 3 words (ORRS/BNE/UDF), overflow guard = 6 words.
12009 assert_eq!(full.len(), zero_only.len() + 12);
12010 assert_eq!(zero_only.len(), both.len() + 24);
12011 let udf_count = |code: &[u8]| {
12012 code.chunks(4)
12013 .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
12014 .count()
12015 };
12016 assert_eq!(udf_count(&full), 2);
12017 assert_eq!(
12018 udf_count(&zero_only),
12019 1,
12020 "A32: overflow guard retained under zero-only elision"
12021 );
12022 assert_eq!(udf_count(&both), 0);
12023 }
12024
12025 /// #633: A32 twin — the conditional-execution overflow guard on the
12026 /// ARM-mode I64DivS, and its absence from I64RemS.
12027 #[test]
12028 fn test_633_a32_i64_divs_overflow_guard() {
12029 let encoder = ArmEncoder::new_arm32();
12030 let mk_divs = ArmOp::I64DivS {
12031 rdlo: Reg::R4,
12032 rdhi: Reg::R5,
12033 rnlo: Reg::R0,
12034 rnhi: Reg::R1,
12035 rmlo: Reg::R2,
12036 rmhi: Reg::R3,
12037 elide_zero_guard: false,
12038 elide_overflow_guard: false,
12039 };
12040 let code = encoder.encode(&mk_divs).unwrap();
12041 let words: Vec<u32> = code
12042 .chunks(4)
12043 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
12044 .collect();
12045 let guard = [
12046 0xE002_C003u32, // AND R12, R2, R3
12047 0xE37C_0001, // CMN R12, #1
12048 0x0350_0000, // CMPEQ R0, #0
12049 0x0351_0102, // CMPEQ R1, #0x80000000
12050 0x1A00_0000, // BNE +1 insn
12051 0xE7F0_00F0, // UDF #0
12052 ];
12053 assert!(
12054 words.windows(6).any(|w| w == guard),
12055 "A32 I64DivS carries the INT64_MIN/-1 overflow guard"
12056 );
12057 let rems = encoder
12058 .encode(&ArmOp::I64RemS {
12059 rdlo: Reg::R4,
12060 rdhi: Reg::R5,
12061 rnlo: Reg::R0,
12062 rnhi: Reg::R1,
12063 rmlo: Reg::R2,
12064 rmhi: Reg::R3,
12065 elide_zero_guard: false,
12066 })
12067 .unwrap();
12068 let rems_udfs = rems
12069 .chunks(4)
12070 .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
12071 .count();
12072 assert_eq!(rems_udfs, 1, "A32 I64RemS keeps only the zero-divisor trap");
12073 }
12074
12075 #[test]
12076 fn test_encode_nop_thumb2() {
12077 let encoder = ArmEncoder::new_thumb2();
12078 let op = ArmOp::Nop;
12079 let code = encoder.encode(&op).unwrap();
12080 assert_eq!(code.len(), 2); // 16-bit
12081
12082 // NOP: 0xBF00 in little-endian
12083 assert_eq!(code, vec![0x00, 0xBF]);
12084 }
12085
12086 // =========================================================================
12087 // i64 Thumb-2 encoding tests
12088 // =========================================================================
12089
12090 #[test]
12091 fn test_encode_i64_add_thumb2() {
12092 let encoder = ArmEncoder::new_thumb2();
12093 let op = ArmOp::I64Add {
12094 rdlo: Reg::R0,
12095 rdhi: Reg::R1,
12096 rnlo: Reg::R0,
12097 rnhi: Reg::R1,
12098 rmlo: Reg::R2,
12099 rmhi: Reg::R3,
12100 };
12101 let code = encoder.encode(&op).unwrap();
12102 // Should emit ADDS (2 bytes) + ADC.W (4 bytes) = 6 bytes
12103 assert_eq!(code.len(), 6, "I64Add should be 6 bytes (ADDS + ADC.W)");
12104 }
12105
12106 #[test]
12107 fn test_encode_i64_sub_thumb2() {
12108 let encoder = ArmEncoder::new_thumb2();
12109 let op = ArmOp::I64Sub {
12110 rdlo: Reg::R0,
12111 rdhi: Reg::R1,
12112 rnlo: Reg::R0,
12113 rnhi: Reg::R1,
12114 rmlo: Reg::R2,
12115 rmhi: Reg::R3,
12116 };
12117 let code = encoder.encode(&op).unwrap();
12118 // Should emit SUBS (2 bytes) + SBC.W (4 bytes) = 6 bytes
12119 assert_eq!(code.len(), 6, "I64Sub should be 6 bytes (SUBS + SBC.W)");
12120 }
12121
12122 #[test]
12123 fn test_encode_i64_and_thumb2() {
12124 let encoder = ArmEncoder::new_thumb2();
12125 let op = ArmOp::I64And {
12126 rdlo: Reg::R0,
12127 rdhi: Reg::R1,
12128 rnlo: Reg::R0,
12129 rnhi: Reg::R1,
12130 rmlo: Reg::R2,
12131 rmhi: Reg::R3,
12132 };
12133 let code = encoder.encode(&op).unwrap();
12134 // AND.W (4 bytes) + AND.W (4 bytes) = 8 bytes
12135 assert!(code.len() >= 4, "I64And should emit at least 4 bytes");
12136 }
12137
12138 #[test]
12139 fn test_encode_i64_or_thumb2() {
12140 let encoder = ArmEncoder::new_thumb2();
12141 let op = ArmOp::I64Or {
12142 rdlo: Reg::R0,
12143 rdhi: Reg::R1,
12144 rnlo: Reg::R0,
12145 rnhi: Reg::R1,
12146 rmlo: Reg::R2,
12147 rmhi: Reg::R3,
12148 };
12149 let code = encoder.encode(&op).unwrap();
12150 assert!(code.len() >= 4, "I64Or should emit at least 4 bytes");
12151 }
12152
12153 #[test]
12154 fn test_encode_i64_xor_thumb2() {
12155 let encoder = ArmEncoder::new_thumb2();
12156 let op = ArmOp::I64Xor {
12157 rdlo: Reg::R0,
12158 rdhi: Reg::R1,
12159 rnlo: Reg::R0,
12160 rnhi: Reg::R1,
12161 rmlo: Reg::R2,
12162 rmhi: Reg::R3,
12163 };
12164 let code = encoder.encode(&op).unwrap();
12165 assert!(code.len() >= 4, "I64Xor should emit at least 4 bytes");
12166 }
12167
12168 #[test]
12169 fn test_encode_i64_const_small_thumb2() {
12170 let encoder = ArmEncoder::new_thumb2();
12171 // Small constant: only needs MOVW for each half
12172 let op = ArmOp::I64Const {
12173 rdlo: Reg::R0,
12174 rdhi: Reg::R1,
12175 value: 42,
12176 };
12177 let code = encoder.encode(&op).unwrap();
12178 // MOVW R0, #42 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes minimum
12179 assert!(code.len() >= 8, "I64Const should emit at least 8 bytes");
12180 }
12181
12182 #[test]
12183 fn test_encode_i64_const_large_thumb2() {
12184 let encoder = ArmEncoder::new_thumb2();
12185 // Large constant: needs MOVW+MOVT for each half
12186 let op = ArmOp::I64Const {
12187 rdlo: Reg::R0,
12188 rdhi: Reg::R1,
12189 value: 0x1234_5678_9ABC_DEF0_u64 as i64,
12190 };
12191 let code = encoder.encode(&op).unwrap();
12192 // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
12193 assert_eq!(
12194 code.len(),
12195 16,
12196 "I64Const with large value should be 16 bytes"
12197 );
12198 }
12199
12200 #[test]
12201 fn test_encode_i64_extend_i32_s_thumb2() {
12202 let encoder = ArmEncoder::new_thumb2();
12203 let op = ArmOp::I64ExtendI32S {
12204 rdlo: Reg::R0,
12205 rdhi: Reg::R1,
12206 rn: Reg::R0,
12207 };
12208 let code = encoder.encode(&op).unwrap();
12209 // When rdlo == rn, only ASR (4 bytes) is emitted
12210 assert_eq!(
12211 code.len(),
12212 4,
12213 "I64ExtendI32S (same reg) should be 4 bytes (ASR only)"
12214 );
12215 }
12216
12217 #[test]
12218 fn test_encode_i64_extend_i32_s_diff_reg_thumb2() {
12219 let encoder = ArmEncoder::new_thumb2();
12220 let op = ArmOp::I64ExtendI32S {
12221 rdlo: Reg::R0,
12222 rdhi: Reg::R1,
12223 rn: Reg::R2,
12224 };
12225 let code = encoder.encode(&op).unwrap();
12226 // MOV rdlo, rn (2 bytes for low regs) + ASR rdhi, rdlo, #31 (4 bytes) = 6 bytes
12227 assert!(
12228 code.len() >= 6,
12229 "I64ExtendI32S (diff reg) should be at least 6 bytes"
12230 );
12231 }
12232
12233 #[test]
12234 fn test_encode_i64_extend_i32_u_thumb2() {
12235 let encoder = ArmEncoder::new_thumb2();
12236 let op = ArmOp::I64ExtendI32U {
12237 rdlo: Reg::R0,
12238 rdhi: Reg::R1,
12239 rn: Reg::R0,
12240 };
12241 let code = encoder.encode(&op).unwrap();
12242 // When rdlo == rn, only MOV rdhi, #0 (2 bytes) is emitted
12243 assert_eq!(
12244 code.len(),
12245 2,
12246 "I64ExtendI32U (same reg) should be 2 bytes (MOV #0 only)"
12247 );
12248 }
12249
12250 #[test]
12251 fn test_encode_i32_wrap_i64_nop_thumb2() {
12252 let encoder = ArmEncoder::new_thumb2();
12253 // When rd == rnlo, should be a NOP
12254 let op = ArmOp::I32WrapI64 {
12255 rd: Reg::R0,
12256 rnlo: Reg::R0,
12257 };
12258 let code = encoder.encode(&op).unwrap();
12259 assert_eq!(code.len(), 2, "I32WrapI64 same reg should be NOP (2 bytes)");
12260 assert_eq!(code, vec![0x00, 0xBF]); // NOP
12261 }
12262
12263 #[test]
12264 fn test_encode_i32_wrap_i64_diff_reg_thumb2() {
12265 let encoder = ArmEncoder::new_thumb2();
12266 let op = ArmOp::I32WrapI64 {
12267 rd: Reg::R2,
12268 rnlo: Reg::R0,
12269 };
12270 let code = encoder.encode(&op).unwrap();
12271 // MOV R2, R0 (2 or 4 bytes)
12272 assert!(
12273 code.len() >= 2,
12274 "I32WrapI64 diff reg should emit at least 2 bytes"
12275 );
12276 }
12277
12278 #[test]
12279 fn test_encode_i64_eqz_thumb2() {
12280 let encoder = ArmEncoder::new_thumb2();
12281 let op = ArmOp::I64Eqz {
12282 rd: Reg::R0,
12283 rnlo: Reg::R0,
12284 rnhi: Reg::R1,
12285 };
12286 let code = encoder.encode(&op).unwrap();
12287 // Delegates to I64SetCondZ which is already encoded
12288 assert!(
12289 code.len() >= 6,
12290 "I64Eqz should emit at least 6 bytes for ORR+ITE+MOV+MOV"
12291 );
12292 }
12293
12294 #[test]
12295 fn test_encode_i64_eq_thumb2() {
12296 let encoder = ArmEncoder::new_thumb2();
12297 let op = ArmOp::I64Eq {
12298 rd: Reg::R0,
12299 rnlo: Reg::R0,
12300 rnhi: Reg::R1,
12301 rmlo: Reg::R2,
12302 rmhi: Reg::R3,
12303 };
12304 let code = encoder.encode(&op).unwrap();
12305 // Delegates to I64SetCond EQ: CMP lo + IT EQ + CMPEQ hi + ITE EQ + MOV 1 + MOV 0
12306 assert!(code.len() >= 10, "I64Eq should emit at least 10 bytes");
12307 }
12308
12309 #[test]
12310 fn test_encode_i64_ldr_thumb2() {
12311 let encoder = ArmEncoder::new_thumb2();
12312 let op = ArmOp::I64Ldr {
12313 rdlo: Reg::R0,
12314 rdhi: Reg::R1,
12315 addr: MemAddr::imm(Reg::SP, 0),
12316 };
12317 let code = encoder.encode(&op).unwrap();
12318 // Two LDR instructions (lo at offset, hi at offset+4)
12319 assert!(code.len() >= 4, "I64Ldr should emit at least 4 bytes");
12320 }
12321
12322 #[test]
12323 fn test_372_i64_ldr_indexed_materializes_address() {
12324 // #372: a memory i64.load carries an index register (R11 + addr + off).
12325 // The encoder must materialize `ip = base + index` (ADD.W) and load via
12326 // `[ip,#off]` — NOT drop the index. A frame (non-indexed) i64.load must
12327 // stay byte-identical (plain `[base,#off]`, no ADD).
12328 let encoder = ArmEncoder::new_thumb2();
12329 let indexed = encoder
12330 .encode(&ArmOp::I64Ldr {
12331 rdlo: Reg::R0,
12332 rdhi: Reg::R1,
12333 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
12334 })
12335 .unwrap();
12336 // ADD.W ip, fp, r0 = eb0b 0c00 (byte-verified vs arm-none-eabi-as).
12337 assert_eq!(
12338 &indexed[0..4],
12339 &[0x0b, 0xeb, 0x00, 0x0c],
12340 "indexed I64Ldr must start with ADD.W ip, base, index"
12341 );
12342 let frame = encoder
12343 .encode(&ArmOp::I64Ldr {
12344 rdlo: Reg::R0,
12345 rdhi: Reg::R1,
12346 addr: MemAddr::imm(Reg::SP, 8),
12347 })
12348 .unwrap();
12349 // No index -> no ADD.W prefix (byte-identical frame access).
12350 assert_ne!(
12351 &frame[0..2],
12352 &[0x0b, 0xeb],
12353 "frame (non-indexed) I64Ldr must NOT emit an ADD.W"
12354 );
12355 }
12356
12357 #[test]
12358 fn test_382_i64_ldst_large_offset_materializes_not_skips() {
12359 // #382: an indexed i64.load/store whose static offset > 0xFFF must
12360 // MATERIALIZE the offset into the base — NOT return Err (skip the fn).
12361 // Sequence for reg_imm(R11, R0, 5000): MOVW ip,#5000 ; ADD ip,r0,ip ;
12362 // ADD ip,ip,fp ; LDR/STR halves at [ip,#0] / [ip,#4]. Byte-verified tail
12363 // vs arm-none-eabi-as.
12364 let encoder = ArmEncoder::new_thumb2();
12365 // 0x1388 > 0xFFF (MemAddr is not Copy, so build it per use).
12366
12367 let ld = encoder
12368 .encode(&ArmOp::I64Ldr {
12369 rdlo: Reg::R0,
12370 rdhi: Reg::R1,
12371 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
12372 })
12373 .expect("large-offset i64.load must lower, not skip");
12374 // MOVW ip,#0x1388 (4) + ADD ip,r0,ip (4) + ADD ip,ip,fp (4) + 2 LDR (8).
12375 assert_eq!(ld.len(), 20, "expected MOVW + 2×ADD + 2×LDR");
12376 // Must NOT be the small-offset `ADD.W ip, fp, r0` (0x0b 0xeb) prefix —
12377 // that path can only reach imm12 offsets.
12378 assert_ne!(
12379 &ld[0..2],
12380 &[0x0b, 0xeb],
12381 "must materialize the large offset"
12382 );
12383 // Effective base built in ip, then halves at [ip,#0] / [ip,#4].
12384 assert_eq!(
12385 &ld[4..20],
12386 &[
12387 0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
12388 0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
12389 0xdc, 0xf8, 0x00, 0x00, // LDR.W r0, [ip, #0]
12390 0xdc, 0xf8, 0x04, 0x10, // LDR.W r1, [ip, #4]
12391 ],
12392 "large-offset i64.load must fold offset into ip and access [ip,#0]/[ip,#4]"
12393 );
12394
12395 // Store: same base materialization, STR halves.
12396 let st = encoder
12397 .encode(&ArmOp::I64Str {
12398 rdlo: Reg::R2,
12399 rdhi: Reg::R3,
12400 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
12401 })
12402 .expect("large-offset i64.store must lower, not skip");
12403 assert_eq!(st.len(), 20);
12404 assert_eq!(
12405 &st[4..20],
12406 &[
12407 0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
12408 0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
12409 0xcc, 0xf8, 0x00, 0x20, // STR.W r2, [ip, #0]
12410 0xcc, 0xf8, 0x04, 0x30, // STR.W r3, [ip, #4]
12411 ],
12412 "large-offset i64.store must fold offset into ip and access [ip,#0]/[ip,#4]"
12413 );
12414
12415 // Small-offset (imm12) indexed access stays byte-identical (#372): the
12416 // effective base is a single `ADD.W ip, fp, r0` and the halves keep the
12417 // folded immediates — NO extra MOVW/ADD.
12418 let small = encoder
12419 .encode(&ArmOp::I64Ldr {
12420 rdlo: Reg::R0,
12421 rdhi: Reg::R1,
12422 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 8),
12423 })
12424 .unwrap();
12425 assert_eq!(
12426 &small[0..4],
12427 &[0x0b, 0xeb, 0x00, 0x0c],
12428 "small-offset indexed i64 must keep the single ADD.W ip, fp, r0"
12429 );
12430 assert_eq!(small.len(), 12, "ADD.W + 2×LDR.W (offset folded in imm12)");
12431 }
12432
12433 /// RQ-63-ARMI64OFF (#1165): A32 `I64Ldr`/`I64Str` at and past the pair
12434 /// form's immediate boundary. Bytes verified against `arm-none-eabi-as`
12435 /// (`.arch armv7-r`, `.arm`). 4091 is the last offset the pair folds (the
12436 /// high half sits at 4095 = imm12 max); 4092 is the straddle the v0.62
12437 /// census declined on (low half fits, high half does not).
12438 #[test]
12439 fn test_1165_a32_i64_ldst_offset_boundary_materializes() {
12440 let enc = ArmEncoder::new_arm32();
12441 let le = |ws: &[u32]| ws.iter().flat_map(|w| w.to_le_bytes()).collect::<Vec<u8>>();
12442 let ld = |off: i32| {
12443 enc.encode(&ArmOp::I64Ldr {
12444 rdlo: Reg::R0,
12445 rdhi: Reg::R1,
12446 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, off),
12447 })
12448 };
12449 // AT the boundary (4091): folded, byte-identical to the pre-#1165 form.
12450 assert_eq!(
12451 ld(4091).unwrap(),
12452 le(&[0xE08BC000, 0xE59C0FFB, 0xE59C1FFF]),
12453 "4091: add ip,fp,r0 ; ldr r0,[ip,#4091] ; ldr r1,[ip,#4095]"
12454 );
12455 // ONE ABOVE (4092): the high half would need #4096 → materialize.
12456 assert_eq!(
12457 ld(4092).unwrap(),
12458 le(&[0xE300CFFC, 0xE08CC00B, 0xE08CC000, 0xE59C0000, 0xE59C1004]),
12459 "4092: movw ip,#4092 ; add ip,ip,fp ; add ip,ip,r0 ; ldr r0,[ip] ; ldr r1,[ip,#4]"
12460 );
12461 // 4095 (low half at the imm12 max), 4096 (both out), 5000.
12462 assert_eq!(ld(4095).unwrap()[..4], le(&[0xE300CFFF])[..]);
12463 assert_eq!(ld(4096).unwrap()[..4], le(&[0xE301C000])[..]);
12464 assert_eq!(
12465 ld(5000).unwrap(),
12466 le(&[0xE301C388, 0xE08CC00B, 0xE08CC000, 0xE59C0000, 0xE59C1004])
12467 );
12468 // 70000 needs MOVT; store form.
12469 assert_eq!(
12470 enc.encode(&ArmOp::I64Str {
12471 rdlo: Reg::R2,
12472 rdhi: Reg::R3,
12473 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 70000),
12474 })
12475 .unwrap(),
12476 le(&[
12477 0xE301C170, 0xE340C001, 0xE08CC00B, 0xE08CC000, 0xE58C2000, 0xE58C3004
12478 ]),
12479 "70000: movw ip,#0x1170 ; movt ip,#1 ; add ip,ip,fp ; add ip,ip,r0 ; str r2,[ip] ; str r3,[ip,#4]"
12480 );
12481 // A memarg >= 2^31 arrives negative (0xFFFFFFF8 as i32 = -8): the full
12482 // 32 bits are materialized, mod 2^32 — never clamped, never masked.
12483 assert_eq!(
12484 ld(-8).unwrap(),
12485 le(&[
12486 0xE30FCFF8, 0xE34FCFFF, 0xE08CC00B, 0xE08CC000, 0xE59C0000, 0xE59C1004
12487 ]),
12488 "0xfffffff8: movw ip,#0xfff8 ; movt ip,#0xffff ; add ; add ; ldr ; ldr"
12489 );
12490 // No index register (frame-style base), out of range: MOVW ; ADD ip,ip,base.
12491 assert_eq!(
12492 enc.encode(&ArmOp::I64Ldr {
12493 rdlo: Reg::R0,
12494 rdhi: Reg::R1,
12495 addr: MemAddr::imm(Reg::SP, 5000),
12496 })
12497 .unwrap(),
12498 le(&[0xE301C388, 0xE08CC00D, 0xE59C0000, 0xE59C1004])
12499 );
12500 // The base IS the scratch: no free register → typed error, not a clobber.
12501 assert!(
12502 enc.encode(&ArmOp::I64Ldr {
12503 rdlo: Reg::R0,
12504 rdhi: Reg::R1,
12505 addr: MemAddr::imm(Reg::R12, 5000),
12506 })
12507 .is_err(),
12508 "materializing over an R12 base must be refused, not clobber it"
12509 );
12510 }
12511
12512 /// RQ-63-ARMI64OFF (#1165): the A32 WORD and SUB-WORD immediate arms used
12513 /// to MASK an out-of-range offset (`& 0xFFF` / `& 0xFF`) — a silent wrong
12514 /// address (`i32.load offset=5000` → `ldr r0,[ip,#904]`, `i32.load16_u
12515 /// offset=256` → `ldrh r0,[ip]`). At the field maximum the bytes are
12516 /// unchanged; one above, the offset is materialized. Bytes verified
12517 /// against `arm-none-eabi-as`.
12518 #[test]
12519 fn test_1165_a32_word_subword_offset_boundary_materializes_not_masks() {
12520 let enc = ArmEncoder::new_arm32();
12521 let le = |ws: &[u32]| ws.iter().flat_map(|w| w.to_le_bytes()).collect::<Vec<u8>>();
12522 let idx = |off: i32| MemAddr::reg_imm(Reg::R11, Reg::R0, off);
12523 let ldr = |addr| enc.encode(&ArmOp::Ldr { rd: Reg::R0, addr }).unwrap();
12524 let ldrh = |addr| enc.encode(&ArmOp::Ldrh { rd: Reg::R0, addr }).unwrap();
12525 let ldrsb = |addr| enc.encode(&ArmOp::Ldrsb { rd: Reg::R0, addr }).unwrap();
12526 let strh = |addr| enc.encode(&ArmOp::Strh { rd: Reg::R1, addr }).unwrap();
12527 let ldrb = |addr| enc.encode(&ArmOp::Ldrb { rd: Reg::R0, addr }).unwrap();
12528 // LDR imm12: 4095 at, 4096 above.
12529 assert_eq!(ldr(idx(4095)), le(&[0xE08BC000, 0xE59C0FFF]));
12530 assert_eq!(
12531 ldr(idx(4096)),
12532 le(&[0xE301C000, 0xE08CC00B, 0xE08CC000, 0xE59C0000]),
12533 "ldr 4096 must materialize, not mask to #0"
12534 );
12535 // LDRH imm8: 255 at, 256 above (the pre-fix bytes were `ldrh r0,[ip]`).
12536 assert_eq!(ldrh(idx(255)), le(&[0xE08BC000, 0xE1DC0FBF]));
12537 assert_eq!(
12538 ldrh(idx(256)),
12539 le(&[0xE300C100, 0xE08CC00B, 0xE08CC000, 0xE1DC00B0]),
12540 "ldrh 256 must materialize, not mask to #0"
12541 );
12542 // LDRSB imm8 (256 above), STRH imm8 (300 above).
12543 assert_eq!(
12544 ldrsb(idx(256)),
12545 le(&[0xE300C100, 0xE08CC00B, 0xE08CC000, 0xE1DC00D0])
12546 );
12547 assert_eq!(
12548 strh(idx(300)),
12549 le(&[0xE300C12C, 0xE08CC00B, 0xE08CC000, 0xE1CC10B0])
12550 );
12551 // LDRB is an imm12 form: 4095 at, 4096 above.
12552 assert_eq!(ldrb(idx(4095)), le(&[0xE08BC000, 0xE5DC0FFF]));
12553 assert_eq!(
12554 ldrb(idx(4096)),
12555 le(&[0xE301C000, 0xE08CC00B, 0xE08CC000, 0xE5DC0000])
12556 );
12557 // No index, out of range: MOVW ; ADD ip,ip,base ; access [ip].
12558 assert_eq!(
12559 ldr(MemAddr::imm(Reg::R0, 5000)),
12560 le(&[0xE301C388, 0xE08CC000, 0xE59C0000])
12561 );
12562 // The immediate arms are now a TRIPWIRE, never a mask.
12563 assert!(encode_mem_addr(&MemAddr::imm(Reg::R1, 0xFFF)).is_ok());
12564 assert!(
12565 encode_mem_addr(&MemAddr::imm(Reg::R1, 0x1000)).is_err(),
12566 "imm12 arm must refuse 0x1000, not mask it to #0"
12567 );
12568 assert!(encode_mem_addr_imm8(&MemAddr::imm(Reg::R1, 0xFF)).is_ok());
12569 assert!(
12570 encode_mem_addr_imm8(&MemAddr::imm(Reg::R1, 0x100)).is_err(),
12571 "imm8 arm must refuse 0x100, not mask it to #0"
12572 );
12573 // Base is the scratch: refused, not clobbered.
12574 assert!(
12575 enc.encode(&ArmOp::Ldr {
12576 rd: Reg::R0,
12577 addr: MemAddr::imm(Reg::R12, 5000),
12578 })
12579 .is_err()
12580 );
12581 }
12582
12583 /// RQ-63-ARMI64OFF (#1165): Thumb-2 `i64_effective_base` used to CLAMP a
12584 /// negative offset (a memarg >= 2^31 after the selector's `as i32` cast)
12585 /// to 0 — `i64.load offset=0xfffffff8` read `[R11+addr]`. It now
12586 /// materializes the full 32 bits (MOVW+MOVT), the arithmetic the i32 word
12587 /// path uses for the same memarg. Bytes verified against
12588 /// `arm-none-eabi-as` (`.arch armv7e-m`, `.thumb`). A FRAME access with a
12589 /// negative offset (no index register) is refused loudly by
12590 /// `check_ldst_imm12`, never clamped.
12591 #[test]
12592 fn test_1165_thumb2_i64_negative_offset_materializes_not_clamps() {
12593 let enc = ArmEncoder::new_thumb2();
12594 let ld = enc
12595 .encode(&ArmOp::I64Ldr {
12596 rdlo: Reg::R0,
12597 rdhi: Reg::R1,
12598 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, -8),
12599 })
12600 .unwrap();
12601 assert_eq!(
12602 ld,
12603 vec![
12604 0x4f, 0xf6, 0xf8, 0x7c, // movw ip, #0xfff8
12605 0xcf, 0xf6, 0xff, 0x7c, // movt ip, #0xffff
12606 0x00, 0xeb, 0x0c, 0x0c, // add.w ip, r0, ip
12607 0x0c, 0xeb, 0x0b, 0x0c, // add.w ip, ip, fp
12608 0xdc, 0xf8, 0x00, 0x00, // ldr.w r0, [ip]
12609 0xdc, 0xf8, 0x04, 0x10, // ldr.w r1, [ip, #4]
12610 ],
12611 "0xfffffff8 must be materialized in full, not clamped to [ip,#0]"
12612 );
12613 let st = enc
12614 .encode(&ArmOp::I64Str {
12615 rdlo: Reg::R2,
12616 rdhi: Reg::R3,
12617 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, i32::MIN),
12618 })
12619 .unwrap();
12620 assert_eq!(
12621 st,
12622 vec![
12623 0x40, 0xf2, 0x00, 0x0c, // movw ip, #0
12624 0xc8, 0xf2, 0x00, 0x0c, // movt ip, #0x8000
12625 0x00, 0xeb, 0x0c, 0x0c, // add.w ip, r0, ip
12626 0x0c, 0xeb, 0x0b, 0x0c, // add.w ip, ip, fp
12627 0xcc, 0xf8, 0x00, 0x20, // str.w r2, [ip]
12628 0xcc, 0xf8, 0x04, 0x30, // str.w r3, [ip, #4]
12629 ]
12630 );
12631 assert!(
12632 enc.encode(&ArmOp::I64Ldr {
12633 rdlo: Reg::R0,
12634 rdhi: Reg::R1,
12635 addr: MemAddr::imm(Reg::SP, -8),
12636 })
12637 .is_err(),
12638 "a negative frame offset must be refused, not clamped to [sp,#0]"
12639 );
12640 }
12641
12642 #[test]
12643 fn test_encode_i64_str_thumb2() {
12644 let encoder = ArmEncoder::new_thumb2();
12645 let op = ArmOp::I64Str {
12646 rdlo: Reg::R0,
12647 rdhi: Reg::R1,
12648 addr: MemAddr::imm(Reg::SP, 0),
12649 };
12650 let code = encoder.encode(&op).unwrap();
12651 // Two STR instructions (lo at offset, hi at offset+4)
12652 assert!(code.len() >= 4, "I64Str should emit at least 4 bytes");
12653 }
12654
12655 #[test]
12656 fn test_encode_i64_all_comparisons_thumb2() {
12657 let encoder = ArmEncoder::new_thumb2();
12658
12659 let ops = vec![
12660 ArmOp::I64Ne {
12661 rd: Reg::R0,
12662 rnlo: Reg::R0,
12663 rnhi: Reg::R1,
12664 rmlo: Reg::R2,
12665 rmhi: Reg::R3,
12666 },
12667 ArmOp::I64LtS {
12668 rd: Reg::R0,
12669 rnlo: Reg::R0,
12670 rnhi: Reg::R1,
12671 rmlo: Reg::R2,
12672 rmhi: Reg::R3,
12673 },
12674 ArmOp::I64LtU {
12675 rd: Reg::R0,
12676 rnlo: Reg::R0,
12677 rnhi: Reg::R1,
12678 rmlo: Reg::R2,
12679 rmhi: Reg::R3,
12680 },
12681 ArmOp::I64LeS {
12682 rd: Reg::R0,
12683 rnlo: Reg::R0,
12684 rnhi: Reg::R1,
12685 rmlo: Reg::R2,
12686 rmhi: Reg::R3,
12687 },
12688 ArmOp::I64LeU {
12689 rd: Reg::R0,
12690 rnlo: Reg::R0,
12691 rnhi: Reg::R1,
12692 rmlo: Reg::R2,
12693 rmhi: Reg::R3,
12694 },
12695 ArmOp::I64GtS {
12696 rd: Reg::R0,
12697 rnlo: Reg::R0,
12698 rnhi: Reg::R1,
12699 rmlo: Reg::R2,
12700 rmhi: Reg::R3,
12701 },
12702 ArmOp::I64GtU {
12703 rd: Reg::R0,
12704 rnlo: Reg::R0,
12705 rnhi: Reg::R1,
12706 rmlo: Reg::R2,
12707 rmhi: Reg::R3,
12708 },
12709 ArmOp::I64GeS {
12710 rd: Reg::R0,
12711 rnlo: Reg::R0,
12712 rnhi: Reg::R1,
12713 rmlo: Reg::R2,
12714 rmhi: Reg::R3,
12715 },
12716 ArmOp::I64GeU {
12717 rd: Reg::R0,
12718 rnlo: Reg::R0,
12719 rnhi: Reg::R1,
12720 rmlo: Reg::R2,
12721 rmhi: Reg::R3,
12722 },
12723 ];
12724
12725 for op in &ops {
12726 let code = encoder.encode(op).unwrap();
12727 assert!(
12728 code.len() >= 8,
12729 "i64 comparison {:?} should emit at least 8 bytes, got {}",
12730 op,
12731 code.len()
12732 );
12733 }
12734 }
12735
12736 #[test]
12737 fn test_encode_i64_const_zero_thumb2() {
12738 let encoder = ArmEncoder::new_thumb2();
12739 let op = ArmOp::I64Const {
12740 rdlo: Reg::R0,
12741 rdhi: Reg::R1,
12742 value: 0,
12743 };
12744 let code = encoder.encode(&op).unwrap();
12745 // MOVW R0, #0 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes
12746 assert_eq!(code.len(), 8, "I64Const(0) should be 8 bytes");
12747 }
12748
12749 #[test]
12750 fn test_encode_i64_const_negative_one_thumb2() {
12751 let encoder = ArmEncoder::new_thumb2();
12752 let op = ArmOp::I64Const {
12753 rdlo: Reg::R0,
12754 rdhi: Reg::R1,
12755 value: -1, // 0xFFFF_FFFF_FFFF_FFFF
12756 };
12757 let code = encoder.encode(&op).unwrap();
12758 // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
12759 assert_eq!(code.len(), 16, "I64Const(-1) should be 16 bytes");
12760 }
12761
12762 // =========================================================================
12763 // Sub-word load/store encoding tests
12764 // =========================================================================
12765
12766 #[test]
12767 fn test_encode_ldrb_arm32() {
12768 let encoder = ArmEncoder::new_arm32();
12769 let op = ArmOp::Ldrb {
12770 rd: Reg::R0,
12771 addr: MemAddr::imm(Reg::R1, 4),
12772 };
12773 let code = encoder.encode(&op).unwrap();
12774 assert_eq!(code.len(), 4, "ARM32 LDRB should be 4 bytes");
12775 // LDRB R0, [R1, #4] = 0xE5D10004
12776 let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12777 assert_eq!(encoded, 0xE5D10004, "Should encode LDRB R0, [R1, #4]");
12778 }
12779
12780 #[test]
12781 fn test_encode_strb_arm32() {
12782 let encoder = ArmEncoder::new_arm32();
12783 let op = ArmOp::Strb {
12784 rd: Reg::R0,
12785 addr: MemAddr::imm(Reg::R1, 0),
12786 };
12787 let code = encoder.encode(&op).unwrap();
12788 assert_eq!(code.len(), 4, "ARM32 STRB should be 4 bytes");
12789 // STRB R0, [R1, #0] = 0xE5C10000
12790 let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12791 assert_eq!(encoded, 0xE5C10000, "Should encode STRB R0, [R1, #0]");
12792 }
12793
12794 #[test]
12795 fn test_encode_ldrh_arm32() {
12796 let encoder = ArmEncoder::new_arm32();
12797 let op = ArmOp::Ldrh {
12798 rd: Reg::R0,
12799 addr: MemAddr::imm(Reg::R1, 2),
12800 };
12801 let code = encoder.encode(&op).unwrap();
12802 assert_eq!(code.len(), 4, "ARM32 LDRH should be 4 bytes");
12803 }
12804
12805 #[test]
12806 fn test_encode_strh_arm32() {
12807 let encoder = ArmEncoder::new_arm32();
12808 let op = ArmOp::Strh {
12809 rd: Reg::R0,
12810 addr: MemAddr::imm(Reg::R1, 0),
12811 };
12812 let code = encoder.encode(&op).unwrap();
12813 assert_eq!(code.len(), 4, "ARM32 STRH should be 4 bytes");
12814 }
12815
12816 #[test]
12817 fn test_encode_ldrsb_arm32() {
12818 let encoder = ArmEncoder::new_arm32();
12819 let op = ArmOp::Ldrsb {
12820 rd: Reg::R0,
12821 addr: MemAddr::imm(Reg::R1, 0),
12822 };
12823 let code = encoder.encode(&op).unwrap();
12824 assert_eq!(code.len(), 4, "ARM32 LDRSB should be 4 bytes");
12825 }
12826
12827 #[test]
12828 fn test_encode_ldrsh_arm32() {
12829 let encoder = ArmEncoder::new_arm32();
12830 let op = ArmOp::Ldrsh {
12831 rd: Reg::R0,
12832 addr: MemAddr::imm(Reg::R1, 0),
12833 };
12834 let code = encoder.encode(&op).unwrap();
12835 assert_eq!(code.len(), 4, "ARM32 LDRSH should be 4 bytes");
12836 }
12837
12838 #[test]
12839 fn test_encode_ldrb_thumb2_16bit() {
12840 let encoder = ArmEncoder::new_thumb2();
12841 let op = ArmOp::Ldrb {
12842 rd: Reg::R0,
12843 addr: MemAddr::imm(Reg::R1, 4),
12844 };
12845 let code = encoder.encode(&op).unwrap();
12846 // Low registers + small offset -> 16-bit encoding
12847 assert_eq!(
12848 code.len(),
12849 2,
12850 "Thumb-2 LDRB with small offset should be 16-bit"
12851 );
12852 }
12853
12854 #[test]
12855 fn test_encode_ldrb_thumb2_32bit() {
12856 let encoder = ArmEncoder::new_thumb2();
12857 let op = ArmOp::Ldrb {
12858 rd: Reg::R0,
12859 addr: MemAddr::imm(Reg::R1, 100), // offset > 31 needs 32-bit
12860 };
12861 let code = encoder.encode(&op).unwrap();
12862 assert_eq!(
12863 code.len(),
12864 4,
12865 "Thumb-2 LDRB with large offset should be 32-bit"
12866 );
12867 }
12868
12869 #[test]
12870 fn test_encode_strb_thumb2_16bit() {
12871 let encoder = ArmEncoder::new_thumb2();
12872 let op = ArmOp::Strb {
12873 rd: Reg::R0,
12874 addr: MemAddr::imm(Reg::R1, 10),
12875 };
12876 let code = encoder.encode(&op).unwrap();
12877 assert_eq!(
12878 code.len(),
12879 2,
12880 "Thumb-2 STRB with small offset should be 16-bit"
12881 );
12882 }
12883
12884 #[test]
12885 fn test_encode_ldrh_thumb2_16bit() {
12886 let encoder = ArmEncoder::new_thumb2();
12887 let op = ArmOp::Ldrh {
12888 rd: Reg::R0,
12889 addr: MemAddr::imm(Reg::R1, 4), // offset aligned to 2, <= 62
12890 };
12891 let code = encoder.encode(&op).unwrap();
12892 assert_eq!(
12893 code.len(),
12894 2,
12895 "Thumb-2 LDRH with small aligned offset should be 16-bit"
12896 );
12897 }
12898
12899 #[test]
12900 fn test_encode_strh_thumb2_16bit() {
12901 let encoder = ArmEncoder::new_thumb2();
12902 let op = ArmOp::Strh {
12903 rd: Reg::R0,
12904 addr: MemAddr::imm(Reg::R1, 4),
12905 };
12906 let code = encoder.encode(&op).unwrap();
12907 assert_eq!(
12908 code.len(),
12909 2,
12910 "Thumb-2 STRH with small aligned offset should be 16-bit"
12911 );
12912 }
12913
12914 #[test]
12915 fn test_encode_ldrsb_thumb2() {
12916 let encoder = ArmEncoder::new_thumb2();
12917 let op = ArmOp::Ldrsb {
12918 rd: Reg::R0,
12919 addr: MemAddr::imm(Reg::R1, 0),
12920 };
12921 let code = encoder.encode(&op).unwrap();
12922 // LDRSB has no 16-bit immediate form, always 32-bit
12923 assert_eq!(code.len(), 4, "Thumb-2 LDRSB should be 32-bit");
12924 }
12925
12926 #[test]
12927 fn test_encode_ldrsh_thumb2() {
12928 let encoder = ArmEncoder::new_thumb2();
12929 let op = ArmOp::Ldrsh {
12930 rd: Reg::R0,
12931 addr: MemAddr::imm(Reg::R1, 0),
12932 };
12933 let code = encoder.encode(&op).unwrap();
12934 assert_eq!(code.len(), 4, "Thumb-2 LDRSH should be 32-bit");
12935 }
12936
12937 #[test]
12938 fn test_encode_memory_size_thumb2() {
12939 // #1441: EXACT BYTES, both page sizes. The previous assertion was
12940 // `!code.is_empty()`, which passes for ANY encoding — including the
12941 // shift-of-32 this lane's whole risk is about.
12942 let encoder = ArmEncoder::new_thumb2();
12943
12944 // Default 64 KiB page: LSR.W R0, R10, #16.
12945 // 1110 1010 010 0 1111 | 0 imm3=100 Rd=0000 imm2=00 01 Rm=1010
12946 let dflt = encoder
12947 .encode(&ArmOp::MemorySize {
12948 rd: Reg::R0,
12949 page_log2: 16,
12950 })
12951 .unwrap();
12952 assert_eq!(
12953 dflt,
12954 vec![0x4F, 0xEA, 0x1A, 0x40],
12955 "the DEFAULT page must stay BYTE-IDENTICAL to the pre-#1441 \
12956 encoding — every frozen fixture depends on it"
12957 );
12958
12959 // `(pagesize 1)`: the byte count IS the page count, so MOV.W R0, R10.
12960 // NOT an LSR with a zero immediate, which encodes a shift BY 32 and
12961 // would make `memory.size` return 0 for every byte-paged memory.
12962 let ps1 = encoder
12963 .encode(&ArmOp::MemorySize {
12964 rd: Reg::R0,
12965 page_log2: 0,
12966 })
12967 .unwrap();
12968 assert_eq!(
12969 ps1,
12970 vec![0x4F, 0xEA, 0x0A, 0x00],
12971 "(pagesize 1) must encode MOV.W R0, R10 — a shift of 0 is not an LSR"
12972 );
12973 assert_ne!(ps1, dflt, "the two page sizes must not emit the same word");
12974
12975 // An unencodable shift REFUSES rather than emitting a wrong word
12976 // (#180/#185: the encoder is Ok-or-Err, never silently wrong).
12977 assert!(
12978 encoder
12979 .encode(&ArmOp::MemorySize {
12980 rd: Reg::R0,
12981 page_log2: 32,
12982 })
12983 .is_err(),
12984 "page_log2 = 32 is not an encodable shift and must be refused"
12985 );
12986 }
12987
12988 #[test]
12989 fn test_encode_memory_grow_thumb2() {
12990 let encoder = ArmEncoder::new_thumb2();
12991 let op = ArmOp::MemoryGrow {
12992 rd: Reg::R0,
12993 rn: Reg::R0,
12994 };
12995 let code = encoder.encode(&op).unwrap();
12996 assert_eq!(code.len(), 4, "MemoryGrow (MVN) should be 32-bit Thumb-2");
12997 }
12998
12999 #[test]
13000 fn test_encode_subword_reg_offset_thumb2() {
13001 let encoder = ArmEncoder::new_thumb2();
13002
13003 // LDRB with register offset
13004 let op = ArmOp::Ldrb {
13005 rd: Reg::R0,
13006 addr: MemAddr::reg(Reg::R1, Reg::R2),
13007 };
13008 let code = encoder.encode(&op).unwrap();
13009 assert_eq!(
13010 code.len(),
13011 4,
13012 "Thumb-2 LDRB with reg offset should be 32-bit"
13013 );
13014
13015 // STRB with register offset
13016 let op = ArmOp::Strb {
13017 rd: Reg::R0,
13018 addr: MemAddr::reg(Reg::R1, Reg::R2),
13019 };
13020 let code = encoder.encode(&op).unwrap();
13021 assert_eq!(
13022 code.len(),
13023 4,
13024 "Thumb-2 STRB with reg offset should be 32-bit"
13025 );
13026
13027 // LDRH with register offset
13028 let op = ArmOp::Ldrh {
13029 rd: Reg::R0,
13030 addr: MemAddr::reg(Reg::R1, Reg::R2),
13031 };
13032 let code = encoder.encode(&op).unwrap();
13033 assert_eq!(
13034 code.len(),
13035 4,
13036 "Thumb-2 LDRH with reg offset should be 32-bit"
13037 );
13038
13039 // STRH with register offset
13040 let op = ArmOp::Strh {
13041 rd: Reg::R0,
13042 addr: MemAddr::reg(Reg::R1, Reg::R2),
13043 };
13044 let code = encoder.encode(&op).unwrap();
13045 assert_eq!(
13046 code.len(),
13047 4,
13048 "Thumb-2 STRH with reg offset should be 32-bit"
13049 );
13050 }
13051
13052 #[test]
13053 fn test_encode_subword_reg_imm_offset_thumb2() {
13054 let encoder = ArmEncoder::new_thumb2();
13055
13056 // LDRB with both register and immediate offset
13057 let op = ArmOp::Ldrb {
13058 rd: Reg::R0,
13059 addr: MemAddr::reg_imm(Reg::R1, Reg::R2, 4),
13060 };
13061 let code = encoder.encode(&op).unwrap();
13062 // ADD R12, R2, #4 (4 bytes) + LDRB R0, [R1, R12] (4 bytes) = 8 bytes
13063 assert_eq!(
13064 code.len(),
13065 8,
13066 "Thumb-2 LDRB with reg+imm offset should be 8 bytes"
13067 );
13068 }
13069
13070 // ========================================================================
13071 // Helium MVE encoding tests
13072 // ========================================================================
13073
13074 #[test]
13075 fn test_encode_mve_addi32_thumb2() {
13076 let encoder = ArmEncoder::new_thumb2();
13077 let op = ArmOp::MveAddI {
13078 qd: QReg::Q0,
13079 qn: QReg::Q1,
13080 qm: QReg::Q2,
13081 size: MveSize::S32,
13082 };
13083 let code = encoder.encode(&op).unwrap();
13084 assert_eq!(
13085 code.len(),
13086 4,
13087 "MVE VADD.I32 should be 4 bytes (Thumb-2 32-bit)"
13088 );
13089 }
13090
13091 #[test]
13092 fn test_encode_mve_subi16_thumb2() {
13093 let encoder = ArmEncoder::new_thumb2();
13094 let op = ArmOp::MveSubI {
13095 qd: QReg::Q0,
13096 qn: QReg::Q1,
13097 qm: QReg::Q2,
13098 size: MveSize::S16,
13099 };
13100 let code = encoder.encode(&op).unwrap();
13101 assert_eq!(code.len(), 4, "MVE VSUB.I16 should be 4 bytes");
13102 }
13103
13104 #[test]
13105 fn test_encode_mve_muli8_thumb2() {
13106 let encoder = ArmEncoder::new_thumb2();
13107 let op = ArmOp::MveMulI {
13108 qd: QReg::Q0,
13109 qn: QReg::Q1,
13110 qm: QReg::Q2,
13111 size: MveSize::S8,
13112 };
13113 let code = encoder.encode(&op).unwrap();
13114 assert_eq!(code.len(), 4, "MVE VMUL.I8 should be 4 bytes");
13115 }
13116
13117 #[test]
13118 fn test_encode_mve_bitwise_thumb2() {
13119 let encoder = ArmEncoder::new_thumb2();
13120
13121 let ops = vec![
13122 ArmOp::MveAnd {
13123 qd: QReg::Q0,
13124 qn: QReg::Q1,
13125 qm: QReg::Q2,
13126 },
13127 ArmOp::MveOrr {
13128 qd: QReg::Q0,
13129 qn: QReg::Q1,
13130 qm: QReg::Q2,
13131 },
13132 ArmOp::MveEor {
13133 qd: QReg::Q0,
13134 qn: QReg::Q1,
13135 qm: QReg::Q2,
13136 },
13137 ArmOp::MveBic {
13138 qd: QReg::Q0,
13139 qn: QReg::Q1,
13140 qm: QReg::Q2,
13141 },
13142 ];
13143 for op in ops {
13144 let code = encoder.encode(&op).unwrap();
13145 assert_eq!(code.len(), 4, "MVE bitwise op should be 4 bytes");
13146 }
13147 }
13148
13149 #[test]
13150 fn test_encode_mve_mvn_thumb2() {
13151 let encoder = ArmEncoder::new_thumb2();
13152 let op = ArmOp::MveMvn {
13153 qd: QReg::Q0,
13154 qm: QReg::Q1,
13155 };
13156 let code = encoder.encode(&op).unwrap();
13157 assert_eq!(code.len(), 4, "MVE VMVN should be 4 bytes");
13158 }
13159
13160 #[test]
13161 fn test_encode_mve_load_store_thumb2() {
13162 let encoder = ArmEncoder::new_thumb2();
13163
13164 let load = ArmOp::MveLoad {
13165 qd: QReg::Q0,
13166 addr: MemAddr::imm(Reg::R0, 16),
13167 };
13168 let code = encoder.encode(&load).unwrap();
13169 assert_eq!(code.len(), 4, "MVE VLDRW.32 should be 4 bytes");
13170
13171 let store = ArmOp::MveStore {
13172 qd: QReg::Q1,
13173 addr: MemAddr::imm(Reg::R1, 0),
13174 };
13175 let code = encoder.encode(&store).unwrap();
13176 assert_eq!(code.len(), 4, "MVE VSTRW.32 should be 4 bytes");
13177 }
13178
13179 #[test]
13180 fn test_encode_mve_const_thumb2() {
13181 let encoder = ArmEncoder::new_thumb2();
13182 let op = ArmOp::MveConst {
13183 qd: QReg::Q0,
13184 bytes: [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0],
13185 };
13186 let code = encoder.encode(&op).unwrap();
13187 // Should be 4 words of (MOVW R12 + VMOV Sn) = 4 * (4+4) = 32 bytes min
13188 // Some words with hi16=0 skip MOVT, so length varies
13189 assert!(
13190 code.len() >= 24,
13191 "MVE const should produce multiple instructions"
13192 );
13193 }
13194
13195 #[test]
13196 fn test_encode_mve_dup_thumb2() {
13197 let encoder = ArmEncoder::new_thumb2();
13198 let op = ArmOp::MveDup {
13199 qd: QReg::Q0,
13200 rn: Reg::R0,
13201 size: MveSize::S32,
13202 };
13203 let code = encoder.encode(&op).unwrap();
13204 assert_eq!(code.len(), 4, "MVE VDUP.32 should be 4 bytes");
13205 }
13206
13207 #[test]
13208 fn test_encode_mve_extract_lane_thumb2() {
13209 let encoder = ArmEncoder::new_thumb2();
13210 let op = ArmOp::MveExtractLane {
13211 rd: Reg::R0,
13212 qn: QReg::Q1,
13213 lane: 2,
13214 size: MveSize::S32,
13215 };
13216 let code = encoder.encode(&op).unwrap();
13217 assert_eq!(code.len(), 4, "MVE extract lane should be 4 bytes");
13218 }
13219
13220 #[test]
13221 fn test_encode_mve_insert_lane_thumb2() {
13222 let encoder = ArmEncoder::new_thumb2();
13223 let op = ArmOp::MveInsertLane {
13224 qd: QReg::Q0,
13225 rn: Reg::R1,
13226 lane: 3,
13227 size: MveSize::S32,
13228 };
13229 let code = encoder.encode(&op).unwrap();
13230 assert_eq!(code.len(), 4, "MVE insert lane should be 4 bytes");
13231 }
13232
13233 #[test]
13234 fn test_encode_mve_addf32_thumb2() {
13235 let encoder = ArmEncoder::new_thumb2();
13236 let op = ArmOp::MveAddF32 {
13237 qd: QReg::Q0,
13238 qn: QReg::Q1,
13239 qm: QReg::Q2,
13240 };
13241 let code = encoder.encode(&op).unwrap();
13242 assert_eq!(code.len(), 4, "MVE VADD.F32 should be 4 bytes");
13243 }
13244
13245 #[test]
13246 fn test_encode_mve_divf32_thumb2() {
13247 let encoder = ArmEncoder::new_thumb2();
13248 let op = ArmOp::MveDivF32 {
13249 qd: QReg::Q0,
13250 qn: QReg::Q1,
13251 qm: QReg::Q2,
13252 };
13253 let code = encoder.encode(&op).unwrap();
13254 // Lane-wise: 4 x VDIV.F32 = 4 x 4 = 16 bytes
13255 assert_eq!(
13256 code.len(),
13257 16,
13258 "MVE VDIV.F32 (lane-wise) should be 16 bytes"
13259 );
13260 }
13261
13262 #[test]
13263 fn test_encode_mve_sqrtf32_thumb2() {
13264 let encoder = ArmEncoder::new_thumb2();
13265 let op = ArmOp::MveSqrtF32 {
13266 qd: QReg::Q0,
13267 qm: QReg::Q1,
13268 };
13269 let code = encoder.encode(&op).unwrap();
13270 // Lane-wise: 4 x VSQRT.F32 = 4 x 4 = 16 bytes
13271 assert_eq!(
13272 code.len(),
13273 16,
13274 "MVE VSQRT.F32 (lane-wise) should be 16 bytes"
13275 );
13276 }
13277
13278 #[test]
13279 fn test_encode_mve_negf32_thumb2() {
13280 let encoder = ArmEncoder::new_thumb2();
13281 let op = ArmOp::MveNegF32 {
13282 qd: QReg::Q0,
13283 qm: QReg::Q1,
13284 };
13285 let code = encoder.encode(&op).unwrap();
13286 assert_eq!(code.len(), 4, "MVE VNEG.F32 should be 4 bytes");
13287 }
13288
13289 #[test]
13290 fn test_encode_mve_absf32_thumb2() {
13291 let encoder = ArmEncoder::new_thumb2();
13292 let op = ArmOp::MveAbsF32 {
13293 qd: QReg::Q0,
13294 qm: QReg::Q1,
13295 };
13296 let code = encoder.encode(&op).unwrap();
13297 assert_eq!(code.len(), 4, "MVE VABS.F32 should be 4 bytes");
13298 }
13299
13300 /// VCR-RA-001 / immediate-folding precondition: pins the Thumb-2 `AND`
13301 /// immediate encoding for the byte range and documents its bound.
13302 ///
13303 /// The `And { Operand2::Imm }` encoder packs the low 12 bits straight into
13304 /// the `i:imm3:imm8` field WITHOUT applying ThumbExpandImm (the modified-
13305 /// immediate expansion). For `imm <= 0xFF` (e.g. gale's int8 clamps
13306 /// `#0x7e` / `#0x7f`) that is correct — `i:imm3 = 0000` means "imm8
13307 /// zero-extended". So `and r2, r0, #0x7e` encodes to the canonical
13308 /// `00 f0 7e 02`. For `imm >= 0x100` the field would need a true
13309 /// ThumbExpandImm pattern (rotation / replication), which is NOT
13310 /// implemented here — so **immediate folding must gate on `imm <= 0xFF`**
13311 /// until the encoder is hardened to ThumbExpandImm/Ok-or-Err (the
13312 /// "encoder must be Ok-or-Err, never silently wrong" principle, #180/#185).
13313 /// This bound covers the measured `flat_flight` waste (#209).
13314 #[test]
13315 fn and_immediate_encodes_correctly_in_byte_range_documents_fold_bound() {
13316 let encoder = ArmEncoder::new_thumb2();
13317 let op = ArmOp::And {
13318 rd: Reg::R2,
13319 rn: Reg::R0,
13320 op2: Operand2::Imm(0x7e),
13321 };
13322 let code = encoder.encode(&op).unwrap();
13323 assert_eq!(
13324 code,
13325 vec![0x00, 0xf0, 0x7e, 0x02],
13326 "and r2, r0, #0x7e must encode to the canonical AND.W T1 (imm8=0x7e)"
13327 );
13328 }
13329
13330 /// #255: the shared ThumbExpandImm reverse-encoder underpinning the
13331 /// data-processing immediate fix. Encodable modified immediates round-trip to
13332 /// the expected `i:imm3:imm8` field; a genuinely non-modified value is `None`
13333 /// (caller must materialize into a register). Note `1000 = 0xFA ror 30` *is*
13334 /// representable (field 0xF7A) — the old encoder mis-encoded it (raw 0x3E8);
13335 /// this encodes it correctly.
13336 #[test]
13337 fn try_thumb_expand_imm_encodes_modified_immediates() {
13338 assert_eq!(try_thumb_expand_imm(0x7e), Some(0x07e)); // zero-extended byte
13339 assert_eq!(try_thumb_expand_imm(0xff), Some(0x0ff));
13340 assert_eq!(try_thumb_expand_imm(0x0001_0001), Some(0x101)); // 0x00XY00XY
13341 assert_eq!(try_thumb_expand_imm(0xff00_ff00), Some(0x2ff)); // 0xXY00XY00
13342 assert_eq!(try_thumb_expand_imm(0xffff_ffff), Some(0x3ff)); // 0xXYXYXYXY
13343 assert_eq!(try_thumb_expand_imm(0x100), Some(0xf80)); // 0x80 ror 31
13344 assert_eq!(try_thumb_expand_imm(0x8000_0000), Some(0x400)); // 0x80 ror 8
13345 assert_eq!(try_thumb_expand_imm(1000), Some(0xf7a)); // 0xFA ror 30
13346 // Genuinely unrepresentable (bits too far apart for an 8-bit window).
13347 assert_eq!(try_thumb_expand_imm(0x101), None);
13348 assert_eq!(try_thumb_expand_imm(0x12345), None);
13349 }
13350
13351 /// #255: CMP/ADDS/SUBS encode any valid modified immediate correctly, and
13352 /// ERROR (not silently mis-encode) on a genuinely unrepresentable one,
13353 /// forcing the selector to materialize into a register — closing the
13354 /// silent-miscompile class of #251/#253.
13355 #[test]
13356 fn cmp_adds_subs_immediate_error_on_non_modified_imm() {
13357 let encoder = ArmEncoder::new_thumb2();
13358 // cmp r0, #0xff → valid → Ok; cmp r0, #1000 → valid (0xFA ror 30) → Ok.
13359 assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 0xff).is_ok());
13360 assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 1000).is_ok());
13361 // cmp r0, #0x101 → NOT a modified immediate → Err (materialize-reg).
13362 assert!(
13363 encoder.encode_thumb32_cmp_imm(&Reg::R0, 0x101).is_err(),
13364 "cmp #0x101 must error, not compare the wrong constant"
13365 );
13366 assert!(
13367 encoder
13368 .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x101)
13369 .is_err()
13370 );
13371 assert!(
13372 encoder
13373 .encode_thumb32_subs(&Reg::R0, &Reg::R0, 0x101)
13374 .is_err()
13375 );
13376 // ...but a valid modified immediate still encodes.
13377 assert!(
13378 encoder
13379 .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x80)
13380 .is_ok()
13381 );
13382 }
13383
13384 /// #257: MLA (multiply-accumulate) encodes as MLS without the bit-4 op flag.
13385 /// `mla r2, r3, r4, r8` (rd=r2, rn=r3, rm=r4, ra=r8) → Thumb-2 `03 fb 04 82`.
13386 #[test]
13387 fn mla_thumb2_encodes_correctly() {
13388 let encoder = ArmEncoder::new_thumb2();
13389 let code = encoder
13390 .encode(&ArmOp::Mla {
13391 rd: Reg::R2,
13392 rn: Reg::R3,
13393 rm: Reg::R4,
13394 ra: Reg::R8,
13395 })
13396 .unwrap();
13397 // hw1 = 0xFB03, hw2 = (8<<12)|(2<<8)|4 = 0x8204
13398 assert_eq!(code, vec![0x03, 0xfb, 0x04, 0x82]);
13399 }
13400
13401 /// #259: LDR/STR (and sub-word) immediate-offset encoders truncated
13402 /// `offset & 0xFFF`, silently targeting the wrong address for offset >= 4096.
13403 /// They now error (the selector must use register-offset addressing) — the
13404 /// load/store sibling of the #253/#255 class. Offsets <= 4095 still encode.
13405 #[test]
13406 fn ldst_imm12_offset_errors_when_out_of_range() {
13407 let encoder = ArmEncoder::new_thumb2();
13408 // offset 0xFFF (4095): valid → Ok; ldr r0, [r1, #4095].
13409 assert!(
13410 encoder
13411 .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0xFFF)
13412 .is_ok()
13413 );
13414 // offset 0x1000 (4096): out of imm12 range → Err (not & 0xFFF → #0).
13415 assert!(
13416 encoder
13417 .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0x1000)
13418 .is_err(),
13419 "ldr offset 4096 must error, not wrap to 0"
13420 );
13421 assert!(
13422 encoder
13423 .encode_thumb32_str(&Reg::R0, &Reg::R1, 0x1000)
13424 .is_err()
13425 );
13426 assert!(
13427 encoder
13428 .encode_thumb32_ldrb_imm(&Reg::R0, &Reg::R1, 5000)
13429 .is_err()
13430 );
13431 assert!(
13432 encoder
13433 .encode_thumb32_strh_imm(&Reg::R0, &Reg::R1, 5000)
13434 .is_err()
13435 );
13436 }
13437
13438 /// Latent miscompile fix: ADD/SUB with a >0xFF immediate (e.g.
13439 /// `add sp, sp, #frame` for a >=256-byte frame) used ADD.W (T3), whose
13440 /// `i:imm3:imm8` is a ThumbExpandImm modified immediate — so `#256` silently
13441 /// encoded as `#0` (stack corruption). Use ADDW/SUBW (T4), a PLAIN 12-bit
13442 /// immediate, for 0x100..=0xFFF; keep T3 for <=0xFF (bit-identical); error
13443 /// beyond 4095.
13444 #[test]
13445 fn add_sub_large_immediate_use_addw_subw_not_misencoded() {
13446 let encoder = ArmEncoder::new_thumb2();
13447 // add sp, sp, #256 → ADDW (T4) SP, SP, #256 = 0d f2 00 1d
13448 assert_eq!(
13449 encoder
13450 .encode(&ArmOp::Add {
13451 rd: Reg::SP,
13452 rn: Reg::SP,
13453 op2: Operand2::Imm(256),
13454 })
13455 .unwrap(),
13456 vec![0x0d, 0xf2, 0x00, 0x1d],
13457 "add sp,sp,#256 must be ADDW (plain imm12), not a mis-encoded ADD.W"
13458 );
13459 // sub sp, sp, #256 → SUBW (T4) SP, SP, #256 = ad f2 00 1d
13460 assert_eq!(
13461 encoder
13462 .encode(&ArmOp::Sub {
13463 rd: Reg::SP,
13464 rn: Reg::SP,
13465 op2: Operand2::Imm(256),
13466 })
13467 .unwrap(),
13468 vec![0xad, 0xf2, 0x00, 0x1d],
13469 );
13470 // > 4095 has no single-instruction encoding → error, not silent wrong.
13471 assert!(
13472 encoder
13473 .encode(&ArmOp::Add {
13474 rd: Reg::SP,
13475 rn: Reg::SP,
13476 op2: Operand2::Imm(5000),
13477 })
13478 .is_err(),
13479 "add #5000 must error (no single ADDW), not mis-encode"
13480 );
13481 }
13482
13483 /// Closes the data-proc immediate class: AND and CMN now go through
13484 /// `try_thumb_expand_imm` like ORR/EOR/CMP — correct for any modified
13485 /// immediate, `Err` (not raw-pack / NOP) on an un-encodable one. The byte
13486 /// range stays bit-identical (`and r2,r0,#0x7e` is unchanged).
13487 #[test]
13488 fn and_cmn_immediate_thumb_expand_else_error() {
13489 let encoder = ArmEncoder::new_thumb2();
13490 // byte range unchanged (bit-identical with the pre-retrofit encoding)
13491 assert_eq!(
13492 encoder
13493 .encode(&ArmOp::And {
13494 rd: Reg::R2,
13495 rn: Reg::R0,
13496 op2: Operand2::Imm(0x7e),
13497 })
13498 .unwrap(),
13499 vec![0x00, 0xf0, 0x7e, 0x02],
13500 );
13501 // a valid replicated modified immediate now encodes (was silently wrong)
13502 assert!(
13503 encoder
13504 .encode(&ArmOp::And {
13505 rd: Reg::R2,
13506 rn: Reg::R0,
13507 op2: Operand2::Imm(0xff00ff00u32 as i32),
13508 })
13509 .is_ok()
13510 );
13511 // a genuinely un-encodable immediate errors (AND was raw-pack; CMN NOP)
13512 assert!(
13513 encoder
13514 .encode(&ArmOp::And {
13515 rd: Reg::R2,
13516 rn: Reg::R0,
13517 op2: Operand2::Imm(0x101),
13518 })
13519 .is_err()
13520 );
13521 assert!(
13522 encoder
13523 .encode(&ArmOp::Cmn {
13524 rn: Reg::R0,
13525 op2: Operand2::Imm(0x101),
13526 })
13527 .is_err(),
13528 "CMN #0x101 must error, not emit a NOP"
13529 );
13530 }
13531
13532 /// VCR-RA-001: ORR/EOR with a small immediate must encode the real
13533 /// instruction (not a silent `0xBF00` NOP). Pins the byte range and the
13534 /// Ok-or-Err bound that makes future Or/Eor immediate folding safe.
13535 #[test]
13536 fn orr_eor_immediate_encode_in_byte_range_else_error() {
13537 let encoder = ArmEncoder::new_thumb2();
13538 // orr r2, r0, #0x7e → ORR.W T1, imm8=0x7e
13539 assert_eq!(
13540 encoder
13541 .encode(&ArmOp::Orr {
13542 rd: Reg::R2,
13543 rn: Reg::R0,
13544 op2: Operand2::Imm(0x7e),
13545 })
13546 .unwrap(),
13547 vec![0x40, 0xf0, 0x7e, 0x02],
13548 );
13549 // eor r2, r0, #0x7e → EOR.W T1, imm8=0x7e
13550 assert_eq!(
13551 encoder
13552 .encode(&ArmOp::Eor {
13553 rd: Reg::R2,
13554 rn: Reg::R0,
13555 op2: Operand2::Imm(0x7e),
13556 })
13557 .unwrap(),
13558 vec![0x80, 0xf0, 0x7e, 0x02],
13559 );
13560 // Out-of-range immediates error rather than silently mis-encode / NOP.
13561 assert!(
13562 encoder
13563 .encode(&ArmOp::Orr {
13564 rd: Reg::R2,
13565 rn: Reg::R0,
13566 op2: Operand2::Imm(0x140),
13567 })
13568 .is_err(),
13569 "ORR #0x140 must error, not emit a NOP"
13570 );
13571 }
13572
13573 #[test]
13574 fn test_encode_mve_different_qregs() {
13575 let encoder = ArmEncoder::new_thumb2();
13576
13577 // Test that different Q-register numbers produce different encodings
13578 let op1 = ArmOp::MveAddI {
13579 qd: QReg::Q0,
13580 qn: QReg::Q0,
13581 qm: QReg::Q0,
13582 size: MveSize::S32,
13583 };
13584 let op2 = ArmOp::MveAddI {
13585 qd: QReg::Q3,
13586 qn: QReg::Q5,
13587 qm: QReg::Q7,
13588 size: MveSize::S32,
13589 };
13590 let code1 = encoder.encode(&op1).unwrap();
13591 let code2 = encoder.encode(&op2).unwrap();
13592 assert_ne!(
13593 code1, code2,
13594 "Different Q-registers should produce different encodings"
13595 );
13596 }
13597
13598 #[test]
13599 fn test_encode_mve_arm32_loud_err() {
13600 // #615: MVE (Helium) is Thumb-2-only. The ARM32 encoder used to emit
13601 // a silent NOP here (dropping the vector op); it must now be a typed
13602 // Err so a broken "MVE implies Thumb" invariant fails loudly.
13603 let encoder = ArmEncoder::new_arm32();
13604 let op = ArmOp::MveAddI {
13605 qd: QReg::Q0,
13606 qn: QReg::Q1,
13607 qm: QReg::Q2,
13608 size: MveSize::S32,
13609 };
13610 let err = encoder
13611 .encode(&op)
13612 .expect_err("ARM32 MVE must be a loud Err, not a silent NOP (#615)");
13613 assert!(
13614 err.to_string().contains("Thumb-2 only"),
13615 "unexpected error message: {err}"
13616 );
13617 }
13618}