Skip to main content

synth_backend/
arm_encoder.rs

1//! ARM Code Encoder - Converts ARM instructions to binary machine code
2//!
3//! Generates ARM32/Thumb-2 machine code from ARM instruction structures
4
5use synth_core::Result;
6use synth_core::target::FPUPrecision;
7use synth_synthesis::contracts::encoding as encoding_contracts;
8use synth_synthesis::{ArmOp, MemAddr, MveSize, Operand2, QReg, Reg, VfpReg};
9
10/// ARM instruction encoding
11pub struct ArmEncoder {
12    /// Use Thumb mode (vs ARM mode)
13    thumb_mode: bool,
14    /// FPU capability for VFP instruction encoding
15    #[allow(dead_code)]
16    fpu: Option<FPUPrecision>,
17}
18
19impl ArmEncoder {
20    /// Create a new ARM encoder in ARM32 mode
21    pub fn new_arm32() -> Self {
22        Self {
23            thumb_mode: false,
24            fpu: None,
25        }
26    }
27
28    /// Create a new ARM encoder in Thumb-2 mode
29    pub fn new_thumb2() -> Self {
30        Self {
31            thumb_mode: true,
32            fpu: None,
33        }
34    }
35
36    /// Create a new Thumb-2 encoder with FPU capability
37    pub fn new_thumb2_with_fpu(fpu: Option<FPUPrecision>) -> Self {
38        Self {
39            thumb_mode: true,
40            fpu,
41        }
42    }
43
44    /// Encode a single ARM instruction to bytes
45    pub fn encode(&self, op: &ArmOp) -> Result<Vec<u8>> {
46        if self.thumb_mode {
47            self.encode_thumb(op)
48        } else {
49            self.encode_arm(op)
50        }
51    }
52
53    /// Encode an ARM instruction in ARM32 mode (32-bit instructions)
54    /// #206: encode an ARM32 (A32) load/store whose address uses a register
55    /// offset (`[rn, rm{, #off}]`). Returns `None` for ops with no register
56    /// offset (the caller falls through to the immediate-form arms). Computes
57    /// `ip = base + rm` then re-encodes the op against `[ip, #off]`, which works
58    /// uniformly for word/byte/halfword/signed forms. IP (R12) is the scratch
59    /// register the selector already treats as clobberable across memory ops.
60    fn encode_arm_reg_offset_mem(&self, op: &ArmOp) -> Result<Option<Vec<u8>>> {
61        use synth_synthesis::Reg;
62        let addr = match op {
63            ArmOp::Ldr { addr, .. }
64            | ArmOp::Str { addr, .. }
65            | ArmOp::Ldrb { addr, .. }
66            | ArmOp::Strb { addr, .. }
67            | ArmOp::Ldrh { addr, .. }
68            | ArmOp::Strh { addr, .. }
69            | ArmOp::Ldrsb { addr, .. }
70            | ArmOp::Ldrsh { addr, .. } => addr,
71            _ => return Ok(None),
72        };
73        let Some(rm) = addr.offset_reg else {
74            return Ok(None);
75        };
76        let ip = Reg::R12;
77        // ADD ip, base, rm  (cond=AL, opcode=ADD, S=0, register operand2)
78        let add: u32 = 0xE0800000
79            | (reg_to_bits(&addr.base) << 16)
80            | (reg_to_bits(&ip) << 12)
81            | reg_to_bits(&rm);
82        let mut bytes = add.to_le_bytes().to_vec();
83        // Re-encode the op against [ip, #off] (immediate form → no offset_reg,
84        // so this recursion hits the immediate arms, not this helper again).
85        let imm_addr = MemAddr::imm(ip, addr.offset);
86        let imm_op = match op {
87            ArmOp::Ldr { rd, .. } => ArmOp::Ldr {
88                rd: *rd,
89                addr: imm_addr,
90            },
91            ArmOp::Str { rd, .. } => ArmOp::Str {
92                rd: *rd,
93                addr: imm_addr,
94            },
95            ArmOp::Ldrb { rd, .. } => ArmOp::Ldrb {
96                rd: *rd,
97                addr: imm_addr,
98            },
99            ArmOp::Strb { rd, .. } => ArmOp::Strb {
100                rd: *rd,
101                addr: imm_addr,
102            },
103            ArmOp::Ldrh { rd, .. } => ArmOp::Ldrh {
104                rd: *rd,
105                addr: imm_addr,
106            },
107            ArmOp::Strh { rd, .. } => ArmOp::Strh {
108                rd: *rd,
109                addr: imm_addr,
110            },
111            ArmOp::Ldrsb { rd, .. } => ArmOp::Ldrsb {
112                rd: *rd,
113                addr: imm_addr,
114            },
115            ArmOp::Ldrsh { rd, .. } => ArmOp::Ldrsh {
116                rd: *rd,
117                addr: imm_addr,
118            },
119            _ => unreachable!(),
120        };
121        bytes.extend(self.encode_arm(&imm_op)?);
122        Ok(Some(bytes))
123    }
124
125    /// #594: A32 expansion of `ArmOp::CallIndirect` — mirror of the Thumb-2
126    /// arm (same contract: R11 holds the function-pointer table base, entry
127    /// `i` is a 4-byte code address, R12 is the encoder-scratch register):
128    ///
129    /// ```text
130    /// MOVW r12, #size        ; #642: table size (compile-time immediate)
131    /// [MOVT r12, #size>>16]  ; only when size exceeds 16 bits
132    /// CMP  idx, r12          ; bounds guard: index >= size must TRAP
133    /// BLO  +1 insn           ; skip the trap when in bounds
134    /// UDF                    ; WASM Core §4.4.8 out-of-bounds trap
135    /// MOV r12, idx, LSL #2   ; table byte offset
136    /// LDR r12, [r11, r12]    ; load function pointer
137    /// BLX r12                ; indirect call
138    /// ```
139    ///
140    /// #650, `table_byte_offset != 0` (a non-zero table of the contiguous
141    /// R11 region): the pointer load becomes
142    /// `ADD r12, r11, r12; LDR r12, [r12, #offset]` — offset 0 keeps the
143    /// single-load form (single-table modules byte-identical by
144    /// construction).
145    ///
146    /// #664, `null_check` (the table has null slots, linked as ZERO words
147    /// per the layout contract): `CMP r12, #0; BNE +1; UDF` between the
148    /// pointer load and the `BLX` — a call reaching an uninitialized slot
149    /// traps (§4.4.8). `false` keeps the expansion byte-identical.
150    ///
151    /// The §4.4.8 type check is discharged at COMPILE time by the selector's
152    /// closed-world verification (the raw code-pointer table carries no
153    /// runtime type ids) — see the #642 selector guard.
154    fn encode_arm_call_indirect(
155        table_index_reg: &Reg,
156        table_size: u32,
157        table_byte_offset: u32,
158        null_check: bool,
159    ) -> Vec<u8> {
160        let idx = reg_to_bits(table_index_reg);
161        let mut bytes = Vec::with_capacity(32);
162        // MOVW r12, #(size & 0xFFFF) — cond=E 0011 0000 imm4 Rd imm12.
163        let size_lo = table_size & 0xFFFF;
164        let movw: u32 = 0xE300_0000 | ((size_lo >> 12) << 16) | (12 << 12) | (size_lo & 0xFFF);
165        bytes.extend_from_slice(&movw.to_le_bytes());
166        // MOVT r12, #(size >> 16) — only for a table size above 16 bits.
167        let size_hi = table_size >> 16;
168        if size_hi != 0 {
169            let movt: u32 = 0xE340_0000 | ((size_hi >> 12) << 16) | (12 << 12) | (size_hi & 0xFFF);
170            bytes.extend_from_slice(&movt.to_le_bytes());
171        }
172        // CMP idx, r12 — cond=E, opcode=1010, S=1, Rn=idx, Rm=r12.
173        let cmp: u32 = 0xE150_000C | (idx << 16);
174        bytes.extend_from_slice(&cmp.to_le_bytes());
175        // BLO +1 insn (skip the UDF when index < size) — cond=LO(0011),
176        // imm24=0: target = branch + 8.
177        bytes.extend_from_slice(&0x3A00_0000u32.to_le_bytes());
178        // UDF — permanently undefined (same trap idiom as the A32 div-by-zero
179        // guards): call_indirect out-of-bounds trap.
180        bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
181        // MOV r12, idx, LSL #2 — data-processing MOV, register op2 with
182        // imm5=2/LSL: cond=E, opcode=1101, S=0, Rd=r12.
183        let mov: u32 = 0xE1A0C000 | (2 << 7) | idx;
184        bytes.extend_from_slice(&mov.to_le_bytes());
185        if table_byte_offset == 0 {
186            // Table 0 (base = R11 itself): the pre-#650 single-load form.
187            // LDR r12, [r11, r12] — register offset, P=1 U=1 B=0 W=0 L=1.
188            let ldr: u32 = 0xE79BC00C;
189            bytes.extend_from_slice(&ldr.to_le_bytes());
190        } else {
191            // #650: fold the table's compile-time base offset into the
192            // pointer load via the LDR imm12 form.
193            assert!(
194                table_byte_offset <= 4095,
195                "call_indirect table base offset {table_byte_offset} exceeds \
196                 LDR imm12 — the selector must have declined this (#650)"
197            );
198            // ADD r12, r11, r12 — data-processing ADD (register).
199            bytes.extend_from_slice(&0xE08BC00Cu32.to_le_bytes());
200            // LDR r12, [r12, #offset] — immediate offset, P=1 U=1 L=1.
201            let ldr: u32 = 0xE59CC000 | (table_byte_offset & 0xFFF);
202            bytes.extend_from_slice(&ldr.to_le_bytes());
203        }
204        // #664: null-slot trap — only when the table image has null slots
205        // (zero-linked words). A fully-initialized table keeps the pre-#664
206        // bytes identical by construction.
207        if null_check {
208            // CMP r12, #0 — data-processing CMP (immediate), Rn=r12.
209            bytes.extend_from_slice(&0xE35C_0000u32.to_le_bytes());
210            // BNE +1 insn (skip the UDF when the pointer is non-null) —
211            // cond=NE(0001), imm24=0: target = branch + 8.
212            bytes.extend_from_slice(&0x1A00_0000u32.to_le_bytes());
213            // UDF — the §4.4.8 uninitialized-element trap (same idiom as
214            // the bounds guard).
215            bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
216        }
217        // BLX r12 — cond=E, 0001 0010 1111 1111 1111 0011, Rm=r12.
218        let blx: u32 = 0xE12FFF3C;
219        bytes.extend_from_slice(&blx.to_le_bytes());
220        bytes
221    }
222
223    /// #615: A32 (ARM-mode) expansions for the multi-instruction ops that the
224    /// Thumb-2 encoder expands but the A32 arm previously encoded as a single
225    /// literal NOP (`0xE1A00000`) — i64 mul / shifts / rotates / comparisons /
226    /// eqz, plus i64 const/load/store/extend/wrap and the i32 SetCond /
227    /// SelectMove pseudo-ops. Each expansion mirrors its Thumb-2 twin's
228    /// register contract and semantics exactly (A32 conditional execution
229    /// replaces the IT blocks). Returns `Ok(None)` for ops this helper does
230    /// not handle; the caller's match encodes or loudly rejects those.
231    fn encode_arm_expanded(&self, op: &ArmOp) -> Result<Option<Vec<u8>>> {
232        use synth_synthesis::Condition;
233
234        /// A32 condition-field bits (instruction bits [31:28]).
235        fn cond_bits(cond: &Condition) -> u32 {
236            match cond {
237                Condition::EQ => 0x0,
238                Condition::NE => 0x1,
239                Condition::HS => 0x2, // CS: unsigned >=
240                Condition::LO => 0x3, // CC: unsigned <
241                Condition::HI => 0x8, // unsigned >
242                Condition::LS => 0x9, // unsigned <=
243                Condition::GE => 0xA,
244                Condition::LT => 0xB,
245                Condition::GT => 0xC,
246                Condition::LE => 0xD,
247            }
248        }
249        fn w(b: &mut Vec<u8>, word: u32) {
250            b.extend_from_slice(&word.to_le_bytes());
251        }
252        /// MOV<cond> rd, #imm (rotated-immediate form; only 0/1 used here).
253        fn mov_cond_imm(b: &mut Vec<u8>, cond: u32, rd: u32, imm: u32) {
254            w(b, (cond << 28) | 0x03A0_0000 | (rd << 12) | imm);
255        }
256        /// After a flag-setting pair: MOV<cond> rd,#1 ; MOV<!cond> rd,#0.
257        fn set_cond(b: &mut Vec<u8>, cond: &Condition, rd: u32) {
258            mov_cond_imm(b, cond_bits(cond), rd, 1);
259            mov_cond_imm(b, cond_bits(&cond.invert()), rd, 0);
260        }
261        /// CMP rn, rm (register form).
262        fn cmp_reg(b: &mut Vec<u8>, rn: u32, rm: u32) {
263            w(b, 0xE150_0000 | (rn << 16) | rm);
264        }
265        /// SBCS rd, rn, rm — the 64-bit compare idiom's high-word subtract.
266        fn sbcs(b: &mut Vec<u8>, rd: u32, rn: u32, rm: u32) {
267            w(b, 0xE0D0_0000 | (rn << 16) | (rd << 12) | rm);
268        }
269        /// MOVW rd, #imm16.
270        fn movw(b: &mut Vec<u8>, rd: u32, v: u32) {
271            w(
272                b,
273                0xE300_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
274            );
275        }
276        /// MOVT rd, #imm16.
277        fn movt(b: &mut Vec<u8>, rd: u32, v: u32) {
278            w(
279                b,
280                0xE340_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
281            );
282        }
283        /// Register-controlled shift: MOV rd, rn, <LSL|LSR|ASR> rs.
284        /// `ty`: 0=LSL, 1=LSR, 2=ASR. A32 uses the bottom byte of rs;
285        /// amounts of 32 or more yield 0 (LSL/LSR) or all-sign (ASR) — same
286        /// semantics the Thumb-2 expansions rely on.
287        fn shift_reg(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, rs: u32) {
288            w(b, 0xE1A0_0010 | (rd << 12) | (rs << 8) | (ty << 5) | rn);
289        }
290        const LSL: u32 = 0;
291        const LSR: u32 = 1;
292        const ASR: u32 = 2;
293        /// Immediate-shift move: MOV rd, rn, <LSL|LSR|ASR> #imm.
294        fn shift_imm(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, imm: u32) {
295            w(
296                b,
297                0xE1A0_0000 | (rd << 12) | ((imm & 0x1F) << 7) | (ty << 5) | rn,
298            );
299        }
300        /// Data-processing register form: `base | rn<<16 | rd<<12 | rm`.
301        /// `base` carries cond/opcode/S (e.g. 0xE090_0000 = ADDS).
302        fn dp_reg(b: &mut Vec<u8>, base: u32, rd: u32, rn: u32, rm: u32) {
303            w(b, base | (rn << 16) | (rd << 12) | rm);
304        }
305        /// ORR rd, rd, rm, LSR #31 — the carry-propagation idiom of the
306        /// shift-subtract division loop (bring rm's MSB into rd's bit 0).
307        fn orr_lsr31(b: &mut Vec<u8>, rd: u32, rm: u32) {
308            w(
309                b,
310                0xE180_0000 | (rd << 16) | (rd << 12) | (31 << 7) | (1 << 5) | rm,
311            );
312        }
313        /// 64-bit two's-complement negate of the lo:hi pair (MVN/MVN/ADDS/ADC).
314        fn negate64(b: &mut Vec<u8>, lo: u32, hi: u32) {
315            w(b, 0xE1E0_0000 | (lo << 12) | lo); //           MVN  lo, lo
316            w(b, 0xE1E0_0000 | (hi << 12) | hi); //           MVN  hi, hi
317            w(b, 0xE290_0001 | (lo << 16) | (lo << 12)); //   ADDS lo, lo, #1
318            w(b, 0xE2A0_0000 | (hi << 16) | (hi << 12)); //   ADC  hi, hi, #0
319        }
320        /// TST x, x ; BPL +4-instructions — the "skip the negate64 when the
321        /// sign bit is clear" guard of the signed div/rem arms.
322        fn skip_negate_if_positive(b: &mut Vec<u8>, x: u32) {
323            w(b, 0xE110_0000 | (x << 16) | x); // TST x, x
324            w(b, 0x5A00_0003); //                 BPL +4 insns (past negate64)
325        }
326        /// The 64-iteration shift-subtract division loop — A32 transcription
327        /// of the Thumb-2 #610 core: dividend R0:R1, divisor R2:R3, quotient
328        /// R4:R5, remainder R6:R7, loop counter in `counter` (R12 or R8).
329        fn div_loop(b: &mut Vec<u8>, counter: u32) {
330            w(b, 0xE3A0_0040 | (counter << 12)); // MOV counter, #64
331            let loop_start = b.len();
332            // quotient <<= 1
333            shift_imm(b, LSL, 5, 5, 1);
334            orr_lsr31(b, 5, 4);
335            shift_imm(b, LSL, 4, 4, 1);
336            // remainder <<= 1, OR in dividend MSB
337            shift_imm(b, LSL, 7, 7, 1);
338            orr_lsr31(b, 7, 6);
339            shift_imm(b, LSL, 6, 6, 1);
340            orr_lsr31(b, 6, 1);
341            // dividend <<= 1
342            shift_imm(b, LSL, 1, 1, 1);
343            orr_lsr31(b, 1, 0);
344            shift_imm(b, LSL, 0, 0, 1);
345            // if remainder >= divisor (64-bit unsigned): subtract, set q bit
346            w(b, 0xE157_0003); // CMP R7, R3      (high words)
347            w(b, 0x8A00_0002); // BHI .subtract   (+2 insns)
348            w(b, 0x3A00_0004); // BLO .next       (+4 insns)
349            w(b, 0xE156_0002); // CMP R6, R2      (low words, highs equal)
350            w(b, 0x3A00_0002); // BLO .next       (+2 insns)
351            w(b, 0xE056_6002); // .subtract: SUBS R6, R6, R2
352            w(b, 0xE0C7_7003); //            SBC  R7, R7, R3
353            w(b, 0xE384_4001); //            ORR  R4, R4, #1
354            // .next: decrement and loop
355            w(b, 0xE250_0001 | (counter << 16) | (counter << 12)); // SUBS counter, #1
356            let diff = (loop_start as i64) - (b.len() as i64 + 8);
357            w(b, 0x1A00_0000 | (((diff / 4) as u32) & 0x00FF_FFFF)); // BNE loop
358        }
359        /// 32-bit population count on working register `x` — A32 transcription
360        /// of the Thumb-2 I64Popcnt per-word core (mul-based fold): `c` is the
361        /// constant register, R12 the shifted temp. Both are clobbered.
362        fn popcnt_word(b: &mut Vec<u8>, x: u32, c: u32) {
363            // x = x - ((x >> 1) & 0x55555555)
364            shift_imm(b, LSR, 12, x, 1);
365            movw(b, c, 0x5555);
366            movt(b, c, 0x5555);
367            dp_reg(b, 0xE000_0000, 12, 12, c); // AND R12, R12, c
368            dp_reg(b, 0xE040_0000, x, x, 12); //  SUB x, x, R12
369            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
370            movw(b, c, 0x3333);
371            movt(b, c, 0x3333);
372            dp_reg(b, 0xE000_0000, 12, x, c); //  AND R12, x, c
373            shift_imm(b, LSR, x, x, 2);
374            dp_reg(b, 0xE000_0000, x, x, c); //   AND x, x, c
375            dp_reg(b, 0xE080_0000, x, x, 12); //  ADD x, x, R12
376            // x = (x + (x >> 4)) & 0x0F0F0F0F
377            shift_imm(b, LSR, 12, x, 4);
378            dp_reg(b, 0xE080_0000, x, x, 12); //  ADD x, x, R12
379            movw(b, c, 0x0F0F);
380            movt(b, c, 0x0F0F);
381            dp_reg(b, 0xE000_0000, x, x, c); //   AND x, x, c
382            // x = (x * 0x01010101) >> 24
383            movw(b, c, 0x0101);
384            movt(b, c, 0x0101);
385            w(b, 0xE000_0090 | (x << 16) | (c << 8) | x); // MUL x, x, c
386            shift_imm(b, LSR, x, x, 24);
387        }
388
389        let mut b: Vec<u8> = Vec::new();
390        match op {
391            // SetCond: materialize a flags-predicate as 0/1 — the A32 twin of
392            // the Thumb `ITE cond; MOV rd,#1; MOV rd,#0`.
393            ArmOp::SetCond { rd, cond } => {
394                set_cond(&mut b, cond, reg_to_bits(rd));
395            }
396
397            // SelectMove: conditional register move (Thumb: IT cond; MOV).
398            ArmOp::SelectMove { rd, rm, cond } => {
399                w(
400                    &mut b,
401                    (cond_bits(cond) << 28)
402                        | 0x01A0_0000
403                        | (reg_to_bits(rd) << 12)
404                        | reg_to_bits(rm),
405                );
406            }
407
408            // I64SetCond: compare two i64 register pairs, 0/1 into rd.
409            // EQ/NE: CMP lo,lo; CMPEQ hi,hi (only if lows equal); set.
410            // Ordered: CMP lo,lo; SBCS rd,hi,hi; set — with the same
411            // operand-swap + condition mapping as the Thumb-2 arm.
412            ArmOp::I64SetCond {
413                rd,
414                rn_lo,
415                rn_hi,
416                rm_lo,
417                rm_hi,
418                cond,
419            } => {
420                let rd_b = reg_to_bits(rd);
421                let (n_lo, n_hi, m_lo, m_hi) = (
422                    reg_to_bits(rn_lo),
423                    reg_to_bits(rn_hi),
424                    reg_to_bits(rm_lo),
425                    reg_to_bits(rm_hi),
426                );
427                match cond {
428                    Condition::EQ | Condition::NE => {
429                        cmp_reg(&mut b, n_lo, m_lo);
430                        // CMP<EQ> rn_hi, rm_hi — compare highs only if lows equal.
431                        w(&mut b, 0x0150_0000 | (n_hi << 16) | m_hi);
432                        set_cond(&mut b, cond, rd_b);
433                    }
434                    // (swap operands?, condition after SBCS) per the Thumb arm:
435                    // LT/GE/LO/HS compare (rn, rm); GT/LE/HI/LS swap to (rm, rn).
436                    Condition::LT => {
437                        cmp_reg(&mut b, n_lo, m_lo);
438                        sbcs(&mut b, rd_b, n_hi, m_hi);
439                        set_cond(&mut b, &Condition::LT, rd_b);
440                    }
441                    Condition::GE => {
442                        cmp_reg(&mut b, n_lo, m_lo);
443                        sbcs(&mut b, rd_b, n_hi, m_hi);
444                        set_cond(&mut b, &Condition::GE, rd_b);
445                    }
446                    Condition::GT => {
447                        cmp_reg(&mut b, m_lo, n_lo);
448                        sbcs(&mut b, rd_b, m_hi, n_hi);
449                        set_cond(&mut b, &Condition::LT, rd_b);
450                    }
451                    Condition::LE => {
452                        cmp_reg(&mut b, m_lo, n_lo);
453                        sbcs(&mut b, rd_b, m_hi, n_hi);
454                        set_cond(&mut b, &Condition::GE, rd_b);
455                    }
456                    Condition::LO => {
457                        cmp_reg(&mut b, n_lo, m_lo);
458                        sbcs(&mut b, rd_b, n_hi, m_hi);
459                        set_cond(&mut b, &Condition::LO, rd_b);
460                    }
461                    Condition::HS => {
462                        cmp_reg(&mut b, n_lo, m_lo);
463                        sbcs(&mut b, rd_b, n_hi, m_hi);
464                        set_cond(&mut b, &Condition::HS, rd_b);
465                    }
466                    Condition::HI => {
467                        cmp_reg(&mut b, m_lo, n_lo);
468                        sbcs(&mut b, rd_b, m_hi, n_hi);
469                        set_cond(&mut b, &Condition::LO, rd_b);
470                    }
471                    Condition::LS => {
472                        cmp_reg(&mut b, m_lo, n_lo);
473                        sbcs(&mut b, rd_b, m_hi, n_hi);
474                        set_cond(&mut b, &Condition::HS, rd_b);
475                    }
476                }
477            }
478
479            // I64SetCondZ: ORRS rd, lo, hi sets Z iff the pair is zero.
480            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
481                let rd_b = reg_to_bits(rd);
482                w(
483                    &mut b,
484                    0xE190_0000 | (reg_to_bits(rn_lo) << 16) | (rd_b << 12) | reg_to_bits(rn_hi),
485                );
486                set_cond(&mut b, &Condition::EQ, rd_b);
487            }
488
489            // i64 comparison wrappers: delegate to I64SetCond/Z, mirroring the
490            // Thumb-2 delegation arms.
491            ArmOp::I64Eqz { rd, rnlo, rnhi } => {
492                return self
493                    .encode_arm(&ArmOp::I64SetCondZ {
494                        rd: *rd,
495                        rn_lo: *rnlo,
496                        rn_hi: *rnhi,
497                    })
498                    .map(Some);
499            }
500            ArmOp::I64Eq {
501                rd,
502                rnlo,
503                rnhi,
504                rmlo,
505                rmhi,
506            }
507            | ArmOp::I64Ne {
508                rd,
509                rnlo,
510                rnhi,
511                rmlo,
512                rmhi,
513            }
514            | ArmOp::I64LtS {
515                rd,
516                rnlo,
517                rnhi,
518                rmlo,
519                rmhi,
520            }
521            | ArmOp::I64LtU {
522                rd,
523                rnlo,
524                rnhi,
525                rmlo,
526                rmhi,
527            }
528            | ArmOp::I64LeS {
529                rd,
530                rnlo,
531                rnhi,
532                rmlo,
533                rmhi,
534            }
535            | ArmOp::I64LeU {
536                rd,
537                rnlo,
538                rnhi,
539                rmlo,
540                rmhi,
541            }
542            | ArmOp::I64GtS {
543                rd,
544                rnlo,
545                rnhi,
546                rmlo,
547                rmhi,
548            }
549            | ArmOp::I64GtU {
550                rd,
551                rnlo,
552                rnhi,
553                rmlo,
554                rmhi,
555            }
556            | ArmOp::I64GeS {
557                rd,
558                rnlo,
559                rnhi,
560                rmlo,
561                rmhi,
562            }
563            | ArmOp::I64GeU {
564                rd,
565                rnlo,
566                rnhi,
567                rmlo,
568                rmhi,
569            } => {
570                let cond = match op {
571                    ArmOp::I64Eq { .. } => Condition::EQ,
572                    ArmOp::I64Ne { .. } => Condition::NE,
573                    ArmOp::I64LtS { .. } => Condition::LT,
574                    ArmOp::I64LtU { .. } => Condition::LO,
575                    ArmOp::I64LeS { .. } => Condition::LE,
576                    ArmOp::I64LeU { .. } => Condition::LS,
577                    ArmOp::I64GtS { .. } => Condition::GT,
578                    ArmOp::I64GtU { .. } => Condition::HI,
579                    ArmOp::I64GeS { .. } => Condition::GE,
580                    _ => Condition::HS,
581                };
582                return self
583                    .encode_arm(&ArmOp::I64SetCond {
584                        rd: *rd,
585                        rn_lo: *rnlo,
586                        rn_hi: *rnhi,
587                        rm_lo: *rmlo,
588                        rm_hi: *rmhi,
589                        cond,
590                    })
591                    .map(Some);
592            }
593
594            // I64Mul: cross products into R12, then UMULL — same sequence and
595            // ordering as the Thumb-2 arm (R12 is encoder scratch, #212).
596            ArmOp::I64Mul {
597                rd_lo,
598                rd_hi,
599                rn_lo,
600                rn_hi,
601                rm_lo,
602                rm_hi,
603            } => {
604                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
605                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
606                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
607                // MUL R12, rn_lo, rm_hi   (R12 = a_lo * b_hi)
608                w(&mut b, 0xE000_0090 | (12 << 16) | (mh << 8) | nl);
609                // MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
610                w(
611                    &mut b,
612                    0xE020_0090 | (12 << 16) | (12 << 12) | (ml << 8) | nh,
613                );
614                // UMULL rd_lo, rd_hi, rn_lo, rm_lo
615                w(
616                    &mut b,
617                    0xE080_0090 | (dh << 16) | (dl << 12) | (ml << 8) | nl,
618                );
619                // ADD rd_hi, rd_hi, R12
620                w(&mut b, 0xE080_0000 | (dh << 16) | (dh << 12) | 12);
621            }
622
623            // I64Shl / I64ShrU / I64ShrS: same small/large-shift structure as
624            // the Thumb-2 arms (rm_hi is the scratch register; amounts are
625            // masked to 6 bits; register-controlled shifts >= 32 yield 0,
626            // which the small path relies on for n = 0).
627            ArmOp::I64Shl {
628                rd_lo,
629                rd_hi,
630                rn_lo,
631                rn_hi,
632                rm_lo,
633                rm_hi,
634            } => {
635                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
636                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
637                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
638                w(&mut b, 0xE200_003F | (ml << 16) | (ml << 12)); // AND  ml, ml, #63
639                w(&mut b, 0xE250_0020 | (ml << 16) | (mh << 12)); // SUBS mh, ml, #32
640                w(&mut b, 0x5A00_0005); //                            BPL  .large
641                w(&mut b, 0xE260_0020 | (ml << 16) | (mh << 12)); // RSB  mh, ml, #32
642                shift_reg(&mut b, LSR, mh, nl, mh); //               mh = lo >> (32-n)
643                shift_reg(&mut b, LSL, dh, nh, ml); //               dh = hi << n
644                w(&mut b, 0xE180_0000 | (dh << 16) | (dh << 12) | mh); // ORR dh, dh, mh
645                shift_reg(&mut b, LSL, dl, nl, ml); //               dl = lo << n
646                w(&mut b, 0xEA00_0001); //                            B    .done
647                shift_reg(&mut b, LSL, dh, nl, mh); //               .large: dh = lo << (n-32)
648                w(&mut b, 0xE3A0_0000 | (dl << 12)); //              MOV  dl, #0
649            }
650            ArmOp::I64ShrU {
651                rd_lo,
652                rd_hi,
653                rn_lo,
654                rn_hi,
655                rm_lo,
656                rm_hi,
657            } => {
658                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
659                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
660                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
661                w(&mut b, 0xE200_003F | (ml << 16) | (ml << 12)); // AND  ml, ml, #63
662                w(&mut b, 0xE250_0020 | (ml << 16) | (mh << 12)); // SUBS mh, ml, #32
663                w(&mut b, 0x5A00_0005); //                            BPL  .large
664                w(&mut b, 0xE260_0020 | (ml << 16) | (mh << 12)); // RSB  mh, ml, #32
665                shift_reg(&mut b, LSL, mh, nh, mh); //               mh = hi << (32-n)
666                shift_reg(&mut b, LSR, dl, nl, ml); //               dl = lo >> n
667                w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | mh); // ORR dl, dl, mh
668                shift_reg(&mut b, LSR, dh, nh, ml); //               dh = hi >> n
669                w(&mut b, 0xEA00_0001); //                            B    .done
670                shift_reg(&mut b, LSR, dl, nh, mh); //               .large: dl = hi >> (n-32)
671                w(&mut b, 0xE3A0_0000 | (dh << 12)); //              MOV  dh, #0
672            }
673            ArmOp::I64ShrS {
674                rd_lo,
675                rd_hi,
676                rn_lo,
677                rn_hi,
678                rm_lo,
679                rm_hi,
680            } => {
681                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
682                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
683                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
684                w(&mut b, 0xE200_003F | (ml << 16) | (ml << 12)); // AND  ml, ml, #63
685                w(&mut b, 0xE250_0020 | (ml << 16) | (mh << 12)); // SUBS mh, ml, #32
686                w(&mut b, 0x5A00_0005); //                            BPL  .large
687                w(&mut b, 0xE260_0020 | (ml << 16) | (mh << 12)); // RSB  mh, ml, #32
688                shift_reg(&mut b, LSL, mh, nh, mh); //               mh = hi << (32-n)
689                shift_reg(&mut b, LSR, dl, nl, ml); //               dl = lo >> n
690                w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | mh); // ORR dl, dl, mh
691                shift_reg(&mut b, ASR, dh, nh, ml); //               dh = hi >> n (arith)
692                w(&mut b, 0xEA00_0001); //                            B    .done
693                shift_reg(&mut b, ASR, dl, nh, mh); //               .large: dl = hi >> (n-32)
694                w(&mut b, 0xE1A0_0040 | (dh << 12) | (31 << 7) | nh); // ASR dh, nh, #31
695            }
696
697            // I64Rotl / I64Rotr: the #610 fixed-ABI wrapper (A32 form) around
698            // the same fixed-register core as the Thumb-2 arms — value in
699            // R0:R1, amount in R2, scratch R3 + R12.
700            ArmOp::I64Rotl {
701                rdlo,
702                rdhi,
703                rnlo,
704                rnhi,
705                shift,
706            } => {
707                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
708                for word in [
709                    0xE202_203Fu32, // AND  R2, R2, #63   (mask amount mod 64)
710                    0xE252_3020,    // SUBS R3, R2, #32   (R3 = n-32, sets N)
711                    0x5A00_0007,    // BPL  .large        (n >= 32)
712                    // --- small rotation (n < 32) ---
713                    0xE262_3020, // RSB  R3, R2, #32   (R3 = 32-n)
714                    0xE1A0_C330, // LSR  R12, R0, R3   (lo >> (32-n))
715                    0xE1A0_3331, // LSR  R3, R1, R3    (hi >> (32-n))
716                    0xE1A0_1211, // LSL  R1, R1, R2    (hi << n)
717                    0xE181_100C, // ORR  R1, R1, R12   (new_hi)
718                    0xE1A0_0210, // LSL  R0, R0, R2    (lo << n)
719                    0xE180_0003, // ORR  R0, R0, R3    (new_lo)
720                    0xEA00_0007, // B    .done
721                    // --- large rotation (n >= 32), R3 = m = n-32 ---
722                    0xE263_2020, // RSB  R2, R3, #32   (R2 = 32-m = 64-n)
723                    0xE1A0_C231, // LSR  R12, R1, R2   (hi >> (64-n))
724                    0xE1A0_2230, // LSR  R2, R0, R2    (lo >> (64-n))
725                    0xE1A0_0310, // LSL  R0, R0, R3    (lo << m)
726                    0xE1A0_1311, // LSL  R1, R1, R3    (hi << m)
727                    0xE180_C00C, // ORR  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
728                    0xE181_0002, // ORR  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
729                    0xE1A0_100C, // MOV  R1, R12       (new_hi into place)
730                ] {
731                    w(&mut b, word);
732                }
733                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
734            }
735            ArmOp::I64Rotr {
736                rdlo,
737                rdhi,
738                rnlo,
739                rnhi,
740                shift,
741            } => {
742                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
743                for word in [
744                    0xE202_203Fu32, // AND  R2, R2, #63   (mask amount mod 64)
745                    0xE252_3020,    // SUBS R3, R2, #32   (R3 = n-32, sets N)
746                    0x5A00_0007,    // BPL  .large        (n >= 32)
747                    // --- small rotation (n < 32) ---
748                    0xE262_3020, // RSB  R3, R2, #32   (R3 = 32-n)
749                    0xE1A0_C311, // LSL  R12, R1, R3   (hi << (32-n))
750                    0xE1A0_3310, // LSL  R3, R0, R3    (lo << (32-n))
751                    0xE1A0_0230, // LSR  R0, R0, R2    (lo >> n)
752                    0xE180_000C, // ORR  R0, R0, R12   (new_lo)
753                    0xE1A0_1231, // LSR  R1, R1, R2    (hi >> n)
754                    0xE181_1003, // ORR  R1, R1, R3    (new_hi)
755                    0xEA00_0007, // B    .done
756                    // --- large rotation (n >= 32), R3 = m = n-32 ---
757                    0xE263_2020, // RSB  R2, R3, #32   (R2 = 32-m = 64-n)
758                    0xE1A0_C210, // LSL  R12, R0, R2   (lo << (64-n))
759                    0xE1A0_2211, // LSL  R2, R1, R2    (hi << (64-n))
760                    0xE1A0_1331, // LSR  R1, R1, R3    (hi >> m)
761                    0xE181_C00C, // ORR  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
762                    0xE1A0_1330, // LSR  R1, R0, R3    (lo >> m)
763                    0xE181_1002, // ORR  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
764                    0xE1A0_000C, // MOV  R0, R12       (new_lo into place)
765                ] {
766                    w(&mut b, word);
767                }
768                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
769            }
770
771            // I64Clz: CLZ(hi), or 32 + CLZ(lo) when hi == 0. Conditional
772            // execution replaces the Thumb branches; like the Thumb arm, the
773            // high word of the result pair (rnhi) is cleared last.
774            ArmOp::I64Clz { rd, rnlo, rnhi } => {
775                let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
776                w(&mut b, 0xE350_0000 | (hi << 16)); //              CMP   rnhi, #0
777                w(&mut b, 0x116F_0F10 | (rd_b << 12) | hi); //       CLZNE rd, rnhi
778                w(&mut b, 0x016F_0F10 | (rd_b << 12) | lo); //       CLZEQ rd, rnlo
779                w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
780                w(&mut b, 0xE3A0_0000 | (hi << 12)); //              MOV   rnhi, #0
781            }
782
783            // I64Ctz: CLZ(RBIT(lo)), or 32 + CLZ(RBIT(hi)) when lo == 0.
784            // RBIT/CLZ leave the flags intact, so the CMP's Z survives to the
785            // conditional ADD.
786            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
787                let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
788                w(&mut b, 0xE350_0000 | (lo << 16)); //              CMP    rnlo, #0
789                w(&mut b, 0x16FF_0F30 | (rd_b << 12) | lo); //       RBITNE rd, rnlo
790                w(&mut b, 0x06FF_0F30 | (rd_b << 12) | hi); //       RBITEQ rd, rnhi
791                w(&mut b, 0xE16F_0F10 | (rd_b << 12) | rd_b); //     CLZ    rd, rd
792                w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
793                w(&mut b, 0xE3A0_0000 | (hi << 12)); //              MOV    rnhi, #0
794            }
795
796            // I64Const: MOVW/MOVT per half (MOVT elided when the half fits in
797            // 16 bits, mirroring the Thumb-2 arm).
798            ArmOp::I64Const { rdlo, rdhi, value } => {
799                let lo32 = *value as u32;
800                let hi32 = (*value >> 32) as u32;
801                movw(&mut b, reg_to_bits(rdlo), lo32 & 0xFFFF);
802                if lo32 > 0xFFFF {
803                    movt(&mut b, reg_to_bits(rdlo), lo32 >> 16);
804                }
805                movw(&mut b, reg_to_bits(rdhi), hi32 & 0xFFFF);
806                if hi32 > 0xFFFF {
807                    movt(&mut b, reg_to_bits(rdhi), hi32 >> 16);
808                }
809            }
810
811            // I64Ldr / I64Str: two word accesses at [base, #off] / #off+4.
812            // A register offset is materialized into IP once (the #206/#372
813            // hazard: dropping it would read the wrong address).
814            ArmOp::I64Ldr { rdlo, rdhi, addr } | ArmOp::I64Str { rdlo, rdhi, addr } => {
815                let base = if let Some(rm) = addr.offset_reg {
816                    // ADD ip, base, rm
817                    w(
818                        &mut b,
819                        0xE080_0000
820                            | (reg_to_bits(&addr.base) << 16)
821                            | (12 << 12)
822                            | reg_to_bits(&rm),
823                    );
824                    12
825                } else {
826                    reg_to_bits(&addr.base)
827                };
828                if addr.offset < 0 || addr.offset > 0xFFB {
829                    return Err(synth_core::Error::synthesis(format!(
830                        "i64 load/store offset {} out of the A32 imm12 range (0..=4091) — materialize the offset into a register",
831                        addr.offset
832                    )));
833                }
834                let off = addr.offset as u32;
835                let opc: u32 = if matches!(op, ArmOp::I64Ldr { .. }) {
836                    0xE590_0000 // LDR
837                } else {
838                    0xE580_0000 // STR
839                };
840                w(&mut b, opc | (base << 16) | (reg_to_bits(rdlo) << 12) | off);
841                w(
842                    &mut b,
843                    opc | (base << 16) | (reg_to_bits(rdhi) << 12) | (off + 4),
844                );
845            }
846
847            // I64ExtendI32S: rdlo = rn; rdhi = rdlo >> 31 (arithmetic).
848            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
849                if rdlo != rn {
850                    w(
851                        &mut b,
852                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
853                    );
854                }
855                w(
856                    &mut b,
857                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
858                );
859            }
860
861            // I64ExtendI32U: rdlo = rn; rdhi = 0.
862            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
863                if rdlo != rn {
864                    w(
865                        &mut b,
866                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
867                    );
868                }
869                w(&mut b, 0xE3A0_0000 | (reg_to_bits(rdhi) << 12));
870            }
871
872            // I64Extend8S / I64Extend16S: SXTB/SXTH then sign-fill the high word.
873            ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
874                w(
875                    &mut b,
876                    0xE6AF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
877                );
878                w(
879                    &mut b,
880                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
881                );
882            }
883            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
884                w(
885                    &mut b,
886                    0xE6BF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
887                );
888                w(
889                    &mut b,
890                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
891                );
892            }
893            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
894                if rdlo != rnlo {
895                    w(
896                        &mut b,
897                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
898                    );
899                }
900                w(
901                    &mut b,
902                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rnlo),
903                );
904            }
905
906            // I32WrapI64: take the low word. When rd == rnlo this is a genuine
907            // no-op (the one case where a NOP word is the correct encoding).
908            ArmOp::I32WrapI64 { rd, rnlo } => {
909                w(
910                    &mut b,
911                    0xE1A0_0000 | (reg_to_bits(rd) << 12) | reg_to_bits(rnlo),
912                );
913            }
914
915            // I64Add / I64Sub: the classic pair — ADDS lo + ADC hi (SUBS/SBC).
916            // The selector emits these as separate Adds/Adc ops; the fused
917            // variants are verification-constructed, but they encode for real.
918            ArmOp::I64Add {
919                rdlo,
920                rdhi,
921                rnlo,
922                rnhi,
923                rmlo,
924                rmhi,
925            } => {
926                dp_reg(
927                    &mut b,
928                    0xE090_0000, // ADDS
929                    reg_to_bits(rdlo),
930                    reg_to_bits(rnlo),
931                    reg_to_bits(rmlo),
932                );
933                dp_reg(
934                    &mut b,
935                    0xE0A0_0000, // ADC
936                    reg_to_bits(rdhi),
937                    reg_to_bits(rnhi),
938                    reg_to_bits(rmhi),
939                );
940            }
941            ArmOp::I64Sub {
942                rdlo,
943                rdhi,
944                rnlo,
945                rnhi,
946                rmlo,
947                rmhi,
948            } => {
949                dp_reg(
950                    &mut b,
951                    0xE050_0000, // SUBS
952                    reg_to_bits(rdlo),
953                    reg_to_bits(rnlo),
954                    reg_to_bits(rmlo),
955                );
956                dp_reg(
957                    &mut b,
958                    0xE0C0_0000, // SBC
959                    reg_to_bits(rdhi),
960                    reg_to_bits(rnhi),
961                    reg_to_bits(rmhi),
962                );
963            }
964
965            // I64And / I64Or / I64Xor: two independent word ops.
966            ArmOp::I64And {
967                rdlo,
968                rdhi,
969                rnlo,
970                rnhi,
971                rmlo,
972                rmhi,
973            }
974            | ArmOp::I64Or {
975                rdlo,
976                rdhi,
977                rnlo,
978                rnhi,
979                rmlo,
980                rmhi,
981            }
982            | ArmOp::I64Xor {
983                rdlo,
984                rdhi,
985                rnlo,
986                rnhi,
987                rmlo,
988                rmhi,
989            } => {
990                let base = match op {
991                    ArmOp::I64And { .. } => 0xE000_0000, // AND
992                    ArmOp::I64Or { .. } => 0xE180_0000,  // ORR
993                    _ => 0xE020_0000,                    // EOR
994                };
995                dp_reg(
996                    &mut b,
997                    base,
998                    reg_to_bits(rdlo),
999                    reg_to_bits(rnlo),
1000                    reg_to_bits(rmlo),
1001                );
1002                dp_reg(
1003                    &mut b,
1004                    base,
1005                    reg_to_bits(rdhi),
1006                    reg_to_bits(rnhi),
1007                    reg_to_bits(rmhi),
1008                );
1009            }
1010
1011            // I64DivU: binary long division — A32 transcription of the Thumb-2
1012            // #610/#613 arm (fixed-ABI marshal, zero-divisor trap, 64-round
1013            // shift-subtract core, quotient to R0:R1, result to rd pair).
1014            ArmOp::I64DivU {
1015                rdlo,
1016                rdhi,
1017                rnlo,
1018                rnhi,
1019                rmlo,
1020                rmhi,
1021                elide_zero_guard,
1022            } => {
1023                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1024                // #494 phase 2b: elided only under a certificate-discharged
1025                // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
1026                if !elide_zero_guard {
1027                    emit_a32_i64_divisor_zero_trap(&mut b);
1028                }
1029                w(&mut b, 0xE92D_00F0); // PUSH {R4-R7}
1030                for r in 4..8u32 {
1031                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1032                }
1033                div_loop(&mut b, 12); // counter in R12 (encoder scratch)
1034                w(&mut b, 0xE1A0_0004); // MOV R0, R4 (quotient lo)
1035                w(&mut b, 0xE1A0_1005); // MOV R1, R5 (quotient hi)
1036                w(&mut b, 0xE8BD_00F0); // POP {R4-R7}
1037                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1038            }
1039
1040            // I64DivS: sign-extract, unsigned core, conditional negate —
1041            // A32 transcription of the Thumb-2 arm.
1042            ArmOp::I64DivS {
1043                rdlo,
1044                rdhi,
1045                rnlo,
1046                rnhi,
1047                rmlo,
1048                rmhi,
1049                elide_zero_guard,
1050                elide_overflow_guard,
1051            } => {
1052                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1053                // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
1054                // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
1055                // the #633 overflow guard falls ONLY to
1056                // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
1057                // divisor-nonzero fact alone must keep it.
1058                if !elide_zero_guard {
1059                    emit_a32_i64_divisor_zero_trap(&mut b);
1060                }
1061                if !elide_overflow_guard {
1062                    // #633: INT64_MIN / -1 overflows — trap like the i32 path
1063                    // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
1064                    emit_a32_i64_divs_overflow_trap(&mut b);
1065                }
1066                w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1067                w(&mut b, 0xE021_9003); // EOR R9, R1, R3 (result sign in MSB)
1068                skip_negate_if_positive(&mut b, 1);
1069                negate64(&mut b, 0, 1);
1070                skip_negate_if_positive(&mut b, 3);
1071                negate64(&mut b, 2, 3);
1072                for r in 4..8u32 {
1073                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1074                }
1075                div_loop(&mut b, 8); // counter in R8 (saved above)
1076                w(&mut b, 0xE1A0_0004); // MOV R0, R4
1077                w(&mut b, 0xE1A0_1005); // MOV R1, R5
1078                skip_negate_if_positive(&mut b, 9);
1079                negate64(&mut b, 0, 1);
1080                w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1081                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1082            }
1083
1084            // I64RemU: same core as I64DivU, returns the remainder (R6:R7).
1085            ArmOp::I64RemU {
1086                rdlo,
1087                rdhi,
1088                rnlo,
1089                rnhi,
1090                rmlo,
1091                rmhi,
1092                elide_zero_guard,
1093            } => {
1094                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1095                if !elide_zero_guard {
1096                    emit_a32_i64_divisor_zero_trap(&mut b);
1097                }
1098                w(&mut b, 0xE92D_01F0); // PUSH {R4-R8}
1099                for r in 4..8u32 {
1100                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1101                }
1102                div_loop(&mut b, 8);
1103                w(&mut b, 0xE1A0_0006); // MOV R0, R6 (remainder lo)
1104                w(&mut b, 0xE1A0_1007); // MOV R1, R7 (remainder hi)
1105                w(&mut b, 0xE8BD_01F0); // POP {R4-R8}
1106                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1107            }
1108
1109            // I64RemS: remainder takes the DIVIDEND's sign (WASM semantics).
1110            ArmOp::I64RemS {
1111                rdlo,
1112                rdhi,
1113                rnlo,
1114                rnhi,
1115                rmlo,
1116                rmhi,
1117                elide_zero_guard,
1118            } => {
1119                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1120                if !elide_zero_guard {
1121                    emit_a32_i64_divisor_zero_trap(&mut b);
1122                }
1123                w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1124                w(&mut b, 0xE1A0_9001); // MOV R9, R1 (dividend sign)
1125                skip_negate_if_positive(&mut b, 1);
1126                negate64(&mut b, 0, 1);
1127                skip_negate_if_positive(&mut b, 3);
1128                negate64(&mut b, 2, 3);
1129                for r in 4..8u32 {
1130                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1131                }
1132                div_loop(&mut b, 8);
1133                w(&mut b, 0xE1A0_0006); // MOV R0, R6
1134                w(&mut b, 0xE1A0_1007); // MOV R1, R7
1135                skip_negate_if_positive(&mut b, 9);
1136                negate64(&mut b, 0, 1);
1137                w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1138                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1139            }
1140
1141            // Popcnt (i32): bit-twiddle expansion (no native A32 popcount),
1142            // mirroring the Thumb-2 arm's register contract (R11 + R12 as
1143            // scratch, shift-add fold, final AND #0x3F).
1144            ArmOp::Popcnt { rd, rm } => {
1145                let rd_b = reg_to_bits(rd);
1146                if rd != rm {
1147                    w(&mut b, 0xE1A0_0000 | (rd_b << 12) | reg_to_bits(rm)); // MOV rd, rm
1148                }
1149                // x = x - ((x >> 1) & 0x55555555)
1150                movw(&mut b, 12, 0x5555);
1151                movt(&mut b, 12, 0x5555);
1152                shift_imm(&mut b, LSR, 11, rd_b, 1);
1153                dp_reg(&mut b, 0xE000_0000, 11, 11, 12); // AND R11, R11, R12
1154                dp_reg(&mut b, 0xE040_0000, rd_b, rd_b, 11); // SUB rd, rd, R11
1155                // x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
1156                movw(&mut b, 12, 0x3333);
1157                movt(&mut b, 12, 0x3333);
1158                dp_reg(&mut b, 0xE000_0000, 11, rd_b, 12); // AND R11, rd, R12
1159                shift_imm(&mut b, LSR, rd_b, rd_b, 2);
1160                dp_reg(&mut b, 0xE000_0000, rd_b, rd_b, 12); // AND rd, rd, R12
1161                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11); // ADD rd, rd, R11
1162                // x = (x + (x >> 4)) & 0x0F0F0F0F
1163                shift_imm(&mut b, LSR, 11, rd_b, 4);
1164                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11); // ADD rd, rd, R11
1165                movw(&mut b, 12, 0x0F0F);
1166                movt(&mut b, 12, 0x0F0F);
1167                dp_reg(&mut b, 0xE000_0000, rd_b, rd_b, 12); // AND rd, rd, R12
1168                // x += x >> 8; x += x >> 16; x &= 0x3F
1169                shift_imm(&mut b, LSR, 11, rd_b, 8);
1170                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11);
1171                shift_imm(&mut b, LSR, 11, rd_b, 16);
1172                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11);
1173                w(&mut b, 0xE200_003F | (rd_b << 16) | (rd_b << 12)); // AND rd, rd, #63
1174            }
1175
1176            // I64Popcnt: POPCNT(lo) + POPCNT(hi) — A32 transcription of the
1177            // Thumb-2 arm (R3/R4/R5 saved, mul-based per-word fold, high
1178            // result word rnhi cleared last, mirroring the Thumb contract).
1179            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
1180                let hi = reg_to_bits(rnhi);
1181                w(&mut b, 0xE92D_0038); // PUSH {R3, R4, R5}
1182                // #632 audit: route rnlo through R12 so a pair living at
1183                // (R3,R4) cannot read a clobbered R4 (sources read before any
1184                // scratch register they could occupy is written).
1185                w(&mut b, 0xE1A0_C000 | reg_to_bits(rnlo)); // MOV R12, rnlo
1186                w(&mut b, 0xE1A0_5000 | hi); //                MOV R5, rnhi
1187                w(&mut b, 0xE1A0_400C); //                     MOV R4, R12
1188                popcnt_word(&mut b, 4, 3);
1189                popcnt_word(&mut b, 5, 3);
1190                // #632: carry the count across the scratch restore in R12 —
1191                // rd is allocator-assigned and can land inside {R3,R4,R5};
1192                // the old `ADD rd, R4, R5` before the POP was destroyed by
1193                // the restore. R12 is never allocatable and never restored.
1194                dp_reg(&mut b, 0xE080_0000, 12, 4, 5); // ADD R12, R4, R5
1195                w(&mut b, 0xE8BD_0038); // POP {R3, R4, R5}
1196                w(&mut b, 0xE1A0_0000 | (reg_to_bits(rd) << 12) | 12); // MOV rd, R12
1197                w(&mut b, 0xE3A0_0000 | (hi << 12)); // MOV rnhi, #0 (i64 hi word)
1198            }
1199
1200            _ => return Ok(None),
1201        }
1202        Ok(Some(b))
1203    }
1204
1205    fn encode_arm(&self, op: &ArmOp) -> Result<Vec<u8>> {
1206        // #615: A32 multi-instruction expansions (i64 arithmetic/shift/rotate/
1207        // compare, SetCond/SelectMove, popcnt, ...). These ops were literal
1208        // NOPs on the A32 path — user-reachable via `--target cortex-r5` —
1209        // so the value silently vanished. Mirror of the #594 CallIndirect
1210        // early-return: if the expansion helper covers the op, its bytes are
1211        // the encoding.
1212        if let Some(bytes) = self.encode_arm_expanded(op)? {
1213            return Ok(bytes);
1214        }
1215        // #206: ARM32 register-offset loads/stores. `encode_mem_addr` only
1216        // returns the 12-bit immediate, so the immediate-form arms below
1217        // silently DROP `addr.offset_reg` — a runtime address index vanished,
1218        // turning `ldr rd,[rn,rm,#off]` into `ldr rd,[rn,#off]` (the access went
1219        // to the wrong address). Compute the effective base into IP and re-encode
1220        // against `[ip, #off]`, which is uniform for word/byte/halfword/signed.
1221        if let Some(bytes) = self.encode_arm_reg_offset_mem(op)? {
1222            return Ok(bytes);
1223        }
1224        // #594: call_indirect was encoded as a literal NOP on the A32 path
1225        // (`--target cortex-r5`) — the call never happened and the function
1226        // silently returned garbage. Emit the same three-instruction expansion
1227        // as the Thumb-2 path (R11 = function-pointer table base, R12 scratch):
1228        //   MOV r12, idx, LSL #2 ; LDR r12, [r11, r12] ; BLX r12
1229        if let ArmOp::CallIndirect {
1230            table_index_reg,
1231            table_size,
1232            table_byte_offset,
1233            null_check,
1234            ..
1235        } = op
1236        {
1237            return Ok(Self::encode_arm_call_indirect(
1238                table_index_reg,
1239                *table_size,
1240                *table_byte_offset,
1241                *null_check,
1242            ));
1243        }
1244        let instr: u32 = match op {
1245            // Data processing instructions
1246            ArmOp::Add { rd, rn, op2 } => {
1247                let rd_bits = reg_to_bits(rd);
1248                let rn_bits = reg_to_bits(rn);
1249                let (op2_bits, i_flag) = encode_operand2(op2)?;
1250
1251                // ADD encoding: cond(4) | 00 | I(1) | 0100 | S(1) | Rn(4) | Rd(4) | operand2(12)
1252                0xE0800000 // condition=always(E), opcode=ADD(0100), S=0
1253                    | (i_flag << 25)
1254                    | (rn_bits << 16)
1255                    | (rd_bits << 12)
1256                    | op2_bits
1257            }
1258
1259            ArmOp::Sub { rd, rn, op2 } => {
1260                let rd_bits = reg_to_bits(rd);
1261                let rn_bits = reg_to_bits(rn);
1262                let (op2_bits, i_flag) = encode_operand2(op2)?;
1263
1264                // SUB encoding: opcode=0010
1265                0xE0400000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1266            }
1267
1268            // i64 support: ADDS, ADC, SUBS, SBC for ARM32
1269            ArmOp::Adds { rd, rn, op2 } => {
1270                let rd_bits = reg_to_bits(rd);
1271                let rn_bits = reg_to_bits(rn);
1272                let (op2_bits, i_flag) = encode_operand2(op2)?;
1273
1274                // ADDS encoding: opcode=0100, S=1
1275                0xE0900000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1276            }
1277
1278            ArmOp::Adc { rd, rn, op2 } => {
1279                let rd_bits = reg_to_bits(rd);
1280                let rn_bits = reg_to_bits(rn);
1281                let (op2_bits, i_flag) = encode_operand2(op2)?;
1282
1283                // ADC encoding: opcode=0101
1284                0xE0A00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1285            }
1286
1287            ArmOp::Subs { rd, rn, op2 } => {
1288                let rd_bits = reg_to_bits(rd);
1289                let rn_bits = reg_to_bits(rn);
1290                let (op2_bits, i_flag) = encode_operand2(op2)?;
1291
1292                // SUBS encoding: opcode=0010, S=1
1293                0xE0500000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1294            }
1295
1296            ArmOp::Sbc { rd, rn, op2 } => {
1297                let rd_bits = reg_to_bits(rd);
1298                let rn_bits = reg_to_bits(rn);
1299                let (op2_bits, i_flag) = encode_operand2(op2)?;
1300
1301                // SBC encoding: opcode=0110
1302                0xE0C00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1303            }
1304
1305            ArmOp::Mul { rd, rn, rm } => {
1306                let rd_bits = reg_to_bits(rd);
1307                let rn_bits = reg_to_bits(rn);
1308                let rm_bits = reg_to_bits(rm);
1309
1310                // MUL encoding: cond(4) | 000000 | A(1) | S(1) | Rd(4) | Rn(4) | Rs(4) | 1001 | Rm(4)
1311                0xE0000090 | (rd_bits << 16) | (rn_bits << 8) | rm_bits
1312            }
1313
1314            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
1315                let rdlo_bits = reg_to_bits(rdlo);
1316                let rdhi_bits = reg_to_bits(rdhi);
1317                let rn_bits = reg_to_bits(rn);
1318                let rm_bits = reg_to_bits(rm);
1319
1320                // UMULL encoding: cond(4) | 0000 1000 | RdHi(4) | RdLo(4) | Rm(4) | 1001 | Rn(4)
1321                0xE0800090 | (rdhi_bits << 16) | (rdlo_bits << 12) | (rm_bits << 8) | rn_bits
1322            }
1323
1324            ArmOp::Sdiv { rd, rn, rm } => {
1325                let rd_bits = reg_to_bits(rd);
1326                let rn_bits = reg_to_bits(rn);
1327                let rm_bits = reg_to_bits(rm);
1328
1329                // SDIV encoding: cond(4) | 01110001 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1330                // ARMv7-M and above
1331                0xE710F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1332            }
1333
1334            ArmOp::Udiv { rd, rn, rm } => {
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
1339                // UDIV encoding: cond(4) | 01110011 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1340                // ARMv7-M and above
1341                0xE730F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1342            }
1343
1344            ArmOp::Mls { rd, rn, rm, ra } => {
1345                let rd_bits = reg_to_bits(rd);
1346                let rn_bits = reg_to_bits(rn);
1347                let rm_bits = reg_to_bits(rm);
1348                let ra_bits = reg_to_bits(ra);
1349
1350                // MLS encoding: cond(4) | 00000110 | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1351                // Rd = Ra - (Rn * Rm)
1352                0xE0600090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1353            }
1354
1355            ArmOp::Mla { rd, rn, rm, ra } => {
1356                let rd_bits = reg_to_bits(rd);
1357                let rn_bits = reg_to_bits(rn);
1358                let rm_bits = reg_to_bits(rm);
1359                let ra_bits = reg_to_bits(ra);
1360
1361                // MLA encoding: cond(4) | 0000001 S | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1362                // Rd = Ra + (Rn * Rm). Base 0xE0200090 (S=0).
1363                0xE0200090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1364            }
1365
1366            ArmOp::And { rd, rn, op2 } => {
1367                let rd_bits = reg_to_bits(rd);
1368                let rn_bits = reg_to_bits(rn);
1369                let (op2_bits, i_flag) = encode_operand2(op2)?;
1370
1371                // AND encoding: opcode=0000
1372                0xE0000000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1373            }
1374
1375            ArmOp::Orr { rd, rn, op2 } => {
1376                let rd_bits = reg_to_bits(rd);
1377                let rn_bits = reg_to_bits(rn);
1378                let (op2_bits, i_flag) = encode_operand2(op2)?;
1379
1380                // ORR encoding: opcode=1100
1381                0xE1800000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1382            }
1383
1384            ArmOp::Eor { rd, rn, op2 } => {
1385                let rd_bits = reg_to_bits(rd);
1386                let rn_bits = reg_to_bits(rn);
1387                let (op2_bits, i_flag) = encode_operand2(op2)?;
1388
1389                // EOR encoding: opcode=0001
1390                0xE0200000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1391            }
1392
1393            // Shift instructions
1394            ArmOp::Lsl { rd, rn, shift } => {
1395                let rd_bits = reg_to_bits(rd);
1396                let rn_bits = reg_to_bits(rn);
1397                let shift_bits = *shift & 0x1F;
1398
1399                // LSL encoding: MOV with shift
1400                0xE1A00000 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1401            }
1402
1403            ArmOp::Lsr { rd, rn, shift } => {
1404                let rd_bits = reg_to_bits(rd);
1405                let rn_bits = reg_to_bits(rn);
1406                let shift_bits = *shift & 0x1F;
1407
1408                // LSR encoding
1409                0xE1A00020 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1410            }
1411
1412            ArmOp::Asr { rd, rn, shift } => {
1413                let rd_bits = reg_to_bits(rd);
1414                let rn_bits = reg_to_bits(rn);
1415                let shift_bits = *shift & 0x1F;
1416
1417                // ASR encoding
1418                0xE1A00040 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1419            }
1420
1421            ArmOp::Ror { rd, rn, shift } => {
1422                let rd_bits = reg_to_bits(rd);
1423                let rn_bits = reg_to_bits(rn);
1424                let shift_bits = *shift & 0x1F;
1425
1426                // ROR encoding: MOV with ROR shift
1427                0xE1A00060 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1428            }
1429
1430            // Register-based shifts (ARM32)
1431            // LSL Rd, Rn, Rm: cond 0001101S 0000 Rd Rs 0001 Rn
1432            ArmOp::LslReg { rd, rn, rm } => {
1433                let rd_bits = reg_to_bits(rd);
1434                let rn_bits = reg_to_bits(rn);
1435                let rm_bits = reg_to_bits(rm);
1436                0xE1A00010 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1437            }
1438            ArmOp::LsrReg { rd, rn, rm } => {
1439                let rd_bits = reg_to_bits(rd);
1440                let rn_bits = reg_to_bits(rn);
1441                let rm_bits = reg_to_bits(rm);
1442                0xE1A00030 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1443            }
1444            ArmOp::AsrReg { rd, rn, rm } => {
1445                let rd_bits = reg_to_bits(rd);
1446                let rn_bits = reg_to_bits(rn);
1447                let rm_bits = reg_to_bits(rm);
1448                0xE1A00050 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1449            }
1450            ArmOp::RorReg { rd, rn, rm } => {
1451                let rd_bits = reg_to_bits(rd);
1452                let rn_bits = reg_to_bits(rn);
1453                let rm_bits = reg_to_bits(rm);
1454                0xE1A00070 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1455            }
1456
1457            // RSB (Reverse Subtract): Rd = imm - Rn
1458            ArmOp::Rsb { rd, rn, imm } => {
1459                let rd_bits = reg_to_bits(rd);
1460                let rn_bits = reg_to_bits(rn);
1461                // RSB encoding: cond(4) | 00 1 0011 S | Rn(4) | Rd(4) | imm12
1462                // Opcode for RSB = 0011, I=1 (immediate), S=0
1463                0xE2600000 | (rn_bits << 16) | (rd_bits << 12) | (*imm & 0xFF)
1464            }
1465
1466            // Bit manipulation instructions
1467            ArmOp::Clz { rd, rm } => {
1468                let rd_bits = reg_to_bits(rd);
1469                let rm_bits = reg_to_bits(rm);
1470
1471                // CLZ encoding: cond(4) | 00010110 | 1111 | Rd(4) | 1111 | 0001 | Rm(4)
1472                // ARMv5T and above
1473                0xE16F0F10 | (rd_bits << 12) | rm_bits
1474            }
1475
1476            ArmOp::Rbit { rd, rm } => {
1477                let rd_bits = reg_to_bits(rd);
1478                let rm_bits = reg_to_bits(rm);
1479
1480                // RBIT encoding: cond(4) | 01101111 | 1111 | Rd(4) | 1111 | 0011 | Rm(4)
1481                // ARMv6T2 and above
1482                0xE6FF0F30 | (rd_bits << 12) | rm_bits
1483            }
1484
1485            ArmOp::Sxtb { rd, rm } => {
1486                let rd_bits = reg_to_bits(rd);
1487                let rm_bits = reg_to_bits(rm);
1488
1489                // SXTB encoding: cond(4) | 01101010 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1490                // ARMv6 and above. rotate=00 for no rotation
1491                0xE6AF0070 | (rd_bits << 12) | rm_bits
1492            }
1493
1494            ArmOp::Sxth { rd, rm } => {
1495                let rd_bits = reg_to_bits(rd);
1496                let rm_bits = reg_to_bits(rm);
1497
1498                // SXTH encoding: cond(4) | 01101011 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1499                // ARMv6 and above. rotate=00 for no rotation
1500                0xE6BF0070 | (rd_bits << 12) | rm_bits
1501            }
1502
1503            ArmOp::Uxtb { rd, rm } => {
1504                let rd_bits = reg_to_bits(rd);
1505                let rm_bits = reg_to_bits(rm);
1506                // UXTB encoding: cond | 01101110 1111 Rd rotate 00 0111 Rm (rotate=00)
1507                0xE6EF0070 | (rd_bits << 12) | rm_bits
1508            }
1509
1510            ArmOp::Uxth { rd, rm } => {
1511                let rd_bits = reg_to_bits(rd);
1512                let rm_bits = reg_to_bits(rm);
1513                // UXTH encoding: cond | 01101111 1111 Rd rotate 00 0111 Rm (rotate=00)
1514                0xE6FF0070 | (rd_bits << 12) | rm_bits
1515            }
1516
1517            // Move instructions
1518            ArmOp::Mov { rd, op2 } => {
1519                let rd_bits = reg_to_bits(rd);
1520                let (op2_bits, i_flag) = encode_operand2(op2)?;
1521
1522                // MOV encoding: opcode=1101
1523                0xE1A00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1524            }
1525
1526            ArmOp::Mvn { rd, op2 } => {
1527                let rd_bits = reg_to_bits(rd);
1528                let (op2_bits, i_flag) = encode_operand2(op2)?;
1529
1530                // MVN encoding: opcode=1111
1531                0xE1E00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1532            }
1533
1534            // MOVW - Move Wide (ARM32)
1535            // Encoding: cond(4) | 0011 0000 | imm4(4) | Rd(4) | imm12(12)
1536            ArmOp::Movw { rd, imm16 } => {
1537                let rd_bits = reg_to_bits(rd);
1538                let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1539                let imm12 = (*imm16 as u32) & 0xFFF;
1540                0xE3000000 | (imm4 << 16) | (rd_bits << 12) | imm12
1541            }
1542
1543            // MOVT - Move Top (ARM32)
1544            // Encoding: cond(4) | 0011 0100 | imm4(4) | Rd(4) | imm12(12)
1545            ArmOp::Movt { rd, imm16 } => {
1546                let rd_bits = reg_to_bits(rd);
1547                let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1548                let imm12 = (*imm16 as u32) & 0xFFF;
1549                0xE3400000 | (imm4 << 16) | (rd_bits << 12) | imm12
1550            }
1551
1552            // #237: symbol-relative MOVW/MOVT (ARM mode) — addend in place, the
1553            // backend records the MOVW_ABS/MOVT_ABS relocation against `symbol`.
1554            ArmOp::MovwSym { rd, addend, .. } => {
1555                let rd_bits = reg_to_bits(rd);
1556                let v = (*addend as u32) & 0xffff;
1557                0xE3000000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1558            }
1559            ArmOp::MovtSym { rd, addend, .. } => {
1560                let rd_bits = reg_to_bits(rd);
1561                let v = ((*addend as u32) >> 16) & 0xffff;
1562                0xE3400000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1563            }
1564
1565            // #345: LdrSym is the Thumb-2 literal-pool address load. A32 mode is
1566            // not used for relocatable native-pointer objects; fail loudly rather
1567            // than miscompile if it is ever reached here.
1568            ArmOp::LdrSym { .. } => {
1569                return Err(synth_core::Error::synthesis(
1570                    "LdrSym (literal-pool address load) is Thumb-2-only",
1571                ));
1572            }
1573
1574            // Compare
1575            ArmOp::Cmp { rn, op2 } => {
1576                let rn_bits = reg_to_bits(rn);
1577                let (op2_bits, i_flag) = encode_operand2(op2)?;
1578
1579                // CMP encoding: opcode=1010, S=1
1580                0xE1500000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1581            }
1582
1583            // Compare Negative (CMN) - computes Rn + op2 and sets flags
1584            ArmOp::Cmn { rn, op2 } => {
1585                let rn_bits = reg_to_bits(rn);
1586                let (op2_bits, i_flag) = encode_operand2(op2)?;
1587
1588                // CMN encoding: opcode=1011, S=1
1589                0xE1700000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1590            }
1591
1592            // Load/Store
1593            ArmOp::Ldr { rd, addr } => {
1594                let rd_bits = reg_to_bits(rd);
1595                let (base_bits, offset_bits) = encode_mem_addr(addr);
1596
1597                // LDR encoding: cond(4) | 01 | I(1) | P(1) | U(1) | B(1) | W(1) | L(1) | Rn(4) | Rd(4) | offset(12)
1598                // P=1 (pre-indexed), U=1 (add offset), L=1 (load)
1599                0xE5900000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1600            }
1601
1602            ArmOp::Str { rd, addr } => {
1603                let rd_bits = reg_to_bits(rd);
1604                let (base_bits, offset_bits) = encode_mem_addr(addr);
1605
1606                // STR encoding: L=0 (store)
1607                0xE5800000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1608            }
1609
1610            // Sub-word loads (ARM32 encoding)
1611            ArmOp::Ldrb { rd, addr } => {
1612                let rd_bits = reg_to_bits(rd);
1613                let (base_bits, offset_bits) = encode_mem_addr(addr);
1614                // LDRB: LDR with B=1 (byte): cond|01|I|P|U|1|W|L|Rn|Rd|offset
1615                0xE5D00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1616            }
1617
1618            ArmOp::Ldrsb { rd, addr } => {
1619                let rd_bits = reg_to_bits(rd);
1620                let (base_bits, offset_bits) = encode_mem_addr(addr);
1621                // LDRSB (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1101|imm4L
1622                // Simplified with immediate offset
1623                let offset_val = offset_bits & 0xFF;
1624                let imm4h = (offset_val >> 4) & 0xF;
1625                let imm4l = offset_val & 0xF;
1626                0xE1D000D0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1627            }
1628
1629            ArmOp::Ldrh { rd, addr } => {
1630                let rd_bits = reg_to_bits(rd);
1631                let (base_bits, offset_bits) = encode_mem_addr(addr);
1632                // LDRH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1011|imm4L
1633                let offset_val = offset_bits & 0xFF;
1634                let imm4h = (offset_val >> 4) & 0xF;
1635                let imm4l = offset_val & 0xF;
1636                0xE1D000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1637            }
1638
1639            ArmOp::Ldrsh { rd, addr } => {
1640                let rd_bits = reg_to_bits(rd);
1641                let (base_bits, offset_bits) = encode_mem_addr(addr);
1642                // LDRSH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1111|imm4L
1643                let offset_val = offset_bits & 0xFF;
1644                let imm4h = (offset_val >> 4) & 0xF;
1645                let imm4l = offset_val & 0xF;
1646                0xE1D000F0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1647            }
1648
1649            // Sub-word stores (ARM32 encoding)
1650            ArmOp::Strb { rd, addr } => {
1651                let rd_bits = reg_to_bits(rd);
1652                let (base_bits, offset_bits) = encode_mem_addr(addr);
1653                // STRB: STR with B=1 (byte): cond|01|I|P|U|1|W|0|Rn|Rd|offset
1654                0xE5C00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1655            }
1656
1657            ArmOp::Strh { rd, addr } => {
1658                let rd_bits = reg_to_bits(rd);
1659                let (base_bits, offset_bits) = encode_mem_addr(addr);
1660                // STRH (misc store): cond|000|P|U|1|W|0|Rn|Rd|imm4H|1011|imm4L
1661                let offset_val = offset_bits & 0xFF;
1662                let imm4h = (offset_val >> 4) & 0xF;
1663                let imm4l = offset_val & 0xF;
1664                0xE1C000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1665            }
1666
1667            // Memory management (ARM32 encoding)
1668            ArmOp::MemorySize { rd } => {
1669                let rd_bits = reg_to_bits(rd);
1670                // MOV rd, R10, LSR #16  (memory size in bytes / 65536 = pages)
1671                // cond|000|1101|S|0000|Rd|shift5|type|0|Rm
1672                // LSR #16: shift5=10000, type=01
1673                0xE1A00820 | (rd_bits << 12) | 0x0A // Rm=R10, shift=16, LSR
1674            }
1675
1676            ArmOp::MemoryGrow { rd, .. } => {
1677                let rd_bits = reg_to_bits(rd);
1678                // On embedded, always fail: MOV rd, #-1
1679                0xE3E00000 | (rd_bits << 12) // MVN rd, #0 = MOV rd, #-1
1680            }
1681
1682            // Label pseudo-instruction: emits no machine code
1683            ArmOp::Label { .. } => {
1684                return Ok(Vec::new());
1685            }
1686
1687            // Branch instructions
1688            ArmOp::B { label: _ } => {
1689                // B encoding: cond(4) | 1010 | offset(24)
1690                // Simplified: branch to offset 0 (will be patched by linker/resolver)
1691                0xEA000000
1692            }
1693
1694            // Conditional branch to label (generic)
1695            ArmOp::Bcc { cond, label: _ } => {
1696                use synth_synthesis::Condition;
1697                let cond_bits: u32 = match cond {
1698                    Condition::EQ => 0x0,
1699                    Condition::NE => 0x1,
1700                    Condition::HS => 0x2,
1701                    Condition::LO => 0x3,
1702                    Condition::HI => 0x8,
1703                    Condition::LS => 0x9,
1704                    Condition::GE => 0xA,
1705                    Condition::LT => 0xB,
1706                    Condition::GT => 0xC,
1707                    Condition::LE => 0xD,
1708                };
1709                // B<cond> with offset 0 (will be patched)
1710                (cond_bits << 28) | 0x0A000000
1711            }
1712
1713            // BHS (Branch if Higher or Same) - used for bounds checking
1714            ArmOp::Bhs { label: _ } => {
1715                // BHS encoding: cond(2=HS) | 1010 | offset(24)
1716                0x2A000000 // BHS with offset 0
1717            }
1718
1719            // BLO (Branch if Lower) - complementary to BHS
1720            ArmOp::Blo { label: _ } => {
1721                // BLO encoding: cond(3=LO) | 1010 | offset(24)
1722                0x3A000000 // BLO with offset 0
1723            }
1724
1725            // Branch with numeric offset (in instructions)
1726            // ARM32 B instruction: offset is in instructions, stored as words
1727            // The offset is relative to PC+8 (due to ARM pipeline)
1728            ArmOp::BOffset { offset } => {
1729                // B encoding: cond(4) | 1010 | offset(24)
1730                // Offset is signed, in words (4-byte units)
1731                // ARM adds PC+8 to the offset, so we need to adjust:
1732                // target = PC + 8 + (offset * 4)
1733                // For backward branch of N instructions: offset = -(N + 2)
1734                // wrapping_sub keeps the encoder total under fuzzing (#186): an
1735                // extreme i32::MIN offset would otherwise overflow-panic; for any
1736                // real branch offset this is identical to `- 2`.
1737                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1738                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1739                0xEA000000 | offset_bits
1740            }
1741
1742            // Conditional branch with numeric offset
1743            ArmOp::BCondOffset { cond, offset } => {
1744                use synth_synthesis::Condition;
1745                let cond_bits: u32 = match cond {
1746                    Condition::EQ => 0x0,
1747                    Condition::NE => 0x1,
1748                    Condition::HS => 0x2,
1749                    Condition::LO => 0x3,
1750                    Condition::HI => 0x8,
1751                    Condition::LS => 0x9,
1752                    Condition::GE => 0xA,
1753                    Condition::LT => 0xB,
1754                    Condition::GT => 0xC,
1755                    Condition::LE => 0xD,
1756                };
1757                // B<cond> encoding: cond(4) | 1010 | offset(24)
1758                // wrapping_sub: total under fuzzing (#186), identical for real offsets.
1759                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1760                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1761                (cond_bits << 28) | 0x0A000000 | offset_bits
1762            }
1763
1764            ArmOp::Bl { label: _ } => {
1765                // BL encoding: cond(4) | 1011 | offset(24)
1766                0xEB000000
1767            }
1768
1769            ArmOp::Bx { rm } => {
1770                let rm_bits = reg_to_bits(rm);
1771
1772                // BX encoding: cond(4) | 000100101111111111110001 | Rm(4)
1773                0xE12FFF10 | rm_bits
1774            }
1775
1776            ArmOp::Blx { rm } => {
1777                let rm_bits = reg_to_bits(rm);
1778
1779                // BLX (register) encoding: cond(4) | 000100101111111111110011 | Rm(4)
1780                0xE12FFF30 | rm_bits
1781            }
1782
1783            ArmOp::Push { regs } => {
1784                // STMDB SP!, {regs} encoding: cond(4) | 100100 | 10 | 1101 | register_list(16)
1785                let mut reg_list: u32 = 0;
1786                for r in regs {
1787                    reg_list |= 1 << reg_to_bits(r);
1788                }
1789                0xE92D0000 | reg_list
1790            }
1791
1792            ArmOp::Pop { regs } => {
1793                // LDMIA SP!, {regs} encoding: cond(4) | 100010 | 11 | 1101 | register_list(16)
1794                let mut reg_list: u32 = 0;
1795                for r in regs {
1796                    reg_list |= 1 << reg_to_bits(r);
1797                }
1798                0xE8BD0000 | reg_list
1799            }
1800
1801            ArmOp::Nop => {
1802                // NOP encoding: MOV R0, R0
1803                0xE1A00000
1804            }
1805
1806            ArmOp::Udf { imm } => {
1807                // UDF (Undefined) encoding in ARM: 0xE7F000F0 | (imm12_hi << 8) | imm4_lo
1808                // We only use imm8, so split into imm4_hi and imm4_lo
1809                let imm8 = *imm as u32;
1810                0xE7F000F0 | ((imm8 & 0xF0) << 4) | (imm8 & 0x0F)
1811            }
1812
1813            // #615: handled by the `encode_arm_expanded` early return at the
1814            // top of this function — a real MOV{cond}/MOV pair now, never a
1815            // silent NOP again.
1816            ArmOp::Popcnt { .. } | ArmOp::SetCond { .. } | ArmOp::SelectMove { .. } => {
1817                unreachable!("handled by encode_arm_expanded (#615)")
1818            }
1819
1820            // Verification-only pseudo-ops: `synth-verify`'s ArmSemantics
1821            // models these, but NO codegen path constructs them (the selector
1822            // lowers select/locals/globals/br_table/call to real instruction
1823            // sequences before the encoder). Encoding one as a NOP silently
1824            // dropped the operation (#615 class); a typed Err keeps the
1825            // encoder total (Ok-or-Err, the `encoder_no_panic` contract)
1826            // while making any future reachability LOUD.
1827            ArmOp::Select { .. }
1828            | ArmOp::LocalGet { .. }
1829            | ArmOp::LocalSet { .. }
1830            | ArmOp::LocalTee { .. }
1831            | ArmOp::GlobalGet { .. }
1832            | ArmOp::GlobalSet { .. }
1833            | ArmOp::BrTable { .. }
1834            | ArmOp::Call { .. } => {
1835                return Err(synth_core::Error::synthesis(format!(
1836                    "verification-only pseudo-op {op:?} reached the A32 encoder — \
1837                     codegen lowers it before encoding; refusing to emit a silent NOP (#615)"
1838                )));
1839            }
1840
1841            // #594: CallIndirect is expanded to a real multi-instruction
1842            // sequence by the early return at the top of this function —
1843            // it must NEVER fall through to a silent NOP again.
1844            ArmOp::CallIndirect { .. } => {
1845                unreachable!("CallIndirect handled by encode_arm_call_indirect (#594)")
1846            }
1847
1848            // #615: every i64 op (and I32WrapI64) is expanded to a real A32
1849            // multi-instruction sequence by `encode_arm_expanded` — the
1850            // "encode as NOP for now" era ended with the value silently
1851            // vanishing on `--target cortex-r5`.
1852            ArmOp::I64Add { .. }
1853            | ArmOp::I64Sub { .. }
1854            | ArmOp::I64DivS { .. }
1855            | ArmOp::I64DivU { .. }
1856            | ArmOp::I64RemS { .. }
1857            | ArmOp::I64RemU { .. }
1858            | ArmOp::I64Clz { .. }
1859            | ArmOp::I64Ctz { .. }
1860            | ArmOp::I64Popcnt { .. }
1861            | ArmOp::I64And { .. }
1862            | ArmOp::I64Or { .. }
1863            | ArmOp::I64Xor { .. }
1864            | ArmOp::I64Eqz { .. }
1865            | ArmOp::I64Eq { .. }
1866            | ArmOp::I64Ne { .. }
1867            | ArmOp::I64LtS { .. }
1868            | ArmOp::I64LtU { .. }
1869            | ArmOp::I64LeS { .. }
1870            | ArmOp::I64LeU { .. }
1871            | ArmOp::I64GtS { .. }
1872            | ArmOp::I64GtU { .. }
1873            | ArmOp::I64GeS { .. }
1874            | ArmOp::I64GeU { .. }
1875            | ArmOp::I64Const { .. }
1876            | ArmOp::I64Ldr { .. }
1877            | ArmOp::I64Str { .. }
1878            | ArmOp::I64ExtendI32S { .. }
1879            | ArmOp::I64ExtendI32U { .. }
1880            | ArmOp::I64Extend8S { .. }
1881            | ArmOp::I64Extend16S { .. }
1882            | ArmOp::I64Extend32S { .. }
1883            | ArmOp::I32WrapI64 { .. } => {
1884                unreachable!("handled by encode_arm_expanded (#615)")
1885            }
1886
1887            // f32 VFP single-precision instructions
1888            ArmOp::F32Add { sd, sn, sm } => encode_vfp_3reg(0xEE300A00, sd, sn, sm)?,
1889            ArmOp::F32Sub { sd, sn, sm } => encode_vfp_3reg(0xEE300A40, sd, sn, sm)?,
1890            ArmOp::F32Mul { sd, sn, sm } => encode_vfp_3reg(0xEE200A00, sd, sn, sm)?,
1891            ArmOp::F32Div { sd, sn, sm } => encode_vfp_3reg(0xEE800A00, sd, sn, sm)?,
1892            ArmOp::F32Abs { sd, sm } => encode_vfp_2reg(0xEEB00AC0, sd, sm)?,
1893            ArmOp::F32Neg { sd, sm } => encode_vfp_2reg(0xEEB10A40, sd, sm)?,
1894            ArmOp::F32Sqrt { sd, sm } => encode_vfp_2reg(0xEEB10AC0, sd, sm)?,
1895
1896            // f32 pseudo-ops — multi-instruction sequences
1897            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
1898            ArmOp::F32Ceil { sd, sm } => {
1899                return self.encode_arm_f32_rounding(sd, sm, 0b01); // Round toward +Inf
1900            }
1901            ArmOp::F32Floor { sd, sm } => {
1902                return self.encode_arm_f32_rounding(sd, sm, 0b10); // Round toward -Inf
1903            }
1904            ArmOp::F32Trunc { sd, sm } => {
1905                return self.encode_arm_f32_rounding(sd, sm, 0b11); // VCVT toward zero
1906            }
1907            ArmOp::F32Nearest { sd, sm } => {
1908                return self.encode_arm_f32_rounding(sd, sm, 0b00); // VCVT to nearest
1909            }
1910            ArmOp::F32Min { sd, sn, sm } => {
1911                return self.encode_arm_f32_minmax(sd, sn, sm, true);
1912            }
1913            ArmOp::F32Max { sd, sn, sm } => {
1914                return self.encode_arm_f32_minmax(sd, sn, sm, false);
1915            }
1916            ArmOp::F32Copysign { sd, sn, sm } => {
1917                return self.encode_arm_f32_copysign(sd, sn, sm);
1918            }
1919
1920            // f32 comparisons — multi-instruction: VCMP + VMRS + conditional MOV
1921            ArmOp::F32Eq { rd, sn, sm } => {
1922                return self.encode_arm_f32_compare(rd, sn, sm, 0x0); // EQ
1923            }
1924            ArmOp::F32Ne { rd, sn, sm } => {
1925                return self.encode_arm_f32_compare(rd, sn, sm, 0x1); // NE
1926            }
1927            ArmOp::F32Lt { rd, sn, sm } => {
1928                return self.encode_arm_f32_compare(rd, sn, sm, 0x4); // MI (less than)
1929            }
1930            ArmOp::F32Le { rd, sn, sm } => {
1931                return self.encode_arm_f32_compare(rd, sn, sm, 0x9); // LS (less or same)
1932            }
1933            ArmOp::F32Gt { rd, sn, sm } => {
1934                return self.encode_arm_f32_compare(rd, sn, sm, 0xC); // GT
1935            }
1936            ArmOp::F32Ge { rd, sn, sm } => {
1937                return self.encode_arm_f32_compare(rd, sn, sm, 0xA); // GE
1938            }
1939
1940            // f32 const — multi-instruction: MOVW + MOVT + VMOV
1941            ArmOp::F32Const { sd, value } => {
1942                return self.encode_arm_f32_const(sd, *value);
1943            }
1944
1945            ArmOp::F32Load { sd, addr } => encode_vfp_ldst(0xED900A00, sd, addr)?,
1946            ArmOp::F32Store { sd, addr } => encode_vfp_ldst(0xED800A00, sd, addr)?,
1947
1948            // f32 conversions — multi-instruction sequences
1949            ArmOp::F32ConvertI32S { sd, rm } => {
1950                return self.encode_arm_f32_convert_i32(sd, rm, true);
1951            }
1952            ArmOp::F32ConvertI32U { sd, rm } => {
1953                return self.encode_arm_f32_convert_i32(sd, rm, false);
1954            }
1955            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
1956                return Err(synth_core::Error::synthesis(
1957                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
1958                ));
1959            }
1960            ArmOp::F32ReinterpretI32 { sd, rm } => encode_vmov_core_sreg(true, sd, rm)?,
1961            ArmOp::I32ReinterpretF32 { rd, sm } => encode_vmov_core_sreg(false, sm, rd)?,
1962            ArmOp::I32TruncF32S { rd, sm } => {
1963                return self.encode_arm_i32_trunc_f32(rd, sm, true);
1964            }
1965            ArmOp::I32TruncF32U { rd, sm } => {
1966                return self.encode_arm_i32_trunc_f32(rd, sm, false);
1967            }
1968
1969            // f64 VFP double-precision instructions (ARM32)
1970            // F64 arithmetic: same as F32 but with sz=1 (bit 8 = 1, cp11 = 0xB)
1971            ArmOp::F64Add { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B00, dd, dn, dm)?,
1972            ArmOp::F64Sub { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B40, dd, dn, dm)?,
1973            ArmOp::F64Mul { dd, dn, dm } => encode_vfp_3reg_f64(0xEE200B00, dd, dn, dm)?,
1974            ArmOp::F64Div { dd, dn, dm } => encode_vfp_3reg_f64(0xEE800B00, dd, dn, dm)?,
1975            ArmOp::F64Abs { dd, dm } => encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?,
1976            ArmOp::F64Neg { dd, dm } => encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?,
1977            ArmOp::F64Sqrt { dd, dm } => encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?,
1978
1979            // f64 pseudo-ops
1980            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
1981            ArmOp::F64Ceil { dd, dm } => {
1982                return self.encode_arm_f64_rounding(dd, dm, 0b01);
1983            }
1984            ArmOp::F64Floor { dd, dm } => {
1985                return self.encode_arm_f64_rounding(dd, dm, 0b10);
1986            }
1987            ArmOp::F64Trunc { dd, dm } => {
1988                return self.encode_arm_f64_rounding(dd, dm, 0b11);
1989            }
1990            ArmOp::F64Nearest { dd, dm } => {
1991                return self.encode_arm_f64_rounding(dd, dm, 0b00);
1992            }
1993            ArmOp::F64Min { dd, dn, dm } => {
1994                return self.encode_arm_f64_minmax(dd, dn, dm, true);
1995            }
1996            ArmOp::F64Max { dd, dn, dm } => {
1997                return self.encode_arm_f64_minmax(dd, dn, dm, false);
1998            }
1999            ArmOp::F64Copysign { dd, dn, dm } => {
2000                return self.encode_arm_f64_copysign(dd, dn, dm);
2001            }
2002
2003            // f64 comparisons
2004            ArmOp::F64Eq { rd, dn, dm } => {
2005                return self.encode_arm_f64_compare(rd, dn, dm, 0x0);
2006            }
2007            ArmOp::F64Ne { rd, dn, dm } => {
2008                return self.encode_arm_f64_compare(rd, dn, dm, 0x1);
2009            }
2010            ArmOp::F64Lt { rd, dn, dm } => {
2011                return self.encode_arm_f64_compare(rd, dn, dm, 0x4);
2012            }
2013            ArmOp::F64Le { rd, dn, dm } => {
2014                return self.encode_arm_f64_compare(rd, dn, dm, 0x9);
2015            }
2016            ArmOp::F64Gt { rd, dn, dm } => {
2017                return self.encode_arm_f64_compare(rd, dn, dm, 0xC);
2018            }
2019            ArmOp::F64Ge { rd, dn, dm } => {
2020                return self.encode_arm_f64_compare(rd, dn, dm, 0xA);
2021            }
2022
2023            ArmOp::F64Const { dd, value } => {
2024                return self.encode_arm_f64_const(dd, *value);
2025            }
2026
2027            ArmOp::F64Load { dd, addr } => encode_vfp_ldst_f64(0xED900B00, dd, addr)?,
2028            ArmOp::F64Store { dd, addr } => encode_vfp_ldst_f64(0xED800B00, dd, addr)?,
2029
2030            ArmOp::F64ConvertI32S { dd, rm } => {
2031                return self.encode_arm_f64_convert_i32(dd, rm, true);
2032            }
2033            ArmOp::F64ConvertI32U { dd, rm } => {
2034                return self.encode_arm_f64_convert_i32(dd, rm, false);
2035            }
2036            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
2037                return Err(synth_core::Error::synthesis(
2038                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2039                ));
2040            }
2041            ArmOp::F64PromoteF32 { dd, sm } => {
2042                return self.encode_arm_f64_promote_f32(dd, sm);
2043            }
2044            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => {
2045                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?
2046            }
2047            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => {
2048                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?
2049            }
2050            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
2051                return Err(synth_core::Error::synthesis(
2052                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
2053                ));
2054            }
2055            ArmOp::I32TruncF64S { rd, dm } => {
2056                return self.encode_arm_i32_trunc_f64(rd, dm, true);
2057            }
2058            ArmOp::I32TruncF64U { rd, dm } => {
2059                return self.encode_arm_i32_trunc_f64(rd, dm, false);
2060            }
2061            // #615: multi-instruction i64 sequences — expanded to real A32 by
2062            // `encode_arm_expanded`, no longer "Thumb-2 only" NOPs.
2063            ArmOp::I64SetCond { .. }
2064            | ArmOp::I64SetCondZ { .. }
2065            | ArmOp::I64Mul { .. }
2066            | ArmOp::I64Shl { .. }
2067            | ArmOp::I64ShrS { .. }
2068            | ArmOp::I64ShrU { .. }
2069            | ArmOp::I64Rotl { .. }
2070            | ArmOp::I64Rotr { .. } => {
2071                unreachable!("handled by encode_arm_expanded (#615)")
2072            }
2073
2074            // MVE instructions — Thumb-2 only (Cortex-M55 is always Thumb-2)
2075            ArmOp::MveLoad { .. }
2076            | ArmOp::MveStore { .. }
2077            | ArmOp::MveConst { .. }
2078            | ArmOp::MveAnd { .. }
2079            | ArmOp::MveOrr { .. }
2080            | ArmOp::MveEor { .. }
2081            | ArmOp::MveMvn { .. }
2082            | ArmOp::MveBic { .. }
2083            | ArmOp::MveAddI { .. }
2084            | ArmOp::MveSubI { .. }
2085            | ArmOp::MveMulI { .. }
2086            | ArmOp::MveNegI { .. }
2087            | ArmOp::MveCmpEqI { .. }
2088            | ArmOp::MveCmpNeI { .. }
2089            | ArmOp::MveCmpLtS { .. }
2090            | ArmOp::MveCmpLtU { .. }
2091            | ArmOp::MveCmpGtS { .. }
2092            | ArmOp::MveCmpGtU { .. }
2093            | ArmOp::MveCmpLeS { .. }
2094            | ArmOp::MveCmpLeU { .. }
2095            | ArmOp::MveCmpGeS { .. }
2096            | ArmOp::MveCmpGeU { .. }
2097            | ArmOp::MveDup { .. }
2098            | ArmOp::MveExtractLane { .. }
2099            | ArmOp::MveInsertLane { .. }
2100            | ArmOp::MveAddF32 { .. }
2101            | ArmOp::MveSubF32 { .. }
2102            | ArmOp::MveMulF32 { .. }
2103            | ArmOp::MveNegF32 { .. }
2104            | ArmOp::MveAbsF32 { .. }
2105            | ArmOp::MveCmpEqF32 { .. }
2106            | ArmOp::MveCmpNeF32 { .. }
2107            | ArmOp::MveCmpLtF32 { .. }
2108            | ArmOp::MveCmpLeF32 { .. }
2109            | ArmOp::MveCmpGtF32 { .. }
2110            | ArmOp::MveCmpGeF32 { .. }
2111            | ArmOp::MveDupF32 { .. }
2112            | ArmOp::MveExtractLaneF32 { .. }
2113            | ArmOp::MveReplaceLaneF32 { .. }
2114            | ArmOp::MveDivF32 { .. }
2115            | ArmOp::MveSqrtF32 { .. } => {
2116                // MVE (Helium) is a Thumb-2-only extension (Cortex-M55); there
2117                // is no A32 encoding. The selector only emits MVE ops for
2118                // Thumb targets — a NOP here silently dropped the vector op
2119                // if that invariant ever broke (#615 class). Err keeps the
2120                // encoder total and the failure loud.
2121                return Err(synth_core::Error::synthesis(format!(
2122                    "MVE op {op:?} has no A32 (ARM-mode) encoding — MVE is Thumb-2 only (#615)"
2123                )));
2124            }
2125        };
2126
2127        // ARM32 instructions are little-endian
2128        Ok(instr.to_le_bytes().to_vec())
2129    }
2130
2131    // === ARM32 VFP multi-instruction helpers ===
2132
2133    /// Encode F32 comparison as ARM32: VCMP.F32 + VMRS + MOV rd,#0 + MOVcond rd,#1
2134    fn encode_arm_f32_compare(
2135        &self,
2136        rd: &Reg,
2137        sn: &VfpReg,
2138        sm: &VfpReg,
2139        cond_code: u32,
2140    ) -> Result<Vec<u8>> {
2141        let mut bytes = Vec::new();
2142
2143        // VCMP.F32 Sn, Sm: 0xEEB40A40 with Sn in Vd position, Sm in Vm position
2144        let sn_num = vfp_sreg_to_num(sn)?;
2145        let sm_num = vfp_sreg_to_num(sm)?;
2146        let (vd, d) = encode_sreg(sn_num);
2147        let (vm, m) = encode_sreg(sm_num);
2148        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2149        bytes.extend_from_slice(&vcmp.to_le_bytes());
2150
2151        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
2152        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2153
2154        // MOV rd, #0: 0xE3A0_0000 | (rd << 12)
2155        let rd_bits = reg_to_bits(rd);
2156        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2157        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2158
2159        // MOVcond rd, #1: cond(4) | 0011 1010 0000 rd(4) 0000 0000 0001
2160        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2161        bytes.extend_from_slice(&mov_one.to_le_bytes());
2162
2163        Ok(bytes)
2164    }
2165
2166    /// Encode F32 constant load as ARM32: MOVW Rt,#lo16 + MOVT Rt,#hi16 + VMOV Sd,Rt
2167    fn encode_arm_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
2168        let mut bytes = Vec::new();
2169        let bits = value.to_bits();
2170
2171        // Use R12 as temp register for constant loading
2172        let rt: u32 = 12; // R12/IP
2173
2174        // MOVW R12, #lo16: 0xE300_C000 | (imm4 << 16) | imm12
2175        let lo16 = bits & 0xFFFF;
2176        let movw = 0xE3000000 | (rt << 12) | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2177        bytes.extend_from_slice(&movw.to_le_bytes());
2178
2179        // MOVT R12, #hi16: 0xE340_C000 | (imm4 << 16) | imm12
2180        let hi16 = (bits >> 16) & 0xFFFF;
2181        let movt = 0xE3400000 | (rt << 12) | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2182        bytes.extend_from_slice(&movt.to_le_bytes());
2183
2184        // VMOV Sd, R12
2185        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
2186        bytes.extend_from_slice(&vmov.to_le_bytes());
2187
2188        Ok(bytes)
2189    }
2190
2191    /// Encode VMOV + VCVT.F32.S32/U32 as ARM32
2192    fn encode_arm_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2193        let mut bytes = Vec::new();
2194
2195        // VMOV Sd, Rm — move integer to VFP register
2196        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
2197        bytes.extend_from_slice(&vmov.to_le_bytes());
2198
2199        // VCVT.F32.S32 Sd, Sd (signed) or VCVT.F32.U32 Sd, Sd (unsigned)
2200        // Base: 0xEEB80A40 (signed) or 0xEEB80AC0 (unsigned)
2201        let sd_num = vfp_sreg_to_num(sd)?;
2202        let (vd, d) = encode_sreg(sd_num);
2203        let (vm, m) = encode_sreg(sd_num); // same register as source
2204        let base = if signed { 0xEEB80A40 } else { 0xEEB80AC0 };
2205        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2206        bytes.extend_from_slice(&vcvt.to_le_bytes());
2207
2208        Ok(bytes)
2209    }
2210
2211    /// Encode F32 rounding pseudo-op as ARM32 via VCVT to integer and back.
2212    /// mode: 0b00=nearest, 0b01=floor(-Inf), 0b10=ceil(+Inf), 0b11=trunc(zero)
2213    /// Strategy: VCVT.S32.F32 Sd, Sm (toward zero), then VCVT.F32.S32 Sd, Sd
2214    /// For ceil/floor/nearest, we use VCVTR (round toward mode) + convert back.
2215    /// Simplified: convert to int (toward zero for trunc) then back to float.
2216    /// Encode F32 rounding as ARM32.
2217    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2218    ///
2219    /// For trunc (mode=0b11): uses VCVTR.S32.F32 (always rounds toward zero).
2220    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant
2221    /// which honours FPSCR rmode), then restores FPSCR.
2222    fn encode_arm_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2223        let mut bytes = Vec::new();
2224        let sm_num = vfp_sreg_to_num(sm)?;
2225        let sd_num = vfp_sreg_to_num(sd)?;
2226        let (vd_s, d_s) = encode_sreg(sd_num);
2227        let (vm_s, m_s) = encode_sreg(sm_num);
2228
2229        if mode == 0b11 {
2230            // Trunc (toward zero): VCVTR.S32.F32 — the "R" variant always truncates.
2231            // 0xEEBD0AC0: bit[7]=1 => round toward zero regardless of FPSCR
2232            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2233            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2234        } else {
2235            // ceil/floor/nearest: manipulate FPSCR rounding mode
2236            let rt: u32 = 12; // R12/IP as temp
2237
2238            // VMRS R12, FPSCR
2239            let vmrs = 0xEEF10A10 | (rt << 12);
2240            bytes.extend_from_slice(&vmrs.to_le_bytes());
2241
2242            // BIC R12, R12, #(3 << 22) — clear RMode bits [23:22]
2243            // 3<<22 = 0x00C00000. ARM rotated imm: 0x03 ror 10 (rotation=5, imm8=0x03)
2244            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2245            bytes.extend_from_slice(&bic.to_le_bytes());
2246
2247            // ORR R12, R12, #(mode << 22) — set desired rounding mode
2248            if mode != 0 {
2249                // mode<<22: rotation=5, imm8=mode
2250                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2251                bytes.extend_from_slice(&orr.to_le_bytes());
2252            }
2253
2254            // VMSR FPSCR, R12
2255            let vmsr = 0xEEE10A10 | (rt << 12);
2256            bytes.extend_from_slice(&vmsr.to_le_bytes());
2257
2258            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rounding mode
2259            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2260            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2261
2262            // Restore FPSCR: clear rmode bits back to nearest (default)
2263            bytes.extend_from_slice(&vmrs.to_le_bytes());
2264            bytes.extend_from_slice(&bic.to_le_bytes());
2265            bytes.extend_from_slice(&vmsr.to_le_bytes());
2266        }
2267
2268        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
2269        let (vd2, d2) = encode_sreg(sd_num);
2270        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
2271        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2272
2273        Ok(bytes)
2274    }
2275
2276    /// Encode F32 min/max as ARM32: VCMP + VMRS + conditional VMOV
2277    fn encode_arm_f32_minmax(
2278        &self,
2279        sd: &VfpReg,
2280        sn: &VfpReg,
2281        sm: &VfpReg,
2282        is_min: bool,
2283    ) -> Result<Vec<u8>> {
2284        let mut bytes = Vec::new();
2285        let sn_num = vfp_sreg_to_num(sn)?;
2286        let sm_num = vfp_sreg_to_num(sm)?;
2287        let sd_num = vfp_sreg_to_num(sd)?;
2288
2289        // VMOV Sd, Sn (start with first operand)
2290        let (vd, d) = encode_sreg(sd_num);
2291        let (vn, n) = encode_sreg(sn_num);
2292        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2293        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2294
2295        // VCMP.F32 Sn, Sm
2296        let (vm, m) = encode_sreg(sm_num);
2297        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2298        bytes.extend_from_slice(&vcmp.to_le_bytes());
2299
2300        // VMRS APSR_nzcv, FPSCR
2301        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2302
2303        // For min: if Sn > Sm (GT), use Sm. Condition = GT (0xC)
2304        // For max: if Sn < Sm (MI/LT), use Sm. Condition = MI (0x4)
2305        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2306
2307        // VMOV{cond} Sd, Sm — conditional VMOV
2308        let vmov_cond = (cond << 28) | 0x0EB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2309        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2310
2311        Ok(bytes)
2312    }
2313
2314    /// Encode F32 copysign as ARM32: extract sign from Sm, magnitude from Sn
2315    fn encode_arm_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2316        let mut bytes = Vec::new();
2317
2318        // VMOV R12, Sm (get sign source bits)
2319        let vmov_sm = encode_vmov_core_sreg(false, sm, &Reg::R12)?;
2320        bytes.extend_from_slice(&vmov_sm.to_le_bytes());
2321
2322        // VMOV R0, Sn (get magnitude source bits) — use R0 as temp
2323        let vmov_sn = encode_vmov_core_sreg(false, sn, &Reg::R0)?;
2324        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2325
2326        // AND R12, R12, #0x80000000 (keep only sign bit)
2327        // Thumb-2 constant 0x80000000 needs special encoding; in ARM32 use rotated imm
2328        // 0x80000000 = 0x02 rotated right by 2 (rotation=1, imm8=0x02)
2329        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2330        bytes.extend_from_slice(&and_sign.to_le_bytes());
2331
2332        // BIC R0, R0, #0x80000000 (clear sign bit from magnitude)
2333        // R0 = register 0, so Rn and Rd fields are 0
2334        let bic_sign = 0xE3C00000u32 | (1 << 8) | 0x02;
2335        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2336
2337        // ORR R0, R0, R12 (combine sign + magnitude)
2338        // R0 = register 0, so Rn and Rd fields are 0
2339        let orr = 0xE1800000u32 | 12;
2340        bytes.extend_from_slice(&orr.to_le_bytes());
2341
2342        // VMOV Sd, R0
2343        let vmov_result = encode_vmov_core_sreg(true, sd, &Reg::R0)?;
2344        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2345
2346        Ok(bytes)
2347    }
2348
2349    /// Encode F64 comparison as ARM32: VCMP.F64 + VMRS + MOV rd,#0 + MOVcond rd,#1
2350    fn encode_arm_f64_compare(
2351        &self,
2352        rd: &Reg,
2353        dn: &VfpReg,
2354        dm: &VfpReg,
2355        cond_code: u32,
2356    ) -> Result<Vec<u8>> {
2357        let mut bytes = Vec::new();
2358
2359        // VCMP.F64 Dn, Dm: 0xEEB40B40 with Dn in Vd position, Dm in Vm position
2360        let dn_num = vfp_dreg_to_num(dn)?;
2361        let dm_num = vfp_dreg_to_num(dm)?;
2362        let (vd, d) = encode_dreg(dn_num);
2363        let (vm, m) = encode_dreg(dm_num);
2364        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2365        bytes.extend_from_slice(&vcmp.to_le_bytes());
2366
2367        // VMRS APSR_nzcv, FPSCR
2368        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2369
2370        // MOV rd, #0
2371        let rd_bits = reg_to_bits(rd);
2372        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2373        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2374
2375        // MOVcond rd, #1
2376        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2377        bytes.extend_from_slice(&mov_one.to_le_bytes());
2378
2379        Ok(bytes)
2380    }
2381
2382    /// Encode F64 constant load as ARM32: MOVW + MOVT + MOVW + MOVT + VMOV
2383    fn encode_arm_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
2384        let mut bytes = Vec::new();
2385        let bits = value.to_bits();
2386        let lo32 = bits as u32;
2387        let hi32 = (bits >> 32) as u32;
2388
2389        // Load low 32 bits into R0 (Rd field = 0 for R0)
2390        let lo16 = lo32 & 0xFFFF;
2391        let movw_r0 = 0xE3000000 | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2392        bytes.extend_from_slice(&movw_r0.to_le_bytes());
2393        let hi16 = (lo32 >> 16) & 0xFFFF;
2394        let movt_r0 = 0xE3400000 | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2395        bytes.extend_from_slice(&movt_r0.to_le_bytes());
2396
2397        // Load high 32 bits into R12
2398        let lo16 = hi32 & 0xFFFF;
2399        let movw_r12 = 0xE3000000 | ((lo16 >> 12) << 16) | (12 << 12) | (lo16 & 0xFFF);
2400        bytes.extend_from_slice(&movw_r12.to_le_bytes());
2401        let hi16 = (hi32 >> 16) & 0xFFFF;
2402        let movt_r12 = 0xE3400000 | ((hi16 >> 12) << 16) | (12 << 12) | (hi16 & 0xFFF);
2403        bytes.extend_from_slice(&movt_r12.to_le_bytes());
2404
2405        // VMOV Dd, R0, R12
2406        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
2407        bytes.extend_from_slice(&vmov.to_le_bytes());
2408
2409        Ok(bytes)
2410    }
2411
2412    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as ARM32
2413    fn encode_arm_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2414        let mut bytes = Vec::new();
2415
2416        // Use S0 as intermediate: VMOV S0, Rm
2417        let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
2418        bytes.extend_from_slice(&vmov.to_le_bytes());
2419
2420        // VCVT.F64.S32 Dd, S0 (signed) or VCVT.F64.U32 Dd, S0 (unsigned)
2421        // Base: 0xEEB80B40 (signed) or 0xEEB80BC0 (unsigned)
2422        let dd_num = vfp_dreg_to_num(dd)?;
2423        let (vd, d) = encode_dreg(dd_num);
2424        let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
2425        // S0 is register 0: Vm=0, M=0
2426        let vcvt = base | (d << 22) | (vd << 12);
2427        bytes.extend_from_slice(&vcvt.to_le_bytes());
2428
2429        Ok(bytes)
2430    }
2431
2432    /// Encode VCVT.F64.F32 Dd, Sm as ARM32 (f32 to f64 promotion)
2433    fn encode_arm_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2434        let dd_num = vfp_dreg_to_num(dd)?;
2435        let sm_num = vfp_sreg_to_num(sm)?;
2436        let (vd, d) = encode_dreg(dd_num);
2437        let (vm, m) = encode_sreg(sm_num);
2438
2439        // VCVT.F64.F32 Dd, Sm: 0xEEB70AC0
2440        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
2441        Ok(vcvt.to_le_bytes().to_vec())
2442    }
2443
2444    /// Encode VCVT.S32/U32.F64 Sd, Dm + VMOV Rd, Sd as ARM32
2445    fn encode_arm_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2446        let mut bytes = Vec::new();
2447        let dm_num = vfp_dreg_to_num(dm)?;
2448        let (vm, m) = encode_dreg(dm_num);
2449
2450        // VCVT.S32.F64 S0, Dm (toward zero) or VCVT.U32.F64 S0, Dm
2451        // S0: Vd=0, D=0
2452        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
2453        let vcvt = base | (m << 5) | vm;
2454        bytes.extend_from_slice(&vcvt.to_le_bytes());
2455
2456        // VMOV Rd, S0
2457        let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
2458        bytes.extend_from_slice(&vmov.to_le_bytes());
2459
2460        Ok(bytes)
2461    }
2462
2463    /// Encode F64 rounding pseudo-op as ARM32 via VCVT to integer and back.
2464    /// Encode F64 rounding as ARM32.
2465    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2466    ///
2467    /// For trunc: uses VCVTR.S32.F64 (always truncates).
2468    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F64 (non-R variant),
2469    /// then restores FPSCR.
2470    fn encode_arm_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2471        let mut bytes = Vec::new();
2472        let dm_num = vfp_dreg_to_num(dm)?;
2473        let dd_num = vfp_dreg_to_num(dd)?;
2474        let (vm, m) = encode_dreg(dm_num);
2475        let (vd, d) = encode_dreg(dd_num);
2476
2477        if mode == 0b11 {
2478            // Trunc (toward zero): VCVTR.S32.F64 — bit[7]=1, always truncates
2479            let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
2480            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2481        } else {
2482            // ceil/floor/nearest: manipulate FPSCR rounding mode
2483            let rt: u32 = 12;
2484
2485            // VMRS R12, FPSCR
2486            let vmrs = 0xEEF10A10 | (rt << 12);
2487            bytes.extend_from_slice(&vmrs.to_le_bytes());
2488
2489            // BIC R12, R12, #(3 << 22)
2490            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2491            bytes.extend_from_slice(&bic.to_le_bytes());
2492
2493            // ORR R12, R12, #(mode << 22)
2494            if mode != 0 {
2495                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2496                bytes.extend_from_slice(&orr.to_le_bytes());
2497            }
2498
2499            // VMSR FPSCR, R12
2500            let vmsr = 0xEEE10A10 | (rt << 12);
2501            bytes.extend_from_slice(&vmsr.to_le_bytes());
2502
2503            // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0), uses FPSCR rmode
2504            let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
2505            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2506
2507            // Restore FPSCR
2508            bytes.extend_from_slice(&vmrs.to_le_bytes());
2509            bytes.extend_from_slice(&bic.to_le_bytes());
2510            bytes.extend_from_slice(&vmsr.to_le_bytes());
2511        }
2512
2513        // VCVT.F64.S32 Dd, S0 (convert back to double)
2514        let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
2515        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2516
2517        Ok(bytes)
2518    }
2519
2520    /// Encode F64 min/max as ARM32: VMOV + VCMP + VMRS + conditional VMOV
2521    fn encode_arm_f64_minmax(
2522        &self,
2523        dd: &VfpReg,
2524        dn: &VfpReg,
2525        dm: &VfpReg,
2526        is_min: bool,
2527    ) -> Result<Vec<u8>> {
2528        let mut bytes = Vec::new();
2529        let dn_num = vfp_dreg_to_num(dn)?;
2530        let dm_num = vfp_dreg_to_num(dm)?;
2531        let dd_num = vfp_dreg_to_num(dd)?;
2532
2533        // VMOV.F64 Dd, Dn (start with first operand)
2534        let (vd, d) = encode_dreg(dd_num);
2535        let (vn, n) = encode_dreg(dn_num);
2536        let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2537        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2538
2539        // VCMP.F64 Dn, Dm
2540        let (vm, m) = encode_dreg(dm_num);
2541        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2542        bytes.extend_from_slice(&vcmp.to_le_bytes());
2543
2544        // VMRS APSR_nzcv, FPSCR
2545        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2546
2547        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2548        let vmov_cond = (cond << 28) | 0x0EB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2549        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2550
2551        Ok(bytes)
2552    }
2553
2554    /// Encode F64 copysign as ARM32
2555    fn encode_arm_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
2556        let mut bytes = Vec::new();
2557
2558        // VMOV R0, R12, Dm (get sign source bits)
2559        let vmov_dm = encode_vmov_core_dreg(false, dm, &Reg::R0, &Reg::R12)?;
2560        bytes.extend_from_slice(&vmov_dm.to_le_bytes());
2561
2562        // VMOV R1, R2, Dn (get magnitude source bits)
2563        // We use R1 (lo) and R2 (hi) for the magnitude
2564        let vmov_dn = encode_vmov_core_dreg(false, dn, &Reg::R1, &Reg::R2)?;
2565        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2566
2567        // AND R12, R12, #0x80000000 (keep only sign bit from hi word)
2568        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2569        bytes.extend_from_slice(&and_sign.to_le_bytes());
2570
2571        // BIC R2, R2, #0x80000000 (clear sign bit from magnitude hi word)
2572        let bic_sign = 0xE3C00000u32 | (2 << 16) | (2 << 12) | (1 << 8) | 0x02;
2573        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2574
2575        // ORR R2, R2, R12 (combine sign + magnitude)
2576        let orr = 0xE1800000u32 | (2 << 16) | (2 << 12) | 12;
2577        bytes.extend_from_slice(&orr.to_le_bytes());
2578
2579        // VMOV Dd, R1, R2
2580        let vmov_result = encode_vmov_core_dreg(true, dd, &Reg::R1, &Reg::R2)?;
2581        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2582
2583        Ok(bytes)
2584    }
2585
2586    /// Encode VCVT.S32/U32.F32 + VMOV as ARM32
2587    fn encode_arm_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2588        let mut bytes = Vec::new();
2589
2590        // VCVT.S32.F32 Sd, Sm (toward zero) or VCVT.U32.F32 Sd, Sm
2591        // We use Sm as both source and destination for the intermediate result
2592        let sm_num = vfp_sreg_to_num(sm)?;
2593        let (vd, d) = encode_sreg(sm_num);
2594        let (vm, m) = encode_sreg(sm_num);
2595        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
2596        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2597        bytes.extend_from_slice(&vcvt.to_le_bytes());
2598
2599        // VMOV Rd, Sm — move result back to core register
2600        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
2601        bytes.extend_from_slice(&vmov.to_le_bytes());
2602
2603        Ok(bytes)
2604    }
2605
2606    /// Encode an ARM instruction in Thumb-2 mode (16-bit or 32-bit instructions)
2607    fn encode_thumb(&self, op: &ArmOp) -> Result<Vec<u8>> {
2608        // Thumb-2 supports both 16-bit and 32-bit instructions
2609        // 32-bit instructions are encoded as two 16-bit halfwords (big-endian order)
2610        match op {
2611            // === 16-bit Thumb encodings ===
2612            ArmOp::Add { rd, rn, op2 } => {
2613                let rd_bits = reg_to_bits(rd) as u16;
2614                let rn_bits = reg_to_bits(rn) as u16;
2615
2616                if let Operand2::Reg(rm) = op2 {
2617                    let rm_bits = reg_to_bits(rm) as u16;
2618                    // 16-bit ADDS only has 3-bit register fields (R0-R7). For
2619                    // high registers (e.g. R12, the MemLoad/MemStore base
2620                    // scratch) the bits overflow into adjacent fields, silently
2621                    // corrupting the operands — issue #178/#180: `add ip,ip,r0`
2622                    // was emitted as `adds r4,r5,r1`. Guard on all three regs
2623                    // being low and fall back to 32-bit ADD.W otherwise, exactly
2624                    // as the Sub handler below does.
2625                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2626                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2627                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2628                        Ok(instr.to_le_bytes().to_vec())
2629                    } else {
2630                        // ADD.W Rd, Rn, Rm (32-bit) for high registers
2631                        self.encode_thumb32_add_reg_raw(
2632                            rd_bits as u32,
2633                            rn_bits as u32,
2634                            rm_bits as u32,
2635                        )
2636                    }
2637                } else if let Operand2::Imm(imm) = op2 {
2638                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2639                        // ADDS Rd, Rn, #imm3 (16-bit): 0001 110 imm3 Rn Rd
2640                        let instr: u16 = 0x1C00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2641                        Ok(instr.to_le_bytes().to_vec())
2642                    } else {
2643                        // Use 32-bit ADD for larger immediates
2644                        self.encode_thumb32_add(rd, rn, *imm as u32)
2645                    }
2646                } else {
2647                    // Fallback to 32-bit encoding
2648                    self.encode_thumb32_add(rd, rn, 0)
2649                }
2650            }
2651
2652            ArmOp::Sub { rd, rn, op2 } => {
2653                let rd_bits = reg_to_bits(rd) as u16;
2654                let rn_bits = reg_to_bits(rn) as u16;
2655
2656                if let Operand2::Reg(rm) = op2 {
2657                    let rm_bits = reg_to_bits(rm) as u16;
2658                    // 16-bit SUBS can only use low registers (R0-R7)
2659                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2660                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2661                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2662                        Ok(instr.to_le_bytes().to_vec())
2663                    } else {
2664                        // Use 32-bit SUB.W for high registers
2665                        self.encode_thumb32_sub_reg_raw(
2666                            rd_bits as u32,
2667                            rn_bits as u32,
2668                            rm_bits as u32,
2669                        )
2670                    }
2671                } else if let Operand2::Imm(imm) = op2 {
2672                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2673                        // SUBS Rd, Rn, #imm3 (16-bit): 0001 111 imm3 Rn Rd
2674                        let instr: u16 = 0x1E00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2675                        Ok(instr.to_le_bytes().to_vec())
2676                    } else {
2677                        self.encode_thumb32_sub(rd, rn, *imm as u32)
2678                    }
2679                } else {
2680                    self.encode_thumb32_sub(rd, rn, 0)
2681                }
2682            }
2683
2684            ArmOp::Mov { rd, op2 } => {
2685                let rd_bits = reg_to_bits(rd) as u16;
2686
2687                if let Operand2::Imm(imm) = op2 {
2688                    // #498: the old test here was the SIGNED `*imm <= 255`,
2689                    // so a negative immediate (e.g. -1) fell into the 16-bit
2690                    // MOVS arm and encoded the wrong VALUE (#(imm & 0xFF) =
2691                    // #0xFF). A positive imm above 0xFFFF was equally wrong:
2692                    // MOVW truncates to 16 bits. Split on the UNSIGNED value:
2693                    // imm8 → MOVS, imm16 → MOVW, anything wider (negative or
2694                    // >0xFFFF) → the full-value MOVW+MOVT pair. No emitter
2695                    // produces the wide shape today (both selectors
2696                    // materialize wide constants as explicit Movw/Movt or
2697                    // Movw+Mvn), so this is byte-identical on shipped paths —
2698                    // it retires the latent wrong-value encodings the
2699                    // `estimator_encoder_agreement` oracle had pinned.
2700                    let uimm = *imm as u32;
2701                    if uimm <= 255 && rd_bits < 8 {
2702                        // MOVS Rd, #imm8 (16-bit): 0010 0 Rd imm8
2703                        let imm_bits = (*imm as u16) & 0xFF;
2704                        let instr: u16 = 0x2000 | (rd_bits << 8) | imm_bits;
2705                        Ok(instr.to_le_bytes().to_vec())
2706                    } else if uimm <= 0xFFFF {
2707                        // Use 32-bit MOVW for 16-bit immediates
2708                        self.encode_thumb32_movw(rd, uimm)
2709                    } else {
2710                        // Full 32-bit value: MOVW low16 + MOVT high16
2711                        let mut bytes = self.encode_thumb32_movw(rd, uimm & 0xFFFF)?;
2712                        bytes.extend(self.encode_thumb32_movt_raw(reg_to_bits(rd), uimm >> 16)?);
2713                        Ok(bytes)
2714                    }
2715                } else if let Operand2::Reg(rm) = op2 {
2716                    let rm_bits = reg_to_bits(rm) as u16;
2717                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
2718                    // D = Rd[3], Rd[2:0] in lower bits
2719                    let d_bit = (rd_bits >> 3) & 1;
2720                    let instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
2721                    Ok(instr.to_le_bytes().to_vec())
2722                } else {
2723                    let instr: u16 = 0xBF00; // NOP fallback
2724                    Ok(instr.to_le_bytes().to_vec())
2725                }
2726            }
2727
2728            ArmOp::Push { regs } => {
2729                // Thumb-2 PUSH encoding:
2730                // If all regs in R0-R7 + LR, use 16-bit: 1011 010 M rrrrrrrr
2731                // Otherwise use 32-bit: STMDB SP!, {regs} = 1110 1001 0010 1101 | 0M0 reglist(13)
2732                let mut reg_list: u16 = 0;
2733                let mut need_32bit = false;
2734                for r in regs {
2735                    let bit = reg_to_bits(r);
2736                    if bit >= 8 && *r != Reg::LR {
2737                        need_32bit = true;
2738                    }
2739                    reg_list |= 1 << bit;
2740                }
2741                if !need_32bit {
2742                    // 16-bit PUSH: 1011 010 M rrrrrrrr
2743                    let m_bit = if reg_list & (1 << 14) != 0 {
2744                        1u16
2745                    } else {
2746                        0u16
2747                    };
2748                    let low_regs = reg_list & 0xFF;
2749                    let instr: u16 = 0xB400 | (m_bit << 8) | low_regs;
2750                    Ok(instr.to_le_bytes().to_vec())
2751                } else {
2752                    // 32-bit STMDB SP!, {regs}: E92D | reglist(16)
2753                    let hw1: u16 = 0xE92D;
2754                    let hw2: u16 = reg_list;
2755                    let mut bytes = hw1.to_le_bytes().to_vec();
2756                    bytes.extend_from_slice(&hw2.to_le_bytes());
2757                    Ok(bytes)
2758                }
2759            }
2760
2761            ArmOp::Pop { regs } => {
2762                // Thumb-2 POP encoding:
2763                // If all regs in R0-R7 + PC, use 16-bit: 1011 110 P rrrrrrrr
2764                // Otherwise use 32-bit: LDMIA SP!, {regs} = 1110 1000 1011 1101 | PM0 reglist(13)
2765                let mut reg_list: u16 = 0;
2766                let mut need_32bit = false;
2767                for r in regs {
2768                    let bit = reg_to_bits(r);
2769                    if bit >= 8 && *r != Reg::PC {
2770                        need_32bit = true;
2771                    }
2772                    reg_list |= 1 << bit;
2773                }
2774                if !need_32bit {
2775                    // 16-bit POP: 1011 110 P rrrrrrrr
2776                    let p_bit = if reg_list & (1 << 15) != 0 {
2777                        1u16
2778                    } else {
2779                        0u16
2780                    };
2781                    let low_regs = reg_list & 0xFF;
2782                    let instr: u16 = 0xBC00 | (p_bit << 8) | low_regs;
2783                    Ok(instr.to_le_bytes().to_vec())
2784                } else {
2785                    // 32-bit LDMIA SP!, {regs}: E8BD | reglist(16)
2786                    let hw1: u16 = 0xE8BD;
2787                    let hw2: u16 = reg_list;
2788                    let mut bytes = hw1.to_le_bytes().to_vec();
2789                    bytes.extend_from_slice(&hw2.to_le_bytes());
2790                    Ok(bytes)
2791                }
2792            }
2793
2794            ArmOp::Nop => {
2795                let instr: u16 = 0xBF00; // NOP in Thumb-2
2796                Ok(instr.to_le_bytes().to_vec())
2797            }
2798
2799            ArmOp::Udf { imm } => {
2800                // UDF (Undefined) in Thumb-2: 16-bit encoding is 0xDE00 | imm8
2801                // This triggers UsageFault/HardFault, used for WASM traps
2802                let instr: u16 = 0xDE00 | (*imm as u16);
2803                let bytes = instr.to_le_bytes().to_vec();
2804                encoding_contracts::verify_thumb16(&bytes);
2805                Ok(bytes)
2806            }
2807
2808            // i64 support: ADDS, ADC, SUBS, SBC for register pair arithmetic
2809            // ADDS sets flags (carry), ADC uses carry from previous ADDS
2810            ArmOp::Adds { rd, rn, op2 } => {
2811                let rd_bits = reg_to_bits(rd) as u16;
2812                let rn_bits = reg_to_bits(rn) as u16;
2813
2814                if let Operand2::Reg(rm) = op2 {
2815                    let rm_bits = reg_to_bits(rm) as u16;
2816                    // 16-bit ADDS is R0-R7 only; i64 pair allocation can place
2817                    // operands in R8-R11, which would overflow the 3-bit fields
2818                    // and corrupt the operands (#178/#180 class). Guard and fall
2819                    // back to 32-bit ADDS.W for high registers.
2820                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2821                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2822                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2823                        Ok(instr.to_le_bytes().to_vec())
2824                    } else {
2825                        self.encode_thumb32_adds_reg_raw(
2826                            rd_bits as u32,
2827                            rn_bits as u32,
2828                            rm_bits as u32,
2829                        )
2830                    }
2831                } else {
2832                    // 32-bit Thumb-2 ADDS with immediate
2833                    self.encode_thumb32_adds(rd, rn, 0)
2834                }
2835            }
2836
2837            // ADC: Add with Carry (Thumb-2 32-bit)
2838            // ADC.W Rd, Rn, Rm: EB40 Rn | 00 Rd 00 Rm
2839            ArmOp::Adc { rd, rn, op2 } => {
2840                let rd_bits = reg_to_bits(rd);
2841                let rn_bits = reg_to_bits(rn);
2842
2843                if let Operand2::Reg(rm) = op2 {
2844                    let rm_bits = reg_to_bits(rm);
2845                    // ADC.W Rd, Rn, Rm (T2): 1110 1011 0100 Rn | 0 000 Rd 00 00 Rm
2846                    let hw1: u16 = (0xEB40 | rn_bits) as u16;
2847                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2848
2849                    let mut bytes = hw1.to_le_bytes().to_vec();
2850                    bytes.extend_from_slice(&hw2.to_le_bytes());
2851                    Ok(bytes)
2852                } else {
2853                    // ADC with immediate - use 32-bit encoding
2854                    let hw1: u16 = (0xF140 | rn_bits) as u16;
2855                    let hw2: u16 = (rd_bits << 8) as u16;
2856                    let mut bytes = hw1.to_le_bytes().to_vec();
2857                    bytes.extend_from_slice(&hw2.to_le_bytes());
2858                    Ok(bytes)
2859                }
2860            }
2861
2862            // SUBS sets flags (borrow), SBC uses borrow from previous SUBS
2863            ArmOp::Subs { rd, rn, op2 } => {
2864                let rd_bits = reg_to_bits(rd) as u16;
2865                let rn_bits = reg_to_bits(rn) as u16;
2866
2867                if let Operand2::Reg(rm) = op2 {
2868                    let rm_bits = reg_to_bits(rm) as u16;
2869                    // 16-bit SUBS is R0-R7 only; high-register i64 pair operands
2870                    // would overflow the 3-bit fields (#178/#180 class). Guard
2871                    // and fall back to 32-bit SUBS.W for high registers.
2872                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2873                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2874                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2875                        Ok(instr.to_le_bytes().to_vec())
2876                    } else {
2877                        self.encode_thumb32_subs_reg_raw(
2878                            rd_bits as u32,
2879                            rn_bits as u32,
2880                            rm_bits as u32,
2881                        )
2882                    }
2883                } else {
2884                    // 32-bit Thumb-2 SUBS with immediate
2885                    self.encode_thumb32_subs(rd, rn, 0)
2886                }
2887            }
2888
2889            // SBC: Subtract with Carry (Thumb-2 32-bit)
2890            // SBC.W Rd, Rn, Rm: EB60 Rn | 00 Rd 00 Rm
2891            ArmOp::Sbc { rd, rn, op2 } => {
2892                let rd_bits = reg_to_bits(rd);
2893                let rn_bits = reg_to_bits(rn);
2894
2895                if let Operand2::Reg(rm) = op2 {
2896                    let rm_bits = reg_to_bits(rm);
2897                    // SBC.W Rd, Rn, Rm (T2): 1110 1011 0110 Rn | 0 000 Rd 00 00 Rm
2898                    let hw1: u16 = (0xEB60 | rn_bits) as u16;
2899                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2900
2901                    let mut bytes = hw1.to_le_bytes().to_vec();
2902                    bytes.extend_from_slice(&hw2.to_le_bytes());
2903                    Ok(bytes)
2904                } else {
2905                    // SBC with immediate - use 32-bit encoding
2906                    let hw1: u16 = (0xF160 | rn_bits) as u16;
2907                    let hw2: u16 = (rd_bits << 8) as u16;
2908                    let mut bytes = hw1.to_le_bytes().to_vec();
2909                    bytes.extend_from_slice(&hw2.to_le_bytes());
2910                    Ok(bytes)
2911                }
2912            }
2913
2914            // === 32-bit Thumb-2 encodings ===
2915
2916            // SDIV: 11111011 1001 Rn 1111 Rd 1111 Rm
2917            ArmOp::Sdiv { rd, rn, rm } => {
2918                let rd_bits = reg_to_bits(rd);
2919                let rn_bits = reg_to_bits(rn);
2920                let rm_bits = reg_to_bits(rm);
2921                reg_bits_checked(rd_bits)?;
2922                reg_bits_checked(rn_bits)?;
2923                reg_bits_checked(rm_bits)?;
2924
2925                // Thumb-2 SDIV: FB90 F0F0 | Rn<<16 | Rd<<8 | Rm
2926                // First halfword: 1111 1011 1001 Rn = 0xFB90 | Rn
2927                // Second halfword: 1111 Rd 1111 Rm = 0xF0F0 | Rd<<8 | Rm
2928                let hw1: u16 = (0xFB90 | rn_bits) as u16;
2929                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
2930
2931                // Thumb-2 32-bit instructions: first halfword, then second halfword (little-endian each)
2932                let mut bytes = hw1.to_le_bytes().to_vec();
2933                bytes.extend_from_slice(&hw2.to_le_bytes());
2934                encoding_contracts::verify_thumb32(&bytes);
2935                Ok(bytes)
2936            }
2937
2938            // UDIV: 11111011 1011 Rn 1111 Rd 1111 Rm
2939            ArmOp::Udiv { rd, rn, rm } => {
2940                let rd_bits = reg_to_bits(rd);
2941                let rn_bits = reg_to_bits(rn);
2942                let rm_bits = reg_to_bits(rm);
2943                reg_bits_checked(rd_bits)?;
2944                reg_bits_checked(rn_bits)?;
2945                reg_bits_checked(rm_bits)?;
2946
2947                // Thumb-2 UDIV: FBB0 F0F0 | Rn<<16 | Rd<<8 | Rm
2948                let hw1: u16 = (0xFBB0 | rn_bits) as u16;
2949                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
2950
2951                let mut bytes = hw1.to_le_bytes().to_vec();
2952                bytes.extend_from_slice(&hw2.to_le_bytes());
2953                encoding_contracts::verify_thumb32(&bytes);
2954                Ok(bytes)
2955            }
2956
2957            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
2958                let rdlo_bits = reg_to_bits(rdlo);
2959                let rdhi_bits = reg_to_bits(rdhi);
2960                let rn_bits = reg_to_bits(rn);
2961                let rm_bits = reg_to_bits(rm);
2962                reg_bits_checked(rdlo_bits)?;
2963                reg_bits_checked(rdhi_bits)?;
2964                reg_bits_checked(rn_bits)?;
2965                reg_bits_checked(rm_bits)?;
2966
2967                // Thumb-2 UMULL: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm
2968                let hw1: u16 = (0xFBA0 | rn_bits) as u16;
2969                let hw2: u16 = ((rdlo_bits << 12) | (rdhi_bits << 8) | rm_bits) as u16;
2970
2971                let mut bytes = hw1.to_le_bytes().to_vec();
2972                bytes.extend_from_slice(&hw2.to_le_bytes());
2973                encoding_contracts::verify_thumb32(&bytes);
2974                Ok(bytes)
2975            }
2976
2977            // MUL (Thumb-2 32-bit): MUL Rd, Rn, Rm
2978            ArmOp::Mul { rd, rn, rm } => {
2979                let rd_bits = reg_to_bits(rd);
2980                let rn_bits = reg_to_bits(rn);
2981                let rm_bits = reg_to_bits(rm);
2982
2983                // Thumb-2 MUL: FB00 F000 | Rn | Rd<<8 | Rm
2984                // 11111011 0000 Rn | 1111 Rd 0000 Rm
2985                let hw1: u16 = (0xFB00 | rn_bits) as u16;
2986                let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
2987
2988                let mut bytes = hw1.to_le_bytes().to_vec();
2989                bytes.extend_from_slice(&hw2.to_le_bytes());
2990                Ok(bytes)
2991            }
2992
2993            // MLS: Rd = Ra - Rn * Rm
2994            ArmOp::Mls { rd, rn, rm, ra } => {
2995                let rd_bits = reg_to_bits(rd);
2996                let rn_bits = reg_to_bits(rn);
2997                let rm_bits = reg_to_bits(rm);
2998                let ra_bits = reg_to_bits(ra);
2999
3000                // Thumb-2 MLS: FB00 Rn | Ra Rd 0001 Rm
3001                // 11111011 0000 Rn | Ra Rd 0001 Rm
3002                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3003                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | 0x10 | rm_bits) as u16;
3004
3005                let mut bytes = hw1.to_le_bytes().to_vec();
3006                bytes.extend_from_slice(&hw2.to_le_bytes());
3007                Ok(bytes)
3008            }
3009
3010            ArmOp::Mla { rd, rn, rm, ra } => {
3011                let rd_bits = reg_to_bits(rd);
3012                let rn_bits = reg_to_bits(rn);
3013                let rm_bits = reg_to_bits(rm);
3014                let ra_bits = reg_to_bits(ra);
3015
3016                // Thumb-2 MLA: FB00 Rn | Ra Rd 0000 Rm — same as MLS without the
3017                // bit-4 (0x10) op flag. rd = ra + rn*rm.
3018                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3019                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | rm_bits) as u16;
3020
3021                let mut bytes = hw1.to_le_bytes().to_vec();
3022                bytes.extend_from_slice(&hw2.to_le_bytes());
3023                Ok(bytes)
3024            }
3025
3026            // AND (Thumb-2 32-bit)
3027            ArmOp::And { rd, rn, op2 } => {
3028                if let Operand2::Reg(rm) = op2 {
3029                    let rd_bits = reg_to_bits(rd);
3030                    let rn_bits = reg_to_bits(rn);
3031                    let rm_bits = reg_to_bits(rm);
3032
3033                    // Thumb-2 AND register: EA00 Rn | 0 Rd 00 00 Rm
3034                    let hw1: u16 = (0xEA00 | rn_bits) as u16;
3035                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3036
3037                    let mut bytes = hw1.to_le_bytes().to_vec();
3038                    bytes.extend_from_slice(&hw2.to_le_bytes());
3039                    Ok(bytes)
3040                } else if let Operand2::Imm(imm) = op2 {
3041                    let rd_bits = reg_to_bits(rd);
3042                    let rn_bits = reg_to_bits(rn);
3043
3044                    // Thumb-2 AND.W immediate T1: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8.
3045                    // The i:imm3:imm8 field is a ThumbExpandImm modified immediate —
3046                    // encode it correctly (or error on an un-encodable value)
3047                    // rather than packing raw bits, closing the silent-miscompile
3048                    // class for AND alongside ORR/EOR (#251) / ADD/SUB (#253) /
3049                    // CMP (#255).
3050                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3051                        synth_core::Error::synthesis(
3052                            "AND immediate is not a valid ThumbExpandImm — materialize into a register",
3053                        )
3054                    })?;
3055                    let i_bit = (field >> 11) & 1;
3056                    let imm3 = (field >> 8) & 0x7;
3057                    let imm8 = field & 0xFF;
3058
3059                    let hw1: u16 = (0xF000 | (i_bit << 10) | rn_bits) as u16;
3060                    let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) 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 {
3066                    // RegShift variant - fallback to NOP
3067                    let instr: u16 = 0xBF00;
3068                    Ok(instr.to_le_bytes().to_vec())
3069                }
3070            }
3071
3072            // ORR (Thumb-2 32-bit)
3073            ArmOp::Orr { rd, rn, op2 } => {
3074                if let Operand2::Reg(rm) = op2 {
3075                    let rd_bits = reg_to_bits(rd);
3076                    let rn_bits = reg_to_bits(rn);
3077                    let rm_bits = reg_to_bits(rm);
3078
3079                    // Thumb-2 ORR: EA40 Rn | 0 Rd 00 00 Rm
3080                    let hw1: u16 = (0xEA40 | rn_bits) as u16;
3081                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3082
3083                    let mut bytes = hw1.to_le_bytes().to_vec();
3084                    bytes.extend_from_slice(&hw2.to_le_bytes());
3085                    Ok(bytes)
3086                } else if let Operand2::Imm(imm) = op2 {
3087                    // ORR.W immediate T1: 11110 i 0 0010 S Rn | 0 imm3 Rd imm8.
3088                    // Only the zero-extended byte form (imm <= 0xFF) is encoded;
3089                    // larger modified immediates need ThumbExpandImm — return an
3090                    // error rather than silently emit a NOP (Ok-or-Err, #180/#185).
3091                    let imm_val = *imm as u32;
3092                    if imm_val > 0xFF {
3093                        return Err(synth_core::Error::synthesis(
3094                            "ORR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3095                        ));
3096                    }
3097                    let rd_bits = reg_to_bits(rd);
3098                    let rn_bits = reg_to_bits(rn);
3099                    let hw1: u16 = (0xF040 | rn_bits) as u16;
3100                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3101                    let mut bytes = hw1.to_le_bytes().to_vec();
3102                    bytes.extend_from_slice(&hw2.to_le_bytes());
3103                    Ok(bytes)
3104                } else {
3105                    let instr: u16 = 0xBF00;
3106                    Ok(instr.to_le_bytes().to_vec())
3107                }
3108            }
3109
3110            // EOR (Thumb-2 32-bit)
3111            ArmOp::Eor { rd, rn, op2 } => {
3112                if let Operand2::Reg(rm) = op2 {
3113                    let rd_bits = reg_to_bits(rd);
3114                    let rn_bits = reg_to_bits(rn);
3115                    let rm_bits = reg_to_bits(rm);
3116
3117                    // Thumb-2 EOR: EA80 Rn | 0 Rd 00 00 Rm
3118                    let hw1: u16 = (0xEA80 | rn_bits) as u16;
3119                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3120
3121                    let mut bytes = hw1.to_le_bytes().to_vec();
3122                    bytes.extend_from_slice(&hw2.to_le_bytes());
3123                    Ok(bytes)
3124                } else if let Operand2::Imm(imm) = op2 {
3125                    // EOR.W immediate T1: 11110 i 0 0100 S Rn | 0 imm3 Rd imm8.
3126                    // Byte form only (imm <= 0xFF); larger needs ThumbExpandImm —
3127                    // error, not a silent NOP (Ok-or-Err, #180/#185).
3128                    let imm_val = *imm as u32;
3129                    if imm_val > 0xFF {
3130                        return Err(synth_core::Error::synthesis(
3131                            "EOR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3132                        ));
3133                    }
3134                    let rd_bits = reg_to_bits(rd);
3135                    let rn_bits = reg_to_bits(rn);
3136                    let hw1: u16 = (0xF080 | rn_bits) as u16;
3137                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3138                    let mut bytes = hw1.to_le_bytes().to_vec();
3139                    bytes.extend_from_slice(&hw2.to_le_bytes());
3140                    Ok(bytes)
3141                } else {
3142                    let instr: u16 = 0xBF00;
3143                    Ok(instr.to_le_bytes().to_vec())
3144                }
3145            }
3146
3147            // Shift operations (16-bit for low registers)
3148            ArmOp::Lsl { rd, rn, shift } => {
3149                let rd_bits = reg_to_bits(rd) as u16;
3150                let rn_bits = reg_to_bits(rn) as u16;
3151                let shift_bits = (*shift as u16) & 0x1F;
3152
3153                if rd_bits < 8 && rn_bits < 8 {
3154                    // LSLS Rd, Rm, #imm5 (16-bit): 0000 0 imm5 Rm Rd
3155                    let instr: u16 = (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3156                    Ok(instr.to_le_bytes().to_vec())
3157                } else {
3158                    // Use 32-bit encoding for high registers
3159                    self.encode_thumb32_shift(rd, rn, *shift, 0b00) // LSL type
3160                }
3161            }
3162
3163            ArmOp::Lsr { rd, rn, shift } => {
3164                let rd_bits = reg_to_bits(rd) as u16;
3165                let rn_bits = reg_to_bits(rn) as u16;
3166                let shift_bits = (*shift as u16) & 0x1F;
3167
3168                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3169                    // LSRS Rd, Rm, #imm5 (16-bit): 0000 1 imm5 Rm Rd
3170                    let instr: u16 = 0x0800 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3171                    Ok(instr.to_le_bytes().to_vec())
3172                } else {
3173                    self.encode_thumb32_shift(rd, rn, *shift, 0b01) // LSR type
3174                }
3175            }
3176
3177            ArmOp::Asr { rd, rn, shift } => {
3178                let rd_bits = reg_to_bits(rd) as u16;
3179                let rn_bits = reg_to_bits(rn) as u16;
3180                let shift_bits = (*shift as u16) & 0x1F;
3181
3182                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3183                    // ASRS Rd, Rm, #imm5 (16-bit): 0001 0 imm5 Rm Rd
3184                    let instr: u16 = 0x1000 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3185                    Ok(instr.to_le_bytes().to_vec())
3186                } else {
3187                    self.encode_thumb32_shift(rd, rn, *shift, 0b10) // ASR type
3188                }
3189            }
3190
3191            ArmOp::Ror { rd, rn, shift } => {
3192                // ROR doesn't have a 16-bit immediate form, use 32-bit
3193                self.encode_thumb32_shift(rd, rn, *shift, 0b11) // ROR type
3194            }
3195
3196            // Register-based shifts (Thumb-2 32-bit)
3197            // Encoding: 11111010 0xxS Rn 1111 Rd 0000 Rm
3198            // xx = shift type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
3199            ArmOp::LslReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b00),
3200            ArmOp::LsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b01),
3201            ArmOp::AsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b10),
3202            ArmOp::RorReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b11),
3203
3204            // RSB (Reverse Subtract): Rd = imm - Rn
3205            // Thumb-2 T2 encoding: 11110 i 0 1110 S Rn | 0 imm3 Rd imm8
3206            ArmOp::Rsb { rd, rn, imm } => {
3207                let rd_bits = reg_to_bits(rd);
3208                let rn_bits = reg_to_bits(rn);
3209                let imm_val = *imm;
3210
3211                let i_bit = (imm_val >> 11) & 1;
3212                let imm3 = (imm_val >> 8) & 0x7;
3213                let imm8 = imm_val & 0xFF;
3214
3215                // hw1: 11110 i 01110 0 Rn  (S=0)
3216                let hw1: u16 = (0xF1C0 | (i_bit << 10) | rn_bits) as u16;
3217                // hw2: 0 imm3 Rd imm8
3218                let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3219
3220                let mut bytes = hw1.to_le_bytes().to_vec();
3221                bytes.extend_from_slice(&hw2.to_le_bytes());
3222                Ok(bytes)
3223            }
3224
3225            // CLZ (Thumb-2 32-bit)
3226            ArmOp::Clz { rd, rm } => {
3227                let rd_bits = reg_to_bits(rd);
3228                let rm_bits = reg_to_bits(rm);
3229
3230                // Thumb-2 CLZ: FAB0 Rm | F8 Rd Rm
3231                // 11111010 1011 Rm | 1111 1000 Rd Rm
3232                let hw1: u16 = (0xFAB0 | rm_bits) as u16;
3233                let hw2: u16 = (0xF080 | (rd_bits << 8) | rm_bits) as u16;
3234
3235                let mut bytes = hw1.to_le_bytes().to_vec();
3236                bytes.extend_from_slice(&hw2.to_le_bytes());
3237                Ok(bytes)
3238            }
3239
3240            // RBIT (Thumb-2 32-bit)
3241            ArmOp::Rbit { rd, rm } => {
3242                let rd_bits = reg_to_bits(rd);
3243                let rm_bits = reg_to_bits(rm);
3244
3245                // Thumb-2 RBIT: FA90 Rm | F0 Rd A0 Rm
3246                // 11111010 1001 Rm | 1111 Rd 1010 Rm
3247                let hw1: u16 = (0xFA90 | rm_bits) as u16;
3248                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rm_bits) as u16;
3249
3250                let mut bytes = hw1.to_le_bytes().to_vec();
3251                bytes.extend_from_slice(&hw2.to_le_bytes());
3252                Ok(bytes)
3253            }
3254
3255            // SXTB (16-bit for low registers)
3256            ArmOp::Sxtb { rd, rm } => {
3257                let rd_bits = reg_to_bits(rd) as u16;
3258                let rm_bits = reg_to_bits(rm) as u16;
3259
3260                if rd_bits < 8 && rm_bits < 8 {
3261                    // SXTB Rd, Rm (16-bit): 1011 0010 01 Rm Rd
3262                    let instr: u16 = 0xB240 | (rm_bits << 3) | rd_bits;
3263                    Ok(instr.to_le_bytes().to_vec())
3264                } else {
3265                    // Thumb-2 SXTB.W: FA4F F(rd)80 (rm)
3266                    // 11111010 0100 1111 | 1111 Rd 10 rotate Rm
3267                    let rd_bits32 = rd_bits as u32;
3268                    let rm_bits32 = rm_bits as u32;
3269                    let hw1: u16 = 0xFA4F;
3270                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3271                    let mut bytes = hw1.to_le_bytes().to_vec();
3272                    bytes.extend_from_slice(&hw2.to_le_bytes());
3273                    Ok(bytes)
3274                }
3275            }
3276
3277            // SXTH (16-bit for low registers)
3278            ArmOp::Sxth { rd, rm } => {
3279                let rd_bits = reg_to_bits(rd) as u16;
3280                let rm_bits = reg_to_bits(rm) as u16;
3281
3282                if rd_bits < 8 && rm_bits < 8 {
3283                    // SXTH Rd, Rm (16-bit): 1011 0010 00 Rm Rd
3284                    let instr: u16 = 0xB200 | (rm_bits << 3) | rd_bits;
3285                    Ok(instr.to_le_bytes().to_vec())
3286                } else {
3287                    // Thumb-2 SXTH.W: FA0F F(rd)80 (rm)
3288                    // 11111010 0000 1111 | 1111 Rd 10 rotate Rm
3289                    let rd_bits32 = rd_bits as u32;
3290                    let rm_bits32 = rm_bits as u32;
3291                    let hw1: u16 = 0xFA0F;
3292                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3293                    let mut bytes = hw1.to_le_bytes().to_vec();
3294                    bytes.extend_from_slice(&hw2.to_le_bytes());
3295                    Ok(bytes)
3296                }
3297            }
3298
3299            // UXTB Rd,Rm — zero-extend byte (rd = rm & 0xff)
3300            ArmOp::Uxtb { rd, rm } => {
3301                let rd_bits = reg_to_bits(rd) as u16;
3302                let rm_bits = reg_to_bits(rm) as u16;
3303                if rd_bits < 8 && rm_bits < 8 {
3304                    // UXTB Rd, Rm (16-bit): 1011 0010 11 Rm Rd
3305                    let instr: u16 = 0xB2C0 | (rm_bits << 3) | rd_bits;
3306                    Ok(instr.to_le_bytes().to_vec())
3307                } else {
3308                    // Thumb-2 UXTB.W: FA5F F(rd)80 (rm)
3309                    let hw1: u16 = 0xFA5F;
3310                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3311                    let mut bytes = hw1.to_le_bytes().to_vec();
3312                    bytes.extend_from_slice(&hw2.to_le_bytes());
3313                    Ok(bytes)
3314                }
3315            }
3316
3317            // UXTH Rd,Rm — zero-extend halfword (rd = rm & 0xffff)
3318            ArmOp::Uxth { rd, rm } => {
3319                let rd_bits = reg_to_bits(rd) as u16;
3320                let rm_bits = reg_to_bits(rm) as u16;
3321                if rd_bits < 8 && rm_bits < 8 {
3322                    // UXTH Rd, Rm (16-bit): 1011 0010 10 Rm Rd
3323                    let instr: u16 = 0xB280 | (rm_bits << 3) | rd_bits;
3324                    Ok(instr.to_le_bytes().to_vec())
3325                } else {
3326                    // Thumb-2 UXTH.W: FA1F F(rd)80 (rm)
3327                    let hw1: u16 = 0xFA1F;
3328                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3329                    let mut bytes = hw1.to_le_bytes().to_vec();
3330                    bytes.extend_from_slice(&hw2.to_le_bytes());
3331                    Ok(bytes)
3332                }
3333            }
3334
3335            // CMP (can be 16-bit for low registers)
3336            ArmOp::Cmp { rn, op2 } => {
3337                let rn_bits = reg_to_bits(rn) as u16;
3338
3339                if let Operand2::Imm(imm) = op2 {
3340                    // Only use 16-bit encoding for non-negative immediates 0-255
3341                    // Negative immediates must use 32-bit encoding
3342                    if *imm >= 0 && *imm <= 255 && rn_bits < 8 {
3343                        // CMP Rn, #imm8 (16-bit): 0010 1 Rn imm8
3344                        let instr: u16 = 0x2800 | (rn_bits << 8) | (*imm as u16 & 0xFF);
3345                        Ok(instr.to_le_bytes().to_vec())
3346                    } else {
3347                        self.encode_thumb32_cmp_imm(rn, *imm as u32)
3348                    }
3349                } else if let Operand2::Reg(rm) = op2 {
3350                    let rm_bits = reg_to_bits(rm) as u16;
3351                    if rn_bits < 8 && rm_bits < 8 {
3352                        // CMP Rn, Rm (16-bit low): 0100 0010 10 Rm Rn
3353                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
3354                        Ok(instr.to_le_bytes().to_vec())
3355                    } else {
3356                        // CMP Rn, Rm (16-bit high): 0100 0101 N Rm Rn[2:0]
3357                        let n_bit = (rn_bits >> 3) & 1;
3358                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
3359                        Ok(instr.to_le_bytes().to_vec())
3360                    }
3361                } else {
3362                    let instr: u16 = 0xBF00;
3363                    Ok(instr.to_le_bytes().to_vec())
3364                }
3365            }
3366
3367            // CMN (Compare Negative) - computes Rn + op2 and sets flags
3368            // CMN Rn, #1 sets Z flag if Rn == -1 (since -1 + 1 = 0)
3369            ArmOp::Cmn { rn, op2 } => {
3370                let rn_bits = reg_to_bits(rn) as u16;
3371
3372                if let Operand2::Imm(imm) = op2 {
3373                    // CMN.W Rn, #imm (32-bit): i:imm3:imm8 is a ThumbExpandImm
3374                    // modified immediate (the field sits in imm3=hw2[14:12],
3375                    // imm8=hw2[7:0], i=hw1[10]). Encode it correctly, or error on
3376                    // an un-encodable value — replacing the old silent `0xBF00`
3377                    // NOP (the last of the silent-miscompile data-proc encoders).
3378                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3379                        synth_core::Error::synthesis(
3380                            "CMN immediate is not a valid ThumbExpandImm — materialize into a register",
3381                        )
3382                    })?;
3383                    let i_bit = (field >> 11) & 1;
3384                    let imm3 = (field >> 8) & 0x7;
3385                    let imm8 = field & 0xFF;
3386                    let hw1: u16 = (0xF110 | (i_bit << 10) as u16) | rn_bits;
3387                    let hw2: u16 = (imm3 << 12) as u16 | 0x0F00 | imm8 as u16;
3388                    let mut bytes = hw1.to_le_bytes().to_vec();
3389                    bytes.extend_from_slice(&hw2.to_le_bytes());
3390                    Ok(bytes)
3391                } else if let Operand2::Reg(rm) = op2 {
3392                    let rm_bits = reg_to_bits(rm) as u16;
3393                    // 16-bit CMN (T1) only encodes R0-R7; high registers overflow
3394                    // the 3-bit fields and corrupt the operands (#184, the #180
3395                    // class). CMN has no high-register 16-bit form, so fall back
3396                    // to 32-bit CMN.W (T2): EB10 Rn | 0F00 Rm (ADD.W with S=1 and
3397                    // Rd discarded as PC/1111).
3398                    if rn_bits < 8 && rm_bits < 8 {
3399                        // CMN Rn, Rm (16-bit): 0100 0010 11 Rm Rn
3400                        let instr: u16 = 0x42C0 | (rm_bits << 3) | rn_bits;
3401                        Ok(instr.to_le_bytes().to_vec())
3402                    } else {
3403                        let hw1: u16 = 0xEB10 | rn_bits;
3404                        let hw2: u16 = 0x0F00 | rm_bits;
3405                        let mut bytes = hw1.to_le_bytes().to_vec();
3406                        bytes.extend_from_slice(&hw2.to_le_bytes());
3407                        Ok(bytes)
3408                    }
3409                } else {
3410                    Ok(vec![0xBF, 0x00])
3411                }
3412            }
3413
3414            // LDR (can be 16-bit for simple cases)
3415            ArmOp::Ldr { rd, addr } => {
3416                let rd_bits = reg_to_bits(rd);
3417                let base_bits = reg_to_bits(&addr.base);
3418
3419                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3420                if let Some(offset_reg) = &addr.offset_reg {
3421                    let rm_bits = reg_to_bits(offset_reg);
3422
3423                    // If there's also an immediate offset, we need to ADD it first
3424                    if addr.offset != 0 {
3425                        // Use R12 (IP) as scratch to avoid clobbering the address register
3426                        // ADD R12, Rm, #offset; LDR Rd, [base, R12]
3427                        let scratch = Reg::R12;
3428                        let mut bytes =
3429                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3430                        bytes.extend(self.encode_thumb32_ldr_reg(rd, &addr.base, &scratch)?);
3431                        return Ok(bytes);
3432                    }
3433
3434                    // Simple register offset: LDR Rd, [Rn, Rm]
3435                    // 16-bit: only if Rd, Rn, Rm < R8
3436                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3437                        // LDR Rd, [Rn, Rm] (16-bit): 0101 100 Rm Rn Rd
3438                        let instr: u16 = 0x5800
3439                            | ((rm_bits as u16) << 6)
3440                            | ((base_bits as u16) << 3)
3441                            | (rd_bits as u16);
3442                        return Ok(instr.to_le_bytes().to_vec());
3443                    }
3444
3445                    // 32-bit register offset
3446                    return self.encode_thumb32_ldr_reg(rd, &addr.base, offset_reg);
3447                }
3448
3449                // Immediate offset mode [base, #imm]
3450                let offset = addr.offset as u32;
3451
3452                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3453                    // LDR Rd, [Rn, #imm5*4] (16-bit): 0110 1 imm5 Rn Rd
3454                    let imm5 = (offset >> 2) as u16;
3455                    let instr: u16 =
3456                        0x6800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3457                    Ok(instr.to_le_bytes().to_vec())
3458                } else {
3459                    self.encode_thumb32_ldr(rd, &addr.base, offset)
3460                }
3461            }
3462
3463            // STR (can be 16-bit for simple cases)
3464            ArmOp::Str { rd, addr } => {
3465                let rd_bits = reg_to_bits(rd);
3466                let base_bits = reg_to_bits(&addr.base);
3467
3468                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3469                if let Some(offset_reg) = &addr.offset_reg {
3470                    let rm_bits = reg_to_bits(offset_reg);
3471
3472                    // If there's also an immediate offset, we need to ADD it first
3473                    if addr.offset != 0 {
3474                        // Use R12 (IP) as scratch to avoid clobbering the address register
3475                        // ADD R12, Rm, #offset; STR Rd, [base, R12]
3476                        let scratch = Reg::R12;
3477                        let mut bytes =
3478                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3479                        bytes.extend(self.encode_thumb32_str_reg(rd, &addr.base, &scratch)?);
3480                        return Ok(bytes);
3481                    }
3482
3483                    // Simple register offset: STR Rd, [Rn, Rm]
3484                    // 16-bit: only if Rd, Rn, Rm < R8
3485                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3486                        // STR Rd, [Rn, Rm] (16-bit): 0101 000 Rm Rn Rd
3487                        let instr: u16 = 0x5000
3488                            | ((rm_bits as u16) << 6)
3489                            | ((base_bits as u16) << 3)
3490                            | (rd_bits as u16);
3491                        return Ok(instr.to_le_bytes().to_vec());
3492                    }
3493
3494                    // 32-bit register offset
3495                    return self.encode_thumb32_str_reg(rd, &addr.base, offset_reg);
3496                }
3497
3498                // Immediate offset mode [base, #imm]
3499                let offset = addr.offset as u32;
3500
3501                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3502                    // STR Rd, [Rn, #imm5*4] (16-bit): 0110 0 imm5 Rn Rd
3503                    let imm5 = (offset >> 2) as u16;
3504                    let instr: u16 =
3505                        0x6000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3506                    Ok(instr.to_le_bytes().to_vec())
3507                } else {
3508                    self.encode_thumb32_str(rd, &addr.base, offset)
3509                }
3510            }
3511
3512            // LDRB (Thumb-2)
3513            ArmOp::Ldrb { rd, addr } => {
3514                let rd_bits = reg_to_bits(rd);
3515                let base_bits = reg_to_bits(&addr.base);
3516
3517                if let Some(offset_reg) = &addr.offset_reg {
3518                    if addr.offset != 0 {
3519                        let scratch = Reg::R12;
3520                        let mut bytes =
3521                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3522                        bytes.extend(self.encode_thumb32_ldrb_reg(rd, &addr.base, &scratch)?);
3523                        return Ok(bytes);
3524                    }
3525                    return self.encode_thumb32_ldrb_reg(rd, &addr.base, offset_reg);
3526                }
3527
3528                let offset = addr.offset as u32;
3529                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3530                    // LDRB Rd, [Rn, #imm5] (16-bit): 0111 1 imm5 Rn Rd
3531                    let instr: u16 = 0x7800
3532                        | ((offset as u16) << 6)
3533                        | ((base_bits as u16) << 3)
3534                        | (rd_bits as u16);
3535                    Ok(instr.to_le_bytes().to_vec())
3536                } else {
3537                    self.encode_thumb32_ldrb_imm(rd, &addr.base, offset)
3538                }
3539            }
3540
3541            // LDRSB (Thumb-2)
3542            ArmOp::Ldrsb { rd, addr } => {
3543                let rd_bits = reg_to_bits(rd);
3544                let base_bits = reg_to_bits(&addr.base);
3545
3546                if let Some(offset_reg) = &addr.offset_reg {
3547                    if addr.offset != 0 {
3548                        let scratch = Reg::R12;
3549                        let mut bytes =
3550                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3551                        bytes.extend(self.encode_thumb32_ldrsb_reg(rd, &addr.base, &scratch)?);
3552                        return Ok(bytes);
3553                    }
3554                    return self.encode_thumb32_ldrsb_reg(rd, &addr.base, offset_reg);
3555                }
3556
3557                let offset = addr.offset as u32;
3558                // LDRSB has no 16-bit immediate form (only register)
3559                // For 16-bit reg form: only if Rd, Rn, Rm < R8
3560                if rd_bits < 8 && base_bits < 8 && offset == 0 {
3561                    // No immediate 16-bit encoding for LDRSB; use 32-bit
3562                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3563                } else {
3564                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3565                }
3566            }
3567
3568            // LDRH (Thumb-2)
3569            ArmOp::Ldrh { rd, addr } => {
3570                let rd_bits = reg_to_bits(rd);
3571                let base_bits = reg_to_bits(&addr.base);
3572
3573                if let Some(offset_reg) = &addr.offset_reg {
3574                    if addr.offset != 0 {
3575                        let scratch = Reg::R12;
3576                        let mut bytes =
3577                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3578                        bytes.extend(self.encode_thumb32_ldrh_reg(rd, &addr.base, &scratch)?);
3579                        return Ok(bytes);
3580                    }
3581                    return self.encode_thumb32_ldrh_reg(rd, &addr.base, offset_reg);
3582                }
3583
3584                let offset = addr.offset as u32;
3585                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3586                    // LDRH Rd, [Rn, #imm5*2] (16-bit): 1000 1 imm5 Rn Rd
3587                    let imm5 = (offset >> 1) as u16;
3588                    let instr: u16 =
3589                        0x8800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3590                    Ok(instr.to_le_bytes().to_vec())
3591                } else {
3592                    self.encode_thumb32_ldrh_imm(rd, &addr.base, offset)
3593                }
3594            }
3595
3596            // LDRSH (Thumb-2)
3597            ArmOp::Ldrsh { rd, addr } => {
3598                if let Some(offset_reg) = &addr.offset_reg {
3599                    if addr.offset != 0 {
3600                        let scratch = Reg::R12;
3601                        let mut bytes =
3602                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3603                        bytes.extend(self.encode_thumb32_ldrsh_reg(rd, &addr.base, &scratch)?);
3604                        return Ok(bytes);
3605                    }
3606                    return self.encode_thumb32_ldrsh_reg(rd, &addr.base, offset_reg);
3607                }
3608
3609                let offset = addr.offset as u32;
3610                self.encode_thumb32_ldrsh_imm(rd, &addr.base, offset)
3611            }
3612
3613            // STRB (Thumb-2)
3614            ArmOp::Strb { rd, addr } => {
3615                let rd_bits = reg_to_bits(rd);
3616                let base_bits = reg_to_bits(&addr.base);
3617
3618                if let Some(offset_reg) = &addr.offset_reg {
3619                    if addr.offset != 0 {
3620                        let scratch = Reg::R12;
3621                        let mut bytes =
3622                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3623                        bytes.extend(self.encode_thumb32_strb_reg(rd, &addr.base, &scratch)?);
3624                        return Ok(bytes);
3625                    }
3626                    return self.encode_thumb32_strb_reg(rd, &addr.base, offset_reg);
3627                }
3628
3629                let offset = addr.offset as u32;
3630                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3631                    // STRB Rd, [Rn, #imm5] (16-bit): 0111 0 imm5 Rn Rd
3632                    let instr: u16 = 0x7000
3633                        | ((offset as u16) << 6)
3634                        | ((base_bits as u16) << 3)
3635                        | (rd_bits as u16);
3636                    Ok(instr.to_le_bytes().to_vec())
3637                } else {
3638                    self.encode_thumb32_strb_imm(rd, &addr.base, offset)
3639                }
3640            }
3641
3642            // STRH (Thumb-2)
3643            ArmOp::Strh { rd, addr } => {
3644                let rd_bits = reg_to_bits(rd);
3645                let base_bits = reg_to_bits(&addr.base);
3646
3647                if let Some(offset_reg) = &addr.offset_reg {
3648                    if addr.offset != 0 {
3649                        let scratch = Reg::R12;
3650                        let mut bytes =
3651                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3652                        bytes.extend(self.encode_thumb32_strh_reg(rd, &addr.base, &scratch)?);
3653                        return Ok(bytes);
3654                    }
3655                    return self.encode_thumb32_strh_reg(rd, &addr.base, offset_reg);
3656                }
3657
3658                let offset = addr.offset as u32;
3659                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3660                    // STRH Rd, [Rn, #imm5*2] (16-bit): 1000 0 imm5 Rn Rd
3661                    let imm5 = (offset >> 1) as u16;
3662                    let instr: u16 =
3663                        0x8000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3664                    Ok(instr.to_le_bytes().to_vec())
3665                } else {
3666                    self.encode_thumb32_strh_imm(rd, &addr.base, offset)
3667                }
3668            }
3669
3670            // MemorySize (Thumb-2)
3671            ArmOp::MemorySize { rd } => {
3672                // LSR rd, R10, #16 — memory size in bytes / 65536 = pages
3673                // Thumb-2 16-bit: LSRS Rd, Rm, #imm5 — 0000 1 imm5 Rm Rd
3674                let rd_bits = reg_to_bits(rd);
3675                let r10_bits = reg_to_bits(&Reg::R10);
3676                if rd_bits < 8 && r10_bits < 8 {
3677                    let instr: u16 =
3678                        0x0800 | (16u16 << 6) | ((r10_bits as u16) << 3) | (rd_bits as u16);
3679                    Ok(instr.to_le_bytes().to_vec())
3680                } else {
3681                    // Thumb-2 32-bit LSR: 1110 1010 010 0 1111 | 0 imm3 Rd imm2 01 Rm
3682                    let imm5: u32 = 16;
3683                    let imm3 = (imm5 >> 2) & 0x7;
3684                    let imm2 = imm5 & 0x3;
3685                    let hw1: u16 = 0xEA4F;
3686                    let hw2: u16 =
3687                        ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | 0x10 | r10_bits) as u16;
3688                    let mut bytes = hw1.to_le_bytes().to_vec();
3689                    bytes.extend_from_slice(&hw2.to_le_bytes());
3690                    Ok(bytes)
3691                }
3692            }
3693
3694            // MemoryGrow (Thumb-2)
3695            ArmOp::MemoryGrow { rd, .. } => {
3696                // On embedded with fixed memory, always return -1 (failure)
3697                // MVN rd, #0 → MOV rd, #-1
3698                // Thumb-2 32-bit: MVN: 1111 0 i 0 0 0 1 1 0 1111 | 0 imm3 Rd imm8
3699                let rd_bits = reg_to_bits(rd);
3700                let hw1: u16 = 0xF06F; // MVN with i=0
3701                let hw2: u16 = (rd_bits << 8) as u16; // imm8=0 → ~0 = 0xFFFFFFFF = -1
3702                let mut bytes = hw1.to_le_bytes().to_vec();
3703                bytes.extend_from_slice(&hw2.to_le_bytes());
3704                Ok(bytes)
3705            }
3706
3707            // BX (16-bit)
3708            ArmOp::Bx { rm } => {
3709                let rm_bits = reg_to_bits(rm) as u16;
3710                // BX Rm (16-bit): 0100 0111 0 Rm 000
3711                let instr: u16 = 0x4700 | (rm_bits << 3);
3712                Ok(instr.to_le_bytes().to_vec())
3713            }
3714
3715            // BLX (16-bit) - Branch with Link and Exchange
3716            // BLX Rm: 0100 0111 1 Rm 000
3717            ArmOp::Blx { rm } => {
3718                let rm_bits = reg_to_bits(rm) as u16;
3719                let instr: u16 = 0x4780 | (rm_bits << 3);
3720                Ok(instr.to_le_bytes().to_vec())
3721            }
3722
3723            // CallIndirect - indirect function call via table lookup
3724            // table_index_reg contains the table index
3725            // Generates (#642): MOVW ip,#size [; MOVT]; CMP idx,ip; BLO +1;
3726            //                   UDF #0; LSL R12,idx,#2; LDR R12,[R11,R12]; BLX R12
3727            // #650, table_byte_offset != 0 (a non-zero table in the contiguous
3728            // R11 region): the pointer load becomes
3729            //                   ADD R12,R11,R12; LDR R12,[R12,#offset]
3730            // #664, null_check (the table has null slots, linked as ZERO
3731            // words): the loaded pointer is null-checked before the BLX —
3732            //                   CMP.W R12,#0; BNE +1; UDF #0
3733            ArmOp::CallIndirect {
3734                rd: _,
3735                type_idx: _,
3736                table_index_reg,
3737                table_size,
3738                table_byte_offset,
3739                null_check,
3740            } => {
3741                let idx_reg = reg_to_bits(table_index_reg);
3742                let mut bytes = Vec::new();
3743
3744                // The expansion:
3745                // 1. Bounds guard (#642): trap (UDF #0, WASM Core §4.4.8) when
3746                //    index >= table size. Without it an out-of-bounds index
3747                //    reads past the table and BLXes whatever word lies there —
3748                //    an uncontrolled indirect branch instead of a trap.
3749                // 2. Multiplies index by 4 (function pointer size)
3750                // 3. Loads function pointer from table (table base in R11)
3751                // 4. Calls the function via BLX
3752                //
3753                // Table base setup must be done by caller/runtime. The type
3754                // check §4.4.8 also requires is discharged at COMPILE time:
3755                // the selector only emits this op after verifying the closed-
3756                // world property that every table entry's signature equals the
3757                // expected type (the raw code-pointer table carries no runtime
3758                // type ids to compare) — see the #642 selector guard.
3759
3760                // MOVW R12, #(size & 0xFFFF) — Thumb-2 T3:
3761                // 11110 i 100100 imm4 | 0 imm3 Rd imm8 (Rd=R12).
3762                let size_lo = *table_size & 0xFFFF;
3763                let hw1: u16 =
3764                    (0xF240 | (((size_lo >> 11) & 1) << 10) | ((size_lo >> 12) & 0xF)) as u16;
3765                let hw2: u16 =
3766                    ((((size_lo >> 8) & 0x7) << 12) | (12 << 8) | (size_lo & 0xFF)) as u16;
3767                bytes.extend_from_slice(&hw1.to_le_bytes());
3768                bytes.extend_from_slice(&hw2.to_le_bytes());
3769                // MOVT R12, #(size >> 16) — only when the table size exceeds
3770                // 16 bits (never in practice, but the guard must not compare
3771                // against a truncated size).
3772                let size_hi = *table_size >> 16;
3773                if size_hi != 0 {
3774                    let hw1: u16 =
3775                        (0xF2C0 | (((size_hi >> 11) & 1) << 10) | ((size_hi >> 12) & 0xF)) as u16;
3776                    let hw2: u16 =
3777                        ((((size_hi >> 8) & 0x7) << 12) | (12 << 8) | (size_hi & 0xFF)) as u16;
3778                    bytes.extend_from_slice(&hw1.to_le_bytes());
3779                    bytes.extend_from_slice(&hw2.to_le_bytes());
3780                }
3781                // CMP idx, R12 — 16-bit T2 (high-register capable):
3782                // 010001 01 N Rm(4) Rn(3), Rn full = N:Rn3.
3783                let cmp: u16 = (0x4500 | ((idx_reg & 8) << 4) | (12 << 3) | (idx_reg & 7)) as u16;
3784                bytes.extend_from_slice(&cmp.to_le_bytes());
3785                // BLO +1 insn (skip the UDF when index < size) — B<cond>.N
3786                // imm8=0: target = branch + 4. LO = unsigned lower.
3787                bytes.extend_from_slice(&0xD300u16.to_le_bytes());
3788                // UDF #0 — call_indirect out-of-bounds trap (same trap idiom as
3789                // the div-by-zero guards).
3790                bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3791
3792                // LSL R12, idx_reg, #2 (multiply index by 4)
3793                // Thumb-2 MOV with shift: 11101010 010 S 1111 | 0 imm3 Rd imm2 type Rm
3794                // LSL: type=00 (bits 5:4), imm5=2 -> imm3=000, imm2=10 (bits 7:6)
3795                // #597: the shift amount was previously shifted into bits 5:4 —
3796                // the TYPE field — encoding `mov.w ip, rm, ASR #32`, which
3797                // destroyed the index and dispatched table entry 0 for every
3798                // call. imm2 lives at bits 7:6.
3799                let hw1: u16 = 0xEA4F_u16; // MOV.W R12, Rm, LSL #2
3800                let hw2: u16 = ((0x0C00 | (0b10 << 6)) | idx_reg) as u16;
3801                bytes.extend_from_slice(&hw1.to_le_bytes());
3802                bytes.extend_from_slice(&hw2.to_le_bytes());
3803
3804                if *table_byte_offset == 0 {
3805                    // Table 0 (base = R11 itself): the pre-#650 single-load
3806                    // form — a single-table module's bytes stay identical BY
3807                    // CONSTRUCTION.
3808                    // LDR R12, [R11, R12] - load function pointer
3809                    // Thumb-2 LDR (register): 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
3810                    // Rn=R11, Rt=R12, Rm=R12, imm2=00 (no shift)
3811                    let ldr_hw1: u16 = 0xF85B; // LDR.W Rt, [R11, Rm]
3812                    let ldr_hw2: u16 = 0xC00C; // Rt=R12, imm2=00, Rm=R12
3813                    bytes.extend_from_slice(&ldr_hw1.to_le_bytes());
3814                    bytes.extend_from_slice(&ldr_hw2.to_le_bytes());
3815                } else {
3816                    // #650: table N of the contiguous R11 region — fold the
3817                    // compile-time base offset into the pointer load via the
3818                    // LDR imm12 form (R12 stays the only scratch, per the
3819                    // #212 convention).
3820                    assert!(
3821                        *table_byte_offset <= 4095,
3822                        "call_indirect table base offset {table_byte_offset} exceeds \
3823                         LDR imm12 — the selector must have declined this (#650)"
3824                    );
3825                    // ADD.W R12, R11, R12 — T3 ADD (register):
3826                    // 11101011000 S=0 Rn=1011 | 0 imm3=000 Rd=1100 imm2=00 type=00 Rm=1100
3827                    bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
3828                    bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
3829                    // LDR.W R12, [R12, #offset] — T3 LDR (immediate):
3830                    // 1111 1000 1101 Rn=1100 | Rt=1100 imm12
3831                    bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
3832                    bytes.extend_from_slice(
3833                        &((0xC000u16) | (*table_byte_offset as u16 & 0x0FFF)).to_le_bytes(),
3834                    );
3835                }
3836
3837                // #664: null-slot trap — ONLY when the table image carries
3838                // null (uninitialized) slots, which the layout contract
3839                // requires to be linked as ZERO words. A fully-initialized
3840                // table skips this branch entirely, keeping the pre-#664
3841                // expansion byte-identical BY CONSTRUCTION (the #650
3842                // offset-0 trick).
3843                if *null_check {
3844                    // CMP.W R12, #0 — T2 CMP (immediate): 11110 i 0 1101 1
3845                    // Rn(4) | 0 imm3 1111 imm8, Rn=R12, imm=0.
3846                    bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
3847                    bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
3848                    // BNE +1 insn (skip the UDF when the pointer is non-null)
3849                    // — B<cond>.N imm8=0: target = branch + 4. NE.
3850                    bytes.extend_from_slice(&0xD100u16.to_le_bytes());
3851                    // UDF #0 — call_indirect null-funcref trap (WASM Core
3852                    // §4.4.8: calling an uninitialized element traps; same
3853                    // trap idiom as the bounds guard above).
3854                    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3855                }
3856
3857                // BLX R12 (call function indirectly)
3858                // BLX Rm (16-bit): 0100 0111 1 Rm 000
3859                let blx: u16 = 0x47E0; // BLX R12
3860                bytes.extend_from_slice(&blx.to_le_bytes());
3861
3862                Ok(bytes)
3863            }
3864
3865            // Label pseudo-instruction: emits no machine code
3866            ArmOp::Label { .. } => Ok(Vec::new()),
3867
3868            // Conditional branch to label (generic) - offset 0, will be patched
3869            ArmOp::Bcc { cond, label: _ } => {
3870                use synth_synthesis::Condition;
3871                let cond_bits: u16 = match cond {
3872                    Condition::EQ => 0x0,
3873                    Condition::NE => 0x1,
3874                    Condition::HS => 0x2,
3875                    Condition::LO => 0x3,
3876                    Condition::HI => 0x8,
3877                    Condition::LS => 0x9,
3878                    Condition::GE => 0xA,
3879                    Condition::LT => 0xB,
3880                    Condition::GT => 0xC,
3881                    Condition::LE => 0xD,
3882                };
3883                // 16-bit B<cond> with offset 0: 1101 cond imm8
3884                let instr: u16 = 0xD000 | (cond_bits << 8);
3885                Ok(instr.to_le_bytes().to_vec())
3886            }
3887
3888            // Branch instructions
3889            ArmOp::B { label: _ } => {
3890                // Simplified: B.N with offset 0
3891                // For real usage, would need label resolution
3892                let instr: u16 = 0xE000; // B.N #0
3893                Ok(instr.to_le_bytes().to_vec())
3894            }
3895
3896            // BHS (Branch if Higher or Same) - used for bounds checking
3897            // Condition code: 0x2 (C set)
3898            ArmOp::Bhs { label: _ } => {
3899                // 16-bit B<cond> with offset 0: 1101 cond imm8
3900                // cond = 0x2 (HS)
3901                let instr: u16 = 0xD200; // BHS.N #0
3902                Ok(instr.to_le_bytes().to_vec())
3903            }
3904
3905            // BLO (Branch if Lower) - complementary to BHS
3906            // Condition code: 0x3 (C clear)
3907            ArmOp::Blo { label: _ } => {
3908                // 16-bit B<cond> with offset 0: 1101 cond imm8
3909                // cond = 0x3 (LO)
3910                let instr: u16 = 0xD300; // BLO.N #0
3911                Ok(instr.to_le_bytes().to_vec())
3912            }
3913
3914            // Branch with numeric offset (Thumb-2)
3915            // Thumb-2 B.W instruction: 32-bit with +-16MB range
3916            ArmOp::BOffset { offset } => {
3917                // offset is already the halfword displacement: (target - branch - 4) / 2
3918                // This is the raw encoded value, accounting for variable-length instructions
3919                let halfword_offset = *offset;
3920
3921                // 16-bit B.N encoding: 1110 0 imm11 (11-bit signed halfword offset)
3922                // Range: -1024 to +1022 halfwords
3923                if (-1024..=1022).contains(&halfword_offset) {
3924                    // 16-bit B.N encoding: 1110 0 imm11
3925                    let imm11 = (halfword_offset as u16) & 0x7FF;
3926                    let instr: u16 = 0xE000 | imm11;
3927                    Ok(instr.to_le_bytes().to_vec())
3928                } else {
3929                    // 32-bit B.W encoding for larger offsets
3930                    // First halfword: 1111 0 S imm10
3931                    // Second halfword: 10 J1 0 J2 imm11
3932                    // Total offset = SignExtend(S:I1:I2:imm10:imm11:0)
3933                    // where I1 = NOT(J1 XOR S), I2 = NOT(J2 XOR S)
3934
3935                    // The B.W (T4) encoding packs the signed offset as:
3936                    //   S:I1:I2:imm10:imm11:0  (25-bit signed, halfword-aligned)
3937                    // where J1 = NOT(I1 XOR S), J2 = NOT(I2 XOR S)
3938                    // Input halfword_offset already equals (target - PC - 4) / 2,
3939                    // so the full byte offset = halfword_offset << 1.
3940                    // The encoding fields split that 25-bit signed value (including the
3941                    // implicit trailing zero) as: S | imm10 | imm11
3942                    // with I1 = bit 23 and I2 = bit 22 of the signed offset.
3943                    let signed_offset = halfword_offset << 1; // byte offset
3944                    let s = if signed_offset < 0 { 1u32 } else { 0u32 };
3945                    let uoffset = signed_offset as u32;
3946                    let imm10 = (uoffset >> 12) & 0x3FF; // bits [21:12]
3947                    let imm11 = (uoffset >> 1) & 0x7FF; // bits [11:1]
3948                    let i1 = (uoffset >> 23) & 1; // bit 23
3949                    let i2 = (uoffset >> 22) & 1; // bit 22
3950                    let j1 = (!(i1 ^ s)) & 1; // J1 = NOT(I1 XOR S)
3951                    let j2 = (!(i2 ^ s)) & 1; // J2 = NOT(I2 XOR S)
3952
3953                    let hw1: u16 = (0xF000 | (s << 10) | imm10) as u16;
3954                    let hw2: u16 = (0x9000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
3955
3956                    let mut bytes = hw1.to_le_bytes().to_vec();
3957                    bytes.extend_from_slice(&hw2.to_le_bytes());
3958                    Ok(bytes)
3959                }
3960            }
3961
3962            // Conditional branch with numeric offset (Thumb-2)
3963            ArmOp::BCondOffset { cond, offset } => {
3964                use synth_synthesis::Condition;
3965                let cond_bits: u16 = match cond {
3966                    Condition::EQ => 0x0,
3967                    Condition::NE => 0x1,
3968                    Condition::HS => 0x2,
3969                    Condition::LO => 0x3,
3970                    Condition::HI => 0x8,
3971                    Condition::LS => 0x9,
3972                    Condition::GE => 0xA,
3973                    Condition::LT => 0xB,
3974                    Condition::GT => 0xC,
3975                    Condition::LE => 0xD,
3976                };
3977
3978                // offset is already the halfword displacement: (target - branch - 4) / 2
3979                // This is the raw imm8 value for 16-bit B<cond> encoding
3980                let halfword_offset = *offset;
3981
3982                // 16-bit B<cond> encoding: 1101 cond imm8
3983                // Range: -256 to +254 halfwords (imm8 is sign-extended and shifted left 1)
3984                if (-128..=127).contains(&halfword_offset) {
3985                    let imm8 = (halfword_offset as u16) & 0xFF;
3986                    let instr: u16 = 0xD000 | (cond_bits << 8) | imm8;
3987                    Ok(instr.to_le_bytes().to_vec())
3988                } else {
3989                    // 32-bit B<cond>.W for larger offsets
3990                    // First halfword: 1111 0 S cond imm6
3991                    // Second halfword: 10 J1 0 J2 imm11
3992                    let offset = halfword_offset >> 1;
3993                    let s = if offset < 0 { 1u32 } else { 0u32 };
3994                    let imm6 = ((offset >> 11) as u32) & 0x3F;
3995                    let imm11 = (offset as u32) & 0x7FF;
3996                    let j1 = if s == 1 { 1 } else { 0 };
3997                    let j2 = if s == 1 { 1 } else { 0 };
3998
3999                    let hw1: u16 = (0xF000 | (s << 10) | ((cond_bits as u32) << 6) | imm6) as u16;
4000                    let hw2: u16 = (0x8000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4001
4002                    let mut bytes = hw1.to_le_bytes().to_vec();
4003                    bytes.extend_from_slice(&hw2.to_le_bytes());
4004                    Ok(bytes)
4005                }
4006            }
4007
4008            ArmOp::Bl { label: _ } => {
4009                // BL is always 32-bit in Thumb-2, encoded here as a relocatable
4010                // placeholder; an R_ARM_THM_CALL relocation patches the target
4011                // (see arm_backend.rs). The placeholder must carry an embedded
4012                // addend of -4 so the relocation nets to exactly the symbol S.
4013                //
4014                // Thumb BL computes `target = (P + 4) + signed_offset`. Under
4015                // R_ARM_THM_CALL the linker resolves using the in-place addend;
4016                // a 0xF800 placeholder (addend 0) lands at S+4 — every call one
4017                // instruction past the callee entry (#174). The correct
4018                // placeholder is what `gas` emits for `bl <extern>`:
4019                //   f7ff fffe  ->  `bl <self>`  (S=1, J1=J2=1, imm = -4 addend),
4020                // i.e. hw1=0xF7FF, hw2=0xFFFE. This nets to S, not S+4.
4021                // (The earlier 0xD000 was worse still — a ~+0x600000 addend,
4022                // the garbage `bl c0000c` and "truncated to fit" of #167.)
4023                let hw1: u16 = 0xF7FF;
4024                let hw2: u16 = 0xFFFE;
4025                let mut bytes = hw1.to_le_bytes().to_vec();
4026                bytes.extend_from_slice(&hw2.to_le_bytes());
4027                Ok(bytes)
4028            }
4029
4030            // MVN
4031            ArmOp::Mvn { rd, op2 } => {
4032                if let Operand2::Reg(rm) = op2 {
4033                    let rd_bits = reg_to_bits(rd) as u16;
4034                    let rm_bits = reg_to_bits(rm) as u16;
4035
4036                    if rd_bits < 8 && rm_bits < 8 {
4037                        // MVNS Rd, Rm (16-bit): 0100 0011 11 Rm Rd
4038                        let instr: u16 = 0x43C0 | (rm_bits << 3) | rd_bits;
4039                        Ok(instr.to_le_bytes().to_vec())
4040                    } else {
4041                        // 32-bit MVN
4042                        let hw1: u16 = 0xEA6F_u16;
4043                        let hw2: u16 = ((reg_to_bits(rd) << 8) | reg_to_bits(rm)) as u16;
4044                        let mut bytes = hw1.to_le_bytes().to_vec();
4045                        bytes.extend_from_slice(&hw2.to_le_bytes());
4046                        Ok(bytes)
4047                    }
4048                } else {
4049                    let instr: u16 = 0xBF00;
4050                    Ok(instr.to_le_bytes().to_vec())
4051                }
4052            }
4053
4054            // MOVW - Move Wide (Thumb-2 32-bit)
4055            ArmOp::Movw { rd, imm16 } => {
4056                self.encode_thumb32_movw_raw(reg_to_bits(rd), *imm16 as u32)
4057            }
4058
4059            // MOVT - Move Top (Thumb-2 32-bit)
4060            ArmOp::Movt { rd, imm16 } => {
4061                self.encode_thumb32_movt_raw(reg_to_bits(rd), *imm16 as u32)
4062            }
4063
4064            // #237: symbol-relative MOVW/MOVT. Encode the addend's low/high 16
4065            // bits in place; the backend records an R_ARM_MOVW_ABS_NC /
4066            // R_ARM_MOVT_ABS relocation against `symbol`, so the linker adds the
4067            // symbol's final address to the in-place addend (REL semantics).
4068            ArmOp::MovwSym { rd, addend, .. } => {
4069                self.encode_thumb32_movw_raw(reg_to_bits(rd), (*addend as u32) & 0xffff)
4070            }
4071            ArmOp::MovtSym { rd, addend, .. } => {
4072                self.encode_thumb32_movt_raw(reg_to_bits(rd), ((*addend as u32) >> 16) & 0xffff)
4073            }
4074
4075            // #345: literal-pool address load — emit a PLACEHOLDER `LDR.W rd,
4076            // [pc, #0]` (U=1, imm12=0). The backend (arm_backend.rs) places the
4077            // 4-byte pool word at the end of the function, records the R_ARM_ABS32
4078            // relocation against `symbol+addend`, and patches the imm12 with the
4079            // real PC-relative distance once the pool offset is known.
4080            // Encoding T2: 1111 1000 1101 1111 | Rt(4) imm12(12), with the literal
4081            // base = Align(PC,4) and PC = address of this instruction + 4.
4082            ArmOp::LdrSym { rd, .. } => {
4083                let rt = reg_to_bits(rd) as u16;
4084                let hw1: u16 = 0xF8DF; // LDR.W (literal), U=1
4085                let hw2: u16 = rt << 12; // imm12 = 0 placeholder
4086                let mut bytes = Vec::with_capacity(4);
4087                bytes.extend_from_slice(&hw1.to_le_bytes());
4088                bytes.extend_from_slice(&hw2.to_le_bytes());
4089                Ok(bytes)
4090            }
4091
4092            // SetCond: Materialize condition flag into register (0 or 1)
4093            // Strategy: ITE <cond>; MOV Rd, #1; MOV Rd, #0
4094            // IMPORTANT: Must use ITE (If-Then-Else) because 16-bit Thumb MOV
4095            // always sets flags (MOVS). We need to evaluate the condition BEFORE
4096            // any MOV instruction clobbers the flags from CMP.
4097            ArmOp::SetCond { rd, cond } => {
4098                let rd_bits = reg_to_bits(rd) as u16;
4099
4100                // Condition code encoding for IT block
4101                use synth_synthesis::Condition;
4102                let cond_bits: u16 = match cond {
4103                    Condition::EQ => 0x0,
4104                    Condition::NE => 0x1,
4105                    Condition::LT => 0xB,
4106                    Condition::LE => 0xD,
4107                    Condition::GT => 0xC,
4108                    Condition::GE => 0xA,
4109                    Condition::LO => 0x3, // CC/LO (unsigned <)
4110                    Condition::LS => 0x9, // LS (unsigned <=)
4111                    Condition::HI => 0x8, // HI (unsigned >)
4112                    Condition::HS => 0x2, // CS/HS (unsigned >=)
4113                };
4114
4115                // ITE <cond>: encodes If-Then-Else block
4116                // The mask field depends on firstcond[0]:
4117                // - If firstcond[0] = 0: mask = 0xC for TE pattern (ITE EQ = BF0C)
4118                // - If firstcond[0] = 1: mask = 0x4 for TE pattern (ITE NE = BF14)
4119                let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4120                let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4121
4122                // Materialize 0/1 into Rd. The 16-bit MOVS (T1) encodes Rd in a
4123                // 3-bit field (bits[10:8]) — only R0–R7. For a high register
4124                // (R8–R12) `rd_bits << 8` overflows into bit 11 and silently
4125                // turns MOVS into CMP (00100 → 00101), corrupting the result
4126                // (this mis-materialized gale's `has_waiter`, so its `local.set`
4127                // stored a stale register → the binary-sem WAKE dispatch read
4128                // garbage). Use the 32-bit MOV.W (T2) for high registers, which
4129                // has a 4-bit Rd field. MOV.W with S=0 doesn't set flags, which
4130                // is fine inside the ITE (the materialized value is the result;
4131                // the flags are not consumed afterwards).
4132                let mut bytes = ite_instr.to_le_bytes().to_vec();
4133                let push_mov = |bytes: &mut Vec<u8>, imm: u16| {
4134                    if rd_bits <= 7 {
4135                        let m: u16 = 0x2000 | (rd_bits << 8) | imm; // 16-bit MOVS Rd,#imm
4136                        bytes.extend_from_slice(&m.to_le_bytes());
4137                    } else {
4138                        // 32-bit MOV.W Rd, #imm (T2): F04F | (Rd<<8) | imm8
4139                        let hw1: u16 = 0xF04F;
4140                        let hw2: u16 = (rd_bits << 8) | imm;
4141                        bytes.extend_from_slice(&hw1.to_le_bytes());
4142                        bytes.extend_from_slice(&hw2.to_le_bytes());
4143                    }
4144                };
4145                push_mov(&mut bytes, 1); // Then branch (condition true)  → 1
4146                push_mov(&mut bytes, 0); // Else branch (condition false) → 0
4147                Ok(bytes)
4148            }
4149
4150            // I64SetCond: Compare two i64 register pairs, result 0/1 in rd
4151            // EQ/NE: CMP lo,lo; IT EQ; CMPEQ hi,hi; ITE <cond>; MOV 1; MOV 0
4152            // LT: CMP lo,lo; SBCS rd,hi,hi; ITE LT; MOV 1; MOV 0
4153            // GT: CMP lo,lo (swapped); SBCS rd,hi,hi (swapped); ITE LT; MOV 1; MOV 0
4154            ArmOp::I64SetCond {
4155                rd,
4156                rn_lo,
4157                rn_hi,
4158                rm_lo,
4159                rm_hi,
4160                cond,
4161            } => {
4162                use synth_synthesis::Condition;
4163                let rd_bits = reg_to_bits(rd) as u16;
4164                let mut bytes = Vec::new();
4165
4166                // Helper: encode CMP Rn, Rm (16-bit)
4167                let encode_cmp_reg = |rn: &synth_synthesis::Reg,
4168                                      rm: &synth_synthesis::Reg|
4169                 -> Vec<u8> {
4170                    let rn_bits = reg_to_bits(rn) as u16;
4171                    let rm_bits = reg_to_bits(rm) as u16;
4172                    if rn_bits < 8 && rm_bits < 8 {
4173                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
4174                        instr.to_le_bytes().to_vec()
4175                    } else {
4176                        let n_bit = (rn_bits >> 3) & 1;
4177                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
4178                        instr.to_le_bytes().to_vec()
4179                    }
4180                };
4181
4182                // Helper: encode ITE <cond> (2 bytes)
4183                let encode_ite = |cond_bits: u16| -> Vec<u8> {
4184                    let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4185                    let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4186                    ite_instr.to_le_bytes().to_vec()
4187                };
4188
4189                // Helper: encode SetCond (ITE + MOV #1 + MOV #0) for given condition
4190                let encode_setcond = |cond_bits: u16, rd_bits: u16| -> Vec<u8> {
4191                    let mut b = encode_ite(cond_bits);
4192                    if rd_bits < 8 {
4193                        let mov_one: u16 = 0x2001 | (rd_bits << 8);
4194                        let mov_zero: u16 = 0x2000 | (rd_bits << 8);
4195                        b.extend_from_slice(&mov_one.to_le_bytes());
4196                        b.extend_from_slice(&mov_zero.to_le_bytes());
4197                    } else {
4198                        // #311: rd >= R8 — the 16-bit MOV imm8 form has a 3-bit
4199                        // rd field; rd_bits<<8 overflows into bit 11 and
4200                        // TRANSMUTES the MOV into CMP (0x2001|0x0800 = 0x2801 =
4201                        // CMP r0,#1): the boolean dies in the flags and the
4202                        // consumer reads a stale register. Use the 32-bit
4203                        // MOV.W (T2: F04F 0000|rd<<8|imm8) — IT-legal,
4204                        // flag-preserving. Same class as H-CODE-9 / #180.
4205                        for imm in [1u16, 0u16] {
4206                            let hw1: u16 = 0xF04F;
4207                            let hw2: u16 = (rd_bits << 8) | imm;
4208                            b.extend_from_slice(&hw1.to_le_bytes());
4209                            b.extend_from_slice(&hw2.to_le_bytes());
4210                        }
4211                    }
4212                    b
4213                };
4214
4215                match cond {
4216                    Condition::EQ | Condition::NE => {
4217                        // CMP rn_lo, rm_lo (compare low words)
4218                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4219
4220                        // IT EQ (execute next instruction only if Z=1)
4221                        let it_eq: u16 = 0xBF08; // IT EQ: cond=0000, mask=1000
4222                        bytes.extend_from_slice(&it_eq.to_le_bytes());
4223
4224                        // CMPEQ rn_hi, rm_hi (compare high words, only if low equal)
4225                        bytes.extend_from_slice(&encode_cmp_reg(rn_hi, rm_hi));
4226
4227                        // ITE <cond>; MOV rd, #1; MOV rd, #0
4228                        let cond_bits: u16 = match cond {
4229                            Condition::EQ => 0x0,
4230                            Condition::NE => 0x1,
4231                            _ => unreachable!(),
4232                        };
4233                        bytes.extend_from_slice(&encode_setcond(cond_bits, rd_bits));
4234                    }
4235
4236                    Condition::LT => {
4237                        // CMP rn_lo, rm_lo (sets C flag for borrow)
4238                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4239
4240                        // SBCS rd, rn_hi, rm_hi (subtract with carry, sets N,V flags)
4241                        // SBCS.W Rd, Rn, Rm: EB70 Rn | 0000 Rd 0000 Rm
4242                        let rn_hi_bits = reg_to_bits(rn_hi);
4243                        let rm_hi_bits = reg_to_bits(rm_hi);
4244                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4245                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4246                        bytes.extend_from_slice(&hw1.to_le_bytes());
4247                        bytes.extend_from_slice(&hw2.to_le_bytes());
4248
4249                        // ITE LT; MOV rd, #1; MOV rd, #0
4250                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4251                    }
4252
4253                    Condition::GT => {
4254                        // GT(a,b) = LT(b,a): swap operands
4255                        // CMP rm_lo, rn_lo (swapped)
4256                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4257
4258                        // SBCS rd, rm_hi, rn_hi (swapped)
4259                        let rm_hi_bits = reg_to_bits(rm_hi);
4260                        let rn_hi_bits = reg_to_bits(rn_hi);
4261                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4262                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4263                        bytes.extend_from_slice(&hw1.to_le_bytes());
4264                        bytes.extend_from_slice(&hw2.to_le_bytes());
4265
4266                        // ITE LT; MOV rd, #1; MOV rd, #0
4267                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4268                    }
4269
4270                    Condition::LE => {
4271                        // LE(a,b) = !GT(a,b): use GT logic but invert result
4272                        // GT(a,b) = LT(b,a): so we do CMP(b,a) and check LT, then invert
4273                        // CMP rm_lo, rn_lo (swapped, same as GT)
4274                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4275
4276                        // SBCS rd, rm_hi, rn_hi (swapped)
4277                        let rm_hi_bits = reg_to_bits(rm_hi);
4278                        let rn_hi_bits = reg_to_bits(rn_hi);
4279                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4280                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4281                        bytes.extend_from_slice(&hw1.to_le_bytes());
4282                        bytes.extend_from_slice(&hw2.to_le_bytes());
4283
4284                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT, so inverting GT result)
4285                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4286                    }
4287
4288                    Condition::GE => {
4289                        // GE(a,b) = !LT(a,b): use LT logic but invert result
4290                        // CMP rn_lo, rm_lo (same as LT)
4291                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4292
4293                        // SBCS rd, rn_hi, rm_hi (same as LT)
4294                        let rn_hi_bits = reg_to_bits(rn_hi);
4295                        let rm_hi_bits = reg_to_bits(rm_hi);
4296                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4297                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4298                        bytes.extend_from_slice(&hw1.to_le_bytes());
4299                        bytes.extend_from_slice(&hw2.to_le_bytes());
4300
4301                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT)
4302                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4303                    }
4304
4305                    // Unsigned comparisons - same instruction sequence, different conditions
4306                    Condition::LO => {
4307                        // LO (unsigned LT): CMP lo, SBCS hi, check C=0
4308                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4309                        let rn_hi_bits = reg_to_bits(rn_hi);
4310                        let rm_hi_bits = reg_to_bits(rm_hi);
4311                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4312                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4313                        bytes.extend_from_slice(&hw1.to_le_bytes());
4314                        bytes.extend_from_slice(&hw2.to_le_bytes());
4315                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4316                    }
4317
4318                    Condition::HI => {
4319                        // HI (unsigned GT): swap operands and check LO
4320                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4321                        let rm_hi_bits = reg_to_bits(rm_hi);
4322                        let rn_hi_bits = reg_to_bits(rn_hi);
4323                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4324                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4325                        bytes.extend_from_slice(&hw1.to_le_bytes());
4326                        bytes.extend_from_slice(&hw2.to_le_bytes());
4327                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4328                    }
4329
4330                    Condition::LS => {
4331                        // LS (unsigned LE): !(a > b) = !(HI), so do HI and invert
4332                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4333                        let rm_hi_bits = reg_to_bits(rm_hi);
4334                        let rn_hi_bits = reg_to_bits(rn_hi);
4335                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4336                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4337                        bytes.extend_from_slice(&hw1.to_le_bytes());
4338                        bytes.extend_from_slice(&hw2.to_le_bytes());
4339                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4340                    }
4341
4342                    Condition::HS => {
4343                        // HS (unsigned GE): !(a < b) = !(LO)
4344                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4345                        let rn_hi_bits = reg_to_bits(rn_hi);
4346                        let rm_hi_bits = reg_to_bits(rm_hi);
4347                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4348                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4349                        bytes.extend_from_slice(&hw1.to_le_bytes());
4350                        bytes.extend_from_slice(&hw2.to_le_bytes());
4351                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4352                    }
4353                }
4354
4355                Ok(bytes)
4356            }
4357
4358            // I64SetCondZ: Test if i64 register pair is zero, result 0/1 in rd
4359            // ORR.W rd, rn_lo, rn_hi; CMP rd, #0; ITE EQ; MOV 1; MOV 0
4360            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
4361                let rd_bits = reg_to_bits(rd);
4362                let rn_lo_bits = reg_to_bits(rn_lo);
4363                let rn_hi_bits = reg_to_bits(rn_hi);
4364                let mut bytes = Vec::new();
4365
4366                // ORR.W rd, rn_lo, rn_hi: EA40 rn_lo | 0000 rd 0000 rn_hi
4367                let hw1: u16 = (0xEA40 | rn_lo_bits) as u16;
4368                let hw2: u16 = ((rd_bits << 8) | rn_hi_bits) as u16;
4369                bytes.extend_from_slice(&hw1.to_le_bytes());
4370                bytes.extend_from_slice(&hw2.to_le_bytes());
4371
4372                // CMP rd, #0 — 16-bit form only for r0-r7 (3-bit rd field);
4373                // high registers take CMP.W (T2: F1B0|rn 0F00|imm8). This was
4374                // H-CODE-9: rd_bits<<8 overflowing the field compared the
4375                // WRONG register. Same hardening as the #311 SetCond fix.
4376                if rd_bits < 8 {
4377                    let cmp_instr: u16 = 0x2800 | ((rd_bits as u16) << 8);
4378                    bytes.extend_from_slice(&cmp_instr.to_le_bytes());
4379                } else {
4380                    let hw1: u16 = 0xF1B0 | (rd_bits as u16);
4381                    let hw2: u16 = 0x0F00;
4382                    bytes.extend_from_slice(&hw1.to_le_bytes());
4383                    bytes.extend_from_slice(&hw2.to_le_bytes());
4384                }
4385
4386                // ITE EQ; MOV rd, #1; MOV rd, #0 (32-bit MOV.W for rd >= R8,
4387                // #311 — see I64SetCond)
4388                let mask = 0xC_u16; // ITE EQ mask: firstcond[0]=0, mask=0xC
4389                let ite_instr: u16 = 0xBF00 | mask;
4390                bytes.extend_from_slice(&ite_instr.to_le_bytes());
4391                if rd_bits < 8 {
4392                    let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
4393                    let mov_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
4394                    bytes.extend_from_slice(&mov_one.to_le_bytes());
4395                    bytes.extend_from_slice(&mov_zero.to_le_bytes());
4396                } else {
4397                    for imm in [1u16, 0u16] {
4398                        let hw1: u16 = 0xF04F;
4399                        let hw2: u16 = ((rd_bits as u16) << 8) | imm;
4400                        bytes.extend_from_slice(&hw1.to_le_bytes());
4401                        bytes.extend_from_slice(&hw2.to_le_bytes());
4402                    }
4403                }
4404
4405                Ok(bytes)
4406            }
4407
4408            // I64Mul: 64-bit multiply using UMULL + MLA cross products
4409            // Formula: result = (a_lo * b_lo) + ((a_lo * b_hi + a_hi * b_lo) << 32)
4410            // Uses R12 as scratch register
4411            ArmOp::I64Mul {
4412                rd_lo,
4413                rd_hi,
4414                rn_lo,
4415                rn_hi,
4416                rm_lo,
4417                rm_hi,
4418            } => {
4419                let rd_lo_bits = reg_to_bits(rd_lo);
4420                let rd_hi_bits = reg_to_bits(rd_hi);
4421                let rn_lo_bits = reg_to_bits(rn_lo);
4422                let rn_hi_bits = reg_to_bits(rn_hi);
4423                let rm_lo_bits = reg_to_bits(rm_lo);
4424                let rm_hi_bits = reg_to_bits(rm_hi);
4425                let r12: u32 = 12; // IP scratch register
4426                let mut bytes = Vec::new();
4427
4428                // 1. MUL R12, rn_lo, rm_hi  (R12 = a_lo * b_hi)
4429                // Thumb-2 MUL: hw1=0xFB00|Rn, hw2=0xF000|(Rd<<8)|Rm
4430                let hw1: u16 = (0xFB00 | rn_lo_bits) as u16;
4431                let hw2: u16 = (0xF000 | (r12 << 8) | rm_hi_bits) as u16;
4432                bytes.extend_from_slice(&hw1.to_le_bytes());
4433                bytes.extend_from_slice(&hw2.to_le_bytes());
4434
4435                // 2. MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
4436                // Thumb-2 MLA: hw1=0xFB00|Rn, hw2=(Ra<<12)|(Rd<<8)|Rm
4437                let hw1: u16 = (0xFB00 | rn_hi_bits) as u16;
4438                let hw2: u16 = ((r12 << 12) | (r12 << 8) | rm_lo_bits) as u16;
4439                bytes.extend_from_slice(&hw1.to_le_bytes());
4440                bytes.extend_from_slice(&hw2.to_le_bytes());
4441
4442                // 3. UMULL rd_lo, rd_hi, rn_lo, rm_lo  (rd_lo:rd_hi = a_lo * b_lo)
4443                // Thumb-2 UMULL: hw1=0xFBA0|Rn, hw2=(RdLo<<12)|(RdHi<<8)|Rm
4444                let hw1: u16 = (0xFBA0 | rn_lo_bits) as u16;
4445                let hw2: u16 = ((rd_lo_bits << 12) | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4446                bytes.extend_from_slice(&hw1.to_le_bytes());
4447                bytes.extend_from_slice(&hw2.to_le_bytes());
4448
4449                // 4. ADD rd_hi, R12  (rd_hi += cross products)
4450                // 16-bit high reg ADD: 01000100 D Rm Rdn[2:0]
4451                let d_bit = (rd_hi_bits >> 3) & 1;
4452                let add_instr: u16 =
4453                    (0x4400 | (d_bit << 7) | (r12 << 3) | (rd_hi_bits & 0x7)) as u16;
4454                bytes.extend_from_slice(&add_instr.to_le_bytes());
4455
4456                Ok(bytes)
4457            }
4458
4459            // I64Shl: 64-bit shift left with branch for n<32 vs n>=32
4460            // rm_hi (R3) is used as temp register
4461            ArmOp::I64Shl {
4462                rd_lo,
4463                rd_hi,
4464                rn_lo,
4465                rn_hi,
4466                rm_lo,
4467                rm_hi,
4468            } => {
4469                let rd_lo_bits = reg_to_bits(rd_lo);
4470                let rd_hi_bits = reg_to_bits(rd_hi);
4471                let rn_lo_bits = reg_to_bits(rn_lo);
4472                let rn_hi_bits = reg_to_bits(rn_hi);
4473                let rm_lo_bits = reg_to_bits(rm_lo);
4474                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4475                let mut bytes = Vec::new();
4476
4477                // AND.W rm_lo, rm_lo, #63  (mask shift amount to 6 bits)
4478                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4479                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4480                bytes.extend_from_slice(&hw1.to_le_bytes());
4481                bytes.extend_from_slice(&hw2.to_le_bytes());
4482
4483                // SUBS.W rm_hi, rm_lo, #32  (rm_hi = n-32, sets flags)
4484                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4485                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4486                bytes.extend_from_slice(&hw1.to_le_bytes());
4487                bytes.extend_from_slice(&hw2.to_le_bytes());
4488
4489                // BPL .large (branch if n >= 32, offset = +10 halfwords)
4490                let bpl: u16 = 0xD50A;
4491                bytes.extend_from_slice(&bpl.to_le_bytes());
4492
4493                // --- Small shift (n < 32) ---
4494                // RSB.W rm_hi, rm_lo, #32  (rm_hi = 32-n)
4495                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4496                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4497                bytes.extend_from_slice(&hw1.to_le_bytes());
4498                bytes.extend_from_slice(&hw2.to_le_bytes());
4499
4500                // LSR.W rm_hi, rn_lo, rm_hi  (rm_hi = lo >> (32-n), overflow bits)
4501                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4502                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4503                bytes.extend_from_slice(&hw1.to_le_bytes());
4504                bytes.extend_from_slice(&hw2.to_le_bytes());
4505
4506                // LSL.W rd_hi, rn_hi, rm_lo  (hi <<= n)
4507                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4508                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4509                bytes.extend_from_slice(&hw1.to_le_bytes());
4510                bytes.extend_from_slice(&hw2.to_le_bytes());
4511
4512                // ORR.W rd_hi, rd_hi, rm_hi  (hi |= overflow bits from lo)
4513                let hw1: u16 = (0xEA40 | rd_hi_bits) as u16;
4514                let hw2: u16 = ((rd_hi_bits << 8) | rm_hi_bits) as u16;
4515                bytes.extend_from_slice(&hw1.to_le_bytes());
4516                bytes.extend_from_slice(&hw2.to_le_bytes());
4517
4518                // LSL.W rd_lo, rn_lo, rm_lo  (lo <<= n)
4519                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4520                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4521                bytes.extend_from_slice(&hw1.to_le_bytes());
4522                bytes.extend_from_slice(&hw2.to_le_bytes());
4523
4524                // B .done (skip large shift: +2 halfwords)
4525                let b_done: u16 = 0xE002;
4526                bytes.extend_from_slice(&b_done.to_le_bytes());
4527
4528                // --- Large shift (n >= 32) ---
4529                // LSL.W rd_hi, rn_lo, rm_hi  (hi = lo << (n-32))
4530                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4531                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_hi_bits) as u16;
4532                bytes.extend_from_slice(&hw1.to_le_bytes());
4533                bytes.extend_from_slice(&hw2.to_le_bytes());
4534
4535                // MOV rd_lo, #0
4536                let mov_zero: u16 = 0x2000 | ((rd_lo_bits as u16) << 8);
4537                bytes.extend_from_slice(&mov_zero.to_le_bytes());
4538
4539                Ok(bytes) // Total: 38 bytes
4540            }
4541
4542            // I64ShrU: 64-bit logical shift right with branch for n<32 vs n>=32
4543            ArmOp::I64ShrU {
4544                rd_lo,
4545                rd_hi,
4546                rn_lo,
4547                rn_hi,
4548                rm_lo,
4549                rm_hi,
4550            } => {
4551                let rd_lo_bits = reg_to_bits(rd_lo);
4552                let rd_hi_bits = reg_to_bits(rd_hi);
4553                let rn_lo_bits = reg_to_bits(rn_lo);
4554                let rn_hi_bits = reg_to_bits(rn_hi);
4555                let rm_lo_bits = reg_to_bits(rm_lo);
4556                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4557                let mut bytes = Vec::new();
4558
4559                // AND.W rm_lo, rm_lo, #63
4560                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4561                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4562                bytes.extend_from_slice(&hw1.to_le_bytes());
4563                bytes.extend_from_slice(&hw2.to_le_bytes());
4564
4565                // SUBS.W rm_hi, rm_lo, #32
4566                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4567                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4568                bytes.extend_from_slice(&hw1.to_le_bytes());
4569                bytes.extend_from_slice(&hw2.to_le_bytes());
4570
4571                // BPL .large (+10 halfwords)
4572                let bpl: u16 = 0xD50A;
4573                bytes.extend_from_slice(&bpl.to_le_bytes());
4574
4575                // --- Small shift (n < 32) ---
4576                // RSB.W rm_hi, rm_lo, #32  (rm_hi = 32-n)
4577                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4578                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4579                bytes.extend_from_slice(&hw1.to_le_bytes());
4580                bytes.extend_from_slice(&hw2.to_le_bytes());
4581
4582                // LSL.W rm_hi, rn_hi, rm_hi  (rm_hi = hi << (32-n), bits flowing to lo)
4583                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4584                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4585                bytes.extend_from_slice(&hw1.to_le_bytes());
4586                bytes.extend_from_slice(&hw2.to_le_bytes());
4587
4588                // LSR.W rd_lo, rn_lo, rm_lo  (lo >>= n)
4589                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4590                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4591                bytes.extend_from_slice(&hw1.to_le_bytes());
4592                bytes.extend_from_slice(&hw2.to_le_bytes());
4593
4594                // ORR.W rd_lo, rd_lo, rm_hi  (lo |= overflow from hi)
4595                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4596                let hw2: u16 = ((rd_lo_bits << 8) | rm_hi_bits) as u16;
4597                bytes.extend_from_slice(&hw1.to_le_bytes());
4598                bytes.extend_from_slice(&hw2.to_le_bytes());
4599
4600                // LSR.W rd_hi, rn_hi, rm_lo  (hi >>= n, logical)
4601                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4602                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4603                bytes.extend_from_slice(&hw1.to_le_bytes());
4604                bytes.extend_from_slice(&hw2.to_le_bytes());
4605
4606                // B .done (+2 halfwords)
4607                let b_done: u16 = 0xE002;
4608                bytes.extend_from_slice(&b_done.to_le_bytes());
4609
4610                // --- Large shift (n >= 32) ---
4611                // LSR.W rd_lo, rn_hi, rm_hi  (lo = hi >> (n-32))
4612                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4613                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_hi_bits) as u16;
4614                bytes.extend_from_slice(&hw1.to_le_bytes());
4615                bytes.extend_from_slice(&hw2.to_le_bytes());
4616
4617                // MOV rd_hi, #0
4618                let mov_zero: u16 = 0x2000 | ((rd_hi_bits as u16) << 8);
4619                bytes.extend_from_slice(&mov_zero.to_le_bytes());
4620
4621                Ok(bytes) // Total: 38 bytes
4622            }
4623
4624            // I64ShrS: 64-bit arithmetic shift right with branch for n<32 vs n>=32
4625            ArmOp::I64ShrS {
4626                rd_lo,
4627                rd_hi,
4628                rn_lo,
4629                rn_hi,
4630                rm_lo,
4631                rm_hi,
4632            } => {
4633                let rd_lo_bits = reg_to_bits(rd_lo);
4634                let rd_hi_bits = reg_to_bits(rd_hi);
4635                let rn_lo_bits = reg_to_bits(rn_lo);
4636                let rn_hi_bits = reg_to_bits(rn_hi);
4637                let rm_lo_bits = reg_to_bits(rm_lo);
4638                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4639                let mut bytes = Vec::new();
4640
4641                // AND.W rm_lo, rm_lo, #63
4642                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4643                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4644                bytes.extend_from_slice(&hw1.to_le_bytes());
4645                bytes.extend_from_slice(&hw2.to_le_bytes());
4646
4647                // SUBS.W rm_hi, rm_lo, #32
4648                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4649                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4650                bytes.extend_from_slice(&hw1.to_le_bytes());
4651                bytes.extend_from_slice(&hw2.to_le_bytes());
4652
4653                // BPL .large (+10 halfwords)
4654                let bpl: u16 = 0xD50A;
4655                bytes.extend_from_slice(&bpl.to_le_bytes());
4656
4657                // --- Small shift (n < 32) ---
4658                // RSB.W rm_hi, rm_lo, #32
4659                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4660                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4661                bytes.extend_from_slice(&hw1.to_le_bytes());
4662                bytes.extend_from_slice(&hw2.to_le_bytes());
4663
4664                // LSL.W rm_hi, rn_hi, rm_hi  (rm_hi = hi << (32-n), bits flowing to lo)
4665                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4666                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4667                bytes.extend_from_slice(&hw1.to_le_bytes());
4668                bytes.extend_from_slice(&hw2.to_le_bytes());
4669
4670                // LSR.W rd_lo, rn_lo, rm_lo  (lo >>= n, logical for lo word)
4671                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4672                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4673                bytes.extend_from_slice(&hw1.to_le_bytes());
4674                bytes.extend_from_slice(&hw2.to_le_bytes());
4675
4676                // ORR.W rd_lo, rd_lo, rm_hi  (lo |= overflow from hi)
4677                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4678                let hw2: u16 = ((rd_lo_bits << 8) | rm_hi_bits) as u16;
4679                bytes.extend_from_slice(&hw1.to_le_bytes());
4680                bytes.extend_from_slice(&hw2.to_le_bytes());
4681
4682                // ASR.W rd_hi, rn_hi, rm_lo  (hi >>= n, arithmetic/sign-extending)
4683                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
4684                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4685                bytes.extend_from_slice(&hw1.to_le_bytes());
4686                bytes.extend_from_slice(&hw2.to_le_bytes());
4687
4688                // B .done (+3 halfwords, large shift is 8 bytes)
4689                let b_done: u16 = 0xE003;
4690                bytes.extend_from_slice(&b_done.to_le_bytes());
4691
4692                // --- Large shift (n >= 32) ---
4693                // ASR.W rd_lo, rn_hi, rm_hi  (lo = hi >>> (n-32))
4694                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
4695                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_hi_bits) as u16;
4696                bytes.extend_from_slice(&hw1.to_le_bytes());
4697                bytes.extend_from_slice(&hw2.to_le_bytes());
4698
4699                // ASR.W rd_hi, rn_hi, #31  (hi = sign extension, all 0s or all 1s)
4700                // Thumb-2 ASR immediate: hw1=0xEA4F, hw2=imm3:Rd:imm2:10:Rm
4701                // imm5=31=11111 → imm3=111, imm2=11
4702                let hw1: u16 = 0xEA4F;
4703                let hw2: u16 = (0x7000 | (rd_hi_bits << 8) | 0x00E0 | rn_hi_bits) as u16;
4704                bytes.extend_from_slice(&hw1.to_le_bytes());
4705                bytes.extend_from_slice(&hw2.to_le_bytes());
4706
4707                Ok(bytes) // Total: 40 bytes
4708            }
4709
4710            // I64Rotl: 64-bit rotate left (#610 rewrite).
4711            // For n < 32: new_hi = (hi << n) | (lo >> (32-n)), new_lo = (lo << n) | (hi >> (32-n))
4712            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
4713            //
4714            // Fixed-reg core: value in R0:R1, amount in R2, scratch R3 + R12
4715            // (all four saved/marshaled by the #610 fixed-ABI wrapper; the
4716            // pre-#610 expansion wrote through the selector's registers with
4717            // colliding R3/R4 scratch and restored the saved R4 OVER the
4718            // result). Relies on ARM register-shift semantics: amounts >= 32
4719            // yield 0 for LSL/LSR, which makes n = 0 and n = 32 exact.
4720            ArmOp::I64Rotl {
4721                rdlo,
4722                rdhi,
4723                rnlo,
4724                rnhi,
4725                shift,
4726            } => {
4727                let mut bytes = Vec::new();
4728                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
4729
4730                let core: [u16; 35] = [
4731                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
4732                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
4733                    0xD50E, //         BPL    .large        (n >= 32)
4734                    // --- small rotation (n < 32) ---
4735                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
4736                    0xFA20, 0xFC03, // LSR.W  R12, R0, R3   (lo >> (32-n))
4737                    0xFA21, 0xF303, // LSR.W  R3, R1, R3    (hi >> (32-n))
4738                    0xFA01, 0xF102, // LSL.W  R1, R1, R2    (hi << n)
4739                    0xEA41, 0x010C, // ORR.W  R1, R1, R12   (new_hi)
4740                    0xFA00, 0xF002, // LSL.W  R0, R0, R2    (lo << n)
4741                    0xEA40, 0x0003, // ORR.W  R0, R0, R3    (new_lo)
4742                    0xE00E, //         B      .done
4743                    // --- large rotation (n >= 32), R3 = m = n-32 ---
4744                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
4745                    0xFA21, 0xFC02, // LSR.W  R12, R1, R2   (hi >> (64-n))
4746                    0xFA20, 0xF202, // LSR.W  R2, R0, R2    (lo >> (64-n))
4747                    0xFA00, 0xF003, // LSL.W  R0, R0, R3    (lo << m)
4748                    0xFA01, 0xF103, // LSL.W  R1, R1, R3    (hi << m)
4749                    0xEA40, 0x0C0C, // ORR.W  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
4750                    0xEA41, 0x0002, // ORR.W  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
4751                    0x4661, //         MOV    R1, R12       (new_hi into place)
4752                            // .done: result in R0:R1
4753                ];
4754                for hw in core {
4755                    bytes.extend_from_slice(&hw.to_le_bytes());
4756                }
4757
4758                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
4759                Ok(bytes) // Total: 102 bytes
4760            }
4761
4762            // I64Rotr: 64-bit rotate right (#610 rewrite).
4763            // For n < 32: new_lo = (lo >> n) | (hi << (32-n)), new_hi = (hi >> n) | (lo << (32-n))
4764            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
4765            //
4766            // Same fixed-reg core contract as I64Rotl: value in R0:R1, amount
4767            // in R2, scratch R3 + R12, all covered by the fixed-ABI wrapper.
4768            ArmOp::I64Rotr {
4769                rdlo,
4770                rdhi,
4771                rnlo,
4772                rnhi,
4773                shift,
4774            } => {
4775                let mut bytes = Vec::new();
4776                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
4777
4778                let core: [u16; 35] = [
4779                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
4780                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
4781                    0xD50E, //         BPL    .large        (n >= 32)
4782                    // --- small rotation (n < 32) ---
4783                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
4784                    0xFA01, 0xFC03, // LSL.W  R12, R1, R3   (hi << (32-n))
4785                    0xFA00, 0xF303, // LSL.W  R3, R0, R3    (lo << (32-n))
4786                    0xFA20, 0xF002, // LSR.W  R0, R0, R2    (lo >> n)
4787                    0xEA40, 0x000C, // ORR.W  R0, R0, R12   (new_lo)
4788                    0xFA21, 0xF102, // LSR.W  R1, R1, R2    (hi >> n)
4789                    0xEA41, 0x0103, // ORR.W  R1, R1, R3    (new_hi)
4790                    0xE00E, //         B      .done
4791                    // --- large rotation (n >= 32), R3 = m = n-32 ---
4792                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
4793                    0xFA00, 0xFC02, // LSL.W  R12, R0, R2   (lo << (64-n))
4794                    0xFA01, 0xF202, // LSL.W  R2, R1, R2    (hi << (64-n))
4795                    0xFA21, 0xF103, // LSR.W  R1, R1, R3    (hi >> m)
4796                    0xEA41, 0x0C0C, // ORR.W  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
4797                    0xFA20, 0xF103, // LSR.W  R1, R0, R3    (lo >> m)
4798                    0xEA41, 0x0102, // ORR.W  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
4799                    0x4660, //         MOV    R0, R12       (new_lo into place)
4800                            // .done: result in R0:R1
4801                ];
4802                for hw in core {
4803                    bytes.extend_from_slice(&hw.to_le_bytes());
4804                }
4805
4806                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
4807                Ok(bytes) // Total: 102 bytes
4808            }
4809
4810            // I64Clz: Count leading zeros in 64-bit value
4811            // If hi != 0: result = CLZ(hi)
4812            // If hi == 0: result = 32 + CLZ(lo)
4813            //
4814            // Layout (using CMP+BNE approach for consistency):
4815            // 0: CMP.W rnhi, #0 (4 bytes)
4816            // 4: BEQ .hi_zero (2 bytes) - branch forward to offset 14
4817            // 6: CLZ.W rd, rnhi (4 bytes)
4818            // 10: B .done (2 bytes) - branch forward to offset 22
4819            // 12: NOP (2 bytes) - padding for alignment
4820            // 14: .hi_zero: CLZ.W rd, rnlo (4 bytes)
4821            // 18: ADD.W rd, rd, #32 (4 bytes)
4822            // 22: .done
4823            ArmOp::I64Clz { rd, rnlo, rnhi } => {
4824                let rd_bits = reg_to_bits(rd);
4825                let rn_lo_bits = reg_to_bits(rnlo);
4826                let rn_hi_bits = reg_to_bits(rnhi);
4827                let mut bytes = Vec::new();
4828
4829                // CMP.W rnhi, #0 (4 bytes at offset 0)
4830                let hw1: u16 = (0xF1B0 | rn_hi_bits) as u16;
4831                let hw2: u16 = 0x0F00;
4832                bytes.extend_from_slice(&hw1.to_le_bytes());
4833                bytes.extend_from_slice(&hw2.to_le_bytes());
4834
4835                // BEQ .hi_zero (2 bytes at offset 4)
4836                // PC = 4 + 4 = 8, target = 14, offset = 6, imm8 = 3
4837                let beq: u16 = 0xD003;
4838                bytes.extend_from_slice(&beq.to_le_bytes());
4839
4840                // CLZ.W rd, rnhi (4 bytes at offset 6)
4841                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4842                let hw1: u16 = (0xFAB0 | rn_hi_bits) as u16;
4843                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_hi_bits) as u16;
4844                bytes.extend_from_slice(&hw1.to_le_bytes());
4845                bytes.extend_from_slice(&hw2.to_le_bytes());
4846
4847                // B .done (2 bytes at offset 10)
4848                // PC = 10 + 4 = 14, target = 22, offset = 8, imm11 = 4
4849                let b_done: u16 = 0xE004;
4850                bytes.extend_from_slice(&b_done.to_le_bytes());
4851
4852                // NOP (2 bytes at offset 12) - padding
4853                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
4854
4855                // .hi_zero: (offset 14)
4856                // CLZ.W rd, rnlo (4 bytes)
4857                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4858                let hw1: u16 = (0xFAB0 | rn_lo_bits) as u16;
4859                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_lo_bits) as u16;
4860                bytes.extend_from_slice(&hw1.to_le_bytes());
4861                bytes.extend_from_slice(&hw2.to_le_bytes());
4862
4863                // ADD.W rd, rd, #32 (4 bytes at offset 18)
4864                let hw1: u16 = (0xF100 | rd_bits) as u16;
4865                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
4866                bytes.extend_from_slice(&hw1.to_le_bytes());
4867                bytes.extend_from_slice(&hw2.to_le_bytes());
4868
4869                // .done: (offset 22)
4870                // i64.clz returns i64, so clear high word: MOV rnhi, #0 (2 bytes)
4871                // MOVS Rn, #0: 0010 0 Rn 00000000
4872                let mov0: u16 = (0x2000 | (rn_hi_bits << 8)) as u16;
4873                bytes.extend_from_slice(&mov0.to_le_bytes());
4874
4875                Ok(bytes)
4876            }
4877
4878            // I64Ctz: Count trailing zeros in 64-bit value
4879            // If lo != 0: result = CTZ(lo) = CLZ(RBIT(lo))
4880            // If lo == 0: result = 32 + CTZ(hi) = 32 + CLZ(RBIT(hi))
4881            //
4882            // Layout:
4883            // 0: CMP.W rnlo, #0 (4 bytes)
4884            // 4: BEQ .lo_zero (2 bytes) - branch to offset 18
4885            // 6: RBIT.W rd, rnlo (4 bytes)
4886            // 10: CLZ.W rd, rd (4 bytes)
4887            // 14: B .done (2 bytes) - branch to offset 30
4888            // 16: NOP (2 bytes) - padding
4889            // 18: .lo_zero: RBIT.W rd, rnhi (4 bytes)
4890            // 22: CLZ.W rd, rd (4 bytes)
4891            // 26: ADD.W rd, rd, #32 (4 bytes)
4892            // 30: .done
4893            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
4894                let rd_bits = reg_to_bits(rd);
4895                let rn_lo_bits = reg_to_bits(rnlo);
4896                let rn_hi_bits = reg_to_bits(rnhi);
4897                let mut bytes = Vec::new();
4898
4899                // CMP.W rnlo, #0 (4 bytes at offset 0)
4900                let hw1: u16 = (0xF1B0 | rn_lo_bits) as u16;
4901                let hw2: u16 = 0x0F00;
4902                bytes.extend_from_slice(&hw1.to_le_bytes());
4903                bytes.extend_from_slice(&hw2.to_le_bytes());
4904
4905                // BEQ .lo_zero (2 bytes at offset 4)
4906                // PC = 4 + 4 = 8, target = 18, offset = 10, imm8 = 5
4907                let beq: u16 = 0xD005;
4908                bytes.extend_from_slice(&beq.to_le_bytes());
4909
4910                // RBIT.W rd, rnlo (4 bytes at offset 6)
4911                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
4912                let hw1: u16 = (0xFA90 | rn_lo_bits) as u16;
4913                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_lo_bits) as u16;
4914                bytes.extend_from_slice(&hw1.to_le_bytes());
4915                bytes.extend_from_slice(&hw2.to_le_bytes());
4916
4917                // CLZ.W rd, rd (4 bytes at offset 10)
4918                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4919                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
4920                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
4921                bytes.extend_from_slice(&hw1.to_le_bytes());
4922                bytes.extend_from_slice(&hw2.to_le_bytes());
4923
4924                // B .done (2 bytes at offset 14)
4925                // PC = 14 + 4 = 18, target = 30, offset = 12, imm11 = 6
4926                let b_done: u16 = 0xE006;
4927                bytes.extend_from_slice(&b_done.to_le_bytes());
4928
4929                // NOP (2 bytes at offset 16) - padding
4930                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
4931
4932                // .lo_zero: (offset 18)
4933                // RBIT.W rd, rnhi (4 bytes)
4934                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
4935                let hw1: u16 = (0xFA90 | rn_hi_bits) as u16;
4936                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_hi_bits) as u16;
4937                bytes.extend_from_slice(&hw1.to_le_bytes());
4938                bytes.extend_from_slice(&hw2.to_le_bytes());
4939
4940                // CLZ.W rd, rd (4 bytes at offset 22)
4941                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4942                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
4943                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
4944                bytes.extend_from_slice(&hw1.to_le_bytes());
4945                bytes.extend_from_slice(&hw2.to_le_bytes());
4946
4947                // ADD.W rd, rd, #32 (4 bytes at offset 26)
4948                let hw1: u16 = (0xF100 | rd_bits) as u16;
4949                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
4950                bytes.extend_from_slice(&hw1.to_le_bytes());
4951                bytes.extend_from_slice(&hw2.to_le_bytes());
4952
4953                // .done: (offset 30)
4954                // i64.ctz returns i64, so clear high word: MOV rnhi, #0 (2 bytes)
4955                let mov0: u16 = (0x2000 | (rn_hi_bits << 8)) as u16;
4956                bytes.extend_from_slice(&mov0.to_le_bytes());
4957
4958                Ok(bytes)
4959            }
4960
4961            // I64Popcnt: Population count of 64-bit value
4962            // result = POPCNT(lo) + POPCNT(hi)
4963            // Using SIMD-style parallel bit counting algorithm
4964            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
4965                let rd_bits = reg_to_bits(rd);
4966                let rn_lo_bits = reg_to_bits(rnlo);
4967                let rn_hi_bits = reg_to_bits(rnhi);
4968                let r12: u32 = 12; // IP scratch
4969                let r3: u32 = 3; // Scratch for hi popcnt result
4970                let mut bytes = Vec::new();
4971
4972                // PUSH {R3, R4, R5} - save scratch registers
4973                bytes.extend_from_slice(&0xB438u16.to_le_bytes());
4974
4975                // Strategy: compute popcnt(lo) -> R4, popcnt(hi) -> R5, add them -> rd
4976                // Using lookup table approach for each byte would be too large
4977                // Using shift-and-add approach instead
4978
4979                // For simplicity and correctness, use the efficient parallel algorithm
4980                // but implement it as a series of inline operations
4981
4982                // Marshal the operand pair into the fixed scratch regs, routing
4983                // rnlo through R12 (#632 audit): writing R4 first corrupted the
4984                // rnhi read for a pair living at (R3,R4) — every source is read
4985                // before any scratch register it could occupy is written.
4986                // MOV R12, rnlo
4987                let mov: u16 = (0x4600 | (1 << 7) | (rn_lo_bits << 3) | 4) as u16;
4988                bytes.extend_from_slice(&mov.to_le_bytes());
4989                // MOV R5, rnhi (R4 untouched so far; rnhi == R5 is a no-op)
4990                let mov: u16 = (0x4600 | (rn_hi_bits << 3) | 5) as u16;
4991                bytes.extend_from_slice(&mov.to_le_bytes());
4992                // MOV R4, R12
4993                bytes.extend_from_slice(&0x4664u16.to_le_bytes());
4994
4995                // --- POPCNT for R4 (lo word) ---
4996                // Step 1: x = x - ((x >> 1) & 0x55555555)
4997                // LSR.W R12, R4, #1
4998                let hw1: u16 = 0xEA4F;
4999                let hw2: u16 = ((r12 << 8) | 0x50 | 4) as u16;
5000                bytes.extend_from_slice(&hw1.to_le_bytes());
5001                bytes.extend_from_slice(&hw2.to_le_bytes());
5002
5003                // Load 0x55555555 into R3 using MOVW/MOVT
5004                // MOVW R3, #0x5555
5005                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5006                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5007                // MOVT R3, #0x5555
5008                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5009                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5010
5011                // AND.W R12, R12, R3
5012                let hw1: u16 = (0xEA00 | r12) as u16;
5013                let hw2: u16 = ((r12 << 8) | r3) as u16;
5014                bytes.extend_from_slice(&hw1.to_le_bytes());
5015                bytes.extend_from_slice(&hw2.to_le_bytes());
5016
5017                // SUB.W R4, R4, R12
5018                let hw1: u16 = (0xEBA0 | 4) as u16;
5019                let hw2: u16 = ((4 << 8) | r12) as u16;
5020                bytes.extend_from_slice(&hw1.to_le_bytes());
5021                bytes.extend_from_slice(&hw2.to_le_bytes());
5022
5023                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5024                // Load 0x33333333 into R3
5025                // MOVW R3, #0x3333
5026                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5027                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5028                // MOVT R3, #0x3333
5029                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5030                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5031
5032                // AND.W R12, R4, R3
5033                let hw1: u16 = (0xEA00 | 4) as u16;
5034                let hw2: u16 = ((r12 << 8) | r3) as u16;
5035                bytes.extend_from_slice(&hw1.to_le_bytes());
5036                bytes.extend_from_slice(&hw2.to_le_bytes());
5037
5038                // LSR.W R4, R4, #2
5039                let hw1: u16 = 0xEA4F;
5040                let hw2: u16 = ((4 << 8) | 0x90 | 4) as u16;
5041                bytes.extend_from_slice(&hw1.to_le_bytes());
5042                bytes.extend_from_slice(&hw2.to_le_bytes());
5043
5044                // AND.W R4, R4, R3
5045                let hw1: u16 = (0xEA00 | 4) as u16;
5046                let hw2: u16 = ((4 << 8) | r3) as u16;
5047                bytes.extend_from_slice(&hw1.to_le_bytes());
5048                bytes.extend_from_slice(&hw2.to_le_bytes());
5049
5050                // ADD.W R4, R4, R12
5051                let hw1: u16 = (0xEB00 | 4) as u16;
5052                let hw2: u16 = ((4 << 8) | r12) as u16;
5053                bytes.extend_from_slice(&hw1.to_le_bytes());
5054                bytes.extend_from_slice(&hw2.to_le_bytes());
5055
5056                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5057                // LSR.W R12, R4, #4
5058                // hw2 = (imm3 << 12) | (Rd << 8) | (imm2 << 6) | (type << 4) | Rm
5059                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5060                let hw1: u16 = 0xEA4F;
5061                let hw2: u16 = (0x1000 | (r12 << 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                // ADD.W R4, R4, R12
5066                let hw1: u16 = (0xEB00 | 4) as u16;
5067                let hw2: u16 = ((4 << 8) | r12) as u16;
5068                bytes.extend_from_slice(&hw1.to_le_bytes());
5069                bytes.extend_from_slice(&hw2.to_le_bytes());
5070
5071                // Load 0x0F0F0F0F into R3
5072                // MOVW R3, #0x0F0F (imm4=0, i=1, imm3=7, imm8=0x0F)
5073                // hw1 = 11110 1 10 0100 0000 = 0xF640
5074                // hw2 = 0 111 0011 00001111 = 0x730F
5075                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5076                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5077                // MOVT R3, #0x0F0F
5078                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5079                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5080
5081                // AND.W R4, R4, R3
5082                let hw1: u16 = (0xEA00 | 4) as u16;
5083                let hw2: u16 = ((4 << 8) | r3) as u16;
5084                bytes.extend_from_slice(&hw1.to_le_bytes());
5085                bytes.extend_from_slice(&hw2.to_le_bytes());
5086
5087                // Step 4: x = x * 0x01010101 >> 24
5088                // Load 0x01010101 into R3
5089                // MOVW R3, #0x0101
5090                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5091                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5092                // MOVT R3, #0x0101
5093                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5094                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5095
5096                // MUL R4, R4, R3
5097                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5098                let hw1: u16 = (0xFB00 | 4) as u16;
5099                let hw2: u16 = (0xF000 | (4 << 8) | r3) as u16;
5100                bytes.extend_from_slice(&hw1.to_le_bytes());
5101                bytes.extend_from_slice(&hw2.to_le_bytes());
5102
5103                // LSR.W R4, R4, #24
5104                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5105                let hw1: u16 = 0xEA4F;
5106                let hw2: u16 = (0x6000 | (4 << 8) | 0x10 | 4) as u16;
5107                bytes.extend_from_slice(&hw1.to_le_bytes());
5108                bytes.extend_from_slice(&hw2.to_le_bytes());
5109
5110                // --- POPCNT for R5 (hi word) - same algorithm ---
5111                // Step 1
5112                let hw1: u16 = 0xEA4F;
5113                let hw2: u16 = ((r12 << 8) | 0x50 | 5) as u16;
5114                bytes.extend_from_slice(&hw1.to_le_bytes());
5115                bytes.extend_from_slice(&hw2.to_le_bytes());
5116
5117                // Load 0x55555555 into R3
5118                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5119                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5120                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5121                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5122
5123                let hw1: u16 = (0xEA00 | r12) as u16;
5124                let hw2: u16 = ((r12 << 8) | r3) as u16;
5125                bytes.extend_from_slice(&hw1.to_le_bytes());
5126                bytes.extend_from_slice(&hw2.to_le_bytes());
5127
5128                let hw1: u16 = (0xEBA0 | 5) as u16;
5129                let hw2: u16 = ((5 << 8) | r12) as u16;
5130                bytes.extend_from_slice(&hw1.to_le_bytes());
5131                bytes.extend_from_slice(&hw2.to_le_bytes());
5132
5133                // Step 2
5134                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5135                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5136                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5137                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5138
5139                let hw1: u16 = (0xEA00 | 5) as u16;
5140                let hw2: u16 = ((r12 << 8) | r3) as u16;
5141                bytes.extend_from_slice(&hw1.to_le_bytes());
5142                bytes.extend_from_slice(&hw2.to_le_bytes());
5143
5144                let hw1: u16 = 0xEA4F;
5145                let hw2: u16 = ((5 << 8) | 0x90 | 5) as u16;
5146                bytes.extend_from_slice(&hw1.to_le_bytes());
5147                bytes.extend_from_slice(&hw2.to_le_bytes());
5148
5149                let hw1: u16 = (0xEA00 | 5) as u16;
5150                let hw2: u16 = ((5 << 8) | r3) as u16;
5151                bytes.extend_from_slice(&hw1.to_le_bytes());
5152                bytes.extend_from_slice(&hw2.to_le_bytes());
5153
5154                let hw1: u16 = (0xEB00 | 5) as u16;
5155                let hw2: u16 = ((5 << 8) | r12) as u16;
5156                bytes.extend_from_slice(&hw1.to_le_bytes());
5157                bytes.extend_from_slice(&hw2.to_le_bytes());
5158
5159                // Step 3: LSR.W R12, R5, #4
5160                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5161                let hw1: u16 = 0xEA4F;
5162                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 5) as u16;
5163                bytes.extend_from_slice(&hw1.to_le_bytes());
5164                bytes.extend_from_slice(&hw2.to_le_bytes());
5165
5166                let hw1: u16 = (0xEB00 | 5) as u16;
5167                let hw2: u16 = ((5 << 8) | r12) as u16;
5168                bytes.extend_from_slice(&hw1.to_le_bytes());
5169                bytes.extend_from_slice(&hw2.to_le_bytes());
5170
5171                // Load 0x0F0F0F0F into R3 (for hi-word)
5172                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5173                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5174                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5175                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5176
5177                let hw1: u16 = (0xEA00 | 5) as u16;
5178                let hw2: u16 = ((5 << 8) | r3) as u16;
5179                bytes.extend_from_slice(&hw1.to_le_bytes());
5180                bytes.extend_from_slice(&hw2.to_le_bytes());
5181
5182                // Step 4
5183                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5184                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5185                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5186                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5187
5188                // MUL R5, R5, R3
5189                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5190                let hw1: u16 = (0xFB00 | 5) as u16;
5191                let hw2: u16 = (0xF000 | (5 << 8) | r3) as u16;
5192                bytes.extend_from_slice(&hw1.to_le_bytes());
5193                bytes.extend_from_slice(&hw2.to_le_bytes());
5194
5195                // LSR.W R5, R5, #24
5196                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5197                let hw1: u16 = 0xEA4F;
5198                let hw2: u16 = (0x6000 | (5 << 8) | 0x10 | 5) as u16;
5199                bytes.extend_from_slice(&hw1.to_le_bytes());
5200                bytes.extend_from_slice(&hw2.to_le_bytes());
5201
5202                // #632: the count must be carried ACROSS the scratch restore
5203                // in a register the POP cannot touch. rd is allocator-assigned
5204                // (any of R0-R8) and can land inside the {R3,R4,R5} restore set
5205                // — the old `ADDS rd, R4, R5; POP {R3,R4,R5}` destroyed the
5206                // result one instruction after computing it (0 for every input
5207                // under qemu). R12 is encoder scratch: never allocatable (#212)
5208                // and never in a restore set, so no choice of rd can collide.
5209                // ADD.W R12, R4, R5
5210                bytes.extend_from_slice(&0xEB04u16.to_le_bytes());
5211                bytes.extend_from_slice(&0x0C05u16.to_le_bytes());
5212
5213                // POP {R3, R4, R5}
5214                bytes.extend_from_slice(&0xBC38u16.to_le_bytes());
5215
5216                // MOV rd, R12 — after the restore. The 4-bit Rd (D:rd) form is
5217                // also total over rd = R8, where the old ADDS T1 3-bit field
5218                // silently corrupted the encoding (#178/#180 class).
5219                let mov: u16 =
5220                    (0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7)) as u16;
5221                bytes.extend_from_slice(&mov.to_le_bytes());
5222
5223                // i64.popcnt returns i64, so clear high word: MOV.W rnhi, #0
5224                // (T2, 4 bytes — total over rnhi = R8, where the old 16-bit
5225                // MOVS encoding overflowed its 3-bit field into CMP R0, #0).
5226                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5227                bytes.extend_from_slice(&(((rn_hi_bits & 0xF) << 8) as u16).to_le_bytes());
5228
5229                Ok(bytes)
5230            }
5231
5232            // I64Extend8S: Sign-extend low 8 bits to 64 bits
5233            // Result: rdlo = sign_extend_8(rnlo), rdhi = rdlo >> 31
5234            ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
5235                let rdlo_bits = reg_to_bits(rdlo);
5236                let rdhi_bits = reg_to_bits(rdhi);
5237                let rnlo_bits = reg_to_bits(rnlo);
5238                let mut bytes = Vec::new();
5239
5240                // SXTB.W rdlo, rnlo (sign-extend byte to 32-bit)
5241                // SXTB T2: hw1 = 0xFA4F, hw2 = 0xF0<Rd><Rm>
5242                let hw1: u16 = 0xFA4F_u16;
5243                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5244                bytes.extend_from_slice(&hw1.to_le_bytes());
5245                bytes.extend_from_slice(&hw2.to_le_bytes());
5246
5247                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5248                // ASR (immediate): hw1 = 0xEA4F, hw2 = imm3:Rd:imm2:type:Rm
5249                // For imm5=31: imm3=111, imm2=11, type=10 (ASR)
5250                // hw2 = (7 << 12) | (rdhi << 8) | (3 << 6) | (2 << 4) | rdlo
5251                let hw1: u16 = 0xEA4F;
5252                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5253                bytes.extend_from_slice(&hw1.to_le_bytes());
5254                bytes.extend_from_slice(&hw2.to_le_bytes());
5255
5256                Ok(bytes)
5257            }
5258
5259            // I64Extend16S: Sign-extend low 16 bits to 64 bits
5260            // Result: rdlo = sign_extend_16(rnlo), rdhi = rdlo >> 31
5261            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
5262                let rdlo_bits = reg_to_bits(rdlo);
5263                let rdhi_bits = reg_to_bits(rdhi);
5264                let rnlo_bits = reg_to_bits(rnlo);
5265                let mut bytes = Vec::new();
5266
5267                // SXTH.W rdlo, rnlo (sign-extend halfword to 32-bit)
5268                // SXTH T2: hw1 = 0xFA0F, hw2 = 0xF0<Rd><Rm>
5269                let hw1: u16 = 0xFA0F_u16;
5270                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5271                bytes.extend_from_slice(&hw1.to_le_bytes());
5272                bytes.extend_from_slice(&hw2.to_le_bytes());
5273
5274                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5275                let hw1: u16 = 0xEA4F;
5276                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5277                bytes.extend_from_slice(&hw1.to_le_bytes());
5278                bytes.extend_from_slice(&hw2.to_le_bytes());
5279
5280                Ok(bytes)
5281            }
5282
5283            // I64Extend32S: Sign-extend low 32 bits to 64 bits
5284            // Result: rdlo = rnlo, rdhi = rnlo >> 31
5285            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
5286                let rdlo_bits = reg_to_bits(rdlo);
5287                let rdhi_bits = reg_to_bits(rdhi);
5288                let rnlo_bits = reg_to_bits(rnlo);
5289                let mut bytes = Vec::new();
5290
5291                // MOV rdlo, rnlo (if different)
5292                if rdlo_bits != rnlo_bits {
5293                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5294                    let d_bit = ((rdlo_bits >> 3) & 1) as u16;
5295                    let mov: u16 = 0x4600
5296                        | (d_bit << 7)
5297                        | ((rnlo_bits as u16) << 3)
5298                        | ((rdlo_bits & 0x7) as u16);
5299                    bytes.extend_from_slice(&mov.to_le_bytes());
5300                }
5301
5302                // ASR.W rdhi, rnlo, #31 (sign-extend to high word)
5303                let hw1: u16 = 0xEA4F;
5304                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rnlo_bits) as u16;
5305                bytes.extend_from_slice(&hw1.to_le_bytes());
5306                bytes.extend_from_slice(&hw2.to_le_bytes());
5307
5308                Ok(bytes)
5309            }
5310
5311            // SelectMove: IT <cond>; MOV{cond} rd, rm
5312            // Conditional move: only execute MOV if condition is true
5313            ArmOp::SelectMove { rd, rm, cond } => {
5314                let rd_bits = reg_to_bits(rd) as u16;
5315                let rm_bits = reg_to_bits(rm) as u16;
5316
5317                // Condition code encoding for IT block
5318                use synth_synthesis::Condition;
5319                let cond_bits: u16 = match cond {
5320                    Condition::EQ => 0x0, // Equal
5321                    Condition::NE => 0x1, // Not equal
5322                    Condition::HS => 0x2, // Higher or same (unsigned >=)
5323                    Condition::LO => 0x3, // Lower (unsigned <)
5324                    Condition::HI => 0x8, // Higher (unsigned >)
5325                    Condition::LS => 0x9, // Lower or same (unsigned <=)
5326                    Condition::GE => 0xA, // Greater or equal (signed)
5327                    Condition::LT => 0xB, // Less than (signed)
5328                    Condition::GT => 0xC, // Greater than (signed)
5329                    Condition::LE => 0xD, // Less or equal (signed)
5330                };
5331
5332                // IT <cond>: single Then block (mask = 0x8 for T only)
5333                // IT instruction: 1011 1111 firstcond mask
5334                let it_instr: u16 = 0xBF00 | (cond_bits << 4) | 0x8;
5335
5336                // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5337                // This MOV will only execute if condition is true due to IT block
5338                let d_bit = (rd_bits >> 3) & 1;
5339                let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5340
5341                // Emit: IT <cond>, MOV rd, rm
5342                let mut bytes = it_instr.to_le_bytes().to_vec();
5343                bytes.extend_from_slice(&mov_instr.to_le_bytes());
5344                Ok(bytes)
5345            }
5346
5347            // Popcnt: Population count (count set bits)
5348            // ARM Cortex-M has no native POPCNT, so we implement the bit manipulation algorithm:
5349            // x = x - ((x >> 1) & 0x55555555);
5350            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
5351            // x = (x + (x >> 4)) & 0x0F0F0F0F;
5352            // x = x + (x >> 8);
5353            // x = x + (x >> 16);
5354            // return x & 0x3F;
5355            //
5356            // Uses rd as working register and R12 as scratch for constants
5357            ArmOp::Popcnt { rd, rm } => {
5358                let mut bytes = Vec::new();
5359
5360                // First, move rm to rd if they're different
5361                if rd != rm {
5362                    let rd_bits = reg_to_bits(rd) as u16;
5363                    let rm_bits = reg_to_bits(rm) as u16;
5364                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5365                    let d_bit = (rd_bits >> 3) & 1;
5366                    let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5367                    bytes.extend_from_slice(&mov_instr.to_le_bytes());
5368                }
5369
5370                // Step 1: x = x - ((x >> 1) & 0x55555555)
5371                // Load 0x55555555 into R12
5372                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x5555)?);
5373                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x5555)?);
5374
5375                // R12_temp = rd >> 1
5376                // We need a second scratch register. Use R11.
5377                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 1)?);
5378
5379                // R11 = R11 & R12 (R11 = (x >> 1) & 0x55555555)
5380                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(11, 11, 12)?);
5381
5382                // rd = rd - R11
5383                bytes.extend_from_slice(&self.encode_thumb32_sub_reg_raw(
5384                    reg_to_bits(rd),
5385                    reg_to_bits(rd),
5386                    11,
5387                )?);
5388
5389                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5390                // Load 0x33333333 into R12
5391                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x3333)?);
5392                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x3333)?);
5393
5394                // R11 = rd & R12
5395                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5396                    11,
5397                    reg_to_bits(rd),
5398                    12,
5399                )?);
5400
5401                // rd = rd >> 2
5402                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(
5403                    reg_to_bits(rd),
5404                    reg_to_bits(rd),
5405                    2,
5406                )?);
5407
5408                // rd = rd & R12
5409                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5410                    reg_to_bits(rd),
5411                    reg_to_bits(rd),
5412                    12,
5413                )?);
5414
5415                // rd = rd + R11
5416                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5417                    reg_to_bits(rd),
5418                    reg_to_bits(rd),
5419                    11,
5420                )?);
5421
5422                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5423                // R11 = rd >> 4
5424                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 4)?);
5425
5426                // rd = rd + R11
5427                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5428                    reg_to_bits(rd),
5429                    reg_to_bits(rd),
5430                    11,
5431                )?);
5432
5433                // Load 0x0F0F0F0F into R12
5434                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x0F0F)?);
5435                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x0F0F)?);
5436
5437                // rd = rd & R12
5438                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5439                    reg_to_bits(rd),
5440                    reg_to_bits(rd),
5441                    12,
5442                )?);
5443
5444                // Step 4: x = x + (x >> 8)
5445                // R11 = rd >> 8
5446                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 8)?);
5447
5448                // rd = rd + R11
5449                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5450                    reg_to_bits(rd),
5451                    reg_to_bits(rd),
5452                    11,
5453                )?);
5454
5455                // Step 5: x = x + (x >> 16)
5456                // R11 = rd >> 16
5457                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 16)?);
5458
5459                // rd = rd + R11
5460                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5461                    reg_to_bits(rd),
5462                    reg_to_bits(rd),
5463                    11,
5464                )?);
5465
5466                // Step 6: return x & 0x3F
5467                // AND with 0x3F (small immediate, can use BIC or AND with immediate)
5468                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5469                    reg_to_bits(rd),
5470                    reg_to_bits(rd),
5471                    0x3F,
5472                )?);
5473
5474                Ok(bytes)
5475            }
5476
5477            // I64DivU: 64-bit unsigned division using binary long division
5478            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = quotient
5479            // Uses: R4-R7, R12 as loop counter (avoid R8 for Renode compatibility)
5480            //
5481            // #610: the fixed-ABI wrapper marshals the selector-assigned
5482            // operand registers into the core's fixed regs and lands the
5483            // result in rd — pre-#610 this arm IGNORED its register fields,
5484            // so the selector read its rd pair (e.g. R4:R5) after the core's
5485            // own POP restored the stale caller values over it: 0 for every
5486            // input. A zero divisor now traps (UDF #0), per WASM semantics.
5487            ArmOp::I64DivU {
5488                rdlo,
5489                rdhi,
5490                rnlo,
5491                rnhi,
5492                rmlo,
5493                rmhi,
5494                elide_zero_guard,
5495            } => {
5496                let mut bytes = Vec::new();
5497                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5498                // #494 phase 2b: elided only under a certificate-discharged
5499                // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
5500                if !elide_zero_guard {
5501                    emit_i64_divisor_zero_trap(&mut bytes);
5502                }
5503
5504                // PUSH {R4-R7} - save scratch registers (NO LR — this is inline code)
5505                // 16-bit PUSH: 1011 010 M rrrrrrrr where M=0 (no LR), r=R4-R7 = 0xF0
5506                // Encoding: 1011 0100 1111 0000 = 0xB4F0
5507                bytes.extend_from_slice(&0xB4F0u16.to_le_bytes());
5508
5509                // Initialize quotient (R4:R5) = 0
5510                bytes.extend_from_slice(&0x2400u16.to_le_bytes()); // MOV R4, #0
5511                bytes.extend_from_slice(&0x2500u16.to_le_bytes()); // MOV R5, #0
5512
5513                // Initialize remainder (R6:R7) = 0
5514                bytes.extend_from_slice(&0x2600u16.to_le_bytes()); // MOV R6, #0
5515                bytes.extend_from_slice(&0x2700u16.to_le_bytes()); // MOV R7, #0
5516
5517                // Initialize loop counter R12 = 64 (use R12 scratch instead of R8)
5518                // MOV.W R12, #64: F04F 0C40
5519                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5520                bytes.extend_from_slice(&0x0C40u16.to_le_bytes());
5521
5522                // Loop start
5523                let loop_start = bytes.len();
5524
5525                // === Loop body: process one bit ===
5526
5527                // 1. Shift quotient R4:R5 left by 1
5528                // LSLS R5, R5, #1 (16-bit: 0000 0010 1010 1101 = 0x006D -> actually 0x002D for LSL R5,R5,#1)
5529                // LSL Rd, Rm, #imm5: 000 00 imm5 Rm Rd = 000 00 00001 101 101 = 0x006D
5530                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5531                // Get carry from R4 into R5: ORR R5, R5, R4 LSR #31
5532                // Thumb-2 ORR with shifted register: EA45 75D4 = ORR.W R5, R5, R4, LSR #31
5533                // 11101010 010 S Rn | 0 imm3 Rd imm2 type Rm
5534                // type=01 (LSR), imm5=31 (imm3=111, imm2=11)
5535                bytes.extend_from_slice(&0xEA45u16.to_le_bytes());
5536                bytes.extend_from_slice(&0x75D4u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5537                // LSLS R4, R4, #1: 000 00 00001 100 100 = 0x0064
5538                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5539
5540                // 2. Shift remainder R6:R7 left by 1, OR in MSB of dividend R1
5541                // LSLS R7, R7, #1
5542                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5543                // ORR.W R7, R7, R6, LSR #31
5544                bytes.extend_from_slice(&0xEA47u16.to_le_bytes());
5545                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5546                // LSLS R6, R6, #1
5547                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5548                // ORR.W R6, R6, R1, LSR #31 (bring in MSB of dividend high)
5549                bytes.extend_from_slice(&0xEA46u16.to_le_bytes());
5550                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5551
5552                // 3. Shift dividend R0:R1 left by 1
5553                // LSLS R1, R1, #1
5554                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5555                // ORR.W R1, R1, R0, LSR #31
5556                bytes.extend_from_slice(&0xEA41u16.to_le_bytes());
5557                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5558                // LSLS R0, R0, #1
5559                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5560
5561                // 4. Compare remainder >= divisor (64-bit unsigned comparison)
5562                // Compare high words first: CMP R7, R3
5563                // CMP Rn, Rm encoding: 0x4280 | (Rm << 3) | Rn
5564                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3 (16-bit)
5565                // BHI means R7 > R3 (unsigned) - definitely subtract
5566                // BLO means R7 < R3 - definitely don't subtract
5567                // BEQ means need to check low words
5568
5569                // If high > divisor high: branch to subtract (forward +offset)
5570                // BHI.N +6 (skip CMP, skip BLO, do subtract)
5571                // BHI: 1101 1000 offset8 where cond=1000 (HI)
5572                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4 (to subtract block)
5573
5574                // If high < divisor high: branch past subtract
5575                // BLO.N +10 (skip to decrement)
5576                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BLO/BCC +12 (past subtract)
5577
5578                // High words equal, compare low: CMP R6, R2
5579                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2 (16-bit)
5580                // BLO/BCC past subtract (skip SUBS+SBC.W+ORR.W = 10 bytes = 4 halfwords from PC+4)
5581                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords (past subtract)
5582
5583                // === Subtract block: remainder -= divisor, quotient |= 1 ===
5584                // SUBS R6, R6, R2
5585                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2 (16-bit)
5586                // SBC R7, R7, R3 (with borrow)
5587                // Thumb-2 SBC.W: EB67 0703 = SBC.W R7, R7, R3
5588                bytes.extend_from_slice(&0xEB67u16.to_le_bytes());
5589                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5590                // ORR R4, R4, #1 (set bit 0 of quotient low)
5591                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5592                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5593
5594                // === Decrement counter and loop ===
5595                // SUBS.W R12, R12, #1 (decrement loop counter)
5596                // SUBS.W R12, R12, #1: F1BC 0C01
5597                bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
5598                bytes.extend_from_slice(&0x0C01u16.to_le_bytes());
5599
5600                // BNE back to loop_start
5601                let branch_offset_bytes = bytes.len() - loop_start + 4; // +4 for pipeline
5602                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5603                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5604                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5605
5606                // === Loop done, move quotient to R0:R1 ===
5607                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5608                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5609
5610                // POP {R4-R7} - restore scratch registers (NO PC — inline code continues)
5611                // 16-bit POP: 1011 110 P rrrrrrrr where P=0 (no PC), r=R4-R7 = 0xF0
5612                // Encoding: 1011 1100 1111 0000 = 0xBCF0
5613                bytes.extend_from_slice(&0xBCF0u16.to_le_bytes());
5614
5615                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5616                Ok(bytes)
5617            }
5618
5619            // I64DivS: 64-bit signed division
5620            // Converts to unsigned, divides, then applies sign
5621            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5622            //   ->  R0:R1 = quotient (signed)
5623            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5624            ArmOp::I64DivS {
5625                rdlo,
5626                rdhi,
5627                rnlo,
5628                rnhi,
5629                rmlo,
5630                rmhi,
5631                elide_zero_guard,
5632                elide_overflow_guard,
5633            } => {
5634                let mut bytes = Vec::new();
5635                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5636                // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
5637                // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
5638                // the #633 overflow guard falls ONLY to
5639                // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
5640                // divisor-nonzero fact alone must keep it.
5641                if !elide_zero_guard {
5642                    emit_i64_divisor_zero_trap(&mut bytes);
5643                }
5644                if !elide_overflow_guard {
5645                    // #633: INT64_MIN / -1 overflows — trap like the i32 path
5646                    // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
5647                    emit_i64_divs_overflow_trap(&mut bytes);
5648                }
5649
5650                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5651                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5652                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5653
5654                // Save result sign in R9: R9 = R1 XOR R3 (sign bit = MSB)
5655                // EOR.W R9, R1, R3
5656                bytes.extend_from_slice(&0xEA81u16.to_le_bytes());
5657                bytes.extend_from_slice(&0x0903u16.to_le_bytes());
5658
5659                // If dividend negative (R1 MSB set), negate it
5660                // TST R1, R1 (check sign)
5661                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5662                // BPL skip_neg_dividend (+10 bytes = 5 halfwords)
5663                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5664
5665                // Negate R0:R1 (64-bit): RSBS R0, R0, #0; SBC R1, R1, R1 LSL #1
5666                // Actually: MVN R0, R0; MVN R1, R1; ADDS R0, R0, #1; ADC R1, R1, #0
5667                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5668                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5669                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5670                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5671                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5672
5673                // If divisor negative (R3 MSB set), negate it
5674                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5675                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5676
5677                // Negate R2:R3
5678                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
5679                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
5680                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
5681                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
5682                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
5683
5684                // === Now do unsigned division (same as I64DivU) ===
5685                // Initialize quotient (R4:R5) = 0
5686                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5687                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5688                // Initialize remainder (R6:R7) = 0
5689                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5690                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5691                // Initialize loop counter R8 = 64
5692                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5693                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5694
5695                let loop_start = bytes.len();
5696
5697                // Shift quotient left
5698                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5699                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5700                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5701                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5702
5703                // Shift remainder left, OR in MSB of dividend
5704                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5705                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5706                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5707                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5708                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5709                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5710
5711                // Shift dividend left
5712                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5713                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5714                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5715                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5716
5717                // Compare and conditionally subtract
5718                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5719                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5720                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5721                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5722                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5723
5724                // Subtract and set quotient bit
5725                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5726                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5727                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5728                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5729                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5730
5731                // Decrement and loop
5732                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
5733                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
5734
5735                let branch_offset_bytes = bytes.len() - loop_start + 4;
5736                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5737                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5738                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5739
5740                // Move quotient to R0:R1
5741                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5742                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5743
5744                // If result should be negative (R9 MSB set), negate R0:R1
5745                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9 (check MSB)
5746                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
5747                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8 (skip negation)
5748
5749                // Negate result R0:R1
5750                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5751                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5752                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5753                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5754                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5755
5756                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
5757                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
5758                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5759
5760                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5761                Ok(bytes)
5762            }
5763
5764            // I64RemU: 64-bit unsigned remainder using binary long division
5765            // Same algorithm as I64DivU but returns remainder instead of quotient
5766            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = remainder
5767            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5768            ArmOp::I64RemU {
5769                rdlo,
5770                rdhi,
5771                rnlo,
5772                rnhi,
5773                rmlo,
5774                rmhi,
5775                elide_zero_guard,
5776            } => {
5777                let mut bytes = Vec::new();
5778                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5779                if !elide_zero_guard {
5780                    emit_i64_divisor_zero_trap(&mut bytes);
5781                }
5782
5783                // PUSH {R4-R8} - save scratch registers (NO LR — inline code)
5784                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5785                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
5786
5787                // Initialize quotient (R4:R5) = 0 (computed but not returned)
5788                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5789                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5790                // Initialize remainder (R6:R7) = 0
5791                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5792                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5793                // Initialize loop counter R8 = 64
5794                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5795                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5796
5797                let loop_start = bytes.len();
5798
5799                // Shift quotient left (not needed for result, but keeps algorithm same)
5800                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5801                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5802                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5803                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5804
5805                // Shift remainder left, OR in MSB of dividend
5806                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5807                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5808                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5809                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5810                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5811                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5812
5813                // Shift dividend left
5814                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5815                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5816                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5817                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5818
5819                // Compare and conditionally subtract
5820                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5821                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5822                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5823                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5824                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5825
5826                // Subtract and set quotient bit
5827                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5828                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5829                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5830                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5831                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5832
5833                // Decrement and loop
5834                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
5835                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
5836
5837                let branch_offset_bytes = bytes.len() - loop_start + 4;
5838                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5839                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5840                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5841
5842                // Move REMAINDER to R0:R1 (difference from I64DivU)
5843                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
5844                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
5845
5846                // POP {R4-R8} - restore scratch registers (NO PC — inline code continues)
5847                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
5848                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
5849
5850                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5851                Ok(bytes)
5852            }
5853
5854            // I64RemS: 64-bit signed remainder
5855            // Remainder sign follows dividend sign (not quotient rule)
5856            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5857            //   ->  R0:R1 = remainder (signed, same sign as dividend)
5858            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5859            ArmOp::I64RemS {
5860                rdlo,
5861                rdhi,
5862                rnlo,
5863                rnhi,
5864                rmlo,
5865                rmhi,
5866                elide_zero_guard,
5867            } => {
5868                let mut bytes = Vec::new();
5869                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5870                if !elide_zero_guard {
5871                    emit_i64_divisor_zero_trap(&mut bytes);
5872                }
5873
5874                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5875                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5876                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5877
5878                // Save dividend sign in R9 (remainder sign = dividend sign)
5879                // MOV R9, R1 (just need the sign bit)
5880                bytes.extend_from_slice(&0x4689u16.to_le_bytes()); // MOV R9, R1
5881
5882                // If dividend negative (R1 MSB set), negate it
5883                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5884                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5885
5886                // Negate R0:R1
5887                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5888                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5889                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5890                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5891                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5892
5893                // If divisor negative (R3 MSB set), negate it
5894                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5895                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5896
5897                // Negate R2:R3
5898                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
5899                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
5900                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
5901                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
5902                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
5903
5904                // === Unsigned division algorithm ===
5905                // Initialize quotient (R4:R5) = 0
5906                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5907                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5908                // Initialize remainder (R6:R7) = 0
5909                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5910                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5911                // Initialize loop counter R8 = 64
5912                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5913                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5914
5915                let loop_start = bytes.len();
5916
5917                // Shift quotient left
5918                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5919                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5920                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5921                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5922
5923                // Shift remainder left, OR in MSB of dividend
5924                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5925                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5926                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5927                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5928                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5929                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5930
5931                // Shift dividend left
5932                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5933                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5934                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5935                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5936
5937                // Compare and conditionally subtract
5938                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5939                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5940                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5941                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5942                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5943
5944                // Subtract and set quotient bit
5945                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5946                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5947                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5948                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5949                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5950
5951                // Decrement and loop
5952                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
5953                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
5954
5955                let branch_offset_bytes = bytes.len() - loop_start + 4;
5956                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5957                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5958                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5959
5960                // Move remainder to R0:R1
5961                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
5962                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
5963
5964                // If original dividend was negative (R9 MSB set), negate remainder
5965                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9
5966                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
5967                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5968
5969                // Negate result R0:R1
5970                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5971                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5972                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5973                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5974                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5975
5976                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
5977                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
5978                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5979
5980                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5981                Ok(bytes)
5982            }
5983
5984            // === F32 VFP single-precision Thumb-2 encodings ===
5985            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
5986            ArmOp::F32Add { sd, sn, sm } => {
5987                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A00, sd, sn, sm)?))
5988            }
5989            ArmOp::F32Sub { sd, sn, sm } => {
5990                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A40, sd, sn, sm)?))
5991            }
5992            ArmOp::F32Mul { sd, sn, sm } => {
5993                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE200A00, sd, sn, sm)?))
5994            }
5995            ArmOp::F32Div { sd, sn, sm } => {
5996                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE800A00, sd, sn, sm)?))
5997            }
5998            ArmOp::F32Abs { sd, sm } => {
5999                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB00AC0, sd, sm)?))
6000            }
6001            ArmOp::F32Neg { sd, sm } => {
6002                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10A40, sd, sm)?))
6003            }
6004            ArmOp::F32Sqrt { sd, sm } => {
6005                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10AC0, sd, sm)?))
6006            }
6007
6008            // f32 pseudo-ops — multi-instruction sequences
6009            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6010            ArmOp::F32Ceil { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b01),
6011            ArmOp::F32Floor { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b10),
6012            ArmOp::F32Trunc { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b11),
6013            ArmOp::F32Nearest { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b00),
6014            ArmOp::F32Min { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, true),
6015            ArmOp::F32Max { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, false),
6016            ArmOp::F32Copysign { sd, sn, sm } => self.encode_thumb_f32_copysign(sd, sn, sm),
6017
6018            // f32 comparisons — VCMP + VMRS + MOV #0 + IT + MOV #1
6019            ArmOp::F32Eq { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x0),
6020            ArmOp::F32Ne { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x1),
6021            ArmOp::F32Lt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x4),
6022            ArmOp::F32Le { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x9),
6023            ArmOp::F32Gt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xC),
6024            ArmOp::F32Ge { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xA),
6025
6026            ArmOp::F32Const { sd, value } => self.encode_thumb_f32_const(sd, *value),
6027
6028            ArmOp::F32Load { sd, addr } => {
6029                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED900A00, sd, addr)?))
6030            }
6031            ArmOp::F32Store { sd, addr } => {
6032                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED800A00, sd, addr)?))
6033            }
6034
6035            ArmOp::F32ConvertI32S { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, true),
6036            ArmOp::F32ConvertI32U { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, false),
6037            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
6038                Err(synth_core::Error::synthesis(
6039                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6040                ))
6041            }
6042            ArmOp::F32ReinterpretI32 { sd, rm } => {
6043                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(true, sd, rm)?))
6044            }
6045            ArmOp::I32ReinterpretF32 { rd, sm } => {
6046                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(false, sm, rd)?))
6047            }
6048            ArmOp::I32TruncF32S { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, true),
6049            ArmOp::I32TruncF32U { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, false),
6050
6051            // === F64 VFP double-precision Thumb-2 encodings ===
6052            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6053            ArmOp::F64Add { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6054                0xEE300B00, dd, dn, dm,
6055            )?)),
6056            ArmOp::F64Sub { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6057                0xEE300B40, dd, dn, dm,
6058            )?)),
6059            ArmOp::F64Mul { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6060                0xEE200B00, dd, dn, dm,
6061            )?)),
6062            ArmOp::F64Div { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6063                0xEE800B00, dd, dn, dm,
6064            )?)),
6065            ArmOp::F64Abs { dd, dm } => {
6066                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?))
6067            }
6068            ArmOp::F64Neg { dd, dm } => {
6069                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?))
6070            }
6071            ArmOp::F64Sqrt { dd, dm } => {
6072                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?))
6073            }
6074
6075            // f64 pseudo-ops
6076            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6077            ArmOp::F64Ceil { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b01),
6078            ArmOp::F64Floor { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b10),
6079            ArmOp::F64Trunc { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b11),
6080            ArmOp::F64Nearest { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b00),
6081            ArmOp::F64Min { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, true),
6082            ArmOp::F64Max { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, false),
6083            ArmOp::F64Copysign { dd, dn, dm } => self.encode_thumb_f64_copysign(dd, dn, dm),
6084
6085            // f64 comparisons
6086            ArmOp::F64Eq { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x0),
6087            ArmOp::F64Ne { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x1),
6088            ArmOp::F64Lt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x4),
6089            ArmOp::F64Le { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x9),
6090            ArmOp::F64Gt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xC),
6091            ArmOp::F64Ge { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xA),
6092
6093            ArmOp::F64Const { dd, value } => self.encode_thumb_f64_const(dd, *value),
6094
6095            ArmOp::F64Load { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6096                0xED900B00, dd, addr,
6097            )?)),
6098            ArmOp::F64Store { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6099                0xED800B00, dd, addr,
6100            )?)),
6101
6102            ArmOp::F64ConvertI32S { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, true),
6103            ArmOp::F64ConvertI32U { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, false),
6104            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
6105                Err(synth_core::Error::synthesis(
6106                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6107                ))
6108            }
6109            ArmOp::F64PromoteF32 { dd, sm } => self.encode_thumb_f64_promote_f32(dd, sm),
6110            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => Ok(vfp_to_thumb_bytes(
6111                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?,
6112            )),
6113            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => Ok(vfp_to_thumb_bytes(
6114                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?,
6115            )),
6116            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
6117                Err(synth_core::Error::synthesis(
6118                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
6119                ))
6120            }
6121            ArmOp::I32TruncF64S { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, true),
6122            ArmOp::I32TruncF64U { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, false),
6123
6124            // ===== i64 operations: encode as multi-instruction Thumb-2 sequences =====
6125
6126            // I64Add: ADDS rdlo, rnlo, rmlo; ADC.W rdhi, rnhi, rmhi
6127            ArmOp::I64Add {
6128                rdlo,
6129                rdhi,
6130                rnlo,
6131                rnhi,
6132                rmlo,
6133                rmhi,
6134            } => {
6135                let mut bytes = Vec::new();
6136                // ADDS rdlo, rnlo, rmlo (16-bit)
6137                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adds {
6138                    rd: *rdlo,
6139                    rn: *rnlo,
6140                    op2: Operand2::Reg(*rmlo),
6141                })?);
6142                // ADC.W rdhi, rnhi, rmhi (32-bit)
6143                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adc {
6144                    rd: *rdhi,
6145                    rn: *rnhi,
6146                    op2: Operand2::Reg(*rmhi),
6147                })?);
6148                Ok(bytes)
6149            }
6150
6151            // I64Sub: SUBS rdlo, rnlo, rmlo; SBC.W rdhi, rnhi, rmhi
6152            ArmOp::I64Sub {
6153                rdlo,
6154                rdhi,
6155                rnlo,
6156                rnhi,
6157                rmlo,
6158                rmhi,
6159            } => {
6160                let mut bytes = Vec::new();
6161                // SUBS rdlo, rnlo, rmlo (16-bit)
6162                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Subs {
6163                    rd: *rdlo,
6164                    rn: *rnlo,
6165                    op2: Operand2::Reg(*rmlo),
6166                })?);
6167                // SBC.W rdhi, rnhi, rmhi (32-bit)
6168                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Sbc {
6169                    rd: *rdhi,
6170                    rn: *rnhi,
6171                    op2: Operand2::Reg(*rmhi),
6172                })?);
6173                Ok(bytes)
6174            }
6175
6176            // I64And: AND rdlo, rnlo, rmlo; AND rdhi, rnhi, rmhi
6177            ArmOp::I64And {
6178                rdlo,
6179                rdhi,
6180                rnlo,
6181                rnhi,
6182                rmlo,
6183                rmhi,
6184            } => {
6185                let mut bytes = Vec::new();
6186                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6187                    rd: *rdlo,
6188                    rn: *rnlo,
6189                    op2: Operand2::Reg(*rmlo),
6190                })?);
6191                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6192                    rd: *rdhi,
6193                    rn: *rnhi,
6194                    op2: Operand2::Reg(*rmhi),
6195                })?);
6196                Ok(bytes)
6197            }
6198
6199            // I64Or: ORR rdlo, rnlo, rmlo; ORR rdhi, rnhi, rmhi
6200            ArmOp::I64Or {
6201                rdlo,
6202                rdhi,
6203                rnlo,
6204                rnhi,
6205                rmlo,
6206                rmhi,
6207            } => {
6208                let mut bytes = Vec::new();
6209                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6210                    rd: *rdlo,
6211                    rn: *rnlo,
6212                    op2: Operand2::Reg(*rmlo),
6213                })?);
6214                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6215                    rd: *rdhi,
6216                    rn: *rnhi,
6217                    op2: Operand2::Reg(*rmhi),
6218                })?);
6219                Ok(bytes)
6220            }
6221
6222            // I64Xor: EOR rdlo, rnlo, rmlo; EOR rdhi, rnhi, rmhi
6223            ArmOp::I64Xor {
6224                rdlo,
6225                rdhi,
6226                rnlo,
6227                rnhi,
6228                rmlo,
6229                rmhi,
6230            } => {
6231                let mut bytes = Vec::new();
6232                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6233                    rd: *rdlo,
6234                    rn: *rnlo,
6235                    op2: Operand2::Reg(*rmlo),
6236                })?);
6237                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6238                    rd: *rdhi,
6239                    rn: *rnhi,
6240                    op2: Operand2::Reg(*rmhi),
6241                })?);
6242                Ok(bytes)
6243            }
6244
6245            // I64Eqz: ORR scratch, lo, hi; ITE EQ; MOV rd, #1; MOV rd, #0
6246            ArmOp::I64Eqz { rd, rnlo, rnhi } => self.encode_thumb(&ArmOp::I64SetCondZ {
6247                rd: *rd,
6248                rn_lo: *rnlo,
6249                rn_hi: *rnhi,
6250            }),
6251
6252            // I64 comparisons: delegate to I64SetCond
6253            ArmOp::I64Eq {
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::EQ,
6266            }),
6267
6268            ArmOp::I64Ne {
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::NE,
6281            }),
6282
6283            ArmOp::I64LtS {
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::LT,
6296            }),
6297
6298            ArmOp::I64LtU {
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::LO,
6311            }),
6312
6313            ArmOp::I64LeS {
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::LE,
6326            }),
6327
6328            ArmOp::I64LeU {
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::LS,
6341            }),
6342
6343            ArmOp::I64GtS {
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::GT,
6356            }),
6357
6358            ArmOp::I64GtU {
6359                rd,
6360                rnlo,
6361                rnhi,
6362                rmlo,
6363                rmhi,
6364            } => self.encode_thumb(&ArmOp::I64SetCond {
6365                rd: *rd,
6366                rn_lo: *rnlo,
6367                rn_hi: *rnhi,
6368                rm_lo: *rmlo,
6369                rm_hi: *rmhi,
6370                cond: synth_synthesis::Condition::HI,
6371            }),
6372
6373            ArmOp::I64GeS {
6374                rd,
6375                rnlo,
6376                rnhi,
6377                rmlo,
6378                rmhi,
6379            } => self.encode_thumb(&ArmOp::I64SetCond {
6380                rd: *rd,
6381                rn_lo: *rnlo,
6382                rn_hi: *rnhi,
6383                rm_lo: *rmlo,
6384                rm_hi: *rmhi,
6385                cond: synth_synthesis::Condition::GE,
6386            }),
6387
6388            ArmOp::I64GeU {
6389                rd,
6390                rnlo,
6391                rnhi,
6392                rmlo,
6393                rmhi,
6394            } => self.encode_thumb(&ArmOp::I64SetCond {
6395                rd: *rd,
6396                rn_lo: *rnlo,
6397                rn_hi: *rnhi,
6398                rm_lo: *rmlo,
6399                rm_hi: *rmhi,
6400                cond: synth_synthesis::Condition::HS,
6401            }),
6402
6403            // I64Const: MOVW rdlo, lo16; MOVT rdlo, hi16; MOVW rdhi, lo16_hi; MOVT rdhi, hi16_hi
6404            ArmOp::I64Const { rdlo, rdhi, value } => {
6405                let lo32 = *value as u32;
6406                let hi32 = (*value >> 32) as u32;
6407                let mut bytes = Vec::new();
6408                // Load low 32 bits into rdlo
6409                bytes.extend_from_slice(
6410                    &self.encode_thumb32_movw_raw(reg_to_bits(rdlo), lo32 & 0xFFFF)?,
6411                );
6412                if lo32 > 0xFFFF {
6413                    bytes.extend_from_slice(
6414                        &self.encode_thumb32_movt_raw(reg_to_bits(rdlo), lo32 >> 16)?,
6415                    );
6416                }
6417                // Load high 32 bits into rdhi
6418                bytes.extend_from_slice(
6419                    &self.encode_thumb32_movw_raw(reg_to_bits(rdhi), hi32 & 0xFFFF)?,
6420                );
6421                if hi32 > 0xFFFF {
6422                    bytes.extend_from_slice(
6423                        &self.encode_thumb32_movt_raw(reg_to_bits(rdhi), hi32 >> 16)?,
6424                    );
6425                }
6426                Ok(bytes)
6427            }
6428
6429            // I64Ldr: LDR rdlo, [base, offset]; LDR rdhi, [base, offset+4]
6430            ArmOp::I64Ldr { rdlo, rdhi, addr } => {
6431                let mut bytes = Vec::new();
6432                // #372/#382: a memory `i64.load` carries an index register
6433                // (`reg_imm(R11, addr_reg, offset)` = R11 + addr + offset). The
6434                // immediate `encode_thumb32_ldr` below uses only base+offset and
6435                // would SILENTLY DROP `offset_reg` — the #206 defect, here for
6436                // i64. `i64_effective_base` materializes the effective base into
6437                // `ip` (and, when `offset+4 > 0xFFF`, folds the offset in too so
6438                // the function is NOT skipped — #382), returning the residual
6439                // imm12 for the two halves. Frame i64 loads (no `offset_reg`, e.g.
6440                // a spilled local at `[SP, #off]`) keep the plain `[base,#off]`
6441                // form unchanged — so existing output is byte-identical.
6442                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6443                bytes.extend_from_slice(&self.encode_thumb32_ldr(rdlo, &base, offset)?);
6444                bytes.extend_from_slice(&self.encode_thumb32_ldr(
6445                    rdhi,
6446                    &base,
6447                    offset.wrapping_add(4),
6448                )?);
6449                Ok(bytes)
6450            }
6451
6452            // I64Str: STR rdlo, [base, offset]; STR rdhi, [base, offset+4]
6453            ArmOp::I64Str { rdlo, rdhi, addr } => {
6454                let mut bytes = Vec::new();
6455                // #372/#382: same index-materialization + large-offset fold as
6456                // I64Ldr (see above).
6457                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6458                bytes.extend_from_slice(&self.encode_thumb32_str(rdlo, &base, offset)?);
6459                bytes.extend_from_slice(&self.encode_thumb32_str(
6460                    rdhi,
6461                    &base,
6462                    offset.wrapping_add(4),
6463                )?);
6464                Ok(bytes)
6465            }
6466
6467            // I64ExtendI32S: MOV rdlo, rn; ASR rdhi, rdlo, #31 (sign-extend)
6468            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
6469                let mut bytes = Vec::new();
6470                if rdlo != rn {
6471                    // MOV rdlo, rn (16-bit)
6472                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6473                        rd: *rdlo,
6474                        op2: Operand2::Reg(*rn),
6475                    })?);
6476                }
6477                // ASR rdhi, rdlo, #31 (sign-extend: fill high word with sign bit)
6478                bytes.extend_from_slice(
6479                    &self.encode_thumb32_shift(rdhi, rdlo, 31, 0b10)?, // ASR type
6480                );
6481                Ok(bytes)
6482            }
6483
6484            // I64ExtendI32U: MOV rdlo, rn; MOV rdhi, #0
6485            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
6486                let mut bytes = Vec::new();
6487                if rdlo != rn {
6488                    // MOV rdlo, rn
6489                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6490                        rd: *rdlo,
6491                        op2: Operand2::Reg(*rn),
6492                    })?);
6493                }
6494                // MOV rdhi, #0 (16-bit: MOVS Rd, #0)
6495                let rdhi_bits = reg_to_bits(rdhi) as u16;
6496                let instr: u16 = 0x2000 | (rdhi_bits << 8);
6497                bytes.extend_from_slice(&instr.to_le_bytes());
6498                Ok(bytes)
6499            }
6500
6501            // I32WrapI64: MOV rd, rnlo (just take low 32 bits)
6502            ArmOp::I32WrapI64 { rd, rnlo } => {
6503                if rd == rnlo {
6504                    // No-op: already in the right register
6505                    let instr: u16 = 0xBF00; // NOP
6506                    Ok(instr.to_le_bytes().to_vec())
6507                } else {
6508                    // MOV rd, rnlo
6509                    self.encode_thumb(&ArmOp::Mov {
6510                        rd: *rd,
6511                        op2: Operand2::Reg(*rnlo),
6512                    })
6513                }
6514            }
6515
6516            // ===== Helium MVE operations (Thumb-2 encoding) =====
6517            ArmOp::MveLoad { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vldrw(qd, addr))),
6518            ArmOp::MveStore { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vstrw(qd, addr))),
6519            ArmOp::MveConst { qd, bytes } => self.encode_thumb_mve_const(qd, bytes),
6520            ArmOp::MveAnd { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6521                0xEF000150, qd, qn, qm,
6522            ))),
6523            ArmOp::MveOrr { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6524                0xEF200150, qd, qn, qm,
6525            ))),
6526            ArmOp::MveEor { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6527                0xFF000150, qd, qn, qm,
6528            ))),
6529            ArmOp::MveMvn { qd, qm } => {
6530                // VMVN Qd, Qm: 0xFFB005C0 | Qd<<12 | Qm
6531                let qd_enc = qreg_to_num(qd);
6532                let qm_enc = qreg_to_num(qm);
6533                let instr: u32 = 0xFFB005C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6534                Ok(vfp_to_thumb_bytes(instr))
6535            }
6536            ArmOp::MveBic { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6537                0xEF100150, qd, qn, qm,
6538            ))),
6539            ArmOp::MveAddI { qd, qn, qm, size } => {
6540                let sz = mve_size_bits(size);
6541                let base: u32 = 0xEF000840 | (sz << 20);
6542                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6543            }
6544            ArmOp::MveSubI { qd, qn, qm, size } => {
6545                let sz = mve_size_bits(size);
6546                let base: u32 = 0xFF000840 | (sz << 20);
6547                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6548            }
6549            ArmOp::MveMulI { qd, qn, qm, size } => {
6550                let sz = mve_size_bits(size);
6551                let base: u32 = 0xEF000950 | (sz << 20);
6552                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6553            }
6554            ArmOp::MveNegI { qd, qm, size } => {
6555                let sz = mve_size_bits(size);
6556                // VNEG.Sx Qd, Qm
6557                let qd_enc = qreg_to_num(qd);
6558                let qm_enc = qreg_to_num(qm);
6559                let base: u32 = 0xFFB103C0 | (sz << 18);
6560                let instr = base | ((qd_enc * 2) << 12) | (qm_enc * 2);
6561                Ok(vfp_to_thumb_bytes(instr))
6562            }
6563            ArmOp::MveDup { qd, rn, size } => {
6564                let sz = mve_size_bits(size);
6565                let qd_enc = qreg_to_num(qd);
6566                let rn_bits = reg_to_bits(rn);
6567                // VDUP.sz Qd, Rn: EEA0 0B10 variant
6568                // size encoding: 00=32, 01=16, 10=8
6569                let be = match sz {
6570                    0 => 0b00u32, // 8-bit
6571                    1 => 0b01,    // 16-bit
6572                    _ => 0b00,    // 32-bit (default)
6573                };
6574                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12) | (be << 5);
6575                Ok(vfp_to_thumb_bytes(instr))
6576            }
6577            ArmOp::MveExtractLane { rd, qn, lane, size } => {
6578                let qn_enc = qreg_to_num(qn);
6579                let rd_bits = reg_to_bits(rd);
6580                // VMOV.sz Rd, Dn[x] — extract from Q-register lane
6581                // For 32-bit: VMOV Rd, Dn — where Dn is the appropriate D-register
6582                let d_reg = qn_enc * 2 + ((*lane as u32) >> 1);
6583                let lane_in_d = (*lane as u32) & 1;
6584                let _sz = mve_size_bits(size);
6585                // VMOV Rd, Dn[x]: EE10 0B10 for 32-bit
6586                let instr: u32 = 0xEE100B10 | (d_reg << 16) | (rd_bits << 12) | (lane_in_d << 21);
6587                Ok(vfp_to_thumb_bytes(instr))
6588            }
6589            ArmOp::MveInsertLane { qd, rn, lane, size } => {
6590                let qd_enc = qreg_to_num(qd);
6591                let rn_bits = reg_to_bits(rn);
6592                let d_reg = qd_enc * 2 + ((*lane as u32) >> 1);
6593                let lane_in_d = (*lane as u32) & 1;
6594                let _sz = mve_size_bits(size);
6595                // VMOV Dn[x], Rn: EE00 0B10 for 32-bit
6596                let instr: u32 = 0xEE000B10 | (d_reg << 16) | (rn_bits << 12) | (lane_in_d << 21);
6597                Ok(vfp_to_thumb_bytes(instr))
6598            }
6599
6600            // MVE float comparisons — emit VCMP + VPSEL sequence (simplified: just VCMP)
6601            ArmOp::MveCmpEqI { qd, qn, qm, size }
6602            | ArmOp::MveCmpNeI { qd, qn, qm, size }
6603            | ArmOp::MveCmpLtS { qd, qn, qm, size }
6604            | ArmOp::MveCmpLtU { qd, qn, qm, size }
6605            | ArmOp::MveCmpGtS { qd, qn, qm, size }
6606            | ArmOp::MveCmpGtU { qd, qn, qm, size }
6607            | ArmOp::MveCmpLeS { qd, qn, qm, size }
6608            | ArmOp::MveCmpLeU { qd, qn, qm, size }
6609            | ArmOp::MveCmpGeS { qd, qn, qm, size }
6610            | ArmOp::MveCmpGeU { qd, qn, qm, size } => {
6611                // Encode as VADD (placeholder encoding — real implementation
6612                // would use VCMP + VPSEL pair)
6613                let sz = mve_size_bits(size);
6614                let base: u32 = 0xEF000840 | (sz << 20);
6615                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6616            }
6617
6618            // f32x4 MVE arithmetic
6619            ArmOp::MveAddF32 { qd, qn, qm } => {
6620                // VADD.F32 Qd, Qn, Qm (MVE): 0xEF000D40
6621                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6622            }
6623            ArmOp::MveSubF32 { qd, qn, qm } => {
6624                // VSUB.F32 Qd, Qn, Qm (MVE): 0xEF200D40
6625                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF200D40, qd, qn, qm)))
6626            }
6627            ArmOp::MveMulF32 { qd, qn, qm } => {
6628                // VMUL.F32 Qd, Qn, Qm (MVE): 0xFF000D50
6629                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xFF000D50, qd, qn, qm)))
6630            }
6631            ArmOp::MveNegF32 { qd, qm } => {
6632                let qd_enc = qreg_to_num(qd);
6633                let qm_enc = qreg_to_num(qm);
6634                // VNEG.F32 Qd, Qm: FFB907C0
6635                let instr: u32 = 0xFFB907C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6636                Ok(vfp_to_thumb_bytes(instr))
6637            }
6638            ArmOp::MveAbsF32 { qd, qm } => {
6639                let qd_enc = qreg_to_num(qd);
6640                let qm_enc = qreg_to_num(qm);
6641                // VABS.F32 Qd, Qm: FFB90740
6642                let instr: u32 = 0xFFB90740 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6643                Ok(vfp_to_thumb_bytes(instr))
6644            }
6645            ArmOp::MveCmpEqF32 { qd, qn, qm }
6646            | ArmOp::MveCmpNeF32 { qd, qn, qm }
6647            | ArmOp::MveCmpLtF32 { qd, qn, qm }
6648            | ArmOp::MveCmpLeF32 { qd, qn, qm }
6649            | ArmOp::MveCmpGtF32 { qd, qn, qm }
6650            | ArmOp::MveCmpGeF32 { qd, qn, qm } => {
6651                // Placeholder: encode as VADD.F32 (real impl needs VCMP.F32 + VPSEL)
6652                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6653            }
6654            ArmOp::MveDupF32 { qd, rn } => {
6655                let qd_enc = qreg_to_num(qd);
6656                let rn_bits = reg_to_bits(rn);
6657                // VDUP.32 Qd, Rn (same encoding as integer VDUP.32)
6658                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12);
6659                Ok(vfp_to_thumb_bytes(instr))
6660            }
6661            ArmOp::MveExtractLaneF32 { rd, qn, lane } => {
6662                let qn_enc = qreg_to_num(qn);
6663                let rd_bits = reg_to_bits(rd);
6664                // VMOV Rd, Sn where Sn = Q*4 + lane
6665                let s_num = qn_enc * 4 + (*lane as u32);
6666                let (vn, n) = encode_sreg(s_num);
6667                let instr: u32 = 0xEE100A10 | (vn << 16) | (rd_bits << 12) | (n << 7);
6668                Ok(vfp_to_thumb_bytes(instr))
6669            }
6670            ArmOp::MveReplaceLaneF32 { qd, rn, lane } => {
6671                let qd_enc = qreg_to_num(qd);
6672                let rn_bits = reg_to_bits(rn);
6673                // VMOV Sn, Rn where Sn = Q*4 + lane
6674                let s_num = qd_enc * 4 + (*lane as u32);
6675                let (vn, n) = encode_sreg(s_num);
6676                let instr: u32 = 0xEE000A10 | (vn << 16) | (rn_bits << 12) | (n << 7);
6677                Ok(vfp_to_thumb_bytes(instr))
6678            }
6679            ArmOp::MveDivF32 { qd, qn, qm } => {
6680                // Lane-wise: extract 4 S-regs, VDIV, insert back
6681                self.encode_thumb_mve_lane_wise_f32_binop(qd, qn, qm, 0xEE800A00)
6682            }
6683            ArmOp::MveSqrtF32 { qd, qm } => {
6684                // Lane-wise: extract 4 S-regs, VSQRT, insert back
6685                self.encode_thumb_mve_lane_wise_f32_sqrt(qd, qm)
6686            }
6687
6688            // Catch-all for any remaining ops
6689            _ => {
6690                let instr: u16 = 0xBF00; // NOP
6691                Ok(instr.to_le_bytes().to_vec())
6692            }
6693        }
6694    }
6695
6696    // === Thumb-2 VFP multi-instruction helpers ===
6697
6698    /// Encode F32 comparison as Thumb-2: VCMP.F32 + VMRS + MOVS rd,#0 + IT + MOV rd,#1
6699    fn encode_thumb_f32_compare(
6700        &self,
6701        rd: &Reg,
6702        sn: &VfpReg,
6703        sm: &VfpReg,
6704        cond_code: u32,
6705    ) -> Result<Vec<u8>> {
6706        let mut bytes = Vec::new();
6707        let rd_bits = reg_to_bits(rd);
6708
6709        // VCMP.F32 Sn, Sm
6710        let sn_num = vfp_sreg_to_num(sn)?;
6711        let sm_num = vfp_sreg_to_num(sm)?;
6712        let (vd, d) = encode_sreg(sn_num);
6713        let (vm, m) = encode_sreg(sm_num);
6714        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
6715        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
6716
6717        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
6718        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
6719
6720        // MOVS Rd, #0 (16-bit): 0010 0 Rd(3) 0000 0000
6721        if rd_bits < 8 {
6722            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
6723            bytes.extend_from_slice(&movs_zero.to_le_bytes());
6724        } else {
6725            // MOV.W Rd, #0 (32-bit Thumb-2)
6726            let hw1: u16 = 0xF04F;
6727            let hw2: u16 = (rd_bits as u16) << 8;
6728            bytes.extend_from_slice(&hw1.to_le_bytes());
6729            bytes.extend_from_slice(&hw2.to_le_bytes());
6730        }
6731
6732        // IT<cond> — If-Then for conditional MOV
6733        // IT encoding: 1011 1111 cond(4) mask(4)
6734        // mask = 0x8 for single "then" (IT)
6735        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
6736        bytes.extend_from_slice(&it.to_le_bytes());
6737
6738        // MOV Rd, #1 (16-bit, conditional due to IT): 0010 0 Rd(3) 0000 0001
6739        if rd_bits < 8 {
6740            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
6741            bytes.extend_from_slice(&mov_one.to_le_bytes());
6742        } else {
6743            // MOV.W Rd, #1 (32-bit)
6744            let hw1: u16 = 0xF04F;
6745            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
6746            bytes.extend_from_slice(&hw1.to_le_bytes());
6747            bytes.extend_from_slice(&hw2.to_le_bytes());
6748        }
6749
6750        Ok(bytes)
6751    }
6752
6753    /// Encode F32 constant load as Thumb-2: MOVW + MOVT + VMOV
6754    fn encode_thumb_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
6755        let mut bytes = Vec::new();
6756        let bits = value.to_bits();
6757        let rt: u32 = 12; // R12/IP as temp
6758
6759        // MOVW R12, #lo16
6760        // Thumb-2 MOVW: 11110 i 10 0100 imm4 | 0 imm3 Rd imm8
6761        let lo16 = bits & 0xFFFF;
6762        let imm4 = (lo16 >> 12) & 0xF;
6763        let i_bit = (lo16 >> 11) & 1;
6764        let imm3 = (lo16 >> 8) & 0x7;
6765        let imm8 = lo16 & 0xFF;
6766        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
6767        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
6768        bytes.extend_from_slice(&hw1.to_le_bytes());
6769        bytes.extend_from_slice(&hw2.to_le_bytes());
6770
6771        // MOVT R12, #hi16
6772        let hi16 = (bits >> 16) & 0xFFFF;
6773        let imm4 = (hi16 >> 12) & 0xF;
6774        let i_bit = (hi16 >> 11) & 1;
6775        let imm3 = (hi16 >> 8) & 0x7;
6776        let imm8 = hi16 & 0xFF;
6777        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
6778        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
6779        bytes.extend_from_slice(&hw1.to_le_bytes());
6780        bytes.extend_from_slice(&hw2.to_le_bytes());
6781
6782        // VMOV Sd, R12
6783        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
6784        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6785
6786        Ok(bytes)
6787    }
6788
6789    /// Encode VMOV + VCVT.F32.xS32 as Thumb-2
6790    fn encode_thumb_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
6791        let mut bytes = Vec::new();
6792
6793        // VMOV Sd, Rm
6794        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
6795        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6796
6797        // VCVT.F32.S32/U32 Sd, Sd
6798        let sd_num = vfp_sreg_to_num(sd)?;
6799        let (vd, d) = encode_sreg(sd_num);
6800        let (vm, m) = encode_sreg(sd_num);
6801        let base = if signed { 0xEEB80A40 } else { 0xEEB80AC0 };
6802        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
6803        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
6804
6805        Ok(bytes)
6806    }
6807
6808    /// Encode F32 rounding pseudo-op as Thumb-2 via VCVT to integer and back
6809    /// Encode F32 rounding as Thumb-2.
6810    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
6811    ///
6812    /// For trunc: uses VCVTR.S32.F32 (always truncates).
6813    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant),
6814    /// then restores FPSCR.
6815    fn encode_thumb_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
6816        let mut bytes = Vec::new();
6817        let sm_num = vfp_sreg_to_num(sm)?;
6818        let sd_num = vfp_sreg_to_num(sd)?;
6819        let (vd_s, d_s) = encode_sreg(sd_num);
6820        let (vm_s, m_s) = encode_sreg(sm_num);
6821
6822        if mode == 0b11 {
6823            // Trunc (toward zero): VCVTR.S32.F32 — bit[7]=1, always truncates
6824            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
6825            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
6826        } else {
6827            // ceil/floor/nearest: manipulate FPSCR rounding mode
6828            let rt: u32 = 12; // R12/IP as temp
6829
6830            // VMRS R12, FPSCR
6831            let vmrs = 0xEEF10A10 | (rt << 12);
6832            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
6833
6834            // BIC.W R12, R12, #(3 << 22) — clear RMode bits [23:22]
6835            // Thumb-2 modified immediate for 3<<22 = 0x00C00000:
6836            // BIC.W encoding: 11110 i 0 0001 S Rn | 0 imm3 Rd imm8
6837            // 0x00C00000 = 0x03 shifted left by 22 => Thumb mod-imm: i=0, imm3=0b101, imm8=0x03
6838            let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF); // BIC, Rn=R12
6839            let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
6840            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
6841            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
6842
6843            // ORR.W R12, R12, #(mode << 22)
6844            if mode != 0 {
6845                let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF); // ORR, Rn=R12
6846                let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
6847                bytes.extend_from_slice(&orr_hw1.to_le_bytes());
6848                bytes.extend_from_slice(&orr_hw2.to_le_bytes());
6849            }
6850
6851            // VMSR FPSCR, R12
6852            let vmsr = 0xEEE10A10 | (rt << 12);
6853            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
6854
6855            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rmode
6856            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
6857            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
6858
6859            // Restore FPSCR: clear rmode bits back to nearest (default)
6860            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
6861            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
6862            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
6863            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
6864        }
6865
6866        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
6867        let (vd2, d2) = encode_sreg(sd_num);
6868        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
6869        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
6870
6871        Ok(bytes)
6872    }
6873
6874    /// Encode F32 min/max as Thumb-2: VMOV + VCMP + VMRS + IT + VMOV
6875    fn encode_thumb_f32_minmax(
6876        &self,
6877        sd: &VfpReg,
6878        sn: &VfpReg,
6879        sm: &VfpReg,
6880        is_min: bool,
6881    ) -> Result<Vec<u8>> {
6882        let mut bytes = Vec::new();
6883        let sn_num = vfp_sreg_to_num(sn)?;
6884        let sm_num = vfp_sreg_to_num(sm)?;
6885        let sd_num = vfp_sreg_to_num(sd)?;
6886
6887        // VMOV.F32 Sd, Sn
6888        let (vd, d) = encode_sreg(sd_num);
6889        let (vn, n) = encode_sreg(sn_num);
6890        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
6891        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sn));
6892
6893        // VCMP.F32 Sn, Sm
6894        let (vm, m) = encode_sreg(sm_num);
6895        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
6896        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
6897
6898        // VMRS APSR_nzcv, FPSCR
6899        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
6900
6901        // IT GT (for min) or IT MI (for max)
6902        let cond: u16 = if is_min { 0xC } else { 0x4 };
6903        let it: u16 = 0xBF00 | (cond << 4) | 0x8;
6904        bytes.extend_from_slice(&it.to_le_bytes());
6905
6906        // VMOV{cond}.F32 Sd, Sm — conditional VMOV in IT block
6907        let vmov_sm = 0xEEB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
6908        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sm));
6909
6910        Ok(bytes)
6911    }
6912
6913    /// Encode F32 copysign as Thumb-2
6914    fn encode_thumb_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
6915        let mut bytes = Vec::new();
6916
6917        // VMOV R12, Sm (get sign source bits)
6918        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
6919            false,
6920            sm,
6921            &Reg::R12,
6922        )?));
6923
6924        // VMOV R0, Sn (get magnitude source bits)
6925        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
6926            false,
6927            sn,
6928            &Reg::R0,
6929        )?));
6930
6931        // AND.W R12, R12, #0x80000000
6932        // Thumb-2 modified immediate: 0x80000000 = constant 0x80 with rotation
6933        // Using T1 encoding: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8
6934        // 0x80000000: i=0, imm3=0b001, imm8=0x00 (rotation=4, value=0x80)
6935        // Actually encoding #0x80000000 as modified constant:
6936        // bit pattern 1 followed by 31 zeros: enc = 0b0100_00000000 = 0x0100? No.
6937        // ARM modified immediate: abcdefgh rotated. 0x80000000 = 0x80 ROR 2 = enc 0x0102
6938        // Actually: value = abcdefgh ROR (2*rot). 0x80 = 10000000, ROR 2 gives 0x20000000.
6939        // For 0x80000000: 0x02 ROR 2 = 0x80000000. So imm12 = (1<<8) | 0x02 = 0x102
6940        let hw1: u16 = 0xF000 | 12; // AND.W R12, R12, #modified_const (i=0, Rn=R12)
6941        let hw2: u16 = (0x1 << 12) | (12 << 8) | 0x02; // imm3=1, Rd=R12, imm8=0x02
6942        bytes.extend_from_slice(&hw1.to_le_bytes());
6943        bytes.extend_from_slice(&hw2.to_le_bytes());
6944
6945        // BIC.W R0, R0, #0x80000000 (R0 = register 0, fields are zero)
6946        let hw1: u16 = 0xF020; // BIC.W R0, R0, #modified_const (i=0, Rn=R0)
6947        let hw2: u16 = (0x1 << 12) | 0x02; // imm3=1, Rd=R0, imm8=0x02
6948        bytes.extend_from_slice(&hw1.to_le_bytes());
6949        bytes.extend_from_slice(&hw2.to_le_bytes());
6950
6951        // ORR.W R0, R0, R12 (R0 = register 0)
6952        let hw1: u16 = 0xEA40; // ORR.W R0, R0, R12 (Rn=R0)
6953        let hw2: u16 = 12; // Rd=R0, Rm=R12
6954        bytes.extend_from_slice(&hw1.to_le_bytes());
6955        bytes.extend_from_slice(&hw2.to_le_bytes());
6956
6957        // VMOV Sd, R0
6958        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
6959            true,
6960            sd,
6961            &Reg::R0,
6962        )?));
6963
6964        Ok(bytes)
6965    }
6966
6967    /// Encode F64 comparison as Thumb-2: VCMP.F64 + VMRS + MOV #0 + IT + MOV #1
6968    fn encode_thumb_f64_compare(
6969        &self,
6970        rd: &Reg,
6971        dn: &VfpReg,
6972        dm: &VfpReg,
6973        cond_code: u32,
6974    ) -> Result<Vec<u8>> {
6975        let mut bytes = Vec::new();
6976        let rd_bits = reg_to_bits(rd);
6977
6978        // VCMP.F64 Dn, Dm
6979        let dn_num = vfp_dreg_to_num(dn)?;
6980        let dm_num = vfp_dreg_to_num(dm)?;
6981        let (vd, d) = encode_dreg(dn_num);
6982        let (vm, m) = encode_dreg(dm_num);
6983        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
6984        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
6985
6986        // VMRS APSR_nzcv, FPSCR
6987        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
6988
6989        // MOVS Rd, #0
6990        if rd_bits < 8 {
6991            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
6992            bytes.extend_from_slice(&movs_zero.to_le_bytes());
6993        } else {
6994            let hw1: u16 = 0xF04F;
6995            let hw2: u16 = (rd_bits as u16) << 8;
6996            bytes.extend_from_slice(&hw1.to_le_bytes());
6997            bytes.extend_from_slice(&hw2.to_le_bytes());
6998        }
6999
7000        // IT<cond>
7001        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7002        bytes.extend_from_slice(&it.to_le_bytes());
7003
7004        // MOV Rd, #1
7005        if rd_bits < 8 {
7006            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7007            bytes.extend_from_slice(&mov_one.to_le_bytes());
7008        } else {
7009            let hw1: u16 = 0xF04F;
7010            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7011            bytes.extend_from_slice(&hw1.to_le_bytes());
7012            bytes.extend_from_slice(&hw2.to_le_bytes());
7013        }
7014
7015        Ok(bytes)
7016    }
7017
7018    /// Encode F64 constant load as Thumb-2: MOVW+MOVT (lo32 into R0) + MOVW+MOVT (hi32 into R12) + VMOV Dd, R0, R12
7019    fn encode_thumb_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
7020        let mut bytes = Vec::new();
7021        let bits = value.to_bits();
7022        let lo32 = bits as u32;
7023        let hi32 = (bits >> 32) as u32;
7024
7025        // MOVW R0, #lo16(lo32)
7026        let lo16 = lo32 & 0xFFFF;
7027        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(0, lo16)?);
7028
7029        // MOVT R0, #hi16(lo32)
7030        let hi16 = (lo32 >> 16) & 0xFFFF;
7031        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(0, hi16)?);
7032
7033        // MOVW R12, #lo16(hi32)
7034        let lo16 = hi32 & 0xFFFF;
7035        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
7036
7037        // MOVT R12, #hi16(hi32)
7038        let hi16 = (hi32 >> 16) & 0xFFFF;
7039        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
7040
7041        // VMOV Dd, R0, R12
7042        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
7043        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7044
7045        Ok(bytes)
7046    }
7047
7048    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as Thumb-2
7049    fn encode_thumb_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7050        let mut bytes = Vec::new();
7051
7052        // VMOV S0, Rm
7053        let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
7054        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7055
7056        // VCVT.F64.S32 Dd, S0 or VCVT.F64.U32 Dd, S0
7057        let dd_num = vfp_dreg_to_num(dd)?;
7058        let (vd, d) = encode_dreg(dd_num);
7059        let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
7060        let vcvt = base | (d << 22) | (vd << 12);
7061        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7062
7063        Ok(bytes)
7064    }
7065
7066    /// Encode VCVT.F64.F32 Dd, Sm as Thumb-2
7067    fn encode_thumb_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7068        let dd_num = vfp_dreg_to_num(dd)?;
7069        let sm_num = vfp_sreg_to_num(sm)?;
7070        let (vd, d) = encode_dreg(dd_num);
7071        let (vm, m) = encode_sreg(sm_num);
7072
7073        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7074        Ok(vfp_to_thumb_bytes(vcvt))
7075    }
7076
7077    /// Encode VCVT.S32/U32.F64 S0, Dm + VMOV Rd, S0 as Thumb-2
7078    fn encode_thumb_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7079        let mut bytes = Vec::new();
7080        let dm_num = vfp_dreg_to_num(dm)?;
7081        let (vm, m) = encode_dreg(dm_num);
7082
7083        // VCVT.S32.F64 S0, Dm or VCVT.U32.F64 S0, Dm
7084        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
7085        let vcvt = base | (m << 5) | vm;
7086        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7087
7088        // VMOV Rd, S0
7089        let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
7090        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7091
7092        Ok(bytes)
7093    }
7094
7095    /// Encode F64 rounding pseudo-op as Thumb-2 via VCVT to integer and back
7096    /// Encode F64 rounding as Thumb-2.
7097    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
7098    fn encode_thumb_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7099        let mut bytes = Vec::new();
7100        let dm_num = vfp_dreg_to_num(dm)?;
7101        let dd_num = vfp_dreg_to_num(dd)?;
7102        let (vm, m) = encode_dreg(dm_num);
7103        let (vd, d) = encode_dreg(dd_num);
7104
7105        if mode == 0b11 {
7106            // Trunc: VCVTR.S32.F64 — bit[7]=1, always truncates
7107            let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
7108            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7109        } else {
7110            let rt: u32 = 12;
7111
7112            // VMRS R12, FPSCR
7113            let vmrs = 0xEEF10A10 | (rt << 12);
7114            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7115
7116            // BIC.W R12, R12, #(3 << 22)
7117            let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF);
7118            let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
7119            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7120            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7121
7122            // ORR.W R12, R12, #(mode << 22)
7123            if mode != 0 {
7124                let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF);
7125                let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
7126                bytes.extend_from_slice(&orr_hw1.to_le_bytes());
7127                bytes.extend_from_slice(&orr_hw2.to_le_bytes());
7128            }
7129
7130            // VMSR FPSCR, R12
7131            let vmsr = 0xEEE10A10 | (rt << 12);
7132            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7133
7134            // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0)
7135            let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
7136            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7137
7138            // Restore FPSCR
7139            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7140            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7141            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7142            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7143        }
7144
7145        // VCVT.F64.S32 Dd, S0
7146        let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
7147        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
7148
7149        Ok(bytes)
7150    }
7151
7152    /// Encode F64 min/max as Thumb-2
7153    fn encode_thumb_f64_minmax(
7154        &self,
7155        dd: &VfpReg,
7156        dn: &VfpReg,
7157        dm: &VfpReg,
7158        is_min: bool,
7159    ) -> Result<Vec<u8>> {
7160        let mut bytes = Vec::new();
7161        let dn_num = vfp_dreg_to_num(dn)?;
7162        let dm_num = vfp_dreg_to_num(dm)?;
7163        let dd_num = vfp_dreg_to_num(dd)?;
7164
7165        // VMOV.F64 Dd, Dn
7166        let (vd, d) = encode_dreg(dd_num);
7167        let (vn, n) = encode_dreg(dn_num);
7168        let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
7169        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_dn));
7170
7171        // VCMP.F64 Dn, Dm
7172        let (vm, m) = encode_dreg(dm_num);
7173        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7174        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7175
7176        // VMRS APSR_nzcv, FPSCR
7177        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7178
7179        // IT GT (for min) or IT MI (for max)
7180        let cond: u16 = if is_min { 0xC } else { 0x4 };
7181        let it: u16 = 0xBF00 | (cond << 4) | 0x8;
7182        bytes.extend_from_slice(&it.to_le_bytes());
7183
7184        // VMOV{cond}.F64 Dd, Dm
7185        let vmov_dm = 0xEEB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7186        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_dm));
7187
7188        Ok(bytes)
7189    }
7190
7191    /// Encode F64 copysign as Thumb-2
7192    fn encode_thumb_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7193        let mut bytes = Vec::new();
7194
7195        // VMOV R0, R12, Dm (get sign source)
7196        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_dreg(
7197            false,
7198            dm,
7199            &Reg::R0,
7200            &Reg::R12,
7201        )?));
7202
7203        // VMOV R1, R2, Dn (get magnitude source)
7204        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_dreg(
7205            false,
7206            dn,
7207            &Reg::R1,
7208            &Reg::R2,
7209        )?));
7210
7211        // AND.W R12, R12, #0x80000000 (i=0, Rn=R12)
7212        let hw1: u16 = 0xF000 | 12;
7213        let hw2: u16 = (0x1 << 12) | (12 << 8) | 0x02;
7214        bytes.extend_from_slice(&hw1.to_le_bytes());
7215        bytes.extend_from_slice(&hw2.to_le_bytes());
7216
7217        // BIC.W R2, R2, #0x80000000 (i=0, Rn=R2)
7218        let hw1: u16 = 0xF020 | 2;
7219        let hw2: u16 = (0x1 << 12) | (2 << 8) | 0x02;
7220        bytes.extend_from_slice(&hw1.to_le_bytes());
7221        bytes.extend_from_slice(&hw2.to_le_bytes());
7222
7223        // ORR.W R2, R2, R12
7224        let hw1: u16 = 0xEA40 | 2;
7225        let hw2: u16 = (2 << 8) | 12;
7226        bytes.extend_from_slice(&hw1.to_le_bytes());
7227        bytes.extend_from_slice(&hw2.to_le_bytes());
7228
7229        // VMOV Dd, R1, R2
7230        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_dreg(
7231            true,
7232            dd,
7233            &Reg::R1,
7234            &Reg::R2,
7235        )?));
7236
7237        Ok(bytes)
7238    }
7239
7240    /// Encode VCVT.S32/U32.F32 + VMOV as Thumb-2
7241    fn encode_thumb_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7242        let mut bytes = Vec::new();
7243
7244        let sm_num = vfp_sreg_to_num(sm)?;
7245        let (vd, d) = encode_sreg(sm_num);
7246        let (vm, m) = encode_sreg(sm_num);
7247        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
7248        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7249        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7250
7251        // VMOV Rd, Sm
7252        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
7253        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7254
7255        Ok(bytes)
7256    }
7257
7258    // === Thumb-2 32-bit encoding helpers ===
7259
7260    /// Encode Thumb-2 32-bit ADD with immediate
7261    fn encode_thumb32_add(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7262        let rd_bits = reg_to_bits(rd);
7263        let rn_bits = reg_to_bits(rn);
7264
7265        // The `i:imm3:imm8` field is split the same way for both forms.
7266        let i_bit = (imm >> 11) & 1;
7267        let imm3 = (imm >> 8) & 0x7;
7268        let imm8 = imm & 0xFF;
7269
7270        let hw1_base = if imm <= 0xFF {
7271            // ADD.W (T3): the field is a ThumbExpandImm modified immediate. For
7272            // imm <= 0xFF (i:imm3 = 0000) it is the zero-extended byte, which is
7273            // correct — keep this form so existing encodings stay bit-identical.
7274            0xF100
7275        } else if imm <= 0xFFF {
7276            // ADDW (T4): a PLAIN 12-bit immediate (0..4095) — no ThumbExpandImm.
7277            // This is what makes `add sp, sp, #frame` correct for frame sizes
7278            // >= 256, which ADD.W (T3) would silently mis-encode (e.g. #256 -> #0).
7279            0xF200
7280        } else {
7281            return Err(synth_core::Error::synthesis(
7282                "ADD immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7283            ));
7284        };
7285
7286        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7287        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7288
7289        let mut bytes = hw1.to_le_bytes().to_vec();
7290        bytes.extend_from_slice(&hw2.to_le_bytes());
7291        Ok(bytes)
7292    }
7293
7294    /// Encode Thumb-2 32-bit SUB with immediate
7295    fn encode_thumb32_sub(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7296        let rd_bits = reg_to_bits(rd);
7297        let rn_bits = reg_to_bits(rn);
7298
7299        let i_bit = (imm >> 11) & 1;
7300        let imm3 = (imm >> 8) & 0x7;
7301        let imm8 = imm & 0xFF;
7302
7303        let hw1_base = if imm <= 0xFF {
7304            // SUB.W (T3) modified immediate — correct for the zero-extended byte
7305            // (imm <= 0xFF). Kept bit-identical for existing encodings.
7306            0xF1A0
7307        } else if imm <= 0xFFF {
7308            // SUBW (T4): plain 12-bit immediate (0..4095). Makes
7309            // `sub sp, sp, #frame` correct for frame sizes >= 256.
7310            0xF2A0
7311        } else {
7312            return Err(synth_core::Error::synthesis(
7313                "SUB immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7314            ));
7315        };
7316
7317        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7318        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7319
7320        let mut bytes = hw1.to_le_bytes().to_vec();
7321        bytes.extend_from_slice(&hw2.to_le_bytes());
7322        Ok(bytes)
7323    }
7324
7325    /// Encode Thumb-2 32-bit ADDS with immediate (sets flags)
7326    fn encode_thumb32_adds(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7327        let rd_bits = reg_to_bits(rd);
7328        let rn_bits = reg_to_bits(rn);
7329
7330        // ADDS.W (flag-setting) has only the modified-immediate form — error on
7331        // an un-encodable value rather than silently add the wrong constant.
7332        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7333            synth_core::Error::synthesis(
7334                "ADDS immediate is not a valid ThumbExpandImm — materialize into a register",
7335            )
7336        })?;
7337        let i_bit = (field >> 11) & 1;
7338        let imm3 = (field >> 8) & 0x7;
7339        let imm8 = field & 0xFF;
7340
7341        // ADDS.W Rd, Rn, #imm (with S=1)
7342        // First halfword: 1111 0 i 0 1000 1 Rn = F110 | i<<10 | Rn
7343        let hw1: u16 = (0xF110 | (i_bit << 10) | rn_bits) as u16;
7344        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7345
7346        let mut bytes = hw1.to_le_bytes().to_vec();
7347        bytes.extend_from_slice(&hw2.to_le_bytes());
7348        Ok(bytes)
7349    }
7350
7351    /// Encode Thumb-2 32-bit SUBS with immediate (sets flags)
7352    fn encode_thumb32_subs(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7353        let rd_bits = reg_to_bits(rd);
7354        let rn_bits = reg_to_bits(rn);
7355
7356        // SUBS.W (flag-setting) has only the modified-immediate form — error on
7357        // an un-encodable value rather than silently subtract the wrong constant.
7358        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7359            synth_core::Error::synthesis(
7360                "SUBS immediate is not a valid ThumbExpandImm — materialize into a register",
7361            )
7362        })?;
7363        let i_bit = (field >> 11) & 1;
7364        let imm3 = (field >> 8) & 0x7;
7365        let imm8 = field & 0xFF;
7366
7367        // SUBS.W Rd, Rn, #imm (with S=1)
7368        // First halfword: 1111 0 i 0 1101 1 Rn = F1B0 | i<<10 | Rn
7369        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7370        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7371
7372        let mut bytes = hw1.to_le_bytes().to_vec();
7373        bytes.extend_from_slice(&hw2.to_le_bytes());
7374        Ok(bytes)
7375    }
7376
7377    /// Encode Thumb-2 32-bit MOVW (16-bit immediate)
7378    ///
7379    /// # Contract (Verus-style)
7380    /// ```text
7381    /// requires rd <= R14
7382    /// ensures result.len() == 4
7383    /// ensures (imm & 0xFFFF) can be reconstructed from the encoding
7384    /// ```
7385    fn encode_thumb32_movw(&self, rd: &Reg, imm: u32) -> Result<Vec<u8>> {
7386        let rd_bits = reg_to_bits(rd);
7387        reg_bits_checked(rd_bits)?;
7388        let imm16 = imm & 0xFFFF;
7389
7390        // MOVW Rd, #imm16
7391        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7392        let imm4 = (imm16 >> 12) & 0xF;
7393        let i_bit = (imm16 >> 11) & 1;
7394        let imm3 = (imm16 >> 8) & 0x7;
7395        let imm8 = imm16 & 0xFF;
7396
7397        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7398        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7399
7400        let mut bytes = hw1.to_le_bytes().to_vec();
7401        bytes.extend_from_slice(&hw2.to_le_bytes());
7402        encoding_contracts::verify_thumb32(&bytes);
7403        Ok(bytes)
7404    }
7405
7406    /// Encode Thumb-2 32-bit shift with immediate
7407    ///
7408    /// # Contract (Verus-style)
7409    /// ```text
7410    /// requires rd <= R14, rm <= R14
7411    /// ensures result.len() == 4
7412    /// ```
7413    fn encode_thumb32_shift(
7414        &self,
7415        rd: &Reg,
7416        rm: &Reg,
7417        shift: u32,
7418        shift_type: u8,
7419    ) -> Result<Vec<u8>> {
7420        let rd_bits = reg_to_bits(rd);
7421        let rm_bits = reg_to_bits(rm);
7422        reg_bits_checked(rd_bits)?;
7423        reg_bits_checked(rm_bits)?;
7424        let imm5 = shift & 0x1F;
7425        let imm2 = imm5 & 0x3;
7426        let imm3 = (imm5 >> 2) & 0x7;
7427
7428        // MOV.W Rd, Rm, <shift> #imm
7429        // EA4F 0 imm3 Rd imm2 type Rm
7430        let hw1: u16 = 0xEA4F;
7431        let hw2: u16 =
7432            ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | ((shift_type as u32) << 4) | rm_bits)
7433                as u16;
7434
7435        let mut bytes = hw1.to_le_bytes().to_vec();
7436        bytes.extend_from_slice(&hw2.to_le_bytes());
7437        Ok(bytes)
7438    }
7439
7440    /// Encode Thumb-2 32-bit shift by register
7441    /// Encoding: 11111010 0xx0 Rn | 1111 Rd 0000 Rm
7442    /// shift_type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
7443    fn encode_thumb32_shift_reg(
7444        &self,
7445        rd: &Reg,
7446        rn: &Reg,
7447        rm: &Reg,
7448        shift_type: u8,
7449    ) -> Result<Vec<u8>> {
7450        let rd_bits = reg_to_bits(rd);
7451        let rn_bits = reg_to_bits(rn);
7452        let rm_bits = reg_to_bits(rm);
7453
7454        // hw1: 1111 1010 0xx0 Rn
7455        let hw1: u16 = (0xFA00 | ((shift_type as u32) << 5) | rn_bits) as u16;
7456        // hw2: 1111 Rd 0000 Rm
7457        let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
7458
7459        let mut bytes = hw1.to_le_bytes().to_vec();
7460        bytes.extend_from_slice(&hw2.to_le_bytes());
7461        Ok(bytes)
7462    }
7463
7464    /// Encode Thumb-2 32-bit CMP with immediate
7465    fn encode_thumb32_cmp_imm(&self, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7466        let rn_bits = reg_to_bits(rn);
7467
7468        // CMP.W has only the modified-immediate form (no plain-imm12 like ADDW),
7469        // so an un-encodable immediate MUST be materialized into a register by
7470        // the selector. Error rather than silently compare the wrong constant.
7471        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7472            synth_core::Error::synthesis(
7473                "CMP immediate is not a valid ThumbExpandImm — materialize into a register",
7474            )
7475        })?;
7476        let i_bit = (field >> 11) & 1;
7477        let imm3 = (field >> 8) & 0x7;
7478        let imm8 = field & 0xFF;
7479
7480        // CMP.W Rn, #imm
7481        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7482        let hw2: u16 = ((imm3 << 12) | 0x0F00 | imm8) as u16;
7483
7484        let mut bytes = hw1.to_le_bytes().to_vec();
7485        bytes.extend_from_slice(&hw2.to_le_bytes());
7486        Ok(bytes)
7487    }
7488
7489    /// #372/#382: resolve the base register AND residual immediate offset for an
7490    /// `I64Ldr`/`I64Str` whose address may carry an index register. Returns
7491    /// `(base, low_offset)`; the caller accesses the halves at `[base,
7492    /// #low_offset]` and `[base, #low_offset + 4]`.
7493    ///
7494    /// - Frame access (no `offset_reg`, e.g. a spilled local at `[SP, #off]`):
7495    ///   returns `(addr.base, off)` and emits NOTHING — byte-identical.
7496    /// - Memory access (`reg_imm(R11, addr, offset)` = `R11 + addr + offset`)
7497    ///   with `offset + 4 <= 0xFFF`: emits `ADD.W ip, base, index` and returns
7498    ///   `(ip, offset)`, folding `offset`/`offset+4` into the halves' imm12.
7499    ///   Byte-identical to the pre-#382 (#372) behavior.
7500    /// - Memory access with `offset + 4 > 0xFFF`: the imm12 form cannot hold the
7501    ///   high half's offset, so `encode_thumb32_ldr`'s `check_ldst_imm12` (#259)
7502    ///   rightly refused it and the WHOLE function was skipped (#382). Instead
7503    ///   MATERIALIZE the offset into the base: `ADD ip, index, #offset` (against
7504    ///   the read-only INDEX register, so `encode_thumb32_add_imm` never trips its
7505    ///   `rd==rn==R12` alias trap), then `ADD.W ip, ip, base` (+ R11), and return
7506    ///   `(ip, 0)` so the halves use `[ip, #0]` / `[ip, #4]`.
7507    ///
7508    /// The effective address is fully materialized into `ip` BEFORE the halves
7509    /// are accessed, so an `rdlo` aliasing the index register is safe.
7510    fn i64_effective_base(&self, bytes: &mut Vec<u8>, addr: &MemAddr) -> Result<(Reg, u32)> {
7511        let offset = if addr.offset < 0 {
7512            0u32
7513        } else {
7514            addr.offset as u32
7515        };
7516        match addr.offset_reg {
7517            Some(idx) => {
7518                let ip = Reg::R12;
7519                if offset.wrapping_add(4) > 0xFFF {
7520                    // Large static offset (#382): fold it (and R11) into ip so the
7521                    // imm12 halves stay in range instead of skipping the function.
7522                    // ADD ip, index, #offset  (index != ip → no add_imm alias trap)
7523                    bytes.extend_from_slice(&self.encode_thumb32_add_imm(&ip, &idx, offset)?);
7524                    // ADD.W ip, ip, base  (+ R11)
7525                    bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
7526                        reg_to_bits(&ip),
7527                        reg_to_bits(&ip),
7528                        reg_to_bits(&addr.base),
7529                    )?);
7530                    Ok((ip, 0))
7531                } else {
7532                    // ADD.W ip, addr.base, idx  (Thumb-2, byte-verified vs as)
7533                    let hw1: u16 = 0xEB00 | reg_to_bits(&addr.base) as u16;
7534                    let hw2: u16 = 0x0C00 | reg_to_bits(&idx) as u16;
7535                    bytes.extend_from_slice(&hw1.to_le_bytes());
7536                    bytes.extend_from_slice(&hw2.to_le_bytes());
7537                    Ok((ip, offset))
7538                }
7539            }
7540            None => Ok((addr.base, offset)),
7541        }
7542    }
7543
7544    /// Encode Thumb-2 32-bit LDR
7545    fn encode_thumb32_ldr(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7546        let rd_bits = reg_to_bits(rd);
7547        let base_bits = reg_to_bits(base);
7548
7549        // LDR.W Rd, [Rn, #imm12]
7550        check_ldst_imm12(offset)?;
7551        let hw1: u16 = (0xF8D0 | base_bits) as u16;
7552        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7553
7554        let mut bytes = hw1.to_le_bytes().to_vec();
7555        bytes.extend_from_slice(&hw2.to_le_bytes());
7556        Ok(bytes)
7557    }
7558
7559    /// Encode Thumb-2 32-bit STR
7560    fn encode_thumb32_str(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7561        let rd_bits = reg_to_bits(rd);
7562        let base_bits = reg_to_bits(base);
7563
7564        // STR.W Rd, [Rn, #imm12]
7565        check_ldst_imm12(offset)?;
7566        let hw1: u16 = (0xF8C0 | base_bits) as u16;
7567        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7568
7569        let mut bytes = hw1.to_le_bytes().to_vec();
7570        bytes.extend_from_slice(&hw2.to_le_bytes());
7571        Ok(bytes)
7572    }
7573
7574    /// Encode Thumb-2 32-bit LDR with register offset: LDR.W Rd, [Rn, Rm]
7575    fn encode_thumb32_ldr_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7576        let rd_bits = reg_to_bits(rd);
7577        let base_bits = reg_to_bits(base);
7578        let rm_bits = reg_to_bits(offset_reg);
7579
7580        // LDR.W Rd, [Rn, Rm, LSL #0]
7581        // Encoding: 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
7582        // imm2 = 00 for no shift (LSL #0)
7583        let hw1: u16 = (0xF850 | base_bits) as u16;
7584        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7585
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 STR with register offset: STR.W Rd, [Rn, Rm]
7592    fn encode_thumb32_str_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7593        let rd_bits = reg_to_bits(rd);
7594        let base_bits = reg_to_bits(base);
7595        let rm_bits = reg_to_bits(offset_reg);
7596
7597        // STR.W Rd, [Rn, Rm, LSL #0]
7598        // Encoding: 1111 1000 0100 Rn | Rt 0000 00 imm2 Rm
7599        // imm2 = 00 for no shift (LSL #0)
7600        let hw1: u16 = (0xF840 | base_bits) as u16;
7601        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7602
7603        let mut bytes = hw1.to_le_bytes().to_vec();
7604        bytes.extend_from_slice(&hw2.to_le_bytes());
7605        Ok(bytes)
7606    }
7607
7608    // === Sub-word load/store Thumb-2 encoding helpers ===
7609
7610    /// Encode Thumb-2 32-bit LDRB with immediate: LDRB.W Rd, [Rn, #imm12]
7611    fn encode_thumb32_ldrb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7612        let rd_bits = reg_to_bits(rd);
7613        let base_bits = reg_to_bits(base);
7614        // LDRB.W Rd, [Rn, #imm12]: 1111 1000 1001 Rn | Rt imm12
7615        check_ldst_imm12(offset)?;
7616        let hw1: u16 = (0xF890 | base_bits) as u16;
7617        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7618        let mut bytes = hw1.to_le_bytes().to_vec();
7619        bytes.extend_from_slice(&hw2.to_le_bytes());
7620        Ok(bytes)
7621    }
7622
7623    /// Encode Thumb-2 32-bit LDRB with register: LDRB.W Rd, [Rn, Rm]
7624    fn encode_thumb32_ldrb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7625        let rd_bits = reg_to_bits(rd);
7626        let base_bits = reg_to_bits(base);
7627        let rm_bits = reg_to_bits(offset_reg);
7628        // LDRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0001 Rn | Rt 0000 00 imm2 Rm
7629        let hw1: u16 = (0xF810 | base_bits) as u16;
7630        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7631        let mut bytes = hw1.to_le_bytes().to_vec();
7632        bytes.extend_from_slice(&hw2.to_le_bytes());
7633        Ok(bytes)
7634    }
7635
7636    /// Encode Thumb-2 32-bit LDRSB with immediate: LDRSB.W Rd, [Rn, #imm12]
7637    fn encode_thumb32_ldrsb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7638        let rd_bits = reg_to_bits(rd);
7639        let base_bits = reg_to_bits(base);
7640        // LDRSB.W Rd, [Rn, #imm12]: 1111 1001 1001 Rn | Rt imm12
7641        check_ldst_imm12(offset)?;
7642        let hw1: u16 = (0xF990 | base_bits) as u16;
7643        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7644        let mut bytes = hw1.to_le_bytes().to_vec();
7645        bytes.extend_from_slice(&hw2.to_le_bytes());
7646        Ok(bytes)
7647    }
7648
7649    /// Encode Thumb-2 32-bit LDRSB with register: LDRSB.W Rd, [Rn, Rm]
7650    fn encode_thumb32_ldrsb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7651        let rd_bits = reg_to_bits(rd);
7652        let base_bits = reg_to_bits(base);
7653        let rm_bits = reg_to_bits(offset_reg);
7654        // LDRSB.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0001 Rn | Rt 0000 00 imm2 Rm
7655        let hw1: u16 = (0xF910 | base_bits) as u16;
7656        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7657        let mut bytes = hw1.to_le_bytes().to_vec();
7658        bytes.extend_from_slice(&hw2.to_le_bytes());
7659        Ok(bytes)
7660    }
7661
7662    /// Encode Thumb-2 32-bit LDRH with immediate: LDRH.W Rd, [Rn, #imm12]
7663    fn encode_thumb32_ldrh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7664        let rd_bits = reg_to_bits(rd);
7665        let base_bits = reg_to_bits(base);
7666        // LDRH.W Rd, [Rn, #imm12]: 1111 1000 1011 Rn | Rt imm12
7667        check_ldst_imm12(offset)?;
7668        let hw1: u16 = (0xF8B0 | base_bits) as u16;
7669        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7670        let mut bytes = hw1.to_le_bytes().to_vec();
7671        bytes.extend_from_slice(&hw2.to_le_bytes());
7672        Ok(bytes)
7673    }
7674
7675    /// Encode Thumb-2 32-bit LDRH with register: LDRH.W Rd, [Rn, Rm]
7676    fn encode_thumb32_ldrh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7677        let rd_bits = reg_to_bits(rd);
7678        let base_bits = reg_to_bits(base);
7679        let rm_bits = reg_to_bits(offset_reg);
7680        // LDRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0011 Rn | Rt 0000 00 imm2 Rm
7681        let hw1: u16 = (0xF830 | base_bits) as u16;
7682        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7683        let mut bytes = hw1.to_le_bytes().to_vec();
7684        bytes.extend_from_slice(&hw2.to_le_bytes());
7685        Ok(bytes)
7686    }
7687
7688    /// Encode Thumb-2 32-bit LDRSH with immediate: LDRSH.W Rd, [Rn, #imm12]
7689    fn encode_thumb32_ldrsh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7690        let rd_bits = reg_to_bits(rd);
7691        let base_bits = reg_to_bits(base);
7692        // LDRSH.W Rd, [Rn, #imm12]: 1111 1001 1011 Rn | Rt imm12
7693        check_ldst_imm12(offset)?;
7694        let hw1: u16 = (0xF9B0 | base_bits) as u16;
7695        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7696        let mut bytes = hw1.to_le_bytes().to_vec();
7697        bytes.extend_from_slice(&hw2.to_le_bytes());
7698        Ok(bytes)
7699    }
7700
7701    /// Encode Thumb-2 32-bit LDRSH with register: LDRSH.W Rd, [Rn, Rm]
7702    fn encode_thumb32_ldrsh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7703        let rd_bits = reg_to_bits(rd);
7704        let base_bits = reg_to_bits(base);
7705        let rm_bits = reg_to_bits(offset_reg);
7706        // LDRSH.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0011 Rn | Rt 0000 00 imm2 Rm
7707        let hw1: u16 = (0xF930 | base_bits) as u16;
7708        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7709        let mut bytes = hw1.to_le_bytes().to_vec();
7710        bytes.extend_from_slice(&hw2.to_le_bytes());
7711        Ok(bytes)
7712    }
7713
7714    /// Encode Thumb-2 32-bit STRB with immediate: STRB.W Rd, [Rn, #imm12]
7715    fn encode_thumb32_strb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7716        let rd_bits = reg_to_bits(rd);
7717        let base_bits = reg_to_bits(base);
7718        // STRB.W Rd, [Rn, #imm12]: 1111 1000 1000 Rn | Rt imm12
7719        check_ldst_imm12(offset)?;
7720        let hw1: u16 = (0xF880 | base_bits) as u16;
7721        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7722        let mut bytes = hw1.to_le_bytes().to_vec();
7723        bytes.extend_from_slice(&hw2.to_le_bytes());
7724        Ok(bytes)
7725    }
7726
7727    /// Encode Thumb-2 32-bit STRB with register: STRB.W Rd, [Rn, Rm]
7728    fn encode_thumb32_strb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7729        let rd_bits = reg_to_bits(rd);
7730        let base_bits = reg_to_bits(base);
7731        let rm_bits = reg_to_bits(offset_reg);
7732        // STRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0000 Rn | Rt 0000 00 imm2 Rm
7733        let hw1: u16 = (0xF800 | base_bits) as u16;
7734        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7735        let mut bytes = hw1.to_le_bytes().to_vec();
7736        bytes.extend_from_slice(&hw2.to_le_bytes());
7737        Ok(bytes)
7738    }
7739
7740    /// Encode Thumb-2 32-bit STRH with immediate: STRH.W Rd, [Rn, #imm12]
7741    fn encode_thumb32_strh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7742        let rd_bits = reg_to_bits(rd);
7743        let base_bits = reg_to_bits(base);
7744        // STRH.W Rd, [Rn, #imm12]: 1111 1000 1010 Rn | Rt imm12
7745        check_ldst_imm12(offset)?;
7746        let hw1: u16 = (0xF8A0 | base_bits) as u16;
7747        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7748        let mut bytes = hw1.to_le_bytes().to_vec();
7749        bytes.extend_from_slice(&hw2.to_le_bytes());
7750        Ok(bytes)
7751    }
7752
7753    /// Encode Thumb-2 32-bit STRH with register: STRH.W Rd, [Rn, Rm]
7754    fn encode_thumb32_strh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7755        let rd_bits = reg_to_bits(rd);
7756        let base_bits = reg_to_bits(base);
7757        let rm_bits = reg_to_bits(offset_reg);
7758        // STRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0010 Rn | Rt 0000 00 imm2 Rm
7759        let hw1: u16 = (0xF820 | base_bits) as u16;
7760        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7761        let mut bytes = hw1.to_le_bytes().to_vec();
7762        bytes.extend_from_slice(&hw2.to_le_bytes());
7763        Ok(bytes)
7764    }
7765
7766    /// Encode Thumb-2 32-bit ADD with immediate: ADD.W Rd, Rn, #imm
7767    fn encode_thumb32_add_imm(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7768        let rd_bits = reg_to_bits(rd);
7769        let rn_bits = reg_to_bits(rn);
7770
7771        // For small immediates, use ADD.W Rd, Rn, #imm12
7772        // Encoding: 1111 0 i 0 1 0 0 0 S Rn | 0 imm3 Rd imm8
7773        // S = 0 (don't update flags)
7774        // The 12-bit immediate is encoded as: i:imm3:imm8
7775        // For simplicity, we only support imm <= 0xFFF (direct encoding)
7776        if imm <= 0xFFF {
7777            let i_bit = (imm >> 11) & 1;
7778            let imm3 = (imm >> 8) & 0x7;
7779            let imm8 = imm & 0xFF;
7780
7781            let hw1: u16 = (0xF100 | (i_bit << 10) | rn_bits) as u16;
7782            let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7783
7784            let mut bytes = hw1.to_le_bytes().to_vec();
7785            bytes.extend_from_slice(&hw2.to_le_bytes());
7786            Ok(bytes)
7787        } else {
7788            // Out-of-range immediate (> 0xFFF): materialize it into a scratch
7789            // register, then ADD.W Rd, Rn, scratch. This is the #180/#185
7790            // "encoder must produce a legal sequence, not assert" class — see #350.
7791            //
7792            // Scratch choice (must NEVER equal Rn, or Rn would be clobbered before
7793            // the ADD reads it):
7794            //   - rd != rn  => use rd itself (rn is untouched, since rd != rn).
7795            //   - rd == rn  => use R12/IP (the reserved encoder scratch). rd/rn are
7796            //                  never R12 (R12 is non-allocatable), so it can't alias.
7797            //
7798            // The materialized value is the same whether or not MOVT is emitted, so
7799            // the byte length depends only on `imm` (and rd==rn) — the size probe and
7800            // the final emit therefore agree (mandatory: the function is encoded twice).
7801            let scratch: u32 = if rd_bits == rn_bits {
7802                12 // R12/IP — in-place add, can't use rd because rd == rn
7803            } else {
7804                rd_bits // rn is preserved because rd != rn
7805            };
7806            // Invariant: the scratch must never alias Rn (would clobber it before
7807            // the ADD reads it). Unreachable in real codegen (rd/rn are never R12,
7808            // which is reserved encoder scratch), but the encoder is also driven by
7809            // the `encoder_no_panic` fuzz harness with ARBITRARY registers — incl.
7810            // rd==rn==R12, which makes scratch (R12) alias Rn. The encoder contract
7811            // (#180/#185) is Ok-or-Err, never a panic, so return a typed error
7812            // instead of asserting. #350 follow-up.
7813            if scratch == rn_bits {
7814                return Err(synth_core::Error::synthesis(format!(
7815                    "ADD #imm: cannot lower #{imm:#x} for Rd==Rn==R12 — no free scratch \
7816                     register (R12 is the reserved encoder scratch and aliases Rn here)"
7817                )));
7818            }
7819
7820            let lo16 = imm & 0xFFFF;
7821            let hi16 = (imm >> 16) & 0xFFFF;
7822
7823            let mut bytes = self.encode_thumb32_movw_raw(scratch, lo16)?;
7824            if hi16 != 0 {
7825                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(scratch, hi16)?);
7826            }
7827            bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rn_bits, scratch)?);
7828            Ok(bytes)
7829        }
7830    }
7831
7832    // === Raw encoding helpers for POPCNT (take register numbers directly) ===
7833
7834    /// Encode Thumb-2 32-bit MOVW (16-bit immediate) - raw version
7835    ///
7836    /// # Contract (Verus-style)
7837    /// ```text
7838    /// requires rd <= 14, imm16 <= 0xFFFF
7839    /// ensures result.len() == 4
7840    /// ```
7841    fn encode_thumb32_movw_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
7842        reg_bits_checked(rd)?;
7843        encoding_contracts::verify_imm16(imm16);
7844        // MOVW Rd, #imm16
7845        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7846        let imm16 = imm16 & 0xFFFF;
7847        let imm4 = (imm16 >> 12) & 0xF;
7848        let i_bit = (imm16 >> 11) & 1;
7849        let imm3 = (imm16 >> 8) & 0x7;
7850        let imm8 = imm16 & 0xFF;
7851
7852        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7853        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
7854
7855        let mut bytes = hw1.to_le_bytes().to_vec();
7856        bytes.extend_from_slice(&hw2.to_le_bytes());
7857        encoding_contracts::verify_thumb32(&bytes);
7858        Ok(bytes)
7859    }
7860
7861    /// Encode Thumb-2 32-bit MOVT (move top 16 bits) - raw version
7862    ///
7863    /// # Contract (Verus-style)
7864    /// ```text
7865    /// requires rd <= 14, imm16 <= 0xFFFF
7866    /// ensures result.len() == 4
7867    /// ```
7868    fn encode_thumb32_movt_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
7869        reg_bits_checked(rd)?;
7870        encoding_contracts::verify_imm16(imm16);
7871        // MOVT Rd, #imm16
7872        // 1111 0 i 10 1 1 0 0 imm4 | 0 imm3 Rd imm8
7873        let imm16 = imm16 & 0xFFFF;
7874        let imm4 = (imm16 >> 12) & 0xF;
7875        let i_bit = (imm16 >> 11) & 1;
7876        let imm3 = (imm16 >> 8) & 0x7;
7877        let imm8 = imm16 & 0xFF;
7878
7879        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
7880        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
7881
7882        let mut bytes = hw1.to_le_bytes().to_vec();
7883        bytes.extend_from_slice(&hw2.to_le_bytes());
7884        encoding_contracts::verify_thumb32(&bytes);
7885        Ok(bytes)
7886    }
7887
7888    /// Encode Thumb-2 32-bit LSR (logical shift right) with immediate - raw version
7889    fn encode_thumb32_lsr_raw(&self, rd: u32, rm: u32, shift: u32) -> Result<Vec<u8>> {
7890        // MOV.W Rd, Rm, LSR #imm
7891        // EA4F 0 imm3 Rd imm2 01 Rm
7892        let imm5 = shift & 0x1F;
7893        let imm2 = imm5 & 0x3;
7894        let imm3 = (imm5 >> 2) & 0x7;
7895
7896        let hw1: u16 = 0xEA4F;
7897        let hw2: u16 = ((imm3 << 12) | (rd << 8) | (imm2 << 6) | (0b01 << 4) | rm) as u16;
7898
7899        let mut bytes = hw1.to_le_bytes().to_vec();
7900        bytes.extend_from_slice(&hw2.to_le_bytes());
7901        Ok(bytes)
7902    }
7903
7904    /// Encode Thumb-2 32-bit AND (register) - raw version
7905    fn encode_thumb32_and_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7906        // AND.W Rd, Rn, Rm
7907        // EA00 Rn | 0 Rd 00 00 Rm
7908        let hw1: u16 = (0xEA00 | rn) as u16;
7909        let hw2: u16 = ((rd << 8) | rm) as u16;
7910
7911        let mut bytes = hw1.to_le_bytes().to_vec();
7912        bytes.extend_from_slice(&hw2.to_le_bytes());
7913        Ok(bytes)
7914    }
7915
7916    /// Encode Thumb-2 32-bit AND with immediate - raw version
7917    fn encode_thumb32_and_imm_raw(&self, rd: u32, rn: u32, imm: u32) -> Result<Vec<u8>> {
7918        // AND.W Rd, Rn, #<modified_immediate>
7919        // For small immediates (0-255), the encoding is simpler
7920        // F0 00 Rn | 0 imm3 Rd imm8
7921        let i_bit = (imm >> 11) & 1;
7922        let imm3 = (imm >> 8) & 0x7;
7923        let imm8 = imm & 0xFF;
7924
7925        let hw1: u16 = (0xF000 | (i_bit << 10) | rn) as u16;
7926        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
7927
7928        let mut bytes = hw1.to_le_bytes().to_vec();
7929        bytes.extend_from_slice(&hw2.to_le_bytes());
7930        Ok(bytes)
7931    }
7932
7933    /// Encode Thumb-2 32-bit SUB (register) - raw version
7934    fn encode_thumb32_sub_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7935        // SUB.W Rd, Rn, Rm
7936        // EBA0 Rn | 0 Rd 00 00 Rm
7937        let hw1: u16 = (0xEBA0 | rn) as u16;
7938        let hw2: u16 = ((rd << 8) | rm) as u16;
7939
7940        let mut bytes = hw1.to_le_bytes().to_vec();
7941        bytes.extend_from_slice(&hw2.to_le_bytes());
7942        Ok(bytes)
7943    }
7944
7945    /// Encode Thumb-2 32-bit ADD (register) - raw version
7946    fn encode_thumb32_add_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7947        // ADD.W Rd, Rn, Rm
7948        // EB00 Rn | 0 Rd 00 00 Rm
7949        let hw1: u16 = (0xEB00 | rn) as u16;
7950        let hw2: u16 = ((rd << 8) | rm) as u16;
7951
7952        let mut bytes = hw1.to_le_bytes().to_vec();
7953        bytes.extend_from_slice(&hw2.to_le_bytes());
7954        Ok(bytes)
7955    }
7956
7957    /// Encode Thumb-2 32-bit ADDS (register, flag-setting) - raw version.
7958    /// Used as the high-register fallback for `ArmOp::Adds` (i64 low-word add)
7959    /// so R8-R11 pair operands don't overflow the 16-bit field — #178/#180.
7960    fn encode_thumb32_adds_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7961        // ADDS.W Rd, Rn, Rm (T3, S=1): EB10 Rn | 0 Rd 00 00 Rm
7962        let hw1: u16 = (0xEB10 | rn) as u16;
7963        let hw2: u16 = ((rd << 8) | rm) as u16;
7964        let mut bytes = hw1.to_le_bytes().to_vec();
7965        bytes.extend_from_slice(&hw2.to_le_bytes());
7966        Ok(bytes)
7967    }
7968
7969    /// Encode Thumb-2 32-bit SUBS (register, flag-setting) - raw version.
7970    /// High-register fallback for `ArmOp::Subs` (i64 low-word subtract) — #178/#180.
7971    fn encode_thumb32_subs_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7972        // SUBS.W Rd, Rn, Rm (T3, S=1): EBB0 Rn | 0 Rd 00 00 Rm
7973        let hw1: u16 = (0xEBB0 | rn) as u16;
7974        let hw2: u16 = ((rd << 8) | rm) as u16;
7975        let mut bytes = hw1.to_le_bytes().to_vec();
7976        bytes.extend_from_slice(&hw2.to_le_bytes());
7977        Ok(bytes)
7978    }
7979
7980    /// Encode a sequence of ARM instructions
7981    pub fn encode_sequence(&self, ops: &[ArmOp]) -> Result<Vec<u8>> {
7982        let mut code = Vec::new();
7983
7984        for op in ops {
7985            let encoded = self.encode(op)?;
7986            code.extend_from_slice(&encoded);
7987        }
7988
7989        Ok(code)
7990    }
7991}
7992
7993/// Convert register to bit encoding (0-15)
7994/// Reverse of the ARMv7-M `ThumbExpandImm`: given a 32-bit immediate, return the
7995/// 12-bit `i:imm3:imm8` field if it is a representable modified immediate, else
7996/// `None` (the caller must materialize the value into a register). This is the
7997/// shared correct path for the data-processing immediate encoders — without it
7998/// they pack raw bits and silently mis-encode any value `> 0xFF` that isn't a
7999/// modified immediate (the silent-miscompile class behind #251/#253/#255).
8000fn try_thumb_expand_imm(value: u32) -> Option<u32> {
8001    // i:imm3 = 0000 → 8-bit value, zero-extended (00000000 00000000 00000000 XY).
8002    if value <= 0xFF {
8003        return Some(value);
8004    }
8005    let b0 = value & 0xFF; // byte 0
8006    let b1 = (value >> 8) & 0xFF; // byte 1
8007    // 0x00XY00XY (i:imm3 = 0001) — XY in bytes 0 and 2
8008    if value == (b0 << 16) | b0 {
8009        return Some(0x100 | b0);
8010    }
8011    // 0xXY00XY00 (i:imm3 = 0010) — XY in bytes 1 and 3
8012    if value == (b1 << 24) | (b1 << 8) {
8013        return Some(0x200 | b1);
8014    }
8015    // 0xXYXYXYXY (i:imm3 = 0011) — XY in all four bytes
8016    if value == (b0 << 24) | (b0 << 16) | (b0 << 8) | b0 {
8017        return Some(0x300 | b0);
8018    }
8019    // An 8-bit value with bit 7 set, rotated right by 8..=31. `rotate_left(rot)`
8020    // undoes the encoded right rotation; if the result is `1bbbbbbb` (0x80..=0xFF)
8021    // the value is representable. imm12[11:7] = rot, imm12[6:0] = low 7 bits.
8022    for rot in 8..=31u32 {
8023        let unrot = value.rotate_left(rot);
8024        if (0x80..=0xFF).contains(&unrot) {
8025            return Some((rot << 7) | (unrot & 0x7F));
8026        }
8027    }
8028    None
8029}
8030
8031/// Guard a Thumb-2 `LDR/STR Rd, [Rn, #imm12]` offset. The imm12 form supports
8032/// `0..=4095`; a larger offset must be materialized into a register by the
8033/// selector (register-offset addressing). Returning `Err` rather than silently
8034/// masking `offset & 0xFFF` closes the wrong-address miscompile class (#259,
8035/// the load/store sibling of #253/#255).
8036fn check_ldst_imm12(offset: u32) -> Result<()> {
8037    if offset > 0xFFF {
8038        Err(synth_core::Error::synthesis(
8039            "load/store immediate offset > 0xFFF (4095) — materialize the offset into a register",
8040        ))
8041    } else {
8042        Ok(())
8043    }
8044}
8045
8046fn reg_to_bits(reg: &Reg) -> u32 {
8047    match reg {
8048        Reg::R0 => 0,
8049        Reg::R1 => 1,
8050        Reg::R2 => 2,
8051        Reg::R3 => 3,
8052        Reg::R4 => 4,
8053        Reg::R5 => 5,
8054        Reg::R6 => 6,
8055        Reg::R7 => 7,
8056        Reg::R8 => 8,
8057        Reg::R9 => 9,
8058        Reg::R10 => 10,
8059        Reg::R11 => 11,
8060        Reg::R12 => 12,
8061        Reg::SP => 13,
8062        Reg::LR => 14,
8063        Reg::PC => 15,
8064    }
8065}
8066
8067// ======================================================================
8068// #610 — i64 fixed-ABI expansion wrappers.
8069//
8070// The hand-written multi-instruction i64 cores (rotl/rotr and the div/rem
8071// shift-subtract loops) compute in FIXED low registers. Before #610 the
8072// div/rem arms ignored their operand fields outright (hardcoded R0:R1 /
8073// R2:R3 in, result to R0:R1) and the rot arms used R3/R4 scratch that
8074// collided with selector-assigned registers — then restored the saved
8075// scratch OVER the result (`POP {R4}` with rd_lo == R4), so the op
8076// returned the caller's stale register: 0 for every input under qemu.
8077//
8078// These wrappers make each core honor its register parameters:
8079//   1. save R0-R3,
8080//   2. marshal the operand registers into the core's fixed input regs via
8081//      the stack (permutation-safe: every source is read before any fixed
8082//      register is written),
8083//   3. run the fixed-reg core (self-preserving for R4+; R12 is encoder
8084//      scratch and never allocatable, #212),
8085//   4. MOV the result pair from R0:R1 into the selector's rd pair,
8086//   5. restore R0-R3, skipping any register the result now occupies.
8087//
8088// All emitted lengths are register-independent so the optimized path's
8089// byte-size estimator (`estimate_arm_byte_size`, pinned by the
8090// estimator↔encoder agreement oracle #498/#511) stays a constant per op.
8091// ======================================================================
8092
8093/// Steps 1+2: `PUSH {R0-R3}`, then marshal `srcs` (operand registers, any of
8094/// R0-R12) into `R0..R<n>` via individual stack pushes. Sources are all read
8095/// before any destination register is written, so arbitrary source/target
8096/// permutations (including operands living in R0-R3) are safe.
8097fn emit_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8098    debug_assert!(srcs.len() <= 4);
8099    // PUSH {R0-R3} — save the caller-visible low registers.
8100    bytes.extend_from_slice(&0xB40Fu16.to_le_bytes());
8101    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8102    for src in srcs.iter().rev() {
8103        let rt = reg_to_bits(src) as u16;
8104        bytes.extend_from_slice(&0xF84Du16.to_le_bytes());
8105        bytes.extend_from_slice(&((rt << 12) | 0x0D04).to_le_bytes());
8106    }
8107    // POP {Ri} — Ri := srcs[i].
8108    for i in 0..srcs.len() as u16 {
8109        bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes());
8110    }
8111}
8112
8113/// Steps 4+5: move the core's R0:R1 result into the selector's rd pair, then
8114/// restore the R0-R3 saved by [`emit_i64_fixed_abi_entry`], skipping any
8115/// register the result now lives in (its saved caller word is discarded).
8116fn emit_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8117    let lo = reg_to_bits(rdlo);
8118    let hi = reg_to_bits(rdhi);
8119    if lo == 1 && hi == 0 {
8120        // A fully swapped pair would clobber one half in either MOV order.
8121        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8122        return Err(synth_core::Error::synthesis(
8123            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8124        ));
8125    }
8126    let mov16 = |bytes: &mut Vec<u8>, rd: u32, rm: u32| {
8127        let d = ((rd >> 3) & 1) as u16;
8128        bytes.extend_from_slice(
8129            &(0x4600u16 | (d << 7) | ((rm as u16) << 3) | ((rd & 7) as u16)).to_le_bytes(),
8130        );
8131    };
8132    if hi == 0 {
8133        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8134        mov16(bytes, lo, 0);
8135        mov16(bytes, hi, 1);
8136    } else {
8137        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8138        mov16(bytes, hi, 1);
8139        mov16(bytes, lo, 0);
8140    }
8141    for i in 0..4u32 {
8142        if i == lo || i == hi {
8143            // The result lives here — drop the saved caller word.
8144            bytes.extend_from_slice(&0xB001u16.to_le_bytes()); // ADD SP, #4
8145        } else {
8146            bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes()); // POP {Ri}
8147        }
8148    }
8149    Ok(())
8150}
8151
8152/// WASM `i64.div_*` / `i64.rem_*` by zero must trap, matching the i32 path's
8153/// cmp/bne/udf guard. Emitted after marshaling, when the divisor pair is in
8154/// R2:R3: `ORRS R12, R2, R3` — `BNE` over a `UDF #0` when nonzero.
8155fn emit_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8156    bytes.extend_from_slice(&0xEA52u16.to_le_bytes()); // ORRS.W R12, R2, R3
8157    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8158    bytes.extend_from_slice(&0xD100u16.to_le_bytes()); // BNE.N +0 (skip the UDF)
8159    bytes.extend_from_slice(&0xDE00u16.to_le_bytes()); // UDF #0 — divide by zero
8160}
8161
8162/// WASM `i64.div_s(INT64_MIN, -1)` must trap (Core §4.3.2 `idiv_s`: the
8163/// quotient +2^63 is unrepresentable), matching the i32 path's overflow
8164/// guard — #633: without it the core negated INT64_MIN onto itself and
8165/// silently returned INT64_MIN. Emitted after marshaling, when the dividend
8166/// pair is in R0:R1 and the divisor pair in R2:R3; R12 is encoder scratch.
8167///
8168/// div_s ONLY — `i64.rem_s(INT64_MIN, -1)` is defined as 0 and must NOT
8169/// trap (`irem_s`), so the I64RemS arm never calls this. 22 bytes,
8170/// register-independent (estimator contract, #498/#511).
8171fn emit_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8172    // AND.W R12, R2, R3 — R12 == 0xFFFFFFFF iff divisor == -1
8173    bytes.extend_from_slice(&0xEA02u16.to_le_bytes());
8174    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8175    // CMN.W R12, #1 — EQ iff both divisor words are all-ones
8176    bytes.extend_from_slice(&0xF11Cu16.to_le_bytes());
8177    bytes.extend_from_slice(&0x0F01u16.to_le_bytes());
8178    // BNE .no_trap
8179    bytes.extend_from_slice(&0xD105u16.to_le_bytes());
8180    // CMP R0, #0 — dividend lo word of INT64_MIN
8181    bytes.extend_from_slice(&0x2800u16.to_le_bytes());
8182    // BNE .no_trap
8183    bytes.extend_from_slice(&0xD103u16.to_le_bytes());
8184    // CMP.W R1, #0x80000000 — dividend hi word of INT64_MIN
8185    bytes.extend_from_slice(&0xF1B1u16.to_le_bytes());
8186    bytes.extend_from_slice(&0x4F00u16.to_le_bytes());
8187    // BNE .no_trap
8188    bytes.extend_from_slice(&0xD100u16.to_le_bytes());
8189    // UDF #0 — signed-division overflow
8190    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
8191    // .no_trap:
8192}
8193
8194// ======================================================================
8195// #615 — A32 (ARM-mode) twins of the #610 i64 fixed-ABI wrappers above.
8196// Identical register contract, A32 encodings: the multi-instruction i64
8197// cores (rotl/rotr, div/rem) compute in fixed low registers (value/dividend
8198// R0:R1, amount R2 / divisor R2:R3, result to R0:R1); the wrappers marshal
8199// the selector-assigned operand registers in and the result out, saving and
8200// restoring the caller-visible R0-R3 around the core.
8201// ======================================================================
8202
8203/// A32 steps 1+2: `STMDB SP!, {R0-R3}`, then marshal `srcs` into `R0..R<n>`
8204/// via individual stack pushes (`STR src, [SP, #-4]!` in reverse order, then
8205/// `LDR Ri, [SP], #4`). Every source is read before any fixed register is
8206/// written, so arbitrary source/target permutations are safe.
8207fn emit_a32_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8208    debug_assert!(srcs.len() <= 4);
8209    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8210    // PUSH {R0-R3} — save the caller-visible low registers.
8211    w(bytes, 0xE92D_000F);
8212    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8213    for src in srcs.iter().rev() {
8214        w(bytes, 0xE52D_0004 | (reg_to_bits(src) << 12));
8215    }
8216    // LDR Ri, [SP], #4 — Ri := srcs[i].
8217    for i in 0..srcs.len() as u32 {
8218        w(bytes, 0xE49D_0004 | (i << 12));
8219    }
8220}
8221
8222/// A32 steps 4+5: move the core's R0:R1 result into the selector's rd pair,
8223/// then restore the R0-R3 saved by [`emit_a32_i64_fixed_abi_entry`], skipping
8224/// any register the result now lives in (its saved caller word is discarded).
8225fn emit_a32_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8226    let lo = reg_to_bits(rdlo);
8227    let hi = reg_to_bits(rdhi);
8228    if lo == 1 && hi == 0 {
8229        // A fully swapped pair would clobber one half in either MOV order.
8230        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8231        return Err(synth_core::Error::synthesis(
8232            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8233        ));
8234    }
8235    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8236    let mov = |bytes: &mut Vec<u8>, rd: u32, rm: u32| w(bytes, 0xE1A0_0000 | (rd << 12) | rm);
8237    if hi == 0 {
8238        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8239        mov(bytes, lo, 0);
8240        mov(bytes, hi, 1);
8241    } else {
8242        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8243        mov(bytes, hi, 1);
8244        mov(bytes, lo, 0);
8245    }
8246    for i in 0..4u32 {
8247        if i == lo || i == hi {
8248            // The result lives here — drop the saved caller word.
8249            w(bytes, 0xE28D_D004); // ADD SP, SP, #4
8250        } else {
8251            w(bytes, 0xE49D_0004 | (i << 12)); // LDR Ri, [SP], #4
8252        }
8253    }
8254    Ok(())
8255}
8256
8257/// A32 zero-divisor trap, emitted after marshaling when the divisor pair is
8258/// in R2:R3: `ORRS R12, R2, R3` sets Z iff the divisor is zero; `BNE` skips a
8259/// `UDF #0` (WASM div/rem-by-zero must trap, matching the Thumb-2 twin).
8260fn emit_a32_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8261    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8262    w(bytes, 0xE192_C003); // ORRS R12, R2, R3
8263    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8264    w(bytes, 0xE7F0_00F0); // UDF #0 — divide by zero
8265}
8266
8267/// A32 twin of [`emit_i64_divs_overflow_trap`] (#633): trap on
8268/// `i64.div_s(INT64_MIN, -1)`. Conditional execution replaces the Thumb
8269/// branches — the CMPEQ chain leaves EQ set only when divisor == -1 AND
8270/// dividend == INT64_MIN. div_s only; rem_s must keep returning 0.
8271fn emit_a32_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8272    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8273    w(bytes, 0xE002_C003); // AND   R12, R2, R3 (== 0xFFFFFFFF iff divisor == -1)
8274    w(bytes, 0xE37C_0001); // CMN   R12, #1     (EQ iff divisor == -1)
8275    w(bytes, 0x0350_0000); // CMPEQ R0, #0      (EQ iff also dividend lo == 0)
8276    w(bytes, 0x0351_0102); // CMPEQ R1, #0x80000000 (EQ iff dividend == INT64_MIN)
8277    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8278    w(bytes, 0xE7F0_00F0); // UDF #0 — signed-division overflow
8279}
8280
8281/// Fallible form of the `verify_reg_bits` contract. PC (R15) is not a valid
8282/// data operand for the Thumb-2 encodings that use this guard (SDIV/UDIV/MLS/…
8283/// are UNPREDICTABLE with PC). Synth's own codegen never emits PC there, but
8284/// the encoder must stay *total* over arbitrary `ArmOp` inputs — the fuzz
8285/// harness (`encoder_no_panic`) requires Ok-or-Err, never a panic. Pre-fix, the
8286/// `debug_assert` in `verify_reg_bits` aborted under `-Cdebug-assertions`.
8287/// Returns a typed Err instead. See #185.
8288fn reg_bits_checked(bits: u32) -> Result<()> {
8289    if bits > 14 {
8290        return Err(synth_core::Error::synthesis(format!(
8291            "register bits {bits} (PC/R15) is not a valid operand for this Thumb-2 encoding"
8292        )));
8293    }
8294    Ok(())
8295}
8296
8297/// Try to encode a 32-bit value as an ARM rotated immediate (imm8 ROR 2*rot4).
8298/// Returns Some((encoded_bits, 1)) if representable, None otherwise.
8299fn try_encode_rotated_imm(val: u32) -> Option<(u32, u32)> {
8300    if val == 0 {
8301        return Some((0, 1));
8302    }
8303    for rot in 0..16u32 {
8304        let shift = rot * 2;
8305        // Rotate left by shift (undo the ROR) to see if result fits in 8 bits
8306        let unrotated = val.rotate_left(shift);
8307        if unrotated <= 0xFF {
8308            // Encoded as: rot4(4 bits) | imm8(8 bits) = rotate_imm << 8 | imm8
8309            return Some(((rot << 8) | unrotated, 1));
8310        }
8311    }
8312    None
8313}
8314
8315/// Encode operand2 field and return (bits, immediate_flag).
8316/// For ARM32 mode, immediates use the rotated-immediate encoding (imm8 ROR 2*rot4).
8317/// Panics if an immediate value cannot be represented. Callers that need large
8318/// immediates should use MOVW/MOVT instead of Operand2::Imm.
8319fn encode_operand2(op2: &Operand2) -> Result<(u32, u32)> {
8320    match op2 {
8321        Operand2::Imm(val) => {
8322            let uval = *val as u32;
8323            // Attempt rotated-immediate encoding (ARM32 Operand2)
8324            if let Some(encoded) = try_encode_rotated_imm(uval) {
8325                Ok(encoded)
8326            } else {
8327                // #378-class honesty: an immediate that can't be expressed as an
8328                // ARM32 rotated immediate is an INTERNAL selector bug — large
8329                // constants must be materialized via MOVW/MOVT, not passed here.
8330                // FAIL HONESTLY with an Err rather than silently masking to
8331                // `uval & 0xFF` and emitting a WRONG immediate. The encoder is
8332                // Ok-or-Err, never corrupt (#180/#185); a loud Err is also why
8333                // this is an Err and not a panic (the `encoder_no_panic` fuzz
8334                // contract — malformed/oversized input must degrade, not crash).
8335                Err(synth_core::Error::synthesis(format!(
8336                    "encode_operand2: immediate {uval:#x} ({val}) is not an ARM32 \
8337                     rotated immediate — the selector must materialize large \
8338                     constants via MOVW/MOVT"
8339                )))
8340            }
8341        }
8342
8343        Operand2::Reg(reg) => {
8344            let reg_bits = reg_to_bits(reg);
8345            Ok((reg_bits, 0)) // I=0 for register
8346        }
8347
8348        Operand2::RegShift {
8349            rm,
8350            shift: _,
8351            amount,
8352        } => {
8353            // Simplified encoding with shift
8354            let rm_bits = reg_to_bits(rm);
8355            let shift_bits = (*amount & 0x1F) << 7;
8356            Ok((shift_bits | rm_bits, 0))
8357        }
8358    }
8359}
8360
8361/// Encode memory address to (base_reg, offset)
8362fn encode_mem_addr(addr: &MemAddr) -> (u32, u32) {
8363    let base_bits = reg_to_bits(&addr.base);
8364    let offset_bits = (addr.offset as u32) & 0xFFF; // 12-bit offset
8365    (base_bits, offset_bits)
8366}
8367
8368/// S-register number: S0=0, S1=1, ..., S31=31
8369fn vfp_sreg_to_num(reg: &VfpReg) -> Result<u32> {
8370    match reg {
8371        VfpReg::S0 => Ok(0),
8372        VfpReg::S1 => Ok(1),
8373        VfpReg::S2 => Ok(2),
8374        VfpReg::S3 => Ok(3),
8375        VfpReg::S4 => Ok(4),
8376        VfpReg::S5 => Ok(5),
8377        VfpReg::S6 => Ok(6),
8378        VfpReg::S7 => Ok(7),
8379        VfpReg::S8 => Ok(8),
8380        VfpReg::S9 => Ok(9),
8381        VfpReg::S10 => Ok(10),
8382        VfpReg::S11 => Ok(11),
8383        VfpReg::S12 => Ok(12),
8384        VfpReg::S13 => Ok(13),
8385        VfpReg::S14 => Ok(14),
8386        VfpReg::S15 => Ok(15),
8387        VfpReg::S16 => Ok(16),
8388        VfpReg::S17 => Ok(17),
8389        VfpReg::S18 => Ok(18),
8390        VfpReg::S19 => Ok(19),
8391        VfpReg::S20 => Ok(20),
8392        VfpReg::S21 => Ok(21),
8393        VfpReg::S22 => Ok(22),
8394        VfpReg::S23 => Ok(23),
8395        VfpReg::S24 => Ok(24),
8396        VfpReg::S25 => Ok(25),
8397        VfpReg::S26 => Ok(26),
8398        VfpReg::S27 => Ok(27),
8399        VfpReg::S28 => Ok(28),
8400        VfpReg::S29 => Ok(29),
8401        VfpReg::S30 => Ok(30),
8402        VfpReg::S31 => Ok(31),
8403        // D-registers are not used in F32 single-precision encodings
8404        _ => Err(synth_core::Error::SynthesisError(
8405            "D-register not supported in single-precision VFP encoding".to_string(),
8406        )),
8407    }
8408}
8409
8410/// D-register number: D0=0, D1=1, ..., D15=15
8411fn vfp_dreg_to_num(reg: &VfpReg) -> Result<u32> {
8412    match reg {
8413        VfpReg::D0 => Ok(0),
8414        VfpReg::D1 => Ok(1),
8415        VfpReg::D2 => Ok(2),
8416        VfpReg::D3 => Ok(3),
8417        VfpReg::D4 => Ok(4),
8418        VfpReg::D5 => Ok(5),
8419        VfpReg::D6 => Ok(6),
8420        VfpReg::D7 => Ok(7),
8421        VfpReg::D8 => Ok(8),
8422        VfpReg::D9 => Ok(9),
8423        VfpReg::D10 => Ok(10),
8424        VfpReg::D11 => Ok(11),
8425        VfpReg::D12 => Ok(12),
8426        VfpReg::D13 => Ok(13),
8427        VfpReg::D14 => Ok(14),
8428        VfpReg::D15 => Ok(15),
8429        // S-registers are not used in F64 double-precision encodings
8430        _ => Err(synth_core::Error::SynthesisError(
8431            "S-register not supported in double-precision VFP encoding".to_string(),
8432        )),
8433    }
8434}
8435
8436/// Split S-register into (Vx[3:0], qualifier_bit) for VFP encoding.
8437/// For an S-register number s: Vx = s >> 1, qualifier = s & 1.
8438/// The qualifier bit goes to D (bit 22), N (bit 7), or M (bit 5) depending on role.
8439fn encode_sreg(s: u32) -> (u32, u32) {
8440    (s >> 1, s & 1)
8441}
8442
8443/// Split D-register into (Vx[3:0], qualifier_bit) for VFP double-precision encoding.
8444/// For a D-register number d: Vx = d & 0xF, qualifier = (d >> 4) & 1.
8445/// For D0-D15, qualifier is always 0.
8446fn encode_dreg(d: u32) -> (u32, u32) {
8447    (d & 0xF, (d >> 4) & 1)
8448}
8449
8450/// Encode a VFP 3-register arithmetic instruction (VADD.F32, VSUB.F32, VMUL.F32, VDIV.F32).
8451/// Returns the full 32-bit instruction word.
8452///
8453/// VFP encoding: [cond 1110] [D opc1 Vn] [Vd 101 sz] [N opc2 M 0 Vm]
8454/// For single-precision (sz=0), coprocessor = 0xA (bits[11:8]).
8455fn encode_vfp_3reg(base: u32, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<u32> {
8456    let sd_num = vfp_sreg_to_num(sd)?;
8457    let sn_num = vfp_sreg_to_num(sn)?;
8458    let sm_num = vfp_sreg_to_num(sm)?;
8459    let (vd, d) = encode_sreg(sd_num);
8460    let (vn, n) = encode_sreg(sn_num);
8461    let (vm, m) = encode_sreg(sm_num);
8462
8463    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8464}
8465
8466/// Encode a VFP 2-register instruction (VNEG.F32, VABS.F32, VSQRT.F32).
8467/// Returns the full 32-bit instruction word.
8468fn encode_vfp_2reg(base: u32, sd: &VfpReg, sm: &VfpReg) -> Result<u32> {
8469    let sd_num = vfp_sreg_to_num(sd)?;
8470    let sm_num = vfp_sreg_to_num(sm)?;
8471    let (vd, d) = encode_sreg(sd_num);
8472    let (vm, m) = encode_sreg(sm_num);
8473
8474    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8475}
8476
8477/// Encode a VFP load/store (VLDR.F32 / VSTR.F32).
8478/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8479/// U bit (bit 23) controls add/subtract offset.
8480fn encode_vfp_ldst(base: u32, sd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8481    let sd_num = vfp_sreg_to_num(sd)?;
8482    let (vd, d) = encode_sreg(sd_num);
8483    let rn = reg_to_bits(&addr.base);
8484
8485    let offset = addr.offset;
8486    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8487    let abs_offset = offset.unsigned_abs();
8488    let imm8 = (abs_offset / 4) & 0xFF;
8489
8490    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8491}
8492
8493/// Encode VMOV between core register and S-register.
8494/// VMOV Sn, Rt: 0xEE00_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8495/// VMOV Rt, Sn: 0xEE10_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8496fn encode_vmov_core_sreg(to_sreg: bool, sreg: &VfpReg, core: &Reg) -> Result<u32> {
8497    let s_num = vfp_sreg_to_num(sreg)?;
8498    let (vn, n) = encode_sreg(s_num);
8499    let rt = reg_to_bits(core);
8500
8501    let base = if to_sreg { 0xEE000A10 } else { 0xEE100A10 };
8502    Ok(base | (vn << 16) | (rt << 12) | (n << 7))
8503}
8504
8505/// Encode a VFP 3-register double-precision instruction (VADD.F64, VSUB.F64, etc.).
8506/// For double-precision (sz=1), coprocessor = 0xB (bits[11:8]).
8507/// The base should have bit 8 = 1 for F64 (0xB suffix instead of 0xA).
8508fn encode_vfp_3reg_f64(base: u32, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<u32> {
8509    let dd_num = vfp_dreg_to_num(dd)?;
8510    let dn_num = vfp_dreg_to_num(dn)?;
8511    let dm_num = vfp_dreg_to_num(dm)?;
8512    let (vd, d) = encode_dreg(dd_num);
8513    let (vn, n) = encode_dreg(dn_num);
8514    let (vm, m) = encode_dreg(dm_num);
8515
8516    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8517}
8518
8519/// Encode a VFP 2-register double-precision instruction (VNEG.F64, VABS.F64, VSQRT.F64).
8520fn encode_vfp_2reg_f64(base: u32, dd: &VfpReg, dm: &VfpReg) -> Result<u32> {
8521    let dd_num = vfp_dreg_to_num(dd)?;
8522    let dm_num = vfp_dreg_to_num(dm)?;
8523    let (vd, d) = encode_dreg(dd_num);
8524    let (vm, m) = encode_dreg(dm_num);
8525
8526    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8527}
8528
8529/// Encode a VFP load/store for double-precision (VLDR.64 / VSTR.64).
8530/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8531fn encode_vfp_ldst_f64(base: u32, dd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8532    let dd_num = vfp_dreg_to_num(dd)?;
8533    let (vd, d) = encode_dreg(dd_num);
8534    let rn = reg_to_bits(&addr.base);
8535
8536    let offset = addr.offset;
8537    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8538    let abs_offset = offset.unsigned_abs();
8539    let imm8 = (abs_offset / 4) & 0xFF;
8540
8541    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8542}
8543
8544/// Encode VMOV between two core registers and a D-register.
8545/// VMOV Dm, Rt, Rt2: 0xEC40_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8546/// VMOV Rt, Rt2, Dm: 0xEC50_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8547fn encode_vmov_core_dreg(
8548    to_dreg: bool,
8549    dreg: &VfpReg,
8550    core_lo: &Reg,
8551    core_hi: &Reg,
8552) -> Result<u32> {
8553    let d_num = vfp_dreg_to_num(dreg)?;
8554    let (vm, m) = encode_dreg(d_num);
8555    let rt = reg_to_bits(core_lo);
8556    let rt2 = reg_to_bits(core_hi);
8557
8558    let base = if to_dreg { 0xEC400B10 } else { 0xEC500B10 };
8559    Ok(base | (rt2 << 16) | (rt << 12) | (m << 5) | vm)
8560}
8561
8562/// Emit a VFP 32-bit instruction as Thumb-2 bytes (two LE halfwords).
8563fn vfp_to_thumb_bytes(instr: u32) -> Vec<u8> {
8564    let hw1 = ((instr >> 16) & 0xFFFF) as u16;
8565    let hw2 = (instr & 0xFFFF) as u16;
8566    let mut bytes = hw1.to_le_bytes().to_vec();
8567    bytes.extend_from_slice(&hw2.to_le_bytes());
8568    bytes
8569}
8570
8571// ============================================================================
8572// Helium MVE encoding helpers
8573// ============================================================================
8574
8575/// Q-register number: Q0=0, Q1=1, ..., Q7=7
8576fn qreg_to_num(reg: &QReg) -> u32 {
8577    match reg {
8578        QReg::Q0 => 0,
8579        QReg::Q1 => 1,
8580        QReg::Q2 => 2,
8581        QReg::Q3 => 3,
8582        QReg::Q4 => 4,
8583        QReg::Q5 => 5,
8584        QReg::Q6 => 6,
8585        QReg::Q7 => 7,
8586    }
8587}
8588
8589/// MVE element size to encoding bits: S8=0b00, S16=0b01, S32=0b10
8590fn mve_size_bits(size: &MveSize) -> u32 {
8591    match size {
8592        MveSize::S8 => 0b00,
8593        MveSize::S16 => 0b01,
8594        MveSize::S32 => 0b10,
8595    }
8596}
8597
8598/// Encode MVE 3-register instruction.
8599/// Q-registers are encoded as D-register pairs: Q0=D0:D1, Q1=D2:D3, etc.
8600/// In NEON/MVE encoding, the Q-register uses D-register number = Qn * 2.
8601fn encode_mve_3reg(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
8602    let d = qreg_to_num(qd) * 2;
8603    let n = qreg_to_num(qn) * 2;
8604    let m = qreg_to_num(qm) * 2;
8605
8606    // Standard NEON/MVE 3-register encoding:
8607    // D bit (bit 22) = Vd[4], Vd[3:0] = bits [15:12]
8608    // N bit (bit 7)  = Vn[4], Vn[3:0] = bits [19:16]
8609    // M bit (bit 5)  = Vm[4], Vm[3:0] = bits [3:0]
8610    let vd = d & 0xF;
8611    let d_bit = (d >> 4) & 1;
8612    let vn = n & 0xF;
8613    let n_bit = (n >> 4) & 1;
8614    let vm = m & 0xF;
8615    let m_bit = (m >> 4) & 1;
8616
8617    base | (d_bit << 22) | (vn << 16) | (vd << 12) | (n_bit << 7) | (m_bit << 5) | vm
8618}
8619
8620/// Encode MVE 3-register bitwise instruction (VAND, VORR, VEOR, VBIC).
8621fn encode_mve_3reg_bitwise(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
8622    encode_mve_3reg(base, qd, qn, qm)
8623}
8624
8625/// Encode MVE VLDRW.32 Qd, [Rn, #offset]
8626/// Format: EC9x xxxx - contiguous load, word-sized elements
8627fn encode_mve_vldrw(qd: &QReg, addr: &MemAddr) -> u32 {
8628    let qd_enc = qreg_to_num(qd) * 2;
8629    let rn = reg_to_bits(&addr.base);
8630    let offset = addr.offset;
8631    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8632    let abs_offset = offset.unsigned_abs();
8633    let imm7 = (abs_offset / 4) & 0x7F; // 7-bit word-aligned offset
8634
8635    // VLDRW.32 Qd, [Rn, #imm]: ED10 xx80 variant
8636    0xED100E80
8637        | (u_bit << 23)
8638        | ((qd_enc >> 4) << 22)
8639        | (rn << 16)
8640        | ((qd_enc & 0xF) << 12)
8641        | (imm7 & 0x7F)
8642}
8643
8644/// Encode MVE VSTRW.32 Qd, [Rn, #offset]
8645fn encode_mve_vstrw(qd: &QReg, addr: &MemAddr) -> u32 {
8646    let qd_enc = qreg_to_num(qd) * 2;
8647    let rn = reg_to_bits(&addr.base);
8648    let offset = addr.offset;
8649    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8650    let abs_offset = offset.unsigned_abs();
8651    let imm7 = (abs_offset / 4) & 0x7F;
8652
8653    0xED000E80
8654        | (u_bit << 23)
8655        | ((qd_enc >> 4) << 22)
8656        | (rn << 16)
8657        | ((qd_enc & 0xF) << 12)
8658        | (imm7 & 0x7F)
8659}
8660
8661impl ArmEncoder {
8662    /// Encode MVE constant load: MOVW+MOVT+VMOV for each 32-bit word, then assemble Q-register
8663    fn encode_thumb_mve_const(&self, qd: &QReg, bytes: &[u8; 16]) -> Result<Vec<u8>> {
8664        let mut result = Vec::new();
8665        let qd_num = qreg_to_num(qd);
8666
8667        // Load each 32-bit word into R12 (temp) then VMOV into S-register
8668        for i in 0..4 {
8669            let word = u32::from_le_bytes([
8670                bytes[i * 4],
8671                bytes[i * 4 + 1],
8672                bytes[i * 4 + 2],
8673                bytes[i * 4 + 3],
8674            ]);
8675            let lo16 = word & 0xFFFF;
8676            let hi16 = (word >> 16) & 0xFFFF;
8677
8678            // MOVW R12, #lo16
8679            result.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
8680            // MOVT R12, #hi16
8681            if hi16 != 0 {
8682                result.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
8683            }
8684
8685            // VMOV Sn, R12 where Sn = Qd*4 + i
8686            let s_num = qd_num * 4 + i as u32;
8687            let (vn, n) = encode_sreg(s_num);
8688            let vmov: u32 = 0xEE000A10 | (vn << 16) | (12 << 12) | (n << 7);
8689            result.extend_from_slice(&vfp_to_thumb_bytes(vmov));
8690        }
8691
8692        Ok(result)
8693    }
8694
8695    /// Encode lane-wise f32 binary operation (VDIV, etc.) via S-register extraction
8696    fn encode_thumb_mve_lane_wise_f32_binop(
8697        &self,
8698        qd: &QReg,
8699        qn: &QReg,
8700        qm: &QReg,
8701        vfp_base: u32,
8702    ) -> Result<Vec<u8>> {
8703        let mut result = Vec::new();
8704        let qd_num = qreg_to_num(qd);
8705        let qn_num = qreg_to_num(qn);
8706        let qm_num = qreg_to_num(qm);
8707
8708        // For each lane 0..3: use S-registers directly (Q aliasing)
8709        for i in 0..4u32 {
8710            let sd = qd_num * 4 + i;
8711            let sn = qn_num * 4 + i;
8712            let sm = qm_num * 4 + i;
8713
8714            let (vd, d) = encode_sreg(sd);
8715            let (vn, n) = encode_sreg(sn);
8716            let (vm, m) = encode_sreg(sm);
8717
8718            let instr = vfp_base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
8719            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
8720        }
8721
8722        Ok(result)
8723    }
8724
8725    /// Encode lane-wise f32 VSQRT via S-register extraction
8726    fn encode_thumb_mve_lane_wise_f32_sqrt(&self, qd: &QReg, qm: &QReg) -> Result<Vec<u8>> {
8727        let mut result = Vec::new();
8728        let qd_num = qreg_to_num(qd);
8729        let qm_num = qreg_to_num(qm);
8730
8731        // VSQRT.F32 base: 0xEEB10AC0
8732        for i in 0..4u32 {
8733            let sd = qd_num * 4 + i;
8734            let sm = qm_num * 4 + i;
8735
8736            let (vd, d) = encode_sreg(sd);
8737            let (vm, m) = encode_sreg(sm);
8738
8739            let instr: u32 = 0xEEB10AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
8740            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
8741        }
8742
8743        Ok(result)
8744    }
8745}
8746
8747#[cfg(test)]
8748mod tests {
8749    use super::*;
8750
8751    #[test]
8752    fn test_encoder_creation() {
8753        let encoder_arm = ArmEncoder::new_arm32();
8754        assert!(!encoder_arm.thumb_mode);
8755
8756        let encoder_thumb = ArmEncoder::new_thumb2();
8757        assert!(encoder_thumb.thumb_mode);
8758    }
8759
8760    /// #204 WAKE-path regression: `SetCond` materialized 0/1 with the 16-bit
8761    /// `MOVS Rd,#imm` (T1), whose Rd field is 3 bits (R0–R7). For a high Rd
8762    /// (R8–R12) `rd_bits << 8` overflows bit 11, flipping the opcode MOVS→CMP
8763    /// (`0x2c00`), so the boolean was never written — gale's `has_waiter` kept a
8764    /// stale value and the binary-sem WAKE dispatch read garbage. High Rd must
8765    /// use the 32-bit `MOV.W` (T2). Verify the bytes, not the IR.
8766    /// #311: the SAME high-Rd MOVS→CMP transmutation as #204, but in the
8767    /// i64 comparison expansions (I64SetCond / I64SetCondZ) — missed by the
8768    /// #204 hardening. With rd=R8 the boolean died in the flags
8769    /// (`ite eq; cmpeq r0,#1; cmpne r0,#0`), so gale's packed-u64 select
8770    /// read a stale register on silicon. High Rd must take MOV.W / CMP.W.
8771    #[test]
8772    fn test_encode_i64setcond_high_reg_uses_mov_w_311() {
8773        use synth_synthesis::{ArmOp, Condition, Reg};
8774        let enc = ArmEncoder::new_thumb2();
8775        let bytes = enc
8776            .encode(&ArmOp::I64SetCond {
8777                rd: Reg::R8,
8778                rn_lo: Reg::R2,
8779                rn_hi: Reg::R3,
8780                rm_lo: Reg::R6,
8781                rm_hi: Reg::R7,
8782                cond: Condition::EQ,
8783            })
8784            .unwrap();
8785        // The 32-bit MOV.W immediate (T2) first halfword is 0xF04F; the
8786        // 16-bit transmuted forms would contain 0x2801/0x2800 (CMP r0,#1/#0).
8787        let halfwords: Vec<u16> = bytes
8788            .chunks(2)
8789            .map(|c| u16::from_le_bytes([c[0], c[1]]))
8790            .collect();
8791        assert!(
8792            halfwords.iter().filter(|&&h| h == 0xF04F).count() == 2,
8793            "high rd must use two MOV.W (T2) encodings, got {halfwords:04x?}"
8794        );
8795        assert!(
8796            !halfwords.contains(&0x2801) && !halfwords.contains(&0x2800),
8797            "no transmuted 16-bit CMP imm: {halfwords:04x?}"
8798        );
8799
8800        let bytes_z = enc
8801            .encode(&ArmOp::I64SetCondZ {
8802                rd: Reg::R8,
8803                rn_lo: Reg::R2,
8804                rn_hi: Reg::R3,
8805            })
8806            .unwrap();
8807        let hw_z: Vec<u16> = bytes_z
8808            .chunks(2)
8809            .map(|c| u16::from_le_bytes([c[0], c[1]]))
8810            .collect();
8811        assert!(
8812            hw_z.iter().filter(|&&h| h == 0xF04F).count() == 2,
8813            "SetCondZ high rd MOV.W: {hw_z:04x?}"
8814        );
8815        // CMP.W rd,#0 (T2) first halfword: 0xF1B0 | rd
8816        assert!(
8817            hw_z.contains(&(0xF1B0 | 8)),
8818            "SetCondZ high rd must use CMP.W: {hw_z:04x?}"
8819        );
8820    }
8821
8822    #[test]
8823    fn test_encode_setcond_high_reg_uses_mov_w_204() {
8824        use synth_synthesis::{ArmOp, Condition, Reg};
8825        let enc = ArmEncoder::new_thumb2();
8826        // R12 (high): must be ITE + MOV.W #1 + MOV.W #0, never a 16-bit MOVS/CMP.
8827        let hi = enc
8828            .encode(&ArmOp::SetCond {
8829                rd: Reg::R12,
8830                cond: Condition::NE,
8831            })
8832            .unwrap();
8833        assert_eq!(hi.len(), 10, "ITE(2) + MOV.W(4) + MOV.W(4): {hi:02x?}");
8834        // both value halfwords are MOV.W (0xF04F) — NOT the corrupt CMP (0x2c..).
8835        assert_eq!(&hi[2..4], &[0x4F, 0xF0], "then = MOV.W: {hi:02x?}");
8836        assert_eq!(&hi[6..8], &[0x4F, 0xF0], "else = MOV.W: {hi:02x?}");
8837        assert_eq!(hi[4] & 0x0F, 0x01, "then imm = #1");
8838        assert_eq!(hi[8] & 0x0F, 0x00, "else imm = #0");
8839        // Low Rd keeps the compact 16-bit MOVS form.
8840        let lo = enc
8841            .encode(&ArmOp::SetCond {
8842                rd: Reg::R0,
8843                cond: Condition::NE,
8844            })
8845            .unwrap();
8846        assert_eq!(lo.len(), 6, "ITE(2) + MOVS(2) + MOVS(2): {lo:02x?}");
8847        assert_eq!(lo[2..4], [0x01, 0x20], "then = MOVS R0,#1");
8848        assert_eq!(lo[4..6], [0x00, 0x20], "else = MOVS R0,#0");
8849    }
8850
8851    /// #209 Opt 1b: UMULL RdLo, RdHi, Rn, Rm encodes correctly on both ISAs.
8852    /// Thumb-2 T1: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm.
8853    /// A32:        cond 0000 1000 RdHi RdLo Rm 1001 Rn.
8854    #[test]
8855    fn test_encode_umull_209b() {
8856        use synth_synthesis::{ArmOp, Reg};
8857        let op = ArmOp::Umull {
8858            rdlo: Reg::R4,
8859            rdhi: Reg::R5,
8860            rn: Reg::R0,
8861            rm: Reg::R3,
8862        };
8863        // Thumb-2: hw1 = 0xFBA0 | 0 = 0xFBA0; hw2 = (4<<12)|(5<<8)|3 = 0x4503.
8864        let t = ArmEncoder::new_thumb2().encode(&op).unwrap();
8865        assert_eq!(
8866            t,
8867            vec![0xA0, 0xFB, 0x03, 0x45],
8868            "umull r4,r5,r0,r3 (T2): {t:02x?}"
8869        );
8870        // A32: 0xE0800090 | (5<<16) | (4<<12) | (3<<8) | 0 = 0xE0854390.
8871        let a = ArmEncoder::new_arm32().encode(&op).unwrap();
8872        assert_eq!(
8873            a,
8874            0xE085_4390u32.to_le_bytes().to_vec(),
8875            "umull (A32): {a:02x?}"
8876        );
8877    }
8878
8879    /// #206 regression: the ARM32 (A32) `Ldr`/`Str` encoders fed `addr` through
8880    /// `encode_mem_addr`, which returns only the 12-bit immediate — so a register
8881    /// offset (`[rn, rm, #off]`) was silently dropped to `[rn, #off]`, sending
8882    /// the access to the wrong runtime address (silent miscompile on the default
8883    /// `--target arm`). A register offset must materialize `ip = rn + rm` and
8884    /// load from `[ip, #off]`. Verify the bytes.
8885    #[test]
8886    fn test_encode_arm32_indexed_load_keeps_index_206() {
8887        use synth_synthesis::{ArmOp, MemAddr, Reg};
8888        let enc = ArmEncoder::new_arm32();
8889        // ldr r0, [r11, r1, #8]  must NOT collapse to a single immediate ldr.
8890        let bytes = enc
8891            .encode(&ArmOp::Ldr {
8892                rd: Reg::R0,
8893                addr: MemAddr::reg_imm(Reg::R11, Reg::R1, 8),
8894            })
8895            .unwrap();
8896        assert_eq!(
8897            bytes.len(),
8898            8,
8899            "expected ADD ip + LDR (2 words): {bytes:02x?}"
8900        );
8901        let add = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
8902        let ldr = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
8903        // ADD ip, r11, r1  = 0xE08BC001
8904        assert_eq!(add, 0xE08B_C001, "ADD ip,r11,r1: {add:#010x}");
8905        // LDR r0, [ip, #8] = 0xE59C0008
8906        assert_eq!(ldr, 0xE59C_0008, "LDR r0,[ip,#8]: {ldr:#010x}");
8907        // A bare immediate ldr (the bug) would be 0xE59B0008 (base=r11) — reject.
8908        assert_ne!(ldr, 0xE59B_0008, "index must not be dropped");
8909    }
8910
8911    /// #594 regression: `call_indirect` on the A32 path (`--target cortex-r5`)
8912    /// was encoded as a literal NOP (0xE1A00000) — the call never happened and
8913    /// the function silently returned the leftover table-index value. The A32
8914    /// encoder must emit a real dispatch expansion, since #642 guarded by an
8915    /// inline bounds check:
8916    /// `MOVW r12, #size; CMP idx, r12; BLO +1; UDF;
8917    ///  MOV r12, idx, LSL #2; LDR r12, [r11, r12]; BLX r12`.
8918    #[test]
8919    fn test_encode_arm32_call_indirect_is_real_call_594() {
8920        use synth_synthesis::{ArmOp, Reg};
8921        let enc = ArmEncoder::new_arm32();
8922        let bytes = enc
8923            .encode(&ArmOp::CallIndirect {
8924                rd: Reg::R0,
8925                type_idx: 0,
8926                table_index_reg: Reg::R0,
8927                table_size: 4,
8928                table_byte_offset: 0,
8929                null_check: false,
8930            })
8931            .unwrap();
8932        assert_eq!(
8933            bytes.len(),
8934            28,
8935            "expected MOVW + CMP + BLO + UDF + MOV + LDR + BLX (7 words): {bytes:02x?}"
8936        );
8937        let words: Vec<u32> = bytes
8938            .chunks_exact(4)
8939            .map(|w| u32::from_le_bytes(w.try_into().unwrap()))
8940            .collect();
8941        // #642 bounds guard: MOVW r12, #4; CMP r0, r12; BLO +1; UDF
8942        assert_eq!(words[0], 0xE300_C004, "MOVW r12,#4: {:#010x}", words[0]);
8943        assert_eq!(words[1], 0xE150_000C, "CMP r0,r12: {:#010x}", words[1]);
8944        assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
8945        assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
8946        // MOV r12, r0, LSL #2 = 0xE1A0C100
8947        assert_eq!(
8948            words[4], 0xE1A0_C100,
8949            "MOV r12,r0,LSL#2: {:#010x}",
8950            words[4]
8951        );
8952        // LDR r12, [r11, r12] = 0xE79BC00C
8953        assert_eq!(
8954            words[5], 0xE79B_C00C,
8955            "LDR r12,[r11,r12]: {:#010x}",
8956            words[5]
8957        );
8958        // BLX r12 = 0xE12FFF3C
8959        assert_eq!(words[6], 0xE12F_FF3C, "BLX r12: {:#010x}", words[6]);
8960        // The bug: a single NOP word. Must never come back.
8961        assert!(
8962            !bytes
8963                .chunks_exact(4)
8964                .any(|w| w == 0xE1A0_0000u32.to_le_bytes()),
8965            "call_indirect must not contain a NOP (#594): {bytes:02x?}"
8966        );
8967
8968        // A non-R0 index register lands in the MOV's Rm and CMP's Rn fields.
8969        let bytes = enc
8970            .encode(&ArmOp::CallIndirect {
8971                rd: Reg::R0,
8972                type_idx: 0,
8973                table_index_reg: Reg::R4,
8974                table_size: 4,
8975                table_byte_offset: 0,
8976                null_check: false,
8977            })
8978            .unwrap();
8979        let cmp = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
8980        assert_eq!(cmp, 0xE154_000C, "CMP r4,r12: {cmp:#010x}");
8981        let mov = u32::from_le_bytes(bytes[16..20].try_into().unwrap());
8982        assert_eq!(mov, 0xE1A0_C104, "MOV r12,r4,LSL#2: {mov:#010x}");
8983    }
8984
8985    /// #642: a table size above 16 bits must not be silently truncated by the
8986    /// MOVW — the A32 guard adds a MOVT for the high half.
8987    #[test]
8988    fn test_encode_arm32_call_indirect_wide_table_size_642() {
8989        use synth_synthesis::{ArmOp, Reg};
8990        let enc = ArmEncoder::new_arm32();
8991        let bytes = enc
8992            .encode(&ArmOp::CallIndirect {
8993                rd: Reg::R0,
8994                type_idx: 0,
8995                table_index_reg: Reg::R0,
8996                table_size: 0x0002_0003,
8997                table_byte_offset: 0,
8998                null_check: false,
8999            })
9000            .unwrap();
9001        assert_eq!(bytes.len(), 32, "MOVT arm adds one word: {bytes:02x?}");
9002        let movw = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9003        let movt = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9004        assert_eq!(movw, 0xE300_C003, "MOVW r12,#3: {movw:#010x}");
9005        assert_eq!(movt, 0xE340_C002, "MOVT r12,#2: {movt:#010x}");
9006    }
9007
9008    /// #597 anchor (justified correctness RE-PIN of the #594-era freeze): the
9009    /// Thumb-2 `CallIndirect` expansion is `mov.w ip, rm, LSL #2; ldr.w ip,
9010    /// [r11, ip]; blx ip`.
9011    ///
9012    /// The #594 PR froze the then-current bytes `4F EA 20 0C ...` whose first
9013    /// word decodes as `mov.w ip, rm, ASR #32` — the intended `LSL #2` had
9014    /// its shift amount in the TYPE field (bits 5:4) instead of imm2 (bits
9015    /// 7:6), so the index was destroyed and every call_indirect dispatched
9016    /// table entry 0 (shipped miscompile, masked by index-0 probes). #597
9017    /// corrects the encoding; new bytes `4F EA 80 0C ...` were
9018    /// execution-validated under unicorn against the wasmtime oracle on a
9019    /// multi-entry table (indexes 0, 1, 3 —
9020    /// scripts/repro/call_indirect_597_differential.py) before this pin was
9021    /// replaced. Old pin: [4F EA 20 0C, 5B F8 0C C0, E0 47] (ASR #32 — must
9022    /// never come back).
9023    #[test]
9024    fn test_encode_thumb_call_indirect_lsl2_597() {
9025        use synth_synthesis::{ArmOp, Reg};
9026        let enc = ArmEncoder::new_thumb2();
9027        let bytes = enc
9028            .encode(&ArmOp::CallIndirect {
9029                rd: Reg::R0,
9030                type_idx: 0,
9031                table_index_reg: Reg::R0,
9032                table_size: 4,
9033                table_byte_offset: 0,
9034                null_check: false,
9035            })
9036            .unwrap();
9037        assert_eq!(
9038            bytes,
9039            vec![
9040                // #642 bounds guard: movw ip,#4; cmp r0,ip; blo +1; udf #0
9041                0x40, 0xF2, 0x04, 0x0C, // movw ip, #4
9042                0x60, 0x45, // cmp r0, ip
9043                0x00, 0xD3, // blo .+4 (skip the udf)
9044                0x00, 0xDE, // udf #0 — OOB index trap (WASM §4.4.8)
9045                // #597-pinned dispatch
9046                0x4F, 0xEA, 0x80, 0x0C, // mov.w ip, r0, lsl #2
9047                0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9048                0xE0, 0x47, // blx ip
9049            ],
9050            "Thumb-2 CallIndirect: bounds guard + mov.w/ldr.w/blx dispatch: {bytes:02x?}"
9051        );
9052        // The #597 bug bytes (ASR #32 dispatch first word) must never come back.
9053        assert!(
9054            !bytes.windows(4).any(|w| w == [0x4F, 0xEA, 0x20, 0x0C]),
9055            "mov.w ip, rm, ASR #32 — the #597 type-field bug"
9056        );
9057
9058        // A non-R0 index register lands in the mov.w's Rm field (hw2 bits 3:0)
9059        // and the cmp's Rn field.
9060        let bytes = enc
9061            .encode(&ArmOp::CallIndirect {
9062                rd: Reg::R0,
9063                type_idx: 0,
9064                table_index_reg: Reg::R4,
9065                table_size: 4,
9066                table_byte_offset: 0,
9067                null_check: false,
9068            })
9069            .unwrap();
9070        assert_eq!(&bytes[4..6], &[0x64, 0x45], "cmp r4, ip: {bytes:02x?}");
9071        assert_eq!(
9072            &bytes[10..14],
9073            &[0x4F, 0xEA, 0x84, 0x0C],
9074            "mov.w ip, r4, LSL #2: {bytes:02x?}"
9075        );
9076    }
9077
9078    /// #642: the Thumb-2 bounds guard for a high-register index (R8 — the top
9079    /// of the allocatable pool) uses the high-reg-capable 16-bit CMP (T2) with
9080    /// the N bit set; a table size above 16 bits adds a MOVT.
9081    #[test]
9082    fn test_encode_thumb_call_indirect_guard_shapes_642() {
9083        use synth_synthesis::{ArmOp, Reg};
9084        let enc = ArmEncoder::new_thumb2();
9085        let bytes = enc
9086            .encode(&ArmOp::CallIndirect {
9087                rd: Reg::R0,
9088                type_idx: 0,
9089                table_index_reg: Reg::R8,
9090                table_size: 3,
9091                table_byte_offset: 0,
9092                null_check: false,
9093            })
9094            .unwrap();
9095        // cmp r8, ip — T2: 0x4500 | N(1)<<7 | Rm(12)<<3 | Rn(0) = 0x45E0
9096        assert_eq!(&bytes[4..6], &[0xE0, 0x45], "cmp r8, ip: {bytes:02x?}");
9097
9098        let bytes = enc
9099            .encode(&ArmOp::CallIndirect {
9100                rd: Reg::R0,
9101                type_idx: 0,
9102                table_index_reg: Reg::R0,
9103                table_size: 0x0002_0003,
9104                table_byte_offset: 0,
9105                null_check: false,
9106            })
9107            .unwrap();
9108        // movw ip,#3 then movt ip,#2 — the size must not be truncated.
9109        assert_eq!(
9110            &bytes[0..8],
9111            &[0x40, 0xF2, 0x03, 0x0C, 0xC0, 0xF2, 0x02, 0x0C],
9112            "movw ip,#3; movt ip,#2: {bytes:02x?}"
9113        );
9114    }
9115
9116    /// #650: a non-zero table base offset (table N of the contiguous R11
9117    /// region) routes the Thumb-2 pointer load through
9118    /// `add.w ip, r11, ip; ldr.w ip, [ip, #offset]` — and offset 0 keeps the
9119    /// pre-#650 single-load bytes IDENTICAL (the by-construction pin).
9120    #[test]
9121    fn test_encode_thumb_call_indirect_table_offset_650() {
9122        use synth_synthesis::{ArmOp, Reg};
9123        let enc = ArmEncoder::new_thumb2();
9124        // falcon's fused-component shape: table 0 has 7 entries, so table 1
9125        // sits at byte offset 28.
9126        let bytes = enc
9127            .encode(&ArmOp::CallIndirect {
9128                rd: Reg::R0,
9129                type_idx: 0,
9130                table_index_reg: Reg::R1,
9131                table_size: 41,
9132                table_byte_offset: 28,
9133                null_check: false,
9134            })
9135            .unwrap();
9136        assert_eq!(
9137            bytes,
9138            vec![
9139                // #642 bounds guard against TABLE 1's OWN size (41)
9140                0x40, 0xF2, 0x29, 0x0C, // movw ip, #41
9141                0x61, 0x45, // cmp r1, ip
9142                0x00, 0xD3, // blo .+4 (skip the udf)
9143                0x00, 0xDE, // udf #0 — OOB trap (WASM §4.4.8)
9144                // dispatch through table 1's base (R11 + 28)
9145                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9146                0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9147                0xDC, 0xF8, 0x1C, 0xC0, // ldr.w ip, [ip, #28]
9148                0xE0, 0x47, // blx ip
9149            ],
9150            "Thumb-2 table-1 dispatch (#650): {bytes:02x?}"
9151        );
9152
9153        // Offset 0 must stay the #597-pinned single-load form (no add.w, no
9154        // imm-form ldr) — single-table byte identity by construction.
9155        let zero = enc
9156            .encode(&ArmOp::CallIndirect {
9157                rd: Reg::R0,
9158                type_idx: 0,
9159                table_index_reg: Reg::R1,
9160                table_size: 41,
9161                table_byte_offset: 0,
9162                null_check: false,
9163            })
9164            .unwrap();
9165        assert_eq!(
9166            &zero[10..],
9167            &[
9168                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9169                0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9170                0xE0, 0x47, // blx ip
9171            ],
9172            "offset 0 keeps the pre-#650 dispatch bytes: {zero:02x?}"
9173        );
9174    }
9175
9176    /// #650: the A32 twin — `add r12, r11, r12; ldr r12, [r12, #offset]` for
9177    /// a non-zero table base offset; offset 0 keeps the #594/#642 form.
9178    #[test]
9179    fn test_encode_arm32_call_indirect_table_offset_650() {
9180        use synth_synthesis::{ArmOp, Reg};
9181        let enc = ArmEncoder::new_arm32();
9182        let bytes = enc
9183            .encode(&ArmOp::CallIndirect {
9184                rd: Reg::R0,
9185                type_idx: 0,
9186                table_index_reg: Reg::R1,
9187                table_size: 41,
9188                table_byte_offset: 28,
9189                null_check: false,
9190            })
9191            .unwrap();
9192        let words: Vec<u32> = bytes
9193            .chunks_exact(4)
9194            .map(|w| u32::from_le_bytes(w.try_into().unwrap()))
9195            .collect();
9196        assert_eq!(words[0], 0xE300_C029, "MOVW r12,#41: {:#010x}", words[0]);
9197        assert_eq!(words[1], 0xE151_000C, "CMP r1,r12: {:#010x}", words[1]);
9198        assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9199        assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9200        assert_eq!(
9201            words[4], 0xE1A0_C101,
9202            "MOV r12,r1,LSL#2: {:#010x}",
9203            words[4]
9204        );
9205        assert_eq!(
9206            words[5], 0xE08B_C00C,
9207            "ADD r12,r11,r12 (#650): {:#010x}",
9208            words[5]
9209        );
9210        assert_eq!(
9211            words[6], 0xE59C_C01C,
9212            "LDR r12,[r12,#28] (#650): {:#010x}",
9213            words[6]
9214        );
9215        assert_eq!(words[7], 0xE12F_FF3C, "BLX r12: {:#010x}", words[7]);
9216    }
9217
9218    /// #664: `null_check` inserts a null-funcref trap between the Thumb-2
9219    /// pointer load and the `BLX` (`cmp.w ip, #0; bne .+4; udf #0`) — a
9220    /// zero-linked (uninitialized) slot must TRAP (WASM §4.4.8), never
9221    /// branch to address 0. `null_check: false` keeps the expansion
9222    /// byte-identical to the pre-#664 form (by-construction pin).
9223    #[test]
9224    fn test_encode_thumb_call_indirect_null_check_664() {
9225        use synth_synthesis::{ArmOp, Reg};
9226        let enc = ArmEncoder::new_thumb2();
9227        let op = |null_check| ArmOp::CallIndirect {
9228            rd: Reg::R0,
9229            type_idx: 0,
9230            table_index_reg: Reg::R1,
9231            table_size: 4,
9232            table_byte_offset: 0,
9233            null_check,
9234        };
9235        let with = enc.encode(&op(true)).unwrap();
9236        let without = enc.encode(&op(false)).unwrap();
9237        // The checked form = the unchecked form with EXACTLY the three-insn
9238        // null check spliced in before the final BLX (byte identity of the
9239        // shared prefix/suffix — nothing else may move).
9240        assert_eq!(
9241            with.len(),
9242            without.len() + 8,
9243            "cmp.w (4) + bne (2) + udf (2): {with:02x?}"
9244        );
9245        let blx_at = without.len() - 2;
9246        assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9247        assert_eq!(
9248            &with[blx_at..],
9249            &[
9250                0xBC, 0xF1, 0x00, 0x0F, // cmp.w ip, #0
9251                0x00, 0xD1, // bne .+4 (skip the udf)
9252                0x00, 0xDE, // udf #0 — null-funcref trap (#664)
9253                0xE0, 0x47, // blx ip
9254            ],
9255            "null check precedes the BLX: {with:02x?}"
9256        );
9257        assert_eq!(&with[with.len() - 2..], &without[blx_at..], "same BLX");
9258    }
9259
9260    /// #664: the A32 twin — `cmp r12, #0; bne .+8; udf` before the `BLX`;
9261    /// `null_check: false` keeps the #594/#642/#650 bytes identical.
9262    #[test]
9263    fn test_encode_arm32_call_indirect_null_check_664() {
9264        use synth_synthesis::{ArmOp, Reg};
9265        let enc = ArmEncoder::new_arm32();
9266        let op = |null_check| ArmOp::CallIndirect {
9267            rd: Reg::R0,
9268            type_idx: 0,
9269            table_index_reg: Reg::R1,
9270            table_size: 4,
9271            table_byte_offset: 0,
9272            null_check,
9273        };
9274        let with = enc.encode(&op(true)).unwrap();
9275        let without = enc.encode(&op(false)).unwrap();
9276        assert_eq!(with.len(), without.len() + 12, "3 A32 words: {with:02x?}");
9277        let blx_at = without.len() - 4;
9278        assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9279        let words: Vec<u32> = with[blx_at..]
9280            .chunks_exact(4)
9281            .map(|w| u32::from_le_bytes(w.try_into().unwrap()))
9282            .collect();
9283        assert_eq!(words[0], 0xE35C_0000, "CMP r12,#0: {:#010x}", words[0]);
9284        assert_eq!(words[1], 0x1A00_0000, "BNE +1 insn: {:#010x}", words[1]);
9285        assert_eq!(words[2], 0xE7F0_00F0, "UDF (null trap): {:#010x}", words[2]);
9286        assert_eq!(words[3], 0xE12F_FF3C, "BLX r12: {:#010x}", words[3]);
9287    }
9288
9289    /// #178/#180 regression: the Thumb `Add`/`Adds`/`Subs` reg-forms used the
9290    /// 16-bit encoding unconditionally. For high registers (R12 base scratch,
9291    /// R8-R11 i64 pairs) the 3-bit register fields overflow and corrupt the
9292    /// operands — `add ip,ip,r0` came out as `adds r4,r5,r1` (0x186C), silently
9293    /// dropping the address operand and miscompiling every optimized memory
9294    /// access. High registers must use the 32-bit `.W` forms.
9295    #[test]
9296    fn test_encode_thumb_add_high_reg_uses_add_w_178_180() {
9297        let encoder = ArmEncoder::new_thumb2();
9298
9299        // add ip, ip, r0  — the exact MemLoad/MemStore base+addr op.
9300        let code = encoder
9301            .encode(&ArmOp::Add {
9302                rd: Reg::R12,
9303                rn: Reg::R12,
9304                op2: Operand2::Reg(Reg::R0),
9305            })
9306            .unwrap();
9307        // ADD.W ip, ip, r0 = EB0C 0C00 (little-endian halfwords).
9308        assert_eq!(
9309            code,
9310            vec![0x0C, 0xEB, 0x00, 0x0C],
9311            "high-reg Thumb ADD must be 32-bit ADD.W (EB0C 0C00), not corrupt 16-bit; got {code:02X?}"
9312        );
9313        // Must NOT be the buggy 16-bit 0x186C (`adds r4,r5,r1`).
9314        assert_ne!(code, vec![0x6C, 0x18], "regressed to corrupt 16-bit ADDS");
9315
9316        // Low-register add stays 16-bit (no regression for the common case).
9317        let lo = encoder
9318            .encode(&ArmOp::Add {
9319                rd: Reg::R1,
9320                rn: Reg::R2,
9321                op2: Operand2::Reg(Reg::R3),
9322            })
9323            .unwrap();
9324        assert_eq!(
9325            lo.len(),
9326            2,
9327            "low-reg ADD should remain 16-bit, got {lo:02X?}"
9328        );
9329    }
9330
9331    /// #178/#180 sibling: i64 low-word `Adds`/`Subs` can land in R8-R11 pairs;
9332    /// those must fall back to 32-bit ADDS.W/SUBS.W (flag-setting preserved).
9333    #[test]
9334    fn test_encode_thumb_adds_subs_high_reg_use_32bit_178_180() {
9335        let encoder = ArmEncoder::new_thumb2();
9336
9337        // adds r10, r10, r8  → ADDS.W = EB1A 0A08
9338        let adds = encoder
9339            .encode(&ArmOp::Adds {
9340                rd: Reg::R10,
9341                rn: Reg::R10,
9342                op2: Operand2::Reg(Reg::R8),
9343            })
9344            .unwrap();
9345        assert_eq!(
9346            adds,
9347            vec![0x1A, 0xEB, 0x08, 0x0A],
9348            "high-reg ADDS must be 32-bit ADDS.W (EB1A 0A08); got {adds:02X?}"
9349        );
9350
9351        // subs r10, r10, r8  → SUBS.W = EBBA 0A08
9352        let subs = encoder
9353            .encode(&ArmOp::Subs {
9354                rd: Reg::R10,
9355                rn: Reg::R10,
9356                op2: Operand2::Reg(Reg::R8),
9357            })
9358            .unwrap();
9359        assert_eq!(
9360            subs,
9361            vec![0xBA, 0xEB, 0x08, 0x0A],
9362            "high-reg SUBS must be 32-bit SUBS.W (EBBA 0A08); got {subs:02X?}"
9363        );
9364    }
9365
9366    /// #184 (sibling of #180): 16-bit CMN (T1) only encodes R0-R7. High registers
9367    /// must use 32-bit CMN.W, not the corrupt truncated 16-bit form.
9368    #[test]
9369    fn test_encode_thumb_cmn_high_reg_uses_cmn_w_184() {
9370        let encoder = ArmEncoder::new_thumb2();
9371
9372        // cmn r10, r8  → CMN.W = EB1A 0F08 (ADD.W S=1, Rd=PC discarded).
9373        let cmn = encoder
9374            .encode(&ArmOp::Cmn {
9375                rn: Reg::R10,
9376                op2: Operand2::Reg(Reg::R8),
9377            })
9378            .unwrap();
9379        assert_eq!(
9380            cmn,
9381            vec![0x1A, 0xEB, 0x08, 0x0F],
9382            "high-reg CMN must be 32-bit CMN.W (EB1A 0F08); got {cmn:02X?}"
9383        );
9384
9385        // Low registers stay 16-bit: cmn r1, r2 = 0x42D1.
9386        let lo = encoder
9387            .encode(&ArmOp::Cmn {
9388                rn: Reg::R1,
9389                op2: Operand2::Reg(Reg::R2),
9390            })
9391            .unwrap();
9392        assert_eq!(
9393            lo.len(),
9394            2,
9395            "low-reg CMN should remain 16-bit, got {lo:02X?}"
9396        );
9397        assert_eq!(lo, vec![0xD1, 0x42], "low-reg CMN bytes wrong: {lo:02X?}");
9398    }
9399
9400    /// #185 regression: feeding PC (R15) as a data operand to a Thumb-2 op that
9401    /// guards its registers must return Err, not panic under debug-assertions.
9402    /// (Synth never emits PC here; the fuzz harness requires encode() be total.)
9403    #[test]
9404    fn test_encode_pc_operand_returns_err_not_panic_185() {
9405        let encoder = ArmEncoder::new_thumb2();
9406        for op in [
9407            ArmOp::Sdiv {
9408                rd: Reg::PC,
9409                rn: Reg::R0,
9410                rm: Reg::R1,
9411            },
9412            ArmOp::Udiv {
9413                rd: Reg::R0,
9414                rn: Reg::PC,
9415                rm: Reg::R1,
9416            },
9417            ArmOp::Sdiv {
9418                rd: Reg::R0,
9419                rn: Reg::R1,
9420                rm: Reg::PC,
9421            },
9422        ] {
9423            let r = encoder.encode(&op);
9424            assert!(
9425                r.is_err(),
9426                "encode({op:?}) must return Err for a PC operand, got {r:?}"
9427            );
9428        }
9429        // Valid registers still encode fine (no false rejection).
9430        assert!(
9431            encoder
9432                .encode(&ArmOp::Sdiv {
9433                    rd: Reg::R0,
9434                    rn: Reg::R1,
9435                    rm: Reg::R2
9436                })
9437                .is_ok()
9438        );
9439    }
9440
9441    #[test]
9442    fn test_encode_nop_arm32() {
9443        let encoder = ArmEncoder::new_arm32();
9444        let code = encoder.encode(&ArmOp::Nop).unwrap();
9445
9446        assert_eq!(code.len(), 4); // ARM32 instructions are 4 bytes
9447        assert_eq!(code, vec![0x00, 0x00, 0xA0, 0xE1]); // MOV R0, R0
9448    }
9449
9450    #[test]
9451    fn test_encode_nop_thumb() {
9452        let encoder = ArmEncoder::new_thumb2();
9453        let code = encoder.encode(&ArmOp::Nop).unwrap();
9454
9455        assert_eq!(code.len(), 2); // Thumb instructions are 2 bytes
9456        assert_eq!(code, vec![0x00, 0xBF]); // NOP
9457    }
9458
9459    #[test]
9460    fn test_encode_mov_immediate_arm32() {
9461        let encoder = ArmEncoder::new_arm32();
9462        let op = ArmOp::Mov {
9463            rd: Reg::R0,
9464            op2: Operand2::Imm(42),
9465        };
9466
9467        let code = encoder.encode(&op).unwrap();
9468        assert_eq!(code.len(), 4);
9469
9470        // Verify it's a MOV instruction (bits should have immediate flag set)
9471        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9472        assert_eq!(instr & 0x0E000000, 0x02000000); // Check I bit is set
9473    }
9474
9475    #[test]
9476    fn test_encode_add_registers_arm32() {
9477        let encoder = ArmEncoder::new_arm32();
9478        let op = ArmOp::Add {
9479            rd: Reg::R0,
9480            rn: Reg::R1,
9481            op2: Operand2::Reg(Reg::R2),
9482        };
9483
9484        let code = encoder.encode(&op).unwrap();
9485        assert_eq!(code.len(), 4);
9486
9487        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9488        // Verify it's an ADD instruction with correct opcode
9489        assert_eq!(instr & 0x0FE00000, 0x00800000);
9490    }
9491
9492    /// #350 — `encode_thumb32_add_imm` must lower an out-of-range immediate
9493    /// (> 0xFFF) to a legal MOVW(/MOVT) + ADD.W-register sequence instead of
9494    /// erroring. The small-imm fast path (imm <= 0xFFF) stays byte-identical.
9495    #[test]
9496    fn test_encode_add_imm_large_350() {
9497        let enc = ArmEncoder::new_thumb2();
9498
9499        // --- Fast path unchanged: imm <= 0xFFF is a single 4-byte ADD.W ---
9500        let small = enc
9501            .encode_thumb32_add_imm(&Reg::R0, &Reg::R1, 0x123)
9502            .unwrap();
9503        assert_eq!(small.len(), 4, "small imm must stay a single instruction");
9504
9505        // helper: decode a Thumb-2 MOVW/MOVT halfword pair back to its imm16
9506        fn movx_imm16(b: &[u8]) -> u32 {
9507            let hw1 = u16::from_le_bytes([b[0], b[1]]) as u32;
9508            let hw2 = u16::from_le_bytes([b[2], b[3]]) as u32;
9509            let imm4 = hw1 & 0xF;
9510            let i = (hw1 >> 10) & 1;
9511            let imm3 = (hw2 >> 12) & 0x7;
9512            let imm8 = hw2 & 0xFF;
9513            (imm4 << 12) | (i << 11) | (imm3 << 8) | imm8
9514        }
9515        fn movx_rd(b: &[u8]) -> u32 {
9516            (u16::from_le_bytes([b[2], b[3]]) as u32 >> 8) & 0xF
9517        }
9518
9519        // --- rd != rn: scratch is rd. imm = 70000 = 0x11170 needs MOVW+MOVT. ---
9520        // 0x11170: lo16 = 0x1170, hi16 = 0x0001
9521        let seq = enc
9522            .encode_thumb32_add_imm(&Reg::R12, &Reg::R0, 70000)
9523            .unwrap();
9524        assert_eq!(seq.len(), 12, "MOVW + MOVT + ADD = 12 bytes");
9525        // MOVW r12, #0x1170
9526        assert_eq!(u16::from_le_bytes([seq[0], seq[1]]) & 0xFBF0, 0xF240);
9527        assert_eq!(movx_rd(&seq[0..4]), 12);
9528        assert_eq!(movx_imm16(&seq[0..4]), 0x1170);
9529        // MOVT r12, #0x0001
9530        assert_eq!(u16::from_le_bytes([seq[4], seq[5]]) & 0xFBF0, 0xF2C0);
9531        assert_eq!(movx_rd(&seq[4..8]), 12);
9532        assert_eq!(movx_imm16(&seq[4..8]), 0x0001);
9533        // ADD.W r12, r0, r12  (EB00 | rn=0 ; rd=12, rm=12)
9534        let add1 = u16::from_le_bytes([seq[8], seq[9]]) as u32;
9535        let add2 = u16::from_le_bytes([seq[10], seq[11]]) as u32;
9536        assert_eq!(add1 & 0xFFF0, 0xEB00);
9537        assert_eq!(add1 & 0xF, 0); // rn = r0
9538        assert_eq!((add2 >> 8) & 0xF, 12); // rd = r12
9539        assert_eq!(add2 & 0xF, 12); // rm = scratch = r12
9540        // The materialized scratch must reconstruct exactly 70000.
9541        assert_eq!(
9542            (movx_imm16(&seq[4..8]) << 16) | movx_imm16(&seq[0..4]),
9543            70000
9544        );
9545
9546        // --- imm <= 0xFFFF: MOVT is skipped (MOVW + ADD = 8 bytes). ---
9547        let seq16 = enc
9548            .encode_thumb32_add_imm(&Reg::R3, &Reg::R0, 0xABCD)
9549            .unwrap();
9550        assert_eq!(seq16.len(), 8, "imm <= 0xFFFF skips MOVT");
9551        assert_eq!(movx_imm16(&seq16[0..4]), 0xABCD);
9552        assert_eq!(movx_rd(&seq16[0..4]), 3); // scratch = rd = r3
9553
9554        // --- rd == rn (in-place add): scratch must be R12, not rd. ---
9555        // imm = 0x12345: lo16 = 0x2345, hi16 = 0x0001
9556        let inplace = enc
9557            .encode_thumb32_add_imm(&Reg::R5, &Reg::R5, 0x12345)
9558            .unwrap();
9559        assert_eq!(inplace.len(), 12);
9560        assert_eq!(movx_rd(&inplace[0..4]), 12, "rd==rn must use R12 scratch");
9561        assert_eq!(
9562            (movx_imm16(&inplace[4..8]) << 16) | movx_imm16(&inplace[0..4]),
9563            0x12345
9564        );
9565        // ADD.W r5, r5, r12 — rm must be the scratch (12), never rn.
9566        let ip_add2 = u16::from_le_bytes([inplace[10], inplace[11]]) as u32;
9567        assert_eq!(ip_add2 & 0xF, 12);
9568        assert_eq!((ip_add2 >> 8) & 0xF, 5);
9569    }
9570
9571    /// #350 follow-up — the `encoder_no_panic` fuzz harness drives the encoder
9572    /// with ARBITRARY registers, including the one case the in-place lowering
9573    /// cannot serve: rd==rn==R12. There the scratch (R12, the reserved encoder
9574    /// register) would alias Rn and clobber it before the ADD reads it. The
9575    /// encoder contract (#180/#185) is Ok-or-Err, never a panic — so this must
9576    /// return Err, not assert. (Real codegen never emits rd==rn==R12 because R12
9577    /// is non-allocatable; this guards only the fuzz/adversarial path.)
9578    #[test]
9579    fn test_encode_add_imm_large_rd_rn_r12_errs_not_panics_350() {
9580        let enc = ArmEncoder::new_thumb2();
9581        // Out-of-range imm with rd==rn==R12: no free scratch -> Err.
9582        let r = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 70000);
9583        assert!(
9584            r.is_err(),
9585            "rd==rn==R12 with out-of-range imm must Err (no free scratch), got {r:?}"
9586        );
9587        // Small imm with rd==rn==R12 still takes the single-instruction fast path
9588        // (no scratch needed) and must succeed — the guard is scoped to the
9589        // out-of-range lowering only.
9590        let small = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 0x10);
9591        assert!(small.is_ok(), "small imm needs no scratch, must stay Ok");
9592    }
9593
9594    /// #378 — `encode_operand2` (ARM32 data-processing operand) must FAIL
9595    /// HONESTLY on an immediate that is not a valid rotated immediate, rather
9596    /// than silently masking it to `imm & 0xFF` and emitting a WRONG
9597    /// instruction. `0x1FF` has 9 set bits, so it cannot come from rotating an
9598    /// 8-bit imm8 — non-encodable. Real codegen materializes large constants via
9599    /// MOVW/MOVT; this guards the encoder's Ok-or-Err contract (#180/#185)
9600    /// directly. It is an Err (not a panic) so the `encoder_no_panic` fuzz
9601    /// harness — which drives arbitrary operands — still passes.
9602    #[test]
9603    fn test_encode_operand2_non_rotatable_imm_errs_not_masks_378() {
9604        let enc = ArmEncoder::new_arm32();
9605        let bad = enc.encode(&ArmOp::Add {
9606            rd: Reg::R0,
9607            rn: Reg::R1,
9608            op2: Operand2::Imm(0x1FF),
9609        });
9610        assert!(
9611            bad.is_err(),
9612            "non-rotatable ARM32 immediate 0x1FF must Err (was silently masked \
9613             to 0xFF), got {bad:?}"
9614        );
9615        // A representable rotated immediate still encodes fine (regression guard).
9616        let ok = enc.encode(&ArmOp::Add {
9617            rd: Reg::R0,
9618            rn: Reg::R1,
9619            op2: Operand2::Imm(0xFF),
9620        });
9621        assert!(
9622            ok.is_ok(),
9623            "0xFF is a valid rotated immediate, must stay Ok"
9624        );
9625    }
9626
9627    #[test]
9628    fn test_encode_ldr_arm32() {
9629        let encoder = ArmEncoder::new_arm32();
9630        let op = ArmOp::Ldr {
9631            rd: Reg::R0,
9632            addr: MemAddr::imm(Reg::R1, 4),
9633        };
9634
9635        let code = encoder.encode(&op).unwrap();
9636        assert_eq!(code.len(), 4);
9637
9638        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9639        // Verify load bit is set
9640        assert_eq!(instr & 0x00100000, 0x00100000);
9641    }
9642
9643    #[test]
9644    fn test_encode_str_arm32() {
9645        let encoder = ArmEncoder::new_arm32();
9646        let op = ArmOp::Str {
9647            rd: Reg::R0,
9648            addr: MemAddr::imm(Reg::SP, 0),
9649        };
9650
9651        let code = encoder.encode(&op).unwrap();
9652        assert_eq!(code.len(), 4);
9653    }
9654
9655    #[test]
9656    fn test_encode_branch_arm32() {
9657        let encoder = ArmEncoder::new_arm32();
9658        let op = ArmOp::Bl {
9659            label: "main".to_string(),
9660        };
9661
9662        let code = encoder.encode(&op).unwrap();
9663        assert_eq!(code.len(), 4);
9664
9665        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9666        // Verify BL opcode
9667        assert_eq!(instr & 0x0F000000, 0x0B000000);
9668    }
9669
9670    /// Regression test for #167 + #174: the Thumb-2 BL relocatable placeholder
9671    /// must carry a -4 addend so an R_ARM_THM_CALL nets to exactly the symbol S.
9672    /// The correct encoding is what `gas` emits for `bl <extern>`: f7ff fffe
9673    /// (hw1=0xF7FF, hw2=0xFFFE), little-endian bytes FF F7 FE FF.
9674    ///   - 0xD000 (J1=J2=0) → ~+0x600000 garbage addend: `bl c0000c` / truncated
9675    ///     to fit (#167).
9676    ///   - 0xF800 (addend 0) → lands at S+4, one instruction past the callee
9677    ///     entry (#174).
9678    ///   - 0xFFFE (addend -4) → lands at S. Correct.
9679    #[test]
9680    fn test_encode_thumb_bl_placeholder_addend_167_174() {
9681        let encoder = ArmEncoder::new_thumb2();
9682        let op = ArmOp::Bl {
9683            label: "callee".to_string(),
9684        };
9685
9686        let code = encoder.encode(&op).unwrap();
9687        assert_eq!(code.len(), 4, "Thumb-2 BL is 32-bit");
9688
9689        let hw1 = u16::from_le_bytes([code[0], code[1]]);
9690        let hw2 = u16::from_le_bytes([code[2], code[3]]);
9691        assert_eq!(hw1, 0xF7FF, "BL first halfword (matches gas `bl <extern>`)");
9692        assert_eq!(
9693            hw2, 0xFFFE,
9694            "BL second halfword must be 0xFFFE (-4 addend → nets to S), not 0xF800 (→ S+4, #174) or 0xD000 (#167)"
9695        );
9696        assert_ne!(hw2, 0xF800, "0xF800 (addend 0) lands at S+4 (#174)");
9697        assert_ne!(hw2, 0xD000, "0xD000 bakes in a ~+0x600000 addend (#167)");
9698    }
9699
9700    #[test]
9701    fn test_encode_sequence() {
9702        let encoder = ArmEncoder::new_arm32();
9703        let ops = vec![
9704            ArmOp::Mov {
9705                rd: Reg::R0,
9706                op2: Operand2::Imm(42),
9707            },
9708            ArmOp::Mov {
9709                rd: Reg::R1,
9710                op2: Operand2::Imm(10),
9711            },
9712            ArmOp::Add {
9713                rd: Reg::R2,
9714                rn: Reg::R0,
9715                op2: Operand2::Reg(Reg::R1),
9716            },
9717        ];
9718
9719        let code = encoder.encode_sequence(&ops).unwrap();
9720        assert_eq!(code.len(), 12); // 3 instructions * 4 bytes
9721    }
9722
9723    #[test]
9724    fn test_reg_to_bits() {
9725        assert_eq!(reg_to_bits(&Reg::R0), 0);
9726        assert_eq!(reg_to_bits(&Reg::R7), 7);
9727        assert_eq!(reg_to_bits(&Reg::SP), 13);
9728        assert_eq!(reg_to_bits(&Reg::LR), 14);
9729        assert_eq!(reg_to_bits(&Reg::PC), 15);
9730    }
9731
9732    #[test]
9733    fn test_encode_bitwise_operations() {
9734        let encoder = ArmEncoder::new_arm32();
9735
9736        let and_op = ArmOp::And {
9737            rd: Reg::R0,
9738            rn: Reg::R1,
9739            op2: Operand2::Reg(Reg::R2),
9740        };
9741        let and_code = encoder.encode(&and_op).unwrap();
9742        assert_eq!(and_code.len(), 4);
9743
9744        let orr_op = ArmOp::Orr {
9745            rd: Reg::R0,
9746            rn: Reg::R1,
9747            op2: Operand2::Reg(Reg::R2),
9748        };
9749        let orr_code = encoder.encode(&orr_op).unwrap();
9750        assert_eq!(orr_code.len(), 4);
9751
9752        let eor_op = ArmOp::Eor {
9753            rd: Reg::R0,
9754            rn: Reg::R1,
9755            op2: Operand2::Reg(Reg::R2),
9756        };
9757        let eor_code = encoder.encode(&eor_op).unwrap();
9758        assert_eq!(eor_code.len(), 4);
9759    }
9760
9761    // === Thumb-2 32-bit encoding tests ===
9762
9763    #[test]
9764    fn test_encode_sdiv_thumb2() {
9765        let encoder = ArmEncoder::new_thumb2();
9766        let op = ArmOp::Sdiv {
9767            rd: Reg::R0,
9768            rn: Reg::R1,
9769            rm: Reg::R2,
9770        };
9771
9772        let code = encoder.encode(&op).unwrap();
9773        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9774
9775        // SDIV R0, R1, R2: 0xFB91 0xF0F2
9776        // First halfword: 0xFB90 | Rn(1) = 0xFB91
9777        // Second halfword: 0xF0F0 | Rd(0)<<8 | Rm(2) = 0xF0F2
9778        // Little-endian: [0x91, 0xFB, 0xF2, 0xF0]
9779        assert_eq!(code[0], 0x91);
9780        assert_eq!(code[1], 0xFB);
9781        assert_eq!(code[2], 0xF2);
9782        assert_eq!(code[3], 0xF0);
9783    }
9784
9785    #[test]
9786    fn test_encode_udiv_thumb2() {
9787        let encoder = ArmEncoder::new_thumb2();
9788        let op = ArmOp::Udiv {
9789            rd: Reg::R0,
9790            rn: Reg::R1,
9791            rm: Reg::R2,
9792        };
9793
9794        let code = encoder.encode(&op).unwrap();
9795        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9796
9797        // UDIV R0, R1, R2: 0xFBB1 0xF0F2
9798        // Little-endian: [0xB1, 0xFB, 0xF2, 0xF0]
9799        assert_eq!(code[0], 0xB1);
9800        assert_eq!(code[1], 0xFB);
9801        assert_eq!(code[2], 0xF2);
9802        assert_eq!(code[3], 0xF0);
9803    }
9804
9805    #[test]
9806    fn test_encode_mul_thumb2() {
9807        let encoder = ArmEncoder::new_thumb2();
9808        let op = ArmOp::Mul {
9809            rd: Reg::R0,
9810            rn: Reg::R1,
9811            rm: Reg::R2,
9812        };
9813
9814        let code = encoder.encode(&op).unwrap();
9815        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9816    }
9817
9818    #[test]
9819    fn test_encode_and_thumb2() {
9820        let encoder = ArmEncoder::new_thumb2();
9821        let op = ArmOp::And {
9822            rd: Reg::R0,
9823            rn: Reg::R1,
9824            op2: Operand2::Reg(Reg::R2),
9825        };
9826
9827        let code = encoder.encode(&op).unwrap();
9828        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9829    }
9830
9831    #[test]
9832    fn test_encode_lsl_thumb2_low_regs() {
9833        let encoder = ArmEncoder::new_thumb2();
9834        let op = ArmOp::Lsl {
9835            rd: Reg::R0,
9836            rn: Reg::R1,
9837            shift: 5,
9838        };
9839
9840        let code = encoder.encode(&op).unwrap();
9841        assert_eq!(code.len(), 2); // 16-bit for low registers
9842    }
9843
9844    #[test]
9845    fn test_encode_clz_thumb2() {
9846        let encoder = ArmEncoder::new_thumb2();
9847        let op = ArmOp::Clz {
9848            rd: Reg::R0,
9849            rm: Reg::R1,
9850        };
9851
9852        let code = encoder.encode(&op).unwrap();
9853        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9854    }
9855
9856    #[test]
9857    fn test_encode_bx_thumb2() {
9858        let encoder = ArmEncoder::new_thumb2();
9859        let op = ArmOp::Bx { rm: Reg::LR };
9860
9861        let code = encoder.encode(&op).unwrap();
9862        assert_eq!(code.len(), 2); // 16-bit instruction
9863
9864        // BX LR: 0x4770
9865        assert_eq!(code, vec![0x70, 0x47]);
9866    }
9867
9868    // ========================================================================
9869    // f32 pseudo-op encoding tests
9870    // ========================================================================
9871
9872    #[test]
9873    fn test_encode_f32_abs_arm32() {
9874        let encoder = ArmEncoder::new_arm32();
9875        let op = ArmOp::F32Abs {
9876            sd: VfpReg::S0,
9877            sm: VfpReg::S2,
9878        };
9879        let code = encoder.encode(&op).unwrap();
9880        assert_eq!(code.len(), 4); // Single VFP instruction
9881    }
9882
9883    #[test]
9884    fn test_encode_f32_neg_arm32() {
9885        let encoder = ArmEncoder::new_arm32();
9886        let op = ArmOp::F32Neg {
9887            sd: VfpReg::S0,
9888            sm: VfpReg::S2,
9889        };
9890        let code = encoder.encode(&op).unwrap();
9891        assert_eq!(code.len(), 4);
9892    }
9893
9894    #[test]
9895    fn test_encode_f32_sqrt_arm32() {
9896        let encoder = ArmEncoder::new_arm32();
9897        let op = ArmOp::F32Sqrt {
9898            sd: VfpReg::S0,
9899            sm: VfpReg::S2,
9900        };
9901        let code = encoder.encode(&op).unwrap();
9902        assert_eq!(code.len(), 4);
9903    }
9904
9905    #[test]
9906    fn test_encode_f32_ceil_arm32() {
9907        let encoder = ArmEncoder::new_arm32();
9908        let op = ArmOp::F32Ceil {
9909            sd: VfpReg::S0,
9910            sm: VfpReg::S2,
9911        };
9912        let code = encoder.encode(&op).unwrap();
9913        // VMRS + BIC + ORR + VMSR + VCVT.S32.F32 + VMRS + BIC + VMSR + VCVT.F32.S32
9914        assert_eq!(code.len(), 36);
9915    }
9916
9917    #[test]
9918    fn test_encode_f32_floor_thumb2() {
9919        let encoder = ArmEncoder::new_thumb2();
9920        let op = ArmOp::F32Floor {
9921            sd: VfpReg::S0,
9922            sm: VfpReg::S2,
9923        };
9924        let code = encoder.encode(&op).unwrap();
9925        // VMRS + BIC.W + ORR.W + VMSR + VCVT + VMRS + BIC.W + VMSR + VCVT.F32.S32
9926        assert_eq!(code.len(), 36);
9927    }
9928
9929    #[test]
9930    fn test_encode_f32_min_arm32() {
9931        let encoder = ArmEncoder::new_arm32();
9932        let op = ArmOp::F32Min {
9933            sd: VfpReg::S0,
9934            sn: VfpReg::S2,
9935            sm: VfpReg::S4,
9936        };
9937        let code = encoder.encode(&op).unwrap();
9938        assert_eq!(code.len(), 16); // VMOV + VCMP + VMRS + conditional VMOV
9939    }
9940
9941    #[test]
9942    fn test_encode_f32_max_thumb2() {
9943        let encoder = ArmEncoder::new_thumb2();
9944        let op = ArmOp::F32Max {
9945            sd: VfpReg::S0,
9946            sn: VfpReg::S2,
9947            sm: VfpReg::S4,
9948        };
9949        let code = encoder.encode(&op).unwrap();
9950        // VMOV(4) + VCMP(4) + VMRS(4) + IT(2) + VMOV(4) = 18
9951        assert_eq!(code.len(), 18);
9952    }
9953
9954    #[test]
9955    fn test_encode_f32_copysign_arm32() {
9956        let encoder = ArmEncoder::new_arm32();
9957        let op = ArmOp::F32Copysign {
9958            sd: VfpReg::S0,
9959            sn: VfpReg::S2,
9960            sm: VfpReg::S4,
9961        };
9962        let code = encoder.encode(&op).unwrap();
9963        // VMOV + VMOV + AND + BIC + ORR + VMOV = 6 * 4 = 24
9964        assert_eq!(code.len(), 24);
9965    }
9966
9967    // ========================================================================
9968    // f64 encoding tests
9969    // ========================================================================
9970
9971    #[test]
9972    fn test_encode_f64_add_arm32() {
9973        let encoder = ArmEncoder::new_arm32();
9974        let op = ArmOp::F64Add {
9975            dd: VfpReg::D0,
9976            dn: VfpReg::D1,
9977            dm: VfpReg::D2,
9978        };
9979        let code = encoder.encode(&op).unwrap();
9980        assert_eq!(code.len(), 4);
9981        // VADD.F64 D0, D1, D2: check coprocessor is cp11 (0xB)
9982        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9983        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
9984    }
9985
9986    #[test]
9987    fn test_encode_f64_sub_thumb2() {
9988        let encoder = ArmEncoder::new_thumb2();
9989        let op = ArmOp::F64Sub {
9990            dd: VfpReg::D0,
9991            dn: VfpReg::D1,
9992            dm: VfpReg::D2,
9993        };
9994        let code = encoder.encode(&op).unwrap();
9995        assert_eq!(code.len(), 4); // 32-bit VFP as two Thumb halfwords
9996    }
9997
9998    #[test]
9999    fn test_encode_f64_mul_arm32() {
10000        let encoder = ArmEncoder::new_arm32();
10001        let op = ArmOp::F64Mul {
10002            dd: VfpReg::D0,
10003            dn: VfpReg::D1,
10004            dm: VfpReg::D2,
10005        };
10006        let code = encoder.encode(&op).unwrap();
10007        assert_eq!(code.len(), 4);
10008    }
10009
10010    #[test]
10011    fn test_encode_f64_div_arm32() {
10012        let encoder = ArmEncoder::new_arm32();
10013        let op = ArmOp::F64Div {
10014            dd: VfpReg::D0,
10015            dn: VfpReg::D1,
10016            dm: VfpReg::D2,
10017        };
10018        let code = encoder.encode(&op).unwrap();
10019        assert_eq!(code.len(), 4);
10020    }
10021
10022    #[test]
10023    fn test_encode_f64_abs_arm32() {
10024        let encoder = ArmEncoder::new_arm32();
10025        let op = ArmOp::F64Abs {
10026            dd: VfpReg::D0,
10027            dm: VfpReg::D2,
10028        };
10029        let code = encoder.encode(&op).unwrap();
10030        assert_eq!(code.len(), 4);
10031    }
10032
10033    #[test]
10034    fn test_encode_f64_neg_arm32() {
10035        let encoder = ArmEncoder::new_arm32();
10036        let op = ArmOp::F64Neg {
10037            dd: VfpReg::D0,
10038            dm: VfpReg::D2,
10039        };
10040        let code = encoder.encode(&op).unwrap();
10041        assert_eq!(code.len(), 4);
10042    }
10043
10044    #[test]
10045    fn test_encode_f64_sqrt_arm32() {
10046        let encoder = ArmEncoder::new_arm32();
10047        let op = ArmOp::F64Sqrt {
10048            dd: VfpReg::D0,
10049            dm: VfpReg::D2,
10050        };
10051        let code = encoder.encode(&op).unwrap();
10052        assert_eq!(code.len(), 4);
10053    }
10054
10055    #[test]
10056    fn test_encode_f64_load_arm32() {
10057        let encoder = ArmEncoder::new_arm32();
10058        let op = ArmOp::F64Load {
10059            dd: VfpReg::D0,
10060            addr: MemAddr::imm(Reg::R0, 8),
10061        };
10062        let code = encoder.encode(&op).unwrap();
10063        assert_eq!(code.len(), 4);
10064        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10065        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11 for F64
10066        assert_eq!(instr & 0xFF, 2); // offset 8 / 4 = 2
10067    }
10068
10069    #[test]
10070    fn test_encode_f64_store_thumb2() {
10071        let encoder = ArmEncoder::new_thumb2();
10072        let op = ArmOp::F64Store {
10073            dd: VfpReg::D0,
10074            addr: MemAddr::imm(Reg::SP, 0),
10075        };
10076        let code = encoder.encode(&op).unwrap();
10077        assert_eq!(code.len(), 4);
10078    }
10079
10080    #[test]
10081    fn test_encode_f64_compare_arm32() {
10082        let encoder = ArmEncoder::new_arm32();
10083        let op = ArmOp::F64Eq {
10084            rd: Reg::R0,
10085            dn: VfpReg::D0,
10086            dm: VfpReg::D1,
10087        };
10088        let code = encoder.encode(&op).unwrap();
10089        assert_eq!(code.len(), 16); // VCMP + VMRS + MOV #0 + MOVcond #1
10090    }
10091
10092    #[test]
10093    fn test_encode_f64_compare_thumb2() {
10094        let encoder = ArmEncoder::new_thumb2();
10095        let op = ArmOp::F64Lt {
10096            rd: Reg::R0,
10097            dn: VfpReg::D0,
10098            dm: VfpReg::D1,
10099        };
10100        let code = encoder.encode(&op).unwrap();
10101        // VCMP(4) + VMRS(4) + MOVS(2) + IT(2) + MOV(2) = 14
10102        assert_eq!(code.len(), 14);
10103    }
10104
10105    #[test]
10106    fn test_encode_f64_const_arm32() {
10107        let encoder = ArmEncoder::new_arm32();
10108        let op = ArmOp::F64Const {
10109            dd: VfpReg::D0,
10110            value: 3.125,
10111        };
10112        let code = encoder.encode(&op).unwrap();
10113        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
10114        assert_eq!(code.len(), 20);
10115    }
10116
10117    #[test]
10118    fn test_encode_f64_const_thumb2() {
10119        let encoder = ArmEncoder::new_thumb2();
10120        let op = ArmOp::F64Const {
10121            dd: VfpReg::D0,
10122            value: 2.5,
10123        };
10124        let code = encoder.encode(&op).unwrap();
10125        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
10126        assert_eq!(code.len(), 20);
10127    }
10128
10129    #[test]
10130    fn test_encode_f64_convert_i32s_arm32() {
10131        let encoder = ArmEncoder::new_arm32();
10132        let op = ArmOp::F64ConvertI32S {
10133            dd: VfpReg::D0,
10134            rm: Reg::R0,
10135        };
10136        let code = encoder.encode(&op).unwrap();
10137        // VMOV(4) + VCVT(4) = 8
10138        assert_eq!(code.len(), 8);
10139    }
10140
10141    #[test]
10142    fn test_encode_f64_promote_f32_arm32() {
10143        let encoder = ArmEncoder::new_arm32();
10144        let op = ArmOp::F64PromoteF32 {
10145            dd: VfpReg::D0,
10146            sm: VfpReg::S0,
10147        };
10148        let code = encoder.encode(&op).unwrap();
10149        assert_eq!(code.len(), 4); // Single VCVT.F64.F32 instruction
10150    }
10151
10152    #[test]
10153    fn test_encode_f64_promote_f32_thumb2() {
10154        let encoder = ArmEncoder::new_thumb2();
10155        let op = ArmOp::F64PromoteF32 {
10156            dd: VfpReg::D0,
10157            sm: VfpReg::S0,
10158        };
10159        let code = encoder.encode(&op).unwrap();
10160        assert_eq!(code.len(), 4);
10161    }
10162
10163    #[test]
10164    fn test_encode_i32_trunc_f64s_arm32() {
10165        let encoder = ArmEncoder::new_arm32();
10166        let op = ArmOp::I32TruncF64S {
10167            rd: Reg::R0,
10168            dm: VfpReg::D0,
10169        };
10170        let code = encoder.encode(&op).unwrap();
10171        // VCVT(4) + VMOV(4) = 8
10172        assert_eq!(code.len(), 8);
10173    }
10174
10175    #[test]
10176    fn test_encode_f64_reinterpret_i64_arm32() {
10177        let encoder = ArmEncoder::new_arm32();
10178        let op = ArmOp::F64ReinterpretI64 {
10179            dd: VfpReg::D0,
10180            rmlo: Reg::R0,
10181            rmhi: Reg::R1,
10182        };
10183        let code = encoder.encode(&op).unwrap();
10184        assert_eq!(code.len(), 4); // Single VMOV instruction
10185    }
10186
10187    #[test]
10188    fn test_encode_i64_reinterpret_f64_thumb2() {
10189        let encoder = ArmEncoder::new_thumb2();
10190        let op = ArmOp::I64ReinterpretF64 {
10191            rdlo: Reg::R0,
10192            rdhi: Reg::R1,
10193            dm: VfpReg::D0,
10194        };
10195        let code = encoder.encode(&op).unwrap();
10196        assert_eq!(code.len(), 4);
10197    }
10198
10199    #[test]
10200    fn test_encode_f64_trunc_thumb2() {
10201        let encoder = ArmEncoder::new_thumb2();
10202        let op = ArmOp::F64Trunc {
10203            dd: VfpReg::D0,
10204            dm: VfpReg::D1,
10205        };
10206        let code = encoder.encode(&op).unwrap();
10207        // Two VFP instructions via Thumb encoding
10208        assert_eq!(code.len(), 8);
10209    }
10210
10211    #[test]
10212    fn test_encode_f64_min_arm32() {
10213        let encoder = ArmEncoder::new_arm32();
10214        let op = ArmOp::F64Min {
10215            dd: VfpReg::D0,
10216            dn: VfpReg::D1,
10217            dm: VfpReg::D2,
10218        };
10219        let code = encoder.encode(&op).unwrap();
10220        // VMOV + VCMP + VMRS + conditional VMOV = 16
10221        assert_eq!(code.len(), 16);
10222    }
10223
10224    #[test]
10225    fn test_f64_cp11_encoding() {
10226        // Verify that F64 instructions use coprocessor 11 (0xB), not 10 (0xA)
10227        let encoder = ArmEncoder::new_arm32();
10228
10229        // F64Add
10230        let code = encoder
10231            .encode(&ArmOp::F64Add {
10232                dd: VfpReg::D0,
10233                dn: VfpReg::D0,
10234                dm: VfpReg::D0,
10235            })
10236            .unwrap();
10237        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10238        assert_eq!((instr >> 8) & 0xF, 0xB, "F64 should use cp11");
10239
10240        // F32Add for comparison
10241        let code = encoder
10242            .encode(&ArmOp::F32Add {
10243                sd: VfpReg::S0,
10244                sn: VfpReg::S0,
10245                sm: VfpReg::S0,
10246            })
10247            .unwrap();
10248        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10249        assert_eq!((instr >> 8) & 0xF, 0xA, "F32 should use cp10");
10250    }
10251
10252    #[test]
10253    fn test_dreg_encoding_higher_registers() {
10254        let encoder = ArmEncoder::new_arm32();
10255
10256        // Test with D15 (highest register)
10257        let op = ArmOp::F64Add {
10258            dd: VfpReg::D15,
10259            dn: VfpReg::D14,
10260            dm: VfpReg::D13,
10261        };
10262        let code = encoder.encode(&op).unwrap();
10263        assert_eq!(code.len(), 4);
10264
10265        // Verify the register encoding worked (instruction is valid)
10266        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10267        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
10268    }
10269
10270    // ========================================================================
10271    // Control flow encoding tests
10272    // ========================================================================
10273
10274    #[test]
10275    fn test_encode_label_emits_no_bytes() {
10276        let encoder = ArmEncoder::new_thumb2();
10277        let op = ArmOp::Label {
10278            name: ".Lblock_end_0".to_string(),
10279        };
10280        let code = encoder.encode(&op).unwrap();
10281        assert!(code.is_empty(), "Label should emit zero bytes");
10282
10283        let encoder32 = ArmEncoder::new_arm32();
10284        let code32 = encoder32.encode(&op).unwrap();
10285        assert!(
10286            code32.is_empty(),
10287            "Label should emit zero bytes in ARM32 too"
10288        );
10289    }
10290
10291    #[test]
10292    fn test_encode_bcc_eq_thumb2() {
10293        use synth_synthesis::Condition;
10294        let encoder = ArmEncoder::new_thumb2();
10295        let op = ArmOp::Bcc {
10296            cond: Condition::EQ,
10297            label: "target".to_string(),
10298        };
10299        let code = encoder.encode(&op).unwrap();
10300        assert_eq!(code.len(), 2); // 16-bit conditional branch
10301
10302        // BEQ with offset 0: 0xD000 in little-endian
10303        assert_eq!(code, vec![0x00, 0xD0]);
10304    }
10305
10306    #[test]
10307    fn test_encode_bcc_ne_thumb2() {
10308        use synth_synthesis::Condition;
10309        let encoder = ArmEncoder::new_thumb2();
10310        let op = ArmOp::Bcc {
10311            cond: Condition::NE,
10312            label: "target".to_string(),
10313        };
10314        let code = encoder.encode(&op).unwrap();
10315        assert_eq!(code.len(), 2);
10316
10317        // BNE with offset 0: 0xD100 in little-endian
10318        assert_eq!(code, vec![0x00, 0xD1]);
10319    }
10320
10321    #[test]
10322    fn test_encode_bcc_arm32() {
10323        use synth_synthesis::Condition;
10324        let encoder = ArmEncoder::new_arm32();
10325        let op = ArmOp::Bcc {
10326            cond: Condition::EQ,
10327            label: "target".to_string(),
10328        };
10329        let code = encoder.encode(&op).unwrap();
10330        assert_eq!(code.len(), 4); // 32-bit ARM instruction
10331
10332        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10333        // BEQ: cond=0x0, opcode=0xA, offset=0
10334        assert_eq!(instr & 0xF0000000, 0x00000000); // EQ condition
10335        assert_eq!(instr & 0x0F000000, 0x0A000000); // Branch opcode
10336    }
10337
10338    #[test]
10339    fn test_encode_udf_thumb2() {
10340        let encoder = ArmEncoder::new_thumb2();
10341        let op = ArmOp::Udf { imm: 0 };
10342        let code = encoder.encode(&op).unwrap();
10343        assert_eq!(code.len(), 2); // 16-bit
10344
10345        // UDF #0: 0xDE00 in little-endian
10346        assert_eq!(code, vec![0x00, 0xDE]);
10347    }
10348
10349    /// #610: the i64 rot/div/rem expansions must land the result in the
10350    /// selector-assigned rd pair and leave R0-R3 preserved (restored from the
10351    /// fixed-ABI wrapper's save area) — pre-#610 the rot expansion's own
10352    /// `POP {R4}` restored stale scratch OVER the result (rd_lo == R4) and
10353    /// the div/rem expansions ignored their register fields outright.
10354    #[test]
10355    fn test_610_i64_rot_expansion_ends_with_rd_movs_and_restore() {
10356        let encoder = ArmEncoder::new_thumb2();
10357        for op in [
10358            ArmOp::I64Rotl {
10359                rdlo: Reg::R4,
10360                rdhi: Reg::R5,
10361                rnlo: Reg::R0,
10362                rnhi: Reg::R1,
10363                shift: Reg::R2,
10364            },
10365            ArmOp::I64Rotr {
10366                rdlo: Reg::R4,
10367                rdhi: Reg::R5,
10368                rnlo: Reg::R0,
10369                rnhi: Reg::R1,
10370                shift: Reg::R2,
10371            },
10372        ] {
10373            let code = encoder.encode(&op).unwrap();
10374            assert_eq!(code.len(), 102, "register-independent size (estimator pin)");
10375            // Tail: MOV r5, r1 (0x460D); MOV r4, r0 (0x4604); POP {r0..r3}
10376            // (rd pair r4:r5 does not overlap the save area — all 4 restored).
10377            let tail: Vec<u16> = code[code.len() - 12..]
10378                .chunks(2)
10379                .map(|c| u16::from_le_bytes([c[0], c[1]]))
10380                .collect();
10381            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
10382        }
10383    }
10384
10385    /// #610: div/rem expansions honor rd and carry the divide-by-zero trap
10386    /// guard (`ORRS R12, R2, R3; BNE +0; UDF #0`) after operand marshaling.
10387    #[test]
10388    fn test_610_i64_div_rem_expansion_guard_and_rd() {
10389        let encoder = ArmEncoder::new_thumb2();
10390        let mk = |which: u8| {
10391            let (rdlo, rdhi, rnlo, rnhi, rmlo, rmhi) =
10392                (Reg::R4, Reg::R5, Reg::R0, Reg::R1, Reg::R2, Reg::R3);
10393            match which {
10394                0 => ArmOp::I64DivU {
10395                    rdlo,
10396                    rdhi,
10397                    rnlo,
10398                    rnhi,
10399                    rmlo,
10400                    rmhi,
10401                    elide_zero_guard: false,
10402                },
10403                1 => ArmOp::I64RemU {
10404                    rdlo,
10405                    rdhi,
10406                    rnlo,
10407                    rnhi,
10408                    rmlo,
10409                    rmhi,
10410                    elide_zero_guard: false,
10411                },
10412                2 => ArmOp::I64DivS {
10413                    rdlo,
10414                    rdhi,
10415                    rnlo,
10416                    rnhi,
10417                    rmlo,
10418                    rmhi,
10419                    elide_zero_guard: false,
10420                    elide_overflow_guard: false,
10421                },
10422                _ => ArmOp::I64RemS {
10423                    rdlo,
10424                    rdhi,
10425                    rnlo,
10426                    rnhi,
10427                    rmlo,
10428                    rmhi,
10429                    elide_zero_guard: false,
10430                },
10431            }
10432        };
10433        for which in 0..4u8 {
10434            let code = encoder.encode(&mk(which)).unwrap();
10435            // Zero-divisor trap guard right after the 26-byte marshal prologue.
10436            let guard: Vec<u16> = code[26..34]
10437                .chunks(2)
10438                .map(|c| u16::from_le_bytes([c[0], c[1]]))
10439                .collect();
10440            assert_eq!(
10441                guard,
10442                vec![0xEA52, 0x0C03, 0xD100, 0xDE00],
10443                "ORRS R12,R2,R3; BNE +0; UDF #0"
10444            );
10445            // Tail: result into rd pair (r5:r4), then restore all of R0-R3.
10446            let tail: Vec<u16> = code[code.len() - 12..]
10447                .chunks(2)
10448                .map(|c| u16::from_le_bytes([c[0], c[1]]))
10449                .collect();
10450            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
10451        }
10452    }
10453
10454    /// #610: when rd overlaps R0-R3 the restore must SKIP the result
10455    /// registers (drop the saved caller word) instead of popping over them.
10456    #[test]
10457    fn test_610_i64_divu_rd_in_r0_r1_skips_restore() {
10458        let encoder = ArmEncoder::new_thumb2();
10459        let code = encoder
10460            .encode(&ArmOp::I64DivU {
10461                rdlo: Reg::R0,
10462                rdhi: Reg::R1,
10463                rnlo: Reg::R0,
10464                rnhi: Reg::R1,
10465                rmlo: Reg::R2,
10466                rmhi: Reg::R3,
10467                elide_zero_guard: false,
10468            })
10469            .unwrap();
10470        let tail: Vec<u16> = code[code.len() - 12..]
10471            .chunks(2)
10472            .map(|c| u16::from_le_bytes([c[0], c[1]]))
10473            .collect();
10474        // MOV r1,r1 / MOV r0,r0 (no-ops, size-stable), ADD SP,#4 twice
10475        // (discard saved r0/r1 — the result lives there), POP {r2}, POP {r3}.
10476        assert_eq!(tail, vec![0x4609, 0x4600, 0xB001, 0xB001, 0xBC04, 0xBC08]);
10477    }
10478
10479    /// #610: a fully swapped rd pair (rd_lo=R1, rd_hi=R0) cannot be
10480    /// materialized by two MOVs in either order — must be a loud Err, never
10481    /// silent corruption. (Selector pairs are consecutive, so unreachable.)
10482    #[test]
10483    fn test_610_i64_swapped_rd_pair_rejected() {
10484        let encoder = ArmEncoder::new_thumb2();
10485        let result = encoder.encode(&ArmOp::I64RemU {
10486            rdlo: Reg::R1,
10487            rdhi: Reg::R0,
10488            rnlo: Reg::R2,
10489            rnhi: Reg::R3,
10490            rmlo: Reg::R4,
10491            rmhi: Reg::R5,
10492            elide_zero_guard: false,
10493        });
10494        assert!(result.is_err(), "swapped rd pair must be rejected loudly");
10495    }
10496
10497    /// #632: the I64Popcnt expansion's own scratch restore (`POP {R3,R4,R5}`)
10498    /// must not clobber the result. Pre-fix the total was materialized with
10499    /// `ADDS rd, R4, R5` BEFORE the pop, so any allocator-assigned
10500    /// rd ∈ {R3,R4,R5} received stale stack garbage. Post-fix the count is
10501    /// carried across the restore in R12 (never allocatable, never restored)
10502    /// and moved into rd only after the pop — structurally rd-independent.
10503    #[test]
10504    fn test_632_i64_popcnt_result_survives_scratch_restore() {
10505        let encoder = ArmEncoder::new_thumb2();
10506        // Every allocatable rd, including the restore set {R3,R4,R5} and R8.
10507        for rd in [
10508            Reg::R0,
10509            Reg::R2,
10510            Reg::R3,
10511            Reg::R4,
10512            Reg::R5,
10513            Reg::R6,
10514            Reg::R8,
10515        ] {
10516            let code = encoder
10517                .encode(&ArmOp::I64Popcnt {
10518                    rd,
10519                    rnlo: Reg::R6,
10520                    rnhi: Reg::R7,
10521                })
10522                .unwrap();
10523            assert_eq!(code.len(), 180, "register-independent size (estimator pin)");
10524            let hw: Vec<u16> = code
10525                .chunks(2)
10526                .map(|c| u16::from_le_bytes([c[0], c[1]]))
10527                .collect();
10528            let pop = hw
10529                .iter()
10530                .position(|&h| h == 0xBC38)
10531                .expect("POP {R3,R4,R5} present");
10532            // Immediately before the POP: ADD.W R12, R4, R5 (the total lives
10533            // in R12, which the POP cannot touch).
10534            assert_eq!(
10535                &hw[pop - 2..pop],
10536                &[0xEB04, 0x0C05],
10537                "total must be carried in R12 across the restore"
10538            );
10539            // Immediately after the POP: MOV rd, R12.
10540            let rd_bits = match rd {
10541                Reg::R8 => 8u16,
10542                Reg::R6 => 6,
10543                Reg::R5 => 5,
10544                Reg::R4 => 4,
10545                Reg::R3 => 3,
10546                Reg::R2 => 2,
10547                _ => 0,
10548            };
10549            let expect_mov = 0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7);
10550            assert_eq!(hw[pop + 1], expect_mov, "MOV rd, R12 after the restore");
10551            // No write into rd between the PUSH and the POP (the old
10552            // pre-restore ADDS is gone).
10553            assert!(
10554                !hw[..pop].contains(&(0x1800 | (5 << 6) | (4 << 3) | rd_bits)),
10555                "no ADDS rd, R4, R5 before the restore pop"
10556            );
10557        }
10558    }
10559
10560    /// #632 audit: the entry marshal must be permutation-safe. Pre-fix
10561    /// `MOV R4, rnlo; MOV R5, rnhi` read a clobbered R4 when the operand
10562    /// pair lived at (R3, R4). Post-fix rnlo routes through R12.
10563    #[test]
10564    fn test_632_i64_popcnt_marshal_pair_at_r3_r4() {
10565        let encoder = ArmEncoder::new_thumb2();
10566        let code = encoder
10567            .encode(&ArmOp::I64Popcnt {
10568                rd: Reg::R0,
10569                rnlo: Reg::R3,
10570                rnhi: Reg::R4,
10571            })
10572            .unwrap();
10573        let hw: Vec<u16> = code
10574            .chunks(2)
10575            .map(|c| u16::from_le_bytes([c[0], c[1]]))
10576            .collect();
10577        // PUSH {R3,R4,R5}; MOV R12, R3; MOV R5, R4 (rnhi read BEFORE any
10578        // write to R4); MOV R4, R12.
10579        assert_eq!(hw[0], 0xB438);
10580        assert_eq!(hw[1], 0x4600 | (1 << 7) | (3 << 3) | 4, "MOV R12, rnlo");
10581        assert_eq!(hw[2], 0x4600 | (4 << 3) | 5, "MOV R5, rnhi");
10582        assert_eq!(hw[3], 0x4664, "MOV R4, R12");
10583    }
10584
10585    /// #632: A32 twin — same structural fix on the ARM-mode path
10586    /// (`--target cortex-r5`): total carried in R12 across the restore.
10587    #[test]
10588    fn test_632_a32_i64_popcnt_result_survives_scratch_restore() {
10589        let encoder = ArmEncoder::new_arm32();
10590        for rd in [Reg::R0, Reg::R3, Reg::R4, Reg::R5, Reg::R8] {
10591            let code = encoder
10592                .encode(&ArmOp::I64Popcnt {
10593                    rd,
10594                    rnlo: Reg::R6,
10595                    rnhi: Reg::R7,
10596                })
10597                .unwrap();
10598            let words: Vec<u32> = code
10599                .chunks(4)
10600                .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
10601                .collect();
10602            let pop = words
10603                .iter()
10604                .position(|&w| w == 0xE8BD_0038)
10605                .expect("POP {R3,R4,R5} present");
10606            assert_eq!(words[pop - 1], 0xE084_C005, "ADD R12, R4, R5 before POP");
10607            let rd_bits = match rd {
10608                Reg::R8 => 8u32,
10609                Reg::R5 => 5,
10610                Reg::R4 => 4,
10611                Reg::R3 => 3,
10612                _ => 0,
10613            };
10614            assert_eq!(
10615                words[pop + 1],
10616                0xE1A0_0000 | (rd_bits << 12) | 12,
10617                "MOV rd, R12 after the restore"
10618            );
10619        }
10620    }
10621
10622    /// #633: I64DivS must carry the INT64_MIN/-1 overflow guard (mirroring
10623    /// the i32 path) right after the zero-divisor guard — dividend in R0:R1,
10624    /// divisor in R2:R3 on the #610/#613 fixed-ABI wrapper path.
10625    #[test]
10626    fn test_633_i64_divs_overflow_guard_emitted() {
10627        let encoder = ArmEncoder::new_thumb2();
10628        let code = encoder
10629            .encode(&ArmOp::I64DivS {
10630                rdlo: Reg::R4,
10631                rdhi: Reg::R5,
10632                rnlo: Reg::R0,
10633                rnhi: Reg::R1,
10634                rmlo: Reg::R2,
10635                rmhi: Reg::R3,
10636                elide_zero_guard: false,
10637                elide_overflow_guard: false,
10638            })
10639            .unwrap();
10640        // 26-byte marshal + 8-byte zero-trap, then the 22-byte overflow guard.
10641        let guard: Vec<u16> = code[34..56]
10642            .chunks(2)
10643            .map(|c| u16::from_le_bytes([c[0], c[1]]))
10644            .collect();
10645        assert_eq!(
10646            guard,
10647            vec![
10648                0xEA02, 0x0C03, // AND.W R12, R2, R3
10649                0xF11C, 0x0F01, // CMN.W R12, #1
10650                0xD105, // BNE .no_trap
10651                0x2800, // CMP R0, #0
10652                0xD103, // BNE .no_trap
10653                0xF1B1, 0x4F00, // CMP.W R1, #0x80000000
10654                0xD100, // BNE .no_trap
10655                0xDE00, // UDF #0 — signed-division overflow
10656            ],
10657            "INT64_MIN/-1 overflow guard after the zero-divisor guard"
10658        );
10659    }
10660
10661    /// #633 fix-guard twin: I64RemS must NOT carry the overflow guard —
10662    /// rem_s(INT64_MIN, -1) is defined as 0 and must not trap. Exactly one
10663    /// UDF (the zero-divisor trap) in the whole expansion.
10664    #[test]
10665    fn test_633_i64_rems_has_no_overflow_guard() {
10666        let encoder = ArmEncoder::new_thumb2();
10667        for (is_rem_s, op) in [
10668            (
10669                true,
10670                ArmOp::I64RemS {
10671                    rdlo: Reg::R4,
10672                    rdhi: Reg::R5,
10673                    rnlo: Reg::R0,
10674                    rnhi: Reg::R1,
10675                    rmlo: Reg::R2,
10676                    rmhi: Reg::R3,
10677                    elide_zero_guard: false,
10678                },
10679            ),
10680            (
10681                false,
10682                ArmOp::I64DivS {
10683                    rdlo: Reg::R4,
10684                    rdhi: Reg::R5,
10685                    rnlo: Reg::R0,
10686                    rnhi: Reg::R1,
10687                    rmlo: Reg::R2,
10688                    rmhi: Reg::R3,
10689                    elide_zero_guard: false,
10690                    elide_overflow_guard: false,
10691                },
10692            ),
10693        ] {
10694            let code = encoder.encode(&op).unwrap();
10695            let udfs = code
10696                .chunks(2)
10697                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
10698                .count();
10699            let want = if is_rem_s { 1 } else { 2 };
10700            assert_eq!(
10701                udfs, want,
10702                "rem_s: zero-trap only; div_s: zero-trap + overflow trap"
10703            );
10704        }
10705    }
10706
10707    /// #494 phase 2b: `elide_zero_guard` drops EXACTLY the 8-byte fused
10708    /// zero-trap (`ORRS.W R12,R2,R3; BNE; UDF #0`) and nothing else — the
10709    /// rest of the expansion is byte-identical (splice check).
10710    #[test]
10711    fn test_494_i64_zero_guard_elision_is_exact_splice() {
10712        let encoder = ArmEncoder::new_thumb2();
10713        let mk = |elide_zero_guard: bool| {
10714            encoder
10715                .encode(&ArmOp::I64DivU {
10716                    rdlo: Reg::R4,
10717                    rdhi: Reg::R5,
10718                    rnlo: Reg::R0,
10719                    rnhi: Reg::R1,
10720                    rmlo: Reg::R2,
10721                    rmhi: Reg::R3,
10722                    elide_zero_guard,
10723                })
10724                .unwrap()
10725        };
10726        let full = mk(false);
10727        let elided = mk(true);
10728        assert_eq!(full.len(), elided.len() + 8, "zero guard is 8 bytes");
10729        // Marshal prologue (26 B) unchanged, guard (8 B) gone, tail identical.
10730        assert_eq!(&full[..26], &elided[..26]);
10731        assert_eq!(
10732            &full[26..34],
10733            &[0x52, 0xEA, 0x03, 0x0C, 0x00, 0xD1, 0x00, 0xDE],
10734            "the spliced-out bytes are exactly ORRS.W; BNE; UDF #0"
10735        );
10736        assert_eq!(&full[34..], &elided[26..]);
10737    }
10738
10739    /// #494 phase 2b two-guard distinction (the #633/#634 synergy): a
10740    /// divisor-nonzero fact elides ONLY the zero guard — the INT64_MIN/-1
10741    /// OVERFLOW guard is a separate obligation and must survive
10742    /// `elide_zero_guard: true`. Pinned on div_s in all flag states.
10743    #[test]
10744    fn test_494_i64_divs_overflow_guard_retained_when_only_zero_elided() {
10745        let encoder = ArmEncoder::new_thumb2();
10746        let mk = |zero: bool, ovf: bool| {
10747            encoder
10748                .encode(&ArmOp::I64DivS {
10749                    rdlo: Reg::R4,
10750                    rdhi: Reg::R5,
10751                    rnlo: Reg::R0,
10752                    rnhi: Reg::R1,
10753                    rmlo: Reg::R2,
10754                    rmhi: Reg::R3,
10755                    elide_zero_guard: zero,
10756                    elide_overflow_guard: ovf,
10757                })
10758                .unwrap()
10759        };
10760        let udf_count = |code: &[u8]| {
10761            code.chunks(2)
10762                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
10763                .count()
10764        };
10765        let full = mk(false, false);
10766        let zero_only = mk(true, false);
10767        let both = mk(true, true);
10768        assert_eq!(udf_count(&full), 2, "baseline: zero trap + overflow trap");
10769        assert_eq!(
10770            udf_count(&zero_only),
10771            1,
10772            "divisor-nonzero elides the zero trap ONLY — the #633 overflow \
10773             guard must be retained"
10774        );
10775        // The retained guard is the 22-byte overflow sequence, now right
10776        // after the 26-byte marshal prologue.
10777        let guard: Vec<u16> = zero_only[26..48]
10778            .chunks(2)
10779            .map(|c| u16::from_le_bytes([c[0], c[1]]))
10780            .collect();
10781        assert_eq!(
10782            guard,
10783            vec![
10784                0xEA02, 0x0C03, 0xF11C, 0x0F01, 0xD105, 0x2800, 0xD103, 0xF1B1, 0x4F00, 0xD100,
10785                0xDE00,
10786            ],
10787            "the surviving guard is the INT64_MIN/-1 overflow trap"
10788        );
10789        assert_eq!(full.len(), zero_only.len() + 8);
10790        assert_eq!(zero_only.len(), both.len() + 22);
10791        assert_eq!(udf_count(&both), 0, "both obligations discharged ⇒ no UDF");
10792    }
10793
10794    /// #494 phase 2b A32 twin: zero-guard elision is an exact 12-byte splice
10795    /// and the A32 overflow guard survives a zero-only elision.
10796    #[test]
10797    fn test_494_a32_i64_guard_elision() {
10798        let encoder = ArmEncoder::new_arm32();
10799        let mk = |zero: bool, ovf: bool| {
10800            encoder
10801                .encode(&ArmOp::I64DivS {
10802                    rdlo: Reg::R4,
10803                    rdhi: Reg::R5,
10804                    rnlo: Reg::R0,
10805                    rnhi: Reg::R1,
10806                    rmlo: Reg::R2,
10807                    rmhi: Reg::R3,
10808                    elide_zero_guard: zero,
10809                    elide_overflow_guard: ovf,
10810                })
10811                .unwrap()
10812        };
10813        let full = mk(false, false);
10814        let zero_only = mk(true, false);
10815        let both = mk(true, true);
10816        // A32 zero guard = 3 words (ORRS/BNE/UDF), overflow guard = 6 words.
10817        assert_eq!(full.len(), zero_only.len() + 12);
10818        assert_eq!(zero_only.len(), both.len() + 24);
10819        let udf_count = |code: &[u8]| {
10820            code.chunks(4)
10821                .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
10822                .count()
10823        };
10824        assert_eq!(udf_count(&full), 2);
10825        assert_eq!(
10826            udf_count(&zero_only),
10827            1,
10828            "A32: overflow guard retained under zero-only elision"
10829        );
10830        assert_eq!(udf_count(&both), 0);
10831    }
10832
10833    /// #633: A32 twin — the conditional-execution overflow guard on the
10834    /// ARM-mode I64DivS, and its absence from I64RemS.
10835    #[test]
10836    fn test_633_a32_i64_divs_overflow_guard() {
10837        let encoder = ArmEncoder::new_arm32();
10838        let mk_divs = ArmOp::I64DivS {
10839            rdlo: Reg::R4,
10840            rdhi: Reg::R5,
10841            rnlo: Reg::R0,
10842            rnhi: Reg::R1,
10843            rmlo: Reg::R2,
10844            rmhi: Reg::R3,
10845            elide_zero_guard: false,
10846            elide_overflow_guard: false,
10847        };
10848        let code = encoder.encode(&mk_divs).unwrap();
10849        let words: Vec<u32> = code
10850            .chunks(4)
10851            .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
10852            .collect();
10853        let guard = [
10854            0xE002_C003u32, // AND   R12, R2, R3
10855            0xE37C_0001,    // CMN   R12, #1
10856            0x0350_0000,    // CMPEQ R0, #0
10857            0x0351_0102,    // CMPEQ R1, #0x80000000
10858            0x1A00_0000,    // BNE +1 insn
10859            0xE7F0_00F0,    // UDF #0
10860        ];
10861        assert!(
10862            words.windows(6).any(|w| w == guard),
10863            "A32 I64DivS carries the INT64_MIN/-1 overflow guard"
10864        );
10865        let rems = encoder
10866            .encode(&ArmOp::I64RemS {
10867                rdlo: Reg::R4,
10868                rdhi: Reg::R5,
10869                rnlo: Reg::R0,
10870                rnhi: Reg::R1,
10871                rmlo: Reg::R2,
10872                rmhi: Reg::R3,
10873                elide_zero_guard: false,
10874            })
10875            .unwrap();
10876        let rems_udfs = rems
10877            .chunks(4)
10878            .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
10879            .count();
10880        assert_eq!(rems_udfs, 1, "A32 I64RemS keeps only the zero-divisor trap");
10881    }
10882
10883    #[test]
10884    fn test_encode_nop_thumb2() {
10885        let encoder = ArmEncoder::new_thumb2();
10886        let op = ArmOp::Nop;
10887        let code = encoder.encode(&op).unwrap();
10888        assert_eq!(code.len(), 2); // 16-bit
10889
10890        // NOP: 0xBF00 in little-endian
10891        assert_eq!(code, vec![0x00, 0xBF]);
10892    }
10893
10894    // =========================================================================
10895    // i64 Thumb-2 encoding tests
10896    // =========================================================================
10897
10898    #[test]
10899    fn test_encode_i64_add_thumb2() {
10900        let encoder = ArmEncoder::new_thumb2();
10901        let op = ArmOp::I64Add {
10902            rdlo: Reg::R0,
10903            rdhi: Reg::R1,
10904            rnlo: Reg::R0,
10905            rnhi: Reg::R1,
10906            rmlo: Reg::R2,
10907            rmhi: Reg::R3,
10908        };
10909        let code = encoder.encode(&op).unwrap();
10910        // Should emit ADDS (2 bytes) + ADC.W (4 bytes) = 6 bytes
10911        assert_eq!(code.len(), 6, "I64Add should be 6 bytes (ADDS + ADC.W)");
10912    }
10913
10914    #[test]
10915    fn test_encode_i64_sub_thumb2() {
10916        let encoder = ArmEncoder::new_thumb2();
10917        let op = ArmOp::I64Sub {
10918            rdlo: Reg::R0,
10919            rdhi: Reg::R1,
10920            rnlo: Reg::R0,
10921            rnhi: Reg::R1,
10922            rmlo: Reg::R2,
10923            rmhi: Reg::R3,
10924        };
10925        let code = encoder.encode(&op).unwrap();
10926        // Should emit SUBS (2 bytes) + SBC.W (4 bytes) = 6 bytes
10927        assert_eq!(code.len(), 6, "I64Sub should be 6 bytes (SUBS + SBC.W)");
10928    }
10929
10930    #[test]
10931    fn test_encode_i64_and_thumb2() {
10932        let encoder = ArmEncoder::new_thumb2();
10933        let op = ArmOp::I64And {
10934            rdlo: Reg::R0,
10935            rdhi: Reg::R1,
10936            rnlo: Reg::R0,
10937            rnhi: Reg::R1,
10938            rmlo: Reg::R2,
10939            rmhi: Reg::R3,
10940        };
10941        let code = encoder.encode(&op).unwrap();
10942        // AND.W (4 bytes) + AND.W (4 bytes) = 8 bytes
10943        assert!(code.len() >= 4, "I64And should emit at least 4 bytes");
10944    }
10945
10946    #[test]
10947    fn test_encode_i64_or_thumb2() {
10948        let encoder = ArmEncoder::new_thumb2();
10949        let op = ArmOp::I64Or {
10950            rdlo: Reg::R0,
10951            rdhi: Reg::R1,
10952            rnlo: Reg::R0,
10953            rnhi: Reg::R1,
10954            rmlo: Reg::R2,
10955            rmhi: Reg::R3,
10956        };
10957        let code = encoder.encode(&op).unwrap();
10958        assert!(code.len() >= 4, "I64Or should emit at least 4 bytes");
10959    }
10960
10961    #[test]
10962    fn test_encode_i64_xor_thumb2() {
10963        let encoder = ArmEncoder::new_thumb2();
10964        let op = ArmOp::I64Xor {
10965            rdlo: Reg::R0,
10966            rdhi: Reg::R1,
10967            rnlo: Reg::R0,
10968            rnhi: Reg::R1,
10969            rmlo: Reg::R2,
10970            rmhi: Reg::R3,
10971        };
10972        let code = encoder.encode(&op).unwrap();
10973        assert!(code.len() >= 4, "I64Xor should emit at least 4 bytes");
10974    }
10975
10976    #[test]
10977    fn test_encode_i64_const_small_thumb2() {
10978        let encoder = ArmEncoder::new_thumb2();
10979        // Small constant: only needs MOVW for each half
10980        let op = ArmOp::I64Const {
10981            rdlo: Reg::R0,
10982            rdhi: Reg::R1,
10983            value: 42,
10984        };
10985        let code = encoder.encode(&op).unwrap();
10986        // MOVW R0, #42 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes minimum
10987        assert!(code.len() >= 8, "I64Const should emit at least 8 bytes");
10988    }
10989
10990    #[test]
10991    fn test_encode_i64_const_large_thumb2() {
10992        let encoder = ArmEncoder::new_thumb2();
10993        // Large constant: needs MOVW+MOVT for each half
10994        let op = ArmOp::I64Const {
10995            rdlo: Reg::R0,
10996            rdhi: Reg::R1,
10997            value: 0x1234_5678_9ABC_DEF0_u64 as i64,
10998        };
10999        let code = encoder.encode(&op).unwrap();
11000        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
11001        assert_eq!(
11002            code.len(),
11003            16,
11004            "I64Const with large value should be 16 bytes"
11005        );
11006    }
11007
11008    #[test]
11009    fn test_encode_i64_extend_i32_s_thumb2() {
11010        let encoder = ArmEncoder::new_thumb2();
11011        let op = ArmOp::I64ExtendI32S {
11012            rdlo: Reg::R0,
11013            rdhi: Reg::R1,
11014            rn: Reg::R0,
11015        };
11016        let code = encoder.encode(&op).unwrap();
11017        // When rdlo == rn, only ASR (4 bytes) is emitted
11018        assert_eq!(
11019            code.len(),
11020            4,
11021            "I64ExtendI32S (same reg) should be 4 bytes (ASR only)"
11022        );
11023    }
11024
11025    #[test]
11026    fn test_encode_i64_extend_i32_s_diff_reg_thumb2() {
11027        let encoder = ArmEncoder::new_thumb2();
11028        let op = ArmOp::I64ExtendI32S {
11029            rdlo: Reg::R0,
11030            rdhi: Reg::R1,
11031            rn: Reg::R2,
11032        };
11033        let code = encoder.encode(&op).unwrap();
11034        // MOV rdlo, rn (2 bytes for low regs) + ASR rdhi, rdlo, #31 (4 bytes) = 6 bytes
11035        assert!(
11036            code.len() >= 6,
11037            "I64ExtendI32S (diff reg) should be at least 6 bytes"
11038        );
11039    }
11040
11041    #[test]
11042    fn test_encode_i64_extend_i32_u_thumb2() {
11043        let encoder = ArmEncoder::new_thumb2();
11044        let op = ArmOp::I64ExtendI32U {
11045            rdlo: Reg::R0,
11046            rdhi: Reg::R1,
11047            rn: Reg::R0,
11048        };
11049        let code = encoder.encode(&op).unwrap();
11050        // When rdlo == rn, only MOV rdhi, #0 (2 bytes) is emitted
11051        assert_eq!(
11052            code.len(),
11053            2,
11054            "I64ExtendI32U (same reg) should be 2 bytes (MOV #0 only)"
11055        );
11056    }
11057
11058    #[test]
11059    fn test_encode_i32_wrap_i64_nop_thumb2() {
11060        let encoder = ArmEncoder::new_thumb2();
11061        // When rd == rnlo, should be a NOP
11062        let op = ArmOp::I32WrapI64 {
11063            rd: Reg::R0,
11064            rnlo: Reg::R0,
11065        };
11066        let code = encoder.encode(&op).unwrap();
11067        assert_eq!(code.len(), 2, "I32WrapI64 same reg should be NOP (2 bytes)");
11068        assert_eq!(code, vec![0x00, 0xBF]); // NOP
11069    }
11070
11071    #[test]
11072    fn test_encode_i32_wrap_i64_diff_reg_thumb2() {
11073        let encoder = ArmEncoder::new_thumb2();
11074        let op = ArmOp::I32WrapI64 {
11075            rd: Reg::R2,
11076            rnlo: Reg::R0,
11077        };
11078        let code = encoder.encode(&op).unwrap();
11079        // MOV R2, R0 (2 or 4 bytes)
11080        assert!(
11081            code.len() >= 2,
11082            "I32WrapI64 diff reg should emit at least 2 bytes"
11083        );
11084    }
11085
11086    #[test]
11087    fn test_encode_i64_eqz_thumb2() {
11088        let encoder = ArmEncoder::new_thumb2();
11089        let op = ArmOp::I64Eqz {
11090            rd: Reg::R0,
11091            rnlo: Reg::R0,
11092            rnhi: Reg::R1,
11093        };
11094        let code = encoder.encode(&op).unwrap();
11095        // Delegates to I64SetCondZ which is already encoded
11096        assert!(
11097            code.len() >= 6,
11098            "I64Eqz should emit at least 6 bytes for ORR+ITE+MOV+MOV"
11099        );
11100    }
11101
11102    #[test]
11103    fn test_encode_i64_eq_thumb2() {
11104        let encoder = ArmEncoder::new_thumb2();
11105        let op = ArmOp::I64Eq {
11106            rd: Reg::R0,
11107            rnlo: Reg::R0,
11108            rnhi: Reg::R1,
11109            rmlo: Reg::R2,
11110            rmhi: Reg::R3,
11111        };
11112        let code = encoder.encode(&op).unwrap();
11113        // Delegates to I64SetCond EQ: CMP lo + IT EQ + CMPEQ hi + ITE EQ + MOV 1 + MOV 0
11114        assert!(code.len() >= 10, "I64Eq should emit at least 10 bytes");
11115    }
11116
11117    #[test]
11118    fn test_encode_i64_ldr_thumb2() {
11119        let encoder = ArmEncoder::new_thumb2();
11120        let op = ArmOp::I64Ldr {
11121            rdlo: Reg::R0,
11122            rdhi: Reg::R1,
11123            addr: MemAddr::imm(Reg::SP, 0),
11124        };
11125        let code = encoder.encode(&op).unwrap();
11126        // Two LDR instructions (lo at offset, hi at offset+4)
11127        assert!(code.len() >= 4, "I64Ldr should emit at least 4 bytes");
11128    }
11129
11130    #[test]
11131    fn test_372_i64_ldr_indexed_materializes_address() {
11132        // #372: a memory i64.load carries an index register (R11 + addr + off).
11133        // The encoder must materialize `ip = base + index` (ADD.W) and load via
11134        // `[ip,#off]` — NOT drop the index. A frame (non-indexed) i64.load must
11135        // stay byte-identical (plain `[base,#off]`, no ADD).
11136        let encoder = ArmEncoder::new_thumb2();
11137        let indexed = encoder
11138            .encode(&ArmOp::I64Ldr {
11139                rdlo: Reg::R0,
11140                rdhi: Reg::R1,
11141                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
11142            })
11143            .unwrap();
11144        // ADD.W ip, fp, r0 = eb0b 0c00 (byte-verified vs arm-none-eabi-as).
11145        assert_eq!(
11146            &indexed[0..4],
11147            &[0x0b, 0xeb, 0x00, 0x0c],
11148            "indexed I64Ldr must start with ADD.W ip, base, index"
11149        );
11150        let frame = encoder
11151            .encode(&ArmOp::I64Ldr {
11152                rdlo: Reg::R0,
11153                rdhi: Reg::R1,
11154                addr: MemAddr::imm(Reg::SP, 8),
11155            })
11156            .unwrap();
11157        // No index -> no ADD.W prefix (byte-identical frame access).
11158        assert_ne!(
11159            &frame[0..2],
11160            &[0x0b, 0xeb],
11161            "frame (non-indexed) I64Ldr must NOT emit an ADD.W"
11162        );
11163    }
11164
11165    #[test]
11166    fn test_382_i64_ldst_large_offset_materializes_not_skips() {
11167        // #382: an indexed i64.load/store whose static offset > 0xFFF must
11168        // MATERIALIZE the offset into the base — NOT return Err (skip the fn).
11169        // Sequence for reg_imm(R11, R0, 5000): MOVW ip,#5000 ; ADD ip,r0,ip ;
11170        // ADD ip,ip,fp ; LDR/STR halves at [ip,#0] / [ip,#4]. Byte-verified tail
11171        // vs arm-none-eabi-as.
11172        let encoder = ArmEncoder::new_thumb2();
11173        // 0x1388 > 0xFFF (MemAddr is not Copy, so build it per use).
11174
11175        let ld = encoder
11176            .encode(&ArmOp::I64Ldr {
11177                rdlo: Reg::R0,
11178                rdhi: Reg::R1,
11179                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
11180            })
11181            .expect("large-offset i64.load must lower, not skip");
11182        // MOVW ip,#0x1388 (4) + ADD ip,r0,ip (4) + ADD ip,ip,fp (4) + 2 LDR (8).
11183        assert_eq!(ld.len(), 20, "expected MOVW + 2×ADD + 2×LDR");
11184        // Must NOT be the small-offset `ADD.W ip, fp, r0` (0x0b 0xeb) prefix —
11185        // that path can only reach imm12 offsets.
11186        assert_ne!(
11187            &ld[0..2],
11188            &[0x0b, 0xeb],
11189            "must materialize the large offset"
11190        );
11191        // Effective base built in ip, then halves at [ip,#0] / [ip,#4].
11192        assert_eq!(
11193            &ld[4..20],
11194            &[
11195                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
11196                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
11197                0xdc, 0xf8, 0x00, 0x00, // LDR.W r0, [ip, #0]
11198                0xdc, 0xf8, 0x04, 0x10, // LDR.W r1, [ip, #4]
11199            ],
11200            "large-offset i64.load must fold offset into ip and access [ip,#0]/[ip,#4]"
11201        );
11202
11203        // Store: same base materialization, STR halves.
11204        let st = encoder
11205            .encode(&ArmOp::I64Str {
11206                rdlo: Reg::R2,
11207                rdhi: Reg::R3,
11208                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
11209            })
11210            .expect("large-offset i64.store must lower, not skip");
11211        assert_eq!(st.len(), 20);
11212        assert_eq!(
11213            &st[4..20],
11214            &[
11215                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
11216                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
11217                0xcc, 0xf8, 0x00, 0x20, // STR.W r2, [ip, #0]
11218                0xcc, 0xf8, 0x04, 0x30, // STR.W r3, [ip, #4]
11219            ],
11220            "large-offset i64.store must fold offset into ip and access [ip,#0]/[ip,#4]"
11221        );
11222
11223        // Small-offset (imm12) indexed access stays byte-identical (#372): the
11224        // effective base is a single `ADD.W ip, fp, r0` and the halves keep the
11225        // folded immediates — NO extra MOVW/ADD.
11226        let small = encoder
11227            .encode(&ArmOp::I64Ldr {
11228                rdlo: Reg::R0,
11229                rdhi: Reg::R1,
11230                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 8),
11231            })
11232            .unwrap();
11233        assert_eq!(
11234            &small[0..4],
11235            &[0x0b, 0xeb, 0x00, 0x0c],
11236            "small-offset indexed i64 must keep the single ADD.W ip, fp, r0"
11237        );
11238        assert_eq!(small.len(), 12, "ADD.W + 2×LDR.W (offset folded in imm12)");
11239    }
11240
11241    #[test]
11242    fn test_encode_i64_str_thumb2() {
11243        let encoder = ArmEncoder::new_thumb2();
11244        let op = ArmOp::I64Str {
11245            rdlo: Reg::R0,
11246            rdhi: Reg::R1,
11247            addr: MemAddr::imm(Reg::SP, 0),
11248        };
11249        let code = encoder.encode(&op).unwrap();
11250        // Two STR instructions (lo at offset, hi at offset+4)
11251        assert!(code.len() >= 4, "I64Str should emit at least 4 bytes");
11252    }
11253
11254    #[test]
11255    fn test_encode_i64_all_comparisons_thumb2() {
11256        let encoder = ArmEncoder::new_thumb2();
11257
11258        let ops = vec![
11259            ArmOp::I64Ne {
11260                rd: Reg::R0,
11261                rnlo: Reg::R0,
11262                rnhi: Reg::R1,
11263                rmlo: Reg::R2,
11264                rmhi: Reg::R3,
11265            },
11266            ArmOp::I64LtS {
11267                rd: Reg::R0,
11268                rnlo: Reg::R0,
11269                rnhi: Reg::R1,
11270                rmlo: Reg::R2,
11271                rmhi: Reg::R3,
11272            },
11273            ArmOp::I64LtU {
11274                rd: Reg::R0,
11275                rnlo: Reg::R0,
11276                rnhi: Reg::R1,
11277                rmlo: Reg::R2,
11278                rmhi: Reg::R3,
11279            },
11280            ArmOp::I64LeS {
11281                rd: Reg::R0,
11282                rnlo: Reg::R0,
11283                rnhi: Reg::R1,
11284                rmlo: Reg::R2,
11285                rmhi: Reg::R3,
11286            },
11287            ArmOp::I64LeU {
11288                rd: Reg::R0,
11289                rnlo: Reg::R0,
11290                rnhi: Reg::R1,
11291                rmlo: Reg::R2,
11292                rmhi: Reg::R3,
11293            },
11294            ArmOp::I64GtS {
11295                rd: Reg::R0,
11296                rnlo: Reg::R0,
11297                rnhi: Reg::R1,
11298                rmlo: Reg::R2,
11299                rmhi: Reg::R3,
11300            },
11301            ArmOp::I64GtU {
11302                rd: Reg::R0,
11303                rnlo: Reg::R0,
11304                rnhi: Reg::R1,
11305                rmlo: Reg::R2,
11306                rmhi: Reg::R3,
11307            },
11308            ArmOp::I64GeS {
11309                rd: Reg::R0,
11310                rnlo: Reg::R0,
11311                rnhi: Reg::R1,
11312                rmlo: Reg::R2,
11313                rmhi: Reg::R3,
11314            },
11315            ArmOp::I64GeU {
11316                rd: Reg::R0,
11317                rnlo: Reg::R0,
11318                rnhi: Reg::R1,
11319                rmlo: Reg::R2,
11320                rmhi: Reg::R3,
11321            },
11322        ];
11323
11324        for op in &ops {
11325            let code = encoder.encode(op).unwrap();
11326            assert!(
11327                code.len() >= 8,
11328                "i64 comparison {:?} should emit at least 8 bytes, got {}",
11329                op,
11330                code.len()
11331            );
11332        }
11333    }
11334
11335    #[test]
11336    fn test_encode_i64_const_zero_thumb2() {
11337        let encoder = ArmEncoder::new_thumb2();
11338        let op = ArmOp::I64Const {
11339            rdlo: Reg::R0,
11340            rdhi: Reg::R1,
11341            value: 0,
11342        };
11343        let code = encoder.encode(&op).unwrap();
11344        // MOVW R0, #0 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes
11345        assert_eq!(code.len(), 8, "I64Const(0) should be 8 bytes");
11346    }
11347
11348    #[test]
11349    fn test_encode_i64_const_negative_one_thumb2() {
11350        let encoder = ArmEncoder::new_thumb2();
11351        let op = ArmOp::I64Const {
11352            rdlo: Reg::R0,
11353            rdhi: Reg::R1,
11354            value: -1, // 0xFFFF_FFFF_FFFF_FFFF
11355        };
11356        let code = encoder.encode(&op).unwrap();
11357        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
11358        assert_eq!(code.len(), 16, "I64Const(-1) should be 16 bytes");
11359    }
11360
11361    // =========================================================================
11362    // Sub-word load/store encoding tests
11363    // =========================================================================
11364
11365    #[test]
11366    fn test_encode_ldrb_arm32() {
11367        let encoder = ArmEncoder::new_arm32();
11368        let op = ArmOp::Ldrb {
11369            rd: Reg::R0,
11370            addr: MemAddr::imm(Reg::R1, 4),
11371        };
11372        let code = encoder.encode(&op).unwrap();
11373        assert_eq!(code.len(), 4, "ARM32 LDRB should be 4 bytes");
11374        // LDRB R0, [R1, #4] = 0xE5D10004
11375        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11376        assert_eq!(encoded, 0xE5D10004, "Should encode LDRB R0, [R1, #4]");
11377    }
11378
11379    #[test]
11380    fn test_encode_strb_arm32() {
11381        let encoder = ArmEncoder::new_arm32();
11382        let op = ArmOp::Strb {
11383            rd: Reg::R0,
11384            addr: MemAddr::imm(Reg::R1, 0),
11385        };
11386        let code = encoder.encode(&op).unwrap();
11387        assert_eq!(code.len(), 4, "ARM32 STRB should be 4 bytes");
11388        // STRB R0, [R1, #0] = 0xE5C10000
11389        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11390        assert_eq!(encoded, 0xE5C10000, "Should encode STRB R0, [R1, #0]");
11391    }
11392
11393    #[test]
11394    fn test_encode_ldrh_arm32() {
11395        let encoder = ArmEncoder::new_arm32();
11396        let op = ArmOp::Ldrh {
11397            rd: Reg::R0,
11398            addr: MemAddr::imm(Reg::R1, 2),
11399        };
11400        let code = encoder.encode(&op).unwrap();
11401        assert_eq!(code.len(), 4, "ARM32 LDRH should be 4 bytes");
11402    }
11403
11404    #[test]
11405    fn test_encode_strh_arm32() {
11406        let encoder = ArmEncoder::new_arm32();
11407        let op = ArmOp::Strh {
11408            rd: Reg::R0,
11409            addr: MemAddr::imm(Reg::R1, 0),
11410        };
11411        let code = encoder.encode(&op).unwrap();
11412        assert_eq!(code.len(), 4, "ARM32 STRH should be 4 bytes");
11413    }
11414
11415    #[test]
11416    fn test_encode_ldrsb_arm32() {
11417        let encoder = ArmEncoder::new_arm32();
11418        let op = ArmOp::Ldrsb {
11419            rd: Reg::R0,
11420            addr: MemAddr::imm(Reg::R1, 0),
11421        };
11422        let code = encoder.encode(&op).unwrap();
11423        assert_eq!(code.len(), 4, "ARM32 LDRSB should be 4 bytes");
11424    }
11425
11426    #[test]
11427    fn test_encode_ldrsh_arm32() {
11428        let encoder = ArmEncoder::new_arm32();
11429        let op = ArmOp::Ldrsh {
11430            rd: Reg::R0,
11431            addr: MemAddr::imm(Reg::R1, 0),
11432        };
11433        let code = encoder.encode(&op).unwrap();
11434        assert_eq!(code.len(), 4, "ARM32 LDRSH should be 4 bytes");
11435    }
11436
11437    #[test]
11438    fn test_encode_ldrb_thumb2_16bit() {
11439        let encoder = ArmEncoder::new_thumb2();
11440        let op = ArmOp::Ldrb {
11441            rd: Reg::R0,
11442            addr: MemAddr::imm(Reg::R1, 4),
11443        };
11444        let code = encoder.encode(&op).unwrap();
11445        // Low registers + small offset -> 16-bit encoding
11446        assert_eq!(
11447            code.len(),
11448            2,
11449            "Thumb-2 LDRB with small offset should be 16-bit"
11450        );
11451    }
11452
11453    #[test]
11454    fn test_encode_ldrb_thumb2_32bit() {
11455        let encoder = ArmEncoder::new_thumb2();
11456        let op = ArmOp::Ldrb {
11457            rd: Reg::R0,
11458            addr: MemAddr::imm(Reg::R1, 100), // offset > 31 needs 32-bit
11459        };
11460        let code = encoder.encode(&op).unwrap();
11461        assert_eq!(
11462            code.len(),
11463            4,
11464            "Thumb-2 LDRB with large offset should be 32-bit"
11465        );
11466    }
11467
11468    #[test]
11469    fn test_encode_strb_thumb2_16bit() {
11470        let encoder = ArmEncoder::new_thumb2();
11471        let op = ArmOp::Strb {
11472            rd: Reg::R0,
11473            addr: MemAddr::imm(Reg::R1, 10),
11474        };
11475        let code = encoder.encode(&op).unwrap();
11476        assert_eq!(
11477            code.len(),
11478            2,
11479            "Thumb-2 STRB with small offset should be 16-bit"
11480        );
11481    }
11482
11483    #[test]
11484    fn test_encode_ldrh_thumb2_16bit() {
11485        let encoder = ArmEncoder::new_thumb2();
11486        let op = ArmOp::Ldrh {
11487            rd: Reg::R0,
11488            addr: MemAddr::imm(Reg::R1, 4), // offset aligned to 2, <= 62
11489        };
11490        let code = encoder.encode(&op).unwrap();
11491        assert_eq!(
11492            code.len(),
11493            2,
11494            "Thumb-2 LDRH with small aligned offset should be 16-bit"
11495        );
11496    }
11497
11498    #[test]
11499    fn test_encode_strh_thumb2_16bit() {
11500        let encoder = ArmEncoder::new_thumb2();
11501        let op = ArmOp::Strh {
11502            rd: Reg::R0,
11503            addr: MemAddr::imm(Reg::R1, 4),
11504        };
11505        let code = encoder.encode(&op).unwrap();
11506        assert_eq!(
11507            code.len(),
11508            2,
11509            "Thumb-2 STRH with small aligned offset should be 16-bit"
11510        );
11511    }
11512
11513    #[test]
11514    fn test_encode_ldrsb_thumb2() {
11515        let encoder = ArmEncoder::new_thumb2();
11516        let op = ArmOp::Ldrsb {
11517            rd: Reg::R0,
11518            addr: MemAddr::imm(Reg::R1, 0),
11519        };
11520        let code = encoder.encode(&op).unwrap();
11521        // LDRSB has no 16-bit immediate form, always 32-bit
11522        assert_eq!(code.len(), 4, "Thumb-2 LDRSB should be 32-bit");
11523    }
11524
11525    #[test]
11526    fn test_encode_ldrsh_thumb2() {
11527        let encoder = ArmEncoder::new_thumb2();
11528        let op = ArmOp::Ldrsh {
11529            rd: Reg::R0,
11530            addr: MemAddr::imm(Reg::R1, 0),
11531        };
11532        let code = encoder.encode(&op).unwrap();
11533        assert_eq!(code.len(), 4, "Thumb-2 LDRSH should be 32-bit");
11534    }
11535
11536    #[test]
11537    fn test_encode_memory_size_thumb2() {
11538        let encoder = ArmEncoder::new_thumb2();
11539        let op = ArmOp::MemorySize { rd: Reg::R0 };
11540        let code = encoder.encode(&op).unwrap();
11541        // R0 and R10 are not both low registers, so this needs careful handling
11542        assert!(!code.is_empty(), "MemorySize should produce code");
11543    }
11544
11545    #[test]
11546    fn test_encode_memory_grow_thumb2() {
11547        let encoder = ArmEncoder::new_thumb2();
11548        let op = ArmOp::MemoryGrow {
11549            rd: Reg::R0,
11550            rn: Reg::R0,
11551        };
11552        let code = encoder.encode(&op).unwrap();
11553        assert_eq!(code.len(), 4, "MemoryGrow (MVN) should be 32-bit Thumb-2");
11554    }
11555
11556    #[test]
11557    fn test_encode_subword_reg_offset_thumb2() {
11558        let encoder = ArmEncoder::new_thumb2();
11559
11560        // LDRB with register offset
11561        let op = ArmOp::Ldrb {
11562            rd: Reg::R0,
11563            addr: MemAddr::reg(Reg::R1, Reg::R2),
11564        };
11565        let code = encoder.encode(&op).unwrap();
11566        assert_eq!(
11567            code.len(),
11568            4,
11569            "Thumb-2 LDRB with reg offset should be 32-bit"
11570        );
11571
11572        // STRB with register offset
11573        let op = ArmOp::Strb {
11574            rd: Reg::R0,
11575            addr: MemAddr::reg(Reg::R1, Reg::R2),
11576        };
11577        let code = encoder.encode(&op).unwrap();
11578        assert_eq!(
11579            code.len(),
11580            4,
11581            "Thumb-2 STRB with reg offset should be 32-bit"
11582        );
11583
11584        // LDRH with register offset
11585        let op = ArmOp::Ldrh {
11586            rd: Reg::R0,
11587            addr: MemAddr::reg(Reg::R1, Reg::R2),
11588        };
11589        let code = encoder.encode(&op).unwrap();
11590        assert_eq!(
11591            code.len(),
11592            4,
11593            "Thumb-2 LDRH with reg offset should be 32-bit"
11594        );
11595
11596        // STRH with register offset
11597        let op = ArmOp::Strh {
11598            rd: Reg::R0,
11599            addr: MemAddr::reg(Reg::R1, Reg::R2),
11600        };
11601        let code = encoder.encode(&op).unwrap();
11602        assert_eq!(
11603            code.len(),
11604            4,
11605            "Thumb-2 STRH with reg offset should be 32-bit"
11606        );
11607    }
11608
11609    #[test]
11610    fn test_encode_subword_reg_imm_offset_thumb2() {
11611        let encoder = ArmEncoder::new_thumb2();
11612
11613        // LDRB with both register and immediate offset
11614        let op = ArmOp::Ldrb {
11615            rd: Reg::R0,
11616            addr: MemAddr::reg_imm(Reg::R1, Reg::R2, 4),
11617        };
11618        let code = encoder.encode(&op).unwrap();
11619        // ADD R12, R2, #4 (4 bytes) + LDRB R0, [R1, R12] (4 bytes) = 8 bytes
11620        assert_eq!(
11621            code.len(),
11622            8,
11623            "Thumb-2 LDRB with reg+imm offset should be 8 bytes"
11624        );
11625    }
11626
11627    // ========================================================================
11628    // Helium MVE encoding tests
11629    // ========================================================================
11630
11631    #[test]
11632    fn test_encode_mve_addi32_thumb2() {
11633        let encoder = ArmEncoder::new_thumb2();
11634        let op = ArmOp::MveAddI {
11635            qd: QReg::Q0,
11636            qn: QReg::Q1,
11637            qm: QReg::Q2,
11638            size: MveSize::S32,
11639        };
11640        let code = encoder.encode(&op).unwrap();
11641        assert_eq!(
11642            code.len(),
11643            4,
11644            "MVE VADD.I32 should be 4 bytes (Thumb-2 32-bit)"
11645        );
11646    }
11647
11648    #[test]
11649    fn test_encode_mve_subi16_thumb2() {
11650        let encoder = ArmEncoder::new_thumb2();
11651        let op = ArmOp::MveSubI {
11652            qd: QReg::Q0,
11653            qn: QReg::Q1,
11654            qm: QReg::Q2,
11655            size: MveSize::S16,
11656        };
11657        let code = encoder.encode(&op).unwrap();
11658        assert_eq!(code.len(), 4, "MVE VSUB.I16 should be 4 bytes");
11659    }
11660
11661    #[test]
11662    fn test_encode_mve_muli8_thumb2() {
11663        let encoder = ArmEncoder::new_thumb2();
11664        let op = ArmOp::MveMulI {
11665            qd: QReg::Q0,
11666            qn: QReg::Q1,
11667            qm: QReg::Q2,
11668            size: MveSize::S8,
11669        };
11670        let code = encoder.encode(&op).unwrap();
11671        assert_eq!(code.len(), 4, "MVE VMUL.I8 should be 4 bytes");
11672    }
11673
11674    #[test]
11675    fn test_encode_mve_bitwise_thumb2() {
11676        let encoder = ArmEncoder::new_thumb2();
11677
11678        let ops = vec![
11679            ArmOp::MveAnd {
11680                qd: QReg::Q0,
11681                qn: QReg::Q1,
11682                qm: QReg::Q2,
11683            },
11684            ArmOp::MveOrr {
11685                qd: QReg::Q0,
11686                qn: QReg::Q1,
11687                qm: QReg::Q2,
11688            },
11689            ArmOp::MveEor {
11690                qd: QReg::Q0,
11691                qn: QReg::Q1,
11692                qm: QReg::Q2,
11693            },
11694            ArmOp::MveBic {
11695                qd: QReg::Q0,
11696                qn: QReg::Q1,
11697                qm: QReg::Q2,
11698            },
11699        ];
11700        for op in ops {
11701            let code = encoder.encode(&op).unwrap();
11702            assert_eq!(code.len(), 4, "MVE bitwise op should be 4 bytes");
11703        }
11704    }
11705
11706    #[test]
11707    fn test_encode_mve_mvn_thumb2() {
11708        let encoder = ArmEncoder::new_thumb2();
11709        let op = ArmOp::MveMvn {
11710            qd: QReg::Q0,
11711            qm: QReg::Q1,
11712        };
11713        let code = encoder.encode(&op).unwrap();
11714        assert_eq!(code.len(), 4, "MVE VMVN should be 4 bytes");
11715    }
11716
11717    #[test]
11718    fn test_encode_mve_load_store_thumb2() {
11719        let encoder = ArmEncoder::new_thumb2();
11720
11721        let load = ArmOp::MveLoad {
11722            qd: QReg::Q0,
11723            addr: MemAddr::imm(Reg::R0, 16),
11724        };
11725        let code = encoder.encode(&load).unwrap();
11726        assert_eq!(code.len(), 4, "MVE VLDRW.32 should be 4 bytes");
11727
11728        let store = ArmOp::MveStore {
11729            qd: QReg::Q1,
11730            addr: MemAddr::imm(Reg::R1, 0),
11731        };
11732        let code = encoder.encode(&store).unwrap();
11733        assert_eq!(code.len(), 4, "MVE VSTRW.32 should be 4 bytes");
11734    }
11735
11736    #[test]
11737    fn test_encode_mve_const_thumb2() {
11738        let encoder = ArmEncoder::new_thumb2();
11739        let op = ArmOp::MveConst {
11740            qd: QReg::Q0,
11741            bytes: [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0],
11742        };
11743        let code = encoder.encode(&op).unwrap();
11744        // Should be 4 words of (MOVW R12 + VMOV Sn) = 4 * (4+4) = 32 bytes min
11745        // Some words with hi16=0 skip MOVT, so length varies
11746        assert!(
11747            code.len() >= 24,
11748            "MVE const should produce multiple instructions"
11749        );
11750    }
11751
11752    #[test]
11753    fn test_encode_mve_dup_thumb2() {
11754        let encoder = ArmEncoder::new_thumb2();
11755        let op = ArmOp::MveDup {
11756            qd: QReg::Q0,
11757            rn: Reg::R0,
11758            size: MveSize::S32,
11759        };
11760        let code = encoder.encode(&op).unwrap();
11761        assert_eq!(code.len(), 4, "MVE VDUP.32 should be 4 bytes");
11762    }
11763
11764    #[test]
11765    fn test_encode_mve_extract_lane_thumb2() {
11766        let encoder = ArmEncoder::new_thumb2();
11767        let op = ArmOp::MveExtractLane {
11768            rd: Reg::R0,
11769            qn: QReg::Q1,
11770            lane: 2,
11771            size: MveSize::S32,
11772        };
11773        let code = encoder.encode(&op).unwrap();
11774        assert_eq!(code.len(), 4, "MVE extract lane should be 4 bytes");
11775    }
11776
11777    #[test]
11778    fn test_encode_mve_insert_lane_thumb2() {
11779        let encoder = ArmEncoder::new_thumb2();
11780        let op = ArmOp::MveInsertLane {
11781            qd: QReg::Q0,
11782            rn: Reg::R1,
11783            lane: 3,
11784            size: MveSize::S32,
11785        };
11786        let code = encoder.encode(&op).unwrap();
11787        assert_eq!(code.len(), 4, "MVE insert lane should be 4 bytes");
11788    }
11789
11790    #[test]
11791    fn test_encode_mve_addf32_thumb2() {
11792        let encoder = ArmEncoder::new_thumb2();
11793        let op = ArmOp::MveAddF32 {
11794            qd: QReg::Q0,
11795            qn: QReg::Q1,
11796            qm: QReg::Q2,
11797        };
11798        let code = encoder.encode(&op).unwrap();
11799        assert_eq!(code.len(), 4, "MVE VADD.F32 should be 4 bytes");
11800    }
11801
11802    #[test]
11803    fn test_encode_mve_divf32_thumb2() {
11804        let encoder = ArmEncoder::new_thumb2();
11805        let op = ArmOp::MveDivF32 {
11806            qd: QReg::Q0,
11807            qn: QReg::Q1,
11808            qm: QReg::Q2,
11809        };
11810        let code = encoder.encode(&op).unwrap();
11811        // Lane-wise: 4 x VDIV.F32 = 4 x 4 = 16 bytes
11812        assert_eq!(
11813            code.len(),
11814            16,
11815            "MVE VDIV.F32 (lane-wise) should be 16 bytes"
11816        );
11817    }
11818
11819    #[test]
11820    fn test_encode_mve_sqrtf32_thumb2() {
11821        let encoder = ArmEncoder::new_thumb2();
11822        let op = ArmOp::MveSqrtF32 {
11823            qd: QReg::Q0,
11824            qm: QReg::Q1,
11825        };
11826        let code = encoder.encode(&op).unwrap();
11827        // Lane-wise: 4 x VSQRT.F32 = 4 x 4 = 16 bytes
11828        assert_eq!(
11829            code.len(),
11830            16,
11831            "MVE VSQRT.F32 (lane-wise) should be 16 bytes"
11832        );
11833    }
11834
11835    #[test]
11836    fn test_encode_mve_negf32_thumb2() {
11837        let encoder = ArmEncoder::new_thumb2();
11838        let op = ArmOp::MveNegF32 {
11839            qd: QReg::Q0,
11840            qm: QReg::Q1,
11841        };
11842        let code = encoder.encode(&op).unwrap();
11843        assert_eq!(code.len(), 4, "MVE VNEG.F32 should be 4 bytes");
11844    }
11845
11846    #[test]
11847    fn test_encode_mve_absf32_thumb2() {
11848        let encoder = ArmEncoder::new_thumb2();
11849        let op = ArmOp::MveAbsF32 {
11850            qd: QReg::Q0,
11851            qm: QReg::Q1,
11852        };
11853        let code = encoder.encode(&op).unwrap();
11854        assert_eq!(code.len(), 4, "MVE VABS.F32 should be 4 bytes");
11855    }
11856
11857    /// VCR-RA-001 / immediate-folding precondition: pins the Thumb-2 `AND`
11858    /// immediate encoding for the byte range and documents its bound.
11859    ///
11860    /// The `And { Operand2::Imm }` encoder packs the low 12 bits straight into
11861    /// the `i:imm3:imm8` field WITHOUT applying ThumbExpandImm (the modified-
11862    /// immediate expansion). For `imm <= 0xFF` (e.g. gale's int8 clamps
11863    /// `#0x7e` / `#0x7f`) that is correct — `i:imm3 = 0000` means "imm8
11864    /// zero-extended". So `and r2, r0, #0x7e` encodes to the canonical
11865    /// `00 f0 7e 02`. For `imm >= 0x100` the field would need a true
11866    /// ThumbExpandImm pattern (rotation / replication), which is NOT
11867    /// implemented here — so **immediate folding must gate on `imm <= 0xFF`**
11868    /// until the encoder is hardened to ThumbExpandImm/Ok-or-Err (the
11869    /// "encoder must be Ok-or-Err, never silently wrong" principle, #180/#185).
11870    /// This bound covers the measured `flat_flight` waste (#209).
11871    #[test]
11872    fn and_immediate_encodes_correctly_in_byte_range_documents_fold_bound() {
11873        let encoder = ArmEncoder::new_thumb2();
11874        let op = ArmOp::And {
11875            rd: Reg::R2,
11876            rn: Reg::R0,
11877            op2: Operand2::Imm(0x7e),
11878        };
11879        let code = encoder.encode(&op).unwrap();
11880        assert_eq!(
11881            code,
11882            vec![0x00, 0xf0, 0x7e, 0x02],
11883            "and r2, r0, #0x7e must encode to the canonical AND.W T1 (imm8=0x7e)"
11884        );
11885    }
11886
11887    /// #255: the shared ThumbExpandImm reverse-encoder underpinning the
11888    /// data-processing immediate fix. Encodable modified immediates round-trip to
11889    /// the expected `i:imm3:imm8` field; a genuinely non-modified value is `None`
11890    /// (caller must materialize into a register). Note `1000 = 0xFA ror 30` *is*
11891    /// representable (field 0xF7A) — the old encoder mis-encoded it (raw 0x3E8);
11892    /// this encodes it correctly.
11893    #[test]
11894    fn try_thumb_expand_imm_encodes_modified_immediates() {
11895        assert_eq!(try_thumb_expand_imm(0x7e), Some(0x07e)); // zero-extended byte
11896        assert_eq!(try_thumb_expand_imm(0xff), Some(0x0ff));
11897        assert_eq!(try_thumb_expand_imm(0x0001_0001), Some(0x101)); // 0x00XY00XY
11898        assert_eq!(try_thumb_expand_imm(0xff00_ff00), Some(0x2ff)); // 0xXY00XY00
11899        assert_eq!(try_thumb_expand_imm(0xffff_ffff), Some(0x3ff)); // 0xXYXYXYXY
11900        assert_eq!(try_thumb_expand_imm(0x100), Some(0xf80)); // 0x80 ror 31
11901        assert_eq!(try_thumb_expand_imm(0x8000_0000), Some(0x400)); // 0x80 ror 8
11902        assert_eq!(try_thumb_expand_imm(1000), Some(0xf7a)); // 0xFA ror 30
11903        // Genuinely unrepresentable (bits too far apart for an 8-bit window).
11904        assert_eq!(try_thumb_expand_imm(0x101), None);
11905        assert_eq!(try_thumb_expand_imm(0x12345), None);
11906    }
11907
11908    /// #255: CMP/ADDS/SUBS encode any valid modified immediate correctly, and
11909    /// ERROR (not silently mis-encode) on a genuinely unrepresentable one,
11910    /// forcing the selector to materialize into a register — closing the
11911    /// silent-miscompile class of #251/#253.
11912    #[test]
11913    fn cmp_adds_subs_immediate_error_on_non_modified_imm() {
11914        let encoder = ArmEncoder::new_thumb2();
11915        // cmp r0, #0xff → valid → Ok; cmp r0, #1000 → valid (0xFA ror 30) → Ok.
11916        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 0xff).is_ok());
11917        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 1000).is_ok());
11918        // cmp r0, #0x101 → NOT a modified immediate → Err (materialize-reg).
11919        assert!(
11920            encoder.encode_thumb32_cmp_imm(&Reg::R0, 0x101).is_err(),
11921            "cmp #0x101 must error, not compare the wrong constant"
11922        );
11923        assert!(
11924            encoder
11925                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x101)
11926                .is_err()
11927        );
11928        assert!(
11929            encoder
11930                .encode_thumb32_subs(&Reg::R0, &Reg::R0, 0x101)
11931                .is_err()
11932        );
11933        // ...but a valid modified immediate still encodes.
11934        assert!(
11935            encoder
11936                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x80)
11937                .is_ok()
11938        );
11939    }
11940
11941    /// #257: MLA (multiply-accumulate) encodes as MLS without the bit-4 op flag.
11942    /// `mla r2, r3, r4, r8` (rd=r2, rn=r3, rm=r4, ra=r8) → Thumb-2 `03 fb 04 82`.
11943    #[test]
11944    fn mla_thumb2_encodes_correctly() {
11945        let encoder = ArmEncoder::new_thumb2();
11946        let code = encoder
11947            .encode(&ArmOp::Mla {
11948                rd: Reg::R2,
11949                rn: Reg::R3,
11950                rm: Reg::R4,
11951                ra: Reg::R8,
11952            })
11953            .unwrap();
11954        // hw1 = 0xFB03, hw2 = (8<<12)|(2<<8)|4 = 0x8204
11955        assert_eq!(code, vec![0x03, 0xfb, 0x04, 0x82]);
11956    }
11957
11958    /// #259: LDR/STR (and sub-word) immediate-offset encoders truncated
11959    /// `offset & 0xFFF`, silently targeting the wrong address for offset >= 4096.
11960    /// They now error (the selector must use register-offset addressing) — the
11961    /// load/store sibling of the #253/#255 class. Offsets <= 4095 still encode.
11962    #[test]
11963    fn ldst_imm12_offset_errors_when_out_of_range() {
11964        let encoder = ArmEncoder::new_thumb2();
11965        // offset 0xFFF (4095): valid → Ok; ldr r0, [r1, #4095].
11966        assert!(
11967            encoder
11968                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0xFFF)
11969                .is_ok()
11970        );
11971        // offset 0x1000 (4096): out of imm12 range → Err (not & 0xFFF → #0).
11972        assert!(
11973            encoder
11974                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0x1000)
11975                .is_err(),
11976            "ldr offset 4096 must error, not wrap to 0"
11977        );
11978        assert!(
11979            encoder
11980                .encode_thumb32_str(&Reg::R0, &Reg::R1, 0x1000)
11981                .is_err()
11982        );
11983        assert!(
11984            encoder
11985                .encode_thumb32_ldrb_imm(&Reg::R0, &Reg::R1, 5000)
11986                .is_err()
11987        );
11988        assert!(
11989            encoder
11990                .encode_thumb32_strh_imm(&Reg::R0, &Reg::R1, 5000)
11991                .is_err()
11992        );
11993    }
11994
11995    /// Latent miscompile fix: ADD/SUB with a >0xFF immediate (e.g.
11996    /// `add sp, sp, #frame` for a >=256-byte frame) used ADD.W (T3), whose
11997    /// `i:imm3:imm8` is a ThumbExpandImm modified immediate — so `#256` silently
11998    /// encoded as `#0` (stack corruption). Use ADDW/SUBW (T4), a PLAIN 12-bit
11999    /// immediate, for 0x100..=0xFFF; keep T3 for <=0xFF (bit-identical); error
12000    /// beyond 4095.
12001    #[test]
12002    fn add_sub_large_immediate_use_addw_subw_not_misencoded() {
12003        let encoder = ArmEncoder::new_thumb2();
12004        // add sp, sp, #256  →  ADDW (T4) SP, SP, #256  =  0d f2 00 1d
12005        assert_eq!(
12006            encoder
12007                .encode(&ArmOp::Add {
12008                    rd: Reg::SP,
12009                    rn: Reg::SP,
12010                    op2: Operand2::Imm(256),
12011                })
12012                .unwrap(),
12013            vec![0x0d, 0xf2, 0x00, 0x1d],
12014            "add sp,sp,#256 must be ADDW (plain imm12), not a mis-encoded ADD.W"
12015        );
12016        // sub sp, sp, #256  →  SUBW (T4) SP, SP, #256  =  ad f2 00 1d
12017        assert_eq!(
12018            encoder
12019                .encode(&ArmOp::Sub {
12020                    rd: Reg::SP,
12021                    rn: Reg::SP,
12022                    op2: Operand2::Imm(256),
12023                })
12024                .unwrap(),
12025            vec![0xad, 0xf2, 0x00, 0x1d],
12026        );
12027        // > 4095 has no single-instruction encoding → error, not silent wrong.
12028        assert!(
12029            encoder
12030                .encode(&ArmOp::Add {
12031                    rd: Reg::SP,
12032                    rn: Reg::SP,
12033                    op2: Operand2::Imm(5000),
12034                })
12035                .is_err(),
12036            "add #5000 must error (no single ADDW), not mis-encode"
12037        );
12038    }
12039
12040    /// Closes the data-proc immediate class: AND and CMN now go through
12041    /// `try_thumb_expand_imm` like ORR/EOR/CMP — correct for any modified
12042    /// immediate, `Err` (not raw-pack / NOP) on an un-encodable one. The byte
12043    /// range stays bit-identical (`and r2,r0,#0x7e` is unchanged).
12044    #[test]
12045    fn and_cmn_immediate_thumb_expand_else_error() {
12046        let encoder = ArmEncoder::new_thumb2();
12047        // byte range unchanged (bit-identical with the pre-retrofit encoding)
12048        assert_eq!(
12049            encoder
12050                .encode(&ArmOp::And {
12051                    rd: Reg::R2,
12052                    rn: Reg::R0,
12053                    op2: Operand2::Imm(0x7e),
12054                })
12055                .unwrap(),
12056            vec![0x00, 0xf0, 0x7e, 0x02],
12057        );
12058        // a valid replicated modified immediate now encodes (was silently wrong)
12059        assert!(
12060            encoder
12061                .encode(&ArmOp::And {
12062                    rd: Reg::R2,
12063                    rn: Reg::R0,
12064                    op2: Operand2::Imm(0xff00ff00u32 as i32),
12065                })
12066                .is_ok()
12067        );
12068        // a genuinely un-encodable immediate errors (AND was raw-pack; CMN NOP)
12069        assert!(
12070            encoder
12071                .encode(&ArmOp::And {
12072                    rd: Reg::R2,
12073                    rn: Reg::R0,
12074                    op2: Operand2::Imm(0x101),
12075                })
12076                .is_err()
12077        );
12078        assert!(
12079            encoder
12080                .encode(&ArmOp::Cmn {
12081                    rn: Reg::R0,
12082                    op2: Operand2::Imm(0x101),
12083                })
12084                .is_err(),
12085            "CMN #0x101 must error, not emit a NOP"
12086        );
12087    }
12088
12089    /// VCR-RA-001: ORR/EOR with a small immediate must encode the real
12090    /// instruction (not a silent `0xBF00` NOP). Pins the byte range and the
12091    /// Ok-or-Err bound that makes future Or/Eor immediate folding safe.
12092    #[test]
12093    fn orr_eor_immediate_encode_in_byte_range_else_error() {
12094        let encoder = ArmEncoder::new_thumb2();
12095        // orr r2, r0, #0x7e  →  ORR.W T1, imm8=0x7e
12096        assert_eq!(
12097            encoder
12098                .encode(&ArmOp::Orr {
12099                    rd: Reg::R2,
12100                    rn: Reg::R0,
12101                    op2: Operand2::Imm(0x7e),
12102                })
12103                .unwrap(),
12104            vec![0x40, 0xf0, 0x7e, 0x02],
12105        );
12106        // eor r2, r0, #0x7e  →  EOR.W T1, imm8=0x7e
12107        assert_eq!(
12108            encoder
12109                .encode(&ArmOp::Eor {
12110                    rd: Reg::R2,
12111                    rn: Reg::R0,
12112                    op2: Operand2::Imm(0x7e),
12113                })
12114                .unwrap(),
12115            vec![0x80, 0xf0, 0x7e, 0x02],
12116        );
12117        // Out-of-range immediates error rather than silently mis-encode / NOP.
12118        assert!(
12119            encoder
12120                .encode(&ArmOp::Orr {
12121                    rd: Reg::R2,
12122                    rn: Reg::R0,
12123                    op2: Operand2::Imm(0x140),
12124                })
12125                .is_err(),
12126            "ORR #0x140 must error, not emit a NOP"
12127        );
12128    }
12129
12130    #[test]
12131    fn test_encode_mve_different_qregs() {
12132        let encoder = ArmEncoder::new_thumb2();
12133
12134        // Test that different Q-register numbers produce different encodings
12135        let op1 = ArmOp::MveAddI {
12136            qd: QReg::Q0,
12137            qn: QReg::Q0,
12138            qm: QReg::Q0,
12139            size: MveSize::S32,
12140        };
12141        let op2 = ArmOp::MveAddI {
12142            qd: QReg::Q3,
12143            qn: QReg::Q5,
12144            qm: QReg::Q7,
12145            size: MveSize::S32,
12146        };
12147        let code1 = encoder.encode(&op1).unwrap();
12148        let code2 = encoder.encode(&op2).unwrap();
12149        assert_ne!(
12150            code1, code2,
12151            "Different Q-registers should produce different encodings"
12152        );
12153    }
12154
12155    #[test]
12156    fn test_encode_mve_arm32_loud_err() {
12157        // #615: MVE (Helium) is Thumb-2-only. The ARM32 encoder used to emit
12158        // a silent NOP here (dropping the vector op); it must now be a typed
12159        // Err so a broken "MVE implies Thumb" invariant fails loudly.
12160        let encoder = ArmEncoder::new_arm32();
12161        let op = ArmOp::MveAddI {
12162            qd: QReg::Q0,
12163            qn: QReg::Q1,
12164            qm: QReg::Q2,
12165            size: MveSize::S32,
12166        };
12167        let err = encoder
12168            .encode(&op)
12169            .expect_err("ARM32 MVE must be a loud Err, not a silent NOP (#615)");
12170        assert!(
12171            err.to_string().contains("Thumb-2 only"),
12172            "unexpected error message: {err}"
12173        );
12174    }
12175}