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    /// The §4.4.8 type check is discharged at COMPILE time by the selector's
147    /// closed-world verification (the raw code-pointer table carries no
148    /// runtime type ids) — see the #642 selector guard.
149    fn encode_arm_call_indirect(
150        table_index_reg: &Reg,
151        table_size: u32,
152        table_byte_offset: u32,
153    ) -> Vec<u8> {
154        let idx = reg_to_bits(table_index_reg);
155        let mut bytes = Vec::with_capacity(32);
156        // MOVW r12, #(size & 0xFFFF) — cond=E 0011 0000 imm4 Rd imm12.
157        let size_lo = table_size & 0xFFFF;
158        let movw: u32 = 0xE300_0000 | ((size_lo >> 12) << 16) | (12 << 12) | (size_lo & 0xFFF);
159        bytes.extend_from_slice(&movw.to_le_bytes());
160        // MOVT r12, #(size >> 16) — only for a table size above 16 bits.
161        let size_hi = table_size >> 16;
162        if size_hi != 0 {
163            let movt: u32 = 0xE340_0000 | ((size_hi >> 12) << 16) | (12 << 12) | (size_hi & 0xFFF);
164            bytes.extend_from_slice(&movt.to_le_bytes());
165        }
166        // CMP idx, r12 — cond=E, opcode=1010, S=1, Rn=idx, Rm=r12.
167        let cmp: u32 = 0xE150_000C | (idx << 16);
168        bytes.extend_from_slice(&cmp.to_le_bytes());
169        // BLO +1 insn (skip the UDF when index < size) — cond=LO(0011),
170        // imm24=0: target = branch + 8.
171        bytes.extend_from_slice(&0x3A00_0000u32.to_le_bytes());
172        // UDF — permanently undefined (same trap idiom as the A32 div-by-zero
173        // guards): call_indirect out-of-bounds trap.
174        bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
175        // MOV r12, idx, LSL #2 — data-processing MOV, register op2 with
176        // imm5=2/LSL: cond=E, opcode=1101, S=0, Rd=r12.
177        let mov: u32 = 0xE1A0C000 | (2 << 7) | idx;
178        bytes.extend_from_slice(&mov.to_le_bytes());
179        if table_byte_offset == 0 {
180            // Table 0 (base = R11 itself): the pre-#650 single-load form.
181            // LDR r12, [r11, r12] — register offset, P=1 U=1 B=0 W=0 L=1.
182            let ldr: u32 = 0xE79BC00C;
183            bytes.extend_from_slice(&ldr.to_le_bytes());
184        } else {
185            // #650: fold the table's compile-time base offset into the
186            // pointer load via the LDR imm12 form.
187            assert!(
188                table_byte_offset <= 4095,
189                "call_indirect table base offset {table_byte_offset} exceeds \
190                 LDR imm12 — the selector must have declined this (#650)"
191            );
192            // ADD r12, r11, r12 — data-processing ADD (register).
193            bytes.extend_from_slice(&0xE08BC00Cu32.to_le_bytes());
194            // LDR r12, [r12, #offset] — immediate offset, P=1 U=1 L=1.
195            let ldr: u32 = 0xE59CC000 | (table_byte_offset & 0xFFF);
196            bytes.extend_from_slice(&ldr.to_le_bytes());
197        }
198        // BLX r12 — cond=E, 0001 0010 1111 1111 1111 0011, Rm=r12.
199        let blx: u32 = 0xE12FFF3C;
200        bytes.extend_from_slice(&blx.to_le_bytes());
201        bytes
202    }
203
204    /// #615: A32 (ARM-mode) expansions for the multi-instruction ops that the
205    /// Thumb-2 encoder expands but the A32 arm previously encoded as a single
206    /// literal NOP (`0xE1A00000`) — i64 mul / shifts / rotates / comparisons /
207    /// eqz, plus i64 const/load/store/extend/wrap and the i32 SetCond /
208    /// SelectMove pseudo-ops. Each expansion mirrors its Thumb-2 twin's
209    /// register contract and semantics exactly (A32 conditional execution
210    /// replaces the IT blocks). Returns `Ok(None)` for ops this helper does
211    /// not handle; the caller's match encodes or loudly rejects those.
212    fn encode_arm_expanded(&self, op: &ArmOp) -> Result<Option<Vec<u8>>> {
213        use synth_synthesis::Condition;
214
215        /// A32 condition-field bits (instruction bits [31:28]).
216        fn cond_bits(cond: &Condition) -> u32 {
217            match cond {
218                Condition::EQ => 0x0,
219                Condition::NE => 0x1,
220                Condition::HS => 0x2, // CS: unsigned >=
221                Condition::LO => 0x3, // CC: unsigned <
222                Condition::HI => 0x8, // unsigned >
223                Condition::LS => 0x9, // unsigned <=
224                Condition::GE => 0xA,
225                Condition::LT => 0xB,
226                Condition::GT => 0xC,
227                Condition::LE => 0xD,
228            }
229        }
230        fn w(b: &mut Vec<u8>, word: u32) {
231            b.extend_from_slice(&word.to_le_bytes());
232        }
233        /// MOV<cond> rd, #imm (rotated-immediate form; only 0/1 used here).
234        fn mov_cond_imm(b: &mut Vec<u8>, cond: u32, rd: u32, imm: u32) {
235            w(b, (cond << 28) | 0x03A0_0000 | (rd << 12) | imm);
236        }
237        /// After a flag-setting pair: MOV<cond> rd,#1 ; MOV<!cond> rd,#0.
238        fn set_cond(b: &mut Vec<u8>, cond: &Condition, rd: u32) {
239            mov_cond_imm(b, cond_bits(cond), rd, 1);
240            mov_cond_imm(b, cond_bits(&cond.invert()), rd, 0);
241        }
242        /// CMP rn, rm (register form).
243        fn cmp_reg(b: &mut Vec<u8>, rn: u32, rm: u32) {
244            w(b, 0xE150_0000 | (rn << 16) | rm);
245        }
246        /// SBCS rd, rn, rm — the 64-bit compare idiom's high-word subtract.
247        fn sbcs(b: &mut Vec<u8>, rd: u32, rn: u32, rm: u32) {
248            w(b, 0xE0D0_0000 | (rn << 16) | (rd << 12) | rm);
249        }
250        /// MOVW rd, #imm16.
251        fn movw(b: &mut Vec<u8>, rd: u32, v: u32) {
252            w(
253                b,
254                0xE300_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
255            );
256        }
257        /// MOVT rd, #imm16.
258        fn movt(b: &mut Vec<u8>, rd: u32, v: u32) {
259            w(
260                b,
261                0xE340_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
262            );
263        }
264        /// Register-controlled shift: MOV rd, rn, <LSL|LSR|ASR> rs.
265        /// `ty`: 0=LSL, 1=LSR, 2=ASR. A32 uses the bottom byte of rs;
266        /// amounts of 32 or more yield 0 (LSL/LSR) or all-sign (ASR) — same
267        /// semantics the Thumb-2 expansions rely on.
268        fn shift_reg(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, rs: u32) {
269            w(b, 0xE1A0_0010 | (rd << 12) | (rs << 8) | (ty << 5) | rn);
270        }
271        const LSL: u32 = 0;
272        const LSR: u32 = 1;
273        const ASR: u32 = 2;
274        /// Immediate-shift move: MOV rd, rn, <LSL|LSR|ASR> #imm.
275        fn shift_imm(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, imm: u32) {
276            w(
277                b,
278                0xE1A0_0000 | (rd << 12) | ((imm & 0x1F) << 7) | (ty << 5) | rn,
279            );
280        }
281        /// Data-processing register form: `base | rn<<16 | rd<<12 | rm`.
282        /// `base` carries cond/opcode/S (e.g. 0xE090_0000 = ADDS).
283        fn dp_reg(b: &mut Vec<u8>, base: u32, rd: u32, rn: u32, rm: u32) {
284            w(b, base | (rn << 16) | (rd << 12) | rm);
285        }
286        /// ORR rd, rd, rm, LSR #31 — the carry-propagation idiom of the
287        /// shift-subtract division loop (bring rm's MSB into rd's bit 0).
288        fn orr_lsr31(b: &mut Vec<u8>, rd: u32, rm: u32) {
289            w(
290                b,
291                0xE180_0000 | (rd << 16) | (rd << 12) | (31 << 7) | (1 << 5) | rm,
292            );
293        }
294        /// 64-bit two's-complement negate of the lo:hi pair (MVN/MVN/ADDS/ADC).
295        fn negate64(b: &mut Vec<u8>, lo: u32, hi: u32) {
296            w(b, 0xE1E0_0000 | (lo << 12) | lo); //           MVN  lo, lo
297            w(b, 0xE1E0_0000 | (hi << 12) | hi); //           MVN  hi, hi
298            w(b, 0xE290_0001 | (lo << 16) | (lo << 12)); //   ADDS lo, lo, #1
299            w(b, 0xE2A0_0000 | (hi << 16) | (hi << 12)); //   ADC  hi, hi, #0
300        }
301        /// TST x, x ; BPL +4-instructions — the "skip the negate64 when the
302        /// sign bit is clear" guard of the signed div/rem arms.
303        fn skip_negate_if_positive(b: &mut Vec<u8>, x: u32) {
304            w(b, 0xE110_0000 | (x << 16) | x); // TST x, x
305            w(b, 0x5A00_0003); //                 BPL +4 insns (past negate64)
306        }
307        /// The 64-iteration shift-subtract division loop — A32 transcription
308        /// of the Thumb-2 #610 core: dividend R0:R1, divisor R2:R3, quotient
309        /// R4:R5, remainder R6:R7, loop counter in `counter` (R12 or R8).
310        fn div_loop(b: &mut Vec<u8>, counter: u32) {
311            w(b, 0xE3A0_0040 | (counter << 12)); // MOV counter, #64
312            let loop_start = b.len();
313            // quotient <<= 1
314            shift_imm(b, LSL, 5, 5, 1);
315            orr_lsr31(b, 5, 4);
316            shift_imm(b, LSL, 4, 4, 1);
317            // remainder <<= 1, OR in dividend MSB
318            shift_imm(b, LSL, 7, 7, 1);
319            orr_lsr31(b, 7, 6);
320            shift_imm(b, LSL, 6, 6, 1);
321            orr_lsr31(b, 6, 1);
322            // dividend <<= 1
323            shift_imm(b, LSL, 1, 1, 1);
324            orr_lsr31(b, 1, 0);
325            shift_imm(b, LSL, 0, 0, 1);
326            // if remainder >= divisor (64-bit unsigned): subtract, set q bit
327            w(b, 0xE157_0003); // CMP R7, R3      (high words)
328            w(b, 0x8A00_0002); // BHI .subtract   (+2 insns)
329            w(b, 0x3A00_0004); // BLO .next       (+4 insns)
330            w(b, 0xE156_0002); // CMP R6, R2      (low words, highs equal)
331            w(b, 0x3A00_0002); // BLO .next       (+2 insns)
332            w(b, 0xE056_6002); // .subtract: SUBS R6, R6, R2
333            w(b, 0xE0C7_7003); //            SBC  R7, R7, R3
334            w(b, 0xE384_4001); //            ORR  R4, R4, #1
335            // .next: decrement and loop
336            w(b, 0xE250_0001 | (counter << 16) | (counter << 12)); // SUBS counter, #1
337            let diff = (loop_start as i64) - (b.len() as i64 + 8);
338            w(b, 0x1A00_0000 | (((diff / 4) as u32) & 0x00FF_FFFF)); // BNE loop
339        }
340        /// 32-bit population count on working register `x` — A32 transcription
341        /// of the Thumb-2 I64Popcnt per-word core (mul-based fold): `c` is the
342        /// constant register, R12 the shifted temp. Both are clobbered.
343        fn popcnt_word(b: &mut Vec<u8>, x: u32, c: u32) {
344            // x = x - ((x >> 1) & 0x55555555)
345            shift_imm(b, LSR, 12, x, 1);
346            movw(b, c, 0x5555);
347            movt(b, c, 0x5555);
348            dp_reg(b, 0xE000_0000, 12, 12, c); // AND R12, R12, c
349            dp_reg(b, 0xE040_0000, x, x, 12); //  SUB x, x, R12
350            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
351            movw(b, c, 0x3333);
352            movt(b, c, 0x3333);
353            dp_reg(b, 0xE000_0000, 12, x, c); //  AND R12, x, c
354            shift_imm(b, LSR, x, x, 2);
355            dp_reg(b, 0xE000_0000, x, x, c); //   AND x, x, c
356            dp_reg(b, 0xE080_0000, x, x, 12); //  ADD x, x, R12
357            // x = (x + (x >> 4)) & 0x0F0F0F0F
358            shift_imm(b, LSR, 12, x, 4);
359            dp_reg(b, 0xE080_0000, x, x, 12); //  ADD x, x, R12
360            movw(b, c, 0x0F0F);
361            movt(b, c, 0x0F0F);
362            dp_reg(b, 0xE000_0000, x, x, c); //   AND x, x, c
363            // x = (x * 0x01010101) >> 24
364            movw(b, c, 0x0101);
365            movt(b, c, 0x0101);
366            w(b, 0xE000_0090 | (x << 16) | (c << 8) | x); // MUL x, x, c
367            shift_imm(b, LSR, x, x, 24);
368        }
369
370        let mut b: Vec<u8> = Vec::new();
371        match op {
372            // SetCond: materialize a flags-predicate as 0/1 — the A32 twin of
373            // the Thumb `ITE cond; MOV rd,#1; MOV rd,#0`.
374            ArmOp::SetCond { rd, cond } => {
375                set_cond(&mut b, cond, reg_to_bits(rd));
376            }
377
378            // SelectMove: conditional register move (Thumb: IT cond; MOV).
379            ArmOp::SelectMove { rd, rm, cond } => {
380                w(
381                    &mut b,
382                    (cond_bits(cond) << 28)
383                        | 0x01A0_0000
384                        | (reg_to_bits(rd) << 12)
385                        | reg_to_bits(rm),
386                );
387            }
388
389            // I64SetCond: compare two i64 register pairs, 0/1 into rd.
390            // EQ/NE: CMP lo,lo; CMPEQ hi,hi (only if lows equal); set.
391            // Ordered: CMP lo,lo; SBCS rd,hi,hi; set — with the same
392            // operand-swap + condition mapping as the Thumb-2 arm.
393            ArmOp::I64SetCond {
394                rd,
395                rn_lo,
396                rn_hi,
397                rm_lo,
398                rm_hi,
399                cond,
400            } => {
401                let rd_b = reg_to_bits(rd);
402                let (n_lo, n_hi, m_lo, m_hi) = (
403                    reg_to_bits(rn_lo),
404                    reg_to_bits(rn_hi),
405                    reg_to_bits(rm_lo),
406                    reg_to_bits(rm_hi),
407                );
408                match cond {
409                    Condition::EQ | Condition::NE => {
410                        cmp_reg(&mut b, n_lo, m_lo);
411                        // CMP<EQ> rn_hi, rm_hi — compare highs only if lows equal.
412                        w(&mut b, 0x0150_0000 | (n_hi << 16) | m_hi);
413                        set_cond(&mut b, cond, rd_b);
414                    }
415                    // (swap operands?, condition after SBCS) per the Thumb arm:
416                    // LT/GE/LO/HS compare (rn, rm); GT/LE/HI/LS swap to (rm, rn).
417                    Condition::LT => {
418                        cmp_reg(&mut b, n_lo, m_lo);
419                        sbcs(&mut b, rd_b, n_hi, m_hi);
420                        set_cond(&mut b, &Condition::LT, rd_b);
421                    }
422                    Condition::GE => {
423                        cmp_reg(&mut b, n_lo, m_lo);
424                        sbcs(&mut b, rd_b, n_hi, m_hi);
425                        set_cond(&mut b, &Condition::GE, rd_b);
426                    }
427                    Condition::GT => {
428                        cmp_reg(&mut b, m_lo, n_lo);
429                        sbcs(&mut b, rd_b, m_hi, n_hi);
430                        set_cond(&mut b, &Condition::LT, rd_b);
431                    }
432                    Condition::LE => {
433                        cmp_reg(&mut b, m_lo, n_lo);
434                        sbcs(&mut b, rd_b, m_hi, n_hi);
435                        set_cond(&mut b, &Condition::GE, rd_b);
436                    }
437                    Condition::LO => {
438                        cmp_reg(&mut b, n_lo, m_lo);
439                        sbcs(&mut b, rd_b, n_hi, m_hi);
440                        set_cond(&mut b, &Condition::LO, rd_b);
441                    }
442                    Condition::HS => {
443                        cmp_reg(&mut b, n_lo, m_lo);
444                        sbcs(&mut b, rd_b, n_hi, m_hi);
445                        set_cond(&mut b, &Condition::HS, rd_b);
446                    }
447                    Condition::HI => {
448                        cmp_reg(&mut b, m_lo, n_lo);
449                        sbcs(&mut b, rd_b, m_hi, n_hi);
450                        set_cond(&mut b, &Condition::LO, rd_b);
451                    }
452                    Condition::LS => {
453                        cmp_reg(&mut b, m_lo, n_lo);
454                        sbcs(&mut b, rd_b, m_hi, n_hi);
455                        set_cond(&mut b, &Condition::HS, rd_b);
456                    }
457                }
458            }
459
460            // I64SetCondZ: ORRS rd, lo, hi sets Z iff the pair is zero.
461            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
462                let rd_b = reg_to_bits(rd);
463                w(
464                    &mut b,
465                    0xE190_0000 | (reg_to_bits(rn_lo) << 16) | (rd_b << 12) | reg_to_bits(rn_hi),
466                );
467                set_cond(&mut b, &Condition::EQ, rd_b);
468            }
469
470            // i64 comparison wrappers: delegate to I64SetCond/Z, mirroring the
471            // Thumb-2 delegation arms.
472            ArmOp::I64Eqz { rd, rnlo, rnhi } => {
473                return self
474                    .encode_arm(&ArmOp::I64SetCondZ {
475                        rd: *rd,
476                        rn_lo: *rnlo,
477                        rn_hi: *rnhi,
478                    })
479                    .map(Some);
480            }
481            ArmOp::I64Eq {
482                rd,
483                rnlo,
484                rnhi,
485                rmlo,
486                rmhi,
487            }
488            | ArmOp::I64Ne {
489                rd,
490                rnlo,
491                rnhi,
492                rmlo,
493                rmhi,
494            }
495            | ArmOp::I64LtS {
496                rd,
497                rnlo,
498                rnhi,
499                rmlo,
500                rmhi,
501            }
502            | ArmOp::I64LtU {
503                rd,
504                rnlo,
505                rnhi,
506                rmlo,
507                rmhi,
508            }
509            | ArmOp::I64LeS {
510                rd,
511                rnlo,
512                rnhi,
513                rmlo,
514                rmhi,
515            }
516            | ArmOp::I64LeU {
517                rd,
518                rnlo,
519                rnhi,
520                rmlo,
521                rmhi,
522            }
523            | ArmOp::I64GtS {
524                rd,
525                rnlo,
526                rnhi,
527                rmlo,
528                rmhi,
529            }
530            | ArmOp::I64GtU {
531                rd,
532                rnlo,
533                rnhi,
534                rmlo,
535                rmhi,
536            }
537            | ArmOp::I64GeS {
538                rd,
539                rnlo,
540                rnhi,
541                rmlo,
542                rmhi,
543            }
544            | ArmOp::I64GeU {
545                rd,
546                rnlo,
547                rnhi,
548                rmlo,
549                rmhi,
550            } => {
551                let cond = match op {
552                    ArmOp::I64Eq { .. } => Condition::EQ,
553                    ArmOp::I64Ne { .. } => Condition::NE,
554                    ArmOp::I64LtS { .. } => Condition::LT,
555                    ArmOp::I64LtU { .. } => Condition::LO,
556                    ArmOp::I64LeS { .. } => Condition::LE,
557                    ArmOp::I64LeU { .. } => Condition::LS,
558                    ArmOp::I64GtS { .. } => Condition::GT,
559                    ArmOp::I64GtU { .. } => Condition::HI,
560                    ArmOp::I64GeS { .. } => Condition::GE,
561                    _ => Condition::HS,
562                };
563                return self
564                    .encode_arm(&ArmOp::I64SetCond {
565                        rd: *rd,
566                        rn_lo: *rnlo,
567                        rn_hi: *rnhi,
568                        rm_lo: *rmlo,
569                        rm_hi: *rmhi,
570                        cond,
571                    })
572                    .map(Some);
573            }
574
575            // I64Mul: cross products into R12, then UMULL — same sequence and
576            // ordering as the Thumb-2 arm (R12 is encoder scratch, #212).
577            ArmOp::I64Mul {
578                rd_lo,
579                rd_hi,
580                rn_lo,
581                rn_hi,
582                rm_lo,
583                rm_hi,
584            } => {
585                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
586                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
587                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
588                // MUL R12, rn_lo, rm_hi   (R12 = a_lo * b_hi)
589                w(&mut b, 0xE000_0090 | (12 << 16) | (mh << 8) | nl);
590                // MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
591                w(
592                    &mut b,
593                    0xE020_0090 | (12 << 16) | (12 << 12) | (ml << 8) | nh,
594                );
595                // UMULL rd_lo, rd_hi, rn_lo, rm_lo
596                w(
597                    &mut b,
598                    0xE080_0090 | (dh << 16) | (dl << 12) | (ml << 8) | nl,
599                );
600                // ADD rd_hi, rd_hi, R12
601                w(&mut b, 0xE080_0000 | (dh << 16) | (dh << 12) | 12);
602            }
603
604            // I64Shl / I64ShrU / I64ShrS: same small/large-shift structure as
605            // the Thumb-2 arms (rm_hi is the scratch register; amounts are
606            // masked to 6 bits; register-controlled shifts >= 32 yield 0,
607            // which the small path relies on for n = 0).
608            ArmOp::I64Shl {
609                rd_lo,
610                rd_hi,
611                rn_lo,
612                rn_hi,
613                rm_lo,
614                rm_hi,
615            } => {
616                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
617                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
618                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
619                w(&mut b, 0xE200_003F | (ml << 16) | (ml << 12)); // AND  ml, ml, #63
620                w(&mut b, 0xE250_0020 | (ml << 16) | (mh << 12)); // SUBS mh, ml, #32
621                w(&mut b, 0x5A00_0005); //                            BPL  .large
622                w(&mut b, 0xE260_0020 | (ml << 16) | (mh << 12)); // RSB  mh, ml, #32
623                shift_reg(&mut b, LSR, mh, nl, mh); //               mh = lo >> (32-n)
624                shift_reg(&mut b, LSL, dh, nh, ml); //               dh = hi << n
625                w(&mut b, 0xE180_0000 | (dh << 16) | (dh << 12) | mh); // ORR dh, dh, mh
626                shift_reg(&mut b, LSL, dl, nl, ml); //               dl = lo << n
627                w(&mut b, 0xEA00_0001); //                            B    .done
628                shift_reg(&mut b, LSL, dh, nl, mh); //               .large: dh = lo << (n-32)
629                w(&mut b, 0xE3A0_0000 | (dl << 12)); //              MOV  dl, #0
630            }
631            ArmOp::I64ShrU {
632                rd_lo,
633                rd_hi,
634                rn_lo,
635                rn_hi,
636                rm_lo,
637                rm_hi,
638            } => {
639                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
640                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
641                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
642                w(&mut b, 0xE200_003F | (ml << 16) | (ml << 12)); // AND  ml, ml, #63
643                w(&mut b, 0xE250_0020 | (ml << 16) | (mh << 12)); // SUBS mh, ml, #32
644                w(&mut b, 0x5A00_0005); //                            BPL  .large
645                w(&mut b, 0xE260_0020 | (ml << 16) | (mh << 12)); // RSB  mh, ml, #32
646                shift_reg(&mut b, LSL, mh, nh, mh); //               mh = hi << (32-n)
647                shift_reg(&mut b, LSR, dl, nl, ml); //               dl = lo >> n
648                w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | mh); // ORR dl, dl, mh
649                shift_reg(&mut b, LSR, dh, nh, ml); //               dh = hi >> n
650                w(&mut b, 0xEA00_0001); //                            B    .done
651                shift_reg(&mut b, LSR, dl, nh, mh); //               .large: dl = hi >> (n-32)
652                w(&mut b, 0xE3A0_0000 | (dh << 12)); //              MOV  dh, #0
653            }
654            ArmOp::I64ShrS {
655                rd_lo,
656                rd_hi,
657                rn_lo,
658                rn_hi,
659                rm_lo,
660                rm_hi,
661            } => {
662                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
663                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
664                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
665                w(&mut b, 0xE200_003F | (ml << 16) | (ml << 12)); // AND  ml, ml, #63
666                w(&mut b, 0xE250_0020 | (ml << 16) | (mh << 12)); // SUBS mh, ml, #32
667                w(&mut b, 0x5A00_0005); //                            BPL  .large
668                w(&mut b, 0xE260_0020 | (ml << 16) | (mh << 12)); // RSB  mh, ml, #32
669                shift_reg(&mut b, LSL, mh, nh, mh); //               mh = hi << (32-n)
670                shift_reg(&mut b, LSR, dl, nl, ml); //               dl = lo >> n
671                w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | mh); // ORR dl, dl, mh
672                shift_reg(&mut b, ASR, dh, nh, ml); //               dh = hi >> n (arith)
673                w(&mut b, 0xEA00_0001); //                            B    .done
674                shift_reg(&mut b, ASR, dl, nh, mh); //               .large: dl = hi >> (n-32)
675                w(&mut b, 0xE1A0_0040 | (dh << 12) | (31 << 7) | nh); // ASR dh, nh, #31
676            }
677
678            // I64Rotl / I64Rotr: the #610 fixed-ABI wrapper (A32 form) around
679            // the same fixed-register core as the Thumb-2 arms — value in
680            // R0:R1, amount in R2, scratch R3 + R12.
681            ArmOp::I64Rotl {
682                rdlo,
683                rdhi,
684                rnlo,
685                rnhi,
686                shift,
687            } => {
688                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
689                for word in [
690                    0xE202_203Fu32, // AND  R2, R2, #63   (mask amount mod 64)
691                    0xE252_3020,    // SUBS R3, R2, #32   (R3 = n-32, sets N)
692                    0x5A00_0007,    // BPL  .large        (n >= 32)
693                    // --- small rotation (n < 32) ---
694                    0xE262_3020, // RSB  R3, R2, #32   (R3 = 32-n)
695                    0xE1A0_C330, // LSR  R12, R0, R3   (lo >> (32-n))
696                    0xE1A0_3331, // LSR  R3, R1, R3    (hi >> (32-n))
697                    0xE1A0_1211, // LSL  R1, R1, R2    (hi << n)
698                    0xE181_100C, // ORR  R1, R1, R12   (new_hi)
699                    0xE1A0_0210, // LSL  R0, R0, R2    (lo << n)
700                    0xE180_0003, // ORR  R0, R0, R3    (new_lo)
701                    0xEA00_0007, // B    .done
702                    // --- large rotation (n >= 32), R3 = m = n-32 ---
703                    0xE263_2020, // RSB  R2, R3, #32   (R2 = 32-m = 64-n)
704                    0xE1A0_C231, // LSR  R12, R1, R2   (hi >> (64-n))
705                    0xE1A0_2230, // LSR  R2, R0, R2    (lo >> (64-n))
706                    0xE1A0_0310, // LSL  R0, R0, R3    (lo << m)
707                    0xE1A0_1311, // LSL  R1, R1, R3    (hi << m)
708                    0xE180_C00C, // ORR  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
709                    0xE181_0002, // ORR  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
710                    0xE1A0_100C, // MOV  R1, R12       (new_hi into place)
711                ] {
712                    w(&mut b, word);
713                }
714                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
715            }
716            ArmOp::I64Rotr {
717                rdlo,
718                rdhi,
719                rnlo,
720                rnhi,
721                shift,
722            } => {
723                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
724                for word in [
725                    0xE202_203Fu32, // AND  R2, R2, #63   (mask amount mod 64)
726                    0xE252_3020,    // SUBS R3, R2, #32   (R3 = n-32, sets N)
727                    0x5A00_0007,    // BPL  .large        (n >= 32)
728                    // --- small rotation (n < 32) ---
729                    0xE262_3020, // RSB  R3, R2, #32   (R3 = 32-n)
730                    0xE1A0_C311, // LSL  R12, R1, R3   (hi << (32-n))
731                    0xE1A0_3310, // LSL  R3, R0, R3    (lo << (32-n))
732                    0xE1A0_0230, // LSR  R0, R0, R2    (lo >> n)
733                    0xE180_000C, // ORR  R0, R0, R12   (new_lo)
734                    0xE1A0_1231, // LSR  R1, R1, R2    (hi >> n)
735                    0xE181_1003, // ORR  R1, R1, R3    (new_hi)
736                    0xEA00_0007, // B    .done
737                    // --- large rotation (n >= 32), R3 = m = n-32 ---
738                    0xE263_2020, // RSB  R2, R3, #32   (R2 = 32-m = 64-n)
739                    0xE1A0_C210, // LSL  R12, R0, R2   (lo << (64-n))
740                    0xE1A0_2211, // LSL  R2, R1, R2    (hi << (64-n))
741                    0xE1A0_1331, // LSR  R1, R1, R3    (hi >> m)
742                    0xE181_C00C, // ORR  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
743                    0xE1A0_1330, // LSR  R1, R0, R3    (lo >> m)
744                    0xE181_1002, // ORR  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
745                    0xE1A0_000C, // MOV  R0, R12       (new_lo into place)
746                ] {
747                    w(&mut b, word);
748                }
749                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
750            }
751
752            // I64Clz: CLZ(hi), or 32 + CLZ(lo) when hi == 0. Conditional
753            // execution replaces the Thumb branches; like the Thumb arm, the
754            // high word of the result pair (rnhi) is cleared last.
755            ArmOp::I64Clz { rd, rnlo, rnhi } => {
756                let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
757                w(&mut b, 0xE350_0000 | (hi << 16)); //              CMP   rnhi, #0
758                w(&mut b, 0x116F_0F10 | (rd_b << 12) | hi); //       CLZNE rd, rnhi
759                w(&mut b, 0x016F_0F10 | (rd_b << 12) | lo); //       CLZEQ rd, rnlo
760                w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
761                w(&mut b, 0xE3A0_0000 | (hi << 12)); //              MOV   rnhi, #0
762            }
763
764            // I64Ctz: CLZ(RBIT(lo)), or 32 + CLZ(RBIT(hi)) when lo == 0.
765            // RBIT/CLZ leave the flags intact, so the CMP's Z survives to the
766            // conditional ADD.
767            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
768                let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
769                w(&mut b, 0xE350_0000 | (lo << 16)); //              CMP    rnlo, #0
770                w(&mut b, 0x16FF_0F30 | (rd_b << 12) | lo); //       RBITNE rd, rnlo
771                w(&mut b, 0x06FF_0F30 | (rd_b << 12) | hi); //       RBITEQ rd, rnhi
772                w(&mut b, 0xE16F_0F10 | (rd_b << 12) | rd_b); //     CLZ    rd, rd
773                w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
774                w(&mut b, 0xE3A0_0000 | (hi << 12)); //              MOV    rnhi, #0
775            }
776
777            // I64Const: MOVW/MOVT per half (MOVT elided when the half fits in
778            // 16 bits, mirroring the Thumb-2 arm).
779            ArmOp::I64Const { rdlo, rdhi, value } => {
780                let lo32 = *value as u32;
781                let hi32 = (*value >> 32) as u32;
782                movw(&mut b, reg_to_bits(rdlo), lo32 & 0xFFFF);
783                if lo32 > 0xFFFF {
784                    movt(&mut b, reg_to_bits(rdlo), lo32 >> 16);
785                }
786                movw(&mut b, reg_to_bits(rdhi), hi32 & 0xFFFF);
787                if hi32 > 0xFFFF {
788                    movt(&mut b, reg_to_bits(rdhi), hi32 >> 16);
789                }
790            }
791
792            // I64Ldr / I64Str: two word accesses at [base, #off] / #off+4.
793            // A register offset is materialized into IP once (the #206/#372
794            // hazard: dropping it would read the wrong address).
795            ArmOp::I64Ldr { rdlo, rdhi, addr } | ArmOp::I64Str { rdlo, rdhi, addr } => {
796                let base = if let Some(rm) = addr.offset_reg {
797                    // ADD ip, base, rm
798                    w(
799                        &mut b,
800                        0xE080_0000
801                            | (reg_to_bits(&addr.base) << 16)
802                            | (12 << 12)
803                            | reg_to_bits(&rm),
804                    );
805                    12
806                } else {
807                    reg_to_bits(&addr.base)
808                };
809                if addr.offset < 0 || addr.offset > 0xFFB {
810                    return Err(synth_core::Error::synthesis(format!(
811                        "i64 load/store offset {} out of the A32 imm12 range (0..=4091) — materialize the offset into a register",
812                        addr.offset
813                    )));
814                }
815                let off = addr.offset as u32;
816                let opc: u32 = if matches!(op, ArmOp::I64Ldr { .. }) {
817                    0xE590_0000 // LDR
818                } else {
819                    0xE580_0000 // STR
820                };
821                w(&mut b, opc | (base << 16) | (reg_to_bits(rdlo) << 12) | off);
822                w(
823                    &mut b,
824                    opc | (base << 16) | (reg_to_bits(rdhi) << 12) | (off + 4),
825                );
826            }
827
828            // I64ExtendI32S: rdlo = rn; rdhi = rdlo >> 31 (arithmetic).
829            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
830                if rdlo != rn {
831                    w(
832                        &mut b,
833                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
834                    );
835                }
836                w(
837                    &mut b,
838                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
839                );
840            }
841
842            // I64ExtendI32U: rdlo = rn; rdhi = 0.
843            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
844                if rdlo != rn {
845                    w(
846                        &mut b,
847                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
848                    );
849                }
850                w(&mut b, 0xE3A0_0000 | (reg_to_bits(rdhi) << 12));
851            }
852
853            // I64Extend8S / I64Extend16S: SXTB/SXTH then sign-fill the high word.
854            ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
855                w(
856                    &mut b,
857                    0xE6AF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
858                );
859                w(
860                    &mut b,
861                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
862                );
863            }
864            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
865                w(
866                    &mut b,
867                    0xE6BF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
868                );
869                w(
870                    &mut b,
871                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
872                );
873            }
874            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
875                if rdlo != rnlo {
876                    w(
877                        &mut b,
878                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
879                    );
880                }
881                w(
882                    &mut b,
883                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rnlo),
884                );
885            }
886
887            // I32WrapI64: take the low word. When rd == rnlo this is a genuine
888            // no-op (the one case where a NOP word is the correct encoding).
889            ArmOp::I32WrapI64 { rd, rnlo } => {
890                w(
891                    &mut b,
892                    0xE1A0_0000 | (reg_to_bits(rd) << 12) | reg_to_bits(rnlo),
893                );
894            }
895
896            // I64Add / I64Sub: the classic pair — ADDS lo + ADC hi (SUBS/SBC).
897            // The selector emits these as separate Adds/Adc ops; the fused
898            // variants are verification-constructed, but they encode for real.
899            ArmOp::I64Add {
900                rdlo,
901                rdhi,
902                rnlo,
903                rnhi,
904                rmlo,
905                rmhi,
906            } => {
907                dp_reg(
908                    &mut b,
909                    0xE090_0000, // ADDS
910                    reg_to_bits(rdlo),
911                    reg_to_bits(rnlo),
912                    reg_to_bits(rmlo),
913                );
914                dp_reg(
915                    &mut b,
916                    0xE0A0_0000, // ADC
917                    reg_to_bits(rdhi),
918                    reg_to_bits(rnhi),
919                    reg_to_bits(rmhi),
920                );
921            }
922            ArmOp::I64Sub {
923                rdlo,
924                rdhi,
925                rnlo,
926                rnhi,
927                rmlo,
928                rmhi,
929            } => {
930                dp_reg(
931                    &mut b,
932                    0xE050_0000, // SUBS
933                    reg_to_bits(rdlo),
934                    reg_to_bits(rnlo),
935                    reg_to_bits(rmlo),
936                );
937                dp_reg(
938                    &mut b,
939                    0xE0C0_0000, // SBC
940                    reg_to_bits(rdhi),
941                    reg_to_bits(rnhi),
942                    reg_to_bits(rmhi),
943                );
944            }
945
946            // I64And / I64Or / I64Xor: two independent word ops.
947            ArmOp::I64And {
948                rdlo,
949                rdhi,
950                rnlo,
951                rnhi,
952                rmlo,
953                rmhi,
954            }
955            | ArmOp::I64Or {
956                rdlo,
957                rdhi,
958                rnlo,
959                rnhi,
960                rmlo,
961                rmhi,
962            }
963            | ArmOp::I64Xor {
964                rdlo,
965                rdhi,
966                rnlo,
967                rnhi,
968                rmlo,
969                rmhi,
970            } => {
971                let base = match op {
972                    ArmOp::I64And { .. } => 0xE000_0000, // AND
973                    ArmOp::I64Or { .. } => 0xE180_0000,  // ORR
974                    _ => 0xE020_0000,                    // EOR
975                };
976                dp_reg(
977                    &mut b,
978                    base,
979                    reg_to_bits(rdlo),
980                    reg_to_bits(rnlo),
981                    reg_to_bits(rmlo),
982                );
983                dp_reg(
984                    &mut b,
985                    base,
986                    reg_to_bits(rdhi),
987                    reg_to_bits(rnhi),
988                    reg_to_bits(rmhi),
989                );
990            }
991
992            // I64DivU: binary long division — A32 transcription of the Thumb-2
993            // #610/#613 arm (fixed-ABI marshal, zero-divisor trap, 64-round
994            // shift-subtract core, quotient to R0:R1, result to rd pair).
995            ArmOp::I64DivU {
996                rdlo,
997                rdhi,
998                rnlo,
999                rnhi,
1000                rmlo,
1001                rmhi,
1002                elide_zero_guard,
1003            } => {
1004                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1005                // #494 phase 2b: elided only under a certificate-discharged
1006                // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
1007                if !elide_zero_guard {
1008                    emit_a32_i64_divisor_zero_trap(&mut b);
1009                }
1010                w(&mut b, 0xE92D_00F0); // PUSH {R4-R7}
1011                for r in 4..8u32 {
1012                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1013                }
1014                div_loop(&mut b, 12); // counter in R12 (encoder scratch)
1015                w(&mut b, 0xE1A0_0004); // MOV R0, R4 (quotient lo)
1016                w(&mut b, 0xE1A0_1005); // MOV R1, R5 (quotient hi)
1017                w(&mut b, 0xE8BD_00F0); // POP {R4-R7}
1018                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1019            }
1020
1021            // I64DivS: sign-extract, unsigned core, conditional negate —
1022            // A32 transcription of the Thumb-2 arm.
1023            ArmOp::I64DivS {
1024                rdlo,
1025                rdhi,
1026                rnlo,
1027                rnhi,
1028                rmlo,
1029                rmhi,
1030                elide_zero_guard,
1031                elide_overflow_guard,
1032            } => {
1033                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1034                // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
1035                // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
1036                // the #633 overflow guard falls ONLY to
1037                // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
1038                // divisor-nonzero fact alone must keep it.
1039                if !elide_zero_guard {
1040                    emit_a32_i64_divisor_zero_trap(&mut b);
1041                }
1042                if !elide_overflow_guard {
1043                    // #633: INT64_MIN / -1 overflows — trap like the i32 path
1044                    // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
1045                    emit_a32_i64_divs_overflow_trap(&mut b);
1046                }
1047                w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1048                w(&mut b, 0xE021_9003); // EOR R9, R1, R3 (result sign in MSB)
1049                skip_negate_if_positive(&mut b, 1);
1050                negate64(&mut b, 0, 1);
1051                skip_negate_if_positive(&mut b, 3);
1052                negate64(&mut b, 2, 3);
1053                for r in 4..8u32 {
1054                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1055                }
1056                div_loop(&mut b, 8); // counter in R8 (saved above)
1057                w(&mut b, 0xE1A0_0004); // MOV R0, R4
1058                w(&mut b, 0xE1A0_1005); // MOV R1, R5
1059                skip_negate_if_positive(&mut b, 9);
1060                negate64(&mut b, 0, 1);
1061                w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1062                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1063            }
1064
1065            // I64RemU: same core as I64DivU, returns the remainder (R6:R7).
1066            ArmOp::I64RemU {
1067                rdlo,
1068                rdhi,
1069                rnlo,
1070                rnhi,
1071                rmlo,
1072                rmhi,
1073                elide_zero_guard,
1074            } => {
1075                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1076                if !elide_zero_guard {
1077                    emit_a32_i64_divisor_zero_trap(&mut b);
1078                }
1079                w(&mut b, 0xE92D_01F0); // PUSH {R4-R8}
1080                for r in 4..8u32 {
1081                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1082                }
1083                div_loop(&mut b, 8);
1084                w(&mut b, 0xE1A0_0006); // MOV R0, R6 (remainder lo)
1085                w(&mut b, 0xE1A0_1007); // MOV R1, R7 (remainder hi)
1086                w(&mut b, 0xE8BD_01F0); // POP {R4-R8}
1087                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1088            }
1089
1090            // I64RemS: remainder takes the DIVIDEND's sign (WASM semantics).
1091            ArmOp::I64RemS {
1092                rdlo,
1093                rdhi,
1094                rnlo,
1095                rnhi,
1096                rmlo,
1097                rmhi,
1098                elide_zero_guard,
1099            } => {
1100                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1101                if !elide_zero_guard {
1102                    emit_a32_i64_divisor_zero_trap(&mut b);
1103                }
1104                w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1105                w(&mut b, 0xE1A0_9001); // MOV R9, R1 (dividend sign)
1106                skip_negate_if_positive(&mut b, 1);
1107                negate64(&mut b, 0, 1);
1108                skip_negate_if_positive(&mut b, 3);
1109                negate64(&mut b, 2, 3);
1110                for r in 4..8u32 {
1111                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1112                }
1113                div_loop(&mut b, 8);
1114                w(&mut b, 0xE1A0_0006); // MOV R0, R6
1115                w(&mut b, 0xE1A0_1007); // MOV R1, R7
1116                skip_negate_if_positive(&mut b, 9);
1117                negate64(&mut b, 0, 1);
1118                w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1119                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1120            }
1121
1122            // Popcnt (i32): bit-twiddle expansion (no native A32 popcount),
1123            // mirroring the Thumb-2 arm's register contract (R11 + R12 as
1124            // scratch, shift-add fold, final AND #0x3F).
1125            ArmOp::Popcnt { rd, rm } => {
1126                let rd_b = reg_to_bits(rd);
1127                if rd != rm {
1128                    w(&mut b, 0xE1A0_0000 | (rd_b << 12) | reg_to_bits(rm)); // MOV rd, rm
1129                }
1130                // x = x - ((x >> 1) & 0x55555555)
1131                movw(&mut b, 12, 0x5555);
1132                movt(&mut b, 12, 0x5555);
1133                shift_imm(&mut b, LSR, 11, rd_b, 1);
1134                dp_reg(&mut b, 0xE000_0000, 11, 11, 12); // AND R11, R11, R12
1135                dp_reg(&mut b, 0xE040_0000, rd_b, rd_b, 11); // SUB rd, rd, R11
1136                // x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
1137                movw(&mut b, 12, 0x3333);
1138                movt(&mut b, 12, 0x3333);
1139                dp_reg(&mut b, 0xE000_0000, 11, rd_b, 12); // AND R11, rd, R12
1140                shift_imm(&mut b, LSR, rd_b, rd_b, 2);
1141                dp_reg(&mut b, 0xE000_0000, rd_b, rd_b, 12); // AND rd, rd, R12
1142                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11); // ADD rd, rd, R11
1143                // x = (x + (x >> 4)) & 0x0F0F0F0F
1144                shift_imm(&mut b, LSR, 11, rd_b, 4);
1145                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11); // ADD rd, rd, R11
1146                movw(&mut b, 12, 0x0F0F);
1147                movt(&mut b, 12, 0x0F0F);
1148                dp_reg(&mut b, 0xE000_0000, rd_b, rd_b, 12); // AND rd, rd, R12
1149                // x += x >> 8; x += x >> 16; x &= 0x3F
1150                shift_imm(&mut b, LSR, 11, rd_b, 8);
1151                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11);
1152                shift_imm(&mut b, LSR, 11, rd_b, 16);
1153                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11);
1154                w(&mut b, 0xE200_003F | (rd_b << 16) | (rd_b << 12)); // AND rd, rd, #63
1155            }
1156
1157            // I64Popcnt: POPCNT(lo) + POPCNT(hi) — A32 transcription of the
1158            // Thumb-2 arm (R3/R4/R5 saved, mul-based per-word fold, high
1159            // result word rnhi cleared last, mirroring the Thumb contract).
1160            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
1161                let hi = reg_to_bits(rnhi);
1162                w(&mut b, 0xE92D_0038); // PUSH {R3, R4, R5}
1163                // #632 audit: route rnlo through R12 so a pair living at
1164                // (R3,R4) cannot read a clobbered R4 (sources read before any
1165                // scratch register they could occupy is written).
1166                w(&mut b, 0xE1A0_C000 | reg_to_bits(rnlo)); // MOV R12, rnlo
1167                w(&mut b, 0xE1A0_5000 | hi); //                MOV R5, rnhi
1168                w(&mut b, 0xE1A0_400C); //                     MOV R4, R12
1169                popcnt_word(&mut b, 4, 3);
1170                popcnt_word(&mut b, 5, 3);
1171                // #632: carry the count across the scratch restore in R12 —
1172                // rd is allocator-assigned and can land inside {R3,R4,R5};
1173                // the old `ADD rd, R4, R5` before the POP was destroyed by
1174                // the restore. R12 is never allocatable and never restored.
1175                dp_reg(&mut b, 0xE080_0000, 12, 4, 5); // ADD R12, R4, R5
1176                w(&mut b, 0xE8BD_0038); // POP {R3, R4, R5}
1177                w(&mut b, 0xE1A0_0000 | (reg_to_bits(rd) << 12) | 12); // MOV rd, R12
1178                w(&mut b, 0xE3A0_0000 | (hi << 12)); // MOV rnhi, #0 (i64 hi word)
1179            }
1180
1181            _ => return Ok(None),
1182        }
1183        Ok(Some(b))
1184    }
1185
1186    fn encode_arm(&self, op: &ArmOp) -> Result<Vec<u8>> {
1187        // #615: A32 multi-instruction expansions (i64 arithmetic/shift/rotate/
1188        // compare, SetCond/SelectMove, popcnt, ...). These ops were literal
1189        // NOPs on the A32 path — user-reachable via `--target cortex-r5` —
1190        // so the value silently vanished. Mirror of the #594 CallIndirect
1191        // early-return: if the expansion helper covers the op, its bytes are
1192        // the encoding.
1193        if let Some(bytes) = self.encode_arm_expanded(op)? {
1194            return Ok(bytes);
1195        }
1196        // #206: ARM32 register-offset loads/stores. `encode_mem_addr` only
1197        // returns the 12-bit immediate, so the immediate-form arms below
1198        // silently DROP `addr.offset_reg` — a runtime address index vanished,
1199        // turning `ldr rd,[rn,rm,#off]` into `ldr rd,[rn,#off]` (the access went
1200        // to the wrong address). Compute the effective base into IP and re-encode
1201        // against `[ip, #off]`, which is uniform for word/byte/halfword/signed.
1202        if let Some(bytes) = self.encode_arm_reg_offset_mem(op)? {
1203            return Ok(bytes);
1204        }
1205        // #594: call_indirect was encoded as a literal NOP on the A32 path
1206        // (`--target cortex-r5`) — the call never happened and the function
1207        // silently returned garbage. Emit the same three-instruction expansion
1208        // as the Thumb-2 path (R11 = function-pointer table base, R12 scratch):
1209        //   MOV r12, idx, LSL #2 ; LDR r12, [r11, r12] ; BLX r12
1210        if let ArmOp::CallIndirect {
1211            table_index_reg,
1212            table_size,
1213            table_byte_offset,
1214            ..
1215        } = op
1216        {
1217            return Ok(Self::encode_arm_call_indirect(
1218                table_index_reg,
1219                *table_size,
1220                *table_byte_offset,
1221            ));
1222        }
1223        let instr: u32 = match op {
1224            // Data processing instructions
1225            ArmOp::Add { rd, rn, op2 } => {
1226                let rd_bits = reg_to_bits(rd);
1227                let rn_bits = reg_to_bits(rn);
1228                let (op2_bits, i_flag) = encode_operand2(op2)?;
1229
1230                // ADD encoding: cond(4) | 00 | I(1) | 0100 | S(1) | Rn(4) | Rd(4) | operand2(12)
1231                0xE0800000 // condition=always(E), opcode=ADD(0100), S=0
1232                    | (i_flag << 25)
1233                    | (rn_bits << 16)
1234                    | (rd_bits << 12)
1235                    | op2_bits
1236            }
1237
1238            ArmOp::Sub { rd, rn, op2 } => {
1239                let rd_bits = reg_to_bits(rd);
1240                let rn_bits = reg_to_bits(rn);
1241                let (op2_bits, i_flag) = encode_operand2(op2)?;
1242
1243                // SUB encoding: opcode=0010
1244                0xE0400000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1245            }
1246
1247            // i64 support: ADDS, ADC, SUBS, SBC for ARM32
1248            ArmOp::Adds { rd, rn, op2 } => {
1249                let rd_bits = reg_to_bits(rd);
1250                let rn_bits = reg_to_bits(rn);
1251                let (op2_bits, i_flag) = encode_operand2(op2)?;
1252
1253                // ADDS encoding: opcode=0100, S=1
1254                0xE0900000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1255            }
1256
1257            ArmOp::Adc { rd, rn, op2 } => {
1258                let rd_bits = reg_to_bits(rd);
1259                let rn_bits = reg_to_bits(rn);
1260                let (op2_bits, i_flag) = encode_operand2(op2)?;
1261
1262                // ADC encoding: opcode=0101
1263                0xE0A00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1264            }
1265
1266            ArmOp::Subs { rd, rn, op2 } => {
1267                let rd_bits = reg_to_bits(rd);
1268                let rn_bits = reg_to_bits(rn);
1269                let (op2_bits, i_flag) = encode_operand2(op2)?;
1270
1271                // SUBS encoding: opcode=0010, S=1
1272                0xE0500000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1273            }
1274
1275            ArmOp::Sbc { rd, rn, op2 } => {
1276                let rd_bits = reg_to_bits(rd);
1277                let rn_bits = reg_to_bits(rn);
1278                let (op2_bits, i_flag) = encode_operand2(op2)?;
1279
1280                // SBC encoding: opcode=0110
1281                0xE0C00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1282            }
1283
1284            ArmOp::Mul { rd, rn, rm } => {
1285                let rd_bits = reg_to_bits(rd);
1286                let rn_bits = reg_to_bits(rn);
1287                let rm_bits = reg_to_bits(rm);
1288
1289                // MUL encoding: cond(4) | 000000 | A(1) | S(1) | Rd(4) | Rn(4) | Rs(4) | 1001 | Rm(4)
1290                0xE0000090 | (rd_bits << 16) | (rn_bits << 8) | rm_bits
1291            }
1292
1293            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
1294                let rdlo_bits = reg_to_bits(rdlo);
1295                let rdhi_bits = reg_to_bits(rdhi);
1296                let rn_bits = reg_to_bits(rn);
1297                let rm_bits = reg_to_bits(rm);
1298
1299                // UMULL encoding: cond(4) | 0000 1000 | RdHi(4) | RdLo(4) | Rm(4) | 1001 | Rn(4)
1300                0xE0800090 | (rdhi_bits << 16) | (rdlo_bits << 12) | (rm_bits << 8) | rn_bits
1301            }
1302
1303            ArmOp::Sdiv { rd, rn, rm } => {
1304                let rd_bits = reg_to_bits(rd);
1305                let rn_bits = reg_to_bits(rn);
1306                let rm_bits = reg_to_bits(rm);
1307
1308                // SDIV encoding: cond(4) | 01110001 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1309                // ARMv7-M and above
1310                0xE710F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1311            }
1312
1313            ArmOp::Udiv { rd, rn, rm } => {
1314                let rd_bits = reg_to_bits(rd);
1315                let rn_bits = reg_to_bits(rn);
1316                let rm_bits = reg_to_bits(rm);
1317
1318                // UDIV encoding: cond(4) | 01110011 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1319                // ARMv7-M and above
1320                0xE730F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1321            }
1322
1323            ArmOp::Mls { rd, rn, rm, ra } => {
1324                let rd_bits = reg_to_bits(rd);
1325                let rn_bits = reg_to_bits(rn);
1326                let rm_bits = reg_to_bits(rm);
1327                let ra_bits = reg_to_bits(ra);
1328
1329                // MLS encoding: cond(4) | 00000110 | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1330                // Rd = Ra - (Rn * Rm)
1331                0xE0600090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1332            }
1333
1334            ArmOp::Mla { rd, rn, rm, ra } => {
1335                let rd_bits = reg_to_bits(rd);
1336                let rn_bits = reg_to_bits(rn);
1337                let rm_bits = reg_to_bits(rm);
1338                let ra_bits = reg_to_bits(ra);
1339
1340                // MLA encoding: cond(4) | 0000001 S | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1341                // Rd = Ra + (Rn * Rm). Base 0xE0200090 (S=0).
1342                0xE0200090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1343            }
1344
1345            ArmOp::And { rd, rn, op2 } => {
1346                let rd_bits = reg_to_bits(rd);
1347                let rn_bits = reg_to_bits(rn);
1348                let (op2_bits, i_flag) = encode_operand2(op2)?;
1349
1350                // AND encoding: opcode=0000
1351                0xE0000000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1352            }
1353
1354            ArmOp::Orr { rd, rn, op2 } => {
1355                let rd_bits = reg_to_bits(rd);
1356                let rn_bits = reg_to_bits(rn);
1357                let (op2_bits, i_flag) = encode_operand2(op2)?;
1358
1359                // ORR encoding: opcode=1100
1360                0xE1800000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1361            }
1362
1363            ArmOp::Eor { rd, rn, op2 } => {
1364                let rd_bits = reg_to_bits(rd);
1365                let rn_bits = reg_to_bits(rn);
1366                let (op2_bits, i_flag) = encode_operand2(op2)?;
1367
1368                // EOR encoding: opcode=0001
1369                0xE0200000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1370            }
1371
1372            // Shift instructions
1373            ArmOp::Lsl { rd, rn, shift } => {
1374                let rd_bits = reg_to_bits(rd);
1375                let rn_bits = reg_to_bits(rn);
1376                let shift_bits = *shift & 0x1F;
1377
1378                // LSL encoding: MOV with shift
1379                0xE1A00000 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1380            }
1381
1382            ArmOp::Lsr { rd, rn, shift } => {
1383                let rd_bits = reg_to_bits(rd);
1384                let rn_bits = reg_to_bits(rn);
1385                let shift_bits = *shift & 0x1F;
1386
1387                // LSR encoding
1388                0xE1A00020 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1389            }
1390
1391            ArmOp::Asr { rd, rn, shift } => {
1392                let rd_bits = reg_to_bits(rd);
1393                let rn_bits = reg_to_bits(rn);
1394                let shift_bits = *shift & 0x1F;
1395
1396                // ASR encoding
1397                0xE1A00040 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1398            }
1399
1400            ArmOp::Ror { rd, rn, shift } => {
1401                let rd_bits = reg_to_bits(rd);
1402                let rn_bits = reg_to_bits(rn);
1403                let shift_bits = *shift & 0x1F;
1404
1405                // ROR encoding: MOV with ROR shift
1406                0xE1A00060 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1407            }
1408
1409            // Register-based shifts (ARM32)
1410            // LSL Rd, Rn, Rm: cond 0001101S 0000 Rd Rs 0001 Rn
1411            ArmOp::LslReg { rd, rn, rm } => {
1412                let rd_bits = reg_to_bits(rd);
1413                let rn_bits = reg_to_bits(rn);
1414                let rm_bits = reg_to_bits(rm);
1415                0xE1A00010 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1416            }
1417            ArmOp::LsrReg { 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                0xE1A00030 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1422            }
1423            ArmOp::AsrReg { rd, rn, rm } => {
1424                let rd_bits = reg_to_bits(rd);
1425                let rn_bits = reg_to_bits(rn);
1426                let rm_bits = reg_to_bits(rm);
1427                0xE1A00050 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1428            }
1429            ArmOp::RorReg { rd, rn, rm } => {
1430                let rd_bits = reg_to_bits(rd);
1431                let rn_bits = reg_to_bits(rn);
1432                let rm_bits = reg_to_bits(rm);
1433                0xE1A00070 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1434            }
1435
1436            // RSB (Reverse Subtract): Rd = imm - Rn
1437            ArmOp::Rsb { rd, rn, imm } => {
1438                let rd_bits = reg_to_bits(rd);
1439                let rn_bits = reg_to_bits(rn);
1440                // RSB encoding: cond(4) | 00 1 0011 S | Rn(4) | Rd(4) | imm12
1441                // Opcode for RSB = 0011, I=1 (immediate), S=0
1442                0xE2600000 | (rn_bits << 16) | (rd_bits << 12) | (*imm & 0xFF)
1443            }
1444
1445            // Bit manipulation instructions
1446            ArmOp::Clz { rd, rm } => {
1447                let rd_bits = reg_to_bits(rd);
1448                let rm_bits = reg_to_bits(rm);
1449
1450                // CLZ encoding: cond(4) | 00010110 | 1111 | Rd(4) | 1111 | 0001 | Rm(4)
1451                // ARMv5T and above
1452                0xE16F0F10 | (rd_bits << 12) | rm_bits
1453            }
1454
1455            ArmOp::Rbit { rd, rm } => {
1456                let rd_bits = reg_to_bits(rd);
1457                let rm_bits = reg_to_bits(rm);
1458
1459                // RBIT encoding: cond(4) | 01101111 | 1111 | Rd(4) | 1111 | 0011 | Rm(4)
1460                // ARMv6T2 and above
1461                0xE6FF0F30 | (rd_bits << 12) | rm_bits
1462            }
1463
1464            ArmOp::Sxtb { rd, rm } => {
1465                let rd_bits = reg_to_bits(rd);
1466                let rm_bits = reg_to_bits(rm);
1467
1468                // SXTB encoding: cond(4) | 01101010 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1469                // ARMv6 and above. rotate=00 for no rotation
1470                0xE6AF0070 | (rd_bits << 12) | rm_bits
1471            }
1472
1473            ArmOp::Sxth { rd, rm } => {
1474                let rd_bits = reg_to_bits(rd);
1475                let rm_bits = reg_to_bits(rm);
1476
1477                // SXTH encoding: cond(4) | 01101011 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1478                // ARMv6 and above. rotate=00 for no rotation
1479                0xE6BF0070 | (rd_bits << 12) | rm_bits
1480            }
1481
1482            ArmOp::Uxtb { rd, rm } => {
1483                let rd_bits = reg_to_bits(rd);
1484                let rm_bits = reg_to_bits(rm);
1485                // UXTB encoding: cond | 01101110 1111 Rd rotate 00 0111 Rm (rotate=00)
1486                0xE6EF0070 | (rd_bits << 12) | rm_bits
1487            }
1488
1489            ArmOp::Uxth { rd, rm } => {
1490                let rd_bits = reg_to_bits(rd);
1491                let rm_bits = reg_to_bits(rm);
1492                // UXTH encoding: cond | 01101111 1111 Rd rotate 00 0111 Rm (rotate=00)
1493                0xE6FF0070 | (rd_bits << 12) | rm_bits
1494            }
1495
1496            // Move instructions
1497            ArmOp::Mov { rd, op2 } => {
1498                let rd_bits = reg_to_bits(rd);
1499                let (op2_bits, i_flag) = encode_operand2(op2)?;
1500
1501                // MOV encoding: opcode=1101
1502                0xE1A00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1503            }
1504
1505            ArmOp::Mvn { rd, op2 } => {
1506                let rd_bits = reg_to_bits(rd);
1507                let (op2_bits, i_flag) = encode_operand2(op2)?;
1508
1509                // MVN encoding: opcode=1111
1510                0xE1E00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1511            }
1512
1513            // MOVW - Move Wide (ARM32)
1514            // Encoding: cond(4) | 0011 0000 | imm4(4) | Rd(4) | imm12(12)
1515            ArmOp::Movw { rd, imm16 } => {
1516                let rd_bits = reg_to_bits(rd);
1517                let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1518                let imm12 = (*imm16 as u32) & 0xFFF;
1519                0xE3000000 | (imm4 << 16) | (rd_bits << 12) | imm12
1520            }
1521
1522            // MOVT - Move Top (ARM32)
1523            // Encoding: cond(4) | 0011 0100 | imm4(4) | Rd(4) | imm12(12)
1524            ArmOp::Movt { rd, imm16 } => {
1525                let rd_bits = reg_to_bits(rd);
1526                let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1527                let imm12 = (*imm16 as u32) & 0xFFF;
1528                0xE3400000 | (imm4 << 16) | (rd_bits << 12) | imm12
1529            }
1530
1531            // #237: symbol-relative MOVW/MOVT (ARM mode) — addend in place, the
1532            // backend records the MOVW_ABS/MOVT_ABS relocation against `symbol`.
1533            ArmOp::MovwSym { rd, addend, .. } => {
1534                let rd_bits = reg_to_bits(rd);
1535                let v = (*addend as u32) & 0xffff;
1536                0xE3000000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1537            }
1538            ArmOp::MovtSym { rd, addend, .. } => {
1539                let rd_bits = reg_to_bits(rd);
1540                let v = ((*addend as u32) >> 16) & 0xffff;
1541                0xE3400000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1542            }
1543
1544            // #345: LdrSym is the Thumb-2 literal-pool address load. A32 mode is
1545            // not used for relocatable native-pointer objects; fail loudly rather
1546            // than miscompile if it is ever reached here.
1547            ArmOp::LdrSym { .. } => {
1548                return Err(synth_core::Error::synthesis(
1549                    "LdrSym (literal-pool address load) is Thumb-2-only",
1550                ));
1551            }
1552
1553            // Compare
1554            ArmOp::Cmp { rn, op2 } => {
1555                let rn_bits = reg_to_bits(rn);
1556                let (op2_bits, i_flag) = encode_operand2(op2)?;
1557
1558                // CMP encoding: opcode=1010, S=1
1559                0xE1500000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1560            }
1561
1562            // Compare Negative (CMN) - computes Rn + op2 and sets flags
1563            ArmOp::Cmn { rn, op2 } => {
1564                let rn_bits = reg_to_bits(rn);
1565                let (op2_bits, i_flag) = encode_operand2(op2)?;
1566
1567                // CMN encoding: opcode=1011, S=1
1568                0xE1700000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1569            }
1570
1571            // Load/Store
1572            ArmOp::Ldr { rd, addr } => {
1573                let rd_bits = reg_to_bits(rd);
1574                let (base_bits, offset_bits) = encode_mem_addr(addr);
1575
1576                // LDR encoding: cond(4) | 01 | I(1) | P(1) | U(1) | B(1) | W(1) | L(1) | Rn(4) | Rd(4) | offset(12)
1577                // P=1 (pre-indexed), U=1 (add offset), L=1 (load)
1578                0xE5900000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1579            }
1580
1581            ArmOp::Str { rd, addr } => {
1582                let rd_bits = reg_to_bits(rd);
1583                let (base_bits, offset_bits) = encode_mem_addr(addr);
1584
1585                // STR encoding: L=0 (store)
1586                0xE5800000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1587            }
1588
1589            // Sub-word loads (ARM32 encoding)
1590            ArmOp::Ldrb { rd, addr } => {
1591                let rd_bits = reg_to_bits(rd);
1592                let (base_bits, offset_bits) = encode_mem_addr(addr);
1593                // LDRB: LDR with B=1 (byte): cond|01|I|P|U|1|W|L|Rn|Rd|offset
1594                0xE5D00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1595            }
1596
1597            ArmOp::Ldrsb { rd, addr } => {
1598                let rd_bits = reg_to_bits(rd);
1599                let (base_bits, offset_bits) = encode_mem_addr(addr);
1600                // LDRSB (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1101|imm4L
1601                // Simplified with immediate offset
1602                let offset_val = offset_bits & 0xFF;
1603                let imm4h = (offset_val >> 4) & 0xF;
1604                let imm4l = offset_val & 0xF;
1605                0xE1D000D0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1606            }
1607
1608            ArmOp::Ldrh { rd, addr } => {
1609                let rd_bits = reg_to_bits(rd);
1610                let (base_bits, offset_bits) = encode_mem_addr(addr);
1611                // LDRH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1011|imm4L
1612                let offset_val = offset_bits & 0xFF;
1613                let imm4h = (offset_val >> 4) & 0xF;
1614                let imm4l = offset_val & 0xF;
1615                0xE1D000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1616            }
1617
1618            ArmOp::Ldrsh { rd, addr } => {
1619                let rd_bits = reg_to_bits(rd);
1620                let (base_bits, offset_bits) = encode_mem_addr(addr);
1621                // LDRSH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1111|imm4L
1622                let offset_val = offset_bits & 0xFF;
1623                let imm4h = (offset_val >> 4) & 0xF;
1624                let imm4l = offset_val & 0xF;
1625                0xE1D000F0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1626            }
1627
1628            // Sub-word stores (ARM32 encoding)
1629            ArmOp::Strb { rd, addr } => {
1630                let rd_bits = reg_to_bits(rd);
1631                let (base_bits, offset_bits) = encode_mem_addr(addr);
1632                // STRB: STR with B=1 (byte): cond|01|I|P|U|1|W|0|Rn|Rd|offset
1633                0xE5C00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1634            }
1635
1636            ArmOp::Strh { rd, addr } => {
1637                let rd_bits = reg_to_bits(rd);
1638                let (base_bits, offset_bits) = encode_mem_addr(addr);
1639                // STRH (misc store): cond|000|P|U|1|W|0|Rn|Rd|imm4H|1011|imm4L
1640                let offset_val = offset_bits & 0xFF;
1641                let imm4h = (offset_val >> 4) & 0xF;
1642                let imm4l = offset_val & 0xF;
1643                0xE1C000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1644            }
1645
1646            // Memory management (ARM32 encoding)
1647            ArmOp::MemorySize { rd } => {
1648                let rd_bits = reg_to_bits(rd);
1649                // MOV rd, R10, LSR #16  (memory size in bytes / 65536 = pages)
1650                // cond|000|1101|S|0000|Rd|shift5|type|0|Rm
1651                // LSR #16: shift5=10000, type=01
1652                0xE1A00820 | (rd_bits << 12) | 0x0A // Rm=R10, shift=16, LSR
1653            }
1654
1655            ArmOp::MemoryGrow { rd, .. } => {
1656                let rd_bits = reg_to_bits(rd);
1657                // On embedded, always fail: MOV rd, #-1
1658                0xE3E00000 | (rd_bits << 12) // MVN rd, #0 = MOV rd, #-1
1659            }
1660
1661            // Label pseudo-instruction: emits no machine code
1662            ArmOp::Label { .. } => {
1663                return Ok(Vec::new());
1664            }
1665
1666            // Branch instructions
1667            ArmOp::B { label: _ } => {
1668                // B encoding: cond(4) | 1010 | offset(24)
1669                // Simplified: branch to offset 0 (will be patched by linker/resolver)
1670                0xEA000000
1671            }
1672
1673            // Conditional branch to label (generic)
1674            ArmOp::Bcc { cond, label: _ } => {
1675                use synth_synthesis::Condition;
1676                let cond_bits: u32 = match cond {
1677                    Condition::EQ => 0x0,
1678                    Condition::NE => 0x1,
1679                    Condition::HS => 0x2,
1680                    Condition::LO => 0x3,
1681                    Condition::HI => 0x8,
1682                    Condition::LS => 0x9,
1683                    Condition::GE => 0xA,
1684                    Condition::LT => 0xB,
1685                    Condition::GT => 0xC,
1686                    Condition::LE => 0xD,
1687                };
1688                // B<cond> with offset 0 (will be patched)
1689                (cond_bits << 28) | 0x0A000000
1690            }
1691
1692            // BHS (Branch if Higher or Same) - used for bounds checking
1693            ArmOp::Bhs { label: _ } => {
1694                // BHS encoding: cond(2=HS) | 1010 | offset(24)
1695                0x2A000000 // BHS with offset 0
1696            }
1697
1698            // BLO (Branch if Lower) - complementary to BHS
1699            ArmOp::Blo { label: _ } => {
1700                // BLO encoding: cond(3=LO) | 1010 | offset(24)
1701                0x3A000000 // BLO with offset 0
1702            }
1703
1704            // Branch with numeric offset (in instructions)
1705            // ARM32 B instruction: offset is in instructions, stored as words
1706            // The offset is relative to PC+8 (due to ARM pipeline)
1707            ArmOp::BOffset { offset } => {
1708                // B encoding: cond(4) | 1010 | offset(24)
1709                // Offset is signed, in words (4-byte units)
1710                // ARM adds PC+8 to the offset, so we need to adjust:
1711                // target = PC + 8 + (offset * 4)
1712                // For backward branch of N instructions: offset = -(N + 2)
1713                // wrapping_sub keeps the encoder total under fuzzing (#186): an
1714                // extreme i32::MIN offset would otherwise overflow-panic; for any
1715                // real branch offset this is identical to `- 2`.
1716                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1717                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1718                0xEA000000 | offset_bits
1719            }
1720
1721            // Conditional branch with numeric offset
1722            ArmOp::BCondOffset { cond, offset } => {
1723                use synth_synthesis::Condition;
1724                let cond_bits: u32 = match cond {
1725                    Condition::EQ => 0x0,
1726                    Condition::NE => 0x1,
1727                    Condition::HS => 0x2,
1728                    Condition::LO => 0x3,
1729                    Condition::HI => 0x8,
1730                    Condition::LS => 0x9,
1731                    Condition::GE => 0xA,
1732                    Condition::LT => 0xB,
1733                    Condition::GT => 0xC,
1734                    Condition::LE => 0xD,
1735                };
1736                // B<cond> encoding: cond(4) | 1010 | offset(24)
1737                // wrapping_sub: total under fuzzing (#186), identical for real offsets.
1738                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1739                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1740                (cond_bits << 28) | 0x0A000000 | offset_bits
1741            }
1742
1743            ArmOp::Bl { label: _ } => {
1744                // BL encoding: cond(4) | 1011 | offset(24)
1745                0xEB000000
1746            }
1747
1748            ArmOp::Bx { rm } => {
1749                let rm_bits = reg_to_bits(rm);
1750
1751                // BX encoding: cond(4) | 000100101111111111110001 | Rm(4)
1752                0xE12FFF10 | rm_bits
1753            }
1754
1755            ArmOp::Blx { rm } => {
1756                let rm_bits = reg_to_bits(rm);
1757
1758                // BLX (register) encoding: cond(4) | 000100101111111111110011 | Rm(4)
1759                0xE12FFF30 | rm_bits
1760            }
1761
1762            ArmOp::Push { regs } => {
1763                // STMDB SP!, {regs} encoding: cond(4) | 100100 | 10 | 1101 | register_list(16)
1764                let mut reg_list: u32 = 0;
1765                for r in regs {
1766                    reg_list |= 1 << reg_to_bits(r);
1767                }
1768                0xE92D0000 | reg_list
1769            }
1770
1771            ArmOp::Pop { regs } => {
1772                // LDMIA SP!, {regs} encoding: cond(4) | 100010 | 11 | 1101 | register_list(16)
1773                let mut reg_list: u32 = 0;
1774                for r in regs {
1775                    reg_list |= 1 << reg_to_bits(r);
1776                }
1777                0xE8BD0000 | reg_list
1778            }
1779
1780            ArmOp::Nop => {
1781                // NOP encoding: MOV R0, R0
1782                0xE1A00000
1783            }
1784
1785            ArmOp::Udf { imm } => {
1786                // UDF (Undefined) encoding in ARM: 0xE7F000F0 | (imm12_hi << 8) | imm4_lo
1787                // We only use imm8, so split into imm4_hi and imm4_lo
1788                let imm8 = *imm as u32;
1789                0xE7F000F0 | ((imm8 & 0xF0) << 4) | (imm8 & 0x0F)
1790            }
1791
1792            // #615: handled by the `encode_arm_expanded` early return at the
1793            // top of this function — a real MOV{cond}/MOV pair now, never a
1794            // silent NOP again.
1795            ArmOp::Popcnt { .. } | ArmOp::SetCond { .. } | ArmOp::SelectMove { .. } => {
1796                unreachable!("handled by encode_arm_expanded (#615)")
1797            }
1798
1799            // Verification-only pseudo-ops: `synth-verify`'s ArmSemantics
1800            // models these, but NO codegen path constructs them (the selector
1801            // lowers select/locals/globals/br_table/call to real instruction
1802            // sequences before the encoder). Encoding one as a NOP silently
1803            // dropped the operation (#615 class); a typed Err keeps the
1804            // encoder total (Ok-or-Err, the `encoder_no_panic` contract)
1805            // while making any future reachability LOUD.
1806            ArmOp::Select { .. }
1807            | ArmOp::LocalGet { .. }
1808            | ArmOp::LocalSet { .. }
1809            | ArmOp::LocalTee { .. }
1810            | ArmOp::GlobalGet { .. }
1811            | ArmOp::GlobalSet { .. }
1812            | ArmOp::BrTable { .. }
1813            | ArmOp::Call { .. } => {
1814                return Err(synth_core::Error::synthesis(format!(
1815                    "verification-only pseudo-op {op:?} reached the A32 encoder — \
1816                     codegen lowers it before encoding; refusing to emit a silent NOP (#615)"
1817                )));
1818            }
1819
1820            // #594: CallIndirect is expanded to a real multi-instruction
1821            // sequence by the early return at the top of this function —
1822            // it must NEVER fall through to a silent NOP again.
1823            ArmOp::CallIndirect { .. } => {
1824                unreachable!("CallIndirect handled by encode_arm_call_indirect (#594)")
1825            }
1826
1827            // #615: every i64 op (and I32WrapI64) is expanded to a real A32
1828            // multi-instruction sequence by `encode_arm_expanded` — the
1829            // "encode as NOP for now" era ended with the value silently
1830            // vanishing on `--target cortex-r5`.
1831            ArmOp::I64Add { .. }
1832            | ArmOp::I64Sub { .. }
1833            | ArmOp::I64DivS { .. }
1834            | ArmOp::I64DivU { .. }
1835            | ArmOp::I64RemS { .. }
1836            | ArmOp::I64RemU { .. }
1837            | ArmOp::I64Clz { .. }
1838            | ArmOp::I64Ctz { .. }
1839            | ArmOp::I64Popcnt { .. }
1840            | ArmOp::I64And { .. }
1841            | ArmOp::I64Or { .. }
1842            | ArmOp::I64Xor { .. }
1843            | ArmOp::I64Eqz { .. }
1844            | ArmOp::I64Eq { .. }
1845            | ArmOp::I64Ne { .. }
1846            | ArmOp::I64LtS { .. }
1847            | ArmOp::I64LtU { .. }
1848            | ArmOp::I64LeS { .. }
1849            | ArmOp::I64LeU { .. }
1850            | ArmOp::I64GtS { .. }
1851            | ArmOp::I64GtU { .. }
1852            | ArmOp::I64GeS { .. }
1853            | ArmOp::I64GeU { .. }
1854            | ArmOp::I64Const { .. }
1855            | ArmOp::I64Ldr { .. }
1856            | ArmOp::I64Str { .. }
1857            | ArmOp::I64ExtendI32S { .. }
1858            | ArmOp::I64ExtendI32U { .. }
1859            | ArmOp::I64Extend8S { .. }
1860            | ArmOp::I64Extend16S { .. }
1861            | ArmOp::I64Extend32S { .. }
1862            | ArmOp::I32WrapI64 { .. } => {
1863                unreachable!("handled by encode_arm_expanded (#615)")
1864            }
1865
1866            // f32 VFP single-precision instructions
1867            ArmOp::F32Add { sd, sn, sm } => encode_vfp_3reg(0xEE300A00, sd, sn, sm)?,
1868            ArmOp::F32Sub { sd, sn, sm } => encode_vfp_3reg(0xEE300A40, sd, sn, sm)?,
1869            ArmOp::F32Mul { sd, sn, sm } => encode_vfp_3reg(0xEE200A00, sd, sn, sm)?,
1870            ArmOp::F32Div { sd, sn, sm } => encode_vfp_3reg(0xEE800A00, sd, sn, sm)?,
1871            ArmOp::F32Abs { sd, sm } => encode_vfp_2reg(0xEEB00AC0, sd, sm)?,
1872            ArmOp::F32Neg { sd, sm } => encode_vfp_2reg(0xEEB10A40, sd, sm)?,
1873            ArmOp::F32Sqrt { sd, sm } => encode_vfp_2reg(0xEEB10AC0, sd, sm)?,
1874
1875            // f32 pseudo-ops — multi-instruction sequences
1876            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
1877            ArmOp::F32Ceil { sd, sm } => {
1878                return self.encode_arm_f32_rounding(sd, sm, 0b01); // Round toward +Inf
1879            }
1880            ArmOp::F32Floor { sd, sm } => {
1881                return self.encode_arm_f32_rounding(sd, sm, 0b10); // Round toward -Inf
1882            }
1883            ArmOp::F32Trunc { sd, sm } => {
1884                return self.encode_arm_f32_rounding(sd, sm, 0b11); // VCVT toward zero
1885            }
1886            ArmOp::F32Nearest { sd, sm } => {
1887                return self.encode_arm_f32_rounding(sd, sm, 0b00); // VCVT to nearest
1888            }
1889            ArmOp::F32Min { sd, sn, sm } => {
1890                return self.encode_arm_f32_minmax(sd, sn, sm, true);
1891            }
1892            ArmOp::F32Max { sd, sn, sm } => {
1893                return self.encode_arm_f32_minmax(sd, sn, sm, false);
1894            }
1895            ArmOp::F32Copysign { sd, sn, sm } => {
1896                return self.encode_arm_f32_copysign(sd, sn, sm);
1897            }
1898
1899            // f32 comparisons — multi-instruction: VCMP + VMRS + conditional MOV
1900            ArmOp::F32Eq { rd, sn, sm } => {
1901                return self.encode_arm_f32_compare(rd, sn, sm, 0x0); // EQ
1902            }
1903            ArmOp::F32Ne { rd, sn, sm } => {
1904                return self.encode_arm_f32_compare(rd, sn, sm, 0x1); // NE
1905            }
1906            ArmOp::F32Lt { rd, sn, sm } => {
1907                return self.encode_arm_f32_compare(rd, sn, sm, 0x4); // MI (less than)
1908            }
1909            ArmOp::F32Le { rd, sn, sm } => {
1910                return self.encode_arm_f32_compare(rd, sn, sm, 0x9); // LS (less or same)
1911            }
1912            ArmOp::F32Gt { rd, sn, sm } => {
1913                return self.encode_arm_f32_compare(rd, sn, sm, 0xC); // GT
1914            }
1915            ArmOp::F32Ge { rd, sn, sm } => {
1916                return self.encode_arm_f32_compare(rd, sn, sm, 0xA); // GE
1917            }
1918
1919            // f32 const — multi-instruction: MOVW + MOVT + VMOV
1920            ArmOp::F32Const { sd, value } => {
1921                return self.encode_arm_f32_const(sd, *value);
1922            }
1923
1924            ArmOp::F32Load { sd, addr } => encode_vfp_ldst(0xED900A00, sd, addr)?,
1925            ArmOp::F32Store { sd, addr } => encode_vfp_ldst(0xED800A00, sd, addr)?,
1926
1927            // f32 conversions — multi-instruction sequences
1928            ArmOp::F32ConvertI32S { sd, rm } => {
1929                return self.encode_arm_f32_convert_i32(sd, rm, true);
1930            }
1931            ArmOp::F32ConvertI32U { sd, rm } => {
1932                return self.encode_arm_f32_convert_i32(sd, rm, false);
1933            }
1934            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
1935                return Err(synth_core::Error::synthesis(
1936                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
1937                ));
1938            }
1939            ArmOp::F32ReinterpretI32 { sd, rm } => encode_vmov_core_sreg(true, sd, rm)?,
1940            ArmOp::I32ReinterpretF32 { rd, sm } => encode_vmov_core_sreg(false, sm, rd)?,
1941            ArmOp::I32TruncF32S { rd, sm } => {
1942                return self.encode_arm_i32_trunc_f32(rd, sm, true);
1943            }
1944            ArmOp::I32TruncF32U { rd, sm } => {
1945                return self.encode_arm_i32_trunc_f32(rd, sm, false);
1946            }
1947
1948            // f64 VFP double-precision instructions (ARM32)
1949            // F64 arithmetic: same as F32 but with sz=1 (bit 8 = 1, cp11 = 0xB)
1950            ArmOp::F64Add { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B00, dd, dn, dm)?,
1951            ArmOp::F64Sub { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B40, dd, dn, dm)?,
1952            ArmOp::F64Mul { dd, dn, dm } => encode_vfp_3reg_f64(0xEE200B00, dd, dn, dm)?,
1953            ArmOp::F64Div { dd, dn, dm } => encode_vfp_3reg_f64(0xEE800B00, dd, dn, dm)?,
1954            ArmOp::F64Abs { dd, dm } => encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?,
1955            ArmOp::F64Neg { dd, dm } => encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?,
1956            ArmOp::F64Sqrt { dd, dm } => encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?,
1957
1958            // f64 pseudo-ops
1959            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
1960            ArmOp::F64Ceil { dd, dm } => {
1961                return self.encode_arm_f64_rounding(dd, dm, 0b01);
1962            }
1963            ArmOp::F64Floor { dd, dm } => {
1964                return self.encode_arm_f64_rounding(dd, dm, 0b10);
1965            }
1966            ArmOp::F64Trunc { dd, dm } => {
1967                return self.encode_arm_f64_rounding(dd, dm, 0b11);
1968            }
1969            ArmOp::F64Nearest { dd, dm } => {
1970                return self.encode_arm_f64_rounding(dd, dm, 0b00);
1971            }
1972            ArmOp::F64Min { dd, dn, dm } => {
1973                return self.encode_arm_f64_minmax(dd, dn, dm, true);
1974            }
1975            ArmOp::F64Max { dd, dn, dm } => {
1976                return self.encode_arm_f64_minmax(dd, dn, dm, false);
1977            }
1978            ArmOp::F64Copysign { dd, dn, dm } => {
1979                return self.encode_arm_f64_copysign(dd, dn, dm);
1980            }
1981
1982            // f64 comparisons
1983            ArmOp::F64Eq { rd, dn, dm } => {
1984                return self.encode_arm_f64_compare(rd, dn, dm, 0x0);
1985            }
1986            ArmOp::F64Ne { rd, dn, dm } => {
1987                return self.encode_arm_f64_compare(rd, dn, dm, 0x1);
1988            }
1989            ArmOp::F64Lt { rd, dn, dm } => {
1990                return self.encode_arm_f64_compare(rd, dn, dm, 0x4);
1991            }
1992            ArmOp::F64Le { rd, dn, dm } => {
1993                return self.encode_arm_f64_compare(rd, dn, dm, 0x9);
1994            }
1995            ArmOp::F64Gt { rd, dn, dm } => {
1996                return self.encode_arm_f64_compare(rd, dn, dm, 0xC);
1997            }
1998            ArmOp::F64Ge { rd, dn, dm } => {
1999                return self.encode_arm_f64_compare(rd, dn, dm, 0xA);
2000            }
2001
2002            ArmOp::F64Const { dd, value } => {
2003                return self.encode_arm_f64_const(dd, *value);
2004            }
2005
2006            ArmOp::F64Load { dd, addr } => encode_vfp_ldst_f64(0xED900B00, dd, addr)?,
2007            ArmOp::F64Store { dd, addr } => encode_vfp_ldst_f64(0xED800B00, dd, addr)?,
2008
2009            ArmOp::F64ConvertI32S { dd, rm } => {
2010                return self.encode_arm_f64_convert_i32(dd, rm, true);
2011            }
2012            ArmOp::F64ConvertI32U { dd, rm } => {
2013                return self.encode_arm_f64_convert_i32(dd, rm, false);
2014            }
2015            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
2016                return Err(synth_core::Error::synthesis(
2017                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2018                ));
2019            }
2020            ArmOp::F64PromoteF32 { dd, sm } => {
2021                return self.encode_arm_f64_promote_f32(dd, sm);
2022            }
2023            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => {
2024                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?
2025            }
2026            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => {
2027                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?
2028            }
2029            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
2030                return Err(synth_core::Error::synthesis(
2031                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
2032                ));
2033            }
2034            ArmOp::I32TruncF64S { rd, dm } => {
2035                return self.encode_arm_i32_trunc_f64(rd, dm, true);
2036            }
2037            ArmOp::I32TruncF64U { rd, dm } => {
2038                return self.encode_arm_i32_trunc_f64(rd, dm, false);
2039            }
2040            // #615: multi-instruction i64 sequences — expanded to real A32 by
2041            // `encode_arm_expanded`, no longer "Thumb-2 only" NOPs.
2042            ArmOp::I64SetCond { .. }
2043            | ArmOp::I64SetCondZ { .. }
2044            | ArmOp::I64Mul { .. }
2045            | ArmOp::I64Shl { .. }
2046            | ArmOp::I64ShrS { .. }
2047            | ArmOp::I64ShrU { .. }
2048            | ArmOp::I64Rotl { .. }
2049            | ArmOp::I64Rotr { .. } => {
2050                unreachable!("handled by encode_arm_expanded (#615)")
2051            }
2052
2053            // MVE instructions — Thumb-2 only (Cortex-M55 is always Thumb-2)
2054            ArmOp::MveLoad { .. }
2055            | ArmOp::MveStore { .. }
2056            | ArmOp::MveConst { .. }
2057            | ArmOp::MveAnd { .. }
2058            | ArmOp::MveOrr { .. }
2059            | ArmOp::MveEor { .. }
2060            | ArmOp::MveMvn { .. }
2061            | ArmOp::MveBic { .. }
2062            | ArmOp::MveAddI { .. }
2063            | ArmOp::MveSubI { .. }
2064            | ArmOp::MveMulI { .. }
2065            | ArmOp::MveNegI { .. }
2066            | ArmOp::MveCmpEqI { .. }
2067            | ArmOp::MveCmpNeI { .. }
2068            | ArmOp::MveCmpLtS { .. }
2069            | ArmOp::MveCmpLtU { .. }
2070            | ArmOp::MveCmpGtS { .. }
2071            | ArmOp::MveCmpGtU { .. }
2072            | ArmOp::MveCmpLeS { .. }
2073            | ArmOp::MveCmpLeU { .. }
2074            | ArmOp::MveCmpGeS { .. }
2075            | ArmOp::MveCmpGeU { .. }
2076            | ArmOp::MveDup { .. }
2077            | ArmOp::MveExtractLane { .. }
2078            | ArmOp::MveInsertLane { .. }
2079            | ArmOp::MveAddF32 { .. }
2080            | ArmOp::MveSubF32 { .. }
2081            | ArmOp::MveMulF32 { .. }
2082            | ArmOp::MveNegF32 { .. }
2083            | ArmOp::MveAbsF32 { .. }
2084            | ArmOp::MveCmpEqF32 { .. }
2085            | ArmOp::MveCmpNeF32 { .. }
2086            | ArmOp::MveCmpLtF32 { .. }
2087            | ArmOp::MveCmpLeF32 { .. }
2088            | ArmOp::MveCmpGtF32 { .. }
2089            | ArmOp::MveCmpGeF32 { .. }
2090            | ArmOp::MveDupF32 { .. }
2091            | ArmOp::MveExtractLaneF32 { .. }
2092            | ArmOp::MveReplaceLaneF32 { .. }
2093            | ArmOp::MveDivF32 { .. }
2094            | ArmOp::MveSqrtF32 { .. } => {
2095                // MVE (Helium) is a Thumb-2-only extension (Cortex-M55); there
2096                // is no A32 encoding. The selector only emits MVE ops for
2097                // Thumb targets — a NOP here silently dropped the vector op
2098                // if that invariant ever broke (#615 class). Err keeps the
2099                // encoder total and the failure loud.
2100                return Err(synth_core::Error::synthesis(format!(
2101                    "MVE op {op:?} has no A32 (ARM-mode) encoding — MVE is Thumb-2 only (#615)"
2102                )));
2103            }
2104        };
2105
2106        // ARM32 instructions are little-endian
2107        Ok(instr.to_le_bytes().to_vec())
2108    }
2109
2110    // === ARM32 VFP multi-instruction helpers ===
2111
2112    /// Encode F32 comparison as ARM32: VCMP.F32 + VMRS + MOV rd,#0 + MOVcond rd,#1
2113    fn encode_arm_f32_compare(
2114        &self,
2115        rd: &Reg,
2116        sn: &VfpReg,
2117        sm: &VfpReg,
2118        cond_code: u32,
2119    ) -> Result<Vec<u8>> {
2120        let mut bytes = Vec::new();
2121
2122        // VCMP.F32 Sn, Sm: 0xEEB40A40 with Sn in Vd position, Sm in Vm position
2123        let sn_num = vfp_sreg_to_num(sn)?;
2124        let sm_num = vfp_sreg_to_num(sm)?;
2125        let (vd, d) = encode_sreg(sn_num);
2126        let (vm, m) = encode_sreg(sm_num);
2127        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2128        bytes.extend_from_slice(&vcmp.to_le_bytes());
2129
2130        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
2131        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2132
2133        // MOV rd, #0: 0xE3A0_0000 | (rd << 12)
2134        let rd_bits = reg_to_bits(rd);
2135        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2136        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2137
2138        // MOVcond rd, #1: cond(4) | 0011 1010 0000 rd(4) 0000 0000 0001
2139        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2140        bytes.extend_from_slice(&mov_one.to_le_bytes());
2141
2142        Ok(bytes)
2143    }
2144
2145    /// Encode F32 constant load as ARM32: MOVW Rt,#lo16 + MOVT Rt,#hi16 + VMOV Sd,Rt
2146    fn encode_arm_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
2147        let mut bytes = Vec::new();
2148        let bits = value.to_bits();
2149
2150        // Use R12 as temp register for constant loading
2151        let rt: u32 = 12; // R12/IP
2152
2153        // MOVW R12, #lo16: 0xE300_C000 | (imm4 << 16) | imm12
2154        let lo16 = bits & 0xFFFF;
2155        let movw = 0xE3000000 | (rt << 12) | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2156        bytes.extend_from_slice(&movw.to_le_bytes());
2157
2158        // MOVT R12, #hi16: 0xE340_C000 | (imm4 << 16) | imm12
2159        let hi16 = (bits >> 16) & 0xFFFF;
2160        let movt = 0xE3400000 | (rt << 12) | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2161        bytes.extend_from_slice(&movt.to_le_bytes());
2162
2163        // VMOV Sd, R12
2164        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
2165        bytes.extend_from_slice(&vmov.to_le_bytes());
2166
2167        Ok(bytes)
2168    }
2169
2170    /// Encode VMOV + VCVT.F32.S32/U32 as ARM32
2171    fn encode_arm_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2172        let mut bytes = Vec::new();
2173
2174        // VMOV Sd, Rm — move integer to VFP register
2175        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
2176        bytes.extend_from_slice(&vmov.to_le_bytes());
2177
2178        // VCVT.F32.S32 Sd, Sd (signed) or VCVT.F32.U32 Sd, Sd (unsigned)
2179        // Base: 0xEEB80A40 (signed) or 0xEEB80AC0 (unsigned)
2180        let sd_num = vfp_sreg_to_num(sd)?;
2181        let (vd, d) = encode_sreg(sd_num);
2182        let (vm, m) = encode_sreg(sd_num); // same register as source
2183        let base = if signed { 0xEEB80A40 } else { 0xEEB80AC0 };
2184        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2185        bytes.extend_from_slice(&vcvt.to_le_bytes());
2186
2187        Ok(bytes)
2188    }
2189
2190    /// Encode F32 rounding pseudo-op as ARM32 via VCVT to integer and back.
2191    /// mode: 0b00=nearest, 0b01=floor(-Inf), 0b10=ceil(+Inf), 0b11=trunc(zero)
2192    /// Strategy: VCVT.S32.F32 Sd, Sm (toward zero), then VCVT.F32.S32 Sd, Sd
2193    /// For ceil/floor/nearest, we use VCVTR (round toward mode) + convert back.
2194    /// Simplified: convert to int (toward zero for trunc) then back to float.
2195    /// Encode F32 rounding as ARM32.
2196    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2197    ///
2198    /// For trunc (mode=0b11): uses VCVTR.S32.F32 (always rounds toward zero).
2199    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant
2200    /// which honours FPSCR rmode), then restores FPSCR.
2201    fn encode_arm_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2202        let mut bytes = Vec::new();
2203        let sm_num = vfp_sreg_to_num(sm)?;
2204        let sd_num = vfp_sreg_to_num(sd)?;
2205        let (vd_s, d_s) = encode_sreg(sd_num);
2206        let (vm_s, m_s) = encode_sreg(sm_num);
2207
2208        if mode == 0b11 {
2209            // Trunc (toward zero): VCVTR.S32.F32 — the "R" variant always truncates.
2210            // 0xEEBD0AC0: bit[7]=1 => round toward zero regardless of FPSCR
2211            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2212            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2213        } else {
2214            // ceil/floor/nearest: manipulate FPSCR rounding mode
2215            let rt: u32 = 12; // R12/IP as temp
2216
2217            // VMRS R12, FPSCR
2218            let vmrs = 0xEEF10A10 | (rt << 12);
2219            bytes.extend_from_slice(&vmrs.to_le_bytes());
2220
2221            // BIC R12, R12, #(3 << 22) — clear RMode bits [23:22]
2222            // 3<<22 = 0x00C00000. ARM rotated imm: 0x03 ror 10 (rotation=5, imm8=0x03)
2223            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2224            bytes.extend_from_slice(&bic.to_le_bytes());
2225
2226            // ORR R12, R12, #(mode << 22) — set desired rounding mode
2227            if mode != 0 {
2228                // mode<<22: rotation=5, imm8=mode
2229                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2230                bytes.extend_from_slice(&orr.to_le_bytes());
2231            }
2232
2233            // VMSR FPSCR, R12
2234            let vmsr = 0xEEE10A10 | (rt << 12);
2235            bytes.extend_from_slice(&vmsr.to_le_bytes());
2236
2237            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rounding mode
2238            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2239            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2240
2241            // Restore FPSCR: clear rmode bits back to nearest (default)
2242            bytes.extend_from_slice(&vmrs.to_le_bytes());
2243            bytes.extend_from_slice(&bic.to_le_bytes());
2244            bytes.extend_from_slice(&vmsr.to_le_bytes());
2245        }
2246
2247        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
2248        let (vd2, d2) = encode_sreg(sd_num);
2249        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
2250        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2251
2252        Ok(bytes)
2253    }
2254
2255    /// Encode F32 min/max as ARM32: VCMP + VMRS + conditional VMOV
2256    fn encode_arm_f32_minmax(
2257        &self,
2258        sd: &VfpReg,
2259        sn: &VfpReg,
2260        sm: &VfpReg,
2261        is_min: bool,
2262    ) -> Result<Vec<u8>> {
2263        let mut bytes = Vec::new();
2264        let sn_num = vfp_sreg_to_num(sn)?;
2265        let sm_num = vfp_sreg_to_num(sm)?;
2266        let sd_num = vfp_sreg_to_num(sd)?;
2267
2268        // VMOV Sd, Sn (start with first operand)
2269        let (vd, d) = encode_sreg(sd_num);
2270        let (vn, n) = encode_sreg(sn_num);
2271        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2272        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2273
2274        // VCMP.F32 Sn, Sm
2275        let (vm, m) = encode_sreg(sm_num);
2276        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2277        bytes.extend_from_slice(&vcmp.to_le_bytes());
2278
2279        // VMRS APSR_nzcv, FPSCR
2280        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2281
2282        // For min: if Sn > Sm (GT), use Sm. Condition = GT (0xC)
2283        // For max: if Sn < Sm (MI/LT), use Sm. Condition = MI (0x4)
2284        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2285
2286        // VMOV{cond} Sd, Sm — conditional VMOV
2287        let vmov_cond = (cond << 28) | 0x0EB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2288        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2289
2290        Ok(bytes)
2291    }
2292
2293    /// Encode F32 copysign as ARM32: extract sign from Sm, magnitude from Sn
2294    fn encode_arm_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2295        let mut bytes = Vec::new();
2296
2297        // VMOV R12, Sm (get sign source bits)
2298        let vmov_sm = encode_vmov_core_sreg(false, sm, &Reg::R12)?;
2299        bytes.extend_from_slice(&vmov_sm.to_le_bytes());
2300
2301        // VMOV R0, Sn (get magnitude source bits) — use R0 as temp
2302        let vmov_sn = encode_vmov_core_sreg(false, sn, &Reg::R0)?;
2303        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2304
2305        // AND R12, R12, #0x80000000 (keep only sign bit)
2306        // Thumb-2 constant 0x80000000 needs special encoding; in ARM32 use rotated imm
2307        // 0x80000000 = 0x02 rotated right by 2 (rotation=1, imm8=0x02)
2308        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2309        bytes.extend_from_slice(&and_sign.to_le_bytes());
2310
2311        // BIC R0, R0, #0x80000000 (clear sign bit from magnitude)
2312        // R0 = register 0, so Rn and Rd fields are 0
2313        let bic_sign = 0xE3C00000u32 | (1 << 8) | 0x02;
2314        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2315
2316        // ORR R0, R0, R12 (combine sign + magnitude)
2317        // R0 = register 0, so Rn and Rd fields are 0
2318        let orr = 0xE1800000u32 | 12;
2319        bytes.extend_from_slice(&orr.to_le_bytes());
2320
2321        // VMOV Sd, R0
2322        let vmov_result = encode_vmov_core_sreg(true, sd, &Reg::R0)?;
2323        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2324
2325        Ok(bytes)
2326    }
2327
2328    /// Encode F64 comparison as ARM32: VCMP.F64 + VMRS + MOV rd,#0 + MOVcond rd,#1
2329    fn encode_arm_f64_compare(
2330        &self,
2331        rd: &Reg,
2332        dn: &VfpReg,
2333        dm: &VfpReg,
2334        cond_code: u32,
2335    ) -> Result<Vec<u8>> {
2336        let mut bytes = Vec::new();
2337
2338        // VCMP.F64 Dn, Dm: 0xEEB40B40 with Dn in Vd position, Dm in Vm position
2339        let dn_num = vfp_dreg_to_num(dn)?;
2340        let dm_num = vfp_dreg_to_num(dm)?;
2341        let (vd, d) = encode_dreg(dn_num);
2342        let (vm, m) = encode_dreg(dm_num);
2343        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2344        bytes.extend_from_slice(&vcmp.to_le_bytes());
2345
2346        // VMRS APSR_nzcv, FPSCR
2347        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2348
2349        // MOV rd, #0
2350        let rd_bits = reg_to_bits(rd);
2351        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2352        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2353
2354        // MOVcond rd, #1
2355        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2356        bytes.extend_from_slice(&mov_one.to_le_bytes());
2357
2358        Ok(bytes)
2359    }
2360
2361    /// Encode F64 constant load as ARM32: MOVW + MOVT + MOVW + MOVT + VMOV
2362    fn encode_arm_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
2363        let mut bytes = Vec::new();
2364        let bits = value.to_bits();
2365        let lo32 = bits as u32;
2366        let hi32 = (bits >> 32) as u32;
2367
2368        // Load low 32 bits into R0 (Rd field = 0 for R0)
2369        let lo16 = lo32 & 0xFFFF;
2370        let movw_r0 = 0xE3000000 | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2371        bytes.extend_from_slice(&movw_r0.to_le_bytes());
2372        let hi16 = (lo32 >> 16) & 0xFFFF;
2373        let movt_r0 = 0xE3400000 | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2374        bytes.extend_from_slice(&movt_r0.to_le_bytes());
2375
2376        // Load high 32 bits into R12
2377        let lo16 = hi32 & 0xFFFF;
2378        let movw_r12 = 0xE3000000 | ((lo16 >> 12) << 16) | (12 << 12) | (lo16 & 0xFFF);
2379        bytes.extend_from_slice(&movw_r12.to_le_bytes());
2380        let hi16 = (hi32 >> 16) & 0xFFFF;
2381        let movt_r12 = 0xE3400000 | ((hi16 >> 12) << 16) | (12 << 12) | (hi16 & 0xFFF);
2382        bytes.extend_from_slice(&movt_r12.to_le_bytes());
2383
2384        // VMOV Dd, R0, R12
2385        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
2386        bytes.extend_from_slice(&vmov.to_le_bytes());
2387
2388        Ok(bytes)
2389    }
2390
2391    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as ARM32
2392    fn encode_arm_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2393        let mut bytes = Vec::new();
2394
2395        // Use S0 as intermediate: VMOV S0, Rm
2396        let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
2397        bytes.extend_from_slice(&vmov.to_le_bytes());
2398
2399        // VCVT.F64.S32 Dd, S0 (signed) or VCVT.F64.U32 Dd, S0 (unsigned)
2400        // Base: 0xEEB80B40 (signed) or 0xEEB80BC0 (unsigned)
2401        let dd_num = vfp_dreg_to_num(dd)?;
2402        let (vd, d) = encode_dreg(dd_num);
2403        let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
2404        // S0 is register 0: Vm=0, M=0
2405        let vcvt = base | (d << 22) | (vd << 12);
2406        bytes.extend_from_slice(&vcvt.to_le_bytes());
2407
2408        Ok(bytes)
2409    }
2410
2411    /// Encode VCVT.F64.F32 Dd, Sm as ARM32 (f32 to f64 promotion)
2412    fn encode_arm_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2413        let dd_num = vfp_dreg_to_num(dd)?;
2414        let sm_num = vfp_sreg_to_num(sm)?;
2415        let (vd, d) = encode_dreg(dd_num);
2416        let (vm, m) = encode_sreg(sm_num);
2417
2418        // VCVT.F64.F32 Dd, Sm: 0xEEB70AC0
2419        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
2420        Ok(vcvt.to_le_bytes().to_vec())
2421    }
2422
2423    /// Encode VCVT.S32/U32.F64 Sd, Dm + VMOV Rd, Sd as ARM32
2424    fn encode_arm_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2425        let mut bytes = Vec::new();
2426        let dm_num = vfp_dreg_to_num(dm)?;
2427        let (vm, m) = encode_dreg(dm_num);
2428
2429        // VCVT.S32.F64 S0, Dm (toward zero) or VCVT.U32.F64 S0, Dm
2430        // S0: Vd=0, D=0
2431        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
2432        let vcvt = base | (m << 5) | vm;
2433        bytes.extend_from_slice(&vcvt.to_le_bytes());
2434
2435        // VMOV Rd, S0
2436        let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
2437        bytes.extend_from_slice(&vmov.to_le_bytes());
2438
2439        Ok(bytes)
2440    }
2441
2442    /// Encode F64 rounding pseudo-op as ARM32 via VCVT to integer and back.
2443    /// Encode F64 rounding as ARM32.
2444    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2445    ///
2446    /// For trunc: uses VCVTR.S32.F64 (always truncates).
2447    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F64 (non-R variant),
2448    /// then restores FPSCR.
2449    fn encode_arm_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2450        let mut bytes = Vec::new();
2451        let dm_num = vfp_dreg_to_num(dm)?;
2452        let dd_num = vfp_dreg_to_num(dd)?;
2453        let (vm, m) = encode_dreg(dm_num);
2454        let (vd, d) = encode_dreg(dd_num);
2455
2456        if mode == 0b11 {
2457            // Trunc (toward zero): VCVTR.S32.F64 — bit[7]=1, always truncates
2458            let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
2459            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2460        } else {
2461            // ceil/floor/nearest: manipulate FPSCR rounding mode
2462            let rt: u32 = 12;
2463
2464            // VMRS R12, FPSCR
2465            let vmrs = 0xEEF10A10 | (rt << 12);
2466            bytes.extend_from_slice(&vmrs.to_le_bytes());
2467
2468            // BIC R12, R12, #(3 << 22)
2469            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2470            bytes.extend_from_slice(&bic.to_le_bytes());
2471
2472            // ORR R12, R12, #(mode << 22)
2473            if mode != 0 {
2474                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2475                bytes.extend_from_slice(&orr.to_le_bytes());
2476            }
2477
2478            // VMSR FPSCR, R12
2479            let vmsr = 0xEEE10A10 | (rt << 12);
2480            bytes.extend_from_slice(&vmsr.to_le_bytes());
2481
2482            // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0), uses FPSCR rmode
2483            let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
2484            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2485
2486            // Restore FPSCR
2487            bytes.extend_from_slice(&vmrs.to_le_bytes());
2488            bytes.extend_from_slice(&bic.to_le_bytes());
2489            bytes.extend_from_slice(&vmsr.to_le_bytes());
2490        }
2491
2492        // VCVT.F64.S32 Dd, S0 (convert back to double)
2493        let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
2494        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2495
2496        Ok(bytes)
2497    }
2498
2499    /// Encode F64 min/max as ARM32: VMOV + VCMP + VMRS + conditional VMOV
2500    fn encode_arm_f64_minmax(
2501        &self,
2502        dd: &VfpReg,
2503        dn: &VfpReg,
2504        dm: &VfpReg,
2505        is_min: bool,
2506    ) -> Result<Vec<u8>> {
2507        let mut bytes = Vec::new();
2508        let dn_num = vfp_dreg_to_num(dn)?;
2509        let dm_num = vfp_dreg_to_num(dm)?;
2510        let dd_num = vfp_dreg_to_num(dd)?;
2511
2512        // VMOV.F64 Dd, Dn (start with first operand)
2513        let (vd, d) = encode_dreg(dd_num);
2514        let (vn, n) = encode_dreg(dn_num);
2515        let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2516        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2517
2518        // VCMP.F64 Dn, Dm
2519        let (vm, m) = encode_dreg(dm_num);
2520        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2521        bytes.extend_from_slice(&vcmp.to_le_bytes());
2522
2523        // VMRS APSR_nzcv, FPSCR
2524        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2525
2526        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2527        let vmov_cond = (cond << 28) | 0x0EB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2528        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2529
2530        Ok(bytes)
2531    }
2532
2533    /// Encode F64 copysign as ARM32
2534    fn encode_arm_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
2535        let mut bytes = Vec::new();
2536
2537        // VMOV R0, R12, Dm (get sign source bits)
2538        let vmov_dm = encode_vmov_core_dreg(false, dm, &Reg::R0, &Reg::R12)?;
2539        bytes.extend_from_slice(&vmov_dm.to_le_bytes());
2540
2541        // VMOV R1, R2, Dn (get magnitude source bits)
2542        // We use R1 (lo) and R2 (hi) for the magnitude
2543        let vmov_dn = encode_vmov_core_dreg(false, dn, &Reg::R1, &Reg::R2)?;
2544        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2545
2546        // AND R12, R12, #0x80000000 (keep only sign bit from hi word)
2547        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2548        bytes.extend_from_slice(&and_sign.to_le_bytes());
2549
2550        // BIC R2, R2, #0x80000000 (clear sign bit from magnitude hi word)
2551        let bic_sign = 0xE3C00000u32 | (2 << 16) | (2 << 12) | (1 << 8) | 0x02;
2552        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2553
2554        // ORR R2, R2, R12 (combine sign + magnitude)
2555        let orr = 0xE1800000u32 | (2 << 16) | (2 << 12) | 12;
2556        bytes.extend_from_slice(&orr.to_le_bytes());
2557
2558        // VMOV Dd, R1, R2
2559        let vmov_result = encode_vmov_core_dreg(true, dd, &Reg::R1, &Reg::R2)?;
2560        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2561
2562        Ok(bytes)
2563    }
2564
2565    /// Encode VCVT.S32/U32.F32 + VMOV as ARM32
2566    fn encode_arm_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2567        let mut bytes = Vec::new();
2568
2569        // VCVT.S32.F32 Sd, Sm (toward zero) or VCVT.U32.F32 Sd, Sm
2570        // We use Sm as both source and destination for the intermediate result
2571        let sm_num = vfp_sreg_to_num(sm)?;
2572        let (vd, d) = encode_sreg(sm_num);
2573        let (vm, m) = encode_sreg(sm_num);
2574        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
2575        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2576        bytes.extend_from_slice(&vcvt.to_le_bytes());
2577
2578        // VMOV Rd, Sm — move result back to core register
2579        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
2580        bytes.extend_from_slice(&vmov.to_le_bytes());
2581
2582        Ok(bytes)
2583    }
2584
2585    /// Encode an ARM instruction in Thumb-2 mode (16-bit or 32-bit instructions)
2586    fn encode_thumb(&self, op: &ArmOp) -> Result<Vec<u8>> {
2587        // Thumb-2 supports both 16-bit and 32-bit instructions
2588        // 32-bit instructions are encoded as two 16-bit halfwords (big-endian order)
2589        match op {
2590            // === 16-bit Thumb encodings ===
2591            ArmOp::Add { rd, rn, op2 } => {
2592                let rd_bits = reg_to_bits(rd) as u16;
2593                let rn_bits = reg_to_bits(rn) as u16;
2594
2595                if let Operand2::Reg(rm) = op2 {
2596                    let rm_bits = reg_to_bits(rm) as u16;
2597                    // 16-bit ADDS only has 3-bit register fields (R0-R7). For
2598                    // high registers (e.g. R12, the MemLoad/MemStore base
2599                    // scratch) the bits overflow into adjacent fields, silently
2600                    // corrupting the operands — issue #178/#180: `add ip,ip,r0`
2601                    // was emitted as `adds r4,r5,r1`. Guard on all three regs
2602                    // being low and fall back to 32-bit ADD.W otherwise, exactly
2603                    // as the Sub handler below does.
2604                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2605                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2606                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2607                        Ok(instr.to_le_bytes().to_vec())
2608                    } else {
2609                        // ADD.W Rd, Rn, Rm (32-bit) for high registers
2610                        self.encode_thumb32_add_reg_raw(
2611                            rd_bits as u32,
2612                            rn_bits as u32,
2613                            rm_bits as u32,
2614                        )
2615                    }
2616                } else if let Operand2::Imm(imm) = op2 {
2617                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2618                        // ADDS Rd, Rn, #imm3 (16-bit): 0001 110 imm3 Rn Rd
2619                        let instr: u16 = 0x1C00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2620                        Ok(instr.to_le_bytes().to_vec())
2621                    } else {
2622                        // Use 32-bit ADD for larger immediates
2623                        self.encode_thumb32_add(rd, rn, *imm as u32)
2624                    }
2625                } else {
2626                    // Fallback to 32-bit encoding
2627                    self.encode_thumb32_add(rd, rn, 0)
2628                }
2629            }
2630
2631            ArmOp::Sub { rd, rn, op2 } => {
2632                let rd_bits = reg_to_bits(rd) as u16;
2633                let rn_bits = reg_to_bits(rn) as u16;
2634
2635                if let Operand2::Reg(rm) = op2 {
2636                    let rm_bits = reg_to_bits(rm) as u16;
2637                    // 16-bit SUBS can only use low registers (R0-R7)
2638                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2639                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2640                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2641                        Ok(instr.to_le_bytes().to_vec())
2642                    } else {
2643                        // Use 32-bit SUB.W for high registers
2644                        self.encode_thumb32_sub_reg_raw(
2645                            rd_bits as u32,
2646                            rn_bits as u32,
2647                            rm_bits as u32,
2648                        )
2649                    }
2650                } else if let Operand2::Imm(imm) = op2 {
2651                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2652                        // SUBS Rd, Rn, #imm3 (16-bit): 0001 111 imm3 Rn Rd
2653                        let instr: u16 = 0x1E00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2654                        Ok(instr.to_le_bytes().to_vec())
2655                    } else {
2656                        self.encode_thumb32_sub(rd, rn, *imm as u32)
2657                    }
2658                } else {
2659                    self.encode_thumb32_sub(rd, rn, 0)
2660                }
2661            }
2662
2663            ArmOp::Mov { rd, op2 } => {
2664                let rd_bits = reg_to_bits(rd) as u16;
2665
2666                if let Operand2::Imm(imm) = op2 {
2667                    // #498: the old test here was the SIGNED `*imm <= 255`,
2668                    // so a negative immediate (e.g. -1) fell into the 16-bit
2669                    // MOVS arm and encoded the wrong VALUE (#(imm & 0xFF) =
2670                    // #0xFF). A positive imm above 0xFFFF was equally wrong:
2671                    // MOVW truncates to 16 bits. Split on the UNSIGNED value:
2672                    // imm8 → MOVS, imm16 → MOVW, anything wider (negative or
2673                    // >0xFFFF) → the full-value MOVW+MOVT pair. No emitter
2674                    // produces the wide shape today (both selectors
2675                    // materialize wide constants as explicit Movw/Movt or
2676                    // Movw+Mvn), so this is byte-identical on shipped paths —
2677                    // it retires the latent wrong-value encodings the
2678                    // `estimator_encoder_agreement` oracle had pinned.
2679                    let uimm = *imm as u32;
2680                    if uimm <= 255 && rd_bits < 8 {
2681                        // MOVS Rd, #imm8 (16-bit): 0010 0 Rd imm8
2682                        let imm_bits = (*imm as u16) & 0xFF;
2683                        let instr: u16 = 0x2000 | (rd_bits << 8) | imm_bits;
2684                        Ok(instr.to_le_bytes().to_vec())
2685                    } else if uimm <= 0xFFFF {
2686                        // Use 32-bit MOVW for 16-bit immediates
2687                        self.encode_thumb32_movw(rd, uimm)
2688                    } else {
2689                        // Full 32-bit value: MOVW low16 + MOVT high16
2690                        let mut bytes = self.encode_thumb32_movw(rd, uimm & 0xFFFF)?;
2691                        bytes.extend(self.encode_thumb32_movt_raw(reg_to_bits(rd), uimm >> 16)?);
2692                        Ok(bytes)
2693                    }
2694                } else if let Operand2::Reg(rm) = op2 {
2695                    let rm_bits = reg_to_bits(rm) as u16;
2696                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
2697                    // D = Rd[3], Rd[2:0] in lower bits
2698                    let d_bit = (rd_bits >> 3) & 1;
2699                    let instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
2700                    Ok(instr.to_le_bytes().to_vec())
2701                } else {
2702                    let instr: u16 = 0xBF00; // NOP fallback
2703                    Ok(instr.to_le_bytes().to_vec())
2704                }
2705            }
2706
2707            ArmOp::Push { regs } => {
2708                // Thumb-2 PUSH encoding:
2709                // If all regs in R0-R7 + LR, use 16-bit: 1011 010 M rrrrrrrr
2710                // Otherwise use 32-bit: STMDB SP!, {regs} = 1110 1001 0010 1101 | 0M0 reglist(13)
2711                let mut reg_list: u16 = 0;
2712                let mut need_32bit = false;
2713                for r in regs {
2714                    let bit = reg_to_bits(r);
2715                    if bit >= 8 && *r != Reg::LR {
2716                        need_32bit = true;
2717                    }
2718                    reg_list |= 1 << bit;
2719                }
2720                if !need_32bit {
2721                    // 16-bit PUSH: 1011 010 M rrrrrrrr
2722                    let m_bit = if reg_list & (1 << 14) != 0 {
2723                        1u16
2724                    } else {
2725                        0u16
2726                    };
2727                    let low_regs = reg_list & 0xFF;
2728                    let instr: u16 = 0xB400 | (m_bit << 8) | low_regs;
2729                    Ok(instr.to_le_bytes().to_vec())
2730                } else {
2731                    // 32-bit STMDB SP!, {regs}: E92D | reglist(16)
2732                    let hw1: u16 = 0xE92D;
2733                    let hw2: u16 = reg_list;
2734                    let mut bytes = hw1.to_le_bytes().to_vec();
2735                    bytes.extend_from_slice(&hw2.to_le_bytes());
2736                    Ok(bytes)
2737                }
2738            }
2739
2740            ArmOp::Pop { regs } => {
2741                // Thumb-2 POP encoding:
2742                // If all regs in R0-R7 + PC, use 16-bit: 1011 110 P rrrrrrrr
2743                // Otherwise use 32-bit: LDMIA SP!, {regs} = 1110 1000 1011 1101 | PM0 reglist(13)
2744                let mut reg_list: u16 = 0;
2745                let mut need_32bit = false;
2746                for r in regs {
2747                    let bit = reg_to_bits(r);
2748                    if bit >= 8 && *r != Reg::PC {
2749                        need_32bit = true;
2750                    }
2751                    reg_list |= 1 << bit;
2752                }
2753                if !need_32bit {
2754                    // 16-bit POP: 1011 110 P rrrrrrrr
2755                    let p_bit = if reg_list & (1 << 15) != 0 {
2756                        1u16
2757                    } else {
2758                        0u16
2759                    };
2760                    let low_regs = reg_list & 0xFF;
2761                    let instr: u16 = 0xBC00 | (p_bit << 8) | low_regs;
2762                    Ok(instr.to_le_bytes().to_vec())
2763                } else {
2764                    // 32-bit LDMIA SP!, {regs}: E8BD | reglist(16)
2765                    let hw1: u16 = 0xE8BD;
2766                    let hw2: u16 = reg_list;
2767                    let mut bytes = hw1.to_le_bytes().to_vec();
2768                    bytes.extend_from_slice(&hw2.to_le_bytes());
2769                    Ok(bytes)
2770                }
2771            }
2772
2773            ArmOp::Nop => {
2774                let instr: u16 = 0xBF00; // NOP in Thumb-2
2775                Ok(instr.to_le_bytes().to_vec())
2776            }
2777
2778            ArmOp::Udf { imm } => {
2779                // UDF (Undefined) in Thumb-2: 16-bit encoding is 0xDE00 | imm8
2780                // This triggers UsageFault/HardFault, used for WASM traps
2781                let instr: u16 = 0xDE00 | (*imm as u16);
2782                let bytes = instr.to_le_bytes().to_vec();
2783                encoding_contracts::verify_thumb16(&bytes);
2784                Ok(bytes)
2785            }
2786
2787            // i64 support: ADDS, ADC, SUBS, SBC for register pair arithmetic
2788            // ADDS sets flags (carry), ADC uses carry from previous ADDS
2789            ArmOp::Adds { rd, rn, op2 } => {
2790                let rd_bits = reg_to_bits(rd) as u16;
2791                let rn_bits = reg_to_bits(rn) as u16;
2792
2793                if let Operand2::Reg(rm) = op2 {
2794                    let rm_bits = reg_to_bits(rm) as u16;
2795                    // 16-bit ADDS is R0-R7 only; i64 pair allocation can place
2796                    // operands in R8-R11, which would overflow the 3-bit fields
2797                    // and corrupt the operands (#178/#180 class). Guard and fall
2798                    // back to 32-bit ADDS.W for high registers.
2799                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2800                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2801                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2802                        Ok(instr.to_le_bytes().to_vec())
2803                    } else {
2804                        self.encode_thumb32_adds_reg_raw(
2805                            rd_bits as u32,
2806                            rn_bits as u32,
2807                            rm_bits as u32,
2808                        )
2809                    }
2810                } else {
2811                    // 32-bit Thumb-2 ADDS with immediate
2812                    self.encode_thumb32_adds(rd, rn, 0)
2813                }
2814            }
2815
2816            // ADC: Add with Carry (Thumb-2 32-bit)
2817            // ADC.W Rd, Rn, Rm: EB40 Rn | 00 Rd 00 Rm
2818            ArmOp::Adc { rd, rn, op2 } => {
2819                let rd_bits = reg_to_bits(rd);
2820                let rn_bits = reg_to_bits(rn);
2821
2822                if let Operand2::Reg(rm) = op2 {
2823                    let rm_bits = reg_to_bits(rm);
2824                    // ADC.W Rd, Rn, Rm (T2): 1110 1011 0100 Rn | 0 000 Rd 00 00 Rm
2825                    let hw1: u16 = (0xEB40 | rn_bits) as u16;
2826                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2827
2828                    let mut bytes = hw1.to_le_bytes().to_vec();
2829                    bytes.extend_from_slice(&hw2.to_le_bytes());
2830                    Ok(bytes)
2831                } else {
2832                    // ADC with immediate - use 32-bit encoding
2833                    let hw1: u16 = (0xF140 | rn_bits) as u16;
2834                    let hw2: u16 = (rd_bits << 8) as u16;
2835                    let mut bytes = hw1.to_le_bytes().to_vec();
2836                    bytes.extend_from_slice(&hw2.to_le_bytes());
2837                    Ok(bytes)
2838                }
2839            }
2840
2841            // SUBS sets flags (borrow), SBC uses borrow from previous SUBS
2842            ArmOp::Subs { rd, rn, op2 } => {
2843                let rd_bits = reg_to_bits(rd) as u16;
2844                let rn_bits = reg_to_bits(rn) as u16;
2845
2846                if let Operand2::Reg(rm) = op2 {
2847                    let rm_bits = reg_to_bits(rm) as u16;
2848                    // 16-bit SUBS is R0-R7 only; high-register i64 pair operands
2849                    // would overflow the 3-bit fields (#178/#180 class). Guard
2850                    // and fall back to 32-bit SUBS.W for high registers.
2851                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2852                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2853                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2854                        Ok(instr.to_le_bytes().to_vec())
2855                    } else {
2856                        self.encode_thumb32_subs_reg_raw(
2857                            rd_bits as u32,
2858                            rn_bits as u32,
2859                            rm_bits as u32,
2860                        )
2861                    }
2862                } else {
2863                    // 32-bit Thumb-2 SUBS with immediate
2864                    self.encode_thumb32_subs(rd, rn, 0)
2865                }
2866            }
2867
2868            // SBC: Subtract with Carry (Thumb-2 32-bit)
2869            // SBC.W Rd, Rn, Rm: EB60 Rn | 00 Rd 00 Rm
2870            ArmOp::Sbc { rd, rn, op2 } => {
2871                let rd_bits = reg_to_bits(rd);
2872                let rn_bits = reg_to_bits(rn);
2873
2874                if let Operand2::Reg(rm) = op2 {
2875                    let rm_bits = reg_to_bits(rm);
2876                    // SBC.W Rd, Rn, Rm (T2): 1110 1011 0110 Rn | 0 000 Rd 00 00 Rm
2877                    let hw1: u16 = (0xEB60 | rn_bits) as u16;
2878                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2879
2880                    let mut bytes = hw1.to_le_bytes().to_vec();
2881                    bytes.extend_from_slice(&hw2.to_le_bytes());
2882                    Ok(bytes)
2883                } else {
2884                    // SBC with immediate - use 32-bit encoding
2885                    let hw1: u16 = (0xF160 | rn_bits) as u16;
2886                    let hw2: u16 = (rd_bits << 8) as u16;
2887                    let mut bytes = hw1.to_le_bytes().to_vec();
2888                    bytes.extend_from_slice(&hw2.to_le_bytes());
2889                    Ok(bytes)
2890                }
2891            }
2892
2893            // === 32-bit Thumb-2 encodings ===
2894
2895            // SDIV: 11111011 1001 Rn 1111 Rd 1111 Rm
2896            ArmOp::Sdiv { rd, rn, rm } => {
2897                let rd_bits = reg_to_bits(rd);
2898                let rn_bits = reg_to_bits(rn);
2899                let rm_bits = reg_to_bits(rm);
2900                reg_bits_checked(rd_bits)?;
2901                reg_bits_checked(rn_bits)?;
2902                reg_bits_checked(rm_bits)?;
2903
2904                // Thumb-2 SDIV: FB90 F0F0 | Rn<<16 | Rd<<8 | Rm
2905                // First halfword: 1111 1011 1001 Rn = 0xFB90 | Rn
2906                // Second halfword: 1111 Rd 1111 Rm = 0xF0F0 | Rd<<8 | Rm
2907                let hw1: u16 = (0xFB90 | rn_bits) as u16;
2908                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
2909
2910                // Thumb-2 32-bit instructions: first halfword, then second halfword (little-endian each)
2911                let mut bytes = hw1.to_le_bytes().to_vec();
2912                bytes.extend_from_slice(&hw2.to_le_bytes());
2913                encoding_contracts::verify_thumb32(&bytes);
2914                Ok(bytes)
2915            }
2916
2917            // UDIV: 11111011 1011 Rn 1111 Rd 1111 Rm
2918            ArmOp::Udiv { rd, rn, rm } => {
2919                let rd_bits = reg_to_bits(rd);
2920                let rn_bits = reg_to_bits(rn);
2921                let rm_bits = reg_to_bits(rm);
2922                reg_bits_checked(rd_bits)?;
2923                reg_bits_checked(rn_bits)?;
2924                reg_bits_checked(rm_bits)?;
2925
2926                // Thumb-2 UDIV: FBB0 F0F0 | Rn<<16 | Rd<<8 | Rm
2927                let hw1: u16 = (0xFBB0 | rn_bits) as u16;
2928                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
2929
2930                let mut bytes = hw1.to_le_bytes().to_vec();
2931                bytes.extend_from_slice(&hw2.to_le_bytes());
2932                encoding_contracts::verify_thumb32(&bytes);
2933                Ok(bytes)
2934            }
2935
2936            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
2937                let rdlo_bits = reg_to_bits(rdlo);
2938                let rdhi_bits = reg_to_bits(rdhi);
2939                let rn_bits = reg_to_bits(rn);
2940                let rm_bits = reg_to_bits(rm);
2941                reg_bits_checked(rdlo_bits)?;
2942                reg_bits_checked(rdhi_bits)?;
2943                reg_bits_checked(rn_bits)?;
2944                reg_bits_checked(rm_bits)?;
2945
2946                // Thumb-2 UMULL: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm
2947                let hw1: u16 = (0xFBA0 | rn_bits) as u16;
2948                let hw2: u16 = ((rdlo_bits << 12) | (rdhi_bits << 8) | rm_bits) as u16;
2949
2950                let mut bytes = hw1.to_le_bytes().to_vec();
2951                bytes.extend_from_slice(&hw2.to_le_bytes());
2952                encoding_contracts::verify_thumb32(&bytes);
2953                Ok(bytes)
2954            }
2955
2956            // MUL (Thumb-2 32-bit): MUL Rd, Rn, Rm
2957            ArmOp::Mul { rd, rn, rm } => {
2958                let rd_bits = reg_to_bits(rd);
2959                let rn_bits = reg_to_bits(rn);
2960                let rm_bits = reg_to_bits(rm);
2961
2962                // Thumb-2 MUL: FB00 F000 | Rn | Rd<<8 | Rm
2963                // 11111011 0000 Rn | 1111 Rd 0000 Rm
2964                let hw1: u16 = (0xFB00 | rn_bits) as u16;
2965                let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
2966
2967                let mut bytes = hw1.to_le_bytes().to_vec();
2968                bytes.extend_from_slice(&hw2.to_le_bytes());
2969                Ok(bytes)
2970            }
2971
2972            // MLS: Rd = Ra - Rn * Rm
2973            ArmOp::Mls { rd, rn, rm, ra } => {
2974                let rd_bits = reg_to_bits(rd);
2975                let rn_bits = reg_to_bits(rn);
2976                let rm_bits = reg_to_bits(rm);
2977                let ra_bits = reg_to_bits(ra);
2978
2979                // Thumb-2 MLS: FB00 Rn | Ra Rd 0001 Rm
2980                // 11111011 0000 Rn | Ra Rd 0001 Rm
2981                let hw1: u16 = (0xFB00 | rn_bits) as u16;
2982                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | 0x10 | rm_bits) as u16;
2983
2984                let mut bytes = hw1.to_le_bytes().to_vec();
2985                bytes.extend_from_slice(&hw2.to_le_bytes());
2986                Ok(bytes)
2987            }
2988
2989            ArmOp::Mla { rd, rn, rm, ra } => {
2990                let rd_bits = reg_to_bits(rd);
2991                let rn_bits = reg_to_bits(rn);
2992                let rm_bits = reg_to_bits(rm);
2993                let ra_bits = reg_to_bits(ra);
2994
2995                // Thumb-2 MLA: FB00 Rn | Ra Rd 0000 Rm — same as MLS without the
2996                // bit-4 (0x10) op flag. rd = ra + rn*rm.
2997                let hw1: u16 = (0xFB00 | rn_bits) as u16;
2998                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | rm_bits) as u16;
2999
3000                let mut bytes = hw1.to_le_bytes().to_vec();
3001                bytes.extend_from_slice(&hw2.to_le_bytes());
3002                Ok(bytes)
3003            }
3004
3005            // AND (Thumb-2 32-bit)
3006            ArmOp::And { rd, rn, op2 } => {
3007                if let Operand2::Reg(rm) = op2 {
3008                    let rd_bits = reg_to_bits(rd);
3009                    let rn_bits = reg_to_bits(rn);
3010                    let rm_bits = reg_to_bits(rm);
3011
3012                    // Thumb-2 AND register: EA00 Rn | 0 Rd 00 00 Rm
3013                    let hw1: u16 = (0xEA00 | rn_bits) as u16;
3014                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3015
3016                    let mut bytes = hw1.to_le_bytes().to_vec();
3017                    bytes.extend_from_slice(&hw2.to_le_bytes());
3018                    Ok(bytes)
3019                } else if let Operand2::Imm(imm) = op2 {
3020                    let rd_bits = reg_to_bits(rd);
3021                    let rn_bits = reg_to_bits(rn);
3022
3023                    // Thumb-2 AND.W immediate T1: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8.
3024                    // The i:imm3:imm8 field is a ThumbExpandImm modified immediate —
3025                    // encode it correctly (or error on an un-encodable value)
3026                    // rather than packing raw bits, closing the silent-miscompile
3027                    // class for AND alongside ORR/EOR (#251) / ADD/SUB (#253) /
3028                    // CMP (#255).
3029                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3030                        synth_core::Error::synthesis(
3031                            "AND immediate is not a valid ThumbExpandImm — materialize into a register",
3032                        )
3033                    })?;
3034                    let i_bit = (field >> 11) & 1;
3035                    let imm3 = (field >> 8) & 0x7;
3036                    let imm8 = field & 0xFF;
3037
3038                    let hw1: u16 = (0xF000 | (i_bit << 10) | rn_bits) as u16;
3039                    let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3040
3041                    let mut bytes = hw1.to_le_bytes().to_vec();
3042                    bytes.extend_from_slice(&hw2.to_le_bytes());
3043                    Ok(bytes)
3044                } else {
3045                    // RegShift variant - fallback to NOP
3046                    let instr: u16 = 0xBF00;
3047                    Ok(instr.to_le_bytes().to_vec())
3048                }
3049            }
3050
3051            // ORR (Thumb-2 32-bit)
3052            ArmOp::Orr { rd, rn, op2 } => {
3053                if let Operand2::Reg(rm) = op2 {
3054                    let rd_bits = reg_to_bits(rd);
3055                    let rn_bits = reg_to_bits(rn);
3056                    let rm_bits = reg_to_bits(rm);
3057
3058                    // Thumb-2 ORR: EA40 Rn | 0 Rd 00 00 Rm
3059                    let hw1: u16 = (0xEA40 | rn_bits) as u16;
3060                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3061
3062                    let mut bytes = hw1.to_le_bytes().to_vec();
3063                    bytes.extend_from_slice(&hw2.to_le_bytes());
3064                    Ok(bytes)
3065                } else if let Operand2::Imm(imm) = op2 {
3066                    // ORR.W immediate T1: 11110 i 0 0010 S Rn | 0 imm3 Rd imm8.
3067                    // Only the zero-extended byte form (imm <= 0xFF) is encoded;
3068                    // larger modified immediates need ThumbExpandImm — return an
3069                    // error rather than silently emit a NOP (Ok-or-Err, #180/#185).
3070                    let imm_val = *imm as u32;
3071                    if imm_val > 0xFF {
3072                        return Err(synth_core::Error::synthesis(
3073                            "ORR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3074                        ));
3075                    }
3076                    let rd_bits = reg_to_bits(rd);
3077                    let rn_bits = reg_to_bits(rn);
3078                    let hw1: u16 = (0xF040 | rn_bits) as u16;
3079                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3080                    let mut bytes = hw1.to_le_bytes().to_vec();
3081                    bytes.extend_from_slice(&hw2.to_le_bytes());
3082                    Ok(bytes)
3083                } else {
3084                    let instr: u16 = 0xBF00;
3085                    Ok(instr.to_le_bytes().to_vec())
3086                }
3087            }
3088
3089            // EOR (Thumb-2 32-bit)
3090            ArmOp::Eor { rd, rn, op2 } => {
3091                if let Operand2::Reg(rm) = op2 {
3092                    let rd_bits = reg_to_bits(rd);
3093                    let rn_bits = reg_to_bits(rn);
3094                    let rm_bits = reg_to_bits(rm);
3095
3096                    // Thumb-2 EOR: EA80 Rn | 0 Rd 00 00 Rm
3097                    let hw1: u16 = (0xEA80 | rn_bits) as u16;
3098                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3099
3100                    let mut bytes = hw1.to_le_bytes().to_vec();
3101                    bytes.extend_from_slice(&hw2.to_le_bytes());
3102                    Ok(bytes)
3103                } else if let Operand2::Imm(imm) = op2 {
3104                    // EOR.W immediate T1: 11110 i 0 0100 S Rn | 0 imm3 Rd imm8.
3105                    // Byte form only (imm <= 0xFF); larger needs ThumbExpandImm —
3106                    // error, not a silent NOP (Ok-or-Err, #180/#185).
3107                    let imm_val = *imm as u32;
3108                    if imm_val > 0xFF {
3109                        return Err(synth_core::Error::synthesis(
3110                            "EOR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3111                        ));
3112                    }
3113                    let rd_bits = reg_to_bits(rd);
3114                    let rn_bits = reg_to_bits(rn);
3115                    let hw1: u16 = (0xF080 | rn_bits) as u16;
3116                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3117                    let mut bytes = hw1.to_le_bytes().to_vec();
3118                    bytes.extend_from_slice(&hw2.to_le_bytes());
3119                    Ok(bytes)
3120                } else {
3121                    let instr: u16 = 0xBF00;
3122                    Ok(instr.to_le_bytes().to_vec())
3123                }
3124            }
3125
3126            // Shift operations (16-bit for low registers)
3127            ArmOp::Lsl { rd, rn, shift } => {
3128                let rd_bits = reg_to_bits(rd) as u16;
3129                let rn_bits = reg_to_bits(rn) as u16;
3130                let shift_bits = (*shift as u16) & 0x1F;
3131
3132                if rd_bits < 8 && rn_bits < 8 {
3133                    // LSLS Rd, Rm, #imm5 (16-bit): 0000 0 imm5 Rm Rd
3134                    let instr: u16 = (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3135                    Ok(instr.to_le_bytes().to_vec())
3136                } else {
3137                    // Use 32-bit encoding for high registers
3138                    self.encode_thumb32_shift(rd, rn, *shift, 0b00) // LSL type
3139                }
3140            }
3141
3142            ArmOp::Lsr { rd, rn, shift } => {
3143                let rd_bits = reg_to_bits(rd) as u16;
3144                let rn_bits = reg_to_bits(rn) as u16;
3145                let shift_bits = (*shift as u16) & 0x1F;
3146
3147                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3148                    // LSRS Rd, Rm, #imm5 (16-bit): 0000 1 imm5 Rm Rd
3149                    let instr: u16 = 0x0800 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3150                    Ok(instr.to_le_bytes().to_vec())
3151                } else {
3152                    self.encode_thumb32_shift(rd, rn, *shift, 0b01) // LSR type
3153                }
3154            }
3155
3156            ArmOp::Asr { rd, rn, shift } => {
3157                let rd_bits = reg_to_bits(rd) as u16;
3158                let rn_bits = reg_to_bits(rn) as u16;
3159                let shift_bits = (*shift as u16) & 0x1F;
3160
3161                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3162                    // ASRS Rd, Rm, #imm5 (16-bit): 0001 0 imm5 Rm Rd
3163                    let instr: u16 = 0x1000 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3164                    Ok(instr.to_le_bytes().to_vec())
3165                } else {
3166                    self.encode_thumb32_shift(rd, rn, *shift, 0b10) // ASR type
3167                }
3168            }
3169
3170            ArmOp::Ror { rd, rn, shift } => {
3171                // ROR doesn't have a 16-bit immediate form, use 32-bit
3172                self.encode_thumb32_shift(rd, rn, *shift, 0b11) // ROR type
3173            }
3174
3175            // Register-based shifts (Thumb-2 32-bit)
3176            // Encoding: 11111010 0xxS Rn 1111 Rd 0000 Rm
3177            // xx = shift type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
3178            ArmOp::LslReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b00),
3179            ArmOp::LsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b01),
3180            ArmOp::AsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b10),
3181            ArmOp::RorReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b11),
3182
3183            // RSB (Reverse Subtract): Rd = imm - Rn
3184            // Thumb-2 T2 encoding: 11110 i 0 1110 S Rn | 0 imm3 Rd imm8
3185            ArmOp::Rsb { rd, rn, imm } => {
3186                let rd_bits = reg_to_bits(rd);
3187                let rn_bits = reg_to_bits(rn);
3188                let imm_val = *imm;
3189
3190                let i_bit = (imm_val >> 11) & 1;
3191                let imm3 = (imm_val >> 8) & 0x7;
3192                let imm8 = imm_val & 0xFF;
3193
3194                // hw1: 11110 i 01110 0 Rn  (S=0)
3195                let hw1: u16 = (0xF1C0 | (i_bit << 10) | rn_bits) as u16;
3196                // hw2: 0 imm3 Rd imm8
3197                let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3198
3199                let mut bytes = hw1.to_le_bytes().to_vec();
3200                bytes.extend_from_slice(&hw2.to_le_bytes());
3201                Ok(bytes)
3202            }
3203
3204            // CLZ (Thumb-2 32-bit)
3205            ArmOp::Clz { rd, rm } => {
3206                let rd_bits = reg_to_bits(rd);
3207                let rm_bits = reg_to_bits(rm);
3208
3209                // Thumb-2 CLZ: FAB0 Rm | F8 Rd Rm
3210                // 11111010 1011 Rm | 1111 1000 Rd Rm
3211                let hw1: u16 = (0xFAB0 | rm_bits) as u16;
3212                let hw2: u16 = (0xF080 | (rd_bits << 8) | rm_bits) as u16;
3213
3214                let mut bytes = hw1.to_le_bytes().to_vec();
3215                bytes.extend_from_slice(&hw2.to_le_bytes());
3216                Ok(bytes)
3217            }
3218
3219            // RBIT (Thumb-2 32-bit)
3220            ArmOp::Rbit { rd, rm } => {
3221                let rd_bits = reg_to_bits(rd);
3222                let rm_bits = reg_to_bits(rm);
3223
3224                // Thumb-2 RBIT: FA90 Rm | F0 Rd A0 Rm
3225                // 11111010 1001 Rm | 1111 Rd 1010 Rm
3226                let hw1: u16 = (0xFA90 | rm_bits) as u16;
3227                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rm_bits) as u16;
3228
3229                let mut bytes = hw1.to_le_bytes().to_vec();
3230                bytes.extend_from_slice(&hw2.to_le_bytes());
3231                Ok(bytes)
3232            }
3233
3234            // SXTB (16-bit for low registers)
3235            ArmOp::Sxtb { rd, rm } => {
3236                let rd_bits = reg_to_bits(rd) as u16;
3237                let rm_bits = reg_to_bits(rm) as u16;
3238
3239                if rd_bits < 8 && rm_bits < 8 {
3240                    // SXTB Rd, Rm (16-bit): 1011 0010 01 Rm Rd
3241                    let instr: u16 = 0xB240 | (rm_bits << 3) | rd_bits;
3242                    Ok(instr.to_le_bytes().to_vec())
3243                } else {
3244                    // Thumb-2 SXTB.W: FA4F F(rd)80 (rm)
3245                    // 11111010 0100 1111 | 1111 Rd 10 rotate Rm
3246                    let rd_bits32 = rd_bits as u32;
3247                    let rm_bits32 = rm_bits as u32;
3248                    let hw1: u16 = 0xFA4F;
3249                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3250                    let mut bytes = hw1.to_le_bytes().to_vec();
3251                    bytes.extend_from_slice(&hw2.to_le_bytes());
3252                    Ok(bytes)
3253                }
3254            }
3255
3256            // SXTH (16-bit for low registers)
3257            ArmOp::Sxth { rd, rm } => {
3258                let rd_bits = reg_to_bits(rd) as u16;
3259                let rm_bits = reg_to_bits(rm) as u16;
3260
3261                if rd_bits < 8 && rm_bits < 8 {
3262                    // SXTH Rd, Rm (16-bit): 1011 0010 00 Rm Rd
3263                    let instr: u16 = 0xB200 | (rm_bits << 3) | rd_bits;
3264                    Ok(instr.to_le_bytes().to_vec())
3265                } else {
3266                    // Thumb-2 SXTH.W: FA0F F(rd)80 (rm)
3267                    // 11111010 0000 1111 | 1111 Rd 10 rotate Rm
3268                    let rd_bits32 = rd_bits as u32;
3269                    let rm_bits32 = rm_bits as u32;
3270                    let hw1: u16 = 0xFA0F;
3271                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3272                    let mut bytes = hw1.to_le_bytes().to_vec();
3273                    bytes.extend_from_slice(&hw2.to_le_bytes());
3274                    Ok(bytes)
3275                }
3276            }
3277
3278            // UXTB Rd,Rm — zero-extend byte (rd = rm & 0xff)
3279            ArmOp::Uxtb { rd, rm } => {
3280                let rd_bits = reg_to_bits(rd) as u16;
3281                let rm_bits = reg_to_bits(rm) as u16;
3282                if rd_bits < 8 && rm_bits < 8 {
3283                    // UXTB Rd, Rm (16-bit): 1011 0010 11 Rm Rd
3284                    let instr: u16 = 0xB2C0 | (rm_bits << 3) | rd_bits;
3285                    Ok(instr.to_le_bytes().to_vec())
3286                } else {
3287                    // Thumb-2 UXTB.W: FA5F F(rd)80 (rm)
3288                    let hw1: u16 = 0xFA5F;
3289                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3290                    let mut bytes = hw1.to_le_bytes().to_vec();
3291                    bytes.extend_from_slice(&hw2.to_le_bytes());
3292                    Ok(bytes)
3293                }
3294            }
3295
3296            // UXTH Rd,Rm — zero-extend halfword (rd = rm & 0xffff)
3297            ArmOp::Uxth { rd, rm } => {
3298                let rd_bits = reg_to_bits(rd) as u16;
3299                let rm_bits = reg_to_bits(rm) as u16;
3300                if rd_bits < 8 && rm_bits < 8 {
3301                    // UXTH Rd, Rm (16-bit): 1011 0010 10 Rm Rd
3302                    let instr: u16 = 0xB280 | (rm_bits << 3) | rd_bits;
3303                    Ok(instr.to_le_bytes().to_vec())
3304                } else {
3305                    // Thumb-2 UXTH.W: FA1F F(rd)80 (rm)
3306                    let hw1: u16 = 0xFA1F;
3307                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3308                    let mut bytes = hw1.to_le_bytes().to_vec();
3309                    bytes.extend_from_slice(&hw2.to_le_bytes());
3310                    Ok(bytes)
3311                }
3312            }
3313
3314            // CMP (can be 16-bit for low registers)
3315            ArmOp::Cmp { rn, op2 } => {
3316                let rn_bits = reg_to_bits(rn) as u16;
3317
3318                if let Operand2::Imm(imm) = op2 {
3319                    // Only use 16-bit encoding for non-negative immediates 0-255
3320                    // Negative immediates must use 32-bit encoding
3321                    if *imm >= 0 && *imm <= 255 && rn_bits < 8 {
3322                        // CMP Rn, #imm8 (16-bit): 0010 1 Rn imm8
3323                        let instr: u16 = 0x2800 | (rn_bits << 8) | (*imm as u16 & 0xFF);
3324                        Ok(instr.to_le_bytes().to_vec())
3325                    } else {
3326                        self.encode_thumb32_cmp_imm(rn, *imm as u32)
3327                    }
3328                } else if let Operand2::Reg(rm) = op2 {
3329                    let rm_bits = reg_to_bits(rm) as u16;
3330                    if rn_bits < 8 && rm_bits < 8 {
3331                        // CMP Rn, Rm (16-bit low): 0100 0010 10 Rm Rn
3332                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
3333                        Ok(instr.to_le_bytes().to_vec())
3334                    } else {
3335                        // CMP Rn, Rm (16-bit high): 0100 0101 N Rm Rn[2:0]
3336                        let n_bit = (rn_bits >> 3) & 1;
3337                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
3338                        Ok(instr.to_le_bytes().to_vec())
3339                    }
3340                } else {
3341                    let instr: u16 = 0xBF00;
3342                    Ok(instr.to_le_bytes().to_vec())
3343                }
3344            }
3345
3346            // CMN (Compare Negative) - computes Rn + op2 and sets flags
3347            // CMN Rn, #1 sets Z flag if Rn == -1 (since -1 + 1 = 0)
3348            ArmOp::Cmn { rn, op2 } => {
3349                let rn_bits = reg_to_bits(rn) as u16;
3350
3351                if let Operand2::Imm(imm) = op2 {
3352                    // CMN.W Rn, #imm (32-bit): i:imm3:imm8 is a ThumbExpandImm
3353                    // modified immediate (the field sits in imm3=hw2[14:12],
3354                    // imm8=hw2[7:0], i=hw1[10]). Encode it correctly, or error on
3355                    // an un-encodable value — replacing the old silent `0xBF00`
3356                    // NOP (the last of the silent-miscompile data-proc encoders).
3357                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3358                        synth_core::Error::synthesis(
3359                            "CMN immediate is not a valid ThumbExpandImm — materialize into a register",
3360                        )
3361                    })?;
3362                    let i_bit = (field >> 11) & 1;
3363                    let imm3 = (field >> 8) & 0x7;
3364                    let imm8 = field & 0xFF;
3365                    let hw1: u16 = (0xF110 | (i_bit << 10) as u16) | rn_bits;
3366                    let hw2: u16 = (imm3 << 12) as u16 | 0x0F00 | imm8 as u16;
3367                    let mut bytes = hw1.to_le_bytes().to_vec();
3368                    bytes.extend_from_slice(&hw2.to_le_bytes());
3369                    Ok(bytes)
3370                } else if let Operand2::Reg(rm) = op2 {
3371                    let rm_bits = reg_to_bits(rm) as u16;
3372                    // 16-bit CMN (T1) only encodes R0-R7; high registers overflow
3373                    // the 3-bit fields and corrupt the operands (#184, the #180
3374                    // class). CMN has no high-register 16-bit form, so fall back
3375                    // to 32-bit CMN.W (T2): EB10 Rn | 0F00 Rm (ADD.W with S=1 and
3376                    // Rd discarded as PC/1111).
3377                    if rn_bits < 8 && rm_bits < 8 {
3378                        // CMN Rn, Rm (16-bit): 0100 0010 11 Rm Rn
3379                        let instr: u16 = 0x42C0 | (rm_bits << 3) | rn_bits;
3380                        Ok(instr.to_le_bytes().to_vec())
3381                    } else {
3382                        let hw1: u16 = 0xEB10 | rn_bits;
3383                        let hw2: u16 = 0x0F00 | rm_bits;
3384                        let mut bytes = hw1.to_le_bytes().to_vec();
3385                        bytes.extend_from_slice(&hw2.to_le_bytes());
3386                        Ok(bytes)
3387                    }
3388                } else {
3389                    Ok(vec![0xBF, 0x00])
3390                }
3391            }
3392
3393            // LDR (can be 16-bit for simple cases)
3394            ArmOp::Ldr { rd, addr } => {
3395                let rd_bits = reg_to_bits(rd);
3396                let base_bits = reg_to_bits(&addr.base);
3397
3398                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3399                if let Some(offset_reg) = &addr.offset_reg {
3400                    let rm_bits = reg_to_bits(offset_reg);
3401
3402                    // If there's also an immediate offset, we need to ADD it first
3403                    if addr.offset != 0 {
3404                        // Use R12 (IP) as scratch to avoid clobbering the address register
3405                        // ADD R12, Rm, #offset; LDR Rd, [base, R12]
3406                        let scratch = Reg::R12;
3407                        let mut bytes =
3408                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3409                        bytes.extend(self.encode_thumb32_ldr_reg(rd, &addr.base, &scratch)?);
3410                        return Ok(bytes);
3411                    }
3412
3413                    // Simple register offset: LDR Rd, [Rn, Rm]
3414                    // 16-bit: only if Rd, Rn, Rm < R8
3415                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3416                        // LDR Rd, [Rn, Rm] (16-bit): 0101 100 Rm Rn Rd
3417                        let instr: u16 = 0x5800
3418                            | ((rm_bits as u16) << 6)
3419                            | ((base_bits as u16) << 3)
3420                            | (rd_bits as u16);
3421                        return Ok(instr.to_le_bytes().to_vec());
3422                    }
3423
3424                    // 32-bit register offset
3425                    return self.encode_thumb32_ldr_reg(rd, &addr.base, offset_reg);
3426                }
3427
3428                // Immediate offset mode [base, #imm]
3429                let offset = addr.offset as u32;
3430
3431                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3432                    // LDR Rd, [Rn, #imm5*4] (16-bit): 0110 1 imm5 Rn Rd
3433                    let imm5 = (offset >> 2) as u16;
3434                    let instr: u16 =
3435                        0x6800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3436                    Ok(instr.to_le_bytes().to_vec())
3437                } else {
3438                    self.encode_thumb32_ldr(rd, &addr.base, offset)
3439                }
3440            }
3441
3442            // STR (can be 16-bit for simple cases)
3443            ArmOp::Str { rd, addr } => {
3444                let rd_bits = reg_to_bits(rd);
3445                let base_bits = reg_to_bits(&addr.base);
3446
3447                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3448                if let Some(offset_reg) = &addr.offset_reg {
3449                    let rm_bits = reg_to_bits(offset_reg);
3450
3451                    // If there's also an immediate offset, we need to ADD it first
3452                    if addr.offset != 0 {
3453                        // Use R12 (IP) as scratch to avoid clobbering the address register
3454                        // ADD R12, Rm, #offset; STR Rd, [base, R12]
3455                        let scratch = Reg::R12;
3456                        let mut bytes =
3457                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3458                        bytes.extend(self.encode_thumb32_str_reg(rd, &addr.base, &scratch)?);
3459                        return Ok(bytes);
3460                    }
3461
3462                    // Simple register offset: STR Rd, [Rn, Rm]
3463                    // 16-bit: only if Rd, Rn, Rm < R8
3464                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3465                        // STR Rd, [Rn, Rm] (16-bit): 0101 000 Rm Rn Rd
3466                        let instr: u16 = 0x5000
3467                            | ((rm_bits as u16) << 6)
3468                            | ((base_bits as u16) << 3)
3469                            | (rd_bits as u16);
3470                        return Ok(instr.to_le_bytes().to_vec());
3471                    }
3472
3473                    // 32-bit register offset
3474                    return self.encode_thumb32_str_reg(rd, &addr.base, offset_reg);
3475                }
3476
3477                // Immediate offset mode [base, #imm]
3478                let offset = addr.offset as u32;
3479
3480                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3481                    // STR Rd, [Rn, #imm5*4] (16-bit): 0110 0 imm5 Rn Rd
3482                    let imm5 = (offset >> 2) as u16;
3483                    let instr: u16 =
3484                        0x6000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3485                    Ok(instr.to_le_bytes().to_vec())
3486                } else {
3487                    self.encode_thumb32_str(rd, &addr.base, offset)
3488                }
3489            }
3490
3491            // LDRB (Thumb-2)
3492            ArmOp::Ldrb { rd, addr } => {
3493                let rd_bits = reg_to_bits(rd);
3494                let base_bits = reg_to_bits(&addr.base);
3495
3496                if let Some(offset_reg) = &addr.offset_reg {
3497                    if addr.offset != 0 {
3498                        let scratch = Reg::R12;
3499                        let mut bytes =
3500                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3501                        bytes.extend(self.encode_thumb32_ldrb_reg(rd, &addr.base, &scratch)?);
3502                        return Ok(bytes);
3503                    }
3504                    return self.encode_thumb32_ldrb_reg(rd, &addr.base, offset_reg);
3505                }
3506
3507                let offset = addr.offset as u32;
3508                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3509                    // LDRB Rd, [Rn, #imm5] (16-bit): 0111 1 imm5 Rn Rd
3510                    let instr: u16 = 0x7800
3511                        | ((offset as u16) << 6)
3512                        | ((base_bits as u16) << 3)
3513                        | (rd_bits as u16);
3514                    Ok(instr.to_le_bytes().to_vec())
3515                } else {
3516                    self.encode_thumb32_ldrb_imm(rd, &addr.base, offset)
3517                }
3518            }
3519
3520            // LDRSB (Thumb-2)
3521            ArmOp::Ldrsb { rd, addr } => {
3522                let rd_bits = reg_to_bits(rd);
3523                let base_bits = reg_to_bits(&addr.base);
3524
3525                if let Some(offset_reg) = &addr.offset_reg {
3526                    if addr.offset != 0 {
3527                        let scratch = Reg::R12;
3528                        let mut bytes =
3529                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3530                        bytes.extend(self.encode_thumb32_ldrsb_reg(rd, &addr.base, &scratch)?);
3531                        return Ok(bytes);
3532                    }
3533                    return self.encode_thumb32_ldrsb_reg(rd, &addr.base, offset_reg);
3534                }
3535
3536                let offset = addr.offset as u32;
3537                // LDRSB has no 16-bit immediate form (only register)
3538                // For 16-bit reg form: only if Rd, Rn, Rm < R8
3539                if rd_bits < 8 && base_bits < 8 && offset == 0 {
3540                    // No immediate 16-bit encoding for LDRSB; use 32-bit
3541                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3542                } else {
3543                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3544                }
3545            }
3546
3547            // LDRH (Thumb-2)
3548            ArmOp::Ldrh { rd, addr } => {
3549                let rd_bits = reg_to_bits(rd);
3550                let base_bits = reg_to_bits(&addr.base);
3551
3552                if let Some(offset_reg) = &addr.offset_reg {
3553                    if addr.offset != 0 {
3554                        let scratch = Reg::R12;
3555                        let mut bytes =
3556                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3557                        bytes.extend(self.encode_thumb32_ldrh_reg(rd, &addr.base, &scratch)?);
3558                        return Ok(bytes);
3559                    }
3560                    return self.encode_thumb32_ldrh_reg(rd, &addr.base, offset_reg);
3561                }
3562
3563                let offset = addr.offset as u32;
3564                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3565                    // LDRH Rd, [Rn, #imm5*2] (16-bit): 1000 1 imm5 Rn Rd
3566                    let imm5 = (offset >> 1) as u16;
3567                    let instr: u16 =
3568                        0x8800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3569                    Ok(instr.to_le_bytes().to_vec())
3570                } else {
3571                    self.encode_thumb32_ldrh_imm(rd, &addr.base, offset)
3572                }
3573            }
3574
3575            // LDRSH (Thumb-2)
3576            ArmOp::Ldrsh { rd, addr } => {
3577                if let Some(offset_reg) = &addr.offset_reg {
3578                    if addr.offset != 0 {
3579                        let scratch = Reg::R12;
3580                        let mut bytes =
3581                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3582                        bytes.extend(self.encode_thumb32_ldrsh_reg(rd, &addr.base, &scratch)?);
3583                        return Ok(bytes);
3584                    }
3585                    return self.encode_thumb32_ldrsh_reg(rd, &addr.base, offset_reg);
3586                }
3587
3588                let offset = addr.offset as u32;
3589                self.encode_thumb32_ldrsh_imm(rd, &addr.base, offset)
3590            }
3591
3592            // STRB (Thumb-2)
3593            ArmOp::Strb { rd, addr } => {
3594                let rd_bits = reg_to_bits(rd);
3595                let base_bits = reg_to_bits(&addr.base);
3596
3597                if let Some(offset_reg) = &addr.offset_reg {
3598                    if addr.offset != 0 {
3599                        let scratch = Reg::R12;
3600                        let mut bytes =
3601                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3602                        bytes.extend(self.encode_thumb32_strb_reg(rd, &addr.base, &scratch)?);
3603                        return Ok(bytes);
3604                    }
3605                    return self.encode_thumb32_strb_reg(rd, &addr.base, offset_reg);
3606                }
3607
3608                let offset = addr.offset as u32;
3609                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3610                    // STRB Rd, [Rn, #imm5] (16-bit): 0111 0 imm5 Rn Rd
3611                    let instr: u16 = 0x7000
3612                        | ((offset as u16) << 6)
3613                        | ((base_bits as u16) << 3)
3614                        | (rd_bits as u16);
3615                    Ok(instr.to_le_bytes().to_vec())
3616                } else {
3617                    self.encode_thumb32_strb_imm(rd, &addr.base, offset)
3618                }
3619            }
3620
3621            // STRH (Thumb-2)
3622            ArmOp::Strh { rd, addr } => {
3623                let rd_bits = reg_to_bits(rd);
3624                let base_bits = reg_to_bits(&addr.base);
3625
3626                if let Some(offset_reg) = &addr.offset_reg {
3627                    if addr.offset != 0 {
3628                        let scratch = Reg::R12;
3629                        let mut bytes =
3630                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3631                        bytes.extend(self.encode_thumb32_strh_reg(rd, &addr.base, &scratch)?);
3632                        return Ok(bytes);
3633                    }
3634                    return self.encode_thumb32_strh_reg(rd, &addr.base, offset_reg);
3635                }
3636
3637                let offset = addr.offset as u32;
3638                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3639                    // STRH Rd, [Rn, #imm5*2] (16-bit): 1000 0 imm5 Rn Rd
3640                    let imm5 = (offset >> 1) as u16;
3641                    let instr: u16 =
3642                        0x8000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3643                    Ok(instr.to_le_bytes().to_vec())
3644                } else {
3645                    self.encode_thumb32_strh_imm(rd, &addr.base, offset)
3646                }
3647            }
3648
3649            // MemorySize (Thumb-2)
3650            ArmOp::MemorySize { rd } => {
3651                // LSR rd, R10, #16 — memory size in bytes / 65536 = pages
3652                // Thumb-2 16-bit: LSRS Rd, Rm, #imm5 — 0000 1 imm5 Rm Rd
3653                let rd_bits = reg_to_bits(rd);
3654                let r10_bits = reg_to_bits(&Reg::R10);
3655                if rd_bits < 8 && r10_bits < 8 {
3656                    let instr: u16 =
3657                        0x0800 | (16u16 << 6) | ((r10_bits as u16) << 3) | (rd_bits as u16);
3658                    Ok(instr.to_le_bytes().to_vec())
3659                } else {
3660                    // Thumb-2 32-bit LSR: 1110 1010 010 0 1111 | 0 imm3 Rd imm2 01 Rm
3661                    let imm5: u32 = 16;
3662                    let imm3 = (imm5 >> 2) & 0x7;
3663                    let imm2 = imm5 & 0x3;
3664                    let hw1: u16 = 0xEA4F;
3665                    let hw2: u16 =
3666                        ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | 0x10 | r10_bits) as u16;
3667                    let mut bytes = hw1.to_le_bytes().to_vec();
3668                    bytes.extend_from_slice(&hw2.to_le_bytes());
3669                    Ok(bytes)
3670                }
3671            }
3672
3673            // MemoryGrow (Thumb-2)
3674            ArmOp::MemoryGrow { rd, .. } => {
3675                // On embedded with fixed memory, always return -1 (failure)
3676                // MVN rd, #0 → MOV rd, #-1
3677                // Thumb-2 32-bit: MVN: 1111 0 i 0 0 0 1 1 0 1111 | 0 imm3 Rd imm8
3678                let rd_bits = reg_to_bits(rd);
3679                let hw1: u16 = 0xF06F; // MVN with i=0
3680                let hw2: u16 = (rd_bits << 8) as u16; // imm8=0 → ~0 = 0xFFFFFFFF = -1
3681                let mut bytes = hw1.to_le_bytes().to_vec();
3682                bytes.extend_from_slice(&hw2.to_le_bytes());
3683                Ok(bytes)
3684            }
3685
3686            // BX (16-bit)
3687            ArmOp::Bx { rm } => {
3688                let rm_bits = reg_to_bits(rm) as u16;
3689                // BX Rm (16-bit): 0100 0111 0 Rm 000
3690                let instr: u16 = 0x4700 | (rm_bits << 3);
3691                Ok(instr.to_le_bytes().to_vec())
3692            }
3693
3694            // BLX (16-bit) - Branch with Link and Exchange
3695            // BLX Rm: 0100 0111 1 Rm 000
3696            ArmOp::Blx { rm } => {
3697                let rm_bits = reg_to_bits(rm) as u16;
3698                let instr: u16 = 0x4780 | (rm_bits << 3);
3699                Ok(instr.to_le_bytes().to_vec())
3700            }
3701
3702            // CallIndirect - indirect function call via table lookup
3703            // table_index_reg contains the table index
3704            // Generates (#642): MOVW ip,#size [; MOVT]; CMP idx,ip; BLO +1;
3705            //                   UDF #0; LSL R12,idx,#2; LDR R12,[R11,R12]; BLX R12
3706            // #650, table_byte_offset != 0 (a non-zero table in the contiguous
3707            // R11 region): the pointer load becomes
3708            //                   ADD R12,R11,R12; LDR R12,[R12,#offset]
3709            ArmOp::CallIndirect {
3710                rd: _,
3711                type_idx: _,
3712                table_index_reg,
3713                table_size,
3714                table_byte_offset,
3715            } => {
3716                let idx_reg = reg_to_bits(table_index_reg);
3717                let mut bytes = Vec::new();
3718
3719                // The expansion:
3720                // 1. Bounds guard (#642): trap (UDF #0, WASM Core §4.4.8) when
3721                //    index >= table size. Without it an out-of-bounds index
3722                //    reads past the table and BLXes whatever word lies there —
3723                //    an uncontrolled indirect branch instead of a trap.
3724                // 2. Multiplies index by 4 (function pointer size)
3725                // 3. Loads function pointer from table (table base in R11)
3726                // 4. Calls the function via BLX
3727                //
3728                // Table base setup must be done by caller/runtime. The type
3729                // check §4.4.8 also requires is discharged at COMPILE time:
3730                // the selector only emits this op after verifying the closed-
3731                // world property that every table entry's signature equals the
3732                // expected type (the raw code-pointer table carries no runtime
3733                // type ids to compare) — see the #642 selector guard.
3734
3735                // MOVW R12, #(size & 0xFFFF) — Thumb-2 T3:
3736                // 11110 i 100100 imm4 | 0 imm3 Rd imm8 (Rd=R12).
3737                let size_lo = *table_size & 0xFFFF;
3738                let hw1: u16 =
3739                    (0xF240 | (((size_lo >> 11) & 1) << 10) | ((size_lo >> 12) & 0xF)) as u16;
3740                let hw2: u16 =
3741                    ((((size_lo >> 8) & 0x7) << 12) | (12 << 8) | (size_lo & 0xFF)) as u16;
3742                bytes.extend_from_slice(&hw1.to_le_bytes());
3743                bytes.extend_from_slice(&hw2.to_le_bytes());
3744                // MOVT R12, #(size >> 16) — only when the table size exceeds
3745                // 16 bits (never in practice, but the guard must not compare
3746                // against a truncated size).
3747                let size_hi = *table_size >> 16;
3748                if size_hi != 0 {
3749                    let hw1: u16 =
3750                        (0xF2C0 | (((size_hi >> 11) & 1) << 10) | ((size_hi >> 12) & 0xF)) as u16;
3751                    let hw2: u16 =
3752                        ((((size_hi >> 8) & 0x7) << 12) | (12 << 8) | (size_hi & 0xFF)) as u16;
3753                    bytes.extend_from_slice(&hw1.to_le_bytes());
3754                    bytes.extend_from_slice(&hw2.to_le_bytes());
3755                }
3756                // CMP idx, R12 — 16-bit T2 (high-register capable):
3757                // 010001 01 N Rm(4) Rn(3), Rn full = N:Rn3.
3758                let cmp: u16 = (0x4500 | ((idx_reg & 8) << 4) | (12 << 3) | (idx_reg & 7)) as u16;
3759                bytes.extend_from_slice(&cmp.to_le_bytes());
3760                // BLO +1 insn (skip the UDF when index < size) — B<cond>.N
3761                // imm8=0: target = branch + 4. LO = unsigned lower.
3762                bytes.extend_from_slice(&0xD300u16.to_le_bytes());
3763                // UDF #0 — call_indirect out-of-bounds trap (same trap idiom as
3764                // the div-by-zero guards).
3765                bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3766
3767                // LSL R12, idx_reg, #2 (multiply index by 4)
3768                // Thumb-2 MOV with shift: 11101010 010 S 1111 | 0 imm3 Rd imm2 type Rm
3769                // LSL: type=00 (bits 5:4), imm5=2 -> imm3=000, imm2=10 (bits 7:6)
3770                // #597: the shift amount was previously shifted into bits 5:4 —
3771                // the TYPE field — encoding `mov.w ip, rm, ASR #32`, which
3772                // destroyed the index and dispatched table entry 0 for every
3773                // call. imm2 lives at bits 7:6.
3774                let hw1: u16 = 0xEA4F_u16; // MOV.W R12, Rm, LSL #2
3775                let hw2: u16 = ((0x0C00 | (0b10 << 6)) | idx_reg) as u16;
3776                bytes.extend_from_slice(&hw1.to_le_bytes());
3777                bytes.extend_from_slice(&hw2.to_le_bytes());
3778
3779                if *table_byte_offset == 0 {
3780                    // Table 0 (base = R11 itself): the pre-#650 single-load
3781                    // form — a single-table module's bytes stay identical BY
3782                    // CONSTRUCTION.
3783                    // LDR R12, [R11, R12] - load function pointer
3784                    // Thumb-2 LDR (register): 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
3785                    // Rn=R11, Rt=R12, Rm=R12, imm2=00 (no shift)
3786                    let ldr_hw1: u16 = 0xF85B; // LDR.W Rt, [R11, Rm]
3787                    let ldr_hw2: u16 = 0xC00C; // Rt=R12, imm2=00, Rm=R12
3788                    bytes.extend_from_slice(&ldr_hw1.to_le_bytes());
3789                    bytes.extend_from_slice(&ldr_hw2.to_le_bytes());
3790                } else {
3791                    // #650: table N of the contiguous R11 region — fold the
3792                    // compile-time base offset into the pointer load via the
3793                    // LDR imm12 form (R12 stays the only scratch, per the
3794                    // #212 convention).
3795                    assert!(
3796                        *table_byte_offset <= 4095,
3797                        "call_indirect table base offset {table_byte_offset} exceeds \
3798                         LDR imm12 — the selector must have declined this (#650)"
3799                    );
3800                    // ADD.W R12, R11, R12 — T3 ADD (register):
3801                    // 11101011000 S=0 Rn=1011 | 0 imm3=000 Rd=1100 imm2=00 type=00 Rm=1100
3802                    bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
3803                    bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
3804                    // LDR.W R12, [R12, #offset] — T3 LDR (immediate):
3805                    // 1111 1000 1101 Rn=1100 | Rt=1100 imm12
3806                    bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
3807                    bytes.extend_from_slice(
3808                        &((0xC000u16) | (*table_byte_offset as u16 & 0x0FFF)).to_le_bytes(),
3809                    );
3810                }
3811
3812                // BLX R12 (call function indirectly)
3813                // BLX Rm (16-bit): 0100 0111 1 Rm 000
3814                let blx: u16 = 0x47E0; // BLX R12
3815                bytes.extend_from_slice(&blx.to_le_bytes());
3816
3817                Ok(bytes)
3818            }
3819
3820            // Label pseudo-instruction: emits no machine code
3821            ArmOp::Label { .. } => Ok(Vec::new()),
3822
3823            // Conditional branch to label (generic) - offset 0, will be patched
3824            ArmOp::Bcc { cond, label: _ } => {
3825                use synth_synthesis::Condition;
3826                let cond_bits: u16 = match cond {
3827                    Condition::EQ => 0x0,
3828                    Condition::NE => 0x1,
3829                    Condition::HS => 0x2,
3830                    Condition::LO => 0x3,
3831                    Condition::HI => 0x8,
3832                    Condition::LS => 0x9,
3833                    Condition::GE => 0xA,
3834                    Condition::LT => 0xB,
3835                    Condition::GT => 0xC,
3836                    Condition::LE => 0xD,
3837                };
3838                // 16-bit B<cond> with offset 0: 1101 cond imm8
3839                let instr: u16 = 0xD000 | (cond_bits << 8);
3840                Ok(instr.to_le_bytes().to_vec())
3841            }
3842
3843            // Branch instructions
3844            ArmOp::B { label: _ } => {
3845                // Simplified: B.N with offset 0
3846                // For real usage, would need label resolution
3847                let instr: u16 = 0xE000; // B.N #0
3848                Ok(instr.to_le_bytes().to_vec())
3849            }
3850
3851            // BHS (Branch if Higher or Same) - used for bounds checking
3852            // Condition code: 0x2 (C set)
3853            ArmOp::Bhs { label: _ } => {
3854                // 16-bit B<cond> with offset 0: 1101 cond imm8
3855                // cond = 0x2 (HS)
3856                let instr: u16 = 0xD200; // BHS.N #0
3857                Ok(instr.to_le_bytes().to_vec())
3858            }
3859
3860            // BLO (Branch if Lower) - complementary to BHS
3861            // Condition code: 0x3 (C clear)
3862            ArmOp::Blo { label: _ } => {
3863                // 16-bit B<cond> with offset 0: 1101 cond imm8
3864                // cond = 0x3 (LO)
3865                let instr: u16 = 0xD300; // BLO.N #0
3866                Ok(instr.to_le_bytes().to_vec())
3867            }
3868
3869            // Branch with numeric offset (Thumb-2)
3870            // Thumb-2 B.W instruction: 32-bit with +-16MB range
3871            ArmOp::BOffset { offset } => {
3872                // offset is already the halfword displacement: (target - branch - 4) / 2
3873                // This is the raw encoded value, accounting for variable-length instructions
3874                let halfword_offset = *offset;
3875
3876                // 16-bit B.N encoding: 1110 0 imm11 (11-bit signed halfword offset)
3877                // Range: -1024 to +1022 halfwords
3878                if (-1024..=1022).contains(&halfword_offset) {
3879                    // 16-bit B.N encoding: 1110 0 imm11
3880                    let imm11 = (halfword_offset as u16) & 0x7FF;
3881                    let instr: u16 = 0xE000 | imm11;
3882                    Ok(instr.to_le_bytes().to_vec())
3883                } else {
3884                    // 32-bit B.W encoding for larger offsets
3885                    // First halfword: 1111 0 S imm10
3886                    // Second halfword: 10 J1 0 J2 imm11
3887                    // Total offset = SignExtend(S:I1:I2:imm10:imm11:0)
3888                    // where I1 = NOT(J1 XOR S), I2 = NOT(J2 XOR S)
3889
3890                    // The B.W (T4) encoding packs the signed offset as:
3891                    //   S:I1:I2:imm10:imm11:0  (25-bit signed, halfword-aligned)
3892                    // where J1 = NOT(I1 XOR S), J2 = NOT(I2 XOR S)
3893                    // Input halfword_offset already equals (target - PC - 4) / 2,
3894                    // so the full byte offset = halfword_offset << 1.
3895                    // The encoding fields split that 25-bit signed value (including the
3896                    // implicit trailing zero) as: S | imm10 | imm11
3897                    // with I1 = bit 23 and I2 = bit 22 of the signed offset.
3898                    let signed_offset = halfword_offset << 1; // byte offset
3899                    let s = if signed_offset < 0 { 1u32 } else { 0u32 };
3900                    let uoffset = signed_offset as u32;
3901                    let imm10 = (uoffset >> 12) & 0x3FF; // bits [21:12]
3902                    let imm11 = (uoffset >> 1) & 0x7FF; // bits [11:1]
3903                    let i1 = (uoffset >> 23) & 1; // bit 23
3904                    let i2 = (uoffset >> 22) & 1; // bit 22
3905                    let j1 = (!(i1 ^ s)) & 1; // J1 = NOT(I1 XOR S)
3906                    let j2 = (!(i2 ^ s)) & 1; // J2 = NOT(I2 XOR S)
3907
3908                    let hw1: u16 = (0xF000 | (s << 10) | imm10) as u16;
3909                    let hw2: u16 = (0x9000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
3910
3911                    let mut bytes = hw1.to_le_bytes().to_vec();
3912                    bytes.extend_from_slice(&hw2.to_le_bytes());
3913                    Ok(bytes)
3914                }
3915            }
3916
3917            // Conditional branch with numeric offset (Thumb-2)
3918            ArmOp::BCondOffset { cond, offset } => {
3919                use synth_synthesis::Condition;
3920                let cond_bits: u16 = match cond {
3921                    Condition::EQ => 0x0,
3922                    Condition::NE => 0x1,
3923                    Condition::HS => 0x2,
3924                    Condition::LO => 0x3,
3925                    Condition::HI => 0x8,
3926                    Condition::LS => 0x9,
3927                    Condition::GE => 0xA,
3928                    Condition::LT => 0xB,
3929                    Condition::GT => 0xC,
3930                    Condition::LE => 0xD,
3931                };
3932
3933                // offset is already the halfword displacement: (target - branch - 4) / 2
3934                // This is the raw imm8 value for 16-bit B<cond> encoding
3935                let halfword_offset = *offset;
3936
3937                // 16-bit B<cond> encoding: 1101 cond imm8
3938                // Range: -256 to +254 halfwords (imm8 is sign-extended and shifted left 1)
3939                if (-128..=127).contains(&halfword_offset) {
3940                    let imm8 = (halfword_offset as u16) & 0xFF;
3941                    let instr: u16 = 0xD000 | (cond_bits << 8) | imm8;
3942                    Ok(instr.to_le_bytes().to_vec())
3943                } else {
3944                    // 32-bit B<cond>.W for larger offsets
3945                    // First halfword: 1111 0 S cond imm6
3946                    // Second halfword: 10 J1 0 J2 imm11
3947                    let offset = halfword_offset >> 1;
3948                    let s = if offset < 0 { 1u32 } else { 0u32 };
3949                    let imm6 = ((offset >> 11) as u32) & 0x3F;
3950                    let imm11 = (offset as u32) & 0x7FF;
3951                    let j1 = if s == 1 { 1 } else { 0 };
3952                    let j2 = if s == 1 { 1 } else { 0 };
3953
3954                    let hw1: u16 = (0xF000 | (s << 10) | ((cond_bits as u32) << 6) | imm6) as u16;
3955                    let hw2: u16 = (0x8000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
3956
3957                    let mut bytes = hw1.to_le_bytes().to_vec();
3958                    bytes.extend_from_slice(&hw2.to_le_bytes());
3959                    Ok(bytes)
3960                }
3961            }
3962
3963            ArmOp::Bl { label: _ } => {
3964                // BL is always 32-bit in Thumb-2, encoded here as a relocatable
3965                // placeholder; an R_ARM_THM_CALL relocation patches the target
3966                // (see arm_backend.rs). The placeholder must carry an embedded
3967                // addend of -4 so the relocation nets to exactly the symbol S.
3968                //
3969                // Thumb BL computes `target = (P + 4) + signed_offset`. Under
3970                // R_ARM_THM_CALL the linker resolves using the in-place addend;
3971                // a 0xF800 placeholder (addend 0) lands at S+4 — every call one
3972                // instruction past the callee entry (#174). The correct
3973                // placeholder is what `gas` emits for `bl <extern>`:
3974                //   f7ff fffe  ->  `bl <self>`  (S=1, J1=J2=1, imm = -4 addend),
3975                // i.e. hw1=0xF7FF, hw2=0xFFFE. This nets to S, not S+4.
3976                // (The earlier 0xD000 was worse still — a ~+0x600000 addend,
3977                // the garbage `bl c0000c` and "truncated to fit" of #167.)
3978                let hw1: u16 = 0xF7FF;
3979                let hw2: u16 = 0xFFFE;
3980                let mut bytes = hw1.to_le_bytes().to_vec();
3981                bytes.extend_from_slice(&hw2.to_le_bytes());
3982                Ok(bytes)
3983            }
3984
3985            // MVN
3986            ArmOp::Mvn { rd, op2 } => {
3987                if let Operand2::Reg(rm) = op2 {
3988                    let rd_bits = reg_to_bits(rd) as u16;
3989                    let rm_bits = reg_to_bits(rm) as u16;
3990
3991                    if rd_bits < 8 && rm_bits < 8 {
3992                        // MVNS Rd, Rm (16-bit): 0100 0011 11 Rm Rd
3993                        let instr: u16 = 0x43C0 | (rm_bits << 3) | rd_bits;
3994                        Ok(instr.to_le_bytes().to_vec())
3995                    } else {
3996                        // 32-bit MVN
3997                        let hw1: u16 = 0xEA6F_u16;
3998                        let hw2: u16 = ((reg_to_bits(rd) << 8) | reg_to_bits(rm)) as u16;
3999                        let mut bytes = hw1.to_le_bytes().to_vec();
4000                        bytes.extend_from_slice(&hw2.to_le_bytes());
4001                        Ok(bytes)
4002                    }
4003                } else {
4004                    let instr: u16 = 0xBF00;
4005                    Ok(instr.to_le_bytes().to_vec())
4006                }
4007            }
4008
4009            // MOVW - Move Wide (Thumb-2 32-bit)
4010            ArmOp::Movw { rd, imm16 } => {
4011                self.encode_thumb32_movw_raw(reg_to_bits(rd), *imm16 as u32)
4012            }
4013
4014            // MOVT - Move Top (Thumb-2 32-bit)
4015            ArmOp::Movt { rd, imm16 } => {
4016                self.encode_thumb32_movt_raw(reg_to_bits(rd), *imm16 as u32)
4017            }
4018
4019            // #237: symbol-relative MOVW/MOVT. Encode the addend's low/high 16
4020            // bits in place; the backend records an R_ARM_MOVW_ABS_NC /
4021            // R_ARM_MOVT_ABS relocation against `symbol`, so the linker adds the
4022            // symbol's final address to the in-place addend (REL semantics).
4023            ArmOp::MovwSym { rd, addend, .. } => {
4024                self.encode_thumb32_movw_raw(reg_to_bits(rd), (*addend as u32) & 0xffff)
4025            }
4026            ArmOp::MovtSym { rd, addend, .. } => {
4027                self.encode_thumb32_movt_raw(reg_to_bits(rd), ((*addend as u32) >> 16) & 0xffff)
4028            }
4029
4030            // #345: literal-pool address load — emit a PLACEHOLDER `LDR.W rd,
4031            // [pc, #0]` (U=1, imm12=0). The backend (arm_backend.rs) places the
4032            // 4-byte pool word at the end of the function, records the R_ARM_ABS32
4033            // relocation against `symbol+addend`, and patches the imm12 with the
4034            // real PC-relative distance once the pool offset is known.
4035            // Encoding T2: 1111 1000 1101 1111 | Rt(4) imm12(12), with the literal
4036            // base = Align(PC,4) and PC = address of this instruction + 4.
4037            ArmOp::LdrSym { rd, .. } => {
4038                let rt = reg_to_bits(rd) as u16;
4039                let hw1: u16 = 0xF8DF; // LDR.W (literal), U=1
4040                let hw2: u16 = rt << 12; // imm12 = 0 placeholder
4041                let mut bytes = Vec::with_capacity(4);
4042                bytes.extend_from_slice(&hw1.to_le_bytes());
4043                bytes.extend_from_slice(&hw2.to_le_bytes());
4044                Ok(bytes)
4045            }
4046
4047            // SetCond: Materialize condition flag into register (0 or 1)
4048            // Strategy: ITE <cond>; MOV Rd, #1; MOV Rd, #0
4049            // IMPORTANT: Must use ITE (If-Then-Else) because 16-bit Thumb MOV
4050            // always sets flags (MOVS). We need to evaluate the condition BEFORE
4051            // any MOV instruction clobbers the flags from CMP.
4052            ArmOp::SetCond { rd, cond } => {
4053                let rd_bits = reg_to_bits(rd) as u16;
4054
4055                // Condition code encoding for IT block
4056                use synth_synthesis::Condition;
4057                let cond_bits: u16 = match cond {
4058                    Condition::EQ => 0x0,
4059                    Condition::NE => 0x1,
4060                    Condition::LT => 0xB,
4061                    Condition::LE => 0xD,
4062                    Condition::GT => 0xC,
4063                    Condition::GE => 0xA,
4064                    Condition::LO => 0x3, // CC/LO (unsigned <)
4065                    Condition::LS => 0x9, // LS (unsigned <=)
4066                    Condition::HI => 0x8, // HI (unsigned >)
4067                    Condition::HS => 0x2, // CS/HS (unsigned >=)
4068                };
4069
4070                // ITE <cond>: encodes If-Then-Else block
4071                // The mask field depends on firstcond[0]:
4072                // - If firstcond[0] = 0: mask = 0xC for TE pattern (ITE EQ = BF0C)
4073                // - If firstcond[0] = 1: mask = 0x4 for TE pattern (ITE NE = BF14)
4074                let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4075                let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4076
4077                // Materialize 0/1 into Rd. The 16-bit MOVS (T1) encodes Rd in a
4078                // 3-bit field (bits[10:8]) — only R0–R7. For a high register
4079                // (R8–R12) `rd_bits << 8` overflows into bit 11 and silently
4080                // turns MOVS into CMP (00100 → 00101), corrupting the result
4081                // (this mis-materialized gale's `has_waiter`, so its `local.set`
4082                // stored a stale register → the binary-sem WAKE dispatch read
4083                // garbage). Use the 32-bit MOV.W (T2) for high registers, which
4084                // has a 4-bit Rd field. MOV.W with S=0 doesn't set flags, which
4085                // is fine inside the ITE (the materialized value is the result;
4086                // the flags are not consumed afterwards).
4087                let mut bytes = ite_instr.to_le_bytes().to_vec();
4088                let push_mov = |bytes: &mut Vec<u8>, imm: u16| {
4089                    if rd_bits <= 7 {
4090                        let m: u16 = 0x2000 | (rd_bits << 8) | imm; // 16-bit MOVS Rd,#imm
4091                        bytes.extend_from_slice(&m.to_le_bytes());
4092                    } else {
4093                        // 32-bit MOV.W Rd, #imm (T2): F04F | (Rd<<8) | imm8
4094                        let hw1: u16 = 0xF04F;
4095                        let hw2: u16 = (rd_bits << 8) | imm;
4096                        bytes.extend_from_slice(&hw1.to_le_bytes());
4097                        bytes.extend_from_slice(&hw2.to_le_bytes());
4098                    }
4099                };
4100                push_mov(&mut bytes, 1); // Then branch (condition true)  → 1
4101                push_mov(&mut bytes, 0); // Else branch (condition false) → 0
4102                Ok(bytes)
4103            }
4104
4105            // I64SetCond: Compare two i64 register pairs, result 0/1 in rd
4106            // EQ/NE: CMP lo,lo; IT EQ; CMPEQ hi,hi; ITE <cond>; MOV 1; MOV 0
4107            // LT: CMP lo,lo; SBCS rd,hi,hi; ITE LT; MOV 1; MOV 0
4108            // GT: CMP lo,lo (swapped); SBCS rd,hi,hi (swapped); ITE LT; MOV 1; MOV 0
4109            ArmOp::I64SetCond {
4110                rd,
4111                rn_lo,
4112                rn_hi,
4113                rm_lo,
4114                rm_hi,
4115                cond,
4116            } => {
4117                use synth_synthesis::Condition;
4118                let rd_bits = reg_to_bits(rd) as u16;
4119                let mut bytes = Vec::new();
4120
4121                // Helper: encode CMP Rn, Rm (16-bit)
4122                let encode_cmp_reg = |rn: &synth_synthesis::Reg,
4123                                      rm: &synth_synthesis::Reg|
4124                 -> Vec<u8> {
4125                    let rn_bits = reg_to_bits(rn) as u16;
4126                    let rm_bits = reg_to_bits(rm) as u16;
4127                    if rn_bits < 8 && rm_bits < 8 {
4128                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
4129                        instr.to_le_bytes().to_vec()
4130                    } else {
4131                        let n_bit = (rn_bits >> 3) & 1;
4132                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
4133                        instr.to_le_bytes().to_vec()
4134                    }
4135                };
4136
4137                // Helper: encode ITE <cond> (2 bytes)
4138                let encode_ite = |cond_bits: u16| -> Vec<u8> {
4139                    let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4140                    let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4141                    ite_instr.to_le_bytes().to_vec()
4142                };
4143
4144                // Helper: encode SetCond (ITE + MOV #1 + MOV #0) for given condition
4145                let encode_setcond = |cond_bits: u16, rd_bits: u16| -> Vec<u8> {
4146                    let mut b = encode_ite(cond_bits);
4147                    if rd_bits < 8 {
4148                        let mov_one: u16 = 0x2001 | (rd_bits << 8);
4149                        let mov_zero: u16 = 0x2000 | (rd_bits << 8);
4150                        b.extend_from_slice(&mov_one.to_le_bytes());
4151                        b.extend_from_slice(&mov_zero.to_le_bytes());
4152                    } else {
4153                        // #311: rd >= R8 — the 16-bit MOV imm8 form has a 3-bit
4154                        // rd field; rd_bits<<8 overflows into bit 11 and
4155                        // TRANSMUTES the MOV into CMP (0x2001|0x0800 = 0x2801 =
4156                        // CMP r0,#1): the boolean dies in the flags and the
4157                        // consumer reads a stale register. Use the 32-bit
4158                        // MOV.W (T2: F04F 0000|rd<<8|imm8) — IT-legal,
4159                        // flag-preserving. Same class as H-CODE-9 / #180.
4160                        for imm in [1u16, 0u16] {
4161                            let hw1: u16 = 0xF04F;
4162                            let hw2: u16 = (rd_bits << 8) | imm;
4163                            b.extend_from_slice(&hw1.to_le_bytes());
4164                            b.extend_from_slice(&hw2.to_le_bytes());
4165                        }
4166                    }
4167                    b
4168                };
4169
4170                match cond {
4171                    Condition::EQ | Condition::NE => {
4172                        // CMP rn_lo, rm_lo (compare low words)
4173                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4174
4175                        // IT EQ (execute next instruction only if Z=1)
4176                        let it_eq: u16 = 0xBF08; // IT EQ: cond=0000, mask=1000
4177                        bytes.extend_from_slice(&it_eq.to_le_bytes());
4178
4179                        // CMPEQ rn_hi, rm_hi (compare high words, only if low equal)
4180                        bytes.extend_from_slice(&encode_cmp_reg(rn_hi, rm_hi));
4181
4182                        // ITE <cond>; MOV rd, #1; MOV rd, #0
4183                        let cond_bits: u16 = match cond {
4184                            Condition::EQ => 0x0,
4185                            Condition::NE => 0x1,
4186                            _ => unreachable!(),
4187                        };
4188                        bytes.extend_from_slice(&encode_setcond(cond_bits, rd_bits));
4189                    }
4190
4191                    Condition::LT => {
4192                        // CMP rn_lo, rm_lo (sets C flag for borrow)
4193                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4194
4195                        // SBCS rd, rn_hi, rm_hi (subtract with carry, sets N,V flags)
4196                        // SBCS.W Rd, Rn, Rm: EB70 Rn | 0000 Rd 0000 Rm
4197                        let rn_hi_bits = reg_to_bits(rn_hi);
4198                        let rm_hi_bits = reg_to_bits(rm_hi);
4199                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4200                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4201                        bytes.extend_from_slice(&hw1.to_le_bytes());
4202                        bytes.extend_from_slice(&hw2.to_le_bytes());
4203
4204                        // ITE LT; MOV rd, #1; MOV rd, #0
4205                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4206                    }
4207
4208                    Condition::GT => {
4209                        // GT(a,b) = LT(b,a): swap operands
4210                        // CMP rm_lo, rn_lo (swapped)
4211                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4212
4213                        // SBCS rd, rm_hi, rn_hi (swapped)
4214                        let rm_hi_bits = reg_to_bits(rm_hi);
4215                        let rn_hi_bits = reg_to_bits(rn_hi);
4216                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4217                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4218                        bytes.extend_from_slice(&hw1.to_le_bytes());
4219                        bytes.extend_from_slice(&hw2.to_le_bytes());
4220
4221                        // ITE LT; MOV rd, #1; MOV rd, #0
4222                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4223                    }
4224
4225                    Condition::LE => {
4226                        // LE(a,b) = !GT(a,b): use GT logic but invert result
4227                        // GT(a,b) = LT(b,a): so we do CMP(b,a) and check LT, then invert
4228                        // CMP rm_lo, rn_lo (swapped, same as GT)
4229                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4230
4231                        // SBCS rd, rm_hi, rn_hi (swapped)
4232                        let rm_hi_bits = reg_to_bits(rm_hi);
4233                        let rn_hi_bits = reg_to_bits(rn_hi);
4234                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4235                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4236                        bytes.extend_from_slice(&hw1.to_le_bytes());
4237                        bytes.extend_from_slice(&hw2.to_le_bytes());
4238
4239                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT, so inverting GT result)
4240                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4241                    }
4242
4243                    Condition::GE => {
4244                        // GE(a,b) = !LT(a,b): use LT logic but invert result
4245                        // CMP rn_lo, rm_lo (same as LT)
4246                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4247
4248                        // SBCS rd, rn_hi, rm_hi (same as LT)
4249                        let rn_hi_bits = reg_to_bits(rn_hi);
4250                        let rm_hi_bits = reg_to_bits(rm_hi);
4251                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4252                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4253                        bytes.extend_from_slice(&hw1.to_le_bytes());
4254                        bytes.extend_from_slice(&hw2.to_le_bytes());
4255
4256                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT)
4257                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4258                    }
4259
4260                    // Unsigned comparisons - same instruction sequence, different conditions
4261                    Condition::LO => {
4262                        // LO (unsigned LT): CMP lo, SBCS hi, check C=0
4263                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4264                        let rn_hi_bits = reg_to_bits(rn_hi);
4265                        let rm_hi_bits = reg_to_bits(rm_hi);
4266                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4267                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4268                        bytes.extend_from_slice(&hw1.to_le_bytes());
4269                        bytes.extend_from_slice(&hw2.to_le_bytes());
4270                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4271                    }
4272
4273                    Condition::HI => {
4274                        // HI (unsigned GT): swap operands and check LO
4275                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4276                        let rm_hi_bits = reg_to_bits(rm_hi);
4277                        let rn_hi_bits = reg_to_bits(rn_hi);
4278                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4279                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4280                        bytes.extend_from_slice(&hw1.to_le_bytes());
4281                        bytes.extend_from_slice(&hw2.to_le_bytes());
4282                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4283                    }
4284
4285                    Condition::LS => {
4286                        // LS (unsigned LE): !(a > b) = !(HI), so do HI and invert
4287                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4288                        let rm_hi_bits = reg_to_bits(rm_hi);
4289                        let rn_hi_bits = reg_to_bits(rn_hi);
4290                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4291                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4292                        bytes.extend_from_slice(&hw1.to_le_bytes());
4293                        bytes.extend_from_slice(&hw2.to_le_bytes());
4294                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4295                    }
4296
4297                    Condition::HS => {
4298                        // HS (unsigned GE): !(a < b) = !(LO)
4299                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4300                        let rn_hi_bits = reg_to_bits(rn_hi);
4301                        let rm_hi_bits = reg_to_bits(rm_hi);
4302                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4303                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4304                        bytes.extend_from_slice(&hw1.to_le_bytes());
4305                        bytes.extend_from_slice(&hw2.to_le_bytes());
4306                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4307                    }
4308                }
4309
4310                Ok(bytes)
4311            }
4312
4313            // I64SetCondZ: Test if i64 register pair is zero, result 0/1 in rd
4314            // ORR.W rd, rn_lo, rn_hi; CMP rd, #0; ITE EQ; MOV 1; MOV 0
4315            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
4316                let rd_bits = reg_to_bits(rd);
4317                let rn_lo_bits = reg_to_bits(rn_lo);
4318                let rn_hi_bits = reg_to_bits(rn_hi);
4319                let mut bytes = Vec::new();
4320
4321                // ORR.W rd, rn_lo, rn_hi: EA40 rn_lo | 0000 rd 0000 rn_hi
4322                let hw1: u16 = (0xEA40 | rn_lo_bits) as u16;
4323                let hw2: u16 = ((rd_bits << 8) | rn_hi_bits) as u16;
4324                bytes.extend_from_slice(&hw1.to_le_bytes());
4325                bytes.extend_from_slice(&hw2.to_le_bytes());
4326
4327                // CMP rd, #0 — 16-bit form only for r0-r7 (3-bit rd field);
4328                // high registers take CMP.W (T2: F1B0|rn 0F00|imm8). This was
4329                // H-CODE-9: rd_bits<<8 overflowing the field compared the
4330                // WRONG register. Same hardening as the #311 SetCond fix.
4331                if rd_bits < 8 {
4332                    let cmp_instr: u16 = 0x2800 | ((rd_bits as u16) << 8);
4333                    bytes.extend_from_slice(&cmp_instr.to_le_bytes());
4334                } else {
4335                    let hw1: u16 = 0xF1B0 | (rd_bits as u16);
4336                    let hw2: u16 = 0x0F00;
4337                    bytes.extend_from_slice(&hw1.to_le_bytes());
4338                    bytes.extend_from_slice(&hw2.to_le_bytes());
4339                }
4340
4341                // ITE EQ; MOV rd, #1; MOV rd, #0 (32-bit MOV.W for rd >= R8,
4342                // #311 — see I64SetCond)
4343                let mask = 0xC_u16; // ITE EQ mask: firstcond[0]=0, mask=0xC
4344                let ite_instr: u16 = 0xBF00 | mask;
4345                bytes.extend_from_slice(&ite_instr.to_le_bytes());
4346                if rd_bits < 8 {
4347                    let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
4348                    let mov_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
4349                    bytes.extend_from_slice(&mov_one.to_le_bytes());
4350                    bytes.extend_from_slice(&mov_zero.to_le_bytes());
4351                } else {
4352                    for imm in [1u16, 0u16] {
4353                        let hw1: u16 = 0xF04F;
4354                        let hw2: u16 = ((rd_bits as u16) << 8) | imm;
4355                        bytes.extend_from_slice(&hw1.to_le_bytes());
4356                        bytes.extend_from_slice(&hw2.to_le_bytes());
4357                    }
4358                }
4359
4360                Ok(bytes)
4361            }
4362
4363            // I64Mul: 64-bit multiply using UMULL + MLA cross products
4364            // Formula: result = (a_lo * b_lo) + ((a_lo * b_hi + a_hi * b_lo) << 32)
4365            // Uses R12 as scratch register
4366            ArmOp::I64Mul {
4367                rd_lo,
4368                rd_hi,
4369                rn_lo,
4370                rn_hi,
4371                rm_lo,
4372                rm_hi,
4373            } => {
4374                let rd_lo_bits = reg_to_bits(rd_lo);
4375                let rd_hi_bits = reg_to_bits(rd_hi);
4376                let rn_lo_bits = reg_to_bits(rn_lo);
4377                let rn_hi_bits = reg_to_bits(rn_hi);
4378                let rm_lo_bits = reg_to_bits(rm_lo);
4379                let rm_hi_bits = reg_to_bits(rm_hi);
4380                let r12: u32 = 12; // IP scratch register
4381                let mut bytes = Vec::new();
4382
4383                // 1. MUL R12, rn_lo, rm_hi  (R12 = a_lo * b_hi)
4384                // Thumb-2 MUL: hw1=0xFB00|Rn, hw2=0xF000|(Rd<<8)|Rm
4385                let hw1: u16 = (0xFB00 | rn_lo_bits) as u16;
4386                let hw2: u16 = (0xF000 | (r12 << 8) | rm_hi_bits) as u16;
4387                bytes.extend_from_slice(&hw1.to_le_bytes());
4388                bytes.extend_from_slice(&hw2.to_le_bytes());
4389
4390                // 2. MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
4391                // Thumb-2 MLA: hw1=0xFB00|Rn, hw2=(Ra<<12)|(Rd<<8)|Rm
4392                let hw1: u16 = (0xFB00 | rn_hi_bits) as u16;
4393                let hw2: u16 = ((r12 << 12) | (r12 << 8) | rm_lo_bits) as u16;
4394                bytes.extend_from_slice(&hw1.to_le_bytes());
4395                bytes.extend_from_slice(&hw2.to_le_bytes());
4396
4397                // 3. UMULL rd_lo, rd_hi, rn_lo, rm_lo  (rd_lo:rd_hi = a_lo * b_lo)
4398                // Thumb-2 UMULL: hw1=0xFBA0|Rn, hw2=(RdLo<<12)|(RdHi<<8)|Rm
4399                let hw1: u16 = (0xFBA0 | rn_lo_bits) as u16;
4400                let hw2: u16 = ((rd_lo_bits << 12) | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4401                bytes.extend_from_slice(&hw1.to_le_bytes());
4402                bytes.extend_from_slice(&hw2.to_le_bytes());
4403
4404                // 4. ADD rd_hi, R12  (rd_hi += cross products)
4405                // 16-bit high reg ADD: 01000100 D Rm Rdn[2:0]
4406                let d_bit = (rd_hi_bits >> 3) & 1;
4407                let add_instr: u16 =
4408                    (0x4400 | (d_bit << 7) | (r12 << 3) | (rd_hi_bits & 0x7)) as u16;
4409                bytes.extend_from_slice(&add_instr.to_le_bytes());
4410
4411                Ok(bytes)
4412            }
4413
4414            // I64Shl: 64-bit shift left with branch for n<32 vs n>=32
4415            // rm_hi (R3) is used as temp register
4416            ArmOp::I64Shl {
4417                rd_lo,
4418                rd_hi,
4419                rn_lo,
4420                rn_hi,
4421                rm_lo,
4422                rm_hi,
4423            } => {
4424                let rd_lo_bits = reg_to_bits(rd_lo);
4425                let rd_hi_bits = reg_to_bits(rd_hi);
4426                let rn_lo_bits = reg_to_bits(rn_lo);
4427                let rn_hi_bits = reg_to_bits(rn_hi);
4428                let rm_lo_bits = reg_to_bits(rm_lo);
4429                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4430                let mut bytes = Vec::new();
4431
4432                // AND.W rm_lo, rm_lo, #63  (mask shift amount to 6 bits)
4433                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4434                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4435                bytes.extend_from_slice(&hw1.to_le_bytes());
4436                bytes.extend_from_slice(&hw2.to_le_bytes());
4437
4438                // SUBS.W rm_hi, rm_lo, #32  (rm_hi = n-32, sets flags)
4439                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4440                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4441                bytes.extend_from_slice(&hw1.to_le_bytes());
4442                bytes.extend_from_slice(&hw2.to_le_bytes());
4443
4444                // BPL .large (branch if n >= 32, offset = +10 halfwords)
4445                let bpl: u16 = 0xD50A;
4446                bytes.extend_from_slice(&bpl.to_le_bytes());
4447
4448                // --- Small shift (n < 32) ---
4449                // RSB.W rm_hi, rm_lo, #32  (rm_hi = 32-n)
4450                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4451                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4452                bytes.extend_from_slice(&hw1.to_le_bytes());
4453                bytes.extend_from_slice(&hw2.to_le_bytes());
4454
4455                // LSR.W rm_hi, rn_lo, rm_hi  (rm_hi = lo >> (32-n), overflow bits)
4456                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4457                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4458                bytes.extend_from_slice(&hw1.to_le_bytes());
4459                bytes.extend_from_slice(&hw2.to_le_bytes());
4460
4461                // LSL.W rd_hi, rn_hi, rm_lo  (hi <<= n)
4462                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4463                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4464                bytes.extend_from_slice(&hw1.to_le_bytes());
4465                bytes.extend_from_slice(&hw2.to_le_bytes());
4466
4467                // ORR.W rd_hi, rd_hi, rm_hi  (hi |= overflow bits from lo)
4468                let hw1: u16 = (0xEA40 | rd_hi_bits) as u16;
4469                let hw2: u16 = ((rd_hi_bits << 8) | rm_hi_bits) as u16;
4470                bytes.extend_from_slice(&hw1.to_le_bytes());
4471                bytes.extend_from_slice(&hw2.to_le_bytes());
4472
4473                // LSL.W rd_lo, rn_lo, rm_lo  (lo <<= n)
4474                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4475                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4476                bytes.extend_from_slice(&hw1.to_le_bytes());
4477                bytes.extend_from_slice(&hw2.to_le_bytes());
4478
4479                // B .done (skip large shift: +2 halfwords)
4480                let b_done: u16 = 0xE002;
4481                bytes.extend_from_slice(&b_done.to_le_bytes());
4482
4483                // --- Large shift (n >= 32) ---
4484                // LSL.W rd_hi, rn_lo, rm_hi  (hi = lo << (n-32))
4485                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4486                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_hi_bits) as u16;
4487                bytes.extend_from_slice(&hw1.to_le_bytes());
4488                bytes.extend_from_slice(&hw2.to_le_bytes());
4489
4490                // MOV rd_lo, #0
4491                let mov_zero: u16 = 0x2000 | ((rd_lo_bits as u16) << 8);
4492                bytes.extend_from_slice(&mov_zero.to_le_bytes());
4493
4494                Ok(bytes) // Total: 38 bytes
4495            }
4496
4497            // I64ShrU: 64-bit logical shift right with branch for n<32 vs n>=32
4498            ArmOp::I64ShrU {
4499                rd_lo,
4500                rd_hi,
4501                rn_lo,
4502                rn_hi,
4503                rm_lo,
4504                rm_hi,
4505            } => {
4506                let rd_lo_bits = reg_to_bits(rd_lo);
4507                let rd_hi_bits = reg_to_bits(rd_hi);
4508                let rn_lo_bits = reg_to_bits(rn_lo);
4509                let rn_hi_bits = reg_to_bits(rn_hi);
4510                let rm_lo_bits = reg_to_bits(rm_lo);
4511                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4512                let mut bytes = Vec::new();
4513
4514                // AND.W rm_lo, rm_lo, #63
4515                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4516                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4517                bytes.extend_from_slice(&hw1.to_le_bytes());
4518                bytes.extend_from_slice(&hw2.to_le_bytes());
4519
4520                // SUBS.W rm_hi, rm_lo, #32
4521                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4522                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4523                bytes.extend_from_slice(&hw1.to_le_bytes());
4524                bytes.extend_from_slice(&hw2.to_le_bytes());
4525
4526                // BPL .large (+10 halfwords)
4527                let bpl: u16 = 0xD50A;
4528                bytes.extend_from_slice(&bpl.to_le_bytes());
4529
4530                // --- Small shift (n < 32) ---
4531                // RSB.W rm_hi, rm_lo, #32  (rm_hi = 32-n)
4532                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4533                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4534                bytes.extend_from_slice(&hw1.to_le_bytes());
4535                bytes.extend_from_slice(&hw2.to_le_bytes());
4536
4537                // LSL.W rm_hi, rn_hi, rm_hi  (rm_hi = hi << (32-n), bits flowing to lo)
4538                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4539                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4540                bytes.extend_from_slice(&hw1.to_le_bytes());
4541                bytes.extend_from_slice(&hw2.to_le_bytes());
4542
4543                // LSR.W rd_lo, rn_lo, rm_lo  (lo >>= n)
4544                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4545                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4546                bytes.extend_from_slice(&hw1.to_le_bytes());
4547                bytes.extend_from_slice(&hw2.to_le_bytes());
4548
4549                // ORR.W rd_lo, rd_lo, rm_hi  (lo |= overflow from hi)
4550                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4551                let hw2: u16 = ((rd_lo_bits << 8) | rm_hi_bits) as u16;
4552                bytes.extend_from_slice(&hw1.to_le_bytes());
4553                bytes.extend_from_slice(&hw2.to_le_bytes());
4554
4555                // LSR.W rd_hi, rn_hi, rm_lo  (hi >>= n, logical)
4556                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4557                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4558                bytes.extend_from_slice(&hw1.to_le_bytes());
4559                bytes.extend_from_slice(&hw2.to_le_bytes());
4560
4561                // B .done (+2 halfwords)
4562                let b_done: u16 = 0xE002;
4563                bytes.extend_from_slice(&b_done.to_le_bytes());
4564
4565                // --- Large shift (n >= 32) ---
4566                // LSR.W rd_lo, rn_hi, rm_hi  (lo = hi >> (n-32))
4567                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4568                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_hi_bits) as u16;
4569                bytes.extend_from_slice(&hw1.to_le_bytes());
4570                bytes.extend_from_slice(&hw2.to_le_bytes());
4571
4572                // MOV rd_hi, #0
4573                let mov_zero: u16 = 0x2000 | ((rd_hi_bits as u16) << 8);
4574                bytes.extend_from_slice(&mov_zero.to_le_bytes());
4575
4576                Ok(bytes) // Total: 38 bytes
4577            }
4578
4579            // I64ShrS: 64-bit arithmetic shift right with branch for n<32 vs n>=32
4580            ArmOp::I64ShrS {
4581                rd_lo,
4582                rd_hi,
4583                rn_lo,
4584                rn_hi,
4585                rm_lo,
4586                rm_hi,
4587            } => {
4588                let rd_lo_bits = reg_to_bits(rd_lo);
4589                let rd_hi_bits = reg_to_bits(rd_hi);
4590                let rn_lo_bits = reg_to_bits(rn_lo);
4591                let rn_hi_bits = reg_to_bits(rn_hi);
4592                let rm_lo_bits = reg_to_bits(rm_lo);
4593                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4594                let mut bytes = Vec::new();
4595
4596                // AND.W rm_lo, rm_lo, #63
4597                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4598                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4599                bytes.extend_from_slice(&hw1.to_le_bytes());
4600                bytes.extend_from_slice(&hw2.to_le_bytes());
4601
4602                // SUBS.W rm_hi, rm_lo, #32
4603                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4604                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4605                bytes.extend_from_slice(&hw1.to_le_bytes());
4606                bytes.extend_from_slice(&hw2.to_le_bytes());
4607
4608                // BPL .large (+10 halfwords)
4609                let bpl: u16 = 0xD50A;
4610                bytes.extend_from_slice(&bpl.to_le_bytes());
4611
4612                // --- Small shift (n < 32) ---
4613                // RSB.W rm_hi, rm_lo, #32
4614                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4615                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4616                bytes.extend_from_slice(&hw1.to_le_bytes());
4617                bytes.extend_from_slice(&hw2.to_le_bytes());
4618
4619                // LSL.W rm_hi, rn_hi, rm_hi  (rm_hi = hi << (32-n), bits flowing to lo)
4620                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4621                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4622                bytes.extend_from_slice(&hw1.to_le_bytes());
4623                bytes.extend_from_slice(&hw2.to_le_bytes());
4624
4625                // LSR.W rd_lo, rn_lo, rm_lo  (lo >>= n, logical for lo word)
4626                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4627                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4628                bytes.extend_from_slice(&hw1.to_le_bytes());
4629                bytes.extend_from_slice(&hw2.to_le_bytes());
4630
4631                // ORR.W rd_lo, rd_lo, rm_hi  (lo |= overflow from hi)
4632                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4633                let hw2: u16 = ((rd_lo_bits << 8) | rm_hi_bits) as u16;
4634                bytes.extend_from_slice(&hw1.to_le_bytes());
4635                bytes.extend_from_slice(&hw2.to_le_bytes());
4636
4637                // ASR.W rd_hi, rn_hi, rm_lo  (hi >>= n, arithmetic/sign-extending)
4638                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
4639                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4640                bytes.extend_from_slice(&hw1.to_le_bytes());
4641                bytes.extend_from_slice(&hw2.to_le_bytes());
4642
4643                // B .done (+3 halfwords, large shift is 8 bytes)
4644                let b_done: u16 = 0xE003;
4645                bytes.extend_from_slice(&b_done.to_le_bytes());
4646
4647                // --- Large shift (n >= 32) ---
4648                // ASR.W rd_lo, rn_hi, rm_hi  (lo = hi >>> (n-32))
4649                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
4650                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_hi_bits) as u16;
4651                bytes.extend_from_slice(&hw1.to_le_bytes());
4652                bytes.extend_from_slice(&hw2.to_le_bytes());
4653
4654                // ASR.W rd_hi, rn_hi, #31  (hi = sign extension, all 0s or all 1s)
4655                // Thumb-2 ASR immediate: hw1=0xEA4F, hw2=imm3:Rd:imm2:10:Rm
4656                // imm5=31=11111 → imm3=111, imm2=11
4657                let hw1: u16 = 0xEA4F;
4658                let hw2: u16 = (0x7000 | (rd_hi_bits << 8) | 0x00E0 | rn_hi_bits) as u16;
4659                bytes.extend_from_slice(&hw1.to_le_bytes());
4660                bytes.extend_from_slice(&hw2.to_le_bytes());
4661
4662                Ok(bytes) // Total: 40 bytes
4663            }
4664
4665            // I64Rotl: 64-bit rotate left (#610 rewrite).
4666            // For n < 32: new_hi = (hi << n) | (lo >> (32-n)), new_lo = (lo << n) | (hi >> (32-n))
4667            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
4668            //
4669            // Fixed-reg core: value in R0:R1, amount in R2, scratch R3 + R12
4670            // (all four saved/marshaled by the #610 fixed-ABI wrapper; the
4671            // pre-#610 expansion wrote through the selector's registers with
4672            // colliding R3/R4 scratch and restored the saved R4 OVER the
4673            // result). Relies on ARM register-shift semantics: amounts >= 32
4674            // yield 0 for LSL/LSR, which makes n = 0 and n = 32 exact.
4675            ArmOp::I64Rotl {
4676                rdlo,
4677                rdhi,
4678                rnlo,
4679                rnhi,
4680                shift,
4681            } => {
4682                let mut bytes = Vec::new();
4683                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
4684
4685                let core: [u16; 35] = [
4686                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
4687                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
4688                    0xD50E, //         BPL    .large        (n >= 32)
4689                    // --- small rotation (n < 32) ---
4690                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
4691                    0xFA20, 0xFC03, // LSR.W  R12, R0, R3   (lo >> (32-n))
4692                    0xFA21, 0xF303, // LSR.W  R3, R1, R3    (hi >> (32-n))
4693                    0xFA01, 0xF102, // LSL.W  R1, R1, R2    (hi << n)
4694                    0xEA41, 0x010C, // ORR.W  R1, R1, R12   (new_hi)
4695                    0xFA00, 0xF002, // LSL.W  R0, R0, R2    (lo << n)
4696                    0xEA40, 0x0003, // ORR.W  R0, R0, R3    (new_lo)
4697                    0xE00E, //         B      .done
4698                    // --- large rotation (n >= 32), R3 = m = n-32 ---
4699                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
4700                    0xFA21, 0xFC02, // LSR.W  R12, R1, R2   (hi >> (64-n))
4701                    0xFA20, 0xF202, // LSR.W  R2, R0, R2    (lo >> (64-n))
4702                    0xFA00, 0xF003, // LSL.W  R0, R0, R3    (lo << m)
4703                    0xFA01, 0xF103, // LSL.W  R1, R1, R3    (hi << m)
4704                    0xEA40, 0x0C0C, // ORR.W  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
4705                    0xEA41, 0x0002, // ORR.W  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
4706                    0x4661, //         MOV    R1, R12       (new_hi into place)
4707                            // .done: result in R0:R1
4708                ];
4709                for hw in core {
4710                    bytes.extend_from_slice(&hw.to_le_bytes());
4711                }
4712
4713                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
4714                Ok(bytes) // Total: 102 bytes
4715            }
4716
4717            // I64Rotr: 64-bit rotate right (#610 rewrite).
4718            // For n < 32: new_lo = (lo >> n) | (hi << (32-n)), new_hi = (hi >> n) | (lo << (32-n))
4719            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
4720            //
4721            // Same fixed-reg core contract as I64Rotl: value in R0:R1, amount
4722            // in R2, scratch R3 + R12, all covered by the fixed-ABI wrapper.
4723            ArmOp::I64Rotr {
4724                rdlo,
4725                rdhi,
4726                rnlo,
4727                rnhi,
4728                shift,
4729            } => {
4730                let mut bytes = Vec::new();
4731                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
4732
4733                let core: [u16; 35] = [
4734                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
4735                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
4736                    0xD50E, //         BPL    .large        (n >= 32)
4737                    // --- small rotation (n < 32) ---
4738                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
4739                    0xFA01, 0xFC03, // LSL.W  R12, R1, R3   (hi << (32-n))
4740                    0xFA00, 0xF303, // LSL.W  R3, R0, R3    (lo << (32-n))
4741                    0xFA20, 0xF002, // LSR.W  R0, R0, R2    (lo >> n)
4742                    0xEA40, 0x000C, // ORR.W  R0, R0, R12   (new_lo)
4743                    0xFA21, 0xF102, // LSR.W  R1, R1, R2    (hi >> n)
4744                    0xEA41, 0x0103, // ORR.W  R1, R1, R3    (new_hi)
4745                    0xE00E, //         B      .done
4746                    // --- large rotation (n >= 32), R3 = m = n-32 ---
4747                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
4748                    0xFA00, 0xFC02, // LSL.W  R12, R0, R2   (lo << (64-n))
4749                    0xFA01, 0xF202, // LSL.W  R2, R1, R2    (hi << (64-n))
4750                    0xFA21, 0xF103, // LSR.W  R1, R1, R3    (hi >> m)
4751                    0xEA41, 0x0C0C, // ORR.W  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
4752                    0xFA20, 0xF103, // LSR.W  R1, R0, R3    (lo >> m)
4753                    0xEA41, 0x0102, // ORR.W  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
4754                    0x4660, //         MOV    R0, R12       (new_lo into place)
4755                            // .done: result in R0:R1
4756                ];
4757                for hw in core {
4758                    bytes.extend_from_slice(&hw.to_le_bytes());
4759                }
4760
4761                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
4762                Ok(bytes) // Total: 102 bytes
4763            }
4764
4765            // I64Clz: Count leading zeros in 64-bit value
4766            // If hi != 0: result = CLZ(hi)
4767            // If hi == 0: result = 32 + CLZ(lo)
4768            //
4769            // Layout (using CMP+BNE approach for consistency):
4770            // 0: CMP.W rnhi, #0 (4 bytes)
4771            // 4: BEQ .hi_zero (2 bytes) - branch forward to offset 14
4772            // 6: CLZ.W rd, rnhi (4 bytes)
4773            // 10: B .done (2 bytes) - branch forward to offset 22
4774            // 12: NOP (2 bytes) - padding for alignment
4775            // 14: .hi_zero: CLZ.W rd, rnlo (4 bytes)
4776            // 18: ADD.W rd, rd, #32 (4 bytes)
4777            // 22: .done
4778            ArmOp::I64Clz { rd, rnlo, rnhi } => {
4779                let rd_bits = reg_to_bits(rd);
4780                let rn_lo_bits = reg_to_bits(rnlo);
4781                let rn_hi_bits = reg_to_bits(rnhi);
4782                let mut bytes = Vec::new();
4783
4784                // CMP.W rnhi, #0 (4 bytes at offset 0)
4785                let hw1: u16 = (0xF1B0 | rn_hi_bits) as u16;
4786                let hw2: u16 = 0x0F00;
4787                bytes.extend_from_slice(&hw1.to_le_bytes());
4788                bytes.extend_from_slice(&hw2.to_le_bytes());
4789
4790                // BEQ .hi_zero (2 bytes at offset 4)
4791                // PC = 4 + 4 = 8, target = 14, offset = 6, imm8 = 3
4792                let beq: u16 = 0xD003;
4793                bytes.extend_from_slice(&beq.to_le_bytes());
4794
4795                // CLZ.W rd, rnhi (4 bytes at offset 6)
4796                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4797                let hw1: u16 = (0xFAB0 | rn_hi_bits) as u16;
4798                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_hi_bits) as u16;
4799                bytes.extend_from_slice(&hw1.to_le_bytes());
4800                bytes.extend_from_slice(&hw2.to_le_bytes());
4801
4802                // B .done (2 bytes at offset 10)
4803                // PC = 10 + 4 = 14, target = 22, offset = 8, imm11 = 4
4804                let b_done: u16 = 0xE004;
4805                bytes.extend_from_slice(&b_done.to_le_bytes());
4806
4807                // NOP (2 bytes at offset 12) - padding
4808                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
4809
4810                // .hi_zero: (offset 14)
4811                // CLZ.W rd, rnlo (4 bytes)
4812                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4813                let hw1: u16 = (0xFAB0 | rn_lo_bits) as u16;
4814                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_lo_bits) as u16;
4815                bytes.extend_from_slice(&hw1.to_le_bytes());
4816                bytes.extend_from_slice(&hw2.to_le_bytes());
4817
4818                // ADD.W rd, rd, #32 (4 bytes at offset 18)
4819                let hw1: u16 = (0xF100 | rd_bits) as u16;
4820                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
4821                bytes.extend_from_slice(&hw1.to_le_bytes());
4822                bytes.extend_from_slice(&hw2.to_le_bytes());
4823
4824                // .done: (offset 22)
4825                // i64.clz returns i64, so clear high word: MOV rnhi, #0 (2 bytes)
4826                // MOVS Rn, #0: 0010 0 Rn 00000000
4827                let mov0: u16 = (0x2000 | (rn_hi_bits << 8)) as u16;
4828                bytes.extend_from_slice(&mov0.to_le_bytes());
4829
4830                Ok(bytes)
4831            }
4832
4833            // I64Ctz: Count trailing zeros in 64-bit value
4834            // If lo != 0: result = CTZ(lo) = CLZ(RBIT(lo))
4835            // If lo == 0: result = 32 + CTZ(hi) = 32 + CLZ(RBIT(hi))
4836            //
4837            // Layout:
4838            // 0: CMP.W rnlo, #0 (4 bytes)
4839            // 4: BEQ .lo_zero (2 bytes) - branch to offset 18
4840            // 6: RBIT.W rd, rnlo (4 bytes)
4841            // 10: CLZ.W rd, rd (4 bytes)
4842            // 14: B .done (2 bytes) - branch to offset 30
4843            // 16: NOP (2 bytes) - padding
4844            // 18: .lo_zero: RBIT.W rd, rnhi (4 bytes)
4845            // 22: CLZ.W rd, rd (4 bytes)
4846            // 26: ADD.W rd, rd, #32 (4 bytes)
4847            // 30: .done
4848            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
4849                let rd_bits = reg_to_bits(rd);
4850                let rn_lo_bits = reg_to_bits(rnlo);
4851                let rn_hi_bits = reg_to_bits(rnhi);
4852                let mut bytes = Vec::new();
4853
4854                // CMP.W rnlo, #0 (4 bytes at offset 0)
4855                let hw1: u16 = (0xF1B0 | rn_lo_bits) as u16;
4856                let hw2: u16 = 0x0F00;
4857                bytes.extend_from_slice(&hw1.to_le_bytes());
4858                bytes.extend_from_slice(&hw2.to_le_bytes());
4859
4860                // BEQ .lo_zero (2 bytes at offset 4)
4861                // PC = 4 + 4 = 8, target = 18, offset = 10, imm8 = 5
4862                let beq: u16 = 0xD005;
4863                bytes.extend_from_slice(&beq.to_le_bytes());
4864
4865                // RBIT.W rd, rnlo (4 bytes at offset 6)
4866                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
4867                let hw1: u16 = (0xFA90 | rn_lo_bits) as u16;
4868                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_lo_bits) as u16;
4869                bytes.extend_from_slice(&hw1.to_le_bytes());
4870                bytes.extend_from_slice(&hw2.to_le_bytes());
4871
4872                // CLZ.W rd, rd (4 bytes at offset 10)
4873                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4874                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
4875                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
4876                bytes.extend_from_slice(&hw1.to_le_bytes());
4877                bytes.extend_from_slice(&hw2.to_le_bytes());
4878
4879                // B .done (2 bytes at offset 14)
4880                // PC = 14 + 4 = 18, target = 30, offset = 12, imm11 = 6
4881                let b_done: u16 = 0xE006;
4882                bytes.extend_from_slice(&b_done.to_le_bytes());
4883
4884                // NOP (2 bytes at offset 16) - padding
4885                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
4886
4887                // .lo_zero: (offset 18)
4888                // RBIT.W rd, rnhi (4 bytes)
4889                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
4890                let hw1: u16 = (0xFA90 | rn_hi_bits) as u16;
4891                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_hi_bits) as u16;
4892                bytes.extend_from_slice(&hw1.to_le_bytes());
4893                bytes.extend_from_slice(&hw2.to_le_bytes());
4894
4895                // CLZ.W rd, rd (4 bytes at offset 22)
4896                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4897                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
4898                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
4899                bytes.extend_from_slice(&hw1.to_le_bytes());
4900                bytes.extend_from_slice(&hw2.to_le_bytes());
4901
4902                // ADD.W rd, rd, #32 (4 bytes at offset 26)
4903                let hw1: u16 = (0xF100 | rd_bits) as u16;
4904                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
4905                bytes.extend_from_slice(&hw1.to_le_bytes());
4906                bytes.extend_from_slice(&hw2.to_le_bytes());
4907
4908                // .done: (offset 30)
4909                // i64.ctz returns i64, so clear high word: MOV rnhi, #0 (2 bytes)
4910                let mov0: u16 = (0x2000 | (rn_hi_bits << 8)) as u16;
4911                bytes.extend_from_slice(&mov0.to_le_bytes());
4912
4913                Ok(bytes)
4914            }
4915
4916            // I64Popcnt: Population count of 64-bit value
4917            // result = POPCNT(lo) + POPCNT(hi)
4918            // Using SIMD-style parallel bit counting algorithm
4919            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
4920                let rd_bits = reg_to_bits(rd);
4921                let rn_lo_bits = reg_to_bits(rnlo);
4922                let rn_hi_bits = reg_to_bits(rnhi);
4923                let r12: u32 = 12; // IP scratch
4924                let r3: u32 = 3; // Scratch for hi popcnt result
4925                let mut bytes = Vec::new();
4926
4927                // PUSH {R3, R4, R5} - save scratch registers
4928                bytes.extend_from_slice(&0xB438u16.to_le_bytes());
4929
4930                // Strategy: compute popcnt(lo) -> R4, popcnt(hi) -> R5, add them -> rd
4931                // Using lookup table approach for each byte would be too large
4932                // Using shift-and-add approach instead
4933
4934                // For simplicity and correctness, use the efficient parallel algorithm
4935                // but implement it as a series of inline operations
4936
4937                // Marshal the operand pair into the fixed scratch regs, routing
4938                // rnlo through R12 (#632 audit): writing R4 first corrupted the
4939                // rnhi read for a pair living at (R3,R4) — every source is read
4940                // before any scratch register it could occupy is written.
4941                // MOV R12, rnlo
4942                let mov: u16 = (0x4600 | (1 << 7) | (rn_lo_bits << 3) | 4) as u16;
4943                bytes.extend_from_slice(&mov.to_le_bytes());
4944                // MOV R5, rnhi (R4 untouched so far; rnhi == R5 is a no-op)
4945                let mov: u16 = (0x4600 | (rn_hi_bits << 3) | 5) as u16;
4946                bytes.extend_from_slice(&mov.to_le_bytes());
4947                // MOV R4, R12
4948                bytes.extend_from_slice(&0x4664u16.to_le_bytes());
4949
4950                // --- POPCNT for R4 (lo word) ---
4951                // Step 1: x = x - ((x >> 1) & 0x55555555)
4952                // LSR.W R12, R4, #1
4953                let hw1: u16 = 0xEA4F;
4954                let hw2: u16 = ((r12 << 8) | 0x50 | 4) as u16;
4955                bytes.extend_from_slice(&hw1.to_le_bytes());
4956                bytes.extend_from_slice(&hw2.to_le_bytes());
4957
4958                // Load 0x55555555 into R3 using MOVW/MOVT
4959                // MOVW R3, #0x5555
4960                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
4961                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
4962                // MOVT R3, #0x5555
4963                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
4964                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
4965
4966                // AND.W R12, R12, R3
4967                let hw1: u16 = (0xEA00 | r12) as u16;
4968                let hw2: u16 = ((r12 << 8) | r3) as u16;
4969                bytes.extend_from_slice(&hw1.to_le_bytes());
4970                bytes.extend_from_slice(&hw2.to_le_bytes());
4971
4972                // SUB.W R4, R4, R12
4973                let hw1: u16 = (0xEBA0 | 4) as u16;
4974                let hw2: u16 = ((4 << 8) | r12) as u16;
4975                bytes.extend_from_slice(&hw1.to_le_bytes());
4976                bytes.extend_from_slice(&hw2.to_le_bytes());
4977
4978                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
4979                // Load 0x33333333 into R3
4980                // MOVW R3, #0x3333
4981                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
4982                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
4983                // MOVT R3, #0x3333
4984                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
4985                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
4986
4987                // AND.W R12, R4, R3
4988                let hw1: u16 = (0xEA00 | 4) as u16;
4989                let hw2: u16 = ((r12 << 8) | r3) as u16;
4990                bytes.extend_from_slice(&hw1.to_le_bytes());
4991                bytes.extend_from_slice(&hw2.to_le_bytes());
4992
4993                // LSR.W R4, R4, #2
4994                let hw1: u16 = 0xEA4F;
4995                let hw2: u16 = ((4 << 8) | 0x90 | 4) as u16;
4996                bytes.extend_from_slice(&hw1.to_le_bytes());
4997                bytes.extend_from_slice(&hw2.to_le_bytes());
4998
4999                // AND.W R4, R4, R3
5000                let hw1: u16 = (0xEA00 | 4) as u16;
5001                let hw2: u16 = ((4 << 8) | r3) as u16;
5002                bytes.extend_from_slice(&hw1.to_le_bytes());
5003                bytes.extend_from_slice(&hw2.to_le_bytes());
5004
5005                // ADD.W R4, R4, R12
5006                let hw1: u16 = (0xEB00 | 4) as u16;
5007                let hw2: u16 = ((4 << 8) | r12) as u16;
5008                bytes.extend_from_slice(&hw1.to_le_bytes());
5009                bytes.extend_from_slice(&hw2.to_le_bytes());
5010
5011                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5012                // LSR.W R12, R4, #4
5013                // hw2 = (imm3 << 12) | (Rd << 8) | (imm2 << 6) | (type << 4) | Rm
5014                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5015                let hw1: u16 = 0xEA4F;
5016                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 4) as u16;
5017                bytes.extend_from_slice(&hw1.to_le_bytes());
5018                bytes.extend_from_slice(&hw2.to_le_bytes());
5019
5020                // ADD.W R4, R4, R12
5021                let hw1: u16 = (0xEB00 | 4) as u16;
5022                let hw2: u16 = ((4 << 8) | r12) as u16;
5023                bytes.extend_from_slice(&hw1.to_le_bytes());
5024                bytes.extend_from_slice(&hw2.to_le_bytes());
5025
5026                // Load 0x0F0F0F0F into R3
5027                // MOVW R3, #0x0F0F (imm4=0, i=1, imm3=7, imm8=0x0F)
5028                // hw1 = 11110 1 10 0100 0000 = 0xF640
5029                // hw2 = 0 111 0011 00001111 = 0x730F
5030                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5031                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5032                // MOVT R3, #0x0F0F
5033                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5034                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5035
5036                // AND.W R4, R4, R3
5037                let hw1: u16 = (0xEA00 | 4) as u16;
5038                let hw2: u16 = ((4 << 8) | r3) as u16;
5039                bytes.extend_from_slice(&hw1.to_le_bytes());
5040                bytes.extend_from_slice(&hw2.to_le_bytes());
5041
5042                // Step 4: x = x * 0x01010101 >> 24
5043                // Load 0x01010101 into R3
5044                // MOVW R3, #0x0101
5045                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5046                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5047                // MOVT R3, #0x0101
5048                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5049                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5050
5051                // MUL R4, R4, R3
5052                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5053                let hw1: u16 = (0xFB00 | 4) as u16;
5054                let hw2: u16 = (0xF000 | (4 << 8) | r3) as u16;
5055                bytes.extend_from_slice(&hw1.to_le_bytes());
5056                bytes.extend_from_slice(&hw2.to_le_bytes());
5057
5058                // LSR.W R4, R4, #24
5059                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5060                let hw1: u16 = 0xEA4F;
5061                let hw2: u16 = (0x6000 | (4 << 8) | 0x10 | 4) as u16;
5062                bytes.extend_from_slice(&hw1.to_le_bytes());
5063                bytes.extend_from_slice(&hw2.to_le_bytes());
5064
5065                // --- POPCNT for R5 (hi word) - same algorithm ---
5066                // Step 1
5067                let hw1: u16 = 0xEA4F;
5068                let hw2: u16 = ((r12 << 8) | 0x50 | 5) as u16;
5069                bytes.extend_from_slice(&hw1.to_le_bytes());
5070                bytes.extend_from_slice(&hw2.to_le_bytes());
5071
5072                // Load 0x55555555 into R3
5073                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5074                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5075                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5076                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5077
5078                let hw1: u16 = (0xEA00 | r12) as u16;
5079                let hw2: u16 = ((r12 << 8) | r3) as u16;
5080                bytes.extend_from_slice(&hw1.to_le_bytes());
5081                bytes.extend_from_slice(&hw2.to_le_bytes());
5082
5083                let hw1: u16 = (0xEBA0 | 5) as u16;
5084                let hw2: u16 = ((5 << 8) | r12) as u16;
5085                bytes.extend_from_slice(&hw1.to_le_bytes());
5086                bytes.extend_from_slice(&hw2.to_le_bytes());
5087
5088                // Step 2
5089                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5090                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5091                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5092                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5093
5094                let hw1: u16 = (0xEA00 | 5) as u16;
5095                let hw2: u16 = ((r12 << 8) | r3) as u16;
5096                bytes.extend_from_slice(&hw1.to_le_bytes());
5097                bytes.extend_from_slice(&hw2.to_le_bytes());
5098
5099                let hw1: u16 = 0xEA4F;
5100                let hw2: u16 = ((5 << 8) | 0x90 | 5) as u16;
5101                bytes.extend_from_slice(&hw1.to_le_bytes());
5102                bytes.extend_from_slice(&hw2.to_le_bytes());
5103
5104                let hw1: u16 = (0xEA00 | 5) as u16;
5105                let hw2: u16 = ((5 << 8) | r3) as u16;
5106                bytes.extend_from_slice(&hw1.to_le_bytes());
5107                bytes.extend_from_slice(&hw2.to_le_bytes());
5108
5109                let hw1: u16 = (0xEB00 | 5) as u16;
5110                let hw2: u16 = ((5 << 8) | r12) as u16;
5111                bytes.extend_from_slice(&hw1.to_le_bytes());
5112                bytes.extend_from_slice(&hw2.to_le_bytes());
5113
5114                // Step 3: LSR.W R12, R5, #4
5115                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5116                let hw1: u16 = 0xEA4F;
5117                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 5) as u16;
5118                bytes.extend_from_slice(&hw1.to_le_bytes());
5119                bytes.extend_from_slice(&hw2.to_le_bytes());
5120
5121                let hw1: u16 = (0xEB00 | 5) as u16;
5122                let hw2: u16 = ((5 << 8) | r12) as u16;
5123                bytes.extend_from_slice(&hw1.to_le_bytes());
5124                bytes.extend_from_slice(&hw2.to_le_bytes());
5125
5126                // Load 0x0F0F0F0F into R3 (for hi-word)
5127                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5128                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5129                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5130                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5131
5132                let hw1: u16 = (0xEA00 | 5) as u16;
5133                let hw2: u16 = ((5 << 8) | r3) as u16;
5134                bytes.extend_from_slice(&hw1.to_le_bytes());
5135                bytes.extend_from_slice(&hw2.to_le_bytes());
5136
5137                // Step 4
5138                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5139                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5140                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5141                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5142
5143                // MUL R5, R5, R3
5144                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5145                let hw1: u16 = (0xFB00 | 5) as u16;
5146                let hw2: u16 = (0xF000 | (5 << 8) | r3) as u16;
5147                bytes.extend_from_slice(&hw1.to_le_bytes());
5148                bytes.extend_from_slice(&hw2.to_le_bytes());
5149
5150                // LSR.W R5, R5, #24
5151                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5152                let hw1: u16 = 0xEA4F;
5153                let hw2: u16 = (0x6000 | (5 << 8) | 0x10 | 5) as u16;
5154                bytes.extend_from_slice(&hw1.to_le_bytes());
5155                bytes.extend_from_slice(&hw2.to_le_bytes());
5156
5157                // #632: the count must be carried ACROSS the scratch restore
5158                // in a register the POP cannot touch. rd is allocator-assigned
5159                // (any of R0-R8) and can land inside the {R3,R4,R5} restore set
5160                // — the old `ADDS rd, R4, R5; POP {R3,R4,R5}` destroyed the
5161                // result one instruction after computing it (0 for every input
5162                // under qemu). R12 is encoder scratch: never allocatable (#212)
5163                // and never in a restore set, so no choice of rd can collide.
5164                // ADD.W R12, R4, R5
5165                bytes.extend_from_slice(&0xEB04u16.to_le_bytes());
5166                bytes.extend_from_slice(&0x0C05u16.to_le_bytes());
5167
5168                // POP {R3, R4, R5}
5169                bytes.extend_from_slice(&0xBC38u16.to_le_bytes());
5170
5171                // MOV rd, R12 — after the restore. The 4-bit Rd (D:rd) form is
5172                // also total over rd = R8, where the old ADDS T1 3-bit field
5173                // silently corrupted the encoding (#178/#180 class).
5174                let mov: u16 =
5175                    (0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7)) as u16;
5176                bytes.extend_from_slice(&mov.to_le_bytes());
5177
5178                // i64.popcnt returns i64, so clear high word: MOV.W rnhi, #0
5179                // (T2, 4 bytes — total over rnhi = R8, where the old 16-bit
5180                // MOVS encoding overflowed its 3-bit field into CMP R0, #0).
5181                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5182                bytes.extend_from_slice(&(((rn_hi_bits & 0xF) << 8) as u16).to_le_bytes());
5183
5184                Ok(bytes)
5185            }
5186
5187            // I64Extend8S: Sign-extend low 8 bits to 64 bits
5188            // Result: rdlo = sign_extend_8(rnlo), rdhi = rdlo >> 31
5189            ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
5190                let rdlo_bits = reg_to_bits(rdlo);
5191                let rdhi_bits = reg_to_bits(rdhi);
5192                let rnlo_bits = reg_to_bits(rnlo);
5193                let mut bytes = Vec::new();
5194
5195                // SXTB.W rdlo, rnlo (sign-extend byte to 32-bit)
5196                // SXTB T2: hw1 = 0xFA4F, hw2 = 0xF0<Rd><Rm>
5197                let hw1: u16 = 0xFA4F_u16;
5198                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5199                bytes.extend_from_slice(&hw1.to_le_bytes());
5200                bytes.extend_from_slice(&hw2.to_le_bytes());
5201
5202                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5203                // ASR (immediate): hw1 = 0xEA4F, hw2 = imm3:Rd:imm2:type:Rm
5204                // For imm5=31: imm3=111, imm2=11, type=10 (ASR)
5205                // hw2 = (7 << 12) | (rdhi << 8) | (3 << 6) | (2 << 4) | rdlo
5206                let hw1: u16 = 0xEA4F;
5207                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5208                bytes.extend_from_slice(&hw1.to_le_bytes());
5209                bytes.extend_from_slice(&hw2.to_le_bytes());
5210
5211                Ok(bytes)
5212            }
5213
5214            // I64Extend16S: Sign-extend low 16 bits to 64 bits
5215            // Result: rdlo = sign_extend_16(rnlo), rdhi = rdlo >> 31
5216            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
5217                let rdlo_bits = reg_to_bits(rdlo);
5218                let rdhi_bits = reg_to_bits(rdhi);
5219                let rnlo_bits = reg_to_bits(rnlo);
5220                let mut bytes = Vec::new();
5221
5222                // SXTH.W rdlo, rnlo (sign-extend halfword to 32-bit)
5223                // SXTH T2: hw1 = 0xFA0F, hw2 = 0xF0<Rd><Rm>
5224                let hw1: u16 = 0xFA0F_u16;
5225                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5226                bytes.extend_from_slice(&hw1.to_le_bytes());
5227                bytes.extend_from_slice(&hw2.to_le_bytes());
5228
5229                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5230                let hw1: u16 = 0xEA4F;
5231                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5232                bytes.extend_from_slice(&hw1.to_le_bytes());
5233                bytes.extend_from_slice(&hw2.to_le_bytes());
5234
5235                Ok(bytes)
5236            }
5237
5238            // I64Extend32S: Sign-extend low 32 bits to 64 bits
5239            // Result: rdlo = rnlo, rdhi = rnlo >> 31
5240            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
5241                let rdlo_bits = reg_to_bits(rdlo);
5242                let rdhi_bits = reg_to_bits(rdhi);
5243                let rnlo_bits = reg_to_bits(rnlo);
5244                let mut bytes = Vec::new();
5245
5246                // MOV rdlo, rnlo (if different)
5247                if rdlo_bits != rnlo_bits {
5248                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5249                    let d_bit = ((rdlo_bits >> 3) & 1) as u16;
5250                    let mov: u16 = 0x4600
5251                        | (d_bit << 7)
5252                        | ((rnlo_bits as u16) << 3)
5253                        | ((rdlo_bits & 0x7) as u16);
5254                    bytes.extend_from_slice(&mov.to_le_bytes());
5255                }
5256
5257                // ASR.W rdhi, rnlo, #31 (sign-extend to high word)
5258                let hw1: u16 = 0xEA4F;
5259                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rnlo_bits) as u16;
5260                bytes.extend_from_slice(&hw1.to_le_bytes());
5261                bytes.extend_from_slice(&hw2.to_le_bytes());
5262
5263                Ok(bytes)
5264            }
5265
5266            // SelectMove: IT <cond>; MOV{cond} rd, rm
5267            // Conditional move: only execute MOV if condition is true
5268            ArmOp::SelectMove { rd, rm, cond } => {
5269                let rd_bits = reg_to_bits(rd) as u16;
5270                let rm_bits = reg_to_bits(rm) as u16;
5271
5272                // Condition code encoding for IT block
5273                use synth_synthesis::Condition;
5274                let cond_bits: u16 = match cond {
5275                    Condition::EQ => 0x0, // Equal
5276                    Condition::NE => 0x1, // Not equal
5277                    Condition::HS => 0x2, // Higher or same (unsigned >=)
5278                    Condition::LO => 0x3, // Lower (unsigned <)
5279                    Condition::HI => 0x8, // Higher (unsigned >)
5280                    Condition::LS => 0x9, // Lower or same (unsigned <=)
5281                    Condition::GE => 0xA, // Greater or equal (signed)
5282                    Condition::LT => 0xB, // Less than (signed)
5283                    Condition::GT => 0xC, // Greater than (signed)
5284                    Condition::LE => 0xD, // Less or equal (signed)
5285                };
5286
5287                // IT <cond>: single Then block (mask = 0x8 for T only)
5288                // IT instruction: 1011 1111 firstcond mask
5289                let it_instr: u16 = 0xBF00 | (cond_bits << 4) | 0x8;
5290
5291                // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5292                // This MOV will only execute if condition is true due to IT block
5293                let d_bit = (rd_bits >> 3) & 1;
5294                let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5295
5296                // Emit: IT <cond>, MOV rd, rm
5297                let mut bytes = it_instr.to_le_bytes().to_vec();
5298                bytes.extend_from_slice(&mov_instr.to_le_bytes());
5299                Ok(bytes)
5300            }
5301
5302            // Popcnt: Population count (count set bits)
5303            // ARM Cortex-M has no native POPCNT, so we implement the bit manipulation algorithm:
5304            // x = x - ((x >> 1) & 0x55555555);
5305            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
5306            // x = (x + (x >> 4)) & 0x0F0F0F0F;
5307            // x = x + (x >> 8);
5308            // x = x + (x >> 16);
5309            // return x & 0x3F;
5310            //
5311            // Uses rd as working register and R12 as scratch for constants
5312            ArmOp::Popcnt { rd, rm } => {
5313                let mut bytes = Vec::new();
5314
5315                // First, move rm to rd if they're different
5316                if rd != rm {
5317                    let rd_bits = reg_to_bits(rd) as u16;
5318                    let rm_bits = reg_to_bits(rm) as u16;
5319                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5320                    let d_bit = (rd_bits >> 3) & 1;
5321                    let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5322                    bytes.extend_from_slice(&mov_instr.to_le_bytes());
5323                }
5324
5325                // Step 1: x = x - ((x >> 1) & 0x55555555)
5326                // Load 0x55555555 into R12
5327                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x5555)?);
5328                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x5555)?);
5329
5330                // R12_temp = rd >> 1
5331                // We need a second scratch register. Use R11.
5332                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 1)?);
5333
5334                // R11 = R11 & R12 (R11 = (x >> 1) & 0x55555555)
5335                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(11, 11, 12)?);
5336
5337                // rd = rd - R11
5338                bytes.extend_from_slice(&self.encode_thumb32_sub_reg_raw(
5339                    reg_to_bits(rd),
5340                    reg_to_bits(rd),
5341                    11,
5342                )?);
5343
5344                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5345                // Load 0x33333333 into R12
5346                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x3333)?);
5347                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x3333)?);
5348
5349                // R11 = rd & R12
5350                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5351                    11,
5352                    reg_to_bits(rd),
5353                    12,
5354                )?);
5355
5356                // rd = rd >> 2
5357                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(
5358                    reg_to_bits(rd),
5359                    reg_to_bits(rd),
5360                    2,
5361                )?);
5362
5363                // rd = rd & R12
5364                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5365                    reg_to_bits(rd),
5366                    reg_to_bits(rd),
5367                    12,
5368                )?);
5369
5370                // rd = rd + R11
5371                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5372                    reg_to_bits(rd),
5373                    reg_to_bits(rd),
5374                    11,
5375                )?);
5376
5377                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5378                // R11 = rd >> 4
5379                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 4)?);
5380
5381                // rd = rd + R11
5382                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5383                    reg_to_bits(rd),
5384                    reg_to_bits(rd),
5385                    11,
5386                )?);
5387
5388                // Load 0x0F0F0F0F into R12
5389                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x0F0F)?);
5390                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x0F0F)?);
5391
5392                // rd = rd & R12
5393                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5394                    reg_to_bits(rd),
5395                    reg_to_bits(rd),
5396                    12,
5397                )?);
5398
5399                // Step 4: x = x + (x >> 8)
5400                // R11 = rd >> 8
5401                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 8)?);
5402
5403                // rd = rd + R11
5404                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5405                    reg_to_bits(rd),
5406                    reg_to_bits(rd),
5407                    11,
5408                )?);
5409
5410                // Step 5: x = x + (x >> 16)
5411                // R11 = rd >> 16
5412                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 16)?);
5413
5414                // rd = rd + R11
5415                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5416                    reg_to_bits(rd),
5417                    reg_to_bits(rd),
5418                    11,
5419                )?);
5420
5421                // Step 6: return x & 0x3F
5422                // AND with 0x3F (small immediate, can use BIC or AND with immediate)
5423                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5424                    reg_to_bits(rd),
5425                    reg_to_bits(rd),
5426                    0x3F,
5427                )?);
5428
5429                Ok(bytes)
5430            }
5431
5432            // I64DivU: 64-bit unsigned division using binary long division
5433            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = quotient
5434            // Uses: R4-R7, R12 as loop counter (avoid R8 for Renode compatibility)
5435            //
5436            // #610: the fixed-ABI wrapper marshals the selector-assigned
5437            // operand registers into the core's fixed regs and lands the
5438            // result in rd — pre-#610 this arm IGNORED its register fields,
5439            // so the selector read its rd pair (e.g. R4:R5) after the core's
5440            // own POP restored the stale caller values over it: 0 for every
5441            // input. A zero divisor now traps (UDF #0), per WASM semantics.
5442            ArmOp::I64DivU {
5443                rdlo,
5444                rdhi,
5445                rnlo,
5446                rnhi,
5447                rmlo,
5448                rmhi,
5449                elide_zero_guard,
5450            } => {
5451                let mut bytes = Vec::new();
5452                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5453                // #494 phase 2b: elided only under a certificate-discharged
5454                // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
5455                if !elide_zero_guard {
5456                    emit_i64_divisor_zero_trap(&mut bytes);
5457                }
5458
5459                // PUSH {R4-R7} - save scratch registers (NO LR — this is inline code)
5460                // 16-bit PUSH: 1011 010 M rrrrrrrr where M=0 (no LR), r=R4-R7 = 0xF0
5461                // Encoding: 1011 0100 1111 0000 = 0xB4F0
5462                bytes.extend_from_slice(&0xB4F0u16.to_le_bytes());
5463
5464                // Initialize quotient (R4:R5) = 0
5465                bytes.extend_from_slice(&0x2400u16.to_le_bytes()); // MOV R4, #0
5466                bytes.extend_from_slice(&0x2500u16.to_le_bytes()); // MOV R5, #0
5467
5468                // Initialize remainder (R6:R7) = 0
5469                bytes.extend_from_slice(&0x2600u16.to_le_bytes()); // MOV R6, #0
5470                bytes.extend_from_slice(&0x2700u16.to_le_bytes()); // MOV R7, #0
5471
5472                // Initialize loop counter R12 = 64 (use R12 scratch instead of R8)
5473                // MOV.W R12, #64: F04F 0C40
5474                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5475                bytes.extend_from_slice(&0x0C40u16.to_le_bytes());
5476
5477                // Loop start
5478                let loop_start = bytes.len();
5479
5480                // === Loop body: process one bit ===
5481
5482                // 1. Shift quotient R4:R5 left by 1
5483                // LSLS R5, R5, #1 (16-bit: 0000 0010 1010 1101 = 0x006D -> actually 0x002D for LSL R5,R5,#1)
5484                // LSL Rd, Rm, #imm5: 000 00 imm5 Rm Rd = 000 00 00001 101 101 = 0x006D
5485                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5486                // Get carry from R4 into R5: ORR R5, R5, R4 LSR #31
5487                // Thumb-2 ORR with shifted register: EA45 75D4 = ORR.W R5, R5, R4, LSR #31
5488                // 11101010 010 S Rn | 0 imm3 Rd imm2 type Rm
5489                // type=01 (LSR), imm5=31 (imm3=111, imm2=11)
5490                bytes.extend_from_slice(&0xEA45u16.to_le_bytes());
5491                bytes.extend_from_slice(&0x75D4u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5492                // LSLS R4, R4, #1: 000 00 00001 100 100 = 0x0064
5493                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5494
5495                // 2. Shift remainder R6:R7 left by 1, OR in MSB of dividend R1
5496                // LSLS R7, R7, #1
5497                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5498                // ORR.W R7, R7, R6, LSR #31
5499                bytes.extend_from_slice(&0xEA47u16.to_le_bytes());
5500                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5501                // LSLS R6, R6, #1
5502                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5503                // ORR.W R6, R6, R1, LSR #31 (bring in MSB of dividend high)
5504                bytes.extend_from_slice(&0xEA46u16.to_le_bytes());
5505                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5506
5507                // 3. Shift dividend R0:R1 left by 1
5508                // LSLS R1, R1, #1
5509                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5510                // ORR.W R1, R1, R0, LSR #31
5511                bytes.extend_from_slice(&0xEA41u16.to_le_bytes());
5512                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5513                // LSLS R0, R0, #1
5514                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5515
5516                // 4. Compare remainder >= divisor (64-bit unsigned comparison)
5517                // Compare high words first: CMP R7, R3
5518                // CMP Rn, Rm encoding: 0x4280 | (Rm << 3) | Rn
5519                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3 (16-bit)
5520                // BHI means R7 > R3 (unsigned) - definitely subtract
5521                // BLO means R7 < R3 - definitely don't subtract
5522                // BEQ means need to check low words
5523
5524                // If high > divisor high: branch to subtract (forward +offset)
5525                // BHI.N +6 (skip CMP, skip BLO, do subtract)
5526                // BHI: 1101 1000 offset8 where cond=1000 (HI)
5527                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4 (to subtract block)
5528
5529                // If high < divisor high: branch past subtract
5530                // BLO.N +10 (skip to decrement)
5531                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BLO/BCC +12 (past subtract)
5532
5533                // High words equal, compare low: CMP R6, R2
5534                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2 (16-bit)
5535                // BLO/BCC past subtract (skip SUBS+SBC.W+ORR.W = 10 bytes = 4 halfwords from PC+4)
5536                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords (past subtract)
5537
5538                // === Subtract block: remainder -= divisor, quotient |= 1 ===
5539                // SUBS R6, R6, R2
5540                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2 (16-bit)
5541                // SBC R7, R7, R3 (with borrow)
5542                // Thumb-2 SBC.W: EB67 0703 = SBC.W R7, R7, R3
5543                bytes.extend_from_slice(&0xEB67u16.to_le_bytes());
5544                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5545                // ORR R4, R4, #1 (set bit 0 of quotient low)
5546                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5547                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5548
5549                // === Decrement counter and loop ===
5550                // SUBS.W R12, R12, #1 (decrement loop counter)
5551                // SUBS.W R12, R12, #1: F1BC 0C01
5552                bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
5553                bytes.extend_from_slice(&0x0C01u16.to_le_bytes());
5554
5555                // BNE back to loop_start
5556                let branch_offset_bytes = bytes.len() - loop_start + 4; // +4 for pipeline
5557                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5558                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5559                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5560
5561                // === Loop done, move quotient to R0:R1 ===
5562                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5563                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5564
5565                // POP {R4-R7} - restore scratch registers (NO PC — inline code continues)
5566                // 16-bit POP: 1011 110 P rrrrrrrr where P=0 (no PC), r=R4-R7 = 0xF0
5567                // Encoding: 1011 1100 1111 0000 = 0xBCF0
5568                bytes.extend_from_slice(&0xBCF0u16.to_le_bytes());
5569
5570                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5571                Ok(bytes)
5572            }
5573
5574            // I64DivS: 64-bit signed division
5575            // Converts to unsigned, divides, then applies sign
5576            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5577            //   ->  R0:R1 = quotient (signed)
5578            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5579            ArmOp::I64DivS {
5580                rdlo,
5581                rdhi,
5582                rnlo,
5583                rnhi,
5584                rmlo,
5585                rmhi,
5586                elide_zero_guard,
5587                elide_overflow_guard,
5588            } => {
5589                let mut bytes = Vec::new();
5590                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5591                // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
5592                // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
5593                // the #633 overflow guard falls ONLY to
5594                // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
5595                // divisor-nonzero fact alone must keep it.
5596                if !elide_zero_guard {
5597                    emit_i64_divisor_zero_trap(&mut bytes);
5598                }
5599                if !elide_overflow_guard {
5600                    // #633: INT64_MIN / -1 overflows — trap like the i32 path
5601                    // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
5602                    emit_i64_divs_overflow_trap(&mut bytes);
5603                }
5604
5605                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5606                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5607                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5608
5609                // Save result sign in R9: R9 = R1 XOR R3 (sign bit = MSB)
5610                // EOR.W R9, R1, R3
5611                bytes.extend_from_slice(&0xEA81u16.to_le_bytes());
5612                bytes.extend_from_slice(&0x0903u16.to_le_bytes());
5613
5614                // If dividend negative (R1 MSB set), negate it
5615                // TST R1, R1 (check sign)
5616                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5617                // BPL skip_neg_dividend (+10 bytes = 5 halfwords)
5618                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5619
5620                // Negate R0:R1 (64-bit): RSBS R0, R0, #0; SBC R1, R1, R1 LSL #1
5621                // Actually: MVN R0, R0; MVN R1, R1; ADDS R0, R0, #1; ADC R1, R1, #0
5622                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5623                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5624                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5625                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5626                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5627
5628                // If divisor negative (R3 MSB set), negate it
5629                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5630                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5631
5632                // Negate R2:R3
5633                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
5634                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
5635                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
5636                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
5637                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
5638
5639                // === Now do unsigned division (same as I64DivU) ===
5640                // Initialize quotient (R4:R5) = 0
5641                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5642                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5643                // Initialize remainder (R6:R7) = 0
5644                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5645                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5646                // Initialize loop counter R8 = 64
5647                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5648                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5649
5650                let loop_start = bytes.len();
5651
5652                // Shift quotient left
5653                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5654                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5655                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5656                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5657
5658                // Shift remainder left, OR in MSB of dividend
5659                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5660                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5661                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5662                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5663                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5664                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5665
5666                // Shift dividend left
5667                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5668                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5669                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5670                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5671
5672                // Compare and conditionally subtract
5673                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5674                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5675                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5676                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5677                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5678
5679                // Subtract and set quotient bit
5680                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5681                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5682                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5683                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5684                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5685
5686                // Decrement and loop
5687                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
5688                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
5689
5690                let branch_offset_bytes = bytes.len() - loop_start + 4;
5691                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5692                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5693                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5694
5695                // Move quotient to R0:R1
5696                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5697                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5698
5699                // If result should be negative (R9 MSB set), negate R0:R1
5700                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9 (check MSB)
5701                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
5702                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8 (skip negation)
5703
5704                // Negate result R0:R1
5705                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5706                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5707                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5708                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5709                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5710
5711                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
5712                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
5713                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5714
5715                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5716                Ok(bytes)
5717            }
5718
5719            // I64RemU: 64-bit unsigned remainder using binary long division
5720            // Same algorithm as I64DivU but returns remainder instead of quotient
5721            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = remainder
5722            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5723            ArmOp::I64RemU {
5724                rdlo,
5725                rdhi,
5726                rnlo,
5727                rnhi,
5728                rmlo,
5729                rmhi,
5730                elide_zero_guard,
5731            } => {
5732                let mut bytes = Vec::new();
5733                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5734                if !elide_zero_guard {
5735                    emit_i64_divisor_zero_trap(&mut bytes);
5736                }
5737
5738                // PUSH {R4-R8} - save scratch registers (NO LR — inline code)
5739                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5740                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
5741
5742                // Initialize quotient (R4:R5) = 0 (computed but not returned)
5743                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5744                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5745                // Initialize remainder (R6:R7) = 0
5746                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5747                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5748                // Initialize loop counter R8 = 64
5749                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5750                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5751
5752                let loop_start = bytes.len();
5753
5754                // Shift quotient left (not needed for result, but keeps algorithm same)
5755                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5756                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5757                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5758                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5759
5760                // Shift remainder left, OR in MSB of dividend
5761                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5762                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5763                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5764                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5765                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5766                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5767
5768                // Shift dividend left
5769                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5770                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5771                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5772                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5773
5774                // Compare and conditionally subtract
5775                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5776                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5777                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5778                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5779                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5780
5781                // Subtract and set quotient bit
5782                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5783                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5784                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5785                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5786                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5787
5788                // Decrement and loop
5789                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
5790                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
5791
5792                let branch_offset_bytes = bytes.len() - loop_start + 4;
5793                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5794                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5795                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5796
5797                // Move REMAINDER to R0:R1 (difference from I64DivU)
5798                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
5799                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
5800
5801                // POP {R4-R8} - restore scratch registers (NO PC — inline code continues)
5802                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
5803                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
5804
5805                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5806                Ok(bytes)
5807            }
5808
5809            // I64RemS: 64-bit signed remainder
5810            // Remainder sign follows dividend sign (not quotient rule)
5811            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5812            //   ->  R0:R1 = remainder (signed, same sign as dividend)
5813            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5814            ArmOp::I64RemS {
5815                rdlo,
5816                rdhi,
5817                rnlo,
5818                rnhi,
5819                rmlo,
5820                rmhi,
5821                elide_zero_guard,
5822            } => {
5823                let mut bytes = Vec::new();
5824                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5825                if !elide_zero_guard {
5826                    emit_i64_divisor_zero_trap(&mut bytes);
5827                }
5828
5829                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5830                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5831                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5832
5833                // Save dividend sign in R9 (remainder sign = dividend sign)
5834                // MOV R9, R1 (just need the sign bit)
5835                bytes.extend_from_slice(&0x4689u16.to_le_bytes()); // MOV R9, R1
5836
5837                // If dividend negative (R1 MSB set), negate it
5838                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5839                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5840
5841                // Negate R0:R1
5842                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5843                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5844                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5845                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5846                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5847
5848                // If divisor negative (R3 MSB set), negate it
5849                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5850                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5851
5852                // Negate R2:R3
5853                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
5854                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
5855                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
5856                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
5857                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
5858
5859                // === Unsigned division algorithm ===
5860                // Initialize quotient (R4:R5) = 0
5861                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5862                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5863                // Initialize remainder (R6:R7) = 0
5864                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5865                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5866                // Initialize loop counter R8 = 64
5867                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5868                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5869
5870                let loop_start = bytes.len();
5871
5872                // Shift quotient left
5873                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5874                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5875                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5876                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5877
5878                // Shift remainder left, OR in MSB of dividend
5879                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5880                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5881                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5882                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5883                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5884                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5885
5886                // Shift dividend left
5887                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5888                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5889                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5890                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5891
5892                // Compare and conditionally subtract
5893                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5894                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5895                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5896                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5897                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5898
5899                // Subtract and set quotient bit
5900                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5901                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5902                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5903                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5904                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5905
5906                // Decrement and loop
5907                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
5908                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
5909
5910                let branch_offset_bytes = bytes.len() - loop_start + 4;
5911                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5912                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5913                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5914
5915                // Move remainder to R0:R1
5916                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
5917                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
5918
5919                // If original dividend was negative (R9 MSB set), negate remainder
5920                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9
5921                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
5922                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5923
5924                // Negate result R0:R1
5925                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5926                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5927                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5928                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5929                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5930
5931                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
5932                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
5933                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5934
5935                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5936                Ok(bytes)
5937            }
5938
5939            // === F32 VFP single-precision Thumb-2 encodings ===
5940            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
5941            ArmOp::F32Add { sd, sn, sm } => {
5942                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A00, sd, sn, sm)?))
5943            }
5944            ArmOp::F32Sub { sd, sn, sm } => {
5945                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A40, sd, sn, sm)?))
5946            }
5947            ArmOp::F32Mul { sd, sn, sm } => {
5948                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE200A00, sd, sn, sm)?))
5949            }
5950            ArmOp::F32Div { sd, sn, sm } => {
5951                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE800A00, sd, sn, sm)?))
5952            }
5953            ArmOp::F32Abs { sd, sm } => {
5954                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB00AC0, sd, sm)?))
5955            }
5956            ArmOp::F32Neg { sd, sm } => {
5957                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10A40, sd, sm)?))
5958            }
5959            ArmOp::F32Sqrt { sd, sm } => {
5960                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10AC0, sd, sm)?))
5961            }
5962
5963            // f32 pseudo-ops — multi-instruction sequences
5964            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
5965            ArmOp::F32Ceil { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b01),
5966            ArmOp::F32Floor { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b10),
5967            ArmOp::F32Trunc { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b11),
5968            ArmOp::F32Nearest { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b00),
5969            ArmOp::F32Min { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, true),
5970            ArmOp::F32Max { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, false),
5971            ArmOp::F32Copysign { sd, sn, sm } => self.encode_thumb_f32_copysign(sd, sn, sm),
5972
5973            // f32 comparisons — VCMP + VMRS + MOV #0 + IT + MOV #1
5974            ArmOp::F32Eq { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x0),
5975            ArmOp::F32Ne { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x1),
5976            ArmOp::F32Lt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x4),
5977            ArmOp::F32Le { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x9),
5978            ArmOp::F32Gt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xC),
5979            ArmOp::F32Ge { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xA),
5980
5981            ArmOp::F32Const { sd, value } => self.encode_thumb_f32_const(sd, *value),
5982
5983            ArmOp::F32Load { sd, addr } => {
5984                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED900A00, sd, addr)?))
5985            }
5986            ArmOp::F32Store { sd, addr } => {
5987                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED800A00, sd, addr)?))
5988            }
5989
5990            ArmOp::F32ConvertI32S { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, true),
5991            ArmOp::F32ConvertI32U { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, false),
5992            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
5993                Err(synth_core::Error::synthesis(
5994                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
5995                ))
5996            }
5997            ArmOp::F32ReinterpretI32 { sd, rm } => {
5998                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(true, sd, rm)?))
5999            }
6000            ArmOp::I32ReinterpretF32 { rd, sm } => {
6001                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(false, sm, rd)?))
6002            }
6003            ArmOp::I32TruncF32S { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, true),
6004            ArmOp::I32TruncF32U { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, false),
6005
6006            // === F64 VFP double-precision Thumb-2 encodings ===
6007            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6008            ArmOp::F64Add { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6009                0xEE300B00, dd, dn, dm,
6010            )?)),
6011            ArmOp::F64Sub { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6012                0xEE300B40, dd, dn, dm,
6013            )?)),
6014            ArmOp::F64Mul { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6015                0xEE200B00, dd, dn, dm,
6016            )?)),
6017            ArmOp::F64Div { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6018                0xEE800B00, dd, dn, dm,
6019            )?)),
6020            ArmOp::F64Abs { dd, dm } => {
6021                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?))
6022            }
6023            ArmOp::F64Neg { dd, dm } => {
6024                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?))
6025            }
6026            ArmOp::F64Sqrt { dd, dm } => {
6027                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?))
6028            }
6029
6030            // f64 pseudo-ops
6031            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6032            ArmOp::F64Ceil { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b01),
6033            ArmOp::F64Floor { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b10),
6034            ArmOp::F64Trunc { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b11),
6035            ArmOp::F64Nearest { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b00),
6036            ArmOp::F64Min { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, true),
6037            ArmOp::F64Max { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, false),
6038            ArmOp::F64Copysign { dd, dn, dm } => self.encode_thumb_f64_copysign(dd, dn, dm),
6039
6040            // f64 comparisons
6041            ArmOp::F64Eq { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x0),
6042            ArmOp::F64Ne { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x1),
6043            ArmOp::F64Lt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x4),
6044            ArmOp::F64Le { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x9),
6045            ArmOp::F64Gt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xC),
6046            ArmOp::F64Ge { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xA),
6047
6048            ArmOp::F64Const { dd, value } => self.encode_thumb_f64_const(dd, *value),
6049
6050            ArmOp::F64Load { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6051                0xED900B00, dd, addr,
6052            )?)),
6053            ArmOp::F64Store { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6054                0xED800B00, dd, addr,
6055            )?)),
6056
6057            ArmOp::F64ConvertI32S { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, true),
6058            ArmOp::F64ConvertI32U { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, false),
6059            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
6060                Err(synth_core::Error::synthesis(
6061                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6062                ))
6063            }
6064            ArmOp::F64PromoteF32 { dd, sm } => self.encode_thumb_f64_promote_f32(dd, sm),
6065            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => Ok(vfp_to_thumb_bytes(
6066                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?,
6067            )),
6068            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => Ok(vfp_to_thumb_bytes(
6069                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?,
6070            )),
6071            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
6072                Err(synth_core::Error::synthesis(
6073                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
6074                ))
6075            }
6076            ArmOp::I32TruncF64S { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, true),
6077            ArmOp::I32TruncF64U { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, false),
6078
6079            // ===== i64 operations: encode as multi-instruction Thumb-2 sequences =====
6080
6081            // I64Add: ADDS rdlo, rnlo, rmlo; ADC.W rdhi, rnhi, rmhi
6082            ArmOp::I64Add {
6083                rdlo,
6084                rdhi,
6085                rnlo,
6086                rnhi,
6087                rmlo,
6088                rmhi,
6089            } => {
6090                let mut bytes = Vec::new();
6091                // ADDS rdlo, rnlo, rmlo (16-bit)
6092                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adds {
6093                    rd: *rdlo,
6094                    rn: *rnlo,
6095                    op2: Operand2::Reg(*rmlo),
6096                })?);
6097                // ADC.W rdhi, rnhi, rmhi (32-bit)
6098                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adc {
6099                    rd: *rdhi,
6100                    rn: *rnhi,
6101                    op2: Operand2::Reg(*rmhi),
6102                })?);
6103                Ok(bytes)
6104            }
6105
6106            // I64Sub: SUBS rdlo, rnlo, rmlo; SBC.W rdhi, rnhi, rmhi
6107            ArmOp::I64Sub {
6108                rdlo,
6109                rdhi,
6110                rnlo,
6111                rnhi,
6112                rmlo,
6113                rmhi,
6114            } => {
6115                let mut bytes = Vec::new();
6116                // SUBS rdlo, rnlo, rmlo (16-bit)
6117                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Subs {
6118                    rd: *rdlo,
6119                    rn: *rnlo,
6120                    op2: Operand2::Reg(*rmlo),
6121                })?);
6122                // SBC.W rdhi, rnhi, rmhi (32-bit)
6123                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Sbc {
6124                    rd: *rdhi,
6125                    rn: *rnhi,
6126                    op2: Operand2::Reg(*rmhi),
6127                })?);
6128                Ok(bytes)
6129            }
6130
6131            // I64And: AND rdlo, rnlo, rmlo; AND rdhi, rnhi, rmhi
6132            ArmOp::I64And {
6133                rdlo,
6134                rdhi,
6135                rnlo,
6136                rnhi,
6137                rmlo,
6138                rmhi,
6139            } => {
6140                let mut bytes = Vec::new();
6141                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6142                    rd: *rdlo,
6143                    rn: *rnlo,
6144                    op2: Operand2::Reg(*rmlo),
6145                })?);
6146                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6147                    rd: *rdhi,
6148                    rn: *rnhi,
6149                    op2: Operand2::Reg(*rmhi),
6150                })?);
6151                Ok(bytes)
6152            }
6153
6154            // I64Or: ORR rdlo, rnlo, rmlo; ORR rdhi, rnhi, rmhi
6155            ArmOp::I64Or {
6156                rdlo,
6157                rdhi,
6158                rnlo,
6159                rnhi,
6160                rmlo,
6161                rmhi,
6162            } => {
6163                let mut bytes = Vec::new();
6164                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6165                    rd: *rdlo,
6166                    rn: *rnlo,
6167                    op2: Operand2::Reg(*rmlo),
6168                })?);
6169                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6170                    rd: *rdhi,
6171                    rn: *rnhi,
6172                    op2: Operand2::Reg(*rmhi),
6173                })?);
6174                Ok(bytes)
6175            }
6176
6177            // I64Xor: EOR rdlo, rnlo, rmlo; EOR rdhi, rnhi, rmhi
6178            ArmOp::I64Xor {
6179                rdlo,
6180                rdhi,
6181                rnlo,
6182                rnhi,
6183                rmlo,
6184                rmhi,
6185            } => {
6186                let mut bytes = Vec::new();
6187                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6188                    rd: *rdlo,
6189                    rn: *rnlo,
6190                    op2: Operand2::Reg(*rmlo),
6191                })?);
6192                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6193                    rd: *rdhi,
6194                    rn: *rnhi,
6195                    op2: Operand2::Reg(*rmhi),
6196                })?);
6197                Ok(bytes)
6198            }
6199
6200            // I64Eqz: ORR scratch, lo, hi; ITE EQ; MOV rd, #1; MOV rd, #0
6201            ArmOp::I64Eqz { rd, rnlo, rnhi } => self.encode_thumb(&ArmOp::I64SetCondZ {
6202                rd: *rd,
6203                rn_lo: *rnlo,
6204                rn_hi: *rnhi,
6205            }),
6206
6207            // I64 comparisons: delegate to I64SetCond
6208            ArmOp::I64Eq {
6209                rd,
6210                rnlo,
6211                rnhi,
6212                rmlo,
6213                rmhi,
6214            } => self.encode_thumb(&ArmOp::I64SetCond {
6215                rd: *rd,
6216                rn_lo: *rnlo,
6217                rn_hi: *rnhi,
6218                rm_lo: *rmlo,
6219                rm_hi: *rmhi,
6220                cond: synth_synthesis::Condition::EQ,
6221            }),
6222
6223            ArmOp::I64Ne {
6224                rd,
6225                rnlo,
6226                rnhi,
6227                rmlo,
6228                rmhi,
6229            } => self.encode_thumb(&ArmOp::I64SetCond {
6230                rd: *rd,
6231                rn_lo: *rnlo,
6232                rn_hi: *rnhi,
6233                rm_lo: *rmlo,
6234                rm_hi: *rmhi,
6235                cond: synth_synthesis::Condition::NE,
6236            }),
6237
6238            ArmOp::I64LtS {
6239                rd,
6240                rnlo,
6241                rnhi,
6242                rmlo,
6243                rmhi,
6244            } => self.encode_thumb(&ArmOp::I64SetCond {
6245                rd: *rd,
6246                rn_lo: *rnlo,
6247                rn_hi: *rnhi,
6248                rm_lo: *rmlo,
6249                rm_hi: *rmhi,
6250                cond: synth_synthesis::Condition::LT,
6251            }),
6252
6253            ArmOp::I64LtU {
6254                rd,
6255                rnlo,
6256                rnhi,
6257                rmlo,
6258                rmhi,
6259            } => self.encode_thumb(&ArmOp::I64SetCond {
6260                rd: *rd,
6261                rn_lo: *rnlo,
6262                rn_hi: *rnhi,
6263                rm_lo: *rmlo,
6264                rm_hi: *rmhi,
6265                cond: synth_synthesis::Condition::LO,
6266            }),
6267
6268            ArmOp::I64LeS {
6269                rd,
6270                rnlo,
6271                rnhi,
6272                rmlo,
6273                rmhi,
6274            } => self.encode_thumb(&ArmOp::I64SetCond {
6275                rd: *rd,
6276                rn_lo: *rnlo,
6277                rn_hi: *rnhi,
6278                rm_lo: *rmlo,
6279                rm_hi: *rmhi,
6280                cond: synth_synthesis::Condition::LE,
6281            }),
6282
6283            ArmOp::I64LeU {
6284                rd,
6285                rnlo,
6286                rnhi,
6287                rmlo,
6288                rmhi,
6289            } => self.encode_thumb(&ArmOp::I64SetCond {
6290                rd: *rd,
6291                rn_lo: *rnlo,
6292                rn_hi: *rnhi,
6293                rm_lo: *rmlo,
6294                rm_hi: *rmhi,
6295                cond: synth_synthesis::Condition::LS,
6296            }),
6297
6298            ArmOp::I64GtS {
6299                rd,
6300                rnlo,
6301                rnhi,
6302                rmlo,
6303                rmhi,
6304            } => self.encode_thumb(&ArmOp::I64SetCond {
6305                rd: *rd,
6306                rn_lo: *rnlo,
6307                rn_hi: *rnhi,
6308                rm_lo: *rmlo,
6309                rm_hi: *rmhi,
6310                cond: synth_synthesis::Condition::GT,
6311            }),
6312
6313            ArmOp::I64GtU {
6314                rd,
6315                rnlo,
6316                rnhi,
6317                rmlo,
6318                rmhi,
6319            } => self.encode_thumb(&ArmOp::I64SetCond {
6320                rd: *rd,
6321                rn_lo: *rnlo,
6322                rn_hi: *rnhi,
6323                rm_lo: *rmlo,
6324                rm_hi: *rmhi,
6325                cond: synth_synthesis::Condition::HI,
6326            }),
6327
6328            ArmOp::I64GeS {
6329                rd,
6330                rnlo,
6331                rnhi,
6332                rmlo,
6333                rmhi,
6334            } => self.encode_thumb(&ArmOp::I64SetCond {
6335                rd: *rd,
6336                rn_lo: *rnlo,
6337                rn_hi: *rnhi,
6338                rm_lo: *rmlo,
6339                rm_hi: *rmhi,
6340                cond: synth_synthesis::Condition::GE,
6341            }),
6342
6343            ArmOp::I64GeU {
6344                rd,
6345                rnlo,
6346                rnhi,
6347                rmlo,
6348                rmhi,
6349            } => self.encode_thumb(&ArmOp::I64SetCond {
6350                rd: *rd,
6351                rn_lo: *rnlo,
6352                rn_hi: *rnhi,
6353                rm_lo: *rmlo,
6354                rm_hi: *rmhi,
6355                cond: synth_synthesis::Condition::HS,
6356            }),
6357
6358            // I64Const: MOVW rdlo, lo16; MOVT rdlo, hi16; MOVW rdhi, lo16_hi; MOVT rdhi, hi16_hi
6359            ArmOp::I64Const { rdlo, rdhi, value } => {
6360                let lo32 = *value as u32;
6361                let hi32 = (*value >> 32) as u32;
6362                let mut bytes = Vec::new();
6363                // Load low 32 bits into rdlo
6364                bytes.extend_from_slice(
6365                    &self.encode_thumb32_movw_raw(reg_to_bits(rdlo), lo32 & 0xFFFF)?,
6366                );
6367                if lo32 > 0xFFFF {
6368                    bytes.extend_from_slice(
6369                        &self.encode_thumb32_movt_raw(reg_to_bits(rdlo), lo32 >> 16)?,
6370                    );
6371                }
6372                // Load high 32 bits into rdhi
6373                bytes.extend_from_slice(
6374                    &self.encode_thumb32_movw_raw(reg_to_bits(rdhi), hi32 & 0xFFFF)?,
6375                );
6376                if hi32 > 0xFFFF {
6377                    bytes.extend_from_slice(
6378                        &self.encode_thumb32_movt_raw(reg_to_bits(rdhi), hi32 >> 16)?,
6379                    );
6380                }
6381                Ok(bytes)
6382            }
6383
6384            // I64Ldr: LDR rdlo, [base, offset]; LDR rdhi, [base, offset+4]
6385            ArmOp::I64Ldr { rdlo, rdhi, addr } => {
6386                let mut bytes = Vec::new();
6387                // #372/#382: a memory `i64.load` carries an index register
6388                // (`reg_imm(R11, addr_reg, offset)` = R11 + addr + offset). The
6389                // immediate `encode_thumb32_ldr` below uses only base+offset and
6390                // would SILENTLY DROP `offset_reg` — the #206 defect, here for
6391                // i64. `i64_effective_base` materializes the effective base into
6392                // `ip` (and, when `offset+4 > 0xFFF`, folds the offset in too so
6393                // the function is NOT skipped — #382), returning the residual
6394                // imm12 for the two halves. Frame i64 loads (no `offset_reg`, e.g.
6395                // a spilled local at `[SP, #off]`) keep the plain `[base,#off]`
6396                // form unchanged — so existing output is byte-identical.
6397                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6398                bytes.extend_from_slice(&self.encode_thumb32_ldr(rdlo, &base, offset)?);
6399                bytes.extend_from_slice(&self.encode_thumb32_ldr(
6400                    rdhi,
6401                    &base,
6402                    offset.wrapping_add(4),
6403                )?);
6404                Ok(bytes)
6405            }
6406
6407            // I64Str: STR rdlo, [base, offset]; STR rdhi, [base, offset+4]
6408            ArmOp::I64Str { rdlo, rdhi, addr } => {
6409                let mut bytes = Vec::new();
6410                // #372/#382: same index-materialization + large-offset fold as
6411                // I64Ldr (see above).
6412                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6413                bytes.extend_from_slice(&self.encode_thumb32_str(rdlo, &base, offset)?);
6414                bytes.extend_from_slice(&self.encode_thumb32_str(
6415                    rdhi,
6416                    &base,
6417                    offset.wrapping_add(4),
6418                )?);
6419                Ok(bytes)
6420            }
6421
6422            // I64ExtendI32S: MOV rdlo, rn; ASR rdhi, rdlo, #31 (sign-extend)
6423            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
6424                let mut bytes = Vec::new();
6425                if rdlo != rn {
6426                    // MOV rdlo, rn (16-bit)
6427                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6428                        rd: *rdlo,
6429                        op2: Operand2::Reg(*rn),
6430                    })?);
6431                }
6432                // ASR rdhi, rdlo, #31 (sign-extend: fill high word with sign bit)
6433                bytes.extend_from_slice(
6434                    &self.encode_thumb32_shift(rdhi, rdlo, 31, 0b10)?, // ASR type
6435                );
6436                Ok(bytes)
6437            }
6438
6439            // I64ExtendI32U: MOV rdlo, rn; MOV rdhi, #0
6440            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
6441                let mut bytes = Vec::new();
6442                if rdlo != rn {
6443                    // MOV rdlo, rn
6444                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6445                        rd: *rdlo,
6446                        op2: Operand2::Reg(*rn),
6447                    })?);
6448                }
6449                // MOV rdhi, #0 (16-bit: MOVS Rd, #0)
6450                let rdhi_bits = reg_to_bits(rdhi) as u16;
6451                let instr: u16 = 0x2000 | (rdhi_bits << 8);
6452                bytes.extend_from_slice(&instr.to_le_bytes());
6453                Ok(bytes)
6454            }
6455
6456            // I32WrapI64: MOV rd, rnlo (just take low 32 bits)
6457            ArmOp::I32WrapI64 { rd, rnlo } => {
6458                if rd == rnlo {
6459                    // No-op: already in the right register
6460                    let instr: u16 = 0xBF00; // NOP
6461                    Ok(instr.to_le_bytes().to_vec())
6462                } else {
6463                    // MOV rd, rnlo
6464                    self.encode_thumb(&ArmOp::Mov {
6465                        rd: *rd,
6466                        op2: Operand2::Reg(*rnlo),
6467                    })
6468                }
6469            }
6470
6471            // ===== Helium MVE operations (Thumb-2 encoding) =====
6472            ArmOp::MveLoad { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vldrw(qd, addr))),
6473            ArmOp::MveStore { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vstrw(qd, addr))),
6474            ArmOp::MveConst { qd, bytes } => self.encode_thumb_mve_const(qd, bytes),
6475            ArmOp::MveAnd { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6476                0xEF000150, qd, qn, qm,
6477            ))),
6478            ArmOp::MveOrr { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6479                0xEF200150, qd, qn, qm,
6480            ))),
6481            ArmOp::MveEor { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6482                0xFF000150, qd, qn, qm,
6483            ))),
6484            ArmOp::MveMvn { qd, qm } => {
6485                // VMVN Qd, Qm: 0xFFB005C0 | Qd<<12 | Qm
6486                let qd_enc = qreg_to_num(qd);
6487                let qm_enc = qreg_to_num(qm);
6488                let instr: u32 = 0xFFB005C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6489                Ok(vfp_to_thumb_bytes(instr))
6490            }
6491            ArmOp::MveBic { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6492                0xEF100150, qd, qn, qm,
6493            ))),
6494            ArmOp::MveAddI { qd, qn, qm, size } => {
6495                let sz = mve_size_bits(size);
6496                let base: u32 = 0xEF000840 | (sz << 20);
6497                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6498            }
6499            ArmOp::MveSubI { qd, qn, qm, size } => {
6500                let sz = mve_size_bits(size);
6501                let base: u32 = 0xFF000840 | (sz << 20);
6502                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6503            }
6504            ArmOp::MveMulI { qd, qn, qm, size } => {
6505                let sz = mve_size_bits(size);
6506                let base: u32 = 0xEF000950 | (sz << 20);
6507                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6508            }
6509            ArmOp::MveNegI { qd, qm, size } => {
6510                let sz = mve_size_bits(size);
6511                // VNEG.Sx Qd, Qm
6512                let qd_enc = qreg_to_num(qd);
6513                let qm_enc = qreg_to_num(qm);
6514                let base: u32 = 0xFFB103C0 | (sz << 18);
6515                let instr = base | ((qd_enc * 2) << 12) | (qm_enc * 2);
6516                Ok(vfp_to_thumb_bytes(instr))
6517            }
6518            ArmOp::MveDup { qd, rn, size } => {
6519                let sz = mve_size_bits(size);
6520                let qd_enc = qreg_to_num(qd);
6521                let rn_bits = reg_to_bits(rn);
6522                // VDUP.sz Qd, Rn: EEA0 0B10 variant
6523                // size encoding: 00=32, 01=16, 10=8
6524                let be = match sz {
6525                    0 => 0b00u32, // 8-bit
6526                    1 => 0b01,    // 16-bit
6527                    _ => 0b00,    // 32-bit (default)
6528                };
6529                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12) | (be << 5);
6530                Ok(vfp_to_thumb_bytes(instr))
6531            }
6532            ArmOp::MveExtractLane { rd, qn, lane, size } => {
6533                let qn_enc = qreg_to_num(qn);
6534                let rd_bits = reg_to_bits(rd);
6535                // VMOV.sz Rd, Dn[x] — extract from Q-register lane
6536                // For 32-bit: VMOV Rd, Dn — where Dn is the appropriate D-register
6537                let d_reg = qn_enc * 2 + ((*lane as u32) >> 1);
6538                let lane_in_d = (*lane as u32) & 1;
6539                let _sz = mve_size_bits(size);
6540                // VMOV Rd, Dn[x]: EE10 0B10 for 32-bit
6541                let instr: u32 = 0xEE100B10 | (d_reg << 16) | (rd_bits << 12) | (lane_in_d << 21);
6542                Ok(vfp_to_thumb_bytes(instr))
6543            }
6544            ArmOp::MveInsertLane { qd, rn, lane, size } => {
6545                let qd_enc = qreg_to_num(qd);
6546                let rn_bits = reg_to_bits(rn);
6547                let d_reg = qd_enc * 2 + ((*lane as u32) >> 1);
6548                let lane_in_d = (*lane as u32) & 1;
6549                let _sz = mve_size_bits(size);
6550                // VMOV Dn[x], Rn: EE00 0B10 for 32-bit
6551                let instr: u32 = 0xEE000B10 | (d_reg << 16) | (rn_bits << 12) | (lane_in_d << 21);
6552                Ok(vfp_to_thumb_bytes(instr))
6553            }
6554
6555            // MVE float comparisons — emit VCMP + VPSEL sequence (simplified: just VCMP)
6556            ArmOp::MveCmpEqI { qd, qn, qm, size }
6557            | ArmOp::MveCmpNeI { qd, qn, qm, size }
6558            | ArmOp::MveCmpLtS { qd, qn, qm, size }
6559            | ArmOp::MveCmpLtU { qd, qn, qm, size }
6560            | ArmOp::MveCmpGtS { qd, qn, qm, size }
6561            | ArmOp::MveCmpGtU { qd, qn, qm, size }
6562            | ArmOp::MveCmpLeS { qd, qn, qm, size }
6563            | ArmOp::MveCmpLeU { qd, qn, qm, size }
6564            | ArmOp::MveCmpGeS { qd, qn, qm, size }
6565            | ArmOp::MveCmpGeU { qd, qn, qm, size } => {
6566                // Encode as VADD (placeholder encoding — real implementation
6567                // would use VCMP + VPSEL pair)
6568                let sz = mve_size_bits(size);
6569                let base: u32 = 0xEF000840 | (sz << 20);
6570                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6571            }
6572
6573            // f32x4 MVE arithmetic
6574            ArmOp::MveAddF32 { qd, qn, qm } => {
6575                // VADD.F32 Qd, Qn, Qm (MVE): 0xEF000D40
6576                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6577            }
6578            ArmOp::MveSubF32 { qd, qn, qm } => {
6579                // VSUB.F32 Qd, Qn, Qm (MVE): 0xEF200D40
6580                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF200D40, qd, qn, qm)))
6581            }
6582            ArmOp::MveMulF32 { qd, qn, qm } => {
6583                // VMUL.F32 Qd, Qn, Qm (MVE): 0xFF000D50
6584                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xFF000D50, qd, qn, qm)))
6585            }
6586            ArmOp::MveNegF32 { qd, qm } => {
6587                let qd_enc = qreg_to_num(qd);
6588                let qm_enc = qreg_to_num(qm);
6589                // VNEG.F32 Qd, Qm: FFB907C0
6590                let instr: u32 = 0xFFB907C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6591                Ok(vfp_to_thumb_bytes(instr))
6592            }
6593            ArmOp::MveAbsF32 { qd, qm } => {
6594                let qd_enc = qreg_to_num(qd);
6595                let qm_enc = qreg_to_num(qm);
6596                // VABS.F32 Qd, Qm: FFB90740
6597                let instr: u32 = 0xFFB90740 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6598                Ok(vfp_to_thumb_bytes(instr))
6599            }
6600            ArmOp::MveCmpEqF32 { qd, qn, qm }
6601            | ArmOp::MveCmpNeF32 { qd, qn, qm }
6602            | ArmOp::MveCmpLtF32 { qd, qn, qm }
6603            | ArmOp::MveCmpLeF32 { qd, qn, qm }
6604            | ArmOp::MveCmpGtF32 { qd, qn, qm }
6605            | ArmOp::MveCmpGeF32 { qd, qn, qm } => {
6606                // Placeholder: encode as VADD.F32 (real impl needs VCMP.F32 + VPSEL)
6607                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6608            }
6609            ArmOp::MveDupF32 { qd, rn } => {
6610                let qd_enc = qreg_to_num(qd);
6611                let rn_bits = reg_to_bits(rn);
6612                // VDUP.32 Qd, Rn (same encoding as integer VDUP.32)
6613                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12);
6614                Ok(vfp_to_thumb_bytes(instr))
6615            }
6616            ArmOp::MveExtractLaneF32 { rd, qn, lane } => {
6617                let qn_enc = qreg_to_num(qn);
6618                let rd_bits = reg_to_bits(rd);
6619                // VMOV Rd, Sn where Sn = Q*4 + lane
6620                let s_num = qn_enc * 4 + (*lane as u32);
6621                let (vn, n) = encode_sreg(s_num);
6622                let instr: u32 = 0xEE100A10 | (vn << 16) | (rd_bits << 12) | (n << 7);
6623                Ok(vfp_to_thumb_bytes(instr))
6624            }
6625            ArmOp::MveReplaceLaneF32 { qd, rn, lane } => {
6626                let qd_enc = qreg_to_num(qd);
6627                let rn_bits = reg_to_bits(rn);
6628                // VMOV Sn, Rn where Sn = Q*4 + lane
6629                let s_num = qd_enc * 4 + (*lane as u32);
6630                let (vn, n) = encode_sreg(s_num);
6631                let instr: u32 = 0xEE000A10 | (vn << 16) | (rn_bits << 12) | (n << 7);
6632                Ok(vfp_to_thumb_bytes(instr))
6633            }
6634            ArmOp::MveDivF32 { qd, qn, qm } => {
6635                // Lane-wise: extract 4 S-regs, VDIV, insert back
6636                self.encode_thumb_mve_lane_wise_f32_binop(qd, qn, qm, 0xEE800A00)
6637            }
6638            ArmOp::MveSqrtF32 { qd, qm } => {
6639                // Lane-wise: extract 4 S-regs, VSQRT, insert back
6640                self.encode_thumb_mve_lane_wise_f32_sqrt(qd, qm)
6641            }
6642
6643            // Catch-all for any remaining ops
6644            _ => {
6645                let instr: u16 = 0xBF00; // NOP
6646                Ok(instr.to_le_bytes().to_vec())
6647            }
6648        }
6649    }
6650
6651    // === Thumb-2 VFP multi-instruction helpers ===
6652
6653    /// Encode F32 comparison as Thumb-2: VCMP.F32 + VMRS + MOVS rd,#0 + IT + MOV rd,#1
6654    fn encode_thumb_f32_compare(
6655        &self,
6656        rd: &Reg,
6657        sn: &VfpReg,
6658        sm: &VfpReg,
6659        cond_code: u32,
6660    ) -> Result<Vec<u8>> {
6661        let mut bytes = Vec::new();
6662        let rd_bits = reg_to_bits(rd);
6663
6664        // VCMP.F32 Sn, Sm
6665        let sn_num = vfp_sreg_to_num(sn)?;
6666        let sm_num = vfp_sreg_to_num(sm)?;
6667        let (vd, d) = encode_sreg(sn_num);
6668        let (vm, m) = encode_sreg(sm_num);
6669        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
6670        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
6671
6672        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
6673        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
6674
6675        // MOVS Rd, #0 (16-bit): 0010 0 Rd(3) 0000 0000
6676        if rd_bits < 8 {
6677            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
6678            bytes.extend_from_slice(&movs_zero.to_le_bytes());
6679        } else {
6680            // MOV.W Rd, #0 (32-bit Thumb-2)
6681            let hw1: u16 = 0xF04F;
6682            let hw2: u16 = (rd_bits as u16) << 8;
6683            bytes.extend_from_slice(&hw1.to_le_bytes());
6684            bytes.extend_from_slice(&hw2.to_le_bytes());
6685        }
6686
6687        // IT<cond> — If-Then for conditional MOV
6688        // IT encoding: 1011 1111 cond(4) mask(4)
6689        // mask = 0x8 for single "then" (IT)
6690        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
6691        bytes.extend_from_slice(&it.to_le_bytes());
6692
6693        // MOV Rd, #1 (16-bit, conditional due to IT): 0010 0 Rd(3) 0000 0001
6694        if rd_bits < 8 {
6695            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
6696            bytes.extend_from_slice(&mov_one.to_le_bytes());
6697        } else {
6698            // MOV.W Rd, #1 (32-bit)
6699            let hw1: u16 = 0xF04F;
6700            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
6701            bytes.extend_from_slice(&hw1.to_le_bytes());
6702            bytes.extend_from_slice(&hw2.to_le_bytes());
6703        }
6704
6705        Ok(bytes)
6706    }
6707
6708    /// Encode F32 constant load as Thumb-2: MOVW + MOVT + VMOV
6709    fn encode_thumb_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
6710        let mut bytes = Vec::new();
6711        let bits = value.to_bits();
6712        let rt: u32 = 12; // R12/IP as temp
6713
6714        // MOVW R12, #lo16
6715        // Thumb-2 MOVW: 11110 i 10 0100 imm4 | 0 imm3 Rd imm8
6716        let lo16 = bits & 0xFFFF;
6717        let imm4 = (lo16 >> 12) & 0xF;
6718        let i_bit = (lo16 >> 11) & 1;
6719        let imm3 = (lo16 >> 8) & 0x7;
6720        let imm8 = lo16 & 0xFF;
6721        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
6722        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
6723        bytes.extend_from_slice(&hw1.to_le_bytes());
6724        bytes.extend_from_slice(&hw2.to_le_bytes());
6725
6726        // MOVT R12, #hi16
6727        let hi16 = (bits >> 16) & 0xFFFF;
6728        let imm4 = (hi16 >> 12) & 0xF;
6729        let i_bit = (hi16 >> 11) & 1;
6730        let imm3 = (hi16 >> 8) & 0x7;
6731        let imm8 = hi16 & 0xFF;
6732        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
6733        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
6734        bytes.extend_from_slice(&hw1.to_le_bytes());
6735        bytes.extend_from_slice(&hw2.to_le_bytes());
6736
6737        // VMOV Sd, R12
6738        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
6739        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6740
6741        Ok(bytes)
6742    }
6743
6744    /// Encode VMOV + VCVT.F32.xS32 as Thumb-2
6745    fn encode_thumb_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
6746        let mut bytes = Vec::new();
6747
6748        // VMOV Sd, Rm
6749        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
6750        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6751
6752        // VCVT.F32.S32/U32 Sd, Sd
6753        let sd_num = vfp_sreg_to_num(sd)?;
6754        let (vd, d) = encode_sreg(sd_num);
6755        let (vm, m) = encode_sreg(sd_num);
6756        let base = if signed { 0xEEB80A40 } else { 0xEEB80AC0 };
6757        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
6758        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
6759
6760        Ok(bytes)
6761    }
6762
6763    /// Encode F32 rounding pseudo-op as Thumb-2 via VCVT to integer and back
6764    /// Encode F32 rounding as Thumb-2.
6765    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
6766    ///
6767    /// For trunc: uses VCVTR.S32.F32 (always truncates).
6768    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant),
6769    /// then restores FPSCR.
6770    fn encode_thumb_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
6771        let mut bytes = Vec::new();
6772        let sm_num = vfp_sreg_to_num(sm)?;
6773        let sd_num = vfp_sreg_to_num(sd)?;
6774        let (vd_s, d_s) = encode_sreg(sd_num);
6775        let (vm_s, m_s) = encode_sreg(sm_num);
6776
6777        if mode == 0b11 {
6778            // Trunc (toward zero): VCVTR.S32.F32 — bit[7]=1, always truncates
6779            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
6780            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
6781        } else {
6782            // ceil/floor/nearest: manipulate FPSCR rounding mode
6783            let rt: u32 = 12; // R12/IP as temp
6784
6785            // VMRS R12, FPSCR
6786            let vmrs = 0xEEF10A10 | (rt << 12);
6787            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
6788
6789            // BIC.W R12, R12, #(3 << 22) — clear RMode bits [23:22]
6790            // Thumb-2 modified immediate for 3<<22 = 0x00C00000:
6791            // BIC.W encoding: 11110 i 0 0001 S Rn | 0 imm3 Rd imm8
6792            // 0x00C00000 = 0x03 shifted left by 22 => Thumb mod-imm: i=0, imm3=0b101, imm8=0x03
6793            let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF); // BIC, Rn=R12
6794            let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
6795            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
6796            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
6797
6798            // ORR.W R12, R12, #(mode << 22)
6799            if mode != 0 {
6800                let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF); // ORR, Rn=R12
6801                let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
6802                bytes.extend_from_slice(&orr_hw1.to_le_bytes());
6803                bytes.extend_from_slice(&orr_hw2.to_le_bytes());
6804            }
6805
6806            // VMSR FPSCR, R12
6807            let vmsr = 0xEEE10A10 | (rt << 12);
6808            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
6809
6810            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rmode
6811            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
6812            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
6813
6814            // Restore FPSCR: clear rmode bits back to nearest (default)
6815            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
6816            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
6817            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
6818            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
6819        }
6820
6821        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
6822        let (vd2, d2) = encode_sreg(sd_num);
6823        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
6824        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
6825
6826        Ok(bytes)
6827    }
6828
6829    /// Encode F32 min/max as Thumb-2: VMOV + VCMP + VMRS + IT + VMOV
6830    fn encode_thumb_f32_minmax(
6831        &self,
6832        sd: &VfpReg,
6833        sn: &VfpReg,
6834        sm: &VfpReg,
6835        is_min: bool,
6836    ) -> Result<Vec<u8>> {
6837        let mut bytes = Vec::new();
6838        let sn_num = vfp_sreg_to_num(sn)?;
6839        let sm_num = vfp_sreg_to_num(sm)?;
6840        let sd_num = vfp_sreg_to_num(sd)?;
6841
6842        // VMOV.F32 Sd, Sn
6843        let (vd, d) = encode_sreg(sd_num);
6844        let (vn, n) = encode_sreg(sn_num);
6845        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
6846        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sn));
6847
6848        // VCMP.F32 Sn, Sm
6849        let (vm, m) = encode_sreg(sm_num);
6850        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
6851        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
6852
6853        // VMRS APSR_nzcv, FPSCR
6854        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
6855
6856        // IT GT (for min) or IT MI (for max)
6857        let cond: u16 = if is_min { 0xC } else { 0x4 };
6858        let it: u16 = 0xBF00 | (cond << 4) | 0x8;
6859        bytes.extend_from_slice(&it.to_le_bytes());
6860
6861        // VMOV{cond}.F32 Sd, Sm — conditional VMOV in IT block
6862        let vmov_sm = 0xEEB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
6863        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sm));
6864
6865        Ok(bytes)
6866    }
6867
6868    /// Encode F32 copysign as Thumb-2
6869    fn encode_thumb_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
6870        let mut bytes = Vec::new();
6871
6872        // VMOV R12, Sm (get sign source bits)
6873        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
6874            false,
6875            sm,
6876            &Reg::R12,
6877        )?));
6878
6879        // VMOV R0, Sn (get magnitude source bits)
6880        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
6881            false,
6882            sn,
6883            &Reg::R0,
6884        )?));
6885
6886        // AND.W R12, R12, #0x80000000
6887        // Thumb-2 modified immediate: 0x80000000 = constant 0x80 with rotation
6888        // Using T1 encoding: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8
6889        // 0x80000000: i=0, imm3=0b001, imm8=0x00 (rotation=4, value=0x80)
6890        // Actually encoding #0x80000000 as modified constant:
6891        // bit pattern 1 followed by 31 zeros: enc = 0b0100_00000000 = 0x0100? No.
6892        // ARM modified immediate: abcdefgh rotated. 0x80000000 = 0x80 ROR 2 = enc 0x0102
6893        // Actually: value = abcdefgh ROR (2*rot). 0x80 = 10000000, ROR 2 gives 0x20000000.
6894        // For 0x80000000: 0x02 ROR 2 = 0x80000000. So imm12 = (1<<8) | 0x02 = 0x102
6895        let hw1: u16 = 0xF000 | 12; // AND.W R12, R12, #modified_const (i=0, Rn=R12)
6896        let hw2: u16 = (0x1 << 12) | (12 << 8) | 0x02; // imm3=1, Rd=R12, imm8=0x02
6897        bytes.extend_from_slice(&hw1.to_le_bytes());
6898        bytes.extend_from_slice(&hw2.to_le_bytes());
6899
6900        // BIC.W R0, R0, #0x80000000 (R0 = register 0, fields are zero)
6901        let hw1: u16 = 0xF020; // BIC.W R0, R0, #modified_const (i=0, Rn=R0)
6902        let hw2: u16 = (0x1 << 12) | 0x02; // imm3=1, Rd=R0, imm8=0x02
6903        bytes.extend_from_slice(&hw1.to_le_bytes());
6904        bytes.extend_from_slice(&hw2.to_le_bytes());
6905
6906        // ORR.W R0, R0, R12 (R0 = register 0)
6907        let hw1: u16 = 0xEA40; // ORR.W R0, R0, R12 (Rn=R0)
6908        let hw2: u16 = 12; // Rd=R0, Rm=R12
6909        bytes.extend_from_slice(&hw1.to_le_bytes());
6910        bytes.extend_from_slice(&hw2.to_le_bytes());
6911
6912        // VMOV Sd, R0
6913        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
6914            true,
6915            sd,
6916            &Reg::R0,
6917        )?));
6918
6919        Ok(bytes)
6920    }
6921
6922    /// Encode F64 comparison as Thumb-2: VCMP.F64 + VMRS + MOV #0 + IT + MOV #1
6923    fn encode_thumb_f64_compare(
6924        &self,
6925        rd: &Reg,
6926        dn: &VfpReg,
6927        dm: &VfpReg,
6928        cond_code: u32,
6929    ) -> Result<Vec<u8>> {
6930        let mut bytes = Vec::new();
6931        let rd_bits = reg_to_bits(rd);
6932
6933        // VCMP.F64 Dn, Dm
6934        let dn_num = vfp_dreg_to_num(dn)?;
6935        let dm_num = vfp_dreg_to_num(dm)?;
6936        let (vd, d) = encode_dreg(dn_num);
6937        let (vm, m) = encode_dreg(dm_num);
6938        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
6939        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
6940
6941        // VMRS APSR_nzcv, FPSCR
6942        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
6943
6944        // MOVS Rd, #0
6945        if rd_bits < 8 {
6946            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
6947            bytes.extend_from_slice(&movs_zero.to_le_bytes());
6948        } else {
6949            let hw1: u16 = 0xF04F;
6950            let hw2: u16 = (rd_bits as u16) << 8;
6951            bytes.extend_from_slice(&hw1.to_le_bytes());
6952            bytes.extend_from_slice(&hw2.to_le_bytes());
6953        }
6954
6955        // IT<cond>
6956        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
6957        bytes.extend_from_slice(&it.to_le_bytes());
6958
6959        // MOV Rd, #1
6960        if rd_bits < 8 {
6961            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
6962            bytes.extend_from_slice(&mov_one.to_le_bytes());
6963        } else {
6964            let hw1: u16 = 0xF04F;
6965            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
6966            bytes.extend_from_slice(&hw1.to_le_bytes());
6967            bytes.extend_from_slice(&hw2.to_le_bytes());
6968        }
6969
6970        Ok(bytes)
6971    }
6972
6973    /// Encode F64 constant load as Thumb-2: MOVW+MOVT (lo32 into R0) + MOVW+MOVT (hi32 into R12) + VMOV Dd, R0, R12
6974    fn encode_thumb_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
6975        let mut bytes = Vec::new();
6976        let bits = value.to_bits();
6977        let lo32 = bits as u32;
6978        let hi32 = (bits >> 32) as u32;
6979
6980        // MOVW R0, #lo16(lo32)
6981        let lo16 = lo32 & 0xFFFF;
6982        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(0, lo16)?);
6983
6984        // MOVT R0, #hi16(lo32)
6985        let hi16 = (lo32 >> 16) & 0xFFFF;
6986        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(0, hi16)?);
6987
6988        // MOVW R12, #lo16(hi32)
6989        let lo16 = hi32 & 0xFFFF;
6990        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
6991
6992        // MOVT R12, #hi16(hi32)
6993        let hi16 = (hi32 >> 16) & 0xFFFF;
6994        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
6995
6996        // VMOV Dd, R0, R12
6997        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
6998        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6999
7000        Ok(bytes)
7001    }
7002
7003    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as Thumb-2
7004    fn encode_thumb_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7005        let mut bytes = Vec::new();
7006
7007        // VMOV S0, Rm
7008        let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
7009        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7010
7011        // VCVT.F64.S32 Dd, S0 or VCVT.F64.U32 Dd, S0
7012        let dd_num = vfp_dreg_to_num(dd)?;
7013        let (vd, d) = encode_dreg(dd_num);
7014        let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
7015        let vcvt = base | (d << 22) | (vd << 12);
7016        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7017
7018        Ok(bytes)
7019    }
7020
7021    /// Encode VCVT.F64.F32 Dd, Sm as Thumb-2
7022    fn encode_thumb_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7023        let dd_num = vfp_dreg_to_num(dd)?;
7024        let sm_num = vfp_sreg_to_num(sm)?;
7025        let (vd, d) = encode_dreg(dd_num);
7026        let (vm, m) = encode_sreg(sm_num);
7027
7028        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7029        Ok(vfp_to_thumb_bytes(vcvt))
7030    }
7031
7032    /// Encode VCVT.S32/U32.F64 S0, Dm + VMOV Rd, S0 as Thumb-2
7033    fn encode_thumb_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7034        let mut bytes = Vec::new();
7035        let dm_num = vfp_dreg_to_num(dm)?;
7036        let (vm, m) = encode_dreg(dm_num);
7037
7038        // VCVT.S32.F64 S0, Dm or VCVT.U32.F64 S0, Dm
7039        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
7040        let vcvt = base | (m << 5) | vm;
7041        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7042
7043        // VMOV Rd, S0
7044        let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
7045        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7046
7047        Ok(bytes)
7048    }
7049
7050    /// Encode F64 rounding pseudo-op as Thumb-2 via VCVT to integer and back
7051    /// Encode F64 rounding as Thumb-2.
7052    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
7053    fn encode_thumb_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7054        let mut bytes = Vec::new();
7055        let dm_num = vfp_dreg_to_num(dm)?;
7056        let dd_num = vfp_dreg_to_num(dd)?;
7057        let (vm, m) = encode_dreg(dm_num);
7058        let (vd, d) = encode_dreg(dd_num);
7059
7060        if mode == 0b11 {
7061            // Trunc: VCVTR.S32.F64 — bit[7]=1, always truncates
7062            let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
7063            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7064        } else {
7065            let rt: u32 = 12;
7066
7067            // VMRS R12, FPSCR
7068            let vmrs = 0xEEF10A10 | (rt << 12);
7069            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7070
7071            // BIC.W R12, R12, #(3 << 22)
7072            let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF);
7073            let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
7074            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7075            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7076
7077            // ORR.W R12, R12, #(mode << 22)
7078            if mode != 0 {
7079                let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF);
7080                let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
7081                bytes.extend_from_slice(&orr_hw1.to_le_bytes());
7082                bytes.extend_from_slice(&orr_hw2.to_le_bytes());
7083            }
7084
7085            // VMSR FPSCR, R12
7086            let vmsr = 0xEEE10A10 | (rt << 12);
7087            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7088
7089            // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0)
7090            let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
7091            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7092
7093            // Restore FPSCR
7094            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7095            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7096            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7097            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7098        }
7099
7100        // VCVT.F64.S32 Dd, S0
7101        let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
7102        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
7103
7104        Ok(bytes)
7105    }
7106
7107    /// Encode F64 min/max as Thumb-2
7108    fn encode_thumb_f64_minmax(
7109        &self,
7110        dd: &VfpReg,
7111        dn: &VfpReg,
7112        dm: &VfpReg,
7113        is_min: bool,
7114    ) -> Result<Vec<u8>> {
7115        let mut bytes = Vec::new();
7116        let dn_num = vfp_dreg_to_num(dn)?;
7117        let dm_num = vfp_dreg_to_num(dm)?;
7118        let dd_num = vfp_dreg_to_num(dd)?;
7119
7120        // VMOV.F64 Dd, Dn
7121        let (vd, d) = encode_dreg(dd_num);
7122        let (vn, n) = encode_dreg(dn_num);
7123        let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
7124        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_dn));
7125
7126        // VCMP.F64 Dn, Dm
7127        let (vm, m) = encode_dreg(dm_num);
7128        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7129        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7130
7131        // VMRS APSR_nzcv, FPSCR
7132        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7133
7134        // IT GT (for min) or IT MI (for max)
7135        let cond: u16 = if is_min { 0xC } else { 0x4 };
7136        let it: u16 = 0xBF00 | (cond << 4) | 0x8;
7137        bytes.extend_from_slice(&it.to_le_bytes());
7138
7139        // VMOV{cond}.F64 Dd, Dm
7140        let vmov_dm = 0xEEB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7141        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_dm));
7142
7143        Ok(bytes)
7144    }
7145
7146    /// Encode F64 copysign as Thumb-2
7147    fn encode_thumb_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7148        let mut bytes = Vec::new();
7149
7150        // VMOV R0, R12, Dm (get sign source)
7151        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_dreg(
7152            false,
7153            dm,
7154            &Reg::R0,
7155            &Reg::R12,
7156        )?));
7157
7158        // VMOV R1, R2, Dn (get magnitude source)
7159        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_dreg(
7160            false,
7161            dn,
7162            &Reg::R1,
7163            &Reg::R2,
7164        )?));
7165
7166        // AND.W R12, R12, #0x80000000 (i=0, Rn=R12)
7167        let hw1: u16 = 0xF000 | 12;
7168        let hw2: u16 = (0x1 << 12) | (12 << 8) | 0x02;
7169        bytes.extend_from_slice(&hw1.to_le_bytes());
7170        bytes.extend_from_slice(&hw2.to_le_bytes());
7171
7172        // BIC.W R2, R2, #0x80000000 (i=0, Rn=R2)
7173        let hw1: u16 = 0xF020 | 2;
7174        let hw2: u16 = (0x1 << 12) | (2 << 8) | 0x02;
7175        bytes.extend_from_slice(&hw1.to_le_bytes());
7176        bytes.extend_from_slice(&hw2.to_le_bytes());
7177
7178        // ORR.W R2, R2, R12
7179        let hw1: u16 = 0xEA40 | 2;
7180        let hw2: u16 = (2 << 8) | 12;
7181        bytes.extend_from_slice(&hw1.to_le_bytes());
7182        bytes.extend_from_slice(&hw2.to_le_bytes());
7183
7184        // VMOV Dd, R1, R2
7185        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_dreg(
7186            true,
7187            dd,
7188            &Reg::R1,
7189            &Reg::R2,
7190        )?));
7191
7192        Ok(bytes)
7193    }
7194
7195    /// Encode VCVT.S32/U32.F32 + VMOV as Thumb-2
7196    fn encode_thumb_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7197        let mut bytes = Vec::new();
7198
7199        let sm_num = vfp_sreg_to_num(sm)?;
7200        let (vd, d) = encode_sreg(sm_num);
7201        let (vm, m) = encode_sreg(sm_num);
7202        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
7203        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7204        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7205
7206        // VMOV Rd, Sm
7207        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
7208        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7209
7210        Ok(bytes)
7211    }
7212
7213    // === Thumb-2 32-bit encoding helpers ===
7214
7215    /// Encode Thumb-2 32-bit ADD with immediate
7216    fn encode_thumb32_add(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7217        let rd_bits = reg_to_bits(rd);
7218        let rn_bits = reg_to_bits(rn);
7219
7220        // The `i:imm3:imm8` field is split the same way for both forms.
7221        let i_bit = (imm >> 11) & 1;
7222        let imm3 = (imm >> 8) & 0x7;
7223        let imm8 = imm & 0xFF;
7224
7225        let hw1_base = if imm <= 0xFF {
7226            // ADD.W (T3): the field is a ThumbExpandImm modified immediate. For
7227            // imm <= 0xFF (i:imm3 = 0000) it is the zero-extended byte, which is
7228            // correct — keep this form so existing encodings stay bit-identical.
7229            0xF100
7230        } else if imm <= 0xFFF {
7231            // ADDW (T4): a PLAIN 12-bit immediate (0..4095) — no ThumbExpandImm.
7232            // This is what makes `add sp, sp, #frame` correct for frame sizes
7233            // >= 256, which ADD.W (T3) would silently mis-encode (e.g. #256 -> #0).
7234            0xF200
7235        } else {
7236            return Err(synth_core::Error::synthesis(
7237                "ADD immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7238            ));
7239        };
7240
7241        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7242        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7243
7244        let mut bytes = hw1.to_le_bytes().to_vec();
7245        bytes.extend_from_slice(&hw2.to_le_bytes());
7246        Ok(bytes)
7247    }
7248
7249    /// Encode Thumb-2 32-bit SUB with immediate
7250    fn encode_thumb32_sub(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7251        let rd_bits = reg_to_bits(rd);
7252        let rn_bits = reg_to_bits(rn);
7253
7254        let i_bit = (imm >> 11) & 1;
7255        let imm3 = (imm >> 8) & 0x7;
7256        let imm8 = imm & 0xFF;
7257
7258        let hw1_base = if imm <= 0xFF {
7259            // SUB.W (T3) modified immediate — correct for the zero-extended byte
7260            // (imm <= 0xFF). Kept bit-identical for existing encodings.
7261            0xF1A0
7262        } else if imm <= 0xFFF {
7263            // SUBW (T4): plain 12-bit immediate (0..4095). Makes
7264            // `sub sp, sp, #frame` correct for frame sizes >= 256.
7265            0xF2A0
7266        } else {
7267            return Err(synth_core::Error::synthesis(
7268                "SUB immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7269            ));
7270        };
7271
7272        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7273        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7274
7275        let mut bytes = hw1.to_le_bytes().to_vec();
7276        bytes.extend_from_slice(&hw2.to_le_bytes());
7277        Ok(bytes)
7278    }
7279
7280    /// Encode Thumb-2 32-bit ADDS with immediate (sets flags)
7281    fn encode_thumb32_adds(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7282        let rd_bits = reg_to_bits(rd);
7283        let rn_bits = reg_to_bits(rn);
7284
7285        // ADDS.W (flag-setting) has only the modified-immediate form — error on
7286        // an un-encodable value rather than silently add the wrong constant.
7287        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7288            synth_core::Error::synthesis(
7289                "ADDS immediate is not a valid ThumbExpandImm — materialize into a register",
7290            )
7291        })?;
7292        let i_bit = (field >> 11) & 1;
7293        let imm3 = (field >> 8) & 0x7;
7294        let imm8 = field & 0xFF;
7295
7296        // ADDS.W Rd, Rn, #imm (with S=1)
7297        // First halfword: 1111 0 i 0 1000 1 Rn = F110 | i<<10 | Rn
7298        let hw1: u16 = (0xF110 | (i_bit << 10) | rn_bits) as u16;
7299        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7300
7301        let mut bytes = hw1.to_le_bytes().to_vec();
7302        bytes.extend_from_slice(&hw2.to_le_bytes());
7303        Ok(bytes)
7304    }
7305
7306    /// Encode Thumb-2 32-bit SUBS with immediate (sets flags)
7307    fn encode_thumb32_subs(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7308        let rd_bits = reg_to_bits(rd);
7309        let rn_bits = reg_to_bits(rn);
7310
7311        // SUBS.W (flag-setting) has only the modified-immediate form — error on
7312        // an un-encodable value rather than silently subtract the wrong constant.
7313        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7314            synth_core::Error::synthesis(
7315                "SUBS immediate is not a valid ThumbExpandImm — materialize into a register",
7316            )
7317        })?;
7318        let i_bit = (field >> 11) & 1;
7319        let imm3 = (field >> 8) & 0x7;
7320        let imm8 = field & 0xFF;
7321
7322        // SUBS.W Rd, Rn, #imm (with S=1)
7323        // First halfword: 1111 0 i 0 1101 1 Rn = F1B0 | i<<10 | Rn
7324        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7325        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7326
7327        let mut bytes = hw1.to_le_bytes().to_vec();
7328        bytes.extend_from_slice(&hw2.to_le_bytes());
7329        Ok(bytes)
7330    }
7331
7332    /// Encode Thumb-2 32-bit MOVW (16-bit immediate)
7333    ///
7334    /// # Contract (Verus-style)
7335    /// ```text
7336    /// requires rd <= R14
7337    /// ensures result.len() == 4
7338    /// ensures (imm & 0xFFFF) can be reconstructed from the encoding
7339    /// ```
7340    fn encode_thumb32_movw(&self, rd: &Reg, imm: u32) -> Result<Vec<u8>> {
7341        let rd_bits = reg_to_bits(rd);
7342        reg_bits_checked(rd_bits)?;
7343        let imm16 = imm & 0xFFFF;
7344
7345        // MOVW Rd, #imm16
7346        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7347        let imm4 = (imm16 >> 12) & 0xF;
7348        let i_bit = (imm16 >> 11) & 1;
7349        let imm3 = (imm16 >> 8) & 0x7;
7350        let imm8 = imm16 & 0xFF;
7351
7352        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7353        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7354
7355        let mut bytes = hw1.to_le_bytes().to_vec();
7356        bytes.extend_from_slice(&hw2.to_le_bytes());
7357        encoding_contracts::verify_thumb32(&bytes);
7358        Ok(bytes)
7359    }
7360
7361    /// Encode Thumb-2 32-bit shift with immediate
7362    ///
7363    /// # Contract (Verus-style)
7364    /// ```text
7365    /// requires rd <= R14, rm <= R14
7366    /// ensures result.len() == 4
7367    /// ```
7368    fn encode_thumb32_shift(
7369        &self,
7370        rd: &Reg,
7371        rm: &Reg,
7372        shift: u32,
7373        shift_type: u8,
7374    ) -> Result<Vec<u8>> {
7375        let rd_bits = reg_to_bits(rd);
7376        let rm_bits = reg_to_bits(rm);
7377        reg_bits_checked(rd_bits)?;
7378        reg_bits_checked(rm_bits)?;
7379        let imm5 = shift & 0x1F;
7380        let imm2 = imm5 & 0x3;
7381        let imm3 = (imm5 >> 2) & 0x7;
7382
7383        // MOV.W Rd, Rm, <shift> #imm
7384        // EA4F 0 imm3 Rd imm2 type Rm
7385        let hw1: u16 = 0xEA4F;
7386        let hw2: u16 =
7387            ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | ((shift_type as u32) << 4) | rm_bits)
7388                as u16;
7389
7390        let mut bytes = hw1.to_le_bytes().to_vec();
7391        bytes.extend_from_slice(&hw2.to_le_bytes());
7392        Ok(bytes)
7393    }
7394
7395    /// Encode Thumb-2 32-bit shift by register
7396    /// Encoding: 11111010 0xx0 Rn | 1111 Rd 0000 Rm
7397    /// shift_type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
7398    fn encode_thumb32_shift_reg(
7399        &self,
7400        rd: &Reg,
7401        rn: &Reg,
7402        rm: &Reg,
7403        shift_type: u8,
7404    ) -> Result<Vec<u8>> {
7405        let rd_bits = reg_to_bits(rd);
7406        let rn_bits = reg_to_bits(rn);
7407        let rm_bits = reg_to_bits(rm);
7408
7409        // hw1: 1111 1010 0xx0 Rn
7410        let hw1: u16 = (0xFA00 | ((shift_type as u32) << 5) | rn_bits) as u16;
7411        // hw2: 1111 Rd 0000 Rm
7412        let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
7413
7414        let mut bytes = hw1.to_le_bytes().to_vec();
7415        bytes.extend_from_slice(&hw2.to_le_bytes());
7416        Ok(bytes)
7417    }
7418
7419    /// Encode Thumb-2 32-bit CMP with immediate
7420    fn encode_thumb32_cmp_imm(&self, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7421        let rn_bits = reg_to_bits(rn);
7422
7423        // CMP.W has only the modified-immediate form (no plain-imm12 like ADDW),
7424        // so an un-encodable immediate MUST be materialized into a register by
7425        // the selector. Error rather than silently compare the wrong constant.
7426        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7427            synth_core::Error::synthesis(
7428                "CMP immediate is not a valid ThumbExpandImm — materialize into a register",
7429            )
7430        })?;
7431        let i_bit = (field >> 11) & 1;
7432        let imm3 = (field >> 8) & 0x7;
7433        let imm8 = field & 0xFF;
7434
7435        // CMP.W Rn, #imm
7436        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7437        let hw2: u16 = ((imm3 << 12) | 0x0F00 | imm8) as u16;
7438
7439        let mut bytes = hw1.to_le_bytes().to_vec();
7440        bytes.extend_from_slice(&hw2.to_le_bytes());
7441        Ok(bytes)
7442    }
7443
7444    /// #372/#382: resolve the base register AND residual immediate offset for an
7445    /// `I64Ldr`/`I64Str` whose address may carry an index register. Returns
7446    /// `(base, low_offset)`; the caller accesses the halves at `[base,
7447    /// #low_offset]` and `[base, #low_offset + 4]`.
7448    ///
7449    /// - Frame access (no `offset_reg`, e.g. a spilled local at `[SP, #off]`):
7450    ///   returns `(addr.base, off)` and emits NOTHING — byte-identical.
7451    /// - Memory access (`reg_imm(R11, addr, offset)` = `R11 + addr + offset`)
7452    ///   with `offset + 4 <= 0xFFF`: emits `ADD.W ip, base, index` and returns
7453    ///   `(ip, offset)`, folding `offset`/`offset+4` into the halves' imm12.
7454    ///   Byte-identical to the pre-#382 (#372) behavior.
7455    /// - Memory access with `offset + 4 > 0xFFF`: the imm12 form cannot hold the
7456    ///   high half's offset, so `encode_thumb32_ldr`'s `check_ldst_imm12` (#259)
7457    ///   rightly refused it and the WHOLE function was skipped (#382). Instead
7458    ///   MATERIALIZE the offset into the base: `ADD ip, index, #offset` (against
7459    ///   the read-only INDEX register, so `encode_thumb32_add_imm` never trips its
7460    ///   `rd==rn==R12` alias trap), then `ADD.W ip, ip, base` (+ R11), and return
7461    ///   `(ip, 0)` so the halves use `[ip, #0]` / `[ip, #4]`.
7462    ///
7463    /// The effective address is fully materialized into `ip` BEFORE the halves
7464    /// are accessed, so an `rdlo` aliasing the index register is safe.
7465    fn i64_effective_base(&self, bytes: &mut Vec<u8>, addr: &MemAddr) -> Result<(Reg, u32)> {
7466        let offset = if addr.offset < 0 {
7467            0u32
7468        } else {
7469            addr.offset as u32
7470        };
7471        match addr.offset_reg {
7472            Some(idx) => {
7473                let ip = Reg::R12;
7474                if offset.wrapping_add(4) > 0xFFF {
7475                    // Large static offset (#382): fold it (and R11) into ip so the
7476                    // imm12 halves stay in range instead of skipping the function.
7477                    // ADD ip, index, #offset  (index != ip → no add_imm alias trap)
7478                    bytes.extend_from_slice(&self.encode_thumb32_add_imm(&ip, &idx, offset)?);
7479                    // ADD.W ip, ip, base  (+ R11)
7480                    bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
7481                        reg_to_bits(&ip),
7482                        reg_to_bits(&ip),
7483                        reg_to_bits(&addr.base),
7484                    )?);
7485                    Ok((ip, 0))
7486                } else {
7487                    // ADD.W ip, addr.base, idx  (Thumb-2, byte-verified vs as)
7488                    let hw1: u16 = 0xEB00 | reg_to_bits(&addr.base) as u16;
7489                    let hw2: u16 = 0x0C00 | reg_to_bits(&idx) as u16;
7490                    bytes.extend_from_slice(&hw1.to_le_bytes());
7491                    bytes.extend_from_slice(&hw2.to_le_bytes());
7492                    Ok((ip, offset))
7493                }
7494            }
7495            None => Ok((addr.base, offset)),
7496        }
7497    }
7498
7499    /// Encode Thumb-2 32-bit LDR
7500    fn encode_thumb32_ldr(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7501        let rd_bits = reg_to_bits(rd);
7502        let base_bits = reg_to_bits(base);
7503
7504        // LDR.W Rd, [Rn, #imm12]
7505        check_ldst_imm12(offset)?;
7506        let hw1: u16 = (0xF8D0 | base_bits) as u16;
7507        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7508
7509        let mut bytes = hw1.to_le_bytes().to_vec();
7510        bytes.extend_from_slice(&hw2.to_le_bytes());
7511        Ok(bytes)
7512    }
7513
7514    /// Encode Thumb-2 32-bit STR
7515    fn encode_thumb32_str(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7516        let rd_bits = reg_to_bits(rd);
7517        let base_bits = reg_to_bits(base);
7518
7519        // STR.W Rd, [Rn, #imm12]
7520        check_ldst_imm12(offset)?;
7521        let hw1: u16 = (0xF8C0 | base_bits) as u16;
7522        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7523
7524        let mut bytes = hw1.to_le_bytes().to_vec();
7525        bytes.extend_from_slice(&hw2.to_le_bytes());
7526        Ok(bytes)
7527    }
7528
7529    /// Encode Thumb-2 32-bit LDR with register offset: LDR.W Rd, [Rn, Rm]
7530    fn encode_thumb32_ldr_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7531        let rd_bits = reg_to_bits(rd);
7532        let base_bits = reg_to_bits(base);
7533        let rm_bits = reg_to_bits(offset_reg);
7534
7535        // LDR.W Rd, [Rn, Rm, LSL #0]
7536        // Encoding: 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
7537        // imm2 = 00 for no shift (LSL #0)
7538        let hw1: u16 = (0xF850 | base_bits) as u16;
7539        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7540
7541        let mut bytes = hw1.to_le_bytes().to_vec();
7542        bytes.extend_from_slice(&hw2.to_le_bytes());
7543        Ok(bytes)
7544    }
7545
7546    /// Encode Thumb-2 32-bit STR with register offset: STR.W Rd, [Rn, Rm]
7547    fn encode_thumb32_str_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7548        let rd_bits = reg_to_bits(rd);
7549        let base_bits = reg_to_bits(base);
7550        let rm_bits = reg_to_bits(offset_reg);
7551
7552        // STR.W Rd, [Rn, Rm, LSL #0]
7553        // Encoding: 1111 1000 0100 Rn | Rt 0000 00 imm2 Rm
7554        // imm2 = 00 for no shift (LSL #0)
7555        let hw1: u16 = (0xF840 | base_bits) as u16;
7556        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7557
7558        let mut bytes = hw1.to_le_bytes().to_vec();
7559        bytes.extend_from_slice(&hw2.to_le_bytes());
7560        Ok(bytes)
7561    }
7562
7563    // === Sub-word load/store Thumb-2 encoding helpers ===
7564
7565    /// Encode Thumb-2 32-bit LDRB with immediate: LDRB.W Rd, [Rn, #imm12]
7566    fn encode_thumb32_ldrb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7567        let rd_bits = reg_to_bits(rd);
7568        let base_bits = reg_to_bits(base);
7569        // LDRB.W Rd, [Rn, #imm12]: 1111 1000 1001 Rn | Rt imm12
7570        check_ldst_imm12(offset)?;
7571        let hw1: u16 = (0xF890 | base_bits) as u16;
7572        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7573        let mut bytes = hw1.to_le_bytes().to_vec();
7574        bytes.extend_from_slice(&hw2.to_le_bytes());
7575        Ok(bytes)
7576    }
7577
7578    /// Encode Thumb-2 32-bit LDRB with register: LDRB.W Rd, [Rn, Rm]
7579    fn encode_thumb32_ldrb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7580        let rd_bits = reg_to_bits(rd);
7581        let base_bits = reg_to_bits(base);
7582        let rm_bits = reg_to_bits(offset_reg);
7583        // LDRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0001 Rn | Rt 0000 00 imm2 Rm
7584        let hw1: u16 = (0xF810 | base_bits) as u16;
7585        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7586        let mut bytes = hw1.to_le_bytes().to_vec();
7587        bytes.extend_from_slice(&hw2.to_le_bytes());
7588        Ok(bytes)
7589    }
7590
7591    /// Encode Thumb-2 32-bit LDRSB with immediate: LDRSB.W Rd, [Rn, #imm12]
7592    fn encode_thumb32_ldrsb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7593        let rd_bits = reg_to_bits(rd);
7594        let base_bits = reg_to_bits(base);
7595        // LDRSB.W Rd, [Rn, #imm12]: 1111 1001 1001 Rn | Rt imm12
7596        check_ldst_imm12(offset)?;
7597        let hw1: u16 = (0xF990 | base_bits) as u16;
7598        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7599        let mut bytes = hw1.to_le_bytes().to_vec();
7600        bytes.extend_from_slice(&hw2.to_le_bytes());
7601        Ok(bytes)
7602    }
7603
7604    /// Encode Thumb-2 32-bit LDRSB with register: LDRSB.W Rd, [Rn, Rm]
7605    fn encode_thumb32_ldrsb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7606        let rd_bits = reg_to_bits(rd);
7607        let base_bits = reg_to_bits(base);
7608        let rm_bits = reg_to_bits(offset_reg);
7609        // LDRSB.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0001 Rn | Rt 0000 00 imm2 Rm
7610        let hw1: u16 = (0xF910 | base_bits) as u16;
7611        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7612        let mut bytes = hw1.to_le_bytes().to_vec();
7613        bytes.extend_from_slice(&hw2.to_le_bytes());
7614        Ok(bytes)
7615    }
7616
7617    /// Encode Thumb-2 32-bit LDRH with immediate: LDRH.W Rd, [Rn, #imm12]
7618    fn encode_thumb32_ldrh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7619        let rd_bits = reg_to_bits(rd);
7620        let base_bits = reg_to_bits(base);
7621        // LDRH.W Rd, [Rn, #imm12]: 1111 1000 1011 Rn | Rt imm12
7622        check_ldst_imm12(offset)?;
7623        let hw1: u16 = (0xF8B0 | base_bits) as u16;
7624        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7625        let mut bytes = hw1.to_le_bytes().to_vec();
7626        bytes.extend_from_slice(&hw2.to_le_bytes());
7627        Ok(bytes)
7628    }
7629
7630    /// Encode Thumb-2 32-bit LDRH with register: LDRH.W Rd, [Rn, Rm]
7631    fn encode_thumb32_ldrh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7632        let rd_bits = reg_to_bits(rd);
7633        let base_bits = reg_to_bits(base);
7634        let rm_bits = reg_to_bits(offset_reg);
7635        // LDRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0011 Rn | Rt 0000 00 imm2 Rm
7636        let hw1: u16 = (0xF830 | base_bits) as u16;
7637        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7638        let mut bytes = hw1.to_le_bytes().to_vec();
7639        bytes.extend_from_slice(&hw2.to_le_bytes());
7640        Ok(bytes)
7641    }
7642
7643    /// Encode Thumb-2 32-bit LDRSH with immediate: LDRSH.W Rd, [Rn, #imm12]
7644    fn encode_thumb32_ldrsh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7645        let rd_bits = reg_to_bits(rd);
7646        let base_bits = reg_to_bits(base);
7647        // LDRSH.W Rd, [Rn, #imm12]: 1111 1001 1011 Rn | Rt imm12
7648        check_ldst_imm12(offset)?;
7649        let hw1: u16 = (0xF9B0 | base_bits) as u16;
7650        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7651        let mut bytes = hw1.to_le_bytes().to_vec();
7652        bytes.extend_from_slice(&hw2.to_le_bytes());
7653        Ok(bytes)
7654    }
7655
7656    /// Encode Thumb-2 32-bit LDRSH with register: LDRSH.W Rd, [Rn, Rm]
7657    fn encode_thumb32_ldrsh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7658        let rd_bits = reg_to_bits(rd);
7659        let base_bits = reg_to_bits(base);
7660        let rm_bits = reg_to_bits(offset_reg);
7661        // LDRSH.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0011 Rn | Rt 0000 00 imm2 Rm
7662        let hw1: u16 = (0xF930 | base_bits) as u16;
7663        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7664        let mut bytes = hw1.to_le_bytes().to_vec();
7665        bytes.extend_from_slice(&hw2.to_le_bytes());
7666        Ok(bytes)
7667    }
7668
7669    /// Encode Thumb-2 32-bit STRB with immediate: STRB.W Rd, [Rn, #imm12]
7670    fn encode_thumb32_strb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7671        let rd_bits = reg_to_bits(rd);
7672        let base_bits = reg_to_bits(base);
7673        // STRB.W Rd, [Rn, #imm12]: 1111 1000 1000 Rn | Rt imm12
7674        check_ldst_imm12(offset)?;
7675        let hw1: u16 = (0xF880 | base_bits) as u16;
7676        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7677        let mut bytes = hw1.to_le_bytes().to_vec();
7678        bytes.extend_from_slice(&hw2.to_le_bytes());
7679        Ok(bytes)
7680    }
7681
7682    /// Encode Thumb-2 32-bit STRB with register: STRB.W Rd, [Rn, Rm]
7683    fn encode_thumb32_strb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7684        let rd_bits = reg_to_bits(rd);
7685        let base_bits = reg_to_bits(base);
7686        let rm_bits = reg_to_bits(offset_reg);
7687        // STRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0000 Rn | Rt 0000 00 imm2 Rm
7688        let hw1: u16 = (0xF800 | base_bits) as u16;
7689        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7690        let mut bytes = hw1.to_le_bytes().to_vec();
7691        bytes.extend_from_slice(&hw2.to_le_bytes());
7692        Ok(bytes)
7693    }
7694
7695    /// Encode Thumb-2 32-bit STRH with immediate: STRH.W Rd, [Rn, #imm12]
7696    fn encode_thumb32_strh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7697        let rd_bits = reg_to_bits(rd);
7698        let base_bits = reg_to_bits(base);
7699        // STRH.W Rd, [Rn, #imm12]: 1111 1000 1010 Rn | Rt imm12
7700        check_ldst_imm12(offset)?;
7701        let hw1: u16 = (0xF8A0 | base_bits) as u16;
7702        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7703        let mut bytes = hw1.to_le_bytes().to_vec();
7704        bytes.extend_from_slice(&hw2.to_le_bytes());
7705        Ok(bytes)
7706    }
7707
7708    /// Encode Thumb-2 32-bit STRH with register: STRH.W Rd, [Rn, Rm]
7709    fn encode_thumb32_strh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7710        let rd_bits = reg_to_bits(rd);
7711        let base_bits = reg_to_bits(base);
7712        let rm_bits = reg_to_bits(offset_reg);
7713        // STRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0010 Rn | Rt 0000 00 imm2 Rm
7714        let hw1: u16 = (0xF820 | base_bits) as u16;
7715        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7716        let mut bytes = hw1.to_le_bytes().to_vec();
7717        bytes.extend_from_slice(&hw2.to_le_bytes());
7718        Ok(bytes)
7719    }
7720
7721    /// Encode Thumb-2 32-bit ADD with immediate: ADD.W Rd, Rn, #imm
7722    fn encode_thumb32_add_imm(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7723        let rd_bits = reg_to_bits(rd);
7724        let rn_bits = reg_to_bits(rn);
7725
7726        // For small immediates, use ADD.W Rd, Rn, #imm12
7727        // Encoding: 1111 0 i 0 1 0 0 0 S Rn | 0 imm3 Rd imm8
7728        // S = 0 (don't update flags)
7729        // The 12-bit immediate is encoded as: i:imm3:imm8
7730        // For simplicity, we only support imm <= 0xFFF (direct encoding)
7731        if imm <= 0xFFF {
7732            let i_bit = (imm >> 11) & 1;
7733            let imm3 = (imm >> 8) & 0x7;
7734            let imm8 = imm & 0xFF;
7735
7736            let hw1: u16 = (0xF100 | (i_bit << 10) | rn_bits) as u16;
7737            let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7738
7739            let mut bytes = hw1.to_le_bytes().to_vec();
7740            bytes.extend_from_slice(&hw2.to_le_bytes());
7741            Ok(bytes)
7742        } else {
7743            // Out-of-range immediate (> 0xFFF): materialize it into a scratch
7744            // register, then ADD.W Rd, Rn, scratch. This is the #180/#185
7745            // "encoder must produce a legal sequence, not assert" class — see #350.
7746            //
7747            // Scratch choice (must NEVER equal Rn, or Rn would be clobbered before
7748            // the ADD reads it):
7749            //   - rd != rn  => use rd itself (rn is untouched, since rd != rn).
7750            //   - rd == rn  => use R12/IP (the reserved encoder scratch). rd/rn are
7751            //                  never R12 (R12 is non-allocatable), so it can't alias.
7752            //
7753            // The materialized value is the same whether or not MOVT is emitted, so
7754            // the byte length depends only on `imm` (and rd==rn) — the size probe and
7755            // the final emit therefore agree (mandatory: the function is encoded twice).
7756            let scratch: u32 = if rd_bits == rn_bits {
7757                12 // R12/IP — in-place add, can't use rd because rd == rn
7758            } else {
7759                rd_bits // rn is preserved because rd != rn
7760            };
7761            // Invariant: the scratch must never alias Rn (would clobber it before
7762            // the ADD reads it). Unreachable in real codegen (rd/rn are never R12,
7763            // which is reserved encoder scratch), but the encoder is also driven by
7764            // the `encoder_no_panic` fuzz harness with ARBITRARY registers — incl.
7765            // rd==rn==R12, which makes scratch (R12) alias Rn. The encoder contract
7766            // (#180/#185) is Ok-or-Err, never a panic, so return a typed error
7767            // instead of asserting. #350 follow-up.
7768            if scratch == rn_bits {
7769                return Err(synth_core::Error::synthesis(format!(
7770                    "ADD #imm: cannot lower #{imm:#x} for Rd==Rn==R12 — no free scratch \
7771                     register (R12 is the reserved encoder scratch and aliases Rn here)"
7772                )));
7773            }
7774
7775            let lo16 = imm & 0xFFFF;
7776            let hi16 = (imm >> 16) & 0xFFFF;
7777
7778            let mut bytes = self.encode_thumb32_movw_raw(scratch, lo16)?;
7779            if hi16 != 0 {
7780                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(scratch, hi16)?);
7781            }
7782            bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rn_bits, scratch)?);
7783            Ok(bytes)
7784        }
7785    }
7786
7787    // === Raw encoding helpers for POPCNT (take register numbers directly) ===
7788
7789    /// Encode Thumb-2 32-bit MOVW (16-bit immediate) - raw version
7790    ///
7791    /// # Contract (Verus-style)
7792    /// ```text
7793    /// requires rd <= 14, imm16 <= 0xFFFF
7794    /// ensures result.len() == 4
7795    /// ```
7796    fn encode_thumb32_movw_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
7797        reg_bits_checked(rd)?;
7798        encoding_contracts::verify_imm16(imm16);
7799        // MOVW Rd, #imm16
7800        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7801        let imm16 = imm16 & 0xFFFF;
7802        let imm4 = (imm16 >> 12) & 0xF;
7803        let i_bit = (imm16 >> 11) & 1;
7804        let imm3 = (imm16 >> 8) & 0x7;
7805        let imm8 = imm16 & 0xFF;
7806
7807        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7808        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
7809
7810        let mut bytes = hw1.to_le_bytes().to_vec();
7811        bytes.extend_from_slice(&hw2.to_le_bytes());
7812        encoding_contracts::verify_thumb32(&bytes);
7813        Ok(bytes)
7814    }
7815
7816    /// Encode Thumb-2 32-bit MOVT (move top 16 bits) - raw version
7817    ///
7818    /// # Contract (Verus-style)
7819    /// ```text
7820    /// requires rd <= 14, imm16 <= 0xFFFF
7821    /// ensures result.len() == 4
7822    /// ```
7823    fn encode_thumb32_movt_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
7824        reg_bits_checked(rd)?;
7825        encoding_contracts::verify_imm16(imm16);
7826        // MOVT Rd, #imm16
7827        // 1111 0 i 10 1 1 0 0 imm4 | 0 imm3 Rd imm8
7828        let imm16 = imm16 & 0xFFFF;
7829        let imm4 = (imm16 >> 12) & 0xF;
7830        let i_bit = (imm16 >> 11) & 1;
7831        let imm3 = (imm16 >> 8) & 0x7;
7832        let imm8 = imm16 & 0xFF;
7833
7834        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
7835        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
7836
7837        let mut bytes = hw1.to_le_bytes().to_vec();
7838        bytes.extend_from_slice(&hw2.to_le_bytes());
7839        encoding_contracts::verify_thumb32(&bytes);
7840        Ok(bytes)
7841    }
7842
7843    /// Encode Thumb-2 32-bit LSR (logical shift right) with immediate - raw version
7844    fn encode_thumb32_lsr_raw(&self, rd: u32, rm: u32, shift: u32) -> Result<Vec<u8>> {
7845        // MOV.W Rd, Rm, LSR #imm
7846        // EA4F 0 imm3 Rd imm2 01 Rm
7847        let imm5 = shift & 0x1F;
7848        let imm2 = imm5 & 0x3;
7849        let imm3 = (imm5 >> 2) & 0x7;
7850
7851        let hw1: u16 = 0xEA4F;
7852        let hw2: u16 = ((imm3 << 12) | (rd << 8) | (imm2 << 6) | (0b01 << 4) | rm) as u16;
7853
7854        let mut bytes = hw1.to_le_bytes().to_vec();
7855        bytes.extend_from_slice(&hw2.to_le_bytes());
7856        Ok(bytes)
7857    }
7858
7859    /// Encode Thumb-2 32-bit AND (register) - raw version
7860    fn encode_thumb32_and_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7861        // AND.W Rd, Rn, Rm
7862        // EA00 Rn | 0 Rd 00 00 Rm
7863        let hw1: u16 = (0xEA00 | rn) as u16;
7864        let hw2: u16 = ((rd << 8) | rm) as u16;
7865
7866        let mut bytes = hw1.to_le_bytes().to_vec();
7867        bytes.extend_from_slice(&hw2.to_le_bytes());
7868        Ok(bytes)
7869    }
7870
7871    /// Encode Thumb-2 32-bit AND with immediate - raw version
7872    fn encode_thumb32_and_imm_raw(&self, rd: u32, rn: u32, imm: u32) -> Result<Vec<u8>> {
7873        // AND.W Rd, Rn, #<modified_immediate>
7874        // For small immediates (0-255), the encoding is simpler
7875        // F0 00 Rn | 0 imm3 Rd imm8
7876        let i_bit = (imm >> 11) & 1;
7877        let imm3 = (imm >> 8) & 0x7;
7878        let imm8 = imm & 0xFF;
7879
7880        let hw1: u16 = (0xF000 | (i_bit << 10) | rn) as u16;
7881        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
7882
7883        let mut bytes = hw1.to_le_bytes().to_vec();
7884        bytes.extend_from_slice(&hw2.to_le_bytes());
7885        Ok(bytes)
7886    }
7887
7888    /// Encode Thumb-2 32-bit SUB (register) - raw version
7889    fn encode_thumb32_sub_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7890        // SUB.W Rd, Rn, Rm
7891        // EBA0 Rn | 0 Rd 00 00 Rm
7892        let hw1: u16 = (0xEBA0 | rn) as u16;
7893        let hw2: u16 = ((rd << 8) | rm) as u16;
7894
7895        let mut bytes = hw1.to_le_bytes().to_vec();
7896        bytes.extend_from_slice(&hw2.to_le_bytes());
7897        Ok(bytes)
7898    }
7899
7900    /// Encode Thumb-2 32-bit ADD (register) - raw version
7901    fn encode_thumb32_add_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7902        // ADD.W Rd, Rn, Rm
7903        // EB00 Rn | 0 Rd 00 00 Rm
7904        let hw1: u16 = (0xEB00 | rn) as u16;
7905        let hw2: u16 = ((rd << 8) | rm) as u16;
7906
7907        let mut bytes = hw1.to_le_bytes().to_vec();
7908        bytes.extend_from_slice(&hw2.to_le_bytes());
7909        Ok(bytes)
7910    }
7911
7912    /// Encode Thumb-2 32-bit ADDS (register, flag-setting) - raw version.
7913    /// Used as the high-register fallback for `ArmOp::Adds` (i64 low-word add)
7914    /// so R8-R11 pair operands don't overflow the 16-bit field — #178/#180.
7915    fn encode_thumb32_adds_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7916        // ADDS.W Rd, Rn, Rm (T3, S=1): EB10 Rn | 0 Rd 00 00 Rm
7917        let hw1: u16 = (0xEB10 | rn) as u16;
7918        let hw2: u16 = ((rd << 8) | rm) as u16;
7919        let mut bytes = hw1.to_le_bytes().to_vec();
7920        bytes.extend_from_slice(&hw2.to_le_bytes());
7921        Ok(bytes)
7922    }
7923
7924    /// Encode Thumb-2 32-bit SUBS (register, flag-setting) - raw version.
7925    /// High-register fallback for `ArmOp::Subs` (i64 low-word subtract) — #178/#180.
7926    fn encode_thumb32_subs_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7927        // SUBS.W Rd, Rn, Rm (T3, S=1): EBB0 Rn | 0 Rd 00 00 Rm
7928        let hw1: u16 = (0xEBB0 | rn) as u16;
7929        let hw2: u16 = ((rd << 8) | rm) as u16;
7930        let mut bytes = hw1.to_le_bytes().to_vec();
7931        bytes.extend_from_slice(&hw2.to_le_bytes());
7932        Ok(bytes)
7933    }
7934
7935    /// Encode a sequence of ARM instructions
7936    pub fn encode_sequence(&self, ops: &[ArmOp]) -> Result<Vec<u8>> {
7937        let mut code = Vec::new();
7938
7939        for op in ops {
7940            let encoded = self.encode(op)?;
7941            code.extend_from_slice(&encoded);
7942        }
7943
7944        Ok(code)
7945    }
7946}
7947
7948/// Convert register to bit encoding (0-15)
7949/// Reverse of the ARMv7-M `ThumbExpandImm`: given a 32-bit immediate, return the
7950/// 12-bit `i:imm3:imm8` field if it is a representable modified immediate, else
7951/// `None` (the caller must materialize the value into a register). This is the
7952/// shared correct path for the data-processing immediate encoders — without it
7953/// they pack raw bits and silently mis-encode any value `> 0xFF` that isn't a
7954/// modified immediate (the silent-miscompile class behind #251/#253/#255).
7955fn try_thumb_expand_imm(value: u32) -> Option<u32> {
7956    // i:imm3 = 0000 → 8-bit value, zero-extended (00000000 00000000 00000000 XY).
7957    if value <= 0xFF {
7958        return Some(value);
7959    }
7960    let b0 = value & 0xFF; // byte 0
7961    let b1 = (value >> 8) & 0xFF; // byte 1
7962    // 0x00XY00XY (i:imm3 = 0001) — XY in bytes 0 and 2
7963    if value == (b0 << 16) | b0 {
7964        return Some(0x100 | b0);
7965    }
7966    // 0xXY00XY00 (i:imm3 = 0010) — XY in bytes 1 and 3
7967    if value == (b1 << 24) | (b1 << 8) {
7968        return Some(0x200 | b1);
7969    }
7970    // 0xXYXYXYXY (i:imm3 = 0011) — XY in all four bytes
7971    if value == (b0 << 24) | (b0 << 16) | (b0 << 8) | b0 {
7972        return Some(0x300 | b0);
7973    }
7974    // An 8-bit value with bit 7 set, rotated right by 8..=31. `rotate_left(rot)`
7975    // undoes the encoded right rotation; if the result is `1bbbbbbb` (0x80..=0xFF)
7976    // the value is representable. imm12[11:7] = rot, imm12[6:0] = low 7 bits.
7977    for rot in 8..=31u32 {
7978        let unrot = value.rotate_left(rot);
7979        if (0x80..=0xFF).contains(&unrot) {
7980            return Some((rot << 7) | (unrot & 0x7F));
7981        }
7982    }
7983    None
7984}
7985
7986/// Guard a Thumb-2 `LDR/STR Rd, [Rn, #imm12]` offset. The imm12 form supports
7987/// `0..=4095`; a larger offset must be materialized into a register by the
7988/// selector (register-offset addressing). Returning `Err` rather than silently
7989/// masking `offset & 0xFFF` closes the wrong-address miscompile class (#259,
7990/// the load/store sibling of #253/#255).
7991fn check_ldst_imm12(offset: u32) -> Result<()> {
7992    if offset > 0xFFF {
7993        Err(synth_core::Error::synthesis(
7994            "load/store immediate offset > 0xFFF (4095) — materialize the offset into a register",
7995        ))
7996    } else {
7997        Ok(())
7998    }
7999}
8000
8001fn reg_to_bits(reg: &Reg) -> u32 {
8002    match reg {
8003        Reg::R0 => 0,
8004        Reg::R1 => 1,
8005        Reg::R2 => 2,
8006        Reg::R3 => 3,
8007        Reg::R4 => 4,
8008        Reg::R5 => 5,
8009        Reg::R6 => 6,
8010        Reg::R7 => 7,
8011        Reg::R8 => 8,
8012        Reg::R9 => 9,
8013        Reg::R10 => 10,
8014        Reg::R11 => 11,
8015        Reg::R12 => 12,
8016        Reg::SP => 13,
8017        Reg::LR => 14,
8018        Reg::PC => 15,
8019    }
8020}
8021
8022// ======================================================================
8023// #610 — i64 fixed-ABI expansion wrappers.
8024//
8025// The hand-written multi-instruction i64 cores (rotl/rotr and the div/rem
8026// shift-subtract loops) compute in FIXED low registers. Before #610 the
8027// div/rem arms ignored their operand fields outright (hardcoded R0:R1 /
8028// R2:R3 in, result to R0:R1) and the rot arms used R3/R4 scratch that
8029// collided with selector-assigned registers — then restored the saved
8030// scratch OVER the result (`POP {R4}` with rd_lo == R4), so the op
8031// returned the caller's stale register: 0 for every input under qemu.
8032//
8033// These wrappers make each core honor its register parameters:
8034//   1. save R0-R3,
8035//   2. marshal the operand registers into the core's fixed input regs via
8036//      the stack (permutation-safe: every source is read before any fixed
8037//      register is written),
8038//   3. run the fixed-reg core (self-preserving for R4+; R12 is encoder
8039//      scratch and never allocatable, #212),
8040//   4. MOV the result pair from R0:R1 into the selector's rd pair,
8041//   5. restore R0-R3, skipping any register the result now occupies.
8042//
8043// All emitted lengths are register-independent so the optimized path's
8044// byte-size estimator (`estimate_arm_byte_size`, pinned by the
8045// estimator↔encoder agreement oracle #498/#511) stays a constant per op.
8046// ======================================================================
8047
8048/// Steps 1+2: `PUSH {R0-R3}`, then marshal `srcs` (operand registers, any of
8049/// R0-R12) into `R0..R<n>` via individual stack pushes. Sources are all read
8050/// before any destination register is written, so arbitrary source/target
8051/// permutations (including operands living in R0-R3) are safe.
8052fn emit_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8053    debug_assert!(srcs.len() <= 4);
8054    // PUSH {R0-R3} — save the caller-visible low registers.
8055    bytes.extend_from_slice(&0xB40Fu16.to_le_bytes());
8056    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8057    for src in srcs.iter().rev() {
8058        let rt = reg_to_bits(src) as u16;
8059        bytes.extend_from_slice(&0xF84Du16.to_le_bytes());
8060        bytes.extend_from_slice(&((rt << 12) | 0x0D04).to_le_bytes());
8061    }
8062    // POP {Ri} — Ri := srcs[i].
8063    for i in 0..srcs.len() as u16 {
8064        bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes());
8065    }
8066}
8067
8068/// Steps 4+5: move the core's R0:R1 result into the selector's rd pair, then
8069/// restore the R0-R3 saved by [`emit_i64_fixed_abi_entry`], skipping any
8070/// register the result now lives in (its saved caller word is discarded).
8071fn emit_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8072    let lo = reg_to_bits(rdlo);
8073    let hi = reg_to_bits(rdhi);
8074    if lo == 1 && hi == 0 {
8075        // A fully swapped pair would clobber one half in either MOV order.
8076        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8077        return Err(synth_core::Error::synthesis(
8078            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8079        ));
8080    }
8081    let mov16 = |bytes: &mut Vec<u8>, rd: u32, rm: u32| {
8082        let d = ((rd >> 3) & 1) as u16;
8083        bytes.extend_from_slice(
8084            &(0x4600u16 | (d << 7) | ((rm as u16) << 3) | ((rd & 7) as u16)).to_le_bytes(),
8085        );
8086    };
8087    if hi == 0 {
8088        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8089        mov16(bytes, lo, 0);
8090        mov16(bytes, hi, 1);
8091    } else {
8092        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8093        mov16(bytes, hi, 1);
8094        mov16(bytes, lo, 0);
8095    }
8096    for i in 0..4u32 {
8097        if i == lo || i == hi {
8098            // The result lives here — drop the saved caller word.
8099            bytes.extend_from_slice(&0xB001u16.to_le_bytes()); // ADD SP, #4
8100        } else {
8101            bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes()); // POP {Ri}
8102        }
8103    }
8104    Ok(())
8105}
8106
8107/// WASM `i64.div_*` / `i64.rem_*` by zero must trap, matching the i32 path's
8108/// cmp/bne/udf guard. Emitted after marshaling, when the divisor pair is in
8109/// R2:R3: `ORRS R12, R2, R3` — `BNE` over a `UDF #0` when nonzero.
8110fn emit_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8111    bytes.extend_from_slice(&0xEA52u16.to_le_bytes()); // ORRS.W R12, R2, R3
8112    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8113    bytes.extend_from_slice(&0xD100u16.to_le_bytes()); // BNE.N +0 (skip the UDF)
8114    bytes.extend_from_slice(&0xDE00u16.to_le_bytes()); // UDF #0 — divide by zero
8115}
8116
8117/// WASM `i64.div_s(INT64_MIN, -1)` must trap (Core §4.3.2 `idiv_s`: the
8118/// quotient +2^63 is unrepresentable), matching the i32 path's overflow
8119/// guard — #633: without it the core negated INT64_MIN onto itself and
8120/// silently returned INT64_MIN. Emitted after marshaling, when the dividend
8121/// pair is in R0:R1 and the divisor pair in R2:R3; R12 is encoder scratch.
8122///
8123/// div_s ONLY — `i64.rem_s(INT64_MIN, -1)` is defined as 0 and must NOT
8124/// trap (`irem_s`), so the I64RemS arm never calls this. 22 bytes,
8125/// register-independent (estimator contract, #498/#511).
8126fn emit_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8127    // AND.W R12, R2, R3 — R12 == 0xFFFFFFFF iff divisor == -1
8128    bytes.extend_from_slice(&0xEA02u16.to_le_bytes());
8129    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8130    // CMN.W R12, #1 — EQ iff both divisor words are all-ones
8131    bytes.extend_from_slice(&0xF11Cu16.to_le_bytes());
8132    bytes.extend_from_slice(&0x0F01u16.to_le_bytes());
8133    // BNE .no_trap
8134    bytes.extend_from_slice(&0xD105u16.to_le_bytes());
8135    // CMP R0, #0 — dividend lo word of INT64_MIN
8136    bytes.extend_from_slice(&0x2800u16.to_le_bytes());
8137    // BNE .no_trap
8138    bytes.extend_from_slice(&0xD103u16.to_le_bytes());
8139    // CMP.W R1, #0x80000000 — dividend hi word of INT64_MIN
8140    bytes.extend_from_slice(&0xF1B1u16.to_le_bytes());
8141    bytes.extend_from_slice(&0x4F00u16.to_le_bytes());
8142    // BNE .no_trap
8143    bytes.extend_from_slice(&0xD100u16.to_le_bytes());
8144    // UDF #0 — signed-division overflow
8145    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
8146    // .no_trap:
8147}
8148
8149// ======================================================================
8150// #615 — A32 (ARM-mode) twins of the #610 i64 fixed-ABI wrappers above.
8151// Identical register contract, A32 encodings: the multi-instruction i64
8152// cores (rotl/rotr, div/rem) compute in fixed low registers (value/dividend
8153// R0:R1, amount R2 / divisor R2:R3, result to R0:R1); the wrappers marshal
8154// the selector-assigned operand registers in and the result out, saving and
8155// restoring the caller-visible R0-R3 around the core.
8156// ======================================================================
8157
8158/// A32 steps 1+2: `STMDB SP!, {R0-R3}`, then marshal `srcs` into `R0..R<n>`
8159/// via individual stack pushes (`STR src, [SP, #-4]!` in reverse order, then
8160/// `LDR Ri, [SP], #4`). Every source is read before any fixed register is
8161/// written, so arbitrary source/target permutations are safe.
8162fn emit_a32_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8163    debug_assert!(srcs.len() <= 4);
8164    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8165    // PUSH {R0-R3} — save the caller-visible low registers.
8166    w(bytes, 0xE92D_000F);
8167    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8168    for src in srcs.iter().rev() {
8169        w(bytes, 0xE52D_0004 | (reg_to_bits(src) << 12));
8170    }
8171    // LDR Ri, [SP], #4 — Ri := srcs[i].
8172    for i in 0..srcs.len() as u32 {
8173        w(bytes, 0xE49D_0004 | (i << 12));
8174    }
8175}
8176
8177/// A32 steps 4+5: move the core's R0:R1 result into the selector's rd pair,
8178/// then restore the R0-R3 saved by [`emit_a32_i64_fixed_abi_entry`], skipping
8179/// any register the result now lives in (its saved caller word is discarded).
8180fn emit_a32_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8181    let lo = reg_to_bits(rdlo);
8182    let hi = reg_to_bits(rdhi);
8183    if lo == 1 && hi == 0 {
8184        // A fully swapped pair would clobber one half in either MOV order.
8185        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8186        return Err(synth_core::Error::synthesis(
8187            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8188        ));
8189    }
8190    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8191    let mov = |bytes: &mut Vec<u8>, rd: u32, rm: u32| w(bytes, 0xE1A0_0000 | (rd << 12) | rm);
8192    if hi == 0 {
8193        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8194        mov(bytes, lo, 0);
8195        mov(bytes, hi, 1);
8196    } else {
8197        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8198        mov(bytes, hi, 1);
8199        mov(bytes, lo, 0);
8200    }
8201    for i in 0..4u32 {
8202        if i == lo || i == hi {
8203            // The result lives here — drop the saved caller word.
8204            w(bytes, 0xE28D_D004); // ADD SP, SP, #4
8205        } else {
8206            w(bytes, 0xE49D_0004 | (i << 12)); // LDR Ri, [SP], #4
8207        }
8208    }
8209    Ok(())
8210}
8211
8212/// A32 zero-divisor trap, emitted after marshaling when the divisor pair is
8213/// in R2:R3: `ORRS R12, R2, R3` sets Z iff the divisor is zero; `BNE` skips a
8214/// `UDF #0` (WASM div/rem-by-zero must trap, matching the Thumb-2 twin).
8215fn emit_a32_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8216    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8217    w(bytes, 0xE192_C003); // ORRS R12, R2, R3
8218    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8219    w(bytes, 0xE7F0_00F0); // UDF #0 — divide by zero
8220}
8221
8222/// A32 twin of [`emit_i64_divs_overflow_trap`] (#633): trap on
8223/// `i64.div_s(INT64_MIN, -1)`. Conditional execution replaces the Thumb
8224/// branches — the CMPEQ chain leaves EQ set only when divisor == -1 AND
8225/// dividend == INT64_MIN. div_s only; rem_s must keep returning 0.
8226fn emit_a32_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8227    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8228    w(bytes, 0xE002_C003); // AND   R12, R2, R3 (== 0xFFFFFFFF iff divisor == -1)
8229    w(bytes, 0xE37C_0001); // CMN   R12, #1     (EQ iff divisor == -1)
8230    w(bytes, 0x0350_0000); // CMPEQ R0, #0      (EQ iff also dividend lo == 0)
8231    w(bytes, 0x0351_0102); // CMPEQ R1, #0x80000000 (EQ iff dividend == INT64_MIN)
8232    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8233    w(bytes, 0xE7F0_00F0); // UDF #0 — signed-division overflow
8234}
8235
8236/// Fallible form of the `verify_reg_bits` contract. PC (R15) is not a valid
8237/// data operand for the Thumb-2 encodings that use this guard (SDIV/UDIV/MLS/…
8238/// are UNPREDICTABLE with PC). Synth's own codegen never emits PC there, but
8239/// the encoder must stay *total* over arbitrary `ArmOp` inputs — the fuzz
8240/// harness (`encoder_no_panic`) requires Ok-or-Err, never a panic. Pre-fix, the
8241/// `debug_assert` in `verify_reg_bits` aborted under `-Cdebug-assertions`.
8242/// Returns a typed Err instead. See #185.
8243fn reg_bits_checked(bits: u32) -> Result<()> {
8244    if bits > 14 {
8245        return Err(synth_core::Error::synthesis(format!(
8246            "register bits {bits} (PC/R15) is not a valid operand for this Thumb-2 encoding"
8247        )));
8248    }
8249    Ok(())
8250}
8251
8252/// Try to encode a 32-bit value as an ARM rotated immediate (imm8 ROR 2*rot4).
8253/// Returns Some((encoded_bits, 1)) if representable, None otherwise.
8254fn try_encode_rotated_imm(val: u32) -> Option<(u32, u32)> {
8255    if val == 0 {
8256        return Some((0, 1));
8257    }
8258    for rot in 0..16u32 {
8259        let shift = rot * 2;
8260        // Rotate left by shift (undo the ROR) to see if result fits in 8 bits
8261        let unrotated = val.rotate_left(shift);
8262        if unrotated <= 0xFF {
8263            // Encoded as: rot4(4 bits) | imm8(8 bits) = rotate_imm << 8 | imm8
8264            return Some(((rot << 8) | unrotated, 1));
8265        }
8266    }
8267    None
8268}
8269
8270/// Encode operand2 field and return (bits, immediate_flag).
8271/// For ARM32 mode, immediates use the rotated-immediate encoding (imm8 ROR 2*rot4).
8272/// Panics if an immediate value cannot be represented. Callers that need large
8273/// immediates should use MOVW/MOVT instead of Operand2::Imm.
8274fn encode_operand2(op2: &Operand2) -> Result<(u32, u32)> {
8275    match op2 {
8276        Operand2::Imm(val) => {
8277            let uval = *val as u32;
8278            // Attempt rotated-immediate encoding (ARM32 Operand2)
8279            if let Some(encoded) = try_encode_rotated_imm(uval) {
8280                Ok(encoded)
8281            } else {
8282                // #378-class honesty: an immediate that can't be expressed as an
8283                // ARM32 rotated immediate is an INTERNAL selector bug — large
8284                // constants must be materialized via MOVW/MOVT, not passed here.
8285                // FAIL HONESTLY with an Err rather than silently masking to
8286                // `uval & 0xFF` and emitting a WRONG immediate. The encoder is
8287                // Ok-or-Err, never corrupt (#180/#185); a loud Err is also why
8288                // this is an Err and not a panic (the `encoder_no_panic` fuzz
8289                // contract — malformed/oversized input must degrade, not crash).
8290                Err(synth_core::Error::synthesis(format!(
8291                    "encode_operand2: immediate {uval:#x} ({val}) is not an ARM32 \
8292                     rotated immediate — the selector must materialize large \
8293                     constants via MOVW/MOVT"
8294                )))
8295            }
8296        }
8297
8298        Operand2::Reg(reg) => {
8299            let reg_bits = reg_to_bits(reg);
8300            Ok((reg_bits, 0)) // I=0 for register
8301        }
8302
8303        Operand2::RegShift {
8304            rm,
8305            shift: _,
8306            amount,
8307        } => {
8308            // Simplified encoding with shift
8309            let rm_bits = reg_to_bits(rm);
8310            let shift_bits = (*amount & 0x1F) << 7;
8311            Ok((shift_bits | rm_bits, 0))
8312        }
8313    }
8314}
8315
8316/// Encode memory address to (base_reg, offset)
8317fn encode_mem_addr(addr: &MemAddr) -> (u32, u32) {
8318    let base_bits = reg_to_bits(&addr.base);
8319    let offset_bits = (addr.offset as u32) & 0xFFF; // 12-bit offset
8320    (base_bits, offset_bits)
8321}
8322
8323/// S-register number: S0=0, S1=1, ..., S31=31
8324fn vfp_sreg_to_num(reg: &VfpReg) -> Result<u32> {
8325    match reg {
8326        VfpReg::S0 => Ok(0),
8327        VfpReg::S1 => Ok(1),
8328        VfpReg::S2 => Ok(2),
8329        VfpReg::S3 => Ok(3),
8330        VfpReg::S4 => Ok(4),
8331        VfpReg::S5 => Ok(5),
8332        VfpReg::S6 => Ok(6),
8333        VfpReg::S7 => Ok(7),
8334        VfpReg::S8 => Ok(8),
8335        VfpReg::S9 => Ok(9),
8336        VfpReg::S10 => Ok(10),
8337        VfpReg::S11 => Ok(11),
8338        VfpReg::S12 => Ok(12),
8339        VfpReg::S13 => Ok(13),
8340        VfpReg::S14 => Ok(14),
8341        VfpReg::S15 => Ok(15),
8342        VfpReg::S16 => Ok(16),
8343        VfpReg::S17 => Ok(17),
8344        VfpReg::S18 => Ok(18),
8345        VfpReg::S19 => Ok(19),
8346        VfpReg::S20 => Ok(20),
8347        VfpReg::S21 => Ok(21),
8348        VfpReg::S22 => Ok(22),
8349        VfpReg::S23 => Ok(23),
8350        VfpReg::S24 => Ok(24),
8351        VfpReg::S25 => Ok(25),
8352        VfpReg::S26 => Ok(26),
8353        VfpReg::S27 => Ok(27),
8354        VfpReg::S28 => Ok(28),
8355        VfpReg::S29 => Ok(29),
8356        VfpReg::S30 => Ok(30),
8357        VfpReg::S31 => Ok(31),
8358        // D-registers are not used in F32 single-precision encodings
8359        _ => Err(synth_core::Error::SynthesisError(
8360            "D-register not supported in single-precision VFP encoding".to_string(),
8361        )),
8362    }
8363}
8364
8365/// D-register number: D0=0, D1=1, ..., D15=15
8366fn vfp_dreg_to_num(reg: &VfpReg) -> Result<u32> {
8367    match reg {
8368        VfpReg::D0 => Ok(0),
8369        VfpReg::D1 => Ok(1),
8370        VfpReg::D2 => Ok(2),
8371        VfpReg::D3 => Ok(3),
8372        VfpReg::D4 => Ok(4),
8373        VfpReg::D5 => Ok(5),
8374        VfpReg::D6 => Ok(6),
8375        VfpReg::D7 => Ok(7),
8376        VfpReg::D8 => Ok(8),
8377        VfpReg::D9 => Ok(9),
8378        VfpReg::D10 => Ok(10),
8379        VfpReg::D11 => Ok(11),
8380        VfpReg::D12 => Ok(12),
8381        VfpReg::D13 => Ok(13),
8382        VfpReg::D14 => Ok(14),
8383        VfpReg::D15 => Ok(15),
8384        // S-registers are not used in F64 double-precision encodings
8385        _ => Err(synth_core::Error::SynthesisError(
8386            "S-register not supported in double-precision VFP encoding".to_string(),
8387        )),
8388    }
8389}
8390
8391/// Split S-register into (Vx[3:0], qualifier_bit) for VFP encoding.
8392/// For an S-register number s: Vx = s >> 1, qualifier = s & 1.
8393/// The qualifier bit goes to D (bit 22), N (bit 7), or M (bit 5) depending on role.
8394fn encode_sreg(s: u32) -> (u32, u32) {
8395    (s >> 1, s & 1)
8396}
8397
8398/// Split D-register into (Vx[3:0], qualifier_bit) for VFP double-precision encoding.
8399/// For a D-register number d: Vx = d & 0xF, qualifier = (d >> 4) & 1.
8400/// For D0-D15, qualifier is always 0.
8401fn encode_dreg(d: u32) -> (u32, u32) {
8402    (d & 0xF, (d >> 4) & 1)
8403}
8404
8405/// Encode a VFP 3-register arithmetic instruction (VADD.F32, VSUB.F32, VMUL.F32, VDIV.F32).
8406/// Returns the full 32-bit instruction word.
8407///
8408/// VFP encoding: [cond 1110] [D opc1 Vn] [Vd 101 sz] [N opc2 M 0 Vm]
8409/// For single-precision (sz=0), coprocessor = 0xA (bits[11:8]).
8410fn encode_vfp_3reg(base: u32, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<u32> {
8411    let sd_num = vfp_sreg_to_num(sd)?;
8412    let sn_num = vfp_sreg_to_num(sn)?;
8413    let sm_num = vfp_sreg_to_num(sm)?;
8414    let (vd, d) = encode_sreg(sd_num);
8415    let (vn, n) = encode_sreg(sn_num);
8416    let (vm, m) = encode_sreg(sm_num);
8417
8418    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8419}
8420
8421/// Encode a VFP 2-register instruction (VNEG.F32, VABS.F32, VSQRT.F32).
8422/// Returns the full 32-bit instruction word.
8423fn encode_vfp_2reg(base: u32, sd: &VfpReg, sm: &VfpReg) -> Result<u32> {
8424    let sd_num = vfp_sreg_to_num(sd)?;
8425    let sm_num = vfp_sreg_to_num(sm)?;
8426    let (vd, d) = encode_sreg(sd_num);
8427    let (vm, m) = encode_sreg(sm_num);
8428
8429    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8430}
8431
8432/// Encode a VFP load/store (VLDR.F32 / VSTR.F32).
8433/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8434/// U bit (bit 23) controls add/subtract offset.
8435fn encode_vfp_ldst(base: u32, sd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8436    let sd_num = vfp_sreg_to_num(sd)?;
8437    let (vd, d) = encode_sreg(sd_num);
8438    let rn = reg_to_bits(&addr.base);
8439
8440    let offset = addr.offset;
8441    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8442    let abs_offset = offset.unsigned_abs();
8443    let imm8 = (abs_offset / 4) & 0xFF;
8444
8445    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8446}
8447
8448/// Encode VMOV between core register and S-register.
8449/// VMOV Sn, Rt: 0xEE00_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8450/// VMOV Rt, Sn: 0xEE10_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8451fn encode_vmov_core_sreg(to_sreg: bool, sreg: &VfpReg, core: &Reg) -> Result<u32> {
8452    let s_num = vfp_sreg_to_num(sreg)?;
8453    let (vn, n) = encode_sreg(s_num);
8454    let rt = reg_to_bits(core);
8455
8456    let base = if to_sreg { 0xEE000A10 } else { 0xEE100A10 };
8457    Ok(base | (vn << 16) | (rt << 12) | (n << 7))
8458}
8459
8460/// Encode a VFP 3-register double-precision instruction (VADD.F64, VSUB.F64, etc.).
8461/// For double-precision (sz=1), coprocessor = 0xB (bits[11:8]).
8462/// The base should have bit 8 = 1 for F64 (0xB suffix instead of 0xA).
8463fn encode_vfp_3reg_f64(base: u32, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<u32> {
8464    let dd_num = vfp_dreg_to_num(dd)?;
8465    let dn_num = vfp_dreg_to_num(dn)?;
8466    let dm_num = vfp_dreg_to_num(dm)?;
8467    let (vd, d) = encode_dreg(dd_num);
8468    let (vn, n) = encode_dreg(dn_num);
8469    let (vm, m) = encode_dreg(dm_num);
8470
8471    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8472}
8473
8474/// Encode a VFP 2-register double-precision instruction (VNEG.F64, VABS.F64, VSQRT.F64).
8475fn encode_vfp_2reg_f64(base: u32, dd: &VfpReg, dm: &VfpReg) -> Result<u32> {
8476    let dd_num = vfp_dreg_to_num(dd)?;
8477    let dm_num = vfp_dreg_to_num(dm)?;
8478    let (vd, d) = encode_dreg(dd_num);
8479    let (vm, m) = encode_dreg(dm_num);
8480
8481    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8482}
8483
8484/// Encode a VFP load/store for double-precision (VLDR.64 / VSTR.64).
8485/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8486fn encode_vfp_ldst_f64(base: u32, dd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8487    let dd_num = vfp_dreg_to_num(dd)?;
8488    let (vd, d) = encode_dreg(dd_num);
8489    let rn = reg_to_bits(&addr.base);
8490
8491    let offset = addr.offset;
8492    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8493    let abs_offset = offset.unsigned_abs();
8494    let imm8 = (abs_offset / 4) & 0xFF;
8495
8496    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8497}
8498
8499/// Encode VMOV between two core registers and a D-register.
8500/// VMOV Dm, Rt, Rt2: 0xEC40_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8501/// VMOV Rt, Rt2, Dm: 0xEC50_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8502fn encode_vmov_core_dreg(
8503    to_dreg: bool,
8504    dreg: &VfpReg,
8505    core_lo: &Reg,
8506    core_hi: &Reg,
8507) -> Result<u32> {
8508    let d_num = vfp_dreg_to_num(dreg)?;
8509    let (vm, m) = encode_dreg(d_num);
8510    let rt = reg_to_bits(core_lo);
8511    let rt2 = reg_to_bits(core_hi);
8512
8513    let base = if to_dreg { 0xEC400B10 } else { 0xEC500B10 };
8514    Ok(base | (rt2 << 16) | (rt << 12) | (m << 5) | vm)
8515}
8516
8517/// Emit a VFP 32-bit instruction as Thumb-2 bytes (two LE halfwords).
8518fn vfp_to_thumb_bytes(instr: u32) -> Vec<u8> {
8519    let hw1 = ((instr >> 16) & 0xFFFF) as u16;
8520    let hw2 = (instr & 0xFFFF) as u16;
8521    let mut bytes = hw1.to_le_bytes().to_vec();
8522    bytes.extend_from_slice(&hw2.to_le_bytes());
8523    bytes
8524}
8525
8526// ============================================================================
8527// Helium MVE encoding helpers
8528// ============================================================================
8529
8530/// Q-register number: Q0=0, Q1=1, ..., Q7=7
8531fn qreg_to_num(reg: &QReg) -> u32 {
8532    match reg {
8533        QReg::Q0 => 0,
8534        QReg::Q1 => 1,
8535        QReg::Q2 => 2,
8536        QReg::Q3 => 3,
8537        QReg::Q4 => 4,
8538        QReg::Q5 => 5,
8539        QReg::Q6 => 6,
8540        QReg::Q7 => 7,
8541    }
8542}
8543
8544/// MVE element size to encoding bits: S8=0b00, S16=0b01, S32=0b10
8545fn mve_size_bits(size: &MveSize) -> u32 {
8546    match size {
8547        MveSize::S8 => 0b00,
8548        MveSize::S16 => 0b01,
8549        MveSize::S32 => 0b10,
8550    }
8551}
8552
8553/// Encode MVE 3-register instruction.
8554/// Q-registers are encoded as D-register pairs: Q0=D0:D1, Q1=D2:D3, etc.
8555/// In NEON/MVE encoding, the Q-register uses D-register number = Qn * 2.
8556fn encode_mve_3reg(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
8557    let d = qreg_to_num(qd) * 2;
8558    let n = qreg_to_num(qn) * 2;
8559    let m = qreg_to_num(qm) * 2;
8560
8561    // Standard NEON/MVE 3-register encoding:
8562    // D bit (bit 22) = Vd[4], Vd[3:0] = bits [15:12]
8563    // N bit (bit 7)  = Vn[4], Vn[3:0] = bits [19:16]
8564    // M bit (bit 5)  = Vm[4], Vm[3:0] = bits [3:0]
8565    let vd = d & 0xF;
8566    let d_bit = (d >> 4) & 1;
8567    let vn = n & 0xF;
8568    let n_bit = (n >> 4) & 1;
8569    let vm = m & 0xF;
8570    let m_bit = (m >> 4) & 1;
8571
8572    base | (d_bit << 22) | (vn << 16) | (vd << 12) | (n_bit << 7) | (m_bit << 5) | vm
8573}
8574
8575/// Encode MVE 3-register bitwise instruction (VAND, VORR, VEOR, VBIC).
8576fn encode_mve_3reg_bitwise(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
8577    encode_mve_3reg(base, qd, qn, qm)
8578}
8579
8580/// Encode MVE VLDRW.32 Qd, [Rn, #offset]
8581/// Format: EC9x xxxx - contiguous load, word-sized elements
8582fn encode_mve_vldrw(qd: &QReg, addr: &MemAddr) -> u32 {
8583    let qd_enc = qreg_to_num(qd) * 2;
8584    let rn = reg_to_bits(&addr.base);
8585    let offset = addr.offset;
8586    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8587    let abs_offset = offset.unsigned_abs();
8588    let imm7 = (abs_offset / 4) & 0x7F; // 7-bit word-aligned offset
8589
8590    // VLDRW.32 Qd, [Rn, #imm]: ED10 xx80 variant
8591    0xED100E80
8592        | (u_bit << 23)
8593        | ((qd_enc >> 4) << 22)
8594        | (rn << 16)
8595        | ((qd_enc & 0xF) << 12)
8596        | (imm7 & 0x7F)
8597}
8598
8599/// Encode MVE VSTRW.32 Qd, [Rn, #offset]
8600fn encode_mve_vstrw(qd: &QReg, addr: &MemAddr) -> u32 {
8601    let qd_enc = qreg_to_num(qd) * 2;
8602    let rn = reg_to_bits(&addr.base);
8603    let offset = addr.offset;
8604    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8605    let abs_offset = offset.unsigned_abs();
8606    let imm7 = (abs_offset / 4) & 0x7F;
8607
8608    0xED000E80
8609        | (u_bit << 23)
8610        | ((qd_enc >> 4) << 22)
8611        | (rn << 16)
8612        | ((qd_enc & 0xF) << 12)
8613        | (imm7 & 0x7F)
8614}
8615
8616impl ArmEncoder {
8617    /// Encode MVE constant load: MOVW+MOVT+VMOV for each 32-bit word, then assemble Q-register
8618    fn encode_thumb_mve_const(&self, qd: &QReg, bytes: &[u8; 16]) -> Result<Vec<u8>> {
8619        let mut result = Vec::new();
8620        let qd_num = qreg_to_num(qd);
8621
8622        // Load each 32-bit word into R12 (temp) then VMOV into S-register
8623        for i in 0..4 {
8624            let word = u32::from_le_bytes([
8625                bytes[i * 4],
8626                bytes[i * 4 + 1],
8627                bytes[i * 4 + 2],
8628                bytes[i * 4 + 3],
8629            ]);
8630            let lo16 = word & 0xFFFF;
8631            let hi16 = (word >> 16) & 0xFFFF;
8632
8633            // MOVW R12, #lo16
8634            result.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
8635            // MOVT R12, #hi16
8636            if hi16 != 0 {
8637                result.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
8638            }
8639
8640            // VMOV Sn, R12 where Sn = Qd*4 + i
8641            let s_num = qd_num * 4 + i as u32;
8642            let (vn, n) = encode_sreg(s_num);
8643            let vmov: u32 = 0xEE000A10 | (vn << 16) | (12 << 12) | (n << 7);
8644            result.extend_from_slice(&vfp_to_thumb_bytes(vmov));
8645        }
8646
8647        Ok(result)
8648    }
8649
8650    /// Encode lane-wise f32 binary operation (VDIV, etc.) via S-register extraction
8651    fn encode_thumb_mve_lane_wise_f32_binop(
8652        &self,
8653        qd: &QReg,
8654        qn: &QReg,
8655        qm: &QReg,
8656        vfp_base: u32,
8657    ) -> Result<Vec<u8>> {
8658        let mut result = Vec::new();
8659        let qd_num = qreg_to_num(qd);
8660        let qn_num = qreg_to_num(qn);
8661        let qm_num = qreg_to_num(qm);
8662
8663        // For each lane 0..3: use S-registers directly (Q aliasing)
8664        for i in 0..4u32 {
8665            let sd = qd_num * 4 + i;
8666            let sn = qn_num * 4 + i;
8667            let sm = qm_num * 4 + i;
8668
8669            let (vd, d) = encode_sreg(sd);
8670            let (vn, n) = encode_sreg(sn);
8671            let (vm, m) = encode_sreg(sm);
8672
8673            let instr = vfp_base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
8674            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
8675        }
8676
8677        Ok(result)
8678    }
8679
8680    /// Encode lane-wise f32 VSQRT via S-register extraction
8681    fn encode_thumb_mve_lane_wise_f32_sqrt(&self, qd: &QReg, qm: &QReg) -> Result<Vec<u8>> {
8682        let mut result = Vec::new();
8683        let qd_num = qreg_to_num(qd);
8684        let qm_num = qreg_to_num(qm);
8685
8686        // VSQRT.F32 base: 0xEEB10AC0
8687        for i in 0..4u32 {
8688            let sd = qd_num * 4 + i;
8689            let sm = qm_num * 4 + i;
8690
8691            let (vd, d) = encode_sreg(sd);
8692            let (vm, m) = encode_sreg(sm);
8693
8694            let instr: u32 = 0xEEB10AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
8695            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
8696        }
8697
8698        Ok(result)
8699    }
8700}
8701
8702#[cfg(test)]
8703mod tests {
8704    use super::*;
8705
8706    #[test]
8707    fn test_encoder_creation() {
8708        let encoder_arm = ArmEncoder::new_arm32();
8709        assert!(!encoder_arm.thumb_mode);
8710
8711        let encoder_thumb = ArmEncoder::new_thumb2();
8712        assert!(encoder_thumb.thumb_mode);
8713    }
8714
8715    /// #204 WAKE-path regression: `SetCond` materialized 0/1 with the 16-bit
8716    /// `MOVS Rd,#imm` (T1), whose Rd field is 3 bits (R0–R7). For a high Rd
8717    /// (R8–R12) `rd_bits << 8` overflows bit 11, flipping the opcode MOVS→CMP
8718    /// (`0x2c00`), so the boolean was never written — gale's `has_waiter` kept a
8719    /// stale value and the binary-sem WAKE dispatch read garbage. High Rd must
8720    /// use the 32-bit `MOV.W` (T2). Verify the bytes, not the IR.
8721    /// #311: the SAME high-Rd MOVS→CMP transmutation as #204, but in the
8722    /// i64 comparison expansions (I64SetCond / I64SetCondZ) — missed by the
8723    /// #204 hardening. With rd=R8 the boolean died in the flags
8724    /// (`ite eq; cmpeq r0,#1; cmpne r0,#0`), so gale's packed-u64 select
8725    /// read a stale register on silicon. High Rd must take MOV.W / CMP.W.
8726    #[test]
8727    fn test_encode_i64setcond_high_reg_uses_mov_w_311() {
8728        use synth_synthesis::{ArmOp, Condition, Reg};
8729        let enc = ArmEncoder::new_thumb2();
8730        let bytes = enc
8731            .encode(&ArmOp::I64SetCond {
8732                rd: Reg::R8,
8733                rn_lo: Reg::R2,
8734                rn_hi: Reg::R3,
8735                rm_lo: Reg::R6,
8736                rm_hi: Reg::R7,
8737                cond: Condition::EQ,
8738            })
8739            .unwrap();
8740        // The 32-bit MOV.W immediate (T2) first halfword is 0xF04F; the
8741        // 16-bit transmuted forms would contain 0x2801/0x2800 (CMP r0,#1/#0).
8742        let halfwords: Vec<u16> = bytes
8743            .chunks(2)
8744            .map(|c| u16::from_le_bytes([c[0], c[1]]))
8745            .collect();
8746        assert!(
8747            halfwords.iter().filter(|&&h| h == 0xF04F).count() == 2,
8748            "high rd must use two MOV.W (T2) encodings, got {halfwords:04x?}"
8749        );
8750        assert!(
8751            !halfwords.contains(&0x2801) && !halfwords.contains(&0x2800),
8752            "no transmuted 16-bit CMP imm: {halfwords:04x?}"
8753        );
8754
8755        let bytes_z = enc
8756            .encode(&ArmOp::I64SetCondZ {
8757                rd: Reg::R8,
8758                rn_lo: Reg::R2,
8759                rn_hi: Reg::R3,
8760            })
8761            .unwrap();
8762        let hw_z: Vec<u16> = bytes_z
8763            .chunks(2)
8764            .map(|c| u16::from_le_bytes([c[0], c[1]]))
8765            .collect();
8766        assert!(
8767            hw_z.iter().filter(|&&h| h == 0xF04F).count() == 2,
8768            "SetCondZ high rd MOV.W: {hw_z:04x?}"
8769        );
8770        // CMP.W rd,#0 (T2) first halfword: 0xF1B0 | rd
8771        assert!(
8772            hw_z.contains(&(0xF1B0 | 8)),
8773            "SetCondZ high rd must use CMP.W: {hw_z:04x?}"
8774        );
8775    }
8776
8777    #[test]
8778    fn test_encode_setcond_high_reg_uses_mov_w_204() {
8779        use synth_synthesis::{ArmOp, Condition, Reg};
8780        let enc = ArmEncoder::new_thumb2();
8781        // R12 (high): must be ITE + MOV.W #1 + MOV.W #0, never a 16-bit MOVS/CMP.
8782        let hi = enc
8783            .encode(&ArmOp::SetCond {
8784                rd: Reg::R12,
8785                cond: Condition::NE,
8786            })
8787            .unwrap();
8788        assert_eq!(hi.len(), 10, "ITE(2) + MOV.W(4) + MOV.W(4): {hi:02x?}");
8789        // both value halfwords are MOV.W (0xF04F) — NOT the corrupt CMP (0x2c..).
8790        assert_eq!(&hi[2..4], &[0x4F, 0xF0], "then = MOV.W: {hi:02x?}");
8791        assert_eq!(&hi[6..8], &[0x4F, 0xF0], "else = MOV.W: {hi:02x?}");
8792        assert_eq!(hi[4] & 0x0F, 0x01, "then imm = #1");
8793        assert_eq!(hi[8] & 0x0F, 0x00, "else imm = #0");
8794        // Low Rd keeps the compact 16-bit MOVS form.
8795        let lo = enc
8796            .encode(&ArmOp::SetCond {
8797                rd: Reg::R0,
8798                cond: Condition::NE,
8799            })
8800            .unwrap();
8801        assert_eq!(lo.len(), 6, "ITE(2) + MOVS(2) + MOVS(2): {lo:02x?}");
8802        assert_eq!(lo[2..4], [0x01, 0x20], "then = MOVS R0,#1");
8803        assert_eq!(lo[4..6], [0x00, 0x20], "else = MOVS R0,#0");
8804    }
8805
8806    /// #209 Opt 1b: UMULL RdLo, RdHi, Rn, Rm encodes correctly on both ISAs.
8807    /// Thumb-2 T1: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm.
8808    /// A32:        cond 0000 1000 RdHi RdLo Rm 1001 Rn.
8809    #[test]
8810    fn test_encode_umull_209b() {
8811        use synth_synthesis::{ArmOp, Reg};
8812        let op = ArmOp::Umull {
8813            rdlo: Reg::R4,
8814            rdhi: Reg::R5,
8815            rn: Reg::R0,
8816            rm: Reg::R3,
8817        };
8818        // Thumb-2: hw1 = 0xFBA0 | 0 = 0xFBA0; hw2 = (4<<12)|(5<<8)|3 = 0x4503.
8819        let t = ArmEncoder::new_thumb2().encode(&op).unwrap();
8820        assert_eq!(
8821            t,
8822            vec![0xA0, 0xFB, 0x03, 0x45],
8823            "umull r4,r5,r0,r3 (T2): {t:02x?}"
8824        );
8825        // A32: 0xE0800090 | (5<<16) | (4<<12) | (3<<8) | 0 = 0xE0854390.
8826        let a = ArmEncoder::new_arm32().encode(&op).unwrap();
8827        assert_eq!(
8828            a,
8829            0xE085_4390u32.to_le_bytes().to_vec(),
8830            "umull (A32): {a:02x?}"
8831        );
8832    }
8833
8834    /// #206 regression: the ARM32 (A32) `Ldr`/`Str` encoders fed `addr` through
8835    /// `encode_mem_addr`, which returns only the 12-bit immediate — so a register
8836    /// offset (`[rn, rm, #off]`) was silently dropped to `[rn, #off]`, sending
8837    /// the access to the wrong runtime address (silent miscompile on the default
8838    /// `--target arm`). A register offset must materialize `ip = rn + rm` and
8839    /// load from `[ip, #off]`. Verify the bytes.
8840    #[test]
8841    fn test_encode_arm32_indexed_load_keeps_index_206() {
8842        use synth_synthesis::{ArmOp, MemAddr, Reg};
8843        let enc = ArmEncoder::new_arm32();
8844        // ldr r0, [r11, r1, #8]  must NOT collapse to a single immediate ldr.
8845        let bytes = enc
8846            .encode(&ArmOp::Ldr {
8847                rd: Reg::R0,
8848                addr: MemAddr::reg_imm(Reg::R11, Reg::R1, 8),
8849            })
8850            .unwrap();
8851        assert_eq!(
8852            bytes.len(),
8853            8,
8854            "expected ADD ip + LDR (2 words): {bytes:02x?}"
8855        );
8856        let add = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
8857        let ldr = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
8858        // ADD ip, r11, r1  = 0xE08BC001
8859        assert_eq!(add, 0xE08B_C001, "ADD ip,r11,r1: {add:#010x}");
8860        // LDR r0, [ip, #8] = 0xE59C0008
8861        assert_eq!(ldr, 0xE59C_0008, "LDR r0,[ip,#8]: {ldr:#010x}");
8862        // A bare immediate ldr (the bug) would be 0xE59B0008 (base=r11) — reject.
8863        assert_ne!(ldr, 0xE59B_0008, "index must not be dropped");
8864    }
8865
8866    /// #594 regression: `call_indirect` on the A32 path (`--target cortex-r5`)
8867    /// was encoded as a literal NOP (0xE1A00000) — the call never happened and
8868    /// the function silently returned the leftover table-index value. The A32
8869    /// encoder must emit a real dispatch expansion, since #642 guarded by an
8870    /// inline bounds check:
8871    /// `MOVW r12, #size; CMP idx, r12; BLO +1; UDF;
8872    ///  MOV r12, idx, LSL #2; LDR r12, [r11, r12]; BLX r12`.
8873    #[test]
8874    fn test_encode_arm32_call_indirect_is_real_call_594() {
8875        use synth_synthesis::{ArmOp, Reg};
8876        let enc = ArmEncoder::new_arm32();
8877        let bytes = enc
8878            .encode(&ArmOp::CallIndirect {
8879                rd: Reg::R0,
8880                type_idx: 0,
8881                table_index_reg: Reg::R0,
8882                table_size: 4,
8883                table_byte_offset: 0,
8884            })
8885            .unwrap();
8886        assert_eq!(
8887            bytes.len(),
8888            28,
8889            "expected MOVW + CMP + BLO + UDF + MOV + LDR + BLX (7 words): {bytes:02x?}"
8890        );
8891        let words: Vec<u32> = bytes
8892            .chunks_exact(4)
8893            .map(|w| u32::from_le_bytes(w.try_into().unwrap()))
8894            .collect();
8895        // #642 bounds guard: MOVW r12, #4; CMP r0, r12; BLO +1; UDF
8896        assert_eq!(words[0], 0xE300_C004, "MOVW r12,#4: {:#010x}", words[0]);
8897        assert_eq!(words[1], 0xE150_000C, "CMP r0,r12: {:#010x}", words[1]);
8898        assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
8899        assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
8900        // MOV r12, r0, LSL #2 = 0xE1A0C100
8901        assert_eq!(
8902            words[4], 0xE1A0_C100,
8903            "MOV r12,r0,LSL#2: {:#010x}",
8904            words[4]
8905        );
8906        // LDR r12, [r11, r12] = 0xE79BC00C
8907        assert_eq!(
8908            words[5], 0xE79B_C00C,
8909            "LDR r12,[r11,r12]: {:#010x}",
8910            words[5]
8911        );
8912        // BLX r12 = 0xE12FFF3C
8913        assert_eq!(words[6], 0xE12F_FF3C, "BLX r12: {:#010x}", words[6]);
8914        // The bug: a single NOP word. Must never come back.
8915        assert!(
8916            !bytes
8917                .chunks_exact(4)
8918                .any(|w| w == 0xE1A0_0000u32.to_le_bytes()),
8919            "call_indirect must not contain a NOP (#594): {bytes:02x?}"
8920        );
8921
8922        // A non-R0 index register lands in the MOV's Rm and CMP's Rn fields.
8923        let bytes = enc
8924            .encode(&ArmOp::CallIndirect {
8925                rd: Reg::R0,
8926                type_idx: 0,
8927                table_index_reg: Reg::R4,
8928                table_size: 4,
8929                table_byte_offset: 0,
8930            })
8931            .unwrap();
8932        let cmp = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
8933        assert_eq!(cmp, 0xE154_000C, "CMP r4,r12: {cmp:#010x}");
8934        let mov = u32::from_le_bytes(bytes[16..20].try_into().unwrap());
8935        assert_eq!(mov, 0xE1A0_C104, "MOV r12,r4,LSL#2: {mov:#010x}");
8936    }
8937
8938    /// #642: a table size above 16 bits must not be silently truncated by the
8939    /// MOVW — the A32 guard adds a MOVT for the high half.
8940    #[test]
8941    fn test_encode_arm32_call_indirect_wide_table_size_642() {
8942        use synth_synthesis::{ArmOp, Reg};
8943        let enc = ArmEncoder::new_arm32();
8944        let bytes = enc
8945            .encode(&ArmOp::CallIndirect {
8946                rd: Reg::R0,
8947                type_idx: 0,
8948                table_index_reg: Reg::R0,
8949                table_size: 0x0002_0003,
8950                table_byte_offset: 0,
8951            })
8952            .unwrap();
8953        assert_eq!(bytes.len(), 32, "MOVT arm adds one word: {bytes:02x?}");
8954        let movw = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
8955        let movt = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
8956        assert_eq!(movw, 0xE300_C003, "MOVW r12,#3: {movw:#010x}");
8957        assert_eq!(movt, 0xE340_C002, "MOVT r12,#2: {movt:#010x}");
8958    }
8959
8960    /// #597 anchor (justified correctness RE-PIN of the #594-era freeze): the
8961    /// Thumb-2 `CallIndirect` expansion is `mov.w ip, rm, LSL #2; ldr.w ip,
8962    /// [r11, ip]; blx ip`.
8963    ///
8964    /// The #594 PR froze the then-current bytes `4F EA 20 0C ...` whose first
8965    /// word decodes as `mov.w ip, rm, ASR #32` — the intended `LSL #2` had
8966    /// its shift amount in the TYPE field (bits 5:4) instead of imm2 (bits
8967    /// 7:6), so the index was destroyed and every call_indirect dispatched
8968    /// table entry 0 (shipped miscompile, masked by index-0 probes). #597
8969    /// corrects the encoding; new bytes `4F EA 80 0C ...` were
8970    /// execution-validated under unicorn against the wasmtime oracle on a
8971    /// multi-entry table (indexes 0, 1, 3 —
8972    /// scripts/repro/call_indirect_597_differential.py) before this pin was
8973    /// replaced. Old pin: [4F EA 20 0C, 5B F8 0C C0, E0 47] (ASR #32 — must
8974    /// never come back).
8975    #[test]
8976    fn test_encode_thumb_call_indirect_lsl2_597() {
8977        use synth_synthesis::{ArmOp, Reg};
8978        let enc = ArmEncoder::new_thumb2();
8979        let bytes = enc
8980            .encode(&ArmOp::CallIndirect {
8981                rd: Reg::R0,
8982                type_idx: 0,
8983                table_index_reg: Reg::R0,
8984                table_size: 4,
8985                table_byte_offset: 0,
8986            })
8987            .unwrap();
8988        assert_eq!(
8989            bytes,
8990            vec![
8991                // #642 bounds guard: movw ip,#4; cmp r0,ip; blo +1; udf #0
8992                0x40, 0xF2, 0x04, 0x0C, // movw ip, #4
8993                0x60, 0x45, // cmp r0, ip
8994                0x00, 0xD3, // blo .+4 (skip the udf)
8995                0x00, 0xDE, // udf #0 — OOB index trap (WASM §4.4.8)
8996                // #597-pinned dispatch
8997                0x4F, 0xEA, 0x80, 0x0C, // mov.w ip, r0, lsl #2
8998                0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
8999                0xE0, 0x47, // blx ip
9000            ],
9001            "Thumb-2 CallIndirect: bounds guard + mov.w/ldr.w/blx dispatch: {bytes:02x?}"
9002        );
9003        // The #597 bug bytes (ASR #32 dispatch first word) must never come back.
9004        assert!(
9005            !bytes.windows(4).any(|w| w == [0x4F, 0xEA, 0x20, 0x0C]),
9006            "mov.w ip, rm, ASR #32 — the #597 type-field bug"
9007        );
9008
9009        // A non-R0 index register lands in the mov.w's Rm field (hw2 bits 3:0)
9010        // and the cmp's Rn field.
9011        let bytes = enc
9012            .encode(&ArmOp::CallIndirect {
9013                rd: Reg::R0,
9014                type_idx: 0,
9015                table_index_reg: Reg::R4,
9016                table_size: 4,
9017                table_byte_offset: 0,
9018            })
9019            .unwrap();
9020        assert_eq!(&bytes[4..6], &[0x64, 0x45], "cmp r4, ip: {bytes:02x?}");
9021        assert_eq!(
9022            &bytes[10..14],
9023            &[0x4F, 0xEA, 0x84, 0x0C],
9024            "mov.w ip, r4, LSL #2: {bytes:02x?}"
9025        );
9026    }
9027
9028    /// #642: the Thumb-2 bounds guard for a high-register index (R8 — the top
9029    /// of the allocatable pool) uses the high-reg-capable 16-bit CMP (T2) with
9030    /// the N bit set; a table size above 16 bits adds a MOVT.
9031    #[test]
9032    fn test_encode_thumb_call_indirect_guard_shapes_642() {
9033        use synth_synthesis::{ArmOp, Reg};
9034        let enc = ArmEncoder::new_thumb2();
9035        let bytes = enc
9036            .encode(&ArmOp::CallIndirect {
9037                rd: Reg::R0,
9038                type_idx: 0,
9039                table_index_reg: Reg::R8,
9040                table_size: 3,
9041                table_byte_offset: 0,
9042            })
9043            .unwrap();
9044        // cmp r8, ip — T2: 0x4500 | N(1)<<7 | Rm(12)<<3 | Rn(0) = 0x45E0
9045        assert_eq!(&bytes[4..6], &[0xE0, 0x45], "cmp r8, ip: {bytes:02x?}");
9046
9047        let bytes = enc
9048            .encode(&ArmOp::CallIndirect {
9049                rd: Reg::R0,
9050                type_idx: 0,
9051                table_index_reg: Reg::R0,
9052                table_size: 0x0002_0003,
9053                table_byte_offset: 0,
9054            })
9055            .unwrap();
9056        // movw ip,#3 then movt ip,#2 — the size must not be truncated.
9057        assert_eq!(
9058            &bytes[0..8],
9059            &[0x40, 0xF2, 0x03, 0x0C, 0xC0, 0xF2, 0x02, 0x0C],
9060            "movw ip,#3; movt ip,#2: {bytes:02x?}"
9061        );
9062    }
9063
9064    /// #650: a non-zero table base offset (table N of the contiguous R11
9065    /// region) routes the Thumb-2 pointer load through
9066    /// `add.w ip, r11, ip; ldr.w ip, [ip, #offset]` — and offset 0 keeps the
9067    /// pre-#650 single-load bytes IDENTICAL (the by-construction pin).
9068    #[test]
9069    fn test_encode_thumb_call_indirect_table_offset_650() {
9070        use synth_synthesis::{ArmOp, Reg};
9071        let enc = ArmEncoder::new_thumb2();
9072        // falcon's fused-component shape: table 0 has 7 entries, so table 1
9073        // sits at byte offset 28.
9074        let bytes = enc
9075            .encode(&ArmOp::CallIndirect {
9076                rd: Reg::R0,
9077                type_idx: 0,
9078                table_index_reg: Reg::R1,
9079                table_size: 41,
9080                table_byte_offset: 28,
9081            })
9082            .unwrap();
9083        assert_eq!(
9084            bytes,
9085            vec![
9086                // #642 bounds guard against TABLE 1's OWN size (41)
9087                0x40, 0xF2, 0x29, 0x0C, // movw ip, #41
9088                0x61, 0x45, // cmp r1, ip
9089                0x00, 0xD3, // blo .+4 (skip the udf)
9090                0x00, 0xDE, // udf #0 — OOB trap (WASM §4.4.8)
9091                // dispatch through table 1's base (R11 + 28)
9092                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9093                0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9094                0xDC, 0xF8, 0x1C, 0xC0, // ldr.w ip, [ip, #28]
9095                0xE0, 0x47, // blx ip
9096            ],
9097            "Thumb-2 table-1 dispatch (#650): {bytes:02x?}"
9098        );
9099
9100        // Offset 0 must stay the #597-pinned single-load form (no add.w, no
9101        // imm-form ldr) — single-table byte identity by construction.
9102        let zero = enc
9103            .encode(&ArmOp::CallIndirect {
9104                rd: Reg::R0,
9105                type_idx: 0,
9106                table_index_reg: Reg::R1,
9107                table_size: 41,
9108                table_byte_offset: 0,
9109            })
9110            .unwrap();
9111        assert_eq!(
9112            &zero[10..],
9113            &[
9114                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9115                0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9116                0xE0, 0x47, // blx ip
9117            ],
9118            "offset 0 keeps the pre-#650 dispatch bytes: {zero:02x?}"
9119        );
9120    }
9121
9122    /// #650: the A32 twin — `add r12, r11, r12; ldr r12, [r12, #offset]` for
9123    /// a non-zero table base offset; offset 0 keeps the #594/#642 form.
9124    #[test]
9125    fn test_encode_arm32_call_indirect_table_offset_650() {
9126        use synth_synthesis::{ArmOp, Reg};
9127        let enc = ArmEncoder::new_arm32();
9128        let bytes = enc
9129            .encode(&ArmOp::CallIndirect {
9130                rd: Reg::R0,
9131                type_idx: 0,
9132                table_index_reg: Reg::R1,
9133                table_size: 41,
9134                table_byte_offset: 28,
9135            })
9136            .unwrap();
9137        let words: Vec<u32> = bytes
9138            .chunks_exact(4)
9139            .map(|w| u32::from_le_bytes(w.try_into().unwrap()))
9140            .collect();
9141        assert_eq!(words[0], 0xE300_C029, "MOVW r12,#41: {:#010x}", words[0]);
9142        assert_eq!(words[1], 0xE151_000C, "CMP r1,r12: {:#010x}", words[1]);
9143        assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9144        assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9145        assert_eq!(
9146            words[4], 0xE1A0_C101,
9147            "MOV r12,r1,LSL#2: {:#010x}",
9148            words[4]
9149        );
9150        assert_eq!(
9151            words[5], 0xE08B_C00C,
9152            "ADD r12,r11,r12 (#650): {:#010x}",
9153            words[5]
9154        );
9155        assert_eq!(
9156            words[6], 0xE59C_C01C,
9157            "LDR r12,[r12,#28] (#650): {:#010x}",
9158            words[6]
9159        );
9160        assert_eq!(words[7], 0xE12F_FF3C, "BLX r12: {:#010x}", words[7]);
9161    }
9162
9163    /// #178/#180 regression: the Thumb `Add`/`Adds`/`Subs` reg-forms used the
9164    /// 16-bit encoding unconditionally. For high registers (R12 base scratch,
9165    /// R8-R11 i64 pairs) the 3-bit register fields overflow and corrupt the
9166    /// operands — `add ip,ip,r0` came out as `adds r4,r5,r1` (0x186C), silently
9167    /// dropping the address operand and miscompiling every optimized memory
9168    /// access. High registers must use the 32-bit `.W` forms.
9169    #[test]
9170    fn test_encode_thumb_add_high_reg_uses_add_w_178_180() {
9171        let encoder = ArmEncoder::new_thumb2();
9172
9173        // add ip, ip, r0  — the exact MemLoad/MemStore base+addr op.
9174        let code = encoder
9175            .encode(&ArmOp::Add {
9176                rd: Reg::R12,
9177                rn: Reg::R12,
9178                op2: Operand2::Reg(Reg::R0),
9179            })
9180            .unwrap();
9181        // ADD.W ip, ip, r0 = EB0C 0C00 (little-endian halfwords).
9182        assert_eq!(
9183            code,
9184            vec![0x0C, 0xEB, 0x00, 0x0C],
9185            "high-reg Thumb ADD must be 32-bit ADD.W (EB0C 0C00), not corrupt 16-bit; got {code:02X?}"
9186        );
9187        // Must NOT be the buggy 16-bit 0x186C (`adds r4,r5,r1`).
9188        assert_ne!(code, vec![0x6C, 0x18], "regressed to corrupt 16-bit ADDS");
9189
9190        // Low-register add stays 16-bit (no regression for the common case).
9191        let lo = encoder
9192            .encode(&ArmOp::Add {
9193                rd: Reg::R1,
9194                rn: Reg::R2,
9195                op2: Operand2::Reg(Reg::R3),
9196            })
9197            .unwrap();
9198        assert_eq!(
9199            lo.len(),
9200            2,
9201            "low-reg ADD should remain 16-bit, got {lo:02X?}"
9202        );
9203    }
9204
9205    /// #178/#180 sibling: i64 low-word `Adds`/`Subs` can land in R8-R11 pairs;
9206    /// those must fall back to 32-bit ADDS.W/SUBS.W (flag-setting preserved).
9207    #[test]
9208    fn test_encode_thumb_adds_subs_high_reg_use_32bit_178_180() {
9209        let encoder = ArmEncoder::new_thumb2();
9210
9211        // adds r10, r10, r8  → ADDS.W = EB1A 0A08
9212        let adds = encoder
9213            .encode(&ArmOp::Adds {
9214                rd: Reg::R10,
9215                rn: Reg::R10,
9216                op2: Operand2::Reg(Reg::R8),
9217            })
9218            .unwrap();
9219        assert_eq!(
9220            adds,
9221            vec![0x1A, 0xEB, 0x08, 0x0A],
9222            "high-reg ADDS must be 32-bit ADDS.W (EB1A 0A08); got {adds:02X?}"
9223        );
9224
9225        // subs r10, r10, r8  → SUBS.W = EBBA 0A08
9226        let subs = encoder
9227            .encode(&ArmOp::Subs {
9228                rd: Reg::R10,
9229                rn: Reg::R10,
9230                op2: Operand2::Reg(Reg::R8),
9231            })
9232            .unwrap();
9233        assert_eq!(
9234            subs,
9235            vec![0xBA, 0xEB, 0x08, 0x0A],
9236            "high-reg SUBS must be 32-bit SUBS.W (EBBA 0A08); got {subs:02X?}"
9237        );
9238    }
9239
9240    /// #184 (sibling of #180): 16-bit CMN (T1) only encodes R0-R7. High registers
9241    /// must use 32-bit CMN.W, not the corrupt truncated 16-bit form.
9242    #[test]
9243    fn test_encode_thumb_cmn_high_reg_uses_cmn_w_184() {
9244        let encoder = ArmEncoder::new_thumb2();
9245
9246        // cmn r10, r8  → CMN.W = EB1A 0F08 (ADD.W S=1, Rd=PC discarded).
9247        let cmn = encoder
9248            .encode(&ArmOp::Cmn {
9249                rn: Reg::R10,
9250                op2: Operand2::Reg(Reg::R8),
9251            })
9252            .unwrap();
9253        assert_eq!(
9254            cmn,
9255            vec![0x1A, 0xEB, 0x08, 0x0F],
9256            "high-reg CMN must be 32-bit CMN.W (EB1A 0F08); got {cmn:02X?}"
9257        );
9258
9259        // Low registers stay 16-bit: cmn r1, r2 = 0x42D1.
9260        let lo = encoder
9261            .encode(&ArmOp::Cmn {
9262                rn: Reg::R1,
9263                op2: Operand2::Reg(Reg::R2),
9264            })
9265            .unwrap();
9266        assert_eq!(
9267            lo.len(),
9268            2,
9269            "low-reg CMN should remain 16-bit, got {lo:02X?}"
9270        );
9271        assert_eq!(lo, vec![0xD1, 0x42], "low-reg CMN bytes wrong: {lo:02X?}");
9272    }
9273
9274    /// #185 regression: feeding PC (R15) as a data operand to a Thumb-2 op that
9275    /// guards its registers must return Err, not panic under debug-assertions.
9276    /// (Synth never emits PC here; the fuzz harness requires encode() be total.)
9277    #[test]
9278    fn test_encode_pc_operand_returns_err_not_panic_185() {
9279        let encoder = ArmEncoder::new_thumb2();
9280        for op in [
9281            ArmOp::Sdiv {
9282                rd: Reg::PC,
9283                rn: Reg::R0,
9284                rm: Reg::R1,
9285            },
9286            ArmOp::Udiv {
9287                rd: Reg::R0,
9288                rn: Reg::PC,
9289                rm: Reg::R1,
9290            },
9291            ArmOp::Sdiv {
9292                rd: Reg::R0,
9293                rn: Reg::R1,
9294                rm: Reg::PC,
9295            },
9296        ] {
9297            let r = encoder.encode(&op);
9298            assert!(
9299                r.is_err(),
9300                "encode({op:?}) must return Err for a PC operand, got {r:?}"
9301            );
9302        }
9303        // Valid registers still encode fine (no false rejection).
9304        assert!(
9305            encoder
9306                .encode(&ArmOp::Sdiv {
9307                    rd: Reg::R0,
9308                    rn: Reg::R1,
9309                    rm: Reg::R2
9310                })
9311                .is_ok()
9312        );
9313    }
9314
9315    #[test]
9316    fn test_encode_nop_arm32() {
9317        let encoder = ArmEncoder::new_arm32();
9318        let code = encoder.encode(&ArmOp::Nop).unwrap();
9319
9320        assert_eq!(code.len(), 4); // ARM32 instructions are 4 bytes
9321        assert_eq!(code, vec![0x00, 0x00, 0xA0, 0xE1]); // MOV R0, R0
9322    }
9323
9324    #[test]
9325    fn test_encode_nop_thumb() {
9326        let encoder = ArmEncoder::new_thumb2();
9327        let code = encoder.encode(&ArmOp::Nop).unwrap();
9328
9329        assert_eq!(code.len(), 2); // Thumb instructions are 2 bytes
9330        assert_eq!(code, vec![0x00, 0xBF]); // NOP
9331    }
9332
9333    #[test]
9334    fn test_encode_mov_immediate_arm32() {
9335        let encoder = ArmEncoder::new_arm32();
9336        let op = ArmOp::Mov {
9337            rd: Reg::R0,
9338            op2: Operand2::Imm(42),
9339        };
9340
9341        let code = encoder.encode(&op).unwrap();
9342        assert_eq!(code.len(), 4);
9343
9344        // Verify it's a MOV instruction (bits should have immediate flag set)
9345        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9346        assert_eq!(instr & 0x0E000000, 0x02000000); // Check I bit is set
9347    }
9348
9349    #[test]
9350    fn test_encode_add_registers_arm32() {
9351        let encoder = ArmEncoder::new_arm32();
9352        let op = ArmOp::Add {
9353            rd: Reg::R0,
9354            rn: Reg::R1,
9355            op2: Operand2::Reg(Reg::R2),
9356        };
9357
9358        let code = encoder.encode(&op).unwrap();
9359        assert_eq!(code.len(), 4);
9360
9361        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9362        // Verify it's an ADD instruction with correct opcode
9363        assert_eq!(instr & 0x0FE00000, 0x00800000);
9364    }
9365
9366    /// #350 — `encode_thumb32_add_imm` must lower an out-of-range immediate
9367    /// (> 0xFFF) to a legal MOVW(/MOVT) + ADD.W-register sequence instead of
9368    /// erroring. The small-imm fast path (imm <= 0xFFF) stays byte-identical.
9369    #[test]
9370    fn test_encode_add_imm_large_350() {
9371        let enc = ArmEncoder::new_thumb2();
9372
9373        // --- Fast path unchanged: imm <= 0xFFF is a single 4-byte ADD.W ---
9374        let small = enc
9375            .encode_thumb32_add_imm(&Reg::R0, &Reg::R1, 0x123)
9376            .unwrap();
9377        assert_eq!(small.len(), 4, "small imm must stay a single instruction");
9378
9379        // helper: decode a Thumb-2 MOVW/MOVT halfword pair back to its imm16
9380        fn movx_imm16(b: &[u8]) -> u32 {
9381            let hw1 = u16::from_le_bytes([b[0], b[1]]) as u32;
9382            let hw2 = u16::from_le_bytes([b[2], b[3]]) as u32;
9383            let imm4 = hw1 & 0xF;
9384            let i = (hw1 >> 10) & 1;
9385            let imm3 = (hw2 >> 12) & 0x7;
9386            let imm8 = hw2 & 0xFF;
9387            (imm4 << 12) | (i << 11) | (imm3 << 8) | imm8
9388        }
9389        fn movx_rd(b: &[u8]) -> u32 {
9390            (u16::from_le_bytes([b[2], b[3]]) as u32 >> 8) & 0xF
9391        }
9392
9393        // --- rd != rn: scratch is rd. imm = 70000 = 0x11170 needs MOVW+MOVT. ---
9394        // 0x11170: lo16 = 0x1170, hi16 = 0x0001
9395        let seq = enc
9396            .encode_thumb32_add_imm(&Reg::R12, &Reg::R0, 70000)
9397            .unwrap();
9398        assert_eq!(seq.len(), 12, "MOVW + MOVT + ADD = 12 bytes");
9399        // MOVW r12, #0x1170
9400        assert_eq!(u16::from_le_bytes([seq[0], seq[1]]) & 0xFBF0, 0xF240);
9401        assert_eq!(movx_rd(&seq[0..4]), 12);
9402        assert_eq!(movx_imm16(&seq[0..4]), 0x1170);
9403        // MOVT r12, #0x0001
9404        assert_eq!(u16::from_le_bytes([seq[4], seq[5]]) & 0xFBF0, 0xF2C0);
9405        assert_eq!(movx_rd(&seq[4..8]), 12);
9406        assert_eq!(movx_imm16(&seq[4..8]), 0x0001);
9407        // ADD.W r12, r0, r12  (EB00 | rn=0 ; rd=12, rm=12)
9408        let add1 = u16::from_le_bytes([seq[8], seq[9]]) as u32;
9409        let add2 = u16::from_le_bytes([seq[10], seq[11]]) as u32;
9410        assert_eq!(add1 & 0xFFF0, 0xEB00);
9411        assert_eq!(add1 & 0xF, 0); // rn = r0
9412        assert_eq!((add2 >> 8) & 0xF, 12); // rd = r12
9413        assert_eq!(add2 & 0xF, 12); // rm = scratch = r12
9414        // The materialized scratch must reconstruct exactly 70000.
9415        assert_eq!(
9416            (movx_imm16(&seq[4..8]) << 16) | movx_imm16(&seq[0..4]),
9417            70000
9418        );
9419
9420        // --- imm <= 0xFFFF: MOVT is skipped (MOVW + ADD = 8 bytes). ---
9421        let seq16 = enc
9422            .encode_thumb32_add_imm(&Reg::R3, &Reg::R0, 0xABCD)
9423            .unwrap();
9424        assert_eq!(seq16.len(), 8, "imm <= 0xFFFF skips MOVT");
9425        assert_eq!(movx_imm16(&seq16[0..4]), 0xABCD);
9426        assert_eq!(movx_rd(&seq16[0..4]), 3); // scratch = rd = r3
9427
9428        // --- rd == rn (in-place add): scratch must be R12, not rd. ---
9429        // imm = 0x12345: lo16 = 0x2345, hi16 = 0x0001
9430        let inplace = enc
9431            .encode_thumb32_add_imm(&Reg::R5, &Reg::R5, 0x12345)
9432            .unwrap();
9433        assert_eq!(inplace.len(), 12);
9434        assert_eq!(movx_rd(&inplace[0..4]), 12, "rd==rn must use R12 scratch");
9435        assert_eq!(
9436            (movx_imm16(&inplace[4..8]) << 16) | movx_imm16(&inplace[0..4]),
9437            0x12345
9438        );
9439        // ADD.W r5, r5, r12 — rm must be the scratch (12), never rn.
9440        let ip_add2 = u16::from_le_bytes([inplace[10], inplace[11]]) as u32;
9441        assert_eq!(ip_add2 & 0xF, 12);
9442        assert_eq!((ip_add2 >> 8) & 0xF, 5);
9443    }
9444
9445    /// #350 follow-up — the `encoder_no_panic` fuzz harness drives the encoder
9446    /// with ARBITRARY registers, including the one case the in-place lowering
9447    /// cannot serve: rd==rn==R12. There the scratch (R12, the reserved encoder
9448    /// register) would alias Rn and clobber it before the ADD reads it. The
9449    /// encoder contract (#180/#185) is Ok-or-Err, never a panic — so this must
9450    /// return Err, not assert. (Real codegen never emits rd==rn==R12 because R12
9451    /// is non-allocatable; this guards only the fuzz/adversarial path.)
9452    #[test]
9453    fn test_encode_add_imm_large_rd_rn_r12_errs_not_panics_350() {
9454        let enc = ArmEncoder::new_thumb2();
9455        // Out-of-range imm with rd==rn==R12: no free scratch -> Err.
9456        let r = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 70000);
9457        assert!(
9458            r.is_err(),
9459            "rd==rn==R12 with out-of-range imm must Err (no free scratch), got {r:?}"
9460        );
9461        // Small imm with rd==rn==R12 still takes the single-instruction fast path
9462        // (no scratch needed) and must succeed — the guard is scoped to the
9463        // out-of-range lowering only.
9464        let small = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 0x10);
9465        assert!(small.is_ok(), "small imm needs no scratch, must stay Ok");
9466    }
9467
9468    /// #378 — `encode_operand2` (ARM32 data-processing operand) must FAIL
9469    /// HONESTLY on an immediate that is not a valid rotated immediate, rather
9470    /// than silently masking it to `imm & 0xFF` and emitting a WRONG
9471    /// instruction. `0x1FF` has 9 set bits, so it cannot come from rotating an
9472    /// 8-bit imm8 — non-encodable. Real codegen materializes large constants via
9473    /// MOVW/MOVT; this guards the encoder's Ok-or-Err contract (#180/#185)
9474    /// directly. It is an Err (not a panic) so the `encoder_no_panic` fuzz
9475    /// harness — which drives arbitrary operands — still passes.
9476    #[test]
9477    fn test_encode_operand2_non_rotatable_imm_errs_not_masks_378() {
9478        let enc = ArmEncoder::new_arm32();
9479        let bad = enc.encode(&ArmOp::Add {
9480            rd: Reg::R0,
9481            rn: Reg::R1,
9482            op2: Operand2::Imm(0x1FF),
9483        });
9484        assert!(
9485            bad.is_err(),
9486            "non-rotatable ARM32 immediate 0x1FF must Err (was silently masked \
9487             to 0xFF), got {bad:?}"
9488        );
9489        // A representable rotated immediate still encodes fine (regression guard).
9490        let ok = enc.encode(&ArmOp::Add {
9491            rd: Reg::R0,
9492            rn: Reg::R1,
9493            op2: Operand2::Imm(0xFF),
9494        });
9495        assert!(
9496            ok.is_ok(),
9497            "0xFF is a valid rotated immediate, must stay Ok"
9498        );
9499    }
9500
9501    #[test]
9502    fn test_encode_ldr_arm32() {
9503        let encoder = ArmEncoder::new_arm32();
9504        let op = ArmOp::Ldr {
9505            rd: Reg::R0,
9506            addr: MemAddr::imm(Reg::R1, 4),
9507        };
9508
9509        let code = encoder.encode(&op).unwrap();
9510        assert_eq!(code.len(), 4);
9511
9512        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9513        // Verify load bit is set
9514        assert_eq!(instr & 0x00100000, 0x00100000);
9515    }
9516
9517    #[test]
9518    fn test_encode_str_arm32() {
9519        let encoder = ArmEncoder::new_arm32();
9520        let op = ArmOp::Str {
9521            rd: Reg::R0,
9522            addr: MemAddr::imm(Reg::SP, 0),
9523        };
9524
9525        let code = encoder.encode(&op).unwrap();
9526        assert_eq!(code.len(), 4);
9527    }
9528
9529    #[test]
9530    fn test_encode_branch_arm32() {
9531        let encoder = ArmEncoder::new_arm32();
9532        let op = ArmOp::Bl {
9533            label: "main".to_string(),
9534        };
9535
9536        let code = encoder.encode(&op).unwrap();
9537        assert_eq!(code.len(), 4);
9538
9539        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9540        // Verify BL opcode
9541        assert_eq!(instr & 0x0F000000, 0x0B000000);
9542    }
9543
9544    /// Regression test for #167 + #174: the Thumb-2 BL relocatable placeholder
9545    /// must carry a -4 addend so an R_ARM_THM_CALL nets to exactly the symbol S.
9546    /// The correct encoding is what `gas` emits for `bl <extern>`: f7ff fffe
9547    /// (hw1=0xF7FF, hw2=0xFFFE), little-endian bytes FF F7 FE FF.
9548    ///   - 0xD000 (J1=J2=0) → ~+0x600000 garbage addend: `bl c0000c` / truncated
9549    ///     to fit (#167).
9550    ///   - 0xF800 (addend 0) → lands at S+4, one instruction past the callee
9551    ///     entry (#174).
9552    ///   - 0xFFFE (addend -4) → lands at S. Correct.
9553    #[test]
9554    fn test_encode_thumb_bl_placeholder_addend_167_174() {
9555        let encoder = ArmEncoder::new_thumb2();
9556        let op = ArmOp::Bl {
9557            label: "callee".to_string(),
9558        };
9559
9560        let code = encoder.encode(&op).unwrap();
9561        assert_eq!(code.len(), 4, "Thumb-2 BL is 32-bit");
9562
9563        let hw1 = u16::from_le_bytes([code[0], code[1]]);
9564        let hw2 = u16::from_le_bytes([code[2], code[3]]);
9565        assert_eq!(hw1, 0xF7FF, "BL first halfword (matches gas `bl <extern>`)");
9566        assert_eq!(
9567            hw2, 0xFFFE,
9568            "BL second halfword must be 0xFFFE (-4 addend → nets to S), not 0xF800 (→ S+4, #174) or 0xD000 (#167)"
9569        );
9570        assert_ne!(hw2, 0xF800, "0xF800 (addend 0) lands at S+4 (#174)");
9571        assert_ne!(hw2, 0xD000, "0xD000 bakes in a ~+0x600000 addend (#167)");
9572    }
9573
9574    #[test]
9575    fn test_encode_sequence() {
9576        let encoder = ArmEncoder::new_arm32();
9577        let ops = vec![
9578            ArmOp::Mov {
9579                rd: Reg::R0,
9580                op2: Operand2::Imm(42),
9581            },
9582            ArmOp::Mov {
9583                rd: Reg::R1,
9584                op2: Operand2::Imm(10),
9585            },
9586            ArmOp::Add {
9587                rd: Reg::R2,
9588                rn: Reg::R0,
9589                op2: Operand2::Reg(Reg::R1),
9590            },
9591        ];
9592
9593        let code = encoder.encode_sequence(&ops).unwrap();
9594        assert_eq!(code.len(), 12); // 3 instructions * 4 bytes
9595    }
9596
9597    #[test]
9598    fn test_reg_to_bits() {
9599        assert_eq!(reg_to_bits(&Reg::R0), 0);
9600        assert_eq!(reg_to_bits(&Reg::R7), 7);
9601        assert_eq!(reg_to_bits(&Reg::SP), 13);
9602        assert_eq!(reg_to_bits(&Reg::LR), 14);
9603        assert_eq!(reg_to_bits(&Reg::PC), 15);
9604    }
9605
9606    #[test]
9607    fn test_encode_bitwise_operations() {
9608        let encoder = ArmEncoder::new_arm32();
9609
9610        let and_op = ArmOp::And {
9611            rd: Reg::R0,
9612            rn: Reg::R1,
9613            op2: Operand2::Reg(Reg::R2),
9614        };
9615        let and_code = encoder.encode(&and_op).unwrap();
9616        assert_eq!(and_code.len(), 4);
9617
9618        let orr_op = ArmOp::Orr {
9619            rd: Reg::R0,
9620            rn: Reg::R1,
9621            op2: Operand2::Reg(Reg::R2),
9622        };
9623        let orr_code = encoder.encode(&orr_op).unwrap();
9624        assert_eq!(orr_code.len(), 4);
9625
9626        let eor_op = ArmOp::Eor {
9627            rd: Reg::R0,
9628            rn: Reg::R1,
9629            op2: Operand2::Reg(Reg::R2),
9630        };
9631        let eor_code = encoder.encode(&eor_op).unwrap();
9632        assert_eq!(eor_code.len(), 4);
9633    }
9634
9635    // === Thumb-2 32-bit encoding tests ===
9636
9637    #[test]
9638    fn test_encode_sdiv_thumb2() {
9639        let encoder = ArmEncoder::new_thumb2();
9640        let op = ArmOp::Sdiv {
9641            rd: Reg::R0,
9642            rn: Reg::R1,
9643            rm: Reg::R2,
9644        };
9645
9646        let code = encoder.encode(&op).unwrap();
9647        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9648
9649        // SDIV R0, R1, R2: 0xFB91 0xF0F2
9650        // First halfword: 0xFB90 | Rn(1) = 0xFB91
9651        // Second halfword: 0xF0F0 | Rd(0)<<8 | Rm(2) = 0xF0F2
9652        // Little-endian: [0x91, 0xFB, 0xF2, 0xF0]
9653        assert_eq!(code[0], 0x91);
9654        assert_eq!(code[1], 0xFB);
9655        assert_eq!(code[2], 0xF2);
9656        assert_eq!(code[3], 0xF0);
9657    }
9658
9659    #[test]
9660    fn test_encode_udiv_thumb2() {
9661        let encoder = ArmEncoder::new_thumb2();
9662        let op = ArmOp::Udiv {
9663            rd: Reg::R0,
9664            rn: Reg::R1,
9665            rm: Reg::R2,
9666        };
9667
9668        let code = encoder.encode(&op).unwrap();
9669        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9670
9671        // UDIV R0, R1, R2: 0xFBB1 0xF0F2
9672        // Little-endian: [0xB1, 0xFB, 0xF2, 0xF0]
9673        assert_eq!(code[0], 0xB1);
9674        assert_eq!(code[1], 0xFB);
9675        assert_eq!(code[2], 0xF2);
9676        assert_eq!(code[3], 0xF0);
9677    }
9678
9679    #[test]
9680    fn test_encode_mul_thumb2() {
9681        let encoder = ArmEncoder::new_thumb2();
9682        let op = ArmOp::Mul {
9683            rd: Reg::R0,
9684            rn: Reg::R1,
9685            rm: Reg::R2,
9686        };
9687
9688        let code = encoder.encode(&op).unwrap();
9689        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9690    }
9691
9692    #[test]
9693    fn test_encode_and_thumb2() {
9694        let encoder = ArmEncoder::new_thumb2();
9695        let op = ArmOp::And {
9696            rd: Reg::R0,
9697            rn: Reg::R1,
9698            op2: Operand2::Reg(Reg::R2),
9699        };
9700
9701        let code = encoder.encode(&op).unwrap();
9702        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9703    }
9704
9705    #[test]
9706    fn test_encode_lsl_thumb2_low_regs() {
9707        let encoder = ArmEncoder::new_thumb2();
9708        let op = ArmOp::Lsl {
9709            rd: Reg::R0,
9710            rn: Reg::R1,
9711            shift: 5,
9712        };
9713
9714        let code = encoder.encode(&op).unwrap();
9715        assert_eq!(code.len(), 2); // 16-bit for low registers
9716    }
9717
9718    #[test]
9719    fn test_encode_clz_thumb2() {
9720        let encoder = ArmEncoder::new_thumb2();
9721        let op = ArmOp::Clz {
9722            rd: Reg::R0,
9723            rm: Reg::R1,
9724        };
9725
9726        let code = encoder.encode(&op).unwrap();
9727        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9728    }
9729
9730    #[test]
9731    fn test_encode_bx_thumb2() {
9732        let encoder = ArmEncoder::new_thumb2();
9733        let op = ArmOp::Bx { rm: Reg::LR };
9734
9735        let code = encoder.encode(&op).unwrap();
9736        assert_eq!(code.len(), 2); // 16-bit instruction
9737
9738        // BX LR: 0x4770
9739        assert_eq!(code, vec![0x70, 0x47]);
9740    }
9741
9742    // ========================================================================
9743    // f32 pseudo-op encoding tests
9744    // ========================================================================
9745
9746    #[test]
9747    fn test_encode_f32_abs_arm32() {
9748        let encoder = ArmEncoder::new_arm32();
9749        let op = ArmOp::F32Abs {
9750            sd: VfpReg::S0,
9751            sm: VfpReg::S2,
9752        };
9753        let code = encoder.encode(&op).unwrap();
9754        assert_eq!(code.len(), 4); // Single VFP instruction
9755    }
9756
9757    #[test]
9758    fn test_encode_f32_neg_arm32() {
9759        let encoder = ArmEncoder::new_arm32();
9760        let op = ArmOp::F32Neg {
9761            sd: VfpReg::S0,
9762            sm: VfpReg::S2,
9763        };
9764        let code = encoder.encode(&op).unwrap();
9765        assert_eq!(code.len(), 4);
9766    }
9767
9768    #[test]
9769    fn test_encode_f32_sqrt_arm32() {
9770        let encoder = ArmEncoder::new_arm32();
9771        let op = ArmOp::F32Sqrt {
9772            sd: VfpReg::S0,
9773            sm: VfpReg::S2,
9774        };
9775        let code = encoder.encode(&op).unwrap();
9776        assert_eq!(code.len(), 4);
9777    }
9778
9779    #[test]
9780    fn test_encode_f32_ceil_arm32() {
9781        let encoder = ArmEncoder::new_arm32();
9782        let op = ArmOp::F32Ceil {
9783            sd: VfpReg::S0,
9784            sm: VfpReg::S2,
9785        };
9786        let code = encoder.encode(&op).unwrap();
9787        // VMRS + BIC + ORR + VMSR + VCVT.S32.F32 + VMRS + BIC + VMSR + VCVT.F32.S32
9788        assert_eq!(code.len(), 36);
9789    }
9790
9791    #[test]
9792    fn test_encode_f32_floor_thumb2() {
9793        let encoder = ArmEncoder::new_thumb2();
9794        let op = ArmOp::F32Floor {
9795            sd: VfpReg::S0,
9796            sm: VfpReg::S2,
9797        };
9798        let code = encoder.encode(&op).unwrap();
9799        // VMRS + BIC.W + ORR.W + VMSR + VCVT + VMRS + BIC.W + VMSR + VCVT.F32.S32
9800        assert_eq!(code.len(), 36);
9801    }
9802
9803    #[test]
9804    fn test_encode_f32_min_arm32() {
9805        let encoder = ArmEncoder::new_arm32();
9806        let op = ArmOp::F32Min {
9807            sd: VfpReg::S0,
9808            sn: VfpReg::S2,
9809            sm: VfpReg::S4,
9810        };
9811        let code = encoder.encode(&op).unwrap();
9812        assert_eq!(code.len(), 16); // VMOV + VCMP + VMRS + conditional VMOV
9813    }
9814
9815    #[test]
9816    fn test_encode_f32_max_thumb2() {
9817        let encoder = ArmEncoder::new_thumb2();
9818        let op = ArmOp::F32Max {
9819            sd: VfpReg::S0,
9820            sn: VfpReg::S2,
9821            sm: VfpReg::S4,
9822        };
9823        let code = encoder.encode(&op).unwrap();
9824        // VMOV(4) + VCMP(4) + VMRS(4) + IT(2) + VMOV(4) = 18
9825        assert_eq!(code.len(), 18);
9826    }
9827
9828    #[test]
9829    fn test_encode_f32_copysign_arm32() {
9830        let encoder = ArmEncoder::new_arm32();
9831        let op = ArmOp::F32Copysign {
9832            sd: VfpReg::S0,
9833            sn: VfpReg::S2,
9834            sm: VfpReg::S4,
9835        };
9836        let code = encoder.encode(&op).unwrap();
9837        // VMOV + VMOV + AND + BIC + ORR + VMOV = 6 * 4 = 24
9838        assert_eq!(code.len(), 24);
9839    }
9840
9841    // ========================================================================
9842    // f64 encoding tests
9843    // ========================================================================
9844
9845    #[test]
9846    fn test_encode_f64_add_arm32() {
9847        let encoder = ArmEncoder::new_arm32();
9848        let op = ArmOp::F64Add {
9849            dd: VfpReg::D0,
9850            dn: VfpReg::D1,
9851            dm: VfpReg::D2,
9852        };
9853        let code = encoder.encode(&op).unwrap();
9854        assert_eq!(code.len(), 4);
9855        // VADD.F64 D0, D1, D2: check coprocessor is cp11 (0xB)
9856        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9857        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
9858    }
9859
9860    #[test]
9861    fn test_encode_f64_sub_thumb2() {
9862        let encoder = ArmEncoder::new_thumb2();
9863        let op = ArmOp::F64Sub {
9864            dd: VfpReg::D0,
9865            dn: VfpReg::D1,
9866            dm: VfpReg::D2,
9867        };
9868        let code = encoder.encode(&op).unwrap();
9869        assert_eq!(code.len(), 4); // 32-bit VFP as two Thumb halfwords
9870    }
9871
9872    #[test]
9873    fn test_encode_f64_mul_arm32() {
9874        let encoder = ArmEncoder::new_arm32();
9875        let op = ArmOp::F64Mul {
9876            dd: VfpReg::D0,
9877            dn: VfpReg::D1,
9878            dm: VfpReg::D2,
9879        };
9880        let code = encoder.encode(&op).unwrap();
9881        assert_eq!(code.len(), 4);
9882    }
9883
9884    #[test]
9885    fn test_encode_f64_div_arm32() {
9886        let encoder = ArmEncoder::new_arm32();
9887        let op = ArmOp::F64Div {
9888            dd: VfpReg::D0,
9889            dn: VfpReg::D1,
9890            dm: VfpReg::D2,
9891        };
9892        let code = encoder.encode(&op).unwrap();
9893        assert_eq!(code.len(), 4);
9894    }
9895
9896    #[test]
9897    fn test_encode_f64_abs_arm32() {
9898        let encoder = ArmEncoder::new_arm32();
9899        let op = ArmOp::F64Abs {
9900            dd: VfpReg::D0,
9901            dm: VfpReg::D2,
9902        };
9903        let code = encoder.encode(&op).unwrap();
9904        assert_eq!(code.len(), 4);
9905    }
9906
9907    #[test]
9908    fn test_encode_f64_neg_arm32() {
9909        let encoder = ArmEncoder::new_arm32();
9910        let op = ArmOp::F64Neg {
9911            dd: VfpReg::D0,
9912            dm: VfpReg::D2,
9913        };
9914        let code = encoder.encode(&op).unwrap();
9915        assert_eq!(code.len(), 4);
9916    }
9917
9918    #[test]
9919    fn test_encode_f64_sqrt_arm32() {
9920        let encoder = ArmEncoder::new_arm32();
9921        let op = ArmOp::F64Sqrt {
9922            dd: VfpReg::D0,
9923            dm: VfpReg::D2,
9924        };
9925        let code = encoder.encode(&op).unwrap();
9926        assert_eq!(code.len(), 4);
9927    }
9928
9929    #[test]
9930    fn test_encode_f64_load_arm32() {
9931        let encoder = ArmEncoder::new_arm32();
9932        let op = ArmOp::F64Load {
9933            dd: VfpReg::D0,
9934            addr: MemAddr::imm(Reg::R0, 8),
9935        };
9936        let code = encoder.encode(&op).unwrap();
9937        assert_eq!(code.len(), 4);
9938        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9939        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11 for F64
9940        assert_eq!(instr & 0xFF, 2); // offset 8 / 4 = 2
9941    }
9942
9943    #[test]
9944    fn test_encode_f64_store_thumb2() {
9945        let encoder = ArmEncoder::new_thumb2();
9946        let op = ArmOp::F64Store {
9947            dd: VfpReg::D0,
9948            addr: MemAddr::imm(Reg::SP, 0),
9949        };
9950        let code = encoder.encode(&op).unwrap();
9951        assert_eq!(code.len(), 4);
9952    }
9953
9954    #[test]
9955    fn test_encode_f64_compare_arm32() {
9956        let encoder = ArmEncoder::new_arm32();
9957        let op = ArmOp::F64Eq {
9958            rd: Reg::R0,
9959            dn: VfpReg::D0,
9960            dm: VfpReg::D1,
9961        };
9962        let code = encoder.encode(&op).unwrap();
9963        assert_eq!(code.len(), 16); // VCMP + VMRS + MOV #0 + MOVcond #1
9964    }
9965
9966    #[test]
9967    fn test_encode_f64_compare_thumb2() {
9968        let encoder = ArmEncoder::new_thumb2();
9969        let op = ArmOp::F64Lt {
9970            rd: Reg::R0,
9971            dn: VfpReg::D0,
9972            dm: VfpReg::D1,
9973        };
9974        let code = encoder.encode(&op).unwrap();
9975        // VCMP(4) + VMRS(4) + MOVS(2) + IT(2) + MOV(2) = 14
9976        assert_eq!(code.len(), 14);
9977    }
9978
9979    #[test]
9980    fn test_encode_f64_const_arm32() {
9981        let encoder = ArmEncoder::new_arm32();
9982        let op = ArmOp::F64Const {
9983            dd: VfpReg::D0,
9984            value: 3.125,
9985        };
9986        let code = encoder.encode(&op).unwrap();
9987        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
9988        assert_eq!(code.len(), 20);
9989    }
9990
9991    #[test]
9992    fn test_encode_f64_const_thumb2() {
9993        let encoder = ArmEncoder::new_thumb2();
9994        let op = ArmOp::F64Const {
9995            dd: VfpReg::D0,
9996            value: 2.5,
9997        };
9998        let code = encoder.encode(&op).unwrap();
9999        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
10000        assert_eq!(code.len(), 20);
10001    }
10002
10003    #[test]
10004    fn test_encode_f64_convert_i32s_arm32() {
10005        let encoder = ArmEncoder::new_arm32();
10006        let op = ArmOp::F64ConvertI32S {
10007            dd: VfpReg::D0,
10008            rm: Reg::R0,
10009        };
10010        let code = encoder.encode(&op).unwrap();
10011        // VMOV(4) + VCVT(4) = 8
10012        assert_eq!(code.len(), 8);
10013    }
10014
10015    #[test]
10016    fn test_encode_f64_promote_f32_arm32() {
10017        let encoder = ArmEncoder::new_arm32();
10018        let op = ArmOp::F64PromoteF32 {
10019            dd: VfpReg::D0,
10020            sm: VfpReg::S0,
10021        };
10022        let code = encoder.encode(&op).unwrap();
10023        assert_eq!(code.len(), 4); // Single VCVT.F64.F32 instruction
10024    }
10025
10026    #[test]
10027    fn test_encode_f64_promote_f32_thumb2() {
10028        let encoder = ArmEncoder::new_thumb2();
10029        let op = ArmOp::F64PromoteF32 {
10030            dd: VfpReg::D0,
10031            sm: VfpReg::S0,
10032        };
10033        let code = encoder.encode(&op).unwrap();
10034        assert_eq!(code.len(), 4);
10035    }
10036
10037    #[test]
10038    fn test_encode_i32_trunc_f64s_arm32() {
10039        let encoder = ArmEncoder::new_arm32();
10040        let op = ArmOp::I32TruncF64S {
10041            rd: Reg::R0,
10042            dm: VfpReg::D0,
10043        };
10044        let code = encoder.encode(&op).unwrap();
10045        // VCVT(4) + VMOV(4) = 8
10046        assert_eq!(code.len(), 8);
10047    }
10048
10049    #[test]
10050    fn test_encode_f64_reinterpret_i64_arm32() {
10051        let encoder = ArmEncoder::new_arm32();
10052        let op = ArmOp::F64ReinterpretI64 {
10053            dd: VfpReg::D0,
10054            rmlo: Reg::R0,
10055            rmhi: Reg::R1,
10056        };
10057        let code = encoder.encode(&op).unwrap();
10058        assert_eq!(code.len(), 4); // Single VMOV instruction
10059    }
10060
10061    #[test]
10062    fn test_encode_i64_reinterpret_f64_thumb2() {
10063        let encoder = ArmEncoder::new_thumb2();
10064        let op = ArmOp::I64ReinterpretF64 {
10065            rdlo: Reg::R0,
10066            rdhi: Reg::R1,
10067            dm: VfpReg::D0,
10068        };
10069        let code = encoder.encode(&op).unwrap();
10070        assert_eq!(code.len(), 4);
10071    }
10072
10073    #[test]
10074    fn test_encode_f64_trunc_thumb2() {
10075        let encoder = ArmEncoder::new_thumb2();
10076        let op = ArmOp::F64Trunc {
10077            dd: VfpReg::D0,
10078            dm: VfpReg::D1,
10079        };
10080        let code = encoder.encode(&op).unwrap();
10081        // Two VFP instructions via Thumb encoding
10082        assert_eq!(code.len(), 8);
10083    }
10084
10085    #[test]
10086    fn test_encode_f64_min_arm32() {
10087        let encoder = ArmEncoder::new_arm32();
10088        let op = ArmOp::F64Min {
10089            dd: VfpReg::D0,
10090            dn: VfpReg::D1,
10091            dm: VfpReg::D2,
10092        };
10093        let code = encoder.encode(&op).unwrap();
10094        // VMOV + VCMP + VMRS + conditional VMOV = 16
10095        assert_eq!(code.len(), 16);
10096    }
10097
10098    #[test]
10099    fn test_f64_cp11_encoding() {
10100        // Verify that F64 instructions use coprocessor 11 (0xB), not 10 (0xA)
10101        let encoder = ArmEncoder::new_arm32();
10102
10103        // F64Add
10104        let code = encoder
10105            .encode(&ArmOp::F64Add {
10106                dd: VfpReg::D0,
10107                dn: VfpReg::D0,
10108                dm: VfpReg::D0,
10109            })
10110            .unwrap();
10111        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10112        assert_eq!((instr >> 8) & 0xF, 0xB, "F64 should use cp11");
10113
10114        // F32Add for comparison
10115        let code = encoder
10116            .encode(&ArmOp::F32Add {
10117                sd: VfpReg::S0,
10118                sn: VfpReg::S0,
10119                sm: VfpReg::S0,
10120            })
10121            .unwrap();
10122        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10123        assert_eq!((instr >> 8) & 0xF, 0xA, "F32 should use cp10");
10124    }
10125
10126    #[test]
10127    fn test_dreg_encoding_higher_registers() {
10128        let encoder = ArmEncoder::new_arm32();
10129
10130        // Test with D15 (highest register)
10131        let op = ArmOp::F64Add {
10132            dd: VfpReg::D15,
10133            dn: VfpReg::D14,
10134            dm: VfpReg::D13,
10135        };
10136        let code = encoder.encode(&op).unwrap();
10137        assert_eq!(code.len(), 4);
10138
10139        // Verify the register encoding worked (instruction is valid)
10140        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10141        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
10142    }
10143
10144    // ========================================================================
10145    // Control flow encoding tests
10146    // ========================================================================
10147
10148    #[test]
10149    fn test_encode_label_emits_no_bytes() {
10150        let encoder = ArmEncoder::new_thumb2();
10151        let op = ArmOp::Label {
10152            name: ".Lblock_end_0".to_string(),
10153        };
10154        let code = encoder.encode(&op).unwrap();
10155        assert!(code.is_empty(), "Label should emit zero bytes");
10156
10157        let encoder32 = ArmEncoder::new_arm32();
10158        let code32 = encoder32.encode(&op).unwrap();
10159        assert!(
10160            code32.is_empty(),
10161            "Label should emit zero bytes in ARM32 too"
10162        );
10163    }
10164
10165    #[test]
10166    fn test_encode_bcc_eq_thumb2() {
10167        use synth_synthesis::Condition;
10168        let encoder = ArmEncoder::new_thumb2();
10169        let op = ArmOp::Bcc {
10170            cond: Condition::EQ,
10171            label: "target".to_string(),
10172        };
10173        let code = encoder.encode(&op).unwrap();
10174        assert_eq!(code.len(), 2); // 16-bit conditional branch
10175
10176        // BEQ with offset 0: 0xD000 in little-endian
10177        assert_eq!(code, vec![0x00, 0xD0]);
10178    }
10179
10180    #[test]
10181    fn test_encode_bcc_ne_thumb2() {
10182        use synth_synthesis::Condition;
10183        let encoder = ArmEncoder::new_thumb2();
10184        let op = ArmOp::Bcc {
10185            cond: Condition::NE,
10186            label: "target".to_string(),
10187        };
10188        let code = encoder.encode(&op).unwrap();
10189        assert_eq!(code.len(), 2);
10190
10191        // BNE with offset 0: 0xD100 in little-endian
10192        assert_eq!(code, vec![0x00, 0xD1]);
10193    }
10194
10195    #[test]
10196    fn test_encode_bcc_arm32() {
10197        use synth_synthesis::Condition;
10198        let encoder = ArmEncoder::new_arm32();
10199        let op = ArmOp::Bcc {
10200            cond: Condition::EQ,
10201            label: "target".to_string(),
10202        };
10203        let code = encoder.encode(&op).unwrap();
10204        assert_eq!(code.len(), 4); // 32-bit ARM instruction
10205
10206        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10207        // BEQ: cond=0x0, opcode=0xA, offset=0
10208        assert_eq!(instr & 0xF0000000, 0x00000000); // EQ condition
10209        assert_eq!(instr & 0x0F000000, 0x0A000000); // Branch opcode
10210    }
10211
10212    #[test]
10213    fn test_encode_udf_thumb2() {
10214        let encoder = ArmEncoder::new_thumb2();
10215        let op = ArmOp::Udf { imm: 0 };
10216        let code = encoder.encode(&op).unwrap();
10217        assert_eq!(code.len(), 2); // 16-bit
10218
10219        // UDF #0: 0xDE00 in little-endian
10220        assert_eq!(code, vec![0x00, 0xDE]);
10221    }
10222
10223    /// #610: the i64 rot/div/rem expansions must land the result in the
10224    /// selector-assigned rd pair and leave R0-R3 preserved (restored from the
10225    /// fixed-ABI wrapper's save area) — pre-#610 the rot expansion's own
10226    /// `POP {R4}` restored stale scratch OVER the result (rd_lo == R4) and
10227    /// the div/rem expansions ignored their register fields outright.
10228    #[test]
10229    fn test_610_i64_rot_expansion_ends_with_rd_movs_and_restore() {
10230        let encoder = ArmEncoder::new_thumb2();
10231        for op in [
10232            ArmOp::I64Rotl {
10233                rdlo: Reg::R4,
10234                rdhi: Reg::R5,
10235                rnlo: Reg::R0,
10236                rnhi: Reg::R1,
10237                shift: Reg::R2,
10238            },
10239            ArmOp::I64Rotr {
10240                rdlo: Reg::R4,
10241                rdhi: Reg::R5,
10242                rnlo: Reg::R0,
10243                rnhi: Reg::R1,
10244                shift: Reg::R2,
10245            },
10246        ] {
10247            let code = encoder.encode(&op).unwrap();
10248            assert_eq!(code.len(), 102, "register-independent size (estimator pin)");
10249            // Tail: MOV r5, r1 (0x460D); MOV r4, r0 (0x4604); POP {r0..r3}
10250            // (rd pair r4:r5 does not overlap the save area — all 4 restored).
10251            let tail: Vec<u16> = code[code.len() - 12..]
10252                .chunks(2)
10253                .map(|c| u16::from_le_bytes([c[0], c[1]]))
10254                .collect();
10255            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
10256        }
10257    }
10258
10259    /// #610: div/rem expansions honor rd and carry the divide-by-zero trap
10260    /// guard (`ORRS R12, R2, R3; BNE +0; UDF #0`) after operand marshaling.
10261    #[test]
10262    fn test_610_i64_div_rem_expansion_guard_and_rd() {
10263        let encoder = ArmEncoder::new_thumb2();
10264        let mk = |which: u8| {
10265            let (rdlo, rdhi, rnlo, rnhi, rmlo, rmhi) =
10266                (Reg::R4, Reg::R5, Reg::R0, Reg::R1, Reg::R2, Reg::R3);
10267            match which {
10268                0 => ArmOp::I64DivU {
10269                    rdlo,
10270                    rdhi,
10271                    rnlo,
10272                    rnhi,
10273                    rmlo,
10274                    rmhi,
10275                    elide_zero_guard: false,
10276                },
10277                1 => ArmOp::I64RemU {
10278                    rdlo,
10279                    rdhi,
10280                    rnlo,
10281                    rnhi,
10282                    rmlo,
10283                    rmhi,
10284                    elide_zero_guard: false,
10285                },
10286                2 => ArmOp::I64DivS {
10287                    rdlo,
10288                    rdhi,
10289                    rnlo,
10290                    rnhi,
10291                    rmlo,
10292                    rmhi,
10293                    elide_zero_guard: false,
10294                    elide_overflow_guard: false,
10295                },
10296                _ => ArmOp::I64RemS {
10297                    rdlo,
10298                    rdhi,
10299                    rnlo,
10300                    rnhi,
10301                    rmlo,
10302                    rmhi,
10303                    elide_zero_guard: false,
10304                },
10305            }
10306        };
10307        for which in 0..4u8 {
10308            let code = encoder.encode(&mk(which)).unwrap();
10309            // Zero-divisor trap guard right after the 26-byte marshal prologue.
10310            let guard: Vec<u16> = code[26..34]
10311                .chunks(2)
10312                .map(|c| u16::from_le_bytes([c[0], c[1]]))
10313                .collect();
10314            assert_eq!(
10315                guard,
10316                vec![0xEA52, 0x0C03, 0xD100, 0xDE00],
10317                "ORRS R12,R2,R3; BNE +0; UDF #0"
10318            );
10319            // Tail: result into rd pair (r5:r4), then restore all of R0-R3.
10320            let tail: Vec<u16> = code[code.len() - 12..]
10321                .chunks(2)
10322                .map(|c| u16::from_le_bytes([c[0], c[1]]))
10323                .collect();
10324            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
10325        }
10326    }
10327
10328    /// #610: when rd overlaps R0-R3 the restore must SKIP the result
10329    /// registers (drop the saved caller word) instead of popping over them.
10330    #[test]
10331    fn test_610_i64_divu_rd_in_r0_r1_skips_restore() {
10332        let encoder = ArmEncoder::new_thumb2();
10333        let code = encoder
10334            .encode(&ArmOp::I64DivU {
10335                rdlo: Reg::R0,
10336                rdhi: Reg::R1,
10337                rnlo: Reg::R0,
10338                rnhi: Reg::R1,
10339                rmlo: Reg::R2,
10340                rmhi: Reg::R3,
10341                elide_zero_guard: false,
10342            })
10343            .unwrap();
10344        let tail: Vec<u16> = code[code.len() - 12..]
10345            .chunks(2)
10346            .map(|c| u16::from_le_bytes([c[0], c[1]]))
10347            .collect();
10348        // MOV r1,r1 / MOV r0,r0 (no-ops, size-stable), ADD SP,#4 twice
10349        // (discard saved r0/r1 — the result lives there), POP {r2}, POP {r3}.
10350        assert_eq!(tail, vec![0x4609, 0x4600, 0xB001, 0xB001, 0xBC04, 0xBC08]);
10351    }
10352
10353    /// #610: a fully swapped rd pair (rd_lo=R1, rd_hi=R0) cannot be
10354    /// materialized by two MOVs in either order — must be a loud Err, never
10355    /// silent corruption. (Selector pairs are consecutive, so unreachable.)
10356    #[test]
10357    fn test_610_i64_swapped_rd_pair_rejected() {
10358        let encoder = ArmEncoder::new_thumb2();
10359        let result = encoder.encode(&ArmOp::I64RemU {
10360            rdlo: Reg::R1,
10361            rdhi: Reg::R0,
10362            rnlo: Reg::R2,
10363            rnhi: Reg::R3,
10364            rmlo: Reg::R4,
10365            rmhi: Reg::R5,
10366            elide_zero_guard: false,
10367        });
10368        assert!(result.is_err(), "swapped rd pair must be rejected loudly");
10369    }
10370
10371    /// #632: the I64Popcnt expansion's own scratch restore (`POP {R3,R4,R5}`)
10372    /// must not clobber the result. Pre-fix the total was materialized with
10373    /// `ADDS rd, R4, R5` BEFORE the pop, so any allocator-assigned
10374    /// rd ∈ {R3,R4,R5} received stale stack garbage. Post-fix the count is
10375    /// carried across the restore in R12 (never allocatable, never restored)
10376    /// and moved into rd only after the pop — structurally rd-independent.
10377    #[test]
10378    fn test_632_i64_popcnt_result_survives_scratch_restore() {
10379        let encoder = ArmEncoder::new_thumb2();
10380        // Every allocatable rd, including the restore set {R3,R4,R5} and R8.
10381        for rd in [
10382            Reg::R0,
10383            Reg::R2,
10384            Reg::R3,
10385            Reg::R4,
10386            Reg::R5,
10387            Reg::R6,
10388            Reg::R8,
10389        ] {
10390            let code = encoder
10391                .encode(&ArmOp::I64Popcnt {
10392                    rd,
10393                    rnlo: Reg::R6,
10394                    rnhi: Reg::R7,
10395                })
10396                .unwrap();
10397            assert_eq!(code.len(), 180, "register-independent size (estimator pin)");
10398            let hw: Vec<u16> = code
10399                .chunks(2)
10400                .map(|c| u16::from_le_bytes([c[0], c[1]]))
10401                .collect();
10402            let pop = hw
10403                .iter()
10404                .position(|&h| h == 0xBC38)
10405                .expect("POP {R3,R4,R5} present");
10406            // Immediately before the POP: ADD.W R12, R4, R5 (the total lives
10407            // in R12, which the POP cannot touch).
10408            assert_eq!(
10409                &hw[pop - 2..pop],
10410                &[0xEB04, 0x0C05],
10411                "total must be carried in R12 across the restore"
10412            );
10413            // Immediately after the POP: MOV rd, R12.
10414            let rd_bits = match rd {
10415                Reg::R8 => 8u16,
10416                Reg::R6 => 6,
10417                Reg::R5 => 5,
10418                Reg::R4 => 4,
10419                Reg::R3 => 3,
10420                Reg::R2 => 2,
10421                _ => 0,
10422            };
10423            let expect_mov = 0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7);
10424            assert_eq!(hw[pop + 1], expect_mov, "MOV rd, R12 after the restore");
10425            // No write into rd between the PUSH and the POP (the old
10426            // pre-restore ADDS is gone).
10427            assert!(
10428                !hw[..pop].contains(&(0x1800 | (5 << 6) | (4 << 3) | rd_bits)),
10429                "no ADDS rd, R4, R5 before the restore pop"
10430            );
10431        }
10432    }
10433
10434    /// #632 audit: the entry marshal must be permutation-safe. Pre-fix
10435    /// `MOV R4, rnlo; MOV R5, rnhi` read a clobbered R4 when the operand
10436    /// pair lived at (R3, R4). Post-fix rnlo routes through R12.
10437    #[test]
10438    fn test_632_i64_popcnt_marshal_pair_at_r3_r4() {
10439        let encoder = ArmEncoder::new_thumb2();
10440        let code = encoder
10441            .encode(&ArmOp::I64Popcnt {
10442                rd: Reg::R0,
10443                rnlo: Reg::R3,
10444                rnhi: Reg::R4,
10445            })
10446            .unwrap();
10447        let hw: Vec<u16> = code
10448            .chunks(2)
10449            .map(|c| u16::from_le_bytes([c[0], c[1]]))
10450            .collect();
10451        // PUSH {R3,R4,R5}; MOV R12, R3; MOV R5, R4 (rnhi read BEFORE any
10452        // write to R4); MOV R4, R12.
10453        assert_eq!(hw[0], 0xB438);
10454        assert_eq!(hw[1], 0x4600 | (1 << 7) | (3 << 3) | 4, "MOV R12, rnlo");
10455        assert_eq!(hw[2], 0x4600 | (4 << 3) | 5, "MOV R5, rnhi");
10456        assert_eq!(hw[3], 0x4664, "MOV R4, R12");
10457    }
10458
10459    /// #632: A32 twin — same structural fix on the ARM-mode path
10460    /// (`--target cortex-r5`): total carried in R12 across the restore.
10461    #[test]
10462    fn test_632_a32_i64_popcnt_result_survives_scratch_restore() {
10463        let encoder = ArmEncoder::new_arm32();
10464        for rd in [Reg::R0, Reg::R3, Reg::R4, Reg::R5, Reg::R8] {
10465            let code = encoder
10466                .encode(&ArmOp::I64Popcnt {
10467                    rd,
10468                    rnlo: Reg::R6,
10469                    rnhi: Reg::R7,
10470                })
10471                .unwrap();
10472            let words: Vec<u32> = code
10473                .chunks(4)
10474                .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
10475                .collect();
10476            let pop = words
10477                .iter()
10478                .position(|&w| w == 0xE8BD_0038)
10479                .expect("POP {R3,R4,R5} present");
10480            assert_eq!(words[pop - 1], 0xE084_C005, "ADD R12, R4, R5 before POP");
10481            let rd_bits = match rd {
10482                Reg::R8 => 8u32,
10483                Reg::R5 => 5,
10484                Reg::R4 => 4,
10485                Reg::R3 => 3,
10486                _ => 0,
10487            };
10488            assert_eq!(
10489                words[pop + 1],
10490                0xE1A0_0000 | (rd_bits << 12) | 12,
10491                "MOV rd, R12 after the restore"
10492            );
10493        }
10494    }
10495
10496    /// #633: I64DivS must carry the INT64_MIN/-1 overflow guard (mirroring
10497    /// the i32 path) right after the zero-divisor guard — dividend in R0:R1,
10498    /// divisor in R2:R3 on the #610/#613 fixed-ABI wrapper path.
10499    #[test]
10500    fn test_633_i64_divs_overflow_guard_emitted() {
10501        let encoder = ArmEncoder::new_thumb2();
10502        let code = encoder
10503            .encode(&ArmOp::I64DivS {
10504                rdlo: Reg::R4,
10505                rdhi: Reg::R5,
10506                rnlo: Reg::R0,
10507                rnhi: Reg::R1,
10508                rmlo: Reg::R2,
10509                rmhi: Reg::R3,
10510                elide_zero_guard: false,
10511                elide_overflow_guard: false,
10512            })
10513            .unwrap();
10514        // 26-byte marshal + 8-byte zero-trap, then the 22-byte overflow guard.
10515        let guard: Vec<u16> = code[34..56]
10516            .chunks(2)
10517            .map(|c| u16::from_le_bytes([c[0], c[1]]))
10518            .collect();
10519        assert_eq!(
10520            guard,
10521            vec![
10522                0xEA02, 0x0C03, // AND.W R12, R2, R3
10523                0xF11C, 0x0F01, // CMN.W R12, #1
10524                0xD105, // BNE .no_trap
10525                0x2800, // CMP R0, #0
10526                0xD103, // BNE .no_trap
10527                0xF1B1, 0x4F00, // CMP.W R1, #0x80000000
10528                0xD100, // BNE .no_trap
10529                0xDE00, // UDF #0 — signed-division overflow
10530            ],
10531            "INT64_MIN/-1 overflow guard after the zero-divisor guard"
10532        );
10533    }
10534
10535    /// #633 fix-guard twin: I64RemS must NOT carry the overflow guard —
10536    /// rem_s(INT64_MIN, -1) is defined as 0 and must not trap. Exactly one
10537    /// UDF (the zero-divisor trap) in the whole expansion.
10538    #[test]
10539    fn test_633_i64_rems_has_no_overflow_guard() {
10540        let encoder = ArmEncoder::new_thumb2();
10541        for (is_rem_s, op) in [
10542            (
10543                true,
10544                ArmOp::I64RemS {
10545                    rdlo: Reg::R4,
10546                    rdhi: Reg::R5,
10547                    rnlo: Reg::R0,
10548                    rnhi: Reg::R1,
10549                    rmlo: Reg::R2,
10550                    rmhi: Reg::R3,
10551                    elide_zero_guard: false,
10552                },
10553            ),
10554            (
10555                false,
10556                ArmOp::I64DivS {
10557                    rdlo: Reg::R4,
10558                    rdhi: Reg::R5,
10559                    rnlo: Reg::R0,
10560                    rnhi: Reg::R1,
10561                    rmlo: Reg::R2,
10562                    rmhi: Reg::R3,
10563                    elide_zero_guard: false,
10564                    elide_overflow_guard: false,
10565                },
10566            ),
10567        ] {
10568            let code = encoder.encode(&op).unwrap();
10569            let udfs = code
10570                .chunks(2)
10571                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
10572                .count();
10573            let want = if is_rem_s { 1 } else { 2 };
10574            assert_eq!(
10575                udfs, want,
10576                "rem_s: zero-trap only; div_s: zero-trap + overflow trap"
10577            );
10578        }
10579    }
10580
10581    /// #494 phase 2b: `elide_zero_guard` drops EXACTLY the 8-byte fused
10582    /// zero-trap (`ORRS.W R12,R2,R3; BNE; UDF #0`) and nothing else — the
10583    /// rest of the expansion is byte-identical (splice check).
10584    #[test]
10585    fn test_494_i64_zero_guard_elision_is_exact_splice() {
10586        let encoder = ArmEncoder::new_thumb2();
10587        let mk = |elide_zero_guard: bool| {
10588            encoder
10589                .encode(&ArmOp::I64DivU {
10590                    rdlo: Reg::R4,
10591                    rdhi: Reg::R5,
10592                    rnlo: Reg::R0,
10593                    rnhi: Reg::R1,
10594                    rmlo: Reg::R2,
10595                    rmhi: Reg::R3,
10596                    elide_zero_guard,
10597                })
10598                .unwrap()
10599        };
10600        let full = mk(false);
10601        let elided = mk(true);
10602        assert_eq!(full.len(), elided.len() + 8, "zero guard is 8 bytes");
10603        // Marshal prologue (26 B) unchanged, guard (8 B) gone, tail identical.
10604        assert_eq!(&full[..26], &elided[..26]);
10605        assert_eq!(
10606            &full[26..34],
10607            &[0x52, 0xEA, 0x03, 0x0C, 0x00, 0xD1, 0x00, 0xDE],
10608            "the spliced-out bytes are exactly ORRS.W; BNE; UDF #0"
10609        );
10610        assert_eq!(&full[34..], &elided[26..]);
10611    }
10612
10613    /// #494 phase 2b two-guard distinction (the #633/#634 synergy): a
10614    /// divisor-nonzero fact elides ONLY the zero guard — the INT64_MIN/-1
10615    /// OVERFLOW guard is a separate obligation and must survive
10616    /// `elide_zero_guard: true`. Pinned on div_s in all flag states.
10617    #[test]
10618    fn test_494_i64_divs_overflow_guard_retained_when_only_zero_elided() {
10619        let encoder = ArmEncoder::new_thumb2();
10620        let mk = |zero: bool, ovf: bool| {
10621            encoder
10622                .encode(&ArmOp::I64DivS {
10623                    rdlo: Reg::R4,
10624                    rdhi: Reg::R5,
10625                    rnlo: Reg::R0,
10626                    rnhi: Reg::R1,
10627                    rmlo: Reg::R2,
10628                    rmhi: Reg::R3,
10629                    elide_zero_guard: zero,
10630                    elide_overflow_guard: ovf,
10631                })
10632                .unwrap()
10633        };
10634        let udf_count = |code: &[u8]| {
10635            code.chunks(2)
10636                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
10637                .count()
10638        };
10639        let full = mk(false, false);
10640        let zero_only = mk(true, false);
10641        let both = mk(true, true);
10642        assert_eq!(udf_count(&full), 2, "baseline: zero trap + overflow trap");
10643        assert_eq!(
10644            udf_count(&zero_only),
10645            1,
10646            "divisor-nonzero elides the zero trap ONLY — the #633 overflow \
10647             guard must be retained"
10648        );
10649        // The retained guard is the 22-byte overflow sequence, now right
10650        // after the 26-byte marshal prologue.
10651        let guard: Vec<u16> = zero_only[26..48]
10652            .chunks(2)
10653            .map(|c| u16::from_le_bytes([c[0], c[1]]))
10654            .collect();
10655        assert_eq!(
10656            guard,
10657            vec![
10658                0xEA02, 0x0C03, 0xF11C, 0x0F01, 0xD105, 0x2800, 0xD103, 0xF1B1, 0x4F00, 0xD100,
10659                0xDE00,
10660            ],
10661            "the surviving guard is the INT64_MIN/-1 overflow trap"
10662        );
10663        assert_eq!(full.len(), zero_only.len() + 8);
10664        assert_eq!(zero_only.len(), both.len() + 22);
10665        assert_eq!(udf_count(&both), 0, "both obligations discharged ⇒ no UDF");
10666    }
10667
10668    /// #494 phase 2b A32 twin: zero-guard elision is an exact 12-byte splice
10669    /// and the A32 overflow guard survives a zero-only elision.
10670    #[test]
10671    fn test_494_a32_i64_guard_elision() {
10672        let encoder = ArmEncoder::new_arm32();
10673        let mk = |zero: bool, ovf: bool| {
10674            encoder
10675                .encode(&ArmOp::I64DivS {
10676                    rdlo: Reg::R4,
10677                    rdhi: Reg::R5,
10678                    rnlo: Reg::R0,
10679                    rnhi: Reg::R1,
10680                    rmlo: Reg::R2,
10681                    rmhi: Reg::R3,
10682                    elide_zero_guard: zero,
10683                    elide_overflow_guard: ovf,
10684                })
10685                .unwrap()
10686        };
10687        let full = mk(false, false);
10688        let zero_only = mk(true, false);
10689        let both = mk(true, true);
10690        // A32 zero guard = 3 words (ORRS/BNE/UDF), overflow guard = 6 words.
10691        assert_eq!(full.len(), zero_only.len() + 12);
10692        assert_eq!(zero_only.len(), both.len() + 24);
10693        let udf_count = |code: &[u8]| {
10694            code.chunks(4)
10695                .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
10696                .count()
10697        };
10698        assert_eq!(udf_count(&full), 2);
10699        assert_eq!(
10700            udf_count(&zero_only),
10701            1,
10702            "A32: overflow guard retained under zero-only elision"
10703        );
10704        assert_eq!(udf_count(&both), 0);
10705    }
10706
10707    /// #633: A32 twin — the conditional-execution overflow guard on the
10708    /// ARM-mode I64DivS, and its absence from I64RemS.
10709    #[test]
10710    fn test_633_a32_i64_divs_overflow_guard() {
10711        let encoder = ArmEncoder::new_arm32();
10712        let mk_divs = ArmOp::I64DivS {
10713            rdlo: Reg::R4,
10714            rdhi: Reg::R5,
10715            rnlo: Reg::R0,
10716            rnhi: Reg::R1,
10717            rmlo: Reg::R2,
10718            rmhi: Reg::R3,
10719            elide_zero_guard: false,
10720            elide_overflow_guard: false,
10721        };
10722        let code = encoder.encode(&mk_divs).unwrap();
10723        let words: Vec<u32> = code
10724            .chunks(4)
10725            .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
10726            .collect();
10727        let guard = [
10728            0xE002_C003u32, // AND   R12, R2, R3
10729            0xE37C_0001,    // CMN   R12, #1
10730            0x0350_0000,    // CMPEQ R0, #0
10731            0x0351_0102,    // CMPEQ R1, #0x80000000
10732            0x1A00_0000,    // BNE +1 insn
10733            0xE7F0_00F0,    // UDF #0
10734        ];
10735        assert!(
10736            words.windows(6).any(|w| w == guard),
10737            "A32 I64DivS carries the INT64_MIN/-1 overflow guard"
10738        );
10739        let rems = encoder
10740            .encode(&ArmOp::I64RemS {
10741                rdlo: Reg::R4,
10742                rdhi: Reg::R5,
10743                rnlo: Reg::R0,
10744                rnhi: Reg::R1,
10745                rmlo: Reg::R2,
10746                rmhi: Reg::R3,
10747                elide_zero_guard: false,
10748            })
10749            .unwrap();
10750        let rems_udfs = rems
10751            .chunks(4)
10752            .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
10753            .count();
10754        assert_eq!(rems_udfs, 1, "A32 I64RemS keeps only the zero-divisor trap");
10755    }
10756
10757    #[test]
10758    fn test_encode_nop_thumb2() {
10759        let encoder = ArmEncoder::new_thumb2();
10760        let op = ArmOp::Nop;
10761        let code = encoder.encode(&op).unwrap();
10762        assert_eq!(code.len(), 2); // 16-bit
10763
10764        // NOP: 0xBF00 in little-endian
10765        assert_eq!(code, vec![0x00, 0xBF]);
10766    }
10767
10768    // =========================================================================
10769    // i64 Thumb-2 encoding tests
10770    // =========================================================================
10771
10772    #[test]
10773    fn test_encode_i64_add_thumb2() {
10774        let encoder = ArmEncoder::new_thumb2();
10775        let op = ArmOp::I64Add {
10776            rdlo: Reg::R0,
10777            rdhi: Reg::R1,
10778            rnlo: Reg::R0,
10779            rnhi: Reg::R1,
10780            rmlo: Reg::R2,
10781            rmhi: Reg::R3,
10782        };
10783        let code = encoder.encode(&op).unwrap();
10784        // Should emit ADDS (2 bytes) + ADC.W (4 bytes) = 6 bytes
10785        assert_eq!(code.len(), 6, "I64Add should be 6 bytes (ADDS + ADC.W)");
10786    }
10787
10788    #[test]
10789    fn test_encode_i64_sub_thumb2() {
10790        let encoder = ArmEncoder::new_thumb2();
10791        let op = ArmOp::I64Sub {
10792            rdlo: Reg::R0,
10793            rdhi: Reg::R1,
10794            rnlo: Reg::R0,
10795            rnhi: Reg::R1,
10796            rmlo: Reg::R2,
10797            rmhi: Reg::R3,
10798        };
10799        let code = encoder.encode(&op).unwrap();
10800        // Should emit SUBS (2 bytes) + SBC.W (4 bytes) = 6 bytes
10801        assert_eq!(code.len(), 6, "I64Sub should be 6 bytes (SUBS + SBC.W)");
10802    }
10803
10804    #[test]
10805    fn test_encode_i64_and_thumb2() {
10806        let encoder = ArmEncoder::new_thumb2();
10807        let op = ArmOp::I64And {
10808            rdlo: Reg::R0,
10809            rdhi: Reg::R1,
10810            rnlo: Reg::R0,
10811            rnhi: Reg::R1,
10812            rmlo: Reg::R2,
10813            rmhi: Reg::R3,
10814        };
10815        let code = encoder.encode(&op).unwrap();
10816        // AND.W (4 bytes) + AND.W (4 bytes) = 8 bytes
10817        assert!(code.len() >= 4, "I64And should emit at least 4 bytes");
10818    }
10819
10820    #[test]
10821    fn test_encode_i64_or_thumb2() {
10822        let encoder = ArmEncoder::new_thumb2();
10823        let op = ArmOp::I64Or {
10824            rdlo: Reg::R0,
10825            rdhi: Reg::R1,
10826            rnlo: Reg::R0,
10827            rnhi: Reg::R1,
10828            rmlo: Reg::R2,
10829            rmhi: Reg::R3,
10830        };
10831        let code = encoder.encode(&op).unwrap();
10832        assert!(code.len() >= 4, "I64Or should emit at least 4 bytes");
10833    }
10834
10835    #[test]
10836    fn test_encode_i64_xor_thumb2() {
10837        let encoder = ArmEncoder::new_thumb2();
10838        let op = ArmOp::I64Xor {
10839            rdlo: Reg::R0,
10840            rdhi: Reg::R1,
10841            rnlo: Reg::R0,
10842            rnhi: Reg::R1,
10843            rmlo: Reg::R2,
10844            rmhi: Reg::R3,
10845        };
10846        let code = encoder.encode(&op).unwrap();
10847        assert!(code.len() >= 4, "I64Xor should emit at least 4 bytes");
10848    }
10849
10850    #[test]
10851    fn test_encode_i64_const_small_thumb2() {
10852        let encoder = ArmEncoder::new_thumb2();
10853        // Small constant: only needs MOVW for each half
10854        let op = ArmOp::I64Const {
10855            rdlo: Reg::R0,
10856            rdhi: Reg::R1,
10857            value: 42,
10858        };
10859        let code = encoder.encode(&op).unwrap();
10860        // MOVW R0, #42 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes minimum
10861        assert!(code.len() >= 8, "I64Const should emit at least 8 bytes");
10862    }
10863
10864    #[test]
10865    fn test_encode_i64_const_large_thumb2() {
10866        let encoder = ArmEncoder::new_thumb2();
10867        // Large constant: needs MOVW+MOVT for each half
10868        let op = ArmOp::I64Const {
10869            rdlo: Reg::R0,
10870            rdhi: Reg::R1,
10871            value: 0x1234_5678_9ABC_DEF0_u64 as i64,
10872        };
10873        let code = encoder.encode(&op).unwrap();
10874        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
10875        assert_eq!(
10876            code.len(),
10877            16,
10878            "I64Const with large value should be 16 bytes"
10879        );
10880    }
10881
10882    #[test]
10883    fn test_encode_i64_extend_i32_s_thumb2() {
10884        let encoder = ArmEncoder::new_thumb2();
10885        let op = ArmOp::I64ExtendI32S {
10886            rdlo: Reg::R0,
10887            rdhi: Reg::R1,
10888            rn: Reg::R0,
10889        };
10890        let code = encoder.encode(&op).unwrap();
10891        // When rdlo == rn, only ASR (4 bytes) is emitted
10892        assert_eq!(
10893            code.len(),
10894            4,
10895            "I64ExtendI32S (same reg) should be 4 bytes (ASR only)"
10896        );
10897    }
10898
10899    #[test]
10900    fn test_encode_i64_extend_i32_s_diff_reg_thumb2() {
10901        let encoder = ArmEncoder::new_thumb2();
10902        let op = ArmOp::I64ExtendI32S {
10903            rdlo: Reg::R0,
10904            rdhi: Reg::R1,
10905            rn: Reg::R2,
10906        };
10907        let code = encoder.encode(&op).unwrap();
10908        // MOV rdlo, rn (2 bytes for low regs) + ASR rdhi, rdlo, #31 (4 bytes) = 6 bytes
10909        assert!(
10910            code.len() >= 6,
10911            "I64ExtendI32S (diff reg) should be at least 6 bytes"
10912        );
10913    }
10914
10915    #[test]
10916    fn test_encode_i64_extend_i32_u_thumb2() {
10917        let encoder = ArmEncoder::new_thumb2();
10918        let op = ArmOp::I64ExtendI32U {
10919            rdlo: Reg::R0,
10920            rdhi: Reg::R1,
10921            rn: Reg::R0,
10922        };
10923        let code = encoder.encode(&op).unwrap();
10924        // When rdlo == rn, only MOV rdhi, #0 (2 bytes) is emitted
10925        assert_eq!(
10926            code.len(),
10927            2,
10928            "I64ExtendI32U (same reg) should be 2 bytes (MOV #0 only)"
10929        );
10930    }
10931
10932    #[test]
10933    fn test_encode_i32_wrap_i64_nop_thumb2() {
10934        let encoder = ArmEncoder::new_thumb2();
10935        // When rd == rnlo, should be a NOP
10936        let op = ArmOp::I32WrapI64 {
10937            rd: Reg::R0,
10938            rnlo: Reg::R0,
10939        };
10940        let code = encoder.encode(&op).unwrap();
10941        assert_eq!(code.len(), 2, "I32WrapI64 same reg should be NOP (2 bytes)");
10942        assert_eq!(code, vec![0x00, 0xBF]); // NOP
10943    }
10944
10945    #[test]
10946    fn test_encode_i32_wrap_i64_diff_reg_thumb2() {
10947        let encoder = ArmEncoder::new_thumb2();
10948        let op = ArmOp::I32WrapI64 {
10949            rd: Reg::R2,
10950            rnlo: Reg::R0,
10951        };
10952        let code = encoder.encode(&op).unwrap();
10953        // MOV R2, R0 (2 or 4 bytes)
10954        assert!(
10955            code.len() >= 2,
10956            "I32WrapI64 diff reg should emit at least 2 bytes"
10957        );
10958    }
10959
10960    #[test]
10961    fn test_encode_i64_eqz_thumb2() {
10962        let encoder = ArmEncoder::new_thumb2();
10963        let op = ArmOp::I64Eqz {
10964            rd: Reg::R0,
10965            rnlo: Reg::R0,
10966            rnhi: Reg::R1,
10967        };
10968        let code = encoder.encode(&op).unwrap();
10969        // Delegates to I64SetCondZ which is already encoded
10970        assert!(
10971            code.len() >= 6,
10972            "I64Eqz should emit at least 6 bytes for ORR+ITE+MOV+MOV"
10973        );
10974    }
10975
10976    #[test]
10977    fn test_encode_i64_eq_thumb2() {
10978        let encoder = ArmEncoder::new_thumb2();
10979        let op = ArmOp::I64Eq {
10980            rd: Reg::R0,
10981            rnlo: Reg::R0,
10982            rnhi: Reg::R1,
10983            rmlo: Reg::R2,
10984            rmhi: Reg::R3,
10985        };
10986        let code = encoder.encode(&op).unwrap();
10987        // Delegates to I64SetCond EQ: CMP lo + IT EQ + CMPEQ hi + ITE EQ + MOV 1 + MOV 0
10988        assert!(code.len() >= 10, "I64Eq should emit at least 10 bytes");
10989    }
10990
10991    #[test]
10992    fn test_encode_i64_ldr_thumb2() {
10993        let encoder = ArmEncoder::new_thumb2();
10994        let op = ArmOp::I64Ldr {
10995            rdlo: Reg::R0,
10996            rdhi: Reg::R1,
10997            addr: MemAddr::imm(Reg::SP, 0),
10998        };
10999        let code = encoder.encode(&op).unwrap();
11000        // Two LDR instructions (lo at offset, hi at offset+4)
11001        assert!(code.len() >= 4, "I64Ldr should emit at least 4 bytes");
11002    }
11003
11004    #[test]
11005    fn test_372_i64_ldr_indexed_materializes_address() {
11006        // #372: a memory i64.load carries an index register (R11 + addr + off).
11007        // The encoder must materialize `ip = base + index` (ADD.W) and load via
11008        // `[ip,#off]` — NOT drop the index. A frame (non-indexed) i64.load must
11009        // stay byte-identical (plain `[base,#off]`, no ADD).
11010        let encoder = ArmEncoder::new_thumb2();
11011        let indexed = encoder
11012            .encode(&ArmOp::I64Ldr {
11013                rdlo: Reg::R0,
11014                rdhi: Reg::R1,
11015                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
11016            })
11017            .unwrap();
11018        // ADD.W ip, fp, r0 = eb0b 0c00 (byte-verified vs arm-none-eabi-as).
11019        assert_eq!(
11020            &indexed[0..4],
11021            &[0x0b, 0xeb, 0x00, 0x0c],
11022            "indexed I64Ldr must start with ADD.W ip, base, index"
11023        );
11024        let frame = encoder
11025            .encode(&ArmOp::I64Ldr {
11026                rdlo: Reg::R0,
11027                rdhi: Reg::R1,
11028                addr: MemAddr::imm(Reg::SP, 8),
11029            })
11030            .unwrap();
11031        // No index -> no ADD.W prefix (byte-identical frame access).
11032        assert_ne!(
11033            &frame[0..2],
11034            &[0x0b, 0xeb],
11035            "frame (non-indexed) I64Ldr must NOT emit an ADD.W"
11036        );
11037    }
11038
11039    #[test]
11040    fn test_382_i64_ldst_large_offset_materializes_not_skips() {
11041        // #382: an indexed i64.load/store whose static offset > 0xFFF must
11042        // MATERIALIZE the offset into the base — NOT return Err (skip the fn).
11043        // Sequence for reg_imm(R11, R0, 5000): MOVW ip,#5000 ; ADD ip,r0,ip ;
11044        // ADD ip,ip,fp ; LDR/STR halves at [ip,#0] / [ip,#4]. Byte-verified tail
11045        // vs arm-none-eabi-as.
11046        let encoder = ArmEncoder::new_thumb2();
11047        // 0x1388 > 0xFFF (MemAddr is not Copy, so build it per use).
11048
11049        let ld = encoder
11050            .encode(&ArmOp::I64Ldr {
11051                rdlo: Reg::R0,
11052                rdhi: Reg::R1,
11053                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
11054            })
11055            .expect("large-offset i64.load must lower, not skip");
11056        // MOVW ip,#0x1388 (4) + ADD ip,r0,ip (4) + ADD ip,ip,fp (4) + 2 LDR (8).
11057        assert_eq!(ld.len(), 20, "expected MOVW + 2×ADD + 2×LDR");
11058        // Must NOT be the small-offset `ADD.W ip, fp, r0` (0x0b 0xeb) prefix —
11059        // that path can only reach imm12 offsets.
11060        assert_ne!(
11061            &ld[0..2],
11062            &[0x0b, 0xeb],
11063            "must materialize the large offset"
11064        );
11065        // Effective base built in ip, then halves at [ip,#0] / [ip,#4].
11066        assert_eq!(
11067            &ld[4..20],
11068            &[
11069                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
11070                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
11071                0xdc, 0xf8, 0x00, 0x00, // LDR.W r0, [ip, #0]
11072                0xdc, 0xf8, 0x04, 0x10, // LDR.W r1, [ip, #4]
11073            ],
11074            "large-offset i64.load must fold offset into ip and access [ip,#0]/[ip,#4]"
11075        );
11076
11077        // Store: same base materialization, STR halves.
11078        let st = encoder
11079            .encode(&ArmOp::I64Str {
11080                rdlo: Reg::R2,
11081                rdhi: Reg::R3,
11082                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
11083            })
11084            .expect("large-offset i64.store must lower, not skip");
11085        assert_eq!(st.len(), 20);
11086        assert_eq!(
11087            &st[4..20],
11088            &[
11089                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
11090                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
11091                0xcc, 0xf8, 0x00, 0x20, // STR.W r2, [ip, #0]
11092                0xcc, 0xf8, 0x04, 0x30, // STR.W r3, [ip, #4]
11093            ],
11094            "large-offset i64.store must fold offset into ip and access [ip,#0]/[ip,#4]"
11095        );
11096
11097        // Small-offset (imm12) indexed access stays byte-identical (#372): the
11098        // effective base is a single `ADD.W ip, fp, r0` and the halves keep the
11099        // folded immediates — NO extra MOVW/ADD.
11100        let small = encoder
11101            .encode(&ArmOp::I64Ldr {
11102                rdlo: Reg::R0,
11103                rdhi: Reg::R1,
11104                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 8),
11105            })
11106            .unwrap();
11107        assert_eq!(
11108            &small[0..4],
11109            &[0x0b, 0xeb, 0x00, 0x0c],
11110            "small-offset indexed i64 must keep the single ADD.W ip, fp, r0"
11111        );
11112        assert_eq!(small.len(), 12, "ADD.W + 2×LDR.W (offset folded in imm12)");
11113    }
11114
11115    #[test]
11116    fn test_encode_i64_str_thumb2() {
11117        let encoder = ArmEncoder::new_thumb2();
11118        let op = ArmOp::I64Str {
11119            rdlo: Reg::R0,
11120            rdhi: Reg::R1,
11121            addr: MemAddr::imm(Reg::SP, 0),
11122        };
11123        let code = encoder.encode(&op).unwrap();
11124        // Two STR instructions (lo at offset, hi at offset+4)
11125        assert!(code.len() >= 4, "I64Str should emit at least 4 bytes");
11126    }
11127
11128    #[test]
11129    fn test_encode_i64_all_comparisons_thumb2() {
11130        let encoder = ArmEncoder::new_thumb2();
11131
11132        let ops = vec![
11133            ArmOp::I64Ne {
11134                rd: Reg::R0,
11135                rnlo: Reg::R0,
11136                rnhi: Reg::R1,
11137                rmlo: Reg::R2,
11138                rmhi: Reg::R3,
11139            },
11140            ArmOp::I64LtS {
11141                rd: Reg::R0,
11142                rnlo: Reg::R0,
11143                rnhi: Reg::R1,
11144                rmlo: Reg::R2,
11145                rmhi: Reg::R3,
11146            },
11147            ArmOp::I64LtU {
11148                rd: Reg::R0,
11149                rnlo: Reg::R0,
11150                rnhi: Reg::R1,
11151                rmlo: Reg::R2,
11152                rmhi: Reg::R3,
11153            },
11154            ArmOp::I64LeS {
11155                rd: Reg::R0,
11156                rnlo: Reg::R0,
11157                rnhi: Reg::R1,
11158                rmlo: Reg::R2,
11159                rmhi: Reg::R3,
11160            },
11161            ArmOp::I64LeU {
11162                rd: Reg::R0,
11163                rnlo: Reg::R0,
11164                rnhi: Reg::R1,
11165                rmlo: Reg::R2,
11166                rmhi: Reg::R3,
11167            },
11168            ArmOp::I64GtS {
11169                rd: Reg::R0,
11170                rnlo: Reg::R0,
11171                rnhi: Reg::R1,
11172                rmlo: Reg::R2,
11173                rmhi: Reg::R3,
11174            },
11175            ArmOp::I64GtU {
11176                rd: Reg::R0,
11177                rnlo: Reg::R0,
11178                rnhi: Reg::R1,
11179                rmlo: Reg::R2,
11180                rmhi: Reg::R3,
11181            },
11182            ArmOp::I64GeS {
11183                rd: Reg::R0,
11184                rnlo: Reg::R0,
11185                rnhi: Reg::R1,
11186                rmlo: Reg::R2,
11187                rmhi: Reg::R3,
11188            },
11189            ArmOp::I64GeU {
11190                rd: Reg::R0,
11191                rnlo: Reg::R0,
11192                rnhi: Reg::R1,
11193                rmlo: Reg::R2,
11194                rmhi: Reg::R3,
11195            },
11196        ];
11197
11198        for op in &ops {
11199            let code = encoder.encode(op).unwrap();
11200            assert!(
11201                code.len() >= 8,
11202                "i64 comparison {:?} should emit at least 8 bytes, got {}",
11203                op,
11204                code.len()
11205            );
11206        }
11207    }
11208
11209    #[test]
11210    fn test_encode_i64_const_zero_thumb2() {
11211        let encoder = ArmEncoder::new_thumb2();
11212        let op = ArmOp::I64Const {
11213            rdlo: Reg::R0,
11214            rdhi: Reg::R1,
11215            value: 0,
11216        };
11217        let code = encoder.encode(&op).unwrap();
11218        // MOVW R0, #0 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes
11219        assert_eq!(code.len(), 8, "I64Const(0) should be 8 bytes");
11220    }
11221
11222    #[test]
11223    fn test_encode_i64_const_negative_one_thumb2() {
11224        let encoder = ArmEncoder::new_thumb2();
11225        let op = ArmOp::I64Const {
11226            rdlo: Reg::R0,
11227            rdhi: Reg::R1,
11228            value: -1, // 0xFFFF_FFFF_FFFF_FFFF
11229        };
11230        let code = encoder.encode(&op).unwrap();
11231        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
11232        assert_eq!(code.len(), 16, "I64Const(-1) should be 16 bytes");
11233    }
11234
11235    // =========================================================================
11236    // Sub-word load/store encoding tests
11237    // =========================================================================
11238
11239    #[test]
11240    fn test_encode_ldrb_arm32() {
11241        let encoder = ArmEncoder::new_arm32();
11242        let op = ArmOp::Ldrb {
11243            rd: Reg::R0,
11244            addr: MemAddr::imm(Reg::R1, 4),
11245        };
11246        let code = encoder.encode(&op).unwrap();
11247        assert_eq!(code.len(), 4, "ARM32 LDRB should be 4 bytes");
11248        // LDRB R0, [R1, #4] = 0xE5D10004
11249        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11250        assert_eq!(encoded, 0xE5D10004, "Should encode LDRB R0, [R1, #4]");
11251    }
11252
11253    #[test]
11254    fn test_encode_strb_arm32() {
11255        let encoder = ArmEncoder::new_arm32();
11256        let op = ArmOp::Strb {
11257            rd: Reg::R0,
11258            addr: MemAddr::imm(Reg::R1, 0),
11259        };
11260        let code = encoder.encode(&op).unwrap();
11261        assert_eq!(code.len(), 4, "ARM32 STRB should be 4 bytes");
11262        // STRB R0, [R1, #0] = 0xE5C10000
11263        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11264        assert_eq!(encoded, 0xE5C10000, "Should encode STRB R0, [R1, #0]");
11265    }
11266
11267    #[test]
11268    fn test_encode_ldrh_arm32() {
11269        let encoder = ArmEncoder::new_arm32();
11270        let op = ArmOp::Ldrh {
11271            rd: Reg::R0,
11272            addr: MemAddr::imm(Reg::R1, 2),
11273        };
11274        let code = encoder.encode(&op).unwrap();
11275        assert_eq!(code.len(), 4, "ARM32 LDRH should be 4 bytes");
11276    }
11277
11278    #[test]
11279    fn test_encode_strh_arm32() {
11280        let encoder = ArmEncoder::new_arm32();
11281        let op = ArmOp::Strh {
11282            rd: Reg::R0,
11283            addr: MemAddr::imm(Reg::R1, 0),
11284        };
11285        let code = encoder.encode(&op).unwrap();
11286        assert_eq!(code.len(), 4, "ARM32 STRH should be 4 bytes");
11287    }
11288
11289    #[test]
11290    fn test_encode_ldrsb_arm32() {
11291        let encoder = ArmEncoder::new_arm32();
11292        let op = ArmOp::Ldrsb {
11293            rd: Reg::R0,
11294            addr: MemAddr::imm(Reg::R1, 0),
11295        };
11296        let code = encoder.encode(&op).unwrap();
11297        assert_eq!(code.len(), 4, "ARM32 LDRSB should be 4 bytes");
11298    }
11299
11300    #[test]
11301    fn test_encode_ldrsh_arm32() {
11302        let encoder = ArmEncoder::new_arm32();
11303        let op = ArmOp::Ldrsh {
11304            rd: Reg::R0,
11305            addr: MemAddr::imm(Reg::R1, 0),
11306        };
11307        let code = encoder.encode(&op).unwrap();
11308        assert_eq!(code.len(), 4, "ARM32 LDRSH should be 4 bytes");
11309    }
11310
11311    #[test]
11312    fn test_encode_ldrb_thumb2_16bit() {
11313        let encoder = ArmEncoder::new_thumb2();
11314        let op = ArmOp::Ldrb {
11315            rd: Reg::R0,
11316            addr: MemAddr::imm(Reg::R1, 4),
11317        };
11318        let code = encoder.encode(&op).unwrap();
11319        // Low registers + small offset -> 16-bit encoding
11320        assert_eq!(
11321            code.len(),
11322            2,
11323            "Thumb-2 LDRB with small offset should be 16-bit"
11324        );
11325    }
11326
11327    #[test]
11328    fn test_encode_ldrb_thumb2_32bit() {
11329        let encoder = ArmEncoder::new_thumb2();
11330        let op = ArmOp::Ldrb {
11331            rd: Reg::R0,
11332            addr: MemAddr::imm(Reg::R1, 100), // offset > 31 needs 32-bit
11333        };
11334        let code = encoder.encode(&op).unwrap();
11335        assert_eq!(
11336            code.len(),
11337            4,
11338            "Thumb-2 LDRB with large offset should be 32-bit"
11339        );
11340    }
11341
11342    #[test]
11343    fn test_encode_strb_thumb2_16bit() {
11344        let encoder = ArmEncoder::new_thumb2();
11345        let op = ArmOp::Strb {
11346            rd: Reg::R0,
11347            addr: MemAddr::imm(Reg::R1, 10),
11348        };
11349        let code = encoder.encode(&op).unwrap();
11350        assert_eq!(
11351            code.len(),
11352            2,
11353            "Thumb-2 STRB with small offset should be 16-bit"
11354        );
11355    }
11356
11357    #[test]
11358    fn test_encode_ldrh_thumb2_16bit() {
11359        let encoder = ArmEncoder::new_thumb2();
11360        let op = ArmOp::Ldrh {
11361            rd: Reg::R0,
11362            addr: MemAddr::imm(Reg::R1, 4), // offset aligned to 2, <= 62
11363        };
11364        let code = encoder.encode(&op).unwrap();
11365        assert_eq!(
11366            code.len(),
11367            2,
11368            "Thumb-2 LDRH with small aligned offset should be 16-bit"
11369        );
11370    }
11371
11372    #[test]
11373    fn test_encode_strh_thumb2_16bit() {
11374        let encoder = ArmEncoder::new_thumb2();
11375        let op = ArmOp::Strh {
11376            rd: Reg::R0,
11377            addr: MemAddr::imm(Reg::R1, 4),
11378        };
11379        let code = encoder.encode(&op).unwrap();
11380        assert_eq!(
11381            code.len(),
11382            2,
11383            "Thumb-2 STRH with small aligned offset should be 16-bit"
11384        );
11385    }
11386
11387    #[test]
11388    fn test_encode_ldrsb_thumb2() {
11389        let encoder = ArmEncoder::new_thumb2();
11390        let op = ArmOp::Ldrsb {
11391            rd: Reg::R0,
11392            addr: MemAddr::imm(Reg::R1, 0),
11393        };
11394        let code = encoder.encode(&op).unwrap();
11395        // LDRSB has no 16-bit immediate form, always 32-bit
11396        assert_eq!(code.len(), 4, "Thumb-2 LDRSB should be 32-bit");
11397    }
11398
11399    #[test]
11400    fn test_encode_ldrsh_thumb2() {
11401        let encoder = ArmEncoder::new_thumb2();
11402        let op = ArmOp::Ldrsh {
11403            rd: Reg::R0,
11404            addr: MemAddr::imm(Reg::R1, 0),
11405        };
11406        let code = encoder.encode(&op).unwrap();
11407        assert_eq!(code.len(), 4, "Thumb-2 LDRSH should be 32-bit");
11408    }
11409
11410    #[test]
11411    fn test_encode_memory_size_thumb2() {
11412        let encoder = ArmEncoder::new_thumb2();
11413        let op = ArmOp::MemorySize { rd: Reg::R0 };
11414        let code = encoder.encode(&op).unwrap();
11415        // R0 and R10 are not both low registers, so this needs careful handling
11416        assert!(!code.is_empty(), "MemorySize should produce code");
11417    }
11418
11419    #[test]
11420    fn test_encode_memory_grow_thumb2() {
11421        let encoder = ArmEncoder::new_thumb2();
11422        let op = ArmOp::MemoryGrow {
11423            rd: Reg::R0,
11424            rn: Reg::R0,
11425        };
11426        let code = encoder.encode(&op).unwrap();
11427        assert_eq!(code.len(), 4, "MemoryGrow (MVN) should be 32-bit Thumb-2");
11428    }
11429
11430    #[test]
11431    fn test_encode_subword_reg_offset_thumb2() {
11432        let encoder = ArmEncoder::new_thumb2();
11433
11434        // LDRB with register offset
11435        let op = ArmOp::Ldrb {
11436            rd: Reg::R0,
11437            addr: MemAddr::reg(Reg::R1, Reg::R2),
11438        };
11439        let code = encoder.encode(&op).unwrap();
11440        assert_eq!(
11441            code.len(),
11442            4,
11443            "Thumb-2 LDRB with reg offset should be 32-bit"
11444        );
11445
11446        // STRB with register offset
11447        let op = ArmOp::Strb {
11448            rd: Reg::R0,
11449            addr: MemAddr::reg(Reg::R1, Reg::R2),
11450        };
11451        let code = encoder.encode(&op).unwrap();
11452        assert_eq!(
11453            code.len(),
11454            4,
11455            "Thumb-2 STRB with reg offset should be 32-bit"
11456        );
11457
11458        // LDRH with register offset
11459        let op = ArmOp::Ldrh {
11460            rd: Reg::R0,
11461            addr: MemAddr::reg(Reg::R1, Reg::R2),
11462        };
11463        let code = encoder.encode(&op).unwrap();
11464        assert_eq!(
11465            code.len(),
11466            4,
11467            "Thumb-2 LDRH with reg offset should be 32-bit"
11468        );
11469
11470        // STRH with register offset
11471        let op = ArmOp::Strh {
11472            rd: Reg::R0,
11473            addr: MemAddr::reg(Reg::R1, Reg::R2),
11474        };
11475        let code = encoder.encode(&op).unwrap();
11476        assert_eq!(
11477            code.len(),
11478            4,
11479            "Thumb-2 STRH with reg offset should be 32-bit"
11480        );
11481    }
11482
11483    #[test]
11484    fn test_encode_subword_reg_imm_offset_thumb2() {
11485        let encoder = ArmEncoder::new_thumb2();
11486
11487        // LDRB with both register and immediate offset
11488        let op = ArmOp::Ldrb {
11489            rd: Reg::R0,
11490            addr: MemAddr::reg_imm(Reg::R1, Reg::R2, 4),
11491        };
11492        let code = encoder.encode(&op).unwrap();
11493        // ADD R12, R2, #4 (4 bytes) + LDRB R0, [R1, R12] (4 bytes) = 8 bytes
11494        assert_eq!(
11495            code.len(),
11496            8,
11497            "Thumb-2 LDRB with reg+imm offset should be 8 bytes"
11498        );
11499    }
11500
11501    // ========================================================================
11502    // Helium MVE encoding tests
11503    // ========================================================================
11504
11505    #[test]
11506    fn test_encode_mve_addi32_thumb2() {
11507        let encoder = ArmEncoder::new_thumb2();
11508        let op = ArmOp::MveAddI {
11509            qd: QReg::Q0,
11510            qn: QReg::Q1,
11511            qm: QReg::Q2,
11512            size: MveSize::S32,
11513        };
11514        let code = encoder.encode(&op).unwrap();
11515        assert_eq!(
11516            code.len(),
11517            4,
11518            "MVE VADD.I32 should be 4 bytes (Thumb-2 32-bit)"
11519        );
11520    }
11521
11522    #[test]
11523    fn test_encode_mve_subi16_thumb2() {
11524        let encoder = ArmEncoder::new_thumb2();
11525        let op = ArmOp::MveSubI {
11526            qd: QReg::Q0,
11527            qn: QReg::Q1,
11528            qm: QReg::Q2,
11529            size: MveSize::S16,
11530        };
11531        let code = encoder.encode(&op).unwrap();
11532        assert_eq!(code.len(), 4, "MVE VSUB.I16 should be 4 bytes");
11533    }
11534
11535    #[test]
11536    fn test_encode_mve_muli8_thumb2() {
11537        let encoder = ArmEncoder::new_thumb2();
11538        let op = ArmOp::MveMulI {
11539            qd: QReg::Q0,
11540            qn: QReg::Q1,
11541            qm: QReg::Q2,
11542            size: MveSize::S8,
11543        };
11544        let code = encoder.encode(&op).unwrap();
11545        assert_eq!(code.len(), 4, "MVE VMUL.I8 should be 4 bytes");
11546    }
11547
11548    #[test]
11549    fn test_encode_mve_bitwise_thumb2() {
11550        let encoder = ArmEncoder::new_thumb2();
11551
11552        let ops = vec![
11553            ArmOp::MveAnd {
11554                qd: QReg::Q0,
11555                qn: QReg::Q1,
11556                qm: QReg::Q2,
11557            },
11558            ArmOp::MveOrr {
11559                qd: QReg::Q0,
11560                qn: QReg::Q1,
11561                qm: QReg::Q2,
11562            },
11563            ArmOp::MveEor {
11564                qd: QReg::Q0,
11565                qn: QReg::Q1,
11566                qm: QReg::Q2,
11567            },
11568            ArmOp::MveBic {
11569                qd: QReg::Q0,
11570                qn: QReg::Q1,
11571                qm: QReg::Q2,
11572            },
11573        ];
11574        for op in ops {
11575            let code = encoder.encode(&op).unwrap();
11576            assert_eq!(code.len(), 4, "MVE bitwise op should be 4 bytes");
11577        }
11578    }
11579
11580    #[test]
11581    fn test_encode_mve_mvn_thumb2() {
11582        let encoder = ArmEncoder::new_thumb2();
11583        let op = ArmOp::MveMvn {
11584            qd: QReg::Q0,
11585            qm: QReg::Q1,
11586        };
11587        let code = encoder.encode(&op).unwrap();
11588        assert_eq!(code.len(), 4, "MVE VMVN should be 4 bytes");
11589    }
11590
11591    #[test]
11592    fn test_encode_mve_load_store_thumb2() {
11593        let encoder = ArmEncoder::new_thumb2();
11594
11595        let load = ArmOp::MveLoad {
11596            qd: QReg::Q0,
11597            addr: MemAddr::imm(Reg::R0, 16),
11598        };
11599        let code = encoder.encode(&load).unwrap();
11600        assert_eq!(code.len(), 4, "MVE VLDRW.32 should be 4 bytes");
11601
11602        let store = ArmOp::MveStore {
11603            qd: QReg::Q1,
11604            addr: MemAddr::imm(Reg::R1, 0),
11605        };
11606        let code = encoder.encode(&store).unwrap();
11607        assert_eq!(code.len(), 4, "MVE VSTRW.32 should be 4 bytes");
11608    }
11609
11610    #[test]
11611    fn test_encode_mve_const_thumb2() {
11612        let encoder = ArmEncoder::new_thumb2();
11613        let op = ArmOp::MveConst {
11614            qd: QReg::Q0,
11615            bytes: [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0],
11616        };
11617        let code = encoder.encode(&op).unwrap();
11618        // Should be 4 words of (MOVW R12 + VMOV Sn) = 4 * (4+4) = 32 bytes min
11619        // Some words with hi16=0 skip MOVT, so length varies
11620        assert!(
11621            code.len() >= 24,
11622            "MVE const should produce multiple instructions"
11623        );
11624    }
11625
11626    #[test]
11627    fn test_encode_mve_dup_thumb2() {
11628        let encoder = ArmEncoder::new_thumb2();
11629        let op = ArmOp::MveDup {
11630            qd: QReg::Q0,
11631            rn: Reg::R0,
11632            size: MveSize::S32,
11633        };
11634        let code = encoder.encode(&op).unwrap();
11635        assert_eq!(code.len(), 4, "MVE VDUP.32 should be 4 bytes");
11636    }
11637
11638    #[test]
11639    fn test_encode_mve_extract_lane_thumb2() {
11640        let encoder = ArmEncoder::new_thumb2();
11641        let op = ArmOp::MveExtractLane {
11642            rd: Reg::R0,
11643            qn: QReg::Q1,
11644            lane: 2,
11645            size: MveSize::S32,
11646        };
11647        let code = encoder.encode(&op).unwrap();
11648        assert_eq!(code.len(), 4, "MVE extract lane should be 4 bytes");
11649    }
11650
11651    #[test]
11652    fn test_encode_mve_insert_lane_thumb2() {
11653        let encoder = ArmEncoder::new_thumb2();
11654        let op = ArmOp::MveInsertLane {
11655            qd: QReg::Q0,
11656            rn: Reg::R1,
11657            lane: 3,
11658            size: MveSize::S32,
11659        };
11660        let code = encoder.encode(&op).unwrap();
11661        assert_eq!(code.len(), 4, "MVE insert lane should be 4 bytes");
11662    }
11663
11664    #[test]
11665    fn test_encode_mve_addf32_thumb2() {
11666        let encoder = ArmEncoder::new_thumb2();
11667        let op = ArmOp::MveAddF32 {
11668            qd: QReg::Q0,
11669            qn: QReg::Q1,
11670            qm: QReg::Q2,
11671        };
11672        let code = encoder.encode(&op).unwrap();
11673        assert_eq!(code.len(), 4, "MVE VADD.F32 should be 4 bytes");
11674    }
11675
11676    #[test]
11677    fn test_encode_mve_divf32_thumb2() {
11678        let encoder = ArmEncoder::new_thumb2();
11679        let op = ArmOp::MveDivF32 {
11680            qd: QReg::Q0,
11681            qn: QReg::Q1,
11682            qm: QReg::Q2,
11683        };
11684        let code = encoder.encode(&op).unwrap();
11685        // Lane-wise: 4 x VDIV.F32 = 4 x 4 = 16 bytes
11686        assert_eq!(
11687            code.len(),
11688            16,
11689            "MVE VDIV.F32 (lane-wise) should be 16 bytes"
11690        );
11691    }
11692
11693    #[test]
11694    fn test_encode_mve_sqrtf32_thumb2() {
11695        let encoder = ArmEncoder::new_thumb2();
11696        let op = ArmOp::MveSqrtF32 {
11697            qd: QReg::Q0,
11698            qm: QReg::Q1,
11699        };
11700        let code = encoder.encode(&op).unwrap();
11701        // Lane-wise: 4 x VSQRT.F32 = 4 x 4 = 16 bytes
11702        assert_eq!(
11703            code.len(),
11704            16,
11705            "MVE VSQRT.F32 (lane-wise) should be 16 bytes"
11706        );
11707    }
11708
11709    #[test]
11710    fn test_encode_mve_negf32_thumb2() {
11711        let encoder = ArmEncoder::new_thumb2();
11712        let op = ArmOp::MveNegF32 {
11713            qd: QReg::Q0,
11714            qm: QReg::Q1,
11715        };
11716        let code = encoder.encode(&op).unwrap();
11717        assert_eq!(code.len(), 4, "MVE VNEG.F32 should be 4 bytes");
11718    }
11719
11720    #[test]
11721    fn test_encode_mve_absf32_thumb2() {
11722        let encoder = ArmEncoder::new_thumb2();
11723        let op = ArmOp::MveAbsF32 {
11724            qd: QReg::Q0,
11725            qm: QReg::Q1,
11726        };
11727        let code = encoder.encode(&op).unwrap();
11728        assert_eq!(code.len(), 4, "MVE VABS.F32 should be 4 bytes");
11729    }
11730
11731    /// VCR-RA-001 / immediate-folding precondition: pins the Thumb-2 `AND`
11732    /// immediate encoding for the byte range and documents its bound.
11733    ///
11734    /// The `And { Operand2::Imm }` encoder packs the low 12 bits straight into
11735    /// the `i:imm3:imm8` field WITHOUT applying ThumbExpandImm (the modified-
11736    /// immediate expansion). For `imm <= 0xFF` (e.g. gale's int8 clamps
11737    /// `#0x7e` / `#0x7f`) that is correct — `i:imm3 = 0000` means "imm8
11738    /// zero-extended". So `and r2, r0, #0x7e` encodes to the canonical
11739    /// `00 f0 7e 02`. For `imm >= 0x100` the field would need a true
11740    /// ThumbExpandImm pattern (rotation / replication), which is NOT
11741    /// implemented here — so **immediate folding must gate on `imm <= 0xFF`**
11742    /// until the encoder is hardened to ThumbExpandImm/Ok-or-Err (the
11743    /// "encoder must be Ok-or-Err, never silently wrong" principle, #180/#185).
11744    /// This bound covers the measured `flat_flight` waste (#209).
11745    #[test]
11746    fn and_immediate_encodes_correctly_in_byte_range_documents_fold_bound() {
11747        let encoder = ArmEncoder::new_thumb2();
11748        let op = ArmOp::And {
11749            rd: Reg::R2,
11750            rn: Reg::R0,
11751            op2: Operand2::Imm(0x7e),
11752        };
11753        let code = encoder.encode(&op).unwrap();
11754        assert_eq!(
11755            code,
11756            vec![0x00, 0xf0, 0x7e, 0x02],
11757            "and r2, r0, #0x7e must encode to the canonical AND.W T1 (imm8=0x7e)"
11758        );
11759    }
11760
11761    /// #255: the shared ThumbExpandImm reverse-encoder underpinning the
11762    /// data-processing immediate fix. Encodable modified immediates round-trip to
11763    /// the expected `i:imm3:imm8` field; a genuinely non-modified value is `None`
11764    /// (caller must materialize into a register). Note `1000 = 0xFA ror 30` *is*
11765    /// representable (field 0xF7A) — the old encoder mis-encoded it (raw 0x3E8);
11766    /// this encodes it correctly.
11767    #[test]
11768    fn try_thumb_expand_imm_encodes_modified_immediates() {
11769        assert_eq!(try_thumb_expand_imm(0x7e), Some(0x07e)); // zero-extended byte
11770        assert_eq!(try_thumb_expand_imm(0xff), Some(0x0ff));
11771        assert_eq!(try_thumb_expand_imm(0x0001_0001), Some(0x101)); // 0x00XY00XY
11772        assert_eq!(try_thumb_expand_imm(0xff00_ff00), Some(0x2ff)); // 0xXY00XY00
11773        assert_eq!(try_thumb_expand_imm(0xffff_ffff), Some(0x3ff)); // 0xXYXYXYXY
11774        assert_eq!(try_thumb_expand_imm(0x100), Some(0xf80)); // 0x80 ror 31
11775        assert_eq!(try_thumb_expand_imm(0x8000_0000), Some(0x400)); // 0x80 ror 8
11776        assert_eq!(try_thumb_expand_imm(1000), Some(0xf7a)); // 0xFA ror 30
11777        // Genuinely unrepresentable (bits too far apart for an 8-bit window).
11778        assert_eq!(try_thumb_expand_imm(0x101), None);
11779        assert_eq!(try_thumb_expand_imm(0x12345), None);
11780    }
11781
11782    /// #255: CMP/ADDS/SUBS encode any valid modified immediate correctly, and
11783    /// ERROR (not silently mis-encode) on a genuinely unrepresentable one,
11784    /// forcing the selector to materialize into a register — closing the
11785    /// silent-miscompile class of #251/#253.
11786    #[test]
11787    fn cmp_adds_subs_immediate_error_on_non_modified_imm() {
11788        let encoder = ArmEncoder::new_thumb2();
11789        // cmp r0, #0xff → valid → Ok; cmp r0, #1000 → valid (0xFA ror 30) → Ok.
11790        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 0xff).is_ok());
11791        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 1000).is_ok());
11792        // cmp r0, #0x101 → NOT a modified immediate → Err (materialize-reg).
11793        assert!(
11794            encoder.encode_thumb32_cmp_imm(&Reg::R0, 0x101).is_err(),
11795            "cmp #0x101 must error, not compare the wrong constant"
11796        );
11797        assert!(
11798            encoder
11799                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x101)
11800                .is_err()
11801        );
11802        assert!(
11803            encoder
11804                .encode_thumb32_subs(&Reg::R0, &Reg::R0, 0x101)
11805                .is_err()
11806        );
11807        // ...but a valid modified immediate still encodes.
11808        assert!(
11809            encoder
11810                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x80)
11811                .is_ok()
11812        );
11813    }
11814
11815    /// #257: MLA (multiply-accumulate) encodes as MLS without the bit-4 op flag.
11816    /// `mla r2, r3, r4, r8` (rd=r2, rn=r3, rm=r4, ra=r8) → Thumb-2 `03 fb 04 82`.
11817    #[test]
11818    fn mla_thumb2_encodes_correctly() {
11819        let encoder = ArmEncoder::new_thumb2();
11820        let code = encoder
11821            .encode(&ArmOp::Mla {
11822                rd: Reg::R2,
11823                rn: Reg::R3,
11824                rm: Reg::R4,
11825                ra: Reg::R8,
11826            })
11827            .unwrap();
11828        // hw1 = 0xFB03, hw2 = (8<<12)|(2<<8)|4 = 0x8204
11829        assert_eq!(code, vec![0x03, 0xfb, 0x04, 0x82]);
11830    }
11831
11832    /// #259: LDR/STR (and sub-word) immediate-offset encoders truncated
11833    /// `offset & 0xFFF`, silently targeting the wrong address for offset >= 4096.
11834    /// They now error (the selector must use register-offset addressing) — the
11835    /// load/store sibling of the #253/#255 class. Offsets <= 4095 still encode.
11836    #[test]
11837    fn ldst_imm12_offset_errors_when_out_of_range() {
11838        let encoder = ArmEncoder::new_thumb2();
11839        // offset 0xFFF (4095): valid → Ok; ldr r0, [r1, #4095].
11840        assert!(
11841            encoder
11842                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0xFFF)
11843                .is_ok()
11844        );
11845        // offset 0x1000 (4096): out of imm12 range → Err (not & 0xFFF → #0).
11846        assert!(
11847            encoder
11848                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0x1000)
11849                .is_err(),
11850            "ldr offset 4096 must error, not wrap to 0"
11851        );
11852        assert!(
11853            encoder
11854                .encode_thumb32_str(&Reg::R0, &Reg::R1, 0x1000)
11855                .is_err()
11856        );
11857        assert!(
11858            encoder
11859                .encode_thumb32_ldrb_imm(&Reg::R0, &Reg::R1, 5000)
11860                .is_err()
11861        );
11862        assert!(
11863            encoder
11864                .encode_thumb32_strh_imm(&Reg::R0, &Reg::R1, 5000)
11865                .is_err()
11866        );
11867    }
11868
11869    /// Latent miscompile fix: ADD/SUB with a >0xFF immediate (e.g.
11870    /// `add sp, sp, #frame` for a >=256-byte frame) used ADD.W (T3), whose
11871    /// `i:imm3:imm8` is a ThumbExpandImm modified immediate — so `#256` silently
11872    /// encoded as `#0` (stack corruption). Use ADDW/SUBW (T4), a PLAIN 12-bit
11873    /// immediate, for 0x100..=0xFFF; keep T3 for <=0xFF (bit-identical); error
11874    /// beyond 4095.
11875    #[test]
11876    fn add_sub_large_immediate_use_addw_subw_not_misencoded() {
11877        let encoder = ArmEncoder::new_thumb2();
11878        // add sp, sp, #256  →  ADDW (T4) SP, SP, #256  =  0d f2 00 1d
11879        assert_eq!(
11880            encoder
11881                .encode(&ArmOp::Add {
11882                    rd: Reg::SP,
11883                    rn: Reg::SP,
11884                    op2: Operand2::Imm(256),
11885                })
11886                .unwrap(),
11887            vec![0x0d, 0xf2, 0x00, 0x1d],
11888            "add sp,sp,#256 must be ADDW (plain imm12), not a mis-encoded ADD.W"
11889        );
11890        // sub sp, sp, #256  →  SUBW (T4) SP, SP, #256  =  ad f2 00 1d
11891        assert_eq!(
11892            encoder
11893                .encode(&ArmOp::Sub {
11894                    rd: Reg::SP,
11895                    rn: Reg::SP,
11896                    op2: Operand2::Imm(256),
11897                })
11898                .unwrap(),
11899            vec![0xad, 0xf2, 0x00, 0x1d],
11900        );
11901        // > 4095 has no single-instruction encoding → error, not silent wrong.
11902        assert!(
11903            encoder
11904                .encode(&ArmOp::Add {
11905                    rd: Reg::SP,
11906                    rn: Reg::SP,
11907                    op2: Operand2::Imm(5000),
11908                })
11909                .is_err(),
11910            "add #5000 must error (no single ADDW), not mis-encode"
11911        );
11912    }
11913
11914    /// Closes the data-proc immediate class: AND and CMN now go through
11915    /// `try_thumb_expand_imm` like ORR/EOR/CMP — correct for any modified
11916    /// immediate, `Err` (not raw-pack / NOP) on an un-encodable one. The byte
11917    /// range stays bit-identical (`and r2,r0,#0x7e` is unchanged).
11918    #[test]
11919    fn and_cmn_immediate_thumb_expand_else_error() {
11920        let encoder = ArmEncoder::new_thumb2();
11921        // byte range unchanged (bit-identical with the pre-retrofit encoding)
11922        assert_eq!(
11923            encoder
11924                .encode(&ArmOp::And {
11925                    rd: Reg::R2,
11926                    rn: Reg::R0,
11927                    op2: Operand2::Imm(0x7e),
11928                })
11929                .unwrap(),
11930            vec![0x00, 0xf0, 0x7e, 0x02],
11931        );
11932        // a valid replicated modified immediate now encodes (was silently wrong)
11933        assert!(
11934            encoder
11935                .encode(&ArmOp::And {
11936                    rd: Reg::R2,
11937                    rn: Reg::R0,
11938                    op2: Operand2::Imm(0xff00ff00u32 as i32),
11939                })
11940                .is_ok()
11941        );
11942        // a genuinely un-encodable immediate errors (AND was raw-pack; CMN NOP)
11943        assert!(
11944            encoder
11945                .encode(&ArmOp::And {
11946                    rd: Reg::R2,
11947                    rn: Reg::R0,
11948                    op2: Operand2::Imm(0x101),
11949                })
11950                .is_err()
11951        );
11952        assert!(
11953            encoder
11954                .encode(&ArmOp::Cmn {
11955                    rn: Reg::R0,
11956                    op2: Operand2::Imm(0x101),
11957                })
11958                .is_err(),
11959            "CMN #0x101 must error, not emit a NOP"
11960        );
11961    }
11962
11963    /// VCR-RA-001: ORR/EOR with a small immediate must encode the real
11964    /// instruction (not a silent `0xBF00` NOP). Pins the byte range and the
11965    /// Ok-or-Err bound that makes future Or/Eor immediate folding safe.
11966    #[test]
11967    fn orr_eor_immediate_encode_in_byte_range_else_error() {
11968        let encoder = ArmEncoder::new_thumb2();
11969        // orr r2, r0, #0x7e  →  ORR.W T1, imm8=0x7e
11970        assert_eq!(
11971            encoder
11972                .encode(&ArmOp::Orr {
11973                    rd: Reg::R2,
11974                    rn: Reg::R0,
11975                    op2: Operand2::Imm(0x7e),
11976                })
11977                .unwrap(),
11978            vec![0x40, 0xf0, 0x7e, 0x02],
11979        );
11980        // eor r2, r0, #0x7e  →  EOR.W T1, imm8=0x7e
11981        assert_eq!(
11982            encoder
11983                .encode(&ArmOp::Eor {
11984                    rd: Reg::R2,
11985                    rn: Reg::R0,
11986                    op2: Operand2::Imm(0x7e),
11987                })
11988                .unwrap(),
11989            vec![0x80, 0xf0, 0x7e, 0x02],
11990        );
11991        // Out-of-range immediates error rather than silently mis-encode / NOP.
11992        assert!(
11993            encoder
11994                .encode(&ArmOp::Orr {
11995                    rd: Reg::R2,
11996                    rn: Reg::R0,
11997                    op2: Operand2::Imm(0x140),
11998                })
11999                .is_err(),
12000            "ORR #0x140 must error, not emit a NOP"
12001        );
12002    }
12003
12004    #[test]
12005    fn test_encode_mve_different_qregs() {
12006        let encoder = ArmEncoder::new_thumb2();
12007
12008        // Test that different Q-register numbers produce different encodings
12009        let op1 = ArmOp::MveAddI {
12010            qd: QReg::Q0,
12011            qn: QReg::Q0,
12012            qm: QReg::Q0,
12013            size: MveSize::S32,
12014        };
12015        let op2 = ArmOp::MveAddI {
12016            qd: QReg::Q3,
12017            qn: QReg::Q5,
12018            qm: QReg::Q7,
12019            size: MveSize::S32,
12020        };
12021        let code1 = encoder.encode(&op1).unwrap();
12022        let code2 = encoder.encode(&op2).unwrap();
12023        assert_ne!(
12024            code1, code2,
12025            "Different Q-registers should produce different encodings"
12026        );
12027    }
12028
12029    #[test]
12030    fn test_encode_mve_arm32_loud_err() {
12031        // #615: MVE (Helium) is Thumb-2-only. The ARM32 encoder used to emit
12032        // a silent NOP here (dropping the vector op); it must now be a typed
12033        // Err so a broken "MVE implies Thumb" invariant fails loudly.
12034        let encoder = ArmEncoder::new_arm32();
12035        let op = ArmOp::MveAddI {
12036            qd: QReg::Q0,
12037            qn: QReg::Q1,
12038            qm: QReg::Q2,
12039            size: MveSize::S32,
12040        };
12041        let err = encoder
12042            .encode(&op)
12043            .expect_err("ARM32 MVE must be a loud Err, not a silent NOP (#615)");
12044        assert!(
12045            err.to_string().contains("Thumb-2 only"),
12046            "unexpected error message: {err}"
12047        );
12048    }
12049}