Skip to main content

synth_backend/
arm_encoder.rs

1//! ARM Code Encoder - Converts ARM instructions to binary machine code
2//!
3//! Generates ARM32/Thumb-2 machine code from ARM instruction structures
4
5use synth_core::Result;
6use synth_core::target::FPUPrecision;
7use synth_synthesis::contracts::encoding as encoding_contracts;
8use synth_synthesis::{ArmOp, MemAddr, MveSize, Operand2, QReg, Reg, VfpReg};
9
10/// ARM instruction encoding
11pub struct ArmEncoder {
12    /// Use Thumb mode (vs ARM mode)
13    thumb_mode: bool,
14    /// FPU capability for VFP instruction encoding
15    #[allow(dead_code)]
16    fpu: Option<FPUPrecision>,
17}
18
19impl ArmEncoder {
20    /// Create a new ARM encoder in ARM32 mode
21    pub fn new_arm32() -> Self {
22        Self {
23            thumb_mode: false,
24            fpu: None,
25        }
26    }
27
28    /// Create a new ARM encoder in Thumb-2 mode
29    pub fn new_thumb2() -> Self {
30        Self {
31            thumb_mode: true,
32            fpu: None,
33        }
34    }
35
36    /// Create a new Thumb-2 encoder with FPU capability
37    pub fn new_thumb2_with_fpu(fpu: Option<FPUPrecision>) -> Self {
38        Self {
39            thumb_mode: true,
40            fpu,
41        }
42    }
43
44    /// Encode a single ARM instruction to bytes
45    pub fn encode(&self, op: &ArmOp) -> Result<Vec<u8>> {
46        if self.thumb_mode {
47            self.encode_thumb(op)
48        } else {
49            self.encode_arm(op)
50        }
51    }
52
53    /// Encode an ARM instruction in ARM32 mode (32-bit instructions)
54    /// #206: encode an ARM32 (A32) load/store whose address uses a register
55    /// offset (`[rn, rm{, #off}]`). Returns `None` for ops with no register
56    /// offset (the caller falls through to the immediate-form arms). Computes
57    /// `ip = base + rm` then re-encodes the op against `[ip, #off]`, which works
58    /// uniformly for word/byte/halfword/signed forms. IP (R12) is the scratch
59    /// register the selector already treats as clobberable across memory ops.
60    fn encode_arm_reg_offset_mem(&self, op: &ArmOp) -> Result<Option<Vec<u8>>> {
61        use synth_synthesis::Reg;
62        let addr = match op {
63            ArmOp::Ldr { addr, .. }
64            | ArmOp::Str { addr, .. }
65            | ArmOp::Ldrb { addr, .. }
66            | ArmOp::Strb { addr, .. }
67            | ArmOp::Ldrh { addr, .. }
68            | ArmOp::Strh { addr, .. }
69            | ArmOp::Ldrsb { addr, .. }
70            | ArmOp::Ldrsh { addr, .. } => addr,
71            _ => return Ok(None),
72        };
73        let Some(rm) = addr.offset_reg else {
74            return Ok(None);
75        };
76        let ip = Reg::R12;
77        // ADD ip, base, rm  (cond=AL, opcode=ADD, S=0, register operand2)
78        let add: u32 = 0xE0800000
79            | (reg_to_bits(&addr.base) << 16)
80            | (reg_to_bits(&ip) << 12)
81            | reg_to_bits(&rm);
82        let mut bytes = add.to_le_bytes().to_vec();
83        // Re-encode the op against [ip, #off] (immediate form → no offset_reg,
84        // so this recursion hits the immediate arms, not this helper again).
85        let imm_addr = MemAddr::imm(ip, addr.offset);
86        let imm_op = match op {
87            ArmOp::Ldr { rd, .. } => ArmOp::Ldr {
88                rd: *rd,
89                addr: imm_addr,
90            },
91            ArmOp::Str { rd, .. } => ArmOp::Str {
92                rd: *rd,
93                addr: imm_addr,
94            },
95            ArmOp::Ldrb { rd, .. } => ArmOp::Ldrb {
96                rd: *rd,
97                addr: imm_addr,
98            },
99            ArmOp::Strb { rd, .. } => ArmOp::Strb {
100                rd: *rd,
101                addr: imm_addr,
102            },
103            ArmOp::Ldrh { rd, .. } => ArmOp::Ldrh {
104                rd: *rd,
105                addr: imm_addr,
106            },
107            ArmOp::Strh { rd, .. } => ArmOp::Strh {
108                rd: *rd,
109                addr: imm_addr,
110            },
111            ArmOp::Ldrsb { rd, .. } => ArmOp::Ldrsb {
112                rd: *rd,
113                addr: imm_addr,
114            },
115            ArmOp::Ldrsh { rd, .. } => ArmOp::Ldrsh {
116                rd: *rd,
117                addr: imm_addr,
118            },
119            _ => unreachable!(),
120        };
121        bytes.extend(self.encode_arm(&imm_op)?);
122        Ok(Some(bytes))
123    }
124
125    /// #594: A32 expansion of `ArmOp::CallIndirect` — mirror of the Thumb-2
126    /// arm (same contract: R11 holds the function-pointer table base, entry
127    /// `i` is a 4-byte code address, R12 is the encoder-scratch register):
128    ///
129    /// ```text
130    /// MOVW r12, #size        ; #642: table size (compile-time immediate)
131    /// [MOVT r12, #size>>16]  ; only when size exceeds 16 bits
132    /// CMP  idx, r12          ; bounds guard: index >= size must TRAP
133    /// BLO  +1 insn           ; skip the trap when in bounds
134    /// UDF                    ; WASM Core §4.4.8 out-of-bounds trap
135    /// MOV r12, idx, LSL #2   ; table byte offset
136    /// LDR r12, [r11, r12]    ; load function pointer
137    /// BLX r12                ; indirect call
138    /// ```
139    ///
140    /// #650, `table_byte_offset != 0` (a non-zero table of the contiguous
141    /// R11 region): the pointer load becomes
142    /// `ADD r12, r11, r12; LDR r12, [r12, #offset]` — offset 0 keeps the
143    /// single-load form (single-table modules byte-identical by
144    /// construction).
145    ///
146    /// #664, `null_check` (the table has null slots, linked as ZERO words
147    /// per the layout contract): `CMP r12, #0; BNE +1; UDF` between the
148    /// pointer load and the `BLX` — a call reaching an uninitialized slot
149    /// traps (§4.4.8). `false` keeps the expansion byte-identical.
150    ///
151    /// #676, `type_check` (heterogeneous table): the §4.4.8 type check is
152    /// discharged at RUNTIME against the type-id sidecar — after the bounds
153    /// guard, `MOV r12, idx, LSL #2; ADD r12, r11, r12;
154    /// LDR r12, [r12, #type_off]; CMP r12, #expected_id; BEQ +1; UDF`
155    /// (mirror of the Thumb-2 arm; the dispatch tail recomputes `idx*4`).
156    /// Null slots carry the reserved class id 0, subsuming the #664 null
157    /// trap. `None` (every homogeneous table — the verdict discharged at
158    /// COMPILE time by the closed-world verification, see the #642 selector
159    /// guard) emits nothing and keeps the expansion byte-identical.
160    fn encode_arm_call_indirect(
161        table_index_reg: &Reg,
162        table_size: u32,
163        table_byte_offset: u32,
164        null_check: bool,
165        type_check: Option<(u32, u32)>,
166    ) -> Vec<u8> {
167        let idx = reg_to_bits(table_index_reg);
168        let mut bytes = Vec::with_capacity(32);
169        // MOVW r12, #(size & 0xFFFF) — cond=E 0011 0000 imm4 Rd imm12.
170        let size_lo = table_size & 0xFFFF;
171        let movw: u32 = 0xE300_0000 | ((size_lo >> 12) << 16) | (12 << 12) | (size_lo & 0xFFF);
172        bytes.extend_from_slice(&movw.to_le_bytes());
173        // MOVT r12, #(size >> 16) — only for a table size above 16 bits.
174        let size_hi = table_size >> 16;
175        if size_hi != 0 {
176            let movt: u32 = 0xE340_0000 | ((size_hi >> 12) << 16) | (12 << 12) | (size_hi & 0xFFF);
177            bytes.extend_from_slice(&movt.to_le_bytes());
178        }
179        // CMP idx, r12 — cond=E, opcode=1010, S=1, Rn=idx, Rm=r12.
180        let cmp: u32 = 0xE150_000C | (idx << 16);
181        bytes.extend_from_slice(&cmp.to_le_bytes());
182        // BLO +1 insn (skip the UDF when index < size) — cond=LO(0011),
183        // imm24=0: target = branch + 8.
184        bytes.extend_from_slice(&0x3A00_0000u32.to_le_bytes());
185        // UDF — permanently undefined (same trap idiom as the A32 div-by-zero
186        // guards): call_indirect out-of-bounds trap.
187        bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
188        // #676: runtime type check for a heterogeneous table — load the
189        // indexed slot's structural class id from the type-id sidecar and
190        // trap on mismatch (§4.4.8). Mirror of the Thumb-2 arm; `None`
191        // emits nothing (homogeneous tables byte-identical by construction).
192        if let Some((expected_id, type_off)) = type_check {
193            // RQ-61-IMMRANGE (#1072): these `debug_assert`s are compiled out
194            // in release, where the `& 0xFF` / `& 0xFFF` masks below would
195            // silently TRUNCATE an out-of-range value (id 256 compares as 0,
196            // letting a NULL slot pass the §4.4.8 check). The enforcement
197            // claim is DEMONSTRATED, not assumed: the sole `Some` producer is
198            // `resolve_runtime_type_check` (instruction_selector.rs), which
199            // loud-declines id > 255 and offset > 4095, and
200            // `test_676_call_indirect_runtime_check_range_declines` trips
201            // both declines (mutation-checked: disabling either turns it red).
202            debug_assert!(expected_id <= 255, "selector enforces the CMP imm8 range");
203            debug_assert!(type_off <= 4095, "selector enforces the LDR imm12 range");
204            // MOV r12, idx, LSL #2 (same as the dispatch tail's scale).
205            bytes.extend_from_slice(&(0xE1A0C000u32 | (2 << 7) | idx).to_le_bytes());
206            // ADD r12, r11, r12 — data-processing ADD (register).
207            bytes.extend_from_slice(&0xE08BC00Cu32.to_le_bytes());
208            // LDR r12, [r12, #type_off] — immediate offset, P=1 U=1 L=1.
209            bytes.extend_from_slice(&(0xE59CC000u32 | (type_off & 0xFFF)).to_le_bytes());
210            // CMP r12, #expected_id — data-processing CMP (immediate).
211            bytes.extend_from_slice(&(0xE35C_0000u32 | (expected_id & 0xFF)).to_le_bytes());
212            // BEQ +1 insn (skip the UDF when the class id matches) —
213            // cond=EQ(0000), imm24=0: target = branch + 8.
214            bytes.extend_from_slice(&0x0A00_0000u32.to_le_bytes());
215            // UDF — the §4.4.8 type-mismatch trap.
216            bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
217        }
218        // MOV r12, idx, LSL #2 — data-processing MOV, register op2 with
219        // imm5=2/LSL: cond=E, opcode=1101, S=0, Rd=r12.
220        let mov: u32 = 0xE1A0C000 | (2 << 7) | idx;
221        bytes.extend_from_slice(&mov.to_le_bytes());
222        if table_byte_offset == 0 {
223            // Table 0 (base = R11 itself): the pre-#650 single-load form.
224            // LDR r12, [r11, r12] — register offset, P=1 U=1 B=0 W=0 L=1.
225            let ldr: u32 = 0xE79BC00C;
226            bytes.extend_from_slice(&ldr.to_le_bytes());
227        } else {
228            // #650: fold the table's compile-time base offset into the
229            // pointer load via the LDR imm12 form.
230            assert!(
231                table_byte_offset <= 4095,
232                "call_indirect table base offset {table_byte_offset} exceeds \
233                 LDR imm12 — the selector must have declined this (#650)"
234            );
235            // ADD r12, r11, r12 — data-processing ADD (register).
236            bytes.extend_from_slice(&0xE08BC00Cu32.to_le_bytes());
237            // LDR r12, [r12, #offset] — immediate offset, P=1 U=1 L=1.
238            let ldr: u32 = 0xE59CC000 | (table_byte_offset & 0xFFF);
239            bytes.extend_from_slice(&ldr.to_le_bytes());
240        }
241        // #664: null-slot trap — only when the table image has null slots
242        // (zero-linked words). A fully-initialized table keeps the pre-#664
243        // bytes identical by construction.
244        if null_check {
245            // CMP r12, #0 — data-processing CMP (immediate), Rn=r12.
246            bytes.extend_from_slice(&0xE35C_0000u32.to_le_bytes());
247            // BNE +1 insn (skip the UDF when the pointer is non-null) —
248            // cond=NE(0001), imm24=0: target = branch + 8.
249            bytes.extend_from_slice(&0x1A00_0000u32.to_le_bytes());
250            // UDF — the §4.4.8 uninitialized-element trap (same idiom as
251            // the bounds guard).
252            bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
253        }
254        // BLX r12 — cond=E, 0001 0010 1111 1111 1111 0011, Rm=r12.
255        let blx: u32 = 0xE12FFF3C;
256        bytes.extend_from_slice(&blx.to_le_bytes());
257        bytes
258    }
259
260    /// #615: A32 (ARM-mode) expansions for the multi-instruction ops that the
261    /// Thumb-2 encoder expands but the A32 arm previously encoded as a single
262    /// literal NOP (`0xE1A00000`) — i64 mul / shifts / rotates / comparisons /
263    /// eqz, plus i64 const/load/store/extend/wrap and the i32 SetCond /
264    /// SelectMove pseudo-ops. Each expansion mirrors its Thumb-2 twin's
265    /// register contract and semantics exactly (A32 conditional execution
266    /// replaces the IT blocks). Returns `Ok(None)` for ops this helper does
267    /// not handle; the caller's match encodes or loudly rejects those.
268    fn encode_arm_expanded(&self, op: &ArmOp) -> Result<Option<Vec<u8>>> {
269        use synth_synthesis::Condition;
270
271        /// A32 condition-field bits (instruction bits [31:28]).
272        fn cond_bits(cond: &Condition) -> u32 {
273            match cond {
274                Condition::EQ => 0x0,
275                Condition::NE => 0x1,
276                Condition::HS => 0x2, // CS: unsigned >=
277                Condition::LO => 0x3, // CC: unsigned <
278                Condition::HI => 0x8, // unsigned >
279                Condition::LS => 0x9, // unsigned <=
280                Condition::GE => 0xA,
281                Condition::LT => 0xB,
282                Condition::GT => 0xC,
283                Condition::LE => 0xD,
284            }
285        }
286        fn w(b: &mut Vec<u8>, word: u32) {
287            b.extend_from_slice(&word.to_le_bytes());
288        }
289        /// MOV<cond> rd, #imm (rotated-immediate form; only 0/1 used here).
290        fn mov_cond_imm(b: &mut Vec<u8>, cond: u32, rd: u32, imm: u32) {
291            w(b, (cond << 28) | 0x03A0_0000 | (rd << 12) | imm);
292        }
293        /// After a flag-setting pair: MOV<cond> rd,#1 ; MOV<!cond> rd,#0.
294        fn set_cond(b: &mut Vec<u8>, cond: &Condition, rd: u32) {
295            mov_cond_imm(b, cond_bits(cond), rd, 1);
296            mov_cond_imm(b, cond_bits(&cond.invert()), rd, 0);
297        }
298        /// CMP rn, rm (register form).
299        fn cmp_reg(b: &mut Vec<u8>, rn: u32, rm: u32) {
300            w(b, 0xE150_0000 | (rn << 16) | rm);
301        }
302        /// SBCS rd, rn, rm — the 64-bit compare idiom's high-word subtract.
303        fn sbcs(b: &mut Vec<u8>, rd: u32, rn: u32, rm: u32) {
304            w(b, 0xE0D0_0000 | (rn << 16) | (rd << 12) | rm);
305        }
306        /// MOVW rd, #imm16.
307        fn movw(b: &mut Vec<u8>, rd: u32, v: u32) {
308            w(
309                b,
310                0xE300_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
311            );
312        }
313        /// MOVT rd, #imm16.
314        fn movt(b: &mut Vec<u8>, rd: u32, v: u32) {
315            w(
316                b,
317                0xE340_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
318            );
319        }
320        /// Register-controlled shift: MOV rd, rn, <LSL|LSR|ASR> rs.
321        /// `ty`: 0=LSL, 1=LSR, 2=ASR. A32 uses the bottom byte of rs;
322        /// amounts of 32 or more yield 0 (LSL/LSR) or all-sign (ASR) — same
323        /// semantics the Thumb-2 expansions rely on.
324        fn shift_reg(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, rs: u32) {
325            w(b, 0xE1A0_0010 | (rd << 12) | (rs << 8) | (ty << 5) | rn);
326        }
327        const LSL: u32 = 0;
328        const LSR: u32 = 1;
329        const ASR: u32 = 2;
330        /// Immediate-shift move: MOV rd, rn, <LSL|LSR|ASR> #imm.
331        fn shift_imm(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, imm: u32) {
332            w(
333                b,
334                0xE1A0_0000 | (rd << 12) | ((imm & 0x1F) << 7) | (ty << 5) | rn,
335            );
336        }
337        /// Data-processing register form: `base | rn<<16 | rd<<12 | rm`.
338        /// `base` carries cond/opcode/S (e.g. 0xE090_0000 = ADDS).
339        fn dp_reg(b: &mut Vec<u8>, base: u32, rd: u32, rn: u32, rm: u32) {
340            w(b, base | (rn << 16) | (rd << 12) | rm);
341        }
342        /// Data-processing with an immediate-shifted register operand:
343        /// `<op> rd, rn, rm, <LSL|LSR|ASR> #imm` — the A32 barrel shifter
344        /// folds a shift into the second operand for free. #1021 uses this to
345        /// run the popcnt SWAR fold on R12 alone (no second scratch, so R11 —
346        /// the linear-memory base — is never touched).
347        fn dp_reg_shift(b: &mut Vec<u8>, base: u32, rd: u32, rn: u32, rm: u32, ty: u32, imm: u32) {
348            w(
349                b,
350                base | (rn << 16) | (rd << 12) | ((imm & 0x1F) << 7) | (ty << 5) | rm,
351            );
352        }
353        /// ORR rd, rd, rm, LSR #31 — the carry-propagation idiom of the
354        /// shift-subtract division loop (bring rm's MSB into rd's bit 0).
355        fn orr_lsr31(b: &mut Vec<u8>, rd: u32, rm: u32) {
356            w(
357                b,
358                0xE180_0000 | (rd << 16) | (rd << 12) | (31 << 7) | (1 << 5) | rm,
359            );
360        }
361        /// 64-bit two's-complement negate of the lo:hi pair (MVN/MVN/ADDS/ADC).
362        fn negate64(b: &mut Vec<u8>, lo: u32, hi: u32) {
363            w(b, 0xE1E0_0000 | (lo << 12) | lo); //           MVN  lo, lo
364            w(b, 0xE1E0_0000 | (hi << 12) | hi); //           MVN  hi, hi
365            w(b, 0xE290_0001 | (lo << 16) | (lo << 12)); //   ADDS lo, lo, #1
366            w(b, 0xE2A0_0000 | (hi << 16) | (hi << 12)); //   ADC  hi, hi, #0
367        }
368        /// TST x, x ; BPL +4-instructions — the "skip the negate64 when the
369        /// sign bit is clear" guard of the signed div/rem arms.
370        fn skip_negate_if_positive(b: &mut Vec<u8>, x: u32) {
371            w(b, 0xE110_0000 | (x << 16) | x); // TST x, x
372            w(b, 0x5A00_0003); //                 BPL +4 insns (past negate64)
373        }
374        /// The 64-iteration shift-subtract division loop — A32 transcription
375        /// of the Thumb-2 #610 core: dividend R0:R1, divisor R2:R3, quotient
376        /// R4:R5, remainder R6:R7, loop counter in `counter` (R12 or R8).
377        fn div_loop(b: &mut Vec<u8>, counter: u32) {
378            w(b, 0xE3A0_0040 | (counter << 12)); // MOV counter, #64
379            let loop_start = b.len();
380            // quotient <<= 1
381            shift_imm(b, LSL, 5, 5, 1);
382            orr_lsr31(b, 5, 4);
383            shift_imm(b, LSL, 4, 4, 1);
384            // remainder <<= 1, OR in dividend MSB
385            shift_imm(b, LSL, 7, 7, 1);
386            orr_lsr31(b, 7, 6);
387            shift_imm(b, LSL, 6, 6, 1);
388            orr_lsr31(b, 6, 1);
389            // dividend <<= 1
390            shift_imm(b, LSL, 1, 1, 1);
391            orr_lsr31(b, 1, 0);
392            shift_imm(b, LSL, 0, 0, 1);
393            // if remainder >= divisor (64-bit unsigned): subtract, set q bit
394            w(b, 0xE157_0003); // CMP R7, R3      (high words)
395            w(b, 0x8A00_0002); // BHI .subtract   (+2 insns)
396            w(b, 0x3A00_0004); // BLO .next       (+4 insns)
397            w(b, 0xE156_0002); // CMP R6, R2      (low words, highs equal)
398            w(b, 0x3A00_0002); // BLO .next       (+2 insns)
399            w(b, 0xE056_6002); // .subtract: SUBS R6, R6, R2
400            w(b, 0xE0C7_7003); //            SBC  R7, R7, R3
401            w(b, 0xE384_4001); //            ORR  R4, R4, #1
402            // .next: decrement and loop
403            w(b, 0xE250_0001 | (counter << 16) | (counter << 12)); // SUBS counter, #1
404            let diff = (loop_start as i64) - (b.len() as i64 + 8);
405            w(b, 0x1A00_0000 | (((diff / 4) as u32) & 0x00FF_FFFF)); // BNE loop
406        }
407        /// 32-bit population count on working register `x` — A32 transcription
408        /// of the Thumb-2 I64Popcnt per-word core (mul-based fold): `c` is the
409        /// constant register, R12 the shifted temp. Both are clobbered.
410        fn popcnt_word(b: &mut Vec<u8>, x: u32, c: u32) {
411            // x = x - ((x >> 1) & 0x55555555)
412            shift_imm(b, LSR, 12, x, 1);
413            movw(b, c, 0x5555);
414            movt(b, c, 0x5555);
415            dp_reg(b, 0xE000_0000, 12, 12, c); // AND R12, R12, c
416            dp_reg(b, 0xE040_0000, x, x, 12); //  SUB x, x, R12
417            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
418            movw(b, c, 0x3333);
419            movt(b, c, 0x3333);
420            dp_reg(b, 0xE000_0000, 12, x, c); //  AND R12, x, c
421            shift_imm(b, LSR, x, x, 2);
422            dp_reg(b, 0xE000_0000, x, x, c); //   AND x, x, c
423            dp_reg(b, 0xE080_0000, x, x, 12); //  ADD x, x, R12
424            // x = (x + (x >> 4)) & 0x0F0F0F0F
425            shift_imm(b, LSR, 12, x, 4);
426            dp_reg(b, 0xE080_0000, x, x, 12); //  ADD x, x, R12
427            movw(b, c, 0x0F0F);
428            movt(b, c, 0x0F0F);
429            dp_reg(b, 0xE000_0000, x, x, c); //   AND x, x, c
430            // x = (x * 0x01010101) >> 24
431            movw(b, c, 0x0101);
432            movt(b, c, 0x0101);
433            w(b, 0xE000_0090 | (x << 16) | (c << 8) | x); // MUL x, x, c
434            shift_imm(b, LSR, x, x, 24);
435        }
436
437        let mut b: Vec<u8> = Vec::new();
438        match op {
439            // SetCond: materialize a flags-predicate as 0/1 — the A32 twin of
440            // the Thumb `ITE cond; MOV rd,#1; MOV rd,#0`.
441            ArmOp::SetCond { rd, cond } => {
442                set_cond(&mut b, cond, reg_to_bits(rd));
443            }
444
445            // SelectMove: conditional register move (Thumb: IT cond; MOV).
446            ArmOp::SelectMove { rd, rm, cond } => {
447                w(
448                    &mut b,
449                    (cond_bits(cond) << 28)
450                        | 0x01A0_0000
451                        | (reg_to_bits(rd) << 12)
452                        | reg_to_bits(rm),
453                );
454            }
455
456            // I64SetCond: compare two i64 register pairs, 0/1 into rd.
457            // EQ/NE: CMP lo,lo; CMPEQ hi,hi (only if lows equal); set.
458            // Ordered: CMP lo,lo; SBCS rd,hi,hi; set — with the same
459            // operand-swap + condition mapping as the Thumb-2 arm.
460            ArmOp::I64SetCond {
461                rd,
462                rn_lo,
463                rn_hi,
464                rm_lo,
465                rm_hi,
466                cond,
467            } => {
468                let rd_b = reg_to_bits(rd);
469                let (n_lo, n_hi, m_lo, m_hi) = (
470                    reg_to_bits(rn_lo),
471                    reg_to_bits(rn_hi),
472                    reg_to_bits(rm_lo),
473                    reg_to_bits(rm_hi),
474                );
475                match cond {
476                    Condition::EQ | Condition::NE => {
477                        cmp_reg(&mut b, n_lo, m_lo);
478                        // CMP<EQ> rn_hi, rm_hi — compare highs only if lows equal.
479                        w(&mut b, 0x0150_0000 | (n_hi << 16) | m_hi);
480                        set_cond(&mut b, cond, rd_b);
481                    }
482                    // (swap operands?, condition after SBCS) per the Thumb arm:
483                    // LT/GE/LO/HS compare (rn, rm); GT/LE/HI/LS swap to (rm, rn).
484                    Condition::LT => {
485                        cmp_reg(&mut b, n_lo, m_lo);
486                        sbcs(&mut b, rd_b, n_hi, m_hi);
487                        set_cond(&mut b, &Condition::LT, rd_b);
488                    }
489                    Condition::GE => {
490                        cmp_reg(&mut b, n_lo, m_lo);
491                        sbcs(&mut b, rd_b, n_hi, m_hi);
492                        set_cond(&mut b, &Condition::GE, rd_b);
493                    }
494                    Condition::GT => {
495                        cmp_reg(&mut b, m_lo, n_lo);
496                        sbcs(&mut b, rd_b, m_hi, n_hi);
497                        set_cond(&mut b, &Condition::LT, rd_b);
498                    }
499                    Condition::LE => {
500                        cmp_reg(&mut b, m_lo, n_lo);
501                        sbcs(&mut b, rd_b, m_hi, n_hi);
502                        set_cond(&mut b, &Condition::GE, rd_b);
503                    }
504                    Condition::LO => {
505                        cmp_reg(&mut b, n_lo, m_lo);
506                        sbcs(&mut b, rd_b, n_hi, m_hi);
507                        set_cond(&mut b, &Condition::LO, rd_b);
508                    }
509                    Condition::HS => {
510                        cmp_reg(&mut b, n_lo, m_lo);
511                        sbcs(&mut b, rd_b, n_hi, m_hi);
512                        set_cond(&mut b, &Condition::HS, rd_b);
513                    }
514                    Condition::HI => {
515                        cmp_reg(&mut b, m_lo, n_lo);
516                        sbcs(&mut b, rd_b, m_hi, n_hi);
517                        set_cond(&mut b, &Condition::LO, rd_b);
518                    }
519                    Condition::LS => {
520                        cmp_reg(&mut b, m_lo, n_lo);
521                        sbcs(&mut b, rd_b, m_hi, n_hi);
522                        set_cond(&mut b, &Condition::HS, rd_b);
523                    }
524                }
525            }
526
527            // I64SetCondZ: ORRS rd, lo, hi sets Z iff the pair is zero.
528            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
529                let rd_b = reg_to_bits(rd);
530                w(
531                    &mut b,
532                    0xE190_0000 | (reg_to_bits(rn_lo) << 16) | (rd_b << 12) | reg_to_bits(rn_hi),
533                );
534                set_cond(&mut b, &Condition::EQ, rd_b);
535            }
536
537            // i64 comparison wrappers: delegate to I64SetCond/Z, mirroring the
538            // Thumb-2 delegation arms.
539            ArmOp::I64Eqz { rd, rnlo, rnhi } => {
540                return self
541                    .encode_arm(&ArmOp::I64SetCondZ {
542                        rd: *rd,
543                        rn_lo: *rnlo,
544                        rn_hi: *rnhi,
545                    })
546                    .map(Some);
547            }
548            ArmOp::I64Eq {
549                rd,
550                rnlo,
551                rnhi,
552                rmlo,
553                rmhi,
554            }
555            | ArmOp::I64Ne {
556                rd,
557                rnlo,
558                rnhi,
559                rmlo,
560                rmhi,
561            }
562            | ArmOp::I64LtS {
563                rd,
564                rnlo,
565                rnhi,
566                rmlo,
567                rmhi,
568            }
569            | ArmOp::I64LtU {
570                rd,
571                rnlo,
572                rnhi,
573                rmlo,
574                rmhi,
575            }
576            | ArmOp::I64LeS {
577                rd,
578                rnlo,
579                rnhi,
580                rmlo,
581                rmhi,
582            }
583            | ArmOp::I64LeU {
584                rd,
585                rnlo,
586                rnhi,
587                rmlo,
588                rmhi,
589            }
590            | ArmOp::I64GtS {
591                rd,
592                rnlo,
593                rnhi,
594                rmlo,
595                rmhi,
596            }
597            | ArmOp::I64GtU {
598                rd,
599                rnlo,
600                rnhi,
601                rmlo,
602                rmhi,
603            }
604            | ArmOp::I64GeS {
605                rd,
606                rnlo,
607                rnhi,
608                rmlo,
609                rmhi,
610            }
611            | ArmOp::I64GeU {
612                rd,
613                rnlo,
614                rnhi,
615                rmlo,
616                rmhi,
617            } => {
618                let cond = match op {
619                    ArmOp::I64Eq { .. } => Condition::EQ,
620                    ArmOp::I64Ne { .. } => Condition::NE,
621                    ArmOp::I64LtS { .. } => Condition::LT,
622                    ArmOp::I64LtU { .. } => Condition::LO,
623                    ArmOp::I64LeS { .. } => Condition::LE,
624                    ArmOp::I64LeU { .. } => Condition::LS,
625                    ArmOp::I64GtS { .. } => Condition::GT,
626                    ArmOp::I64GtU { .. } => Condition::HI,
627                    ArmOp::I64GeS { .. } => Condition::GE,
628                    _ => Condition::HS,
629                };
630                return self
631                    .encode_arm(&ArmOp::I64SetCond {
632                        rd: *rd,
633                        rn_lo: *rnlo,
634                        rn_hi: *rnhi,
635                        rm_lo: *rmlo,
636                        rm_hi: *rmhi,
637                        cond,
638                    })
639                    .map(Some);
640            }
641
642            // I64Mul: cross products into R12, then UMULL — same sequence and
643            // ordering as the Thumb-2 arm (R12 is encoder scratch, #212).
644            ArmOp::I64Mul {
645                rd_lo,
646                rd_hi,
647                rn_lo,
648                rn_hi,
649                rm_lo,
650                rm_hi,
651            } => {
652                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
653                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
654                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
655                // MUL R12, rn_lo, rm_hi   (R12 = a_lo * b_hi)
656                w(&mut b, 0xE000_0090 | (12 << 16) | (mh << 8) | nl);
657                // MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
658                w(
659                    &mut b,
660                    0xE020_0090 | (12 << 16) | (12 << 12) | (ml << 8) | nh,
661                );
662                // UMULL rd_lo, rd_hi, rn_lo, rm_lo
663                w(
664                    &mut b,
665                    0xE080_0090 | (dh << 16) | (dl << 12) | (ml << 8) | nl,
666                );
667                // ADD rd_hi, rd_hi, R12
668                w(&mut b, 0xE080_0000 | (dh << 16) | (dh << 12) | 12);
669            }
670
671            // I64Shl / I64ShrU / I64ShrS: same small/large-shift structure as
672            // the Thumb-2 arms. #1048: the expansion must never write its own
673            // input operands — the pre-#1048 A32 arms masked the amount in
674            // place (`AND ml, ml, #63`) and used the amount's home high
675            // register as scratch, identically to the Thumb-2 defect. R12
676            // (encoder scratch, never allocatable, #212) is the ONLY
677            // temporary; the masked amount is re-derived from the untouched
678            // rm_lo where a second live temp would otherwise be needed.
679            // Register-controlled shifts >= 32 yield 0, which the small path
680            // relies on for n = 0. Same #1039-style loud alias guards as the
681            // Thumb-2 arms.
682            ArmOp::I64Shl {
683                rd_lo,
684                rd_hi,
685                rn_lo,
686                rn_hi,
687                rm_lo,
688                rm_hi: _,
689            } => {
690                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
691                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
692                let ml = reg_to_bits(rm_lo);
693                if dh == nl || dh == ml {
694                    return Err(synth_core::Error::synthesis(format!(
695                        "I64Shl (A32): rd_hi {rd_hi:?} aliases an input ({rn_lo:?}/{rm_lo:?}) still live inside the expansion (#1048)"
696                    )));
697                }
698                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
699                w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
700                w(&mut b, 0x5A00_0007); //                            BPL  .large
701                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
702                shift_reg(&mut b, LSL, dh, nh, 12); //               dh = hi << n
703                w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB  r12, r12, #32
704                shift_reg(&mut b, LSR, 12, nl, 12); //               r12 = lo >> (32-n)
705                w(&mut b, 0xE180_0000 | (dh << 16) | (dh << 12) | 12); // ORR dh, dh, r12
706                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
707                shift_reg(&mut b, LSL, dl, nl, 12); //               dl = lo << n
708                w(&mut b, 0xEA00_0001); //                            B    .done
709                shift_reg(&mut b, LSL, dh, nl, 12); //               .large: dh = lo << (n-32)
710                w(&mut b, 0xE3A0_0000 | (dl << 12)); //              MOV  dl, #0
711            }
712            ArmOp::I64ShrU {
713                rd_lo,
714                rd_hi,
715                rn_lo,
716                rn_hi,
717                rm_lo,
718                rm_hi: _,
719            } => {
720                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
721                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
722                let ml = reg_to_bits(rm_lo);
723                if dl == nh || dl == ml {
724                    return Err(synth_core::Error::synthesis(format!(
725                        "I64ShrU (A32): rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
726                    )));
727                }
728                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
729                w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
730                w(&mut b, 0x5A00_0007); //                            BPL  .large
731                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
732                shift_reg(&mut b, LSR, dl, nl, 12); //               dl = lo >> n
733                w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB  r12, r12, #32
734                shift_reg(&mut b, LSL, 12, nh, 12); //               r12 = hi << (32-n)
735                w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | 12); // ORR dl, dl, r12
736                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
737                shift_reg(&mut b, LSR, dh, nh, 12); //               dh = hi >> n
738                w(&mut b, 0xEA00_0001); //                            B    .done
739                shift_reg(&mut b, LSR, dl, nh, 12); //               .large: dl = hi >> (n-32)
740                w(&mut b, 0xE3A0_0000 | (dh << 12)); //              MOV  dh, #0
741            }
742            ArmOp::I64ShrS {
743                rd_lo,
744                rd_hi,
745                rn_lo,
746                rn_hi,
747                rm_lo,
748                rm_hi: _,
749            } => {
750                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
751                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
752                let ml = reg_to_bits(rm_lo);
753                if dl == nh || dl == ml {
754                    return Err(synth_core::Error::synthesis(format!(
755                        "I64ShrS (A32): rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
756                    )));
757                }
758                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
759                w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
760                w(&mut b, 0x5A00_0007); //                            BPL  .large
761                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
762                shift_reg(&mut b, LSR, dl, nl, 12); //               dl = lo >> n
763                w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB  r12, r12, #32
764                shift_reg(&mut b, LSL, 12, nh, 12); //               r12 = hi << (32-n)
765                w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | 12); // ORR dl, dl, r12
766                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
767                shift_reg(&mut b, ASR, dh, nh, 12); //               dh = hi >> n (arith)
768                w(&mut b, 0xEA00_0001); //                            B    .done
769                shift_reg(&mut b, ASR, dl, nh, 12); //               .large: dl = hi >> (n-32)
770                w(&mut b, 0xE1A0_0040 | (dh << 12) | (31 << 7) | nh); // ASR dh, nh, #31
771            }
772
773            // I64Rotl / I64Rotr: the #610 fixed-ABI wrapper (A32 form) around
774            // the same fixed-register core as the Thumb-2 arms — value in
775            // R0:R1, amount in R2, scratch R3 + R12.
776            ArmOp::I64Rotl {
777                rdlo,
778                rdhi,
779                rnlo,
780                rnhi,
781                shift,
782            } => {
783                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
784                for word in [
785                    0xE202_203Fu32, // AND  R2, R2, #63   (mask amount mod 64)
786                    0xE252_3020,    // SUBS R3, R2, #32   (R3 = n-32, sets N)
787                    0x5A00_0007,    // BPL  .large        (n >= 32)
788                    // --- small rotation (n < 32) ---
789                    0xE262_3020, // RSB  R3, R2, #32   (R3 = 32-n)
790                    0xE1A0_C330, // LSR  R12, R0, R3   (lo >> (32-n))
791                    0xE1A0_3331, // LSR  R3, R1, R3    (hi >> (32-n))
792                    0xE1A0_1211, // LSL  R1, R1, R2    (hi << n)
793                    0xE181_100C, // ORR  R1, R1, R12   (new_hi)
794                    0xE1A0_0210, // LSL  R0, R0, R2    (lo << n)
795                    0xE180_0003, // ORR  R0, R0, R3    (new_lo)
796                    0xEA00_0007, // B    .done
797                    // --- large rotation (n >= 32), R3 = m = n-32 ---
798                    0xE263_2020, // RSB  R2, R3, #32   (R2 = 32-m = 64-n)
799                    0xE1A0_C231, // LSR  R12, R1, R2   (hi >> (64-n))
800                    0xE1A0_2230, // LSR  R2, R0, R2    (lo >> (64-n))
801                    0xE1A0_0310, // LSL  R0, R0, R3    (lo << m)
802                    0xE1A0_1311, // LSL  R1, R1, R3    (hi << m)
803                    0xE180_C00C, // ORR  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
804                    0xE181_0002, // ORR  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
805                    0xE1A0_100C, // MOV  R1, R12       (new_hi into place)
806                ] {
807                    w(&mut b, word);
808                }
809                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
810            }
811            ArmOp::I64Rotr {
812                rdlo,
813                rdhi,
814                rnlo,
815                rnhi,
816                shift,
817            } => {
818                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
819                for word in [
820                    0xE202_203Fu32, // AND  R2, R2, #63   (mask amount mod 64)
821                    0xE252_3020,    // SUBS R3, R2, #32   (R3 = n-32, sets N)
822                    0x5A00_0007,    // BPL  .large        (n >= 32)
823                    // --- small rotation (n < 32) ---
824                    0xE262_3020, // RSB  R3, R2, #32   (R3 = 32-n)
825                    0xE1A0_C311, // LSL  R12, R1, R3   (hi << (32-n))
826                    0xE1A0_3310, // LSL  R3, R0, R3    (lo << (32-n))
827                    0xE1A0_0230, // LSR  R0, R0, R2    (lo >> n)
828                    0xE180_000C, // ORR  R0, R0, R12   (new_lo)
829                    0xE1A0_1231, // LSR  R1, R1, R2    (hi >> n)
830                    0xE181_1003, // ORR  R1, R1, R3    (new_hi)
831                    0xEA00_0007, // B    .done
832                    // --- large rotation (n >= 32), R3 = m = n-32 ---
833                    0xE263_2020, // RSB  R2, R3, #32   (R2 = 32-m = 64-n)
834                    0xE1A0_C210, // LSL  R12, R0, R2   (lo << (64-n))
835                    0xE1A0_2211, // LSL  R2, R1, R2    (hi << (64-n))
836                    0xE1A0_1331, // LSR  R1, R1, R3    (hi >> m)
837                    0xE181_C00C, // ORR  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
838                    0xE1A0_1330, // LSR  R1, R0, R3    (lo >> m)
839                    0xE181_1002, // ORR  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
840                    0xE1A0_000C, // MOV  R0, R12       (new_lo into place)
841                ] {
842                    w(&mut b, word);
843                }
844                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
845            }
846
847            // I64Clz: CLZ(hi), or 32 + CLZ(lo) when hi == 0. Conditional
848            // execution replaces the Thumb branches; like the Thumb arm, the
849            // high word of the result pair (rnhi) is cleared last.
850            ArmOp::I64Clz { rd, rnlo, rnhi } => {
851                let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
852                w(&mut b, 0xE350_0000 | (hi << 16)); //              CMP   rnhi, #0
853                w(&mut b, 0x116F_0F10 | (rd_b << 12) | hi); //       CLZNE rd, rnhi
854                w(&mut b, 0x016F_0F10 | (rd_b << 12) | lo); //       CLZEQ rd, rnlo
855                w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
856                // #1048: the former trailing `MOV rnhi, #0` is GONE — it
857                // wrote the OPERAND's home high register (see the Thumb-2
858                // I64Clz comment). Callers that relied on the implicit clear
859                // emit their own explicit hi-zero op.
860            }
861
862            // I64Ctz: CLZ(RBIT(lo)), or 32 + CLZ(RBIT(hi)) when lo == 0.
863            // RBIT/CLZ leave the flags intact, so the CMP's Z survives to the
864            // conditional ADD.
865            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
866                let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
867                w(&mut b, 0xE350_0000 | (lo << 16)); //              CMP    rnlo, #0
868                w(&mut b, 0x16FF_0F30 | (rd_b << 12) | lo); //       RBITNE rd, rnlo
869                w(&mut b, 0x06FF_0F30 | (rd_b << 12) | hi); //       RBITEQ rd, rnhi
870                w(&mut b, 0xE16F_0F10 | (rd_b << 12) | rd_b); //     CLZ    rd, rd
871                w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
872                // #1048: no trailing `MOV rnhi, #0` — see I64Clz above.
873            }
874
875            // I64Const: MOVW/MOVT per half (MOVT elided when the half fits in
876            // 16 bits, mirroring the Thumb-2 arm).
877            ArmOp::I64Const { rdlo, rdhi, value } => {
878                let lo32 = *value as u32;
879                let hi32 = (*value >> 32) as u32;
880                movw(&mut b, reg_to_bits(rdlo), lo32 & 0xFFFF);
881                if lo32 > 0xFFFF {
882                    movt(&mut b, reg_to_bits(rdlo), lo32 >> 16);
883                }
884                movw(&mut b, reg_to_bits(rdhi), hi32 & 0xFFFF);
885                if hi32 > 0xFFFF {
886                    movt(&mut b, reg_to_bits(rdhi), hi32 >> 16);
887                }
888            }
889
890            // I64Ldr / I64Str: two word accesses at [base, #off] / #off+4.
891            // A register offset is materialized into IP once (the #206/#372
892            // hazard: dropping it would read the wrong address).
893            ArmOp::I64Ldr { rdlo, rdhi, addr } | ArmOp::I64Str { rdlo, rdhi, addr } => {
894                let base = if let Some(rm) = addr.offset_reg {
895                    // ADD ip, base, rm
896                    w(
897                        &mut b,
898                        0xE080_0000
899                            | (reg_to_bits(&addr.base) << 16)
900                            | (12 << 12)
901                            | reg_to_bits(&rm),
902                    );
903                    12
904                } else {
905                    reg_to_bits(&addr.base)
906                };
907                if addr.offset < 0 || addr.offset > 0xFFB {
908                    return Err(synth_core::Error::synthesis(format!(
909                        "i64 load/store offset {} out of the A32 imm12 range (0..=4091) — materialize the offset into a register",
910                        addr.offset
911                    )));
912                }
913                let off = addr.offset as u32;
914                let opc: u32 = if matches!(op, ArmOp::I64Ldr { .. }) {
915                    0xE590_0000 // LDR
916                } else {
917                    0xE580_0000 // STR
918                };
919                w(&mut b, opc | (base << 16) | (reg_to_bits(rdlo) << 12) | off);
920                w(
921                    &mut b,
922                    opc | (base << 16) | (reg_to_bits(rdhi) << 12) | (off + 4),
923                );
924            }
925
926            // I64ExtendI32S: rdlo = rn; rdhi = rdlo >> 31 (arithmetic).
927            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
928                if rdlo != rn {
929                    w(
930                        &mut b,
931                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
932                    );
933                }
934                w(
935                    &mut b,
936                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
937                );
938            }
939
940            // I64ExtendI32U: rdlo = rn; rdhi = 0.
941            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
942                if rdlo != rn {
943                    w(
944                        &mut b,
945                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
946                    );
947                }
948                w(&mut b, 0xE3A0_0000 | (reg_to_bits(rdhi) << 12));
949            }
950
951            // I64Extend8S / I64Extend16S: SXTB/SXTH then sign-fill the high word.
952            ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
953                w(
954                    &mut b,
955                    0xE6AF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
956                );
957                w(
958                    &mut b,
959                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
960                );
961            }
962            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
963                w(
964                    &mut b,
965                    0xE6BF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
966                );
967                w(
968                    &mut b,
969                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
970                );
971            }
972            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
973                if rdlo != rnlo {
974                    w(
975                        &mut b,
976                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
977                    );
978                }
979                w(
980                    &mut b,
981                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rnlo),
982                );
983            }
984
985            // I32WrapI64: take the low word. When rd == rnlo this is a genuine
986            // no-op (the one case where a NOP word is the correct encoding).
987            ArmOp::I32WrapI64 { rd, rnlo } => {
988                w(
989                    &mut b,
990                    0xE1A0_0000 | (reg_to_bits(rd) << 12) | reg_to_bits(rnlo),
991                );
992            }
993
994            // I64Add / I64Sub: the classic pair — ADDS lo + ADC hi (SUBS/SBC).
995            // The selector emits these as separate Adds/Adc ops; the fused
996            // variants are verification-constructed, but they encode for real.
997            ArmOp::I64Add {
998                rdlo,
999                rdhi,
1000                rnlo,
1001                rnhi,
1002                rmlo,
1003                rmhi,
1004            } => {
1005                dp_reg(
1006                    &mut b,
1007                    0xE090_0000, // ADDS
1008                    reg_to_bits(rdlo),
1009                    reg_to_bits(rnlo),
1010                    reg_to_bits(rmlo),
1011                );
1012                dp_reg(
1013                    &mut b,
1014                    0xE0A0_0000, // ADC
1015                    reg_to_bits(rdhi),
1016                    reg_to_bits(rnhi),
1017                    reg_to_bits(rmhi),
1018                );
1019            }
1020            ArmOp::I64Sub {
1021                rdlo,
1022                rdhi,
1023                rnlo,
1024                rnhi,
1025                rmlo,
1026                rmhi,
1027            } => {
1028                dp_reg(
1029                    &mut b,
1030                    0xE050_0000, // SUBS
1031                    reg_to_bits(rdlo),
1032                    reg_to_bits(rnlo),
1033                    reg_to_bits(rmlo),
1034                );
1035                dp_reg(
1036                    &mut b,
1037                    0xE0C0_0000, // SBC
1038                    reg_to_bits(rdhi),
1039                    reg_to_bits(rnhi),
1040                    reg_to_bits(rmhi),
1041                );
1042            }
1043
1044            // I64And / I64Or / I64Xor: two independent word ops.
1045            ArmOp::I64And {
1046                rdlo,
1047                rdhi,
1048                rnlo,
1049                rnhi,
1050                rmlo,
1051                rmhi,
1052            }
1053            | ArmOp::I64Or {
1054                rdlo,
1055                rdhi,
1056                rnlo,
1057                rnhi,
1058                rmlo,
1059                rmhi,
1060            }
1061            | ArmOp::I64Xor {
1062                rdlo,
1063                rdhi,
1064                rnlo,
1065                rnhi,
1066                rmlo,
1067                rmhi,
1068            } => {
1069                let base = match op {
1070                    ArmOp::I64And { .. } => 0xE000_0000, // AND
1071                    ArmOp::I64Or { .. } => 0xE180_0000,  // ORR
1072                    _ => 0xE020_0000,                    // EOR
1073                };
1074                dp_reg(
1075                    &mut b,
1076                    base,
1077                    reg_to_bits(rdlo),
1078                    reg_to_bits(rnlo),
1079                    reg_to_bits(rmlo),
1080                );
1081                dp_reg(
1082                    &mut b,
1083                    base,
1084                    reg_to_bits(rdhi),
1085                    reg_to_bits(rnhi),
1086                    reg_to_bits(rmhi),
1087                );
1088            }
1089
1090            // I64DivU: binary long division — A32 transcription of the Thumb-2
1091            // #610/#613 arm (fixed-ABI marshal, zero-divisor trap, 64-round
1092            // shift-subtract core, quotient to R0:R1, result to rd pair).
1093            ArmOp::I64DivU {
1094                rdlo,
1095                rdhi,
1096                rnlo,
1097                rnhi,
1098                rmlo,
1099                rmhi,
1100                elide_zero_guard,
1101            } => {
1102                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1103                // #494 phase 2b: elided only under a certificate-discharged
1104                // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
1105                if !elide_zero_guard {
1106                    emit_a32_i64_divisor_zero_trap(&mut b);
1107                }
1108                w(&mut b, 0xE92D_00F0); // PUSH {R4-R7}
1109                for r in 4..8u32 {
1110                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1111                }
1112                div_loop(&mut b, 12); // counter in R12 (encoder scratch)
1113                w(&mut b, 0xE1A0_0004); // MOV R0, R4 (quotient lo)
1114                w(&mut b, 0xE1A0_1005); // MOV R1, R5 (quotient hi)
1115                w(&mut b, 0xE8BD_00F0); // POP {R4-R7}
1116                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1117            }
1118
1119            // I64DivS: sign-extract, unsigned core, conditional negate —
1120            // A32 transcription of the Thumb-2 arm.
1121            ArmOp::I64DivS {
1122                rdlo,
1123                rdhi,
1124                rnlo,
1125                rnhi,
1126                rmlo,
1127                rmhi,
1128                elide_zero_guard,
1129                elide_overflow_guard,
1130            } => {
1131                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1132                // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
1133                // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
1134                // the #633 overflow guard falls ONLY to
1135                // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
1136                // divisor-nonzero fact alone must keep it.
1137                if !elide_zero_guard {
1138                    emit_a32_i64_divisor_zero_trap(&mut b);
1139                }
1140                if !elide_overflow_guard {
1141                    // #633: INT64_MIN / -1 overflows — trap like the i32 path
1142                    // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
1143                    emit_a32_i64_divs_overflow_trap(&mut b);
1144                }
1145                w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1146                w(&mut b, 0xE021_9003); // EOR R9, R1, R3 (result sign in MSB)
1147                skip_negate_if_positive(&mut b, 1);
1148                negate64(&mut b, 0, 1);
1149                skip_negate_if_positive(&mut b, 3);
1150                negate64(&mut b, 2, 3);
1151                for r in 4..8u32 {
1152                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1153                }
1154                div_loop(&mut b, 8); // counter in R8 (saved above)
1155                w(&mut b, 0xE1A0_0004); // MOV R0, R4
1156                w(&mut b, 0xE1A0_1005); // MOV R1, R5
1157                skip_negate_if_positive(&mut b, 9);
1158                negate64(&mut b, 0, 1);
1159                w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1160                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1161            }
1162
1163            // I64RemU: same core as I64DivU, returns the remainder (R6:R7).
1164            ArmOp::I64RemU {
1165                rdlo,
1166                rdhi,
1167                rnlo,
1168                rnhi,
1169                rmlo,
1170                rmhi,
1171                elide_zero_guard,
1172            } => {
1173                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1174                if !elide_zero_guard {
1175                    emit_a32_i64_divisor_zero_trap(&mut b);
1176                }
1177                w(&mut b, 0xE92D_01F0); // PUSH {R4-R8}
1178                for r in 4..8u32 {
1179                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1180                }
1181                div_loop(&mut b, 8);
1182                w(&mut b, 0xE1A0_0006); // MOV R0, R6 (remainder lo)
1183                w(&mut b, 0xE1A0_1007); // MOV R1, R7 (remainder hi)
1184                w(&mut b, 0xE8BD_01F0); // POP {R4-R8}
1185                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1186            }
1187
1188            // I64RemS: remainder takes the DIVIDEND's sign (WASM semantics).
1189            ArmOp::I64RemS {
1190                rdlo,
1191                rdhi,
1192                rnlo,
1193                rnhi,
1194                rmlo,
1195                rmhi,
1196                elide_zero_guard,
1197            } => {
1198                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1199                if !elide_zero_guard {
1200                    emit_a32_i64_divisor_zero_trap(&mut b);
1201                }
1202                w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1203                w(&mut b, 0xE1A0_9001); // MOV R9, R1 (dividend sign)
1204                skip_negate_if_positive(&mut b, 1);
1205                negate64(&mut b, 0, 1);
1206                skip_negate_if_positive(&mut b, 3);
1207                negate64(&mut b, 2, 3);
1208                for r in 4..8u32 {
1209                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1210                }
1211                div_loop(&mut b, 8);
1212                w(&mut b, 0xE1A0_0006); // MOV R0, R6
1213                w(&mut b, 0xE1A0_1007); // MOV R1, R7
1214                skip_negate_if_positive(&mut b, 9);
1215                negate64(&mut b, 0, 1);
1216                w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1217                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1218            }
1219
1220            // Popcnt (i32): bit-twiddle expansion (no native A32 popcount),
1221            // mirroring the Thumb-2 arm's #1021 register contract: R12 is the
1222            // ONLY scratch. The previous transcription copied the old Thumb
1223            // contract's R11 borrow — but R11 is the linear-memory base on
1224            // this path too, so it inherited the same live miscompile. A32
1225            // has no ThumbExpandImm for 0xXYXYXYXY masks, so instead the
1226            // barrel shifter folds each shift into the mask AND itself
1227            // (`AND R12, R12, rd, LSR #n`), and step 2 recovers `x & C` from
1228            // one term via `x - (((x >> 2) & C) << 2) = x & C` — the second
1229            // temp disappears algebraically. Straight-line, no PUSH/POP,
1230            // nothing to skip on a trap edge.
1231            ArmOp::Popcnt { rd, rm } => {
1232                let rd_b = reg_to_bits(rd);
1233                // Defensive (#1021), same contract as the Thumb-2 arm.
1234                if rd_b >= 11 {
1235                    return Err(synth_core::Error::synthesis(
1236                        "Popcnt destination must be R0-R10: R11 is the linear-memory \
1237                         base and R12 is the expansion's scratch (#1021)",
1238                    ));
1239                }
1240                if rd != rm {
1241                    w(&mut b, 0xE1A0_0000 | (rd_b << 12) | reg_to_bits(rm)); // MOV rd, rm
1242                }
1243                // x = x - ((x >> 1) & 0x55555555)
1244                movw(&mut b, 12, 0x5555);
1245                movt(&mut b, 12, 0x5555);
1246                dp_reg_shift(&mut b, 0xE000_0000, 12, 12, rd_b, LSR, 1); // AND R12, R12, rd, LSR #1
1247                dp_reg(&mut b, 0xE040_0000, rd_b, rd_b, 12); //             SUB rd, rd, R12
1248                // x = (x & 0x33333333) + ((x >> 2) & 0x33333333), one temp:
1249                //   R12 = (x >> 2) & C; x - (R12 << 2) = x & C; then + R12.
1250                movw(&mut b, 12, 0x3333);
1251                movt(&mut b, 12, 0x3333);
1252                dp_reg_shift(&mut b, 0xE000_0000, 12, 12, rd_b, LSR, 2); // AND R12, R12, rd, LSR #2
1253                dp_reg_shift(&mut b, 0xE040_0000, rd_b, rd_b, 12, LSL, 2); // SUB rd, rd, R12, LSL #2
1254                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 12); //              ADD rd, rd, R12
1255                // x = (x + (x >> 4)) & 0x0F0F0F0F
1256                dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 4); // ADD rd, rd, rd, LSR #4
1257                movw(&mut b, 12, 0x0F0F);
1258                movt(&mut b, 12, 0x0F0F);
1259                dp_reg(&mut b, 0xE000_0000, rd_b, rd_b, 12); // AND rd, rd, R12
1260                // x += x >> 8; x += x >> 16; x &= 0x3F
1261                dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 8);
1262                dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 16);
1263                w(&mut b, 0xE200_003F | (rd_b << 16) | (rd_b << 12)); // AND rd, rd, #63
1264            }
1265
1266            // I64Popcnt: POPCNT(lo) + POPCNT(hi) — A32 transcription of the
1267            // Thumb-2 arm (R3/R4/R5 saved, mul-based per-word fold, high
1268            // result word rnhi cleared last, mirroring the Thumb contract).
1269            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
1270                let hi = reg_to_bits(rnhi);
1271                w(&mut b, 0xE92D_0038); // PUSH {R3, R4, R5}
1272                // #632 audit: route rnlo through R12 so a pair living at
1273                // (R3,R4) cannot read a clobbered R4 (sources read before any
1274                // scratch register they could occupy is written).
1275                w(&mut b, 0xE1A0_C000 | reg_to_bits(rnlo)); // MOV R12, rnlo
1276                w(&mut b, 0xE1A0_5000 | hi); //                MOV R5, rnhi
1277                w(&mut b, 0xE1A0_400C); //                     MOV R4, R12
1278                popcnt_word(&mut b, 4, 3);
1279                popcnt_word(&mut b, 5, 3);
1280                // #632: carry the count across the scratch restore in R12 —
1281                // rd is allocator-assigned and can land inside {R3,R4,R5};
1282                // the old `ADD rd, R4, R5` before the POP was destroyed by
1283                // the restore. R12 is never allocatable and never restored.
1284                dp_reg(&mut b, 0xE080_0000, 12, 4, 5); // ADD R12, R4, R5
1285                w(&mut b, 0xE8BD_0038); // POP {R3, R4, R5}
1286                w(&mut b, 0xE1A0_0000 | (reg_to_bits(rd) << 12) | 12); // MOV rd, R12
1287                // #1048: no trailing `MOV rnhi, #0` — the hi-word clear wrote
1288                // the OPERAND's home high register; callers emit it explicitly.
1289            }
1290
1291            _ => return Ok(None),
1292        }
1293        Ok(Some(b))
1294    }
1295
1296    fn encode_arm(&self, op: &ArmOp) -> Result<Vec<u8>> {
1297        // #615: A32 multi-instruction expansions (i64 arithmetic/shift/rotate/
1298        // compare, SetCond/SelectMove, popcnt, ...). These ops were literal
1299        // NOPs on the A32 path — user-reachable via `--target cortex-r5` —
1300        // so the value silently vanished. Mirror of the #594 CallIndirect
1301        // early-return: if the expansion helper covers the op, its bytes are
1302        // the encoding.
1303        if let Some(bytes) = self.encode_arm_expanded(op)? {
1304            return Ok(bytes);
1305        }
1306        // #206: ARM32 register-offset loads/stores. `encode_mem_addr` only
1307        // returns the 12-bit immediate, so the immediate-form arms below
1308        // silently DROP `addr.offset_reg` — a runtime address index vanished,
1309        // turning `ldr rd,[rn,rm,#off]` into `ldr rd,[rn,#off]` (the access went
1310        // to the wrong address). Compute the effective base into IP and re-encode
1311        // against `[ip, #off]`, which is uniform for word/byte/halfword/signed.
1312        if let Some(bytes) = self.encode_arm_reg_offset_mem(op)? {
1313            return Ok(bytes);
1314        }
1315        // #594: call_indirect was encoded as a literal NOP on the A32 path
1316        // (`--target cortex-r5`) — the call never happened and the function
1317        // silently returned garbage. Emit the same three-instruction expansion
1318        // as the Thumb-2 path (R11 = function-pointer table base, R12 scratch):
1319        //   MOV r12, idx, LSL #2 ; LDR r12, [r11, r12] ; BLX r12
1320        if let ArmOp::CallIndirect {
1321            table_index_reg,
1322            table_size,
1323            table_byte_offset,
1324            null_check,
1325            type_check,
1326            ..
1327        } = op
1328        {
1329            return Ok(Self::encode_arm_call_indirect(
1330                table_index_reg,
1331                *table_size,
1332                *table_byte_offset,
1333                *null_check,
1334                *type_check,
1335            ));
1336        }
1337        let instr: u32 = match op {
1338            // Data processing instructions
1339            ArmOp::Add { rd, rn, op2 } => {
1340                let rd_bits = reg_to_bits(rd);
1341                let rn_bits = reg_to_bits(rn);
1342                let (op2_bits, i_flag) = encode_operand2(op2)?;
1343
1344                // ADD encoding: cond(4) | 00 | I(1) | 0100 | S(1) | Rn(4) | Rd(4) | operand2(12)
1345                0xE0800000 // condition=always(E), opcode=ADD(0100), S=0
1346                    | (i_flag << 25)
1347                    | (rn_bits << 16)
1348                    | (rd_bits << 12)
1349                    | op2_bits
1350            }
1351
1352            ArmOp::Sub { rd, rn, op2 } => {
1353                let rd_bits = reg_to_bits(rd);
1354                let rn_bits = reg_to_bits(rn);
1355                let (op2_bits, i_flag) = encode_operand2(op2)?;
1356
1357                // SUB encoding: opcode=0010
1358                0xE0400000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1359            }
1360
1361            // i64 support: ADDS, ADC, SUBS, SBC for ARM32
1362            ArmOp::Adds { rd, rn, op2 } => {
1363                let rd_bits = reg_to_bits(rd);
1364                let rn_bits = reg_to_bits(rn);
1365                let (op2_bits, i_flag) = encode_operand2(op2)?;
1366
1367                // ADDS encoding: opcode=0100, S=1
1368                0xE0900000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1369            }
1370
1371            ArmOp::Adc { rd, rn, op2 } => {
1372                let rd_bits = reg_to_bits(rd);
1373                let rn_bits = reg_to_bits(rn);
1374                let (op2_bits, i_flag) = encode_operand2(op2)?;
1375
1376                // ADC encoding: opcode=0101
1377                0xE0A00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1378            }
1379
1380            ArmOp::Subs { rd, rn, op2 } => {
1381                let rd_bits = reg_to_bits(rd);
1382                let rn_bits = reg_to_bits(rn);
1383                let (op2_bits, i_flag) = encode_operand2(op2)?;
1384
1385                // SUBS encoding: opcode=0010, S=1
1386                0xE0500000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1387            }
1388
1389            ArmOp::Sbc { rd, rn, op2 } => {
1390                let rd_bits = reg_to_bits(rd);
1391                let rn_bits = reg_to_bits(rn);
1392                let (op2_bits, i_flag) = encode_operand2(op2)?;
1393
1394                // SBC encoding: opcode=0110
1395                0xE0C00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1396            }
1397
1398            ArmOp::Mul { rd, rn, rm } => {
1399                let rd_bits = reg_to_bits(rd);
1400                let rn_bits = reg_to_bits(rn);
1401                let rm_bits = reg_to_bits(rm);
1402
1403                // MUL encoding: cond(4) | 000000 | A(1) | S(1) | Rd(4) | Rn(4) | Rs(4) | 1001 | Rm(4)
1404                0xE0000090 | (rd_bits << 16) | (rn_bits << 8) | rm_bits
1405            }
1406
1407            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
1408                let rdlo_bits = reg_to_bits(rdlo);
1409                let rdhi_bits = reg_to_bits(rdhi);
1410                let rn_bits = reg_to_bits(rn);
1411                let rm_bits = reg_to_bits(rm);
1412
1413                // UMULL encoding: cond(4) | 0000 1000 | RdHi(4) | RdLo(4) | Rm(4) | 1001 | Rn(4)
1414                0xE0800090 | (rdhi_bits << 16) | (rdlo_bits << 12) | (rm_bits << 8) | rn_bits
1415            }
1416
1417            ArmOp::Sdiv { rd, rn, rm } => {
1418                let rd_bits = reg_to_bits(rd);
1419                let rn_bits = reg_to_bits(rn);
1420                let rm_bits = reg_to_bits(rm);
1421
1422                // SDIV encoding: cond(4) | 01110001 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1423                // ARMv7-M and above
1424                0xE710F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1425            }
1426
1427            ArmOp::Udiv { rd, rn, rm } => {
1428                let rd_bits = reg_to_bits(rd);
1429                let rn_bits = reg_to_bits(rn);
1430                let rm_bits = reg_to_bits(rm);
1431
1432                // UDIV encoding: cond(4) | 01110011 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1433                // ARMv7-M and above
1434                0xE730F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1435            }
1436
1437            ArmOp::Mls { rd, rn, rm, ra } => {
1438                let rd_bits = reg_to_bits(rd);
1439                let rn_bits = reg_to_bits(rn);
1440                let rm_bits = reg_to_bits(rm);
1441                let ra_bits = reg_to_bits(ra);
1442
1443                // MLS encoding: cond(4) | 00000110 | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1444                // Rd = Ra - (Rn * Rm)
1445                0xE0600090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1446            }
1447
1448            ArmOp::Mla { rd, rn, rm, ra } => {
1449                let rd_bits = reg_to_bits(rd);
1450                let rn_bits = reg_to_bits(rn);
1451                let rm_bits = reg_to_bits(rm);
1452                let ra_bits = reg_to_bits(ra);
1453
1454                // MLA encoding: cond(4) | 0000001 S | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1455                // Rd = Ra + (Rn * Rm). Base 0xE0200090 (S=0).
1456                0xE0200090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1457            }
1458
1459            ArmOp::And { rd, rn, op2 } => {
1460                let rd_bits = reg_to_bits(rd);
1461                let rn_bits = reg_to_bits(rn);
1462                let (op2_bits, i_flag) = encode_operand2(op2)?;
1463
1464                // AND encoding: opcode=0000
1465                0xE0000000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1466            }
1467
1468            ArmOp::Orr { rd, rn, op2 } => {
1469                let rd_bits = reg_to_bits(rd);
1470                let rn_bits = reg_to_bits(rn);
1471                let (op2_bits, i_flag) = encode_operand2(op2)?;
1472
1473                // ORR encoding: opcode=1100
1474                0xE1800000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1475            }
1476
1477            ArmOp::Eor { rd, rn, op2 } => {
1478                let rd_bits = reg_to_bits(rd);
1479                let rn_bits = reg_to_bits(rn);
1480                let (op2_bits, i_flag) = encode_operand2(op2)?;
1481
1482                // EOR encoding: opcode=0001
1483                0xE0200000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1484            }
1485
1486            // Shift instructions
1487            ArmOp::Lsl { rd, rn, shift } => {
1488                let rd_bits = reg_to_bits(rd);
1489                let rn_bits = reg_to_bits(rn);
1490                let shift_bits = *shift & 0x1F;
1491
1492                // LSL encoding: MOV with shift
1493                0xE1A00000 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1494            }
1495
1496            ArmOp::Lsr { rd, rn, shift } => {
1497                let rd_bits = reg_to_bits(rd);
1498                let rn_bits = reg_to_bits(rn);
1499                let shift_bits = *shift & 0x1F;
1500
1501                // LSR encoding
1502                0xE1A00020 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1503            }
1504
1505            ArmOp::Asr { rd, rn, shift } => {
1506                let rd_bits = reg_to_bits(rd);
1507                let rn_bits = reg_to_bits(rn);
1508                let shift_bits = *shift & 0x1F;
1509
1510                // ASR encoding
1511                0xE1A00040 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1512            }
1513
1514            ArmOp::Ror { rd, rn, shift } => {
1515                let rd_bits = reg_to_bits(rd);
1516                let rn_bits = reg_to_bits(rn);
1517                let shift_bits = *shift & 0x1F;
1518
1519                // ROR encoding: MOV with ROR shift
1520                0xE1A00060 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1521            }
1522
1523            // Register-based shifts (ARM32)
1524            // LSL Rd, Rn, Rm: cond 0001101S 0000 Rd Rs 0001 Rn
1525            ArmOp::LslReg { rd, rn, rm } => {
1526                let rd_bits = reg_to_bits(rd);
1527                let rn_bits = reg_to_bits(rn);
1528                let rm_bits = reg_to_bits(rm);
1529                0xE1A00010 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1530            }
1531            ArmOp::LsrReg { rd, rn, rm } => {
1532                let rd_bits = reg_to_bits(rd);
1533                let rn_bits = reg_to_bits(rn);
1534                let rm_bits = reg_to_bits(rm);
1535                0xE1A00030 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1536            }
1537            ArmOp::AsrReg { rd, rn, rm } => {
1538                let rd_bits = reg_to_bits(rd);
1539                let rn_bits = reg_to_bits(rn);
1540                let rm_bits = reg_to_bits(rm);
1541                0xE1A00050 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1542            }
1543            ArmOp::RorReg { rd, rn, rm } => {
1544                let rd_bits = reg_to_bits(rd);
1545                let rn_bits = reg_to_bits(rn);
1546                let rm_bits = reg_to_bits(rm);
1547                0xE1A00070 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1548            }
1549
1550            // RSB (Reverse Subtract): Rd = imm - Rn
1551            ArmOp::Rsb { rd, rn, imm } => {
1552                let rd_bits = reg_to_bits(rd);
1553                let rn_bits = reg_to_bits(rn);
1554                // RSB encoding: cond(4) | 00 1 0011 S | Rn(4) | Rd(4) | imm12
1555                // Opcode for RSB = 0011, I=1 (immediate), S=0
1556                //
1557                // #681 class audit: the A32 imm12 is a rotate(4):imm8 modified
1558                // immediate; `*imm & 0xFF` silently encoded a WRONG constant
1559                // for imm > 0xFF (#378 masking class). All current emitters use
1560                // imm 32, so erroring here is byte-identical for real codegen.
1561                if *imm > 0xFF {
1562                    return Err(synth_core::Error::synthesis(
1563                        "A32 RSB immediate > 0xFF requires a rotated-immediate encoding \
1564                         (not supported) — materialize into a register",
1565                    ));
1566                }
1567                0xE2600000 | (rn_bits << 16) | (rd_bits << 12) | (*imm & 0xFF)
1568            }
1569
1570            // Bit manipulation instructions
1571            ArmOp::Clz { rd, rm } => {
1572                let rd_bits = reg_to_bits(rd);
1573                let rm_bits = reg_to_bits(rm);
1574
1575                // CLZ encoding: cond(4) | 00010110 | 1111 | Rd(4) | 1111 | 0001 | Rm(4)
1576                // ARMv5T and above
1577                0xE16F0F10 | (rd_bits << 12) | rm_bits
1578            }
1579
1580            ArmOp::Rbit { rd, rm } => {
1581                let rd_bits = reg_to_bits(rd);
1582                let rm_bits = reg_to_bits(rm);
1583
1584                // RBIT encoding: cond(4) | 01101111 | 1111 | Rd(4) | 1111 | 0011 | Rm(4)
1585                // ARMv6T2 and above
1586                0xE6FF0F30 | (rd_bits << 12) | rm_bits
1587            }
1588
1589            ArmOp::Sxtb { rd, rm } => {
1590                let rd_bits = reg_to_bits(rd);
1591                let rm_bits = reg_to_bits(rm);
1592
1593                // SXTB encoding: cond(4) | 01101010 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1594                // ARMv6 and above. rotate=00 for no rotation
1595                0xE6AF0070 | (rd_bits << 12) | rm_bits
1596            }
1597
1598            ArmOp::Sxth { rd, rm } => {
1599                let rd_bits = reg_to_bits(rd);
1600                let rm_bits = reg_to_bits(rm);
1601
1602                // SXTH encoding: cond(4) | 01101011 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1603                // ARMv6 and above. rotate=00 for no rotation
1604                0xE6BF0070 | (rd_bits << 12) | rm_bits
1605            }
1606
1607            ArmOp::Uxtb { rd, rm } => {
1608                let rd_bits = reg_to_bits(rd);
1609                let rm_bits = reg_to_bits(rm);
1610                // UXTB encoding: cond | 01101110 1111 Rd rotate 00 0111 Rm (rotate=00)
1611                0xE6EF0070 | (rd_bits << 12) | rm_bits
1612            }
1613
1614            ArmOp::Uxth { rd, rm } => {
1615                let rd_bits = reg_to_bits(rd);
1616                let rm_bits = reg_to_bits(rm);
1617                // UXTH encoding: cond | 01101111 1111 Rd rotate 00 0111 Rm (rotate=00)
1618                0xE6FF0070 | (rd_bits << 12) | rm_bits
1619            }
1620
1621            // Move instructions
1622            ArmOp::Mov { rd, op2 } => {
1623                let rd_bits = reg_to_bits(rd);
1624                let (op2_bits, i_flag) = encode_operand2(op2)?;
1625
1626                // MOV encoding: opcode=1101
1627                0xE1A00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1628            }
1629
1630            ArmOp::Mvn { rd, op2 } => {
1631                let rd_bits = reg_to_bits(rd);
1632                let (op2_bits, i_flag) = encode_operand2(op2)?;
1633
1634                // MVN encoding: opcode=1111
1635                0xE1E00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1636            }
1637
1638            // MOVW - Move Wide (ARM32)
1639            // Encoding: cond(4) | 0011 0000 | imm4(4) | Rd(4) | imm12(12)
1640            ArmOp::Movw { rd, imm16 } => {
1641                let rd_bits = reg_to_bits(rd);
1642                let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1643                let imm12 = (*imm16 as u32) & 0xFFF;
1644                0xE3000000 | (imm4 << 16) | (rd_bits << 12) | imm12
1645            }
1646
1647            // MOVT - Move Top (ARM32)
1648            // Encoding: cond(4) | 0011 0100 | imm4(4) | Rd(4) | imm12(12)
1649            ArmOp::Movt { rd, imm16 } => {
1650                let rd_bits = reg_to_bits(rd);
1651                let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1652                let imm12 = (*imm16 as u32) & 0xFFF;
1653                0xE3400000 | (imm4 << 16) | (rd_bits << 12) | imm12
1654            }
1655
1656            // #237: symbol-relative MOVW/MOVT (ARM mode) — addend in place, the
1657            // backend records the MOVW_ABS/MOVT_ABS relocation against `symbol`.
1658            ArmOp::MovwSym { rd, addend, .. } => {
1659                let rd_bits = reg_to_bits(rd);
1660                let v = (*addend as u32) & 0xffff;
1661                0xE3000000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1662            }
1663            ArmOp::MovtSym { rd, addend, .. } => {
1664                let rd_bits = reg_to_bits(rd);
1665                let v = ((*addend as u32) >> 16) & 0xffff;
1666                0xE3400000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1667            }
1668
1669            // #345: LdrSym is the Thumb-2 literal-pool address load. A32 mode is
1670            // not used for relocatable native-pointer objects; fail loudly rather
1671            // than miscompile if it is ever reached here.
1672            ArmOp::LdrSym { .. } => {
1673                return Err(synth_core::Error::synthesis(
1674                    "LdrSym (literal-pool address load) is Thumb-2-only",
1675                ));
1676            }
1677
1678            // Compare
1679            ArmOp::Cmp { rn, op2 } => {
1680                let rn_bits = reg_to_bits(rn);
1681                let (op2_bits, i_flag) = encode_operand2(op2)?;
1682
1683                // CMP encoding: opcode=1010, S=1
1684                0xE1500000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1685            }
1686
1687            // Compare Negative (CMN) - computes Rn + op2 and sets flags
1688            ArmOp::Cmn { rn, op2 } => {
1689                let rn_bits = reg_to_bits(rn);
1690                let (op2_bits, i_flag) = encode_operand2(op2)?;
1691
1692                // CMN encoding: opcode=1011, S=1
1693                0xE1700000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1694            }
1695
1696            // Load/Store
1697            ArmOp::Ldr { rd, addr } => {
1698                let rd_bits = reg_to_bits(rd);
1699                let (base_bits, offset_bits) = encode_mem_addr(addr);
1700
1701                // LDR encoding: cond(4) | 01 | I(1) | P(1) | U(1) | B(1) | W(1) | L(1) | Rn(4) | Rd(4) | offset(12)
1702                // P=1 (pre-indexed), U=1 (add offset), L=1 (load)
1703                0xE5900000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1704            }
1705
1706            ArmOp::Str { rd, addr } => {
1707                let rd_bits = reg_to_bits(rd);
1708                let (base_bits, offset_bits) = encode_mem_addr(addr);
1709
1710                // STR encoding: L=0 (store)
1711                0xE5800000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1712            }
1713
1714            // Sub-word loads (ARM32 encoding)
1715            ArmOp::Ldrb { rd, addr } => {
1716                let rd_bits = reg_to_bits(rd);
1717                let (base_bits, offset_bits) = encode_mem_addr(addr);
1718                // LDRB: LDR with B=1 (byte): cond|01|I|P|U|1|W|L|Rn|Rd|offset
1719                0xE5D00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1720            }
1721
1722            ArmOp::Ldrsb { rd, addr } => {
1723                let rd_bits = reg_to_bits(rd);
1724                let (base_bits, offset_bits) = encode_mem_addr(addr);
1725                // LDRSB (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1101|imm4L
1726                // Simplified with immediate offset
1727                let offset_val = offset_bits & 0xFF;
1728                let imm4h = (offset_val >> 4) & 0xF;
1729                let imm4l = offset_val & 0xF;
1730                0xE1D000D0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1731            }
1732
1733            ArmOp::Ldrh { rd, addr } => {
1734                let rd_bits = reg_to_bits(rd);
1735                let (base_bits, offset_bits) = encode_mem_addr(addr);
1736                // LDRH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1011|imm4L
1737                let offset_val = offset_bits & 0xFF;
1738                let imm4h = (offset_val >> 4) & 0xF;
1739                let imm4l = offset_val & 0xF;
1740                0xE1D000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1741            }
1742
1743            ArmOp::Ldrsh { rd, addr } => {
1744                let rd_bits = reg_to_bits(rd);
1745                let (base_bits, offset_bits) = encode_mem_addr(addr);
1746                // LDRSH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1111|imm4L
1747                let offset_val = offset_bits & 0xFF;
1748                let imm4h = (offset_val >> 4) & 0xF;
1749                let imm4l = offset_val & 0xF;
1750                0xE1D000F0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1751            }
1752
1753            // Sub-word stores (ARM32 encoding)
1754            ArmOp::Strb { rd, addr } => {
1755                let rd_bits = reg_to_bits(rd);
1756                let (base_bits, offset_bits) = encode_mem_addr(addr);
1757                // STRB: STR with B=1 (byte): cond|01|I|P|U|1|W|0|Rn|Rd|offset
1758                0xE5C00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1759            }
1760
1761            ArmOp::Strh { rd, addr } => {
1762                let rd_bits = reg_to_bits(rd);
1763                let (base_bits, offset_bits) = encode_mem_addr(addr);
1764                // STRH (misc store): cond|000|P|U|1|W|0|Rn|Rd|imm4H|1011|imm4L
1765                let offset_val = offset_bits & 0xFF;
1766                let imm4h = (offset_val >> 4) & 0xF;
1767                let imm4l = offset_val & 0xF;
1768                0xE1C000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1769            }
1770
1771            // Memory management (ARM32 encoding)
1772            ArmOp::MemorySize { rd } => {
1773                let rd_bits = reg_to_bits(rd);
1774                // MOV rd, R10, LSR #16  (memory size in bytes / 65536 = pages)
1775                // cond|000|1101|S|0000|Rd|shift5|type|0|Rm
1776                // LSR #16: shift5=10000, type=01
1777                0xE1A00820 | (rd_bits << 12) | 0x0A // Rm=R10, shift=16, LSR
1778            }
1779
1780            ArmOp::MemoryGrow { rd, .. } => {
1781                let rd_bits = reg_to_bits(rd);
1782                // On embedded, always fail: MOV rd, #-1
1783                0xE3E00000 | (rd_bits << 12) // MVN rd, #0 = MOV rd, #-1
1784            }
1785
1786            // Label pseudo-instruction: emits no machine code
1787            ArmOp::Label { .. } => {
1788                return Ok(Vec::new());
1789            }
1790
1791            // Branch instructions
1792            ArmOp::B { label: _ } => {
1793                // B encoding: cond(4) | 1010 | offset(24)
1794                // Simplified: branch to offset 0 (will be patched by linker/resolver)
1795                0xEA000000
1796            }
1797
1798            // Conditional branch to label (generic)
1799            ArmOp::Bcc { cond, label: _ } => {
1800                use synth_synthesis::Condition;
1801                let cond_bits: u32 = match cond {
1802                    Condition::EQ => 0x0,
1803                    Condition::NE => 0x1,
1804                    Condition::HS => 0x2,
1805                    Condition::LO => 0x3,
1806                    Condition::HI => 0x8,
1807                    Condition::LS => 0x9,
1808                    Condition::GE => 0xA,
1809                    Condition::LT => 0xB,
1810                    Condition::GT => 0xC,
1811                    Condition::LE => 0xD,
1812                };
1813                // B<cond> with offset 0 (will be patched)
1814                (cond_bits << 28) | 0x0A000000
1815            }
1816
1817            // BHS (Branch if Higher or Same) - used for bounds checking
1818            ArmOp::Bhs { label: _ } => {
1819                // BHS encoding: cond(2=HS) | 1010 | offset(24)
1820                0x2A000000 // BHS with offset 0
1821            }
1822
1823            // BLO (Branch if Lower) - complementary to BHS
1824            ArmOp::Blo { label: _ } => {
1825                // BLO encoding: cond(3=LO) | 1010 | offset(24)
1826                0x3A000000 // BLO with offset 0
1827            }
1828
1829            // Branch with numeric offset (in instructions)
1830            // ARM32 B instruction: offset is in instructions, stored as words
1831            // The offset is relative to PC+8 (due to ARM pipeline)
1832            ArmOp::BOffset { offset } => {
1833                // B encoding: cond(4) | 1010 | offset(24)
1834                // Offset is signed, in words (4-byte units)
1835                // ARM adds PC+8 to the offset, so we need to adjust:
1836                // target = PC + 8 + (offset * 4)
1837                // For backward branch of N instructions: offset = -(N + 2)
1838                // wrapping_sub keeps the encoder total under fuzzing (#186): an
1839                // extreme i32::MIN offset would otherwise overflow-panic; for any
1840                // real branch offset this is identical to `- 2`.
1841                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1842                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1843                0xEA000000 | offset_bits
1844            }
1845
1846            // Conditional branch with numeric offset
1847            ArmOp::BCondOffset { cond, offset } => {
1848                use synth_synthesis::Condition;
1849                let cond_bits: u32 = match cond {
1850                    Condition::EQ => 0x0,
1851                    Condition::NE => 0x1,
1852                    Condition::HS => 0x2,
1853                    Condition::LO => 0x3,
1854                    Condition::HI => 0x8,
1855                    Condition::LS => 0x9,
1856                    Condition::GE => 0xA,
1857                    Condition::LT => 0xB,
1858                    Condition::GT => 0xC,
1859                    Condition::LE => 0xD,
1860                };
1861                // B<cond> encoding: cond(4) | 1010 | offset(24)
1862                // wrapping_sub: total under fuzzing (#186), identical for real offsets.
1863                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1864                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1865                (cond_bits << 28) | 0x0A000000 | offset_bits
1866            }
1867
1868            ArmOp::Bl { label: _ } => {
1869                // BL encoding: cond(4) | 1011 | offset(24). Relocatable
1870                // placeholder; an R_ARM_CALL relocation patches the target.
1871                //
1872                // #1040: the placeholder must carry an embedded addend of -8,
1873                // not 0. A32 `BL` computes `target = P + 8 + (imm24 << 2)`, so
1874                // under REL semantics a 0 addend (`eb000000`) resolves two
1875                // instructions PAST the callee entry — the A32 twin of the
1876                // Thumb #174 bug. The correct word is what `gas` emits for
1877                // `bl <extern>` in ARM mode:
1878                //   ebfffffe  ->  `bl <self>`  (imm24 = -2, offset = -8),
1879                // which nets to exactly S. Verified against
1880                // `arm-none-eabi-as -march=armv7-r`, which emits `ebfffffe`
1881                // with an R_ARM_CALL relocation.
1882                0xEBFFFFFE
1883            }
1884
1885            ArmOp::Bx { rm } => {
1886                let rm_bits = reg_to_bits(rm);
1887
1888                // BX encoding: cond(4) | 000100101111111111110001 | Rm(4)
1889                0xE12FFF10 | rm_bits
1890            }
1891
1892            ArmOp::Blx { rm } => {
1893                let rm_bits = reg_to_bits(rm);
1894
1895                // BLX (register) encoding: cond(4) | 000100101111111111110011 | Rm(4)
1896                0xE12FFF30 | rm_bits
1897            }
1898
1899            ArmOp::Push { regs } => {
1900                // STMDB SP!, {regs} encoding: cond(4) | 100100 | 10 | 1101 | register_list(16)
1901                let mut reg_list: u32 = 0;
1902                for r in regs {
1903                    reg_list |= 1 << reg_to_bits(r);
1904                }
1905                0xE92D0000 | reg_list
1906            }
1907
1908            ArmOp::Pop { regs } => {
1909                // LDMIA SP!, {regs} encoding: cond(4) | 100010 | 11 | 1101 | register_list(16)
1910                let mut reg_list: u32 = 0;
1911                for r in regs {
1912                    reg_list |= 1 << reg_to_bits(r);
1913                }
1914                0xE8BD0000 | reg_list
1915            }
1916
1917            ArmOp::Nop => {
1918                // NOP encoding: MOV R0, R0
1919                0xE1A00000
1920            }
1921
1922            ArmOp::Udf { imm } => {
1923                // UDF (Undefined) encoding in ARM: 0xE7F000F0 | (imm12_hi << 8) | imm4_lo
1924                // We only use imm8, so split into imm4_hi and imm4_lo
1925                let imm8 = *imm as u32;
1926                0xE7F000F0 | ((imm8 & 0xF0) << 4) | (imm8 & 0x0F)
1927            }
1928
1929            // #615: handled by the `encode_arm_expanded` early return at the
1930            // top of this function — a real MOV{cond}/MOV pair now, never a
1931            // silent NOP again.
1932            ArmOp::Popcnt { .. } | ArmOp::SetCond { .. } | ArmOp::SelectMove { .. } => {
1933                unreachable!("handled by encode_arm_expanded (#615)")
1934            }
1935
1936            // Verification-only pseudo-ops: `synth-verify`'s ArmSemantics
1937            // models these, but NO codegen path constructs them (the selector
1938            // lowers select/locals/globals/br_table/call to real instruction
1939            // sequences before the encoder). Encoding one as a NOP silently
1940            // dropped the operation (#615 class); a typed Err keeps the
1941            // encoder total (Ok-or-Err, the `encoder_no_panic` contract)
1942            // while making any future reachability LOUD.
1943            ArmOp::Select { .. }
1944            | ArmOp::LocalGet { .. }
1945            | ArmOp::LocalSet { .. }
1946            | ArmOp::LocalTee { .. }
1947            | ArmOp::GlobalGet { .. }
1948            | ArmOp::GlobalSet { .. }
1949            | ArmOp::BrTable { .. }
1950            | ArmOp::Call { .. } => {
1951                return Err(synth_core::Error::synthesis(format!(
1952                    "verification-only pseudo-op {op:?} reached the A32 encoder — \
1953                     codegen lowers it before encoding; refusing to emit a silent NOP (#615)"
1954                )));
1955            }
1956
1957            // #594: CallIndirect is expanded to a real multi-instruction
1958            // sequence by the early return at the top of this function —
1959            // it must NEVER fall through to a silent NOP again.
1960            ArmOp::CallIndirect { .. } => {
1961                unreachable!("CallIndirect handled by encode_arm_call_indirect (#594)")
1962            }
1963
1964            // #615: every i64 op (and I32WrapI64) is expanded to a real A32
1965            // multi-instruction sequence by `encode_arm_expanded` — the
1966            // "encode as NOP for now" era ended with the value silently
1967            // vanishing on `--target cortex-r5`.
1968            ArmOp::I64Add { .. }
1969            | ArmOp::I64Sub { .. }
1970            | ArmOp::I64DivS { .. }
1971            | ArmOp::I64DivU { .. }
1972            | ArmOp::I64RemS { .. }
1973            | ArmOp::I64RemU { .. }
1974            | ArmOp::I64Clz { .. }
1975            | ArmOp::I64Ctz { .. }
1976            | ArmOp::I64Popcnt { .. }
1977            | ArmOp::I64And { .. }
1978            | ArmOp::I64Or { .. }
1979            | ArmOp::I64Xor { .. }
1980            | ArmOp::I64Eqz { .. }
1981            | ArmOp::I64Eq { .. }
1982            | ArmOp::I64Ne { .. }
1983            | ArmOp::I64LtS { .. }
1984            | ArmOp::I64LtU { .. }
1985            | ArmOp::I64LeS { .. }
1986            | ArmOp::I64LeU { .. }
1987            | ArmOp::I64GtS { .. }
1988            | ArmOp::I64GtU { .. }
1989            | ArmOp::I64GeS { .. }
1990            | ArmOp::I64GeU { .. }
1991            | ArmOp::I64Const { .. }
1992            | ArmOp::I64Ldr { .. }
1993            | ArmOp::I64Str { .. }
1994            | ArmOp::I64ExtendI32S { .. }
1995            | ArmOp::I64ExtendI32U { .. }
1996            | ArmOp::I64Extend8S { .. }
1997            | ArmOp::I64Extend16S { .. }
1998            | ArmOp::I64Extend32S { .. }
1999            | ArmOp::I32WrapI64 { .. } => {
2000                unreachable!("handled by encode_arm_expanded (#615)")
2001            }
2002
2003            // f32 VFP single-precision instructions
2004            ArmOp::F32Add { sd, sn, sm } => encode_vfp_3reg(0xEE300A00, sd, sn, sm)?,
2005            ArmOp::F32Sub { sd, sn, sm } => encode_vfp_3reg(0xEE300A40, sd, sn, sm)?,
2006            ArmOp::F32Mul { sd, sn, sm } => encode_vfp_3reg(0xEE200A00, sd, sn, sm)?,
2007            ArmOp::F32Div { sd, sn, sm } => encode_vfp_3reg(0xEE800A00, sd, sn, sm)?,
2008            ArmOp::F32Abs { sd, sm } => encode_vfp_2reg(0xEEB00AC0, sd, sm)?,
2009            ArmOp::F32Neg { sd, sm } => encode_vfp_2reg(0xEEB10A40, sd, sm)?,
2010            ArmOp::F32Sqrt { sd, sm } => encode_vfp_2reg(0xEEB10AC0, sd, sm)?,
2011
2012            // f32 pseudo-ops — multi-instruction sequences
2013            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
2014            ArmOp::F32Ceil { sd, sm } => {
2015                return self.encode_arm_f32_rounding(sd, sm, 0b01); // Round toward +Inf
2016            }
2017            ArmOp::F32Floor { sd, sm } => {
2018                return self.encode_arm_f32_rounding(sd, sm, 0b10); // Round toward -Inf
2019            }
2020            ArmOp::F32Trunc { sd, sm } => {
2021                return self.encode_arm_f32_rounding(sd, sm, 0b11); // VCVT toward zero
2022            }
2023            ArmOp::F32Nearest { sd, sm } => {
2024                return self.encode_arm_f32_rounding(sd, sm, 0b00); // VCVT to nearest
2025            }
2026            ArmOp::F32Min { sd, sn, sm } => {
2027                return self.encode_arm_f32_minmax(sd, sn, sm, true);
2028            }
2029            ArmOp::F32Max { sd, sn, sm } => {
2030                return self.encode_arm_f32_minmax(sd, sn, sm, false);
2031            }
2032            ArmOp::F32Copysign { sd, sn, sm } => {
2033                return self.encode_arm_f32_copysign(sd, sn, sm);
2034            }
2035
2036            // f32 comparisons — multi-instruction: VCMP + VMRS + conditional MOV
2037            ArmOp::F32Eq { rd, sn, sm } => {
2038                return self.encode_arm_f32_compare(rd, sn, sm, 0x0); // EQ
2039            }
2040            ArmOp::F32Ne { rd, sn, sm } => {
2041                return self.encode_arm_f32_compare(rd, sn, sm, 0x1); // NE
2042            }
2043            ArmOp::F32Lt { rd, sn, sm } => {
2044                return self.encode_arm_f32_compare(rd, sn, sm, 0x4); // MI (less than)
2045            }
2046            ArmOp::F32Le { rd, sn, sm } => {
2047                return self.encode_arm_f32_compare(rd, sn, sm, 0x9); // LS (less or same)
2048            }
2049            ArmOp::F32Gt { rd, sn, sm } => {
2050                return self.encode_arm_f32_compare(rd, sn, sm, 0xC); // GT
2051            }
2052            ArmOp::F32Ge { rd, sn, sm } => {
2053                return self.encode_arm_f32_compare(rd, sn, sm, 0xA); // GE
2054            }
2055
2056            // f32 const — multi-instruction: MOVW + MOVT + VMOV
2057            ArmOp::F32Const { sd, value } => {
2058                return self.encode_arm_f32_const(sd, *value);
2059            }
2060
2061            ArmOp::F32Load { sd, addr } => encode_vfp_ldst(0xED900A00, sd, addr)?,
2062            ArmOp::F32Store { sd, addr } => encode_vfp_ldst(0xED800A00, sd, addr)?,
2063
2064            // f32 conversions — multi-instruction sequences
2065            ArmOp::F32ConvertI32S { sd, rm } => {
2066                return self.encode_arm_f32_convert_i32(sd, rm, true);
2067            }
2068            ArmOp::F32ConvertI32U { sd, rm } => {
2069                return self.encode_arm_f32_convert_i32(sd, rm, false);
2070            }
2071            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
2072                return Err(synth_core::Error::synthesis(
2073                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2074                ));
2075            }
2076            ArmOp::F32ReinterpretI32 { sd, rm } => encode_vmov_core_sreg(true, sd, rm)?,
2077            ArmOp::I32ReinterpretF32 { rd, sm } => encode_vmov_core_sreg(false, sm, rd)?,
2078            ArmOp::I32TruncF32S { rd, sm } => {
2079                return self.encode_arm_i32_trunc_f32(rd, sm, true);
2080            }
2081            ArmOp::I32TruncF32U { rd, sm } => {
2082                return self.encode_arm_i32_trunc_f32(rd, sm, false);
2083            }
2084
2085            // f64 VFP double-precision instructions (ARM32)
2086            // F64 arithmetic: same as F32 but with sz=1 (bit 8 = 1, cp11 = 0xB)
2087            ArmOp::F64Add { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B00, dd, dn, dm)?,
2088            ArmOp::F64Sub { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B40, dd, dn, dm)?,
2089            ArmOp::F64Mul { dd, dn, dm } => encode_vfp_3reg_f64(0xEE200B00, dd, dn, dm)?,
2090            ArmOp::F64Div { dd, dn, dm } => encode_vfp_3reg_f64(0xEE800B00, dd, dn, dm)?,
2091            ArmOp::F64Abs { dd, dm } => encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?,
2092            ArmOp::F64Neg { dd, dm } => encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?,
2093            ArmOp::F64Sqrt { dd, dm } => encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?,
2094
2095            // f64 pseudo-ops
2096            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
2097            ArmOp::F64Ceil { dd, dm } => {
2098                return self.encode_arm_f64_rounding(dd, dm, 0b01);
2099            }
2100            ArmOp::F64Floor { dd, dm } => {
2101                return self.encode_arm_f64_rounding(dd, dm, 0b10);
2102            }
2103            ArmOp::F64Trunc { dd, dm } => {
2104                return self.encode_arm_f64_rounding(dd, dm, 0b11);
2105            }
2106            ArmOp::F64Nearest { dd, dm } => {
2107                return self.encode_arm_f64_rounding(dd, dm, 0b00);
2108            }
2109            ArmOp::F64Min { dd, dn, dm } => {
2110                return self.encode_arm_f64_minmax(dd, dn, dm, true);
2111            }
2112            ArmOp::F64Max { dd, dn, dm } => {
2113                return self.encode_arm_f64_minmax(dd, dn, dm, false);
2114            }
2115            ArmOp::F64Copysign { dd, dn, dm } => {
2116                return self.encode_arm_f64_copysign(dd, dn, dm);
2117            }
2118
2119            // f64 comparisons
2120            ArmOp::F64Eq { rd, dn, dm } => {
2121                return self.encode_arm_f64_compare(rd, dn, dm, 0x0);
2122            }
2123            ArmOp::F64Ne { rd, dn, dm } => {
2124                return self.encode_arm_f64_compare(rd, dn, dm, 0x1);
2125            }
2126            ArmOp::F64Lt { rd, dn, dm } => {
2127                return self.encode_arm_f64_compare(rd, dn, dm, 0x4);
2128            }
2129            ArmOp::F64Le { rd, dn, dm } => {
2130                return self.encode_arm_f64_compare(rd, dn, dm, 0x9);
2131            }
2132            ArmOp::F64Gt { rd, dn, dm } => {
2133                return self.encode_arm_f64_compare(rd, dn, dm, 0xC);
2134            }
2135            ArmOp::F64Ge { rd, dn, dm } => {
2136                return self.encode_arm_f64_compare(rd, dn, dm, 0xA);
2137            }
2138
2139            ArmOp::F64Const { dd, value } => {
2140                return self.encode_arm_f64_const(dd, *value);
2141            }
2142
2143            ArmOp::F64Load { dd, addr } => encode_vfp_ldst_f64(0xED900B00, dd, addr)?,
2144            ArmOp::F64Store { dd, addr } => encode_vfp_ldst_f64(0xED800B00, dd, addr)?,
2145
2146            ArmOp::F64ConvertI32S { dd, rm } => {
2147                return self.encode_arm_f64_convert_i32(dd, rm, true);
2148            }
2149            ArmOp::F64ConvertI32U { dd, rm } => {
2150                return self.encode_arm_f64_convert_i32(dd, rm, false);
2151            }
2152            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
2153                return Err(synth_core::Error::synthesis(
2154                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2155                ));
2156            }
2157            ArmOp::F64PromoteF32 { dd, sm } => {
2158                return self.encode_arm_f64_promote_f32(dd, sm);
2159            }
2160            // GI-FPU-002 (#369): no synth A32 target carries an FPU (cortex-r5
2161            // has none — the selector declines every float op there), so the
2162            // A32 encoder refuses loudly instead of shipping an untested
2163            // encoding (#615: never a silent wrong byte).
2164            ArmOp::F32DemoteF64 { .. } => {
2165                return Err(synth_core::Error::synthesis(
2166                    "F32DemoteF64 has no A32 encoding (no A32 target has an FPU)",
2167                ));
2168            }
2169            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => {
2170                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?
2171            }
2172            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => {
2173                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?
2174            }
2175            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
2176                return Err(synth_core::Error::synthesis(
2177                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
2178                ));
2179            }
2180            ArmOp::I32TruncF64S { rd, dm } => {
2181                return self.encode_arm_i32_trunc_f64(rd, dm, true);
2182            }
2183            ArmOp::I32TruncF64U { rd, dm } => {
2184                return self.encode_arm_i32_trunc_f64(rd, dm, false);
2185            }
2186            // #615: multi-instruction i64 sequences — expanded to real A32 by
2187            // `encode_arm_expanded`, no longer "Thumb-2 only" NOPs.
2188            ArmOp::I64SetCond { .. }
2189            | ArmOp::I64SetCondZ { .. }
2190            | ArmOp::I64Mul { .. }
2191            | ArmOp::I64Shl { .. }
2192            | ArmOp::I64ShrS { .. }
2193            | ArmOp::I64ShrU { .. }
2194            | ArmOp::I64Rotl { .. }
2195            | ArmOp::I64Rotr { .. } => {
2196                unreachable!("handled by encode_arm_expanded (#615)")
2197            }
2198
2199            // MVE instructions — Thumb-2 only (Cortex-M55 is always Thumb-2)
2200            ArmOp::MveLoad { .. }
2201            | ArmOp::MveStore { .. }
2202            | ArmOp::MveConst { .. }
2203            | ArmOp::MveAnd { .. }
2204            | ArmOp::MveOrr { .. }
2205            | ArmOp::MveEor { .. }
2206            | ArmOp::MveMvn { .. }
2207            | ArmOp::MveBic { .. }
2208            | ArmOp::MveAddI { .. }
2209            | ArmOp::MveSubI { .. }
2210            | ArmOp::MveMulI { .. }
2211            | ArmOp::MveNegI { .. }
2212            | ArmOp::MveCmpEqI { .. }
2213            | ArmOp::MveCmpNeI { .. }
2214            | ArmOp::MveCmpLtS { .. }
2215            | ArmOp::MveCmpLtU { .. }
2216            | ArmOp::MveCmpGtS { .. }
2217            | ArmOp::MveCmpGtU { .. }
2218            | ArmOp::MveCmpLeS { .. }
2219            | ArmOp::MveCmpLeU { .. }
2220            | ArmOp::MveCmpGeS { .. }
2221            | ArmOp::MveCmpGeU { .. }
2222            | ArmOp::MveDup { .. }
2223            | ArmOp::MveExtractLane { .. }
2224            | ArmOp::MveInsertLane { .. }
2225            | ArmOp::MveAddF32 { .. }
2226            | ArmOp::MveSubF32 { .. }
2227            | ArmOp::MveMulF32 { .. }
2228            | ArmOp::MveNegF32 { .. }
2229            | ArmOp::MveAbsF32 { .. }
2230            | ArmOp::MveCmpEqF32 { .. }
2231            | ArmOp::MveCmpNeF32 { .. }
2232            | ArmOp::MveCmpLtF32 { .. }
2233            | ArmOp::MveCmpLeF32 { .. }
2234            | ArmOp::MveCmpGtF32 { .. }
2235            | ArmOp::MveCmpGeF32 { .. }
2236            | ArmOp::MveDupF32 { .. }
2237            | ArmOp::MveExtractLaneF32 { .. }
2238            | ArmOp::MveReplaceLaneF32 { .. }
2239            | ArmOp::MveDivF32 { .. }
2240            | ArmOp::MveSqrtF32 { .. } => {
2241                // MVE (Helium) is a Thumb-2-only extension (Cortex-M55); there
2242                // is no A32 encoding. The selector only emits MVE ops for
2243                // Thumb targets — a NOP here silently dropped the vector op
2244                // if that invariant ever broke (#615 class). Err keeps the
2245                // encoder total and the failure loud.
2246                return Err(synth_core::Error::synthesis(format!(
2247                    "MVE op {op:?} has no A32 (ARM-mode) encoding — MVE is Thumb-2 only (#615)"
2248                )));
2249            }
2250        };
2251
2252        // ARM32 instructions are little-endian
2253        Ok(instr.to_le_bytes().to_vec())
2254    }
2255
2256    // === ARM32 VFP multi-instruction helpers ===
2257
2258    /// Encode F32 comparison as ARM32: VCMP.F32 + VMRS + MOV rd,#0 + MOVcond rd,#1
2259    fn encode_arm_f32_compare(
2260        &self,
2261        rd: &Reg,
2262        sn: &VfpReg,
2263        sm: &VfpReg,
2264        cond_code: u32,
2265    ) -> Result<Vec<u8>> {
2266        let mut bytes = Vec::new();
2267
2268        // VCMP.F32 Sn, Sm: 0xEEB40A40 with Sn in Vd position, Sm in Vm position
2269        let sn_num = vfp_sreg_to_num(sn)?;
2270        let sm_num = vfp_sreg_to_num(sm)?;
2271        let (vd, d) = encode_sreg(sn_num);
2272        let (vm, m) = encode_sreg(sm_num);
2273        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2274        bytes.extend_from_slice(&vcmp.to_le_bytes());
2275
2276        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
2277        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2278
2279        // MOV rd, #0: 0xE3A0_0000 | (rd << 12)
2280        let rd_bits = reg_to_bits(rd);
2281        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2282        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2283
2284        // MOVcond rd, #1: cond(4) | 0011 1010 0000 rd(4) 0000 0000 0001
2285        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2286        bytes.extend_from_slice(&mov_one.to_le_bytes());
2287
2288        Ok(bytes)
2289    }
2290
2291    /// Encode F32 constant load as ARM32: MOVW Rt,#lo16 + MOVT Rt,#hi16 + VMOV Sd,Rt
2292    fn encode_arm_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
2293        let mut bytes = Vec::new();
2294        let bits = value.to_bits();
2295
2296        // Use R12 as temp register for constant loading
2297        let rt: u32 = 12; // R12/IP
2298
2299        // MOVW R12, #lo16: 0xE300_C000 | (imm4 << 16) | imm12
2300        let lo16 = bits & 0xFFFF;
2301        let movw = 0xE3000000 | (rt << 12) | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2302        bytes.extend_from_slice(&movw.to_le_bytes());
2303
2304        // MOVT R12, #hi16: 0xE340_C000 | (imm4 << 16) | imm12
2305        let hi16 = (bits >> 16) & 0xFFFF;
2306        let movt = 0xE3400000 | (rt << 12) | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2307        bytes.extend_from_slice(&movt.to_le_bytes());
2308
2309        // VMOV Sd, R12
2310        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
2311        bytes.extend_from_slice(&vmov.to_le_bytes());
2312
2313        Ok(bytes)
2314    }
2315
2316    /// Encode VMOV + VCVT.F32.S32/U32 as ARM32
2317    fn encode_arm_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2318        let mut bytes = Vec::new();
2319
2320        // VMOV Sd, Rm — move integer to VFP register
2321        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
2322        bytes.extend_from_slice(&vmov.to_le_bytes());
2323
2324        // VCVT.F32.S32 Sd, Sd (signed) or VCVT.F32.U32 Sd, Sd (unsigned).
2325        // The "op" bit (bit 7) selects signedness: 1 = signed (S32), 0 =
2326        // unsigned (U32). So signed = 0xEEB80AC0, unsigned = 0xEEB80A40 —
2327        // objdump confirms 0xEEB80A40 decodes to `vcvt.f32.u32` (GI-FPU-002:
2328        // the two were previously swapped, silently making `convert_i32_s`
2329        // an unsigned conversion).
2330        let sd_num = vfp_sreg_to_num(sd)?;
2331        let (vd, d) = encode_sreg(sd_num);
2332        let (vm, m) = encode_sreg(sd_num); // same register as source
2333        let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
2334        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2335        bytes.extend_from_slice(&vcvt.to_le_bytes());
2336
2337        Ok(bytes)
2338    }
2339
2340    /// Encode F32 rounding pseudo-op as ARM32 via VCVT to integer and back.
2341    /// mode: 0b00=nearest, 0b01=floor(-Inf), 0b10=ceil(+Inf), 0b11=trunc(zero)
2342    /// Strategy: VCVT.S32.F32 Sd, Sm (toward zero), then VCVT.F32.S32 Sd, Sd
2343    /// For ceil/floor/nearest, we use VCVTR (round toward mode) + convert back.
2344    /// Simplified: convert to int (toward zero for trunc) then back to float.
2345    /// Encode F32 rounding as ARM32.
2346    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2347    ///
2348    /// For trunc (mode=0b11): uses VCVTR.S32.F32 (always rounds toward zero).
2349    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant
2350    /// which honours FPSCR rmode), then restores FPSCR.
2351    fn encode_arm_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2352        let mut bytes = Vec::new();
2353        let sm_num = vfp_sreg_to_num(sm)?;
2354        let sd_num = vfp_sreg_to_num(sd)?;
2355        let (vd_s, d_s) = encode_sreg(sd_num);
2356        let (vm_s, m_s) = encode_sreg(sm_num);
2357
2358        if mode == 0b11 {
2359            // Trunc (toward zero): VCVTR.S32.F32 — the "R" variant always truncates.
2360            // 0xEEBD0AC0: bit[7]=1 => round toward zero regardless of FPSCR
2361            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2362            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2363        } else {
2364            // ceil/floor/nearest: manipulate FPSCR rounding mode
2365            let rt: u32 = 12; // R12/IP as temp
2366
2367            // VMRS R12, FPSCR
2368            let vmrs = 0xEEF10A10 | (rt << 12);
2369            bytes.extend_from_slice(&vmrs.to_le_bytes());
2370
2371            // BIC R12, R12, #(3 << 22) — clear RMode bits [23:22]
2372            // 3<<22 = 0x00C00000. ARM rotated imm: 0x03 ror 10 (rotation=5, imm8=0x03)
2373            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2374            bytes.extend_from_slice(&bic.to_le_bytes());
2375
2376            // ORR R12, R12, #(mode << 22) — set desired rounding mode
2377            if mode != 0 {
2378                // mode<<22: rotation=5, imm8=mode
2379                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2380                bytes.extend_from_slice(&orr.to_le_bytes());
2381            }
2382
2383            // VMSR FPSCR, R12
2384            let vmsr = 0xEEE10A10 | (rt << 12);
2385            bytes.extend_from_slice(&vmsr.to_le_bytes());
2386
2387            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rounding mode
2388            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2389            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2390
2391            // Restore FPSCR: clear rmode bits back to nearest (default)
2392            bytes.extend_from_slice(&vmrs.to_le_bytes());
2393            bytes.extend_from_slice(&bic.to_le_bytes());
2394            bytes.extend_from_slice(&vmsr.to_le_bytes());
2395        }
2396
2397        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
2398        let (vd2, d2) = encode_sreg(sd_num);
2399        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
2400        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2401
2402        Ok(bytes)
2403    }
2404
2405    /// Encode F32 min/max as ARM32: VCMP + VMRS + conditional VMOV
2406    fn encode_arm_f32_minmax(
2407        &self,
2408        sd: &VfpReg,
2409        sn: &VfpReg,
2410        sm: &VfpReg,
2411        is_min: bool,
2412    ) -> Result<Vec<u8>> {
2413        let mut bytes = Vec::new();
2414        let sn_num = vfp_sreg_to_num(sn)?;
2415        let sm_num = vfp_sreg_to_num(sm)?;
2416        let sd_num = vfp_sreg_to_num(sd)?;
2417
2418        // VMOV Sd, Sn (start with first operand)
2419        let (vd, d) = encode_sreg(sd_num);
2420        let (vn, n) = encode_sreg(sn_num);
2421        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2422        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2423
2424        // VCMP.F32 Sn, Sm
2425        let (vm, m) = encode_sreg(sm_num);
2426        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2427        bytes.extend_from_slice(&vcmp.to_le_bytes());
2428
2429        // VMRS APSR_nzcv, FPSCR
2430        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2431
2432        // For min: if Sn > Sm (GT), use Sm. Condition = GT (0xC)
2433        // For max: if Sn < Sm (MI/LT), use Sm. Condition = MI (0x4)
2434        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2435
2436        // VMOV{cond} Sd, Sm — conditional VMOV
2437        let vmov_cond = (cond << 28) | 0x0EB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2438        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2439
2440        Ok(bytes)
2441    }
2442
2443    /// Encode F32 copysign as ARM32: extract sign from Sm, magnitude from Sn
2444    fn encode_arm_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2445        let mut bytes = Vec::new();
2446
2447        // VMOV R12, Sm (get sign source bits)
2448        let vmov_sm = encode_vmov_core_sreg(false, sm, &Reg::R12)?;
2449        bytes.extend_from_slice(&vmov_sm.to_le_bytes());
2450
2451        // VMOV R0, Sn (get magnitude source bits) — use R0 as temp
2452        let vmov_sn = encode_vmov_core_sreg(false, sn, &Reg::R0)?;
2453        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2454
2455        // AND R12, R12, #0x80000000 (keep only sign bit)
2456        // Thumb-2 constant 0x80000000 needs special encoding; in ARM32 use rotated imm
2457        // 0x80000000 = 0x02 rotated right by 2 (rotation=1, imm8=0x02)
2458        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2459        bytes.extend_from_slice(&and_sign.to_le_bytes());
2460
2461        // BIC R0, R0, #0x80000000 (clear sign bit from magnitude)
2462        // R0 = register 0, so Rn and Rd fields are 0
2463        let bic_sign = 0xE3C00000u32 | (1 << 8) | 0x02;
2464        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2465
2466        // ORR R0, R0, R12 (combine sign + magnitude)
2467        // R0 = register 0, so Rn and Rd fields are 0
2468        let orr = 0xE1800000u32 | 12;
2469        bytes.extend_from_slice(&orr.to_le_bytes());
2470
2471        // VMOV Sd, R0
2472        let vmov_result = encode_vmov_core_sreg(true, sd, &Reg::R0)?;
2473        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2474
2475        Ok(bytes)
2476    }
2477
2478    /// Encode F64 comparison as ARM32: VCMP.F64 + VMRS + MOV rd,#0 + MOVcond rd,#1
2479    fn encode_arm_f64_compare(
2480        &self,
2481        rd: &Reg,
2482        dn: &VfpReg,
2483        dm: &VfpReg,
2484        cond_code: u32,
2485    ) -> Result<Vec<u8>> {
2486        let mut bytes = Vec::new();
2487
2488        // VCMP.F64 Dn, Dm: 0xEEB40B40 with Dn in Vd position, Dm in Vm position
2489        let dn_num = vfp_dreg_to_num(dn)?;
2490        let dm_num = vfp_dreg_to_num(dm)?;
2491        let (vd, d) = encode_dreg(dn_num);
2492        let (vm, m) = encode_dreg(dm_num);
2493        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2494        bytes.extend_from_slice(&vcmp.to_le_bytes());
2495
2496        // VMRS APSR_nzcv, FPSCR
2497        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2498
2499        // MOV rd, #0
2500        let rd_bits = reg_to_bits(rd);
2501        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2502        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2503
2504        // MOVcond rd, #1
2505        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2506        bytes.extend_from_slice(&mov_one.to_le_bytes());
2507
2508        Ok(bytes)
2509    }
2510
2511    /// Encode F64 constant load as ARM32: MOVW + MOVT + MOVW + MOVT + VMOV
2512    fn encode_arm_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
2513        let mut bytes = Vec::new();
2514        let bits = value.to_bits();
2515        let lo32 = bits as u32;
2516        let hi32 = (bits >> 32) as u32;
2517
2518        // Load low 32 bits into R0 (Rd field = 0 for R0)
2519        let lo16 = lo32 & 0xFFFF;
2520        let movw_r0 = 0xE3000000 | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2521        bytes.extend_from_slice(&movw_r0.to_le_bytes());
2522        let hi16 = (lo32 >> 16) & 0xFFFF;
2523        let movt_r0 = 0xE3400000 | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2524        bytes.extend_from_slice(&movt_r0.to_le_bytes());
2525
2526        // Load high 32 bits into R12
2527        let lo16 = hi32 & 0xFFFF;
2528        let movw_r12 = 0xE3000000 | ((lo16 >> 12) << 16) | (12 << 12) | (lo16 & 0xFFF);
2529        bytes.extend_from_slice(&movw_r12.to_le_bytes());
2530        let hi16 = (hi32 >> 16) & 0xFFFF;
2531        let movt_r12 = 0xE3400000 | ((hi16 >> 12) << 16) | (12 << 12) | (hi16 & 0xFFF);
2532        bytes.extend_from_slice(&movt_r12.to_le_bytes());
2533
2534        // VMOV Dd, R0, R12
2535        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
2536        bytes.extend_from_slice(&vmov.to_le_bytes());
2537
2538        Ok(bytes)
2539    }
2540
2541    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as ARM32
2542    fn encode_arm_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2543        let mut bytes = Vec::new();
2544
2545        // Use S0 as intermediate: VMOV S0, Rm
2546        let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
2547        bytes.extend_from_slice(&vmov.to_le_bytes());
2548
2549        // VCVT.F64.S32 Dd, S0 (signed) or VCVT.F64.U32 Dd, S0 (unsigned)
2550        // Base: 0xEEB80B40 (signed) or 0xEEB80BC0 (unsigned)
2551        let dd_num = vfp_dreg_to_num(dd)?;
2552        let (vd, d) = encode_dreg(dd_num);
2553        let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
2554        // S0 is register 0: Vm=0, M=0
2555        let vcvt = base | (d << 22) | (vd << 12);
2556        bytes.extend_from_slice(&vcvt.to_le_bytes());
2557
2558        Ok(bytes)
2559    }
2560
2561    /// Encode VCVT.F64.F32 Dd, Sm as ARM32 (f32 to f64 promotion)
2562    fn encode_arm_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2563        let dd_num = vfp_dreg_to_num(dd)?;
2564        let sm_num = vfp_sreg_to_num(sm)?;
2565        let (vd, d) = encode_dreg(dd_num);
2566        let (vm, m) = encode_sreg(sm_num);
2567
2568        // VCVT.F64.F32 Dd, Sm: 0xEEB70AC0
2569        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
2570        Ok(vcvt.to_le_bytes().to_vec())
2571    }
2572
2573    /// Encode VCVT.S32/U32.F64 Sd, Dm + VMOV Rd, Sd as ARM32
2574    fn encode_arm_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2575        let mut bytes = Vec::new();
2576        let dm_num = vfp_dreg_to_num(dm)?;
2577        let (vm, m) = encode_dreg(dm_num);
2578
2579        // VCVT.S32.F64 S0, Dm (toward zero) or VCVT.U32.F64 S0, Dm
2580        // S0: Vd=0, D=0
2581        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
2582        let vcvt = base | (m << 5) | vm;
2583        bytes.extend_from_slice(&vcvt.to_le_bytes());
2584
2585        // VMOV Rd, S0
2586        let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
2587        bytes.extend_from_slice(&vmov.to_le_bytes());
2588
2589        Ok(bytes)
2590    }
2591
2592    /// Encode F64 rounding pseudo-op as ARM32 via VCVT to integer and back.
2593    /// Encode F64 rounding as ARM32.
2594    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2595    ///
2596    /// For trunc: uses VCVTR.S32.F64 (always truncates).
2597    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F64 (non-R variant),
2598    /// then restores FPSCR.
2599    fn encode_arm_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2600        let mut bytes = Vec::new();
2601        let dm_num = vfp_dreg_to_num(dm)?;
2602        let dd_num = vfp_dreg_to_num(dd)?;
2603        let (vm, m) = encode_dreg(dm_num);
2604        let (vd, d) = encode_dreg(dd_num);
2605
2606        if mode == 0b11 {
2607            // Trunc (toward zero): VCVTR.S32.F64 — bit[7]=1, always truncates
2608            let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
2609            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2610        } else {
2611            // ceil/floor/nearest: manipulate FPSCR rounding mode
2612            let rt: u32 = 12;
2613
2614            // VMRS R12, FPSCR
2615            let vmrs = 0xEEF10A10 | (rt << 12);
2616            bytes.extend_from_slice(&vmrs.to_le_bytes());
2617
2618            // BIC R12, R12, #(3 << 22)
2619            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2620            bytes.extend_from_slice(&bic.to_le_bytes());
2621
2622            // ORR R12, R12, #(mode << 22)
2623            if mode != 0 {
2624                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2625                bytes.extend_from_slice(&orr.to_le_bytes());
2626            }
2627
2628            // VMSR FPSCR, R12
2629            let vmsr = 0xEEE10A10 | (rt << 12);
2630            bytes.extend_from_slice(&vmsr.to_le_bytes());
2631
2632            // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0), uses FPSCR rmode
2633            let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
2634            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2635
2636            // Restore FPSCR
2637            bytes.extend_from_slice(&vmrs.to_le_bytes());
2638            bytes.extend_from_slice(&bic.to_le_bytes());
2639            bytes.extend_from_slice(&vmsr.to_le_bytes());
2640        }
2641
2642        // VCVT.F64.S32 Dd, S0 (convert back to double)
2643        let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
2644        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2645
2646        Ok(bytes)
2647    }
2648
2649    /// Encode F64 min/max as ARM32: VMOV + VCMP + VMRS + conditional VMOV
2650    fn encode_arm_f64_minmax(
2651        &self,
2652        dd: &VfpReg,
2653        dn: &VfpReg,
2654        dm: &VfpReg,
2655        is_min: bool,
2656    ) -> Result<Vec<u8>> {
2657        let mut bytes = Vec::new();
2658        let dn_num = vfp_dreg_to_num(dn)?;
2659        let dm_num = vfp_dreg_to_num(dm)?;
2660        let dd_num = vfp_dreg_to_num(dd)?;
2661
2662        // VMOV.F64 Dd, Dn (start with first operand)
2663        let (vd, d) = encode_dreg(dd_num);
2664        let (vn, n) = encode_dreg(dn_num);
2665        let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2666        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2667
2668        // VCMP.F64 Dn, Dm
2669        let (vm, m) = encode_dreg(dm_num);
2670        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2671        bytes.extend_from_slice(&vcmp.to_le_bytes());
2672
2673        // VMRS APSR_nzcv, FPSCR
2674        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2675
2676        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2677        let vmov_cond = (cond << 28) | 0x0EB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2678        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2679
2680        Ok(bytes)
2681    }
2682
2683    /// Encode F64 copysign as ARM32
2684    fn encode_arm_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
2685        let mut bytes = Vec::new();
2686
2687        // VMOV R0, R12, Dm (get sign source bits)
2688        let vmov_dm = encode_vmov_core_dreg(false, dm, &Reg::R0, &Reg::R12)?;
2689        bytes.extend_from_slice(&vmov_dm.to_le_bytes());
2690
2691        // VMOV R1, R2, Dn (get magnitude source bits)
2692        // We use R1 (lo) and R2 (hi) for the magnitude
2693        let vmov_dn = encode_vmov_core_dreg(false, dn, &Reg::R1, &Reg::R2)?;
2694        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2695
2696        // AND R12, R12, #0x80000000 (keep only sign bit from hi word)
2697        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2698        bytes.extend_from_slice(&and_sign.to_le_bytes());
2699
2700        // BIC R2, R2, #0x80000000 (clear sign bit from magnitude hi word)
2701        let bic_sign = 0xE3C00000u32 | (2 << 16) | (2 << 12) | (1 << 8) | 0x02;
2702        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2703
2704        // ORR R2, R2, R12 (combine sign + magnitude)
2705        let orr = 0xE1800000u32 | (2 << 16) | (2 << 12) | 12;
2706        bytes.extend_from_slice(&orr.to_le_bytes());
2707
2708        // VMOV Dd, R1, R2
2709        let vmov_result = encode_vmov_core_dreg(true, dd, &Reg::R1, &Reg::R2)?;
2710        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2711
2712        Ok(bytes)
2713    }
2714
2715    /// Encode VCVT.S32/U32.F32 + VMOV as ARM32
2716    fn encode_arm_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2717        let mut bytes = Vec::new();
2718
2719        // VCVT.S32.F32 Sd, Sm (toward zero) or VCVT.U32.F32 Sd, Sm
2720        // We use Sm as both source and destination for the intermediate result
2721        let sm_num = vfp_sreg_to_num(sm)?;
2722        let (vd, d) = encode_sreg(sm_num);
2723        let (vm, m) = encode_sreg(sm_num);
2724        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
2725        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2726        bytes.extend_from_slice(&vcvt.to_le_bytes());
2727
2728        // VMOV Rd, Sm — move result back to core register
2729        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
2730        bytes.extend_from_slice(&vmov.to_le_bytes());
2731
2732        Ok(bytes)
2733    }
2734
2735    /// Encode an ARM instruction in Thumb-2 mode (16-bit or 32-bit instructions)
2736    fn encode_thumb(&self, op: &ArmOp) -> Result<Vec<u8>> {
2737        // Thumb-2 supports both 16-bit and 32-bit instructions
2738        // 32-bit instructions are encoded as two 16-bit halfwords (big-endian order)
2739        match op {
2740            // === 16-bit Thumb encodings ===
2741            ArmOp::Add { rd, rn, op2 } => {
2742                let rd_bits = reg_to_bits(rd) as u16;
2743                let rn_bits = reg_to_bits(rn) as u16;
2744
2745                if let Operand2::Reg(rm) = op2 {
2746                    let rm_bits = reg_to_bits(rm) as u16;
2747                    // 16-bit ADDS only has 3-bit register fields (R0-R7). For
2748                    // high registers (e.g. R12, the MemLoad/MemStore base
2749                    // scratch) the bits overflow into adjacent fields, silently
2750                    // corrupting the operands — issue #178/#180: `add ip,ip,r0`
2751                    // was emitted as `adds r4,r5,r1`. Guard on all three regs
2752                    // being low and fall back to 32-bit ADD.W otherwise, exactly
2753                    // as the Sub handler below does.
2754                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2755                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2756                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2757                        Ok(instr.to_le_bytes().to_vec())
2758                    } else {
2759                        // ADD.W Rd, Rn, Rm (32-bit) for high registers
2760                        self.encode_thumb32_add_reg_raw(
2761                            rd_bits as u32,
2762                            rn_bits as u32,
2763                            rm_bits as u32,
2764                        )
2765                    }
2766                } else if let Operand2::Imm(imm) = op2 {
2767                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2768                        // ADDS Rd, Rn, #imm3 (16-bit): 0001 110 imm3 Rn Rd
2769                        let instr: u16 = 0x1C00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2770                        Ok(instr.to_le_bytes().to_vec())
2771                    } else {
2772                        // Use 32-bit ADD for larger immediates
2773                        self.encode_thumb32_add(rd, rn, *imm as u32)
2774                    }
2775                } else {
2776                    // Fallback to 32-bit encoding
2777                    self.encode_thumb32_add(rd, rn, 0)
2778                }
2779            }
2780
2781            ArmOp::Sub { rd, rn, op2 } => {
2782                let rd_bits = reg_to_bits(rd) as u16;
2783                let rn_bits = reg_to_bits(rn) as u16;
2784
2785                if let Operand2::Reg(rm) = op2 {
2786                    let rm_bits = reg_to_bits(rm) as u16;
2787                    // 16-bit SUBS can only use low registers (R0-R7)
2788                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2789                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2790                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2791                        Ok(instr.to_le_bytes().to_vec())
2792                    } else {
2793                        // Use 32-bit SUB.W for high registers
2794                        self.encode_thumb32_sub_reg_raw(
2795                            rd_bits as u32,
2796                            rn_bits as u32,
2797                            rm_bits as u32,
2798                        )
2799                    }
2800                } else if let Operand2::Imm(imm) = op2 {
2801                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2802                        // SUBS Rd, Rn, #imm3 (16-bit): 0001 111 imm3 Rn Rd
2803                        let instr: u16 = 0x1E00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2804                        Ok(instr.to_le_bytes().to_vec())
2805                    } else {
2806                        self.encode_thumb32_sub(rd, rn, *imm as u32)
2807                    }
2808                } else {
2809                    self.encode_thumb32_sub(rd, rn, 0)
2810                }
2811            }
2812
2813            ArmOp::Mov { rd, op2 } => {
2814                let rd_bits = reg_to_bits(rd) as u16;
2815
2816                if let Operand2::Imm(imm) = op2 {
2817                    // #498: the old test here was the SIGNED `*imm <= 255`,
2818                    // so a negative immediate (e.g. -1) fell into the 16-bit
2819                    // MOVS arm and encoded the wrong VALUE (#(imm & 0xFF) =
2820                    // #0xFF). A positive imm above 0xFFFF was equally wrong:
2821                    // MOVW truncates to 16 bits. Split on the UNSIGNED value:
2822                    // imm8 → MOVS, imm16 → MOVW, anything wider (negative or
2823                    // >0xFFFF) → the full-value MOVW+MOVT pair. No emitter
2824                    // produces the wide shape today (both selectors
2825                    // materialize wide constants as explicit Movw/Movt or
2826                    // Movw+Mvn), so this is byte-identical on shipped paths —
2827                    // it retires the latent wrong-value encodings the
2828                    // `estimator_encoder_agreement` oracle had pinned.
2829                    let uimm = *imm as u32;
2830                    if uimm <= 255 && rd_bits < 8 {
2831                        // MOVS Rd, #imm8 (16-bit): 0010 0 Rd imm8
2832                        let imm_bits = (*imm as u16) & 0xFF;
2833                        let instr: u16 = 0x2000 | (rd_bits << 8) | imm_bits;
2834                        Ok(instr.to_le_bytes().to_vec())
2835                    } else if uimm <= 0xFFFF {
2836                        // Use 32-bit MOVW for 16-bit immediates
2837                        self.encode_thumb32_movw(rd, uimm)
2838                    } else {
2839                        // Full 32-bit value: MOVW low16 + MOVT high16
2840                        let mut bytes = self.encode_thumb32_movw(rd, uimm & 0xFFFF)?;
2841                        bytes.extend(self.encode_thumb32_movt_raw(reg_to_bits(rd), uimm >> 16)?);
2842                        Ok(bytes)
2843                    }
2844                } else if let Operand2::Reg(rm) = op2 {
2845                    let rm_bits = reg_to_bits(rm) as u16;
2846                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
2847                    // D = Rd[3], Rd[2:0] in lower bits
2848                    let d_bit = (rd_bits >> 3) & 1;
2849                    let instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
2850                    Ok(instr.to_le_bytes().to_vec())
2851                } else {
2852                    let instr: u16 = 0xBF00; // NOP fallback
2853                    Ok(instr.to_le_bytes().to_vec())
2854                }
2855            }
2856
2857            ArmOp::Push { regs } => {
2858                // Thumb-2 PUSH encoding:
2859                // If all regs in R0-R7 + LR, use 16-bit: 1011 010 M rrrrrrrr
2860                // Otherwise use 32-bit: STMDB SP!, {regs} = 1110 1001 0010 1101 | 0M0 reglist(13)
2861                let mut reg_list: u16 = 0;
2862                let mut need_32bit = false;
2863                for r in regs {
2864                    let bit = reg_to_bits(r);
2865                    if bit >= 8 && *r != Reg::LR {
2866                        need_32bit = true;
2867                    }
2868                    reg_list |= 1 << bit;
2869                }
2870                if !need_32bit {
2871                    // 16-bit PUSH: 1011 010 M rrrrrrrr
2872                    let m_bit = if reg_list & (1 << 14) != 0 {
2873                        1u16
2874                    } else {
2875                        0u16
2876                    };
2877                    let low_regs = reg_list & 0xFF;
2878                    let instr: u16 = 0xB400 | (m_bit << 8) | low_regs;
2879                    Ok(instr.to_le_bytes().to_vec())
2880                } else {
2881                    // 32-bit STMDB SP!, {regs}: E92D | reglist(16)
2882                    let hw1: u16 = 0xE92D;
2883                    let hw2: u16 = reg_list;
2884                    let mut bytes = hw1.to_le_bytes().to_vec();
2885                    bytes.extend_from_slice(&hw2.to_le_bytes());
2886                    Ok(bytes)
2887                }
2888            }
2889
2890            ArmOp::Pop { regs } => {
2891                // Thumb-2 POP encoding:
2892                // If all regs in R0-R7 + PC, use 16-bit: 1011 110 P rrrrrrrr
2893                // Otherwise use 32-bit: LDMIA SP!, {regs} = 1110 1000 1011 1101 | PM0 reglist(13)
2894                let mut reg_list: u16 = 0;
2895                let mut need_32bit = false;
2896                for r in regs {
2897                    let bit = reg_to_bits(r);
2898                    if bit >= 8 && *r != Reg::PC {
2899                        need_32bit = true;
2900                    }
2901                    reg_list |= 1 << bit;
2902                }
2903                if !need_32bit {
2904                    // 16-bit POP: 1011 110 P rrrrrrrr
2905                    let p_bit = if reg_list & (1 << 15) != 0 {
2906                        1u16
2907                    } else {
2908                        0u16
2909                    };
2910                    let low_regs = reg_list & 0xFF;
2911                    let instr: u16 = 0xBC00 | (p_bit << 8) | low_regs;
2912                    Ok(instr.to_le_bytes().to_vec())
2913                } else {
2914                    // 32-bit LDMIA SP!, {regs}: E8BD | reglist(16)
2915                    let hw1: u16 = 0xE8BD;
2916                    let hw2: u16 = reg_list;
2917                    let mut bytes = hw1.to_le_bytes().to_vec();
2918                    bytes.extend_from_slice(&hw2.to_le_bytes());
2919                    Ok(bytes)
2920                }
2921            }
2922
2923            ArmOp::Nop => {
2924                let instr: u16 = 0xBF00; // NOP in Thumb-2
2925                Ok(instr.to_le_bytes().to_vec())
2926            }
2927
2928            ArmOp::Udf { imm } => {
2929                // UDF (Undefined) in Thumb-2: 16-bit encoding is 0xDE00 | imm8
2930                // This triggers UsageFault/HardFault, used for WASM traps
2931                let instr: u16 = 0xDE00 | (*imm as u16);
2932                let bytes = instr.to_le_bytes().to_vec();
2933                encoding_contracts::verify_thumb16(&bytes);
2934                Ok(bytes)
2935            }
2936
2937            // i64 support: ADDS, ADC, SUBS, SBC for register pair arithmetic
2938            // ADDS sets flags (carry), ADC uses carry from previous ADDS
2939            ArmOp::Adds { rd, rn, op2 } => {
2940                let rd_bits = reg_to_bits(rd) as u16;
2941                let rn_bits = reg_to_bits(rn) as u16;
2942
2943                if let Operand2::Reg(rm) = op2 {
2944                    let rm_bits = reg_to_bits(rm) as u16;
2945                    // 16-bit ADDS is R0-R7 only; i64 pair allocation can place
2946                    // operands in R8-R11, which would overflow the 3-bit fields
2947                    // and corrupt the operands (#178/#180 class). Guard and fall
2948                    // back to 32-bit ADDS.W for high registers.
2949                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2950                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2951                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2952                        Ok(instr.to_le_bytes().to_vec())
2953                    } else {
2954                        self.encode_thumb32_adds_reg_raw(
2955                            rd_bits as u32,
2956                            rn_bits as u32,
2957                            rm_bits as u32,
2958                        )
2959                    }
2960                } else {
2961                    // 32-bit Thumb-2 ADDS with immediate
2962                    self.encode_thumb32_adds(rd, rn, 0)
2963                }
2964            }
2965
2966            // ADC: Add with Carry (Thumb-2 32-bit)
2967            // ADC.W Rd, Rn, Rm: EB40 Rn | 00 Rd 00 Rm
2968            ArmOp::Adc { rd, rn, op2 } => {
2969                let rd_bits = reg_to_bits(rd);
2970                let rn_bits = reg_to_bits(rn);
2971
2972                if let Operand2::Reg(rm) = op2 {
2973                    let rm_bits = reg_to_bits(rm);
2974                    // ADC.W Rd, Rn, Rm (T2): 1110 1011 0100 Rn | 0 000 Rd 00 00 Rm
2975                    let hw1: u16 = (0xEB40 | rn_bits) as u16;
2976                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2977
2978                    let mut bytes = hw1.to_le_bytes().to_vec();
2979                    bytes.extend_from_slice(&hw2.to_le_bytes());
2980                    Ok(bytes)
2981                } else {
2982                    // ADC with immediate - use 32-bit encoding
2983                    let hw1: u16 = (0xF140 | rn_bits) as u16;
2984                    let hw2: u16 = (rd_bits << 8) as u16;
2985                    let mut bytes = hw1.to_le_bytes().to_vec();
2986                    bytes.extend_from_slice(&hw2.to_le_bytes());
2987                    Ok(bytes)
2988                }
2989            }
2990
2991            // SUBS sets flags (borrow), SBC uses borrow from previous SUBS
2992            ArmOp::Subs { rd, rn, op2 } => {
2993                let rd_bits = reg_to_bits(rd) as u16;
2994                let rn_bits = reg_to_bits(rn) as u16;
2995
2996                if let Operand2::Reg(rm) = op2 {
2997                    let rm_bits = reg_to_bits(rm) as u16;
2998                    // 16-bit SUBS is R0-R7 only; high-register i64 pair operands
2999                    // would overflow the 3-bit fields (#178/#180 class). Guard
3000                    // and fall back to 32-bit SUBS.W for high registers.
3001                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
3002                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
3003                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
3004                        Ok(instr.to_le_bytes().to_vec())
3005                    } else {
3006                        self.encode_thumb32_subs_reg_raw(
3007                            rd_bits as u32,
3008                            rn_bits as u32,
3009                            rm_bits as u32,
3010                        )
3011                    }
3012                } else {
3013                    // 32-bit Thumb-2 SUBS with immediate
3014                    self.encode_thumb32_subs(rd, rn, 0)
3015                }
3016            }
3017
3018            // SBC: Subtract with Carry (Thumb-2 32-bit)
3019            // SBC.W Rd, Rn, Rm: EB60 Rn | 00 Rd 00 Rm
3020            ArmOp::Sbc { rd, rn, op2 } => {
3021                let rd_bits = reg_to_bits(rd);
3022                let rn_bits = reg_to_bits(rn);
3023
3024                if let Operand2::Reg(rm) = op2 {
3025                    let rm_bits = reg_to_bits(rm);
3026                    // SBC.W Rd, Rn, Rm (T2): 1110 1011 0110 Rn | 0 000 Rd 00 00 Rm
3027                    let hw1: u16 = (0xEB60 | rn_bits) as u16;
3028                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3029
3030                    let mut bytes = hw1.to_le_bytes().to_vec();
3031                    bytes.extend_from_slice(&hw2.to_le_bytes());
3032                    Ok(bytes)
3033                } else {
3034                    // SBC with immediate - use 32-bit encoding
3035                    let hw1: u16 = (0xF160 | rn_bits) as u16;
3036                    let hw2: u16 = (rd_bits << 8) as u16;
3037                    let mut bytes = hw1.to_le_bytes().to_vec();
3038                    bytes.extend_from_slice(&hw2.to_le_bytes());
3039                    Ok(bytes)
3040                }
3041            }
3042
3043            // === 32-bit Thumb-2 encodings ===
3044
3045            // SDIV: 11111011 1001 Rn 1111 Rd 1111 Rm
3046            ArmOp::Sdiv { rd, rn, rm } => {
3047                let rd_bits = reg_to_bits(rd);
3048                let rn_bits = reg_to_bits(rn);
3049                let rm_bits = reg_to_bits(rm);
3050                reg_bits_checked(rd_bits)?;
3051                reg_bits_checked(rn_bits)?;
3052                reg_bits_checked(rm_bits)?;
3053
3054                // Thumb-2 SDIV: FB90 F0F0 | Rn<<16 | Rd<<8 | Rm
3055                // First halfword: 1111 1011 1001 Rn = 0xFB90 | Rn
3056                // Second halfword: 1111 Rd 1111 Rm = 0xF0F0 | Rd<<8 | Rm
3057                let hw1: u16 = (0xFB90 | rn_bits) as u16;
3058                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
3059
3060                // Thumb-2 32-bit instructions: first halfword, then second halfword (little-endian each)
3061                let mut bytes = hw1.to_le_bytes().to_vec();
3062                bytes.extend_from_slice(&hw2.to_le_bytes());
3063                encoding_contracts::verify_thumb32(&bytes);
3064                Ok(bytes)
3065            }
3066
3067            // UDIV: 11111011 1011 Rn 1111 Rd 1111 Rm
3068            ArmOp::Udiv { rd, rn, rm } => {
3069                let rd_bits = reg_to_bits(rd);
3070                let rn_bits = reg_to_bits(rn);
3071                let rm_bits = reg_to_bits(rm);
3072                reg_bits_checked(rd_bits)?;
3073                reg_bits_checked(rn_bits)?;
3074                reg_bits_checked(rm_bits)?;
3075
3076                // Thumb-2 UDIV: FBB0 F0F0 | Rn<<16 | Rd<<8 | Rm
3077                let hw1: u16 = (0xFBB0 | rn_bits) as u16;
3078                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
3079
3080                let mut bytes = hw1.to_le_bytes().to_vec();
3081                bytes.extend_from_slice(&hw2.to_le_bytes());
3082                encoding_contracts::verify_thumb32(&bytes);
3083                Ok(bytes)
3084            }
3085
3086            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
3087                let rdlo_bits = reg_to_bits(rdlo);
3088                let rdhi_bits = reg_to_bits(rdhi);
3089                let rn_bits = reg_to_bits(rn);
3090                let rm_bits = reg_to_bits(rm);
3091                reg_bits_checked(rdlo_bits)?;
3092                reg_bits_checked(rdhi_bits)?;
3093                reg_bits_checked(rn_bits)?;
3094                reg_bits_checked(rm_bits)?;
3095
3096                // Thumb-2 UMULL: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm
3097                let hw1: u16 = (0xFBA0 | rn_bits) as u16;
3098                let hw2: u16 = ((rdlo_bits << 12) | (rdhi_bits << 8) | rm_bits) as u16;
3099
3100                let mut bytes = hw1.to_le_bytes().to_vec();
3101                bytes.extend_from_slice(&hw2.to_le_bytes());
3102                encoding_contracts::verify_thumb32(&bytes);
3103                Ok(bytes)
3104            }
3105
3106            // MUL (Thumb-2 32-bit): MUL Rd, Rn, Rm
3107            ArmOp::Mul { rd, rn, rm } => {
3108                let rd_bits = reg_to_bits(rd);
3109                let rn_bits = reg_to_bits(rn);
3110                let rm_bits = reg_to_bits(rm);
3111
3112                // Thumb-2 MUL: FB00 F000 | Rn | Rd<<8 | Rm
3113                // 11111011 0000 Rn | 1111 Rd 0000 Rm
3114                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3115                let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
3116
3117                let mut bytes = hw1.to_le_bytes().to_vec();
3118                bytes.extend_from_slice(&hw2.to_le_bytes());
3119                Ok(bytes)
3120            }
3121
3122            // MLS: Rd = Ra - Rn * Rm
3123            ArmOp::Mls { rd, rn, rm, ra } => {
3124                let rd_bits = reg_to_bits(rd);
3125                let rn_bits = reg_to_bits(rn);
3126                let rm_bits = reg_to_bits(rm);
3127                let ra_bits = reg_to_bits(ra);
3128
3129                // Thumb-2 MLS: FB00 Rn | Ra Rd 0001 Rm
3130                // 11111011 0000 Rn | Ra Rd 0001 Rm
3131                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3132                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | 0x10 | rm_bits) as u16;
3133
3134                let mut bytes = hw1.to_le_bytes().to_vec();
3135                bytes.extend_from_slice(&hw2.to_le_bytes());
3136                Ok(bytes)
3137            }
3138
3139            ArmOp::Mla { rd, rn, rm, ra } => {
3140                let rd_bits = reg_to_bits(rd);
3141                let rn_bits = reg_to_bits(rn);
3142                let rm_bits = reg_to_bits(rm);
3143                let ra_bits = reg_to_bits(ra);
3144
3145                // Thumb-2 MLA: FB00 Rn | Ra Rd 0000 Rm — same as MLS without the
3146                // bit-4 (0x10) op flag. rd = ra + rn*rm.
3147                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3148                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | rm_bits) as u16;
3149
3150                let mut bytes = hw1.to_le_bytes().to_vec();
3151                bytes.extend_from_slice(&hw2.to_le_bytes());
3152                Ok(bytes)
3153            }
3154
3155            // AND (Thumb-2 32-bit)
3156            ArmOp::And { rd, rn, op2 } => {
3157                if let Operand2::Reg(rm) = op2 {
3158                    let rd_bits = reg_to_bits(rd);
3159                    let rn_bits = reg_to_bits(rn);
3160                    let rm_bits = reg_to_bits(rm);
3161
3162                    // Thumb-2 AND register: EA00 Rn | 0 Rd 00 00 Rm
3163                    let hw1: u16 = (0xEA00 | rn_bits) as u16;
3164                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3165
3166                    let mut bytes = hw1.to_le_bytes().to_vec();
3167                    bytes.extend_from_slice(&hw2.to_le_bytes());
3168                    Ok(bytes)
3169                } else if let Operand2::Imm(imm) = op2 {
3170                    let rd_bits = reg_to_bits(rd);
3171                    let rn_bits = reg_to_bits(rn);
3172
3173                    // Thumb-2 AND.W immediate T1: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8.
3174                    // The i:imm3:imm8 field is a ThumbExpandImm modified immediate —
3175                    // encode it correctly (or error on an un-encodable value)
3176                    // rather than packing raw bits, closing the silent-miscompile
3177                    // class for AND alongside ORR/EOR (#251) / ADD/SUB (#253) /
3178                    // CMP (#255).
3179                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3180                        synth_core::Error::synthesis(
3181                            "AND immediate is not a valid ThumbExpandImm — materialize into a register",
3182                        )
3183                    })?;
3184                    let i_bit = (field >> 11) & 1;
3185                    let imm3 = (field >> 8) & 0x7;
3186                    let imm8 = field & 0xFF;
3187
3188                    let hw1: u16 = (0xF000 | (i_bit << 10) | rn_bits) as u16;
3189                    let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3190
3191                    let mut bytes = hw1.to_le_bytes().to_vec();
3192                    bytes.extend_from_slice(&hw2.to_le_bytes());
3193                    Ok(bytes)
3194                } else {
3195                    // RegShift variant - fallback to NOP
3196                    let instr: u16 = 0xBF00;
3197                    Ok(instr.to_le_bytes().to_vec())
3198                }
3199            }
3200
3201            // ORR (Thumb-2 32-bit)
3202            ArmOp::Orr { rd, rn, op2 } => {
3203                if let Operand2::Reg(rm) = op2 {
3204                    let rd_bits = reg_to_bits(rd);
3205                    let rn_bits = reg_to_bits(rn);
3206                    let rm_bits = reg_to_bits(rm);
3207
3208                    // Thumb-2 ORR: EA40 Rn | 0 Rd 00 00 Rm
3209                    let hw1: u16 = (0xEA40 | rn_bits) as u16;
3210                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3211
3212                    let mut bytes = hw1.to_le_bytes().to_vec();
3213                    bytes.extend_from_slice(&hw2.to_le_bytes());
3214                    Ok(bytes)
3215                } else if let Operand2::Imm(imm) = op2 {
3216                    // ORR.W immediate T1: 11110 i 0 0010 S Rn | 0 imm3 Rd imm8.
3217                    // Only the zero-extended byte form (imm <= 0xFF) is encoded;
3218                    // larger modified immediates need ThumbExpandImm — return an
3219                    // error rather than silently emit a NOP (Ok-or-Err, #180/#185).
3220                    let imm_val = *imm as u32;
3221                    if imm_val > 0xFF {
3222                        return Err(synth_core::Error::synthesis(
3223                            "ORR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3224                        ));
3225                    }
3226                    let rd_bits = reg_to_bits(rd);
3227                    let rn_bits = reg_to_bits(rn);
3228                    let hw1: u16 = (0xF040 | rn_bits) as u16;
3229                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3230                    let mut bytes = hw1.to_le_bytes().to_vec();
3231                    bytes.extend_from_slice(&hw2.to_le_bytes());
3232                    Ok(bytes)
3233                } else {
3234                    let instr: u16 = 0xBF00;
3235                    Ok(instr.to_le_bytes().to_vec())
3236                }
3237            }
3238
3239            // EOR (Thumb-2 32-bit)
3240            ArmOp::Eor { rd, rn, op2 } => {
3241                if let Operand2::Reg(rm) = op2 {
3242                    let rd_bits = reg_to_bits(rd);
3243                    let rn_bits = reg_to_bits(rn);
3244                    let rm_bits = reg_to_bits(rm);
3245
3246                    // Thumb-2 EOR: EA80 Rn | 0 Rd 00 00 Rm
3247                    let hw1: u16 = (0xEA80 | rn_bits) as u16;
3248                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3249
3250                    let mut bytes = hw1.to_le_bytes().to_vec();
3251                    bytes.extend_from_slice(&hw2.to_le_bytes());
3252                    Ok(bytes)
3253                } else if let Operand2::Imm(imm) = op2 {
3254                    // EOR.W immediate T1: 11110 i 0 0100 S Rn | 0 imm3 Rd imm8.
3255                    // Byte form only (imm <= 0xFF); larger needs ThumbExpandImm —
3256                    // error, not a silent NOP (Ok-or-Err, #180/#185).
3257                    let imm_val = *imm as u32;
3258                    if imm_val > 0xFF {
3259                        return Err(synth_core::Error::synthesis(
3260                            "EOR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3261                        ));
3262                    }
3263                    let rd_bits = reg_to_bits(rd);
3264                    let rn_bits = reg_to_bits(rn);
3265                    let hw1: u16 = (0xF080 | rn_bits) as u16;
3266                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3267                    let mut bytes = hw1.to_le_bytes().to_vec();
3268                    bytes.extend_from_slice(&hw2.to_le_bytes());
3269                    Ok(bytes)
3270                } else {
3271                    let instr: u16 = 0xBF00;
3272                    Ok(instr.to_le_bytes().to_vec())
3273                }
3274            }
3275
3276            // Shift operations (16-bit for low registers)
3277            ArmOp::Lsl { rd, rn, shift } => {
3278                let rd_bits = reg_to_bits(rd) as u16;
3279                let rn_bits = reg_to_bits(rn) as u16;
3280                let shift_bits = (*shift as u16) & 0x1F;
3281
3282                if rd_bits < 8 && rn_bits < 8 {
3283                    // LSLS Rd, Rm, #imm5 (16-bit): 0000 0 imm5 Rm Rd
3284                    let instr: u16 = (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3285                    Ok(instr.to_le_bytes().to_vec())
3286                } else {
3287                    // Use 32-bit encoding for high registers
3288                    self.encode_thumb32_shift(rd, rn, *shift, 0b00) // LSL type
3289                }
3290            }
3291
3292            ArmOp::Lsr { rd, rn, shift } => {
3293                let rd_bits = reg_to_bits(rd) as u16;
3294                let rn_bits = reg_to_bits(rn) as u16;
3295                let shift_bits = (*shift as u16) & 0x1F;
3296
3297                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3298                    // LSRS Rd, Rm, #imm5 (16-bit): 0000 1 imm5 Rm Rd
3299                    let instr: u16 = 0x0800 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3300                    Ok(instr.to_le_bytes().to_vec())
3301                } else {
3302                    self.encode_thumb32_shift(rd, rn, *shift, 0b01) // LSR type
3303                }
3304            }
3305
3306            ArmOp::Asr { rd, rn, shift } => {
3307                let rd_bits = reg_to_bits(rd) as u16;
3308                let rn_bits = reg_to_bits(rn) as u16;
3309                let shift_bits = (*shift as u16) & 0x1F;
3310
3311                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3312                    // ASRS Rd, Rm, #imm5 (16-bit): 0001 0 imm5 Rm Rd
3313                    let instr: u16 = 0x1000 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3314                    Ok(instr.to_le_bytes().to_vec())
3315                } else {
3316                    self.encode_thumb32_shift(rd, rn, *shift, 0b10) // ASR type
3317                }
3318            }
3319
3320            ArmOp::Ror { rd, rn, shift } => {
3321                // ROR doesn't have a 16-bit immediate form, use 32-bit
3322                self.encode_thumb32_shift(rd, rn, *shift, 0b11) // ROR type
3323            }
3324
3325            // Register-based shifts (Thumb-2 32-bit)
3326            // Encoding: 11111010 0xxS Rn 1111 Rd 0000 Rm
3327            // xx = shift type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
3328            ArmOp::LslReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b00),
3329            ArmOp::LsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b01),
3330            ArmOp::AsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b10),
3331            ArmOp::RorReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b11),
3332
3333            // RSB (Reverse Subtract): Rd = imm - Rn
3334            // Thumb-2 T2 encoding: 11110 i 0 1110 S Rn | 0 imm3 Rd imm8
3335            ArmOp::Rsb { rd, rn, imm } => {
3336                let rd_bits = reg_to_bits(rd);
3337                let rn_bits = reg_to_bits(rn);
3338
3339                // #681 class audit: the T2 `i:imm3:imm8` field is a
3340                // ThumbExpandImm modified immediate and RSB has NO plain-imm12
3341                // (T4-style) form — packing a raw value > 0xFF silently encodes
3342                // a different constant (#253/#255 class). All current emitters
3343                // use imm 32 (shift complement), which expands to itself, so
3344                // this gate is byte-identical for existing codegen.
3345                let field = try_thumb_expand_imm(*imm).ok_or_else(|| {
3346                    synth_core::Error::synthesis(
3347                        "RSB immediate is not a valid ThumbExpandImm — materialize into a register",
3348                    )
3349                })?;
3350                let i_bit = (field >> 11) & 1;
3351                let imm3 = (field >> 8) & 0x7;
3352                let imm8 = field & 0xFF;
3353
3354                // hw1: 11110 i 01110 0 Rn  (S=0)
3355                let hw1: u16 = (0xF1C0 | (i_bit << 10) | rn_bits) as u16;
3356                // hw2: 0 imm3 Rd imm8
3357                let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3358
3359                let mut bytes = hw1.to_le_bytes().to_vec();
3360                bytes.extend_from_slice(&hw2.to_le_bytes());
3361                Ok(bytes)
3362            }
3363
3364            // CLZ (Thumb-2 32-bit)
3365            ArmOp::Clz { rd, rm } => {
3366                let rd_bits = reg_to_bits(rd);
3367                let rm_bits = reg_to_bits(rm);
3368
3369                // Thumb-2 CLZ: FAB0 Rm | F8 Rd Rm
3370                // 11111010 1011 Rm | 1111 1000 Rd Rm
3371                let hw1: u16 = (0xFAB0 | rm_bits) as u16;
3372                let hw2: u16 = (0xF080 | (rd_bits << 8) | rm_bits) as u16;
3373
3374                let mut bytes = hw1.to_le_bytes().to_vec();
3375                bytes.extend_from_slice(&hw2.to_le_bytes());
3376                Ok(bytes)
3377            }
3378
3379            // RBIT (Thumb-2 32-bit)
3380            ArmOp::Rbit { rd, rm } => {
3381                let rd_bits = reg_to_bits(rd);
3382                let rm_bits = reg_to_bits(rm);
3383
3384                // Thumb-2 RBIT: FA90 Rm | F0 Rd A0 Rm
3385                // 11111010 1001 Rm | 1111 Rd 1010 Rm
3386                let hw1: u16 = (0xFA90 | rm_bits) as u16;
3387                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rm_bits) as u16;
3388
3389                let mut bytes = hw1.to_le_bytes().to_vec();
3390                bytes.extend_from_slice(&hw2.to_le_bytes());
3391                Ok(bytes)
3392            }
3393
3394            // SXTB (16-bit for low registers)
3395            ArmOp::Sxtb { rd, rm } => {
3396                let rd_bits = reg_to_bits(rd) as u16;
3397                let rm_bits = reg_to_bits(rm) as u16;
3398
3399                if rd_bits < 8 && rm_bits < 8 {
3400                    // SXTB Rd, Rm (16-bit): 1011 0010 01 Rm Rd
3401                    let instr: u16 = 0xB240 | (rm_bits << 3) | rd_bits;
3402                    Ok(instr.to_le_bytes().to_vec())
3403                } else {
3404                    // Thumb-2 SXTB.W: FA4F F(rd)80 (rm)
3405                    // 11111010 0100 1111 | 1111 Rd 10 rotate Rm
3406                    let rd_bits32 = rd_bits as u32;
3407                    let rm_bits32 = rm_bits as u32;
3408                    let hw1: u16 = 0xFA4F;
3409                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3410                    let mut bytes = hw1.to_le_bytes().to_vec();
3411                    bytes.extend_from_slice(&hw2.to_le_bytes());
3412                    Ok(bytes)
3413                }
3414            }
3415
3416            // SXTH (16-bit for low registers)
3417            ArmOp::Sxth { rd, rm } => {
3418                let rd_bits = reg_to_bits(rd) as u16;
3419                let rm_bits = reg_to_bits(rm) as u16;
3420
3421                if rd_bits < 8 && rm_bits < 8 {
3422                    // SXTH Rd, Rm (16-bit): 1011 0010 00 Rm Rd
3423                    let instr: u16 = 0xB200 | (rm_bits << 3) | rd_bits;
3424                    Ok(instr.to_le_bytes().to_vec())
3425                } else {
3426                    // Thumb-2 SXTH.W: FA0F F(rd)80 (rm)
3427                    // 11111010 0000 1111 | 1111 Rd 10 rotate Rm
3428                    let rd_bits32 = rd_bits as u32;
3429                    let rm_bits32 = rm_bits as u32;
3430                    let hw1: u16 = 0xFA0F;
3431                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3432                    let mut bytes = hw1.to_le_bytes().to_vec();
3433                    bytes.extend_from_slice(&hw2.to_le_bytes());
3434                    Ok(bytes)
3435                }
3436            }
3437
3438            // UXTB Rd,Rm — zero-extend byte (rd = rm & 0xff)
3439            ArmOp::Uxtb { rd, rm } => {
3440                let rd_bits = reg_to_bits(rd) as u16;
3441                let rm_bits = reg_to_bits(rm) as u16;
3442                if rd_bits < 8 && rm_bits < 8 {
3443                    // UXTB Rd, Rm (16-bit): 1011 0010 11 Rm Rd
3444                    let instr: u16 = 0xB2C0 | (rm_bits << 3) | rd_bits;
3445                    Ok(instr.to_le_bytes().to_vec())
3446                } else {
3447                    // Thumb-2 UXTB.W: FA5F F(rd)80 (rm)
3448                    let hw1: u16 = 0xFA5F;
3449                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3450                    let mut bytes = hw1.to_le_bytes().to_vec();
3451                    bytes.extend_from_slice(&hw2.to_le_bytes());
3452                    Ok(bytes)
3453                }
3454            }
3455
3456            // UXTH Rd,Rm — zero-extend halfword (rd = rm & 0xffff)
3457            ArmOp::Uxth { rd, rm } => {
3458                let rd_bits = reg_to_bits(rd) as u16;
3459                let rm_bits = reg_to_bits(rm) as u16;
3460                if rd_bits < 8 && rm_bits < 8 {
3461                    // UXTH Rd, Rm (16-bit): 1011 0010 10 Rm Rd
3462                    let instr: u16 = 0xB280 | (rm_bits << 3) | rd_bits;
3463                    Ok(instr.to_le_bytes().to_vec())
3464                } else {
3465                    // Thumb-2 UXTH.W: FA1F F(rd)80 (rm)
3466                    let hw1: u16 = 0xFA1F;
3467                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3468                    let mut bytes = hw1.to_le_bytes().to_vec();
3469                    bytes.extend_from_slice(&hw2.to_le_bytes());
3470                    Ok(bytes)
3471                }
3472            }
3473
3474            // CMP (can be 16-bit for low registers)
3475            ArmOp::Cmp { rn, op2 } => {
3476                let rn_bits = reg_to_bits(rn) as u16;
3477
3478                if let Operand2::Imm(imm) = op2 {
3479                    // Only use 16-bit encoding for non-negative immediates 0-255
3480                    // Negative immediates must use 32-bit encoding
3481                    if *imm >= 0 && *imm <= 255 && rn_bits < 8 {
3482                        // CMP Rn, #imm8 (16-bit): 0010 1 Rn imm8
3483                        let instr: u16 = 0x2800 | (rn_bits << 8) | (*imm as u16 & 0xFF);
3484                        Ok(instr.to_le_bytes().to_vec())
3485                    } else {
3486                        self.encode_thumb32_cmp_imm(rn, *imm as u32)
3487                    }
3488                } else if let Operand2::Reg(rm) = op2 {
3489                    let rm_bits = reg_to_bits(rm) as u16;
3490                    if rn_bits < 8 && rm_bits < 8 {
3491                        // CMP Rn, Rm (16-bit low): 0100 0010 10 Rm Rn
3492                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
3493                        Ok(instr.to_le_bytes().to_vec())
3494                    } else {
3495                        // CMP Rn, Rm (16-bit high): 0100 0101 N Rm Rn[2:0]
3496                        let n_bit = (rn_bits >> 3) & 1;
3497                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
3498                        Ok(instr.to_le_bytes().to_vec())
3499                    }
3500                } else {
3501                    let instr: u16 = 0xBF00;
3502                    Ok(instr.to_le_bytes().to_vec())
3503                }
3504            }
3505
3506            // CMN (Compare Negative) - computes Rn + op2 and sets flags
3507            // CMN Rn, #1 sets Z flag if Rn == -1 (since -1 + 1 = 0)
3508            ArmOp::Cmn { rn, op2 } => {
3509                let rn_bits = reg_to_bits(rn) as u16;
3510
3511                if let Operand2::Imm(imm) = op2 {
3512                    // CMN.W Rn, #imm (32-bit): i:imm3:imm8 is a ThumbExpandImm
3513                    // modified immediate (the field sits in imm3=hw2[14:12],
3514                    // imm8=hw2[7:0], i=hw1[10]). Encode it correctly, or error on
3515                    // an un-encodable value — replacing the old silent `0xBF00`
3516                    // NOP (the last of the silent-miscompile data-proc encoders).
3517                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3518                        synth_core::Error::synthesis(
3519                            "CMN immediate is not a valid ThumbExpandImm — materialize into a register",
3520                        )
3521                    })?;
3522                    let i_bit = (field >> 11) & 1;
3523                    let imm3 = (field >> 8) & 0x7;
3524                    let imm8 = field & 0xFF;
3525                    let hw1: u16 = (0xF110 | (i_bit << 10) as u16) | rn_bits;
3526                    let hw2: u16 = (imm3 << 12) as u16 | 0x0F00 | imm8 as u16;
3527                    let mut bytes = hw1.to_le_bytes().to_vec();
3528                    bytes.extend_from_slice(&hw2.to_le_bytes());
3529                    Ok(bytes)
3530                } else if let Operand2::Reg(rm) = op2 {
3531                    let rm_bits = reg_to_bits(rm) as u16;
3532                    // 16-bit CMN (T1) only encodes R0-R7; high registers overflow
3533                    // the 3-bit fields and corrupt the operands (#184, the #180
3534                    // class). CMN has no high-register 16-bit form, so fall back
3535                    // to 32-bit CMN.W (T2): EB10 Rn | 0F00 Rm (ADD.W with S=1 and
3536                    // Rd discarded as PC/1111).
3537                    if rn_bits < 8 && rm_bits < 8 {
3538                        // CMN Rn, Rm (16-bit): 0100 0010 11 Rm Rn
3539                        let instr: u16 = 0x42C0 | (rm_bits << 3) | rn_bits;
3540                        Ok(instr.to_le_bytes().to_vec())
3541                    } else {
3542                        let hw1: u16 = 0xEB10 | rn_bits;
3543                        let hw2: u16 = 0x0F00 | rm_bits;
3544                        let mut bytes = hw1.to_le_bytes().to_vec();
3545                        bytes.extend_from_slice(&hw2.to_le_bytes());
3546                        Ok(bytes)
3547                    }
3548                } else {
3549                    Ok(vec![0xBF, 0x00])
3550                }
3551            }
3552
3553            // LDR (can be 16-bit for simple cases)
3554            ArmOp::Ldr { rd, addr } => {
3555                let rd_bits = reg_to_bits(rd);
3556                let base_bits = reg_to_bits(&addr.base);
3557
3558                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3559                if let Some(offset_reg) = &addr.offset_reg {
3560                    let rm_bits = reg_to_bits(offset_reg);
3561
3562                    // If there's also an immediate offset, we need to ADD it first
3563                    if addr.offset != 0 {
3564                        // Use R12 (IP) as scratch to avoid clobbering the address register
3565                        // ADD R12, Rm, #offset; LDR Rd, [base, R12]
3566                        let scratch = Reg::R12;
3567                        let mut bytes =
3568                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3569                        bytes.extend(self.encode_thumb32_ldr_reg(rd, &addr.base, &scratch)?);
3570                        return Ok(bytes);
3571                    }
3572
3573                    // Simple register offset: LDR Rd, [Rn, Rm]
3574                    // 16-bit: only if Rd, Rn, Rm < R8
3575                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3576                        // LDR Rd, [Rn, Rm] (16-bit): 0101 100 Rm Rn Rd
3577                        let instr: u16 = 0x5800
3578                            | ((rm_bits as u16) << 6)
3579                            | ((base_bits as u16) << 3)
3580                            | (rd_bits as u16);
3581                        return Ok(instr.to_le_bytes().to_vec());
3582                    }
3583
3584                    // 32-bit register offset
3585                    return self.encode_thumb32_ldr_reg(rd, &addr.base, offset_reg);
3586                }
3587
3588                // Immediate offset mode [base, #imm]
3589                let offset = addr.offset as u32;
3590
3591                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3592                    // LDR Rd, [Rn, #imm5*4] (16-bit): 0110 1 imm5 Rn Rd
3593                    let imm5 = (offset >> 2) as u16;
3594                    let instr: u16 =
3595                        0x6800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3596                    Ok(instr.to_le_bytes().to_vec())
3597                } else {
3598                    self.encode_thumb32_ldr(rd, &addr.base, offset)
3599                }
3600            }
3601
3602            // STR (can be 16-bit for simple cases)
3603            ArmOp::Str { rd, addr } => {
3604                let rd_bits = reg_to_bits(rd);
3605                let base_bits = reg_to_bits(&addr.base);
3606
3607                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3608                if let Some(offset_reg) = &addr.offset_reg {
3609                    let rm_bits = reg_to_bits(offset_reg);
3610
3611                    // If there's also an immediate offset, we need to ADD it first
3612                    if addr.offset != 0 {
3613                        // Use R12 (IP) as scratch to avoid clobbering the address register
3614                        // ADD R12, Rm, #offset; STR Rd, [base, R12]
3615                        let scratch = Reg::R12;
3616                        let mut bytes =
3617                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3618                        bytes.extend(self.encode_thumb32_str_reg(rd, &addr.base, &scratch)?);
3619                        return Ok(bytes);
3620                    }
3621
3622                    // Simple register offset: STR Rd, [Rn, Rm]
3623                    // 16-bit: only if Rd, Rn, Rm < R8
3624                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3625                        // STR Rd, [Rn, Rm] (16-bit): 0101 000 Rm Rn Rd
3626                        let instr: u16 = 0x5000
3627                            | ((rm_bits as u16) << 6)
3628                            | ((base_bits as u16) << 3)
3629                            | (rd_bits as u16);
3630                        return Ok(instr.to_le_bytes().to_vec());
3631                    }
3632
3633                    // 32-bit register offset
3634                    return self.encode_thumb32_str_reg(rd, &addr.base, offset_reg);
3635                }
3636
3637                // Immediate offset mode [base, #imm]
3638                let offset = addr.offset as u32;
3639
3640                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3641                    // STR Rd, [Rn, #imm5*4] (16-bit): 0110 0 imm5 Rn Rd
3642                    let imm5 = (offset >> 2) as u16;
3643                    let instr: u16 =
3644                        0x6000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3645                    Ok(instr.to_le_bytes().to_vec())
3646                } else {
3647                    self.encode_thumb32_str(rd, &addr.base, offset)
3648                }
3649            }
3650
3651            // LDRB (Thumb-2)
3652            ArmOp::Ldrb { rd, addr } => {
3653                let rd_bits = reg_to_bits(rd);
3654                let base_bits = reg_to_bits(&addr.base);
3655
3656                if let Some(offset_reg) = &addr.offset_reg {
3657                    if addr.offset != 0 {
3658                        let scratch = Reg::R12;
3659                        let mut bytes =
3660                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3661                        bytes.extend(self.encode_thumb32_ldrb_reg(rd, &addr.base, &scratch)?);
3662                        return Ok(bytes);
3663                    }
3664                    return self.encode_thumb32_ldrb_reg(rd, &addr.base, offset_reg);
3665                }
3666
3667                let offset = addr.offset as u32;
3668                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3669                    // LDRB Rd, [Rn, #imm5] (16-bit): 0111 1 imm5 Rn Rd
3670                    let instr: u16 = 0x7800
3671                        | ((offset as u16) << 6)
3672                        | ((base_bits as u16) << 3)
3673                        | (rd_bits as u16);
3674                    Ok(instr.to_le_bytes().to_vec())
3675                } else {
3676                    self.encode_thumb32_ldrb_imm(rd, &addr.base, offset)
3677                }
3678            }
3679
3680            // LDRSB (Thumb-2)
3681            ArmOp::Ldrsb { rd, addr } => {
3682                let rd_bits = reg_to_bits(rd);
3683                let base_bits = reg_to_bits(&addr.base);
3684
3685                if let Some(offset_reg) = &addr.offset_reg {
3686                    if addr.offset != 0 {
3687                        let scratch = Reg::R12;
3688                        let mut bytes =
3689                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3690                        bytes.extend(self.encode_thumb32_ldrsb_reg(rd, &addr.base, &scratch)?);
3691                        return Ok(bytes);
3692                    }
3693                    return self.encode_thumb32_ldrsb_reg(rd, &addr.base, offset_reg);
3694                }
3695
3696                let offset = addr.offset as u32;
3697                // LDRSB has no 16-bit immediate form (only register)
3698                // For 16-bit reg form: only if Rd, Rn, Rm < R8
3699                if rd_bits < 8 && base_bits < 8 && offset == 0 {
3700                    // No immediate 16-bit encoding for LDRSB; use 32-bit
3701                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3702                } else {
3703                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3704                }
3705            }
3706
3707            // LDRH (Thumb-2)
3708            ArmOp::Ldrh { rd, addr } => {
3709                let rd_bits = reg_to_bits(rd);
3710                let base_bits = reg_to_bits(&addr.base);
3711
3712                if let Some(offset_reg) = &addr.offset_reg {
3713                    if addr.offset != 0 {
3714                        let scratch = Reg::R12;
3715                        let mut bytes =
3716                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3717                        bytes.extend(self.encode_thumb32_ldrh_reg(rd, &addr.base, &scratch)?);
3718                        return Ok(bytes);
3719                    }
3720                    return self.encode_thumb32_ldrh_reg(rd, &addr.base, offset_reg);
3721                }
3722
3723                let offset = addr.offset as u32;
3724                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3725                    // LDRH Rd, [Rn, #imm5*2] (16-bit): 1000 1 imm5 Rn Rd
3726                    let imm5 = (offset >> 1) as u16;
3727                    let instr: u16 =
3728                        0x8800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3729                    Ok(instr.to_le_bytes().to_vec())
3730                } else {
3731                    self.encode_thumb32_ldrh_imm(rd, &addr.base, offset)
3732                }
3733            }
3734
3735            // LDRSH (Thumb-2)
3736            ArmOp::Ldrsh { rd, addr } => {
3737                if let Some(offset_reg) = &addr.offset_reg {
3738                    if addr.offset != 0 {
3739                        let scratch = Reg::R12;
3740                        let mut bytes =
3741                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3742                        bytes.extend(self.encode_thumb32_ldrsh_reg(rd, &addr.base, &scratch)?);
3743                        return Ok(bytes);
3744                    }
3745                    return self.encode_thumb32_ldrsh_reg(rd, &addr.base, offset_reg);
3746                }
3747
3748                let offset = addr.offset as u32;
3749                self.encode_thumb32_ldrsh_imm(rd, &addr.base, offset)
3750            }
3751
3752            // STRB (Thumb-2)
3753            ArmOp::Strb { rd, addr } => {
3754                let rd_bits = reg_to_bits(rd);
3755                let base_bits = reg_to_bits(&addr.base);
3756
3757                if let Some(offset_reg) = &addr.offset_reg {
3758                    if addr.offset != 0 {
3759                        let scratch = Reg::R12;
3760                        let mut bytes =
3761                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3762                        bytes.extend(self.encode_thumb32_strb_reg(rd, &addr.base, &scratch)?);
3763                        return Ok(bytes);
3764                    }
3765                    return self.encode_thumb32_strb_reg(rd, &addr.base, offset_reg);
3766                }
3767
3768                let offset = addr.offset as u32;
3769                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3770                    // STRB Rd, [Rn, #imm5] (16-bit): 0111 0 imm5 Rn Rd
3771                    let instr: u16 = 0x7000
3772                        | ((offset as u16) << 6)
3773                        | ((base_bits as u16) << 3)
3774                        | (rd_bits as u16);
3775                    Ok(instr.to_le_bytes().to_vec())
3776                } else {
3777                    self.encode_thumb32_strb_imm(rd, &addr.base, offset)
3778                }
3779            }
3780
3781            // STRH (Thumb-2)
3782            ArmOp::Strh { rd, addr } => {
3783                let rd_bits = reg_to_bits(rd);
3784                let base_bits = reg_to_bits(&addr.base);
3785
3786                if let Some(offset_reg) = &addr.offset_reg {
3787                    if addr.offset != 0 {
3788                        let scratch = Reg::R12;
3789                        let mut bytes =
3790                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3791                        bytes.extend(self.encode_thumb32_strh_reg(rd, &addr.base, &scratch)?);
3792                        return Ok(bytes);
3793                    }
3794                    return self.encode_thumb32_strh_reg(rd, &addr.base, offset_reg);
3795                }
3796
3797                let offset = addr.offset as u32;
3798                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3799                    // STRH Rd, [Rn, #imm5*2] (16-bit): 1000 0 imm5 Rn Rd
3800                    let imm5 = (offset >> 1) as u16;
3801                    let instr: u16 =
3802                        0x8000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3803                    Ok(instr.to_le_bytes().to_vec())
3804                } else {
3805                    self.encode_thumb32_strh_imm(rd, &addr.base, offset)
3806                }
3807            }
3808
3809            // MemorySize (Thumb-2)
3810            ArmOp::MemorySize { rd } => {
3811                // LSR rd, R10, #16 — memory size in bytes / 65536 = pages
3812                // Thumb-2 16-bit: LSRS Rd, Rm, #imm5 — 0000 1 imm5 Rm Rd
3813                let rd_bits = reg_to_bits(rd);
3814                let r10_bits = reg_to_bits(&Reg::R10);
3815                if rd_bits < 8 && r10_bits < 8 {
3816                    let instr: u16 =
3817                        0x0800 | (16u16 << 6) | ((r10_bits as u16) << 3) | (rd_bits as u16);
3818                    Ok(instr.to_le_bytes().to_vec())
3819                } else {
3820                    // Thumb-2 32-bit LSR: 1110 1010 010 0 1111 | 0 imm3 Rd imm2 01 Rm
3821                    let imm5: u32 = 16;
3822                    let imm3 = (imm5 >> 2) & 0x7;
3823                    let imm2 = imm5 & 0x3;
3824                    let hw1: u16 = 0xEA4F;
3825                    let hw2: u16 =
3826                        ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | 0x10 | r10_bits) as u16;
3827                    let mut bytes = hw1.to_le_bytes().to_vec();
3828                    bytes.extend_from_slice(&hw2.to_le_bytes());
3829                    Ok(bytes)
3830                }
3831            }
3832
3833            // MemoryGrow (Thumb-2)
3834            ArmOp::MemoryGrow { rd, .. } => {
3835                // On embedded with fixed memory, always return -1 (failure)
3836                // MVN rd, #0 → MOV rd, #-1
3837                // Thumb-2 32-bit: MVN: 1111 0 i 0 0 0 1 1 0 1111 | 0 imm3 Rd imm8
3838                let rd_bits = reg_to_bits(rd);
3839                let hw1: u16 = 0xF06F; // MVN with i=0
3840                let hw2: u16 = (rd_bits << 8) as u16; // imm8=0 → ~0 = 0xFFFFFFFF = -1
3841                let mut bytes = hw1.to_le_bytes().to_vec();
3842                bytes.extend_from_slice(&hw2.to_le_bytes());
3843                Ok(bytes)
3844            }
3845
3846            // BX (16-bit)
3847            ArmOp::Bx { rm } => {
3848                let rm_bits = reg_to_bits(rm) as u16;
3849                // BX Rm (16-bit): 0100 0111 0 Rm 000
3850                let instr: u16 = 0x4700 | (rm_bits << 3);
3851                Ok(instr.to_le_bytes().to_vec())
3852            }
3853
3854            // BLX (16-bit) - Branch with Link and Exchange
3855            // BLX Rm: 0100 0111 1 Rm 000
3856            ArmOp::Blx { rm } => {
3857                let rm_bits = reg_to_bits(rm) as u16;
3858                let instr: u16 = 0x4780 | (rm_bits << 3);
3859                Ok(instr.to_le_bytes().to_vec())
3860            }
3861
3862            // CallIndirect - indirect function call via table lookup
3863            // table_index_reg contains the table index
3864            // Generates (#642): MOVW ip,#size [; MOVT]; CMP idx,ip; BLO +1;
3865            //                   UDF #0; LSL R12,idx,#2; LDR R12,[R11,R12]; BLX R12
3866            // #650, table_byte_offset != 0 (a non-zero table in the contiguous
3867            // R11 region): the pointer load becomes
3868            //                   ADD R12,R11,R12; LDR R12,[R12,#offset]
3869            // #664, null_check (the table has null slots, linked as ZERO
3870            // words): the loaded pointer is null-checked before the BLX —
3871            //                   CMP.W R12,#0; BNE +1; UDF #0
3872            // #676, type_check (heterogeneous table — runtime §4.4.8 type
3873            // check against the type-id sidecar at R11+off): after the
3874            // bounds guard —
3875            //                   LSL R12,idx,#2; ADD R12,R11,R12;
3876            //                   LDR R12,[R12,#type_off]; CMP.W R12,#id;
3877            //                   BEQ +1; UDF #0
3878            // (the dispatch tail then recomputes idx*4 — idx stays live).
3879            ArmOp::CallIndirect {
3880                rd: _,
3881                type_idx: _,
3882                table_index_reg,
3883                table_size,
3884                table_byte_offset,
3885                null_check,
3886                type_check,
3887            } => {
3888                let idx_reg = reg_to_bits(table_index_reg);
3889                let mut bytes = Vec::new();
3890
3891                // The expansion:
3892                // 1. Bounds guard (#642): trap (UDF #0, WASM Core §4.4.8) when
3893                //    index >= table size. Without it an out-of-bounds index
3894                //    reads past the table and BLXes whatever word lies there —
3895                //    an uncontrolled indirect branch instead of a trap.
3896                // 2. Multiplies index by 4 (function pointer size)
3897                // 3. Loads function pointer from table (table base in R11)
3898                // 4. Calls the function via BLX
3899                //
3900                // Table base setup must be done by caller/runtime. The type
3901                // check §4.4.8 also requires is discharged at COMPILE time:
3902                // the selector only emits this op after verifying the closed-
3903                // world property that every table entry's signature equals the
3904                // expected type (the raw code-pointer table carries no runtime
3905                // type ids to compare) — see the #642 selector guard.
3906
3907                // MOVW R12, #(size & 0xFFFF) — Thumb-2 T3:
3908                // 11110 i 100100 imm4 | 0 imm3 Rd imm8 (Rd=R12).
3909                let size_lo = *table_size & 0xFFFF;
3910                let hw1: u16 =
3911                    (0xF240 | (((size_lo >> 11) & 1) << 10) | ((size_lo >> 12) & 0xF)) as u16;
3912                let hw2: u16 =
3913                    ((((size_lo >> 8) & 0x7) << 12) | (12 << 8) | (size_lo & 0xFF)) as u16;
3914                bytes.extend_from_slice(&hw1.to_le_bytes());
3915                bytes.extend_from_slice(&hw2.to_le_bytes());
3916                // MOVT R12, #(size >> 16) — only when the table size exceeds
3917                // 16 bits (never in practice, but the guard must not compare
3918                // against a truncated size).
3919                let size_hi = *table_size >> 16;
3920                if size_hi != 0 {
3921                    let hw1: u16 =
3922                        (0xF2C0 | (((size_hi >> 11) & 1) << 10) | ((size_hi >> 12) & 0xF)) as u16;
3923                    let hw2: u16 =
3924                        ((((size_hi >> 8) & 0x7) << 12) | (12 << 8) | (size_hi & 0xFF)) as u16;
3925                    bytes.extend_from_slice(&hw1.to_le_bytes());
3926                    bytes.extend_from_slice(&hw2.to_le_bytes());
3927                }
3928                // CMP idx, R12 — 16-bit T2 (high-register capable):
3929                // 010001 01 N Rm(4) Rn(3), Rn full = N:Rn3.
3930                let cmp: u16 = (0x4500 | ((idx_reg & 8) << 4) | (12 << 3) | (idx_reg & 7)) as u16;
3931                bytes.extend_from_slice(&cmp.to_le_bytes());
3932                // BLO +1 insn (skip the UDF when index < size) — B<cond>.N
3933                // imm8=0: target = branch + 4. LO = unsigned lower.
3934                bytes.extend_from_slice(&0xD300u16.to_le_bytes());
3935                // UDF #0 — call_indirect out-of-bounds trap (same trap idiom as
3936                // the div-by-zero guards).
3937                bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3938
3939                // #676: runtime type check — ONLY for a heterogeneous table
3940                // (mixed signatures, closed-world verdict impossible). Load
3941                // the indexed slot's structural class id from the type-id
3942                // sidecar (`R11 + type_off + idx*4`; `type_off` = sidecar
3943                // base + this table's base offset, a compile-time constant)
3944                // and compare it against the expected type's class id — a
3945                // mismatch is the WASM Core §4.4.8 type trap. Null slots
3946                // carry the reserved id 0, so this compare subsumes the
3947                // #664 null trap (the selector passes `null_check: false`).
3948                // `None` emits NOTHING: every homogeneous table keeps the
3949                // pre-#676 bytes identical BY CONSTRUCTION. R12 stays the
3950                // only scratch (#212); the dispatch tail below recomputes
3951                // idx*4 — the index register is never clobbered here.
3952                if let Some((expected_id, type_off)) = type_check {
3953                    // RQ-61-IMMRANGE (#1072): compiled out in release, where
3954                    // the masks below silently TRUNCATE (id 256 compares as
3955                    // 0 — a NULL slot would pass the §4.4.8 check). The
3956                    // enforcement claim is DEMONSTRATED: the sole `Some`
3957                    // producer, `resolve_runtime_type_check`, loud-declines
3958                    // id > 255 / offset > 4095, tripped by
3959                    // `test_676_call_indirect_runtime_check_range_declines`
3960                    // (mutation-checked — see the A32 twin above).
3961                    debug_assert!(*expected_id <= 255, "selector enforces the CMP imm8 range");
3962                    debug_assert!(*type_off <= 4095, "selector enforces the LDR imm12 range");
3963                    // MOV.W R12, idx, LSL #2 (same encoding as the dispatch
3964                    // tail's index scale below).
3965                    bytes.extend_from_slice(&0xEA4Fu16.to_le_bytes());
3966                    bytes.extend_from_slice(
3967                        &(((0x0C00 | (0b10 << 6)) | idx_reg) as u16).to_le_bytes(),
3968                    );
3969                    // ADD.W R12, R11, R12 (the #650 base-add form).
3970                    bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
3971                    bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
3972                    // LDR.W R12, [R12, #type_off] — T3 LDR (immediate):
3973                    // 1111 1000 1101 Rn=1100 | Rt=1100 imm12.
3974                    bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
3975                    bytes.extend_from_slice(
3976                        &(0xC000u16 | (*type_off as u16 & 0x0FFF)).to_le_bytes(),
3977                    );
3978                    // CMP.W R12, #expected_id — T2 CMP (immediate), imm8
3979                    // (same form as the #664 null check's CMP.W R12, #0).
3980                    bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
3981                    bytes.extend_from_slice(
3982                        &(0x0F00u16 | (*expected_id as u16 & 0xFF)).to_le_bytes(),
3983                    );
3984                    // BEQ +1 insn (skip the UDF when the class id matches) —
3985                    // B<cond>.N imm8=0: target = branch + 4. EQ.
3986                    bytes.extend_from_slice(&0xD000u16.to_le_bytes());
3987                    // UDF #0 — the §4.4.8 type-mismatch trap (same idiom as
3988                    // the bounds guard above).
3989                    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3990                }
3991
3992                // LSL R12, idx_reg, #2 (multiply index by 4)
3993                // Thumb-2 MOV with shift: 11101010 010 S 1111 | 0 imm3 Rd imm2 type Rm
3994                // LSL: type=00 (bits 5:4), imm5=2 -> imm3=000, imm2=10 (bits 7:6)
3995                // #597: the shift amount was previously shifted into bits 5:4 —
3996                // the TYPE field — encoding `mov.w ip, rm, ASR #32`, which
3997                // destroyed the index and dispatched table entry 0 for every
3998                // call. imm2 lives at bits 7:6.
3999                let hw1: u16 = 0xEA4F_u16; // MOV.W R12, Rm, LSL #2
4000                let hw2: u16 = ((0x0C00 | (0b10 << 6)) | idx_reg) as u16;
4001                bytes.extend_from_slice(&hw1.to_le_bytes());
4002                bytes.extend_from_slice(&hw2.to_le_bytes());
4003
4004                if *table_byte_offset == 0 {
4005                    // Table 0 (base = R11 itself): the pre-#650 single-load
4006                    // form — a single-table module's bytes stay identical BY
4007                    // CONSTRUCTION.
4008                    // LDR R12, [R11, R12] - load function pointer
4009                    // Thumb-2 LDR (register): 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
4010                    // Rn=R11, Rt=R12, Rm=R12, imm2=00 (no shift)
4011                    let ldr_hw1: u16 = 0xF85B; // LDR.W Rt, [R11, Rm]
4012                    let ldr_hw2: u16 = 0xC00C; // Rt=R12, imm2=00, Rm=R12
4013                    bytes.extend_from_slice(&ldr_hw1.to_le_bytes());
4014                    bytes.extend_from_slice(&ldr_hw2.to_le_bytes());
4015                } else {
4016                    // #650: table N of the contiguous R11 region — fold the
4017                    // compile-time base offset into the pointer load via the
4018                    // LDR imm12 form (R12 stays the only scratch, per the
4019                    // #212 convention).
4020                    assert!(
4021                        *table_byte_offset <= 4095,
4022                        "call_indirect table base offset {table_byte_offset} exceeds \
4023                         LDR imm12 — the selector must have declined this (#650)"
4024                    );
4025                    // ADD.W R12, R11, R12 — T3 ADD (register):
4026                    // 11101011000 S=0 Rn=1011 | 0 imm3=000 Rd=1100 imm2=00 type=00 Rm=1100
4027                    bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
4028                    bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
4029                    // LDR.W R12, [R12, #offset] — T3 LDR (immediate):
4030                    // 1111 1000 1101 Rn=1100 | Rt=1100 imm12
4031                    bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
4032                    bytes.extend_from_slice(
4033                        &((0xC000u16) | (*table_byte_offset as u16 & 0x0FFF)).to_le_bytes(),
4034                    );
4035                }
4036
4037                // #664: null-slot trap — ONLY when the table image carries
4038                // null (uninitialized) slots, which the layout contract
4039                // requires to be linked as ZERO words. A fully-initialized
4040                // table skips this branch entirely, keeping the pre-#664
4041                // expansion byte-identical BY CONSTRUCTION (the #650
4042                // offset-0 trick).
4043                if *null_check {
4044                    // CMP.W R12, #0 — T2 CMP (immediate): 11110 i 0 1101 1
4045                    // Rn(4) | 0 imm3 1111 imm8, Rn=R12, imm=0.
4046                    bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
4047                    bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
4048                    // BNE +1 insn (skip the UDF when the pointer is non-null)
4049                    // — B<cond>.N imm8=0: target = branch + 4. NE.
4050                    bytes.extend_from_slice(&0xD100u16.to_le_bytes());
4051                    // UDF #0 — call_indirect null-funcref trap (WASM Core
4052                    // §4.4.8: calling an uninitialized element traps; same
4053                    // trap idiom as the bounds guard above).
4054                    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
4055                }
4056
4057                // BLX R12 (call function indirectly)
4058                // BLX Rm (16-bit): 0100 0111 1 Rm 000
4059                let blx: u16 = 0x47E0; // BLX R12
4060                bytes.extend_from_slice(&blx.to_le_bytes());
4061
4062                Ok(bytes)
4063            }
4064
4065            // Label pseudo-instruction: emits no machine code
4066            ArmOp::Label { .. } => Ok(Vec::new()),
4067
4068            // Conditional branch to label (generic) - offset 0, will be patched
4069            ArmOp::Bcc { cond, label: _ } => {
4070                use synth_synthesis::Condition;
4071                let cond_bits: u16 = match cond {
4072                    Condition::EQ => 0x0,
4073                    Condition::NE => 0x1,
4074                    Condition::HS => 0x2,
4075                    Condition::LO => 0x3,
4076                    Condition::HI => 0x8,
4077                    Condition::LS => 0x9,
4078                    Condition::GE => 0xA,
4079                    Condition::LT => 0xB,
4080                    Condition::GT => 0xC,
4081                    Condition::LE => 0xD,
4082                };
4083                // 16-bit B<cond> with offset 0: 1101 cond imm8
4084                let instr: u16 = 0xD000 | (cond_bits << 8);
4085                Ok(instr.to_le_bytes().to_vec())
4086            }
4087
4088            // Branch instructions
4089            ArmOp::B { label: _ } => {
4090                // Simplified: B.N with offset 0
4091                // For real usage, would need label resolution
4092                let instr: u16 = 0xE000; // B.N #0
4093                Ok(instr.to_le_bytes().to_vec())
4094            }
4095
4096            // BHS (Branch if Higher or Same) - used for bounds checking
4097            // Condition code: 0x2 (C set)
4098            ArmOp::Bhs { label: _ } => {
4099                // 16-bit B<cond> with offset 0: 1101 cond imm8
4100                // cond = 0x2 (HS)
4101                let instr: u16 = 0xD200; // BHS.N #0
4102                Ok(instr.to_le_bytes().to_vec())
4103            }
4104
4105            // BLO (Branch if Lower) - complementary to BHS
4106            // Condition code: 0x3 (C clear)
4107            ArmOp::Blo { label: _ } => {
4108                // 16-bit B<cond> with offset 0: 1101 cond imm8
4109                // cond = 0x3 (LO)
4110                let instr: u16 = 0xD300; // BLO.N #0
4111                Ok(instr.to_le_bytes().to_vec())
4112            }
4113
4114            // Branch with numeric offset (Thumb-2)
4115            // Thumb-2 B.W instruction: 32-bit with +-16MB range
4116            ArmOp::BOffset { offset } => {
4117                // offset is already the halfword displacement: (target - branch - 4) / 2
4118                // This is the raw encoded value, accounting for variable-length instructions
4119                let halfword_offset = *offset;
4120
4121                // 16-bit B.N encoding: 1110 0 imm11 (11-bit signed halfword offset)
4122                // Range: -1024 to +1022 halfwords
4123                if (-1024..=1022).contains(&halfword_offset) {
4124                    // 16-bit B.N encoding: 1110 0 imm11
4125                    let imm11 = (halfword_offset as u16) & 0x7FF;
4126                    let instr: u16 = 0xE000 | imm11;
4127                    Ok(instr.to_le_bytes().to_vec())
4128                } else {
4129                    // 32-bit B.W encoding for larger offsets
4130                    // First halfword: 1111 0 S imm10
4131                    // Second halfword: 10 J1 0 J2 imm11
4132                    // Total offset = SignExtend(S:I1:I2:imm10:imm11:0)
4133                    // where I1 = NOT(J1 XOR S), I2 = NOT(J2 XOR S)
4134
4135                    // The B.W (T4) encoding packs the signed offset as:
4136                    //   S:I1:I2:imm10:imm11:0  (25-bit signed, halfword-aligned)
4137                    // where J1 = NOT(I1 XOR S), J2 = NOT(I2 XOR S)
4138                    // Input halfword_offset already equals (target - PC - 4) / 2,
4139                    // so the full byte offset = halfword_offset << 1.
4140                    // The encoding fields split that 25-bit signed value (including the
4141                    // implicit trailing zero) as: S | imm10 | imm11
4142                    // with I1 = bit 23 and I2 = bit 22 of the signed offset.
4143                    let signed_offset = halfword_offset << 1; // byte offset
4144                    let s = if signed_offset < 0 { 1u32 } else { 0u32 };
4145                    let uoffset = signed_offset as u32;
4146                    let imm10 = (uoffset >> 12) & 0x3FF; // bits [21:12]
4147                    let imm11 = (uoffset >> 1) & 0x7FF; // bits [11:1]
4148                    let i1 = (uoffset >> 23) & 1; // bit 23
4149                    let i2 = (uoffset >> 22) & 1; // bit 22
4150                    let j1 = (!(i1 ^ s)) & 1; // J1 = NOT(I1 XOR S)
4151                    let j2 = (!(i2 ^ s)) & 1; // J2 = NOT(I2 XOR S)
4152
4153                    let hw1: u16 = (0xF000 | (s << 10) | imm10) as u16;
4154                    let hw2: u16 = (0x9000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4155
4156                    let mut bytes = hw1.to_le_bytes().to_vec();
4157                    bytes.extend_from_slice(&hw2.to_le_bytes());
4158                    Ok(bytes)
4159                }
4160            }
4161
4162            // Conditional branch with numeric offset (Thumb-2)
4163            ArmOp::BCondOffset { cond, offset } => {
4164                use synth_synthesis::Condition;
4165                let cond_bits: u16 = match cond {
4166                    Condition::EQ => 0x0,
4167                    Condition::NE => 0x1,
4168                    Condition::HS => 0x2,
4169                    Condition::LO => 0x3,
4170                    Condition::HI => 0x8,
4171                    Condition::LS => 0x9,
4172                    Condition::GE => 0xA,
4173                    Condition::LT => 0xB,
4174                    Condition::GT => 0xC,
4175                    Condition::LE => 0xD,
4176                };
4177
4178                // offset is already the halfword displacement: (target - branch - 4) / 2
4179                // This is the raw imm8 value for 16-bit B<cond> encoding
4180                let halfword_offset = *offset;
4181
4182                // 16-bit B<cond> encoding: 1101 cond imm8
4183                // Range: -256 to +254 halfwords (imm8 is sign-extended and shifted left 1)
4184                if (-128..=127).contains(&halfword_offset) {
4185                    let imm8 = (halfword_offset as u16) & 0xFF;
4186                    let instr: u16 = 0xD000 | (cond_bits << 8) | imm8;
4187                    Ok(instr.to_le_bytes().to_vec())
4188                } else {
4189                    // 32-bit B<cond>.W (encoding T3) for larger offsets
4190                    // First halfword: 1111 0 S cond(4) imm6
4191                    // Second halfword: 10 J1 0 J2 imm11
4192                    //
4193                    // Per ARMv7-M, the branch BYTE offset is
4194                    // SignExtend(S:J2:J1:imm6:imm11:'0'), i.e. the field value
4195                    // S:J2:J1:imm6:imm11 IS the signed 20-bit HALFWORD offset —
4196                    // imm11/imm6/J1/J2/S take `halfword_offset` bits [10:0],
4197                    // [16:11], 17, 18 and 19 directly (mirroring the T4
4198                    // unconditional arm above).
4199                    //
4200                    // #740: this arm previously packed `halfword_offset >> 1`
4201                    // into imm6:imm11 — HALVING the displacement — so every
4202                    // wide conditional branch (span > 254 bytes) landed at half
4203                    // its intended offset: gust_poll's loop-head `br_if` to an
4204                    // outer block end jumped mid-shape. Narrow (16-bit) B<cond>
4205                    // encodings were unaffected, which is why short-range CF
4206                    // fixtures never caught it.
4207                    if !(-(1 << 19)..(1 << 19)).contains(&halfword_offset) {
4208                        return Err(synth_core::Error::synthesis(format!(
4209                            "B<cond>.W (T3) halfword offset {halfword_offset} exceeds \
4210                             the signed 20-bit encoding range (±1 MB) — refusing to \
4211                             emit a truncated branch"
4212                        )));
4213                    }
4214                    let u = halfword_offset as u32;
4215                    let imm11 = u & 0x7FF; // halfword offset bits [10:0]
4216                    let imm6 = (u >> 11) & 0x3F; // bits [16:11]
4217                    let j1 = (u >> 17) & 1; // bit 17
4218                    let j2 = (u >> 18) & 1; // bit 18
4219                    let s = (u >> 19) & 1; // sign (range-checked above)
4220
4221                    let hw1: u16 = (0xF000 | (s << 10) | ((cond_bits as u32) << 6) | imm6) as u16;
4222                    let hw2: u16 = (0x8000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4223
4224                    let mut bytes = hw1.to_le_bytes().to_vec();
4225                    bytes.extend_from_slice(&hw2.to_le_bytes());
4226                    Ok(bytes)
4227                }
4228            }
4229
4230            ArmOp::Bl { label: _ } => {
4231                // BL is always 32-bit in Thumb-2, encoded here as a relocatable
4232                // placeholder; an R_ARM_THM_CALL relocation patches the target
4233                // (see arm_backend.rs). The placeholder must carry an embedded
4234                // addend of -4 so the relocation nets to exactly the symbol S.
4235                //
4236                // Thumb BL computes `target = (P + 4) + signed_offset`. Under
4237                // R_ARM_THM_CALL the linker resolves using the in-place addend;
4238                // a 0xF800 placeholder (addend 0) lands at S+4 — every call one
4239                // instruction past the callee entry (#174). The correct
4240                // placeholder is what `gas` emits for `bl <extern>`:
4241                //   f7ff fffe  ->  `bl <self>`  (S=1, J1=J2=1, imm = -4 addend),
4242                // i.e. hw1=0xF7FF, hw2=0xFFFE. This nets to S, not S+4.
4243                // (The earlier 0xD000 was worse still — a ~+0x600000 addend,
4244                // the garbage `bl c0000c` and "truncated to fit" of #167.)
4245                let hw1: u16 = 0xF7FF;
4246                let hw2: u16 = 0xFFFE;
4247                let mut bytes = hw1.to_le_bytes().to_vec();
4248                bytes.extend_from_slice(&hw2.to_le_bytes());
4249                Ok(bytes)
4250            }
4251
4252            // MVN
4253            ArmOp::Mvn { rd, op2 } => {
4254                if let Operand2::Reg(rm) = op2 {
4255                    let rd_bits = reg_to_bits(rd) as u16;
4256                    let rm_bits = reg_to_bits(rm) as u16;
4257
4258                    if rd_bits < 8 && rm_bits < 8 {
4259                        // MVNS Rd, Rm (16-bit): 0100 0011 11 Rm Rd
4260                        let instr: u16 = 0x43C0 | (rm_bits << 3) | rd_bits;
4261                        Ok(instr.to_le_bytes().to_vec())
4262                    } else {
4263                        // 32-bit MVN
4264                        let hw1: u16 = 0xEA6F_u16;
4265                        let hw2: u16 = ((reg_to_bits(rd) << 8) | reg_to_bits(rm)) as u16;
4266                        let mut bytes = hw1.to_le_bytes().to_vec();
4267                        bytes.extend_from_slice(&hw2.to_le_bytes());
4268                        Ok(bytes)
4269                    }
4270                } else {
4271                    let instr: u16 = 0xBF00;
4272                    Ok(instr.to_le_bytes().to_vec())
4273                }
4274            }
4275
4276            // MOVW - Move Wide (Thumb-2 32-bit)
4277            ArmOp::Movw { rd, imm16 } => {
4278                self.encode_thumb32_movw_raw(reg_to_bits(rd), *imm16 as u32)
4279            }
4280
4281            // MOVT - Move Top (Thumb-2 32-bit)
4282            ArmOp::Movt { rd, imm16 } => {
4283                self.encode_thumb32_movt_raw(reg_to_bits(rd), *imm16 as u32)
4284            }
4285
4286            // #237: symbol-relative MOVW/MOVT. Encode the addend's low/high 16
4287            // bits in place; the backend records an R_ARM_MOVW_ABS_NC /
4288            // R_ARM_MOVT_ABS relocation against `symbol`, so the linker adds the
4289            // symbol's final address to the in-place addend (REL semantics).
4290            ArmOp::MovwSym { rd, addend, .. } => {
4291                self.encode_thumb32_movw_raw(reg_to_bits(rd), (*addend as u32) & 0xffff)
4292            }
4293            ArmOp::MovtSym { rd, addend, .. } => {
4294                self.encode_thumb32_movt_raw(reg_to_bits(rd), ((*addend as u32) >> 16) & 0xffff)
4295            }
4296
4297            // #345: literal-pool address load — emit a PLACEHOLDER `LDR.W rd,
4298            // [pc, #0]` (U=1, imm12=0). The backend (arm_backend.rs) places the
4299            // 4-byte pool word at the end of the function, records the R_ARM_ABS32
4300            // relocation against `symbol+addend`, and patches the imm12 with the
4301            // real PC-relative distance once the pool offset is known.
4302            // Encoding T2: 1111 1000 1101 1111 | Rt(4) imm12(12), with the literal
4303            // base = Align(PC,4) and PC = address of this instruction + 4.
4304            ArmOp::LdrSym { rd, .. } => {
4305                let rt = reg_to_bits(rd) as u16;
4306                let hw1: u16 = 0xF8DF; // LDR.W (literal), U=1
4307                let hw2: u16 = rt << 12; // imm12 = 0 placeholder
4308                let mut bytes = Vec::with_capacity(4);
4309                bytes.extend_from_slice(&hw1.to_le_bytes());
4310                bytes.extend_from_slice(&hw2.to_le_bytes());
4311                Ok(bytes)
4312            }
4313
4314            // SetCond: Materialize condition flag into register (0 or 1)
4315            // Strategy: ITE <cond>; MOV Rd, #1; MOV Rd, #0
4316            // IMPORTANT: Must use ITE (If-Then-Else) because 16-bit Thumb MOV
4317            // always sets flags (MOVS). We need to evaluate the condition BEFORE
4318            // any MOV instruction clobbers the flags from CMP.
4319            ArmOp::SetCond { rd, cond } => {
4320                let rd_bits = reg_to_bits(rd) as u16;
4321
4322                // Condition code encoding for IT block
4323                use synth_synthesis::Condition;
4324                let cond_bits: u16 = match cond {
4325                    Condition::EQ => 0x0,
4326                    Condition::NE => 0x1,
4327                    Condition::LT => 0xB,
4328                    Condition::LE => 0xD,
4329                    Condition::GT => 0xC,
4330                    Condition::GE => 0xA,
4331                    Condition::LO => 0x3, // CC/LO (unsigned <)
4332                    Condition::LS => 0x9, // LS (unsigned <=)
4333                    Condition::HI => 0x8, // HI (unsigned >)
4334                    Condition::HS => 0x2, // CS/HS (unsigned >=)
4335                };
4336
4337                // ITE <cond>: encodes If-Then-Else block
4338                // The mask field depends on firstcond[0]:
4339                // - If firstcond[0] = 0: mask = 0xC for TE pattern (ITE EQ = BF0C)
4340                // - If firstcond[0] = 1: mask = 0x4 for TE pattern (ITE NE = BF14)
4341                let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4342                let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4343
4344                // Materialize 0/1 into Rd. The 16-bit MOVS (T1) encodes Rd in a
4345                // 3-bit field (bits[10:8]) — only R0–R7. For a high register
4346                // (R8–R12) `rd_bits << 8` overflows into bit 11 and silently
4347                // turns MOVS into CMP (00100 → 00101), corrupting the result
4348                // (this mis-materialized gale's `has_waiter`, so its `local.set`
4349                // stored a stale register → the binary-sem WAKE dispatch read
4350                // garbage). Use the 32-bit MOV.W (T2) for high registers, which
4351                // has a 4-bit Rd field. MOV.W with S=0 doesn't set flags, which
4352                // is fine inside the ITE (the materialized value is the result;
4353                // the flags are not consumed afterwards).
4354                let mut bytes = ite_instr.to_le_bytes().to_vec();
4355                let push_mov = |bytes: &mut Vec<u8>, imm: u16| {
4356                    if rd_bits <= 7 {
4357                        let m: u16 = 0x2000 | (rd_bits << 8) | imm; // 16-bit MOVS Rd,#imm
4358                        bytes.extend_from_slice(&m.to_le_bytes());
4359                    } else {
4360                        // 32-bit MOV.W Rd, #imm (T2): F04F | (Rd<<8) | imm8
4361                        let hw1: u16 = 0xF04F;
4362                        let hw2: u16 = (rd_bits << 8) | imm;
4363                        bytes.extend_from_slice(&hw1.to_le_bytes());
4364                        bytes.extend_from_slice(&hw2.to_le_bytes());
4365                    }
4366                };
4367                push_mov(&mut bytes, 1); // Then branch (condition true)  → 1
4368                push_mov(&mut bytes, 0); // Else branch (condition false) → 0
4369                Ok(bytes)
4370            }
4371
4372            // I64SetCond: Compare two i64 register pairs, result 0/1 in rd
4373            // EQ/NE: CMP lo,lo; IT EQ; CMPEQ hi,hi; ITE <cond>; MOV 1; MOV 0
4374            // LT: CMP lo,lo; SBCS rd,hi,hi; ITE LT; MOV 1; MOV 0
4375            // GT: CMP lo,lo (swapped); SBCS rd,hi,hi (swapped); ITE LT; MOV 1; MOV 0
4376            ArmOp::I64SetCond {
4377                rd,
4378                rn_lo,
4379                rn_hi,
4380                rm_lo,
4381                rm_hi,
4382                cond,
4383            } => {
4384                use synth_synthesis::Condition;
4385                let rd_bits = reg_to_bits(rd) as u16;
4386                let mut bytes = Vec::new();
4387
4388                // Helper: encode CMP Rn, Rm (16-bit)
4389                let encode_cmp_reg = |rn: &synth_synthesis::Reg,
4390                                      rm: &synth_synthesis::Reg|
4391                 -> Vec<u8> {
4392                    let rn_bits = reg_to_bits(rn) as u16;
4393                    let rm_bits = reg_to_bits(rm) as u16;
4394                    if rn_bits < 8 && rm_bits < 8 {
4395                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
4396                        instr.to_le_bytes().to_vec()
4397                    } else {
4398                        let n_bit = (rn_bits >> 3) & 1;
4399                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
4400                        instr.to_le_bytes().to_vec()
4401                    }
4402                };
4403
4404                // Helper: encode ITE <cond> (2 bytes)
4405                let encode_ite = |cond_bits: u16| -> Vec<u8> {
4406                    let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4407                    let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4408                    ite_instr.to_le_bytes().to_vec()
4409                };
4410
4411                // Helper: encode SetCond (ITE + MOV #1 + MOV #0) for given condition
4412                let encode_setcond = |cond_bits: u16, rd_bits: u16| -> Vec<u8> {
4413                    let mut b = encode_ite(cond_bits);
4414                    if rd_bits < 8 {
4415                        let mov_one: u16 = 0x2001 | (rd_bits << 8);
4416                        let mov_zero: u16 = 0x2000 | (rd_bits << 8);
4417                        b.extend_from_slice(&mov_one.to_le_bytes());
4418                        b.extend_from_slice(&mov_zero.to_le_bytes());
4419                    } else {
4420                        // #311: rd >= R8 — the 16-bit MOV imm8 form has a 3-bit
4421                        // rd field; rd_bits<<8 overflows into bit 11 and
4422                        // TRANSMUTES the MOV into CMP (0x2001|0x0800 = 0x2801 =
4423                        // CMP r0,#1): the boolean dies in the flags and the
4424                        // consumer reads a stale register. Use the 32-bit
4425                        // MOV.W (T2: F04F 0000|rd<<8|imm8) — IT-legal,
4426                        // flag-preserving. Same class as H-CODE-9 / #180.
4427                        for imm in [1u16, 0u16] {
4428                            let hw1: u16 = 0xF04F;
4429                            let hw2: u16 = (rd_bits << 8) | imm;
4430                            b.extend_from_slice(&hw1.to_le_bytes());
4431                            b.extend_from_slice(&hw2.to_le_bytes());
4432                        }
4433                    }
4434                    b
4435                };
4436
4437                match cond {
4438                    Condition::EQ | Condition::NE => {
4439                        // CMP rn_lo, rm_lo (compare low words)
4440                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4441
4442                        // IT EQ (execute next instruction only if Z=1)
4443                        let it_eq: u16 = 0xBF08; // IT EQ: cond=0000, mask=1000
4444                        bytes.extend_from_slice(&it_eq.to_le_bytes());
4445
4446                        // CMPEQ rn_hi, rm_hi (compare high words, only if low equal)
4447                        bytes.extend_from_slice(&encode_cmp_reg(rn_hi, rm_hi));
4448
4449                        // ITE <cond>; MOV rd, #1; MOV rd, #0
4450                        let cond_bits: u16 = match cond {
4451                            Condition::EQ => 0x0,
4452                            Condition::NE => 0x1,
4453                            _ => unreachable!(),
4454                        };
4455                        bytes.extend_from_slice(&encode_setcond(cond_bits, rd_bits));
4456                    }
4457
4458                    Condition::LT => {
4459                        // CMP rn_lo, rm_lo (sets C flag for borrow)
4460                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4461
4462                        // SBCS rd, rn_hi, rm_hi (subtract with carry, sets N,V flags)
4463                        // SBCS.W Rd, Rn, Rm: EB70 Rn | 0000 Rd 0000 Rm
4464                        let rn_hi_bits = reg_to_bits(rn_hi);
4465                        let rm_hi_bits = reg_to_bits(rm_hi);
4466                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4467                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4468                        bytes.extend_from_slice(&hw1.to_le_bytes());
4469                        bytes.extend_from_slice(&hw2.to_le_bytes());
4470
4471                        // ITE LT; MOV rd, #1; MOV rd, #0
4472                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4473                    }
4474
4475                    Condition::GT => {
4476                        // GT(a,b) = LT(b,a): swap operands
4477                        // CMP rm_lo, rn_lo (swapped)
4478                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4479
4480                        // SBCS rd, rm_hi, rn_hi (swapped)
4481                        let rm_hi_bits = reg_to_bits(rm_hi);
4482                        let rn_hi_bits = reg_to_bits(rn_hi);
4483                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4484                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4485                        bytes.extend_from_slice(&hw1.to_le_bytes());
4486                        bytes.extend_from_slice(&hw2.to_le_bytes());
4487
4488                        // ITE LT; MOV rd, #1; MOV rd, #0
4489                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4490                    }
4491
4492                    Condition::LE => {
4493                        // LE(a,b) = !GT(a,b): use GT logic but invert result
4494                        // GT(a,b) = LT(b,a): so we do CMP(b,a) and check LT, then invert
4495                        // CMP rm_lo, rn_lo (swapped, same as GT)
4496                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4497
4498                        // SBCS rd, rm_hi, rn_hi (swapped)
4499                        let rm_hi_bits = reg_to_bits(rm_hi);
4500                        let rn_hi_bits = reg_to_bits(rn_hi);
4501                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4502                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4503                        bytes.extend_from_slice(&hw1.to_le_bytes());
4504                        bytes.extend_from_slice(&hw2.to_le_bytes());
4505
4506                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT, so inverting GT result)
4507                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4508                    }
4509
4510                    Condition::GE => {
4511                        // GE(a,b) = !LT(a,b): use LT logic but invert result
4512                        // CMP rn_lo, rm_lo (same as LT)
4513                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4514
4515                        // SBCS rd, rn_hi, rm_hi (same as LT)
4516                        let rn_hi_bits = reg_to_bits(rn_hi);
4517                        let rm_hi_bits = reg_to_bits(rm_hi);
4518                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4519                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4520                        bytes.extend_from_slice(&hw1.to_le_bytes());
4521                        bytes.extend_from_slice(&hw2.to_le_bytes());
4522
4523                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT)
4524                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4525                    }
4526
4527                    // Unsigned comparisons - same instruction sequence, different conditions
4528                    Condition::LO => {
4529                        // LO (unsigned LT): CMP lo, SBCS hi, check C=0
4530                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4531                        let rn_hi_bits = reg_to_bits(rn_hi);
4532                        let rm_hi_bits = reg_to_bits(rm_hi);
4533                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4534                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4535                        bytes.extend_from_slice(&hw1.to_le_bytes());
4536                        bytes.extend_from_slice(&hw2.to_le_bytes());
4537                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4538                    }
4539
4540                    Condition::HI => {
4541                        // HI (unsigned GT): swap operands and check LO
4542                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4543                        let rm_hi_bits = reg_to_bits(rm_hi);
4544                        let rn_hi_bits = reg_to_bits(rn_hi);
4545                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4546                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4547                        bytes.extend_from_slice(&hw1.to_le_bytes());
4548                        bytes.extend_from_slice(&hw2.to_le_bytes());
4549                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4550                    }
4551
4552                    Condition::LS => {
4553                        // LS (unsigned LE): !(a > b) = !(HI), so do HI and invert
4554                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4555                        let rm_hi_bits = reg_to_bits(rm_hi);
4556                        let rn_hi_bits = reg_to_bits(rn_hi);
4557                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4558                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4559                        bytes.extend_from_slice(&hw1.to_le_bytes());
4560                        bytes.extend_from_slice(&hw2.to_le_bytes());
4561                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4562                    }
4563
4564                    Condition::HS => {
4565                        // HS (unsigned GE): !(a < b) = !(LO)
4566                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4567                        let rn_hi_bits = reg_to_bits(rn_hi);
4568                        let rm_hi_bits = reg_to_bits(rm_hi);
4569                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4570                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4571                        bytes.extend_from_slice(&hw1.to_le_bytes());
4572                        bytes.extend_from_slice(&hw2.to_le_bytes());
4573                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4574                    }
4575                }
4576
4577                Ok(bytes)
4578            }
4579
4580            // I64SetCondZ: Test if i64 register pair is zero, result 0/1 in rd
4581            // ORR.W rd, rn_lo, rn_hi; CMP rd, #0; ITE EQ; MOV 1; MOV 0
4582            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
4583                let rd_bits = reg_to_bits(rd);
4584                let rn_lo_bits = reg_to_bits(rn_lo);
4585                let rn_hi_bits = reg_to_bits(rn_hi);
4586                let mut bytes = Vec::new();
4587
4588                // ORR.W rd, rn_lo, rn_hi: EA40 rn_lo | 0000 rd 0000 rn_hi
4589                let hw1: u16 = (0xEA40 | rn_lo_bits) as u16;
4590                let hw2: u16 = ((rd_bits << 8) | rn_hi_bits) as u16;
4591                bytes.extend_from_slice(&hw1.to_le_bytes());
4592                bytes.extend_from_slice(&hw2.to_le_bytes());
4593
4594                // CMP rd, #0 — 16-bit form only for r0-r7 (3-bit rd field);
4595                // high registers take CMP.W (T2: F1B0|rn 0F00|imm8). This was
4596                // H-CODE-9: rd_bits<<8 overflowing the field compared the
4597                // WRONG register. Same hardening as the #311 SetCond fix.
4598                if rd_bits < 8 {
4599                    let cmp_instr: u16 = 0x2800 | ((rd_bits as u16) << 8);
4600                    bytes.extend_from_slice(&cmp_instr.to_le_bytes());
4601                } else {
4602                    let hw1: u16 = 0xF1B0 | (rd_bits as u16);
4603                    let hw2: u16 = 0x0F00;
4604                    bytes.extend_from_slice(&hw1.to_le_bytes());
4605                    bytes.extend_from_slice(&hw2.to_le_bytes());
4606                }
4607
4608                // ITE EQ; MOV rd, #1; MOV rd, #0 (32-bit MOV.W for rd >= R8,
4609                // #311 — see I64SetCond)
4610                let mask = 0xC_u16; // ITE EQ mask: firstcond[0]=0, mask=0xC
4611                let ite_instr: u16 = 0xBF00 | mask;
4612                bytes.extend_from_slice(&ite_instr.to_le_bytes());
4613                if rd_bits < 8 {
4614                    let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
4615                    let mov_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
4616                    bytes.extend_from_slice(&mov_one.to_le_bytes());
4617                    bytes.extend_from_slice(&mov_zero.to_le_bytes());
4618                } else {
4619                    for imm in [1u16, 0u16] {
4620                        let hw1: u16 = 0xF04F;
4621                        let hw2: u16 = ((rd_bits as u16) << 8) | imm;
4622                        bytes.extend_from_slice(&hw1.to_le_bytes());
4623                        bytes.extend_from_slice(&hw2.to_le_bytes());
4624                    }
4625                }
4626
4627                Ok(bytes)
4628            }
4629
4630            // I64Mul: 64-bit multiply using UMULL + MLA cross products
4631            // Formula: result = (a_lo * b_lo) + ((a_lo * b_hi + a_hi * b_lo) << 32)
4632            // Uses R12 as scratch register
4633            ArmOp::I64Mul {
4634                rd_lo,
4635                rd_hi,
4636                rn_lo,
4637                rn_hi,
4638                rm_lo,
4639                rm_hi,
4640            } => {
4641                let rd_lo_bits = reg_to_bits(rd_lo);
4642                let rd_hi_bits = reg_to_bits(rd_hi);
4643                let rn_lo_bits = reg_to_bits(rn_lo);
4644                let rn_hi_bits = reg_to_bits(rn_hi);
4645                let rm_lo_bits = reg_to_bits(rm_lo);
4646                let rm_hi_bits = reg_to_bits(rm_hi);
4647                let r12: u32 = 12; // IP scratch register
4648                let mut bytes = Vec::new();
4649
4650                // 1. MUL R12, rn_lo, rm_hi  (R12 = a_lo * b_hi)
4651                // Thumb-2 MUL: hw1=0xFB00|Rn, hw2=0xF000|(Rd<<8)|Rm
4652                let hw1: u16 = (0xFB00 | rn_lo_bits) as u16;
4653                let hw2: u16 = (0xF000 | (r12 << 8) | rm_hi_bits) as u16;
4654                bytes.extend_from_slice(&hw1.to_le_bytes());
4655                bytes.extend_from_slice(&hw2.to_le_bytes());
4656
4657                // 2. MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
4658                // Thumb-2 MLA: hw1=0xFB00|Rn, hw2=(Ra<<12)|(Rd<<8)|Rm
4659                let hw1: u16 = (0xFB00 | rn_hi_bits) as u16;
4660                let hw2: u16 = ((r12 << 12) | (r12 << 8) | rm_lo_bits) as u16;
4661                bytes.extend_from_slice(&hw1.to_le_bytes());
4662                bytes.extend_from_slice(&hw2.to_le_bytes());
4663
4664                // 3. UMULL rd_lo, rd_hi, rn_lo, rm_lo  (rd_lo:rd_hi = a_lo * b_lo)
4665                // Thumb-2 UMULL: hw1=0xFBA0|Rn, hw2=(RdLo<<12)|(RdHi<<8)|Rm
4666                let hw1: u16 = (0xFBA0 | rn_lo_bits) as u16;
4667                let hw2: u16 = ((rd_lo_bits << 12) | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4668                bytes.extend_from_slice(&hw1.to_le_bytes());
4669                bytes.extend_from_slice(&hw2.to_le_bytes());
4670
4671                // 4. ADD rd_hi, R12  (rd_hi += cross products)
4672                // 16-bit high reg ADD: 01000100 D Rm Rdn[2:0]
4673                let d_bit = (rd_hi_bits >> 3) & 1;
4674                let add_instr: u16 =
4675                    (0x4400 | (d_bit << 7) | (r12 << 3) | (rd_hi_bits & 0x7)) as u16;
4676                bytes.extend_from_slice(&add_instr.to_le_bytes());
4677
4678                Ok(bytes)
4679            }
4680
4681            // I64Shl: 64-bit shift left with branch for n<32 vs n>=32.
4682            //
4683            // #1048: the expansion must NEVER write its own input operands.
4684            // The pre-#1048 sequence masked the amount IN PLACE
4685            // (`AND.W rm_lo, rm_lo, #63`) and used the amount's home high
4686            // register `rm_hi` as scratch (`SUBS.W rm_hi, rm_lo, #32`, RSB,
4687            // LSR) — so re-reading the amount after the shift returned a
4688            // mangled value (amt=64 read back 0, amt=67 read back 3). The
4689            // rewrite uses R12 — encoder scratch, never allocatable (#212) —
4690            // as the ONLY temporary, re-deriving the masked amount from the
4691            // untouched rm_lo whenever a second live temp would otherwise be
4692            // needed. This matches the Rocq/SMT pseudo-op models
4693            // (I64ShlPseudo writes rd_lo/rd_hi ONLY), which were proven over
4694            // exactly this non-clobbering contract all along.
4695            ArmOp::I64Shl {
4696                rd_lo,
4697                rd_hi,
4698                rn_lo,
4699                rn_hi,
4700                rm_lo,
4701                rm_hi: _,
4702            } => {
4703                let rd_lo_bits = reg_to_bits(rd_lo);
4704                let rd_hi_bits = reg_to_bits(rd_hi);
4705                let rn_lo_bits = reg_to_bits(rn_lo);
4706                let rn_hi_bits = reg_to_bits(rn_hi);
4707                let rm_lo_bits = reg_to_bits(rm_lo);
4708                let r12: u32 = 12; // the only scratch — never allocatable
4709                let mut bytes = Vec::new();
4710
4711                // #1039 house style: refuse a destination that would collide
4712                // with an input still needed after the destination is first
4713                // written, loudly — never misassemble. rd_hi is written before
4714                // rn_lo and rm_lo are last read; the in-place form
4715                // rd == rn (select_default) has rd_hi == rn_hi and stays legal.
4716                if rd_hi_bits == rn_lo_bits || rd_hi_bits == rm_lo_bits {
4717                    return Err(synth_core::Error::synthesis(format!(
4718                        "I64Shl: rd_hi {rd_hi:?} aliases an input ({rn_lo:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4719                    )));
4720                }
4721
4722                // AND.W R12, rm_lo, #63  (n — the amount register is only READ)
4723                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4724                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4725                bytes.extend_from_slice(&hw1.to_le_bytes());
4726                bytes.extend_from_slice(&hw2.to_le_bytes());
4727
4728                // SUBS.W R12, R12, #32  (R12 = n-32, sets flags)
4729                let hw1: u16 = (0xF1B0 | r12) as u16;
4730                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4731                bytes.extend_from_slice(&hw1.to_le_bytes());
4732                bytes.extend_from_slice(&hw2.to_le_bytes());
4733
4734                // BPL .large (branch if n >= 32, offset = +14 halfwords)
4735                let bpl: u16 = 0xD50E;
4736                bytes.extend_from_slice(&bpl.to_le_bytes());
4737
4738                // --- Small shift (n < 32) ---
4739                // AND.W R12, rm_lo, #63  (n again — R12 held n-32)
4740                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4741                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4742                bytes.extend_from_slice(&hw1.to_le_bytes());
4743                bytes.extend_from_slice(&hw2.to_le_bytes());
4744
4745                // LSL.W rd_hi, rn_hi, R12  (hi << n; rn_hi's last read)
4746                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4747                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4748                bytes.extend_from_slice(&hw1.to_le_bytes());
4749                bytes.extend_from_slice(&hw2.to_le_bytes());
4750
4751                // RSB.W R12, R12, #32  (R12 = 32-n)
4752                let hw1: u16 = (0xF1C0 | r12) as u16;
4753                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4754                bytes.extend_from_slice(&hw1.to_le_bytes());
4755                bytes.extend_from_slice(&hw2.to_le_bytes());
4756
4757                // LSR.W R12, rn_lo, R12  (overflow = lo >> (32-n); n=0 gives
4758                // a register shift by 32 which yields 0 — exact)
4759                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4760                let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4761                bytes.extend_from_slice(&hw1.to_le_bytes());
4762                bytes.extend_from_slice(&hw2.to_le_bytes());
4763
4764                // ORR.W rd_hi, rd_hi, R12  (hi |= overflow bits from lo)
4765                let hw1: u16 = (0xEA40 | rd_hi_bits) as u16;
4766                let hw2: u16 = ((rd_hi_bits << 8) | r12) as u16;
4767                bytes.extend_from_slice(&hw1.to_le_bytes());
4768                bytes.extend_from_slice(&hw2.to_le_bytes());
4769
4770                // AND.W R12, rm_lo, #63  (n once more for the low half)
4771                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4772                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4773                bytes.extend_from_slice(&hw1.to_le_bytes());
4774                bytes.extend_from_slice(&hw2.to_le_bytes());
4775
4776                // LSL.W rd_lo, rn_lo, R12  (lo << n)
4777                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4778                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4779                bytes.extend_from_slice(&hw1.to_le_bytes());
4780                bytes.extend_from_slice(&hw2.to_le_bytes());
4781
4782                // B .done — `.done` is the END of the expansion, i.e. PAST the
4783                // large-shift arm's trailing zero-fill. #916: that zero-fill is
4784                // 1 halfword for a low rd_lo but 2 for R8-R12 (MOV.W), so the
4785                // displacement is DERIVED from its real width instead of the
4786                // hard-coded 0xE002 — widening the MOV without this would
4787                // overshoot `.done` and turn a data miscompile into a
4788                // control-flow one. Thumb `B` reads PC as its own address + 4
4789                // (= +2 halfwords), so imm11 = (large-arm halfwords) - 1.
4790                let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_lo_bits);
4791                let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4792                bytes.extend_from_slice(&b_done.to_le_bytes());
4793
4794                // --- Large shift (n >= 32) --- (R12 still holds n-32)
4795                // LSL.W rd_hi, rn_lo, R12  (hi = lo << (n-32))
4796                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4797                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4798                bytes.extend_from_slice(&hw1.to_le_bytes());
4799                bytes.extend_from_slice(&hw2.to_le_bytes());
4800
4801                // MOV rd_lo, #0 (#916: MOV.W for rd_lo >= R8). NOTE the order
4802                // is load-bearing — zeroing rd_lo BEFORE the LSL.W would
4803                // destroy rn_lo in the in-place case rd_lo == rn_lo, so this
4804                // cannot be reordered to dodge the displacement change.
4805                emit_thumb_zero_fill(&mut bytes, rd_lo_bits);
4806
4807                Ok(bytes) // 46 bytes (48 when rd_lo >= R8 takes MOV.W)
4808            }
4809
4810            // I64ShrU: 64-bit logical shift right with branch for n<32 vs
4811            // n>=32. #1048: R12-only scratch, operands never written — see
4812            // the I64Shl comment for the full rationale.
4813            ArmOp::I64ShrU {
4814                rd_lo,
4815                rd_hi,
4816                rn_lo,
4817                rn_hi,
4818                rm_lo,
4819                rm_hi: _,
4820            } => {
4821                let rd_lo_bits = reg_to_bits(rd_lo);
4822                let rd_hi_bits = reg_to_bits(rd_hi);
4823                let rn_lo_bits = reg_to_bits(rn_lo);
4824                let rn_hi_bits = reg_to_bits(rn_hi);
4825                let rm_lo_bits = reg_to_bits(rm_lo);
4826                let r12: u32 = 12; // the only scratch — never allocatable
4827                let mut bytes = Vec::new();
4828
4829                // #1039 house style: rd_lo is written before rn_hi and rm_lo
4830                // are last read — refuse the collision loudly. The in-place
4831                // form rd == rn (select_default) has rd_lo == rn_lo and stays
4832                // legal.
4833                if rd_lo_bits == rn_hi_bits || rd_lo_bits == rm_lo_bits {
4834                    return Err(synth_core::Error::synthesis(format!(
4835                        "I64ShrU: rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4836                    )));
4837                }
4838
4839                // AND.W R12, rm_lo, #63  (n — the amount register is only READ)
4840                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4841                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4842                bytes.extend_from_slice(&hw1.to_le_bytes());
4843                bytes.extend_from_slice(&hw2.to_le_bytes());
4844
4845                // SUBS.W R12, R12, #32  (R12 = n-32, sets flags)
4846                let hw1: u16 = (0xF1B0 | r12) as u16;
4847                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4848                bytes.extend_from_slice(&hw1.to_le_bytes());
4849                bytes.extend_from_slice(&hw2.to_le_bytes());
4850
4851                // BPL .large (+14 halfwords)
4852                let bpl: u16 = 0xD50E;
4853                bytes.extend_from_slice(&bpl.to_le_bytes());
4854
4855                // --- Small shift (n < 32) ---
4856                // AND.W R12, rm_lo, #63  (n again — R12 held n-32)
4857                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4858                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4859                bytes.extend_from_slice(&hw1.to_le_bytes());
4860                bytes.extend_from_slice(&hw2.to_le_bytes());
4861
4862                // LSR.W rd_lo, rn_lo, R12  (lo >> n; rn_lo's last read)
4863                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4864                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4865                bytes.extend_from_slice(&hw1.to_le_bytes());
4866                bytes.extend_from_slice(&hw2.to_le_bytes());
4867
4868                // RSB.W R12, R12, #32  (R12 = 32-n)
4869                let hw1: u16 = (0xF1C0 | r12) as u16;
4870                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4871                bytes.extend_from_slice(&hw1.to_le_bytes());
4872                bytes.extend_from_slice(&hw2.to_le_bytes());
4873
4874                // LSL.W R12, rn_hi, R12  (overflow = hi << (32-n); n=0 gives
4875                // a register shift by 32 which yields 0 — exact)
4876                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4877                let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4878                bytes.extend_from_slice(&hw1.to_le_bytes());
4879                bytes.extend_from_slice(&hw2.to_le_bytes());
4880
4881                // ORR.W rd_lo, rd_lo, R12  (lo |= overflow from hi)
4882                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4883                let hw2: u16 = ((rd_lo_bits << 8) | r12) as u16;
4884                bytes.extend_from_slice(&hw1.to_le_bytes());
4885                bytes.extend_from_slice(&hw2.to_le_bytes());
4886
4887                // AND.W R12, rm_lo, #63  (n once more for the high half)
4888                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4889                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4890                bytes.extend_from_slice(&hw1.to_le_bytes());
4891                bytes.extend_from_slice(&hw2.to_le_bytes());
4892
4893                // LSR.W rd_hi, rn_hi, R12  (hi >> n, logical)
4894                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4895                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4896                bytes.extend_from_slice(&hw1.to_le_bytes());
4897                bytes.extend_from_slice(&hw2.to_le_bytes());
4898
4899                // B .done — see I64Shl: `.done` is the END of the expansion,
4900                // past the trailing zero-fill, so the displacement is derived
4901                // from that zero-fill's real width (#916).
4902                let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_hi_bits);
4903                let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4904                bytes.extend_from_slice(&b_done.to_le_bytes());
4905
4906                // --- Large shift (n >= 32) --- (R12 still holds n-32)
4907                // LSR.W rd_lo, rn_hi, R12  (lo = hi >> (n-32))
4908                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4909                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4910                bytes.extend_from_slice(&hw1.to_le_bytes());
4911                bytes.extend_from_slice(&hw2.to_le_bytes());
4912
4913                // MOV rd_hi, #0 (#916: MOV.W for rd_hi >= R8). Order is
4914                // load-bearing: the LSR.W above reads rn_hi, which may BE
4915                // rd_hi in the in-place case.
4916                emit_thumb_zero_fill(&mut bytes, rd_hi_bits);
4917
4918                Ok(bytes) // 46 bytes (48 when rd_hi >= R8 takes MOV.W)
4919            }
4920
4921            // I64ShrS: 64-bit arithmetic shift right with branch for n<32 vs
4922            // n>=32. #1048: R12-only scratch, operands never written — see
4923            // the I64Shl comment for the full rationale.
4924            ArmOp::I64ShrS {
4925                rd_lo,
4926                rd_hi,
4927                rn_lo,
4928                rn_hi,
4929                rm_lo,
4930                rm_hi: _,
4931            } => {
4932                let rd_lo_bits = reg_to_bits(rd_lo);
4933                let rd_hi_bits = reg_to_bits(rd_hi);
4934                let rn_lo_bits = reg_to_bits(rn_lo);
4935                let rn_hi_bits = reg_to_bits(rn_hi);
4936                let rm_lo_bits = reg_to_bits(rm_lo);
4937                let r12: u32 = 12; // the only scratch — never allocatable
4938                let mut bytes = Vec::new();
4939
4940                // #1039 house style: rd_lo is written before rn_hi and rm_lo
4941                // are last read (on BOTH arms of the diamond — the large arm's
4942                // trailing `ASR rd_hi, rn_hi, #31` also reads rn_hi after
4943                // rd_lo is written). The in-place form rd == rn stays legal.
4944                if rd_lo_bits == rn_hi_bits || rd_lo_bits == rm_lo_bits {
4945                    return Err(synth_core::Error::synthesis(format!(
4946                        "I64ShrS: rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4947                    )));
4948                }
4949
4950                // AND.W R12, rm_lo, #63  (n — the amount register is only READ)
4951                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4952                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4953                bytes.extend_from_slice(&hw1.to_le_bytes());
4954                bytes.extend_from_slice(&hw2.to_le_bytes());
4955
4956                // SUBS.W R12, R12, #32  (R12 = n-32, sets flags)
4957                let hw1: u16 = (0xF1B0 | r12) as u16;
4958                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4959                bytes.extend_from_slice(&hw1.to_le_bytes());
4960                bytes.extend_from_slice(&hw2.to_le_bytes());
4961
4962                // BPL .large (+14 halfwords)
4963                let bpl: u16 = 0xD50E;
4964                bytes.extend_from_slice(&bpl.to_le_bytes());
4965
4966                // --- Small shift (n < 32) ---
4967                // AND.W R12, rm_lo, #63  (n again — R12 held n-32)
4968                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4969                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4970                bytes.extend_from_slice(&hw1.to_le_bytes());
4971                bytes.extend_from_slice(&hw2.to_le_bytes());
4972
4973                // LSR.W rd_lo, rn_lo, R12  (lo >> n, logical for lo word)
4974                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4975                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4976                bytes.extend_from_slice(&hw1.to_le_bytes());
4977                bytes.extend_from_slice(&hw2.to_le_bytes());
4978
4979                // RSB.W R12, R12, #32  (R12 = 32-n)
4980                let hw1: u16 = (0xF1C0 | r12) as u16;
4981                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4982                bytes.extend_from_slice(&hw1.to_le_bytes());
4983                bytes.extend_from_slice(&hw2.to_le_bytes());
4984
4985                // LSL.W R12, rn_hi, R12  (overflow = hi << (32-n); n=0 gives
4986                // a register shift by 32 which yields 0 — exact)
4987                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4988                let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4989                bytes.extend_from_slice(&hw1.to_le_bytes());
4990                bytes.extend_from_slice(&hw2.to_le_bytes());
4991
4992                // ORR.W rd_lo, rd_lo, R12  (lo |= overflow from hi)
4993                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4994                let hw2: u16 = ((rd_lo_bits << 8) | r12) as u16;
4995                bytes.extend_from_slice(&hw1.to_le_bytes());
4996                bytes.extend_from_slice(&hw2.to_le_bytes());
4997
4998                // AND.W R12, rm_lo, #63  (n once more for the high half)
4999                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
5000                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
5001                bytes.extend_from_slice(&hw1.to_le_bytes());
5002                bytes.extend_from_slice(&hw2.to_le_bytes());
5003
5004                // ASR.W rd_hi, rn_hi, R12  (hi >> n, arithmetic/sign-extending)
5005                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
5006                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
5007                bytes.extend_from_slice(&hw1.to_le_bytes());
5008                bytes.extend_from_slice(&hw2.to_le_bytes());
5009
5010                // B .done (+3 halfwords, large shift is 8 bytes)
5011                let b_done: u16 = 0xE003;
5012                bytes.extend_from_slice(&b_done.to_le_bytes());
5013
5014                // --- Large shift (n >= 32) --- (R12 still holds n-32)
5015                // ASR.W rd_lo, rn_hi, R12  (lo = hi >>> (n-32))
5016                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
5017                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
5018                bytes.extend_from_slice(&hw1.to_le_bytes());
5019                bytes.extend_from_slice(&hw2.to_le_bytes());
5020
5021                // ASR.W rd_hi, rn_hi, #31  (hi = sign extension, all 0s or all 1s)
5022                // Thumb-2 ASR immediate: hw1=0xEA4F, hw2=imm3:Rd:imm2:10:Rm
5023                // imm5=31=11111 → imm3=111, imm2=11
5024                let hw1: u16 = 0xEA4F;
5025                let hw2: u16 = (0x7000 | (rd_hi_bits << 8) | 0x00E0 | rn_hi_bits) as u16;
5026                bytes.extend_from_slice(&hw1.to_le_bytes());
5027                bytes.extend_from_slice(&hw2.to_le_bytes());
5028
5029                Ok(bytes) // Total: 48 bytes
5030            }
5031
5032            // I64Rotl: 64-bit rotate left (#610 rewrite).
5033            // For n < 32: new_hi = (hi << n) | (lo >> (32-n)), new_lo = (lo << n) | (hi >> (32-n))
5034            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
5035            //
5036            // Fixed-reg core: value in R0:R1, amount in R2, scratch R3 + R12
5037            // (all four saved/marshaled by the #610 fixed-ABI wrapper; the
5038            // pre-#610 expansion wrote through the selector's registers with
5039            // colliding R3/R4 scratch and restored the saved R4 OVER the
5040            // result). Relies on ARM register-shift semantics: amounts >= 32
5041            // yield 0 for LSL/LSR, which makes n = 0 and n = 32 exact.
5042            ArmOp::I64Rotl {
5043                rdlo,
5044                rdhi,
5045                rnlo,
5046                rnhi,
5047                shift,
5048            } => {
5049                let mut bytes = Vec::new();
5050                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
5051
5052                let core: [u16; 35] = [
5053                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
5054                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
5055                    0xD50E, //         BPL    .large        (n >= 32)
5056                    // --- small rotation (n < 32) ---
5057                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
5058                    0xFA20, 0xFC03, // LSR.W  R12, R0, R3   (lo >> (32-n))
5059                    0xFA21, 0xF303, // LSR.W  R3, R1, R3    (hi >> (32-n))
5060                    0xFA01, 0xF102, // LSL.W  R1, R1, R2    (hi << n)
5061                    0xEA41, 0x010C, // ORR.W  R1, R1, R12   (new_hi)
5062                    0xFA00, 0xF002, // LSL.W  R0, R0, R2    (lo << n)
5063                    0xEA40, 0x0003, // ORR.W  R0, R0, R3    (new_lo)
5064                    0xE00E, //         B      .done
5065                    // --- large rotation (n >= 32), R3 = m = n-32 ---
5066                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
5067                    0xFA21, 0xFC02, // LSR.W  R12, R1, R2   (hi >> (64-n))
5068                    0xFA20, 0xF202, // LSR.W  R2, R0, R2    (lo >> (64-n))
5069                    0xFA00, 0xF003, // LSL.W  R0, R0, R3    (lo << m)
5070                    0xFA01, 0xF103, // LSL.W  R1, R1, R3    (hi << m)
5071                    0xEA40, 0x0C0C, // ORR.W  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
5072                    0xEA41, 0x0002, // ORR.W  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
5073                    0x4661, //         MOV    R1, R12       (new_hi into place)
5074                            // .done: result in R0:R1
5075                ];
5076                for hw in core {
5077                    bytes.extend_from_slice(&hw.to_le_bytes());
5078                }
5079
5080                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5081                Ok(bytes) // Total: 102 bytes
5082            }
5083
5084            // I64Rotr: 64-bit rotate right (#610 rewrite).
5085            // For n < 32: new_lo = (lo >> n) | (hi << (32-n)), new_hi = (hi >> n) | (lo << (32-n))
5086            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
5087            //
5088            // Same fixed-reg core contract as I64Rotl: value in R0:R1, amount
5089            // in R2, scratch R3 + R12, all covered by the fixed-ABI wrapper.
5090            ArmOp::I64Rotr {
5091                rdlo,
5092                rdhi,
5093                rnlo,
5094                rnhi,
5095                shift,
5096            } => {
5097                let mut bytes = Vec::new();
5098                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
5099
5100                let core: [u16; 35] = [
5101                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
5102                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
5103                    0xD50E, //         BPL    .large        (n >= 32)
5104                    // --- small rotation (n < 32) ---
5105                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
5106                    0xFA01, 0xFC03, // LSL.W  R12, R1, R3   (hi << (32-n))
5107                    0xFA00, 0xF303, // LSL.W  R3, R0, R3    (lo << (32-n))
5108                    0xFA20, 0xF002, // LSR.W  R0, R0, R2    (lo >> n)
5109                    0xEA40, 0x000C, // ORR.W  R0, R0, R12   (new_lo)
5110                    0xFA21, 0xF102, // LSR.W  R1, R1, R2    (hi >> n)
5111                    0xEA41, 0x0103, // ORR.W  R1, R1, R3    (new_hi)
5112                    0xE00E, //         B      .done
5113                    // --- large rotation (n >= 32), R3 = m = n-32 ---
5114                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
5115                    0xFA00, 0xFC02, // LSL.W  R12, R0, R2   (lo << (64-n))
5116                    0xFA01, 0xF202, // LSL.W  R2, R1, R2    (hi << (64-n))
5117                    0xFA21, 0xF103, // LSR.W  R1, R1, R3    (hi >> m)
5118                    0xEA41, 0x0C0C, // ORR.W  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
5119                    0xFA20, 0xF103, // LSR.W  R1, R0, R3    (lo >> m)
5120                    0xEA41, 0x0102, // ORR.W  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
5121                    0x4660, //         MOV    R0, R12       (new_lo into place)
5122                            // .done: result in R0:R1
5123                ];
5124                for hw in core {
5125                    bytes.extend_from_slice(&hw.to_le_bytes());
5126                }
5127
5128                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5129                Ok(bytes) // Total: 102 bytes
5130            }
5131
5132            // I64Clz: Count leading zeros in 64-bit value
5133            // If hi != 0: result = CLZ(hi)
5134            // If hi == 0: result = 32 + CLZ(lo)
5135            //
5136            // Layout (using CMP+BNE approach for consistency):
5137            // 0: CMP.W rnhi, #0 (4 bytes)
5138            // 4: BEQ .hi_zero (2 bytes) - branch forward to offset 14
5139            // 6: CLZ.W rd, rnhi (4 bytes)
5140            // 10: B .done (2 bytes) - branch forward to offset 22
5141            // 12: NOP (2 bytes) - padding for alignment
5142            // 14: .hi_zero: CLZ.W rd, rnlo (4 bytes)
5143            // 18: ADD.W rd, rd, #32 (4 bytes)
5144            // 22: .done
5145            ArmOp::I64Clz { rd, rnlo, rnhi } => {
5146                let rd_bits = reg_to_bits(rd);
5147                let rn_lo_bits = reg_to_bits(rnlo);
5148                let rn_hi_bits = reg_to_bits(rnhi);
5149                let mut bytes = Vec::new();
5150
5151                // CMP.W rnhi, #0 (4 bytes at offset 0)
5152                let hw1: u16 = (0xF1B0 | rn_hi_bits) as u16;
5153                let hw2: u16 = 0x0F00;
5154                bytes.extend_from_slice(&hw1.to_le_bytes());
5155                bytes.extend_from_slice(&hw2.to_le_bytes());
5156
5157                // BEQ .hi_zero (2 bytes at offset 4)
5158                // PC = 4 + 4 = 8, target = 14, offset = 6, imm8 = 3
5159                let beq: u16 = 0xD003;
5160                bytes.extend_from_slice(&beq.to_le_bytes());
5161
5162                // CLZ.W rd, rnhi (4 bytes at offset 6)
5163                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5164                let hw1: u16 = (0xFAB0 | rn_hi_bits) as u16;
5165                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_hi_bits) as u16;
5166                bytes.extend_from_slice(&hw1.to_le_bytes());
5167                bytes.extend_from_slice(&hw2.to_le_bytes());
5168
5169                // B .done (2 bytes at offset 10)
5170                // PC = 10 + 4 = 14, target = 22, offset = 8, imm11 = 4
5171                let b_done: u16 = 0xE004;
5172                bytes.extend_from_slice(&b_done.to_le_bytes());
5173
5174                // NOP (2 bytes at offset 12) - padding
5175                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5176
5177                // .hi_zero: (offset 14)
5178                // CLZ.W rd, rnlo (4 bytes)
5179                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5180                let hw1: u16 = (0xFAB0 | rn_lo_bits) as u16;
5181                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_lo_bits) as u16;
5182                bytes.extend_from_slice(&hw1.to_le_bytes());
5183                bytes.extend_from_slice(&hw2.to_le_bytes());
5184
5185                // ADD.W rd, rd, #32 (4 bytes at offset 18)
5186                let hw1: u16 = (0xF100 | rd_bits) as u16;
5187                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5188                bytes.extend_from_slice(&hw1.to_le_bytes());
5189                bytes.extend_from_slice(&hw2.to_le_bytes());
5190
5191                // .done: (offset 22 — the end of the expansion)
5192                //
5193                // #1048: the former trailing hi-word clear (`MOV rnhi, #0`)
5194                // is GONE. It was aimed at the RESULT's high half but wrote
5195                // the OPERAND's home high register — a real executed
5196                // miscompile on the direct selector, which allocates a fresh
5197                // destination pair and zeroes its own dst_hi, leaving the
5198                // operand's hi limb destroyed for any later re-read. The
5199                // Rocq/SMT models of I64ClzPseudo always said "writes rd
5200                // ONLY"; the callers that relied on the implicit clear
5201                // (select_default, optimizer_bridge) now emit their own
5202                // explicit hi-zero op. `B .done` above targets offset 22 =
5203                // past-the-end, `BEQ` targets offset 14 — no displacement
5204                // moves.
5205
5206                Ok(bytes) // 22 bytes, register-independent
5207            }
5208
5209            // I64Ctz: Count trailing zeros in 64-bit value
5210            // If lo != 0: result = CTZ(lo) = CLZ(RBIT(lo))
5211            // If lo == 0: result = 32 + CTZ(hi) = 32 + CLZ(RBIT(hi))
5212            //
5213            // Layout:
5214            // 0: CMP.W rnlo, #0 (4 bytes)
5215            // 4: BEQ .lo_zero (2 bytes) - branch to offset 18
5216            // 6: RBIT.W rd, rnlo (4 bytes)
5217            // 10: CLZ.W rd, rd (4 bytes)
5218            // 14: B .done (2 bytes) - branch to offset 30
5219            // 16: NOP (2 bytes) - padding
5220            // 18: .lo_zero: RBIT.W rd, rnhi (4 bytes)
5221            // 22: CLZ.W rd, rd (4 bytes)
5222            // 26: ADD.W rd, rd, #32 (4 bytes)
5223            // 30: .done
5224            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
5225                let rd_bits = reg_to_bits(rd);
5226                let rn_lo_bits = reg_to_bits(rnlo);
5227                let rn_hi_bits = reg_to_bits(rnhi);
5228                let mut bytes = Vec::new();
5229
5230                // CMP.W rnlo, #0 (4 bytes at offset 0)
5231                let hw1: u16 = (0xF1B0 | rn_lo_bits) as u16;
5232                let hw2: u16 = 0x0F00;
5233                bytes.extend_from_slice(&hw1.to_le_bytes());
5234                bytes.extend_from_slice(&hw2.to_le_bytes());
5235
5236                // BEQ .lo_zero (2 bytes at offset 4)
5237                // PC = 4 + 4 = 8, target = 18, offset = 10, imm8 = 5
5238                let beq: u16 = 0xD005;
5239                bytes.extend_from_slice(&beq.to_le_bytes());
5240
5241                // RBIT.W rd, rnlo (4 bytes at offset 6)
5242                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5243                let hw1: u16 = (0xFA90 | rn_lo_bits) as u16;
5244                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_lo_bits) as u16;
5245                bytes.extend_from_slice(&hw1.to_le_bytes());
5246                bytes.extend_from_slice(&hw2.to_le_bytes());
5247
5248                // CLZ.W rd, rd (4 bytes at offset 10)
5249                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5250                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5251                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5252                bytes.extend_from_slice(&hw1.to_le_bytes());
5253                bytes.extend_from_slice(&hw2.to_le_bytes());
5254
5255                // B .done (2 bytes at offset 14)
5256                // PC = 14 + 4 = 18, target = 30, offset = 12, imm11 = 6
5257                let b_done: u16 = 0xE006;
5258                bytes.extend_from_slice(&b_done.to_le_bytes());
5259
5260                // NOP (2 bytes at offset 16) - padding
5261                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5262
5263                // .lo_zero: (offset 18)
5264                // RBIT.W rd, rnhi (4 bytes)
5265                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5266                let hw1: u16 = (0xFA90 | rn_hi_bits) as u16;
5267                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_hi_bits) as u16;
5268                bytes.extend_from_slice(&hw1.to_le_bytes());
5269                bytes.extend_from_slice(&hw2.to_le_bytes());
5270
5271                // CLZ.W rd, rd (4 bytes at offset 22)
5272                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5273                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5274                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5275                bytes.extend_from_slice(&hw1.to_le_bytes());
5276                bytes.extend_from_slice(&hw2.to_le_bytes());
5277
5278                // ADD.W rd, rd, #32 (4 bytes at offset 26)
5279                let hw1: u16 = (0xF100 | rd_bits) as u16;
5280                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5281                bytes.extend_from_slice(&hw1.to_le_bytes());
5282                bytes.extend_from_slice(&hw2.to_le_bytes());
5283
5284                // .done: (offset 30 — the end of the expansion)
5285                // #1048: the former trailing `MOV rnhi, #0` is GONE — it
5286                // wrote the OPERAND's home high register (see the I64Clz
5287                // comment above). `B .done` targets offset 30 = past-the-end,
5288                // `BEQ` targets offset 18 — no displacement moves.
5289
5290                Ok(bytes) // 30 bytes, register-independent
5291            }
5292
5293            // I64Popcnt: Population count of 64-bit value
5294            // result = POPCNT(lo) + POPCNT(hi)
5295            // Using SIMD-style parallel bit counting algorithm
5296            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
5297                let rd_bits = reg_to_bits(rd);
5298                let rn_lo_bits = reg_to_bits(rnlo);
5299                let rn_hi_bits = reg_to_bits(rnhi);
5300                let r12: u32 = 12; // IP scratch
5301                let r3: u32 = 3; // Scratch for hi popcnt result
5302                let mut bytes = Vec::new();
5303
5304                // PUSH {R3, R4, R5} - save scratch registers
5305                bytes.extend_from_slice(&0xB438u16.to_le_bytes());
5306
5307                // Strategy: compute popcnt(lo) -> R4, popcnt(hi) -> R5, add them -> rd
5308                // Using lookup table approach for each byte would be too large
5309                // Using shift-and-add approach instead
5310
5311                // For simplicity and correctness, use the efficient parallel algorithm
5312                // but implement it as a series of inline operations
5313
5314                // Marshal the operand pair into the fixed scratch regs, routing
5315                // rnlo through R12 (#632 audit): writing R4 first corrupted the
5316                // rnhi read for a pair living at (R3,R4) — every source is read
5317                // before any scratch register it could occupy is written.
5318                // MOV R12, rnlo
5319                let mov: u16 = (0x4600 | (1 << 7) | (rn_lo_bits << 3) | 4) as u16;
5320                bytes.extend_from_slice(&mov.to_le_bytes());
5321                // MOV R5, rnhi (R4 untouched so far; rnhi == R5 is a no-op)
5322                let mov: u16 = (0x4600 | (rn_hi_bits << 3) | 5) as u16;
5323                bytes.extend_from_slice(&mov.to_le_bytes());
5324                // MOV R4, R12
5325                bytes.extend_from_slice(&0x4664u16.to_le_bytes());
5326
5327                // --- POPCNT for R4 (lo word) ---
5328                // Step 1: x = x - ((x >> 1) & 0x55555555)
5329                // LSR.W R12, R4, #1
5330                let hw1: u16 = 0xEA4F;
5331                let hw2: u16 = ((r12 << 8) | 0x50 | 4) as u16;
5332                bytes.extend_from_slice(&hw1.to_le_bytes());
5333                bytes.extend_from_slice(&hw2.to_le_bytes());
5334
5335                // Load 0x55555555 into R3 using MOVW/MOVT
5336                // MOVW R3, #0x5555
5337                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5338                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5339                // MOVT R3, #0x5555
5340                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5341                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5342
5343                // AND.W R12, R12, R3
5344                let hw1: u16 = (0xEA00 | r12) as u16;
5345                let hw2: u16 = ((r12 << 8) | r3) as u16;
5346                bytes.extend_from_slice(&hw1.to_le_bytes());
5347                bytes.extend_from_slice(&hw2.to_le_bytes());
5348
5349                // SUB.W R4, R4, R12
5350                let hw1: u16 = (0xEBA0 | 4) as u16;
5351                let hw2: u16 = ((4 << 8) | r12) as u16;
5352                bytes.extend_from_slice(&hw1.to_le_bytes());
5353                bytes.extend_from_slice(&hw2.to_le_bytes());
5354
5355                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5356                // Load 0x33333333 into R3
5357                // MOVW R3, #0x3333
5358                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5359                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5360                // MOVT R3, #0x3333
5361                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5362                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5363
5364                // AND.W R12, R4, R3
5365                let hw1: u16 = (0xEA00 | 4) as u16;
5366                let hw2: u16 = ((r12 << 8) | r3) as u16;
5367                bytes.extend_from_slice(&hw1.to_le_bytes());
5368                bytes.extend_from_slice(&hw2.to_le_bytes());
5369
5370                // LSR.W R4, R4, #2
5371                let hw1: u16 = 0xEA4F;
5372                let hw2: u16 = ((4 << 8) | 0x90 | 4) as u16;
5373                bytes.extend_from_slice(&hw1.to_le_bytes());
5374                bytes.extend_from_slice(&hw2.to_le_bytes());
5375
5376                // AND.W R4, R4, R3
5377                let hw1: u16 = (0xEA00 | 4) as u16;
5378                let hw2: u16 = ((4 << 8) | r3) as u16;
5379                bytes.extend_from_slice(&hw1.to_le_bytes());
5380                bytes.extend_from_slice(&hw2.to_le_bytes());
5381
5382                // ADD.W R4, R4, R12
5383                let hw1: u16 = (0xEB00 | 4) as u16;
5384                let hw2: u16 = ((4 << 8) | r12) as u16;
5385                bytes.extend_from_slice(&hw1.to_le_bytes());
5386                bytes.extend_from_slice(&hw2.to_le_bytes());
5387
5388                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5389                // LSR.W R12, R4, #4
5390                // hw2 = (imm3 << 12) | (Rd << 8) | (imm2 << 6) | (type << 4) | Rm
5391                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5392                let hw1: u16 = 0xEA4F;
5393                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 4) as u16;
5394                bytes.extend_from_slice(&hw1.to_le_bytes());
5395                bytes.extend_from_slice(&hw2.to_le_bytes());
5396
5397                // ADD.W R4, R4, R12
5398                let hw1: u16 = (0xEB00 | 4) as u16;
5399                let hw2: u16 = ((4 << 8) | r12) as u16;
5400                bytes.extend_from_slice(&hw1.to_le_bytes());
5401                bytes.extend_from_slice(&hw2.to_le_bytes());
5402
5403                // Load 0x0F0F0F0F into R3
5404                // MOVW R3, #0x0F0F (imm4=0, i=1, imm3=7, imm8=0x0F)
5405                // hw1 = 11110 1 10 0100 0000 = 0xF640
5406                // hw2 = 0 111 0011 00001111 = 0x730F
5407                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5408                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5409                // MOVT R3, #0x0F0F
5410                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5411                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5412
5413                // AND.W R4, R4, R3
5414                let hw1: u16 = (0xEA00 | 4) as u16;
5415                let hw2: u16 = ((4 << 8) | r3) as u16;
5416                bytes.extend_from_slice(&hw1.to_le_bytes());
5417                bytes.extend_from_slice(&hw2.to_le_bytes());
5418
5419                // Step 4: x = x * 0x01010101 >> 24
5420                // Load 0x01010101 into R3
5421                // MOVW R3, #0x0101
5422                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5423                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5424                // MOVT R3, #0x0101
5425                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5426                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5427
5428                // MUL R4, R4, R3
5429                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5430                let hw1: u16 = (0xFB00 | 4) as u16;
5431                let hw2: u16 = (0xF000 | (4 << 8) | r3) as u16;
5432                bytes.extend_from_slice(&hw1.to_le_bytes());
5433                bytes.extend_from_slice(&hw2.to_le_bytes());
5434
5435                // LSR.W R4, R4, #24
5436                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5437                let hw1: u16 = 0xEA4F;
5438                let hw2: u16 = (0x6000 | (4 << 8) | 0x10 | 4) as u16;
5439                bytes.extend_from_slice(&hw1.to_le_bytes());
5440                bytes.extend_from_slice(&hw2.to_le_bytes());
5441
5442                // --- POPCNT for R5 (hi word) - same algorithm ---
5443                // Step 1
5444                let hw1: u16 = 0xEA4F;
5445                let hw2: u16 = ((r12 << 8) | 0x50 | 5) as u16;
5446                bytes.extend_from_slice(&hw1.to_le_bytes());
5447                bytes.extend_from_slice(&hw2.to_le_bytes());
5448
5449                // Load 0x55555555 into R3
5450                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5451                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5452                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5453                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5454
5455                let hw1: u16 = (0xEA00 | r12) as u16;
5456                let hw2: u16 = ((r12 << 8) | r3) as u16;
5457                bytes.extend_from_slice(&hw1.to_le_bytes());
5458                bytes.extend_from_slice(&hw2.to_le_bytes());
5459
5460                let hw1: u16 = (0xEBA0 | 5) as u16;
5461                let hw2: u16 = ((5 << 8) | r12) as u16;
5462                bytes.extend_from_slice(&hw1.to_le_bytes());
5463                bytes.extend_from_slice(&hw2.to_le_bytes());
5464
5465                // Step 2
5466                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5467                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5468                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5469                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5470
5471                let hw1: u16 = (0xEA00 | 5) as u16;
5472                let hw2: u16 = ((r12 << 8) | r3) as u16;
5473                bytes.extend_from_slice(&hw1.to_le_bytes());
5474                bytes.extend_from_slice(&hw2.to_le_bytes());
5475
5476                let hw1: u16 = 0xEA4F;
5477                let hw2: u16 = ((5 << 8) | 0x90 | 5) as u16;
5478                bytes.extend_from_slice(&hw1.to_le_bytes());
5479                bytes.extend_from_slice(&hw2.to_le_bytes());
5480
5481                let hw1: u16 = (0xEA00 | 5) as u16;
5482                let hw2: u16 = ((5 << 8) | r3) as u16;
5483                bytes.extend_from_slice(&hw1.to_le_bytes());
5484                bytes.extend_from_slice(&hw2.to_le_bytes());
5485
5486                let hw1: u16 = (0xEB00 | 5) as u16;
5487                let hw2: u16 = ((5 << 8) | r12) as u16;
5488                bytes.extend_from_slice(&hw1.to_le_bytes());
5489                bytes.extend_from_slice(&hw2.to_le_bytes());
5490
5491                // Step 3: LSR.W R12, R5, #4
5492                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5493                let hw1: u16 = 0xEA4F;
5494                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 5) as u16;
5495                bytes.extend_from_slice(&hw1.to_le_bytes());
5496                bytes.extend_from_slice(&hw2.to_le_bytes());
5497
5498                let hw1: u16 = (0xEB00 | 5) as u16;
5499                let hw2: u16 = ((5 << 8) | r12) as u16;
5500                bytes.extend_from_slice(&hw1.to_le_bytes());
5501                bytes.extend_from_slice(&hw2.to_le_bytes());
5502
5503                // Load 0x0F0F0F0F into R3 (for hi-word)
5504                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5505                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5506                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5507                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5508
5509                let hw1: u16 = (0xEA00 | 5) as u16;
5510                let hw2: u16 = ((5 << 8) | r3) as u16;
5511                bytes.extend_from_slice(&hw1.to_le_bytes());
5512                bytes.extend_from_slice(&hw2.to_le_bytes());
5513
5514                // Step 4
5515                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5516                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5517                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5518                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5519
5520                // MUL R5, R5, R3
5521                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5522                let hw1: u16 = (0xFB00 | 5) as u16;
5523                let hw2: u16 = (0xF000 | (5 << 8) | r3) as u16;
5524                bytes.extend_from_slice(&hw1.to_le_bytes());
5525                bytes.extend_from_slice(&hw2.to_le_bytes());
5526
5527                // LSR.W R5, R5, #24
5528                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5529                let hw1: u16 = 0xEA4F;
5530                let hw2: u16 = (0x6000 | (5 << 8) | 0x10 | 5) as u16;
5531                bytes.extend_from_slice(&hw1.to_le_bytes());
5532                bytes.extend_from_slice(&hw2.to_le_bytes());
5533
5534                // #632: the count must be carried ACROSS the scratch restore
5535                // in a register the POP cannot touch. rd is allocator-assigned
5536                // (any of R0-R8) and can land inside the {R3,R4,R5} restore set
5537                // — the old `ADDS rd, R4, R5; POP {R3,R4,R5}` destroyed the
5538                // result one instruction after computing it (0 for every input
5539                // under qemu). R12 is encoder scratch: never allocatable (#212)
5540                // and never in a restore set, so no choice of rd can collide.
5541                // ADD.W R12, R4, R5
5542                bytes.extend_from_slice(&0xEB04u16.to_le_bytes());
5543                bytes.extend_from_slice(&0x0C05u16.to_le_bytes());
5544
5545                // POP {R3, R4, R5}
5546                bytes.extend_from_slice(&0xBC38u16.to_le_bytes());
5547
5548                // MOV rd, R12 — after the restore. The 4-bit Rd (D:rd) form is
5549                // also total over rd = R8, where the old ADDS T1 3-bit field
5550                // silently corrupted the encoding (#178/#180 class).
5551                let mov: u16 =
5552                    (0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7)) as u16;
5553                bytes.extend_from_slice(&mov.to_le_bytes());
5554
5555                // #1048: the former trailing `MOV.W rnhi, #0` hi-word clear
5556                // is GONE — it wrote the OPERAND's home high register (see
5557                // the I64Clz comment). Callers that relied on the implicit
5558                // clear emit their own explicit hi-zero op.
5559
5560                Ok(bytes)
5561            }
5562
5563            // I64Extend8S: Sign-extend low 8 bits to 64 bits
5564            // Result: rdlo = sign_extend_8(rnlo), rdhi = rdlo >> 31
5565            ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
5566                let rdlo_bits = reg_to_bits(rdlo);
5567                let rdhi_bits = reg_to_bits(rdhi);
5568                let rnlo_bits = reg_to_bits(rnlo);
5569                let mut bytes = Vec::new();
5570
5571                // SXTB.W rdlo, rnlo (sign-extend byte to 32-bit)
5572                // SXTB T2: hw1 = 0xFA4F, hw2 = 0xF0<Rd><Rm>
5573                let hw1: u16 = 0xFA4F_u16;
5574                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5575                bytes.extend_from_slice(&hw1.to_le_bytes());
5576                bytes.extend_from_slice(&hw2.to_le_bytes());
5577
5578                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5579                // ASR (immediate): hw1 = 0xEA4F, hw2 = imm3:Rd:imm2:type:Rm
5580                // For imm5=31: imm3=111, imm2=11, type=10 (ASR)
5581                // hw2 = (7 << 12) | (rdhi << 8) | (3 << 6) | (2 << 4) | rdlo
5582                let hw1: u16 = 0xEA4F;
5583                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5584                bytes.extend_from_slice(&hw1.to_le_bytes());
5585                bytes.extend_from_slice(&hw2.to_le_bytes());
5586
5587                Ok(bytes)
5588            }
5589
5590            // I64Extend16S: Sign-extend low 16 bits to 64 bits
5591            // Result: rdlo = sign_extend_16(rnlo), rdhi = rdlo >> 31
5592            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
5593                let rdlo_bits = reg_to_bits(rdlo);
5594                let rdhi_bits = reg_to_bits(rdhi);
5595                let rnlo_bits = reg_to_bits(rnlo);
5596                let mut bytes = Vec::new();
5597
5598                // SXTH.W rdlo, rnlo (sign-extend halfword to 32-bit)
5599                // SXTH T2: hw1 = 0xFA0F, hw2 = 0xF0<Rd><Rm>
5600                let hw1: u16 = 0xFA0F_u16;
5601                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5602                bytes.extend_from_slice(&hw1.to_le_bytes());
5603                bytes.extend_from_slice(&hw2.to_le_bytes());
5604
5605                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5606                let hw1: u16 = 0xEA4F;
5607                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5608                bytes.extend_from_slice(&hw1.to_le_bytes());
5609                bytes.extend_from_slice(&hw2.to_le_bytes());
5610
5611                Ok(bytes)
5612            }
5613
5614            // I64Extend32S: Sign-extend low 32 bits to 64 bits
5615            // Result: rdlo = rnlo, rdhi = rnlo >> 31
5616            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
5617                let rdlo_bits = reg_to_bits(rdlo);
5618                let rdhi_bits = reg_to_bits(rdhi);
5619                let rnlo_bits = reg_to_bits(rnlo);
5620                let mut bytes = Vec::new();
5621
5622                // MOV rdlo, rnlo (if different)
5623                if rdlo_bits != rnlo_bits {
5624                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5625                    let d_bit = ((rdlo_bits >> 3) & 1) as u16;
5626                    let mov: u16 = 0x4600
5627                        | (d_bit << 7)
5628                        | ((rnlo_bits as u16) << 3)
5629                        | ((rdlo_bits & 0x7) as u16);
5630                    bytes.extend_from_slice(&mov.to_le_bytes());
5631                }
5632
5633                // ASR.W rdhi, rnlo, #31 (sign-extend to high word)
5634                let hw1: u16 = 0xEA4F;
5635                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rnlo_bits) as u16;
5636                bytes.extend_from_slice(&hw1.to_le_bytes());
5637                bytes.extend_from_slice(&hw2.to_le_bytes());
5638
5639                Ok(bytes)
5640            }
5641
5642            // SelectMove: IT <cond>; MOV{cond} rd, rm
5643            // Conditional move: only execute MOV if condition is true
5644            ArmOp::SelectMove { rd, rm, cond } => {
5645                let rd_bits = reg_to_bits(rd) as u16;
5646                let rm_bits = reg_to_bits(rm) as u16;
5647
5648                // Condition code encoding for IT block
5649                use synth_synthesis::Condition;
5650                let cond_bits: u16 = match cond {
5651                    Condition::EQ => 0x0, // Equal
5652                    Condition::NE => 0x1, // Not equal
5653                    Condition::HS => 0x2, // Higher or same (unsigned >=)
5654                    Condition::LO => 0x3, // Lower (unsigned <)
5655                    Condition::HI => 0x8, // Higher (unsigned >)
5656                    Condition::LS => 0x9, // Lower or same (unsigned <=)
5657                    Condition::GE => 0xA, // Greater or equal (signed)
5658                    Condition::LT => 0xB, // Less than (signed)
5659                    Condition::GT => 0xC, // Greater than (signed)
5660                    Condition::LE => 0xD, // Less or equal (signed)
5661                };
5662
5663                // IT <cond>: single Then block (mask = 0x8 for T only)
5664                // IT instruction: 1011 1111 firstcond mask
5665                let it_instr: u16 = 0xBF00 | (cond_bits << 4) | 0x8;
5666
5667                // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5668                // This MOV will only execute if condition is true due to IT block
5669                let d_bit = (rd_bits >> 3) & 1;
5670                let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5671
5672                // Emit: IT <cond>, MOV rd, rm
5673                let mut bytes = it_instr.to_le_bytes().to_vec();
5674                bytes.extend_from_slice(&mov_instr.to_le_bytes());
5675                Ok(bytes)
5676            }
5677
5678            // Popcnt: Population count (count set bits)
5679            // ARM Cortex-M has no native POPCNT, so we implement the bit manipulation algorithm:
5680            // x = x - ((x >> 1) & 0x55555555);
5681            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
5682            // x = (x + (x >> 4)) & 0x0F0F0F0F;
5683            // x = x + (x >> 8);
5684            // x = x + (x >> 16);
5685            // return x & 0x3F;
5686            //
5687            // #1021: R12 (IP, never allocatable) is the ONLY scratch. The
5688            // previous expansion borrowed R11 as a second temp — but R11 is
5689            // the WASM linear-memory base, materialized at entry and read by
5690            // every later LDR/STR, and it is NOT in the pushed set, so the
5691            // clobber leaked to the CALLER too (a live memory-safety
5692            // miscompile: loads through `base = x >> 16`). The second temp is
5693            // eliminated the way the healthy i64.popcnt discipline implies —
5694            // never touch an unsaved register — but without its PUSH/POP
5695            // wrapper: the SWAR masks 0x55555555 / 0x33333333 / 0x0F0F0F0F
5696            // are all `0xXYXYXYXY` ThumbExpandImm modified immediates, so
5697            // each AND takes its mask from the instruction itself and R12
5698            // alone carries every intermediate. Straight-line, no branches,
5699            // no stack traffic — nothing to skip on a trap edge.
5700            ArmOp::Popcnt { rd, rm } => {
5701                let rd_bits = reg_to_bits(rd);
5702                // Defensive (#1021): rd = R11/R12/SP/PC would silently
5703                // corrupt the linear-memory base, the expansion's own
5704                // scratch, or the stack. The selector never assigns them
5705                // (pool R0-R8); refuse loudly if that ever changes.
5706                if rd_bits >= 11 {
5707                    return Err(synth_core::Error::synthesis(
5708                        "Popcnt destination must be R0-R10: R11 is the linear-memory \
5709                         base and R12 is the expansion's scratch (#1021)",
5710                    ));
5711                }
5712                let mut bytes = Vec::new();
5713
5714                // First, move rm to rd if they're different
5715                if rd != rm {
5716                    let rm_bits = reg_to_bits(rm) as u16;
5717                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5718                    let d_bit = ((rd_bits as u16) >> 3) & 1;
5719                    let mov_instr: u16 =
5720                        0x4600 | (d_bit << 7) | (rm_bits << 3) | ((rd_bits as u16) & 0x7);
5721                    bytes.extend_from_slice(&mov_instr.to_le_bytes());
5722                }
5723
5724                // Step 1: x = x - ((x >> 1) & 0x55555555)
5725                // R12 = rd >> 1
5726                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 1)?);
5727                // R12 = R12 & 0x55555555 (modified immediate, no constant reg)
5728                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(12, 12, 0x5555_5555)?);
5729                // rd = rd - R12
5730                bytes.extend_from_slice(&self.encode_thumb32_sub_reg_raw(rd_bits, rd_bits, 12)?);
5731
5732                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5733                // R12 = rd & 0x33333333
5734                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5735                    12,
5736                    rd_bits,
5737                    0x3333_3333,
5738                )?);
5739                // rd = rd >> 2
5740                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(rd_bits, rd_bits, 2)?);
5741                // rd = rd & 0x33333333
5742                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5743                    rd_bits,
5744                    rd_bits,
5745                    0x3333_3333,
5746                )?);
5747                // rd = rd + R12
5748                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5749
5750                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5751                // R12 = rd >> 4
5752                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 4)?);
5753                // rd = rd + R12
5754                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5755                // rd = rd & 0x0F0F0F0F
5756                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5757                    rd_bits,
5758                    rd_bits,
5759                    0x0F0F_0F0F,
5760                )?);
5761
5762                // Step 4: x = x + (x >> 8)
5763                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 8)?);
5764                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5765
5766                // Step 5: x = x + (x >> 16)
5767                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 16)?);
5768                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5769
5770                // Step 6: return x & 0x3F
5771                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(rd_bits, rd_bits, 0x3F)?);
5772
5773                Ok(bytes)
5774            }
5775
5776            // I64DivU: 64-bit unsigned division using binary long division
5777            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = quotient
5778            // Uses: R4-R7, R12 as loop counter (avoid R8 for Renode compatibility)
5779            //
5780            // #610: the fixed-ABI wrapper marshals the selector-assigned
5781            // operand registers into the core's fixed regs and lands the
5782            // result in rd — pre-#610 this arm IGNORED its register fields,
5783            // so the selector read its rd pair (e.g. R4:R5) after the core's
5784            // own POP restored the stale caller values over it: 0 for every
5785            // input. A zero divisor now traps (UDF #0), per WASM semantics.
5786            ArmOp::I64DivU {
5787                rdlo,
5788                rdhi,
5789                rnlo,
5790                rnhi,
5791                rmlo,
5792                rmhi,
5793                elide_zero_guard,
5794            } => {
5795                let mut bytes = Vec::new();
5796                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5797                // #494 phase 2b: elided only under a certificate-discharged
5798                // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
5799                if !elide_zero_guard {
5800                    emit_i64_divisor_zero_trap(&mut bytes);
5801                }
5802
5803                // PUSH {R4-R7} - save scratch registers (NO LR — this is inline code)
5804                // 16-bit PUSH: 1011 010 M rrrrrrrr where M=0 (no LR), r=R4-R7 = 0xF0
5805                // Encoding: 1011 0100 1111 0000 = 0xB4F0
5806                bytes.extend_from_slice(&0xB4F0u16.to_le_bytes());
5807
5808                // Initialize quotient (R4:R5) = 0
5809                bytes.extend_from_slice(&0x2400u16.to_le_bytes()); // MOV R4, #0
5810                bytes.extend_from_slice(&0x2500u16.to_le_bytes()); // MOV R5, #0
5811
5812                // Initialize remainder (R6:R7) = 0
5813                bytes.extend_from_slice(&0x2600u16.to_le_bytes()); // MOV R6, #0
5814                bytes.extend_from_slice(&0x2700u16.to_le_bytes()); // MOV R7, #0
5815
5816                // Initialize loop counter R12 = 64 (use R12 scratch instead of R8)
5817                // MOV.W R12, #64: F04F 0C40
5818                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5819                bytes.extend_from_slice(&0x0C40u16.to_le_bytes());
5820
5821                // Loop start
5822                let loop_start = bytes.len();
5823
5824                // === Loop body: process one bit ===
5825
5826                // 1. Shift quotient R4:R5 left by 1
5827                // LSLS R5, R5, #1 (16-bit: 0000 0010 1010 1101 = 0x006D -> actually 0x002D for LSL R5,R5,#1)
5828                // LSL Rd, Rm, #imm5: 000 00 imm5 Rm Rd = 000 00 00001 101 101 = 0x006D
5829                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5830                // Get carry from R4 into R5: ORR R5, R5, R4 LSR #31
5831                // Thumb-2 ORR with shifted register: EA45 75D4 = ORR.W R5, R5, R4, LSR #31
5832                // 11101010 010 S Rn | 0 imm3 Rd imm2 type Rm
5833                // type=01 (LSR), imm5=31 (imm3=111, imm2=11)
5834                bytes.extend_from_slice(&0xEA45u16.to_le_bytes());
5835                bytes.extend_from_slice(&0x75D4u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5836                // LSLS R4, R4, #1: 000 00 00001 100 100 = 0x0064
5837                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5838
5839                // 2. Shift remainder R6:R7 left by 1, OR in MSB of dividend R1
5840                // LSLS R7, R7, #1
5841                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5842                // ORR.W R7, R7, R6, LSR #31
5843                bytes.extend_from_slice(&0xEA47u16.to_le_bytes());
5844                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5845                // LSLS R6, R6, #1
5846                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5847                // ORR.W R6, R6, R1, LSR #31 (bring in MSB of dividend high)
5848                bytes.extend_from_slice(&0xEA46u16.to_le_bytes());
5849                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5850
5851                // 3. Shift dividend R0:R1 left by 1
5852                // LSLS R1, R1, #1
5853                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5854                // ORR.W R1, R1, R0, LSR #31
5855                bytes.extend_from_slice(&0xEA41u16.to_le_bytes());
5856                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5857                // LSLS R0, R0, #1
5858                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5859
5860                // 4. Compare remainder >= divisor (64-bit unsigned comparison)
5861                // Compare high words first: CMP R7, R3
5862                // CMP Rn, Rm encoding: 0x4280 | (Rm << 3) | Rn
5863                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3 (16-bit)
5864                // BHI means R7 > R3 (unsigned) - definitely subtract
5865                // BLO means R7 < R3 - definitely don't subtract
5866                // BEQ means need to check low words
5867
5868                // If high > divisor high: branch to subtract (forward +offset)
5869                // BHI.N +6 (skip CMP, skip BLO, do subtract)
5870                // BHI: 1101 1000 offset8 where cond=1000 (HI)
5871                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4 (to subtract block)
5872
5873                // If high < divisor high: branch past subtract
5874                // BLO.N +10 (skip to decrement)
5875                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BLO/BCC +12 (past subtract)
5876
5877                // High words equal, compare low: CMP R6, R2
5878                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2 (16-bit)
5879                // BLO/BCC past subtract (skip SUBS+SBC.W+ORR.W = 10 bytes = 4 halfwords from PC+4)
5880                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords (past subtract)
5881
5882                // === Subtract block: remainder -= divisor, quotient |= 1 ===
5883                // SUBS R6, R6, R2
5884                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2 (16-bit)
5885                // SBC R7, R7, R3 (with borrow)
5886                // Thumb-2 SBC.W: EB67 0703 = SBC.W R7, R7, R3
5887                bytes.extend_from_slice(&0xEB67u16.to_le_bytes());
5888                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5889                // ORR R4, R4, #1 (set bit 0 of quotient low)
5890                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5891                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5892
5893                // === Decrement counter and loop ===
5894                // SUBS.W R12, R12, #1 (decrement loop counter)
5895                // SUBS.W R12, R12, #1: F1BC 0C01
5896                bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
5897                bytes.extend_from_slice(&0x0C01u16.to_le_bytes());
5898
5899                // BNE back to loop_start
5900                let branch_offset_bytes = bytes.len() - loop_start + 4; // +4 for pipeline
5901                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5902                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5903                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5904
5905                // === Loop done, move quotient to R0:R1 ===
5906                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5907                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5908
5909                // POP {R4-R7} - restore scratch registers (NO PC — inline code continues)
5910                // 16-bit POP: 1011 110 P rrrrrrrr where P=0 (no PC), r=R4-R7 = 0xF0
5911                // Encoding: 1011 1100 1111 0000 = 0xBCF0
5912                bytes.extend_from_slice(&0xBCF0u16.to_le_bytes());
5913
5914                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5915                Ok(bytes)
5916            }
5917
5918            // I64DivS: 64-bit signed division
5919            // Converts to unsigned, divides, then applies sign
5920            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5921            //   ->  R0:R1 = quotient (signed)
5922            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5923            ArmOp::I64DivS {
5924                rdlo,
5925                rdhi,
5926                rnlo,
5927                rnhi,
5928                rmlo,
5929                rmhi,
5930                elide_zero_guard,
5931                elide_overflow_guard,
5932            } => {
5933                let mut bytes = Vec::new();
5934                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5935                // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
5936                // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
5937                // the #633 overflow guard falls ONLY to
5938                // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
5939                // divisor-nonzero fact alone must keep it.
5940                if !elide_zero_guard {
5941                    emit_i64_divisor_zero_trap(&mut bytes);
5942                }
5943                if !elide_overflow_guard {
5944                    // #633: INT64_MIN / -1 overflows — trap like the i32 path
5945                    // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
5946                    emit_i64_divs_overflow_trap(&mut bytes);
5947                }
5948
5949                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5950                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5951                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5952
5953                // Save result sign in R9: R9 = R1 XOR R3 (sign bit = MSB)
5954                // EOR.W R9, R1, R3
5955                bytes.extend_from_slice(&0xEA81u16.to_le_bytes());
5956                bytes.extend_from_slice(&0x0903u16.to_le_bytes());
5957
5958                // If dividend negative (R1 MSB set), negate it
5959                // TST R1, R1 (check sign)
5960                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5961                // BPL skip_neg_dividend (+10 bytes = 5 halfwords)
5962                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5963
5964                // Negate R0:R1 (64-bit): RSBS R0, R0, #0; SBC R1, R1, R1 LSL #1
5965                // Actually: MVN R0, R0; MVN R1, R1; ADDS R0, R0, #1; ADC R1, R1, #0
5966                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5967                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5968                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5969                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5970                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5971
5972                // If divisor negative (R3 MSB set), negate it
5973                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5974                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5975
5976                // Negate R2:R3
5977                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
5978                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
5979                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
5980                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
5981                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
5982
5983                // === Now do unsigned division (same as I64DivU) ===
5984                // Initialize quotient (R4:R5) = 0
5985                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5986                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5987                // Initialize remainder (R6:R7) = 0
5988                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5989                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5990                // Initialize loop counter R8 = 64
5991                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5992                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5993
5994                let loop_start = bytes.len();
5995
5996                // Shift quotient left
5997                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5998                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5999                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6000                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6001
6002                // Shift remainder left, OR in MSB of dividend
6003                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6004                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6005                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6006                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6007                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6008                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6009
6010                // Shift dividend left
6011                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6012                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6013                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6014                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6015
6016                // Compare and conditionally subtract
6017                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6018                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6019                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6020                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6021                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6022
6023                // Subtract and set quotient bit
6024                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6025                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6026                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6027                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6028                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6029
6030                // Decrement and loop
6031                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6032                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6033
6034                let branch_offset_bytes = bytes.len() - loop_start + 4;
6035                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6036                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6037                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6038
6039                // Move quotient to R0:R1
6040                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
6041                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
6042
6043                // If result should be negative (R9 MSB set), negate R0:R1
6044                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9 (check MSB)
6045                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6046                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8 (skip negation)
6047
6048                // Negate result R0:R1
6049                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6050                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6051                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6052                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6053                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6054
6055                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6056                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6057                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6058
6059                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6060                Ok(bytes)
6061            }
6062
6063            // I64RemU: 64-bit unsigned remainder using binary long division
6064            // Same algorithm as I64DivU but returns remainder instead of quotient
6065            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = remainder
6066            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6067            ArmOp::I64RemU {
6068                rdlo,
6069                rdhi,
6070                rnlo,
6071                rnhi,
6072                rmlo,
6073                rmhi,
6074                elide_zero_guard,
6075            } => {
6076                let mut bytes = Vec::new();
6077                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6078                if !elide_zero_guard {
6079                    emit_i64_divisor_zero_trap(&mut bytes);
6080                }
6081
6082                // PUSH {R4-R8} - save scratch registers (NO LR — inline code)
6083                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6084                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6085
6086                // Initialize quotient (R4:R5) = 0 (computed but not returned)
6087                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6088                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6089                // Initialize remainder (R6:R7) = 0
6090                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6091                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6092                // Initialize loop counter R8 = 64
6093                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6094                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6095
6096                let loop_start = bytes.len();
6097
6098                // Shift quotient left (not needed for result, but keeps algorithm same)
6099                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6100                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6101                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6102                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6103
6104                // Shift remainder left, OR in MSB of dividend
6105                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6106                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6107                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6108                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6109                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6110                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6111
6112                // Shift dividend left
6113                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6114                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6115                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6116                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6117
6118                // Compare and conditionally subtract
6119                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6120                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6121                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6122                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6123                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6124
6125                // Subtract and set quotient bit
6126                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6127                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6128                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6129                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6130                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6131
6132                // Decrement and loop
6133                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6134                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6135
6136                let branch_offset_bytes = bytes.len() - loop_start + 4;
6137                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6138                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6139                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6140
6141                // Move REMAINDER to R0:R1 (difference from I64DivU)
6142                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6143                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6144
6145                // POP {R4-R8} - restore scratch registers (NO PC — inline code continues)
6146                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6147                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6148
6149                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6150                Ok(bytes)
6151            }
6152
6153            // I64RemS: 64-bit signed remainder
6154            // Remainder sign follows dividend sign (not quotient rule)
6155            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
6156            //   ->  R0:R1 = remainder (signed, same sign as dividend)
6157            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6158            ArmOp::I64RemS {
6159                rdlo,
6160                rdhi,
6161                rnlo,
6162                rnhi,
6163                rmlo,
6164                rmhi,
6165                elide_zero_guard,
6166            } => {
6167                let mut bytes = Vec::new();
6168                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6169                if !elide_zero_guard {
6170                    emit_i64_divisor_zero_trap(&mut bytes);
6171                }
6172
6173                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
6174                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6175                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6176
6177                // Save dividend sign in R9 (remainder sign = dividend sign)
6178                // MOV R9, R1 (just need the sign bit)
6179                bytes.extend_from_slice(&0x4689u16.to_le_bytes()); // MOV R9, R1
6180
6181                // If dividend negative (R1 MSB set), negate it
6182                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
6183                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6184
6185                // Negate R0:R1
6186                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6187                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6188                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6189                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6190                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6191
6192                // If divisor negative (R3 MSB set), negate it
6193                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
6194                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6195
6196                // Negate R2:R3
6197                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
6198                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
6199                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
6200                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
6201                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
6202
6203                // === Unsigned division algorithm ===
6204                // Initialize quotient (R4:R5) = 0
6205                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6206                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6207                // Initialize remainder (R6:R7) = 0
6208                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6209                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6210                // Initialize loop counter R8 = 64
6211                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6212                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6213
6214                let loop_start = bytes.len();
6215
6216                // Shift quotient left
6217                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6218                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6219                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6220                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6221
6222                // Shift remainder left, OR in MSB of dividend
6223                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6224                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6225                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6226                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6227                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6228                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6229
6230                // Shift dividend left
6231                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6232                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6233                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6234                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6235
6236                // Compare and conditionally subtract
6237                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6238                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6239                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6240                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6241                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6242
6243                // Subtract and set quotient bit
6244                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6245                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6246                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6247                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6248                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6249
6250                // Decrement and loop
6251                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6252                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6253
6254                let branch_offset_bytes = bytes.len() - loop_start + 4;
6255                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6256                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6257                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6258
6259                // Move remainder to R0:R1
6260                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6261                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6262
6263                // If original dividend was negative (R9 MSB set), negate remainder
6264                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9
6265                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6266                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6267
6268                // Negate result R0:R1
6269                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6270                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6271                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6272                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6273                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6274
6275                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6276                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6277                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6278
6279                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6280                Ok(bytes)
6281            }
6282
6283            // === F32 VFP single-precision Thumb-2 encodings ===
6284            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6285            ArmOp::F32Add { sd, sn, sm } => {
6286                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A00, sd, sn, sm)?))
6287            }
6288            ArmOp::F32Sub { sd, sn, sm } => {
6289                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A40, sd, sn, sm)?))
6290            }
6291            ArmOp::F32Mul { sd, sn, sm } => {
6292                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE200A00, sd, sn, sm)?))
6293            }
6294            ArmOp::F32Div { sd, sn, sm } => {
6295                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE800A00, sd, sn, sm)?))
6296            }
6297            ArmOp::F32Abs { sd, sm } => {
6298                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB00AC0, sd, sm)?))
6299            }
6300            ArmOp::F32Neg { sd, sm } => {
6301                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10A40, sd, sm)?))
6302            }
6303            ArmOp::F32Sqrt { sd, sm } => {
6304                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10AC0, sd, sm)?))
6305            }
6306
6307            // f32 pseudo-ops — multi-instruction sequences
6308            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6309            ArmOp::F32Ceil { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b01),
6310            ArmOp::F32Floor { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b10),
6311            ArmOp::F32Trunc { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b11),
6312            ArmOp::F32Nearest { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b00),
6313            ArmOp::F32Min { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, true),
6314            ArmOp::F32Max { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, false),
6315            ArmOp::F32Copysign { sd, sn, sm } => self.encode_thumb_f32_copysign(sd, sn, sm),
6316
6317            // f32 comparisons — VCMP + VMRS + MOV #0 + IT + MOV #1
6318            ArmOp::F32Eq { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x0),
6319            ArmOp::F32Ne { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x1),
6320            ArmOp::F32Lt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x4),
6321            ArmOp::F32Le { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x9),
6322            ArmOp::F32Gt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xC),
6323            ArmOp::F32Ge { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xA),
6324
6325            ArmOp::F32Const { sd, value } => self.encode_thumb_f32_const(sd, *value),
6326
6327            ArmOp::F32Load { sd, addr } => {
6328                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED900A00, sd, addr)?))
6329            }
6330            ArmOp::F32Store { sd, addr } => {
6331                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED800A00, sd, addr)?))
6332            }
6333
6334            ArmOp::F32ConvertI32S { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, true),
6335            ArmOp::F32ConvertI32U { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, false),
6336            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
6337                Err(synth_core::Error::synthesis(
6338                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6339                ))
6340            }
6341            ArmOp::F32ReinterpretI32 { sd, rm } => {
6342                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(true, sd, rm)?))
6343            }
6344            ArmOp::I32ReinterpretF32 { rd, sm } => {
6345                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(false, sm, rd)?))
6346            }
6347            ArmOp::I32TruncF32S { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, true),
6348            ArmOp::I32TruncF32U { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, false),
6349
6350            // === F64 VFP double-precision Thumb-2 encodings ===
6351            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6352            ArmOp::F64Add { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6353                0xEE300B00, dd, dn, dm,
6354            )?)),
6355            ArmOp::F64Sub { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6356                0xEE300B40, dd, dn, dm,
6357            )?)),
6358            ArmOp::F64Mul { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6359                0xEE200B00, dd, dn, dm,
6360            )?)),
6361            ArmOp::F64Div { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6362                0xEE800B00, dd, dn, dm,
6363            )?)),
6364            ArmOp::F64Abs { dd, dm } => {
6365                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?))
6366            }
6367            ArmOp::F64Neg { dd, dm } => {
6368                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?))
6369            }
6370            ArmOp::F64Sqrt { dd, dm } => {
6371                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?))
6372            }
6373
6374            // f64 pseudo-ops
6375            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6376            ArmOp::F64Ceil { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b01),
6377            ArmOp::F64Floor { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b10),
6378            ArmOp::F64Trunc { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b11),
6379            ArmOp::F64Nearest { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b00),
6380            ArmOp::F64Min { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, true),
6381            ArmOp::F64Max { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, false),
6382            ArmOp::F64Copysign { dd, dn, dm } => self.encode_thumb_f64_copysign(dd, dn, dm),
6383
6384            // f64 comparisons
6385            ArmOp::F64Eq { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x0),
6386            ArmOp::F64Ne { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x1),
6387            ArmOp::F64Lt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x4),
6388            ArmOp::F64Le { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x9),
6389            ArmOp::F64Gt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xC),
6390            ArmOp::F64Ge { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xA),
6391
6392            ArmOp::F64Const { dd, value } => self.encode_thumb_f64_const(dd, *value),
6393
6394            ArmOp::F64Load { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6395                0xED900B00, dd, addr,
6396            )?)),
6397            ArmOp::F64Store { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6398                0xED800B00, dd, addr,
6399            )?)),
6400
6401            ArmOp::F64ConvertI32S { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, true),
6402            ArmOp::F64ConvertI32U { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, false),
6403            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
6404                Err(synth_core::Error::synthesis(
6405                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6406                ))
6407            }
6408            ArmOp::F64PromoteF32 { dd, sm } => self.encode_thumb_f64_promote_f32(dd, sm),
6409            ArmOp::F32DemoteF64 { sd, dm } => self.encode_thumb_f32_demote_f64(sd, dm),
6410            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => Ok(vfp_to_thumb_bytes(
6411                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?,
6412            )),
6413            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => Ok(vfp_to_thumb_bytes(
6414                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?,
6415            )),
6416            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
6417                Err(synth_core::Error::synthesis(
6418                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
6419                ))
6420            }
6421            ArmOp::I32TruncF64S { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, true),
6422            ArmOp::I32TruncF64U { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, false),
6423
6424            // ===== i64 operations: encode as multi-instruction Thumb-2 sequences =====
6425
6426            // I64Add: ADDS rdlo, rnlo, rmlo; ADC.W rdhi, rnhi, rmhi
6427            ArmOp::I64Add {
6428                rdlo,
6429                rdhi,
6430                rnlo,
6431                rnhi,
6432                rmlo,
6433                rmhi,
6434            } => {
6435                let mut bytes = Vec::new();
6436                // ADDS rdlo, rnlo, rmlo (16-bit)
6437                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adds {
6438                    rd: *rdlo,
6439                    rn: *rnlo,
6440                    op2: Operand2::Reg(*rmlo),
6441                })?);
6442                // ADC.W rdhi, rnhi, rmhi (32-bit)
6443                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adc {
6444                    rd: *rdhi,
6445                    rn: *rnhi,
6446                    op2: Operand2::Reg(*rmhi),
6447                })?);
6448                Ok(bytes)
6449            }
6450
6451            // I64Sub: SUBS rdlo, rnlo, rmlo; SBC.W rdhi, rnhi, rmhi
6452            ArmOp::I64Sub {
6453                rdlo,
6454                rdhi,
6455                rnlo,
6456                rnhi,
6457                rmlo,
6458                rmhi,
6459            } => {
6460                let mut bytes = Vec::new();
6461                // SUBS rdlo, rnlo, rmlo (16-bit)
6462                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Subs {
6463                    rd: *rdlo,
6464                    rn: *rnlo,
6465                    op2: Operand2::Reg(*rmlo),
6466                })?);
6467                // SBC.W rdhi, rnhi, rmhi (32-bit)
6468                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Sbc {
6469                    rd: *rdhi,
6470                    rn: *rnhi,
6471                    op2: Operand2::Reg(*rmhi),
6472                })?);
6473                Ok(bytes)
6474            }
6475
6476            // I64And: AND rdlo, rnlo, rmlo; AND rdhi, rnhi, rmhi
6477            ArmOp::I64And {
6478                rdlo,
6479                rdhi,
6480                rnlo,
6481                rnhi,
6482                rmlo,
6483                rmhi,
6484            } => {
6485                let mut bytes = Vec::new();
6486                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6487                    rd: *rdlo,
6488                    rn: *rnlo,
6489                    op2: Operand2::Reg(*rmlo),
6490                })?);
6491                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6492                    rd: *rdhi,
6493                    rn: *rnhi,
6494                    op2: Operand2::Reg(*rmhi),
6495                })?);
6496                Ok(bytes)
6497            }
6498
6499            // I64Or: ORR rdlo, rnlo, rmlo; ORR rdhi, rnhi, rmhi
6500            ArmOp::I64Or {
6501                rdlo,
6502                rdhi,
6503                rnlo,
6504                rnhi,
6505                rmlo,
6506                rmhi,
6507            } => {
6508                let mut bytes = Vec::new();
6509                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6510                    rd: *rdlo,
6511                    rn: *rnlo,
6512                    op2: Operand2::Reg(*rmlo),
6513                })?);
6514                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6515                    rd: *rdhi,
6516                    rn: *rnhi,
6517                    op2: Operand2::Reg(*rmhi),
6518                })?);
6519                Ok(bytes)
6520            }
6521
6522            // I64Xor: EOR rdlo, rnlo, rmlo; EOR rdhi, rnhi, rmhi
6523            ArmOp::I64Xor {
6524                rdlo,
6525                rdhi,
6526                rnlo,
6527                rnhi,
6528                rmlo,
6529                rmhi,
6530            } => {
6531                let mut bytes = Vec::new();
6532                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6533                    rd: *rdlo,
6534                    rn: *rnlo,
6535                    op2: Operand2::Reg(*rmlo),
6536                })?);
6537                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6538                    rd: *rdhi,
6539                    rn: *rnhi,
6540                    op2: Operand2::Reg(*rmhi),
6541                })?);
6542                Ok(bytes)
6543            }
6544
6545            // I64Eqz: ORR scratch, lo, hi; ITE EQ; MOV rd, #1; MOV rd, #0
6546            ArmOp::I64Eqz { rd, rnlo, rnhi } => self.encode_thumb(&ArmOp::I64SetCondZ {
6547                rd: *rd,
6548                rn_lo: *rnlo,
6549                rn_hi: *rnhi,
6550            }),
6551
6552            // I64 comparisons: delegate to I64SetCond
6553            ArmOp::I64Eq {
6554                rd,
6555                rnlo,
6556                rnhi,
6557                rmlo,
6558                rmhi,
6559            } => self.encode_thumb(&ArmOp::I64SetCond {
6560                rd: *rd,
6561                rn_lo: *rnlo,
6562                rn_hi: *rnhi,
6563                rm_lo: *rmlo,
6564                rm_hi: *rmhi,
6565                cond: synth_synthesis::Condition::EQ,
6566            }),
6567
6568            ArmOp::I64Ne {
6569                rd,
6570                rnlo,
6571                rnhi,
6572                rmlo,
6573                rmhi,
6574            } => self.encode_thumb(&ArmOp::I64SetCond {
6575                rd: *rd,
6576                rn_lo: *rnlo,
6577                rn_hi: *rnhi,
6578                rm_lo: *rmlo,
6579                rm_hi: *rmhi,
6580                cond: synth_synthesis::Condition::NE,
6581            }),
6582
6583            ArmOp::I64LtS {
6584                rd,
6585                rnlo,
6586                rnhi,
6587                rmlo,
6588                rmhi,
6589            } => self.encode_thumb(&ArmOp::I64SetCond {
6590                rd: *rd,
6591                rn_lo: *rnlo,
6592                rn_hi: *rnhi,
6593                rm_lo: *rmlo,
6594                rm_hi: *rmhi,
6595                cond: synth_synthesis::Condition::LT,
6596            }),
6597
6598            ArmOp::I64LtU {
6599                rd,
6600                rnlo,
6601                rnhi,
6602                rmlo,
6603                rmhi,
6604            } => self.encode_thumb(&ArmOp::I64SetCond {
6605                rd: *rd,
6606                rn_lo: *rnlo,
6607                rn_hi: *rnhi,
6608                rm_lo: *rmlo,
6609                rm_hi: *rmhi,
6610                cond: synth_synthesis::Condition::LO,
6611            }),
6612
6613            ArmOp::I64LeS {
6614                rd,
6615                rnlo,
6616                rnhi,
6617                rmlo,
6618                rmhi,
6619            } => self.encode_thumb(&ArmOp::I64SetCond {
6620                rd: *rd,
6621                rn_lo: *rnlo,
6622                rn_hi: *rnhi,
6623                rm_lo: *rmlo,
6624                rm_hi: *rmhi,
6625                cond: synth_synthesis::Condition::LE,
6626            }),
6627
6628            ArmOp::I64LeU {
6629                rd,
6630                rnlo,
6631                rnhi,
6632                rmlo,
6633                rmhi,
6634            } => self.encode_thumb(&ArmOp::I64SetCond {
6635                rd: *rd,
6636                rn_lo: *rnlo,
6637                rn_hi: *rnhi,
6638                rm_lo: *rmlo,
6639                rm_hi: *rmhi,
6640                cond: synth_synthesis::Condition::LS,
6641            }),
6642
6643            ArmOp::I64GtS {
6644                rd,
6645                rnlo,
6646                rnhi,
6647                rmlo,
6648                rmhi,
6649            } => self.encode_thumb(&ArmOp::I64SetCond {
6650                rd: *rd,
6651                rn_lo: *rnlo,
6652                rn_hi: *rnhi,
6653                rm_lo: *rmlo,
6654                rm_hi: *rmhi,
6655                cond: synth_synthesis::Condition::GT,
6656            }),
6657
6658            ArmOp::I64GtU {
6659                rd,
6660                rnlo,
6661                rnhi,
6662                rmlo,
6663                rmhi,
6664            } => self.encode_thumb(&ArmOp::I64SetCond {
6665                rd: *rd,
6666                rn_lo: *rnlo,
6667                rn_hi: *rnhi,
6668                rm_lo: *rmlo,
6669                rm_hi: *rmhi,
6670                cond: synth_synthesis::Condition::HI,
6671            }),
6672
6673            ArmOp::I64GeS {
6674                rd,
6675                rnlo,
6676                rnhi,
6677                rmlo,
6678                rmhi,
6679            } => self.encode_thumb(&ArmOp::I64SetCond {
6680                rd: *rd,
6681                rn_lo: *rnlo,
6682                rn_hi: *rnhi,
6683                rm_lo: *rmlo,
6684                rm_hi: *rmhi,
6685                cond: synth_synthesis::Condition::GE,
6686            }),
6687
6688            ArmOp::I64GeU {
6689                rd,
6690                rnlo,
6691                rnhi,
6692                rmlo,
6693                rmhi,
6694            } => self.encode_thumb(&ArmOp::I64SetCond {
6695                rd: *rd,
6696                rn_lo: *rnlo,
6697                rn_hi: *rnhi,
6698                rm_lo: *rmlo,
6699                rm_hi: *rmhi,
6700                cond: synth_synthesis::Condition::HS,
6701            }),
6702
6703            // I64Const: MOVW rdlo, lo16; MOVT rdlo, hi16; MOVW rdhi, lo16_hi; MOVT rdhi, hi16_hi
6704            ArmOp::I64Const { rdlo, rdhi, value } => {
6705                let lo32 = *value as u32;
6706                let hi32 = (*value >> 32) as u32;
6707                let mut bytes = Vec::new();
6708                // Load low 32 bits into rdlo
6709                bytes.extend_from_slice(
6710                    &self.encode_thumb32_movw_raw(reg_to_bits(rdlo), lo32 & 0xFFFF)?,
6711                );
6712                if lo32 > 0xFFFF {
6713                    bytes.extend_from_slice(
6714                        &self.encode_thumb32_movt_raw(reg_to_bits(rdlo), lo32 >> 16)?,
6715                    );
6716                }
6717                // Load high 32 bits into rdhi
6718                bytes.extend_from_slice(
6719                    &self.encode_thumb32_movw_raw(reg_to_bits(rdhi), hi32 & 0xFFFF)?,
6720                );
6721                if hi32 > 0xFFFF {
6722                    bytes.extend_from_slice(
6723                        &self.encode_thumb32_movt_raw(reg_to_bits(rdhi), hi32 >> 16)?,
6724                    );
6725                }
6726                Ok(bytes)
6727            }
6728
6729            // I64Ldr: LDR rdlo, [base, offset]; LDR rdhi, [base, offset+4]
6730            ArmOp::I64Ldr { rdlo, rdhi, addr } => {
6731                let mut bytes = Vec::new();
6732                // #372/#382: a memory `i64.load` carries an index register
6733                // (`reg_imm(R11, addr_reg, offset)` = R11 + addr + offset). The
6734                // immediate `encode_thumb32_ldr` below uses only base+offset and
6735                // would SILENTLY DROP `offset_reg` — the #206 defect, here for
6736                // i64. `i64_effective_base` materializes the effective base into
6737                // `ip` (and, when `offset+4 > 0xFFF`, folds the offset in too so
6738                // the function is NOT skipped — #382), returning the residual
6739                // imm12 for the two halves. Frame i64 loads (no `offset_reg`, e.g.
6740                // a spilled local at `[SP, #off]`) keep the plain `[base,#off]`
6741                // form unchanged — so existing output is byte-identical.
6742                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6743                bytes.extend_from_slice(&self.encode_thumb32_ldr(rdlo, &base, offset)?);
6744                bytes.extend_from_slice(&self.encode_thumb32_ldr(
6745                    rdhi,
6746                    &base,
6747                    offset.wrapping_add(4),
6748                )?);
6749                Ok(bytes)
6750            }
6751
6752            // I64Str: STR rdlo, [base, offset]; STR rdhi, [base, offset+4]
6753            ArmOp::I64Str { rdlo, rdhi, addr } => {
6754                let mut bytes = Vec::new();
6755                // #372/#382: same index-materialization + large-offset fold as
6756                // I64Ldr (see above).
6757                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6758                bytes.extend_from_slice(&self.encode_thumb32_str(rdlo, &base, offset)?);
6759                bytes.extend_from_slice(&self.encode_thumb32_str(
6760                    rdhi,
6761                    &base,
6762                    offset.wrapping_add(4),
6763                )?);
6764                Ok(bytes)
6765            }
6766
6767            // I64ExtendI32S: MOV rdlo, rn; ASR rdhi, rdlo, #31 (sign-extend)
6768            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
6769                let mut bytes = Vec::new();
6770                if rdlo != rn {
6771                    // MOV rdlo, rn (16-bit)
6772                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6773                        rd: *rdlo,
6774                        op2: Operand2::Reg(*rn),
6775                    })?);
6776                }
6777                // ASR rdhi, rdlo, #31 (sign-extend: fill high word with sign bit)
6778                bytes.extend_from_slice(
6779                    &self.encode_thumb32_shift(rdhi, rdlo, 31, 0b10)?, // ASR type
6780                );
6781                Ok(bytes)
6782            }
6783
6784            // I64ExtendI32U: MOV rdlo, rn; MOV rdhi, #0
6785            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
6786                let mut bytes = Vec::new();
6787                if rdlo != rn {
6788                    // MOV rdlo, rn
6789                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6790                        rd: *rdlo,
6791                        op2: Operand2::Reg(*rn),
6792                    })?);
6793                }
6794                // MOV rdhi, #0 (#916: MOV.W for rdhi >= R8). Unconditional
6795                // site with no branches in the expansion — before the fix this
6796                // emitted the literal two-instruction stream [4608, 2800], half
6797                // of which was `CMP r0,#0` rather than the high-word clear, so
6798                // every i64.extend_i32_u into a high pair leaked stale bits.
6799                emit_thumb_zero_fill(&mut bytes, reg_to_bits(rdhi));
6800                Ok(bytes)
6801            }
6802
6803            // I32WrapI64: MOV rd, rnlo (just take low 32 bits)
6804            ArmOp::I32WrapI64 { rd, rnlo } => {
6805                if rd == rnlo {
6806                    // No-op: already in the right register
6807                    let instr: u16 = 0xBF00; // NOP
6808                    Ok(instr.to_le_bytes().to_vec())
6809                } else {
6810                    // MOV rd, rnlo
6811                    self.encode_thumb(&ArmOp::Mov {
6812                        rd: *rd,
6813                        op2: Operand2::Reg(*rnlo),
6814                    })
6815                }
6816            }
6817
6818            // ===== Helium MVE operations (Thumb-2 encoding) =====
6819            ArmOp::MveLoad { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vldrw(qd, addr))),
6820            ArmOp::MveStore { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vstrw(qd, addr))),
6821            ArmOp::MveConst { qd, bytes } => self.encode_thumb_mve_const(qd, bytes),
6822            ArmOp::MveAnd { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6823                0xEF000150, qd, qn, qm,
6824            ))),
6825            ArmOp::MveOrr { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6826                0xEF200150, qd, qn, qm,
6827            ))),
6828            ArmOp::MveEor { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6829                0xFF000150, qd, qn, qm,
6830            ))),
6831            ArmOp::MveMvn { qd, qm } => {
6832                // VMVN Qd, Qm: 0xFFB005C0 | Qd<<12 | Qm
6833                let qd_enc = qreg_to_num(qd);
6834                let qm_enc = qreg_to_num(qm);
6835                let instr: u32 = 0xFFB005C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6836                Ok(vfp_to_thumb_bytes(instr))
6837            }
6838            ArmOp::MveBic { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6839                0xEF100150, qd, qn, qm,
6840            ))),
6841            ArmOp::MveAddI { qd, qn, qm, size } => {
6842                let sz = mve_size_bits(size);
6843                let base: u32 = 0xEF000840 | (sz << 20);
6844                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6845            }
6846            ArmOp::MveSubI { qd, qn, qm, size } => {
6847                let sz = mve_size_bits(size);
6848                let base: u32 = 0xFF000840 | (sz << 20);
6849                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6850            }
6851            ArmOp::MveMulI { qd, qn, qm, size } => {
6852                let sz = mve_size_bits(size);
6853                let base: u32 = 0xEF000950 | (sz << 20);
6854                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6855            }
6856            ArmOp::MveNegI { qd, qm, size } => {
6857                let sz = mve_size_bits(size);
6858                // VNEG.Sx Qd, Qm
6859                let qd_enc = qreg_to_num(qd);
6860                let qm_enc = qreg_to_num(qm);
6861                let base: u32 = 0xFFB103C0 | (sz << 18);
6862                let instr = base | ((qd_enc * 2) << 12) | (qm_enc * 2);
6863                Ok(vfp_to_thumb_bytes(instr))
6864            }
6865            ArmOp::MveDup { qd, rn, size } => {
6866                let sz = mve_size_bits(size);
6867                let qd_enc = qreg_to_num(qd);
6868                let rn_bits = reg_to_bits(rn);
6869                // VDUP.sz Qd, Rn: EEA0 0B10 variant
6870                // size encoding: 00=32, 01=16, 10=8
6871                let be = match sz {
6872                    0 => 0b00u32, // 8-bit
6873                    1 => 0b01,    // 16-bit
6874                    _ => 0b00,    // 32-bit (default)
6875                };
6876                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12) | (be << 5);
6877                Ok(vfp_to_thumb_bytes(instr))
6878            }
6879            ArmOp::MveExtractLane { rd, qn, lane, size } => {
6880                let qn_enc = qreg_to_num(qn);
6881                let rd_bits = reg_to_bits(rd);
6882                // VMOV.sz Rd, Dn[x] — extract from Q-register lane
6883                // For 32-bit: VMOV Rd, Dn — where Dn is the appropriate D-register
6884                let d_reg = qn_enc * 2 + ((*lane as u32) >> 1);
6885                let lane_in_d = (*lane as u32) & 1;
6886                let _sz = mve_size_bits(size);
6887                // VMOV Rd, Dn[x]: EE10 0B10 for 32-bit
6888                let instr: u32 = 0xEE100B10 | (d_reg << 16) | (rd_bits << 12) | (lane_in_d << 21);
6889                Ok(vfp_to_thumb_bytes(instr))
6890            }
6891            ArmOp::MveInsertLane { qd, rn, lane, size } => {
6892                let qd_enc = qreg_to_num(qd);
6893                let rn_bits = reg_to_bits(rn);
6894                let d_reg = qd_enc * 2 + ((*lane as u32) >> 1);
6895                let lane_in_d = (*lane as u32) & 1;
6896                let _sz = mve_size_bits(size);
6897                // VMOV Dn[x], Rn: EE00 0B10 for 32-bit
6898                let instr: u32 = 0xEE000B10 | (d_reg << 16) | (rn_bits << 12) | (lane_in_d << 21);
6899                Ok(vfp_to_thumb_bytes(instr))
6900            }
6901
6902            // MVE float comparisons — emit VCMP + VPSEL sequence (simplified: just VCMP)
6903            ArmOp::MveCmpEqI { qd, qn, qm, size }
6904            | ArmOp::MveCmpNeI { qd, qn, qm, size }
6905            | ArmOp::MveCmpLtS { qd, qn, qm, size }
6906            | ArmOp::MveCmpLtU { qd, qn, qm, size }
6907            | ArmOp::MveCmpGtS { qd, qn, qm, size }
6908            | ArmOp::MveCmpGtU { qd, qn, qm, size }
6909            | ArmOp::MveCmpLeS { qd, qn, qm, size }
6910            | ArmOp::MveCmpLeU { qd, qn, qm, size }
6911            | ArmOp::MveCmpGeS { qd, qn, qm, size }
6912            | ArmOp::MveCmpGeU { qd, qn, qm, size } => {
6913                // Encode as VADD (placeholder encoding — real implementation
6914                // would use VCMP + VPSEL pair)
6915                let sz = mve_size_bits(size);
6916                let base: u32 = 0xEF000840 | (sz << 20);
6917                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6918            }
6919
6920            // f32x4 MVE arithmetic
6921            ArmOp::MveAddF32 { qd, qn, qm } => {
6922                // VADD.F32 Qd, Qn, Qm (MVE): 0xEF000D40
6923                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6924            }
6925            ArmOp::MveSubF32 { qd, qn, qm } => {
6926                // VSUB.F32 Qd, Qn, Qm (MVE): 0xEF200D40
6927                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF200D40, qd, qn, qm)))
6928            }
6929            ArmOp::MveMulF32 { qd, qn, qm } => {
6930                // VMUL.F32 Qd, Qn, Qm (MVE): 0xFF000D50
6931                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xFF000D50, qd, qn, qm)))
6932            }
6933            ArmOp::MveNegF32 { qd, qm } => {
6934                let qd_enc = qreg_to_num(qd);
6935                let qm_enc = qreg_to_num(qm);
6936                // VNEG.F32 Qd, Qm: FFB907C0
6937                let instr: u32 = 0xFFB907C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6938                Ok(vfp_to_thumb_bytes(instr))
6939            }
6940            ArmOp::MveAbsF32 { qd, qm } => {
6941                let qd_enc = qreg_to_num(qd);
6942                let qm_enc = qreg_to_num(qm);
6943                // VABS.F32 Qd, Qm: FFB90740
6944                let instr: u32 = 0xFFB90740 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6945                Ok(vfp_to_thumb_bytes(instr))
6946            }
6947            ArmOp::MveCmpEqF32 { qd, qn, qm }
6948            | ArmOp::MveCmpNeF32 { qd, qn, qm }
6949            | ArmOp::MveCmpLtF32 { qd, qn, qm }
6950            | ArmOp::MveCmpLeF32 { qd, qn, qm }
6951            | ArmOp::MveCmpGtF32 { qd, qn, qm }
6952            | ArmOp::MveCmpGeF32 { qd, qn, qm } => {
6953                // Placeholder: encode as VADD.F32 (real impl needs VCMP.F32 + VPSEL)
6954                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6955            }
6956            ArmOp::MveDupF32 { qd, rn } => {
6957                let qd_enc = qreg_to_num(qd);
6958                let rn_bits = reg_to_bits(rn);
6959                // VDUP.32 Qd, Rn (same encoding as integer VDUP.32)
6960                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12);
6961                Ok(vfp_to_thumb_bytes(instr))
6962            }
6963            ArmOp::MveExtractLaneF32 { rd, qn, lane } => {
6964                let qn_enc = qreg_to_num(qn);
6965                let rd_bits = reg_to_bits(rd);
6966                // VMOV Rd, Sn where Sn = Q*4 + lane
6967                let s_num = qn_enc * 4 + (*lane as u32);
6968                let (vn, n) = encode_sreg(s_num);
6969                let instr: u32 = 0xEE100A10 | (vn << 16) | (rd_bits << 12) | (n << 7);
6970                Ok(vfp_to_thumb_bytes(instr))
6971            }
6972            ArmOp::MveReplaceLaneF32 { qd, rn, lane } => {
6973                let qd_enc = qreg_to_num(qd);
6974                let rn_bits = reg_to_bits(rn);
6975                // VMOV Sn, Rn where Sn = Q*4 + lane
6976                let s_num = qd_enc * 4 + (*lane as u32);
6977                let (vn, n) = encode_sreg(s_num);
6978                let instr: u32 = 0xEE000A10 | (vn << 16) | (rn_bits << 12) | (n << 7);
6979                Ok(vfp_to_thumb_bytes(instr))
6980            }
6981            ArmOp::MveDivF32 { qd, qn, qm } => {
6982                // Lane-wise: extract 4 S-regs, VDIV, insert back
6983                self.encode_thumb_mve_lane_wise_f32_binop(qd, qn, qm, 0xEE800A00)
6984            }
6985            ArmOp::MveSqrtF32 { qd, qm } => {
6986                // Lane-wise: extract 4 S-regs, VSQRT, insert back
6987                self.encode_thumb_mve_lane_wise_f32_sqrt(qd, qm)
6988            }
6989
6990            // Catch-all for any remaining ops
6991            _ => {
6992                let instr: u16 = 0xBF00; // NOP
6993                Ok(instr.to_le_bytes().to_vec())
6994            }
6995        }
6996    }
6997
6998    // === Thumb-2 VFP multi-instruction helpers ===
6999
7000    /// Encode F32 comparison as Thumb-2: VCMP.F32 + VMRS + MOVS rd,#0 + IT + MOV rd,#1
7001    fn encode_thumb_f32_compare(
7002        &self,
7003        rd: &Reg,
7004        sn: &VfpReg,
7005        sm: &VfpReg,
7006        cond_code: u32,
7007    ) -> Result<Vec<u8>> {
7008        let mut bytes = Vec::new();
7009        let rd_bits = reg_to_bits(rd);
7010
7011        // #709 (bug found under #708/#709): the `MOVS Rd,#0` below is a
7012        // FLAG-SETTING 16-bit move. Emitting it AFTER `VMRS APSR_nzcv, FPSCR`
7013        // (as the original code did) clobbered the N/Z/C/V flags the VMRS just
7014        // transferred from the VFP compare, so the following `IT<cond>` read
7015        // stale flags and every f32 comparison silently returned 0 (verified:
7016        // `flt(1.0,2.0)` → 0 on Cortex-M4F). The 619 harness never caught it
7017        // because it deliberately skipped compare EXECUTION on a false premise
7018        // (unicorn DOES model VMRS→APSR). Fix: materialize the `#0` FIRST, then
7019        // VCMP+VMRS set the flags the `IT` consumes. Instruction sizes are
7020        // unchanged (pure reorder), so the estimator↔encoder oracle (#511) is
7021        // untouched — only the byte ORDER differs.
7022
7023        // MOVS Rd, #0 (16-bit): 0010 0 Rd(3) 0000 0000 — its flag side effect
7024        // is immediately overwritten by the VMRS below.
7025        if rd_bits < 8 {
7026            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
7027            bytes.extend_from_slice(&movs_zero.to_le_bytes());
7028        } else {
7029            // MOV.W Rd, #0 (32-bit Thumb-2)
7030            let hw1: u16 = 0xF04F;
7031            let hw2: u16 = (rd_bits as u16) << 8;
7032            bytes.extend_from_slice(&hw1.to_le_bytes());
7033            bytes.extend_from_slice(&hw2.to_le_bytes());
7034        }
7035
7036        // VCMP.F32 Sn, Sm
7037        let sn_num = vfp_sreg_to_num(sn)?;
7038        let sm_num = vfp_sreg_to_num(sm)?;
7039        let (vd, d) = encode_sreg(sn_num);
7040        let (vm, m) = encode_sreg(sm_num);
7041        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7042        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7043
7044        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10 (sets the flags IT consumes)
7045        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7046
7047        // IT<cond> — If-Then for conditional MOV
7048        // IT encoding: 1011 1111 cond(4) mask(4)
7049        // mask = 0x8 for single "then" (IT)
7050        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7051        bytes.extend_from_slice(&it.to_le_bytes());
7052
7053        // MOV Rd, #1 (16-bit, conditional due to IT): 0010 0 Rd(3) 0000 0001
7054        if rd_bits < 8 {
7055            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7056            bytes.extend_from_slice(&mov_one.to_le_bytes());
7057        } else {
7058            // MOV.W Rd, #1 (32-bit)
7059            let hw1: u16 = 0xF04F;
7060            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7061            bytes.extend_from_slice(&hw1.to_le_bytes());
7062            bytes.extend_from_slice(&hw2.to_le_bytes());
7063        }
7064
7065        Ok(bytes)
7066    }
7067
7068    /// Encode F32 constant load as Thumb-2: MOVW + MOVT + VMOV
7069    fn encode_thumb_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
7070        let mut bytes = Vec::new();
7071        let bits = value.to_bits();
7072        let rt: u32 = 12; // R12/IP as temp
7073
7074        // MOVW R12, #lo16
7075        // Thumb-2 MOVW: 11110 i 10 0100 imm4 | 0 imm3 Rd imm8
7076        let lo16 = bits & 0xFFFF;
7077        let imm4 = (lo16 >> 12) & 0xF;
7078        let i_bit = (lo16 >> 11) & 1;
7079        let imm3 = (lo16 >> 8) & 0x7;
7080        let imm8 = lo16 & 0xFF;
7081        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7082        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
7083        bytes.extend_from_slice(&hw1.to_le_bytes());
7084        bytes.extend_from_slice(&hw2.to_le_bytes());
7085
7086        // MOVT R12, #hi16
7087        let hi16 = (bits >> 16) & 0xFFFF;
7088        let imm4 = (hi16 >> 12) & 0xF;
7089        let i_bit = (hi16 >> 11) & 1;
7090        let imm3 = (hi16 >> 8) & 0x7;
7091        let imm8 = hi16 & 0xFF;
7092        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
7093        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
7094        bytes.extend_from_slice(&hw1.to_le_bytes());
7095        bytes.extend_from_slice(&hw2.to_le_bytes());
7096
7097        // VMOV Sd, R12
7098        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
7099        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7100
7101        Ok(bytes)
7102    }
7103
7104    /// Encode VMOV + VCVT.F32.xS32 as Thumb-2
7105    fn encode_thumb_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7106        let mut bytes = Vec::new();
7107
7108        // VMOV Sd, Rm
7109        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
7110        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7111
7112        // VCVT.F32.S32/U32 Sd, Sd. Bit 7 (op) = 1 for signed (S32), 0 for
7113        // unsigned (U32): signed = 0xEEB80AC0, unsigned = 0xEEB80A40
7114        // (GI-FPU-002: previously swapped — see the ARM32 twin).
7115        let sd_num = vfp_sreg_to_num(sd)?;
7116        let (vd, d) = encode_sreg(sd_num);
7117        let (vm, m) = encode_sreg(sd_num);
7118        let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
7119        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7120        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7121
7122        Ok(bytes)
7123    }
7124
7125    /// Encode F32 rounding pseudo-op as Thumb-2 via VCVT to integer and back
7126    /// Encode F32 rounding as Thumb-2.
7127    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
7128    ///
7129    /// For trunc: uses VCVTR.S32.F32 (always truncates).
7130    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant),
7131    /// then restores FPSCR.
7132    fn encode_thumb_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7133        let mut bytes = Vec::new();
7134        let sm_num = vfp_sreg_to_num(sm)?;
7135        let sd_num = vfp_sreg_to_num(sd)?;
7136        let (vd_s, d_s) = encode_sreg(sd_num);
7137        let (vm_s, m_s) = encode_sreg(sm_num);
7138
7139        if mode == 0b11 {
7140            // Trunc (toward zero): VCVTR.S32.F32 — bit[7]=1, always truncates
7141            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7142            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7143        } else {
7144            // ceil/floor/nearest: manipulate FPSCR rounding mode
7145            let rt: u32 = 12; // R12/IP as temp
7146
7147            // VMRS R12, FPSCR
7148            let vmrs = 0xEEF10A10 | (rt << 12);
7149            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7150
7151            // BIC.W R12, R12, #(3 << 22) — clear RMode bits [23:22]
7152            // Thumb-2 modified immediate for 3<<22 = 0x00C00000:
7153            // BIC.W encoding: 11110 i 0 0001 S Rn | 0 imm3 Rd imm8
7154            // 0x00C00000 = 0x03 shifted left by 22 => Thumb mod-imm: i=0, imm3=0b101, imm8=0x03
7155            let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF); // BIC, Rn=R12
7156            let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
7157            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7158            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7159
7160            // ORR.W R12, R12, #(mode << 22)
7161            if mode != 0 {
7162                let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF); // ORR, Rn=R12
7163                let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
7164                bytes.extend_from_slice(&orr_hw1.to_le_bytes());
7165                bytes.extend_from_slice(&orr_hw2.to_le_bytes());
7166            }
7167
7168            // VMSR FPSCR, R12
7169            let vmsr = 0xEEE10A10 | (rt << 12);
7170            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7171
7172            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rmode
7173            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7174            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7175
7176            // Restore FPSCR: clear rmode bits back to nearest (default)
7177            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7178            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7179            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7180            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7181        }
7182
7183        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
7184        let (vd2, d2) = encode_sreg(sd_num);
7185        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
7186        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
7187
7188        Ok(bytes)
7189    }
7190
7191    /// Encode F32 min/max as Thumb-2: VMOV + VCMP + VMRS + IT + VMOV
7192    fn encode_thumb_f32_minmax(
7193        &self,
7194        sd: &VfpReg,
7195        sn: &VfpReg,
7196        sm: &VfpReg,
7197        is_min: bool,
7198    ) -> Result<Vec<u8>> {
7199        let mut bytes = Vec::new();
7200        let sn_num = vfp_sreg_to_num(sn)?;
7201        let sm_num = vfp_sreg_to_num(sm)?;
7202        let sd_num = vfp_sreg_to_num(sd)?;
7203
7204        // VMOV.F32 Sd, Sn
7205        let (vd, d) = encode_sreg(sd_num);
7206        let (vn, n) = encode_sreg(sn_num);
7207        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
7208        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sn));
7209
7210        // VCMP.F32 Sn, Sm
7211        let (vm, m) = encode_sreg(sm_num);
7212        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7213        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7214
7215        // VMRS APSR_nzcv, FPSCR
7216        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7217
7218        // IT GT (for min) or IT MI (for max)
7219        let cond: u16 = if is_min { 0xC } else { 0x4 };
7220        let it: u16 = 0xBF00 | (cond << 4) | 0x8;
7221        bytes.extend_from_slice(&it.to_le_bytes());
7222
7223        // VMOV{cond}.F32 Sd, Sm — conditional VMOV in IT block
7224        let vmov_sm = 0xEEB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7225        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sm));
7226
7227        Ok(bytes)
7228    }
7229
7230    /// Encode F32 copysign as Thumb-2
7231    /// Encode F32 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7232    /// encoder scratch, #212), the flags, and Sd:
7233    ///
7234    ///   VMOV R12, Sm ; CMP R12, #0    (N flag = the sign bit)
7235    ///   VABS.F32 Sd, Sn               (magnitude, sign cleared)
7236    ///   IT MI ; VNEG.F32(MI) Sd, Sd
7237    ///
7238    /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7239    /// The R12 capture happens BEFORE Sd is written, so Sd aliasing Sn or Sm
7240    /// is safe. (The previous sequence staged the magnitude through R0 —
7241    /// clobbering a live allocator-owned value, the #615 class; caught while
7242    /// composing the F64 twin for #369.)
7243    fn encode_thumb_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7244        let mut bytes = Vec::new();
7245
7246        // VMOV R12, Sm (sign source bits)
7247        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
7248            false,
7249            sm,
7250            &Reg::R12,
7251        )?));
7252        // CMP.W R12, #0 — N = bit31 (the sign, incl. -0.0 / -NaN).
7253        bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7254        bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7255        // VABS.F32 Sd, Sn
7256        let sd_num = vfp_sreg_to_num(sd)?;
7257        let sn_num = vfp_sreg_to_num(sn)?;
7258        let (vd, d) = encode_sreg(sd_num);
7259        let (vn, n) = encode_sreg(sn_num);
7260        let vabs = 0xEEB00AC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7261        bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7262        // IT MI ; VNEG.F32(MI) Sd, Sd
7263        bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7264        let vneg = 0xEEB10A40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7265        bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7266
7267        Ok(bytes)
7268    }
7269
7270    /// Encode F64 comparison as Thumb-2: VCMP.F64 + VMRS + MOV #0 + IT + MOV #1
7271    fn encode_thumb_f64_compare(
7272        &self,
7273        rd: &Reg,
7274        dn: &VfpReg,
7275        dm: &VfpReg,
7276        cond_code: u32,
7277    ) -> Result<Vec<u8>> {
7278        let mut bytes = Vec::new();
7279        let rd_bits = reg_to_bits(rd);
7280
7281        // #712-class fix (found at f64-phase-2 wiring, #369): the 16-bit
7282        // `MOVS Rd,#0` is FLAG-SETTING. The original order emitted it AFTER
7283        // `VMRS APSR_nzcv, FPSCR`, clobbering the N/Z/C/V flags the VMRS just
7284        // transferred, so the following `IT<cond>` read stale flags and every
7285        // f64 comparison silently returned 0 — the exact bug the f32 compare
7286        // encoder shipped with and #712 fixed. Same fix: materialize the `#0`
7287        // FIRST (its flag side effect is overwritten by the VMRS), then
7288        // VCMP+VMRS set the flags the IT consumes. Pure reorder — sizes
7289        // unchanged.
7290
7291        // MOVS Rd, #0
7292        if rd_bits < 8 {
7293            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
7294            bytes.extend_from_slice(&movs_zero.to_le_bytes());
7295        } else {
7296            let hw1: u16 = 0xF04F;
7297            let hw2: u16 = (rd_bits as u16) << 8;
7298            bytes.extend_from_slice(&hw1.to_le_bytes());
7299            bytes.extend_from_slice(&hw2.to_le_bytes());
7300        }
7301
7302        // VCMP.F64 Dn, Dm
7303        let dn_num = vfp_dreg_to_num(dn)?;
7304        let dm_num = vfp_dreg_to_num(dm)?;
7305        let (vd, d) = encode_dreg(dn_num);
7306        let (vm, m) = encode_dreg(dm_num);
7307        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7308        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7309
7310        // VMRS APSR_nzcv, FPSCR (sets the flags the IT consumes)
7311        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7312
7313        // IT<cond>
7314        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7315        bytes.extend_from_slice(&it.to_le_bytes());
7316
7317        // MOV Rd, #1
7318        if rd_bits < 8 {
7319            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7320            bytes.extend_from_slice(&mov_one.to_le_bytes());
7321        } else {
7322            let hw1: u16 = 0xF04F;
7323            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7324            bytes.extend_from_slice(&hw1.to_le_bytes());
7325            bytes.extend_from_slice(&hw2.to_le_bytes());
7326        }
7327
7328        Ok(bytes)
7329    }
7330
7331    /// Encode F64 constant load as Thumb-2: MOVW+MOVT (lo32 into R0) + MOVW+MOVT (hi32 into R12) + VMOV Dd, R0, R12
7332    fn encode_thumb_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
7333        let mut bytes = Vec::new();
7334        let bits = value.to_bits();
7335        let lo32 = bits as u32;
7336        let hi32 = (bits >> 32) as u32;
7337
7338        // MOVW R0, #lo16(lo32)
7339        let lo16 = lo32 & 0xFFFF;
7340        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(0, lo16)?);
7341
7342        // MOVT R0, #hi16(lo32)
7343        let hi16 = (lo32 >> 16) & 0xFFFF;
7344        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(0, hi16)?);
7345
7346        // MOVW R12, #lo16(hi32)
7347        let lo16 = hi32 & 0xFFFF;
7348        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
7349
7350        // MOVT R12, #hi16(hi32)
7351        let hi16 = (hi32 >> 16) & 0xFFFF;
7352        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
7353
7354        // VMOV Dd, R0, R12
7355        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
7356        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7357
7358        Ok(bytes)
7359    }
7360
7361    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as Thumb-2
7362    /// Encode i32 → f64 conversion as Thumb-2. The integer stages through the
7363    /// DESTINATION's own low S-alias (`S(2d)`) — allocator-owned by
7364    /// definition — never S0 (which may hold a live value; the previous
7365    /// pseudo-op's S0 staging was the #615 class). Also fixes the SWAPPED
7366    /// signed/unsigned VCVT bases (bit7 = 1 is SIGNED — the same swap the f32
7367    /// twin had; latent here because f64.convert_i32_* was decode-dropped
7368    /// until #369): clang-verified vcvt.f64.s32 d1,s2 = eeb8 1bc1,
7369    /// vcvt.f64.u32 d1,s2 = eeb8 1b41.
7370    fn encode_thumb_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7371        let dd_num = vfp_dreg_to_num(dd)?;
7372        if dd_num > 7 {
7373            return Err(synth_core::Error::synthesis(format!(
7374                "F64ConvertI32: destination {dd:?} has no S-register alias \
7375                 (D8..D15) — the selector allocates only D0..D7"
7376            )));
7377        }
7378        let mut bytes = Vec::new();
7379
7380        // VMOV S(2d), Rm — stage the integer in the destination's low word.
7381        let (vn_s, n_s) = encode_sreg(2 * dd_num);
7382        let rt = reg_to_bits(rm);
7383        let vmov = 0xEE000A10 | (vn_s << 16) | (rt << 12) | (n_s << 7);
7384        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7385
7386        // VCVT.F64.S32/U32 Dd, S(2d)
7387        let (vd, d) = encode_dreg(dd_num);
7388        let (vm, m) = encode_sreg(2 * dd_num);
7389        let base = if signed { 0xEEB80BC0 } else { 0xEEB80B40 };
7390        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7391        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7392
7393        Ok(bytes)
7394    }
7395
7396    /// Encode VCVT.F64.F32 Dd, Sm as Thumb-2
7397    fn encode_thumb_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7398        let dd_num = vfp_dreg_to_num(dd)?;
7399        let sm_num = vfp_sreg_to_num(sm)?;
7400        let (vd, d) = encode_dreg(dd_num);
7401        let (vm, m) = encode_sreg(sm_num);
7402
7403        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7404        Ok(vfp_to_thumb_bytes(vcvt))
7405    }
7406
7407    /// Encode VCVT.F32.F64 Sd, Dm (f32.demote_f64) as Thumb-2 — single
7408    /// instruction, round-to-nearest-even per FPSCR default, exactly WASM
7409    /// §4.3.3 demote (clang-verified: vcvt.f32.f64 s1,d2 = eef7 0bc2).
7410    fn encode_thumb_f32_demote_f64(&self, sd: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7411        let sd_num = vfp_sreg_to_num(sd)?;
7412        let dm_num = vfp_dreg_to_num(dm)?;
7413        let (vd, d) = encode_sreg(sd_num);
7414        let (vm, m) = encode_dreg(dm_num);
7415
7416        let vcvt = 0xEEB70BC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7417        Ok(vfp_to_thumb_bytes(vcvt))
7418    }
7419
7420    /// Encode f64 → i32 truncation as Thumb-2 (round-toward-zero VCVT).
7421    ///
7422    /// The 32-bit result stages through the SOURCE's own low S-alias, `S(2m)`,
7423    /// clobbering half of an operand the selector has already popped. The
7424    /// overlapping write is well-defined: VCVT reads its source operand before
7425    /// writing (compilers emit `vcvt.f32.f64 s0, d0` routinely).
7426    ///
7427    /// # The one precondition, and who actually provides it
7428    ///
7429    /// `dm` must be a DEAD TEMP — never a pinned param/local home. That is the
7430    /// whole safety argument, and it is worth naming the guarantor precisely
7431    /// (#946): **`select_with_stack`** provides it, by copying a home into a
7432    /// fresh D-temp first. Visible in the shipped output for
7433    /// `(func (param f64) (result i32) (i32.trunc_f64_s (local.get 0)))`:
7434    ///
7435    /// ```text
7436    ///   vmov r1, r2, d0     ; read the param out of its AAPCS-VFP home D0
7437    ///   vmov d1, r1, r2     ; ...into a fresh D-temp
7438    ///   vcvt.s32.f64 s2, d1 ; convert from the TEMP, staging into its own S2
7439    /// ```
7440    ///
7441    /// `InstructionSelector::select` / `select_default` do NOT provide it —
7442    /// `alloc_vfp_dreg` is a bare round-robin `(n + 1) % 16` with no liveness
7443    /// or home test. That path is not reachable from `synth compile`
7444    /// (`arm_backend.rs` calls `select_with_stack` exclusively; the only
7445    /// non-test caller of `select` is `examples/compile_add.rs`), so this is
7446    /// not a live miscompile — but a caller reaching that `pub` API directly
7447    /// gets no such guarantee.
7448    ///
7449    /// # What this deliberately does NOT claim
7450    ///
7451    /// An earlier version of this comment said the staging register is "never
7452    /// S0, which may hold an unrelated live value (the #615 class)". **That is
7453    /// false**, and measurably so: for
7454    /// `(func (result i32) (i32.trunc_f64_s (f64.const 3.7)))` the shipped
7455    /// compiler emits `vcvt.s32.f64 s0, d0`.
7456    ///
7457    /// It is also unnecessary. S0 is only dangerous as an *unrelated* scratch;
7458    /// here it is always the low half of `dm` itself, which the precondition
7459    /// above already makes dead. Naming a guard the code does not have — and
7460    /// does not need — invites a future reader to lean on it. The dead-temp
7461    /// precondition is the only thing holding this up.
7462    fn encode_thumb_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7463        let dm_num = vfp_dreg_to_num(dm)?;
7464        if dm_num > 7 {
7465            return Err(synth_core::Error::synthesis(format!(
7466                "I32TruncF64: source {dm:?} has no S-register alias \
7467                 (D8..D15) — the selector allocates only D0..D7"
7468            )));
7469        }
7470        let mut bytes = Vec::new();
7471
7472        // VCVT.S32/U32.F64 S(2m), Dm (clang-verified:
7473        // vcvt.s32.f64 s1,d2 = eefd 0bc2 ; vcvt.u32.f64 s1,d2 = eefc 0bc2)
7474        let (vm, m) = encode_dreg(dm_num);
7475        let (vd_s, d_s) = encode_sreg(2 * dm_num);
7476        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
7477        let vcvt = base | (d_s << 22) | (vd_s << 12) | (m << 5) | vm;
7478        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7479
7480        // VMOV Rd, S(2m)
7481        let rt = reg_to_bits(rd);
7482        let vmov = 0xEE100A10 | (vd_s << 16) | (rt << 12) | (d_s << 7);
7483        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7484
7485        Ok(bytes)
7486    }
7487
7488    /// Encode F64 rounding as a SINGLE Thumb-2 VRINT (FPv5 / cortex-m7dp).
7489    /// `mode` keeps the legacy FPSCR-RMode numbering of the callers —
7490    /// 0b00=nearest(ties-to-even)→VRINTN, 0b01=+inf(ceil)→VRINTP,
7491    /// 0b10=-inf(floor)→VRINTM, 0b11=zero(trunc)→VRINTZ — but the rounding
7492    /// mode is now ENCODED in the instruction, not smuggled through FPSCR.
7493    /// (The previous pseudo-op round-tripped through a 32-bit integer in S0:
7494    /// wrong for |x| >= 2^31, NaN/±inf collapsed to 0, -0.0 lost, and it
7495    /// CLOBBERED S0/R12 behind the allocator's back — the #615 class.)
7496    /// VRINT quietens an sNaN and preserves the sign of ±0.0/NaN per IEEE 754
7497    /// roundToIntegral, which is exactly WASM Core §4.3.3 f64.ceil/floor/
7498    /// trunc/nearest. VRINTN/P/M live in the FE "always-execute" space (never
7499    /// IT-conditional; none of these sequences emits them inside an IT block).
7500    fn encode_thumb_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7501        let dd_num = vfp_dreg_to_num(dd)?;
7502        let dm_num = vfp_dreg_to_num(dm)?;
7503        let (vd, d) = encode_dreg(dd_num);
7504        let (vm, m) = encode_dreg(dm_num);
7505        // clang-verified bases (thumbv7em, fpv5-d16):
7506        //   vrintn.f64 d1,d2 = feb9 1b42 ; vrintp = feba 1b42
7507        //   vrintm.f64 d1,d2 = febb 1b42 ; vrintz = eeb6 1bc2
7508        let base: u32 = match mode {
7509            0b00 => 0xFEB90B40, // VRINTN.F64 (round to nearest, ties to even)
7510            0b01 => 0xFEBA0B40, // VRINTP.F64 (round toward +inf)
7511            0b10 => 0xFEBB0B40, // VRINTM.F64 (round toward -inf)
7512            _ => 0xEEB60BC0,    // VRINTZ.F64 (round toward zero)
7513        };
7514        Ok(vfp_to_thumb_bytes(
7515            base | (d << 22) | (vd << 12) | (m << 5) | vm,
7516        ))
7517    }
7518
7519    /// Encode F64 min/max as Thumb-2 with WASM Core §4.3.3 semantics:
7520    ///
7521    ///   VCMP.F64 Dn, Dm ; VMRS APSR_nzcv, FPSCR
7522    ///   VMINNM.F64/VMAXNM.F64 Dd, Dn, Dm      (FPv5; -0.0 < +0.0 ordered)
7523    ///   IT VS ; VADD.F64(VS) Dd, Dn, Dm       (unordered ⇒ NaN-propagating)
7524    ///
7525    /// VMINNM/VMAXNM alone are IEEE minNum/maxNum, which return the NUMBER
7526    /// when exactly one operand is NaN — WASM requires NaN. The VS-guarded
7527    /// VADD overwrites the result with a quiet NaN whenever the compare was
7528    /// unordered (either operand NaN); on the ordered path VMINNM/VMAXNM
7529    /// order -0.0 below +0.0, matching WASM's min(+0,-0) = -0 / max = +0.
7530    /// Clobbers ONLY Dd and the flags (the previous pseudo-op's ordered IT
7531    /// GT/MI select returned the WRONG operand for NaN and ±0 mixes).
7532    ///
7533    /// Ok-or-Err: `dd` must not alias `dn`/`dm` — the VS fix-up reads them
7534    /// AFTER VMINNM wrote `dd` (the selector always allocates a fresh
7535    /// destination while both sources are still marked live).
7536    fn encode_thumb_f64_minmax(
7537        &self,
7538        dd: &VfpReg,
7539        dn: &VfpReg,
7540        dm: &VfpReg,
7541        is_min: bool,
7542    ) -> Result<Vec<u8>> {
7543        if dd == dn || dd == dm {
7544            return Err(synth_core::Error::synthesis(format!(
7545                "F64{}: destination {dd:?} aliases a source ({dn:?},{dm:?}) — \
7546                 the unordered NaN fix-up would read a clobbered operand \
7547                 (compiler bug: the selector must allocate a fresh D-temp)",
7548                if is_min { "Min" } else { "Max" },
7549            )));
7550        }
7551        let mut bytes = Vec::new();
7552        let dd_num = vfp_dreg_to_num(dd)?;
7553        let dn_num = vfp_dreg_to_num(dn)?;
7554        let dm_num = vfp_dreg_to_num(dm)?;
7555        let (vd, d) = encode_dreg(dd_num);
7556        let (vn, n) = encode_dreg(dn_num);
7557        let (vm, m) = encode_dreg(dm_num);
7558
7559        // VCMP.F64 Dn, Dm (clang-verified: vcmp.f64 d2,d3 = eeb4 2b43)
7560        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7561        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7562        // VMRS APSR_nzcv, FPSCR
7563        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7564        // VMINNM.F64 / VMAXNM.F64 Dd, Dn, Dm (clang-verified:
7565        // vminnm.f64 d1,d2,d3 = fe82 1b43 ; vmaxnm = fe82 1b03)
7566        let base: u32 = if is_min { 0xFE800B40 } else { 0xFE800B00 };
7567        let vnm = base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7568        bytes.extend_from_slice(&vfp_to_thumb_bytes(vnm));
7569        // IT VS (unordered ⇒ at least one NaN operand)
7570        bytes.extend_from_slice(&0xBF68_u16.to_le_bytes());
7571        // VADD.F64(VS) Dd, Dn, Dm — NaN + x propagates a quiet NaN
7572        let vadd = 0xEE300B00 | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7573        bytes.extend_from_slice(&vfp_to_thumb_bytes(vadd));
7574
7575        Ok(bytes)
7576    }
7577
7578    /// Encode F64 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7579    /// encoder scratch, #212), the flags, and Dd:
7580    ///
7581    ///   VMOV R12, S(2m+1)   (high word of the SIGN source Dm)
7582    ///   CMP  R12, #0        (N flag = the sign bit)
7583    ///   VABS.F64 Dd, Dn     (magnitude, sign cleared)
7584    ///   IT MI ; VNEG.F64(MI) Dd, Dd
7585    ///
7586    /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7587    /// The R12 capture happens BEFORE Dd is written, so Dd aliasing Dn or Dm
7588    /// is safe. (The previous pseudo-op clobbered R0/R1/R2 behind the
7589    /// allocator's back — the #615 class.)
7590    fn encode_thumb_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7591        let dm_num = vfp_dreg_to_num(dm)?;
7592        if dm_num > 7 {
7593            return Err(synth_core::Error::synthesis(format!(
7594                "F64Copysign: sign source {dm:?} has no S-register alias \
7595                 (D8..D15) — the selector allocates only D0..D7"
7596            )));
7597        }
7598        let mut bytes = Vec::new();
7599        // VMOV R12, S(2m+1) — the sign source's high word.
7600        let (vn_s, n_s) = encode_sreg(2 * dm_num + 1);
7601        let vmov = 0xEE100A10 | (vn_s << 16) | (12 << 12) | (n_s << 7);
7602        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7603        // CMP R12, #0 (T2: CMP.W R12, #0) — N = bit31 of the sign word.
7604        bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7605        bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7606        // VABS.F64 Dd, Dn
7607        let dd_num = vfp_dreg_to_num(dd)?;
7608        let dn_num = vfp_dreg_to_num(dn)?;
7609        let (vd, d) = encode_dreg(dd_num);
7610        let (vn, n) = encode_dreg(dn_num);
7611        let vabs = 0xEEB00BC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7612        bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7613        // IT MI ; VNEG.F64(MI) Dd, Dd
7614        bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7615        let vneg = 0xEEB10B40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7616        bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7617
7618        Ok(bytes)
7619    }
7620
7621    /// Encode VCVT.S32/U32.F32 + VMOV as Thumb-2
7622    fn encode_thumb_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7623        let mut bytes = Vec::new();
7624
7625        let sm_num = vfp_sreg_to_num(sm)?;
7626        let (vd, d) = encode_sreg(sm_num);
7627        let (vm, m) = encode_sreg(sm_num);
7628        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
7629        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7630        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7631
7632        // VMOV Rd, Sm
7633        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
7634        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7635
7636        Ok(bytes)
7637    }
7638
7639    // === Thumb-2 32-bit encoding helpers ===
7640
7641    /// Encode Thumb-2 32-bit ADD with immediate
7642    fn encode_thumb32_add(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7643        let rd_bits = reg_to_bits(rd);
7644        let rn_bits = reg_to_bits(rn);
7645
7646        // The `i:imm3:imm8` field is split the same way for both forms.
7647        let i_bit = (imm >> 11) & 1;
7648        let imm3 = (imm >> 8) & 0x7;
7649        let imm8 = imm & 0xFF;
7650
7651        let hw1_base = if imm <= 0xFF {
7652            // ADD.W (T3): the field is a ThumbExpandImm modified immediate. For
7653            // imm <= 0xFF (i:imm3 = 0000) it is the zero-extended byte, which is
7654            // correct — keep this form so existing encodings stay bit-identical.
7655            0xF100
7656        } else if imm <= 0xFFF {
7657            // ADDW (T4): a PLAIN 12-bit immediate (0..4095) — no ThumbExpandImm.
7658            // This is what makes `add sp, sp, #frame` correct for frame sizes
7659            // >= 256, which ADD.W (T3) would silently mis-encode (e.g. #256 -> #0).
7660            0xF200
7661        } else {
7662            return Err(synth_core::Error::synthesis(
7663                "ADD immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7664            ));
7665        };
7666
7667        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7668        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7669
7670        let mut bytes = hw1.to_le_bytes().to_vec();
7671        bytes.extend_from_slice(&hw2.to_le_bytes());
7672        Ok(bytes)
7673    }
7674
7675    /// Encode Thumb-2 32-bit SUB with immediate
7676    fn encode_thumb32_sub(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7677        let rd_bits = reg_to_bits(rd);
7678        let rn_bits = reg_to_bits(rn);
7679
7680        let i_bit = (imm >> 11) & 1;
7681        let imm3 = (imm >> 8) & 0x7;
7682        let imm8 = imm & 0xFF;
7683
7684        let hw1_base = if imm <= 0xFF {
7685            // SUB.W (T3) modified immediate — correct for the zero-extended byte
7686            // (imm <= 0xFF). Kept bit-identical for existing encodings.
7687            0xF1A0
7688        } else if imm <= 0xFFF {
7689            // SUBW (T4): plain 12-bit immediate (0..4095). Makes
7690            // `sub sp, sp, #frame` correct for frame sizes >= 256.
7691            0xF2A0
7692        } else {
7693            return Err(synth_core::Error::synthesis(
7694                "SUB immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7695            ));
7696        };
7697
7698        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7699        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7700
7701        let mut bytes = hw1.to_le_bytes().to_vec();
7702        bytes.extend_from_slice(&hw2.to_le_bytes());
7703        Ok(bytes)
7704    }
7705
7706    /// Encode Thumb-2 32-bit ADDS with immediate (sets flags)
7707    fn encode_thumb32_adds(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7708        let rd_bits = reg_to_bits(rd);
7709        let rn_bits = reg_to_bits(rn);
7710
7711        // ADDS.W (flag-setting) has only the modified-immediate form — error on
7712        // an un-encodable value rather than silently add the wrong constant.
7713        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7714            synth_core::Error::synthesis(
7715                "ADDS immediate is not a valid ThumbExpandImm — materialize into a register",
7716            )
7717        })?;
7718        let i_bit = (field >> 11) & 1;
7719        let imm3 = (field >> 8) & 0x7;
7720        let imm8 = field & 0xFF;
7721
7722        // ADDS.W Rd, Rn, #imm (with S=1)
7723        // First halfword: 1111 0 i 0 1000 1 Rn = F110 | i<<10 | Rn
7724        let hw1: u16 = (0xF110 | (i_bit << 10) | rn_bits) as u16;
7725        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7726
7727        let mut bytes = hw1.to_le_bytes().to_vec();
7728        bytes.extend_from_slice(&hw2.to_le_bytes());
7729        Ok(bytes)
7730    }
7731
7732    /// Encode Thumb-2 32-bit SUBS with immediate (sets flags)
7733    fn encode_thumb32_subs(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7734        let rd_bits = reg_to_bits(rd);
7735        let rn_bits = reg_to_bits(rn);
7736
7737        // SUBS.W (flag-setting) has only the modified-immediate form — error on
7738        // an un-encodable value rather than silently subtract the wrong constant.
7739        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7740            synth_core::Error::synthesis(
7741                "SUBS immediate is not a valid ThumbExpandImm — materialize into a register",
7742            )
7743        })?;
7744        let i_bit = (field >> 11) & 1;
7745        let imm3 = (field >> 8) & 0x7;
7746        let imm8 = field & 0xFF;
7747
7748        // SUBS.W Rd, Rn, #imm (with S=1)
7749        // First halfword: 1111 0 i 0 1101 1 Rn = F1B0 | i<<10 | Rn
7750        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7751        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7752
7753        let mut bytes = hw1.to_le_bytes().to_vec();
7754        bytes.extend_from_slice(&hw2.to_le_bytes());
7755        Ok(bytes)
7756    }
7757
7758    /// Encode Thumb-2 32-bit MOVW (16-bit immediate)
7759    ///
7760    /// # Contract (Verus-style)
7761    /// ```text
7762    /// requires rd <= R14
7763    /// ensures result.len() == 4
7764    /// ensures (imm & 0xFFFF) can be reconstructed from the encoding
7765    /// ```
7766    fn encode_thumb32_movw(&self, rd: &Reg, imm: u32) -> Result<Vec<u8>> {
7767        let rd_bits = reg_to_bits(rd);
7768        reg_bits_checked(rd_bits)?;
7769        let imm16 = imm & 0xFFFF;
7770
7771        // MOVW Rd, #imm16
7772        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7773        let imm4 = (imm16 >> 12) & 0xF;
7774        let i_bit = (imm16 >> 11) & 1;
7775        let imm3 = (imm16 >> 8) & 0x7;
7776        let imm8 = imm16 & 0xFF;
7777
7778        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7779        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7780
7781        let mut bytes = hw1.to_le_bytes().to_vec();
7782        bytes.extend_from_slice(&hw2.to_le_bytes());
7783        encoding_contracts::verify_thumb32(&bytes);
7784        Ok(bytes)
7785    }
7786
7787    /// Encode Thumb-2 32-bit shift with immediate
7788    ///
7789    /// # Contract (Verus-style)
7790    /// ```text
7791    /// requires rd <= R14, rm <= R14
7792    /// ensures result.len() == 4
7793    /// ```
7794    fn encode_thumb32_shift(
7795        &self,
7796        rd: &Reg,
7797        rm: &Reg,
7798        shift: u32,
7799        shift_type: u8,
7800    ) -> Result<Vec<u8>> {
7801        let rd_bits = reg_to_bits(rd);
7802        let rm_bits = reg_to_bits(rm);
7803        reg_bits_checked(rd_bits)?;
7804        reg_bits_checked(rm_bits)?;
7805        let imm5 = shift & 0x1F;
7806        let imm2 = imm5 & 0x3;
7807        let imm3 = (imm5 >> 2) & 0x7;
7808
7809        // MOV.W Rd, Rm, <shift> #imm
7810        // EA4F 0 imm3 Rd imm2 type Rm
7811        let hw1: u16 = 0xEA4F;
7812        let hw2: u16 =
7813            ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | ((shift_type as u32) << 4) | rm_bits)
7814                as u16;
7815
7816        let mut bytes = hw1.to_le_bytes().to_vec();
7817        bytes.extend_from_slice(&hw2.to_le_bytes());
7818        Ok(bytes)
7819    }
7820
7821    /// Encode Thumb-2 32-bit shift by register
7822    /// Encoding: 11111010 0xx0 Rn | 1111 Rd 0000 Rm
7823    /// shift_type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
7824    fn encode_thumb32_shift_reg(
7825        &self,
7826        rd: &Reg,
7827        rn: &Reg,
7828        rm: &Reg,
7829        shift_type: u8,
7830    ) -> Result<Vec<u8>> {
7831        let rd_bits = reg_to_bits(rd);
7832        let rn_bits = reg_to_bits(rn);
7833        let rm_bits = reg_to_bits(rm);
7834
7835        // hw1: 1111 1010 0xx0 Rn
7836        let hw1: u16 = (0xFA00 | ((shift_type as u32) << 5) | rn_bits) as u16;
7837        // hw2: 1111 Rd 0000 Rm
7838        let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
7839
7840        let mut bytes = hw1.to_le_bytes().to_vec();
7841        bytes.extend_from_slice(&hw2.to_le_bytes());
7842        Ok(bytes)
7843    }
7844
7845    /// Encode Thumb-2 32-bit CMP with immediate
7846    fn encode_thumb32_cmp_imm(&self, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7847        let rn_bits = reg_to_bits(rn);
7848
7849        // CMP.W has only the modified-immediate form (no plain-imm12 like ADDW),
7850        // so an un-encodable immediate MUST be materialized into a register by
7851        // the selector. Error rather than silently compare the wrong constant.
7852        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7853            synth_core::Error::synthesis(
7854                "CMP immediate is not a valid ThumbExpandImm — materialize into a register",
7855            )
7856        })?;
7857        let i_bit = (field >> 11) & 1;
7858        let imm3 = (field >> 8) & 0x7;
7859        let imm8 = field & 0xFF;
7860
7861        // CMP.W Rn, #imm
7862        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7863        let hw2: u16 = ((imm3 << 12) | 0x0F00 | imm8) as u16;
7864
7865        let mut bytes = hw1.to_le_bytes().to_vec();
7866        bytes.extend_from_slice(&hw2.to_le_bytes());
7867        Ok(bytes)
7868    }
7869
7870    /// #372/#382: resolve the base register AND residual immediate offset for an
7871    /// `I64Ldr`/`I64Str` whose address may carry an index register. Returns
7872    /// `(base, low_offset)`; the caller accesses the halves at `[base,
7873    /// #low_offset]` and `[base, #low_offset + 4]`.
7874    ///
7875    /// - Frame access (no `offset_reg`, e.g. a spilled local at `[SP, #off]`):
7876    ///   returns `(addr.base, off)` and emits NOTHING — byte-identical.
7877    /// - Memory access (`reg_imm(R11, addr, offset)` = `R11 + addr + offset`)
7878    ///   with `offset + 4 <= 0xFFF`: emits `ADD.W ip, base, index` and returns
7879    ///   `(ip, offset)`, folding `offset`/`offset+4` into the halves' imm12.
7880    ///   Byte-identical to the pre-#382 (#372) behavior.
7881    /// - Memory access with `offset + 4 > 0xFFF`: the imm12 form cannot hold the
7882    ///   high half's offset, so `encode_thumb32_ldr`'s `check_ldst_imm12` (#259)
7883    ///   rightly refused it and the WHOLE function was skipped (#382). Instead
7884    ///   MATERIALIZE the offset into the base: `ADD ip, index, #offset` (against
7885    ///   the read-only INDEX register, so `encode_thumb32_add_imm` never trips its
7886    ///   `rd==rn==R12` alias trap), then `ADD.W ip, ip, base` (+ R11), and return
7887    ///   `(ip, 0)` so the halves use `[ip, #0]` / `[ip, #4]`.
7888    ///
7889    /// The effective address is fully materialized into `ip` BEFORE the halves
7890    /// are accessed, so an `rdlo` aliasing the index register is safe.
7891    fn i64_effective_base(&self, bytes: &mut Vec<u8>, addr: &MemAddr) -> Result<(Reg, u32)> {
7892        let offset = if addr.offset < 0 {
7893            0u32
7894        } else {
7895            addr.offset as u32
7896        };
7897        match addr.offset_reg {
7898            Some(idx) => {
7899                let ip = Reg::R12;
7900                if offset.wrapping_add(4) > 0xFFF {
7901                    // Large static offset (#382): fold it (and R11) into ip so the
7902                    // imm12 halves stay in range instead of skipping the function.
7903                    // ADD ip, index, #offset  (index != ip → no add_imm alias trap)
7904                    bytes.extend_from_slice(&self.encode_thumb32_add_imm(&ip, &idx, offset)?);
7905                    // ADD.W ip, ip, base  (+ R11)
7906                    bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
7907                        reg_to_bits(&ip),
7908                        reg_to_bits(&ip),
7909                        reg_to_bits(&addr.base),
7910                    )?);
7911                    Ok((ip, 0))
7912                } else {
7913                    // ADD.W ip, addr.base, idx  (Thumb-2, byte-verified vs as)
7914                    let hw1: u16 = 0xEB00 | reg_to_bits(&addr.base) as u16;
7915                    let hw2: u16 = 0x0C00 | reg_to_bits(&idx) as u16;
7916                    bytes.extend_from_slice(&hw1.to_le_bytes());
7917                    bytes.extend_from_slice(&hw2.to_le_bytes());
7918                    Ok((ip, offset))
7919                }
7920            }
7921            None => Ok((addr.base, offset)),
7922        }
7923    }
7924
7925    /// Encode Thumb-2 32-bit LDR
7926    fn encode_thumb32_ldr(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7927        let rd_bits = reg_to_bits(rd);
7928        let base_bits = reg_to_bits(base);
7929
7930        // LDR.W Rd, [Rn, #imm12]
7931        check_ldst_imm12(offset)?;
7932        let hw1: u16 = (0xF8D0 | base_bits) as u16;
7933        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7934
7935        let mut bytes = hw1.to_le_bytes().to_vec();
7936        bytes.extend_from_slice(&hw2.to_le_bytes());
7937        Ok(bytes)
7938    }
7939
7940    /// Encode Thumb-2 32-bit STR
7941    fn encode_thumb32_str(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7942        let rd_bits = reg_to_bits(rd);
7943        let base_bits = reg_to_bits(base);
7944
7945        // STR.W Rd, [Rn, #imm12]
7946        check_ldst_imm12(offset)?;
7947        let hw1: u16 = (0xF8C0 | base_bits) as u16;
7948        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7949
7950        let mut bytes = hw1.to_le_bytes().to_vec();
7951        bytes.extend_from_slice(&hw2.to_le_bytes());
7952        Ok(bytes)
7953    }
7954
7955    /// Encode Thumb-2 32-bit LDR with register offset: LDR.W Rd, [Rn, Rm]
7956    fn encode_thumb32_ldr_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7957        let rd_bits = reg_to_bits(rd);
7958        let base_bits = reg_to_bits(base);
7959        let rm_bits = reg_to_bits(offset_reg);
7960
7961        // LDR.W Rd, [Rn, Rm, LSL #0]
7962        // Encoding: 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
7963        // imm2 = 00 for no shift (LSL #0)
7964        let hw1: u16 = (0xF850 | base_bits) as u16;
7965        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7966
7967        let mut bytes = hw1.to_le_bytes().to_vec();
7968        bytes.extend_from_slice(&hw2.to_le_bytes());
7969        Ok(bytes)
7970    }
7971
7972    /// Encode Thumb-2 32-bit STR with register offset: STR.W Rd, [Rn, Rm]
7973    fn encode_thumb32_str_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7974        let rd_bits = reg_to_bits(rd);
7975        let base_bits = reg_to_bits(base);
7976        let rm_bits = reg_to_bits(offset_reg);
7977
7978        // STR.W Rd, [Rn, Rm, LSL #0]
7979        // Encoding: 1111 1000 0100 Rn | Rt 0000 00 imm2 Rm
7980        // imm2 = 00 for no shift (LSL #0)
7981        let hw1: u16 = (0xF840 | base_bits) as u16;
7982        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7983
7984        let mut bytes = hw1.to_le_bytes().to_vec();
7985        bytes.extend_from_slice(&hw2.to_le_bytes());
7986        Ok(bytes)
7987    }
7988
7989    // === Sub-word load/store Thumb-2 encoding helpers ===
7990
7991    /// Encode Thumb-2 32-bit LDRB with immediate: LDRB.W Rd, [Rn, #imm12]
7992    fn encode_thumb32_ldrb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7993        let rd_bits = reg_to_bits(rd);
7994        let base_bits = reg_to_bits(base);
7995        // LDRB.W Rd, [Rn, #imm12]: 1111 1000 1001 Rn | Rt imm12
7996        check_ldst_imm12(offset)?;
7997        let hw1: u16 = (0xF890 | base_bits) as u16;
7998        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7999        let mut bytes = hw1.to_le_bytes().to_vec();
8000        bytes.extend_from_slice(&hw2.to_le_bytes());
8001        Ok(bytes)
8002    }
8003
8004    /// Encode Thumb-2 32-bit LDRB with register: LDRB.W Rd, [Rn, Rm]
8005    fn encode_thumb32_ldrb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8006        let rd_bits = reg_to_bits(rd);
8007        let base_bits = reg_to_bits(base);
8008        let rm_bits = reg_to_bits(offset_reg);
8009        // LDRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0001 Rn | Rt 0000 00 imm2 Rm
8010        let hw1: u16 = (0xF810 | base_bits) as u16;
8011        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8012        let mut bytes = hw1.to_le_bytes().to_vec();
8013        bytes.extend_from_slice(&hw2.to_le_bytes());
8014        Ok(bytes)
8015    }
8016
8017    /// Encode Thumb-2 32-bit LDRSB with immediate: LDRSB.W Rd, [Rn, #imm12]
8018    fn encode_thumb32_ldrsb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8019        let rd_bits = reg_to_bits(rd);
8020        let base_bits = reg_to_bits(base);
8021        // LDRSB.W Rd, [Rn, #imm12]: 1111 1001 1001 Rn | Rt imm12
8022        check_ldst_imm12(offset)?;
8023        let hw1: u16 = (0xF990 | base_bits) as u16;
8024        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8025        let mut bytes = hw1.to_le_bytes().to_vec();
8026        bytes.extend_from_slice(&hw2.to_le_bytes());
8027        Ok(bytes)
8028    }
8029
8030    /// Encode Thumb-2 32-bit LDRSB with register: LDRSB.W Rd, [Rn, Rm]
8031    fn encode_thumb32_ldrsb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8032        let rd_bits = reg_to_bits(rd);
8033        let base_bits = reg_to_bits(base);
8034        let rm_bits = reg_to_bits(offset_reg);
8035        // LDRSB.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0001 Rn | Rt 0000 00 imm2 Rm
8036        let hw1: u16 = (0xF910 | base_bits) as u16;
8037        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8038        let mut bytes = hw1.to_le_bytes().to_vec();
8039        bytes.extend_from_slice(&hw2.to_le_bytes());
8040        Ok(bytes)
8041    }
8042
8043    /// Encode Thumb-2 32-bit LDRH with immediate: LDRH.W Rd, [Rn, #imm12]
8044    fn encode_thumb32_ldrh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8045        let rd_bits = reg_to_bits(rd);
8046        let base_bits = reg_to_bits(base);
8047        // LDRH.W Rd, [Rn, #imm12]: 1111 1000 1011 Rn | Rt imm12
8048        check_ldst_imm12(offset)?;
8049        let hw1: u16 = (0xF8B0 | base_bits) as u16;
8050        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8051        let mut bytes = hw1.to_le_bytes().to_vec();
8052        bytes.extend_from_slice(&hw2.to_le_bytes());
8053        Ok(bytes)
8054    }
8055
8056    /// Encode Thumb-2 32-bit LDRH with register: LDRH.W Rd, [Rn, Rm]
8057    fn encode_thumb32_ldrh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8058        let rd_bits = reg_to_bits(rd);
8059        let base_bits = reg_to_bits(base);
8060        let rm_bits = reg_to_bits(offset_reg);
8061        // LDRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0011 Rn | Rt 0000 00 imm2 Rm
8062        let hw1: u16 = (0xF830 | base_bits) as u16;
8063        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8064        let mut bytes = hw1.to_le_bytes().to_vec();
8065        bytes.extend_from_slice(&hw2.to_le_bytes());
8066        Ok(bytes)
8067    }
8068
8069    /// Encode Thumb-2 32-bit LDRSH with immediate: LDRSH.W Rd, [Rn, #imm12]
8070    fn encode_thumb32_ldrsh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8071        let rd_bits = reg_to_bits(rd);
8072        let base_bits = reg_to_bits(base);
8073        // LDRSH.W Rd, [Rn, #imm12]: 1111 1001 1011 Rn | Rt imm12
8074        check_ldst_imm12(offset)?;
8075        let hw1: u16 = (0xF9B0 | base_bits) as u16;
8076        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8077        let mut bytes = hw1.to_le_bytes().to_vec();
8078        bytes.extend_from_slice(&hw2.to_le_bytes());
8079        Ok(bytes)
8080    }
8081
8082    /// Encode Thumb-2 32-bit LDRSH with register: LDRSH.W Rd, [Rn, Rm]
8083    fn encode_thumb32_ldrsh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8084        let rd_bits = reg_to_bits(rd);
8085        let base_bits = reg_to_bits(base);
8086        let rm_bits = reg_to_bits(offset_reg);
8087        // LDRSH.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0011 Rn | Rt 0000 00 imm2 Rm
8088        let hw1: u16 = (0xF930 | base_bits) as u16;
8089        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8090        let mut bytes = hw1.to_le_bytes().to_vec();
8091        bytes.extend_from_slice(&hw2.to_le_bytes());
8092        Ok(bytes)
8093    }
8094
8095    /// Encode Thumb-2 32-bit STRB with immediate: STRB.W Rd, [Rn, #imm12]
8096    fn encode_thumb32_strb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8097        let rd_bits = reg_to_bits(rd);
8098        let base_bits = reg_to_bits(base);
8099        // STRB.W Rd, [Rn, #imm12]: 1111 1000 1000 Rn | Rt imm12
8100        check_ldst_imm12(offset)?;
8101        let hw1: u16 = (0xF880 | base_bits) as u16;
8102        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8103        let mut bytes = hw1.to_le_bytes().to_vec();
8104        bytes.extend_from_slice(&hw2.to_le_bytes());
8105        Ok(bytes)
8106    }
8107
8108    /// Encode Thumb-2 32-bit STRB with register: STRB.W Rd, [Rn, Rm]
8109    fn encode_thumb32_strb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8110        let rd_bits = reg_to_bits(rd);
8111        let base_bits = reg_to_bits(base);
8112        let rm_bits = reg_to_bits(offset_reg);
8113        // STRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0000 Rn | Rt 0000 00 imm2 Rm
8114        let hw1: u16 = (0xF800 | base_bits) as u16;
8115        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8116        let mut bytes = hw1.to_le_bytes().to_vec();
8117        bytes.extend_from_slice(&hw2.to_le_bytes());
8118        Ok(bytes)
8119    }
8120
8121    /// Encode Thumb-2 32-bit STRH with immediate: STRH.W Rd, [Rn, #imm12]
8122    fn encode_thumb32_strh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8123        let rd_bits = reg_to_bits(rd);
8124        let base_bits = reg_to_bits(base);
8125        // STRH.W Rd, [Rn, #imm12]: 1111 1000 1010 Rn | Rt imm12
8126        check_ldst_imm12(offset)?;
8127        let hw1: u16 = (0xF8A0 | base_bits) as u16;
8128        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8129        let mut bytes = hw1.to_le_bytes().to_vec();
8130        bytes.extend_from_slice(&hw2.to_le_bytes());
8131        Ok(bytes)
8132    }
8133
8134    /// Encode Thumb-2 32-bit STRH with register: STRH.W Rd, [Rn, Rm]
8135    fn encode_thumb32_strh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8136        let rd_bits = reg_to_bits(rd);
8137        let base_bits = reg_to_bits(base);
8138        let rm_bits = reg_to_bits(offset_reg);
8139        // STRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0010 Rn | Rt 0000 00 imm2 Rm
8140        let hw1: u16 = (0xF820 | base_bits) as u16;
8141        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8142        let mut bytes = hw1.to_le_bytes().to_vec();
8143        bytes.extend_from_slice(&hw2.to_le_bytes());
8144        Ok(bytes)
8145    }
8146
8147    /// Encode Thumb-2 32-bit ADD with immediate: ADD.W Rd, Rn, #imm
8148    fn encode_thumb32_add_imm(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
8149        let rd_bits = reg_to_bits(rd);
8150        let rn_bits = reg_to_bits(rn);
8151
8152        // In-range immediates (<= 0xFFF) delegate to `encode_thumb32_add`,
8153        // which picks the correct form per value:
8154        //   - imm <= 0xFF  -> ADD.W (T3). Its `i:imm3:imm8` field is a
8155        //     ThumbExpandImm MODIFIED immediate — raw == expanded only here.
8156        //   - 0x100..=0xFFF -> ADDW (T4, 0xF200): a PLAIN 12-bit immediate.
8157        //
8158        // #681: this function used to pack the raw value into the T3 field for
8159        // ALL imm <= 0xFFF. ThumbExpandImm(0x200) = 0 and ThumbExpandImm(0x400)
8160        // = 0x8000_0000, so every dynamic-address load/store with a static
8161        // offset in 0x100..=0xFFF silently computed a WRONG address — and in
8162        // --safety-bounds software the guard checked the intended address while
8163        // the access used the mis-encoded one (bounds bypass). Same
8164        // ThumbExpandImm raw-packing class as #253/#255, reached via #382.
8165        if imm <= 0xFFF {
8166            self.encode_thumb32_add(rd, rn, imm)
8167        } else {
8168            // Out-of-range immediate (> 0xFFF): materialize it into a scratch
8169            // register, then ADD.W Rd, Rn, scratch. This is the #180/#185
8170            // "encoder must produce a legal sequence, not assert" class — see #350.
8171            //
8172            // Scratch choice (must NEVER equal Rn, or Rn would be clobbered before
8173            // the ADD reads it):
8174            //   - rd != rn  => use rd itself (rn is untouched, since rd != rn).
8175            //   - rd == rn  => use R12/IP (the reserved encoder scratch). rd/rn are
8176            //                  never R12 (R12 is non-allocatable), so it can't alias.
8177            //
8178            // The materialized value is the same whether or not MOVT is emitted, so
8179            // the byte length depends only on `imm` (and rd==rn) — the size probe and
8180            // the final emit therefore agree (mandatory: the function is encoded twice).
8181            let scratch: u32 = if rd_bits == rn_bits {
8182                12 // R12/IP — in-place add, can't use rd because rd == rn
8183            } else {
8184                rd_bits // rn is preserved because rd != rn
8185            };
8186            // Invariant: the scratch must never alias Rn (would clobber it before
8187            // the ADD reads it). Unreachable in real codegen (rd/rn are never R12,
8188            // which is reserved encoder scratch), but the encoder is also driven by
8189            // the `encoder_no_panic` fuzz harness with ARBITRARY registers — incl.
8190            // rd==rn==R12, which makes scratch (R12) alias Rn. The encoder contract
8191            // (#180/#185) is Ok-or-Err, never a panic, so return a typed error
8192            // instead of asserting. #350 follow-up.
8193            if scratch == rn_bits {
8194                return Err(synth_core::Error::synthesis(format!(
8195                    "ADD #imm: cannot lower #{imm:#x} for Rd==Rn==R12 — no free scratch \
8196                     register (R12 is the reserved encoder scratch and aliases Rn here)"
8197                )));
8198            }
8199
8200            let lo16 = imm & 0xFFFF;
8201            let hi16 = (imm >> 16) & 0xFFFF;
8202
8203            let mut bytes = self.encode_thumb32_movw_raw(scratch, lo16)?;
8204            if hi16 != 0 {
8205                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(scratch, hi16)?);
8206            }
8207            bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rn_bits, scratch)?);
8208            Ok(bytes)
8209        }
8210    }
8211
8212    // === Raw encoding helpers for POPCNT (take register numbers directly) ===
8213
8214    /// Encode Thumb-2 32-bit MOVW (16-bit immediate) - raw version
8215    ///
8216    /// # Contract (Verus-style)
8217    /// ```text
8218    /// requires rd <= 14, imm16 <= 0xFFFF
8219    /// ensures result.len() == 4
8220    /// ```
8221    fn encode_thumb32_movw_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8222        reg_bits_checked(rd)?;
8223        encoding_contracts::verify_imm16(imm16);
8224        // MOVW Rd, #imm16
8225        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
8226        let imm16 = imm16 & 0xFFFF;
8227        let imm4 = (imm16 >> 12) & 0xF;
8228        let i_bit = (imm16 >> 11) & 1;
8229        let imm3 = (imm16 >> 8) & 0x7;
8230        let imm8 = imm16 & 0xFF;
8231
8232        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
8233        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8234
8235        let mut bytes = hw1.to_le_bytes().to_vec();
8236        bytes.extend_from_slice(&hw2.to_le_bytes());
8237        encoding_contracts::verify_thumb32(&bytes);
8238        Ok(bytes)
8239    }
8240
8241    /// Encode Thumb-2 32-bit MOVT (move top 16 bits) - raw version
8242    ///
8243    /// # Contract (Verus-style)
8244    /// ```text
8245    /// requires rd <= 14, imm16 <= 0xFFFF
8246    /// ensures result.len() == 4
8247    /// ```
8248    fn encode_thumb32_movt_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8249        reg_bits_checked(rd)?;
8250        encoding_contracts::verify_imm16(imm16);
8251        // MOVT Rd, #imm16
8252        // 1111 0 i 10 1 1 0 0 imm4 | 0 imm3 Rd imm8
8253        let imm16 = imm16 & 0xFFFF;
8254        let imm4 = (imm16 >> 12) & 0xF;
8255        let i_bit = (imm16 >> 11) & 1;
8256        let imm3 = (imm16 >> 8) & 0x7;
8257        let imm8 = imm16 & 0xFF;
8258
8259        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
8260        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8261
8262        let mut bytes = hw1.to_le_bytes().to_vec();
8263        bytes.extend_from_slice(&hw2.to_le_bytes());
8264        encoding_contracts::verify_thumb32(&bytes);
8265        Ok(bytes)
8266    }
8267
8268    /// Encode Thumb-2 32-bit LSR (logical shift right) with immediate - raw version
8269    fn encode_thumb32_lsr_raw(&self, rd: u32, rm: u32, shift: u32) -> Result<Vec<u8>> {
8270        // MOV.W Rd, Rm, LSR #imm
8271        // EA4F 0 imm3 Rd imm2 01 Rm
8272        let imm5 = shift & 0x1F;
8273        let imm2 = imm5 & 0x3;
8274        let imm3 = (imm5 >> 2) & 0x7;
8275
8276        let hw1: u16 = 0xEA4F;
8277        let hw2: u16 = ((imm3 << 12) | (rd << 8) | (imm2 << 6) | (0b01 << 4) | rm) as u16;
8278
8279        let mut bytes = hw1.to_le_bytes().to_vec();
8280        bytes.extend_from_slice(&hw2.to_le_bytes());
8281        Ok(bytes)
8282    }
8283
8284    /// Encode Thumb-2 32-bit AND with immediate - raw version
8285    fn encode_thumb32_and_imm_raw(&self, rd: u32, rn: u32, imm: u32) -> Result<Vec<u8>> {
8286        // AND.W Rd, Rn, #<modified_immediate>
8287        // F0 00 Rn | 0 imm3 Rd imm8
8288        //
8289        // #681 class audit: the field is a ThumbExpandImm modified immediate,
8290        // not a raw value. The only current caller (POPCNT final mask) passes
8291        // 0x3F, which expands to itself — the gate is byte-identical today and
8292        // closes the raw-packing landmine for any future caller.
8293        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
8294            synth_core::Error::synthesis(
8295                "AND immediate is not a valid ThumbExpandImm — materialize into a register",
8296            )
8297        })?;
8298        let i_bit = (field >> 11) & 1;
8299        let imm3 = (field >> 8) & 0x7;
8300        let imm8 = field & 0xFF;
8301
8302        let hw1: u16 = (0xF000 | (i_bit << 10) | rn) as u16;
8303        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8304
8305        let mut bytes = hw1.to_le_bytes().to_vec();
8306        bytes.extend_from_slice(&hw2.to_le_bytes());
8307        Ok(bytes)
8308    }
8309
8310    /// Encode Thumb-2 32-bit SUB (register) - raw version
8311    fn encode_thumb32_sub_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8312        // SUB.W Rd, Rn, Rm
8313        // EBA0 Rn | 0 Rd 00 00 Rm
8314        let hw1: u16 = (0xEBA0 | rn) as u16;
8315        let hw2: u16 = ((rd << 8) | rm) as u16;
8316
8317        let mut bytes = hw1.to_le_bytes().to_vec();
8318        bytes.extend_from_slice(&hw2.to_le_bytes());
8319        Ok(bytes)
8320    }
8321
8322    /// Encode Thumb-2 32-bit ADD (register) - raw version
8323    fn encode_thumb32_add_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8324        // ADD.W Rd, Rn, Rm
8325        // EB00 Rn | 0 Rd 00 00 Rm
8326        let hw1: u16 = (0xEB00 | rn) as u16;
8327        let hw2: u16 = ((rd << 8) | rm) as u16;
8328
8329        let mut bytes = hw1.to_le_bytes().to_vec();
8330        bytes.extend_from_slice(&hw2.to_le_bytes());
8331        Ok(bytes)
8332    }
8333
8334    /// Encode Thumb-2 32-bit ADDS (register, flag-setting) - raw version.
8335    /// Used as the high-register fallback for `ArmOp::Adds` (i64 low-word add)
8336    /// so R8-R11 pair operands don't overflow the 16-bit field — #178/#180.
8337    fn encode_thumb32_adds_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8338        // ADDS.W Rd, Rn, Rm (T3, S=1): EB10 Rn | 0 Rd 00 00 Rm
8339        let hw1: u16 = (0xEB10 | rn) as u16;
8340        let hw2: u16 = ((rd << 8) | rm) as u16;
8341        let mut bytes = hw1.to_le_bytes().to_vec();
8342        bytes.extend_from_slice(&hw2.to_le_bytes());
8343        Ok(bytes)
8344    }
8345
8346    /// Encode Thumb-2 32-bit SUBS (register, flag-setting) - raw version.
8347    /// High-register fallback for `ArmOp::Subs` (i64 low-word subtract) — #178/#180.
8348    fn encode_thumb32_subs_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8349        // SUBS.W Rd, Rn, Rm (T3, S=1): EBB0 Rn | 0 Rd 00 00 Rm
8350        let hw1: u16 = (0xEBB0 | rn) as u16;
8351        let hw2: u16 = ((rd << 8) | rm) as u16;
8352        let mut bytes = hw1.to_le_bytes().to_vec();
8353        bytes.extend_from_slice(&hw2.to_le_bytes());
8354        Ok(bytes)
8355    }
8356
8357    /// Encode a sequence of ARM instructions
8358    pub fn encode_sequence(&self, ops: &[ArmOp]) -> Result<Vec<u8>> {
8359        let mut code = Vec::new();
8360
8361        for op in ops {
8362            let encoded = self.encode(op)?;
8363            code.extend_from_slice(&encoded);
8364        }
8365
8366        Ok(code)
8367    }
8368}
8369
8370/// Convert register to bit encoding (0-15)
8371/// Reverse of the ARMv7-M `ThumbExpandImm`: given a 32-bit immediate, return the
8372/// 12-bit `i:imm3:imm8` field if it is a representable modified immediate, else
8373/// `None` (the caller must materialize the value into a register). This is the
8374/// shared correct path for the data-processing immediate encoders — without it
8375/// they pack raw bits and silently mis-encode any value `> 0xFF` that isn't a
8376/// modified immediate (the silent-miscompile class behind #251/#253/#255).
8377fn try_thumb_expand_imm(value: u32) -> Option<u32> {
8378    // i:imm3 = 0000 → 8-bit value, zero-extended (00000000 00000000 00000000 XY).
8379    if value <= 0xFF {
8380        return Some(value);
8381    }
8382    let b0 = value & 0xFF; // byte 0
8383    let b1 = (value >> 8) & 0xFF; // byte 1
8384    // 0x00XY00XY (i:imm3 = 0001) — XY in bytes 0 and 2
8385    if value == (b0 << 16) | b0 {
8386        return Some(0x100 | b0);
8387    }
8388    // 0xXY00XY00 (i:imm3 = 0010) — XY in bytes 1 and 3
8389    if value == (b1 << 24) | (b1 << 8) {
8390        return Some(0x200 | b1);
8391    }
8392    // 0xXYXYXYXY (i:imm3 = 0011) — XY in all four bytes
8393    if value == (b0 << 24) | (b0 << 16) | (b0 << 8) | b0 {
8394        return Some(0x300 | b0);
8395    }
8396    // An 8-bit value with bit 7 set, rotated right by 8..=31. `rotate_left(rot)`
8397    // undoes the encoded right rotation; if the result is `1bbbbbbb` (0x80..=0xFF)
8398    // the value is representable. imm12[11:7] = rot, imm12[6:0] = low 7 bits.
8399    for rot in 8..=31u32 {
8400        let unrot = value.rotate_left(rot);
8401        if (0x80..=0xFF).contains(&unrot) {
8402            return Some((rot << 7) | (unrot & 0x7F));
8403        }
8404    }
8405    None
8406}
8407
8408/// Guard a Thumb-2 `LDR/STR Rd, [Rn, #imm12]` offset. The imm12 form supports
8409/// `0..=4095`; a larger offset must be materialized into a register by the
8410/// selector (register-offset addressing). Returning `Err` rather than silently
8411/// masking `offset & 0xFFF` closes the wrong-address miscompile class (#259,
8412/// the load/store sibling of #253/#255).
8413fn check_ldst_imm12(offset: u32) -> Result<()> {
8414    if offset > 0xFFF {
8415        Err(synth_core::Error::synthesis(
8416            "load/store immediate offset > 0xFFF (4095) — materialize the offset into a register",
8417        ))
8418    } else {
8419        Ok(())
8420    }
8421}
8422
8423/// #916 — emit `Rd = 0` in Thumb-2, correctly for EVERY destination register.
8424///
8425/// The 16-bit `MOVS Rd, #imm8` (T1) is `0010 0 Rd(3) imm8` — the Rd field is
8426/// **three bits**. For R8-R12 `reg_to_bits` yields 8..12, so `rd_bits << 8`
8427/// overflows into bit 11 and `0x2000 | 0x0800` is `0x2800` = `CMP r0, #0`:
8428/// not a move at all. The destination is never written (it keeps stale data)
8429/// and the flags are clobbered. Same class as #180 / H-CODE-9, and the same
8430/// defect #311 fixed for `I64SetCond`.
8431///
8432/// High registers therefore take the 32-bit `MOV.W Rd, #imm8` (T2,
8433/// `F04F 0000 | Rd<<8 | imm8`), whose Rd field is four bits. `MOV.W` with S=0
8434/// does not set flags, which is what these zero-fill sites want anyway.
8435///
8436/// **Callers with branches must consult [`thumb_zero_fill_halfwords`].** This
8437/// emits 1 halfword for R0-R7 and 2 for R8-R12; any branch whose target lies
8438/// PAST this instruction moves when it widens and its displacement has to be
8439/// derived rather than hard-coded. (A branch targeting this instruction's own
8440/// address is unaffected — an instruction cannot move itself.)
8441fn emit_thumb_zero_fill(bytes: &mut Vec<u8>, rd_bits: u32) {
8442    if rd_bits < 8 {
8443        let movs: u16 = 0x2000 | ((rd_bits as u16) << 8);
8444        bytes.extend_from_slice(&movs.to_le_bytes());
8445    } else {
8446        bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
8447        bytes.extend_from_slice(&((rd_bits as u16) << 8).to_le_bytes());
8448    }
8449}
8450
8451/// Halfword length of the encoding [`emit_thumb_zero_fill`] picks for
8452/// `rd_bits`. Branch displacements spanning the zero-fill derive from this so
8453/// the encoder cannot drift from itself (#916; the byte-size estimator mirrors
8454/// it in `synth_synthesis::estimate_arm_byte_size`, pinned by the #498
8455/// `estimator_encoder_agreement` oracle).
8456fn thumb_zero_fill_halfwords(rd_bits: u32) -> u16 {
8457    if rd_bits < 8 { 1 } else { 2 }
8458}
8459
8460fn reg_to_bits(reg: &Reg) -> u32 {
8461    match reg {
8462        Reg::R0 => 0,
8463        Reg::R1 => 1,
8464        Reg::R2 => 2,
8465        Reg::R3 => 3,
8466        Reg::R4 => 4,
8467        Reg::R5 => 5,
8468        Reg::R6 => 6,
8469        Reg::R7 => 7,
8470        Reg::R8 => 8,
8471        Reg::R9 => 9,
8472        Reg::R10 => 10,
8473        Reg::R11 => 11,
8474        Reg::R12 => 12,
8475        Reg::SP => 13,
8476        Reg::LR => 14,
8477        Reg::PC => 15,
8478    }
8479}
8480
8481// ======================================================================
8482// #610 — i64 fixed-ABI expansion wrappers.
8483//
8484// The hand-written multi-instruction i64 cores (rotl/rotr and the div/rem
8485// shift-subtract loops) compute in FIXED low registers. Before #610 the
8486// div/rem arms ignored their operand fields outright (hardcoded R0:R1 /
8487// R2:R3 in, result to R0:R1) and the rot arms used R3/R4 scratch that
8488// collided with selector-assigned registers — then restored the saved
8489// scratch OVER the result (`POP {R4}` with rd_lo == R4), so the op
8490// returned the caller's stale register: 0 for every input under qemu.
8491//
8492// These wrappers make each core honor its register parameters:
8493//   1. save R0-R3,
8494//   2. marshal the operand registers into the core's fixed input regs via
8495//      the stack (permutation-safe: every source is read before any fixed
8496//      register is written),
8497//   3. run the fixed-reg core (self-preserving for R4+; R12 is encoder
8498//      scratch and never allocatable, #212),
8499//   4. MOV the result pair from R0:R1 into the selector's rd pair,
8500//   5. restore R0-R3, skipping any register the result now occupies.
8501//
8502// All emitted lengths are register-independent so the optimized path's
8503// byte-size estimator (`estimate_arm_byte_size`, pinned by the
8504// estimator↔encoder agreement oracle #498/#511) stays a constant per op.
8505// ======================================================================
8506
8507/// Steps 1+2: `PUSH {R0-R3}`, then marshal `srcs` (operand registers, any of
8508/// R0-R12) into `R0..R<n>` via individual stack pushes. Sources are all read
8509/// before any destination register is written, so arbitrary source/target
8510/// permutations (including operands living in R0-R3) are safe.
8511fn emit_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8512    debug_assert!(srcs.len() <= 4);
8513    // PUSH {R0-R3} — save the caller-visible low registers.
8514    bytes.extend_from_slice(&0xB40Fu16.to_le_bytes());
8515    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8516    for src in srcs.iter().rev() {
8517        let rt = reg_to_bits(src) as u16;
8518        bytes.extend_from_slice(&0xF84Du16.to_le_bytes());
8519        bytes.extend_from_slice(&((rt << 12) | 0x0D04).to_le_bytes());
8520    }
8521    // POP {Ri} — Ri := srcs[i].
8522    for i in 0..srcs.len() as u16 {
8523        bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes());
8524    }
8525}
8526
8527/// Steps 4+5: move the core's R0:R1 result into the selector's rd pair, then
8528/// restore the R0-R3 saved by [`emit_i64_fixed_abi_entry`], skipping any
8529/// register the result now lives in (its saved caller word is discarded).
8530fn emit_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8531    let lo = reg_to_bits(rdlo);
8532    let hi = reg_to_bits(rdhi);
8533    if lo == 1 && hi == 0 {
8534        // A fully swapped pair would clobber one half in either MOV order.
8535        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8536        return Err(synth_core::Error::synthesis(
8537            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8538        ));
8539    }
8540    let mov16 = |bytes: &mut Vec<u8>, rd: u32, rm: u32| {
8541        let d = ((rd >> 3) & 1) as u16;
8542        bytes.extend_from_slice(
8543            &(0x4600u16 | (d << 7) | ((rm as u16) << 3) | ((rd & 7) as u16)).to_le_bytes(),
8544        );
8545    };
8546    if hi == 0 {
8547        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8548        mov16(bytes, lo, 0);
8549        mov16(bytes, hi, 1);
8550    } else {
8551        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8552        mov16(bytes, hi, 1);
8553        mov16(bytes, lo, 0);
8554    }
8555    for i in 0..4u32 {
8556        if i == lo || i == hi {
8557            // The result lives here — drop the saved caller word.
8558            bytes.extend_from_slice(&0xB001u16.to_le_bytes()); // ADD SP, #4
8559        } else {
8560            bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes()); // POP {Ri}
8561        }
8562    }
8563    Ok(())
8564}
8565
8566/// WASM `i64.div_*` / `i64.rem_*` by zero must trap, matching the i32 path's
8567/// cmp/bne/udf guard. Emitted after marshaling, when the divisor pair is in
8568/// R2:R3: `ORRS R12, R2, R3` — `BNE` over a `UDF #0` when nonzero.
8569fn emit_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8570    bytes.extend_from_slice(&0xEA52u16.to_le_bytes()); // ORRS.W R12, R2, R3
8571    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8572    bytes.extend_from_slice(&0xD100u16.to_le_bytes()); // BNE.N +0 (skip the UDF)
8573    bytes.extend_from_slice(&0xDE00u16.to_le_bytes()); // UDF #0 — divide by zero
8574}
8575
8576/// WASM `i64.div_s(INT64_MIN, -1)` must trap (Core §4.3.2 `idiv_s`: the
8577/// quotient +2^63 is unrepresentable), matching the i32 path's overflow
8578/// guard — #633: without it the core negated INT64_MIN onto itself and
8579/// silently returned INT64_MIN. Emitted after marshaling, when the dividend
8580/// pair is in R0:R1 and the divisor pair in R2:R3; R12 is encoder scratch.
8581///
8582/// div_s ONLY — `i64.rem_s(INT64_MIN, -1)` is defined as 0 and must NOT
8583/// trap (`irem_s`), so the I64RemS arm never calls this. 22 bytes,
8584/// register-independent (estimator contract, #498/#511).
8585fn emit_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8586    // AND.W R12, R2, R3 — R12 == 0xFFFFFFFF iff divisor == -1
8587    bytes.extend_from_slice(&0xEA02u16.to_le_bytes());
8588    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8589    // CMN.W R12, #1 — EQ iff both divisor words are all-ones
8590    bytes.extend_from_slice(&0xF11Cu16.to_le_bytes());
8591    bytes.extend_from_slice(&0x0F01u16.to_le_bytes());
8592    // BNE .no_trap
8593    bytes.extend_from_slice(&0xD105u16.to_le_bytes());
8594    // CMP R0, #0 — dividend lo word of INT64_MIN
8595    bytes.extend_from_slice(&0x2800u16.to_le_bytes());
8596    // BNE .no_trap
8597    bytes.extend_from_slice(&0xD103u16.to_le_bytes());
8598    // CMP.W R1, #0x80000000 — dividend hi word of INT64_MIN
8599    bytes.extend_from_slice(&0xF1B1u16.to_le_bytes());
8600    bytes.extend_from_slice(&0x4F00u16.to_le_bytes());
8601    // BNE .no_trap
8602    bytes.extend_from_slice(&0xD100u16.to_le_bytes());
8603    // UDF #0 — signed-division overflow
8604    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
8605    // .no_trap:
8606}
8607
8608// ======================================================================
8609// #615 — A32 (ARM-mode) twins of the #610 i64 fixed-ABI wrappers above.
8610// Identical register contract, A32 encodings: the multi-instruction i64
8611// cores (rotl/rotr, div/rem) compute in fixed low registers (value/dividend
8612// R0:R1, amount R2 / divisor R2:R3, result to R0:R1); the wrappers marshal
8613// the selector-assigned operand registers in and the result out, saving and
8614// restoring the caller-visible R0-R3 around the core.
8615// ======================================================================
8616
8617/// A32 steps 1+2: `STMDB SP!, {R0-R3}`, then marshal `srcs` into `R0..R<n>`
8618/// via individual stack pushes (`STR src, [SP, #-4]!` in reverse order, then
8619/// `LDR Ri, [SP], #4`). Every source is read before any fixed register is
8620/// written, so arbitrary source/target permutations are safe.
8621fn emit_a32_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8622    debug_assert!(srcs.len() <= 4);
8623    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8624    // PUSH {R0-R3} — save the caller-visible low registers.
8625    w(bytes, 0xE92D_000F);
8626    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8627    for src in srcs.iter().rev() {
8628        w(bytes, 0xE52D_0004 | (reg_to_bits(src) << 12));
8629    }
8630    // LDR Ri, [SP], #4 — Ri := srcs[i].
8631    for i in 0..srcs.len() as u32 {
8632        w(bytes, 0xE49D_0004 | (i << 12));
8633    }
8634}
8635
8636/// A32 steps 4+5: move the core's R0:R1 result into the selector's rd pair,
8637/// then restore the R0-R3 saved by [`emit_a32_i64_fixed_abi_entry`], skipping
8638/// any register the result now lives in (its saved caller word is discarded).
8639fn emit_a32_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8640    let lo = reg_to_bits(rdlo);
8641    let hi = reg_to_bits(rdhi);
8642    if lo == 1 && hi == 0 {
8643        // A fully swapped pair would clobber one half in either MOV order.
8644        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8645        return Err(synth_core::Error::synthesis(
8646            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8647        ));
8648    }
8649    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8650    let mov = |bytes: &mut Vec<u8>, rd: u32, rm: u32| w(bytes, 0xE1A0_0000 | (rd << 12) | rm);
8651    if hi == 0 {
8652        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8653        mov(bytes, lo, 0);
8654        mov(bytes, hi, 1);
8655    } else {
8656        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8657        mov(bytes, hi, 1);
8658        mov(bytes, lo, 0);
8659    }
8660    for i in 0..4u32 {
8661        if i == lo || i == hi {
8662            // The result lives here — drop the saved caller word.
8663            w(bytes, 0xE28D_D004); // ADD SP, SP, #4
8664        } else {
8665            w(bytes, 0xE49D_0004 | (i << 12)); // LDR Ri, [SP], #4
8666        }
8667    }
8668    Ok(())
8669}
8670
8671/// A32 zero-divisor trap, emitted after marshaling when the divisor pair is
8672/// in R2:R3: `ORRS R12, R2, R3` sets Z iff the divisor is zero; `BNE` skips a
8673/// `UDF #0` (WASM div/rem-by-zero must trap, matching the Thumb-2 twin).
8674fn emit_a32_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8675    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8676    w(bytes, 0xE192_C003); // ORRS R12, R2, R3
8677    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8678    w(bytes, 0xE7F0_00F0); // UDF #0 — divide by zero
8679}
8680
8681/// A32 twin of [`emit_i64_divs_overflow_trap`] (#633): trap on
8682/// `i64.div_s(INT64_MIN, -1)`. Conditional execution replaces the Thumb
8683/// branches — the CMPEQ chain leaves EQ set only when divisor == -1 AND
8684/// dividend == INT64_MIN. div_s only; rem_s must keep returning 0.
8685fn emit_a32_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8686    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8687    w(bytes, 0xE002_C003); // AND   R12, R2, R3 (== 0xFFFFFFFF iff divisor == -1)
8688    w(bytes, 0xE37C_0001); // CMN   R12, #1     (EQ iff divisor == -1)
8689    w(bytes, 0x0350_0000); // CMPEQ R0, #0      (EQ iff also dividend lo == 0)
8690    w(bytes, 0x0351_0102); // CMPEQ R1, #0x80000000 (EQ iff dividend == INT64_MIN)
8691    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8692    w(bytes, 0xE7F0_00F0); // UDF #0 — signed-division overflow
8693}
8694
8695/// Fallible form of the `verify_reg_bits` contract. PC (R15) is not a valid
8696/// data operand for the Thumb-2 encodings that use this guard (SDIV/UDIV/MLS/…
8697/// are UNPREDICTABLE with PC). Synth's own codegen never emits PC there, but
8698/// the encoder must stay *total* over arbitrary `ArmOp` inputs — the fuzz
8699/// harness (`encoder_no_panic`) requires Ok-or-Err, never a panic. Pre-fix, the
8700/// `debug_assert` in `verify_reg_bits` aborted under `-Cdebug-assertions`.
8701/// Returns a typed Err instead. See #185.
8702fn reg_bits_checked(bits: u32) -> Result<()> {
8703    if bits > 14 {
8704        return Err(synth_core::Error::synthesis(format!(
8705            "register bits {bits} (PC/R15) is not a valid operand for this Thumb-2 encoding"
8706        )));
8707    }
8708    Ok(())
8709}
8710
8711/// Try to encode a 32-bit value as an ARM rotated immediate (imm8 ROR 2*rot4).
8712/// Returns Some((encoded_bits, 1)) if representable, None otherwise.
8713fn try_encode_rotated_imm(val: u32) -> Option<(u32, u32)> {
8714    if val == 0 {
8715        return Some((0, 1));
8716    }
8717    for rot in 0..16u32 {
8718        let shift = rot * 2;
8719        // Rotate left by shift (undo the ROR) to see if result fits in 8 bits
8720        let unrotated = val.rotate_left(shift);
8721        if unrotated <= 0xFF {
8722            // Encoded as: rot4(4 bits) | imm8(8 bits) = rotate_imm << 8 | imm8
8723            return Some(((rot << 8) | unrotated, 1));
8724        }
8725    }
8726    None
8727}
8728
8729/// Encode operand2 field and return (bits, immediate_flag).
8730/// For ARM32 mode, immediates use the rotated-immediate encoding (imm8 ROR 2*rot4).
8731/// Panics if an immediate value cannot be represented. Callers that need large
8732/// immediates should use MOVW/MOVT instead of Operand2::Imm.
8733fn encode_operand2(op2: &Operand2) -> Result<(u32, u32)> {
8734    match op2 {
8735        Operand2::Imm(val) => {
8736            let uval = *val as u32;
8737            // Attempt rotated-immediate encoding (ARM32 Operand2)
8738            if let Some(encoded) = try_encode_rotated_imm(uval) {
8739                Ok(encoded)
8740            } else {
8741                // #378-class honesty: an immediate that can't be expressed as an
8742                // ARM32 rotated immediate is an INTERNAL selector bug — large
8743                // constants must be materialized via MOVW/MOVT, not passed here.
8744                // FAIL HONESTLY with an Err rather than silently masking to
8745                // `uval & 0xFF` and emitting a WRONG immediate. The encoder is
8746                // Ok-or-Err, never corrupt (#180/#185); a loud Err is also why
8747                // this is an Err and not a panic (the `encoder_no_panic` fuzz
8748                // contract — malformed/oversized input must degrade, not crash).
8749                Err(synth_core::Error::synthesis(format!(
8750                    "encode_operand2: immediate {uval:#x} ({val}) is not an ARM32 \
8751                     rotated immediate — the selector must materialize large \
8752                     constants via MOVW/MOVT"
8753                )))
8754            }
8755        }
8756
8757        Operand2::Reg(reg) => {
8758            let reg_bits = reg_to_bits(reg);
8759            Ok((reg_bits, 0)) // I=0 for register
8760        }
8761
8762        Operand2::RegShift {
8763            rm,
8764            shift: _,
8765            amount,
8766        } => {
8767            // Simplified encoding with shift
8768            let rm_bits = reg_to_bits(rm);
8769            let shift_bits = (*amount & 0x1F) << 7;
8770            Ok((shift_bits | rm_bits, 0))
8771        }
8772    }
8773}
8774
8775/// Encode memory address to (base_reg, offset)
8776fn encode_mem_addr(addr: &MemAddr) -> (u32, u32) {
8777    let base_bits = reg_to_bits(&addr.base);
8778    let offset_bits = (addr.offset as u32) & 0xFFF; // 12-bit offset
8779    (base_bits, offset_bits)
8780}
8781
8782/// S-register number: S0=0, S1=1, ..., S31=31
8783fn vfp_sreg_to_num(reg: &VfpReg) -> Result<u32> {
8784    match reg {
8785        VfpReg::S0 => Ok(0),
8786        VfpReg::S1 => Ok(1),
8787        VfpReg::S2 => Ok(2),
8788        VfpReg::S3 => Ok(3),
8789        VfpReg::S4 => Ok(4),
8790        VfpReg::S5 => Ok(5),
8791        VfpReg::S6 => Ok(6),
8792        VfpReg::S7 => Ok(7),
8793        VfpReg::S8 => Ok(8),
8794        VfpReg::S9 => Ok(9),
8795        VfpReg::S10 => Ok(10),
8796        VfpReg::S11 => Ok(11),
8797        VfpReg::S12 => Ok(12),
8798        VfpReg::S13 => Ok(13),
8799        VfpReg::S14 => Ok(14),
8800        VfpReg::S15 => Ok(15),
8801        VfpReg::S16 => Ok(16),
8802        VfpReg::S17 => Ok(17),
8803        VfpReg::S18 => Ok(18),
8804        VfpReg::S19 => Ok(19),
8805        VfpReg::S20 => Ok(20),
8806        VfpReg::S21 => Ok(21),
8807        VfpReg::S22 => Ok(22),
8808        VfpReg::S23 => Ok(23),
8809        VfpReg::S24 => Ok(24),
8810        VfpReg::S25 => Ok(25),
8811        VfpReg::S26 => Ok(26),
8812        VfpReg::S27 => Ok(27),
8813        VfpReg::S28 => Ok(28),
8814        VfpReg::S29 => Ok(29),
8815        VfpReg::S30 => Ok(30),
8816        VfpReg::S31 => Ok(31),
8817        // D-registers are not used in F32 single-precision encodings
8818        _ => Err(synth_core::Error::SynthesisError(
8819            "D-register not supported in single-precision VFP encoding".to_string(),
8820        )),
8821    }
8822}
8823
8824/// D-register number: D0=0, D1=1, ..., D15=15
8825fn vfp_dreg_to_num(reg: &VfpReg) -> Result<u32> {
8826    match reg {
8827        VfpReg::D0 => Ok(0),
8828        VfpReg::D1 => Ok(1),
8829        VfpReg::D2 => Ok(2),
8830        VfpReg::D3 => Ok(3),
8831        VfpReg::D4 => Ok(4),
8832        VfpReg::D5 => Ok(5),
8833        VfpReg::D6 => Ok(6),
8834        VfpReg::D7 => Ok(7),
8835        VfpReg::D8 => Ok(8),
8836        VfpReg::D9 => Ok(9),
8837        VfpReg::D10 => Ok(10),
8838        VfpReg::D11 => Ok(11),
8839        VfpReg::D12 => Ok(12),
8840        VfpReg::D13 => Ok(13),
8841        VfpReg::D14 => Ok(14),
8842        VfpReg::D15 => Ok(15),
8843        // S-registers are not used in F64 double-precision encodings
8844        _ => Err(synth_core::Error::SynthesisError(
8845            "S-register not supported in double-precision VFP encoding".to_string(),
8846        )),
8847    }
8848}
8849
8850/// Split S-register into (Vx[3:0], qualifier_bit) for VFP encoding.
8851/// For an S-register number s: Vx = s >> 1, qualifier = s & 1.
8852/// The qualifier bit goes to D (bit 22), N (bit 7), or M (bit 5) depending on role.
8853fn encode_sreg(s: u32) -> (u32, u32) {
8854    (s >> 1, s & 1)
8855}
8856
8857/// Split D-register into (Vx[3:0], qualifier_bit) for VFP double-precision encoding.
8858/// For a D-register number d: Vx = d & 0xF, qualifier = (d >> 4) & 1.
8859/// For D0-D15, qualifier is always 0.
8860fn encode_dreg(d: u32) -> (u32, u32) {
8861    (d & 0xF, (d >> 4) & 1)
8862}
8863
8864/// Encode a VFP 3-register arithmetic instruction (VADD.F32, VSUB.F32, VMUL.F32, VDIV.F32).
8865/// Returns the full 32-bit instruction word.
8866///
8867/// VFP encoding: [cond 1110] [D opc1 Vn] [Vd 101 sz] [N opc2 M 0 Vm]
8868/// For single-precision (sz=0), coprocessor = 0xA (bits[11:8]).
8869fn encode_vfp_3reg(base: u32, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<u32> {
8870    let sd_num = vfp_sreg_to_num(sd)?;
8871    let sn_num = vfp_sreg_to_num(sn)?;
8872    let sm_num = vfp_sreg_to_num(sm)?;
8873    let (vd, d) = encode_sreg(sd_num);
8874    let (vn, n) = encode_sreg(sn_num);
8875    let (vm, m) = encode_sreg(sm_num);
8876
8877    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8878}
8879
8880/// Encode a VFP 2-register instruction (VNEG.F32, VABS.F32, VSQRT.F32).
8881/// Returns the full 32-bit instruction word.
8882fn encode_vfp_2reg(base: u32, sd: &VfpReg, sm: &VfpReg) -> Result<u32> {
8883    let sd_num = vfp_sreg_to_num(sd)?;
8884    let sm_num = vfp_sreg_to_num(sm)?;
8885    let (vd, d) = encode_sreg(sd_num);
8886    let (vm, m) = encode_sreg(sm_num);
8887
8888    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8889}
8890
8891/// Encode a VFP load/store (VLDR.F32 / VSTR.F32).
8892/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8893/// U bit (bit 23) controls add/subtract offset.
8894fn encode_vfp_ldst(base: u32, sd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8895    let sd_num = vfp_sreg_to_num(sd)?;
8896    let (vd, d) = encode_sreg(sd_num);
8897    let rn = reg_to_bits(&addr.base);
8898
8899    let offset = addr.offset;
8900    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8901    let abs_offset = offset.unsigned_abs();
8902    let imm8 = (abs_offset / 4) & 0xFF;
8903
8904    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8905}
8906
8907/// Encode VMOV between core register and S-register.
8908/// VMOV Sn, Rt: 0xEE00_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8909/// VMOV Rt, Sn: 0xEE10_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8910fn encode_vmov_core_sreg(to_sreg: bool, sreg: &VfpReg, core: &Reg) -> Result<u32> {
8911    let s_num = vfp_sreg_to_num(sreg)?;
8912    let (vn, n) = encode_sreg(s_num);
8913    let rt = reg_to_bits(core);
8914
8915    let base = if to_sreg { 0xEE000A10 } else { 0xEE100A10 };
8916    Ok(base | (vn << 16) | (rt << 12) | (n << 7))
8917}
8918
8919/// Encode a VFP 3-register double-precision instruction (VADD.F64, VSUB.F64, etc.).
8920/// For double-precision (sz=1), coprocessor = 0xB (bits[11:8]).
8921/// The base should have bit 8 = 1 for F64 (0xB suffix instead of 0xA).
8922fn encode_vfp_3reg_f64(base: u32, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<u32> {
8923    let dd_num = vfp_dreg_to_num(dd)?;
8924    let dn_num = vfp_dreg_to_num(dn)?;
8925    let dm_num = vfp_dreg_to_num(dm)?;
8926    let (vd, d) = encode_dreg(dd_num);
8927    let (vn, n) = encode_dreg(dn_num);
8928    let (vm, m) = encode_dreg(dm_num);
8929
8930    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8931}
8932
8933/// Encode a VFP 2-register double-precision instruction (VNEG.F64, VABS.F64, VSQRT.F64).
8934fn encode_vfp_2reg_f64(base: u32, dd: &VfpReg, dm: &VfpReg) -> Result<u32> {
8935    let dd_num = vfp_dreg_to_num(dd)?;
8936    let dm_num = vfp_dreg_to_num(dm)?;
8937    let (vd, d) = encode_dreg(dd_num);
8938    let (vm, m) = encode_dreg(dm_num);
8939
8940    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8941}
8942
8943/// Encode a VFP load/store for double-precision (VLDR.64 / VSTR.64).
8944/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8945fn encode_vfp_ldst_f64(base: u32, dd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8946    let dd_num = vfp_dreg_to_num(dd)?;
8947    let (vd, d) = encode_dreg(dd_num);
8948    let rn = reg_to_bits(&addr.base);
8949
8950    let offset = addr.offset;
8951    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8952    let abs_offset = offset.unsigned_abs();
8953    let imm8 = (abs_offset / 4) & 0xFF;
8954
8955    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8956}
8957
8958/// Encode VMOV between two core registers and a D-register.
8959/// VMOV Dm, Rt, Rt2: 0xEC40_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8960/// VMOV Rt, Rt2, Dm: 0xEC50_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8961fn encode_vmov_core_dreg(
8962    to_dreg: bool,
8963    dreg: &VfpReg,
8964    core_lo: &Reg,
8965    core_hi: &Reg,
8966) -> Result<u32> {
8967    let d_num = vfp_dreg_to_num(dreg)?;
8968    let (vm, m) = encode_dreg(d_num);
8969    let rt = reg_to_bits(core_lo);
8970    let rt2 = reg_to_bits(core_hi);
8971
8972    let base = if to_dreg { 0xEC400B10 } else { 0xEC500B10 };
8973    Ok(base | (rt2 << 16) | (rt << 12) | (m << 5) | vm)
8974}
8975
8976/// Emit a VFP 32-bit instruction as Thumb-2 bytes (two LE halfwords).
8977fn vfp_to_thumb_bytes(instr: u32) -> Vec<u8> {
8978    let hw1 = ((instr >> 16) & 0xFFFF) as u16;
8979    let hw2 = (instr & 0xFFFF) as u16;
8980    let mut bytes = hw1.to_le_bytes().to_vec();
8981    bytes.extend_from_slice(&hw2.to_le_bytes());
8982    bytes
8983}
8984
8985// ============================================================================
8986// Helium MVE encoding helpers
8987// ============================================================================
8988
8989/// Q-register number: Q0=0, Q1=1, ..., Q7=7
8990fn qreg_to_num(reg: &QReg) -> u32 {
8991    match reg {
8992        QReg::Q0 => 0,
8993        QReg::Q1 => 1,
8994        QReg::Q2 => 2,
8995        QReg::Q3 => 3,
8996        QReg::Q4 => 4,
8997        QReg::Q5 => 5,
8998        QReg::Q6 => 6,
8999        QReg::Q7 => 7,
9000    }
9001}
9002
9003/// MVE element size to encoding bits: S8=0b00, S16=0b01, S32=0b10
9004fn mve_size_bits(size: &MveSize) -> u32 {
9005    match size {
9006        MveSize::S8 => 0b00,
9007        MveSize::S16 => 0b01,
9008        MveSize::S32 => 0b10,
9009    }
9010}
9011
9012/// Encode MVE 3-register instruction.
9013/// Q-registers are encoded as D-register pairs: Q0=D0:D1, Q1=D2:D3, etc.
9014/// In NEON/MVE encoding, the Q-register uses D-register number = Qn * 2.
9015fn encode_mve_3reg(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
9016    let d = qreg_to_num(qd) * 2;
9017    let n = qreg_to_num(qn) * 2;
9018    let m = qreg_to_num(qm) * 2;
9019
9020    // Standard NEON/MVE 3-register encoding:
9021    // D bit (bit 22) = Vd[4], Vd[3:0] = bits [15:12]
9022    // N bit (bit 7)  = Vn[4], Vn[3:0] = bits [19:16]
9023    // M bit (bit 5)  = Vm[4], Vm[3:0] = bits [3:0]
9024    let vd = d & 0xF;
9025    let d_bit = (d >> 4) & 1;
9026    let vn = n & 0xF;
9027    let n_bit = (n >> 4) & 1;
9028    let vm = m & 0xF;
9029    let m_bit = (m >> 4) & 1;
9030
9031    base | (d_bit << 22) | (vn << 16) | (vd << 12) | (n_bit << 7) | (m_bit << 5) | vm
9032}
9033
9034/// Encode MVE 3-register bitwise instruction (VAND, VORR, VEOR, VBIC).
9035fn encode_mve_3reg_bitwise(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
9036    encode_mve_3reg(base, qd, qn, qm)
9037}
9038
9039/// Encode MVE VLDRW.32 Qd, [Rn, #offset]
9040/// Format: EC9x xxxx - contiguous load, word-sized elements
9041fn encode_mve_vldrw(qd: &QReg, addr: &MemAddr) -> u32 {
9042    let qd_enc = qreg_to_num(qd) * 2;
9043    let rn = reg_to_bits(&addr.base);
9044    let offset = addr.offset;
9045    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9046    let abs_offset = offset.unsigned_abs();
9047    let imm7 = (abs_offset / 4) & 0x7F; // 7-bit word-aligned offset
9048
9049    // VLDRW.32 Qd, [Rn, #imm]: ED10 xx80 variant
9050    0xED100E80
9051        | (u_bit << 23)
9052        | ((qd_enc >> 4) << 22)
9053        | (rn << 16)
9054        | ((qd_enc & 0xF) << 12)
9055        | (imm7 & 0x7F)
9056}
9057
9058/// Encode MVE VSTRW.32 Qd, [Rn, #offset]
9059fn encode_mve_vstrw(qd: &QReg, addr: &MemAddr) -> u32 {
9060    let qd_enc = qreg_to_num(qd) * 2;
9061    let rn = reg_to_bits(&addr.base);
9062    let offset = addr.offset;
9063    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9064    let abs_offset = offset.unsigned_abs();
9065    let imm7 = (abs_offset / 4) & 0x7F;
9066
9067    0xED000E80
9068        | (u_bit << 23)
9069        | ((qd_enc >> 4) << 22)
9070        | (rn << 16)
9071        | ((qd_enc & 0xF) << 12)
9072        | (imm7 & 0x7F)
9073}
9074
9075impl ArmEncoder {
9076    /// Encode MVE constant load: MOVW+MOVT+VMOV for each 32-bit word, then assemble Q-register
9077    fn encode_thumb_mve_const(&self, qd: &QReg, bytes: &[u8; 16]) -> Result<Vec<u8>> {
9078        let mut result = Vec::new();
9079        let qd_num = qreg_to_num(qd);
9080
9081        // Load each 32-bit word into R12 (temp) then VMOV into S-register
9082        for i in 0..4 {
9083            let word = u32::from_le_bytes([
9084                bytes[i * 4],
9085                bytes[i * 4 + 1],
9086                bytes[i * 4 + 2],
9087                bytes[i * 4 + 3],
9088            ]);
9089            let lo16 = word & 0xFFFF;
9090            let hi16 = (word >> 16) & 0xFFFF;
9091
9092            // MOVW R12, #lo16
9093            result.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
9094            // MOVT R12, #hi16
9095            if hi16 != 0 {
9096                result.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
9097            }
9098
9099            // VMOV Sn, R12 where Sn = Qd*4 + i
9100            let s_num = qd_num * 4 + i as u32;
9101            let (vn, n) = encode_sreg(s_num);
9102            let vmov: u32 = 0xEE000A10 | (vn << 16) | (12 << 12) | (n << 7);
9103            result.extend_from_slice(&vfp_to_thumb_bytes(vmov));
9104        }
9105
9106        Ok(result)
9107    }
9108
9109    /// Encode lane-wise f32 binary operation (VDIV, etc.) via S-register extraction
9110    fn encode_thumb_mve_lane_wise_f32_binop(
9111        &self,
9112        qd: &QReg,
9113        qn: &QReg,
9114        qm: &QReg,
9115        vfp_base: u32,
9116    ) -> Result<Vec<u8>> {
9117        let mut result = Vec::new();
9118        let qd_num = qreg_to_num(qd);
9119        let qn_num = qreg_to_num(qn);
9120        let qm_num = qreg_to_num(qm);
9121
9122        // For each lane 0..3: use S-registers directly (Q aliasing)
9123        for i in 0..4u32 {
9124            let sd = qd_num * 4 + i;
9125            let sn = qn_num * 4 + i;
9126            let sm = qm_num * 4 + i;
9127
9128            let (vd, d) = encode_sreg(sd);
9129            let (vn, n) = encode_sreg(sn);
9130            let (vm, m) = encode_sreg(sm);
9131
9132            let instr = vfp_base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
9133            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9134        }
9135
9136        Ok(result)
9137    }
9138
9139    /// Encode lane-wise f32 VSQRT via S-register extraction
9140    fn encode_thumb_mve_lane_wise_f32_sqrt(&self, qd: &QReg, qm: &QReg) -> Result<Vec<u8>> {
9141        let mut result = Vec::new();
9142        let qd_num = qreg_to_num(qd);
9143        let qm_num = qreg_to_num(qm);
9144
9145        // VSQRT.F32 base: 0xEEB10AC0
9146        for i in 0..4u32 {
9147            let sd = qd_num * 4 + i;
9148            let sm = qm_num * 4 + i;
9149
9150            let (vd, d) = encode_sreg(sd);
9151            let (vm, m) = encode_sreg(sm);
9152
9153            let instr: u32 = 0xEEB10AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
9154            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9155        }
9156
9157        Ok(result)
9158    }
9159}
9160
9161/// VCR-TIER-001 (#1021/#1048) — the SCRATCH CONTRACT of an `ArmOp`'s encoder
9162/// expansion: the registers, beyond the op's declared RESULT registers and the
9163/// globally sanctioned R12/IP encoder scratch, that the expansion may leave
9164/// modified when it completes. Transient-but-restored traffic (push/pop through
9165/// the expansion's own stack red-zone, SP restored on exit) is not "modified".
9166///
9167/// This is the SINGLE declaration site for that contract — the one place a
9168/// future expansion that must borrow a register says so (the repo rule: one
9169/// declaration site, no duplicate copy that can silently drift). It is deliberately NOT derived
9170/// from the expansion's observed behavior: a contract read off the bytes would
9171/// rubber-stamp any clobber. Intent is declared here; the canary gate
9172/// (`scripts/repro/expansion_canary_gate_1021.py`) executes the REAL emitted
9173/// bytes of every variant the shipped rule table emits, on both backends, with
9174/// every non-contract register holding a distinctive canary, and fails on any
9175/// undeclared write — and on any declared register the expansion never
9176/// actually writes, so an over-broad declaration cannot hollow the gate.
9177///
9178/// The default is the STRICTEST reading — result registers only — which is
9179/// exactly the silent claim the atomic `ArmSemantics` pseudo-op model already
9180/// makes (#1021: an atomic model of a multi-instruction expansion is a silent
9181/// claim that the expansion is scratch-free). Today the table is EMPTY: #1039
9182/// reworked `i32.popcnt` off R11 (the linear-memory base) and #1048 reworked
9183/// the i64 shifts and bit-counts off their own operand registers, so every
9184/// expansion of every rule-emitted variant is R12-only on both Thumb-2 and
9185/// A32. Backend-independent for the same reason; if a backend's expansion ever
9186/// diverges, this signature grows a backend parameter in the same PR.
9187pub fn expansion_scratch_contract(op: &ArmOp) -> &'static [Reg] {
9188    // No variant currently borrows any register beyond R12. A new declaration
9189    // is added as a `match op { .. }` arm here — nowhere else.
9190    let _ = op;
9191    &[]
9192}
9193
9194#[cfg(test)]
9195mod tests {
9196    use super::*;
9197
9198    #[test]
9199    fn test_encoder_creation() {
9200        let encoder_arm = ArmEncoder::new_arm32();
9201        assert!(!encoder_arm.thumb_mode);
9202
9203        let encoder_thumb = ArmEncoder::new_thumb2();
9204        assert!(encoder_thumb.thumb_mode);
9205    }
9206
9207    /// #204 WAKE-path regression: `SetCond` materialized 0/1 with the 16-bit
9208    /// `MOVS Rd,#imm` (T1), whose Rd field is 3 bits (R0–R7). For a high Rd
9209    /// (R8–R12) `rd_bits << 8` overflows bit 11, flipping the opcode MOVS→CMP
9210    /// (`0x2c00`), so the boolean was never written — gale's `has_waiter` kept a
9211    /// stale value and the binary-sem WAKE dispatch read garbage. High Rd must
9212    /// use the 32-bit `MOV.W` (T2). Verify the bytes, not the IR.
9213    /// #311: the SAME high-Rd MOVS→CMP transmutation as #204, but in the
9214    /// i64 comparison expansions (I64SetCond / I64SetCondZ) — missed by the
9215    /// #204 hardening. With rd=R8 the boolean died in the flags
9216    /// (`ite eq; cmpeq r0,#1; cmpne r0,#0`), so gale's packed-u64 select
9217    /// read a stale register on silicon. High Rd must take MOV.W / CMP.W.
9218    #[test]
9219    fn test_encode_i64setcond_high_reg_uses_mov_w_311() {
9220        use synth_synthesis::{ArmOp, Condition, Reg};
9221        let enc = ArmEncoder::new_thumb2();
9222        let bytes = enc
9223            .encode(&ArmOp::I64SetCond {
9224                rd: Reg::R8,
9225                rn_lo: Reg::R2,
9226                rn_hi: Reg::R3,
9227                rm_lo: Reg::R6,
9228                rm_hi: Reg::R7,
9229                cond: Condition::EQ,
9230            })
9231            .unwrap();
9232        // The 32-bit MOV.W immediate (T2) first halfword is 0xF04F; the
9233        // 16-bit transmuted forms would contain 0x2801/0x2800 (CMP r0,#1/#0).
9234        let halfwords: Vec<u16> = bytes
9235            .chunks(2)
9236            .map(|c| u16::from_le_bytes([c[0], c[1]]))
9237            .collect();
9238        assert!(
9239            halfwords.iter().filter(|&&h| h == 0xF04F).count() == 2,
9240            "high rd must use two MOV.W (T2) encodings, got {halfwords:04x?}"
9241        );
9242        assert!(
9243            !halfwords.contains(&0x2801) && !halfwords.contains(&0x2800),
9244            "no transmuted 16-bit CMP imm: {halfwords:04x?}"
9245        );
9246
9247        let bytes_z = enc
9248            .encode(&ArmOp::I64SetCondZ {
9249                rd: Reg::R8,
9250                rn_lo: Reg::R2,
9251                rn_hi: Reg::R3,
9252            })
9253            .unwrap();
9254        let hw_z: Vec<u16> = bytes_z
9255            .chunks(2)
9256            .map(|c| u16::from_le_bytes([c[0], c[1]]))
9257            .collect();
9258        assert!(
9259            hw_z.iter().filter(|&&h| h == 0xF04F).count() == 2,
9260            "SetCondZ high rd MOV.W: {hw_z:04x?}"
9261        );
9262        // CMP.W rd,#0 (T2) first halfword: 0xF1B0 | rd
9263        assert!(
9264            hw_z.contains(&(0xF1B0 | 8)),
9265            "SetCondZ high rd must use CMP.W: {hw_z:04x?}"
9266        );
9267    }
9268
9269    #[test]
9270    fn test_encode_setcond_high_reg_uses_mov_w_204() {
9271        use synth_synthesis::{ArmOp, Condition, Reg};
9272        let enc = ArmEncoder::new_thumb2();
9273        // R12 (high): must be ITE + MOV.W #1 + MOV.W #0, never a 16-bit MOVS/CMP.
9274        let hi = enc
9275            .encode(&ArmOp::SetCond {
9276                rd: Reg::R12,
9277                cond: Condition::NE,
9278            })
9279            .unwrap();
9280        assert_eq!(hi.len(), 10, "ITE(2) + MOV.W(4) + MOV.W(4): {hi:02x?}");
9281        // both value halfwords are MOV.W (0xF04F) — NOT the corrupt CMP (0x2c..).
9282        assert_eq!(&hi[2..4], &[0x4F, 0xF0], "then = MOV.W: {hi:02x?}");
9283        assert_eq!(&hi[6..8], &[0x4F, 0xF0], "else = MOV.W: {hi:02x?}");
9284        assert_eq!(hi[4] & 0x0F, 0x01, "then imm = #1");
9285        assert_eq!(hi[8] & 0x0F, 0x00, "else imm = #0");
9286        // Low Rd keeps the compact 16-bit MOVS form.
9287        let lo = enc
9288            .encode(&ArmOp::SetCond {
9289                rd: Reg::R0,
9290                cond: Condition::NE,
9291            })
9292            .unwrap();
9293        assert_eq!(lo.len(), 6, "ITE(2) + MOVS(2) + MOVS(2): {lo:02x?}");
9294        assert_eq!(lo[2..4], [0x01, 0x20], "then = MOVS R0,#1");
9295        assert_eq!(lo[4..6], [0x00, 0x20], "else = MOVS R0,#0");
9296    }
9297
9298    /// #209 Opt 1b: UMULL RdLo, RdHi, Rn, Rm encodes correctly on both ISAs.
9299    /// Thumb-2 T1: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm.
9300    /// A32:        cond 0000 1000 RdHi RdLo Rm 1001 Rn.
9301    #[test]
9302    fn test_encode_umull_209b() {
9303        use synth_synthesis::{ArmOp, Reg};
9304        let op = ArmOp::Umull {
9305            rdlo: Reg::R4,
9306            rdhi: Reg::R5,
9307            rn: Reg::R0,
9308            rm: Reg::R3,
9309        };
9310        // Thumb-2: hw1 = 0xFBA0 | 0 = 0xFBA0; hw2 = (4<<12)|(5<<8)|3 = 0x4503.
9311        let t = ArmEncoder::new_thumb2().encode(&op).unwrap();
9312        assert_eq!(
9313            t,
9314            vec![0xA0, 0xFB, 0x03, 0x45],
9315            "umull r4,r5,r0,r3 (T2): {t:02x?}"
9316        );
9317        // A32: 0xE0800090 | (5<<16) | (4<<12) | (3<<8) | 0 = 0xE0854390.
9318        let a = ArmEncoder::new_arm32().encode(&op).unwrap();
9319        assert_eq!(
9320            a,
9321            0xE085_4390u32.to_le_bytes().to_vec(),
9322            "umull (A32): {a:02x?}"
9323        );
9324    }
9325
9326    /// #206 regression: the ARM32 (A32) `Ldr`/`Str` encoders fed `addr` through
9327    /// `encode_mem_addr`, which returns only the 12-bit immediate — so a register
9328    /// offset (`[rn, rm, #off]`) was silently dropped to `[rn, #off]`, sending
9329    /// the access to the wrong runtime address (silent miscompile on the default
9330    /// `--target arm`). A register offset must materialize `ip = rn + rm` and
9331    /// load from `[ip, #off]`. Verify the bytes.
9332    #[test]
9333    fn test_encode_arm32_indexed_load_keeps_index_206() {
9334        use synth_synthesis::{ArmOp, MemAddr, Reg};
9335        let enc = ArmEncoder::new_arm32();
9336        // ldr r0, [r11, r1, #8]  must NOT collapse to a single immediate ldr.
9337        let bytes = enc
9338            .encode(&ArmOp::Ldr {
9339                rd: Reg::R0,
9340                addr: MemAddr::reg_imm(Reg::R11, Reg::R1, 8),
9341            })
9342            .unwrap();
9343        assert_eq!(
9344            bytes.len(),
9345            8,
9346            "expected ADD ip + LDR (2 words): {bytes:02x?}"
9347        );
9348        let add = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9349        let ldr = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9350        // ADD ip, r11, r1  = 0xE08BC001
9351        assert_eq!(add, 0xE08B_C001, "ADD ip,r11,r1: {add:#010x}");
9352        // LDR r0, [ip, #8] = 0xE59C0008
9353        assert_eq!(ldr, 0xE59C_0008, "LDR r0,[ip,#8]: {ldr:#010x}");
9354        // A bare immediate ldr (the bug) would be 0xE59B0008 (base=r11) — reject.
9355        assert_ne!(ldr, 0xE59B_0008, "index must not be dropped");
9356    }
9357
9358    /// #594 regression: `call_indirect` on the A32 path (`--target cortex-r5`)
9359    /// was encoded as a literal NOP (0xE1A00000) — the call never happened and
9360    /// the function silently returned the leftover table-index value. The A32
9361    /// encoder must emit a real dispatch expansion, since #642 guarded by an
9362    /// inline bounds check:
9363    /// `MOVW r12, #size; CMP idx, r12; BLO +1; UDF;
9364    ///  MOV r12, idx, LSL #2; LDR r12, [r11, r12]; BLX r12`.
9365    #[test]
9366    fn test_encode_arm32_call_indirect_is_real_call_594() {
9367        use synth_synthesis::{ArmOp, Reg};
9368        let enc = ArmEncoder::new_arm32();
9369        let bytes = enc
9370            .encode(&ArmOp::CallIndirect {
9371                rd: Reg::R0,
9372                type_idx: 0,
9373                table_index_reg: Reg::R0,
9374                table_size: 4,
9375                table_byte_offset: 0,
9376                null_check: false,
9377                type_check: None,
9378            })
9379            .unwrap();
9380        assert_eq!(
9381            bytes.len(),
9382            28,
9383            "expected MOVW + CMP + BLO + UDF + MOV + LDR + BLX (7 words): {bytes:02x?}"
9384        );
9385        let words: Vec<u32> = bytes
9386            .as_chunks::<4>()
9387            .0
9388            .iter()
9389            .map(|&w| u32::from_le_bytes(w))
9390            .collect();
9391        // #642 bounds guard: MOVW r12, #4; CMP r0, r12; BLO +1; UDF
9392        assert_eq!(words[0], 0xE300_C004, "MOVW r12,#4: {:#010x}", words[0]);
9393        assert_eq!(words[1], 0xE150_000C, "CMP r0,r12: {:#010x}", words[1]);
9394        assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9395        assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9396        // MOV r12, r0, LSL #2 = 0xE1A0C100
9397        assert_eq!(
9398            words[4], 0xE1A0_C100,
9399            "MOV r12,r0,LSL#2: {:#010x}",
9400            words[4]
9401        );
9402        // LDR r12, [r11, r12] = 0xE79BC00C
9403        assert_eq!(
9404            words[5], 0xE79B_C00C,
9405            "LDR r12,[r11,r12]: {:#010x}",
9406            words[5]
9407        );
9408        // BLX r12 = 0xE12FFF3C
9409        assert_eq!(words[6], 0xE12F_FF3C, "BLX r12: {:#010x}", words[6]);
9410        // The bug: a single NOP word. Must never come back.
9411        assert!(
9412            !bytes
9413                .as_chunks::<4>()
9414                .0
9415                .iter()
9416                .any(|&w| w == 0xE1A0_0000u32.to_le_bytes()),
9417            "call_indirect must not contain a NOP (#594): {bytes:02x?}"
9418        );
9419
9420        // A non-R0 index register lands in the MOV's Rm and CMP's Rn fields.
9421        let bytes = enc
9422            .encode(&ArmOp::CallIndirect {
9423                rd: Reg::R0,
9424                type_idx: 0,
9425                table_index_reg: Reg::R4,
9426                table_size: 4,
9427                table_byte_offset: 0,
9428                null_check: false,
9429                type_check: None,
9430            })
9431            .unwrap();
9432        let cmp = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9433        assert_eq!(cmp, 0xE154_000C, "CMP r4,r12: {cmp:#010x}");
9434        let mov = u32::from_le_bytes(bytes[16..20].try_into().unwrap());
9435        assert_eq!(mov, 0xE1A0_C104, "MOV r12,r4,LSL#2: {mov:#010x}");
9436    }
9437
9438    /// #642: a table size above 16 bits must not be silently truncated by the
9439    /// MOVW — the A32 guard adds a MOVT for the high half.
9440    #[test]
9441    fn test_encode_arm32_call_indirect_wide_table_size_642() {
9442        use synth_synthesis::{ArmOp, Reg};
9443        let enc = ArmEncoder::new_arm32();
9444        let bytes = enc
9445            .encode(&ArmOp::CallIndirect {
9446                rd: Reg::R0,
9447                type_idx: 0,
9448                table_index_reg: Reg::R0,
9449                table_size: 0x0002_0003,
9450                table_byte_offset: 0,
9451                null_check: false,
9452                type_check: None,
9453            })
9454            .unwrap();
9455        assert_eq!(bytes.len(), 32, "MOVT arm adds one word: {bytes:02x?}");
9456        let movw = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9457        let movt = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9458        assert_eq!(movw, 0xE300_C003, "MOVW r12,#3: {movw:#010x}");
9459        assert_eq!(movt, 0xE340_C002, "MOVT r12,#2: {movt:#010x}");
9460    }
9461
9462    /// #597 anchor (justified correctness RE-PIN of the #594-era freeze): the
9463    /// Thumb-2 `CallIndirect` expansion is `mov.w ip, rm, LSL #2; ldr.w ip,
9464    /// [r11, ip]; blx ip`.
9465    ///
9466    /// The #594 PR froze the then-current bytes `4F EA 20 0C ...` whose first
9467    /// word decodes as `mov.w ip, rm, ASR #32` — the intended `LSL #2` had
9468    /// its shift amount in the TYPE field (bits 5:4) instead of imm2 (bits
9469    /// 7:6), so the index was destroyed and every call_indirect dispatched
9470    /// table entry 0 (shipped miscompile, masked by index-0 probes). #597
9471    /// corrects the encoding; new bytes `4F EA 80 0C ...` were
9472    /// execution-validated under unicorn against the wasmtime oracle on a
9473    /// multi-entry table (indexes 0, 1, 3 —
9474    /// scripts/repro/call_indirect_597_differential.py) before this pin was
9475    /// replaced. Old pin: [4F EA 20 0C, 5B F8 0C C0, E0 47] (ASR #32 — must
9476    /// never come back).
9477    #[test]
9478    fn test_encode_thumb_call_indirect_lsl2_597() {
9479        use synth_synthesis::{ArmOp, Reg};
9480        let enc = ArmEncoder::new_thumb2();
9481        let bytes = enc
9482            .encode(&ArmOp::CallIndirect {
9483                rd: Reg::R0,
9484                type_idx: 0,
9485                table_index_reg: Reg::R0,
9486                table_size: 4,
9487                table_byte_offset: 0,
9488                null_check: false,
9489                type_check: None,
9490            })
9491            .unwrap();
9492        assert_eq!(
9493            bytes,
9494            vec![
9495                // #642 bounds guard: movw ip,#4; cmp r0,ip; blo +1; udf #0
9496                0x40, 0xF2, 0x04, 0x0C, // movw ip, #4
9497                0x60, 0x45, // cmp r0, ip
9498                0x00, 0xD3, // blo .+4 (skip the udf)
9499                0x00, 0xDE, // udf #0 — OOB index trap (WASM §4.4.8)
9500                // #597-pinned dispatch
9501                0x4F, 0xEA, 0x80, 0x0C, // mov.w ip, r0, lsl #2
9502                0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9503                0xE0, 0x47, // blx ip
9504            ],
9505            "Thumb-2 CallIndirect: bounds guard + mov.w/ldr.w/blx dispatch: {bytes:02x?}"
9506        );
9507        // The #597 bug bytes (ASR #32 dispatch first word) must never come back.
9508        assert!(
9509            !bytes.windows(4).any(|w| w == [0x4F, 0xEA, 0x20, 0x0C]),
9510            "mov.w ip, rm, ASR #32 — the #597 type-field bug"
9511        );
9512
9513        // A non-R0 index register lands in the mov.w's Rm field (hw2 bits 3:0)
9514        // and the cmp's Rn field.
9515        let bytes = enc
9516            .encode(&ArmOp::CallIndirect {
9517                rd: Reg::R0,
9518                type_idx: 0,
9519                table_index_reg: Reg::R4,
9520                table_size: 4,
9521                table_byte_offset: 0,
9522                null_check: false,
9523                type_check: None,
9524            })
9525            .unwrap();
9526        assert_eq!(&bytes[4..6], &[0x64, 0x45], "cmp r4, ip: {bytes:02x?}");
9527        assert_eq!(
9528            &bytes[10..14],
9529            &[0x4F, 0xEA, 0x84, 0x0C],
9530            "mov.w ip, r4, LSL #2: {bytes:02x?}"
9531        );
9532    }
9533
9534    /// #642: the Thumb-2 bounds guard for a high-register index (R8 — the top
9535    /// of the allocatable pool) uses the high-reg-capable 16-bit CMP (T2) with
9536    /// the N bit set; a table size above 16 bits adds a MOVT.
9537    #[test]
9538    fn test_encode_thumb_call_indirect_guard_shapes_642() {
9539        use synth_synthesis::{ArmOp, Reg};
9540        let enc = ArmEncoder::new_thumb2();
9541        let bytes = enc
9542            .encode(&ArmOp::CallIndirect {
9543                rd: Reg::R0,
9544                type_idx: 0,
9545                table_index_reg: Reg::R8,
9546                table_size: 3,
9547                table_byte_offset: 0,
9548                null_check: false,
9549                type_check: None,
9550            })
9551            .unwrap();
9552        // cmp r8, ip — T2: 0x4500 | N(1)<<7 | Rm(12)<<3 | Rn(0) = 0x45E0
9553        assert_eq!(&bytes[4..6], &[0xE0, 0x45], "cmp r8, ip: {bytes:02x?}");
9554
9555        let bytes = enc
9556            .encode(&ArmOp::CallIndirect {
9557                rd: Reg::R0,
9558                type_idx: 0,
9559                table_index_reg: Reg::R0,
9560                table_size: 0x0002_0003,
9561                table_byte_offset: 0,
9562                null_check: false,
9563                type_check: None,
9564            })
9565            .unwrap();
9566        // movw ip,#3 then movt ip,#2 — the size must not be truncated.
9567        assert_eq!(
9568            &bytes[0..8],
9569            &[0x40, 0xF2, 0x03, 0x0C, 0xC0, 0xF2, 0x02, 0x0C],
9570            "movw ip,#3; movt ip,#2: {bytes:02x?}"
9571        );
9572    }
9573
9574    /// #650: a non-zero table base offset (table N of the contiguous R11
9575    /// region) routes the Thumb-2 pointer load through
9576    /// `add.w ip, r11, ip; ldr.w ip, [ip, #offset]` — and offset 0 keeps the
9577    /// pre-#650 single-load bytes IDENTICAL (the by-construction pin).
9578    #[test]
9579    fn test_encode_thumb_call_indirect_table_offset_650() {
9580        use synth_synthesis::{ArmOp, Reg};
9581        let enc = ArmEncoder::new_thumb2();
9582        // falcon's fused-component shape: table 0 has 7 entries, so table 1
9583        // sits at byte offset 28.
9584        let bytes = enc
9585            .encode(&ArmOp::CallIndirect {
9586                rd: Reg::R0,
9587                type_idx: 0,
9588                table_index_reg: Reg::R1,
9589                table_size: 41,
9590                table_byte_offset: 28,
9591                null_check: false,
9592                type_check: None,
9593            })
9594            .unwrap();
9595        assert_eq!(
9596            bytes,
9597            vec![
9598                // #642 bounds guard against TABLE 1's OWN size (41)
9599                0x40, 0xF2, 0x29, 0x0C, // movw ip, #41
9600                0x61, 0x45, // cmp r1, ip
9601                0x00, 0xD3, // blo .+4 (skip the udf)
9602                0x00, 0xDE, // udf #0 — OOB trap (WASM §4.4.8)
9603                // dispatch through table 1's base (R11 + 28)
9604                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9605                0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9606                0xDC, 0xF8, 0x1C, 0xC0, // ldr.w ip, [ip, #28]
9607                0xE0, 0x47, // blx ip
9608            ],
9609            "Thumb-2 table-1 dispatch (#650): {bytes:02x?}"
9610        );
9611
9612        // Offset 0 must stay the #597-pinned single-load form (no add.w, no
9613        // imm-form ldr) — single-table byte identity by construction.
9614        let zero = enc
9615            .encode(&ArmOp::CallIndirect {
9616                rd: Reg::R0,
9617                type_idx: 0,
9618                table_index_reg: Reg::R1,
9619                table_size: 41,
9620                table_byte_offset: 0,
9621                null_check: false,
9622                type_check: None,
9623            })
9624            .unwrap();
9625        assert_eq!(
9626            &zero[10..],
9627            &[
9628                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9629                0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9630                0xE0, 0x47, // blx ip
9631            ],
9632            "offset 0 keeps the pre-#650 dispatch bytes: {zero:02x?}"
9633        );
9634    }
9635
9636    /// #650: the A32 twin — `add r12, r11, r12; ldr r12, [r12, #offset]` for
9637    /// a non-zero table base offset; offset 0 keeps the #594/#642 form.
9638    #[test]
9639    fn test_encode_arm32_call_indirect_table_offset_650() {
9640        use synth_synthesis::{ArmOp, Reg};
9641        let enc = ArmEncoder::new_arm32();
9642        let bytes = enc
9643            .encode(&ArmOp::CallIndirect {
9644                rd: Reg::R0,
9645                type_idx: 0,
9646                table_index_reg: Reg::R1,
9647                table_size: 41,
9648                table_byte_offset: 28,
9649                null_check: false,
9650                type_check: None,
9651            })
9652            .unwrap();
9653        let words: Vec<u32> = bytes
9654            .as_chunks::<4>()
9655            .0
9656            .iter()
9657            .map(|&w| u32::from_le_bytes(w))
9658            .collect();
9659        assert_eq!(words[0], 0xE300_C029, "MOVW r12,#41: {:#010x}", words[0]);
9660        assert_eq!(words[1], 0xE151_000C, "CMP r1,r12: {:#010x}", words[1]);
9661        assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9662        assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9663        assert_eq!(
9664            words[4], 0xE1A0_C101,
9665            "MOV r12,r1,LSL#2: {:#010x}",
9666            words[4]
9667        );
9668        assert_eq!(
9669            words[5], 0xE08B_C00C,
9670            "ADD r12,r11,r12 (#650): {:#010x}",
9671            words[5]
9672        );
9673        assert_eq!(
9674            words[6], 0xE59C_C01C,
9675            "LDR r12,[r12,#28] (#650): {:#010x}",
9676            words[6]
9677        );
9678        assert_eq!(words[7], 0xE12F_FF3C, "BLX r12: {:#010x}", words[7]);
9679    }
9680
9681    /// #664: `null_check` inserts a null-funcref trap between the Thumb-2
9682    /// pointer load and the `BLX` (`cmp.w ip, #0; bne .+4; udf #0`) — a
9683    /// zero-linked (uninitialized) slot must TRAP (WASM §4.4.8), never
9684    /// branch to address 0. `null_check: false` keeps the expansion
9685    /// byte-identical to the pre-#664 form (by-construction pin).
9686    #[test]
9687    fn test_encode_thumb_call_indirect_null_check_664() {
9688        use synth_synthesis::{ArmOp, Reg};
9689        let enc = ArmEncoder::new_thumb2();
9690        let op = |null_check| ArmOp::CallIndirect {
9691            rd: Reg::R0,
9692            type_idx: 0,
9693            table_index_reg: Reg::R1,
9694            table_size: 4,
9695            table_byte_offset: 0,
9696            null_check,
9697            type_check: None,
9698        };
9699        let with = enc.encode(&op(true)).unwrap();
9700        let without = enc.encode(&op(false)).unwrap();
9701        // The checked form = the unchecked form with EXACTLY the three-insn
9702        // null check spliced in before the final BLX (byte identity of the
9703        // shared prefix/suffix — nothing else may move).
9704        assert_eq!(
9705            with.len(),
9706            without.len() + 8,
9707            "cmp.w (4) + bne (2) + udf (2): {with:02x?}"
9708        );
9709        let blx_at = without.len() - 2;
9710        assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9711        assert_eq!(
9712            &with[blx_at..],
9713            &[
9714                0xBC, 0xF1, 0x00, 0x0F, // cmp.w ip, #0
9715                0x00, 0xD1, // bne .+4 (skip the udf)
9716                0x00, 0xDE, // udf #0 — null-funcref trap (#664)
9717                0xE0, 0x47, // blx ip
9718            ],
9719            "null check precedes the BLX: {with:02x?}"
9720        );
9721        assert_eq!(&with[with.len() - 2..], &without[blx_at..], "same BLX");
9722    }
9723
9724    /// #664: the A32 twin — `cmp r12, #0; bne .+8; udf` before the `BLX`;
9725    /// `null_check: false` keeps the #594/#642/#650 bytes identical.
9726    #[test]
9727    fn test_encode_arm32_call_indirect_null_check_664() {
9728        use synth_synthesis::{ArmOp, Reg};
9729        let enc = ArmEncoder::new_arm32();
9730        let op = |null_check| ArmOp::CallIndirect {
9731            rd: Reg::R0,
9732            type_idx: 0,
9733            table_index_reg: Reg::R1,
9734            table_size: 4,
9735            table_byte_offset: 0,
9736            null_check,
9737            type_check: None,
9738        };
9739        let with = enc.encode(&op(true)).unwrap();
9740        let without = enc.encode(&op(false)).unwrap();
9741        assert_eq!(with.len(), without.len() + 12, "3 A32 words: {with:02x?}");
9742        let blx_at = without.len() - 4;
9743        assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9744        let words: Vec<u32> = with[blx_at..]
9745            .as_chunks::<4>()
9746            .0
9747            .iter()
9748            .map(|&w| u32::from_le_bytes(w))
9749            .collect();
9750        assert_eq!(words[0], 0xE35C_0000, "CMP r12,#0: {:#010x}", words[0]);
9751        assert_eq!(words[1], 0x1A00_0000, "BNE +1 insn: {:#010x}", words[1]);
9752        assert_eq!(words[2], 0xE7F0_00F0, "UDF (null trap): {:#010x}", words[2]);
9753        assert_eq!(words[3], 0xE12F_FF3C, "BLX r12: {:#010x}", words[3]);
9754    }
9755
9756    /// #676: `type_check` splices the runtime type check — scale the index,
9757    /// load the slot's structural class id from the type-id sidecar
9758    /// (`ldr.w ip, [ip, #type_off]`), compare against the expected class id
9759    /// and trap on mismatch (WASM §4.4.8) — between the bounds guard and
9760    /// the dispatch tail. `type_check: None` keeps the expansion
9761    /// byte-identical to the pre-#676 form (by-construction pin, the same
9762    /// trick as #650 offset-0 / #664 `null_check: false`).
9763    #[test]
9764    fn test_encode_thumb_call_indirect_type_check_676() {
9765        use synth_synthesis::{ArmOp, Reg};
9766        let enc = ArmEncoder::new_thumb2();
9767        let op = |type_check| ArmOp::CallIndirect {
9768            rd: Reg::R0,
9769            type_idx: 1,
9770            table_index_reg: Reg::R1,
9771            table_size: 5,
9772            table_byte_offset: 0,
9773            null_check: false,
9774            type_check,
9775        };
9776        let with = enc.encode(&op(Some((2, 20)))).unwrap();
9777        let without = enc.encode(&op(None)).unwrap();
9778        // The checked form = the unchecked form with EXACTLY the six-insn
9779        // type check spliced in after the bounds guard (byte identity of
9780        // the shared prefix/suffix — nothing else may move).
9781        assert_eq!(
9782            with.len(),
9783            without.len() + 20,
9784            "lsl.w(4)+add.w(4)+ldr.w(4)+cmp.w(4)+beq(2)+udf(2): {with:02x?}"
9785        );
9786        // Bounds guard: movw(4) + cmp(2) + blo(2) + udf(2) = 10 bytes.
9787        let guard_end = 10;
9788        assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
9789        assert_eq!(
9790            &with[guard_end..guard_end + 20],
9791            &[
9792                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9793                0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9794                0xDC, 0xF8, 0x14, 0xC0, // ldr.w ip, [ip, #20] — sidecar slot id
9795                0xBC, 0xF1, 0x02, 0x0F, // cmp.w ip, #2 — expected class id
9796                0x00, 0xD0, // beq .+4 (skip the udf on a match)
9797                0x00, 0xDE, // udf #0 — §4.4.8 type-mismatch trap (#676)
9798            ],
9799            "type check follows the bounds guard: {with:02x?}"
9800        );
9801        assert_eq!(
9802            &with[guard_end + 20..],
9803            &without[guard_end..],
9804            "dispatch tail unchanged (idx*4 recomputed)"
9805        );
9806    }
9807
9808    /// #676: the A32 twin — `mov r12, idx, lsl #2; add r12, r11, r12;
9809    /// ldr r12, [r12, #type_off]; cmp r12, #id; beq .+8; udf` after the
9810    /// bounds guard; `type_check: None` keeps the #594/#642/#650/#664
9811    /// bytes identical.
9812    #[test]
9813    fn test_encode_arm32_call_indirect_type_check_676() {
9814        use synth_synthesis::{ArmOp, Reg};
9815        let enc = ArmEncoder::new_arm32();
9816        let op = |type_check| ArmOp::CallIndirect {
9817            rd: Reg::R0,
9818            type_idx: 1,
9819            table_index_reg: Reg::R1,
9820            table_size: 5,
9821            table_byte_offset: 0,
9822            null_check: false,
9823            type_check,
9824        };
9825        let with = enc.encode(&op(Some((2, 20)))).unwrap();
9826        let without = enc.encode(&op(None)).unwrap();
9827        assert_eq!(with.len(), without.len() + 24, "6 A32 words: {with:02x?}");
9828        // Bounds guard: movw + cmp + blo + udf = 4 words = 16 bytes.
9829        let guard_end = 16;
9830        assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
9831        let words: Vec<u32> = with[guard_end..guard_end + 24]
9832            .as_chunks::<4>()
9833            .0
9834            .iter()
9835            .map(|&w| u32::from_le_bytes(w))
9836            .collect();
9837        assert_eq!(
9838            words[0], 0xE1A0_C101,
9839            "MOV r12,r1,LSL#2: {:#010x}",
9840            words[0]
9841        );
9842        assert_eq!(words[1], 0xE08B_C00C, "ADD r12,r11,r12: {:#010x}", words[1]);
9843        assert_eq!(
9844            words[2], 0xE59C_C014,
9845            "LDR r12,[r12,#20] (sidecar): {:#010x}",
9846            words[2]
9847        );
9848        assert_eq!(
9849            words[3], 0xE35C_0002,
9850            "CMP r12,#2 (expected class id): {:#010x}",
9851            words[3]
9852        );
9853        assert_eq!(words[4], 0x0A00_0000, "BEQ +1 insn: {:#010x}", words[4]);
9854        assert_eq!(
9855            words[5], 0xE7F0_00F0,
9856            "UDF (type-mismatch trap): {:#010x}",
9857            words[5]
9858        );
9859        assert_eq!(
9860            &with[guard_end + 24..],
9861            &without[guard_end..],
9862            "dispatch tail unchanged"
9863        );
9864    }
9865
9866    /// #178/#180 regression: the Thumb `Add`/`Adds`/`Subs` reg-forms used the
9867    /// 16-bit encoding unconditionally. For high registers (R12 base scratch,
9868    /// R8-R11 i64 pairs) the 3-bit register fields overflow and corrupt the
9869    /// operands — `add ip,ip,r0` came out as `adds r4,r5,r1` (0x186C), silently
9870    /// dropping the address operand and miscompiling every optimized memory
9871    /// access. High registers must use the 32-bit `.W` forms.
9872    #[test]
9873    fn test_encode_thumb_add_high_reg_uses_add_w_178_180() {
9874        let encoder = ArmEncoder::new_thumb2();
9875
9876        // add ip, ip, r0  — the exact MemLoad/MemStore base+addr op.
9877        let code = encoder
9878            .encode(&ArmOp::Add {
9879                rd: Reg::R12,
9880                rn: Reg::R12,
9881                op2: Operand2::Reg(Reg::R0),
9882            })
9883            .unwrap();
9884        // ADD.W ip, ip, r0 = EB0C 0C00 (little-endian halfwords).
9885        assert_eq!(
9886            code,
9887            vec![0x0C, 0xEB, 0x00, 0x0C],
9888            "high-reg Thumb ADD must be 32-bit ADD.W (EB0C 0C00), not corrupt 16-bit; got {code:02X?}"
9889        );
9890        // Must NOT be the buggy 16-bit 0x186C (`adds r4,r5,r1`).
9891        assert_ne!(code, vec![0x6C, 0x18], "regressed to corrupt 16-bit ADDS");
9892
9893        // Low-register add stays 16-bit (no regression for the common case).
9894        let lo = encoder
9895            .encode(&ArmOp::Add {
9896                rd: Reg::R1,
9897                rn: Reg::R2,
9898                op2: Operand2::Reg(Reg::R3),
9899            })
9900            .unwrap();
9901        assert_eq!(
9902            lo.len(),
9903            2,
9904            "low-reg ADD should remain 16-bit, got {lo:02X?}"
9905        );
9906    }
9907
9908    /// #178/#180 sibling: i64 low-word `Adds`/`Subs` can land in R8-R11 pairs;
9909    /// those must fall back to 32-bit ADDS.W/SUBS.W (flag-setting preserved).
9910    #[test]
9911    fn test_encode_thumb_adds_subs_high_reg_use_32bit_178_180() {
9912        let encoder = ArmEncoder::new_thumb2();
9913
9914        // adds r10, r10, r8  → ADDS.W = EB1A 0A08
9915        let adds = encoder
9916            .encode(&ArmOp::Adds {
9917                rd: Reg::R10,
9918                rn: Reg::R10,
9919                op2: Operand2::Reg(Reg::R8),
9920            })
9921            .unwrap();
9922        assert_eq!(
9923            adds,
9924            vec![0x1A, 0xEB, 0x08, 0x0A],
9925            "high-reg ADDS must be 32-bit ADDS.W (EB1A 0A08); got {adds:02X?}"
9926        );
9927
9928        // subs r10, r10, r8  → SUBS.W = EBBA 0A08
9929        let subs = encoder
9930            .encode(&ArmOp::Subs {
9931                rd: Reg::R10,
9932                rn: Reg::R10,
9933                op2: Operand2::Reg(Reg::R8),
9934            })
9935            .unwrap();
9936        assert_eq!(
9937            subs,
9938            vec![0xBA, 0xEB, 0x08, 0x0A],
9939            "high-reg SUBS must be 32-bit SUBS.W (EBBA 0A08); got {subs:02X?}"
9940        );
9941    }
9942
9943    /// #184 (sibling of #180): 16-bit CMN (T1) only encodes R0-R7. High registers
9944    /// must use 32-bit CMN.W, not the corrupt truncated 16-bit form.
9945    #[test]
9946    fn test_encode_thumb_cmn_high_reg_uses_cmn_w_184() {
9947        let encoder = ArmEncoder::new_thumb2();
9948
9949        // cmn r10, r8  → CMN.W = EB1A 0F08 (ADD.W S=1, Rd=PC discarded).
9950        let cmn = encoder
9951            .encode(&ArmOp::Cmn {
9952                rn: Reg::R10,
9953                op2: Operand2::Reg(Reg::R8),
9954            })
9955            .unwrap();
9956        assert_eq!(
9957            cmn,
9958            vec![0x1A, 0xEB, 0x08, 0x0F],
9959            "high-reg CMN must be 32-bit CMN.W (EB1A 0F08); got {cmn:02X?}"
9960        );
9961
9962        // Low registers stay 16-bit: cmn r1, r2 = 0x42D1.
9963        let lo = encoder
9964            .encode(&ArmOp::Cmn {
9965                rn: Reg::R1,
9966                op2: Operand2::Reg(Reg::R2),
9967            })
9968            .unwrap();
9969        assert_eq!(
9970            lo.len(),
9971            2,
9972            "low-reg CMN should remain 16-bit, got {lo:02X?}"
9973        );
9974        assert_eq!(lo, vec![0xD1, 0x42], "low-reg CMN bytes wrong: {lo:02X?}");
9975    }
9976
9977    /// #185 regression: feeding PC (R15) as a data operand to a Thumb-2 op that
9978    /// guards its registers must return Err, not panic under debug-assertions.
9979    /// (Synth never emits PC here; the fuzz harness requires encode() be total.)
9980    #[test]
9981    fn test_encode_pc_operand_returns_err_not_panic_185() {
9982        let encoder = ArmEncoder::new_thumb2();
9983        for op in [
9984            ArmOp::Sdiv {
9985                rd: Reg::PC,
9986                rn: Reg::R0,
9987                rm: Reg::R1,
9988            },
9989            ArmOp::Udiv {
9990                rd: Reg::R0,
9991                rn: Reg::PC,
9992                rm: Reg::R1,
9993            },
9994            ArmOp::Sdiv {
9995                rd: Reg::R0,
9996                rn: Reg::R1,
9997                rm: Reg::PC,
9998            },
9999        ] {
10000            let r = encoder.encode(&op);
10001            assert!(
10002                r.is_err(),
10003                "encode({op:?}) must return Err for a PC operand, got {r:?}"
10004            );
10005        }
10006        // Valid registers still encode fine (no false rejection).
10007        assert!(
10008            encoder
10009                .encode(&ArmOp::Sdiv {
10010                    rd: Reg::R0,
10011                    rn: Reg::R1,
10012                    rm: Reg::R2
10013                })
10014                .is_ok()
10015        );
10016    }
10017
10018    #[test]
10019    fn test_encode_nop_arm32() {
10020        let encoder = ArmEncoder::new_arm32();
10021        let code = encoder.encode(&ArmOp::Nop).unwrap();
10022
10023        assert_eq!(code.len(), 4); // ARM32 instructions are 4 bytes
10024        assert_eq!(code, vec![0x00, 0x00, 0xA0, 0xE1]); // MOV R0, R0
10025    }
10026
10027    #[test]
10028    fn test_encode_nop_thumb() {
10029        let encoder = ArmEncoder::new_thumb2();
10030        let code = encoder.encode(&ArmOp::Nop).unwrap();
10031
10032        assert_eq!(code.len(), 2); // Thumb instructions are 2 bytes
10033        assert_eq!(code, vec![0x00, 0xBF]); // NOP
10034    }
10035
10036    #[test]
10037    fn test_encode_mov_immediate_arm32() {
10038        let encoder = ArmEncoder::new_arm32();
10039        let op = ArmOp::Mov {
10040            rd: Reg::R0,
10041            op2: Operand2::Imm(42),
10042        };
10043
10044        let code = encoder.encode(&op).unwrap();
10045        assert_eq!(code.len(), 4);
10046
10047        // Verify it's a MOV instruction (bits should have immediate flag set)
10048        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10049        assert_eq!(instr & 0x0E000000, 0x02000000); // Check I bit is set
10050    }
10051
10052    #[test]
10053    fn test_encode_add_registers_arm32() {
10054        let encoder = ArmEncoder::new_arm32();
10055        let op = ArmOp::Add {
10056            rd: Reg::R0,
10057            rn: Reg::R1,
10058            op2: Operand2::Reg(Reg::R2),
10059        };
10060
10061        let code = encoder.encode(&op).unwrap();
10062        assert_eq!(code.len(), 4);
10063
10064        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10065        // Verify it's an ADD instruction with correct opcode
10066        assert_eq!(instr & 0x0FE00000, 0x00800000);
10067    }
10068
10069    /// #350 — `encode_thumb32_add_imm` must lower an out-of-range immediate
10070    /// (> 0xFFF) to a legal MOVW(/MOVT) + ADD.W-register sequence instead of
10071    /// erroring. The small-imm fast path (imm <= 0xFFF) stays byte-identical.
10072    #[test]
10073    fn test_encode_add_imm_large_350() {
10074        let enc = ArmEncoder::new_thumb2();
10075
10076        // --- Fast path: imm <= 0xFFF is a single 4-byte instruction, and the
10077        // VALUE must be right (#681: this test used to assert only the length,
10078        // letting the raw-packed T3 mis-encoding of 0x123 pass CI). 0x123 is
10079        // not ThumbExpandImm-representable, so it must be ADDW (T4, plain
10080        // imm12): clang `addw r0, r1, #0x123` = f201 0023.
10081        let small = enc
10082            .encode_thumb32_add_imm(&Reg::R0, &Reg::R1, 0x123)
10083            .unwrap();
10084        assert_eq!(small, vec![0x01, 0xF2, 0x23, 0x10], "ADDW r0, r1, #0x123");
10085
10086        // helper: decode a Thumb-2 MOVW/MOVT halfword pair back to its imm16
10087        fn movx_imm16(b: &[u8]) -> u32 {
10088            let hw1 = u16::from_le_bytes([b[0], b[1]]) as u32;
10089            let hw2 = u16::from_le_bytes([b[2], b[3]]) as u32;
10090            let imm4 = hw1 & 0xF;
10091            let i = (hw1 >> 10) & 1;
10092            let imm3 = (hw2 >> 12) & 0x7;
10093            let imm8 = hw2 & 0xFF;
10094            (imm4 << 12) | (i << 11) | (imm3 << 8) | imm8
10095        }
10096        fn movx_rd(b: &[u8]) -> u32 {
10097            (u16::from_le_bytes([b[2], b[3]]) as u32 >> 8) & 0xF
10098        }
10099
10100        // --- rd != rn: scratch is rd. imm = 70000 = 0x11170 needs MOVW+MOVT. ---
10101        // 0x11170: lo16 = 0x1170, hi16 = 0x0001
10102        let seq = enc
10103            .encode_thumb32_add_imm(&Reg::R12, &Reg::R0, 70000)
10104            .unwrap();
10105        assert_eq!(seq.len(), 12, "MOVW + MOVT + ADD = 12 bytes");
10106        // MOVW r12, #0x1170
10107        assert_eq!(u16::from_le_bytes([seq[0], seq[1]]) & 0xFBF0, 0xF240);
10108        assert_eq!(movx_rd(&seq[0..4]), 12);
10109        assert_eq!(movx_imm16(&seq[0..4]), 0x1170);
10110        // MOVT r12, #0x0001
10111        assert_eq!(u16::from_le_bytes([seq[4], seq[5]]) & 0xFBF0, 0xF2C0);
10112        assert_eq!(movx_rd(&seq[4..8]), 12);
10113        assert_eq!(movx_imm16(&seq[4..8]), 0x0001);
10114        // ADD.W r12, r0, r12  (EB00 | rn=0 ; rd=12, rm=12)
10115        let add1 = u16::from_le_bytes([seq[8], seq[9]]) as u32;
10116        let add2 = u16::from_le_bytes([seq[10], seq[11]]) as u32;
10117        assert_eq!(add1 & 0xFFF0, 0xEB00);
10118        assert_eq!(add1 & 0xF, 0); // rn = r0
10119        assert_eq!((add2 >> 8) & 0xF, 12); // rd = r12
10120        assert_eq!(add2 & 0xF, 12); // rm = scratch = r12
10121        // The materialized scratch must reconstruct exactly 70000.
10122        assert_eq!(
10123            (movx_imm16(&seq[4..8]) << 16) | movx_imm16(&seq[0..4]),
10124            70000
10125        );
10126
10127        // --- imm <= 0xFFFF: MOVT is skipped (MOVW + ADD = 8 bytes). ---
10128        let seq16 = enc
10129            .encode_thumb32_add_imm(&Reg::R3, &Reg::R0, 0xABCD)
10130            .unwrap();
10131        assert_eq!(seq16.len(), 8, "imm <= 0xFFFF skips MOVT");
10132        assert_eq!(movx_imm16(&seq16[0..4]), 0xABCD);
10133        assert_eq!(movx_rd(&seq16[0..4]), 3); // scratch = rd = r3
10134
10135        // --- rd == rn (in-place add): scratch must be R12, not rd. ---
10136        // imm = 0x12345: lo16 = 0x2345, hi16 = 0x0001
10137        let inplace = enc
10138            .encode_thumb32_add_imm(&Reg::R5, &Reg::R5, 0x12345)
10139            .unwrap();
10140        assert_eq!(inplace.len(), 12);
10141        assert_eq!(movx_rd(&inplace[0..4]), 12, "rd==rn must use R12 scratch");
10142        assert_eq!(
10143            (movx_imm16(&inplace[4..8]) << 16) | movx_imm16(&inplace[0..4]),
10144            0x12345
10145        );
10146        // ADD.W r5, r5, r12 — rm must be the scratch (12), never rn.
10147        let ip_add2 = u16::from_le_bytes([inplace[10], inplace[11]]) as u32;
10148        assert_eq!(ip_add2 & 0xF, 12);
10149        assert_eq!((ip_add2 >> 8) & 0xF, 5);
10150    }
10151
10152    /// #681 — `encode_thumb32_add_imm` packed a RAW immediate into the T3
10153    /// ADD.W `i:imm3:imm8` field, which is a ThumbExpandImm MODIFIED immediate:
10154    /// ThumbExpandImm(0x200) = 0, ThumbExpandImm(0x400) = 0x8000_0000. Every
10155    /// dynamic-address load/store with a static offset in 0x100..=0xFFF
10156    /// computed a wrong address (and bypassed --safety-bounds software: the
10157    /// guard checked the intended address, the access used the mis-encoded
10158    /// one). Fix: imm <= 0xFF keeps T3 (raw == expanded there, bit-identical);
10159    /// 0x100..=0xFFF uses ADDW (T4, plain imm12) — same lowering
10160    /// `encode_thumb32_add` already uses per #253.
10161    ///
10162    /// Every expected byte sequence below is pinned against clang
10163    /// (`-target thumbv7m-none-eabi`) output, bit-for-bit (#544 pattern).
10164    #[test]
10165    fn test_encode_add_imm_thumb_expand_681() {
10166        let enc = ArmEncoder::new_thumb2();
10167        let add = |rd: &Reg, rn: &Reg, imm: u32| enc.encode_thumb32_add_imm(rd, rn, imm).unwrap();
10168
10169        // imm <= 0xFF stays T3 ADD.W (raw == ThumbExpandImm-expanded):
10170        // clang: add.w r12, r0, #0xff  = f100 0cff
10171        assert_eq!(add(&Reg::R12, &Reg::R0, 0xFF), vec![0x00, 0xF1, 0xFF, 0x0C]);
10172
10173        // 0x100..=0xFFF must be ADDW (T4, plain imm12). The old T3 raw packing
10174        // decoded as +0 (0x100/0x200), +0x80000000 (0x400), etc.
10175        // clang: addw r12, r0, #0x100 = f200 1c00
10176        assert_eq!(
10177            add(&Reg::R12, &Reg::R0, 0x100),
10178            vec![0x00, 0xF2, 0x00, 0x1C]
10179        );
10180        // clang: addw r12, r0, #0x104 = f200 1c04
10181        assert_eq!(
10182            add(&Reg::R12, &Reg::R0, 0x104),
10183            vec![0x00, 0xF2, 0x04, 0x1C]
10184        );
10185        // clang: addw r12, r0, #0x200 = f200 2c00
10186        assert_eq!(
10187            add(&Reg::R12, &Reg::R0, 0x200),
10188            vec![0x00, 0xF2, 0x00, 0x2C]
10189        );
10190        // clang: addw r12, r0, #0x3fc = f200 3cfc
10191        assert_eq!(
10192            add(&Reg::R12, &Reg::R0, 0x3FC),
10193            vec![0x00, 0xF2, 0xFC, 0x3C]
10194        );
10195        // clang: addw r12, r0, #0x400 = f200 4c00
10196        assert_eq!(
10197            add(&Reg::R12, &Reg::R0, 0x400),
10198            vec![0x00, 0xF2, 0x00, 0x4C]
10199        );
10200        // clang: addw r12, r0, #0xfff = f600 7cff
10201        assert_eq!(
10202            add(&Reg::R12, &Reg::R0, 0xFFF),
10203            vec![0x00, 0xF6, 0xFF, 0x7C]
10204        );
10205        // Non-scratch rd/rn — clang: addw r1, r2, #0x104 = f202 1104
10206        assert_eq!(add(&Reg::R1, &Reg::R2, 0x104), vec![0x02, 0xF2, 0x04, 0x11]);
10207    }
10208
10209    /// #681 class audit — the T2 RSB and AND.W immediate fields are also
10210    /// ThumbExpandImm-coded and were raw-packed. Neither has a plain-imm12
10211    /// (T4-style) form, so a non-representable immediate must Err loudly
10212    /// (#253/#255/#378 class: never silently encode a different constant).
10213    /// Existing emitters only use representable values (RSB #32, AND #0x3F),
10214    /// pinned here bit-for-bit against clang.
10215    #[test]
10216    fn test_rsb_and_imm_thumb_expand_gate_681() {
10217        let enc = ArmEncoder::new_thumb2();
10218
10219        // clang: rsb.w r3, r2, #0x20 = f1c2 0320 — byte-identical to before.
10220        let rsb = enc
10221            .encode(&ArmOp::Rsb {
10222                rd: Reg::R3,
10223                rn: Reg::R2,
10224                imm: 32,
10225            })
10226            .unwrap();
10227        assert_eq!(rsb, vec![0xC2, 0xF1, 0x20, 0x03]);
10228
10229        // 0x101 is not ThumbExpandImm-representable -> must Err, not mis-encode.
10230        assert!(
10231            enc.encode(&ArmOp::Rsb {
10232                rd: Reg::R3,
10233                rn: Reg::R2,
10234                imm: 0x101,
10235            })
10236            .is_err(),
10237            "non-ThumbExpandImm RSB immediate must Err"
10238        );
10239
10240        // clang: and r4, r4, #0x3f = f004 043f — byte-identical to before.
10241        let and = enc.encode_thumb32_and_imm_raw(4, 4, 0x3F).unwrap();
10242        assert_eq!(and, vec![0x04, 0xF0, 0x3F, 0x04]);
10243        assert!(
10244            enc.encode_thumb32_and_imm_raw(4, 4, 0x101).is_err(),
10245            "non-ThumbExpandImm AND immediate must Err"
10246        );
10247
10248        // A32 RSB: imm12 is a rotate:imm8 modified immediate; > 0xFF used to be
10249        // silently masked to `imm & 0xFF` (#378 masking class) -> must Err.
10250        let a32 = ArmEncoder::new_arm32();
10251        assert!(
10252            a32.encode(&ArmOp::Rsb {
10253                rd: Reg::R3,
10254                rn: Reg::R2,
10255                imm: 0x120,
10256            })
10257            .is_err(),
10258            "A32 RSB immediate > 0xFF must Err, not mask"
10259        );
10260        // imm 32 (the only value real codegen emits) still encodes.
10261        assert!(
10262            a32.encode(&ArmOp::Rsb {
10263                rd: Reg::R3,
10264                rn: Reg::R2,
10265                imm: 32,
10266            })
10267            .is_ok()
10268        );
10269    }
10270
10271    /// #350 follow-up — the `encoder_no_panic` fuzz harness drives the encoder
10272    /// with ARBITRARY registers, including the one case the in-place lowering
10273    /// cannot serve: rd==rn==R12. There the scratch (R12, the reserved encoder
10274    /// register) would alias Rn and clobber it before the ADD reads it. The
10275    /// encoder contract (#180/#185) is Ok-or-Err, never a panic — so this must
10276    /// return Err, not assert. (Real codegen never emits rd==rn==R12 because R12
10277    /// is non-allocatable; this guards only the fuzz/adversarial path.)
10278    #[test]
10279    fn test_encode_add_imm_large_rd_rn_r12_errs_not_panics_350() {
10280        let enc = ArmEncoder::new_thumb2();
10281        // Out-of-range imm with rd==rn==R12: no free scratch -> Err.
10282        let r = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 70000);
10283        assert!(
10284            r.is_err(),
10285            "rd==rn==R12 with out-of-range imm must Err (no free scratch), got {r:?}"
10286        );
10287        // Small imm with rd==rn==R12 still takes the single-instruction fast path
10288        // (no scratch needed) and must succeed — the guard is scoped to the
10289        // out-of-range lowering only.
10290        let small = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 0x10);
10291        assert!(small.is_ok(), "small imm needs no scratch, must stay Ok");
10292    }
10293
10294    /// #378 — `encode_operand2` (ARM32 data-processing operand) must FAIL
10295    /// HONESTLY on an immediate that is not a valid rotated immediate, rather
10296    /// than silently masking it to `imm & 0xFF` and emitting a WRONG
10297    /// instruction. `0x1FF` has 9 set bits, so it cannot come from rotating an
10298    /// 8-bit imm8 — non-encodable. Real codegen materializes large constants via
10299    /// MOVW/MOVT; this guards the encoder's Ok-or-Err contract (#180/#185)
10300    /// directly. It is an Err (not a panic) so the `encoder_no_panic` fuzz
10301    /// harness — which drives arbitrary operands — still passes.
10302    #[test]
10303    fn test_encode_operand2_non_rotatable_imm_errs_not_masks_378() {
10304        let enc = ArmEncoder::new_arm32();
10305        let bad = enc.encode(&ArmOp::Add {
10306            rd: Reg::R0,
10307            rn: Reg::R1,
10308            op2: Operand2::Imm(0x1FF),
10309        });
10310        assert!(
10311            bad.is_err(),
10312            "non-rotatable ARM32 immediate 0x1FF must Err (was silently masked \
10313             to 0xFF), got {bad:?}"
10314        );
10315        // A representable rotated immediate still encodes fine (regression guard).
10316        let ok = enc.encode(&ArmOp::Add {
10317            rd: Reg::R0,
10318            rn: Reg::R1,
10319            op2: Operand2::Imm(0xFF),
10320        });
10321        assert!(
10322            ok.is_ok(),
10323            "0xFF is a valid rotated immediate, must stay Ok"
10324        );
10325    }
10326
10327    #[test]
10328    fn test_encode_ldr_arm32() {
10329        let encoder = ArmEncoder::new_arm32();
10330        let op = ArmOp::Ldr {
10331            rd: Reg::R0,
10332            addr: MemAddr::imm(Reg::R1, 4),
10333        };
10334
10335        let code = encoder.encode(&op).unwrap();
10336        assert_eq!(code.len(), 4);
10337
10338        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10339        // Verify load bit is set
10340        assert_eq!(instr & 0x00100000, 0x00100000);
10341    }
10342
10343    #[test]
10344    fn test_encode_str_arm32() {
10345        let encoder = ArmEncoder::new_arm32();
10346        let op = ArmOp::Str {
10347            rd: Reg::R0,
10348            addr: MemAddr::imm(Reg::SP, 0),
10349        };
10350
10351        let code = encoder.encode(&op).unwrap();
10352        assert_eq!(code.len(), 4);
10353    }
10354
10355    #[test]
10356    fn test_encode_branch_arm32() {
10357        let encoder = ArmEncoder::new_arm32();
10358        let op = ArmOp::Bl {
10359            label: "main".to_string(),
10360        };
10361
10362        let code = encoder.encode(&op).unwrap();
10363        assert_eq!(code.len(), 4);
10364
10365        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10366        // Verify BL opcode
10367        assert_eq!(instr & 0x0F000000, 0x0B000000);
10368    }
10369
10370    /// #1040: the A32 BL relocatable placeholder must carry an embedded addend
10371    /// of -8 so an R_ARM_CALL nets to exactly the symbol S — the A32 twin of
10372    /// the Thumb #167/#174 test above. A32 `BL` computes
10373    /// `target = P + 8 + (imm24 << 2)`, so:
10374    ///   - `eb000000` (imm24 = 0, addend 0) lands at S+8, two instructions
10375    ///     past the callee entry. This is what synth emitted before #1040, and
10376    ///     combined with the mislabelled R_ARM_THM_CALL a real linker
10377    ///     (`arm-none-eabi-ld`) corrupted the word to `eaca0000` — opcode
10378    ///     `eb` (BL) flipped to `ea` (B), so LR was never set at all.
10379    ///   - `ebfffffe` (imm24 = -2, offset -8) is `bl <self>` and nets to S.
10380    ///     This is exactly what `arm-none-eabi-as -march=armv7-r` emits for
10381    ///     `bl <extern>`, verified directly rather than derived from the ABI.
10382    #[test]
10383    fn test_encode_arm32_bl_placeholder_addend_1040() {
10384        let encoder = ArmEncoder::new_arm32();
10385        let code = encoder
10386            .encode(&ArmOp::Bl {
10387                label: "callee".to_string(),
10388            })
10389            .unwrap();
10390        assert_eq!(code.len(), 4, "A32 BL is one 32-bit word");
10391        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10392        assert_eq!(
10393            instr, 0xEBFF_FFFE,
10394            "A32 BL placeholder must be `ebfffffe` (gas's `bl <extern>`), not              `eb000000` — a 0 addend resolves two instructions past the callee              entry (#1040)"
10395        );
10396        // Spell the addend out so a future edit cannot satisfy the literal by
10397        // accident: sign-extended imm24, word-scaled, plus the +8 pipeline
10398        // bias, must be exactly 0 (branch-to-self).
10399        let imm24 = instr & 0x00FF_FFFF;
10400        let signed = ((imm24 as i32) << 8) >> 8;
10401        assert_eq!(
10402            8 + (signed << 2),
10403            0,
10404            "placeholder must branch to itself so the REL addend is -8"
10405        );
10406    }
10407
10408    /// Regression test for #167 + #174: the Thumb-2 BL relocatable placeholder
10409    /// must carry a -4 addend so an R_ARM_THM_CALL nets to exactly the symbol S.
10410    /// The correct encoding is what `gas` emits for `bl <extern>`: f7ff fffe
10411    /// (hw1=0xF7FF, hw2=0xFFFE), little-endian bytes FF F7 FE FF.
10412    ///   - 0xD000 (J1=J2=0) → ~+0x600000 garbage addend: `bl c0000c` / truncated
10413    ///     to fit (#167).
10414    ///   - 0xF800 (addend 0) → lands at S+4, one instruction past the callee
10415    ///     entry (#174).
10416    ///   - 0xFFFE (addend -4) → lands at S. Correct.
10417    #[test]
10418    fn test_encode_thumb_bl_placeholder_addend_167_174() {
10419        let encoder = ArmEncoder::new_thumb2();
10420        let op = ArmOp::Bl {
10421            label: "callee".to_string(),
10422        };
10423
10424        let code = encoder.encode(&op).unwrap();
10425        assert_eq!(code.len(), 4, "Thumb-2 BL is 32-bit");
10426
10427        let hw1 = u16::from_le_bytes([code[0], code[1]]);
10428        let hw2 = u16::from_le_bytes([code[2], code[3]]);
10429        assert_eq!(hw1, 0xF7FF, "BL first halfword (matches gas `bl <extern>`)");
10430        assert_eq!(
10431            hw2, 0xFFFE,
10432            "BL second halfword must be 0xFFFE (-4 addend → nets to S), not 0xF800 (→ S+4, #174) or 0xD000 (#167)"
10433        );
10434        assert_ne!(hw2, 0xF800, "0xF800 (addend 0) lands at S+4 (#174)");
10435        assert_ne!(hw2, 0xD000, "0xD000 bakes in a ~+0x600000 addend (#167)");
10436    }
10437
10438    /// #740: the Thumb-2 32-bit B<cond>.W (encoding T3) must pack the
10439    /// HALFWORD offset directly into S:J2:J1:imm6:imm11 — the byte offset is
10440    /// SignExtend(S:J2:J1:imm6:imm11:'0'). The old arm packed
10441    /// `halfword_offset >> 1`, HALVING every wide conditional branch's
10442    /// displacement: gust_poll's loop-head `br_if` to an outer block end
10443    /// landed mid-shape (a spurious state write + spurious calls on the
10444    /// empty-budget path). Narrow (16-bit) B<cond> was unaffected — only
10445    /// spans > 254 bytes hit the bug. Bytes cross-checked against the llvm
10446    /// disassembler (`bne.w #0x224` = f040 8112).
10447    #[test]
10448    fn test_encode_thumb_bcond_wide_t3_halfword_offset_740() {
10449        use synth_synthesis::Condition;
10450        let encoder = ArmEncoder::new_thumb2();
10451
10452        // gust_poll's loop-head edge: NE, +0x112 halfwords (+0x224 bytes).
10453        let code = encoder
10454            .encode(&ArmOp::BCondOffset {
10455                cond: Condition::NE,
10456                offset: 0x112,
10457            })
10458            .unwrap();
10459        assert_eq!(code.len(), 4, "offset beyond ±127 halfwords must be wide");
10460        let hw1 = u16::from_le_bytes([code[0], code[1]]);
10461        let hw2 = u16::from_le_bytes([code[2], code[3]]);
10462        assert_eq!(hw1, 0xF040, "T3 hw1: 1111 0 S=0 cond=NE imm6=0");
10463        assert_eq!(
10464            hw2, 0x8112,
10465            "T3 hw2 imm11 must carry halfword offset bits [10:0] directly — \
10466             0x8089 (offset>>1) is the halved #740 miscompile"
10467        );
10468
10469        // Backward wide branch: EQ, -0x100 halfwords. S=1, J2=J1=1,
10470        // imm6=0b111111, imm11=0x700 → f43f af00.
10471        let code = encoder
10472            .encode(&ArmOp::BCondOffset {
10473                cond: Condition::EQ,
10474                offset: -0x100,
10475            })
10476            .unwrap();
10477        assert_eq!(code.len(), 4);
10478        let hw1 = u16::from_le_bytes([code[0], code[1]]);
10479        let hw2 = u16::from_le_bytes([code[2], code[3]]);
10480        assert_eq!(hw1, 0xF43F, "T3 hw1: S=1, cond=EQ, imm6=0x3F");
10481        assert_eq!(hw2, 0xAF00, "T3 hw2: J1=1 J2=1 imm11=0x700");
10482
10483        // Narrow encoding stays byte-identical (in-range offsets untouched).
10484        let code = encoder
10485            .encode(&ArmOp::BCondOffset {
10486                cond: Condition::EQ,
10487                offset: 5,
10488            })
10489            .unwrap();
10490        assert_eq!(code, vec![0x05, 0xD0], "narrow B<cond> unchanged");
10491
10492        // Out of the signed 20-bit T3 range: loud Err, never a truncated jump.
10493        assert!(
10494            encoder
10495                .encode(&ArmOp::BCondOffset {
10496                    cond: Condition::NE,
10497                    offset: 1 << 19,
10498                })
10499                .is_err(),
10500            "out-of-range T3 offset must be a loud decline"
10501        );
10502    }
10503
10504    #[test]
10505    fn test_encode_sequence() {
10506        let encoder = ArmEncoder::new_arm32();
10507        let ops = vec![
10508            ArmOp::Mov {
10509                rd: Reg::R0,
10510                op2: Operand2::Imm(42),
10511            },
10512            ArmOp::Mov {
10513                rd: Reg::R1,
10514                op2: Operand2::Imm(10),
10515            },
10516            ArmOp::Add {
10517                rd: Reg::R2,
10518                rn: Reg::R0,
10519                op2: Operand2::Reg(Reg::R1),
10520            },
10521        ];
10522
10523        let code = encoder.encode_sequence(&ops).unwrap();
10524        assert_eq!(code.len(), 12); // 3 instructions * 4 bytes
10525    }
10526
10527    #[test]
10528    fn test_reg_to_bits() {
10529        assert_eq!(reg_to_bits(&Reg::R0), 0);
10530        assert_eq!(reg_to_bits(&Reg::R7), 7);
10531        assert_eq!(reg_to_bits(&Reg::SP), 13);
10532        assert_eq!(reg_to_bits(&Reg::LR), 14);
10533        assert_eq!(reg_to_bits(&Reg::PC), 15);
10534    }
10535
10536    #[test]
10537    fn test_encode_bitwise_operations() {
10538        let encoder = ArmEncoder::new_arm32();
10539
10540        let and_op = ArmOp::And {
10541            rd: Reg::R0,
10542            rn: Reg::R1,
10543            op2: Operand2::Reg(Reg::R2),
10544        };
10545        let and_code = encoder.encode(&and_op).unwrap();
10546        assert_eq!(and_code.len(), 4);
10547
10548        let orr_op = ArmOp::Orr {
10549            rd: Reg::R0,
10550            rn: Reg::R1,
10551            op2: Operand2::Reg(Reg::R2),
10552        };
10553        let orr_code = encoder.encode(&orr_op).unwrap();
10554        assert_eq!(orr_code.len(), 4);
10555
10556        let eor_op = ArmOp::Eor {
10557            rd: Reg::R0,
10558            rn: Reg::R1,
10559            op2: Operand2::Reg(Reg::R2),
10560        };
10561        let eor_code = encoder.encode(&eor_op).unwrap();
10562        assert_eq!(eor_code.len(), 4);
10563    }
10564
10565    // === Thumb-2 32-bit encoding tests ===
10566
10567    #[test]
10568    fn test_encode_sdiv_thumb2() {
10569        let encoder = ArmEncoder::new_thumb2();
10570        let op = ArmOp::Sdiv {
10571            rd: Reg::R0,
10572            rn: Reg::R1,
10573            rm: Reg::R2,
10574        };
10575
10576        let code = encoder.encode(&op).unwrap();
10577        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10578
10579        // SDIV R0, R1, R2: 0xFB91 0xF0F2
10580        // First halfword: 0xFB90 | Rn(1) = 0xFB91
10581        // Second halfword: 0xF0F0 | Rd(0)<<8 | Rm(2) = 0xF0F2
10582        // Little-endian: [0x91, 0xFB, 0xF2, 0xF0]
10583        assert_eq!(code[0], 0x91);
10584        assert_eq!(code[1], 0xFB);
10585        assert_eq!(code[2], 0xF2);
10586        assert_eq!(code[3], 0xF0);
10587    }
10588
10589    #[test]
10590    fn test_encode_udiv_thumb2() {
10591        let encoder = ArmEncoder::new_thumb2();
10592        let op = ArmOp::Udiv {
10593            rd: Reg::R0,
10594            rn: Reg::R1,
10595            rm: Reg::R2,
10596        };
10597
10598        let code = encoder.encode(&op).unwrap();
10599        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10600
10601        // UDIV R0, R1, R2: 0xFBB1 0xF0F2
10602        // Little-endian: [0xB1, 0xFB, 0xF2, 0xF0]
10603        assert_eq!(code[0], 0xB1);
10604        assert_eq!(code[1], 0xFB);
10605        assert_eq!(code[2], 0xF2);
10606        assert_eq!(code[3], 0xF0);
10607    }
10608
10609    #[test]
10610    fn test_encode_mul_thumb2() {
10611        let encoder = ArmEncoder::new_thumb2();
10612        let op = ArmOp::Mul {
10613            rd: Reg::R0,
10614            rn: Reg::R1,
10615            rm: Reg::R2,
10616        };
10617
10618        let code = encoder.encode(&op).unwrap();
10619        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10620    }
10621
10622    #[test]
10623    fn test_encode_and_thumb2() {
10624        let encoder = ArmEncoder::new_thumb2();
10625        let op = ArmOp::And {
10626            rd: Reg::R0,
10627            rn: Reg::R1,
10628            op2: Operand2::Reg(Reg::R2),
10629        };
10630
10631        let code = encoder.encode(&op).unwrap();
10632        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10633    }
10634
10635    #[test]
10636    fn test_encode_lsl_thumb2_low_regs() {
10637        let encoder = ArmEncoder::new_thumb2();
10638        let op = ArmOp::Lsl {
10639            rd: Reg::R0,
10640            rn: Reg::R1,
10641            shift: 5,
10642        };
10643
10644        let code = encoder.encode(&op).unwrap();
10645        assert_eq!(code.len(), 2); // 16-bit for low registers
10646    }
10647
10648    #[test]
10649    fn test_encode_clz_thumb2() {
10650        let encoder = ArmEncoder::new_thumb2();
10651        let op = ArmOp::Clz {
10652            rd: Reg::R0,
10653            rm: Reg::R1,
10654        };
10655
10656        let code = encoder.encode(&op).unwrap();
10657        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10658    }
10659
10660    #[test]
10661    fn test_encode_bx_thumb2() {
10662        let encoder = ArmEncoder::new_thumb2();
10663        let op = ArmOp::Bx { rm: Reg::LR };
10664
10665        let code = encoder.encode(&op).unwrap();
10666        assert_eq!(code.len(), 2); // 16-bit instruction
10667
10668        // BX LR: 0x4770
10669        assert_eq!(code, vec![0x70, 0x47]);
10670    }
10671
10672    // ========================================================================
10673    // f32 pseudo-op encoding tests
10674    // ========================================================================
10675
10676    #[test]
10677    fn test_encode_f32_abs_arm32() {
10678        let encoder = ArmEncoder::new_arm32();
10679        let op = ArmOp::F32Abs {
10680            sd: VfpReg::S0,
10681            sm: VfpReg::S2,
10682        };
10683        let code = encoder.encode(&op).unwrap();
10684        assert_eq!(code.len(), 4); // Single VFP instruction
10685    }
10686
10687    #[test]
10688    fn test_encode_f32_neg_arm32() {
10689        let encoder = ArmEncoder::new_arm32();
10690        let op = ArmOp::F32Neg {
10691            sd: VfpReg::S0,
10692            sm: VfpReg::S2,
10693        };
10694        let code = encoder.encode(&op).unwrap();
10695        assert_eq!(code.len(), 4);
10696    }
10697
10698    #[test]
10699    fn test_encode_f32_sqrt_arm32() {
10700        let encoder = ArmEncoder::new_arm32();
10701        let op = ArmOp::F32Sqrt {
10702            sd: VfpReg::S0,
10703            sm: VfpReg::S2,
10704        };
10705        let code = encoder.encode(&op).unwrap();
10706        assert_eq!(code.len(), 4);
10707    }
10708
10709    #[test]
10710    fn test_encode_f32_ceil_arm32() {
10711        let encoder = ArmEncoder::new_arm32();
10712        let op = ArmOp::F32Ceil {
10713            sd: VfpReg::S0,
10714            sm: VfpReg::S2,
10715        };
10716        let code = encoder.encode(&op).unwrap();
10717        // VMRS + BIC + ORR + VMSR + VCVT.S32.F32 + VMRS + BIC + VMSR + VCVT.F32.S32
10718        assert_eq!(code.len(), 36);
10719    }
10720
10721    #[test]
10722    fn test_encode_f32_floor_thumb2() {
10723        let encoder = ArmEncoder::new_thumb2();
10724        let op = ArmOp::F32Floor {
10725            sd: VfpReg::S0,
10726            sm: VfpReg::S2,
10727        };
10728        let code = encoder.encode(&op).unwrap();
10729        // VMRS + BIC.W + ORR.W + VMSR + VCVT + VMRS + BIC.W + VMSR + VCVT.F32.S32
10730        assert_eq!(code.len(), 36);
10731    }
10732
10733    #[test]
10734    fn test_encode_f32_min_arm32() {
10735        let encoder = ArmEncoder::new_arm32();
10736        let op = ArmOp::F32Min {
10737            sd: VfpReg::S0,
10738            sn: VfpReg::S2,
10739            sm: VfpReg::S4,
10740        };
10741        let code = encoder.encode(&op).unwrap();
10742        assert_eq!(code.len(), 16); // VMOV + VCMP + VMRS + conditional VMOV
10743    }
10744
10745    #[test]
10746    fn test_encode_f32_max_thumb2() {
10747        let encoder = ArmEncoder::new_thumb2();
10748        let op = ArmOp::F32Max {
10749            sd: VfpReg::S0,
10750            sn: VfpReg::S2,
10751            sm: VfpReg::S4,
10752        };
10753        let code = encoder.encode(&op).unwrap();
10754        // VMOV(4) + VCMP(4) + VMRS(4) + IT(2) + VMOV(4) = 18
10755        assert_eq!(code.len(), 18);
10756    }
10757
10758    #[test]
10759    fn test_encode_f32_copysign_arm32() {
10760        let encoder = ArmEncoder::new_arm32();
10761        let op = ArmOp::F32Copysign {
10762            sd: VfpReg::S0,
10763            sn: VfpReg::S2,
10764            sm: VfpReg::S4,
10765        };
10766        let code = encoder.encode(&op).unwrap();
10767        // VMOV + VMOV + AND + BIC + ORR + VMOV = 6 * 4 = 24
10768        assert_eq!(code.len(), 24);
10769    }
10770
10771    // ========================================================================
10772    // f64 encoding tests
10773    // ========================================================================
10774
10775    #[test]
10776    fn test_encode_f64_add_arm32() {
10777        let encoder = ArmEncoder::new_arm32();
10778        let op = ArmOp::F64Add {
10779            dd: VfpReg::D0,
10780            dn: VfpReg::D1,
10781            dm: VfpReg::D2,
10782        };
10783        let code = encoder.encode(&op).unwrap();
10784        assert_eq!(code.len(), 4);
10785        // VADD.F64 D0, D1, D2: check coprocessor is cp11 (0xB)
10786        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10787        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
10788    }
10789
10790    #[test]
10791    fn test_encode_f64_sub_thumb2() {
10792        let encoder = ArmEncoder::new_thumb2();
10793        let op = ArmOp::F64Sub {
10794            dd: VfpReg::D0,
10795            dn: VfpReg::D1,
10796            dm: VfpReg::D2,
10797        };
10798        let code = encoder.encode(&op).unwrap();
10799        assert_eq!(code.len(), 4); // 32-bit VFP as two Thumb halfwords
10800    }
10801
10802    #[test]
10803    fn test_encode_f64_mul_arm32() {
10804        let encoder = ArmEncoder::new_arm32();
10805        let op = ArmOp::F64Mul {
10806            dd: VfpReg::D0,
10807            dn: VfpReg::D1,
10808            dm: VfpReg::D2,
10809        };
10810        let code = encoder.encode(&op).unwrap();
10811        assert_eq!(code.len(), 4);
10812    }
10813
10814    #[test]
10815    fn test_encode_f64_div_arm32() {
10816        let encoder = ArmEncoder::new_arm32();
10817        let op = ArmOp::F64Div {
10818            dd: VfpReg::D0,
10819            dn: VfpReg::D1,
10820            dm: VfpReg::D2,
10821        };
10822        let code = encoder.encode(&op).unwrap();
10823        assert_eq!(code.len(), 4);
10824    }
10825
10826    #[test]
10827    fn test_encode_f64_abs_arm32() {
10828        let encoder = ArmEncoder::new_arm32();
10829        let op = ArmOp::F64Abs {
10830            dd: VfpReg::D0,
10831            dm: VfpReg::D2,
10832        };
10833        let code = encoder.encode(&op).unwrap();
10834        assert_eq!(code.len(), 4);
10835    }
10836
10837    #[test]
10838    fn test_encode_f64_neg_arm32() {
10839        let encoder = ArmEncoder::new_arm32();
10840        let op = ArmOp::F64Neg {
10841            dd: VfpReg::D0,
10842            dm: VfpReg::D2,
10843        };
10844        let code = encoder.encode(&op).unwrap();
10845        assert_eq!(code.len(), 4);
10846    }
10847
10848    #[test]
10849    fn test_encode_f64_sqrt_arm32() {
10850        let encoder = ArmEncoder::new_arm32();
10851        let op = ArmOp::F64Sqrt {
10852            dd: VfpReg::D0,
10853            dm: VfpReg::D2,
10854        };
10855        let code = encoder.encode(&op).unwrap();
10856        assert_eq!(code.len(), 4);
10857    }
10858
10859    #[test]
10860    fn test_encode_f64_load_arm32() {
10861        let encoder = ArmEncoder::new_arm32();
10862        let op = ArmOp::F64Load {
10863            dd: VfpReg::D0,
10864            addr: MemAddr::imm(Reg::R0, 8),
10865        };
10866        let code = encoder.encode(&op).unwrap();
10867        assert_eq!(code.len(), 4);
10868        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10869        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11 for F64
10870        assert_eq!(instr & 0xFF, 2); // offset 8 / 4 = 2
10871    }
10872
10873    #[test]
10874    fn test_encode_f64_store_thumb2() {
10875        let encoder = ArmEncoder::new_thumb2();
10876        let op = ArmOp::F64Store {
10877            dd: VfpReg::D0,
10878            addr: MemAddr::imm(Reg::SP, 0),
10879        };
10880        let code = encoder.encode(&op).unwrap();
10881        assert_eq!(code.len(), 4);
10882    }
10883
10884    #[test]
10885    fn test_encode_f64_compare_arm32() {
10886        let encoder = ArmEncoder::new_arm32();
10887        let op = ArmOp::F64Eq {
10888            rd: Reg::R0,
10889            dn: VfpReg::D0,
10890            dm: VfpReg::D1,
10891        };
10892        let code = encoder.encode(&op).unwrap();
10893        assert_eq!(code.len(), 16); // VCMP + VMRS + MOV #0 + MOVcond #1
10894    }
10895
10896    #[test]
10897    fn test_encode_f64_compare_thumb2() {
10898        let encoder = ArmEncoder::new_thumb2();
10899        let op = ArmOp::F64Lt {
10900            rd: Reg::R0,
10901            dn: VfpReg::D0,
10902            dm: VfpReg::D1,
10903        };
10904        let code = encoder.encode(&op).unwrap();
10905        // VCMP(4) + VMRS(4) + MOVS(2) + IT(2) + MOV(2) = 14
10906        assert_eq!(code.len(), 14);
10907    }
10908
10909    #[test]
10910    fn test_encode_f64_const_arm32() {
10911        let encoder = ArmEncoder::new_arm32();
10912        let op = ArmOp::F64Const {
10913            dd: VfpReg::D0,
10914            value: 3.125,
10915        };
10916        let code = encoder.encode(&op).unwrap();
10917        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
10918        assert_eq!(code.len(), 20);
10919    }
10920
10921    #[test]
10922    fn test_encode_f64_const_thumb2() {
10923        let encoder = ArmEncoder::new_thumb2();
10924        let op = ArmOp::F64Const {
10925            dd: VfpReg::D0,
10926            value: 2.5,
10927        };
10928        let code = encoder.encode(&op).unwrap();
10929        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
10930        assert_eq!(code.len(), 20);
10931    }
10932
10933    #[test]
10934    fn test_encode_f64_convert_i32s_arm32() {
10935        let encoder = ArmEncoder::new_arm32();
10936        let op = ArmOp::F64ConvertI32S {
10937            dd: VfpReg::D0,
10938            rm: Reg::R0,
10939        };
10940        let code = encoder.encode(&op).unwrap();
10941        // VMOV(4) + VCVT(4) = 8
10942        assert_eq!(code.len(), 8);
10943    }
10944
10945    #[test]
10946    fn test_encode_f64_promote_f32_arm32() {
10947        let encoder = ArmEncoder::new_arm32();
10948        let op = ArmOp::F64PromoteF32 {
10949            dd: VfpReg::D0,
10950            sm: VfpReg::S0,
10951        };
10952        let code = encoder.encode(&op).unwrap();
10953        assert_eq!(code.len(), 4); // Single VCVT.F64.F32 instruction
10954    }
10955
10956    #[test]
10957    fn test_encode_f64_promote_f32_thumb2() {
10958        let encoder = ArmEncoder::new_thumb2();
10959        let op = ArmOp::F64PromoteF32 {
10960            dd: VfpReg::D0,
10961            sm: VfpReg::S0,
10962        };
10963        let code = encoder.encode(&op).unwrap();
10964        assert_eq!(code.len(), 4);
10965    }
10966
10967    #[test]
10968    fn test_encode_i32_trunc_f64s_arm32() {
10969        let encoder = ArmEncoder::new_arm32();
10970        let op = ArmOp::I32TruncF64S {
10971            rd: Reg::R0,
10972            dm: VfpReg::D0,
10973        };
10974        let code = encoder.encode(&op).unwrap();
10975        // VCVT(4) + VMOV(4) = 8
10976        assert_eq!(code.len(), 8);
10977    }
10978
10979    #[test]
10980    fn test_encode_f64_reinterpret_i64_arm32() {
10981        let encoder = ArmEncoder::new_arm32();
10982        let op = ArmOp::F64ReinterpretI64 {
10983            dd: VfpReg::D0,
10984            rmlo: Reg::R0,
10985            rmhi: Reg::R1,
10986        };
10987        let code = encoder.encode(&op).unwrap();
10988        assert_eq!(code.len(), 4); // Single VMOV instruction
10989    }
10990
10991    #[test]
10992    fn test_encode_i64_reinterpret_f64_thumb2() {
10993        let encoder = ArmEncoder::new_thumb2();
10994        let op = ArmOp::I64ReinterpretF64 {
10995            rdlo: Reg::R0,
10996            rdhi: Reg::R1,
10997            dm: VfpReg::D0,
10998        };
10999        let code = encoder.encode(&op).unwrap();
11000        assert_eq!(code.len(), 4);
11001    }
11002
11003    #[test]
11004    fn test_encode_f64_trunc_thumb2() {
11005        let encoder = ArmEncoder::new_thumb2();
11006        let op = ArmOp::F64Trunc {
11007            dd: VfpReg::D0,
11008            dm: VfpReg::D1,
11009        };
11010        let code = encoder.encode(&op).unwrap();
11011        // GI-FPU-002 phase 3 (#369): a single VRINTZ.F64 (clang-verified
11012        // vrintz.f64 d0,d1 base) — no more FPSCR dance / S0 clobber.
11013        assert_eq!(code.len(), 4);
11014        assert_eq!(code, vec![0xb6, 0xee, 0xc1, 0x0b]);
11015    }
11016
11017    /// GI-FPU-002 phase 3 (#369): the rewritten f64 tail sequences, byte-exact
11018    /// against clang (`-target thumbv7em-none-eabi -mfpu=fpv5-d16`). Each
11019    /// clobbers ONLY its destination (+R12/flags where noted) — the previous
11020    /// pseudo-ops staged through live S0/R0-R2 (the #615 class) and the
11021    /// min/max/rounding semantics were wrong (ordered IT select returned the
11022    /// wrong operand on NaN/±0; rounding round-tripped through a 32-bit int).
11023    #[test]
11024    fn test_369_f64_tail_thumb2_encodings_match_clang() {
11025        let enc = ArmEncoder::new_thumb2();
11026        // vrintn/vrintp/vrintm.f64 d1, d2 (FE space, never IT'd).
11027        for (op, want) in [
11028            (
11029                ArmOp::F64Nearest {
11030                    dd: VfpReg::D1,
11031                    dm: VfpReg::D2,
11032                },
11033                vec![0xb9, 0xfe, 0x42, 0x1b],
11034            ),
11035            (
11036                ArmOp::F64Ceil {
11037                    dd: VfpReg::D1,
11038                    dm: VfpReg::D2,
11039                },
11040                vec![0xba, 0xfe, 0x42, 0x1b],
11041            ),
11042            (
11043                ArmOp::F64Floor {
11044                    dd: VfpReg::D1,
11045                    dm: VfpReg::D2,
11046                },
11047                vec![0xbb, 0xfe, 0x42, 0x1b],
11048            ),
11049        ] {
11050            assert_eq!(enc.encode(&op).unwrap(), want, "{op:?}");
11051        }
11052        // vcmp.f64 d1,d2 ; vmrs ; vminnm.f64 d0,d1,d2 ; it vs ; vaddvs.f64
11053        let min = enc
11054            .encode(&ArmOp::F64Min {
11055                dd: VfpReg::D0,
11056                dn: VfpReg::D1,
11057                dm: VfpReg::D2,
11058            })
11059            .unwrap();
11060        assert_eq!(
11061            min,
11062            vec![
11063                0xb4, 0xee, 0x42, 0x1b, // vcmp.f64 d1, d2
11064                0xf1, 0xee, 0x10, 0xfa, // vmrs APSR_nzcv, fpscr
11065                0x81, 0xfe, 0x42, 0x0b, // vminnm.f64 d0, d1, d2
11066                0x68, 0xbf, // it vs
11067                0x31, 0xee, 0x02, 0x0b, // vaddvs.f64 d0, d1, d2
11068            ]
11069        );
11070        // vmaxnm variant flips only bit6 of the VMINNM word.
11071        let max = enc
11072            .encode(&ArmOp::F64Max {
11073                dd: VfpReg::D0,
11074                dn: VfpReg::D1,
11075                dm: VfpReg::D2,
11076            })
11077            .unwrap();
11078        assert_eq!(&max[8..12], &[0x81, 0xfe, 0x02, 0x0b]);
11079        // Destination aliasing a source must ERR (the NaN fix-up would read
11080        // a clobbered operand), never encode.
11081        assert!(
11082            enc.encode(&ArmOp::F64Min {
11083                dd: VfpReg::D1,
11084                dn: VfpReg::D1,
11085                dm: VfpReg::D2,
11086            })
11087            .is_err()
11088        );
11089        // copysign d0,(mag)d1,(sign)d2:
11090        // vmov r12,s5 ; cmp.w r12,#0 ; vabs.f64 d0,d1 ; it mi ; vnegmi.f64 d0,d0
11091        let cs = enc
11092            .encode(&ArmOp::F64Copysign {
11093                dd: VfpReg::D0,
11094                dn: VfpReg::D1,
11095                dm: VfpReg::D2,
11096            })
11097            .unwrap();
11098        assert_eq!(
11099            cs,
11100            vec![
11101                0x12, 0xee, 0x90, 0xca, // vmov r12, s5
11102                0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
11103                0xb0, 0xee, 0xc1, 0x0b, // vabs.f64 d0, d1
11104                0x48, 0xbf, // it mi
11105                0xb1, 0xee, 0x40, 0x0b, // vnegmi.f64 d0, d0
11106            ]
11107        );
11108        // f32 copysign s0,(mag)s1,(sign)s2 — the R0-clobber-free rewrite:
11109        // vmov r12,s2 ; cmp.w r12,#0 ; vabs.f32 s0,s1 ; it mi ; vnegmi.f32
11110        let cs32 = enc
11111            .encode(&ArmOp::F32Copysign {
11112                sd: VfpReg::S0,
11113                sn: VfpReg::S1,
11114                sm: VfpReg::S2,
11115            })
11116            .unwrap();
11117        assert_eq!(
11118            cs32,
11119            vec![
11120                0x11, 0xee, 0x10, 0xca, // vmov r12, s2
11121                0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
11122                0xb0, 0xee, 0xe0, 0x0a, // vabs.f32 s0, s1
11123                0x48, 0xbf, // it mi
11124                0xb1, 0xee, 0x40, 0x0a, // vnegmi.f32 s0, s0
11125            ]
11126        );
11127        // i32 -> f64 stages through the DESTINATION's S-alias (never S0) and
11128        // uses the CORRECT signed/unsigned VCVT bases (previously swapped):
11129        // vmov s0,r3 ; vcvt.f64.s32 d0,s0
11130        let conv_s = enc
11131            .encode(&ArmOp::F64ConvertI32S {
11132                dd: VfpReg::D0,
11133                rm: Reg::R3,
11134            })
11135            .unwrap();
11136        assert_eq!(
11137            conv_s,
11138            vec![
11139                0x00, 0xee, 0x10, 0x3a, // vmov s0, r3
11140                0xb8, 0xee, 0xc0, 0x0b, // vcvt.f64.s32 d0, s0
11141            ]
11142        );
11143        let conv_u = enc
11144            .encode(&ArmOp::F64ConvertI32U {
11145                dd: VfpReg::D0,
11146                rm: Reg::R3,
11147            })
11148            .unwrap();
11149        assert_eq!(&conv_u[4..8], &[0xb8, 0xee, 0x40, 0x0b]); // vcvt.f64.u32
11150        // f64 -> i32 stages through the SOURCE's S-alias (never S0):
11151        // vcvt.s32.f64 s2,d1 ; vmov r3,s2
11152        let trunc_s = enc
11153            .encode(&ArmOp::I32TruncF64S {
11154                rd: Reg::R3,
11155                dm: VfpReg::D1,
11156            })
11157            .unwrap();
11158        assert_eq!(
11159            trunc_s,
11160            vec![
11161                0xbd, 0xee, 0xc1, 0x1b, // vcvt.s32.f64 s2, d1
11162                0x11, 0xee, 0x10, 0x3a, // vmov r3, s2
11163            ]
11164        );
11165        let trunc_u = enc
11166            .encode(&ArmOp::I32TruncF64U {
11167                rd: Reg::R3,
11168                dm: VfpReg::D1,
11169            })
11170            .unwrap();
11171        assert_eq!(&trunc_u[0..4], &[0xbc, 0xee, 0xc1, 0x1b]); // vcvt.u32.f64
11172        // f32.demote_f64: vcvt.f32.f64 s1, d2
11173        let demote = enc
11174            .encode(&ArmOp::F32DemoteF64 {
11175                sd: VfpReg::S1,
11176                dm: VfpReg::D2,
11177            })
11178            .unwrap();
11179        assert_eq!(demote, vec![0xf7, 0xee, 0xc2, 0x0b]);
11180    }
11181
11182    #[test]
11183    fn test_encode_f64_min_arm32() {
11184        let encoder = ArmEncoder::new_arm32();
11185        let op = ArmOp::F64Min {
11186            dd: VfpReg::D0,
11187            dn: VfpReg::D1,
11188            dm: VfpReg::D2,
11189        };
11190        let code = encoder.encode(&op).unwrap();
11191        // VMOV + VCMP + VMRS + conditional VMOV = 16
11192        assert_eq!(code.len(), 16);
11193    }
11194
11195    #[test]
11196    fn test_f64_cp11_encoding() {
11197        // Verify that F64 instructions use coprocessor 11 (0xB), not 10 (0xA)
11198        let encoder = ArmEncoder::new_arm32();
11199
11200        // F64Add
11201        let code = encoder
11202            .encode(&ArmOp::F64Add {
11203                dd: VfpReg::D0,
11204                dn: VfpReg::D0,
11205                dm: VfpReg::D0,
11206            })
11207            .unwrap();
11208        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11209        assert_eq!((instr >> 8) & 0xF, 0xB, "F64 should use cp11");
11210
11211        // F32Add for comparison
11212        let code = encoder
11213            .encode(&ArmOp::F32Add {
11214                sd: VfpReg::S0,
11215                sn: VfpReg::S0,
11216                sm: VfpReg::S0,
11217            })
11218            .unwrap();
11219        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11220        assert_eq!((instr >> 8) & 0xF, 0xA, "F32 should use cp10");
11221    }
11222
11223    #[test]
11224    fn test_dreg_encoding_higher_registers() {
11225        let encoder = ArmEncoder::new_arm32();
11226
11227        // Test with D15 (highest register)
11228        let op = ArmOp::F64Add {
11229            dd: VfpReg::D15,
11230            dn: VfpReg::D14,
11231            dm: VfpReg::D13,
11232        };
11233        let code = encoder.encode(&op).unwrap();
11234        assert_eq!(code.len(), 4);
11235
11236        // Verify the register encoding worked (instruction is valid)
11237        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11238        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
11239    }
11240
11241    // ========================================================================
11242    // Control flow encoding tests
11243    // ========================================================================
11244
11245    #[test]
11246    fn test_encode_label_emits_no_bytes() {
11247        let encoder = ArmEncoder::new_thumb2();
11248        let op = ArmOp::Label {
11249            name: ".Lblock_end_0".to_string(),
11250        };
11251        let code = encoder.encode(&op).unwrap();
11252        assert!(code.is_empty(), "Label should emit zero bytes");
11253
11254        let encoder32 = ArmEncoder::new_arm32();
11255        let code32 = encoder32.encode(&op).unwrap();
11256        assert!(
11257            code32.is_empty(),
11258            "Label should emit zero bytes in ARM32 too"
11259        );
11260    }
11261
11262    #[test]
11263    fn test_encode_bcc_eq_thumb2() {
11264        use synth_synthesis::Condition;
11265        let encoder = ArmEncoder::new_thumb2();
11266        let op = ArmOp::Bcc {
11267            cond: Condition::EQ,
11268            label: "target".to_string(),
11269        };
11270        let code = encoder.encode(&op).unwrap();
11271        assert_eq!(code.len(), 2); // 16-bit conditional branch
11272
11273        // BEQ with offset 0: 0xD000 in little-endian
11274        assert_eq!(code, vec![0x00, 0xD0]);
11275    }
11276
11277    #[test]
11278    fn test_encode_bcc_ne_thumb2() {
11279        use synth_synthesis::Condition;
11280        let encoder = ArmEncoder::new_thumb2();
11281        let op = ArmOp::Bcc {
11282            cond: Condition::NE,
11283            label: "target".to_string(),
11284        };
11285        let code = encoder.encode(&op).unwrap();
11286        assert_eq!(code.len(), 2);
11287
11288        // BNE with offset 0: 0xD100 in little-endian
11289        assert_eq!(code, vec![0x00, 0xD1]);
11290    }
11291
11292    #[test]
11293    fn test_encode_bcc_arm32() {
11294        use synth_synthesis::Condition;
11295        let encoder = ArmEncoder::new_arm32();
11296        let op = ArmOp::Bcc {
11297            cond: Condition::EQ,
11298            label: "target".to_string(),
11299        };
11300        let code = encoder.encode(&op).unwrap();
11301        assert_eq!(code.len(), 4); // 32-bit ARM instruction
11302
11303        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11304        // BEQ: cond=0x0, opcode=0xA, offset=0
11305        assert_eq!(instr & 0xF0000000, 0x00000000); // EQ condition
11306        assert_eq!(instr & 0x0F000000, 0x0A000000); // Branch opcode
11307    }
11308
11309    #[test]
11310    fn test_encode_udf_thumb2() {
11311        let encoder = ArmEncoder::new_thumb2();
11312        let op = ArmOp::Udf { imm: 0 };
11313        let code = encoder.encode(&op).unwrap();
11314        assert_eq!(code.len(), 2); // 16-bit
11315
11316        // UDF #0: 0xDE00 in little-endian
11317        assert_eq!(code, vec![0x00, 0xDE]);
11318    }
11319
11320    /// #610: the i64 rot/div/rem expansions must land the result in the
11321    /// selector-assigned rd pair and leave R0-R3 preserved (restored from the
11322    /// fixed-ABI wrapper's save area) — pre-#610 the rot expansion's own
11323    /// `POP {R4}` restored stale scratch OVER the result (rd_lo == R4) and
11324    /// the div/rem expansions ignored their register fields outright.
11325    #[test]
11326    fn test_610_i64_rot_expansion_ends_with_rd_movs_and_restore() {
11327        let encoder = ArmEncoder::new_thumb2();
11328        for op in [
11329            ArmOp::I64Rotl {
11330                rdlo: Reg::R4,
11331                rdhi: Reg::R5,
11332                rnlo: Reg::R0,
11333                rnhi: Reg::R1,
11334                shift: Reg::R2,
11335            },
11336            ArmOp::I64Rotr {
11337                rdlo: Reg::R4,
11338                rdhi: Reg::R5,
11339                rnlo: Reg::R0,
11340                rnhi: Reg::R1,
11341                shift: Reg::R2,
11342            },
11343        ] {
11344            let code = encoder.encode(&op).unwrap();
11345            assert_eq!(code.len(), 102, "register-independent size (estimator pin)");
11346            // Tail: MOV r5, r1 (0x460D); MOV r4, r0 (0x4604); POP {r0..r3}
11347            // (rd pair r4:r5 does not overlap the save area — all 4 restored).
11348            let tail: Vec<u16> = code[code.len() - 12..]
11349                .chunks(2)
11350                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11351                .collect();
11352            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11353        }
11354    }
11355
11356    /// #610: div/rem expansions honor rd and carry the divide-by-zero trap
11357    /// guard (`ORRS R12, R2, R3; BNE +0; UDF #0`) after operand marshaling.
11358    #[test]
11359    fn test_610_i64_div_rem_expansion_guard_and_rd() {
11360        let encoder = ArmEncoder::new_thumb2();
11361        let mk = |which: u8| {
11362            let (rdlo, rdhi, rnlo, rnhi, rmlo, rmhi) =
11363                (Reg::R4, Reg::R5, Reg::R0, Reg::R1, Reg::R2, Reg::R3);
11364            match which {
11365                0 => ArmOp::I64DivU {
11366                    rdlo,
11367                    rdhi,
11368                    rnlo,
11369                    rnhi,
11370                    rmlo,
11371                    rmhi,
11372                    elide_zero_guard: false,
11373                },
11374                1 => ArmOp::I64RemU {
11375                    rdlo,
11376                    rdhi,
11377                    rnlo,
11378                    rnhi,
11379                    rmlo,
11380                    rmhi,
11381                    elide_zero_guard: false,
11382                },
11383                2 => ArmOp::I64DivS {
11384                    rdlo,
11385                    rdhi,
11386                    rnlo,
11387                    rnhi,
11388                    rmlo,
11389                    rmhi,
11390                    elide_zero_guard: false,
11391                    elide_overflow_guard: false,
11392                },
11393                _ => ArmOp::I64RemS {
11394                    rdlo,
11395                    rdhi,
11396                    rnlo,
11397                    rnhi,
11398                    rmlo,
11399                    rmhi,
11400                    elide_zero_guard: false,
11401                },
11402            }
11403        };
11404        for which in 0..4u8 {
11405            let code = encoder.encode(&mk(which)).unwrap();
11406            // Zero-divisor trap guard right after the 26-byte marshal prologue.
11407            let guard: Vec<u16> = code[26..34]
11408                .chunks(2)
11409                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11410                .collect();
11411            assert_eq!(
11412                guard,
11413                vec![0xEA52, 0x0C03, 0xD100, 0xDE00],
11414                "ORRS R12,R2,R3; BNE +0; UDF #0"
11415            );
11416            // Tail: result into rd pair (r5:r4), then restore all of R0-R3.
11417            let tail: Vec<u16> = code[code.len() - 12..]
11418                .chunks(2)
11419                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11420                .collect();
11421            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11422        }
11423    }
11424
11425    /// #610: when rd overlaps R0-R3 the restore must SKIP the result
11426    /// registers (drop the saved caller word) instead of popping over them.
11427    #[test]
11428    fn test_610_i64_divu_rd_in_r0_r1_skips_restore() {
11429        let encoder = ArmEncoder::new_thumb2();
11430        let code = encoder
11431            .encode(&ArmOp::I64DivU {
11432                rdlo: Reg::R0,
11433                rdhi: Reg::R1,
11434                rnlo: Reg::R0,
11435                rnhi: Reg::R1,
11436                rmlo: Reg::R2,
11437                rmhi: Reg::R3,
11438                elide_zero_guard: false,
11439            })
11440            .unwrap();
11441        let tail: Vec<u16> = code[code.len() - 12..]
11442            .chunks(2)
11443            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11444            .collect();
11445        // MOV r1,r1 / MOV r0,r0 (no-ops, size-stable), ADD SP,#4 twice
11446        // (discard saved r0/r1 — the result lives there), POP {r2}, POP {r3}.
11447        assert_eq!(tail, vec![0x4609, 0x4600, 0xB001, 0xB001, 0xBC04, 0xBC08]);
11448    }
11449
11450    /// #610: a fully swapped rd pair (rd_lo=R1, rd_hi=R0) cannot be
11451    /// materialized by two MOVs in either order — must be a loud Err, never
11452    /// silent corruption. (Selector pairs are consecutive, so unreachable.)
11453    #[test]
11454    fn test_610_i64_swapped_rd_pair_rejected() {
11455        let encoder = ArmEncoder::new_thumb2();
11456        let result = encoder.encode(&ArmOp::I64RemU {
11457            rdlo: Reg::R1,
11458            rdhi: Reg::R0,
11459            rnlo: Reg::R2,
11460            rnhi: Reg::R3,
11461            rmlo: Reg::R4,
11462            rmhi: Reg::R5,
11463            elide_zero_guard: false,
11464        });
11465        assert!(result.is_err(), "swapped rd pair must be rejected loudly");
11466    }
11467
11468    /// #632: the I64Popcnt expansion's own scratch restore (`POP {R3,R4,R5}`)
11469    /// must not clobber the result. Pre-fix the total was materialized with
11470    /// `ADDS rd, R4, R5` BEFORE the pop, so any allocator-assigned
11471    /// rd ∈ {R3,R4,R5} received stale stack garbage. Post-fix the count is
11472    /// carried across the restore in R12 (never allocatable, never restored)
11473    /// and moved into rd only after the pop — structurally rd-independent.
11474    #[test]
11475    fn test_632_i64_popcnt_result_survives_scratch_restore() {
11476        let encoder = ArmEncoder::new_thumb2();
11477        // Every allocatable rd, including the restore set {R3,R4,R5} and R8.
11478        for rd in [
11479            Reg::R0,
11480            Reg::R2,
11481            Reg::R3,
11482            Reg::R4,
11483            Reg::R5,
11484            Reg::R6,
11485            Reg::R8,
11486        ] {
11487            let code = encoder
11488                .encode(&ArmOp::I64Popcnt {
11489                    rd,
11490                    rnlo: Reg::R6,
11491                    rnhi: Reg::R7,
11492                })
11493                .unwrap();
11494            assert_eq!(code.len(), 176, "register-independent size (estimator pin)");
11495            let hw: Vec<u16> = code
11496                .chunks(2)
11497                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11498                .collect();
11499            let pop = hw
11500                .iter()
11501                .position(|&h| h == 0xBC38)
11502                .expect("POP {R3,R4,R5} present");
11503            // Immediately before the POP: ADD.W R12, R4, R5 (the total lives
11504            // in R12, which the POP cannot touch).
11505            assert_eq!(
11506                &hw[pop - 2..pop],
11507                &[0xEB04, 0x0C05],
11508                "total must be carried in R12 across the restore"
11509            );
11510            // Immediately after the POP: MOV rd, R12.
11511            let rd_bits = match rd {
11512                Reg::R8 => 8u16,
11513                Reg::R6 => 6,
11514                Reg::R5 => 5,
11515                Reg::R4 => 4,
11516                Reg::R3 => 3,
11517                Reg::R2 => 2,
11518                _ => 0,
11519            };
11520            let expect_mov = 0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7);
11521            assert_eq!(hw[pop + 1], expect_mov, "MOV rd, R12 after the restore");
11522            // No write into rd between the PUSH and the POP (the old
11523            // pre-restore ADDS is gone).
11524            assert!(
11525                !hw[..pop].contains(&(0x1800 | (5 << 6) | (4 << 3) | rd_bits)),
11526                "no ADDS rd, R4, R5 before the restore pop"
11527            );
11528        }
11529    }
11530
11531    /// #632 audit: the entry marshal must be permutation-safe. Pre-fix
11532    /// `MOV R4, rnlo; MOV R5, rnhi` read a clobbered R4 when the operand
11533    /// pair lived at (R3, R4). Post-fix rnlo routes through R12.
11534    #[test]
11535    fn test_632_i64_popcnt_marshal_pair_at_r3_r4() {
11536        let encoder = ArmEncoder::new_thumb2();
11537        let code = encoder
11538            .encode(&ArmOp::I64Popcnt {
11539                rd: Reg::R0,
11540                rnlo: Reg::R3,
11541                rnhi: Reg::R4,
11542            })
11543            .unwrap();
11544        let hw: Vec<u16> = code
11545            .chunks(2)
11546            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11547            .collect();
11548        // PUSH {R3,R4,R5}; MOV R12, R3; MOV R5, R4 (rnhi read BEFORE any
11549        // write to R4); MOV R4, R12.
11550        assert_eq!(hw[0], 0xB438);
11551        assert_eq!(hw[1], 0x4600 | (1 << 7) | (3 << 3) | 4, "MOV R12, rnlo");
11552        assert_eq!(hw[2], 0x4600 | (4 << 3) | 5, "MOV R5, rnhi");
11553        assert_eq!(hw[3], 0x4664, "MOV R4, R12");
11554    }
11555
11556    /// #632: A32 twin — same structural fix on the ARM-mode path
11557    /// (`--target cortex-r5`): total carried in R12 across the restore.
11558    #[test]
11559    fn test_632_a32_i64_popcnt_result_survives_scratch_restore() {
11560        let encoder = ArmEncoder::new_arm32();
11561        for rd in [Reg::R0, Reg::R3, Reg::R4, Reg::R5, Reg::R8] {
11562            let code = encoder
11563                .encode(&ArmOp::I64Popcnt {
11564                    rd,
11565                    rnlo: Reg::R6,
11566                    rnhi: Reg::R7,
11567                })
11568                .unwrap();
11569            let words: Vec<u32> = code
11570                .chunks(4)
11571                .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11572                .collect();
11573            let pop = words
11574                .iter()
11575                .position(|&w| w == 0xE8BD_0038)
11576                .expect("POP {R3,R4,R5} present");
11577            assert_eq!(words[pop - 1], 0xE084_C005, "ADD R12, R4, R5 before POP");
11578            let rd_bits = match rd {
11579                Reg::R8 => 8u32,
11580                Reg::R5 => 5,
11581                Reg::R4 => 4,
11582                Reg::R3 => 3,
11583                _ => 0,
11584            };
11585            assert_eq!(
11586                words[pop + 1],
11587                0xE1A0_0000 | (rd_bits << 12) | 12,
11588                "MOV rd, R12 after the restore"
11589            );
11590        }
11591    }
11592
11593    /// #633: I64DivS must carry the INT64_MIN/-1 overflow guard (mirroring
11594    /// the i32 path) right after the zero-divisor guard — dividend in R0:R1,
11595    /// divisor in R2:R3 on the #610/#613 fixed-ABI wrapper path.
11596    #[test]
11597    fn test_633_i64_divs_overflow_guard_emitted() {
11598        let encoder = ArmEncoder::new_thumb2();
11599        let code = encoder
11600            .encode(&ArmOp::I64DivS {
11601                rdlo: Reg::R4,
11602                rdhi: Reg::R5,
11603                rnlo: Reg::R0,
11604                rnhi: Reg::R1,
11605                rmlo: Reg::R2,
11606                rmhi: Reg::R3,
11607                elide_zero_guard: false,
11608                elide_overflow_guard: false,
11609            })
11610            .unwrap();
11611        // 26-byte marshal + 8-byte zero-trap, then the 22-byte overflow guard.
11612        let guard: Vec<u16> = code[34..56]
11613            .chunks(2)
11614            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11615            .collect();
11616        assert_eq!(
11617            guard,
11618            vec![
11619                0xEA02, 0x0C03, // AND.W R12, R2, R3
11620                0xF11C, 0x0F01, // CMN.W R12, #1
11621                0xD105, // BNE .no_trap
11622                0x2800, // CMP R0, #0
11623                0xD103, // BNE .no_trap
11624                0xF1B1, 0x4F00, // CMP.W R1, #0x80000000
11625                0xD100, // BNE .no_trap
11626                0xDE00, // UDF #0 — signed-division overflow
11627            ],
11628            "INT64_MIN/-1 overflow guard after the zero-divisor guard"
11629        );
11630    }
11631
11632    /// #633 fix-guard twin: I64RemS must NOT carry the overflow guard —
11633    /// rem_s(INT64_MIN, -1) is defined as 0 and must not trap. Exactly one
11634    /// UDF (the zero-divisor trap) in the whole expansion.
11635    #[test]
11636    fn test_633_i64_rems_has_no_overflow_guard() {
11637        let encoder = ArmEncoder::new_thumb2();
11638        for (is_rem_s, op) in [
11639            (
11640                true,
11641                ArmOp::I64RemS {
11642                    rdlo: Reg::R4,
11643                    rdhi: Reg::R5,
11644                    rnlo: Reg::R0,
11645                    rnhi: Reg::R1,
11646                    rmlo: Reg::R2,
11647                    rmhi: Reg::R3,
11648                    elide_zero_guard: false,
11649                },
11650            ),
11651            (
11652                false,
11653                ArmOp::I64DivS {
11654                    rdlo: Reg::R4,
11655                    rdhi: Reg::R5,
11656                    rnlo: Reg::R0,
11657                    rnhi: Reg::R1,
11658                    rmlo: Reg::R2,
11659                    rmhi: Reg::R3,
11660                    elide_zero_guard: false,
11661                    elide_overflow_guard: false,
11662                },
11663            ),
11664        ] {
11665            let code = encoder.encode(&op).unwrap();
11666            let udfs = code
11667                .chunks(2)
11668                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11669                .count();
11670            let want = if is_rem_s { 1 } else { 2 };
11671            assert_eq!(
11672                udfs, want,
11673                "rem_s: zero-trap only; div_s: zero-trap + overflow trap"
11674            );
11675        }
11676    }
11677
11678    /// #494 phase 2b: `elide_zero_guard` drops EXACTLY the 8-byte fused
11679    /// zero-trap (`ORRS.W R12,R2,R3; BNE; UDF #0`) and nothing else — the
11680    /// rest of the expansion is byte-identical (splice check).
11681    #[test]
11682    fn test_494_i64_zero_guard_elision_is_exact_splice() {
11683        let encoder = ArmEncoder::new_thumb2();
11684        let mk = |elide_zero_guard: bool| {
11685            encoder
11686                .encode(&ArmOp::I64DivU {
11687                    rdlo: Reg::R4,
11688                    rdhi: Reg::R5,
11689                    rnlo: Reg::R0,
11690                    rnhi: Reg::R1,
11691                    rmlo: Reg::R2,
11692                    rmhi: Reg::R3,
11693                    elide_zero_guard,
11694                })
11695                .unwrap()
11696        };
11697        let full = mk(false);
11698        let elided = mk(true);
11699        assert_eq!(full.len(), elided.len() + 8, "zero guard is 8 bytes");
11700        // Marshal prologue (26 B) unchanged, guard (8 B) gone, tail identical.
11701        assert_eq!(&full[..26], &elided[..26]);
11702        assert_eq!(
11703            &full[26..34],
11704            &[0x52, 0xEA, 0x03, 0x0C, 0x00, 0xD1, 0x00, 0xDE],
11705            "the spliced-out bytes are exactly ORRS.W; BNE; UDF #0"
11706        );
11707        assert_eq!(&full[34..], &elided[26..]);
11708    }
11709
11710    /// #494 phase 2b two-guard distinction (the #633/#634 synergy): a
11711    /// divisor-nonzero fact elides ONLY the zero guard — the INT64_MIN/-1
11712    /// OVERFLOW guard is a separate obligation and must survive
11713    /// `elide_zero_guard: true`. Pinned on div_s in all flag states.
11714    #[test]
11715    fn test_494_i64_divs_overflow_guard_retained_when_only_zero_elided() {
11716        let encoder = ArmEncoder::new_thumb2();
11717        let mk = |zero: bool, ovf: bool| {
11718            encoder
11719                .encode(&ArmOp::I64DivS {
11720                    rdlo: Reg::R4,
11721                    rdhi: Reg::R5,
11722                    rnlo: Reg::R0,
11723                    rnhi: Reg::R1,
11724                    rmlo: Reg::R2,
11725                    rmhi: Reg::R3,
11726                    elide_zero_guard: zero,
11727                    elide_overflow_guard: ovf,
11728                })
11729                .unwrap()
11730        };
11731        let udf_count = |code: &[u8]| {
11732            code.chunks(2)
11733                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11734                .count()
11735        };
11736        let full = mk(false, false);
11737        let zero_only = mk(true, false);
11738        let both = mk(true, true);
11739        assert_eq!(udf_count(&full), 2, "baseline: zero trap + overflow trap");
11740        assert_eq!(
11741            udf_count(&zero_only),
11742            1,
11743            "divisor-nonzero elides the zero trap ONLY — the #633 overflow \
11744             guard must be retained"
11745        );
11746        // The retained guard is the 22-byte overflow sequence, now right
11747        // after the 26-byte marshal prologue.
11748        let guard: Vec<u16> = zero_only[26..48]
11749            .chunks(2)
11750            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11751            .collect();
11752        assert_eq!(
11753            guard,
11754            vec![
11755                0xEA02, 0x0C03, 0xF11C, 0x0F01, 0xD105, 0x2800, 0xD103, 0xF1B1, 0x4F00, 0xD100,
11756                0xDE00,
11757            ],
11758            "the surviving guard is the INT64_MIN/-1 overflow trap"
11759        );
11760        assert_eq!(full.len(), zero_only.len() + 8);
11761        assert_eq!(zero_only.len(), both.len() + 22);
11762        assert_eq!(udf_count(&both), 0, "both obligations discharged ⇒ no UDF");
11763    }
11764
11765    /// #494 phase 2b A32 twin: zero-guard elision is an exact 12-byte splice
11766    /// and the A32 overflow guard survives a zero-only elision.
11767    #[test]
11768    fn test_494_a32_i64_guard_elision() {
11769        let encoder = ArmEncoder::new_arm32();
11770        let mk = |zero: bool, ovf: bool| {
11771            encoder
11772                .encode(&ArmOp::I64DivS {
11773                    rdlo: Reg::R4,
11774                    rdhi: Reg::R5,
11775                    rnlo: Reg::R0,
11776                    rnhi: Reg::R1,
11777                    rmlo: Reg::R2,
11778                    rmhi: Reg::R3,
11779                    elide_zero_guard: zero,
11780                    elide_overflow_guard: ovf,
11781                })
11782                .unwrap()
11783        };
11784        let full = mk(false, false);
11785        let zero_only = mk(true, false);
11786        let both = mk(true, true);
11787        // A32 zero guard = 3 words (ORRS/BNE/UDF), overflow guard = 6 words.
11788        assert_eq!(full.len(), zero_only.len() + 12);
11789        assert_eq!(zero_only.len(), both.len() + 24);
11790        let udf_count = |code: &[u8]| {
11791            code.chunks(4)
11792                .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
11793                .count()
11794        };
11795        assert_eq!(udf_count(&full), 2);
11796        assert_eq!(
11797            udf_count(&zero_only),
11798            1,
11799            "A32: overflow guard retained under zero-only elision"
11800        );
11801        assert_eq!(udf_count(&both), 0);
11802    }
11803
11804    /// #633: A32 twin — the conditional-execution overflow guard on the
11805    /// ARM-mode I64DivS, and its absence from I64RemS.
11806    #[test]
11807    fn test_633_a32_i64_divs_overflow_guard() {
11808        let encoder = ArmEncoder::new_arm32();
11809        let mk_divs = ArmOp::I64DivS {
11810            rdlo: Reg::R4,
11811            rdhi: Reg::R5,
11812            rnlo: Reg::R0,
11813            rnhi: Reg::R1,
11814            rmlo: Reg::R2,
11815            rmhi: Reg::R3,
11816            elide_zero_guard: false,
11817            elide_overflow_guard: false,
11818        };
11819        let code = encoder.encode(&mk_divs).unwrap();
11820        let words: Vec<u32> = code
11821            .chunks(4)
11822            .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11823            .collect();
11824        let guard = [
11825            0xE002_C003u32, // AND   R12, R2, R3
11826            0xE37C_0001,    // CMN   R12, #1
11827            0x0350_0000,    // CMPEQ R0, #0
11828            0x0351_0102,    // CMPEQ R1, #0x80000000
11829            0x1A00_0000,    // BNE +1 insn
11830            0xE7F0_00F0,    // UDF #0
11831        ];
11832        assert!(
11833            words.windows(6).any(|w| w == guard),
11834            "A32 I64DivS carries the INT64_MIN/-1 overflow guard"
11835        );
11836        let rems = encoder
11837            .encode(&ArmOp::I64RemS {
11838                rdlo: Reg::R4,
11839                rdhi: Reg::R5,
11840                rnlo: Reg::R0,
11841                rnhi: Reg::R1,
11842                rmlo: Reg::R2,
11843                rmhi: Reg::R3,
11844                elide_zero_guard: false,
11845            })
11846            .unwrap();
11847        let rems_udfs = rems
11848            .chunks(4)
11849            .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
11850            .count();
11851        assert_eq!(rems_udfs, 1, "A32 I64RemS keeps only the zero-divisor trap");
11852    }
11853
11854    #[test]
11855    fn test_encode_nop_thumb2() {
11856        let encoder = ArmEncoder::new_thumb2();
11857        let op = ArmOp::Nop;
11858        let code = encoder.encode(&op).unwrap();
11859        assert_eq!(code.len(), 2); // 16-bit
11860
11861        // NOP: 0xBF00 in little-endian
11862        assert_eq!(code, vec![0x00, 0xBF]);
11863    }
11864
11865    // =========================================================================
11866    // i64 Thumb-2 encoding tests
11867    // =========================================================================
11868
11869    #[test]
11870    fn test_encode_i64_add_thumb2() {
11871        let encoder = ArmEncoder::new_thumb2();
11872        let op = ArmOp::I64Add {
11873            rdlo: Reg::R0,
11874            rdhi: Reg::R1,
11875            rnlo: Reg::R0,
11876            rnhi: Reg::R1,
11877            rmlo: Reg::R2,
11878            rmhi: Reg::R3,
11879        };
11880        let code = encoder.encode(&op).unwrap();
11881        // Should emit ADDS (2 bytes) + ADC.W (4 bytes) = 6 bytes
11882        assert_eq!(code.len(), 6, "I64Add should be 6 bytes (ADDS + ADC.W)");
11883    }
11884
11885    #[test]
11886    fn test_encode_i64_sub_thumb2() {
11887        let encoder = ArmEncoder::new_thumb2();
11888        let op = ArmOp::I64Sub {
11889            rdlo: Reg::R0,
11890            rdhi: Reg::R1,
11891            rnlo: Reg::R0,
11892            rnhi: Reg::R1,
11893            rmlo: Reg::R2,
11894            rmhi: Reg::R3,
11895        };
11896        let code = encoder.encode(&op).unwrap();
11897        // Should emit SUBS (2 bytes) + SBC.W (4 bytes) = 6 bytes
11898        assert_eq!(code.len(), 6, "I64Sub should be 6 bytes (SUBS + SBC.W)");
11899    }
11900
11901    #[test]
11902    fn test_encode_i64_and_thumb2() {
11903        let encoder = ArmEncoder::new_thumb2();
11904        let op = ArmOp::I64And {
11905            rdlo: Reg::R0,
11906            rdhi: Reg::R1,
11907            rnlo: Reg::R0,
11908            rnhi: Reg::R1,
11909            rmlo: Reg::R2,
11910            rmhi: Reg::R3,
11911        };
11912        let code = encoder.encode(&op).unwrap();
11913        // AND.W (4 bytes) + AND.W (4 bytes) = 8 bytes
11914        assert!(code.len() >= 4, "I64And should emit at least 4 bytes");
11915    }
11916
11917    #[test]
11918    fn test_encode_i64_or_thumb2() {
11919        let encoder = ArmEncoder::new_thumb2();
11920        let op = ArmOp::I64Or {
11921            rdlo: Reg::R0,
11922            rdhi: Reg::R1,
11923            rnlo: Reg::R0,
11924            rnhi: Reg::R1,
11925            rmlo: Reg::R2,
11926            rmhi: Reg::R3,
11927        };
11928        let code = encoder.encode(&op).unwrap();
11929        assert!(code.len() >= 4, "I64Or should emit at least 4 bytes");
11930    }
11931
11932    #[test]
11933    fn test_encode_i64_xor_thumb2() {
11934        let encoder = ArmEncoder::new_thumb2();
11935        let op = ArmOp::I64Xor {
11936            rdlo: Reg::R0,
11937            rdhi: Reg::R1,
11938            rnlo: Reg::R0,
11939            rnhi: Reg::R1,
11940            rmlo: Reg::R2,
11941            rmhi: Reg::R3,
11942        };
11943        let code = encoder.encode(&op).unwrap();
11944        assert!(code.len() >= 4, "I64Xor should emit at least 4 bytes");
11945    }
11946
11947    #[test]
11948    fn test_encode_i64_const_small_thumb2() {
11949        let encoder = ArmEncoder::new_thumb2();
11950        // Small constant: only needs MOVW for each half
11951        let op = ArmOp::I64Const {
11952            rdlo: Reg::R0,
11953            rdhi: Reg::R1,
11954            value: 42,
11955        };
11956        let code = encoder.encode(&op).unwrap();
11957        // MOVW R0, #42 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes minimum
11958        assert!(code.len() >= 8, "I64Const should emit at least 8 bytes");
11959    }
11960
11961    #[test]
11962    fn test_encode_i64_const_large_thumb2() {
11963        let encoder = ArmEncoder::new_thumb2();
11964        // Large constant: needs MOVW+MOVT for each half
11965        let op = ArmOp::I64Const {
11966            rdlo: Reg::R0,
11967            rdhi: Reg::R1,
11968            value: 0x1234_5678_9ABC_DEF0_u64 as i64,
11969        };
11970        let code = encoder.encode(&op).unwrap();
11971        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
11972        assert_eq!(
11973            code.len(),
11974            16,
11975            "I64Const with large value should be 16 bytes"
11976        );
11977    }
11978
11979    #[test]
11980    fn test_encode_i64_extend_i32_s_thumb2() {
11981        let encoder = ArmEncoder::new_thumb2();
11982        let op = ArmOp::I64ExtendI32S {
11983            rdlo: Reg::R0,
11984            rdhi: Reg::R1,
11985            rn: Reg::R0,
11986        };
11987        let code = encoder.encode(&op).unwrap();
11988        // When rdlo == rn, only ASR (4 bytes) is emitted
11989        assert_eq!(
11990            code.len(),
11991            4,
11992            "I64ExtendI32S (same reg) should be 4 bytes (ASR only)"
11993        );
11994    }
11995
11996    #[test]
11997    fn test_encode_i64_extend_i32_s_diff_reg_thumb2() {
11998        let encoder = ArmEncoder::new_thumb2();
11999        let op = ArmOp::I64ExtendI32S {
12000            rdlo: Reg::R0,
12001            rdhi: Reg::R1,
12002            rn: Reg::R2,
12003        };
12004        let code = encoder.encode(&op).unwrap();
12005        // MOV rdlo, rn (2 bytes for low regs) + ASR rdhi, rdlo, #31 (4 bytes) = 6 bytes
12006        assert!(
12007            code.len() >= 6,
12008            "I64ExtendI32S (diff reg) should be at least 6 bytes"
12009        );
12010    }
12011
12012    #[test]
12013    fn test_encode_i64_extend_i32_u_thumb2() {
12014        let encoder = ArmEncoder::new_thumb2();
12015        let op = ArmOp::I64ExtendI32U {
12016            rdlo: Reg::R0,
12017            rdhi: Reg::R1,
12018            rn: Reg::R0,
12019        };
12020        let code = encoder.encode(&op).unwrap();
12021        // When rdlo == rn, only MOV rdhi, #0 (2 bytes) is emitted
12022        assert_eq!(
12023            code.len(),
12024            2,
12025            "I64ExtendI32U (same reg) should be 2 bytes (MOV #0 only)"
12026        );
12027    }
12028
12029    #[test]
12030    fn test_encode_i32_wrap_i64_nop_thumb2() {
12031        let encoder = ArmEncoder::new_thumb2();
12032        // When rd == rnlo, should be a NOP
12033        let op = ArmOp::I32WrapI64 {
12034            rd: Reg::R0,
12035            rnlo: Reg::R0,
12036        };
12037        let code = encoder.encode(&op).unwrap();
12038        assert_eq!(code.len(), 2, "I32WrapI64 same reg should be NOP (2 bytes)");
12039        assert_eq!(code, vec![0x00, 0xBF]); // NOP
12040    }
12041
12042    #[test]
12043    fn test_encode_i32_wrap_i64_diff_reg_thumb2() {
12044        let encoder = ArmEncoder::new_thumb2();
12045        let op = ArmOp::I32WrapI64 {
12046            rd: Reg::R2,
12047            rnlo: Reg::R0,
12048        };
12049        let code = encoder.encode(&op).unwrap();
12050        // MOV R2, R0 (2 or 4 bytes)
12051        assert!(
12052            code.len() >= 2,
12053            "I32WrapI64 diff reg should emit at least 2 bytes"
12054        );
12055    }
12056
12057    #[test]
12058    fn test_encode_i64_eqz_thumb2() {
12059        let encoder = ArmEncoder::new_thumb2();
12060        let op = ArmOp::I64Eqz {
12061            rd: Reg::R0,
12062            rnlo: Reg::R0,
12063            rnhi: Reg::R1,
12064        };
12065        let code = encoder.encode(&op).unwrap();
12066        // Delegates to I64SetCondZ which is already encoded
12067        assert!(
12068            code.len() >= 6,
12069            "I64Eqz should emit at least 6 bytes for ORR+ITE+MOV+MOV"
12070        );
12071    }
12072
12073    #[test]
12074    fn test_encode_i64_eq_thumb2() {
12075        let encoder = ArmEncoder::new_thumb2();
12076        let op = ArmOp::I64Eq {
12077            rd: Reg::R0,
12078            rnlo: Reg::R0,
12079            rnhi: Reg::R1,
12080            rmlo: Reg::R2,
12081            rmhi: Reg::R3,
12082        };
12083        let code = encoder.encode(&op).unwrap();
12084        // Delegates to I64SetCond EQ: CMP lo + IT EQ + CMPEQ hi + ITE EQ + MOV 1 + MOV 0
12085        assert!(code.len() >= 10, "I64Eq should emit at least 10 bytes");
12086    }
12087
12088    #[test]
12089    fn test_encode_i64_ldr_thumb2() {
12090        let encoder = ArmEncoder::new_thumb2();
12091        let op = ArmOp::I64Ldr {
12092            rdlo: Reg::R0,
12093            rdhi: Reg::R1,
12094            addr: MemAddr::imm(Reg::SP, 0),
12095        };
12096        let code = encoder.encode(&op).unwrap();
12097        // Two LDR instructions (lo at offset, hi at offset+4)
12098        assert!(code.len() >= 4, "I64Ldr should emit at least 4 bytes");
12099    }
12100
12101    #[test]
12102    fn test_372_i64_ldr_indexed_materializes_address() {
12103        // #372: a memory i64.load carries an index register (R11 + addr + off).
12104        // The encoder must materialize `ip = base + index` (ADD.W) and load via
12105        // `[ip,#off]` — NOT drop the index. A frame (non-indexed) i64.load must
12106        // stay byte-identical (plain `[base,#off]`, no ADD).
12107        let encoder = ArmEncoder::new_thumb2();
12108        let indexed = encoder
12109            .encode(&ArmOp::I64Ldr {
12110                rdlo: Reg::R0,
12111                rdhi: Reg::R1,
12112                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
12113            })
12114            .unwrap();
12115        // ADD.W ip, fp, r0 = eb0b 0c00 (byte-verified vs arm-none-eabi-as).
12116        assert_eq!(
12117            &indexed[0..4],
12118            &[0x0b, 0xeb, 0x00, 0x0c],
12119            "indexed I64Ldr must start with ADD.W ip, base, index"
12120        );
12121        let frame = encoder
12122            .encode(&ArmOp::I64Ldr {
12123                rdlo: Reg::R0,
12124                rdhi: Reg::R1,
12125                addr: MemAddr::imm(Reg::SP, 8),
12126            })
12127            .unwrap();
12128        // No index -> no ADD.W prefix (byte-identical frame access).
12129        assert_ne!(
12130            &frame[0..2],
12131            &[0x0b, 0xeb],
12132            "frame (non-indexed) I64Ldr must NOT emit an ADD.W"
12133        );
12134    }
12135
12136    #[test]
12137    fn test_382_i64_ldst_large_offset_materializes_not_skips() {
12138        // #382: an indexed i64.load/store whose static offset > 0xFFF must
12139        // MATERIALIZE the offset into the base — NOT return Err (skip the fn).
12140        // Sequence for reg_imm(R11, R0, 5000): MOVW ip,#5000 ; ADD ip,r0,ip ;
12141        // ADD ip,ip,fp ; LDR/STR halves at [ip,#0] / [ip,#4]. Byte-verified tail
12142        // vs arm-none-eabi-as.
12143        let encoder = ArmEncoder::new_thumb2();
12144        // 0x1388 > 0xFFF (MemAddr is not Copy, so build it per use).
12145
12146        let ld = encoder
12147            .encode(&ArmOp::I64Ldr {
12148                rdlo: Reg::R0,
12149                rdhi: Reg::R1,
12150                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
12151            })
12152            .expect("large-offset i64.load must lower, not skip");
12153        // MOVW ip,#0x1388 (4) + ADD ip,r0,ip (4) + ADD ip,ip,fp (4) + 2 LDR (8).
12154        assert_eq!(ld.len(), 20, "expected MOVW + 2×ADD + 2×LDR");
12155        // Must NOT be the small-offset `ADD.W ip, fp, r0` (0x0b 0xeb) prefix —
12156        // that path can only reach imm12 offsets.
12157        assert_ne!(
12158            &ld[0..2],
12159            &[0x0b, 0xeb],
12160            "must materialize the large offset"
12161        );
12162        // Effective base built in ip, then halves at [ip,#0] / [ip,#4].
12163        assert_eq!(
12164            &ld[4..20],
12165            &[
12166                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
12167                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
12168                0xdc, 0xf8, 0x00, 0x00, // LDR.W r0, [ip, #0]
12169                0xdc, 0xf8, 0x04, 0x10, // LDR.W r1, [ip, #4]
12170            ],
12171            "large-offset i64.load must fold offset into ip and access [ip,#0]/[ip,#4]"
12172        );
12173
12174        // Store: same base materialization, STR halves.
12175        let st = encoder
12176            .encode(&ArmOp::I64Str {
12177                rdlo: Reg::R2,
12178                rdhi: Reg::R3,
12179                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
12180            })
12181            .expect("large-offset i64.store must lower, not skip");
12182        assert_eq!(st.len(), 20);
12183        assert_eq!(
12184            &st[4..20],
12185            &[
12186                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
12187                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
12188                0xcc, 0xf8, 0x00, 0x20, // STR.W r2, [ip, #0]
12189                0xcc, 0xf8, 0x04, 0x30, // STR.W r3, [ip, #4]
12190            ],
12191            "large-offset i64.store must fold offset into ip and access [ip,#0]/[ip,#4]"
12192        );
12193
12194        // Small-offset (imm12) indexed access stays byte-identical (#372): the
12195        // effective base is a single `ADD.W ip, fp, r0` and the halves keep the
12196        // folded immediates — NO extra MOVW/ADD.
12197        let small = encoder
12198            .encode(&ArmOp::I64Ldr {
12199                rdlo: Reg::R0,
12200                rdhi: Reg::R1,
12201                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 8),
12202            })
12203            .unwrap();
12204        assert_eq!(
12205            &small[0..4],
12206            &[0x0b, 0xeb, 0x00, 0x0c],
12207            "small-offset indexed i64 must keep the single ADD.W ip, fp, r0"
12208        );
12209        assert_eq!(small.len(), 12, "ADD.W + 2×LDR.W (offset folded in imm12)");
12210    }
12211
12212    #[test]
12213    fn test_encode_i64_str_thumb2() {
12214        let encoder = ArmEncoder::new_thumb2();
12215        let op = ArmOp::I64Str {
12216            rdlo: Reg::R0,
12217            rdhi: Reg::R1,
12218            addr: MemAddr::imm(Reg::SP, 0),
12219        };
12220        let code = encoder.encode(&op).unwrap();
12221        // Two STR instructions (lo at offset, hi at offset+4)
12222        assert!(code.len() >= 4, "I64Str should emit at least 4 bytes");
12223    }
12224
12225    #[test]
12226    fn test_encode_i64_all_comparisons_thumb2() {
12227        let encoder = ArmEncoder::new_thumb2();
12228
12229        let ops = vec![
12230            ArmOp::I64Ne {
12231                rd: Reg::R0,
12232                rnlo: Reg::R0,
12233                rnhi: Reg::R1,
12234                rmlo: Reg::R2,
12235                rmhi: Reg::R3,
12236            },
12237            ArmOp::I64LtS {
12238                rd: Reg::R0,
12239                rnlo: Reg::R0,
12240                rnhi: Reg::R1,
12241                rmlo: Reg::R2,
12242                rmhi: Reg::R3,
12243            },
12244            ArmOp::I64LtU {
12245                rd: Reg::R0,
12246                rnlo: Reg::R0,
12247                rnhi: Reg::R1,
12248                rmlo: Reg::R2,
12249                rmhi: Reg::R3,
12250            },
12251            ArmOp::I64LeS {
12252                rd: Reg::R0,
12253                rnlo: Reg::R0,
12254                rnhi: Reg::R1,
12255                rmlo: Reg::R2,
12256                rmhi: Reg::R3,
12257            },
12258            ArmOp::I64LeU {
12259                rd: Reg::R0,
12260                rnlo: Reg::R0,
12261                rnhi: Reg::R1,
12262                rmlo: Reg::R2,
12263                rmhi: Reg::R3,
12264            },
12265            ArmOp::I64GtS {
12266                rd: Reg::R0,
12267                rnlo: Reg::R0,
12268                rnhi: Reg::R1,
12269                rmlo: Reg::R2,
12270                rmhi: Reg::R3,
12271            },
12272            ArmOp::I64GtU {
12273                rd: Reg::R0,
12274                rnlo: Reg::R0,
12275                rnhi: Reg::R1,
12276                rmlo: Reg::R2,
12277                rmhi: Reg::R3,
12278            },
12279            ArmOp::I64GeS {
12280                rd: Reg::R0,
12281                rnlo: Reg::R0,
12282                rnhi: Reg::R1,
12283                rmlo: Reg::R2,
12284                rmhi: Reg::R3,
12285            },
12286            ArmOp::I64GeU {
12287                rd: Reg::R0,
12288                rnlo: Reg::R0,
12289                rnhi: Reg::R1,
12290                rmlo: Reg::R2,
12291                rmhi: Reg::R3,
12292            },
12293        ];
12294
12295        for op in &ops {
12296            let code = encoder.encode(op).unwrap();
12297            assert!(
12298                code.len() >= 8,
12299                "i64 comparison {:?} should emit at least 8 bytes, got {}",
12300                op,
12301                code.len()
12302            );
12303        }
12304    }
12305
12306    #[test]
12307    fn test_encode_i64_const_zero_thumb2() {
12308        let encoder = ArmEncoder::new_thumb2();
12309        let op = ArmOp::I64Const {
12310            rdlo: Reg::R0,
12311            rdhi: Reg::R1,
12312            value: 0,
12313        };
12314        let code = encoder.encode(&op).unwrap();
12315        // MOVW R0, #0 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes
12316        assert_eq!(code.len(), 8, "I64Const(0) should be 8 bytes");
12317    }
12318
12319    #[test]
12320    fn test_encode_i64_const_negative_one_thumb2() {
12321        let encoder = ArmEncoder::new_thumb2();
12322        let op = ArmOp::I64Const {
12323            rdlo: Reg::R0,
12324            rdhi: Reg::R1,
12325            value: -1, // 0xFFFF_FFFF_FFFF_FFFF
12326        };
12327        let code = encoder.encode(&op).unwrap();
12328        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
12329        assert_eq!(code.len(), 16, "I64Const(-1) should be 16 bytes");
12330    }
12331
12332    // =========================================================================
12333    // Sub-word load/store encoding tests
12334    // =========================================================================
12335
12336    #[test]
12337    fn test_encode_ldrb_arm32() {
12338        let encoder = ArmEncoder::new_arm32();
12339        let op = ArmOp::Ldrb {
12340            rd: Reg::R0,
12341            addr: MemAddr::imm(Reg::R1, 4),
12342        };
12343        let code = encoder.encode(&op).unwrap();
12344        assert_eq!(code.len(), 4, "ARM32 LDRB should be 4 bytes");
12345        // LDRB R0, [R1, #4] = 0xE5D10004
12346        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12347        assert_eq!(encoded, 0xE5D10004, "Should encode LDRB R0, [R1, #4]");
12348    }
12349
12350    #[test]
12351    fn test_encode_strb_arm32() {
12352        let encoder = ArmEncoder::new_arm32();
12353        let op = ArmOp::Strb {
12354            rd: Reg::R0,
12355            addr: MemAddr::imm(Reg::R1, 0),
12356        };
12357        let code = encoder.encode(&op).unwrap();
12358        assert_eq!(code.len(), 4, "ARM32 STRB should be 4 bytes");
12359        // STRB R0, [R1, #0] = 0xE5C10000
12360        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12361        assert_eq!(encoded, 0xE5C10000, "Should encode STRB R0, [R1, #0]");
12362    }
12363
12364    #[test]
12365    fn test_encode_ldrh_arm32() {
12366        let encoder = ArmEncoder::new_arm32();
12367        let op = ArmOp::Ldrh {
12368            rd: Reg::R0,
12369            addr: MemAddr::imm(Reg::R1, 2),
12370        };
12371        let code = encoder.encode(&op).unwrap();
12372        assert_eq!(code.len(), 4, "ARM32 LDRH should be 4 bytes");
12373    }
12374
12375    #[test]
12376    fn test_encode_strh_arm32() {
12377        let encoder = ArmEncoder::new_arm32();
12378        let op = ArmOp::Strh {
12379            rd: Reg::R0,
12380            addr: MemAddr::imm(Reg::R1, 0),
12381        };
12382        let code = encoder.encode(&op).unwrap();
12383        assert_eq!(code.len(), 4, "ARM32 STRH should be 4 bytes");
12384    }
12385
12386    #[test]
12387    fn test_encode_ldrsb_arm32() {
12388        let encoder = ArmEncoder::new_arm32();
12389        let op = ArmOp::Ldrsb {
12390            rd: Reg::R0,
12391            addr: MemAddr::imm(Reg::R1, 0),
12392        };
12393        let code = encoder.encode(&op).unwrap();
12394        assert_eq!(code.len(), 4, "ARM32 LDRSB should be 4 bytes");
12395    }
12396
12397    #[test]
12398    fn test_encode_ldrsh_arm32() {
12399        let encoder = ArmEncoder::new_arm32();
12400        let op = ArmOp::Ldrsh {
12401            rd: Reg::R0,
12402            addr: MemAddr::imm(Reg::R1, 0),
12403        };
12404        let code = encoder.encode(&op).unwrap();
12405        assert_eq!(code.len(), 4, "ARM32 LDRSH should be 4 bytes");
12406    }
12407
12408    #[test]
12409    fn test_encode_ldrb_thumb2_16bit() {
12410        let encoder = ArmEncoder::new_thumb2();
12411        let op = ArmOp::Ldrb {
12412            rd: Reg::R0,
12413            addr: MemAddr::imm(Reg::R1, 4),
12414        };
12415        let code = encoder.encode(&op).unwrap();
12416        // Low registers + small offset -> 16-bit encoding
12417        assert_eq!(
12418            code.len(),
12419            2,
12420            "Thumb-2 LDRB with small offset should be 16-bit"
12421        );
12422    }
12423
12424    #[test]
12425    fn test_encode_ldrb_thumb2_32bit() {
12426        let encoder = ArmEncoder::new_thumb2();
12427        let op = ArmOp::Ldrb {
12428            rd: Reg::R0,
12429            addr: MemAddr::imm(Reg::R1, 100), // offset > 31 needs 32-bit
12430        };
12431        let code = encoder.encode(&op).unwrap();
12432        assert_eq!(
12433            code.len(),
12434            4,
12435            "Thumb-2 LDRB with large offset should be 32-bit"
12436        );
12437    }
12438
12439    #[test]
12440    fn test_encode_strb_thumb2_16bit() {
12441        let encoder = ArmEncoder::new_thumb2();
12442        let op = ArmOp::Strb {
12443            rd: Reg::R0,
12444            addr: MemAddr::imm(Reg::R1, 10),
12445        };
12446        let code = encoder.encode(&op).unwrap();
12447        assert_eq!(
12448            code.len(),
12449            2,
12450            "Thumb-2 STRB with small offset should be 16-bit"
12451        );
12452    }
12453
12454    #[test]
12455    fn test_encode_ldrh_thumb2_16bit() {
12456        let encoder = ArmEncoder::new_thumb2();
12457        let op = ArmOp::Ldrh {
12458            rd: Reg::R0,
12459            addr: MemAddr::imm(Reg::R1, 4), // offset aligned to 2, <= 62
12460        };
12461        let code = encoder.encode(&op).unwrap();
12462        assert_eq!(
12463            code.len(),
12464            2,
12465            "Thumb-2 LDRH with small aligned offset should be 16-bit"
12466        );
12467    }
12468
12469    #[test]
12470    fn test_encode_strh_thumb2_16bit() {
12471        let encoder = ArmEncoder::new_thumb2();
12472        let op = ArmOp::Strh {
12473            rd: Reg::R0,
12474            addr: MemAddr::imm(Reg::R1, 4),
12475        };
12476        let code = encoder.encode(&op).unwrap();
12477        assert_eq!(
12478            code.len(),
12479            2,
12480            "Thumb-2 STRH with small aligned offset should be 16-bit"
12481        );
12482    }
12483
12484    #[test]
12485    fn test_encode_ldrsb_thumb2() {
12486        let encoder = ArmEncoder::new_thumb2();
12487        let op = ArmOp::Ldrsb {
12488            rd: Reg::R0,
12489            addr: MemAddr::imm(Reg::R1, 0),
12490        };
12491        let code = encoder.encode(&op).unwrap();
12492        // LDRSB has no 16-bit immediate form, always 32-bit
12493        assert_eq!(code.len(), 4, "Thumb-2 LDRSB should be 32-bit");
12494    }
12495
12496    #[test]
12497    fn test_encode_ldrsh_thumb2() {
12498        let encoder = ArmEncoder::new_thumb2();
12499        let op = ArmOp::Ldrsh {
12500            rd: Reg::R0,
12501            addr: MemAddr::imm(Reg::R1, 0),
12502        };
12503        let code = encoder.encode(&op).unwrap();
12504        assert_eq!(code.len(), 4, "Thumb-2 LDRSH should be 32-bit");
12505    }
12506
12507    #[test]
12508    fn test_encode_memory_size_thumb2() {
12509        let encoder = ArmEncoder::new_thumb2();
12510        let op = ArmOp::MemorySize { rd: Reg::R0 };
12511        let code = encoder.encode(&op).unwrap();
12512        // R0 and R10 are not both low registers, so this needs careful handling
12513        assert!(!code.is_empty(), "MemorySize should produce code");
12514    }
12515
12516    #[test]
12517    fn test_encode_memory_grow_thumb2() {
12518        let encoder = ArmEncoder::new_thumb2();
12519        let op = ArmOp::MemoryGrow {
12520            rd: Reg::R0,
12521            rn: Reg::R0,
12522        };
12523        let code = encoder.encode(&op).unwrap();
12524        assert_eq!(code.len(), 4, "MemoryGrow (MVN) should be 32-bit Thumb-2");
12525    }
12526
12527    #[test]
12528    fn test_encode_subword_reg_offset_thumb2() {
12529        let encoder = ArmEncoder::new_thumb2();
12530
12531        // LDRB with register offset
12532        let op = ArmOp::Ldrb {
12533            rd: Reg::R0,
12534            addr: MemAddr::reg(Reg::R1, Reg::R2),
12535        };
12536        let code = encoder.encode(&op).unwrap();
12537        assert_eq!(
12538            code.len(),
12539            4,
12540            "Thumb-2 LDRB with reg offset should be 32-bit"
12541        );
12542
12543        // STRB with register offset
12544        let op = ArmOp::Strb {
12545            rd: Reg::R0,
12546            addr: MemAddr::reg(Reg::R1, Reg::R2),
12547        };
12548        let code = encoder.encode(&op).unwrap();
12549        assert_eq!(
12550            code.len(),
12551            4,
12552            "Thumb-2 STRB with reg offset should be 32-bit"
12553        );
12554
12555        // LDRH with register offset
12556        let op = ArmOp::Ldrh {
12557            rd: Reg::R0,
12558            addr: MemAddr::reg(Reg::R1, Reg::R2),
12559        };
12560        let code = encoder.encode(&op).unwrap();
12561        assert_eq!(
12562            code.len(),
12563            4,
12564            "Thumb-2 LDRH with reg offset should be 32-bit"
12565        );
12566
12567        // STRH with register offset
12568        let op = ArmOp::Strh {
12569            rd: Reg::R0,
12570            addr: MemAddr::reg(Reg::R1, Reg::R2),
12571        };
12572        let code = encoder.encode(&op).unwrap();
12573        assert_eq!(
12574            code.len(),
12575            4,
12576            "Thumb-2 STRH with reg offset should be 32-bit"
12577        );
12578    }
12579
12580    #[test]
12581    fn test_encode_subword_reg_imm_offset_thumb2() {
12582        let encoder = ArmEncoder::new_thumb2();
12583
12584        // LDRB with both register and immediate offset
12585        let op = ArmOp::Ldrb {
12586            rd: Reg::R0,
12587            addr: MemAddr::reg_imm(Reg::R1, Reg::R2, 4),
12588        };
12589        let code = encoder.encode(&op).unwrap();
12590        // ADD R12, R2, #4 (4 bytes) + LDRB R0, [R1, R12] (4 bytes) = 8 bytes
12591        assert_eq!(
12592            code.len(),
12593            8,
12594            "Thumb-2 LDRB with reg+imm offset should be 8 bytes"
12595        );
12596    }
12597
12598    // ========================================================================
12599    // Helium MVE encoding tests
12600    // ========================================================================
12601
12602    #[test]
12603    fn test_encode_mve_addi32_thumb2() {
12604        let encoder = ArmEncoder::new_thumb2();
12605        let op = ArmOp::MveAddI {
12606            qd: QReg::Q0,
12607            qn: QReg::Q1,
12608            qm: QReg::Q2,
12609            size: MveSize::S32,
12610        };
12611        let code = encoder.encode(&op).unwrap();
12612        assert_eq!(
12613            code.len(),
12614            4,
12615            "MVE VADD.I32 should be 4 bytes (Thumb-2 32-bit)"
12616        );
12617    }
12618
12619    #[test]
12620    fn test_encode_mve_subi16_thumb2() {
12621        let encoder = ArmEncoder::new_thumb2();
12622        let op = ArmOp::MveSubI {
12623            qd: QReg::Q0,
12624            qn: QReg::Q1,
12625            qm: QReg::Q2,
12626            size: MveSize::S16,
12627        };
12628        let code = encoder.encode(&op).unwrap();
12629        assert_eq!(code.len(), 4, "MVE VSUB.I16 should be 4 bytes");
12630    }
12631
12632    #[test]
12633    fn test_encode_mve_muli8_thumb2() {
12634        let encoder = ArmEncoder::new_thumb2();
12635        let op = ArmOp::MveMulI {
12636            qd: QReg::Q0,
12637            qn: QReg::Q1,
12638            qm: QReg::Q2,
12639            size: MveSize::S8,
12640        };
12641        let code = encoder.encode(&op).unwrap();
12642        assert_eq!(code.len(), 4, "MVE VMUL.I8 should be 4 bytes");
12643    }
12644
12645    #[test]
12646    fn test_encode_mve_bitwise_thumb2() {
12647        let encoder = ArmEncoder::new_thumb2();
12648
12649        let ops = vec![
12650            ArmOp::MveAnd {
12651                qd: QReg::Q0,
12652                qn: QReg::Q1,
12653                qm: QReg::Q2,
12654            },
12655            ArmOp::MveOrr {
12656                qd: QReg::Q0,
12657                qn: QReg::Q1,
12658                qm: QReg::Q2,
12659            },
12660            ArmOp::MveEor {
12661                qd: QReg::Q0,
12662                qn: QReg::Q1,
12663                qm: QReg::Q2,
12664            },
12665            ArmOp::MveBic {
12666                qd: QReg::Q0,
12667                qn: QReg::Q1,
12668                qm: QReg::Q2,
12669            },
12670        ];
12671        for op in ops {
12672            let code = encoder.encode(&op).unwrap();
12673            assert_eq!(code.len(), 4, "MVE bitwise op should be 4 bytes");
12674        }
12675    }
12676
12677    #[test]
12678    fn test_encode_mve_mvn_thumb2() {
12679        let encoder = ArmEncoder::new_thumb2();
12680        let op = ArmOp::MveMvn {
12681            qd: QReg::Q0,
12682            qm: QReg::Q1,
12683        };
12684        let code = encoder.encode(&op).unwrap();
12685        assert_eq!(code.len(), 4, "MVE VMVN should be 4 bytes");
12686    }
12687
12688    #[test]
12689    fn test_encode_mve_load_store_thumb2() {
12690        let encoder = ArmEncoder::new_thumb2();
12691
12692        let load = ArmOp::MveLoad {
12693            qd: QReg::Q0,
12694            addr: MemAddr::imm(Reg::R0, 16),
12695        };
12696        let code = encoder.encode(&load).unwrap();
12697        assert_eq!(code.len(), 4, "MVE VLDRW.32 should be 4 bytes");
12698
12699        let store = ArmOp::MveStore {
12700            qd: QReg::Q1,
12701            addr: MemAddr::imm(Reg::R1, 0),
12702        };
12703        let code = encoder.encode(&store).unwrap();
12704        assert_eq!(code.len(), 4, "MVE VSTRW.32 should be 4 bytes");
12705    }
12706
12707    #[test]
12708    fn test_encode_mve_const_thumb2() {
12709        let encoder = ArmEncoder::new_thumb2();
12710        let op = ArmOp::MveConst {
12711            qd: QReg::Q0,
12712            bytes: [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0],
12713        };
12714        let code = encoder.encode(&op).unwrap();
12715        // Should be 4 words of (MOVW R12 + VMOV Sn) = 4 * (4+4) = 32 bytes min
12716        // Some words with hi16=0 skip MOVT, so length varies
12717        assert!(
12718            code.len() >= 24,
12719            "MVE const should produce multiple instructions"
12720        );
12721    }
12722
12723    #[test]
12724    fn test_encode_mve_dup_thumb2() {
12725        let encoder = ArmEncoder::new_thumb2();
12726        let op = ArmOp::MveDup {
12727            qd: QReg::Q0,
12728            rn: Reg::R0,
12729            size: MveSize::S32,
12730        };
12731        let code = encoder.encode(&op).unwrap();
12732        assert_eq!(code.len(), 4, "MVE VDUP.32 should be 4 bytes");
12733    }
12734
12735    #[test]
12736    fn test_encode_mve_extract_lane_thumb2() {
12737        let encoder = ArmEncoder::new_thumb2();
12738        let op = ArmOp::MveExtractLane {
12739            rd: Reg::R0,
12740            qn: QReg::Q1,
12741            lane: 2,
12742            size: MveSize::S32,
12743        };
12744        let code = encoder.encode(&op).unwrap();
12745        assert_eq!(code.len(), 4, "MVE extract lane should be 4 bytes");
12746    }
12747
12748    #[test]
12749    fn test_encode_mve_insert_lane_thumb2() {
12750        let encoder = ArmEncoder::new_thumb2();
12751        let op = ArmOp::MveInsertLane {
12752            qd: QReg::Q0,
12753            rn: Reg::R1,
12754            lane: 3,
12755            size: MveSize::S32,
12756        };
12757        let code = encoder.encode(&op).unwrap();
12758        assert_eq!(code.len(), 4, "MVE insert lane should be 4 bytes");
12759    }
12760
12761    #[test]
12762    fn test_encode_mve_addf32_thumb2() {
12763        let encoder = ArmEncoder::new_thumb2();
12764        let op = ArmOp::MveAddF32 {
12765            qd: QReg::Q0,
12766            qn: QReg::Q1,
12767            qm: QReg::Q2,
12768        };
12769        let code = encoder.encode(&op).unwrap();
12770        assert_eq!(code.len(), 4, "MVE VADD.F32 should be 4 bytes");
12771    }
12772
12773    #[test]
12774    fn test_encode_mve_divf32_thumb2() {
12775        let encoder = ArmEncoder::new_thumb2();
12776        let op = ArmOp::MveDivF32 {
12777            qd: QReg::Q0,
12778            qn: QReg::Q1,
12779            qm: QReg::Q2,
12780        };
12781        let code = encoder.encode(&op).unwrap();
12782        // Lane-wise: 4 x VDIV.F32 = 4 x 4 = 16 bytes
12783        assert_eq!(
12784            code.len(),
12785            16,
12786            "MVE VDIV.F32 (lane-wise) should be 16 bytes"
12787        );
12788    }
12789
12790    #[test]
12791    fn test_encode_mve_sqrtf32_thumb2() {
12792        let encoder = ArmEncoder::new_thumb2();
12793        let op = ArmOp::MveSqrtF32 {
12794            qd: QReg::Q0,
12795            qm: QReg::Q1,
12796        };
12797        let code = encoder.encode(&op).unwrap();
12798        // Lane-wise: 4 x VSQRT.F32 = 4 x 4 = 16 bytes
12799        assert_eq!(
12800            code.len(),
12801            16,
12802            "MVE VSQRT.F32 (lane-wise) should be 16 bytes"
12803        );
12804    }
12805
12806    #[test]
12807    fn test_encode_mve_negf32_thumb2() {
12808        let encoder = ArmEncoder::new_thumb2();
12809        let op = ArmOp::MveNegF32 {
12810            qd: QReg::Q0,
12811            qm: QReg::Q1,
12812        };
12813        let code = encoder.encode(&op).unwrap();
12814        assert_eq!(code.len(), 4, "MVE VNEG.F32 should be 4 bytes");
12815    }
12816
12817    #[test]
12818    fn test_encode_mve_absf32_thumb2() {
12819        let encoder = ArmEncoder::new_thumb2();
12820        let op = ArmOp::MveAbsF32 {
12821            qd: QReg::Q0,
12822            qm: QReg::Q1,
12823        };
12824        let code = encoder.encode(&op).unwrap();
12825        assert_eq!(code.len(), 4, "MVE VABS.F32 should be 4 bytes");
12826    }
12827
12828    /// VCR-RA-001 / immediate-folding precondition: pins the Thumb-2 `AND`
12829    /// immediate encoding for the byte range and documents its bound.
12830    ///
12831    /// The `And { Operand2::Imm }` encoder packs the low 12 bits straight into
12832    /// the `i:imm3:imm8` field WITHOUT applying ThumbExpandImm (the modified-
12833    /// immediate expansion). For `imm <= 0xFF` (e.g. gale's int8 clamps
12834    /// `#0x7e` / `#0x7f`) that is correct — `i:imm3 = 0000` means "imm8
12835    /// zero-extended". So `and r2, r0, #0x7e` encodes to the canonical
12836    /// `00 f0 7e 02`. For `imm >= 0x100` the field would need a true
12837    /// ThumbExpandImm pattern (rotation / replication), which is NOT
12838    /// implemented here — so **immediate folding must gate on `imm <= 0xFF`**
12839    /// until the encoder is hardened to ThumbExpandImm/Ok-or-Err (the
12840    /// "encoder must be Ok-or-Err, never silently wrong" principle, #180/#185).
12841    /// This bound covers the measured `flat_flight` waste (#209).
12842    #[test]
12843    fn and_immediate_encodes_correctly_in_byte_range_documents_fold_bound() {
12844        let encoder = ArmEncoder::new_thumb2();
12845        let op = ArmOp::And {
12846            rd: Reg::R2,
12847            rn: Reg::R0,
12848            op2: Operand2::Imm(0x7e),
12849        };
12850        let code = encoder.encode(&op).unwrap();
12851        assert_eq!(
12852            code,
12853            vec![0x00, 0xf0, 0x7e, 0x02],
12854            "and r2, r0, #0x7e must encode to the canonical AND.W T1 (imm8=0x7e)"
12855        );
12856    }
12857
12858    /// #255: the shared ThumbExpandImm reverse-encoder underpinning the
12859    /// data-processing immediate fix. Encodable modified immediates round-trip to
12860    /// the expected `i:imm3:imm8` field; a genuinely non-modified value is `None`
12861    /// (caller must materialize into a register). Note `1000 = 0xFA ror 30` *is*
12862    /// representable (field 0xF7A) — the old encoder mis-encoded it (raw 0x3E8);
12863    /// this encodes it correctly.
12864    #[test]
12865    fn try_thumb_expand_imm_encodes_modified_immediates() {
12866        assert_eq!(try_thumb_expand_imm(0x7e), Some(0x07e)); // zero-extended byte
12867        assert_eq!(try_thumb_expand_imm(0xff), Some(0x0ff));
12868        assert_eq!(try_thumb_expand_imm(0x0001_0001), Some(0x101)); // 0x00XY00XY
12869        assert_eq!(try_thumb_expand_imm(0xff00_ff00), Some(0x2ff)); // 0xXY00XY00
12870        assert_eq!(try_thumb_expand_imm(0xffff_ffff), Some(0x3ff)); // 0xXYXYXYXY
12871        assert_eq!(try_thumb_expand_imm(0x100), Some(0xf80)); // 0x80 ror 31
12872        assert_eq!(try_thumb_expand_imm(0x8000_0000), Some(0x400)); // 0x80 ror 8
12873        assert_eq!(try_thumb_expand_imm(1000), Some(0xf7a)); // 0xFA ror 30
12874        // Genuinely unrepresentable (bits too far apart for an 8-bit window).
12875        assert_eq!(try_thumb_expand_imm(0x101), None);
12876        assert_eq!(try_thumb_expand_imm(0x12345), None);
12877    }
12878
12879    /// #255: CMP/ADDS/SUBS encode any valid modified immediate correctly, and
12880    /// ERROR (not silently mis-encode) on a genuinely unrepresentable one,
12881    /// forcing the selector to materialize into a register — closing the
12882    /// silent-miscompile class of #251/#253.
12883    #[test]
12884    fn cmp_adds_subs_immediate_error_on_non_modified_imm() {
12885        let encoder = ArmEncoder::new_thumb2();
12886        // cmp r0, #0xff → valid → Ok; cmp r0, #1000 → valid (0xFA ror 30) → Ok.
12887        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 0xff).is_ok());
12888        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 1000).is_ok());
12889        // cmp r0, #0x101 → NOT a modified immediate → Err (materialize-reg).
12890        assert!(
12891            encoder.encode_thumb32_cmp_imm(&Reg::R0, 0x101).is_err(),
12892            "cmp #0x101 must error, not compare the wrong constant"
12893        );
12894        assert!(
12895            encoder
12896                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x101)
12897                .is_err()
12898        );
12899        assert!(
12900            encoder
12901                .encode_thumb32_subs(&Reg::R0, &Reg::R0, 0x101)
12902                .is_err()
12903        );
12904        // ...but a valid modified immediate still encodes.
12905        assert!(
12906            encoder
12907                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x80)
12908                .is_ok()
12909        );
12910    }
12911
12912    /// #257: MLA (multiply-accumulate) encodes as MLS without the bit-4 op flag.
12913    /// `mla r2, r3, r4, r8` (rd=r2, rn=r3, rm=r4, ra=r8) → Thumb-2 `03 fb 04 82`.
12914    #[test]
12915    fn mla_thumb2_encodes_correctly() {
12916        let encoder = ArmEncoder::new_thumb2();
12917        let code = encoder
12918            .encode(&ArmOp::Mla {
12919                rd: Reg::R2,
12920                rn: Reg::R3,
12921                rm: Reg::R4,
12922                ra: Reg::R8,
12923            })
12924            .unwrap();
12925        // hw1 = 0xFB03, hw2 = (8<<12)|(2<<8)|4 = 0x8204
12926        assert_eq!(code, vec![0x03, 0xfb, 0x04, 0x82]);
12927    }
12928
12929    /// #259: LDR/STR (and sub-word) immediate-offset encoders truncated
12930    /// `offset & 0xFFF`, silently targeting the wrong address for offset >= 4096.
12931    /// They now error (the selector must use register-offset addressing) — the
12932    /// load/store sibling of the #253/#255 class. Offsets <= 4095 still encode.
12933    #[test]
12934    fn ldst_imm12_offset_errors_when_out_of_range() {
12935        let encoder = ArmEncoder::new_thumb2();
12936        // offset 0xFFF (4095): valid → Ok; ldr r0, [r1, #4095].
12937        assert!(
12938            encoder
12939                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0xFFF)
12940                .is_ok()
12941        );
12942        // offset 0x1000 (4096): out of imm12 range → Err (not & 0xFFF → #0).
12943        assert!(
12944            encoder
12945                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0x1000)
12946                .is_err(),
12947            "ldr offset 4096 must error, not wrap to 0"
12948        );
12949        assert!(
12950            encoder
12951                .encode_thumb32_str(&Reg::R0, &Reg::R1, 0x1000)
12952                .is_err()
12953        );
12954        assert!(
12955            encoder
12956                .encode_thumb32_ldrb_imm(&Reg::R0, &Reg::R1, 5000)
12957                .is_err()
12958        );
12959        assert!(
12960            encoder
12961                .encode_thumb32_strh_imm(&Reg::R0, &Reg::R1, 5000)
12962                .is_err()
12963        );
12964    }
12965
12966    /// Latent miscompile fix: ADD/SUB with a >0xFF immediate (e.g.
12967    /// `add sp, sp, #frame` for a >=256-byte frame) used ADD.W (T3), whose
12968    /// `i:imm3:imm8` is a ThumbExpandImm modified immediate — so `#256` silently
12969    /// encoded as `#0` (stack corruption). Use ADDW/SUBW (T4), a PLAIN 12-bit
12970    /// immediate, for 0x100..=0xFFF; keep T3 for <=0xFF (bit-identical); error
12971    /// beyond 4095.
12972    #[test]
12973    fn add_sub_large_immediate_use_addw_subw_not_misencoded() {
12974        let encoder = ArmEncoder::new_thumb2();
12975        // add sp, sp, #256  →  ADDW (T4) SP, SP, #256  =  0d f2 00 1d
12976        assert_eq!(
12977            encoder
12978                .encode(&ArmOp::Add {
12979                    rd: Reg::SP,
12980                    rn: Reg::SP,
12981                    op2: Operand2::Imm(256),
12982                })
12983                .unwrap(),
12984            vec![0x0d, 0xf2, 0x00, 0x1d],
12985            "add sp,sp,#256 must be ADDW (plain imm12), not a mis-encoded ADD.W"
12986        );
12987        // sub sp, sp, #256  →  SUBW (T4) SP, SP, #256  =  ad f2 00 1d
12988        assert_eq!(
12989            encoder
12990                .encode(&ArmOp::Sub {
12991                    rd: Reg::SP,
12992                    rn: Reg::SP,
12993                    op2: Operand2::Imm(256),
12994                })
12995                .unwrap(),
12996            vec![0xad, 0xf2, 0x00, 0x1d],
12997        );
12998        // > 4095 has no single-instruction encoding → error, not silent wrong.
12999        assert!(
13000            encoder
13001                .encode(&ArmOp::Add {
13002                    rd: Reg::SP,
13003                    rn: Reg::SP,
13004                    op2: Operand2::Imm(5000),
13005                })
13006                .is_err(),
13007            "add #5000 must error (no single ADDW), not mis-encode"
13008        );
13009    }
13010
13011    /// Closes the data-proc immediate class: AND and CMN now go through
13012    /// `try_thumb_expand_imm` like ORR/EOR/CMP — correct for any modified
13013    /// immediate, `Err` (not raw-pack / NOP) on an un-encodable one. The byte
13014    /// range stays bit-identical (`and r2,r0,#0x7e` is unchanged).
13015    #[test]
13016    fn and_cmn_immediate_thumb_expand_else_error() {
13017        let encoder = ArmEncoder::new_thumb2();
13018        // byte range unchanged (bit-identical with the pre-retrofit encoding)
13019        assert_eq!(
13020            encoder
13021                .encode(&ArmOp::And {
13022                    rd: Reg::R2,
13023                    rn: Reg::R0,
13024                    op2: Operand2::Imm(0x7e),
13025                })
13026                .unwrap(),
13027            vec![0x00, 0xf0, 0x7e, 0x02],
13028        );
13029        // a valid replicated modified immediate now encodes (was silently wrong)
13030        assert!(
13031            encoder
13032                .encode(&ArmOp::And {
13033                    rd: Reg::R2,
13034                    rn: Reg::R0,
13035                    op2: Operand2::Imm(0xff00ff00u32 as i32),
13036                })
13037                .is_ok()
13038        );
13039        // a genuinely un-encodable immediate errors (AND was raw-pack; CMN NOP)
13040        assert!(
13041            encoder
13042                .encode(&ArmOp::And {
13043                    rd: Reg::R2,
13044                    rn: Reg::R0,
13045                    op2: Operand2::Imm(0x101),
13046                })
13047                .is_err()
13048        );
13049        assert!(
13050            encoder
13051                .encode(&ArmOp::Cmn {
13052                    rn: Reg::R0,
13053                    op2: Operand2::Imm(0x101),
13054                })
13055                .is_err(),
13056            "CMN #0x101 must error, not emit a NOP"
13057        );
13058    }
13059
13060    /// VCR-RA-001: ORR/EOR with a small immediate must encode the real
13061    /// instruction (not a silent `0xBF00` NOP). Pins the byte range and the
13062    /// Ok-or-Err bound that makes future Or/Eor immediate folding safe.
13063    #[test]
13064    fn orr_eor_immediate_encode_in_byte_range_else_error() {
13065        let encoder = ArmEncoder::new_thumb2();
13066        // orr r2, r0, #0x7e  →  ORR.W T1, imm8=0x7e
13067        assert_eq!(
13068            encoder
13069                .encode(&ArmOp::Orr {
13070                    rd: Reg::R2,
13071                    rn: Reg::R0,
13072                    op2: Operand2::Imm(0x7e),
13073                })
13074                .unwrap(),
13075            vec![0x40, 0xf0, 0x7e, 0x02],
13076        );
13077        // eor r2, r0, #0x7e  →  EOR.W T1, imm8=0x7e
13078        assert_eq!(
13079            encoder
13080                .encode(&ArmOp::Eor {
13081                    rd: Reg::R2,
13082                    rn: Reg::R0,
13083                    op2: Operand2::Imm(0x7e),
13084                })
13085                .unwrap(),
13086            vec![0x80, 0xf0, 0x7e, 0x02],
13087        );
13088        // Out-of-range immediates error rather than silently mis-encode / NOP.
13089        assert!(
13090            encoder
13091                .encode(&ArmOp::Orr {
13092                    rd: Reg::R2,
13093                    rn: Reg::R0,
13094                    op2: Operand2::Imm(0x140),
13095                })
13096                .is_err(),
13097            "ORR #0x140 must error, not emit a NOP"
13098        );
13099    }
13100
13101    #[test]
13102    fn test_encode_mve_different_qregs() {
13103        let encoder = ArmEncoder::new_thumb2();
13104
13105        // Test that different Q-register numbers produce different encodings
13106        let op1 = ArmOp::MveAddI {
13107            qd: QReg::Q0,
13108            qn: QReg::Q0,
13109            qm: QReg::Q0,
13110            size: MveSize::S32,
13111        };
13112        let op2 = ArmOp::MveAddI {
13113            qd: QReg::Q3,
13114            qn: QReg::Q5,
13115            qm: QReg::Q7,
13116            size: MveSize::S32,
13117        };
13118        let code1 = encoder.encode(&op1).unwrap();
13119        let code2 = encoder.encode(&op2).unwrap();
13120        assert_ne!(
13121            code1, code2,
13122            "Different Q-registers should produce different encodings"
13123        );
13124    }
13125
13126    #[test]
13127    fn test_encode_mve_arm32_loud_err() {
13128        // #615: MVE (Helium) is Thumb-2-only. The ARM32 encoder used to emit
13129        // a silent NOP here (dropping the vector op); it must now be a typed
13130        // Err so a broken "MVE implies Thumb" invariant fails loudly.
13131        let encoder = ArmEncoder::new_arm32();
13132        let op = ArmOp::MveAddI {
13133            qd: QReg::Q0,
13134            qn: QReg::Q1,
13135            qm: QReg::Q2,
13136            size: MveSize::S32,
13137        };
13138        let err = encoder
13139            .encode(&op)
13140            .expect_err("ARM32 MVE must be a loud Err, not a silent NOP (#615)");
13141        assert!(
13142            err.to_string().contains("Thumb-2 only"),
13143            "unexpected error message: {err}"
13144        );
13145    }
13146}