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    /// The §4.4.8 type check is discharged at COMPILE time by the selector's
141    /// closed-world verification (the raw code-pointer table carries no
142    /// runtime type ids) — see the #642 selector guard.
143    fn encode_arm_call_indirect(table_index_reg: &Reg, table_size: u32) -> Vec<u8> {
144        let idx = reg_to_bits(table_index_reg);
145        let mut bytes = Vec::with_capacity(32);
146        // MOVW r12, #(size & 0xFFFF) — cond=E 0011 0000 imm4 Rd imm12.
147        let size_lo = table_size & 0xFFFF;
148        let movw: u32 = 0xE300_0000 | ((size_lo >> 12) << 16) | (12 << 12) | (size_lo & 0xFFF);
149        bytes.extend_from_slice(&movw.to_le_bytes());
150        // MOVT r12, #(size >> 16) — only for a table size above 16 bits.
151        let size_hi = table_size >> 16;
152        if size_hi != 0 {
153            let movt: u32 = 0xE340_0000 | ((size_hi >> 12) << 16) | (12 << 12) | (size_hi & 0xFFF);
154            bytes.extend_from_slice(&movt.to_le_bytes());
155        }
156        // CMP idx, r12 — cond=E, opcode=1010, S=1, Rn=idx, Rm=r12.
157        let cmp: u32 = 0xE150_000C | (idx << 16);
158        bytes.extend_from_slice(&cmp.to_le_bytes());
159        // BLO +1 insn (skip the UDF when index < size) — cond=LO(0011),
160        // imm24=0: target = branch + 8.
161        bytes.extend_from_slice(&0x3A00_0000u32.to_le_bytes());
162        // UDF — permanently undefined (same trap idiom as the A32 div-by-zero
163        // guards): call_indirect out-of-bounds trap.
164        bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
165        // MOV r12, idx, LSL #2 — data-processing MOV, register op2 with
166        // imm5=2/LSL: cond=E, opcode=1101, S=0, Rd=r12.
167        let mov: u32 = 0xE1A0C000 | (2 << 7) | idx;
168        bytes.extend_from_slice(&mov.to_le_bytes());
169        // LDR r12, [r11, r12] — register offset, P=1 U=1 B=0 W=0 L=1.
170        let ldr: u32 = 0xE79BC00C;
171        bytes.extend_from_slice(&ldr.to_le_bytes());
172        // BLX r12 — cond=E, 0001 0010 1111 1111 1111 0011, Rm=r12.
173        let blx: u32 = 0xE12FFF3C;
174        bytes.extend_from_slice(&blx.to_le_bytes());
175        bytes
176    }
177
178    /// #615: A32 (ARM-mode) expansions for the multi-instruction ops that the
179    /// Thumb-2 encoder expands but the A32 arm previously encoded as a single
180    /// literal NOP (`0xE1A00000`) — i64 mul / shifts / rotates / comparisons /
181    /// eqz, plus i64 const/load/store/extend/wrap and the i32 SetCond /
182    /// SelectMove pseudo-ops. Each expansion mirrors its Thumb-2 twin's
183    /// register contract and semantics exactly (A32 conditional execution
184    /// replaces the IT blocks). Returns `Ok(None)` for ops this helper does
185    /// not handle; the caller's match encodes or loudly rejects those.
186    fn encode_arm_expanded(&self, op: &ArmOp) -> Result<Option<Vec<u8>>> {
187        use synth_synthesis::Condition;
188
189        /// A32 condition-field bits (instruction bits [31:28]).
190        fn cond_bits(cond: &Condition) -> u32 {
191            match cond {
192                Condition::EQ => 0x0,
193                Condition::NE => 0x1,
194                Condition::HS => 0x2, // CS: unsigned >=
195                Condition::LO => 0x3, // CC: unsigned <
196                Condition::HI => 0x8, // unsigned >
197                Condition::LS => 0x9, // unsigned <=
198                Condition::GE => 0xA,
199                Condition::LT => 0xB,
200                Condition::GT => 0xC,
201                Condition::LE => 0xD,
202            }
203        }
204        fn w(b: &mut Vec<u8>, word: u32) {
205            b.extend_from_slice(&word.to_le_bytes());
206        }
207        /// MOV<cond> rd, #imm (rotated-immediate form; only 0/1 used here).
208        fn mov_cond_imm(b: &mut Vec<u8>, cond: u32, rd: u32, imm: u32) {
209            w(b, (cond << 28) | 0x03A0_0000 | (rd << 12) | imm);
210        }
211        /// After a flag-setting pair: MOV<cond> rd,#1 ; MOV<!cond> rd,#0.
212        fn set_cond(b: &mut Vec<u8>, cond: &Condition, rd: u32) {
213            mov_cond_imm(b, cond_bits(cond), rd, 1);
214            mov_cond_imm(b, cond_bits(&cond.invert()), rd, 0);
215        }
216        /// CMP rn, rm (register form).
217        fn cmp_reg(b: &mut Vec<u8>, rn: u32, rm: u32) {
218            w(b, 0xE150_0000 | (rn << 16) | rm);
219        }
220        /// SBCS rd, rn, rm — the 64-bit compare idiom's high-word subtract.
221        fn sbcs(b: &mut Vec<u8>, rd: u32, rn: u32, rm: u32) {
222            w(b, 0xE0D0_0000 | (rn << 16) | (rd << 12) | rm);
223        }
224        /// MOVW rd, #imm16.
225        fn movw(b: &mut Vec<u8>, rd: u32, v: u32) {
226            w(
227                b,
228                0xE300_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
229            );
230        }
231        /// MOVT rd, #imm16.
232        fn movt(b: &mut Vec<u8>, rd: u32, v: u32) {
233            w(
234                b,
235                0xE340_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
236            );
237        }
238        /// Register-controlled shift: MOV rd, rn, <LSL|LSR|ASR> rs.
239        /// `ty`: 0=LSL, 1=LSR, 2=ASR. A32 uses the bottom byte of rs;
240        /// amounts of 32 or more yield 0 (LSL/LSR) or all-sign (ASR) — same
241        /// semantics the Thumb-2 expansions rely on.
242        fn shift_reg(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, rs: u32) {
243            w(b, 0xE1A0_0010 | (rd << 12) | (rs << 8) | (ty << 5) | rn);
244        }
245        const LSL: u32 = 0;
246        const LSR: u32 = 1;
247        const ASR: u32 = 2;
248        /// Immediate-shift move: MOV rd, rn, <LSL|LSR|ASR> #imm.
249        fn shift_imm(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, imm: u32) {
250            w(
251                b,
252                0xE1A0_0000 | (rd << 12) | ((imm & 0x1F) << 7) | (ty << 5) | rn,
253            );
254        }
255        /// Data-processing register form: `base | rn<<16 | rd<<12 | rm`.
256        /// `base` carries cond/opcode/S (e.g. 0xE090_0000 = ADDS).
257        fn dp_reg(b: &mut Vec<u8>, base: u32, rd: u32, rn: u32, rm: u32) {
258            w(b, base | (rn << 16) | (rd << 12) | rm);
259        }
260        /// ORR rd, rd, rm, LSR #31 — the carry-propagation idiom of the
261        /// shift-subtract division loop (bring rm's MSB into rd's bit 0).
262        fn orr_lsr31(b: &mut Vec<u8>, rd: u32, rm: u32) {
263            w(
264                b,
265                0xE180_0000 | (rd << 16) | (rd << 12) | (31 << 7) | (1 << 5) | rm,
266            );
267        }
268        /// 64-bit two's-complement negate of the lo:hi pair (MVN/MVN/ADDS/ADC).
269        fn negate64(b: &mut Vec<u8>, lo: u32, hi: u32) {
270            w(b, 0xE1E0_0000 | (lo << 12) | lo); //           MVN  lo, lo
271            w(b, 0xE1E0_0000 | (hi << 12) | hi); //           MVN  hi, hi
272            w(b, 0xE290_0001 | (lo << 16) | (lo << 12)); //   ADDS lo, lo, #1
273            w(b, 0xE2A0_0000 | (hi << 16) | (hi << 12)); //   ADC  hi, hi, #0
274        }
275        /// TST x, x ; BPL +4-instructions — the "skip the negate64 when the
276        /// sign bit is clear" guard of the signed div/rem arms.
277        fn skip_negate_if_positive(b: &mut Vec<u8>, x: u32) {
278            w(b, 0xE110_0000 | (x << 16) | x); // TST x, x
279            w(b, 0x5A00_0003); //                 BPL +4 insns (past negate64)
280        }
281        /// The 64-iteration shift-subtract division loop — A32 transcription
282        /// of the Thumb-2 #610 core: dividend R0:R1, divisor R2:R3, quotient
283        /// R4:R5, remainder R6:R7, loop counter in `counter` (R12 or R8).
284        fn div_loop(b: &mut Vec<u8>, counter: u32) {
285            w(b, 0xE3A0_0040 | (counter << 12)); // MOV counter, #64
286            let loop_start = b.len();
287            // quotient <<= 1
288            shift_imm(b, LSL, 5, 5, 1);
289            orr_lsr31(b, 5, 4);
290            shift_imm(b, LSL, 4, 4, 1);
291            // remainder <<= 1, OR in dividend MSB
292            shift_imm(b, LSL, 7, 7, 1);
293            orr_lsr31(b, 7, 6);
294            shift_imm(b, LSL, 6, 6, 1);
295            orr_lsr31(b, 6, 1);
296            // dividend <<= 1
297            shift_imm(b, LSL, 1, 1, 1);
298            orr_lsr31(b, 1, 0);
299            shift_imm(b, LSL, 0, 0, 1);
300            // if remainder >= divisor (64-bit unsigned): subtract, set q bit
301            w(b, 0xE157_0003); // CMP R7, R3      (high words)
302            w(b, 0x8A00_0002); // BHI .subtract   (+2 insns)
303            w(b, 0x3A00_0004); // BLO .next       (+4 insns)
304            w(b, 0xE156_0002); // CMP R6, R2      (low words, highs equal)
305            w(b, 0x3A00_0002); // BLO .next       (+2 insns)
306            w(b, 0xE056_6002); // .subtract: SUBS R6, R6, R2
307            w(b, 0xE0C7_7003); //            SBC  R7, R7, R3
308            w(b, 0xE384_4001); //            ORR  R4, R4, #1
309            // .next: decrement and loop
310            w(b, 0xE250_0001 | (counter << 16) | (counter << 12)); // SUBS counter, #1
311            let diff = (loop_start as i64) - (b.len() as i64 + 8);
312            w(b, 0x1A00_0000 | (((diff / 4) as u32) & 0x00FF_FFFF)); // BNE loop
313        }
314        /// 32-bit population count on working register `x` — A32 transcription
315        /// of the Thumb-2 I64Popcnt per-word core (mul-based fold): `c` is the
316        /// constant register, R12 the shifted temp. Both are clobbered.
317        fn popcnt_word(b: &mut Vec<u8>, x: u32, c: u32) {
318            // x = x - ((x >> 1) & 0x55555555)
319            shift_imm(b, LSR, 12, x, 1);
320            movw(b, c, 0x5555);
321            movt(b, c, 0x5555);
322            dp_reg(b, 0xE000_0000, 12, 12, c); // AND R12, R12, c
323            dp_reg(b, 0xE040_0000, x, x, 12); //  SUB x, x, R12
324            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
325            movw(b, c, 0x3333);
326            movt(b, c, 0x3333);
327            dp_reg(b, 0xE000_0000, 12, x, c); //  AND R12, x, c
328            shift_imm(b, LSR, x, x, 2);
329            dp_reg(b, 0xE000_0000, x, x, c); //   AND x, x, c
330            dp_reg(b, 0xE080_0000, x, x, 12); //  ADD x, x, R12
331            // x = (x + (x >> 4)) & 0x0F0F0F0F
332            shift_imm(b, LSR, 12, x, 4);
333            dp_reg(b, 0xE080_0000, x, x, 12); //  ADD x, x, R12
334            movw(b, c, 0x0F0F);
335            movt(b, c, 0x0F0F);
336            dp_reg(b, 0xE000_0000, x, x, c); //   AND x, x, c
337            // x = (x * 0x01010101) >> 24
338            movw(b, c, 0x0101);
339            movt(b, c, 0x0101);
340            w(b, 0xE000_0090 | (x << 16) | (c << 8) | x); // MUL x, x, c
341            shift_imm(b, LSR, x, x, 24);
342        }
343
344        let mut b: Vec<u8> = Vec::new();
345        match op {
346            // SetCond: materialize a flags-predicate as 0/1 — the A32 twin of
347            // the Thumb `ITE cond; MOV rd,#1; MOV rd,#0`.
348            ArmOp::SetCond { rd, cond } => {
349                set_cond(&mut b, cond, reg_to_bits(rd));
350            }
351
352            // SelectMove: conditional register move (Thumb: IT cond; MOV).
353            ArmOp::SelectMove { rd, rm, cond } => {
354                w(
355                    &mut b,
356                    (cond_bits(cond) << 28)
357                        | 0x01A0_0000
358                        | (reg_to_bits(rd) << 12)
359                        | reg_to_bits(rm),
360                );
361            }
362
363            // I64SetCond: compare two i64 register pairs, 0/1 into rd.
364            // EQ/NE: CMP lo,lo; CMPEQ hi,hi (only if lows equal); set.
365            // Ordered: CMP lo,lo; SBCS rd,hi,hi; set — with the same
366            // operand-swap + condition mapping as the Thumb-2 arm.
367            ArmOp::I64SetCond {
368                rd,
369                rn_lo,
370                rn_hi,
371                rm_lo,
372                rm_hi,
373                cond,
374            } => {
375                let rd_b = reg_to_bits(rd);
376                let (n_lo, n_hi, m_lo, m_hi) = (
377                    reg_to_bits(rn_lo),
378                    reg_to_bits(rn_hi),
379                    reg_to_bits(rm_lo),
380                    reg_to_bits(rm_hi),
381                );
382                match cond {
383                    Condition::EQ | Condition::NE => {
384                        cmp_reg(&mut b, n_lo, m_lo);
385                        // CMP<EQ> rn_hi, rm_hi — compare highs only if lows equal.
386                        w(&mut b, 0x0150_0000 | (n_hi << 16) | m_hi);
387                        set_cond(&mut b, cond, rd_b);
388                    }
389                    // (swap operands?, condition after SBCS) per the Thumb arm:
390                    // LT/GE/LO/HS compare (rn, rm); GT/LE/HI/LS swap to (rm, rn).
391                    Condition::LT => {
392                        cmp_reg(&mut b, n_lo, m_lo);
393                        sbcs(&mut b, rd_b, n_hi, m_hi);
394                        set_cond(&mut b, &Condition::LT, rd_b);
395                    }
396                    Condition::GE => {
397                        cmp_reg(&mut b, n_lo, m_lo);
398                        sbcs(&mut b, rd_b, n_hi, m_hi);
399                        set_cond(&mut b, &Condition::GE, rd_b);
400                    }
401                    Condition::GT => {
402                        cmp_reg(&mut b, m_lo, n_lo);
403                        sbcs(&mut b, rd_b, m_hi, n_hi);
404                        set_cond(&mut b, &Condition::LT, rd_b);
405                    }
406                    Condition::LE => {
407                        cmp_reg(&mut b, m_lo, n_lo);
408                        sbcs(&mut b, rd_b, m_hi, n_hi);
409                        set_cond(&mut b, &Condition::GE, rd_b);
410                    }
411                    Condition::LO => {
412                        cmp_reg(&mut b, n_lo, m_lo);
413                        sbcs(&mut b, rd_b, n_hi, m_hi);
414                        set_cond(&mut b, &Condition::LO, rd_b);
415                    }
416                    Condition::HS => {
417                        cmp_reg(&mut b, n_lo, m_lo);
418                        sbcs(&mut b, rd_b, n_hi, m_hi);
419                        set_cond(&mut b, &Condition::HS, rd_b);
420                    }
421                    Condition::HI => {
422                        cmp_reg(&mut b, m_lo, n_lo);
423                        sbcs(&mut b, rd_b, m_hi, n_hi);
424                        set_cond(&mut b, &Condition::LO, rd_b);
425                    }
426                    Condition::LS => {
427                        cmp_reg(&mut b, m_lo, n_lo);
428                        sbcs(&mut b, rd_b, m_hi, n_hi);
429                        set_cond(&mut b, &Condition::HS, rd_b);
430                    }
431                }
432            }
433
434            // I64SetCondZ: ORRS rd, lo, hi sets Z iff the pair is zero.
435            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
436                let rd_b = reg_to_bits(rd);
437                w(
438                    &mut b,
439                    0xE190_0000 | (reg_to_bits(rn_lo) << 16) | (rd_b << 12) | reg_to_bits(rn_hi),
440                );
441                set_cond(&mut b, &Condition::EQ, rd_b);
442            }
443
444            // i64 comparison wrappers: delegate to I64SetCond/Z, mirroring the
445            // Thumb-2 delegation arms.
446            ArmOp::I64Eqz { rd, rnlo, rnhi } => {
447                return self
448                    .encode_arm(&ArmOp::I64SetCondZ {
449                        rd: *rd,
450                        rn_lo: *rnlo,
451                        rn_hi: *rnhi,
452                    })
453                    .map(Some);
454            }
455            ArmOp::I64Eq {
456                rd,
457                rnlo,
458                rnhi,
459                rmlo,
460                rmhi,
461            }
462            | ArmOp::I64Ne {
463                rd,
464                rnlo,
465                rnhi,
466                rmlo,
467                rmhi,
468            }
469            | ArmOp::I64LtS {
470                rd,
471                rnlo,
472                rnhi,
473                rmlo,
474                rmhi,
475            }
476            | ArmOp::I64LtU {
477                rd,
478                rnlo,
479                rnhi,
480                rmlo,
481                rmhi,
482            }
483            | ArmOp::I64LeS {
484                rd,
485                rnlo,
486                rnhi,
487                rmlo,
488                rmhi,
489            }
490            | ArmOp::I64LeU {
491                rd,
492                rnlo,
493                rnhi,
494                rmlo,
495                rmhi,
496            }
497            | ArmOp::I64GtS {
498                rd,
499                rnlo,
500                rnhi,
501                rmlo,
502                rmhi,
503            }
504            | ArmOp::I64GtU {
505                rd,
506                rnlo,
507                rnhi,
508                rmlo,
509                rmhi,
510            }
511            | ArmOp::I64GeS {
512                rd,
513                rnlo,
514                rnhi,
515                rmlo,
516                rmhi,
517            }
518            | ArmOp::I64GeU {
519                rd,
520                rnlo,
521                rnhi,
522                rmlo,
523                rmhi,
524            } => {
525                let cond = match op {
526                    ArmOp::I64Eq { .. } => Condition::EQ,
527                    ArmOp::I64Ne { .. } => Condition::NE,
528                    ArmOp::I64LtS { .. } => Condition::LT,
529                    ArmOp::I64LtU { .. } => Condition::LO,
530                    ArmOp::I64LeS { .. } => Condition::LE,
531                    ArmOp::I64LeU { .. } => Condition::LS,
532                    ArmOp::I64GtS { .. } => Condition::GT,
533                    ArmOp::I64GtU { .. } => Condition::HI,
534                    ArmOp::I64GeS { .. } => Condition::GE,
535                    _ => Condition::HS,
536                };
537                return self
538                    .encode_arm(&ArmOp::I64SetCond {
539                        rd: *rd,
540                        rn_lo: *rnlo,
541                        rn_hi: *rnhi,
542                        rm_lo: *rmlo,
543                        rm_hi: *rmhi,
544                        cond,
545                    })
546                    .map(Some);
547            }
548
549            // I64Mul: cross products into R12, then UMULL — same sequence and
550            // ordering as the Thumb-2 arm (R12 is encoder scratch, #212).
551            ArmOp::I64Mul {
552                rd_lo,
553                rd_hi,
554                rn_lo,
555                rn_hi,
556                rm_lo,
557                rm_hi,
558            } => {
559                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
560                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
561                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
562                // MUL R12, rn_lo, rm_hi   (R12 = a_lo * b_hi)
563                w(&mut b, 0xE000_0090 | (12 << 16) | (mh << 8) | nl);
564                // MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
565                w(
566                    &mut b,
567                    0xE020_0090 | (12 << 16) | (12 << 12) | (ml << 8) | nh,
568                );
569                // UMULL rd_lo, rd_hi, rn_lo, rm_lo
570                w(
571                    &mut b,
572                    0xE080_0090 | (dh << 16) | (dl << 12) | (ml << 8) | nl,
573                );
574                // ADD rd_hi, rd_hi, R12
575                w(&mut b, 0xE080_0000 | (dh << 16) | (dh << 12) | 12);
576            }
577
578            // I64Shl / I64ShrU / I64ShrS: same small/large-shift structure as
579            // the Thumb-2 arms (rm_hi is the scratch register; amounts are
580            // masked to 6 bits; register-controlled shifts >= 32 yield 0,
581            // which the small path relies on for n = 0).
582            ArmOp::I64Shl {
583                rd_lo,
584                rd_hi,
585                rn_lo,
586                rn_hi,
587                rm_lo,
588                rm_hi,
589            } => {
590                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
591                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
592                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
593                w(&mut b, 0xE200_003F | (ml << 16) | (ml << 12)); // AND  ml, ml, #63
594                w(&mut b, 0xE250_0020 | (ml << 16) | (mh << 12)); // SUBS mh, ml, #32
595                w(&mut b, 0x5A00_0005); //                            BPL  .large
596                w(&mut b, 0xE260_0020 | (ml << 16) | (mh << 12)); // RSB  mh, ml, #32
597                shift_reg(&mut b, LSR, mh, nl, mh); //               mh = lo >> (32-n)
598                shift_reg(&mut b, LSL, dh, nh, ml); //               dh = hi << n
599                w(&mut b, 0xE180_0000 | (dh << 16) | (dh << 12) | mh); // ORR dh, dh, mh
600                shift_reg(&mut b, LSL, dl, nl, ml); //               dl = lo << n
601                w(&mut b, 0xEA00_0001); //                            B    .done
602                shift_reg(&mut b, LSL, dh, nl, mh); //               .large: dh = lo << (n-32)
603                w(&mut b, 0xE3A0_0000 | (dl << 12)); //              MOV  dl, #0
604            }
605            ArmOp::I64ShrU {
606                rd_lo,
607                rd_hi,
608                rn_lo,
609                rn_hi,
610                rm_lo,
611                rm_hi,
612            } => {
613                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
614                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
615                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
616                w(&mut b, 0xE200_003F | (ml << 16) | (ml << 12)); // AND  ml, ml, #63
617                w(&mut b, 0xE250_0020 | (ml << 16) | (mh << 12)); // SUBS mh, ml, #32
618                w(&mut b, 0x5A00_0005); //                            BPL  .large
619                w(&mut b, 0xE260_0020 | (ml << 16) | (mh << 12)); // RSB  mh, ml, #32
620                shift_reg(&mut b, LSL, mh, nh, mh); //               mh = hi << (32-n)
621                shift_reg(&mut b, LSR, dl, nl, ml); //               dl = lo >> n
622                w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | mh); // ORR dl, dl, mh
623                shift_reg(&mut b, LSR, dh, nh, ml); //               dh = hi >> n
624                w(&mut b, 0xEA00_0001); //                            B    .done
625                shift_reg(&mut b, LSR, dl, nh, mh); //               .large: dl = hi >> (n-32)
626                w(&mut b, 0xE3A0_0000 | (dh << 12)); //              MOV  dh, #0
627            }
628            ArmOp::I64ShrS {
629                rd_lo,
630                rd_hi,
631                rn_lo,
632                rn_hi,
633                rm_lo,
634                rm_hi,
635            } => {
636                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
637                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
638                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
639                w(&mut b, 0xE200_003F | (ml << 16) | (ml << 12)); // AND  ml, ml, #63
640                w(&mut b, 0xE250_0020 | (ml << 16) | (mh << 12)); // SUBS mh, ml, #32
641                w(&mut b, 0x5A00_0005); //                            BPL  .large
642                w(&mut b, 0xE260_0020 | (ml << 16) | (mh << 12)); // RSB  mh, ml, #32
643                shift_reg(&mut b, LSL, mh, nh, mh); //               mh = hi << (32-n)
644                shift_reg(&mut b, LSR, dl, nl, ml); //               dl = lo >> n
645                w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | mh); // ORR dl, dl, mh
646                shift_reg(&mut b, ASR, dh, nh, ml); //               dh = hi >> n (arith)
647                w(&mut b, 0xEA00_0001); //                            B    .done
648                shift_reg(&mut b, ASR, dl, nh, mh); //               .large: dl = hi >> (n-32)
649                w(&mut b, 0xE1A0_0040 | (dh << 12) | (31 << 7) | nh); // ASR dh, nh, #31
650            }
651
652            // I64Rotl / I64Rotr: the #610 fixed-ABI wrapper (A32 form) around
653            // the same fixed-register core as the Thumb-2 arms — value in
654            // R0:R1, amount in R2, scratch R3 + R12.
655            ArmOp::I64Rotl {
656                rdlo,
657                rdhi,
658                rnlo,
659                rnhi,
660                shift,
661            } => {
662                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
663                for word in [
664                    0xE202_203Fu32, // AND  R2, R2, #63   (mask amount mod 64)
665                    0xE252_3020,    // SUBS R3, R2, #32   (R3 = n-32, sets N)
666                    0x5A00_0007,    // BPL  .large        (n >= 32)
667                    // --- small rotation (n < 32) ---
668                    0xE262_3020, // RSB  R3, R2, #32   (R3 = 32-n)
669                    0xE1A0_C330, // LSR  R12, R0, R3   (lo >> (32-n))
670                    0xE1A0_3331, // LSR  R3, R1, R3    (hi >> (32-n))
671                    0xE1A0_1211, // LSL  R1, R1, R2    (hi << n)
672                    0xE181_100C, // ORR  R1, R1, R12   (new_hi)
673                    0xE1A0_0210, // LSL  R0, R0, R2    (lo << n)
674                    0xE180_0003, // ORR  R0, R0, R3    (new_lo)
675                    0xEA00_0007, // B    .done
676                    // --- large rotation (n >= 32), R3 = m = n-32 ---
677                    0xE263_2020, // RSB  R2, R3, #32   (R2 = 32-m = 64-n)
678                    0xE1A0_C231, // LSR  R12, R1, R2   (hi >> (64-n))
679                    0xE1A0_2230, // LSR  R2, R0, R2    (lo >> (64-n))
680                    0xE1A0_0310, // LSL  R0, R0, R3    (lo << m)
681                    0xE1A0_1311, // LSL  R1, R1, R3    (hi << m)
682                    0xE180_C00C, // ORR  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
683                    0xE181_0002, // ORR  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
684                    0xE1A0_100C, // MOV  R1, R12       (new_hi into place)
685                ] {
686                    w(&mut b, word);
687                }
688                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
689            }
690            ArmOp::I64Rotr {
691                rdlo,
692                rdhi,
693                rnlo,
694                rnhi,
695                shift,
696            } => {
697                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
698                for word in [
699                    0xE202_203Fu32, // AND  R2, R2, #63   (mask amount mod 64)
700                    0xE252_3020,    // SUBS R3, R2, #32   (R3 = n-32, sets N)
701                    0x5A00_0007,    // BPL  .large        (n >= 32)
702                    // --- small rotation (n < 32) ---
703                    0xE262_3020, // RSB  R3, R2, #32   (R3 = 32-n)
704                    0xE1A0_C311, // LSL  R12, R1, R3   (hi << (32-n))
705                    0xE1A0_3310, // LSL  R3, R0, R3    (lo << (32-n))
706                    0xE1A0_0230, // LSR  R0, R0, R2    (lo >> n)
707                    0xE180_000C, // ORR  R0, R0, R12   (new_lo)
708                    0xE1A0_1231, // LSR  R1, R1, R2    (hi >> n)
709                    0xE181_1003, // ORR  R1, R1, R3    (new_hi)
710                    0xEA00_0007, // B    .done
711                    // --- large rotation (n >= 32), R3 = m = n-32 ---
712                    0xE263_2020, // RSB  R2, R3, #32   (R2 = 32-m = 64-n)
713                    0xE1A0_C210, // LSL  R12, R0, R2   (lo << (64-n))
714                    0xE1A0_2211, // LSL  R2, R1, R2    (hi << (64-n))
715                    0xE1A0_1331, // LSR  R1, R1, R3    (hi >> m)
716                    0xE181_C00C, // ORR  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
717                    0xE1A0_1330, // LSR  R1, R0, R3    (lo >> m)
718                    0xE181_1002, // ORR  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
719                    0xE1A0_000C, // MOV  R0, R12       (new_lo into place)
720                ] {
721                    w(&mut b, word);
722                }
723                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
724            }
725
726            // I64Clz: CLZ(hi), or 32 + CLZ(lo) when hi == 0. Conditional
727            // execution replaces the Thumb branches; like the Thumb arm, the
728            // high word of the result pair (rnhi) is cleared last.
729            ArmOp::I64Clz { rd, rnlo, rnhi } => {
730                let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
731                w(&mut b, 0xE350_0000 | (hi << 16)); //              CMP   rnhi, #0
732                w(&mut b, 0x116F_0F10 | (rd_b << 12) | hi); //       CLZNE rd, rnhi
733                w(&mut b, 0x016F_0F10 | (rd_b << 12) | lo); //       CLZEQ rd, rnlo
734                w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
735                w(&mut b, 0xE3A0_0000 | (hi << 12)); //              MOV   rnhi, #0
736            }
737
738            // I64Ctz: CLZ(RBIT(lo)), or 32 + CLZ(RBIT(hi)) when lo == 0.
739            // RBIT/CLZ leave the flags intact, so the CMP's Z survives to the
740            // conditional ADD.
741            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
742                let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
743                w(&mut b, 0xE350_0000 | (lo << 16)); //              CMP    rnlo, #0
744                w(&mut b, 0x16FF_0F30 | (rd_b << 12) | lo); //       RBITNE rd, rnlo
745                w(&mut b, 0x06FF_0F30 | (rd_b << 12) | hi); //       RBITEQ rd, rnhi
746                w(&mut b, 0xE16F_0F10 | (rd_b << 12) | rd_b); //     CLZ    rd, rd
747                w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
748                w(&mut b, 0xE3A0_0000 | (hi << 12)); //              MOV    rnhi, #0
749            }
750
751            // I64Const: MOVW/MOVT per half (MOVT elided when the half fits in
752            // 16 bits, mirroring the Thumb-2 arm).
753            ArmOp::I64Const { rdlo, rdhi, value } => {
754                let lo32 = *value as u32;
755                let hi32 = (*value >> 32) as u32;
756                movw(&mut b, reg_to_bits(rdlo), lo32 & 0xFFFF);
757                if lo32 > 0xFFFF {
758                    movt(&mut b, reg_to_bits(rdlo), lo32 >> 16);
759                }
760                movw(&mut b, reg_to_bits(rdhi), hi32 & 0xFFFF);
761                if hi32 > 0xFFFF {
762                    movt(&mut b, reg_to_bits(rdhi), hi32 >> 16);
763                }
764            }
765
766            // I64Ldr / I64Str: two word accesses at [base, #off] / #off+4.
767            // A register offset is materialized into IP once (the #206/#372
768            // hazard: dropping it would read the wrong address).
769            ArmOp::I64Ldr { rdlo, rdhi, addr } | ArmOp::I64Str { rdlo, rdhi, addr } => {
770                let base = if let Some(rm) = addr.offset_reg {
771                    // ADD ip, base, rm
772                    w(
773                        &mut b,
774                        0xE080_0000
775                            | (reg_to_bits(&addr.base) << 16)
776                            | (12 << 12)
777                            | reg_to_bits(&rm),
778                    );
779                    12
780                } else {
781                    reg_to_bits(&addr.base)
782                };
783                if addr.offset < 0 || addr.offset > 0xFFB {
784                    return Err(synth_core::Error::synthesis(format!(
785                        "i64 load/store offset {} out of the A32 imm12 range (0..=4091) — materialize the offset into a register",
786                        addr.offset
787                    )));
788                }
789                let off = addr.offset as u32;
790                let opc: u32 = if matches!(op, ArmOp::I64Ldr { .. }) {
791                    0xE590_0000 // LDR
792                } else {
793                    0xE580_0000 // STR
794                };
795                w(&mut b, opc | (base << 16) | (reg_to_bits(rdlo) << 12) | off);
796                w(
797                    &mut b,
798                    opc | (base << 16) | (reg_to_bits(rdhi) << 12) | (off + 4),
799                );
800            }
801
802            // I64ExtendI32S: rdlo = rn; rdhi = rdlo >> 31 (arithmetic).
803            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
804                if rdlo != rn {
805                    w(
806                        &mut b,
807                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
808                    );
809                }
810                w(
811                    &mut b,
812                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
813                );
814            }
815
816            // I64ExtendI32U: rdlo = rn; rdhi = 0.
817            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
818                if rdlo != rn {
819                    w(
820                        &mut b,
821                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
822                    );
823                }
824                w(&mut b, 0xE3A0_0000 | (reg_to_bits(rdhi) << 12));
825            }
826
827            // I64Extend8S / I64Extend16S: SXTB/SXTH then sign-fill the high word.
828            ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
829                w(
830                    &mut b,
831                    0xE6AF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
832                );
833                w(
834                    &mut b,
835                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
836                );
837            }
838            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
839                w(
840                    &mut b,
841                    0xE6BF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
842                );
843                w(
844                    &mut b,
845                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
846                );
847            }
848            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
849                if rdlo != rnlo {
850                    w(
851                        &mut b,
852                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
853                    );
854                }
855                w(
856                    &mut b,
857                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rnlo),
858                );
859            }
860
861            // I32WrapI64: take the low word. When rd == rnlo this is a genuine
862            // no-op (the one case where a NOP word is the correct encoding).
863            ArmOp::I32WrapI64 { rd, rnlo } => {
864                w(
865                    &mut b,
866                    0xE1A0_0000 | (reg_to_bits(rd) << 12) | reg_to_bits(rnlo),
867                );
868            }
869
870            // I64Add / I64Sub: the classic pair — ADDS lo + ADC hi (SUBS/SBC).
871            // The selector emits these as separate Adds/Adc ops; the fused
872            // variants are verification-constructed, but they encode for real.
873            ArmOp::I64Add {
874                rdlo,
875                rdhi,
876                rnlo,
877                rnhi,
878                rmlo,
879                rmhi,
880            } => {
881                dp_reg(
882                    &mut b,
883                    0xE090_0000, // ADDS
884                    reg_to_bits(rdlo),
885                    reg_to_bits(rnlo),
886                    reg_to_bits(rmlo),
887                );
888                dp_reg(
889                    &mut b,
890                    0xE0A0_0000, // ADC
891                    reg_to_bits(rdhi),
892                    reg_to_bits(rnhi),
893                    reg_to_bits(rmhi),
894                );
895            }
896            ArmOp::I64Sub {
897                rdlo,
898                rdhi,
899                rnlo,
900                rnhi,
901                rmlo,
902                rmhi,
903            } => {
904                dp_reg(
905                    &mut b,
906                    0xE050_0000, // SUBS
907                    reg_to_bits(rdlo),
908                    reg_to_bits(rnlo),
909                    reg_to_bits(rmlo),
910                );
911                dp_reg(
912                    &mut b,
913                    0xE0C0_0000, // SBC
914                    reg_to_bits(rdhi),
915                    reg_to_bits(rnhi),
916                    reg_to_bits(rmhi),
917                );
918            }
919
920            // I64And / I64Or / I64Xor: two independent word ops.
921            ArmOp::I64And {
922                rdlo,
923                rdhi,
924                rnlo,
925                rnhi,
926                rmlo,
927                rmhi,
928            }
929            | ArmOp::I64Or {
930                rdlo,
931                rdhi,
932                rnlo,
933                rnhi,
934                rmlo,
935                rmhi,
936            }
937            | ArmOp::I64Xor {
938                rdlo,
939                rdhi,
940                rnlo,
941                rnhi,
942                rmlo,
943                rmhi,
944            } => {
945                let base = match op {
946                    ArmOp::I64And { .. } => 0xE000_0000, // AND
947                    ArmOp::I64Or { .. } => 0xE180_0000,  // ORR
948                    _ => 0xE020_0000,                    // EOR
949                };
950                dp_reg(
951                    &mut b,
952                    base,
953                    reg_to_bits(rdlo),
954                    reg_to_bits(rnlo),
955                    reg_to_bits(rmlo),
956                );
957                dp_reg(
958                    &mut b,
959                    base,
960                    reg_to_bits(rdhi),
961                    reg_to_bits(rnhi),
962                    reg_to_bits(rmhi),
963                );
964            }
965
966            // I64DivU: binary long division — A32 transcription of the Thumb-2
967            // #610/#613 arm (fixed-ABI marshal, zero-divisor trap, 64-round
968            // shift-subtract core, quotient to R0:R1, result to rd pair).
969            ArmOp::I64DivU {
970                rdlo,
971                rdhi,
972                rnlo,
973                rnhi,
974                rmlo,
975                rmhi,
976                elide_zero_guard,
977            } => {
978                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
979                // #494 phase 2b: elided only under a certificate-discharged
980                // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
981                if !elide_zero_guard {
982                    emit_a32_i64_divisor_zero_trap(&mut b);
983                }
984                w(&mut b, 0xE92D_00F0); // PUSH {R4-R7}
985                for r in 4..8u32 {
986                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
987                }
988                div_loop(&mut b, 12); // counter in R12 (encoder scratch)
989                w(&mut b, 0xE1A0_0004); // MOV R0, R4 (quotient lo)
990                w(&mut b, 0xE1A0_1005); // MOV R1, R5 (quotient hi)
991                w(&mut b, 0xE8BD_00F0); // POP {R4-R7}
992                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
993            }
994
995            // I64DivS: sign-extract, unsigned core, conditional negate —
996            // A32 transcription of the Thumb-2 arm.
997            ArmOp::I64DivS {
998                rdlo,
999                rdhi,
1000                rnlo,
1001                rnhi,
1002                rmlo,
1003                rmhi,
1004                elide_zero_guard,
1005                elide_overflow_guard,
1006            } => {
1007                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1008                // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
1009                // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
1010                // the #633 overflow guard falls ONLY to
1011                // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
1012                // divisor-nonzero fact alone must keep it.
1013                if !elide_zero_guard {
1014                    emit_a32_i64_divisor_zero_trap(&mut b);
1015                }
1016                if !elide_overflow_guard {
1017                    // #633: INT64_MIN / -1 overflows — trap like the i32 path
1018                    // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
1019                    emit_a32_i64_divs_overflow_trap(&mut b);
1020                }
1021                w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1022                w(&mut b, 0xE021_9003); // EOR R9, R1, R3 (result sign in MSB)
1023                skip_negate_if_positive(&mut b, 1);
1024                negate64(&mut b, 0, 1);
1025                skip_negate_if_positive(&mut b, 3);
1026                negate64(&mut b, 2, 3);
1027                for r in 4..8u32 {
1028                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1029                }
1030                div_loop(&mut b, 8); // counter in R8 (saved above)
1031                w(&mut b, 0xE1A0_0004); // MOV R0, R4
1032                w(&mut b, 0xE1A0_1005); // MOV R1, R5
1033                skip_negate_if_positive(&mut b, 9);
1034                negate64(&mut b, 0, 1);
1035                w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1036                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1037            }
1038
1039            // I64RemU: same core as I64DivU, returns the remainder (R6:R7).
1040            ArmOp::I64RemU {
1041                rdlo,
1042                rdhi,
1043                rnlo,
1044                rnhi,
1045                rmlo,
1046                rmhi,
1047                elide_zero_guard,
1048            } => {
1049                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1050                if !elide_zero_guard {
1051                    emit_a32_i64_divisor_zero_trap(&mut b);
1052                }
1053                w(&mut b, 0xE92D_01F0); // PUSH {R4-R8}
1054                for r in 4..8u32 {
1055                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1056                }
1057                div_loop(&mut b, 8);
1058                w(&mut b, 0xE1A0_0006); // MOV R0, R6 (remainder lo)
1059                w(&mut b, 0xE1A0_1007); // MOV R1, R7 (remainder hi)
1060                w(&mut b, 0xE8BD_01F0); // POP {R4-R8}
1061                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1062            }
1063
1064            // I64RemS: remainder takes the DIVIDEND's sign (WASM semantics).
1065            ArmOp::I64RemS {
1066                rdlo,
1067                rdhi,
1068                rnlo,
1069                rnhi,
1070                rmlo,
1071                rmhi,
1072                elide_zero_guard,
1073            } => {
1074                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1075                if !elide_zero_guard {
1076                    emit_a32_i64_divisor_zero_trap(&mut b);
1077                }
1078                w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1079                w(&mut b, 0xE1A0_9001); // MOV R9, R1 (dividend sign)
1080                skip_negate_if_positive(&mut b, 1);
1081                negate64(&mut b, 0, 1);
1082                skip_negate_if_positive(&mut b, 3);
1083                negate64(&mut b, 2, 3);
1084                for r in 4..8u32 {
1085                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1086                }
1087                div_loop(&mut b, 8);
1088                w(&mut b, 0xE1A0_0006); // MOV R0, R6
1089                w(&mut b, 0xE1A0_1007); // MOV R1, R7
1090                skip_negate_if_positive(&mut b, 9);
1091                negate64(&mut b, 0, 1);
1092                w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1093                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1094            }
1095
1096            // Popcnt (i32): bit-twiddle expansion (no native A32 popcount),
1097            // mirroring the Thumb-2 arm's register contract (R11 + R12 as
1098            // scratch, shift-add fold, final AND #0x3F).
1099            ArmOp::Popcnt { rd, rm } => {
1100                let rd_b = reg_to_bits(rd);
1101                if rd != rm {
1102                    w(&mut b, 0xE1A0_0000 | (rd_b << 12) | reg_to_bits(rm)); // MOV rd, rm
1103                }
1104                // x = x - ((x >> 1) & 0x55555555)
1105                movw(&mut b, 12, 0x5555);
1106                movt(&mut b, 12, 0x5555);
1107                shift_imm(&mut b, LSR, 11, rd_b, 1);
1108                dp_reg(&mut b, 0xE000_0000, 11, 11, 12); // AND R11, R11, R12
1109                dp_reg(&mut b, 0xE040_0000, rd_b, rd_b, 11); // SUB rd, rd, R11
1110                // x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
1111                movw(&mut b, 12, 0x3333);
1112                movt(&mut b, 12, 0x3333);
1113                dp_reg(&mut b, 0xE000_0000, 11, rd_b, 12); // AND R11, rd, R12
1114                shift_imm(&mut b, LSR, rd_b, rd_b, 2);
1115                dp_reg(&mut b, 0xE000_0000, rd_b, rd_b, 12); // AND rd, rd, R12
1116                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11); // ADD rd, rd, R11
1117                // x = (x + (x >> 4)) & 0x0F0F0F0F
1118                shift_imm(&mut b, LSR, 11, rd_b, 4);
1119                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11); // ADD rd, rd, R11
1120                movw(&mut b, 12, 0x0F0F);
1121                movt(&mut b, 12, 0x0F0F);
1122                dp_reg(&mut b, 0xE000_0000, rd_b, rd_b, 12); // AND rd, rd, R12
1123                // x += x >> 8; x += x >> 16; x &= 0x3F
1124                shift_imm(&mut b, LSR, 11, rd_b, 8);
1125                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11);
1126                shift_imm(&mut b, LSR, 11, rd_b, 16);
1127                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11);
1128                w(&mut b, 0xE200_003F | (rd_b << 16) | (rd_b << 12)); // AND rd, rd, #63
1129            }
1130
1131            // I64Popcnt: POPCNT(lo) + POPCNT(hi) — A32 transcription of the
1132            // Thumb-2 arm (R3/R4/R5 saved, mul-based per-word fold, high
1133            // result word rnhi cleared last, mirroring the Thumb contract).
1134            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
1135                let hi = reg_to_bits(rnhi);
1136                w(&mut b, 0xE92D_0038); // PUSH {R3, R4, R5}
1137                // #632 audit: route rnlo through R12 so a pair living at
1138                // (R3,R4) cannot read a clobbered R4 (sources read before any
1139                // scratch register they could occupy is written).
1140                w(&mut b, 0xE1A0_C000 | reg_to_bits(rnlo)); // MOV R12, rnlo
1141                w(&mut b, 0xE1A0_5000 | hi); //                MOV R5, rnhi
1142                w(&mut b, 0xE1A0_400C); //                     MOV R4, R12
1143                popcnt_word(&mut b, 4, 3);
1144                popcnt_word(&mut b, 5, 3);
1145                // #632: carry the count across the scratch restore in R12 —
1146                // rd is allocator-assigned and can land inside {R3,R4,R5};
1147                // the old `ADD rd, R4, R5` before the POP was destroyed by
1148                // the restore. R12 is never allocatable and never restored.
1149                dp_reg(&mut b, 0xE080_0000, 12, 4, 5); // ADD R12, R4, R5
1150                w(&mut b, 0xE8BD_0038); // POP {R3, R4, R5}
1151                w(&mut b, 0xE1A0_0000 | (reg_to_bits(rd) << 12) | 12); // MOV rd, R12
1152                w(&mut b, 0xE3A0_0000 | (hi << 12)); // MOV rnhi, #0 (i64 hi word)
1153            }
1154
1155            _ => return Ok(None),
1156        }
1157        Ok(Some(b))
1158    }
1159
1160    fn encode_arm(&self, op: &ArmOp) -> Result<Vec<u8>> {
1161        // #615: A32 multi-instruction expansions (i64 arithmetic/shift/rotate/
1162        // compare, SetCond/SelectMove, popcnt, ...). These ops were literal
1163        // NOPs on the A32 path — user-reachable via `--target cortex-r5` —
1164        // so the value silently vanished. Mirror of the #594 CallIndirect
1165        // early-return: if the expansion helper covers the op, its bytes are
1166        // the encoding.
1167        if let Some(bytes) = self.encode_arm_expanded(op)? {
1168            return Ok(bytes);
1169        }
1170        // #206: ARM32 register-offset loads/stores. `encode_mem_addr` only
1171        // returns the 12-bit immediate, so the immediate-form arms below
1172        // silently DROP `addr.offset_reg` — a runtime address index vanished,
1173        // turning `ldr rd,[rn,rm,#off]` into `ldr rd,[rn,#off]` (the access went
1174        // to the wrong address). Compute the effective base into IP and re-encode
1175        // against `[ip, #off]`, which is uniform for word/byte/halfword/signed.
1176        if let Some(bytes) = self.encode_arm_reg_offset_mem(op)? {
1177            return Ok(bytes);
1178        }
1179        // #594: call_indirect was encoded as a literal NOP on the A32 path
1180        // (`--target cortex-r5`) — the call never happened and the function
1181        // silently returned garbage. Emit the same three-instruction expansion
1182        // as the Thumb-2 path (R11 = function-pointer table base, R12 scratch):
1183        //   MOV r12, idx, LSL #2 ; LDR r12, [r11, r12] ; BLX r12
1184        if let ArmOp::CallIndirect {
1185            table_index_reg,
1186            table_size,
1187            ..
1188        } = op
1189        {
1190            return Ok(Self::encode_arm_call_indirect(table_index_reg, *table_size));
1191        }
1192        let instr: u32 = match op {
1193            // Data processing instructions
1194            ArmOp::Add { rd, rn, op2 } => {
1195                let rd_bits = reg_to_bits(rd);
1196                let rn_bits = reg_to_bits(rn);
1197                let (op2_bits, i_flag) = encode_operand2(op2)?;
1198
1199                // ADD encoding: cond(4) | 00 | I(1) | 0100 | S(1) | Rn(4) | Rd(4) | operand2(12)
1200                0xE0800000 // condition=always(E), opcode=ADD(0100), S=0
1201                    | (i_flag << 25)
1202                    | (rn_bits << 16)
1203                    | (rd_bits << 12)
1204                    | op2_bits
1205            }
1206
1207            ArmOp::Sub { rd, rn, op2 } => {
1208                let rd_bits = reg_to_bits(rd);
1209                let rn_bits = reg_to_bits(rn);
1210                let (op2_bits, i_flag) = encode_operand2(op2)?;
1211
1212                // SUB encoding: opcode=0010
1213                0xE0400000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1214            }
1215
1216            // i64 support: ADDS, ADC, SUBS, SBC for ARM32
1217            ArmOp::Adds { rd, rn, op2 } => {
1218                let rd_bits = reg_to_bits(rd);
1219                let rn_bits = reg_to_bits(rn);
1220                let (op2_bits, i_flag) = encode_operand2(op2)?;
1221
1222                // ADDS encoding: opcode=0100, S=1
1223                0xE0900000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1224            }
1225
1226            ArmOp::Adc { rd, rn, op2 } => {
1227                let rd_bits = reg_to_bits(rd);
1228                let rn_bits = reg_to_bits(rn);
1229                let (op2_bits, i_flag) = encode_operand2(op2)?;
1230
1231                // ADC encoding: opcode=0101
1232                0xE0A00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1233            }
1234
1235            ArmOp::Subs { rd, rn, op2 } => {
1236                let rd_bits = reg_to_bits(rd);
1237                let rn_bits = reg_to_bits(rn);
1238                let (op2_bits, i_flag) = encode_operand2(op2)?;
1239
1240                // SUBS encoding: opcode=0010, S=1
1241                0xE0500000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1242            }
1243
1244            ArmOp::Sbc { rd, rn, op2 } => {
1245                let rd_bits = reg_to_bits(rd);
1246                let rn_bits = reg_to_bits(rn);
1247                let (op2_bits, i_flag) = encode_operand2(op2)?;
1248
1249                // SBC encoding: opcode=0110
1250                0xE0C00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1251            }
1252
1253            ArmOp::Mul { rd, rn, rm } => {
1254                let rd_bits = reg_to_bits(rd);
1255                let rn_bits = reg_to_bits(rn);
1256                let rm_bits = reg_to_bits(rm);
1257
1258                // MUL encoding: cond(4) | 000000 | A(1) | S(1) | Rd(4) | Rn(4) | Rs(4) | 1001 | Rm(4)
1259                0xE0000090 | (rd_bits << 16) | (rn_bits << 8) | rm_bits
1260            }
1261
1262            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
1263                let rdlo_bits = reg_to_bits(rdlo);
1264                let rdhi_bits = reg_to_bits(rdhi);
1265                let rn_bits = reg_to_bits(rn);
1266                let rm_bits = reg_to_bits(rm);
1267
1268                // UMULL encoding: cond(4) | 0000 1000 | RdHi(4) | RdLo(4) | Rm(4) | 1001 | Rn(4)
1269                0xE0800090 | (rdhi_bits << 16) | (rdlo_bits << 12) | (rm_bits << 8) | rn_bits
1270            }
1271
1272            ArmOp::Sdiv { rd, rn, rm } => {
1273                let rd_bits = reg_to_bits(rd);
1274                let rn_bits = reg_to_bits(rn);
1275                let rm_bits = reg_to_bits(rm);
1276
1277                // SDIV encoding: cond(4) | 01110001 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1278                // ARMv7-M and above
1279                0xE710F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1280            }
1281
1282            ArmOp::Udiv { rd, rn, rm } => {
1283                let rd_bits = reg_to_bits(rd);
1284                let rn_bits = reg_to_bits(rn);
1285                let rm_bits = reg_to_bits(rm);
1286
1287                // UDIV encoding: cond(4) | 01110011 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1288                // ARMv7-M and above
1289                0xE730F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1290            }
1291
1292            ArmOp::Mls { rd, rn, rm, ra } => {
1293                let rd_bits = reg_to_bits(rd);
1294                let rn_bits = reg_to_bits(rn);
1295                let rm_bits = reg_to_bits(rm);
1296                let ra_bits = reg_to_bits(ra);
1297
1298                // MLS encoding: cond(4) | 00000110 | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1299                // Rd = Ra - (Rn * Rm)
1300                0xE0600090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1301            }
1302
1303            ArmOp::Mla { rd, rn, rm, ra } => {
1304                let rd_bits = reg_to_bits(rd);
1305                let rn_bits = reg_to_bits(rn);
1306                let rm_bits = reg_to_bits(rm);
1307                let ra_bits = reg_to_bits(ra);
1308
1309                // MLA encoding: cond(4) | 0000001 S | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1310                // Rd = Ra + (Rn * Rm). Base 0xE0200090 (S=0).
1311                0xE0200090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1312            }
1313
1314            ArmOp::And { rd, rn, op2 } => {
1315                let rd_bits = reg_to_bits(rd);
1316                let rn_bits = reg_to_bits(rn);
1317                let (op2_bits, i_flag) = encode_operand2(op2)?;
1318
1319                // AND encoding: opcode=0000
1320                0xE0000000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1321            }
1322
1323            ArmOp::Orr { rd, rn, op2 } => {
1324                let rd_bits = reg_to_bits(rd);
1325                let rn_bits = reg_to_bits(rn);
1326                let (op2_bits, i_flag) = encode_operand2(op2)?;
1327
1328                // ORR encoding: opcode=1100
1329                0xE1800000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1330            }
1331
1332            ArmOp::Eor { rd, rn, op2 } => {
1333                let rd_bits = reg_to_bits(rd);
1334                let rn_bits = reg_to_bits(rn);
1335                let (op2_bits, i_flag) = encode_operand2(op2)?;
1336
1337                // EOR encoding: opcode=0001
1338                0xE0200000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1339            }
1340
1341            // Shift instructions
1342            ArmOp::Lsl { rd, rn, shift } => {
1343                let rd_bits = reg_to_bits(rd);
1344                let rn_bits = reg_to_bits(rn);
1345                let shift_bits = *shift & 0x1F;
1346
1347                // LSL encoding: MOV with shift
1348                0xE1A00000 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1349            }
1350
1351            ArmOp::Lsr { rd, rn, shift } => {
1352                let rd_bits = reg_to_bits(rd);
1353                let rn_bits = reg_to_bits(rn);
1354                let shift_bits = *shift & 0x1F;
1355
1356                // LSR encoding
1357                0xE1A00020 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1358            }
1359
1360            ArmOp::Asr { rd, rn, shift } => {
1361                let rd_bits = reg_to_bits(rd);
1362                let rn_bits = reg_to_bits(rn);
1363                let shift_bits = *shift & 0x1F;
1364
1365                // ASR encoding
1366                0xE1A00040 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1367            }
1368
1369            ArmOp::Ror { rd, rn, shift } => {
1370                let rd_bits = reg_to_bits(rd);
1371                let rn_bits = reg_to_bits(rn);
1372                let shift_bits = *shift & 0x1F;
1373
1374                // ROR encoding: MOV with ROR shift
1375                0xE1A00060 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1376            }
1377
1378            // Register-based shifts (ARM32)
1379            // LSL Rd, Rn, Rm: cond 0001101S 0000 Rd Rs 0001 Rn
1380            ArmOp::LslReg { rd, rn, rm } => {
1381                let rd_bits = reg_to_bits(rd);
1382                let rn_bits = reg_to_bits(rn);
1383                let rm_bits = reg_to_bits(rm);
1384                0xE1A00010 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1385            }
1386            ArmOp::LsrReg { rd, rn, rm } => {
1387                let rd_bits = reg_to_bits(rd);
1388                let rn_bits = reg_to_bits(rn);
1389                let rm_bits = reg_to_bits(rm);
1390                0xE1A00030 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1391            }
1392            ArmOp::AsrReg { rd, rn, rm } => {
1393                let rd_bits = reg_to_bits(rd);
1394                let rn_bits = reg_to_bits(rn);
1395                let rm_bits = reg_to_bits(rm);
1396                0xE1A00050 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1397            }
1398            ArmOp::RorReg { rd, rn, rm } => {
1399                let rd_bits = reg_to_bits(rd);
1400                let rn_bits = reg_to_bits(rn);
1401                let rm_bits = reg_to_bits(rm);
1402                0xE1A00070 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1403            }
1404
1405            // RSB (Reverse Subtract): Rd = imm - Rn
1406            ArmOp::Rsb { rd, rn, imm } => {
1407                let rd_bits = reg_to_bits(rd);
1408                let rn_bits = reg_to_bits(rn);
1409                // RSB encoding: cond(4) | 00 1 0011 S | Rn(4) | Rd(4) | imm12
1410                // Opcode for RSB = 0011, I=1 (immediate), S=0
1411                0xE2600000 | (rn_bits << 16) | (rd_bits << 12) | (*imm & 0xFF)
1412            }
1413
1414            // Bit manipulation instructions
1415            ArmOp::Clz { rd, rm } => {
1416                let rd_bits = reg_to_bits(rd);
1417                let rm_bits = reg_to_bits(rm);
1418
1419                // CLZ encoding: cond(4) | 00010110 | 1111 | Rd(4) | 1111 | 0001 | Rm(4)
1420                // ARMv5T and above
1421                0xE16F0F10 | (rd_bits << 12) | rm_bits
1422            }
1423
1424            ArmOp::Rbit { rd, rm } => {
1425                let rd_bits = reg_to_bits(rd);
1426                let rm_bits = reg_to_bits(rm);
1427
1428                // RBIT encoding: cond(4) | 01101111 | 1111 | Rd(4) | 1111 | 0011 | Rm(4)
1429                // ARMv6T2 and above
1430                0xE6FF0F30 | (rd_bits << 12) | rm_bits
1431            }
1432
1433            ArmOp::Sxtb { rd, rm } => {
1434                let rd_bits = reg_to_bits(rd);
1435                let rm_bits = reg_to_bits(rm);
1436
1437                // SXTB encoding: cond(4) | 01101010 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1438                // ARMv6 and above. rotate=00 for no rotation
1439                0xE6AF0070 | (rd_bits << 12) | rm_bits
1440            }
1441
1442            ArmOp::Sxth { rd, rm } => {
1443                let rd_bits = reg_to_bits(rd);
1444                let rm_bits = reg_to_bits(rm);
1445
1446                // SXTH encoding: cond(4) | 01101011 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1447                // ARMv6 and above. rotate=00 for no rotation
1448                0xE6BF0070 | (rd_bits << 12) | rm_bits
1449            }
1450
1451            ArmOp::Uxtb { rd, rm } => {
1452                let rd_bits = reg_to_bits(rd);
1453                let rm_bits = reg_to_bits(rm);
1454                // UXTB encoding: cond | 01101110 1111 Rd rotate 00 0111 Rm (rotate=00)
1455                0xE6EF0070 | (rd_bits << 12) | rm_bits
1456            }
1457
1458            ArmOp::Uxth { rd, rm } => {
1459                let rd_bits = reg_to_bits(rd);
1460                let rm_bits = reg_to_bits(rm);
1461                // UXTH encoding: cond | 01101111 1111 Rd rotate 00 0111 Rm (rotate=00)
1462                0xE6FF0070 | (rd_bits << 12) | rm_bits
1463            }
1464
1465            // Move instructions
1466            ArmOp::Mov { rd, op2 } => {
1467                let rd_bits = reg_to_bits(rd);
1468                let (op2_bits, i_flag) = encode_operand2(op2)?;
1469
1470                // MOV encoding: opcode=1101
1471                0xE1A00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1472            }
1473
1474            ArmOp::Mvn { rd, op2 } => {
1475                let rd_bits = reg_to_bits(rd);
1476                let (op2_bits, i_flag) = encode_operand2(op2)?;
1477
1478                // MVN encoding: opcode=1111
1479                0xE1E00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1480            }
1481
1482            // MOVW - Move Wide (ARM32)
1483            // Encoding: cond(4) | 0011 0000 | imm4(4) | Rd(4) | imm12(12)
1484            ArmOp::Movw { rd, imm16 } => {
1485                let rd_bits = reg_to_bits(rd);
1486                let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1487                let imm12 = (*imm16 as u32) & 0xFFF;
1488                0xE3000000 | (imm4 << 16) | (rd_bits << 12) | imm12
1489            }
1490
1491            // MOVT - Move Top (ARM32)
1492            // Encoding: cond(4) | 0011 0100 | imm4(4) | Rd(4) | imm12(12)
1493            ArmOp::Movt { rd, imm16 } => {
1494                let rd_bits = reg_to_bits(rd);
1495                let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1496                let imm12 = (*imm16 as u32) & 0xFFF;
1497                0xE3400000 | (imm4 << 16) | (rd_bits << 12) | imm12
1498            }
1499
1500            // #237: symbol-relative MOVW/MOVT (ARM mode) — addend in place, the
1501            // backend records the MOVW_ABS/MOVT_ABS relocation against `symbol`.
1502            ArmOp::MovwSym { rd, addend, .. } => {
1503                let rd_bits = reg_to_bits(rd);
1504                let v = (*addend as u32) & 0xffff;
1505                0xE3000000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1506            }
1507            ArmOp::MovtSym { rd, addend, .. } => {
1508                let rd_bits = reg_to_bits(rd);
1509                let v = ((*addend as u32) >> 16) & 0xffff;
1510                0xE3400000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1511            }
1512
1513            // #345: LdrSym is the Thumb-2 literal-pool address load. A32 mode is
1514            // not used for relocatable native-pointer objects; fail loudly rather
1515            // than miscompile if it is ever reached here.
1516            ArmOp::LdrSym { .. } => {
1517                return Err(synth_core::Error::synthesis(
1518                    "LdrSym (literal-pool address load) is Thumb-2-only",
1519                ));
1520            }
1521
1522            // Compare
1523            ArmOp::Cmp { rn, op2 } => {
1524                let rn_bits = reg_to_bits(rn);
1525                let (op2_bits, i_flag) = encode_operand2(op2)?;
1526
1527                // CMP encoding: opcode=1010, S=1
1528                0xE1500000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1529            }
1530
1531            // Compare Negative (CMN) - computes Rn + op2 and sets flags
1532            ArmOp::Cmn { rn, op2 } => {
1533                let rn_bits = reg_to_bits(rn);
1534                let (op2_bits, i_flag) = encode_operand2(op2)?;
1535
1536                // CMN encoding: opcode=1011, S=1
1537                0xE1700000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1538            }
1539
1540            // Load/Store
1541            ArmOp::Ldr { rd, addr } => {
1542                let rd_bits = reg_to_bits(rd);
1543                let (base_bits, offset_bits) = encode_mem_addr(addr);
1544
1545                // LDR encoding: cond(4) | 01 | I(1) | P(1) | U(1) | B(1) | W(1) | L(1) | Rn(4) | Rd(4) | offset(12)
1546                // P=1 (pre-indexed), U=1 (add offset), L=1 (load)
1547                0xE5900000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1548            }
1549
1550            ArmOp::Str { rd, addr } => {
1551                let rd_bits = reg_to_bits(rd);
1552                let (base_bits, offset_bits) = encode_mem_addr(addr);
1553
1554                // STR encoding: L=0 (store)
1555                0xE5800000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1556            }
1557
1558            // Sub-word loads (ARM32 encoding)
1559            ArmOp::Ldrb { rd, addr } => {
1560                let rd_bits = reg_to_bits(rd);
1561                let (base_bits, offset_bits) = encode_mem_addr(addr);
1562                // LDRB: LDR with B=1 (byte): cond|01|I|P|U|1|W|L|Rn|Rd|offset
1563                0xE5D00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1564            }
1565
1566            ArmOp::Ldrsb { rd, addr } => {
1567                let rd_bits = reg_to_bits(rd);
1568                let (base_bits, offset_bits) = encode_mem_addr(addr);
1569                // LDRSB (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1101|imm4L
1570                // Simplified with immediate offset
1571                let offset_val = offset_bits & 0xFF;
1572                let imm4h = (offset_val >> 4) & 0xF;
1573                let imm4l = offset_val & 0xF;
1574                0xE1D000D0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1575            }
1576
1577            ArmOp::Ldrh { rd, addr } => {
1578                let rd_bits = reg_to_bits(rd);
1579                let (base_bits, offset_bits) = encode_mem_addr(addr);
1580                // LDRH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1011|imm4L
1581                let offset_val = offset_bits & 0xFF;
1582                let imm4h = (offset_val >> 4) & 0xF;
1583                let imm4l = offset_val & 0xF;
1584                0xE1D000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1585            }
1586
1587            ArmOp::Ldrsh { rd, addr } => {
1588                let rd_bits = reg_to_bits(rd);
1589                let (base_bits, offset_bits) = encode_mem_addr(addr);
1590                // LDRSH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1111|imm4L
1591                let offset_val = offset_bits & 0xFF;
1592                let imm4h = (offset_val >> 4) & 0xF;
1593                let imm4l = offset_val & 0xF;
1594                0xE1D000F0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1595            }
1596
1597            // Sub-word stores (ARM32 encoding)
1598            ArmOp::Strb { rd, addr } => {
1599                let rd_bits = reg_to_bits(rd);
1600                let (base_bits, offset_bits) = encode_mem_addr(addr);
1601                // STRB: STR with B=1 (byte): cond|01|I|P|U|1|W|0|Rn|Rd|offset
1602                0xE5C00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1603            }
1604
1605            ArmOp::Strh { rd, addr } => {
1606                let rd_bits = reg_to_bits(rd);
1607                let (base_bits, offset_bits) = encode_mem_addr(addr);
1608                // STRH (misc store): cond|000|P|U|1|W|0|Rn|Rd|imm4H|1011|imm4L
1609                let offset_val = offset_bits & 0xFF;
1610                let imm4h = (offset_val >> 4) & 0xF;
1611                let imm4l = offset_val & 0xF;
1612                0xE1C000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1613            }
1614
1615            // Memory management (ARM32 encoding)
1616            ArmOp::MemorySize { rd } => {
1617                let rd_bits = reg_to_bits(rd);
1618                // MOV rd, R10, LSR #16  (memory size in bytes / 65536 = pages)
1619                // cond|000|1101|S|0000|Rd|shift5|type|0|Rm
1620                // LSR #16: shift5=10000, type=01
1621                0xE1A00820 | (rd_bits << 12) | 0x0A // Rm=R10, shift=16, LSR
1622            }
1623
1624            ArmOp::MemoryGrow { rd, .. } => {
1625                let rd_bits = reg_to_bits(rd);
1626                // On embedded, always fail: MOV rd, #-1
1627                0xE3E00000 | (rd_bits << 12) // MVN rd, #0 = MOV rd, #-1
1628            }
1629
1630            // Label pseudo-instruction: emits no machine code
1631            ArmOp::Label { .. } => {
1632                return Ok(Vec::new());
1633            }
1634
1635            // Branch instructions
1636            ArmOp::B { label: _ } => {
1637                // B encoding: cond(4) | 1010 | offset(24)
1638                // Simplified: branch to offset 0 (will be patched by linker/resolver)
1639                0xEA000000
1640            }
1641
1642            // Conditional branch to label (generic)
1643            ArmOp::Bcc { cond, label: _ } => {
1644                use synth_synthesis::Condition;
1645                let cond_bits: u32 = match cond {
1646                    Condition::EQ => 0x0,
1647                    Condition::NE => 0x1,
1648                    Condition::HS => 0x2,
1649                    Condition::LO => 0x3,
1650                    Condition::HI => 0x8,
1651                    Condition::LS => 0x9,
1652                    Condition::GE => 0xA,
1653                    Condition::LT => 0xB,
1654                    Condition::GT => 0xC,
1655                    Condition::LE => 0xD,
1656                };
1657                // B<cond> with offset 0 (will be patched)
1658                (cond_bits << 28) | 0x0A000000
1659            }
1660
1661            // BHS (Branch if Higher or Same) - used for bounds checking
1662            ArmOp::Bhs { label: _ } => {
1663                // BHS encoding: cond(2=HS) | 1010 | offset(24)
1664                0x2A000000 // BHS with offset 0
1665            }
1666
1667            // BLO (Branch if Lower) - complementary to BHS
1668            ArmOp::Blo { label: _ } => {
1669                // BLO encoding: cond(3=LO) | 1010 | offset(24)
1670                0x3A000000 // BLO with offset 0
1671            }
1672
1673            // Branch with numeric offset (in instructions)
1674            // ARM32 B instruction: offset is in instructions, stored as words
1675            // The offset is relative to PC+8 (due to ARM pipeline)
1676            ArmOp::BOffset { offset } => {
1677                // B encoding: cond(4) | 1010 | offset(24)
1678                // Offset is signed, in words (4-byte units)
1679                // ARM adds PC+8 to the offset, so we need to adjust:
1680                // target = PC + 8 + (offset * 4)
1681                // For backward branch of N instructions: offset = -(N + 2)
1682                // wrapping_sub keeps the encoder total under fuzzing (#186): an
1683                // extreme i32::MIN offset would otherwise overflow-panic; for any
1684                // real branch offset this is identical to `- 2`.
1685                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1686                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1687                0xEA000000 | offset_bits
1688            }
1689
1690            // Conditional branch with numeric offset
1691            ArmOp::BCondOffset { cond, offset } => {
1692                use synth_synthesis::Condition;
1693                let cond_bits: u32 = match cond {
1694                    Condition::EQ => 0x0,
1695                    Condition::NE => 0x1,
1696                    Condition::HS => 0x2,
1697                    Condition::LO => 0x3,
1698                    Condition::HI => 0x8,
1699                    Condition::LS => 0x9,
1700                    Condition::GE => 0xA,
1701                    Condition::LT => 0xB,
1702                    Condition::GT => 0xC,
1703                    Condition::LE => 0xD,
1704                };
1705                // B<cond> encoding: cond(4) | 1010 | offset(24)
1706                // wrapping_sub: total under fuzzing (#186), identical for real offsets.
1707                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1708                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1709                (cond_bits << 28) | 0x0A000000 | offset_bits
1710            }
1711
1712            ArmOp::Bl { label: _ } => {
1713                // BL encoding: cond(4) | 1011 | offset(24)
1714                0xEB000000
1715            }
1716
1717            ArmOp::Bx { rm } => {
1718                let rm_bits = reg_to_bits(rm);
1719
1720                // BX encoding: cond(4) | 000100101111111111110001 | Rm(4)
1721                0xE12FFF10 | rm_bits
1722            }
1723
1724            ArmOp::Blx { rm } => {
1725                let rm_bits = reg_to_bits(rm);
1726
1727                // BLX (register) encoding: cond(4) | 000100101111111111110011 | Rm(4)
1728                0xE12FFF30 | rm_bits
1729            }
1730
1731            ArmOp::Push { regs } => {
1732                // STMDB SP!, {regs} encoding: cond(4) | 100100 | 10 | 1101 | register_list(16)
1733                let mut reg_list: u32 = 0;
1734                for r in regs {
1735                    reg_list |= 1 << reg_to_bits(r);
1736                }
1737                0xE92D0000 | reg_list
1738            }
1739
1740            ArmOp::Pop { regs } => {
1741                // LDMIA SP!, {regs} encoding: cond(4) | 100010 | 11 | 1101 | register_list(16)
1742                let mut reg_list: u32 = 0;
1743                for r in regs {
1744                    reg_list |= 1 << reg_to_bits(r);
1745                }
1746                0xE8BD0000 | reg_list
1747            }
1748
1749            ArmOp::Nop => {
1750                // NOP encoding: MOV R0, R0
1751                0xE1A00000
1752            }
1753
1754            ArmOp::Udf { imm } => {
1755                // UDF (Undefined) encoding in ARM: 0xE7F000F0 | (imm12_hi << 8) | imm4_lo
1756                // We only use imm8, so split into imm4_hi and imm4_lo
1757                let imm8 = *imm as u32;
1758                0xE7F000F0 | ((imm8 & 0xF0) << 4) | (imm8 & 0x0F)
1759            }
1760
1761            // #615: handled by the `encode_arm_expanded` early return at the
1762            // top of this function — a real MOV{cond}/MOV pair now, never a
1763            // silent NOP again.
1764            ArmOp::Popcnt { .. } | ArmOp::SetCond { .. } | ArmOp::SelectMove { .. } => {
1765                unreachable!("handled by encode_arm_expanded (#615)")
1766            }
1767
1768            // Verification-only pseudo-ops: `synth-verify`'s ArmSemantics
1769            // models these, but NO codegen path constructs them (the selector
1770            // lowers select/locals/globals/br_table/call to real instruction
1771            // sequences before the encoder). Encoding one as a NOP silently
1772            // dropped the operation (#615 class); a typed Err keeps the
1773            // encoder total (Ok-or-Err, the `encoder_no_panic` contract)
1774            // while making any future reachability LOUD.
1775            ArmOp::Select { .. }
1776            | ArmOp::LocalGet { .. }
1777            | ArmOp::LocalSet { .. }
1778            | ArmOp::LocalTee { .. }
1779            | ArmOp::GlobalGet { .. }
1780            | ArmOp::GlobalSet { .. }
1781            | ArmOp::BrTable { .. }
1782            | ArmOp::Call { .. } => {
1783                return Err(synth_core::Error::synthesis(format!(
1784                    "verification-only pseudo-op {op:?} reached the A32 encoder — \
1785                     codegen lowers it before encoding; refusing to emit a silent NOP (#615)"
1786                )));
1787            }
1788
1789            // #594: CallIndirect is expanded to a real multi-instruction
1790            // sequence by the early return at the top of this function —
1791            // it must NEVER fall through to a silent NOP again.
1792            ArmOp::CallIndirect { .. } => {
1793                unreachable!("CallIndirect handled by encode_arm_call_indirect (#594)")
1794            }
1795
1796            // #615: every i64 op (and I32WrapI64) is expanded to a real A32
1797            // multi-instruction sequence by `encode_arm_expanded` — the
1798            // "encode as NOP for now" era ended with the value silently
1799            // vanishing on `--target cortex-r5`.
1800            ArmOp::I64Add { .. }
1801            | ArmOp::I64Sub { .. }
1802            | ArmOp::I64DivS { .. }
1803            | ArmOp::I64DivU { .. }
1804            | ArmOp::I64RemS { .. }
1805            | ArmOp::I64RemU { .. }
1806            | ArmOp::I64Clz { .. }
1807            | ArmOp::I64Ctz { .. }
1808            | ArmOp::I64Popcnt { .. }
1809            | ArmOp::I64And { .. }
1810            | ArmOp::I64Or { .. }
1811            | ArmOp::I64Xor { .. }
1812            | ArmOp::I64Eqz { .. }
1813            | ArmOp::I64Eq { .. }
1814            | ArmOp::I64Ne { .. }
1815            | ArmOp::I64LtS { .. }
1816            | ArmOp::I64LtU { .. }
1817            | ArmOp::I64LeS { .. }
1818            | ArmOp::I64LeU { .. }
1819            | ArmOp::I64GtS { .. }
1820            | ArmOp::I64GtU { .. }
1821            | ArmOp::I64GeS { .. }
1822            | ArmOp::I64GeU { .. }
1823            | ArmOp::I64Const { .. }
1824            | ArmOp::I64Ldr { .. }
1825            | ArmOp::I64Str { .. }
1826            | ArmOp::I64ExtendI32S { .. }
1827            | ArmOp::I64ExtendI32U { .. }
1828            | ArmOp::I64Extend8S { .. }
1829            | ArmOp::I64Extend16S { .. }
1830            | ArmOp::I64Extend32S { .. }
1831            | ArmOp::I32WrapI64 { .. } => {
1832                unreachable!("handled by encode_arm_expanded (#615)")
1833            }
1834
1835            // f32 VFP single-precision instructions
1836            ArmOp::F32Add { sd, sn, sm } => encode_vfp_3reg(0xEE300A00, sd, sn, sm)?,
1837            ArmOp::F32Sub { sd, sn, sm } => encode_vfp_3reg(0xEE300A40, sd, sn, sm)?,
1838            ArmOp::F32Mul { sd, sn, sm } => encode_vfp_3reg(0xEE200A00, sd, sn, sm)?,
1839            ArmOp::F32Div { sd, sn, sm } => encode_vfp_3reg(0xEE800A00, sd, sn, sm)?,
1840            ArmOp::F32Abs { sd, sm } => encode_vfp_2reg(0xEEB00AC0, sd, sm)?,
1841            ArmOp::F32Neg { sd, sm } => encode_vfp_2reg(0xEEB10A40, sd, sm)?,
1842            ArmOp::F32Sqrt { sd, sm } => encode_vfp_2reg(0xEEB10AC0, sd, sm)?,
1843
1844            // f32 pseudo-ops — multi-instruction sequences
1845            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
1846            ArmOp::F32Ceil { sd, sm } => {
1847                return self.encode_arm_f32_rounding(sd, sm, 0b01); // Round toward +Inf
1848            }
1849            ArmOp::F32Floor { sd, sm } => {
1850                return self.encode_arm_f32_rounding(sd, sm, 0b10); // Round toward -Inf
1851            }
1852            ArmOp::F32Trunc { sd, sm } => {
1853                return self.encode_arm_f32_rounding(sd, sm, 0b11); // VCVT toward zero
1854            }
1855            ArmOp::F32Nearest { sd, sm } => {
1856                return self.encode_arm_f32_rounding(sd, sm, 0b00); // VCVT to nearest
1857            }
1858            ArmOp::F32Min { sd, sn, sm } => {
1859                return self.encode_arm_f32_minmax(sd, sn, sm, true);
1860            }
1861            ArmOp::F32Max { sd, sn, sm } => {
1862                return self.encode_arm_f32_minmax(sd, sn, sm, false);
1863            }
1864            ArmOp::F32Copysign { sd, sn, sm } => {
1865                return self.encode_arm_f32_copysign(sd, sn, sm);
1866            }
1867
1868            // f32 comparisons — multi-instruction: VCMP + VMRS + conditional MOV
1869            ArmOp::F32Eq { rd, sn, sm } => {
1870                return self.encode_arm_f32_compare(rd, sn, sm, 0x0); // EQ
1871            }
1872            ArmOp::F32Ne { rd, sn, sm } => {
1873                return self.encode_arm_f32_compare(rd, sn, sm, 0x1); // NE
1874            }
1875            ArmOp::F32Lt { rd, sn, sm } => {
1876                return self.encode_arm_f32_compare(rd, sn, sm, 0x4); // MI (less than)
1877            }
1878            ArmOp::F32Le { rd, sn, sm } => {
1879                return self.encode_arm_f32_compare(rd, sn, sm, 0x9); // LS (less or same)
1880            }
1881            ArmOp::F32Gt { rd, sn, sm } => {
1882                return self.encode_arm_f32_compare(rd, sn, sm, 0xC); // GT
1883            }
1884            ArmOp::F32Ge { rd, sn, sm } => {
1885                return self.encode_arm_f32_compare(rd, sn, sm, 0xA); // GE
1886            }
1887
1888            // f32 const — multi-instruction: MOVW + MOVT + VMOV
1889            ArmOp::F32Const { sd, value } => {
1890                return self.encode_arm_f32_const(sd, *value);
1891            }
1892
1893            ArmOp::F32Load { sd, addr } => encode_vfp_ldst(0xED900A00, sd, addr)?,
1894            ArmOp::F32Store { sd, addr } => encode_vfp_ldst(0xED800A00, sd, addr)?,
1895
1896            // f32 conversions — multi-instruction sequences
1897            ArmOp::F32ConvertI32S { sd, rm } => {
1898                return self.encode_arm_f32_convert_i32(sd, rm, true);
1899            }
1900            ArmOp::F32ConvertI32U { sd, rm } => {
1901                return self.encode_arm_f32_convert_i32(sd, rm, false);
1902            }
1903            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
1904                return Err(synth_core::Error::synthesis(
1905                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
1906                ));
1907            }
1908            ArmOp::F32ReinterpretI32 { sd, rm } => encode_vmov_core_sreg(true, sd, rm)?,
1909            ArmOp::I32ReinterpretF32 { rd, sm } => encode_vmov_core_sreg(false, sm, rd)?,
1910            ArmOp::I32TruncF32S { rd, sm } => {
1911                return self.encode_arm_i32_trunc_f32(rd, sm, true);
1912            }
1913            ArmOp::I32TruncF32U { rd, sm } => {
1914                return self.encode_arm_i32_trunc_f32(rd, sm, false);
1915            }
1916
1917            // f64 VFP double-precision instructions (ARM32)
1918            // F64 arithmetic: same as F32 but with sz=1 (bit 8 = 1, cp11 = 0xB)
1919            ArmOp::F64Add { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B00, dd, dn, dm)?,
1920            ArmOp::F64Sub { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B40, dd, dn, dm)?,
1921            ArmOp::F64Mul { dd, dn, dm } => encode_vfp_3reg_f64(0xEE200B00, dd, dn, dm)?,
1922            ArmOp::F64Div { dd, dn, dm } => encode_vfp_3reg_f64(0xEE800B00, dd, dn, dm)?,
1923            ArmOp::F64Abs { dd, dm } => encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?,
1924            ArmOp::F64Neg { dd, dm } => encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?,
1925            ArmOp::F64Sqrt { dd, dm } => encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?,
1926
1927            // f64 pseudo-ops
1928            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
1929            ArmOp::F64Ceil { dd, dm } => {
1930                return self.encode_arm_f64_rounding(dd, dm, 0b01);
1931            }
1932            ArmOp::F64Floor { dd, dm } => {
1933                return self.encode_arm_f64_rounding(dd, dm, 0b10);
1934            }
1935            ArmOp::F64Trunc { dd, dm } => {
1936                return self.encode_arm_f64_rounding(dd, dm, 0b11);
1937            }
1938            ArmOp::F64Nearest { dd, dm } => {
1939                return self.encode_arm_f64_rounding(dd, dm, 0b00);
1940            }
1941            ArmOp::F64Min { dd, dn, dm } => {
1942                return self.encode_arm_f64_minmax(dd, dn, dm, true);
1943            }
1944            ArmOp::F64Max { dd, dn, dm } => {
1945                return self.encode_arm_f64_minmax(dd, dn, dm, false);
1946            }
1947            ArmOp::F64Copysign { dd, dn, dm } => {
1948                return self.encode_arm_f64_copysign(dd, dn, dm);
1949            }
1950
1951            // f64 comparisons
1952            ArmOp::F64Eq { rd, dn, dm } => {
1953                return self.encode_arm_f64_compare(rd, dn, dm, 0x0);
1954            }
1955            ArmOp::F64Ne { rd, dn, dm } => {
1956                return self.encode_arm_f64_compare(rd, dn, dm, 0x1);
1957            }
1958            ArmOp::F64Lt { rd, dn, dm } => {
1959                return self.encode_arm_f64_compare(rd, dn, dm, 0x4);
1960            }
1961            ArmOp::F64Le { rd, dn, dm } => {
1962                return self.encode_arm_f64_compare(rd, dn, dm, 0x9);
1963            }
1964            ArmOp::F64Gt { rd, dn, dm } => {
1965                return self.encode_arm_f64_compare(rd, dn, dm, 0xC);
1966            }
1967            ArmOp::F64Ge { rd, dn, dm } => {
1968                return self.encode_arm_f64_compare(rd, dn, dm, 0xA);
1969            }
1970
1971            ArmOp::F64Const { dd, value } => {
1972                return self.encode_arm_f64_const(dd, *value);
1973            }
1974
1975            ArmOp::F64Load { dd, addr } => encode_vfp_ldst_f64(0xED900B00, dd, addr)?,
1976            ArmOp::F64Store { dd, addr } => encode_vfp_ldst_f64(0xED800B00, dd, addr)?,
1977
1978            ArmOp::F64ConvertI32S { dd, rm } => {
1979                return self.encode_arm_f64_convert_i32(dd, rm, true);
1980            }
1981            ArmOp::F64ConvertI32U { dd, rm } => {
1982                return self.encode_arm_f64_convert_i32(dd, rm, false);
1983            }
1984            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
1985                return Err(synth_core::Error::synthesis(
1986                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
1987                ));
1988            }
1989            ArmOp::F64PromoteF32 { dd, sm } => {
1990                return self.encode_arm_f64_promote_f32(dd, sm);
1991            }
1992            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => {
1993                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?
1994            }
1995            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => {
1996                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?
1997            }
1998            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
1999                return Err(synth_core::Error::synthesis(
2000                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
2001                ));
2002            }
2003            ArmOp::I32TruncF64S { rd, dm } => {
2004                return self.encode_arm_i32_trunc_f64(rd, dm, true);
2005            }
2006            ArmOp::I32TruncF64U { rd, dm } => {
2007                return self.encode_arm_i32_trunc_f64(rd, dm, false);
2008            }
2009            // #615: multi-instruction i64 sequences — expanded to real A32 by
2010            // `encode_arm_expanded`, no longer "Thumb-2 only" NOPs.
2011            ArmOp::I64SetCond { .. }
2012            | ArmOp::I64SetCondZ { .. }
2013            | ArmOp::I64Mul { .. }
2014            | ArmOp::I64Shl { .. }
2015            | ArmOp::I64ShrS { .. }
2016            | ArmOp::I64ShrU { .. }
2017            | ArmOp::I64Rotl { .. }
2018            | ArmOp::I64Rotr { .. } => {
2019                unreachable!("handled by encode_arm_expanded (#615)")
2020            }
2021
2022            // MVE instructions — Thumb-2 only (Cortex-M55 is always Thumb-2)
2023            ArmOp::MveLoad { .. }
2024            | ArmOp::MveStore { .. }
2025            | ArmOp::MveConst { .. }
2026            | ArmOp::MveAnd { .. }
2027            | ArmOp::MveOrr { .. }
2028            | ArmOp::MveEor { .. }
2029            | ArmOp::MveMvn { .. }
2030            | ArmOp::MveBic { .. }
2031            | ArmOp::MveAddI { .. }
2032            | ArmOp::MveSubI { .. }
2033            | ArmOp::MveMulI { .. }
2034            | ArmOp::MveNegI { .. }
2035            | ArmOp::MveCmpEqI { .. }
2036            | ArmOp::MveCmpNeI { .. }
2037            | ArmOp::MveCmpLtS { .. }
2038            | ArmOp::MveCmpLtU { .. }
2039            | ArmOp::MveCmpGtS { .. }
2040            | ArmOp::MveCmpGtU { .. }
2041            | ArmOp::MveCmpLeS { .. }
2042            | ArmOp::MveCmpLeU { .. }
2043            | ArmOp::MveCmpGeS { .. }
2044            | ArmOp::MveCmpGeU { .. }
2045            | ArmOp::MveDup { .. }
2046            | ArmOp::MveExtractLane { .. }
2047            | ArmOp::MveInsertLane { .. }
2048            | ArmOp::MveAddF32 { .. }
2049            | ArmOp::MveSubF32 { .. }
2050            | ArmOp::MveMulF32 { .. }
2051            | ArmOp::MveNegF32 { .. }
2052            | ArmOp::MveAbsF32 { .. }
2053            | ArmOp::MveCmpEqF32 { .. }
2054            | ArmOp::MveCmpNeF32 { .. }
2055            | ArmOp::MveCmpLtF32 { .. }
2056            | ArmOp::MveCmpLeF32 { .. }
2057            | ArmOp::MveCmpGtF32 { .. }
2058            | ArmOp::MveCmpGeF32 { .. }
2059            | ArmOp::MveDupF32 { .. }
2060            | ArmOp::MveExtractLaneF32 { .. }
2061            | ArmOp::MveReplaceLaneF32 { .. }
2062            | ArmOp::MveDivF32 { .. }
2063            | ArmOp::MveSqrtF32 { .. } => {
2064                // MVE (Helium) is a Thumb-2-only extension (Cortex-M55); there
2065                // is no A32 encoding. The selector only emits MVE ops for
2066                // Thumb targets — a NOP here silently dropped the vector op
2067                // if that invariant ever broke (#615 class). Err keeps the
2068                // encoder total and the failure loud.
2069                return Err(synth_core::Error::synthesis(format!(
2070                    "MVE op {op:?} has no A32 (ARM-mode) encoding — MVE is Thumb-2 only (#615)"
2071                )));
2072            }
2073        };
2074
2075        // ARM32 instructions are little-endian
2076        Ok(instr.to_le_bytes().to_vec())
2077    }
2078
2079    // === ARM32 VFP multi-instruction helpers ===
2080
2081    /// Encode F32 comparison as ARM32: VCMP.F32 + VMRS + MOV rd,#0 + MOVcond rd,#1
2082    fn encode_arm_f32_compare(
2083        &self,
2084        rd: &Reg,
2085        sn: &VfpReg,
2086        sm: &VfpReg,
2087        cond_code: u32,
2088    ) -> Result<Vec<u8>> {
2089        let mut bytes = Vec::new();
2090
2091        // VCMP.F32 Sn, Sm: 0xEEB40A40 with Sn in Vd position, Sm in Vm position
2092        let sn_num = vfp_sreg_to_num(sn)?;
2093        let sm_num = vfp_sreg_to_num(sm)?;
2094        let (vd, d) = encode_sreg(sn_num);
2095        let (vm, m) = encode_sreg(sm_num);
2096        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2097        bytes.extend_from_slice(&vcmp.to_le_bytes());
2098
2099        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
2100        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2101
2102        // MOV rd, #0: 0xE3A0_0000 | (rd << 12)
2103        let rd_bits = reg_to_bits(rd);
2104        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2105        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2106
2107        // MOVcond rd, #1: cond(4) | 0011 1010 0000 rd(4) 0000 0000 0001
2108        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2109        bytes.extend_from_slice(&mov_one.to_le_bytes());
2110
2111        Ok(bytes)
2112    }
2113
2114    /// Encode F32 constant load as ARM32: MOVW Rt,#lo16 + MOVT Rt,#hi16 + VMOV Sd,Rt
2115    fn encode_arm_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
2116        let mut bytes = Vec::new();
2117        let bits = value.to_bits();
2118
2119        // Use R12 as temp register for constant loading
2120        let rt: u32 = 12; // R12/IP
2121
2122        // MOVW R12, #lo16: 0xE300_C000 | (imm4 << 16) | imm12
2123        let lo16 = bits & 0xFFFF;
2124        let movw = 0xE3000000 | (rt << 12) | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2125        bytes.extend_from_slice(&movw.to_le_bytes());
2126
2127        // MOVT R12, #hi16: 0xE340_C000 | (imm4 << 16) | imm12
2128        let hi16 = (bits >> 16) & 0xFFFF;
2129        let movt = 0xE3400000 | (rt << 12) | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2130        bytes.extend_from_slice(&movt.to_le_bytes());
2131
2132        // VMOV Sd, R12
2133        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
2134        bytes.extend_from_slice(&vmov.to_le_bytes());
2135
2136        Ok(bytes)
2137    }
2138
2139    /// Encode VMOV + VCVT.F32.S32/U32 as ARM32
2140    fn encode_arm_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2141        let mut bytes = Vec::new();
2142
2143        // VMOV Sd, Rm — move integer to VFP register
2144        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
2145        bytes.extend_from_slice(&vmov.to_le_bytes());
2146
2147        // VCVT.F32.S32 Sd, Sd (signed) or VCVT.F32.U32 Sd, Sd (unsigned)
2148        // Base: 0xEEB80A40 (signed) or 0xEEB80AC0 (unsigned)
2149        let sd_num = vfp_sreg_to_num(sd)?;
2150        let (vd, d) = encode_sreg(sd_num);
2151        let (vm, m) = encode_sreg(sd_num); // same register as source
2152        let base = if signed { 0xEEB80A40 } else { 0xEEB80AC0 };
2153        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2154        bytes.extend_from_slice(&vcvt.to_le_bytes());
2155
2156        Ok(bytes)
2157    }
2158
2159    /// Encode F32 rounding pseudo-op as ARM32 via VCVT to integer and back.
2160    /// mode: 0b00=nearest, 0b01=floor(-Inf), 0b10=ceil(+Inf), 0b11=trunc(zero)
2161    /// Strategy: VCVT.S32.F32 Sd, Sm (toward zero), then VCVT.F32.S32 Sd, Sd
2162    /// For ceil/floor/nearest, we use VCVTR (round toward mode) + convert back.
2163    /// Simplified: convert to int (toward zero for trunc) then back to float.
2164    /// Encode F32 rounding as ARM32.
2165    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2166    ///
2167    /// For trunc (mode=0b11): uses VCVTR.S32.F32 (always rounds toward zero).
2168    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant
2169    /// which honours FPSCR rmode), then restores FPSCR.
2170    fn encode_arm_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2171        let mut bytes = Vec::new();
2172        let sm_num = vfp_sreg_to_num(sm)?;
2173        let sd_num = vfp_sreg_to_num(sd)?;
2174        let (vd_s, d_s) = encode_sreg(sd_num);
2175        let (vm_s, m_s) = encode_sreg(sm_num);
2176
2177        if mode == 0b11 {
2178            // Trunc (toward zero): VCVTR.S32.F32 — the "R" variant always truncates.
2179            // 0xEEBD0AC0: bit[7]=1 => round toward zero regardless of FPSCR
2180            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2181            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2182        } else {
2183            // ceil/floor/nearest: manipulate FPSCR rounding mode
2184            let rt: u32 = 12; // R12/IP as temp
2185
2186            // VMRS R12, FPSCR
2187            let vmrs = 0xEEF10A10 | (rt << 12);
2188            bytes.extend_from_slice(&vmrs.to_le_bytes());
2189
2190            // BIC R12, R12, #(3 << 22) — clear RMode bits [23:22]
2191            // 3<<22 = 0x00C00000. ARM rotated imm: 0x03 ror 10 (rotation=5, imm8=0x03)
2192            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2193            bytes.extend_from_slice(&bic.to_le_bytes());
2194
2195            // ORR R12, R12, #(mode << 22) — set desired rounding mode
2196            if mode != 0 {
2197                // mode<<22: rotation=5, imm8=mode
2198                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2199                bytes.extend_from_slice(&orr.to_le_bytes());
2200            }
2201
2202            // VMSR FPSCR, R12
2203            let vmsr = 0xEEE10A10 | (rt << 12);
2204            bytes.extend_from_slice(&vmsr.to_le_bytes());
2205
2206            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rounding mode
2207            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2208            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2209
2210            // Restore FPSCR: clear rmode bits back to nearest (default)
2211            bytes.extend_from_slice(&vmrs.to_le_bytes());
2212            bytes.extend_from_slice(&bic.to_le_bytes());
2213            bytes.extend_from_slice(&vmsr.to_le_bytes());
2214        }
2215
2216        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
2217        let (vd2, d2) = encode_sreg(sd_num);
2218        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
2219        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2220
2221        Ok(bytes)
2222    }
2223
2224    /// Encode F32 min/max as ARM32: VCMP + VMRS + conditional VMOV
2225    fn encode_arm_f32_minmax(
2226        &self,
2227        sd: &VfpReg,
2228        sn: &VfpReg,
2229        sm: &VfpReg,
2230        is_min: bool,
2231    ) -> Result<Vec<u8>> {
2232        let mut bytes = Vec::new();
2233        let sn_num = vfp_sreg_to_num(sn)?;
2234        let sm_num = vfp_sreg_to_num(sm)?;
2235        let sd_num = vfp_sreg_to_num(sd)?;
2236
2237        // VMOV Sd, Sn (start with first operand)
2238        let (vd, d) = encode_sreg(sd_num);
2239        let (vn, n) = encode_sreg(sn_num);
2240        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2241        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2242
2243        // VCMP.F32 Sn, Sm
2244        let (vm, m) = encode_sreg(sm_num);
2245        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2246        bytes.extend_from_slice(&vcmp.to_le_bytes());
2247
2248        // VMRS APSR_nzcv, FPSCR
2249        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2250
2251        // For min: if Sn > Sm (GT), use Sm. Condition = GT (0xC)
2252        // For max: if Sn < Sm (MI/LT), use Sm. Condition = MI (0x4)
2253        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2254
2255        // VMOV{cond} Sd, Sm — conditional VMOV
2256        let vmov_cond = (cond << 28) | 0x0EB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2257        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2258
2259        Ok(bytes)
2260    }
2261
2262    /// Encode F32 copysign as ARM32: extract sign from Sm, magnitude from Sn
2263    fn encode_arm_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2264        let mut bytes = Vec::new();
2265
2266        // VMOV R12, Sm (get sign source bits)
2267        let vmov_sm = encode_vmov_core_sreg(false, sm, &Reg::R12)?;
2268        bytes.extend_from_slice(&vmov_sm.to_le_bytes());
2269
2270        // VMOV R0, Sn (get magnitude source bits) — use R0 as temp
2271        let vmov_sn = encode_vmov_core_sreg(false, sn, &Reg::R0)?;
2272        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2273
2274        // AND R12, R12, #0x80000000 (keep only sign bit)
2275        // Thumb-2 constant 0x80000000 needs special encoding; in ARM32 use rotated imm
2276        // 0x80000000 = 0x02 rotated right by 2 (rotation=1, imm8=0x02)
2277        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2278        bytes.extend_from_slice(&and_sign.to_le_bytes());
2279
2280        // BIC R0, R0, #0x80000000 (clear sign bit from magnitude)
2281        // R0 = register 0, so Rn and Rd fields are 0
2282        let bic_sign = 0xE3C00000u32 | (1 << 8) | 0x02;
2283        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2284
2285        // ORR R0, R0, R12 (combine sign + magnitude)
2286        // R0 = register 0, so Rn and Rd fields are 0
2287        let orr = 0xE1800000u32 | 12;
2288        bytes.extend_from_slice(&orr.to_le_bytes());
2289
2290        // VMOV Sd, R0
2291        let vmov_result = encode_vmov_core_sreg(true, sd, &Reg::R0)?;
2292        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2293
2294        Ok(bytes)
2295    }
2296
2297    /// Encode F64 comparison as ARM32: VCMP.F64 + VMRS + MOV rd,#0 + MOVcond rd,#1
2298    fn encode_arm_f64_compare(
2299        &self,
2300        rd: &Reg,
2301        dn: &VfpReg,
2302        dm: &VfpReg,
2303        cond_code: u32,
2304    ) -> Result<Vec<u8>> {
2305        let mut bytes = Vec::new();
2306
2307        // VCMP.F64 Dn, Dm: 0xEEB40B40 with Dn in Vd position, Dm in Vm position
2308        let dn_num = vfp_dreg_to_num(dn)?;
2309        let dm_num = vfp_dreg_to_num(dm)?;
2310        let (vd, d) = encode_dreg(dn_num);
2311        let (vm, m) = encode_dreg(dm_num);
2312        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2313        bytes.extend_from_slice(&vcmp.to_le_bytes());
2314
2315        // VMRS APSR_nzcv, FPSCR
2316        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2317
2318        // MOV rd, #0
2319        let rd_bits = reg_to_bits(rd);
2320        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2321        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2322
2323        // MOVcond rd, #1
2324        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2325        bytes.extend_from_slice(&mov_one.to_le_bytes());
2326
2327        Ok(bytes)
2328    }
2329
2330    /// Encode F64 constant load as ARM32: MOVW + MOVT + MOVW + MOVT + VMOV
2331    fn encode_arm_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
2332        let mut bytes = Vec::new();
2333        let bits = value.to_bits();
2334        let lo32 = bits as u32;
2335        let hi32 = (bits >> 32) as u32;
2336
2337        // Load low 32 bits into R0 (Rd field = 0 for R0)
2338        let lo16 = lo32 & 0xFFFF;
2339        let movw_r0 = 0xE3000000 | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2340        bytes.extend_from_slice(&movw_r0.to_le_bytes());
2341        let hi16 = (lo32 >> 16) & 0xFFFF;
2342        let movt_r0 = 0xE3400000 | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2343        bytes.extend_from_slice(&movt_r0.to_le_bytes());
2344
2345        // Load high 32 bits into R12
2346        let lo16 = hi32 & 0xFFFF;
2347        let movw_r12 = 0xE3000000 | ((lo16 >> 12) << 16) | (12 << 12) | (lo16 & 0xFFF);
2348        bytes.extend_from_slice(&movw_r12.to_le_bytes());
2349        let hi16 = (hi32 >> 16) & 0xFFFF;
2350        let movt_r12 = 0xE3400000 | ((hi16 >> 12) << 16) | (12 << 12) | (hi16 & 0xFFF);
2351        bytes.extend_from_slice(&movt_r12.to_le_bytes());
2352
2353        // VMOV Dd, R0, R12
2354        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
2355        bytes.extend_from_slice(&vmov.to_le_bytes());
2356
2357        Ok(bytes)
2358    }
2359
2360    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as ARM32
2361    fn encode_arm_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2362        let mut bytes = Vec::new();
2363
2364        // Use S0 as intermediate: VMOV S0, Rm
2365        let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
2366        bytes.extend_from_slice(&vmov.to_le_bytes());
2367
2368        // VCVT.F64.S32 Dd, S0 (signed) or VCVT.F64.U32 Dd, S0 (unsigned)
2369        // Base: 0xEEB80B40 (signed) or 0xEEB80BC0 (unsigned)
2370        let dd_num = vfp_dreg_to_num(dd)?;
2371        let (vd, d) = encode_dreg(dd_num);
2372        let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
2373        // S0 is register 0: Vm=0, M=0
2374        let vcvt = base | (d << 22) | (vd << 12);
2375        bytes.extend_from_slice(&vcvt.to_le_bytes());
2376
2377        Ok(bytes)
2378    }
2379
2380    /// Encode VCVT.F64.F32 Dd, Sm as ARM32 (f32 to f64 promotion)
2381    fn encode_arm_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2382        let dd_num = vfp_dreg_to_num(dd)?;
2383        let sm_num = vfp_sreg_to_num(sm)?;
2384        let (vd, d) = encode_dreg(dd_num);
2385        let (vm, m) = encode_sreg(sm_num);
2386
2387        // VCVT.F64.F32 Dd, Sm: 0xEEB70AC0
2388        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
2389        Ok(vcvt.to_le_bytes().to_vec())
2390    }
2391
2392    /// Encode VCVT.S32/U32.F64 Sd, Dm + VMOV Rd, Sd as ARM32
2393    fn encode_arm_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2394        let mut bytes = Vec::new();
2395        let dm_num = vfp_dreg_to_num(dm)?;
2396        let (vm, m) = encode_dreg(dm_num);
2397
2398        // VCVT.S32.F64 S0, Dm (toward zero) or VCVT.U32.F64 S0, Dm
2399        // S0: Vd=0, D=0
2400        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
2401        let vcvt = base | (m << 5) | vm;
2402        bytes.extend_from_slice(&vcvt.to_le_bytes());
2403
2404        // VMOV Rd, S0
2405        let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
2406        bytes.extend_from_slice(&vmov.to_le_bytes());
2407
2408        Ok(bytes)
2409    }
2410
2411    /// Encode F64 rounding pseudo-op as ARM32 via VCVT to integer and back.
2412    /// Encode F64 rounding as ARM32.
2413    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2414    ///
2415    /// For trunc: uses VCVTR.S32.F64 (always truncates).
2416    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F64 (non-R variant),
2417    /// then restores FPSCR.
2418    fn encode_arm_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2419        let mut bytes = Vec::new();
2420        let dm_num = vfp_dreg_to_num(dm)?;
2421        let dd_num = vfp_dreg_to_num(dd)?;
2422        let (vm, m) = encode_dreg(dm_num);
2423        let (vd, d) = encode_dreg(dd_num);
2424
2425        if mode == 0b11 {
2426            // Trunc (toward zero): VCVTR.S32.F64 — bit[7]=1, always truncates
2427            let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
2428            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2429        } else {
2430            // ceil/floor/nearest: manipulate FPSCR rounding mode
2431            let rt: u32 = 12;
2432
2433            // VMRS R12, FPSCR
2434            let vmrs = 0xEEF10A10 | (rt << 12);
2435            bytes.extend_from_slice(&vmrs.to_le_bytes());
2436
2437            // BIC R12, R12, #(3 << 22)
2438            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2439            bytes.extend_from_slice(&bic.to_le_bytes());
2440
2441            // ORR R12, R12, #(mode << 22)
2442            if mode != 0 {
2443                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2444                bytes.extend_from_slice(&orr.to_le_bytes());
2445            }
2446
2447            // VMSR FPSCR, R12
2448            let vmsr = 0xEEE10A10 | (rt << 12);
2449            bytes.extend_from_slice(&vmsr.to_le_bytes());
2450
2451            // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0), uses FPSCR rmode
2452            let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
2453            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2454
2455            // Restore FPSCR
2456            bytes.extend_from_slice(&vmrs.to_le_bytes());
2457            bytes.extend_from_slice(&bic.to_le_bytes());
2458            bytes.extend_from_slice(&vmsr.to_le_bytes());
2459        }
2460
2461        // VCVT.F64.S32 Dd, S0 (convert back to double)
2462        let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
2463        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2464
2465        Ok(bytes)
2466    }
2467
2468    /// Encode F64 min/max as ARM32: VMOV + VCMP + VMRS + conditional VMOV
2469    fn encode_arm_f64_minmax(
2470        &self,
2471        dd: &VfpReg,
2472        dn: &VfpReg,
2473        dm: &VfpReg,
2474        is_min: bool,
2475    ) -> Result<Vec<u8>> {
2476        let mut bytes = Vec::new();
2477        let dn_num = vfp_dreg_to_num(dn)?;
2478        let dm_num = vfp_dreg_to_num(dm)?;
2479        let dd_num = vfp_dreg_to_num(dd)?;
2480
2481        // VMOV.F64 Dd, Dn (start with first operand)
2482        let (vd, d) = encode_dreg(dd_num);
2483        let (vn, n) = encode_dreg(dn_num);
2484        let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2485        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2486
2487        // VCMP.F64 Dn, Dm
2488        let (vm, m) = encode_dreg(dm_num);
2489        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2490        bytes.extend_from_slice(&vcmp.to_le_bytes());
2491
2492        // VMRS APSR_nzcv, FPSCR
2493        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2494
2495        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2496        let vmov_cond = (cond << 28) | 0x0EB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2497        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2498
2499        Ok(bytes)
2500    }
2501
2502    /// Encode F64 copysign as ARM32
2503    fn encode_arm_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
2504        let mut bytes = Vec::new();
2505
2506        // VMOV R0, R12, Dm (get sign source bits)
2507        let vmov_dm = encode_vmov_core_dreg(false, dm, &Reg::R0, &Reg::R12)?;
2508        bytes.extend_from_slice(&vmov_dm.to_le_bytes());
2509
2510        // VMOV R1, R2, Dn (get magnitude source bits)
2511        // We use R1 (lo) and R2 (hi) for the magnitude
2512        let vmov_dn = encode_vmov_core_dreg(false, dn, &Reg::R1, &Reg::R2)?;
2513        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2514
2515        // AND R12, R12, #0x80000000 (keep only sign bit from hi word)
2516        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2517        bytes.extend_from_slice(&and_sign.to_le_bytes());
2518
2519        // BIC R2, R2, #0x80000000 (clear sign bit from magnitude hi word)
2520        let bic_sign = 0xE3C00000u32 | (2 << 16) | (2 << 12) | (1 << 8) | 0x02;
2521        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2522
2523        // ORR R2, R2, R12 (combine sign + magnitude)
2524        let orr = 0xE1800000u32 | (2 << 16) | (2 << 12) | 12;
2525        bytes.extend_from_slice(&orr.to_le_bytes());
2526
2527        // VMOV Dd, R1, R2
2528        let vmov_result = encode_vmov_core_dreg(true, dd, &Reg::R1, &Reg::R2)?;
2529        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2530
2531        Ok(bytes)
2532    }
2533
2534    /// Encode VCVT.S32/U32.F32 + VMOV as ARM32
2535    fn encode_arm_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2536        let mut bytes = Vec::new();
2537
2538        // VCVT.S32.F32 Sd, Sm (toward zero) or VCVT.U32.F32 Sd, Sm
2539        // We use Sm as both source and destination for the intermediate result
2540        let sm_num = vfp_sreg_to_num(sm)?;
2541        let (vd, d) = encode_sreg(sm_num);
2542        let (vm, m) = encode_sreg(sm_num);
2543        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
2544        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2545        bytes.extend_from_slice(&vcvt.to_le_bytes());
2546
2547        // VMOV Rd, Sm — move result back to core register
2548        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
2549        bytes.extend_from_slice(&vmov.to_le_bytes());
2550
2551        Ok(bytes)
2552    }
2553
2554    /// Encode an ARM instruction in Thumb-2 mode (16-bit or 32-bit instructions)
2555    fn encode_thumb(&self, op: &ArmOp) -> Result<Vec<u8>> {
2556        // Thumb-2 supports both 16-bit and 32-bit instructions
2557        // 32-bit instructions are encoded as two 16-bit halfwords (big-endian order)
2558        match op {
2559            // === 16-bit Thumb encodings ===
2560            ArmOp::Add { rd, rn, op2 } => {
2561                let rd_bits = reg_to_bits(rd) as u16;
2562                let rn_bits = reg_to_bits(rn) as u16;
2563
2564                if let Operand2::Reg(rm) = op2 {
2565                    let rm_bits = reg_to_bits(rm) as u16;
2566                    // 16-bit ADDS only has 3-bit register fields (R0-R7). For
2567                    // high registers (e.g. R12, the MemLoad/MemStore base
2568                    // scratch) the bits overflow into adjacent fields, silently
2569                    // corrupting the operands — issue #178/#180: `add ip,ip,r0`
2570                    // was emitted as `adds r4,r5,r1`. Guard on all three regs
2571                    // being low and fall back to 32-bit ADD.W otherwise, exactly
2572                    // as the Sub handler below does.
2573                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2574                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2575                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2576                        Ok(instr.to_le_bytes().to_vec())
2577                    } else {
2578                        // ADD.W Rd, Rn, Rm (32-bit) for high registers
2579                        self.encode_thumb32_add_reg_raw(
2580                            rd_bits as u32,
2581                            rn_bits as u32,
2582                            rm_bits as u32,
2583                        )
2584                    }
2585                } else if let Operand2::Imm(imm) = op2 {
2586                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2587                        // ADDS Rd, Rn, #imm3 (16-bit): 0001 110 imm3 Rn Rd
2588                        let instr: u16 = 0x1C00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2589                        Ok(instr.to_le_bytes().to_vec())
2590                    } else {
2591                        // Use 32-bit ADD for larger immediates
2592                        self.encode_thumb32_add(rd, rn, *imm as u32)
2593                    }
2594                } else {
2595                    // Fallback to 32-bit encoding
2596                    self.encode_thumb32_add(rd, rn, 0)
2597                }
2598            }
2599
2600            ArmOp::Sub { rd, rn, op2 } => {
2601                let rd_bits = reg_to_bits(rd) as u16;
2602                let rn_bits = reg_to_bits(rn) as u16;
2603
2604                if let Operand2::Reg(rm) = op2 {
2605                    let rm_bits = reg_to_bits(rm) as u16;
2606                    // 16-bit SUBS can only use low registers (R0-R7)
2607                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2608                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2609                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2610                        Ok(instr.to_le_bytes().to_vec())
2611                    } else {
2612                        // Use 32-bit SUB.W for high registers
2613                        self.encode_thumb32_sub_reg_raw(
2614                            rd_bits as u32,
2615                            rn_bits as u32,
2616                            rm_bits as u32,
2617                        )
2618                    }
2619                } else if let Operand2::Imm(imm) = op2 {
2620                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2621                        // SUBS Rd, Rn, #imm3 (16-bit): 0001 111 imm3 Rn Rd
2622                        let instr: u16 = 0x1E00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2623                        Ok(instr.to_le_bytes().to_vec())
2624                    } else {
2625                        self.encode_thumb32_sub(rd, rn, *imm as u32)
2626                    }
2627                } else {
2628                    self.encode_thumb32_sub(rd, rn, 0)
2629                }
2630            }
2631
2632            ArmOp::Mov { rd, op2 } => {
2633                let rd_bits = reg_to_bits(rd) as u16;
2634
2635                if let Operand2::Imm(imm) = op2 {
2636                    // #498: the old test here was the SIGNED `*imm <= 255`,
2637                    // so a negative immediate (e.g. -1) fell into the 16-bit
2638                    // MOVS arm and encoded the wrong VALUE (#(imm & 0xFF) =
2639                    // #0xFF). A positive imm above 0xFFFF was equally wrong:
2640                    // MOVW truncates to 16 bits. Split on the UNSIGNED value:
2641                    // imm8 → MOVS, imm16 → MOVW, anything wider (negative or
2642                    // >0xFFFF) → the full-value MOVW+MOVT pair. No emitter
2643                    // produces the wide shape today (both selectors
2644                    // materialize wide constants as explicit Movw/Movt or
2645                    // Movw+Mvn), so this is byte-identical on shipped paths —
2646                    // it retires the latent wrong-value encodings the
2647                    // `estimator_encoder_agreement` oracle had pinned.
2648                    let uimm = *imm as u32;
2649                    if uimm <= 255 && rd_bits < 8 {
2650                        // MOVS Rd, #imm8 (16-bit): 0010 0 Rd imm8
2651                        let imm_bits = (*imm as u16) & 0xFF;
2652                        let instr: u16 = 0x2000 | (rd_bits << 8) | imm_bits;
2653                        Ok(instr.to_le_bytes().to_vec())
2654                    } else if uimm <= 0xFFFF {
2655                        // Use 32-bit MOVW for 16-bit immediates
2656                        self.encode_thumb32_movw(rd, uimm)
2657                    } else {
2658                        // Full 32-bit value: MOVW low16 + MOVT high16
2659                        let mut bytes = self.encode_thumb32_movw(rd, uimm & 0xFFFF)?;
2660                        bytes.extend(self.encode_thumb32_movt_raw(reg_to_bits(rd), uimm >> 16)?);
2661                        Ok(bytes)
2662                    }
2663                } else if let Operand2::Reg(rm) = op2 {
2664                    let rm_bits = reg_to_bits(rm) as u16;
2665                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
2666                    // D = Rd[3], Rd[2:0] in lower bits
2667                    let d_bit = (rd_bits >> 3) & 1;
2668                    let instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
2669                    Ok(instr.to_le_bytes().to_vec())
2670                } else {
2671                    let instr: u16 = 0xBF00; // NOP fallback
2672                    Ok(instr.to_le_bytes().to_vec())
2673                }
2674            }
2675
2676            ArmOp::Push { regs } => {
2677                // Thumb-2 PUSH encoding:
2678                // If all regs in R0-R7 + LR, use 16-bit: 1011 010 M rrrrrrrr
2679                // Otherwise use 32-bit: STMDB SP!, {regs} = 1110 1001 0010 1101 | 0M0 reglist(13)
2680                let mut reg_list: u16 = 0;
2681                let mut need_32bit = false;
2682                for r in regs {
2683                    let bit = reg_to_bits(r);
2684                    if bit >= 8 && *r != Reg::LR {
2685                        need_32bit = true;
2686                    }
2687                    reg_list |= 1 << bit;
2688                }
2689                if !need_32bit {
2690                    // 16-bit PUSH: 1011 010 M rrrrrrrr
2691                    let m_bit = if reg_list & (1 << 14) != 0 {
2692                        1u16
2693                    } else {
2694                        0u16
2695                    };
2696                    let low_regs = reg_list & 0xFF;
2697                    let instr: u16 = 0xB400 | (m_bit << 8) | low_regs;
2698                    Ok(instr.to_le_bytes().to_vec())
2699                } else {
2700                    // 32-bit STMDB SP!, {regs}: E92D | reglist(16)
2701                    let hw1: u16 = 0xE92D;
2702                    let hw2: u16 = reg_list;
2703                    let mut bytes = hw1.to_le_bytes().to_vec();
2704                    bytes.extend_from_slice(&hw2.to_le_bytes());
2705                    Ok(bytes)
2706                }
2707            }
2708
2709            ArmOp::Pop { regs } => {
2710                // Thumb-2 POP encoding:
2711                // If all regs in R0-R7 + PC, use 16-bit: 1011 110 P rrrrrrrr
2712                // Otherwise use 32-bit: LDMIA SP!, {regs} = 1110 1000 1011 1101 | PM0 reglist(13)
2713                let mut reg_list: u16 = 0;
2714                let mut need_32bit = false;
2715                for r in regs {
2716                    let bit = reg_to_bits(r);
2717                    if bit >= 8 && *r != Reg::PC {
2718                        need_32bit = true;
2719                    }
2720                    reg_list |= 1 << bit;
2721                }
2722                if !need_32bit {
2723                    // 16-bit POP: 1011 110 P rrrrrrrr
2724                    let p_bit = if reg_list & (1 << 15) != 0 {
2725                        1u16
2726                    } else {
2727                        0u16
2728                    };
2729                    let low_regs = reg_list & 0xFF;
2730                    let instr: u16 = 0xBC00 | (p_bit << 8) | low_regs;
2731                    Ok(instr.to_le_bytes().to_vec())
2732                } else {
2733                    // 32-bit LDMIA SP!, {regs}: E8BD | reglist(16)
2734                    let hw1: u16 = 0xE8BD;
2735                    let hw2: u16 = reg_list;
2736                    let mut bytes = hw1.to_le_bytes().to_vec();
2737                    bytes.extend_from_slice(&hw2.to_le_bytes());
2738                    Ok(bytes)
2739                }
2740            }
2741
2742            ArmOp::Nop => {
2743                let instr: u16 = 0xBF00; // NOP in Thumb-2
2744                Ok(instr.to_le_bytes().to_vec())
2745            }
2746
2747            ArmOp::Udf { imm } => {
2748                // UDF (Undefined) in Thumb-2: 16-bit encoding is 0xDE00 | imm8
2749                // This triggers UsageFault/HardFault, used for WASM traps
2750                let instr: u16 = 0xDE00 | (*imm as u16);
2751                let bytes = instr.to_le_bytes().to_vec();
2752                encoding_contracts::verify_thumb16(&bytes);
2753                Ok(bytes)
2754            }
2755
2756            // i64 support: ADDS, ADC, SUBS, SBC for register pair arithmetic
2757            // ADDS sets flags (carry), ADC uses carry from previous ADDS
2758            ArmOp::Adds { rd, rn, op2 } => {
2759                let rd_bits = reg_to_bits(rd) as u16;
2760                let rn_bits = reg_to_bits(rn) as u16;
2761
2762                if let Operand2::Reg(rm) = op2 {
2763                    let rm_bits = reg_to_bits(rm) as u16;
2764                    // 16-bit ADDS is R0-R7 only; i64 pair allocation can place
2765                    // operands in R8-R11, which would overflow the 3-bit fields
2766                    // and corrupt the operands (#178/#180 class). Guard and fall
2767                    // back to 32-bit ADDS.W for high registers.
2768                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2769                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2770                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2771                        Ok(instr.to_le_bytes().to_vec())
2772                    } else {
2773                        self.encode_thumb32_adds_reg_raw(
2774                            rd_bits as u32,
2775                            rn_bits as u32,
2776                            rm_bits as u32,
2777                        )
2778                    }
2779                } else {
2780                    // 32-bit Thumb-2 ADDS with immediate
2781                    self.encode_thumb32_adds(rd, rn, 0)
2782                }
2783            }
2784
2785            // ADC: Add with Carry (Thumb-2 32-bit)
2786            // ADC.W Rd, Rn, Rm: EB40 Rn | 00 Rd 00 Rm
2787            ArmOp::Adc { rd, rn, op2 } => {
2788                let rd_bits = reg_to_bits(rd);
2789                let rn_bits = reg_to_bits(rn);
2790
2791                if let Operand2::Reg(rm) = op2 {
2792                    let rm_bits = reg_to_bits(rm);
2793                    // ADC.W Rd, Rn, Rm (T2): 1110 1011 0100 Rn | 0 000 Rd 00 00 Rm
2794                    let hw1: u16 = (0xEB40 | rn_bits) as u16;
2795                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2796
2797                    let mut bytes = hw1.to_le_bytes().to_vec();
2798                    bytes.extend_from_slice(&hw2.to_le_bytes());
2799                    Ok(bytes)
2800                } else {
2801                    // ADC with immediate - use 32-bit encoding
2802                    let hw1: u16 = (0xF140 | rn_bits) as u16;
2803                    let hw2: u16 = (rd_bits << 8) as u16;
2804                    let mut bytes = hw1.to_le_bytes().to_vec();
2805                    bytes.extend_from_slice(&hw2.to_le_bytes());
2806                    Ok(bytes)
2807                }
2808            }
2809
2810            // SUBS sets flags (borrow), SBC uses borrow from previous SUBS
2811            ArmOp::Subs { rd, rn, op2 } => {
2812                let rd_bits = reg_to_bits(rd) as u16;
2813                let rn_bits = reg_to_bits(rn) as u16;
2814
2815                if let Operand2::Reg(rm) = op2 {
2816                    let rm_bits = reg_to_bits(rm) as u16;
2817                    // 16-bit SUBS is R0-R7 only; high-register i64 pair operands
2818                    // would overflow the 3-bit fields (#178/#180 class). Guard
2819                    // and fall back to 32-bit SUBS.W for high registers.
2820                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2821                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2822                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2823                        Ok(instr.to_le_bytes().to_vec())
2824                    } else {
2825                        self.encode_thumb32_subs_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 SUBS with immediate
2833                    self.encode_thumb32_subs(rd, rn, 0)
2834                }
2835            }
2836
2837            // SBC: Subtract with Carry (Thumb-2 32-bit)
2838            // SBC.W Rd, Rn, Rm: EB60 Rn | 00 Rd 00 Rm
2839            ArmOp::Sbc { 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                    // SBC.W Rd, Rn, Rm (T2): 1110 1011 0110 Rn | 0 000 Rd 00 00 Rm
2846                    let hw1: u16 = (0xEB60 | 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                    // SBC with immediate - use 32-bit encoding
2854                    let hw1: u16 = (0xF160 | 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            // === 32-bit Thumb-2 encodings ===
2863
2864            // SDIV: 11111011 1001 Rn 1111 Rd 1111 Rm
2865            ArmOp::Sdiv { rd, rn, rm } => {
2866                let rd_bits = reg_to_bits(rd);
2867                let rn_bits = reg_to_bits(rn);
2868                let rm_bits = reg_to_bits(rm);
2869                reg_bits_checked(rd_bits)?;
2870                reg_bits_checked(rn_bits)?;
2871                reg_bits_checked(rm_bits)?;
2872
2873                // Thumb-2 SDIV: FB90 F0F0 | Rn<<16 | Rd<<8 | Rm
2874                // First halfword: 1111 1011 1001 Rn = 0xFB90 | Rn
2875                // Second halfword: 1111 Rd 1111 Rm = 0xF0F0 | Rd<<8 | Rm
2876                let hw1: u16 = (0xFB90 | rn_bits) as u16;
2877                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
2878
2879                // Thumb-2 32-bit instructions: first halfword, then second halfword (little-endian each)
2880                let mut bytes = hw1.to_le_bytes().to_vec();
2881                bytes.extend_from_slice(&hw2.to_le_bytes());
2882                encoding_contracts::verify_thumb32(&bytes);
2883                Ok(bytes)
2884            }
2885
2886            // UDIV: 11111011 1011 Rn 1111 Rd 1111 Rm
2887            ArmOp::Udiv { rd, rn, rm } => {
2888                let rd_bits = reg_to_bits(rd);
2889                let rn_bits = reg_to_bits(rn);
2890                let rm_bits = reg_to_bits(rm);
2891                reg_bits_checked(rd_bits)?;
2892                reg_bits_checked(rn_bits)?;
2893                reg_bits_checked(rm_bits)?;
2894
2895                // Thumb-2 UDIV: FBB0 F0F0 | Rn<<16 | Rd<<8 | Rm
2896                let hw1: u16 = (0xFBB0 | rn_bits) as u16;
2897                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
2898
2899                let mut bytes = hw1.to_le_bytes().to_vec();
2900                bytes.extend_from_slice(&hw2.to_le_bytes());
2901                encoding_contracts::verify_thumb32(&bytes);
2902                Ok(bytes)
2903            }
2904
2905            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
2906                let rdlo_bits = reg_to_bits(rdlo);
2907                let rdhi_bits = reg_to_bits(rdhi);
2908                let rn_bits = reg_to_bits(rn);
2909                let rm_bits = reg_to_bits(rm);
2910                reg_bits_checked(rdlo_bits)?;
2911                reg_bits_checked(rdhi_bits)?;
2912                reg_bits_checked(rn_bits)?;
2913                reg_bits_checked(rm_bits)?;
2914
2915                // Thumb-2 UMULL: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm
2916                let hw1: u16 = (0xFBA0 | rn_bits) as u16;
2917                let hw2: u16 = ((rdlo_bits << 12) | (rdhi_bits << 8) | rm_bits) as u16;
2918
2919                let mut bytes = hw1.to_le_bytes().to_vec();
2920                bytes.extend_from_slice(&hw2.to_le_bytes());
2921                encoding_contracts::verify_thumb32(&bytes);
2922                Ok(bytes)
2923            }
2924
2925            // MUL (Thumb-2 32-bit): MUL Rd, Rn, Rm
2926            ArmOp::Mul { rd, rn, rm } => {
2927                let rd_bits = reg_to_bits(rd);
2928                let rn_bits = reg_to_bits(rn);
2929                let rm_bits = reg_to_bits(rm);
2930
2931                // Thumb-2 MUL: FB00 F000 | Rn | Rd<<8 | Rm
2932                // 11111011 0000 Rn | 1111 Rd 0000 Rm
2933                let hw1: u16 = (0xFB00 | rn_bits) as u16;
2934                let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
2935
2936                let mut bytes = hw1.to_le_bytes().to_vec();
2937                bytes.extend_from_slice(&hw2.to_le_bytes());
2938                Ok(bytes)
2939            }
2940
2941            // MLS: Rd = Ra - Rn * Rm
2942            ArmOp::Mls { rd, rn, rm, ra } => {
2943                let rd_bits = reg_to_bits(rd);
2944                let rn_bits = reg_to_bits(rn);
2945                let rm_bits = reg_to_bits(rm);
2946                let ra_bits = reg_to_bits(ra);
2947
2948                // Thumb-2 MLS: FB00 Rn | Ra Rd 0001 Rm
2949                // 11111011 0000 Rn | Ra Rd 0001 Rm
2950                let hw1: u16 = (0xFB00 | rn_bits) as u16;
2951                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | 0x10 | rm_bits) as u16;
2952
2953                let mut bytes = hw1.to_le_bytes().to_vec();
2954                bytes.extend_from_slice(&hw2.to_le_bytes());
2955                Ok(bytes)
2956            }
2957
2958            ArmOp::Mla { rd, rn, rm, ra } => {
2959                let rd_bits = reg_to_bits(rd);
2960                let rn_bits = reg_to_bits(rn);
2961                let rm_bits = reg_to_bits(rm);
2962                let ra_bits = reg_to_bits(ra);
2963
2964                // Thumb-2 MLA: FB00 Rn | Ra Rd 0000 Rm — same as MLS without the
2965                // bit-4 (0x10) op flag. rd = ra + rn*rm.
2966                let hw1: u16 = (0xFB00 | rn_bits) as u16;
2967                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | rm_bits) as u16;
2968
2969                let mut bytes = hw1.to_le_bytes().to_vec();
2970                bytes.extend_from_slice(&hw2.to_le_bytes());
2971                Ok(bytes)
2972            }
2973
2974            // AND (Thumb-2 32-bit)
2975            ArmOp::And { rd, rn, op2 } => {
2976                if let Operand2::Reg(rm) = op2 {
2977                    let rd_bits = reg_to_bits(rd);
2978                    let rn_bits = reg_to_bits(rn);
2979                    let rm_bits = reg_to_bits(rm);
2980
2981                    // Thumb-2 AND register: EA00 Rn | 0 Rd 00 00 Rm
2982                    let hw1: u16 = (0xEA00 | rn_bits) as u16;
2983                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2984
2985                    let mut bytes = hw1.to_le_bytes().to_vec();
2986                    bytes.extend_from_slice(&hw2.to_le_bytes());
2987                    Ok(bytes)
2988                } else if let Operand2::Imm(imm) = op2 {
2989                    let rd_bits = reg_to_bits(rd);
2990                    let rn_bits = reg_to_bits(rn);
2991
2992                    // Thumb-2 AND.W immediate T1: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8.
2993                    // The i:imm3:imm8 field is a ThumbExpandImm modified immediate —
2994                    // encode it correctly (or error on an un-encodable value)
2995                    // rather than packing raw bits, closing the silent-miscompile
2996                    // class for AND alongside ORR/EOR (#251) / ADD/SUB (#253) /
2997                    // CMP (#255).
2998                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
2999                        synth_core::Error::synthesis(
3000                            "AND immediate is not a valid ThumbExpandImm — materialize into a register",
3001                        )
3002                    })?;
3003                    let i_bit = (field >> 11) & 1;
3004                    let imm3 = (field >> 8) & 0x7;
3005                    let imm8 = field & 0xFF;
3006
3007                    let hw1: u16 = (0xF000 | (i_bit << 10) | rn_bits) as u16;
3008                    let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3009
3010                    let mut bytes = hw1.to_le_bytes().to_vec();
3011                    bytes.extend_from_slice(&hw2.to_le_bytes());
3012                    Ok(bytes)
3013                } else {
3014                    // RegShift variant - fallback to NOP
3015                    let instr: u16 = 0xBF00;
3016                    Ok(instr.to_le_bytes().to_vec())
3017                }
3018            }
3019
3020            // ORR (Thumb-2 32-bit)
3021            ArmOp::Orr { rd, rn, op2 } => {
3022                if let Operand2::Reg(rm) = op2 {
3023                    let rd_bits = reg_to_bits(rd);
3024                    let rn_bits = reg_to_bits(rn);
3025                    let rm_bits = reg_to_bits(rm);
3026
3027                    // Thumb-2 ORR: EA40 Rn | 0 Rd 00 00 Rm
3028                    let hw1: u16 = (0xEA40 | rn_bits) as u16;
3029                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3030
3031                    let mut bytes = hw1.to_le_bytes().to_vec();
3032                    bytes.extend_from_slice(&hw2.to_le_bytes());
3033                    Ok(bytes)
3034                } else if let Operand2::Imm(imm) = op2 {
3035                    // ORR.W immediate T1: 11110 i 0 0010 S Rn | 0 imm3 Rd imm8.
3036                    // Only the zero-extended byte form (imm <= 0xFF) is encoded;
3037                    // larger modified immediates need ThumbExpandImm — return an
3038                    // error rather than silently emit a NOP (Ok-or-Err, #180/#185).
3039                    let imm_val = *imm as u32;
3040                    if imm_val > 0xFF {
3041                        return Err(synth_core::Error::synthesis(
3042                            "ORR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3043                        ));
3044                    }
3045                    let rd_bits = reg_to_bits(rd);
3046                    let rn_bits = reg_to_bits(rn);
3047                    let hw1: u16 = (0xF040 | rn_bits) as u16;
3048                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3049                    let mut bytes = hw1.to_le_bytes().to_vec();
3050                    bytes.extend_from_slice(&hw2.to_le_bytes());
3051                    Ok(bytes)
3052                } else {
3053                    let instr: u16 = 0xBF00;
3054                    Ok(instr.to_le_bytes().to_vec())
3055                }
3056            }
3057
3058            // EOR (Thumb-2 32-bit)
3059            ArmOp::Eor { rd, rn, op2 } => {
3060                if let Operand2::Reg(rm) = op2 {
3061                    let rd_bits = reg_to_bits(rd);
3062                    let rn_bits = reg_to_bits(rn);
3063                    let rm_bits = reg_to_bits(rm);
3064
3065                    // Thumb-2 EOR: EA80 Rn | 0 Rd 00 00 Rm
3066                    let hw1: u16 = (0xEA80 | rn_bits) as u16;
3067                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3068
3069                    let mut bytes = hw1.to_le_bytes().to_vec();
3070                    bytes.extend_from_slice(&hw2.to_le_bytes());
3071                    Ok(bytes)
3072                } else if let Operand2::Imm(imm) = op2 {
3073                    // EOR.W immediate T1: 11110 i 0 0100 S Rn | 0 imm3 Rd imm8.
3074                    // Byte form only (imm <= 0xFF); larger needs ThumbExpandImm —
3075                    // error, not a silent NOP (Ok-or-Err, #180/#185).
3076                    let imm_val = *imm as u32;
3077                    if imm_val > 0xFF {
3078                        return Err(synth_core::Error::synthesis(
3079                            "EOR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3080                        ));
3081                    }
3082                    let rd_bits = reg_to_bits(rd);
3083                    let rn_bits = reg_to_bits(rn);
3084                    let hw1: u16 = (0xF080 | rn_bits) as u16;
3085                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3086                    let mut bytes = hw1.to_le_bytes().to_vec();
3087                    bytes.extend_from_slice(&hw2.to_le_bytes());
3088                    Ok(bytes)
3089                } else {
3090                    let instr: u16 = 0xBF00;
3091                    Ok(instr.to_le_bytes().to_vec())
3092                }
3093            }
3094
3095            // Shift operations (16-bit for low registers)
3096            ArmOp::Lsl { rd, rn, shift } => {
3097                let rd_bits = reg_to_bits(rd) as u16;
3098                let rn_bits = reg_to_bits(rn) as u16;
3099                let shift_bits = (*shift as u16) & 0x1F;
3100
3101                if rd_bits < 8 && rn_bits < 8 {
3102                    // LSLS Rd, Rm, #imm5 (16-bit): 0000 0 imm5 Rm Rd
3103                    let instr: u16 = (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3104                    Ok(instr.to_le_bytes().to_vec())
3105                } else {
3106                    // Use 32-bit encoding for high registers
3107                    self.encode_thumb32_shift(rd, rn, *shift, 0b00) // LSL type
3108                }
3109            }
3110
3111            ArmOp::Lsr { rd, rn, shift } => {
3112                let rd_bits = reg_to_bits(rd) as u16;
3113                let rn_bits = reg_to_bits(rn) as u16;
3114                let shift_bits = (*shift as u16) & 0x1F;
3115
3116                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3117                    // LSRS Rd, Rm, #imm5 (16-bit): 0000 1 imm5 Rm Rd
3118                    let instr: u16 = 0x0800 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3119                    Ok(instr.to_le_bytes().to_vec())
3120                } else {
3121                    self.encode_thumb32_shift(rd, rn, *shift, 0b01) // LSR type
3122                }
3123            }
3124
3125            ArmOp::Asr { rd, rn, shift } => {
3126                let rd_bits = reg_to_bits(rd) as u16;
3127                let rn_bits = reg_to_bits(rn) as u16;
3128                let shift_bits = (*shift as u16) & 0x1F;
3129
3130                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3131                    // ASRS Rd, Rm, #imm5 (16-bit): 0001 0 imm5 Rm Rd
3132                    let instr: u16 = 0x1000 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3133                    Ok(instr.to_le_bytes().to_vec())
3134                } else {
3135                    self.encode_thumb32_shift(rd, rn, *shift, 0b10) // ASR type
3136                }
3137            }
3138
3139            ArmOp::Ror { rd, rn, shift } => {
3140                // ROR doesn't have a 16-bit immediate form, use 32-bit
3141                self.encode_thumb32_shift(rd, rn, *shift, 0b11) // ROR type
3142            }
3143
3144            // Register-based shifts (Thumb-2 32-bit)
3145            // Encoding: 11111010 0xxS Rn 1111 Rd 0000 Rm
3146            // xx = shift type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
3147            ArmOp::LslReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b00),
3148            ArmOp::LsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b01),
3149            ArmOp::AsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b10),
3150            ArmOp::RorReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b11),
3151
3152            // RSB (Reverse Subtract): Rd = imm - Rn
3153            // Thumb-2 T2 encoding: 11110 i 0 1110 S Rn | 0 imm3 Rd imm8
3154            ArmOp::Rsb { rd, rn, imm } => {
3155                let rd_bits = reg_to_bits(rd);
3156                let rn_bits = reg_to_bits(rn);
3157                let imm_val = *imm;
3158
3159                let i_bit = (imm_val >> 11) & 1;
3160                let imm3 = (imm_val >> 8) & 0x7;
3161                let imm8 = imm_val & 0xFF;
3162
3163                // hw1: 11110 i 01110 0 Rn  (S=0)
3164                let hw1: u16 = (0xF1C0 | (i_bit << 10) | rn_bits) as u16;
3165                // hw2: 0 imm3 Rd imm8
3166                let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3167
3168                let mut bytes = hw1.to_le_bytes().to_vec();
3169                bytes.extend_from_slice(&hw2.to_le_bytes());
3170                Ok(bytes)
3171            }
3172
3173            // CLZ (Thumb-2 32-bit)
3174            ArmOp::Clz { rd, rm } => {
3175                let rd_bits = reg_to_bits(rd);
3176                let rm_bits = reg_to_bits(rm);
3177
3178                // Thumb-2 CLZ: FAB0 Rm | F8 Rd Rm
3179                // 11111010 1011 Rm | 1111 1000 Rd Rm
3180                let hw1: u16 = (0xFAB0 | rm_bits) as u16;
3181                let hw2: u16 = (0xF080 | (rd_bits << 8) | rm_bits) as u16;
3182
3183                let mut bytes = hw1.to_le_bytes().to_vec();
3184                bytes.extend_from_slice(&hw2.to_le_bytes());
3185                Ok(bytes)
3186            }
3187
3188            // RBIT (Thumb-2 32-bit)
3189            ArmOp::Rbit { rd, rm } => {
3190                let rd_bits = reg_to_bits(rd);
3191                let rm_bits = reg_to_bits(rm);
3192
3193                // Thumb-2 RBIT: FA90 Rm | F0 Rd A0 Rm
3194                // 11111010 1001 Rm | 1111 Rd 1010 Rm
3195                let hw1: u16 = (0xFA90 | rm_bits) as u16;
3196                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rm_bits) as u16;
3197
3198                let mut bytes = hw1.to_le_bytes().to_vec();
3199                bytes.extend_from_slice(&hw2.to_le_bytes());
3200                Ok(bytes)
3201            }
3202
3203            // SXTB (16-bit for low registers)
3204            ArmOp::Sxtb { rd, rm } => {
3205                let rd_bits = reg_to_bits(rd) as u16;
3206                let rm_bits = reg_to_bits(rm) as u16;
3207
3208                if rd_bits < 8 && rm_bits < 8 {
3209                    // SXTB Rd, Rm (16-bit): 1011 0010 01 Rm Rd
3210                    let instr: u16 = 0xB240 | (rm_bits << 3) | rd_bits;
3211                    Ok(instr.to_le_bytes().to_vec())
3212                } else {
3213                    // Thumb-2 SXTB.W: FA4F F(rd)80 (rm)
3214                    // 11111010 0100 1111 | 1111 Rd 10 rotate Rm
3215                    let rd_bits32 = rd_bits as u32;
3216                    let rm_bits32 = rm_bits as u32;
3217                    let hw1: u16 = 0xFA4F;
3218                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3219                    let mut bytes = hw1.to_le_bytes().to_vec();
3220                    bytes.extend_from_slice(&hw2.to_le_bytes());
3221                    Ok(bytes)
3222                }
3223            }
3224
3225            // SXTH (16-bit for low registers)
3226            ArmOp::Sxth { rd, rm } => {
3227                let rd_bits = reg_to_bits(rd) as u16;
3228                let rm_bits = reg_to_bits(rm) as u16;
3229
3230                if rd_bits < 8 && rm_bits < 8 {
3231                    // SXTH Rd, Rm (16-bit): 1011 0010 00 Rm Rd
3232                    let instr: u16 = 0xB200 | (rm_bits << 3) | rd_bits;
3233                    Ok(instr.to_le_bytes().to_vec())
3234                } else {
3235                    // Thumb-2 SXTH.W: FA0F F(rd)80 (rm)
3236                    // 11111010 0000 1111 | 1111 Rd 10 rotate Rm
3237                    let rd_bits32 = rd_bits as u32;
3238                    let rm_bits32 = rm_bits as u32;
3239                    let hw1: u16 = 0xFA0F;
3240                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3241                    let mut bytes = hw1.to_le_bytes().to_vec();
3242                    bytes.extend_from_slice(&hw2.to_le_bytes());
3243                    Ok(bytes)
3244                }
3245            }
3246
3247            // UXTB Rd,Rm — zero-extend byte (rd = rm & 0xff)
3248            ArmOp::Uxtb { rd, rm } => {
3249                let rd_bits = reg_to_bits(rd) as u16;
3250                let rm_bits = reg_to_bits(rm) as u16;
3251                if rd_bits < 8 && rm_bits < 8 {
3252                    // UXTB Rd, Rm (16-bit): 1011 0010 11 Rm Rd
3253                    let instr: u16 = 0xB2C0 | (rm_bits << 3) | rd_bits;
3254                    Ok(instr.to_le_bytes().to_vec())
3255                } else {
3256                    // Thumb-2 UXTB.W: FA5F F(rd)80 (rm)
3257                    let hw1: u16 = 0xFA5F;
3258                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3259                    let mut bytes = hw1.to_le_bytes().to_vec();
3260                    bytes.extend_from_slice(&hw2.to_le_bytes());
3261                    Ok(bytes)
3262                }
3263            }
3264
3265            // UXTH Rd,Rm — zero-extend halfword (rd = rm & 0xffff)
3266            ArmOp::Uxth { rd, rm } => {
3267                let rd_bits = reg_to_bits(rd) as u16;
3268                let rm_bits = reg_to_bits(rm) as u16;
3269                if rd_bits < 8 && rm_bits < 8 {
3270                    // UXTH Rd, Rm (16-bit): 1011 0010 10 Rm Rd
3271                    let instr: u16 = 0xB280 | (rm_bits << 3) | rd_bits;
3272                    Ok(instr.to_le_bytes().to_vec())
3273                } else {
3274                    // Thumb-2 UXTH.W: FA1F F(rd)80 (rm)
3275                    let hw1: u16 = 0xFA1F;
3276                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3277                    let mut bytes = hw1.to_le_bytes().to_vec();
3278                    bytes.extend_from_slice(&hw2.to_le_bytes());
3279                    Ok(bytes)
3280                }
3281            }
3282
3283            // CMP (can be 16-bit for low registers)
3284            ArmOp::Cmp { rn, op2 } => {
3285                let rn_bits = reg_to_bits(rn) as u16;
3286
3287                if let Operand2::Imm(imm) = op2 {
3288                    // Only use 16-bit encoding for non-negative immediates 0-255
3289                    // Negative immediates must use 32-bit encoding
3290                    if *imm >= 0 && *imm <= 255 && rn_bits < 8 {
3291                        // CMP Rn, #imm8 (16-bit): 0010 1 Rn imm8
3292                        let instr: u16 = 0x2800 | (rn_bits << 8) | (*imm as u16 & 0xFF);
3293                        Ok(instr.to_le_bytes().to_vec())
3294                    } else {
3295                        self.encode_thumb32_cmp_imm(rn, *imm as u32)
3296                    }
3297                } else if let Operand2::Reg(rm) = op2 {
3298                    let rm_bits = reg_to_bits(rm) as u16;
3299                    if rn_bits < 8 && rm_bits < 8 {
3300                        // CMP Rn, Rm (16-bit low): 0100 0010 10 Rm Rn
3301                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
3302                        Ok(instr.to_le_bytes().to_vec())
3303                    } else {
3304                        // CMP Rn, Rm (16-bit high): 0100 0101 N Rm Rn[2:0]
3305                        let n_bit = (rn_bits >> 3) & 1;
3306                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
3307                        Ok(instr.to_le_bytes().to_vec())
3308                    }
3309                } else {
3310                    let instr: u16 = 0xBF00;
3311                    Ok(instr.to_le_bytes().to_vec())
3312                }
3313            }
3314
3315            // CMN (Compare Negative) - computes Rn + op2 and sets flags
3316            // CMN Rn, #1 sets Z flag if Rn == -1 (since -1 + 1 = 0)
3317            ArmOp::Cmn { rn, op2 } => {
3318                let rn_bits = reg_to_bits(rn) as u16;
3319
3320                if let Operand2::Imm(imm) = op2 {
3321                    // CMN.W Rn, #imm (32-bit): i:imm3:imm8 is a ThumbExpandImm
3322                    // modified immediate (the field sits in imm3=hw2[14:12],
3323                    // imm8=hw2[7:0], i=hw1[10]). Encode it correctly, or error on
3324                    // an un-encodable value — replacing the old silent `0xBF00`
3325                    // NOP (the last of the silent-miscompile data-proc encoders).
3326                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3327                        synth_core::Error::synthesis(
3328                            "CMN immediate is not a valid ThumbExpandImm — materialize into a register",
3329                        )
3330                    })?;
3331                    let i_bit = (field >> 11) & 1;
3332                    let imm3 = (field >> 8) & 0x7;
3333                    let imm8 = field & 0xFF;
3334                    let hw1: u16 = (0xF110 | (i_bit << 10) as u16) | rn_bits;
3335                    let hw2: u16 = (imm3 << 12) as u16 | 0x0F00 | imm8 as u16;
3336                    let mut bytes = hw1.to_le_bytes().to_vec();
3337                    bytes.extend_from_slice(&hw2.to_le_bytes());
3338                    Ok(bytes)
3339                } else if let Operand2::Reg(rm) = op2 {
3340                    let rm_bits = reg_to_bits(rm) as u16;
3341                    // 16-bit CMN (T1) only encodes R0-R7; high registers overflow
3342                    // the 3-bit fields and corrupt the operands (#184, the #180
3343                    // class). CMN has no high-register 16-bit form, so fall back
3344                    // to 32-bit CMN.W (T2): EB10 Rn | 0F00 Rm (ADD.W with S=1 and
3345                    // Rd discarded as PC/1111).
3346                    if rn_bits < 8 && rm_bits < 8 {
3347                        // CMN Rn, Rm (16-bit): 0100 0010 11 Rm Rn
3348                        let instr: u16 = 0x42C0 | (rm_bits << 3) | rn_bits;
3349                        Ok(instr.to_le_bytes().to_vec())
3350                    } else {
3351                        let hw1: u16 = 0xEB10 | rn_bits;
3352                        let hw2: u16 = 0x0F00 | rm_bits;
3353                        let mut bytes = hw1.to_le_bytes().to_vec();
3354                        bytes.extend_from_slice(&hw2.to_le_bytes());
3355                        Ok(bytes)
3356                    }
3357                } else {
3358                    Ok(vec![0xBF, 0x00])
3359                }
3360            }
3361
3362            // LDR (can be 16-bit for simple cases)
3363            ArmOp::Ldr { rd, addr } => {
3364                let rd_bits = reg_to_bits(rd);
3365                let base_bits = reg_to_bits(&addr.base);
3366
3367                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3368                if let Some(offset_reg) = &addr.offset_reg {
3369                    let rm_bits = reg_to_bits(offset_reg);
3370
3371                    // If there's also an immediate offset, we need to ADD it first
3372                    if addr.offset != 0 {
3373                        // Use R12 (IP) as scratch to avoid clobbering the address register
3374                        // ADD R12, Rm, #offset; LDR Rd, [base, R12]
3375                        let scratch = Reg::R12;
3376                        let mut bytes =
3377                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3378                        bytes.extend(self.encode_thumb32_ldr_reg(rd, &addr.base, &scratch)?);
3379                        return Ok(bytes);
3380                    }
3381
3382                    // Simple register offset: LDR Rd, [Rn, Rm]
3383                    // 16-bit: only if Rd, Rn, Rm < R8
3384                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3385                        // LDR Rd, [Rn, Rm] (16-bit): 0101 100 Rm Rn Rd
3386                        let instr: u16 = 0x5800
3387                            | ((rm_bits as u16) << 6)
3388                            | ((base_bits as u16) << 3)
3389                            | (rd_bits as u16);
3390                        return Ok(instr.to_le_bytes().to_vec());
3391                    }
3392
3393                    // 32-bit register offset
3394                    return self.encode_thumb32_ldr_reg(rd, &addr.base, offset_reg);
3395                }
3396
3397                // Immediate offset mode [base, #imm]
3398                let offset = addr.offset as u32;
3399
3400                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3401                    // LDR Rd, [Rn, #imm5*4] (16-bit): 0110 1 imm5 Rn Rd
3402                    let imm5 = (offset >> 2) as u16;
3403                    let instr: u16 =
3404                        0x6800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3405                    Ok(instr.to_le_bytes().to_vec())
3406                } else {
3407                    self.encode_thumb32_ldr(rd, &addr.base, offset)
3408                }
3409            }
3410
3411            // STR (can be 16-bit for simple cases)
3412            ArmOp::Str { rd, addr } => {
3413                let rd_bits = reg_to_bits(rd);
3414                let base_bits = reg_to_bits(&addr.base);
3415
3416                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3417                if let Some(offset_reg) = &addr.offset_reg {
3418                    let rm_bits = reg_to_bits(offset_reg);
3419
3420                    // If there's also an immediate offset, we need to ADD it first
3421                    if addr.offset != 0 {
3422                        // Use R12 (IP) as scratch to avoid clobbering the address register
3423                        // ADD R12, Rm, #offset; STR Rd, [base, R12]
3424                        let scratch = Reg::R12;
3425                        let mut bytes =
3426                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3427                        bytes.extend(self.encode_thumb32_str_reg(rd, &addr.base, &scratch)?);
3428                        return Ok(bytes);
3429                    }
3430
3431                    // Simple register offset: STR Rd, [Rn, Rm]
3432                    // 16-bit: only if Rd, Rn, Rm < R8
3433                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3434                        // STR Rd, [Rn, Rm] (16-bit): 0101 000 Rm Rn Rd
3435                        let instr: u16 = 0x5000
3436                            | ((rm_bits as u16) << 6)
3437                            | ((base_bits as u16) << 3)
3438                            | (rd_bits as u16);
3439                        return Ok(instr.to_le_bytes().to_vec());
3440                    }
3441
3442                    // 32-bit register offset
3443                    return self.encode_thumb32_str_reg(rd, &addr.base, offset_reg);
3444                }
3445
3446                // Immediate offset mode [base, #imm]
3447                let offset = addr.offset as u32;
3448
3449                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3450                    // STR Rd, [Rn, #imm5*4] (16-bit): 0110 0 imm5 Rn Rd
3451                    let imm5 = (offset >> 2) as u16;
3452                    let instr: u16 =
3453                        0x6000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3454                    Ok(instr.to_le_bytes().to_vec())
3455                } else {
3456                    self.encode_thumb32_str(rd, &addr.base, offset)
3457                }
3458            }
3459
3460            // LDRB (Thumb-2)
3461            ArmOp::Ldrb { rd, addr } => {
3462                let rd_bits = reg_to_bits(rd);
3463                let base_bits = reg_to_bits(&addr.base);
3464
3465                if let Some(offset_reg) = &addr.offset_reg {
3466                    if addr.offset != 0 {
3467                        let scratch = Reg::R12;
3468                        let mut bytes =
3469                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3470                        bytes.extend(self.encode_thumb32_ldrb_reg(rd, &addr.base, &scratch)?);
3471                        return Ok(bytes);
3472                    }
3473                    return self.encode_thumb32_ldrb_reg(rd, &addr.base, offset_reg);
3474                }
3475
3476                let offset = addr.offset as u32;
3477                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3478                    // LDRB Rd, [Rn, #imm5] (16-bit): 0111 1 imm5 Rn Rd
3479                    let instr: u16 = 0x7800
3480                        | ((offset as u16) << 6)
3481                        | ((base_bits as u16) << 3)
3482                        | (rd_bits as u16);
3483                    Ok(instr.to_le_bytes().to_vec())
3484                } else {
3485                    self.encode_thumb32_ldrb_imm(rd, &addr.base, offset)
3486                }
3487            }
3488
3489            // LDRSB (Thumb-2)
3490            ArmOp::Ldrsb { rd, addr } => {
3491                let rd_bits = reg_to_bits(rd);
3492                let base_bits = reg_to_bits(&addr.base);
3493
3494                if let Some(offset_reg) = &addr.offset_reg {
3495                    if addr.offset != 0 {
3496                        let scratch = Reg::R12;
3497                        let mut bytes =
3498                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3499                        bytes.extend(self.encode_thumb32_ldrsb_reg(rd, &addr.base, &scratch)?);
3500                        return Ok(bytes);
3501                    }
3502                    return self.encode_thumb32_ldrsb_reg(rd, &addr.base, offset_reg);
3503                }
3504
3505                let offset = addr.offset as u32;
3506                // LDRSB has no 16-bit immediate form (only register)
3507                // For 16-bit reg form: only if Rd, Rn, Rm < R8
3508                if rd_bits < 8 && base_bits < 8 && offset == 0 {
3509                    // No immediate 16-bit encoding for LDRSB; use 32-bit
3510                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3511                } else {
3512                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3513                }
3514            }
3515
3516            // LDRH (Thumb-2)
3517            ArmOp::Ldrh { rd, addr } => {
3518                let rd_bits = reg_to_bits(rd);
3519                let base_bits = reg_to_bits(&addr.base);
3520
3521                if let Some(offset_reg) = &addr.offset_reg {
3522                    if addr.offset != 0 {
3523                        let scratch = Reg::R12;
3524                        let mut bytes =
3525                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3526                        bytes.extend(self.encode_thumb32_ldrh_reg(rd, &addr.base, &scratch)?);
3527                        return Ok(bytes);
3528                    }
3529                    return self.encode_thumb32_ldrh_reg(rd, &addr.base, offset_reg);
3530                }
3531
3532                let offset = addr.offset as u32;
3533                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3534                    // LDRH Rd, [Rn, #imm5*2] (16-bit): 1000 1 imm5 Rn Rd
3535                    let imm5 = (offset >> 1) as u16;
3536                    let instr: u16 =
3537                        0x8800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3538                    Ok(instr.to_le_bytes().to_vec())
3539                } else {
3540                    self.encode_thumb32_ldrh_imm(rd, &addr.base, offset)
3541                }
3542            }
3543
3544            // LDRSH (Thumb-2)
3545            ArmOp::Ldrsh { rd, addr } => {
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_ldrsh_reg(rd, &addr.base, &scratch)?);
3552                        return Ok(bytes);
3553                    }
3554                    return self.encode_thumb32_ldrsh_reg(rd, &addr.base, offset_reg);
3555                }
3556
3557                let offset = addr.offset as u32;
3558                self.encode_thumb32_ldrsh_imm(rd, &addr.base, offset)
3559            }
3560
3561            // STRB (Thumb-2)
3562            ArmOp::Strb { rd, addr } => {
3563                let rd_bits = reg_to_bits(rd);
3564                let base_bits = reg_to_bits(&addr.base);
3565
3566                if let Some(offset_reg) = &addr.offset_reg {
3567                    if addr.offset != 0 {
3568                        let scratch = Reg::R12;
3569                        let mut bytes =
3570                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3571                        bytes.extend(self.encode_thumb32_strb_reg(rd, &addr.base, &scratch)?);
3572                        return Ok(bytes);
3573                    }
3574                    return self.encode_thumb32_strb_reg(rd, &addr.base, offset_reg);
3575                }
3576
3577                let offset = addr.offset as u32;
3578                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3579                    // STRB Rd, [Rn, #imm5] (16-bit): 0111 0 imm5 Rn Rd
3580                    let instr: u16 = 0x7000
3581                        | ((offset as u16) << 6)
3582                        | ((base_bits as u16) << 3)
3583                        | (rd_bits as u16);
3584                    Ok(instr.to_le_bytes().to_vec())
3585                } else {
3586                    self.encode_thumb32_strb_imm(rd, &addr.base, offset)
3587                }
3588            }
3589
3590            // STRH (Thumb-2)
3591            ArmOp::Strh { rd, addr } => {
3592                let rd_bits = reg_to_bits(rd);
3593                let base_bits = reg_to_bits(&addr.base);
3594
3595                if let Some(offset_reg) = &addr.offset_reg {
3596                    if addr.offset != 0 {
3597                        let scratch = Reg::R12;
3598                        let mut bytes =
3599                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3600                        bytes.extend(self.encode_thumb32_strh_reg(rd, &addr.base, &scratch)?);
3601                        return Ok(bytes);
3602                    }
3603                    return self.encode_thumb32_strh_reg(rd, &addr.base, offset_reg);
3604                }
3605
3606                let offset = addr.offset as u32;
3607                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3608                    // STRH Rd, [Rn, #imm5*2] (16-bit): 1000 0 imm5 Rn Rd
3609                    let imm5 = (offset >> 1) as u16;
3610                    let instr: u16 =
3611                        0x8000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3612                    Ok(instr.to_le_bytes().to_vec())
3613                } else {
3614                    self.encode_thumb32_strh_imm(rd, &addr.base, offset)
3615                }
3616            }
3617
3618            // MemorySize (Thumb-2)
3619            ArmOp::MemorySize { rd } => {
3620                // LSR rd, R10, #16 — memory size in bytes / 65536 = pages
3621                // Thumb-2 16-bit: LSRS Rd, Rm, #imm5 — 0000 1 imm5 Rm Rd
3622                let rd_bits = reg_to_bits(rd);
3623                let r10_bits = reg_to_bits(&Reg::R10);
3624                if rd_bits < 8 && r10_bits < 8 {
3625                    let instr: u16 =
3626                        0x0800 | (16u16 << 6) | ((r10_bits as u16) << 3) | (rd_bits as u16);
3627                    Ok(instr.to_le_bytes().to_vec())
3628                } else {
3629                    // Thumb-2 32-bit LSR: 1110 1010 010 0 1111 | 0 imm3 Rd imm2 01 Rm
3630                    let imm5: u32 = 16;
3631                    let imm3 = (imm5 >> 2) & 0x7;
3632                    let imm2 = imm5 & 0x3;
3633                    let hw1: u16 = 0xEA4F;
3634                    let hw2: u16 =
3635                        ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | 0x10 | r10_bits) as u16;
3636                    let mut bytes = hw1.to_le_bytes().to_vec();
3637                    bytes.extend_from_slice(&hw2.to_le_bytes());
3638                    Ok(bytes)
3639                }
3640            }
3641
3642            // MemoryGrow (Thumb-2)
3643            ArmOp::MemoryGrow { rd, .. } => {
3644                // On embedded with fixed memory, always return -1 (failure)
3645                // MVN rd, #0 → MOV rd, #-1
3646                // Thumb-2 32-bit: MVN: 1111 0 i 0 0 0 1 1 0 1111 | 0 imm3 Rd imm8
3647                let rd_bits = reg_to_bits(rd);
3648                let hw1: u16 = 0xF06F; // MVN with i=0
3649                let hw2: u16 = (rd_bits << 8) as u16; // imm8=0 → ~0 = 0xFFFFFFFF = -1
3650                let mut bytes = hw1.to_le_bytes().to_vec();
3651                bytes.extend_from_slice(&hw2.to_le_bytes());
3652                Ok(bytes)
3653            }
3654
3655            // BX (16-bit)
3656            ArmOp::Bx { rm } => {
3657                let rm_bits = reg_to_bits(rm) as u16;
3658                // BX Rm (16-bit): 0100 0111 0 Rm 000
3659                let instr: u16 = 0x4700 | (rm_bits << 3);
3660                Ok(instr.to_le_bytes().to_vec())
3661            }
3662
3663            // BLX (16-bit) - Branch with Link and Exchange
3664            // BLX Rm: 0100 0111 1 Rm 000
3665            ArmOp::Blx { rm } => {
3666                let rm_bits = reg_to_bits(rm) as u16;
3667                let instr: u16 = 0x4780 | (rm_bits << 3);
3668                Ok(instr.to_le_bytes().to_vec())
3669            }
3670
3671            // CallIndirect - indirect function call via table lookup
3672            // table_index_reg contains the table index
3673            // Generates (#642): MOVW ip,#size [; MOVT]; CMP idx,ip; BLO +1;
3674            //                   UDF #0; LSL R12,idx,#2; LDR R12,[R11,R12]; BLX R12
3675            ArmOp::CallIndirect {
3676                rd: _,
3677                type_idx: _,
3678                table_index_reg,
3679                table_size,
3680            } => {
3681                let idx_reg = reg_to_bits(table_index_reg);
3682                let mut bytes = Vec::new();
3683
3684                // The expansion:
3685                // 1. Bounds guard (#642): trap (UDF #0, WASM Core §4.4.8) when
3686                //    index >= table size. Without it an out-of-bounds index
3687                //    reads past the table and BLXes whatever word lies there —
3688                //    an uncontrolled indirect branch instead of a trap.
3689                // 2. Multiplies index by 4 (function pointer size)
3690                // 3. Loads function pointer from table (table base in R11)
3691                // 4. Calls the function via BLX
3692                //
3693                // Table base setup must be done by caller/runtime. The type
3694                // check §4.4.8 also requires is discharged at COMPILE time:
3695                // the selector only emits this op after verifying the closed-
3696                // world property that every table entry's signature equals the
3697                // expected type (the raw code-pointer table carries no runtime
3698                // type ids to compare) — see the #642 selector guard.
3699
3700                // MOVW R12, #(size & 0xFFFF) — Thumb-2 T3:
3701                // 11110 i 100100 imm4 | 0 imm3 Rd imm8 (Rd=R12).
3702                let size_lo = *table_size & 0xFFFF;
3703                let hw1: u16 =
3704                    (0xF240 | (((size_lo >> 11) & 1) << 10) | ((size_lo >> 12) & 0xF)) as u16;
3705                let hw2: u16 =
3706                    ((((size_lo >> 8) & 0x7) << 12) | (12 << 8) | (size_lo & 0xFF)) as u16;
3707                bytes.extend_from_slice(&hw1.to_le_bytes());
3708                bytes.extend_from_slice(&hw2.to_le_bytes());
3709                // MOVT R12, #(size >> 16) — only when the table size exceeds
3710                // 16 bits (never in practice, but the guard must not compare
3711                // against a truncated size).
3712                let size_hi = *table_size >> 16;
3713                if size_hi != 0 {
3714                    let hw1: u16 =
3715                        (0xF2C0 | (((size_hi >> 11) & 1) << 10) | ((size_hi >> 12) & 0xF)) as u16;
3716                    let hw2: u16 =
3717                        ((((size_hi >> 8) & 0x7) << 12) | (12 << 8) | (size_hi & 0xFF)) as u16;
3718                    bytes.extend_from_slice(&hw1.to_le_bytes());
3719                    bytes.extend_from_slice(&hw2.to_le_bytes());
3720                }
3721                // CMP idx, R12 — 16-bit T2 (high-register capable):
3722                // 010001 01 N Rm(4) Rn(3), Rn full = N:Rn3.
3723                let cmp: u16 = (0x4500 | ((idx_reg & 8) << 4) | (12 << 3) | (idx_reg & 7)) as u16;
3724                bytes.extend_from_slice(&cmp.to_le_bytes());
3725                // BLO +1 insn (skip the UDF when index < size) — B<cond>.N
3726                // imm8=0: target = branch + 4. LO = unsigned lower.
3727                bytes.extend_from_slice(&0xD300u16.to_le_bytes());
3728                // UDF #0 — call_indirect out-of-bounds trap (same trap idiom as
3729                // the div-by-zero guards).
3730                bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3731
3732                // LSL R12, idx_reg, #2 (multiply index by 4)
3733                // Thumb-2 MOV with shift: 11101010 010 S 1111 | 0 imm3 Rd imm2 type Rm
3734                // LSL: type=00 (bits 5:4), imm5=2 -> imm3=000, imm2=10 (bits 7:6)
3735                // #597: the shift amount was previously shifted into bits 5:4 —
3736                // the TYPE field — encoding `mov.w ip, rm, ASR #32`, which
3737                // destroyed the index and dispatched table entry 0 for every
3738                // call. imm2 lives at bits 7:6.
3739                let hw1: u16 = 0xEA4F_u16; // MOV.W R12, Rm, LSL #2
3740                let hw2: u16 = ((0x0C00 | (0b10 << 6)) | idx_reg) as u16;
3741                bytes.extend_from_slice(&hw1.to_le_bytes());
3742                bytes.extend_from_slice(&hw2.to_le_bytes());
3743
3744                // LDR R12, [R11, R12] - load function pointer
3745                // Thumb-2 LDR (register): 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
3746                // Rn=R11, Rt=R12, Rm=R12, imm2=00 (no shift)
3747                let ldr_hw1: u16 = 0xF85B; // LDR.W Rt, [R11, Rm]
3748                let ldr_hw2: u16 = 0xC00C; // Rt=R12, imm2=00, Rm=R12
3749                bytes.extend_from_slice(&ldr_hw1.to_le_bytes());
3750                bytes.extend_from_slice(&ldr_hw2.to_le_bytes());
3751
3752                // BLX R12 (call function indirectly)
3753                // BLX Rm (16-bit): 0100 0111 1 Rm 000
3754                let blx: u16 = 0x47E0; // BLX R12
3755                bytes.extend_from_slice(&blx.to_le_bytes());
3756
3757                Ok(bytes)
3758            }
3759
3760            // Label pseudo-instruction: emits no machine code
3761            ArmOp::Label { .. } => Ok(Vec::new()),
3762
3763            // Conditional branch to label (generic) - offset 0, will be patched
3764            ArmOp::Bcc { cond, label: _ } => {
3765                use synth_synthesis::Condition;
3766                let cond_bits: u16 = match cond {
3767                    Condition::EQ => 0x0,
3768                    Condition::NE => 0x1,
3769                    Condition::HS => 0x2,
3770                    Condition::LO => 0x3,
3771                    Condition::HI => 0x8,
3772                    Condition::LS => 0x9,
3773                    Condition::GE => 0xA,
3774                    Condition::LT => 0xB,
3775                    Condition::GT => 0xC,
3776                    Condition::LE => 0xD,
3777                };
3778                // 16-bit B<cond> with offset 0: 1101 cond imm8
3779                let instr: u16 = 0xD000 | (cond_bits << 8);
3780                Ok(instr.to_le_bytes().to_vec())
3781            }
3782
3783            // Branch instructions
3784            ArmOp::B { label: _ } => {
3785                // Simplified: B.N with offset 0
3786                // For real usage, would need label resolution
3787                let instr: u16 = 0xE000; // B.N #0
3788                Ok(instr.to_le_bytes().to_vec())
3789            }
3790
3791            // BHS (Branch if Higher or Same) - used for bounds checking
3792            // Condition code: 0x2 (C set)
3793            ArmOp::Bhs { label: _ } => {
3794                // 16-bit B<cond> with offset 0: 1101 cond imm8
3795                // cond = 0x2 (HS)
3796                let instr: u16 = 0xD200; // BHS.N #0
3797                Ok(instr.to_le_bytes().to_vec())
3798            }
3799
3800            // BLO (Branch if Lower) - complementary to BHS
3801            // Condition code: 0x3 (C clear)
3802            ArmOp::Blo { label: _ } => {
3803                // 16-bit B<cond> with offset 0: 1101 cond imm8
3804                // cond = 0x3 (LO)
3805                let instr: u16 = 0xD300; // BLO.N #0
3806                Ok(instr.to_le_bytes().to_vec())
3807            }
3808
3809            // Branch with numeric offset (Thumb-2)
3810            // Thumb-2 B.W instruction: 32-bit with +-16MB range
3811            ArmOp::BOffset { offset } => {
3812                // offset is already the halfword displacement: (target - branch - 4) / 2
3813                // This is the raw encoded value, accounting for variable-length instructions
3814                let halfword_offset = *offset;
3815
3816                // 16-bit B.N encoding: 1110 0 imm11 (11-bit signed halfword offset)
3817                // Range: -1024 to +1022 halfwords
3818                if (-1024..=1022).contains(&halfword_offset) {
3819                    // 16-bit B.N encoding: 1110 0 imm11
3820                    let imm11 = (halfword_offset as u16) & 0x7FF;
3821                    let instr: u16 = 0xE000 | imm11;
3822                    Ok(instr.to_le_bytes().to_vec())
3823                } else {
3824                    // 32-bit B.W encoding for larger offsets
3825                    // First halfword: 1111 0 S imm10
3826                    // Second halfword: 10 J1 0 J2 imm11
3827                    // Total offset = SignExtend(S:I1:I2:imm10:imm11:0)
3828                    // where I1 = NOT(J1 XOR S), I2 = NOT(J2 XOR S)
3829
3830                    // The B.W (T4) encoding packs the signed offset as:
3831                    //   S:I1:I2:imm10:imm11:0  (25-bit signed, halfword-aligned)
3832                    // where J1 = NOT(I1 XOR S), J2 = NOT(I2 XOR S)
3833                    // Input halfword_offset already equals (target - PC - 4) / 2,
3834                    // so the full byte offset = halfword_offset << 1.
3835                    // The encoding fields split that 25-bit signed value (including the
3836                    // implicit trailing zero) as: S | imm10 | imm11
3837                    // with I1 = bit 23 and I2 = bit 22 of the signed offset.
3838                    let signed_offset = halfword_offset << 1; // byte offset
3839                    let s = if signed_offset < 0 { 1u32 } else { 0u32 };
3840                    let uoffset = signed_offset as u32;
3841                    let imm10 = (uoffset >> 12) & 0x3FF; // bits [21:12]
3842                    let imm11 = (uoffset >> 1) & 0x7FF; // bits [11:1]
3843                    let i1 = (uoffset >> 23) & 1; // bit 23
3844                    let i2 = (uoffset >> 22) & 1; // bit 22
3845                    let j1 = (!(i1 ^ s)) & 1; // J1 = NOT(I1 XOR S)
3846                    let j2 = (!(i2 ^ s)) & 1; // J2 = NOT(I2 XOR S)
3847
3848                    let hw1: u16 = (0xF000 | (s << 10) | imm10) as u16;
3849                    let hw2: u16 = (0x9000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
3850
3851                    let mut bytes = hw1.to_le_bytes().to_vec();
3852                    bytes.extend_from_slice(&hw2.to_le_bytes());
3853                    Ok(bytes)
3854                }
3855            }
3856
3857            // Conditional branch with numeric offset (Thumb-2)
3858            ArmOp::BCondOffset { cond, offset } => {
3859                use synth_synthesis::Condition;
3860                let cond_bits: u16 = match cond {
3861                    Condition::EQ => 0x0,
3862                    Condition::NE => 0x1,
3863                    Condition::HS => 0x2,
3864                    Condition::LO => 0x3,
3865                    Condition::HI => 0x8,
3866                    Condition::LS => 0x9,
3867                    Condition::GE => 0xA,
3868                    Condition::LT => 0xB,
3869                    Condition::GT => 0xC,
3870                    Condition::LE => 0xD,
3871                };
3872
3873                // offset is already the halfword displacement: (target - branch - 4) / 2
3874                // This is the raw imm8 value for 16-bit B<cond> encoding
3875                let halfword_offset = *offset;
3876
3877                // 16-bit B<cond> encoding: 1101 cond imm8
3878                // Range: -256 to +254 halfwords (imm8 is sign-extended and shifted left 1)
3879                if (-128..=127).contains(&halfword_offset) {
3880                    let imm8 = (halfword_offset as u16) & 0xFF;
3881                    let instr: u16 = 0xD000 | (cond_bits << 8) | imm8;
3882                    Ok(instr.to_le_bytes().to_vec())
3883                } else {
3884                    // 32-bit B<cond>.W for larger offsets
3885                    // First halfword: 1111 0 S cond imm6
3886                    // Second halfword: 10 J1 0 J2 imm11
3887                    let offset = halfword_offset >> 1;
3888                    let s = if offset < 0 { 1u32 } else { 0u32 };
3889                    let imm6 = ((offset >> 11) as u32) & 0x3F;
3890                    let imm11 = (offset as u32) & 0x7FF;
3891                    let j1 = if s == 1 { 1 } else { 0 };
3892                    let j2 = if s == 1 { 1 } else { 0 };
3893
3894                    let hw1: u16 = (0xF000 | (s << 10) | ((cond_bits as u32) << 6) | imm6) as u16;
3895                    let hw2: u16 = (0x8000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
3896
3897                    let mut bytes = hw1.to_le_bytes().to_vec();
3898                    bytes.extend_from_slice(&hw2.to_le_bytes());
3899                    Ok(bytes)
3900                }
3901            }
3902
3903            ArmOp::Bl { label: _ } => {
3904                // BL is always 32-bit in Thumb-2, encoded here as a relocatable
3905                // placeholder; an R_ARM_THM_CALL relocation patches the target
3906                // (see arm_backend.rs). The placeholder must carry an embedded
3907                // addend of -4 so the relocation nets to exactly the symbol S.
3908                //
3909                // Thumb BL computes `target = (P + 4) + signed_offset`. Under
3910                // R_ARM_THM_CALL the linker resolves using the in-place addend;
3911                // a 0xF800 placeholder (addend 0) lands at S+4 — every call one
3912                // instruction past the callee entry (#174). The correct
3913                // placeholder is what `gas` emits for `bl <extern>`:
3914                //   f7ff fffe  ->  `bl <self>`  (S=1, J1=J2=1, imm = -4 addend),
3915                // i.e. hw1=0xF7FF, hw2=0xFFFE. This nets to S, not S+4.
3916                // (The earlier 0xD000 was worse still — a ~+0x600000 addend,
3917                // the garbage `bl c0000c` and "truncated to fit" of #167.)
3918                let hw1: u16 = 0xF7FF;
3919                let hw2: u16 = 0xFFFE;
3920                let mut bytes = hw1.to_le_bytes().to_vec();
3921                bytes.extend_from_slice(&hw2.to_le_bytes());
3922                Ok(bytes)
3923            }
3924
3925            // MVN
3926            ArmOp::Mvn { rd, op2 } => {
3927                if let Operand2::Reg(rm) = op2 {
3928                    let rd_bits = reg_to_bits(rd) as u16;
3929                    let rm_bits = reg_to_bits(rm) as u16;
3930
3931                    if rd_bits < 8 && rm_bits < 8 {
3932                        // MVNS Rd, Rm (16-bit): 0100 0011 11 Rm Rd
3933                        let instr: u16 = 0x43C0 | (rm_bits << 3) | rd_bits;
3934                        Ok(instr.to_le_bytes().to_vec())
3935                    } else {
3936                        // 32-bit MVN
3937                        let hw1: u16 = 0xEA6F_u16;
3938                        let hw2: u16 = ((reg_to_bits(rd) << 8) | reg_to_bits(rm)) as u16;
3939                        let mut bytes = hw1.to_le_bytes().to_vec();
3940                        bytes.extend_from_slice(&hw2.to_le_bytes());
3941                        Ok(bytes)
3942                    }
3943                } else {
3944                    let instr: u16 = 0xBF00;
3945                    Ok(instr.to_le_bytes().to_vec())
3946                }
3947            }
3948
3949            // MOVW - Move Wide (Thumb-2 32-bit)
3950            ArmOp::Movw { rd, imm16 } => {
3951                self.encode_thumb32_movw_raw(reg_to_bits(rd), *imm16 as u32)
3952            }
3953
3954            // MOVT - Move Top (Thumb-2 32-bit)
3955            ArmOp::Movt { rd, imm16 } => {
3956                self.encode_thumb32_movt_raw(reg_to_bits(rd), *imm16 as u32)
3957            }
3958
3959            // #237: symbol-relative MOVW/MOVT. Encode the addend's low/high 16
3960            // bits in place; the backend records an R_ARM_MOVW_ABS_NC /
3961            // R_ARM_MOVT_ABS relocation against `symbol`, so the linker adds the
3962            // symbol's final address to the in-place addend (REL semantics).
3963            ArmOp::MovwSym { rd, addend, .. } => {
3964                self.encode_thumb32_movw_raw(reg_to_bits(rd), (*addend as u32) & 0xffff)
3965            }
3966            ArmOp::MovtSym { rd, addend, .. } => {
3967                self.encode_thumb32_movt_raw(reg_to_bits(rd), ((*addend as u32) >> 16) & 0xffff)
3968            }
3969
3970            // #345: literal-pool address load — emit a PLACEHOLDER `LDR.W rd,
3971            // [pc, #0]` (U=1, imm12=0). The backend (arm_backend.rs) places the
3972            // 4-byte pool word at the end of the function, records the R_ARM_ABS32
3973            // relocation against `symbol+addend`, and patches the imm12 with the
3974            // real PC-relative distance once the pool offset is known.
3975            // Encoding T2: 1111 1000 1101 1111 | Rt(4) imm12(12), with the literal
3976            // base = Align(PC,4) and PC = address of this instruction + 4.
3977            ArmOp::LdrSym { rd, .. } => {
3978                let rt = reg_to_bits(rd) as u16;
3979                let hw1: u16 = 0xF8DF; // LDR.W (literal), U=1
3980                let hw2: u16 = rt << 12; // imm12 = 0 placeholder
3981                let mut bytes = Vec::with_capacity(4);
3982                bytes.extend_from_slice(&hw1.to_le_bytes());
3983                bytes.extend_from_slice(&hw2.to_le_bytes());
3984                Ok(bytes)
3985            }
3986
3987            // SetCond: Materialize condition flag into register (0 or 1)
3988            // Strategy: ITE <cond>; MOV Rd, #1; MOV Rd, #0
3989            // IMPORTANT: Must use ITE (If-Then-Else) because 16-bit Thumb MOV
3990            // always sets flags (MOVS). We need to evaluate the condition BEFORE
3991            // any MOV instruction clobbers the flags from CMP.
3992            ArmOp::SetCond { rd, cond } => {
3993                let rd_bits = reg_to_bits(rd) as u16;
3994
3995                // Condition code encoding for IT block
3996                use synth_synthesis::Condition;
3997                let cond_bits: u16 = match cond {
3998                    Condition::EQ => 0x0,
3999                    Condition::NE => 0x1,
4000                    Condition::LT => 0xB,
4001                    Condition::LE => 0xD,
4002                    Condition::GT => 0xC,
4003                    Condition::GE => 0xA,
4004                    Condition::LO => 0x3, // CC/LO (unsigned <)
4005                    Condition::LS => 0x9, // LS (unsigned <=)
4006                    Condition::HI => 0x8, // HI (unsigned >)
4007                    Condition::HS => 0x2, // CS/HS (unsigned >=)
4008                };
4009
4010                // ITE <cond>: encodes If-Then-Else block
4011                // The mask field depends on firstcond[0]:
4012                // - If firstcond[0] = 0: mask = 0xC for TE pattern (ITE EQ = BF0C)
4013                // - If firstcond[0] = 1: mask = 0x4 for TE pattern (ITE NE = BF14)
4014                let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4015                let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4016
4017                // Materialize 0/1 into Rd. The 16-bit MOVS (T1) encodes Rd in a
4018                // 3-bit field (bits[10:8]) — only R0–R7. For a high register
4019                // (R8–R12) `rd_bits << 8` overflows into bit 11 and silently
4020                // turns MOVS into CMP (00100 → 00101), corrupting the result
4021                // (this mis-materialized gale's `has_waiter`, so its `local.set`
4022                // stored a stale register → the binary-sem WAKE dispatch read
4023                // garbage). Use the 32-bit MOV.W (T2) for high registers, which
4024                // has a 4-bit Rd field. MOV.W with S=0 doesn't set flags, which
4025                // is fine inside the ITE (the materialized value is the result;
4026                // the flags are not consumed afterwards).
4027                let mut bytes = ite_instr.to_le_bytes().to_vec();
4028                let push_mov = |bytes: &mut Vec<u8>, imm: u16| {
4029                    if rd_bits <= 7 {
4030                        let m: u16 = 0x2000 | (rd_bits << 8) | imm; // 16-bit MOVS Rd,#imm
4031                        bytes.extend_from_slice(&m.to_le_bytes());
4032                    } else {
4033                        // 32-bit MOV.W Rd, #imm (T2): F04F | (Rd<<8) | imm8
4034                        let hw1: u16 = 0xF04F;
4035                        let hw2: u16 = (rd_bits << 8) | imm;
4036                        bytes.extend_from_slice(&hw1.to_le_bytes());
4037                        bytes.extend_from_slice(&hw2.to_le_bytes());
4038                    }
4039                };
4040                push_mov(&mut bytes, 1); // Then branch (condition true)  → 1
4041                push_mov(&mut bytes, 0); // Else branch (condition false) → 0
4042                Ok(bytes)
4043            }
4044
4045            // I64SetCond: Compare two i64 register pairs, result 0/1 in rd
4046            // EQ/NE: CMP lo,lo; IT EQ; CMPEQ hi,hi; ITE <cond>; MOV 1; MOV 0
4047            // LT: CMP lo,lo; SBCS rd,hi,hi; ITE LT; MOV 1; MOV 0
4048            // GT: CMP lo,lo (swapped); SBCS rd,hi,hi (swapped); ITE LT; MOV 1; MOV 0
4049            ArmOp::I64SetCond {
4050                rd,
4051                rn_lo,
4052                rn_hi,
4053                rm_lo,
4054                rm_hi,
4055                cond,
4056            } => {
4057                use synth_synthesis::Condition;
4058                let rd_bits = reg_to_bits(rd) as u16;
4059                let mut bytes = Vec::new();
4060
4061                // Helper: encode CMP Rn, Rm (16-bit)
4062                let encode_cmp_reg = |rn: &synth_synthesis::Reg,
4063                                      rm: &synth_synthesis::Reg|
4064                 -> Vec<u8> {
4065                    let rn_bits = reg_to_bits(rn) as u16;
4066                    let rm_bits = reg_to_bits(rm) as u16;
4067                    if rn_bits < 8 && rm_bits < 8 {
4068                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
4069                        instr.to_le_bytes().to_vec()
4070                    } else {
4071                        let n_bit = (rn_bits >> 3) & 1;
4072                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
4073                        instr.to_le_bytes().to_vec()
4074                    }
4075                };
4076
4077                // Helper: encode ITE <cond> (2 bytes)
4078                let encode_ite = |cond_bits: u16| -> Vec<u8> {
4079                    let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4080                    let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4081                    ite_instr.to_le_bytes().to_vec()
4082                };
4083
4084                // Helper: encode SetCond (ITE + MOV #1 + MOV #0) for given condition
4085                let encode_setcond = |cond_bits: u16, rd_bits: u16| -> Vec<u8> {
4086                    let mut b = encode_ite(cond_bits);
4087                    if rd_bits < 8 {
4088                        let mov_one: u16 = 0x2001 | (rd_bits << 8);
4089                        let mov_zero: u16 = 0x2000 | (rd_bits << 8);
4090                        b.extend_from_slice(&mov_one.to_le_bytes());
4091                        b.extend_from_slice(&mov_zero.to_le_bytes());
4092                    } else {
4093                        // #311: rd >= R8 — the 16-bit MOV imm8 form has a 3-bit
4094                        // rd field; rd_bits<<8 overflows into bit 11 and
4095                        // TRANSMUTES the MOV into CMP (0x2001|0x0800 = 0x2801 =
4096                        // CMP r0,#1): the boolean dies in the flags and the
4097                        // consumer reads a stale register. Use the 32-bit
4098                        // MOV.W (T2: F04F 0000|rd<<8|imm8) — IT-legal,
4099                        // flag-preserving. Same class as H-CODE-9 / #180.
4100                        for imm in [1u16, 0u16] {
4101                            let hw1: u16 = 0xF04F;
4102                            let hw2: u16 = (rd_bits << 8) | imm;
4103                            b.extend_from_slice(&hw1.to_le_bytes());
4104                            b.extend_from_slice(&hw2.to_le_bytes());
4105                        }
4106                    }
4107                    b
4108                };
4109
4110                match cond {
4111                    Condition::EQ | Condition::NE => {
4112                        // CMP rn_lo, rm_lo (compare low words)
4113                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4114
4115                        // IT EQ (execute next instruction only if Z=1)
4116                        let it_eq: u16 = 0xBF08; // IT EQ: cond=0000, mask=1000
4117                        bytes.extend_from_slice(&it_eq.to_le_bytes());
4118
4119                        // CMPEQ rn_hi, rm_hi (compare high words, only if low equal)
4120                        bytes.extend_from_slice(&encode_cmp_reg(rn_hi, rm_hi));
4121
4122                        // ITE <cond>; MOV rd, #1; MOV rd, #0
4123                        let cond_bits: u16 = match cond {
4124                            Condition::EQ => 0x0,
4125                            Condition::NE => 0x1,
4126                            _ => unreachable!(),
4127                        };
4128                        bytes.extend_from_slice(&encode_setcond(cond_bits, rd_bits));
4129                    }
4130
4131                    Condition::LT => {
4132                        // CMP rn_lo, rm_lo (sets C flag for borrow)
4133                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4134
4135                        // SBCS rd, rn_hi, rm_hi (subtract with carry, sets N,V flags)
4136                        // SBCS.W Rd, Rn, Rm: EB70 Rn | 0000 Rd 0000 Rm
4137                        let rn_hi_bits = reg_to_bits(rn_hi);
4138                        let rm_hi_bits = reg_to_bits(rm_hi);
4139                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4140                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4141                        bytes.extend_from_slice(&hw1.to_le_bytes());
4142                        bytes.extend_from_slice(&hw2.to_le_bytes());
4143
4144                        // ITE LT; MOV rd, #1; MOV rd, #0
4145                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4146                    }
4147
4148                    Condition::GT => {
4149                        // GT(a,b) = LT(b,a): swap operands
4150                        // CMP rm_lo, rn_lo (swapped)
4151                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4152
4153                        // SBCS rd, rm_hi, rn_hi (swapped)
4154                        let rm_hi_bits = reg_to_bits(rm_hi);
4155                        let rn_hi_bits = reg_to_bits(rn_hi);
4156                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4157                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4158                        bytes.extend_from_slice(&hw1.to_le_bytes());
4159                        bytes.extend_from_slice(&hw2.to_le_bytes());
4160
4161                        // ITE LT; MOV rd, #1; MOV rd, #0
4162                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4163                    }
4164
4165                    Condition::LE => {
4166                        // LE(a,b) = !GT(a,b): use GT logic but invert result
4167                        // GT(a,b) = LT(b,a): so we do CMP(b,a) and check LT, then invert
4168                        // CMP rm_lo, rn_lo (swapped, same as GT)
4169                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4170
4171                        // SBCS rd, rm_hi, rn_hi (swapped)
4172                        let rm_hi_bits = reg_to_bits(rm_hi);
4173                        let rn_hi_bits = reg_to_bits(rn_hi);
4174                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4175                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4176                        bytes.extend_from_slice(&hw1.to_le_bytes());
4177                        bytes.extend_from_slice(&hw2.to_le_bytes());
4178
4179                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT, so inverting GT result)
4180                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4181                    }
4182
4183                    Condition::GE => {
4184                        // GE(a,b) = !LT(a,b): use LT logic but invert result
4185                        // CMP rn_lo, rm_lo (same as LT)
4186                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4187
4188                        // SBCS rd, rn_hi, rm_hi (same as LT)
4189                        let rn_hi_bits = reg_to_bits(rn_hi);
4190                        let rm_hi_bits = reg_to_bits(rm_hi);
4191                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4192                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4193                        bytes.extend_from_slice(&hw1.to_le_bytes());
4194                        bytes.extend_from_slice(&hw2.to_le_bytes());
4195
4196                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT)
4197                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4198                    }
4199
4200                    // Unsigned comparisons - same instruction sequence, different conditions
4201                    Condition::LO => {
4202                        // LO (unsigned LT): CMP lo, SBCS hi, check C=0
4203                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4204                        let rn_hi_bits = reg_to_bits(rn_hi);
4205                        let rm_hi_bits = reg_to_bits(rm_hi);
4206                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4207                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4208                        bytes.extend_from_slice(&hw1.to_le_bytes());
4209                        bytes.extend_from_slice(&hw2.to_le_bytes());
4210                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4211                    }
4212
4213                    Condition::HI => {
4214                        // HI (unsigned GT): swap operands and check LO
4215                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4216                        let rm_hi_bits = reg_to_bits(rm_hi);
4217                        let rn_hi_bits = reg_to_bits(rn_hi);
4218                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4219                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4220                        bytes.extend_from_slice(&hw1.to_le_bytes());
4221                        bytes.extend_from_slice(&hw2.to_le_bytes());
4222                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4223                    }
4224
4225                    Condition::LS => {
4226                        // LS (unsigned LE): !(a > b) = !(HI), so do HI and invert
4227                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4228                        let rm_hi_bits = reg_to_bits(rm_hi);
4229                        let rn_hi_bits = reg_to_bits(rn_hi);
4230                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4231                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4232                        bytes.extend_from_slice(&hw1.to_le_bytes());
4233                        bytes.extend_from_slice(&hw2.to_le_bytes());
4234                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4235                    }
4236
4237                    Condition::HS => {
4238                        // HS (unsigned GE): !(a < b) = !(LO)
4239                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4240                        let rn_hi_bits = reg_to_bits(rn_hi);
4241                        let rm_hi_bits = reg_to_bits(rm_hi);
4242                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4243                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4244                        bytes.extend_from_slice(&hw1.to_le_bytes());
4245                        bytes.extend_from_slice(&hw2.to_le_bytes());
4246                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4247                    }
4248                }
4249
4250                Ok(bytes)
4251            }
4252
4253            // I64SetCondZ: Test if i64 register pair is zero, result 0/1 in rd
4254            // ORR.W rd, rn_lo, rn_hi; CMP rd, #0; ITE EQ; MOV 1; MOV 0
4255            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
4256                let rd_bits = reg_to_bits(rd);
4257                let rn_lo_bits = reg_to_bits(rn_lo);
4258                let rn_hi_bits = reg_to_bits(rn_hi);
4259                let mut bytes = Vec::new();
4260
4261                // ORR.W rd, rn_lo, rn_hi: EA40 rn_lo | 0000 rd 0000 rn_hi
4262                let hw1: u16 = (0xEA40 | rn_lo_bits) as u16;
4263                let hw2: u16 = ((rd_bits << 8) | rn_hi_bits) as u16;
4264                bytes.extend_from_slice(&hw1.to_le_bytes());
4265                bytes.extend_from_slice(&hw2.to_le_bytes());
4266
4267                // CMP rd, #0 — 16-bit form only for r0-r7 (3-bit rd field);
4268                // high registers take CMP.W (T2: F1B0|rn 0F00|imm8). This was
4269                // H-CODE-9: rd_bits<<8 overflowing the field compared the
4270                // WRONG register. Same hardening as the #311 SetCond fix.
4271                if rd_bits < 8 {
4272                    let cmp_instr: u16 = 0x2800 | ((rd_bits as u16) << 8);
4273                    bytes.extend_from_slice(&cmp_instr.to_le_bytes());
4274                } else {
4275                    let hw1: u16 = 0xF1B0 | (rd_bits as u16);
4276                    let hw2: u16 = 0x0F00;
4277                    bytes.extend_from_slice(&hw1.to_le_bytes());
4278                    bytes.extend_from_slice(&hw2.to_le_bytes());
4279                }
4280
4281                // ITE EQ; MOV rd, #1; MOV rd, #0 (32-bit MOV.W for rd >= R8,
4282                // #311 — see I64SetCond)
4283                let mask = 0xC_u16; // ITE EQ mask: firstcond[0]=0, mask=0xC
4284                let ite_instr: u16 = 0xBF00 | mask;
4285                bytes.extend_from_slice(&ite_instr.to_le_bytes());
4286                if rd_bits < 8 {
4287                    let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
4288                    let mov_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
4289                    bytes.extend_from_slice(&mov_one.to_le_bytes());
4290                    bytes.extend_from_slice(&mov_zero.to_le_bytes());
4291                } else {
4292                    for imm in [1u16, 0u16] {
4293                        let hw1: u16 = 0xF04F;
4294                        let hw2: u16 = ((rd_bits as u16) << 8) | imm;
4295                        bytes.extend_from_slice(&hw1.to_le_bytes());
4296                        bytes.extend_from_slice(&hw2.to_le_bytes());
4297                    }
4298                }
4299
4300                Ok(bytes)
4301            }
4302
4303            // I64Mul: 64-bit multiply using UMULL + MLA cross products
4304            // Formula: result = (a_lo * b_lo) + ((a_lo * b_hi + a_hi * b_lo) << 32)
4305            // Uses R12 as scratch register
4306            ArmOp::I64Mul {
4307                rd_lo,
4308                rd_hi,
4309                rn_lo,
4310                rn_hi,
4311                rm_lo,
4312                rm_hi,
4313            } => {
4314                let rd_lo_bits = reg_to_bits(rd_lo);
4315                let rd_hi_bits = reg_to_bits(rd_hi);
4316                let rn_lo_bits = reg_to_bits(rn_lo);
4317                let rn_hi_bits = reg_to_bits(rn_hi);
4318                let rm_lo_bits = reg_to_bits(rm_lo);
4319                let rm_hi_bits = reg_to_bits(rm_hi);
4320                let r12: u32 = 12; // IP scratch register
4321                let mut bytes = Vec::new();
4322
4323                // 1. MUL R12, rn_lo, rm_hi  (R12 = a_lo * b_hi)
4324                // Thumb-2 MUL: hw1=0xFB00|Rn, hw2=0xF000|(Rd<<8)|Rm
4325                let hw1: u16 = (0xFB00 | rn_lo_bits) as u16;
4326                let hw2: u16 = (0xF000 | (r12 << 8) | rm_hi_bits) as u16;
4327                bytes.extend_from_slice(&hw1.to_le_bytes());
4328                bytes.extend_from_slice(&hw2.to_le_bytes());
4329
4330                // 2. MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
4331                // Thumb-2 MLA: hw1=0xFB00|Rn, hw2=(Ra<<12)|(Rd<<8)|Rm
4332                let hw1: u16 = (0xFB00 | rn_hi_bits) as u16;
4333                let hw2: u16 = ((r12 << 12) | (r12 << 8) | rm_lo_bits) as u16;
4334                bytes.extend_from_slice(&hw1.to_le_bytes());
4335                bytes.extend_from_slice(&hw2.to_le_bytes());
4336
4337                // 3. UMULL rd_lo, rd_hi, rn_lo, rm_lo  (rd_lo:rd_hi = a_lo * b_lo)
4338                // Thumb-2 UMULL: hw1=0xFBA0|Rn, hw2=(RdLo<<12)|(RdHi<<8)|Rm
4339                let hw1: u16 = (0xFBA0 | rn_lo_bits) as u16;
4340                let hw2: u16 = ((rd_lo_bits << 12) | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4341                bytes.extend_from_slice(&hw1.to_le_bytes());
4342                bytes.extend_from_slice(&hw2.to_le_bytes());
4343
4344                // 4. ADD rd_hi, R12  (rd_hi += cross products)
4345                // 16-bit high reg ADD: 01000100 D Rm Rdn[2:0]
4346                let d_bit = (rd_hi_bits >> 3) & 1;
4347                let add_instr: u16 =
4348                    (0x4400 | (d_bit << 7) | (r12 << 3) | (rd_hi_bits & 0x7)) as u16;
4349                bytes.extend_from_slice(&add_instr.to_le_bytes());
4350
4351                Ok(bytes)
4352            }
4353
4354            // I64Shl: 64-bit shift left with branch for n<32 vs n>=32
4355            // rm_hi (R3) is used as temp register
4356            ArmOp::I64Shl {
4357                rd_lo,
4358                rd_hi,
4359                rn_lo,
4360                rn_hi,
4361                rm_lo,
4362                rm_hi,
4363            } => {
4364                let rd_lo_bits = reg_to_bits(rd_lo);
4365                let rd_hi_bits = reg_to_bits(rd_hi);
4366                let rn_lo_bits = reg_to_bits(rn_lo);
4367                let rn_hi_bits = reg_to_bits(rn_hi);
4368                let rm_lo_bits = reg_to_bits(rm_lo);
4369                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4370                let mut bytes = Vec::new();
4371
4372                // AND.W rm_lo, rm_lo, #63  (mask shift amount to 6 bits)
4373                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4374                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4375                bytes.extend_from_slice(&hw1.to_le_bytes());
4376                bytes.extend_from_slice(&hw2.to_le_bytes());
4377
4378                // SUBS.W rm_hi, rm_lo, #32  (rm_hi = n-32, sets flags)
4379                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4380                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4381                bytes.extend_from_slice(&hw1.to_le_bytes());
4382                bytes.extend_from_slice(&hw2.to_le_bytes());
4383
4384                // BPL .large (branch if n >= 32, offset = +10 halfwords)
4385                let bpl: u16 = 0xD50A;
4386                bytes.extend_from_slice(&bpl.to_le_bytes());
4387
4388                // --- Small shift (n < 32) ---
4389                // RSB.W rm_hi, rm_lo, #32  (rm_hi = 32-n)
4390                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4391                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4392                bytes.extend_from_slice(&hw1.to_le_bytes());
4393                bytes.extend_from_slice(&hw2.to_le_bytes());
4394
4395                // LSR.W rm_hi, rn_lo, rm_hi  (rm_hi = lo >> (32-n), overflow bits)
4396                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4397                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4398                bytes.extend_from_slice(&hw1.to_le_bytes());
4399                bytes.extend_from_slice(&hw2.to_le_bytes());
4400
4401                // LSL.W rd_hi, rn_hi, rm_lo  (hi <<= n)
4402                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4403                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4404                bytes.extend_from_slice(&hw1.to_le_bytes());
4405                bytes.extend_from_slice(&hw2.to_le_bytes());
4406
4407                // ORR.W rd_hi, rd_hi, rm_hi  (hi |= overflow bits from lo)
4408                let hw1: u16 = (0xEA40 | rd_hi_bits) as u16;
4409                let hw2: u16 = ((rd_hi_bits << 8) | rm_hi_bits) as u16;
4410                bytes.extend_from_slice(&hw1.to_le_bytes());
4411                bytes.extend_from_slice(&hw2.to_le_bytes());
4412
4413                // LSL.W rd_lo, rn_lo, rm_lo  (lo <<= n)
4414                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4415                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4416                bytes.extend_from_slice(&hw1.to_le_bytes());
4417                bytes.extend_from_slice(&hw2.to_le_bytes());
4418
4419                // B .done (skip large shift: +2 halfwords)
4420                let b_done: u16 = 0xE002;
4421                bytes.extend_from_slice(&b_done.to_le_bytes());
4422
4423                // --- Large shift (n >= 32) ---
4424                // LSL.W rd_hi, rn_lo, rm_hi  (hi = lo << (n-32))
4425                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4426                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_hi_bits) as u16;
4427                bytes.extend_from_slice(&hw1.to_le_bytes());
4428                bytes.extend_from_slice(&hw2.to_le_bytes());
4429
4430                // MOV rd_lo, #0
4431                let mov_zero: u16 = 0x2000 | ((rd_lo_bits as u16) << 8);
4432                bytes.extend_from_slice(&mov_zero.to_le_bytes());
4433
4434                Ok(bytes) // Total: 38 bytes
4435            }
4436
4437            // I64ShrU: 64-bit logical shift right with branch for n<32 vs n>=32
4438            ArmOp::I64ShrU {
4439                rd_lo,
4440                rd_hi,
4441                rn_lo,
4442                rn_hi,
4443                rm_lo,
4444                rm_hi,
4445            } => {
4446                let rd_lo_bits = reg_to_bits(rd_lo);
4447                let rd_hi_bits = reg_to_bits(rd_hi);
4448                let rn_lo_bits = reg_to_bits(rn_lo);
4449                let rn_hi_bits = reg_to_bits(rn_hi);
4450                let rm_lo_bits = reg_to_bits(rm_lo);
4451                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4452                let mut bytes = Vec::new();
4453
4454                // AND.W rm_lo, rm_lo, #63
4455                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4456                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4457                bytes.extend_from_slice(&hw1.to_le_bytes());
4458                bytes.extend_from_slice(&hw2.to_le_bytes());
4459
4460                // SUBS.W rm_hi, rm_lo, #32
4461                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4462                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4463                bytes.extend_from_slice(&hw1.to_le_bytes());
4464                bytes.extend_from_slice(&hw2.to_le_bytes());
4465
4466                // BPL .large (+10 halfwords)
4467                let bpl: u16 = 0xD50A;
4468                bytes.extend_from_slice(&bpl.to_le_bytes());
4469
4470                // --- Small shift (n < 32) ---
4471                // RSB.W rm_hi, rm_lo, #32  (rm_hi = 32-n)
4472                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4473                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4474                bytes.extend_from_slice(&hw1.to_le_bytes());
4475                bytes.extend_from_slice(&hw2.to_le_bytes());
4476
4477                // LSL.W rm_hi, rn_hi, rm_hi  (rm_hi = hi << (32-n), bits flowing to lo)
4478                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4479                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4480                bytes.extend_from_slice(&hw1.to_le_bytes());
4481                bytes.extend_from_slice(&hw2.to_le_bytes());
4482
4483                // LSR.W rd_lo, rn_lo, rm_lo  (lo >>= n)
4484                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4485                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4486                bytes.extend_from_slice(&hw1.to_le_bytes());
4487                bytes.extend_from_slice(&hw2.to_le_bytes());
4488
4489                // ORR.W rd_lo, rd_lo, rm_hi  (lo |= overflow from hi)
4490                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4491                let hw2: u16 = ((rd_lo_bits << 8) | rm_hi_bits) as u16;
4492                bytes.extend_from_slice(&hw1.to_le_bytes());
4493                bytes.extend_from_slice(&hw2.to_le_bytes());
4494
4495                // LSR.W rd_hi, rn_hi, rm_lo  (hi >>= n, logical)
4496                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4497                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4498                bytes.extend_from_slice(&hw1.to_le_bytes());
4499                bytes.extend_from_slice(&hw2.to_le_bytes());
4500
4501                // B .done (+2 halfwords)
4502                let b_done: u16 = 0xE002;
4503                bytes.extend_from_slice(&b_done.to_le_bytes());
4504
4505                // --- Large shift (n >= 32) ---
4506                // LSR.W rd_lo, rn_hi, rm_hi  (lo = hi >> (n-32))
4507                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4508                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_hi_bits) as u16;
4509                bytes.extend_from_slice(&hw1.to_le_bytes());
4510                bytes.extend_from_slice(&hw2.to_le_bytes());
4511
4512                // MOV rd_hi, #0
4513                let mov_zero: u16 = 0x2000 | ((rd_hi_bits as u16) << 8);
4514                bytes.extend_from_slice(&mov_zero.to_le_bytes());
4515
4516                Ok(bytes) // Total: 38 bytes
4517            }
4518
4519            // I64ShrS: 64-bit arithmetic shift right with branch for n<32 vs n>=32
4520            ArmOp::I64ShrS {
4521                rd_lo,
4522                rd_hi,
4523                rn_lo,
4524                rn_hi,
4525                rm_lo,
4526                rm_hi,
4527            } => {
4528                let rd_lo_bits = reg_to_bits(rd_lo);
4529                let rd_hi_bits = reg_to_bits(rd_hi);
4530                let rn_lo_bits = reg_to_bits(rn_lo);
4531                let rn_hi_bits = reg_to_bits(rn_hi);
4532                let rm_lo_bits = reg_to_bits(rm_lo);
4533                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4534                let mut bytes = Vec::new();
4535
4536                // AND.W rm_lo, rm_lo, #63
4537                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4538                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4539                bytes.extend_from_slice(&hw1.to_le_bytes());
4540                bytes.extend_from_slice(&hw2.to_le_bytes());
4541
4542                // SUBS.W rm_hi, rm_lo, #32
4543                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4544                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4545                bytes.extend_from_slice(&hw1.to_le_bytes());
4546                bytes.extend_from_slice(&hw2.to_le_bytes());
4547
4548                // BPL .large (+10 halfwords)
4549                let bpl: u16 = 0xD50A;
4550                bytes.extend_from_slice(&bpl.to_le_bytes());
4551
4552                // --- Small shift (n < 32) ---
4553                // RSB.W rm_hi, rm_lo, #32
4554                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4555                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4556                bytes.extend_from_slice(&hw1.to_le_bytes());
4557                bytes.extend_from_slice(&hw2.to_le_bytes());
4558
4559                // LSL.W rm_hi, rn_hi, rm_hi  (rm_hi = hi << (32-n), bits flowing to lo)
4560                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4561                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4562                bytes.extend_from_slice(&hw1.to_le_bytes());
4563                bytes.extend_from_slice(&hw2.to_le_bytes());
4564
4565                // LSR.W rd_lo, rn_lo, rm_lo  (lo >>= n, logical for lo word)
4566                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4567                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4568                bytes.extend_from_slice(&hw1.to_le_bytes());
4569                bytes.extend_from_slice(&hw2.to_le_bytes());
4570
4571                // ORR.W rd_lo, rd_lo, rm_hi  (lo |= overflow from hi)
4572                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4573                let hw2: u16 = ((rd_lo_bits << 8) | rm_hi_bits) as u16;
4574                bytes.extend_from_slice(&hw1.to_le_bytes());
4575                bytes.extend_from_slice(&hw2.to_le_bytes());
4576
4577                // ASR.W rd_hi, rn_hi, rm_lo  (hi >>= n, arithmetic/sign-extending)
4578                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
4579                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4580                bytes.extend_from_slice(&hw1.to_le_bytes());
4581                bytes.extend_from_slice(&hw2.to_le_bytes());
4582
4583                // B .done (+3 halfwords, large shift is 8 bytes)
4584                let b_done: u16 = 0xE003;
4585                bytes.extend_from_slice(&b_done.to_le_bytes());
4586
4587                // --- Large shift (n >= 32) ---
4588                // ASR.W rd_lo, rn_hi, rm_hi  (lo = hi >>> (n-32))
4589                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
4590                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_hi_bits) as u16;
4591                bytes.extend_from_slice(&hw1.to_le_bytes());
4592                bytes.extend_from_slice(&hw2.to_le_bytes());
4593
4594                // ASR.W rd_hi, rn_hi, #31  (hi = sign extension, all 0s or all 1s)
4595                // Thumb-2 ASR immediate: hw1=0xEA4F, hw2=imm3:Rd:imm2:10:Rm
4596                // imm5=31=11111 → imm3=111, imm2=11
4597                let hw1: u16 = 0xEA4F;
4598                let hw2: u16 = (0x7000 | (rd_hi_bits << 8) | 0x00E0 | rn_hi_bits) as u16;
4599                bytes.extend_from_slice(&hw1.to_le_bytes());
4600                bytes.extend_from_slice(&hw2.to_le_bytes());
4601
4602                Ok(bytes) // Total: 40 bytes
4603            }
4604
4605            // I64Rotl: 64-bit rotate left (#610 rewrite).
4606            // For n < 32: new_hi = (hi << n) | (lo >> (32-n)), new_lo = (lo << n) | (hi >> (32-n))
4607            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
4608            //
4609            // Fixed-reg core: value in R0:R1, amount in R2, scratch R3 + R12
4610            // (all four saved/marshaled by the #610 fixed-ABI wrapper; the
4611            // pre-#610 expansion wrote through the selector's registers with
4612            // colliding R3/R4 scratch and restored the saved R4 OVER the
4613            // result). Relies on ARM register-shift semantics: amounts >= 32
4614            // yield 0 for LSL/LSR, which makes n = 0 and n = 32 exact.
4615            ArmOp::I64Rotl {
4616                rdlo,
4617                rdhi,
4618                rnlo,
4619                rnhi,
4620                shift,
4621            } => {
4622                let mut bytes = Vec::new();
4623                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
4624
4625                let core: [u16; 35] = [
4626                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
4627                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
4628                    0xD50E, //         BPL    .large        (n >= 32)
4629                    // --- small rotation (n < 32) ---
4630                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
4631                    0xFA20, 0xFC03, // LSR.W  R12, R0, R3   (lo >> (32-n))
4632                    0xFA21, 0xF303, // LSR.W  R3, R1, R3    (hi >> (32-n))
4633                    0xFA01, 0xF102, // LSL.W  R1, R1, R2    (hi << n)
4634                    0xEA41, 0x010C, // ORR.W  R1, R1, R12   (new_hi)
4635                    0xFA00, 0xF002, // LSL.W  R0, R0, R2    (lo << n)
4636                    0xEA40, 0x0003, // ORR.W  R0, R0, R3    (new_lo)
4637                    0xE00E, //         B      .done
4638                    // --- large rotation (n >= 32), R3 = m = n-32 ---
4639                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
4640                    0xFA21, 0xFC02, // LSR.W  R12, R1, R2   (hi >> (64-n))
4641                    0xFA20, 0xF202, // LSR.W  R2, R0, R2    (lo >> (64-n))
4642                    0xFA00, 0xF003, // LSL.W  R0, R0, R3    (lo << m)
4643                    0xFA01, 0xF103, // LSL.W  R1, R1, R3    (hi << m)
4644                    0xEA40, 0x0C0C, // ORR.W  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
4645                    0xEA41, 0x0002, // ORR.W  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
4646                    0x4661, //         MOV    R1, R12       (new_hi into place)
4647                            // .done: result in R0:R1
4648                ];
4649                for hw in core {
4650                    bytes.extend_from_slice(&hw.to_le_bytes());
4651                }
4652
4653                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
4654                Ok(bytes) // Total: 102 bytes
4655            }
4656
4657            // I64Rotr: 64-bit rotate right (#610 rewrite).
4658            // For n < 32: new_lo = (lo >> n) | (hi << (32-n)), new_hi = (hi >> n) | (lo << (32-n))
4659            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
4660            //
4661            // Same fixed-reg core contract as I64Rotl: value in R0:R1, amount
4662            // in R2, scratch R3 + R12, all covered by the fixed-ABI wrapper.
4663            ArmOp::I64Rotr {
4664                rdlo,
4665                rdhi,
4666                rnlo,
4667                rnhi,
4668                shift,
4669            } => {
4670                let mut bytes = Vec::new();
4671                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
4672
4673                let core: [u16; 35] = [
4674                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
4675                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
4676                    0xD50E, //         BPL    .large        (n >= 32)
4677                    // --- small rotation (n < 32) ---
4678                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
4679                    0xFA01, 0xFC03, // LSL.W  R12, R1, R3   (hi << (32-n))
4680                    0xFA00, 0xF303, // LSL.W  R3, R0, R3    (lo << (32-n))
4681                    0xFA20, 0xF002, // LSR.W  R0, R0, R2    (lo >> n)
4682                    0xEA40, 0x000C, // ORR.W  R0, R0, R12   (new_lo)
4683                    0xFA21, 0xF102, // LSR.W  R1, R1, R2    (hi >> n)
4684                    0xEA41, 0x0103, // ORR.W  R1, R1, R3    (new_hi)
4685                    0xE00E, //         B      .done
4686                    // --- large rotation (n >= 32), R3 = m = n-32 ---
4687                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
4688                    0xFA00, 0xFC02, // LSL.W  R12, R0, R2   (lo << (64-n))
4689                    0xFA01, 0xF202, // LSL.W  R2, R1, R2    (hi << (64-n))
4690                    0xFA21, 0xF103, // LSR.W  R1, R1, R3    (hi >> m)
4691                    0xEA41, 0x0C0C, // ORR.W  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
4692                    0xFA20, 0xF103, // LSR.W  R1, R0, R3    (lo >> m)
4693                    0xEA41, 0x0102, // ORR.W  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
4694                    0x4660, //         MOV    R0, R12       (new_lo into place)
4695                            // .done: result in R0:R1
4696                ];
4697                for hw in core {
4698                    bytes.extend_from_slice(&hw.to_le_bytes());
4699                }
4700
4701                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
4702                Ok(bytes) // Total: 102 bytes
4703            }
4704
4705            // I64Clz: Count leading zeros in 64-bit value
4706            // If hi != 0: result = CLZ(hi)
4707            // If hi == 0: result = 32 + CLZ(lo)
4708            //
4709            // Layout (using CMP+BNE approach for consistency):
4710            // 0: CMP.W rnhi, #0 (4 bytes)
4711            // 4: BEQ .hi_zero (2 bytes) - branch forward to offset 14
4712            // 6: CLZ.W rd, rnhi (4 bytes)
4713            // 10: B .done (2 bytes) - branch forward to offset 22
4714            // 12: NOP (2 bytes) - padding for alignment
4715            // 14: .hi_zero: CLZ.W rd, rnlo (4 bytes)
4716            // 18: ADD.W rd, rd, #32 (4 bytes)
4717            // 22: .done
4718            ArmOp::I64Clz { rd, rnlo, rnhi } => {
4719                let rd_bits = reg_to_bits(rd);
4720                let rn_lo_bits = reg_to_bits(rnlo);
4721                let rn_hi_bits = reg_to_bits(rnhi);
4722                let mut bytes = Vec::new();
4723
4724                // CMP.W rnhi, #0 (4 bytes at offset 0)
4725                let hw1: u16 = (0xF1B0 | rn_hi_bits) as u16;
4726                let hw2: u16 = 0x0F00;
4727                bytes.extend_from_slice(&hw1.to_le_bytes());
4728                bytes.extend_from_slice(&hw2.to_le_bytes());
4729
4730                // BEQ .hi_zero (2 bytes at offset 4)
4731                // PC = 4 + 4 = 8, target = 14, offset = 6, imm8 = 3
4732                let beq: u16 = 0xD003;
4733                bytes.extend_from_slice(&beq.to_le_bytes());
4734
4735                // CLZ.W rd, rnhi (4 bytes at offset 6)
4736                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4737                let hw1: u16 = (0xFAB0 | rn_hi_bits) as u16;
4738                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_hi_bits) as u16;
4739                bytes.extend_from_slice(&hw1.to_le_bytes());
4740                bytes.extend_from_slice(&hw2.to_le_bytes());
4741
4742                // B .done (2 bytes at offset 10)
4743                // PC = 10 + 4 = 14, target = 22, offset = 8, imm11 = 4
4744                let b_done: u16 = 0xE004;
4745                bytes.extend_from_slice(&b_done.to_le_bytes());
4746
4747                // NOP (2 bytes at offset 12) - padding
4748                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
4749
4750                // .hi_zero: (offset 14)
4751                // CLZ.W rd, rnlo (4 bytes)
4752                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4753                let hw1: u16 = (0xFAB0 | rn_lo_bits) as u16;
4754                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_lo_bits) as u16;
4755                bytes.extend_from_slice(&hw1.to_le_bytes());
4756                bytes.extend_from_slice(&hw2.to_le_bytes());
4757
4758                // ADD.W rd, rd, #32 (4 bytes at offset 18)
4759                let hw1: u16 = (0xF100 | rd_bits) as u16;
4760                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
4761                bytes.extend_from_slice(&hw1.to_le_bytes());
4762                bytes.extend_from_slice(&hw2.to_le_bytes());
4763
4764                // .done: (offset 22)
4765                // i64.clz returns i64, so clear high word: MOV rnhi, #0 (2 bytes)
4766                // MOVS Rn, #0: 0010 0 Rn 00000000
4767                let mov0: u16 = (0x2000 | (rn_hi_bits << 8)) as u16;
4768                bytes.extend_from_slice(&mov0.to_le_bytes());
4769
4770                Ok(bytes)
4771            }
4772
4773            // I64Ctz: Count trailing zeros in 64-bit value
4774            // If lo != 0: result = CTZ(lo) = CLZ(RBIT(lo))
4775            // If lo == 0: result = 32 + CTZ(hi) = 32 + CLZ(RBIT(hi))
4776            //
4777            // Layout:
4778            // 0: CMP.W rnlo, #0 (4 bytes)
4779            // 4: BEQ .lo_zero (2 bytes) - branch to offset 18
4780            // 6: RBIT.W rd, rnlo (4 bytes)
4781            // 10: CLZ.W rd, rd (4 bytes)
4782            // 14: B .done (2 bytes) - branch to offset 30
4783            // 16: NOP (2 bytes) - padding
4784            // 18: .lo_zero: RBIT.W rd, rnhi (4 bytes)
4785            // 22: CLZ.W rd, rd (4 bytes)
4786            // 26: ADD.W rd, rd, #32 (4 bytes)
4787            // 30: .done
4788            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
4789                let rd_bits = reg_to_bits(rd);
4790                let rn_lo_bits = reg_to_bits(rnlo);
4791                let rn_hi_bits = reg_to_bits(rnhi);
4792                let mut bytes = Vec::new();
4793
4794                // CMP.W rnlo, #0 (4 bytes at offset 0)
4795                let hw1: u16 = (0xF1B0 | rn_lo_bits) as u16;
4796                let hw2: u16 = 0x0F00;
4797                bytes.extend_from_slice(&hw1.to_le_bytes());
4798                bytes.extend_from_slice(&hw2.to_le_bytes());
4799
4800                // BEQ .lo_zero (2 bytes at offset 4)
4801                // PC = 4 + 4 = 8, target = 18, offset = 10, imm8 = 5
4802                let beq: u16 = 0xD005;
4803                bytes.extend_from_slice(&beq.to_le_bytes());
4804
4805                // RBIT.W rd, rnlo (4 bytes at offset 6)
4806                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
4807                let hw1: u16 = (0xFA90 | rn_lo_bits) as u16;
4808                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_lo_bits) as u16;
4809                bytes.extend_from_slice(&hw1.to_le_bytes());
4810                bytes.extend_from_slice(&hw2.to_le_bytes());
4811
4812                // CLZ.W rd, rd (4 bytes at offset 10)
4813                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4814                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
4815                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
4816                bytes.extend_from_slice(&hw1.to_le_bytes());
4817                bytes.extend_from_slice(&hw2.to_le_bytes());
4818
4819                // B .done (2 bytes at offset 14)
4820                // PC = 14 + 4 = 18, target = 30, offset = 12, imm11 = 6
4821                let b_done: u16 = 0xE006;
4822                bytes.extend_from_slice(&b_done.to_le_bytes());
4823
4824                // NOP (2 bytes at offset 16) - padding
4825                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
4826
4827                // .lo_zero: (offset 18)
4828                // RBIT.W rd, rnhi (4 bytes)
4829                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
4830                let hw1: u16 = (0xFA90 | rn_hi_bits) as u16;
4831                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_hi_bits) as u16;
4832                bytes.extend_from_slice(&hw1.to_le_bytes());
4833                bytes.extend_from_slice(&hw2.to_le_bytes());
4834
4835                // CLZ.W rd, rd (4 bytes at offset 22)
4836                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4837                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
4838                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
4839                bytes.extend_from_slice(&hw1.to_le_bytes());
4840                bytes.extend_from_slice(&hw2.to_le_bytes());
4841
4842                // ADD.W rd, rd, #32 (4 bytes at offset 26)
4843                let hw1: u16 = (0xF100 | rd_bits) as u16;
4844                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
4845                bytes.extend_from_slice(&hw1.to_le_bytes());
4846                bytes.extend_from_slice(&hw2.to_le_bytes());
4847
4848                // .done: (offset 30)
4849                // i64.ctz returns i64, so clear high word: MOV rnhi, #0 (2 bytes)
4850                let mov0: u16 = (0x2000 | (rn_hi_bits << 8)) as u16;
4851                bytes.extend_from_slice(&mov0.to_le_bytes());
4852
4853                Ok(bytes)
4854            }
4855
4856            // I64Popcnt: Population count of 64-bit value
4857            // result = POPCNT(lo) + POPCNT(hi)
4858            // Using SIMD-style parallel bit counting algorithm
4859            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
4860                let rd_bits = reg_to_bits(rd);
4861                let rn_lo_bits = reg_to_bits(rnlo);
4862                let rn_hi_bits = reg_to_bits(rnhi);
4863                let r12: u32 = 12; // IP scratch
4864                let r3: u32 = 3; // Scratch for hi popcnt result
4865                let mut bytes = Vec::new();
4866
4867                // PUSH {R3, R4, R5} - save scratch registers
4868                bytes.extend_from_slice(&0xB438u16.to_le_bytes());
4869
4870                // Strategy: compute popcnt(lo) -> R4, popcnt(hi) -> R5, add them -> rd
4871                // Using lookup table approach for each byte would be too large
4872                // Using shift-and-add approach instead
4873
4874                // For simplicity and correctness, use the efficient parallel algorithm
4875                // but implement it as a series of inline operations
4876
4877                // Marshal the operand pair into the fixed scratch regs, routing
4878                // rnlo through R12 (#632 audit): writing R4 first corrupted the
4879                // rnhi read for a pair living at (R3,R4) — every source is read
4880                // before any scratch register it could occupy is written.
4881                // MOV R12, rnlo
4882                let mov: u16 = (0x4600 | (1 << 7) | (rn_lo_bits << 3) | 4) as u16;
4883                bytes.extend_from_slice(&mov.to_le_bytes());
4884                // MOV R5, rnhi (R4 untouched so far; rnhi == R5 is a no-op)
4885                let mov: u16 = (0x4600 | (rn_hi_bits << 3) | 5) as u16;
4886                bytes.extend_from_slice(&mov.to_le_bytes());
4887                // MOV R4, R12
4888                bytes.extend_from_slice(&0x4664u16.to_le_bytes());
4889
4890                // --- POPCNT for R4 (lo word) ---
4891                // Step 1: x = x - ((x >> 1) & 0x55555555)
4892                // LSR.W R12, R4, #1
4893                let hw1: u16 = 0xEA4F;
4894                let hw2: u16 = ((r12 << 8) | 0x50 | 4) as u16;
4895                bytes.extend_from_slice(&hw1.to_le_bytes());
4896                bytes.extend_from_slice(&hw2.to_le_bytes());
4897
4898                // Load 0x55555555 into R3 using MOVW/MOVT
4899                // MOVW R3, #0x5555
4900                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
4901                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
4902                // MOVT R3, #0x5555
4903                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
4904                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
4905
4906                // AND.W R12, R12, R3
4907                let hw1: u16 = (0xEA00 | r12) as u16;
4908                let hw2: u16 = ((r12 << 8) | r3) as u16;
4909                bytes.extend_from_slice(&hw1.to_le_bytes());
4910                bytes.extend_from_slice(&hw2.to_le_bytes());
4911
4912                // SUB.W R4, R4, R12
4913                let hw1: u16 = (0xEBA0 | 4) as u16;
4914                let hw2: u16 = ((4 << 8) | r12) as u16;
4915                bytes.extend_from_slice(&hw1.to_le_bytes());
4916                bytes.extend_from_slice(&hw2.to_le_bytes());
4917
4918                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
4919                // Load 0x33333333 into R3
4920                // MOVW R3, #0x3333
4921                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
4922                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
4923                // MOVT R3, #0x3333
4924                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
4925                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
4926
4927                // AND.W R12, R4, R3
4928                let hw1: u16 = (0xEA00 | 4) as u16;
4929                let hw2: u16 = ((r12 << 8) | r3) as u16;
4930                bytes.extend_from_slice(&hw1.to_le_bytes());
4931                bytes.extend_from_slice(&hw2.to_le_bytes());
4932
4933                // LSR.W R4, R4, #2
4934                let hw1: u16 = 0xEA4F;
4935                let hw2: u16 = ((4 << 8) | 0x90 | 4) as u16;
4936                bytes.extend_from_slice(&hw1.to_le_bytes());
4937                bytes.extend_from_slice(&hw2.to_le_bytes());
4938
4939                // AND.W R4, R4, R3
4940                let hw1: u16 = (0xEA00 | 4) as u16;
4941                let hw2: u16 = ((4 << 8) | r3) as u16;
4942                bytes.extend_from_slice(&hw1.to_le_bytes());
4943                bytes.extend_from_slice(&hw2.to_le_bytes());
4944
4945                // ADD.W R4, R4, R12
4946                let hw1: u16 = (0xEB00 | 4) as u16;
4947                let hw2: u16 = ((4 << 8) | r12) as u16;
4948                bytes.extend_from_slice(&hw1.to_le_bytes());
4949                bytes.extend_from_slice(&hw2.to_le_bytes());
4950
4951                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
4952                // LSR.W R12, R4, #4
4953                // hw2 = (imm3 << 12) | (Rd << 8) | (imm2 << 6) | (type << 4) | Rm
4954                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
4955                let hw1: u16 = 0xEA4F;
4956                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 4) as u16;
4957                bytes.extend_from_slice(&hw1.to_le_bytes());
4958                bytes.extend_from_slice(&hw2.to_le_bytes());
4959
4960                // ADD.W R4, R4, R12
4961                let hw1: u16 = (0xEB00 | 4) as u16;
4962                let hw2: u16 = ((4 << 8) | r12) as u16;
4963                bytes.extend_from_slice(&hw1.to_le_bytes());
4964                bytes.extend_from_slice(&hw2.to_le_bytes());
4965
4966                // Load 0x0F0F0F0F into R3
4967                // MOVW R3, #0x0F0F (imm4=0, i=1, imm3=7, imm8=0x0F)
4968                // hw1 = 11110 1 10 0100 0000 = 0xF640
4969                // hw2 = 0 111 0011 00001111 = 0x730F
4970                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
4971                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
4972                // MOVT R3, #0x0F0F
4973                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
4974                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
4975
4976                // AND.W R4, R4, R3
4977                let hw1: u16 = (0xEA00 | 4) as u16;
4978                let hw2: u16 = ((4 << 8) | r3) as u16;
4979                bytes.extend_from_slice(&hw1.to_le_bytes());
4980                bytes.extend_from_slice(&hw2.to_le_bytes());
4981
4982                // Step 4: x = x * 0x01010101 >> 24
4983                // Load 0x01010101 into R3
4984                // MOVW R3, #0x0101
4985                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
4986                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
4987                // MOVT R3, #0x0101
4988                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
4989                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
4990
4991                // MUL R4, R4, R3
4992                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
4993                let hw1: u16 = (0xFB00 | 4) as u16;
4994                let hw2: u16 = (0xF000 | (4 << 8) | r3) as u16;
4995                bytes.extend_from_slice(&hw1.to_le_bytes());
4996                bytes.extend_from_slice(&hw2.to_le_bytes());
4997
4998                // LSR.W R4, R4, #24
4999                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5000                let hw1: u16 = 0xEA4F;
5001                let hw2: u16 = (0x6000 | (4 << 8) | 0x10 | 4) as u16;
5002                bytes.extend_from_slice(&hw1.to_le_bytes());
5003                bytes.extend_from_slice(&hw2.to_le_bytes());
5004
5005                // --- POPCNT for R5 (hi word) - same algorithm ---
5006                // Step 1
5007                let hw1: u16 = 0xEA4F;
5008                let hw2: u16 = ((r12 << 8) | 0x50 | 5) as u16;
5009                bytes.extend_from_slice(&hw1.to_le_bytes());
5010                bytes.extend_from_slice(&hw2.to_le_bytes());
5011
5012                // Load 0x55555555 into R3
5013                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5014                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5015                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5016                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5017
5018                let hw1: u16 = (0xEA00 | r12) as u16;
5019                let hw2: u16 = ((r12 << 8) | r3) as u16;
5020                bytes.extend_from_slice(&hw1.to_le_bytes());
5021                bytes.extend_from_slice(&hw2.to_le_bytes());
5022
5023                let hw1: u16 = (0xEBA0 | 5) as u16;
5024                let hw2: u16 = ((5 << 8) | r12) as u16;
5025                bytes.extend_from_slice(&hw1.to_le_bytes());
5026                bytes.extend_from_slice(&hw2.to_le_bytes());
5027
5028                // Step 2
5029                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5030                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5031                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5032                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5033
5034                let hw1: u16 = (0xEA00 | 5) as u16;
5035                let hw2: u16 = ((r12 << 8) | r3) as u16;
5036                bytes.extend_from_slice(&hw1.to_le_bytes());
5037                bytes.extend_from_slice(&hw2.to_le_bytes());
5038
5039                let hw1: u16 = 0xEA4F;
5040                let hw2: u16 = ((5 << 8) | 0x90 | 5) as u16;
5041                bytes.extend_from_slice(&hw1.to_le_bytes());
5042                bytes.extend_from_slice(&hw2.to_le_bytes());
5043
5044                let hw1: u16 = (0xEA00 | 5) as u16;
5045                let hw2: u16 = ((5 << 8) | r3) as u16;
5046                bytes.extend_from_slice(&hw1.to_le_bytes());
5047                bytes.extend_from_slice(&hw2.to_le_bytes());
5048
5049                let hw1: u16 = (0xEB00 | 5) as u16;
5050                let hw2: u16 = ((5 << 8) | r12) as u16;
5051                bytes.extend_from_slice(&hw1.to_le_bytes());
5052                bytes.extend_from_slice(&hw2.to_le_bytes());
5053
5054                // Step 3: LSR.W R12, R5, #4
5055                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5056                let hw1: u16 = 0xEA4F;
5057                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 5) as u16;
5058                bytes.extend_from_slice(&hw1.to_le_bytes());
5059                bytes.extend_from_slice(&hw2.to_le_bytes());
5060
5061                let hw1: u16 = (0xEB00 | 5) as u16;
5062                let hw2: u16 = ((5 << 8) | r12) as u16;
5063                bytes.extend_from_slice(&hw1.to_le_bytes());
5064                bytes.extend_from_slice(&hw2.to_le_bytes());
5065
5066                // Load 0x0F0F0F0F into R3 (for hi-word)
5067                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5068                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5069                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5070                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5071
5072                let hw1: u16 = (0xEA00 | 5) as u16;
5073                let hw2: u16 = ((5 << 8) | r3) as u16;
5074                bytes.extend_from_slice(&hw1.to_le_bytes());
5075                bytes.extend_from_slice(&hw2.to_le_bytes());
5076
5077                // Step 4
5078                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5079                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5080                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5081                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5082
5083                // MUL R5, R5, R3
5084                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5085                let hw1: u16 = (0xFB00 | 5) as u16;
5086                let hw2: u16 = (0xF000 | (5 << 8) | r3) as u16;
5087                bytes.extend_from_slice(&hw1.to_le_bytes());
5088                bytes.extend_from_slice(&hw2.to_le_bytes());
5089
5090                // LSR.W R5, R5, #24
5091                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5092                let hw1: u16 = 0xEA4F;
5093                let hw2: u16 = (0x6000 | (5 << 8) | 0x10 | 5) as u16;
5094                bytes.extend_from_slice(&hw1.to_le_bytes());
5095                bytes.extend_from_slice(&hw2.to_le_bytes());
5096
5097                // #632: the count must be carried ACROSS the scratch restore
5098                // in a register the POP cannot touch. rd is allocator-assigned
5099                // (any of R0-R8) and can land inside the {R3,R4,R5} restore set
5100                // — the old `ADDS rd, R4, R5; POP {R3,R4,R5}` destroyed the
5101                // result one instruction after computing it (0 for every input
5102                // under qemu). R12 is encoder scratch: never allocatable (#212)
5103                // and never in a restore set, so no choice of rd can collide.
5104                // ADD.W R12, R4, R5
5105                bytes.extend_from_slice(&0xEB04u16.to_le_bytes());
5106                bytes.extend_from_slice(&0x0C05u16.to_le_bytes());
5107
5108                // POP {R3, R4, R5}
5109                bytes.extend_from_slice(&0xBC38u16.to_le_bytes());
5110
5111                // MOV rd, R12 — after the restore. The 4-bit Rd (D:rd) form is
5112                // also total over rd = R8, where the old ADDS T1 3-bit field
5113                // silently corrupted the encoding (#178/#180 class).
5114                let mov: u16 =
5115                    (0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7)) as u16;
5116                bytes.extend_from_slice(&mov.to_le_bytes());
5117
5118                // i64.popcnt returns i64, so clear high word: MOV.W rnhi, #0
5119                // (T2, 4 bytes — total over rnhi = R8, where the old 16-bit
5120                // MOVS encoding overflowed its 3-bit field into CMP R0, #0).
5121                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5122                bytes.extend_from_slice(&(((rn_hi_bits & 0xF) << 8) as u16).to_le_bytes());
5123
5124                Ok(bytes)
5125            }
5126
5127            // I64Extend8S: Sign-extend low 8 bits to 64 bits
5128            // Result: rdlo = sign_extend_8(rnlo), rdhi = rdlo >> 31
5129            ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
5130                let rdlo_bits = reg_to_bits(rdlo);
5131                let rdhi_bits = reg_to_bits(rdhi);
5132                let rnlo_bits = reg_to_bits(rnlo);
5133                let mut bytes = Vec::new();
5134
5135                // SXTB.W rdlo, rnlo (sign-extend byte to 32-bit)
5136                // SXTB T2: hw1 = 0xFA4F, hw2 = 0xF0<Rd><Rm>
5137                let hw1: u16 = 0xFA4F_u16;
5138                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5139                bytes.extend_from_slice(&hw1.to_le_bytes());
5140                bytes.extend_from_slice(&hw2.to_le_bytes());
5141
5142                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5143                // ASR (immediate): hw1 = 0xEA4F, hw2 = imm3:Rd:imm2:type:Rm
5144                // For imm5=31: imm3=111, imm2=11, type=10 (ASR)
5145                // hw2 = (7 << 12) | (rdhi << 8) | (3 << 6) | (2 << 4) | rdlo
5146                let hw1: u16 = 0xEA4F;
5147                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5148                bytes.extend_from_slice(&hw1.to_le_bytes());
5149                bytes.extend_from_slice(&hw2.to_le_bytes());
5150
5151                Ok(bytes)
5152            }
5153
5154            // I64Extend16S: Sign-extend low 16 bits to 64 bits
5155            // Result: rdlo = sign_extend_16(rnlo), rdhi = rdlo >> 31
5156            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
5157                let rdlo_bits = reg_to_bits(rdlo);
5158                let rdhi_bits = reg_to_bits(rdhi);
5159                let rnlo_bits = reg_to_bits(rnlo);
5160                let mut bytes = Vec::new();
5161
5162                // SXTH.W rdlo, rnlo (sign-extend halfword to 32-bit)
5163                // SXTH T2: hw1 = 0xFA0F, hw2 = 0xF0<Rd><Rm>
5164                let hw1: u16 = 0xFA0F_u16;
5165                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5166                bytes.extend_from_slice(&hw1.to_le_bytes());
5167                bytes.extend_from_slice(&hw2.to_le_bytes());
5168
5169                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5170                let hw1: u16 = 0xEA4F;
5171                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5172                bytes.extend_from_slice(&hw1.to_le_bytes());
5173                bytes.extend_from_slice(&hw2.to_le_bytes());
5174
5175                Ok(bytes)
5176            }
5177
5178            // I64Extend32S: Sign-extend low 32 bits to 64 bits
5179            // Result: rdlo = rnlo, rdhi = rnlo >> 31
5180            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
5181                let rdlo_bits = reg_to_bits(rdlo);
5182                let rdhi_bits = reg_to_bits(rdhi);
5183                let rnlo_bits = reg_to_bits(rnlo);
5184                let mut bytes = Vec::new();
5185
5186                // MOV rdlo, rnlo (if different)
5187                if rdlo_bits != rnlo_bits {
5188                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5189                    let d_bit = ((rdlo_bits >> 3) & 1) as u16;
5190                    let mov: u16 = 0x4600
5191                        | (d_bit << 7)
5192                        | ((rnlo_bits as u16) << 3)
5193                        | ((rdlo_bits & 0x7) as u16);
5194                    bytes.extend_from_slice(&mov.to_le_bytes());
5195                }
5196
5197                // ASR.W rdhi, rnlo, #31 (sign-extend to high word)
5198                let hw1: u16 = 0xEA4F;
5199                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rnlo_bits) as u16;
5200                bytes.extend_from_slice(&hw1.to_le_bytes());
5201                bytes.extend_from_slice(&hw2.to_le_bytes());
5202
5203                Ok(bytes)
5204            }
5205
5206            // SelectMove: IT <cond>; MOV{cond} rd, rm
5207            // Conditional move: only execute MOV if condition is true
5208            ArmOp::SelectMove { rd, rm, cond } => {
5209                let rd_bits = reg_to_bits(rd) as u16;
5210                let rm_bits = reg_to_bits(rm) as u16;
5211
5212                // Condition code encoding for IT block
5213                use synth_synthesis::Condition;
5214                let cond_bits: u16 = match cond {
5215                    Condition::EQ => 0x0, // Equal
5216                    Condition::NE => 0x1, // Not equal
5217                    Condition::HS => 0x2, // Higher or same (unsigned >=)
5218                    Condition::LO => 0x3, // Lower (unsigned <)
5219                    Condition::HI => 0x8, // Higher (unsigned >)
5220                    Condition::LS => 0x9, // Lower or same (unsigned <=)
5221                    Condition::GE => 0xA, // Greater or equal (signed)
5222                    Condition::LT => 0xB, // Less than (signed)
5223                    Condition::GT => 0xC, // Greater than (signed)
5224                    Condition::LE => 0xD, // Less or equal (signed)
5225                };
5226
5227                // IT <cond>: single Then block (mask = 0x8 for T only)
5228                // IT instruction: 1011 1111 firstcond mask
5229                let it_instr: u16 = 0xBF00 | (cond_bits << 4) | 0x8;
5230
5231                // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5232                // This MOV will only execute if condition is true due to IT block
5233                let d_bit = (rd_bits >> 3) & 1;
5234                let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5235
5236                // Emit: IT <cond>, MOV rd, rm
5237                let mut bytes = it_instr.to_le_bytes().to_vec();
5238                bytes.extend_from_slice(&mov_instr.to_le_bytes());
5239                Ok(bytes)
5240            }
5241
5242            // Popcnt: Population count (count set bits)
5243            // ARM Cortex-M has no native POPCNT, so we implement the bit manipulation algorithm:
5244            // x = x - ((x >> 1) & 0x55555555);
5245            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
5246            // x = (x + (x >> 4)) & 0x0F0F0F0F;
5247            // x = x + (x >> 8);
5248            // x = x + (x >> 16);
5249            // return x & 0x3F;
5250            //
5251            // Uses rd as working register and R12 as scratch for constants
5252            ArmOp::Popcnt { rd, rm } => {
5253                let mut bytes = Vec::new();
5254
5255                // First, move rm to rd if they're different
5256                if rd != rm {
5257                    let rd_bits = reg_to_bits(rd) as u16;
5258                    let rm_bits = reg_to_bits(rm) as u16;
5259                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5260                    let d_bit = (rd_bits >> 3) & 1;
5261                    let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5262                    bytes.extend_from_slice(&mov_instr.to_le_bytes());
5263                }
5264
5265                // Step 1: x = x - ((x >> 1) & 0x55555555)
5266                // Load 0x55555555 into R12
5267                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x5555)?);
5268                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x5555)?);
5269
5270                // R12_temp = rd >> 1
5271                // We need a second scratch register. Use R11.
5272                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 1)?);
5273
5274                // R11 = R11 & R12 (R11 = (x >> 1) & 0x55555555)
5275                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(11, 11, 12)?);
5276
5277                // rd = rd - R11
5278                bytes.extend_from_slice(&self.encode_thumb32_sub_reg_raw(
5279                    reg_to_bits(rd),
5280                    reg_to_bits(rd),
5281                    11,
5282                )?);
5283
5284                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5285                // Load 0x33333333 into R12
5286                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x3333)?);
5287                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x3333)?);
5288
5289                // R11 = rd & R12
5290                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5291                    11,
5292                    reg_to_bits(rd),
5293                    12,
5294                )?);
5295
5296                // rd = rd >> 2
5297                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(
5298                    reg_to_bits(rd),
5299                    reg_to_bits(rd),
5300                    2,
5301                )?);
5302
5303                // rd = rd & R12
5304                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5305                    reg_to_bits(rd),
5306                    reg_to_bits(rd),
5307                    12,
5308                )?);
5309
5310                // rd = rd + R11
5311                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5312                    reg_to_bits(rd),
5313                    reg_to_bits(rd),
5314                    11,
5315                )?);
5316
5317                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5318                // R11 = rd >> 4
5319                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 4)?);
5320
5321                // rd = rd + R11
5322                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5323                    reg_to_bits(rd),
5324                    reg_to_bits(rd),
5325                    11,
5326                )?);
5327
5328                // Load 0x0F0F0F0F into R12
5329                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x0F0F)?);
5330                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x0F0F)?);
5331
5332                // rd = rd & R12
5333                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5334                    reg_to_bits(rd),
5335                    reg_to_bits(rd),
5336                    12,
5337                )?);
5338
5339                // Step 4: x = x + (x >> 8)
5340                // R11 = rd >> 8
5341                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 8)?);
5342
5343                // rd = rd + R11
5344                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5345                    reg_to_bits(rd),
5346                    reg_to_bits(rd),
5347                    11,
5348                )?);
5349
5350                // Step 5: x = x + (x >> 16)
5351                // R11 = rd >> 16
5352                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 16)?);
5353
5354                // rd = rd + R11
5355                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5356                    reg_to_bits(rd),
5357                    reg_to_bits(rd),
5358                    11,
5359                )?);
5360
5361                // Step 6: return x & 0x3F
5362                // AND with 0x3F (small immediate, can use BIC or AND with immediate)
5363                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5364                    reg_to_bits(rd),
5365                    reg_to_bits(rd),
5366                    0x3F,
5367                )?);
5368
5369                Ok(bytes)
5370            }
5371
5372            // I64DivU: 64-bit unsigned division using binary long division
5373            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = quotient
5374            // Uses: R4-R7, R12 as loop counter (avoid R8 for Renode compatibility)
5375            //
5376            // #610: the fixed-ABI wrapper marshals the selector-assigned
5377            // operand registers into the core's fixed regs and lands the
5378            // result in rd — pre-#610 this arm IGNORED its register fields,
5379            // so the selector read its rd pair (e.g. R4:R5) after the core's
5380            // own POP restored the stale caller values over it: 0 for every
5381            // input. A zero divisor now traps (UDF #0), per WASM semantics.
5382            ArmOp::I64DivU {
5383                rdlo,
5384                rdhi,
5385                rnlo,
5386                rnhi,
5387                rmlo,
5388                rmhi,
5389                elide_zero_guard,
5390            } => {
5391                let mut bytes = Vec::new();
5392                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5393                // #494 phase 2b: elided only under a certificate-discharged
5394                // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
5395                if !elide_zero_guard {
5396                    emit_i64_divisor_zero_trap(&mut bytes);
5397                }
5398
5399                // PUSH {R4-R7} - save scratch registers (NO LR — this is inline code)
5400                // 16-bit PUSH: 1011 010 M rrrrrrrr where M=0 (no LR), r=R4-R7 = 0xF0
5401                // Encoding: 1011 0100 1111 0000 = 0xB4F0
5402                bytes.extend_from_slice(&0xB4F0u16.to_le_bytes());
5403
5404                // Initialize quotient (R4:R5) = 0
5405                bytes.extend_from_slice(&0x2400u16.to_le_bytes()); // MOV R4, #0
5406                bytes.extend_from_slice(&0x2500u16.to_le_bytes()); // MOV R5, #0
5407
5408                // Initialize remainder (R6:R7) = 0
5409                bytes.extend_from_slice(&0x2600u16.to_le_bytes()); // MOV R6, #0
5410                bytes.extend_from_slice(&0x2700u16.to_le_bytes()); // MOV R7, #0
5411
5412                // Initialize loop counter R12 = 64 (use R12 scratch instead of R8)
5413                // MOV.W R12, #64: F04F 0C40
5414                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5415                bytes.extend_from_slice(&0x0C40u16.to_le_bytes());
5416
5417                // Loop start
5418                let loop_start = bytes.len();
5419
5420                // === Loop body: process one bit ===
5421
5422                // 1. Shift quotient R4:R5 left by 1
5423                // LSLS R5, R5, #1 (16-bit: 0000 0010 1010 1101 = 0x006D -> actually 0x002D for LSL R5,R5,#1)
5424                // LSL Rd, Rm, #imm5: 000 00 imm5 Rm Rd = 000 00 00001 101 101 = 0x006D
5425                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5426                // Get carry from R4 into R5: ORR R5, R5, R4 LSR #31
5427                // Thumb-2 ORR with shifted register: EA45 75D4 = ORR.W R5, R5, R4, LSR #31
5428                // 11101010 010 S Rn | 0 imm3 Rd imm2 type Rm
5429                // type=01 (LSR), imm5=31 (imm3=111, imm2=11)
5430                bytes.extend_from_slice(&0xEA45u16.to_le_bytes());
5431                bytes.extend_from_slice(&0x75D4u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5432                // LSLS R4, R4, #1: 000 00 00001 100 100 = 0x0064
5433                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5434
5435                // 2. Shift remainder R6:R7 left by 1, OR in MSB of dividend R1
5436                // LSLS R7, R7, #1
5437                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5438                // ORR.W R7, R7, R6, LSR #31
5439                bytes.extend_from_slice(&0xEA47u16.to_le_bytes());
5440                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5441                // LSLS R6, R6, #1
5442                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5443                // ORR.W R6, R6, R1, LSR #31 (bring in MSB of dividend high)
5444                bytes.extend_from_slice(&0xEA46u16.to_le_bytes());
5445                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5446
5447                // 3. Shift dividend R0:R1 left by 1
5448                // LSLS R1, R1, #1
5449                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5450                // ORR.W R1, R1, R0, LSR #31
5451                bytes.extend_from_slice(&0xEA41u16.to_le_bytes());
5452                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5453                // LSLS R0, R0, #1
5454                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5455
5456                // 4. Compare remainder >= divisor (64-bit unsigned comparison)
5457                // Compare high words first: CMP R7, R3
5458                // CMP Rn, Rm encoding: 0x4280 | (Rm << 3) | Rn
5459                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3 (16-bit)
5460                // BHI means R7 > R3 (unsigned) - definitely subtract
5461                // BLO means R7 < R3 - definitely don't subtract
5462                // BEQ means need to check low words
5463
5464                // If high > divisor high: branch to subtract (forward +offset)
5465                // BHI.N +6 (skip CMP, skip BLO, do subtract)
5466                // BHI: 1101 1000 offset8 where cond=1000 (HI)
5467                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4 (to subtract block)
5468
5469                // If high < divisor high: branch past subtract
5470                // BLO.N +10 (skip to decrement)
5471                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BLO/BCC +12 (past subtract)
5472
5473                // High words equal, compare low: CMP R6, R2
5474                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2 (16-bit)
5475                // BLO/BCC past subtract (skip SUBS+SBC.W+ORR.W = 10 bytes = 4 halfwords from PC+4)
5476                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords (past subtract)
5477
5478                // === Subtract block: remainder -= divisor, quotient |= 1 ===
5479                // SUBS R6, R6, R2
5480                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2 (16-bit)
5481                // SBC R7, R7, R3 (with borrow)
5482                // Thumb-2 SBC.W: EB67 0703 = SBC.W R7, R7, R3
5483                bytes.extend_from_slice(&0xEB67u16.to_le_bytes());
5484                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5485                // ORR R4, R4, #1 (set bit 0 of quotient low)
5486                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5487                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5488
5489                // === Decrement counter and loop ===
5490                // SUBS.W R12, R12, #1 (decrement loop counter)
5491                // SUBS.W R12, R12, #1: F1BC 0C01
5492                bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
5493                bytes.extend_from_slice(&0x0C01u16.to_le_bytes());
5494
5495                // BNE back to loop_start
5496                let branch_offset_bytes = bytes.len() - loop_start + 4; // +4 for pipeline
5497                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5498                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5499                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5500
5501                // === Loop done, move quotient to R0:R1 ===
5502                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5503                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5504
5505                // POP {R4-R7} - restore scratch registers (NO PC — inline code continues)
5506                // 16-bit POP: 1011 110 P rrrrrrrr where P=0 (no PC), r=R4-R7 = 0xF0
5507                // Encoding: 1011 1100 1111 0000 = 0xBCF0
5508                bytes.extend_from_slice(&0xBCF0u16.to_le_bytes());
5509
5510                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5511                Ok(bytes)
5512            }
5513
5514            // I64DivS: 64-bit signed division
5515            // Converts to unsigned, divides, then applies sign
5516            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5517            //   ->  R0:R1 = quotient (signed)
5518            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5519            ArmOp::I64DivS {
5520                rdlo,
5521                rdhi,
5522                rnlo,
5523                rnhi,
5524                rmlo,
5525                rmhi,
5526                elide_zero_guard,
5527                elide_overflow_guard,
5528            } => {
5529                let mut bytes = Vec::new();
5530                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5531                // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
5532                // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
5533                // the #633 overflow guard falls ONLY to
5534                // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
5535                // divisor-nonzero fact alone must keep it.
5536                if !elide_zero_guard {
5537                    emit_i64_divisor_zero_trap(&mut bytes);
5538                }
5539                if !elide_overflow_guard {
5540                    // #633: INT64_MIN / -1 overflows — trap like the i32 path
5541                    // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
5542                    emit_i64_divs_overflow_trap(&mut bytes);
5543                }
5544
5545                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5546                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5547                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5548
5549                // Save result sign in R9: R9 = R1 XOR R3 (sign bit = MSB)
5550                // EOR.W R9, R1, R3
5551                bytes.extend_from_slice(&0xEA81u16.to_le_bytes());
5552                bytes.extend_from_slice(&0x0903u16.to_le_bytes());
5553
5554                // If dividend negative (R1 MSB set), negate it
5555                // TST R1, R1 (check sign)
5556                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5557                // BPL skip_neg_dividend (+10 bytes = 5 halfwords)
5558                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5559
5560                // Negate R0:R1 (64-bit): RSBS R0, R0, #0; SBC R1, R1, R1 LSL #1
5561                // Actually: MVN R0, R0; MVN R1, R1; ADDS R0, R0, #1; ADC R1, R1, #0
5562                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5563                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5564                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5565                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5566                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5567
5568                // If divisor negative (R3 MSB set), negate it
5569                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5570                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5571
5572                // Negate R2:R3
5573                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
5574                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
5575                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
5576                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
5577                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
5578
5579                // === Now do unsigned division (same as I64DivU) ===
5580                // Initialize quotient (R4:R5) = 0
5581                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5582                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5583                // Initialize remainder (R6:R7) = 0
5584                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5585                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5586                // Initialize loop counter R8 = 64
5587                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5588                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5589
5590                let loop_start = bytes.len();
5591
5592                // Shift quotient left
5593                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5594                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5595                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5596                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5597
5598                // Shift remainder left, OR in MSB of dividend
5599                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5600                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5601                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5602                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5603                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5604                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5605
5606                // Shift dividend left
5607                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5608                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5609                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5610                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5611
5612                // Compare and conditionally subtract
5613                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5614                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5615                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5616                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5617                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5618
5619                // Subtract and set quotient bit
5620                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5621                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5622                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5623                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5624                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5625
5626                // Decrement and loop
5627                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
5628                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
5629
5630                let branch_offset_bytes = bytes.len() - loop_start + 4;
5631                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5632                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5633                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5634
5635                // Move quotient to R0:R1
5636                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5637                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5638
5639                // If result should be negative (R9 MSB set), negate R0:R1
5640                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9 (check MSB)
5641                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
5642                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8 (skip negation)
5643
5644                // Negate result R0:R1
5645                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5646                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5647                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5648                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5649                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5650
5651                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
5652                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
5653                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5654
5655                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5656                Ok(bytes)
5657            }
5658
5659            // I64RemU: 64-bit unsigned remainder using binary long division
5660            // Same algorithm as I64DivU but returns remainder instead of quotient
5661            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = remainder
5662            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5663            ArmOp::I64RemU {
5664                rdlo,
5665                rdhi,
5666                rnlo,
5667                rnhi,
5668                rmlo,
5669                rmhi,
5670                elide_zero_guard,
5671            } => {
5672                let mut bytes = Vec::new();
5673                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5674                if !elide_zero_guard {
5675                    emit_i64_divisor_zero_trap(&mut bytes);
5676                }
5677
5678                // PUSH {R4-R8} - save scratch registers (NO LR — inline code)
5679                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5680                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
5681
5682                // Initialize quotient (R4:R5) = 0 (computed but not returned)
5683                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5684                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5685                // Initialize remainder (R6:R7) = 0
5686                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5687                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5688                // Initialize loop counter R8 = 64
5689                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5690                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5691
5692                let loop_start = bytes.len();
5693
5694                // Shift quotient left (not needed for result, but keeps algorithm same)
5695                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5696                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5697                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5698                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5699
5700                // Shift remainder left, OR in MSB of dividend
5701                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5702                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5703                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5704                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5705                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5706                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5707
5708                // Shift dividend left
5709                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5710                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5711                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5712                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5713
5714                // Compare and conditionally subtract
5715                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5716                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5717                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5718                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5719                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5720
5721                // Subtract and set quotient bit
5722                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5723                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5724                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5725                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5726                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5727
5728                // Decrement and loop
5729                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
5730                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
5731
5732                let branch_offset_bytes = bytes.len() - loop_start + 4;
5733                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5734                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5735                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5736
5737                // Move REMAINDER to R0:R1 (difference from I64DivU)
5738                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
5739                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
5740
5741                // POP {R4-R8} - restore scratch registers (NO PC — inline code continues)
5742                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
5743                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
5744
5745                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5746                Ok(bytes)
5747            }
5748
5749            // I64RemS: 64-bit signed remainder
5750            // Remainder sign follows dividend sign (not quotient rule)
5751            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5752            //   ->  R0:R1 = remainder (signed, same sign as dividend)
5753            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5754            ArmOp::I64RemS {
5755                rdlo,
5756                rdhi,
5757                rnlo,
5758                rnhi,
5759                rmlo,
5760                rmhi,
5761                elide_zero_guard,
5762            } => {
5763                let mut bytes = Vec::new();
5764                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5765                if !elide_zero_guard {
5766                    emit_i64_divisor_zero_trap(&mut bytes);
5767                }
5768
5769                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5770                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5771                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5772
5773                // Save dividend sign in R9 (remainder sign = dividend sign)
5774                // MOV R9, R1 (just need the sign bit)
5775                bytes.extend_from_slice(&0x4689u16.to_le_bytes()); // MOV R9, R1
5776
5777                // If dividend negative (R1 MSB set), negate it
5778                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5779                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5780
5781                // Negate R0:R1
5782                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5783                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5784                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5785                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5786                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5787
5788                // If divisor negative (R3 MSB set), negate it
5789                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5790                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5791
5792                // Negate R2:R3
5793                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
5794                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
5795                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
5796                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
5797                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
5798
5799                // === Unsigned division algorithm ===
5800                // Initialize quotient (R4:R5) = 0
5801                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5802                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5803                // Initialize remainder (R6:R7) = 0
5804                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5805                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5806                // Initialize loop counter R8 = 64
5807                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5808                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5809
5810                let loop_start = bytes.len();
5811
5812                // Shift quotient left
5813                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5814                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5815                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5816                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5817
5818                // Shift remainder left, OR in MSB of dividend
5819                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5820                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5821                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5822                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5823                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5824                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5825
5826                // Shift dividend left
5827                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5828                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5829                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5830                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5831
5832                // Compare and conditionally subtract
5833                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5834                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5835                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5836                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5837                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5838
5839                // Subtract and set quotient bit
5840                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5841                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5842                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5843                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5844                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5845
5846                // Decrement and loop
5847                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
5848                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
5849
5850                let branch_offset_bytes = bytes.len() - loop_start + 4;
5851                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5852                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5853                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5854
5855                // Move remainder to R0:R1
5856                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
5857                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
5858
5859                // If original dividend was negative (R9 MSB set), negate remainder
5860                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9
5861                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
5862                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5863
5864                // Negate result R0:R1
5865                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5866                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5867                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5868                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5869                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5870
5871                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
5872                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
5873                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5874
5875                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5876                Ok(bytes)
5877            }
5878
5879            // === F32 VFP single-precision Thumb-2 encodings ===
5880            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
5881            ArmOp::F32Add { sd, sn, sm } => {
5882                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A00, sd, sn, sm)?))
5883            }
5884            ArmOp::F32Sub { sd, sn, sm } => {
5885                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A40, sd, sn, sm)?))
5886            }
5887            ArmOp::F32Mul { sd, sn, sm } => {
5888                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE200A00, sd, sn, sm)?))
5889            }
5890            ArmOp::F32Div { sd, sn, sm } => {
5891                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE800A00, sd, sn, sm)?))
5892            }
5893            ArmOp::F32Abs { sd, sm } => {
5894                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB00AC0, sd, sm)?))
5895            }
5896            ArmOp::F32Neg { sd, sm } => {
5897                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10A40, sd, sm)?))
5898            }
5899            ArmOp::F32Sqrt { sd, sm } => {
5900                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10AC0, sd, sm)?))
5901            }
5902
5903            // f32 pseudo-ops — multi-instruction sequences
5904            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
5905            ArmOp::F32Ceil { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b01),
5906            ArmOp::F32Floor { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b10),
5907            ArmOp::F32Trunc { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b11),
5908            ArmOp::F32Nearest { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b00),
5909            ArmOp::F32Min { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, true),
5910            ArmOp::F32Max { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, false),
5911            ArmOp::F32Copysign { sd, sn, sm } => self.encode_thumb_f32_copysign(sd, sn, sm),
5912
5913            // f32 comparisons — VCMP + VMRS + MOV #0 + IT + MOV #1
5914            ArmOp::F32Eq { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x0),
5915            ArmOp::F32Ne { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x1),
5916            ArmOp::F32Lt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x4),
5917            ArmOp::F32Le { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x9),
5918            ArmOp::F32Gt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xC),
5919            ArmOp::F32Ge { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xA),
5920
5921            ArmOp::F32Const { sd, value } => self.encode_thumb_f32_const(sd, *value),
5922
5923            ArmOp::F32Load { sd, addr } => {
5924                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED900A00, sd, addr)?))
5925            }
5926            ArmOp::F32Store { sd, addr } => {
5927                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED800A00, sd, addr)?))
5928            }
5929
5930            ArmOp::F32ConvertI32S { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, true),
5931            ArmOp::F32ConvertI32U { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, false),
5932            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
5933                Err(synth_core::Error::synthesis(
5934                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
5935                ))
5936            }
5937            ArmOp::F32ReinterpretI32 { sd, rm } => {
5938                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(true, sd, rm)?))
5939            }
5940            ArmOp::I32ReinterpretF32 { rd, sm } => {
5941                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(false, sm, rd)?))
5942            }
5943            ArmOp::I32TruncF32S { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, true),
5944            ArmOp::I32TruncF32U { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, false),
5945
5946            // === F64 VFP double-precision Thumb-2 encodings ===
5947            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
5948            ArmOp::F64Add { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
5949                0xEE300B00, dd, dn, dm,
5950            )?)),
5951            ArmOp::F64Sub { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
5952                0xEE300B40, dd, dn, dm,
5953            )?)),
5954            ArmOp::F64Mul { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
5955                0xEE200B00, dd, dn, dm,
5956            )?)),
5957            ArmOp::F64Div { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
5958                0xEE800B00, dd, dn, dm,
5959            )?)),
5960            ArmOp::F64Abs { dd, dm } => {
5961                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?))
5962            }
5963            ArmOp::F64Neg { dd, dm } => {
5964                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?))
5965            }
5966            ArmOp::F64Sqrt { dd, dm } => {
5967                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?))
5968            }
5969
5970            // f64 pseudo-ops
5971            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
5972            ArmOp::F64Ceil { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b01),
5973            ArmOp::F64Floor { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b10),
5974            ArmOp::F64Trunc { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b11),
5975            ArmOp::F64Nearest { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b00),
5976            ArmOp::F64Min { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, true),
5977            ArmOp::F64Max { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, false),
5978            ArmOp::F64Copysign { dd, dn, dm } => self.encode_thumb_f64_copysign(dd, dn, dm),
5979
5980            // f64 comparisons
5981            ArmOp::F64Eq { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x0),
5982            ArmOp::F64Ne { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x1),
5983            ArmOp::F64Lt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x4),
5984            ArmOp::F64Le { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x9),
5985            ArmOp::F64Gt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xC),
5986            ArmOp::F64Ge { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xA),
5987
5988            ArmOp::F64Const { dd, value } => self.encode_thumb_f64_const(dd, *value),
5989
5990            ArmOp::F64Load { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
5991                0xED900B00, dd, addr,
5992            )?)),
5993            ArmOp::F64Store { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
5994                0xED800B00, dd, addr,
5995            )?)),
5996
5997            ArmOp::F64ConvertI32S { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, true),
5998            ArmOp::F64ConvertI32U { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, false),
5999            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
6000                Err(synth_core::Error::synthesis(
6001                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6002                ))
6003            }
6004            ArmOp::F64PromoteF32 { dd, sm } => self.encode_thumb_f64_promote_f32(dd, sm),
6005            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => Ok(vfp_to_thumb_bytes(
6006                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?,
6007            )),
6008            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => Ok(vfp_to_thumb_bytes(
6009                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?,
6010            )),
6011            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
6012                Err(synth_core::Error::synthesis(
6013                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
6014                ))
6015            }
6016            ArmOp::I32TruncF64S { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, true),
6017            ArmOp::I32TruncF64U { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, false),
6018
6019            // ===== i64 operations: encode as multi-instruction Thumb-2 sequences =====
6020
6021            // I64Add: ADDS rdlo, rnlo, rmlo; ADC.W rdhi, rnhi, rmhi
6022            ArmOp::I64Add {
6023                rdlo,
6024                rdhi,
6025                rnlo,
6026                rnhi,
6027                rmlo,
6028                rmhi,
6029            } => {
6030                let mut bytes = Vec::new();
6031                // ADDS rdlo, rnlo, rmlo (16-bit)
6032                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adds {
6033                    rd: *rdlo,
6034                    rn: *rnlo,
6035                    op2: Operand2::Reg(*rmlo),
6036                })?);
6037                // ADC.W rdhi, rnhi, rmhi (32-bit)
6038                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adc {
6039                    rd: *rdhi,
6040                    rn: *rnhi,
6041                    op2: Operand2::Reg(*rmhi),
6042                })?);
6043                Ok(bytes)
6044            }
6045
6046            // I64Sub: SUBS rdlo, rnlo, rmlo; SBC.W rdhi, rnhi, rmhi
6047            ArmOp::I64Sub {
6048                rdlo,
6049                rdhi,
6050                rnlo,
6051                rnhi,
6052                rmlo,
6053                rmhi,
6054            } => {
6055                let mut bytes = Vec::new();
6056                // SUBS rdlo, rnlo, rmlo (16-bit)
6057                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Subs {
6058                    rd: *rdlo,
6059                    rn: *rnlo,
6060                    op2: Operand2::Reg(*rmlo),
6061                })?);
6062                // SBC.W rdhi, rnhi, rmhi (32-bit)
6063                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Sbc {
6064                    rd: *rdhi,
6065                    rn: *rnhi,
6066                    op2: Operand2::Reg(*rmhi),
6067                })?);
6068                Ok(bytes)
6069            }
6070
6071            // I64And: AND rdlo, rnlo, rmlo; AND rdhi, rnhi, rmhi
6072            ArmOp::I64And {
6073                rdlo,
6074                rdhi,
6075                rnlo,
6076                rnhi,
6077                rmlo,
6078                rmhi,
6079            } => {
6080                let mut bytes = Vec::new();
6081                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6082                    rd: *rdlo,
6083                    rn: *rnlo,
6084                    op2: Operand2::Reg(*rmlo),
6085                })?);
6086                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6087                    rd: *rdhi,
6088                    rn: *rnhi,
6089                    op2: Operand2::Reg(*rmhi),
6090                })?);
6091                Ok(bytes)
6092            }
6093
6094            // I64Or: ORR rdlo, rnlo, rmlo; ORR rdhi, rnhi, rmhi
6095            ArmOp::I64Or {
6096                rdlo,
6097                rdhi,
6098                rnlo,
6099                rnhi,
6100                rmlo,
6101                rmhi,
6102            } => {
6103                let mut bytes = Vec::new();
6104                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6105                    rd: *rdlo,
6106                    rn: *rnlo,
6107                    op2: Operand2::Reg(*rmlo),
6108                })?);
6109                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6110                    rd: *rdhi,
6111                    rn: *rnhi,
6112                    op2: Operand2::Reg(*rmhi),
6113                })?);
6114                Ok(bytes)
6115            }
6116
6117            // I64Xor: EOR rdlo, rnlo, rmlo; EOR rdhi, rnhi, rmhi
6118            ArmOp::I64Xor {
6119                rdlo,
6120                rdhi,
6121                rnlo,
6122                rnhi,
6123                rmlo,
6124                rmhi,
6125            } => {
6126                let mut bytes = Vec::new();
6127                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6128                    rd: *rdlo,
6129                    rn: *rnlo,
6130                    op2: Operand2::Reg(*rmlo),
6131                })?);
6132                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6133                    rd: *rdhi,
6134                    rn: *rnhi,
6135                    op2: Operand2::Reg(*rmhi),
6136                })?);
6137                Ok(bytes)
6138            }
6139
6140            // I64Eqz: ORR scratch, lo, hi; ITE EQ; MOV rd, #1; MOV rd, #0
6141            ArmOp::I64Eqz { rd, rnlo, rnhi } => self.encode_thumb(&ArmOp::I64SetCondZ {
6142                rd: *rd,
6143                rn_lo: *rnlo,
6144                rn_hi: *rnhi,
6145            }),
6146
6147            // I64 comparisons: delegate to I64SetCond
6148            ArmOp::I64Eq {
6149                rd,
6150                rnlo,
6151                rnhi,
6152                rmlo,
6153                rmhi,
6154            } => self.encode_thumb(&ArmOp::I64SetCond {
6155                rd: *rd,
6156                rn_lo: *rnlo,
6157                rn_hi: *rnhi,
6158                rm_lo: *rmlo,
6159                rm_hi: *rmhi,
6160                cond: synth_synthesis::Condition::EQ,
6161            }),
6162
6163            ArmOp::I64Ne {
6164                rd,
6165                rnlo,
6166                rnhi,
6167                rmlo,
6168                rmhi,
6169            } => self.encode_thumb(&ArmOp::I64SetCond {
6170                rd: *rd,
6171                rn_lo: *rnlo,
6172                rn_hi: *rnhi,
6173                rm_lo: *rmlo,
6174                rm_hi: *rmhi,
6175                cond: synth_synthesis::Condition::NE,
6176            }),
6177
6178            ArmOp::I64LtS {
6179                rd,
6180                rnlo,
6181                rnhi,
6182                rmlo,
6183                rmhi,
6184            } => self.encode_thumb(&ArmOp::I64SetCond {
6185                rd: *rd,
6186                rn_lo: *rnlo,
6187                rn_hi: *rnhi,
6188                rm_lo: *rmlo,
6189                rm_hi: *rmhi,
6190                cond: synth_synthesis::Condition::LT,
6191            }),
6192
6193            ArmOp::I64LtU {
6194                rd,
6195                rnlo,
6196                rnhi,
6197                rmlo,
6198                rmhi,
6199            } => self.encode_thumb(&ArmOp::I64SetCond {
6200                rd: *rd,
6201                rn_lo: *rnlo,
6202                rn_hi: *rnhi,
6203                rm_lo: *rmlo,
6204                rm_hi: *rmhi,
6205                cond: synth_synthesis::Condition::LO,
6206            }),
6207
6208            ArmOp::I64LeS {
6209                rd,
6210                rnlo,
6211                rnhi,
6212                rmlo,
6213                rmhi,
6214            } => self.encode_thumb(&ArmOp::I64SetCond {
6215                rd: *rd,
6216                rn_lo: *rnlo,
6217                rn_hi: *rnhi,
6218                rm_lo: *rmlo,
6219                rm_hi: *rmhi,
6220                cond: synth_synthesis::Condition::LE,
6221            }),
6222
6223            ArmOp::I64LeU {
6224                rd,
6225                rnlo,
6226                rnhi,
6227                rmlo,
6228                rmhi,
6229            } => self.encode_thumb(&ArmOp::I64SetCond {
6230                rd: *rd,
6231                rn_lo: *rnlo,
6232                rn_hi: *rnhi,
6233                rm_lo: *rmlo,
6234                rm_hi: *rmhi,
6235                cond: synth_synthesis::Condition::LS,
6236            }),
6237
6238            ArmOp::I64GtS {
6239                rd,
6240                rnlo,
6241                rnhi,
6242                rmlo,
6243                rmhi,
6244            } => self.encode_thumb(&ArmOp::I64SetCond {
6245                rd: *rd,
6246                rn_lo: *rnlo,
6247                rn_hi: *rnhi,
6248                rm_lo: *rmlo,
6249                rm_hi: *rmhi,
6250                cond: synth_synthesis::Condition::GT,
6251            }),
6252
6253            ArmOp::I64GtU {
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::HI,
6266            }),
6267
6268            ArmOp::I64GeS {
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::GE,
6281            }),
6282
6283            ArmOp::I64GeU {
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::HS,
6296            }),
6297
6298            // I64Const: MOVW rdlo, lo16; MOVT rdlo, hi16; MOVW rdhi, lo16_hi; MOVT rdhi, hi16_hi
6299            ArmOp::I64Const { rdlo, rdhi, value } => {
6300                let lo32 = *value as u32;
6301                let hi32 = (*value >> 32) as u32;
6302                let mut bytes = Vec::new();
6303                // Load low 32 bits into rdlo
6304                bytes.extend_from_slice(
6305                    &self.encode_thumb32_movw_raw(reg_to_bits(rdlo), lo32 & 0xFFFF)?,
6306                );
6307                if lo32 > 0xFFFF {
6308                    bytes.extend_from_slice(
6309                        &self.encode_thumb32_movt_raw(reg_to_bits(rdlo), lo32 >> 16)?,
6310                    );
6311                }
6312                // Load high 32 bits into rdhi
6313                bytes.extend_from_slice(
6314                    &self.encode_thumb32_movw_raw(reg_to_bits(rdhi), hi32 & 0xFFFF)?,
6315                );
6316                if hi32 > 0xFFFF {
6317                    bytes.extend_from_slice(
6318                        &self.encode_thumb32_movt_raw(reg_to_bits(rdhi), hi32 >> 16)?,
6319                    );
6320                }
6321                Ok(bytes)
6322            }
6323
6324            // I64Ldr: LDR rdlo, [base, offset]; LDR rdhi, [base, offset+4]
6325            ArmOp::I64Ldr { rdlo, rdhi, addr } => {
6326                let mut bytes = Vec::new();
6327                // #372/#382: a memory `i64.load` carries an index register
6328                // (`reg_imm(R11, addr_reg, offset)` = R11 + addr + offset). The
6329                // immediate `encode_thumb32_ldr` below uses only base+offset and
6330                // would SILENTLY DROP `offset_reg` — the #206 defect, here for
6331                // i64. `i64_effective_base` materializes the effective base into
6332                // `ip` (and, when `offset+4 > 0xFFF`, folds the offset in too so
6333                // the function is NOT skipped — #382), returning the residual
6334                // imm12 for the two halves. Frame i64 loads (no `offset_reg`, e.g.
6335                // a spilled local at `[SP, #off]`) keep the plain `[base,#off]`
6336                // form unchanged — so existing output is byte-identical.
6337                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6338                bytes.extend_from_slice(&self.encode_thumb32_ldr(rdlo, &base, offset)?);
6339                bytes.extend_from_slice(&self.encode_thumb32_ldr(
6340                    rdhi,
6341                    &base,
6342                    offset.wrapping_add(4),
6343                )?);
6344                Ok(bytes)
6345            }
6346
6347            // I64Str: STR rdlo, [base, offset]; STR rdhi, [base, offset+4]
6348            ArmOp::I64Str { rdlo, rdhi, addr } => {
6349                let mut bytes = Vec::new();
6350                // #372/#382: same index-materialization + large-offset fold as
6351                // I64Ldr (see above).
6352                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6353                bytes.extend_from_slice(&self.encode_thumb32_str(rdlo, &base, offset)?);
6354                bytes.extend_from_slice(&self.encode_thumb32_str(
6355                    rdhi,
6356                    &base,
6357                    offset.wrapping_add(4),
6358                )?);
6359                Ok(bytes)
6360            }
6361
6362            // I64ExtendI32S: MOV rdlo, rn; ASR rdhi, rdlo, #31 (sign-extend)
6363            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
6364                let mut bytes = Vec::new();
6365                if rdlo != rn {
6366                    // MOV rdlo, rn (16-bit)
6367                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6368                        rd: *rdlo,
6369                        op2: Operand2::Reg(*rn),
6370                    })?);
6371                }
6372                // ASR rdhi, rdlo, #31 (sign-extend: fill high word with sign bit)
6373                bytes.extend_from_slice(
6374                    &self.encode_thumb32_shift(rdhi, rdlo, 31, 0b10)?, // ASR type
6375                );
6376                Ok(bytes)
6377            }
6378
6379            // I64ExtendI32U: MOV rdlo, rn; MOV rdhi, #0
6380            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
6381                let mut bytes = Vec::new();
6382                if rdlo != rn {
6383                    // MOV rdlo, rn
6384                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6385                        rd: *rdlo,
6386                        op2: Operand2::Reg(*rn),
6387                    })?);
6388                }
6389                // MOV rdhi, #0 (16-bit: MOVS Rd, #0)
6390                let rdhi_bits = reg_to_bits(rdhi) as u16;
6391                let instr: u16 = 0x2000 | (rdhi_bits << 8);
6392                bytes.extend_from_slice(&instr.to_le_bytes());
6393                Ok(bytes)
6394            }
6395
6396            // I32WrapI64: MOV rd, rnlo (just take low 32 bits)
6397            ArmOp::I32WrapI64 { rd, rnlo } => {
6398                if rd == rnlo {
6399                    // No-op: already in the right register
6400                    let instr: u16 = 0xBF00; // NOP
6401                    Ok(instr.to_le_bytes().to_vec())
6402                } else {
6403                    // MOV rd, rnlo
6404                    self.encode_thumb(&ArmOp::Mov {
6405                        rd: *rd,
6406                        op2: Operand2::Reg(*rnlo),
6407                    })
6408                }
6409            }
6410
6411            // ===== Helium MVE operations (Thumb-2 encoding) =====
6412            ArmOp::MveLoad { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vldrw(qd, addr))),
6413            ArmOp::MveStore { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vstrw(qd, addr))),
6414            ArmOp::MveConst { qd, bytes } => self.encode_thumb_mve_const(qd, bytes),
6415            ArmOp::MveAnd { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6416                0xEF000150, qd, qn, qm,
6417            ))),
6418            ArmOp::MveOrr { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6419                0xEF200150, qd, qn, qm,
6420            ))),
6421            ArmOp::MveEor { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6422                0xFF000150, qd, qn, qm,
6423            ))),
6424            ArmOp::MveMvn { qd, qm } => {
6425                // VMVN Qd, Qm: 0xFFB005C0 | Qd<<12 | Qm
6426                let qd_enc = qreg_to_num(qd);
6427                let qm_enc = qreg_to_num(qm);
6428                let instr: u32 = 0xFFB005C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6429                Ok(vfp_to_thumb_bytes(instr))
6430            }
6431            ArmOp::MveBic { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6432                0xEF100150, qd, qn, qm,
6433            ))),
6434            ArmOp::MveAddI { qd, qn, qm, size } => {
6435                let sz = mve_size_bits(size);
6436                let base: u32 = 0xEF000840 | (sz << 20);
6437                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6438            }
6439            ArmOp::MveSubI { qd, qn, qm, size } => {
6440                let sz = mve_size_bits(size);
6441                let base: u32 = 0xFF000840 | (sz << 20);
6442                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6443            }
6444            ArmOp::MveMulI { qd, qn, qm, size } => {
6445                let sz = mve_size_bits(size);
6446                let base: u32 = 0xEF000950 | (sz << 20);
6447                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6448            }
6449            ArmOp::MveNegI { qd, qm, size } => {
6450                let sz = mve_size_bits(size);
6451                // VNEG.Sx Qd, Qm
6452                let qd_enc = qreg_to_num(qd);
6453                let qm_enc = qreg_to_num(qm);
6454                let base: u32 = 0xFFB103C0 | (sz << 18);
6455                let instr = base | ((qd_enc * 2) << 12) | (qm_enc * 2);
6456                Ok(vfp_to_thumb_bytes(instr))
6457            }
6458            ArmOp::MveDup { qd, rn, size } => {
6459                let sz = mve_size_bits(size);
6460                let qd_enc = qreg_to_num(qd);
6461                let rn_bits = reg_to_bits(rn);
6462                // VDUP.sz Qd, Rn: EEA0 0B10 variant
6463                // size encoding: 00=32, 01=16, 10=8
6464                let be = match sz {
6465                    0 => 0b00u32, // 8-bit
6466                    1 => 0b01,    // 16-bit
6467                    _ => 0b00,    // 32-bit (default)
6468                };
6469                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12) | (be << 5);
6470                Ok(vfp_to_thumb_bytes(instr))
6471            }
6472            ArmOp::MveExtractLane { rd, qn, lane, size } => {
6473                let qn_enc = qreg_to_num(qn);
6474                let rd_bits = reg_to_bits(rd);
6475                // VMOV.sz Rd, Dn[x] — extract from Q-register lane
6476                // For 32-bit: VMOV Rd, Dn — where Dn is the appropriate D-register
6477                let d_reg = qn_enc * 2 + ((*lane as u32) >> 1);
6478                let lane_in_d = (*lane as u32) & 1;
6479                let _sz = mve_size_bits(size);
6480                // VMOV Rd, Dn[x]: EE10 0B10 for 32-bit
6481                let instr: u32 = 0xEE100B10 | (d_reg << 16) | (rd_bits << 12) | (lane_in_d << 21);
6482                Ok(vfp_to_thumb_bytes(instr))
6483            }
6484            ArmOp::MveInsertLane { qd, rn, lane, size } => {
6485                let qd_enc = qreg_to_num(qd);
6486                let rn_bits = reg_to_bits(rn);
6487                let d_reg = qd_enc * 2 + ((*lane as u32) >> 1);
6488                let lane_in_d = (*lane as u32) & 1;
6489                let _sz = mve_size_bits(size);
6490                // VMOV Dn[x], Rn: EE00 0B10 for 32-bit
6491                let instr: u32 = 0xEE000B10 | (d_reg << 16) | (rn_bits << 12) | (lane_in_d << 21);
6492                Ok(vfp_to_thumb_bytes(instr))
6493            }
6494
6495            // MVE float comparisons — emit VCMP + VPSEL sequence (simplified: just VCMP)
6496            ArmOp::MveCmpEqI { qd, qn, qm, size }
6497            | ArmOp::MveCmpNeI { qd, qn, qm, size }
6498            | ArmOp::MveCmpLtS { qd, qn, qm, size }
6499            | ArmOp::MveCmpLtU { qd, qn, qm, size }
6500            | ArmOp::MveCmpGtS { qd, qn, qm, size }
6501            | ArmOp::MveCmpGtU { qd, qn, qm, size }
6502            | ArmOp::MveCmpLeS { qd, qn, qm, size }
6503            | ArmOp::MveCmpLeU { qd, qn, qm, size }
6504            | ArmOp::MveCmpGeS { qd, qn, qm, size }
6505            | ArmOp::MveCmpGeU { qd, qn, qm, size } => {
6506                // Encode as VADD (placeholder encoding — real implementation
6507                // would use VCMP + VPSEL pair)
6508                let sz = mve_size_bits(size);
6509                let base: u32 = 0xEF000840 | (sz << 20);
6510                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6511            }
6512
6513            // f32x4 MVE arithmetic
6514            ArmOp::MveAddF32 { qd, qn, qm } => {
6515                // VADD.F32 Qd, Qn, Qm (MVE): 0xEF000D40
6516                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6517            }
6518            ArmOp::MveSubF32 { qd, qn, qm } => {
6519                // VSUB.F32 Qd, Qn, Qm (MVE): 0xEF200D40
6520                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF200D40, qd, qn, qm)))
6521            }
6522            ArmOp::MveMulF32 { qd, qn, qm } => {
6523                // VMUL.F32 Qd, Qn, Qm (MVE): 0xFF000D50
6524                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xFF000D50, qd, qn, qm)))
6525            }
6526            ArmOp::MveNegF32 { qd, qm } => {
6527                let qd_enc = qreg_to_num(qd);
6528                let qm_enc = qreg_to_num(qm);
6529                // VNEG.F32 Qd, Qm: FFB907C0
6530                let instr: u32 = 0xFFB907C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6531                Ok(vfp_to_thumb_bytes(instr))
6532            }
6533            ArmOp::MveAbsF32 { qd, qm } => {
6534                let qd_enc = qreg_to_num(qd);
6535                let qm_enc = qreg_to_num(qm);
6536                // VABS.F32 Qd, Qm: FFB90740
6537                let instr: u32 = 0xFFB90740 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6538                Ok(vfp_to_thumb_bytes(instr))
6539            }
6540            ArmOp::MveCmpEqF32 { qd, qn, qm }
6541            | ArmOp::MveCmpNeF32 { qd, qn, qm }
6542            | ArmOp::MveCmpLtF32 { qd, qn, qm }
6543            | ArmOp::MveCmpLeF32 { qd, qn, qm }
6544            | ArmOp::MveCmpGtF32 { qd, qn, qm }
6545            | ArmOp::MveCmpGeF32 { qd, qn, qm } => {
6546                // Placeholder: encode as VADD.F32 (real impl needs VCMP.F32 + VPSEL)
6547                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6548            }
6549            ArmOp::MveDupF32 { qd, rn } => {
6550                let qd_enc = qreg_to_num(qd);
6551                let rn_bits = reg_to_bits(rn);
6552                // VDUP.32 Qd, Rn (same encoding as integer VDUP.32)
6553                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12);
6554                Ok(vfp_to_thumb_bytes(instr))
6555            }
6556            ArmOp::MveExtractLaneF32 { rd, qn, lane } => {
6557                let qn_enc = qreg_to_num(qn);
6558                let rd_bits = reg_to_bits(rd);
6559                // VMOV Rd, Sn where Sn = Q*4 + lane
6560                let s_num = qn_enc * 4 + (*lane as u32);
6561                let (vn, n) = encode_sreg(s_num);
6562                let instr: u32 = 0xEE100A10 | (vn << 16) | (rd_bits << 12) | (n << 7);
6563                Ok(vfp_to_thumb_bytes(instr))
6564            }
6565            ArmOp::MveReplaceLaneF32 { qd, rn, lane } => {
6566                let qd_enc = qreg_to_num(qd);
6567                let rn_bits = reg_to_bits(rn);
6568                // VMOV Sn, Rn where Sn = Q*4 + lane
6569                let s_num = qd_enc * 4 + (*lane as u32);
6570                let (vn, n) = encode_sreg(s_num);
6571                let instr: u32 = 0xEE000A10 | (vn << 16) | (rn_bits << 12) | (n << 7);
6572                Ok(vfp_to_thumb_bytes(instr))
6573            }
6574            ArmOp::MveDivF32 { qd, qn, qm } => {
6575                // Lane-wise: extract 4 S-regs, VDIV, insert back
6576                self.encode_thumb_mve_lane_wise_f32_binop(qd, qn, qm, 0xEE800A00)
6577            }
6578            ArmOp::MveSqrtF32 { qd, qm } => {
6579                // Lane-wise: extract 4 S-regs, VSQRT, insert back
6580                self.encode_thumb_mve_lane_wise_f32_sqrt(qd, qm)
6581            }
6582
6583            // Catch-all for any remaining ops
6584            _ => {
6585                let instr: u16 = 0xBF00; // NOP
6586                Ok(instr.to_le_bytes().to_vec())
6587            }
6588        }
6589    }
6590
6591    // === Thumb-2 VFP multi-instruction helpers ===
6592
6593    /// Encode F32 comparison as Thumb-2: VCMP.F32 + VMRS + MOVS rd,#0 + IT + MOV rd,#1
6594    fn encode_thumb_f32_compare(
6595        &self,
6596        rd: &Reg,
6597        sn: &VfpReg,
6598        sm: &VfpReg,
6599        cond_code: u32,
6600    ) -> Result<Vec<u8>> {
6601        let mut bytes = Vec::new();
6602        let rd_bits = reg_to_bits(rd);
6603
6604        // VCMP.F32 Sn, Sm
6605        let sn_num = vfp_sreg_to_num(sn)?;
6606        let sm_num = vfp_sreg_to_num(sm)?;
6607        let (vd, d) = encode_sreg(sn_num);
6608        let (vm, m) = encode_sreg(sm_num);
6609        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
6610        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
6611
6612        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
6613        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
6614
6615        // MOVS Rd, #0 (16-bit): 0010 0 Rd(3) 0000 0000
6616        if rd_bits < 8 {
6617            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
6618            bytes.extend_from_slice(&movs_zero.to_le_bytes());
6619        } else {
6620            // MOV.W Rd, #0 (32-bit Thumb-2)
6621            let hw1: u16 = 0xF04F;
6622            let hw2: u16 = (rd_bits as u16) << 8;
6623            bytes.extend_from_slice(&hw1.to_le_bytes());
6624            bytes.extend_from_slice(&hw2.to_le_bytes());
6625        }
6626
6627        // IT<cond> — If-Then for conditional MOV
6628        // IT encoding: 1011 1111 cond(4) mask(4)
6629        // mask = 0x8 for single "then" (IT)
6630        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
6631        bytes.extend_from_slice(&it.to_le_bytes());
6632
6633        // MOV Rd, #1 (16-bit, conditional due to IT): 0010 0 Rd(3) 0000 0001
6634        if rd_bits < 8 {
6635            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
6636            bytes.extend_from_slice(&mov_one.to_le_bytes());
6637        } else {
6638            // MOV.W Rd, #1 (32-bit)
6639            let hw1: u16 = 0xF04F;
6640            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
6641            bytes.extend_from_slice(&hw1.to_le_bytes());
6642            bytes.extend_from_slice(&hw2.to_le_bytes());
6643        }
6644
6645        Ok(bytes)
6646    }
6647
6648    /// Encode F32 constant load as Thumb-2: MOVW + MOVT + VMOV
6649    fn encode_thumb_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
6650        let mut bytes = Vec::new();
6651        let bits = value.to_bits();
6652        let rt: u32 = 12; // R12/IP as temp
6653
6654        // MOVW R12, #lo16
6655        // Thumb-2 MOVW: 11110 i 10 0100 imm4 | 0 imm3 Rd imm8
6656        let lo16 = bits & 0xFFFF;
6657        let imm4 = (lo16 >> 12) & 0xF;
6658        let i_bit = (lo16 >> 11) & 1;
6659        let imm3 = (lo16 >> 8) & 0x7;
6660        let imm8 = lo16 & 0xFF;
6661        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
6662        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
6663        bytes.extend_from_slice(&hw1.to_le_bytes());
6664        bytes.extend_from_slice(&hw2.to_le_bytes());
6665
6666        // MOVT R12, #hi16
6667        let hi16 = (bits >> 16) & 0xFFFF;
6668        let imm4 = (hi16 >> 12) & 0xF;
6669        let i_bit = (hi16 >> 11) & 1;
6670        let imm3 = (hi16 >> 8) & 0x7;
6671        let imm8 = hi16 & 0xFF;
6672        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
6673        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
6674        bytes.extend_from_slice(&hw1.to_le_bytes());
6675        bytes.extend_from_slice(&hw2.to_le_bytes());
6676
6677        // VMOV Sd, R12
6678        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
6679        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6680
6681        Ok(bytes)
6682    }
6683
6684    /// Encode VMOV + VCVT.F32.xS32 as Thumb-2
6685    fn encode_thumb_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
6686        let mut bytes = Vec::new();
6687
6688        // VMOV Sd, Rm
6689        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
6690        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6691
6692        // VCVT.F32.S32/U32 Sd, Sd
6693        let sd_num = vfp_sreg_to_num(sd)?;
6694        let (vd, d) = encode_sreg(sd_num);
6695        let (vm, m) = encode_sreg(sd_num);
6696        let base = if signed { 0xEEB80A40 } else { 0xEEB80AC0 };
6697        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
6698        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
6699
6700        Ok(bytes)
6701    }
6702
6703    /// Encode F32 rounding pseudo-op as Thumb-2 via VCVT to integer and back
6704    /// Encode F32 rounding as Thumb-2.
6705    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
6706    ///
6707    /// For trunc: uses VCVTR.S32.F32 (always truncates).
6708    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant),
6709    /// then restores FPSCR.
6710    fn encode_thumb_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
6711        let mut bytes = Vec::new();
6712        let sm_num = vfp_sreg_to_num(sm)?;
6713        let sd_num = vfp_sreg_to_num(sd)?;
6714        let (vd_s, d_s) = encode_sreg(sd_num);
6715        let (vm_s, m_s) = encode_sreg(sm_num);
6716
6717        if mode == 0b11 {
6718            // Trunc (toward zero): VCVTR.S32.F32 — bit[7]=1, always truncates
6719            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
6720            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
6721        } else {
6722            // ceil/floor/nearest: manipulate FPSCR rounding mode
6723            let rt: u32 = 12; // R12/IP as temp
6724
6725            // VMRS R12, FPSCR
6726            let vmrs = 0xEEF10A10 | (rt << 12);
6727            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
6728
6729            // BIC.W R12, R12, #(3 << 22) — clear RMode bits [23:22]
6730            // Thumb-2 modified immediate for 3<<22 = 0x00C00000:
6731            // BIC.W encoding: 11110 i 0 0001 S Rn | 0 imm3 Rd imm8
6732            // 0x00C00000 = 0x03 shifted left by 22 => Thumb mod-imm: i=0, imm3=0b101, imm8=0x03
6733            let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF); // BIC, Rn=R12
6734            let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
6735            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
6736            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
6737
6738            // ORR.W R12, R12, #(mode << 22)
6739            if mode != 0 {
6740                let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF); // ORR, Rn=R12
6741                let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
6742                bytes.extend_from_slice(&orr_hw1.to_le_bytes());
6743                bytes.extend_from_slice(&orr_hw2.to_le_bytes());
6744            }
6745
6746            // VMSR FPSCR, R12
6747            let vmsr = 0xEEE10A10 | (rt << 12);
6748            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
6749
6750            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rmode
6751            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
6752            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
6753
6754            // Restore FPSCR: clear rmode bits back to nearest (default)
6755            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
6756            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
6757            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
6758            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
6759        }
6760
6761        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
6762        let (vd2, d2) = encode_sreg(sd_num);
6763        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
6764        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
6765
6766        Ok(bytes)
6767    }
6768
6769    /// Encode F32 min/max as Thumb-2: VMOV + VCMP + VMRS + IT + VMOV
6770    fn encode_thumb_f32_minmax(
6771        &self,
6772        sd: &VfpReg,
6773        sn: &VfpReg,
6774        sm: &VfpReg,
6775        is_min: bool,
6776    ) -> Result<Vec<u8>> {
6777        let mut bytes = Vec::new();
6778        let sn_num = vfp_sreg_to_num(sn)?;
6779        let sm_num = vfp_sreg_to_num(sm)?;
6780        let sd_num = vfp_sreg_to_num(sd)?;
6781
6782        // VMOV.F32 Sd, Sn
6783        let (vd, d) = encode_sreg(sd_num);
6784        let (vn, n) = encode_sreg(sn_num);
6785        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
6786        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sn));
6787
6788        // VCMP.F32 Sn, Sm
6789        let (vm, m) = encode_sreg(sm_num);
6790        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
6791        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
6792
6793        // VMRS APSR_nzcv, FPSCR
6794        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
6795
6796        // IT GT (for min) or IT MI (for max)
6797        let cond: u16 = if is_min { 0xC } else { 0x4 };
6798        let it: u16 = 0xBF00 | (cond << 4) | 0x8;
6799        bytes.extend_from_slice(&it.to_le_bytes());
6800
6801        // VMOV{cond}.F32 Sd, Sm — conditional VMOV in IT block
6802        let vmov_sm = 0xEEB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
6803        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sm));
6804
6805        Ok(bytes)
6806    }
6807
6808    /// Encode F32 copysign as Thumb-2
6809    fn encode_thumb_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
6810        let mut bytes = Vec::new();
6811
6812        // VMOV R12, Sm (get sign source bits)
6813        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
6814            false,
6815            sm,
6816            &Reg::R12,
6817        )?));
6818
6819        // VMOV R0, Sn (get magnitude source bits)
6820        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
6821            false,
6822            sn,
6823            &Reg::R0,
6824        )?));
6825
6826        // AND.W R12, R12, #0x80000000
6827        // Thumb-2 modified immediate: 0x80000000 = constant 0x80 with rotation
6828        // Using T1 encoding: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8
6829        // 0x80000000: i=0, imm3=0b001, imm8=0x00 (rotation=4, value=0x80)
6830        // Actually encoding #0x80000000 as modified constant:
6831        // bit pattern 1 followed by 31 zeros: enc = 0b0100_00000000 = 0x0100? No.
6832        // ARM modified immediate: abcdefgh rotated. 0x80000000 = 0x80 ROR 2 = enc 0x0102
6833        // Actually: value = abcdefgh ROR (2*rot). 0x80 = 10000000, ROR 2 gives 0x20000000.
6834        // For 0x80000000: 0x02 ROR 2 = 0x80000000. So imm12 = (1<<8) | 0x02 = 0x102
6835        let hw1: u16 = 0xF000 | 12; // AND.W R12, R12, #modified_const (i=0, Rn=R12)
6836        let hw2: u16 = (0x1 << 12) | (12 << 8) | 0x02; // imm3=1, Rd=R12, imm8=0x02
6837        bytes.extend_from_slice(&hw1.to_le_bytes());
6838        bytes.extend_from_slice(&hw2.to_le_bytes());
6839
6840        // BIC.W R0, R0, #0x80000000 (R0 = register 0, fields are zero)
6841        let hw1: u16 = 0xF020; // BIC.W R0, R0, #modified_const (i=0, Rn=R0)
6842        let hw2: u16 = (0x1 << 12) | 0x02; // imm3=1, Rd=R0, imm8=0x02
6843        bytes.extend_from_slice(&hw1.to_le_bytes());
6844        bytes.extend_from_slice(&hw2.to_le_bytes());
6845
6846        // ORR.W R0, R0, R12 (R0 = register 0)
6847        let hw1: u16 = 0xEA40; // ORR.W R0, R0, R12 (Rn=R0)
6848        let hw2: u16 = 12; // Rd=R0, Rm=R12
6849        bytes.extend_from_slice(&hw1.to_le_bytes());
6850        bytes.extend_from_slice(&hw2.to_le_bytes());
6851
6852        // VMOV Sd, R0
6853        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
6854            true,
6855            sd,
6856            &Reg::R0,
6857        )?));
6858
6859        Ok(bytes)
6860    }
6861
6862    /// Encode F64 comparison as Thumb-2: VCMP.F64 + VMRS + MOV #0 + IT + MOV #1
6863    fn encode_thumb_f64_compare(
6864        &self,
6865        rd: &Reg,
6866        dn: &VfpReg,
6867        dm: &VfpReg,
6868        cond_code: u32,
6869    ) -> Result<Vec<u8>> {
6870        let mut bytes = Vec::new();
6871        let rd_bits = reg_to_bits(rd);
6872
6873        // VCMP.F64 Dn, Dm
6874        let dn_num = vfp_dreg_to_num(dn)?;
6875        let dm_num = vfp_dreg_to_num(dm)?;
6876        let (vd, d) = encode_dreg(dn_num);
6877        let (vm, m) = encode_dreg(dm_num);
6878        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
6879        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
6880
6881        // VMRS APSR_nzcv, FPSCR
6882        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
6883
6884        // MOVS Rd, #0
6885        if rd_bits < 8 {
6886            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
6887            bytes.extend_from_slice(&movs_zero.to_le_bytes());
6888        } else {
6889            let hw1: u16 = 0xF04F;
6890            let hw2: u16 = (rd_bits as u16) << 8;
6891            bytes.extend_from_slice(&hw1.to_le_bytes());
6892            bytes.extend_from_slice(&hw2.to_le_bytes());
6893        }
6894
6895        // IT<cond>
6896        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
6897        bytes.extend_from_slice(&it.to_le_bytes());
6898
6899        // MOV Rd, #1
6900        if rd_bits < 8 {
6901            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
6902            bytes.extend_from_slice(&mov_one.to_le_bytes());
6903        } else {
6904            let hw1: u16 = 0xF04F;
6905            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
6906            bytes.extend_from_slice(&hw1.to_le_bytes());
6907            bytes.extend_from_slice(&hw2.to_le_bytes());
6908        }
6909
6910        Ok(bytes)
6911    }
6912
6913    /// Encode F64 constant load as Thumb-2: MOVW+MOVT (lo32 into R0) + MOVW+MOVT (hi32 into R12) + VMOV Dd, R0, R12
6914    fn encode_thumb_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
6915        let mut bytes = Vec::new();
6916        let bits = value.to_bits();
6917        let lo32 = bits as u32;
6918        let hi32 = (bits >> 32) as u32;
6919
6920        // MOVW R0, #lo16(lo32)
6921        let lo16 = lo32 & 0xFFFF;
6922        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(0, lo16)?);
6923
6924        // MOVT R0, #hi16(lo32)
6925        let hi16 = (lo32 >> 16) & 0xFFFF;
6926        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(0, hi16)?);
6927
6928        // MOVW R12, #lo16(hi32)
6929        let lo16 = hi32 & 0xFFFF;
6930        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
6931
6932        // MOVT R12, #hi16(hi32)
6933        let hi16 = (hi32 >> 16) & 0xFFFF;
6934        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
6935
6936        // VMOV Dd, R0, R12
6937        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
6938        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6939
6940        Ok(bytes)
6941    }
6942
6943    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as Thumb-2
6944    fn encode_thumb_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
6945        let mut bytes = Vec::new();
6946
6947        // VMOV S0, Rm
6948        let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
6949        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6950
6951        // VCVT.F64.S32 Dd, S0 or VCVT.F64.U32 Dd, S0
6952        let dd_num = vfp_dreg_to_num(dd)?;
6953        let (vd, d) = encode_dreg(dd_num);
6954        let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
6955        let vcvt = base | (d << 22) | (vd << 12);
6956        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
6957
6958        Ok(bytes)
6959    }
6960
6961    /// Encode VCVT.F64.F32 Dd, Sm as Thumb-2
6962    fn encode_thumb_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
6963        let dd_num = vfp_dreg_to_num(dd)?;
6964        let sm_num = vfp_sreg_to_num(sm)?;
6965        let (vd, d) = encode_dreg(dd_num);
6966        let (vm, m) = encode_sreg(sm_num);
6967
6968        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
6969        Ok(vfp_to_thumb_bytes(vcvt))
6970    }
6971
6972    /// Encode VCVT.S32/U32.F64 S0, Dm + VMOV Rd, S0 as Thumb-2
6973    fn encode_thumb_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
6974        let mut bytes = Vec::new();
6975        let dm_num = vfp_dreg_to_num(dm)?;
6976        let (vm, m) = encode_dreg(dm_num);
6977
6978        // VCVT.S32.F64 S0, Dm or VCVT.U32.F64 S0, Dm
6979        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
6980        let vcvt = base | (m << 5) | vm;
6981        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
6982
6983        // VMOV Rd, S0
6984        let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
6985        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6986
6987        Ok(bytes)
6988    }
6989
6990    /// Encode F64 rounding pseudo-op as Thumb-2 via VCVT to integer and back
6991    /// Encode F64 rounding as Thumb-2.
6992    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
6993    fn encode_thumb_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
6994        let mut bytes = Vec::new();
6995        let dm_num = vfp_dreg_to_num(dm)?;
6996        let dd_num = vfp_dreg_to_num(dd)?;
6997        let (vm, m) = encode_dreg(dm_num);
6998        let (vd, d) = encode_dreg(dd_num);
6999
7000        if mode == 0b11 {
7001            // Trunc: VCVTR.S32.F64 — bit[7]=1, always truncates
7002            let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
7003            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7004        } else {
7005            let rt: u32 = 12;
7006
7007            // VMRS R12, FPSCR
7008            let vmrs = 0xEEF10A10 | (rt << 12);
7009            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7010
7011            // BIC.W R12, R12, #(3 << 22)
7012            let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF);
7013            let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
7014            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7015            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7016
7017            // ORR.W R12, R12, #(mode << 22)
7018            if mode != 0 {
7019                let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF);
7020                let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
7021                bytes.extend_from_slice(&orr_hw1.to_le_bytes());
7022                bytes.extend_from_slice(&orr_hw2.to_le_bytes());
7023            }
7024
7025            // VMSR FPSCR, R12
7026            let vmsr = 0xEEE10A10 | (rt << 12);
7027            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7028
7029            // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0)
7030            let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
7031            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7032
7033            // Restore FPSCR
7034            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7035            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7036            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7037            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7038        }
7039
7040        // VCVT.F64.S32 Dd, S0
7041        let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
7042        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
7043
7044        Ok(bytes)
7045    }
7046
7047    /// Encode F64 min/max as Thumb-2
7048    fn encode_thumb_f64_minmax(
7049        &self,
7050        dd: &VfpReg,
7051        dn: &VfpReg,
7052        dm: &VfpReg,
7053        is_min: bool,
7054    ) -> Result<Vec<u8>> {
7055        let mut bytes = Vec::new();
7056        let dn_num = vfp_dreg_to_num(dn)?;
7057        let dm_num = vfp_dreg_to_num(dm)?;
7058        let dd_num = vfp_dreg_to_num(dd)?;
7059
7060        // VMOV.F64 Dd, Dn
7061        let (vd, d) = encode_dreg(dd_num);
7062        let (vn, n) = encode_dreg(dn_num);
7063        let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
7064        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_dn));
7065
7066        // VCMP.F64 Dn, Dm
7067        let (vm, m) = encode_dreg(dm_num);
7068        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7069        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7070
7071        // VMRS APSR_nzcv, FPSCR
7072        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7073
7074        // IT GT (for min) or IT MI (for max)
7075        let cond: u16 = if is_min { 0xC } else { 0x4 };
7076        let it: u16 = 0xBF00 | (cond << 4) | 0x8;
7077        bytes.extend_from_slice(&it.to_le_bytes());
7078
7079        // VMOV{cond}.F64 Dd, Dm
7080        let vmov_dm = 0xEEB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7081        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_dm));
7082
7083        Ok(bytes)
7084    }
7085
7086    /// Encode F64 copysign as Thumb-2
7087    fn encode_thumb_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7088        let mut bytes = Vec::new();
7089
7090        // VMOV R0, R12, Dm (get sign source)
7091        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_dreg(
7092            false,
7093            dm,
7094            &Reg::R0,
7095            &Reg::R12,
7096        )?));
7097
7098        // VMOV R1, R2, Dn (get magnitude source)
7099        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_dreg(
7100            false,
7101            dn,
7102            &Reg::R1,
7103            &Reg::R2,
7104        )?));
7105
7106        // AND.W R12, R12, #0x80000000 (i=0, Rn=R12)
7107        let hw1: u16 = 0xF000 | 12;
7108        let hw2: u16 = (0x1 << 12) | (12 << 8) | 0x02;
7109        bytes.extend_from_slice(&hw1.to_le_bytes());
7110        bytes.extend_from_slice(&hw2.to_le_bytes());
7111
7112        // BIC.W R2, R2, #0x80000000 (i=0, Rn=R2)
7113        let hw1: u16 = 0xF020 | 2;
7114        let hw2: u16 = (0x1 << 12) | (2 << 8) | 0x02;
7115        bytes.extend_from_slice(&hw1.to_le_bytes());
7116        bytes.extend_from_slice(&hw2.to_le_bytes());
7117
7118        // ORR.W R2, R2, R12
7119        let hw1: u16 = 0xEA40 | 2;
7120        let hw2: u16 = (2 << 8) | 12;
7121        bytes.extend_from_slice(&hw1.to_le_bytes());
7122        bytes.extend_from_slice(&hw2.to_le_bytes());
7123
7124        // VMOV Dd, R1, R2
7125        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_dreg(
7126            true,
7127            dd,
7128            &Reg::R1,
7129            &Reg::R2,
7130        )?));
7131
7132        Ok(bytes)
7133    }
7134
7135    /// Encode VCVT.S32/U32.F32 + VMOV as Thumb-2
7136    fn encode_thumb_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7137        let mut bytes = Vec::new();
7138
7139        let sm_num = vfp_sreg_to_num(sm)?;
7140        let (vd, d) = encode_sreg(sm_num);
7141        let (vm, m) = encode_sreg(sm_num);
7142        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
7143        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7144        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7145
7146        // VMOV Rd, Sm
7147        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
7148        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7149
7150        Ok(bytes)
7151    }
7152
7153    // === Thumb-2 32-bit encoding helpers ===
7154
7155    /// Encode Thumb-2 32-bit ADD with immediate
7156    fn encode_thumb32_add(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7157        let rd_bits = reg_to_bits(rd);
7158        let rn_bits = reg_to_bits(rn);
7159
7160        // The `i:imm3:imm8` field is split the same way for both forms.
7161        let i_bit = (imm >> 11) & 1;
7162        let imm3 = (imm >> 8) & 0x7;
7163        let imm8 = imm & 0xFF;
7164
7165        let hw1_base = if imm <= 0xFF {
7166            // ADD.W (T3): the field is a ThumbExpandImm modified immediate. For
7167            // imm <= 0xFF (i:imm3 = 0000) it is the zero-extended byte, which is
7168            // correct — keep this form so existing encodings stay bit-identical.
7169            0xF100
7170        } else if imm <= 0xFFF {
7171            // ADDW (T4): a PLAIN 12-bit immediate (0..4095) — no ThumbExpandImm.
7172            // This is what makes `add sp, sp, #frame` correct for frame sizes
7173            // >= 256, which ADD.W (T3) would silently mis-encode (e.g. #256 -> #0).
7174            0xF200
7175        } else {
7176            return Err(synth_core::Error::synthesis(
7177                "ADD immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7178            ));
7179        };
7180
7181        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7182        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7183
7184        let mut bytes = hw1.to_le_bytes().to_vec();
7185        bytes.extend_from_slice(&hw2.to_le_bytes());
7186        Ok(bytes)
7187    }
7188
7189    /// Encode Thumb-2 32-bit SUB with immediate
7190    fn encode_thumb32_sub(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7191        let rd_bits = reg_to_bits(rd);
7192        let rn_bits = reg_to_bits(rn);
7193
7194        let i_bit = (imm >> 11) & 1;
7195        let imm3 = (imm >> 8) & 0x7;
7196        let imm8 = imm & 0xFF;
7197
7198        let hw1_base = if imm <= 0xFF {
7199            // SUB.W (T3) modified immediate — correct for the zero-extended byte
7200            // (imm <= 0xFF). Kept bit-identical for existing encodings.
7201            0xF1A0
7202        } else if imm <= 0xFFF {
7203            // SUBW (T4): plain 12-bit immediate (0..4095). Makes
7204            // `sub sp, sp, #frame` correct for frame sizes >= 256.
7205            0xF2A0
7206        } else {
7207            return Err(synth_core::Error::synthesis(
7208                "SUB immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7209            ));
7210        };
7211
7212        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7213        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7214
7215        let mut bytes = hw1.to_le_bytes().to_vec();
7216        bytes.extend_from_slice(&hw2.to_le_bytes());
7217        Ok(bytes)
7218    }
7219
7220    /// Encode Thumb-2 32-bit ADDS with immediate (sets flags)
7221    fn encode_thumb32_adds(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7222        let rd_bits = reg_to_bits(rd);
7223        let rn_bits = reg_to_bits(rn);
7224
7225        // ADDS.W (flag-setting) has only the modified-immediate form — error on
7226        // an un-encodable value rather than silently add the wrong constant.
7227        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7228            synth_core::Error::synthesis(
7229                "ADDS immediate is not a valid ThumbExpandImm — materialize into a register",
7230            )
7231        })?;
7232        let i_bit = (field >> 11) & 1;
7233        let imm3 = (field >> 8) & 0x7;
7234        let imm8 = field & 0xFF;
7235
7236        // ADDS.W Rd, Rn, #imm (with S=1)
7237        // First halfword: 1111 0 i 0 1000 1 Rn = F110 | i<<10 | Rn
7238        let hw1: u16 = (0xF110 | (i_bit << 10) | rn_bits) as u16;
7239        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7240
7241        let mut bytes = hw1.to_le_bytes().to_vec();
7242        bytes.extend_from_slice(&hw2.to_le_bytes());
7243        Ok(bytes)
7244    }
7245
7246    /// Encode Thumb-2 32-bit SUBS with immediate (sets flags)
7247    fn encode_thumb32_subs(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7248        let rd_bits = reg_to_bits(rd);
7249        let rn_bits = reg_to_bits(rn);
7250
7251        // SUBS.W (flag-setting) has only the modified-immediate form — error on
7252        // an un-encodable value rather than silently subtract the wrong constant.
7253        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7254            synth_core::Error::synthesis(
7255                "SUBS immediate is not a valid ThumbExpandImm — materialize into a register",
7256            )
7257        })?;
7258        let i_bit = (field >> 11) & 1;
7259        let imm3 = (field >> 8) & 0x7;
7260        let imm8 = field & 0xFF;
7261
7262        // SUBS.W Rd, Rn, #imm (with S=1)
7263        // First halfword: 1111 0 i 0 1101 1 Rn = F1B0 | i<<10 | Rn
7264        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7265        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7266
7267        let mut bytes = hw1.to_le_bytes().to_vec();
7268        bytes.extend_from_slice(&hw2.to_le_bytes());
7269        Ok(bytes)
7270    }
7271
7272    /// Encode Thumb-2 32-bit MOVW (16-bit immediate)
7273    ///
7274    /// # Contract (Verus-style)
7275    /// ```text
7276    /// requires rd <= R14
7277    /// ensures result.len() == 4
7278    /// ensures (imm & 0xFFFF) can be reconstructed from the encoding
7279    /// ```
7280    fn encode_thumb32_movw(&self, rd: &Reg, imm: u32) -> Result<Vec<u8>> {
7281        let rd_bits = reg_to_bits(rd);
7282        reg_bits_checked(rd_bits)?;
7283        let imm16 = imm & 0xFFFF;
7284
7285        // MOVW Rd, #imm16
7286        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7287        let imm4 = (imm16 >> 12) & 0xF;
7288        let i_bit = (imm16 >> 11) & 1;
7289        let imm3 = (imm16 >> 8) & 0x7;
7290        let imm8 = imm16 & 0xFF;
7291
7292        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7293        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7294
7295        let mut bytes = hw1.to_le_bytes().to_vec();
7296        bytes.extend_from_slice(&hw2.to_le_bytes());
7297        encoding_contracts::verify_thumb32(&bytes);
7298        Ok(bytes)
7299    }
7300
7301    /// Encode Thumb-2 32-bit shift with immediate
7302    ///
7303    /// # Contract (Verus-style)
7304    /// ```text
7305    /// requires rd <= R14, rm <= R14
7306    /// ensures result.len() == 4
7307    /// ```
7308    fn encode_thumb32_shift(
7309        &self,
7310        rd: &Reg,
7311        rm: &Reg,
7312        shift: u32,
7313        shift_type: u8,
7314    ) -> Result<Vec<u8>> {
7315        let rd_bits = reg_to_bits(rd);
7316        let rm_bits = reg_to_bits(rm);
7317        reg_bits_checked(rd_bits)?;
7318        reg_bits_checked(rm_bits)?;
7319        let imm5 = shift & 0x1F;
7320        let imm2 = imm5 & 0x3;
7321        let imm3 = (imm5 >> 2) & 0x7;
7322
7323        // MOV.W Rd, Rm, <shift> #imm
7324        // EA4F 0 imm3 Rd imm2 type Rm
7325        let hw1: u16 = 0xEA4F;
7326        let hw2: u16 =
7327            ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | ((shift_type as u32) << 4) | rm_bits)
7328                as u16;
7329
7330        let mut bytes = hw1.to_le_bytes().to_vec();
7331        bytes.extend_from_slice(&hw2.to_le_bytes());
7332        Ok(bytes)
7333    }
7334
7335    /// Encode Thumb-2 32-bit shift by register
7336    /// Encoding: 11111010 0xx0 Rn | 1111 Rd 0000 Rm
7337    /// shift_type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
7338    fn encode_thumb32_shift_reg(
7339        &self,
7340        rd: &Reg,
7341        rn: &Reg,
7342        rm: &Reg,
7343        shift_type: u8,
7344    ) -> Result<Vec<u8>> {
7345        let rd_bits = reg_to_bits(rd);
7346        let rn_bits = reg_to_bits(rn);
7347        let rm_bits = reg_to_bits(rm);
7348
7349        // hw1: 1111 1010 0xx0 Rn
7350        let hw1: u16 = (0xFA00 | ((shift_type as u32) << 5) | rn_bits) as u16;
7351        // hw2: 1111 Rd 0000 Rm
7352        let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
7353
7354        let mut bytes = hw1.to_le_bytes().to_vec();
7355        bytes.extend_from_slice(&hw2.to_le_bytes());
7356        Ok(bytes)
7357    }
7358
7359    /// Encode Thumb-2 32-bit CMP with immediate
7360    fn encode_thumb32_cmp_imm(&self, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7361        let rn_bits = reg_to_bits(rn);
7362
7363        // CMP.W has only the modified-immediate form (no plain-imm12 like ADDW),
7364        // so an un-encodable immediate MUST be materialized into a register by
7365        // the selector. Error rather than silently compare the wrong constant.
7366        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7367            synth_core::Error::synthesis(
7368                "CMP immediate is not a valid ThumbExpandImm — materialize into a register",
7369            )
7370        })?;
7371        let i_bit = (field >> 11) & 1;
7372        let imm3 = (field >> 8) & 0x7;
7373        let imm8 = field & 0xFF;
7374
7375        // CMP.W Rn, #imm
7376        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7377        let hw2: u16 = ((imm3 << 12) | 0x0F00 | imm8) as u16;
7378
7379        let mut bytes = hw1.to_le_bytes().to_vec();
7380        bytes.extend_from_slice(&hw2.to_le_bytes());
7381        Ok(bytes)
7382    }
7383
7384    /// #372/#382: resolve the base register AND residual immediate offset for an
7385    /// `I64Ldr`/`I64Str` whose address may carry an index register. Returns
7386    /// `(base, low_offset)`; the caller accesses the halves at `[base,
7387    /// #low_offset]` and `[base, #low_offset + 4]`.
7388    ///
7389    /// - Frame access (no `offset_reg`, e.g. a spilled local at `[SP, #off]`):
7390    ///   returns `(addr.base, off)` and emits NOTHING — byte-identical.
7391    /// - Memory access (`reg_imm(R11, addr, offset)` = `R11 + addr + offset`)
7392    ///   with `offset + 4 <= 0xFFF`: emits `ADD.W ip, base, index` and returns
7393    ///   `(ip, offset)`, folding `offset`/`offset+4` into the halves' imm12.
7394    ///   Byte-identical to the pre-#382 (#372) behavior.
7395    /// - Memory access with `offset + 4 > 0xFFF`: the imm12 form cannot hold the
7396    ///   high half's offset, so `encode_thumb32_ldr`'s `check_ldst_imm12` (#259)
7397    ///   rightly refused it and the WHOLE function was skipped (#382). Instead
7398    ///   MATERIALIZE the offset into the base: `ADD ip, index, #offset` (against
7399    ///   the read-only INDEX register, so `encode_thumb32_add_imm` never trips its
7400    ///   `rd==rn==R12` alias trap), then `ADD.W ip, ip, base` (+ R11), and return
7401    ///   `(ip, 0)` so the halves use `[ip, #0]` / `[ip, #4]`.
7402    ///
7403    /// The effective address is fully materialized into `ip` BEFORE the halves
7404    /// are accessed, so an `rdlo` aliasing the index register is safe.
7405    fn i64_effective_base(&self, bytes: &mut Vec<u8>, addr: &MemAddr) -> Result<(Reg, u32)> {
7406        let offset = if addr.offset < 0 {
7407            0u32
7408        } else {
7409            addr.offset as u32
7410        };
7411        match addr.offset_reg {
7412            Some(idx) => {
7413                let ip = Reg::R12;
7414                if offset.wrapping_add(4) > 0xFFF {
7415                    // Large static offset (#382): fold it (and R11) into ip so the
7416                    // imm12 halves stay in range instead of skipping the function.
7417                    // ADD ip, index, #offset  (index != ip → no add_imm alias trap)
7418                    bytes.extend_from_slice(&self.encode_thumb32_add_imm(&ip, &idx, offset)?);
7419                    // ADD.W ip, ip, base  (+ R11)
7420                    bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
7421                        reg_to_bits(&ip),
7422                        reg_to_bits(&ip),
7423                        reg_to_bits(&addr.base),
7424                    )?);
7425                    Ok((ip, 0))
7426                } else {
7427                    // ADD.W ip, addr.base, idx  (Thumb-2, byte-verified vs as)
7428                    let hw1: u16 = 0xEB00 | reg_to_bits(&addr.base) as u16;
7429                    let hw2: u16 = 0x0C00 | reg_to_bits(&idx) as u16;
7430                    bytes.extend_from_slice(&hw1.to_le_bytes());
7431                    bytes.extend_from_slice(&hw2.to_le_bytes());
7432                    Ok((ip, offset))
7433                }
7434            }
7435            None => Ok((addr.base, offset)),
7436        }
7437    }
7438
7439    /// Encode Thumb-2 32-bit LDR
7440    fn encode_thumb32_ldr(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7441        let rd_bits = reg_to_bits(rd);
7442        let base_bits = reg_to_bits(base);
7443
7444        // LDR.W Rd, [Rn, #imm12]
7445        check_ldst_imm12(offset)?;
7446        let hw1: u16 = (0xF8D0 | base_bits) as u16;
7447        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7448
7449        let mut bytes = hw1.to_le_bytes().to_vec();
7450        bytes.extend_from_slice(&hw2.to_le_bytes());
7451        Ok(bytes)
7452    }
7453
7454    /// Encode Thumb-2 32-bit STR
7455    fn encode_thumb32_str(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7456        let rd_bits = reg_to_bits(rd);
7457        let base_bits = reg_to_bits(base);
7458
7459        // STR.W Rd, [Rn, #imm12]
7460        check_ldst_imm12(offset)?;
7461        let hw1: u16 = (0xF8C0 | base_bits) as u16;
7462        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7463
7464        let mut bytes = hw1.to_le_bytes().to_vec();
7465        bytes.extend_from_slice(&hw2.to_le_bytes());
7466        Ok(bytes)
7467    }
7468
7469    /// Encode Thumb-2 32-bit LDR with register offset: LDR.W Rd, [Rn, Rm]
7470    fn encode_thumb32_ldr_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7471        let rd_bits = reg_to_bits(rd);
7472        let base_bits = reg_to_bits(base);
7473        let rm_bits = reg_to_bits(offset_reg);
7474
7475        // LDR.W Rd, [Rn, Rm, LSL #0]
7476        // Encoding: 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
7477        // imm2 = 00 for no shift (LSL #0)
7478        let hw1: u16 = (0xF850 | base_bits) as u16;
7479        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7480
7481        let mut bytes = hw1.to_le_bytes().to_vec();
7482        bytes.extend_from_slice(&hw2.to_le_bytes());
7483        Ok(bytes)
7484    }
7485
7486    /// Encode Thumb-2 32-bit STR with register offset: STR.W Rd, [Rn, Rm]
7487    fn encode_thumb32_str_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7488        let rd_bits = reg_to_bits(rd);
7489        let base_bits = reg_to_bits(base);
7490        let rm_bits = reg_to_bits(offset_reg);
7491
7492        // STR.W Rd, [Rn, Rm, LSL #0]
7493        // Encoding: 1111 1000 0100 Rn | Rt 0000 00 imm2 Rm
7494        // imm2 = 00 for no shift (LSL #0)
7495        let hw1: u16 = (0xF840 | base_bits) as u16;
7496        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7497
7498        let mut bytes = hw1.to_le_bytes().to_vec();
7499        bytes.extend_from_slice(&hw2.to_le_bytes());
7500        Ok(bytes)
7501    }
7502
7503    // === Sub-word load/store Thumb-2 encoding helpers ===
7504
7505    /// Encode Thumb-2 32-bit LDRB with immediate: LDRB.W Rd, [Rn, #imm12]
7506    fn encode_thumb32_ldrb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7507        let rd_bits = reg_to_bits(rd);
7508        let base_bits = reg_to_bits(base);
7509        // LDRB.W Rd, [Rn, #imm12]: 1111 1000 1001 Rn | Rt imm12
7510        check_ldst_imm12(offset)?;
7511        let hw1: u16 = (0xF890 | base_bits) as u16;
7512        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7513        let mut bytes = hw1.to_le_bytes().to_vec();
7514        bytes.extend_from_slice(&hw2.to_le_bytes());
7515        Ok(bytes)
7516    }
7517
7518    /// Encode Thumb-2 32-bit LDRB with register: LDRB.W Rd, [Rn, Rm]
7519    fn encode_thumb32_ldrb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7520        let rd_bits = reg_to_bits(rd);
7521        let base_bits = reg_to_bits(base);
7522        let rm_bits = reg_to_bits(offset_reg);
7523        // LDRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0001 Rn | Rt 0000 00 imm2 Rm
7524        let hw1: u16 = (0xF810 | base_bits) as u16;
7525        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7526        let mut bytes = hw1.to_le_bytes().to_vec();
7527        bytes.extend_from_slice(&hw2.to_le_bytes());
7528        Ok(bytes)
7529    }
7530
7531    /// Encode Thumb-2 32-bit LDRSB with immediate: LDRSB.W Rd, [Rn, #imm12]
7532    fn encode_thumb32_ldrsb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7533        let rd_bits = reg_to_bits(rd);
7534        let base_bits = reg_to_bits(base);
7535        // LDRSB.W Rd, [Rn, #imm12]: 1111 1001 1001 Rn | Rt imm12
7536        check_ldst_imm12(offset)?;
7537        let hw1: u16 = (0xF990 | base_bits) as u16;
7538        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7539        let mut bytes = hw1.to_le_bytes().to_vec();
7540        bytes.extend_from_slice(&hw2.to_le_bytes());
7541        Ok(bytes)
7542    }
7543
7544    /// Encode Thumb-2 32-bit LDRSB with register: LDRSB.W Rd, [Rn, Rm]
7545    fn encode_thumb32_ldrsb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7546        let rd_bits = reg_to_bits(rd);
7547        let base_bits = reg_to_bits(base);
7548        let rm_bits = reg_to_bits(offset_reg);
7549        // LDRSB.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0001 Rn | Rt 0000 00 imm2 Rm
7550        let hw1: u16 = (0xF910 | base_bits) as u16;
7551        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7552        let mut bytes = hw1.to_le_bytes().to_vec();
7553        bytes.extend_from_slice(&hw2.to_le_bytes());
7554        Ok(bytes)
7555    }
7556
7557    /// Encode Thumb-2 32-bit LDRH with immediate: LDRH.W Rd, [Rn, #imm12]
7558    fn encode_thumb32_ldrh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7559        let rd_bits = reg_to_bits(rd);
7560        let base_bits = reg_to_bits(base);
7561        // LDRH.W Rd, [Rn, #imm12]: 1111 1000 1011 Rn | Rt imm12
7562        check_ldst_imm12(offset)?;
7563        let hw1: u16 = (0xF8B0 | base_bits) as u16;
7564        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7565        let mut bytes = hw1.to_le_bytes().to_vec();
7566        bytes.extend_from_slice(&hw2.to_le_bytes());
7567        Ok(bytes)
7568    }
7569
7570    /// Encode Thumb-2 32-bit LDRH with register: LDRH.W Rd, [Rn, Rm]
7571    fn encode_thumb32_ldrh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7572        let rd_bits = reg_to_bits(rd);
7573        let base_bits = reg_to_bits(base);
7574        let rm_bits = reg_to_bits(offset_reg);
7575        // LDRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0011 Rn | Rt 0000 00 imm2 Rm
7576        let hw1: u16 = (0xF830 | base_bits) as u16;
7577        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7578        let mut bytes = hw1.to_le_bytes().to_vec();
7579        bytes.extend_from_slice(&hw2.to_le_bytes());
7580        Ok(bytes)
7581    }
7582
7583    /// Encode Thumb-2 32-bit LDRSH with immediate: LDRSH.W Rd, [Rn, #imm12]
7584    fn encode_thumb32_ldrsh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7585        let rd_bits = reg_to_bits(rd);
7586        let base_bits = reg_to_bits(base);
7587        // LDRSH.W Rd, [Rn, #imm12]: 1111 1001 1011 Rn | Rt imm12
7588        check_ldst_imm12(offset)?;
7589        let hw1: u16 = (0xF9B0 | base_bits) as u16;
7590        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7591        let mut bytes = hw1.to_le_bytes().to_vec();
7592        bytes.extend_from_slice(&hw2.to_le_bytes());
7593        Ok(bytes)
7594    }
7595
7596    /// Encode Thumb-2 32-bit LDRSH with register: LDRSH.W Rd, [Rn, Rm]
7597    fn encode_thumb32_ldrsh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7598        let rd_bits = reg_to_bits(rd);
7599        let base_bits = reg_to_bits(base);
7600        let rm_bits = reg_to_bits(offset_reg);
7601        // LDRSH.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0011 Rn | Rt 0000 00 imm2 Rm
7602        let hw1: u16 = (0xF930 | base_bits) as u16;
7603        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7604        let mut bytes = hw1.to_le_bytes().to_vec();
7605        bytes.extend_from_slice(&hw2.to_le_bytes());
7606        Ok(bytes)
7607    }
7608
7609    /// Encode Thumb-2 32-bit STRB with immediate: STRB.W Rd, [Rn, #imm12]
7610    fn encode_thumb32_strb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7611        let rd_bits = reg_to_bits(rd);
7612        let base_bits = reg_to_bits(base);
7613        // STRB.W Rd, [Rn, #imm12]: 1111 1000 1000 Rn | Rt imm12
7614        check_ldst_imm12(offset)?;
7615        let hw1: u16 = (0xF880 | base_bits) as u16;
7616        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7617        let mut bytes = hw1.to_le_bytes().to_vec();
7618        bytes.extend_from_slice(&hw2.to_le_bytes());
7619        Ok(bytes)
7620    }
7621
7622    /// Encode Thumb-2 32-bit STRB with register: STRB.W Rd, [Rn, Rm]
7623    fn encode_thumb32_strb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7624        let rd_bits = reg_to_bits(rd);
7625        let base_bits = reg_to_bits(base);
7626        let rm_bits = reg_to_bits(offset_reg);
7627        // STRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0000 Rn | Rt 0000 00 imm2 Rm
7628        let hw1: u16 = (0xF800 | base_bits) as u16;
7629        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7630        let mut bytes = hw1.to_le_bytes().to_vec();
7631        bytes.extend_from_slice(&hw2.to_le_bytes());
7632        Ok(bytes)
7633    }
7634
7635    /// Encode Thumb-2 32-bit STRH with immediate: STRH.W Rd, [Rn, #imm12]
7636    fn encode_thumb32_strh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7637        let rd_bits = reg_to_bits(rd);
7638        let base_bits = reg_to_bits(base);
7639        // STRH.W Rd, [Rn, #imm12]: 1111 1000 1010 Rn | Rt imm12
7640        check_ldst_imm12(offset)?;
7641        let hw1: u16 = (0xF8A0 | base_bits) as u16;
7642        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7643        let mut bytes = hw1.to_le_bytes().to_vec();
7644        bytes.extend_from_slice(&hw2.to_le_bytes());
7645        Ok(bytes)
7646    }
7647
7648    /// Encode Thumb-2 32-bit STRH with register: STRH.W Rd, [Rn, Rm]
7649    fn encode_thumb32_strh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7650        let rd_bits = reg_to_bits(rd);
7651        let base_bits = reg_to_bits(base);
7652        let rm_bits = reg_to_bits(offset_reg);
7653        // STRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0010 Rn | Rt 0000 00 imm2 Rm
7654        let hw1: u16 = (0xF820 | base_bits) as u16;
7655        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7656        let mut bytes = hw1.to_le_bytes().to_vec();
7657        bytes.extend_from_slice(&hw2.to_le_bytes());
7658        Ok(bytes)
7659    }
7660
7661    /// Encode Thumb-2 32-bit ADD with immediate: ADD.W Rd, Rn, #imm
7662    fn encode_thumb32_add_imm(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7663        let rd_bits = reg_to_bits(rd);
7664        let rn_bits = reg_to_bits(rn);
7665
7666        // For small immediates, use ADD.W Rd, Rn, #imm12
7667        // Encoding: 1111 0 i 0 1 0 0 0 S Rn | 0 imm3 Rd imm8
7668        // S = 0 (don't update flags)
7669        // The 12-bit immediate is encoded as: i:imm3:imm8
7670        // For simplicity, we only support imm <= 0xFFF (direct encoding)
7671        if imm <= 0xFFF {
7672            let i_bit = (imm >> 11) & 1;
7673            let imm3 = (imm >> 8) & 0x7;
7674            let imm8 = imm & 0xFF;
7675
7676            let hw1: u16 = (0xF100 | (i_bit << 10) | rn_bits) as u16;
7677            let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7678
7679            let mut bytes = hw1.to_le_bytes().to_vec();
7680            bytes.extend_from_slice(&hw2.to_le_bytes());
7681            Ok(bytes)
7682        } else {
7683            // Out-of-range immediate (> 0xFFF): materialize it into a scratch
7684            // register, then ADD.W Rd, Rn, scratch. This is the #180/#185
7685            // "encoder must produce a legal sequence, not assert" class — see #350.
7686            //
7687            // Scratch choice (must NEVER equal Rn, or Rn would be clobbered before
7688            // the ADD reads it):
7689            //   - rd != rn  => use rd itself (rn is untouched, since rd != rn).
7690            //   - rd == rn  => use R12/IP (the reserved encoder scratch). rd/rn are
7691            //                  never R12 (R12 is non-allocatable), so it can't alias.
7692            //
7693            // The materialized value is the same whether or not MOVT is emitted, so
7694            // the byte length depends only on `imm` (and rd==rn) — the size probe and
7695            // the final emit therefore agree (mandatory: the function is encoded twice).
7696            let scratch: u32 = if rd_bits == rn_bits {
7697                12 // R12/IP — in-place add, can't use rd because rd == rn
7698            } else {
7699                rd_bits // rn is preserved because rd != rn
7700            };
7701            // Invariant: the scratch must never alias Rn (would clobber it before
7702            // the ADD reads it). Unreachable in real codegen (rd/rn are never R12,
7703            // which is reserved encoder scratch), but the encoder is also driven by
7704            // the `encoder_no_panic` fuzz harness with ARBITRARY registers — incl.
7705            // rd==rn==R12, which makes scratch (R12) alias Rn. The encoder contract
7706            // (#180/#185) is Ok-or-Err, never a panic, so return a typed error
7707            // instead of asserting. #350 follow-up.
7708            if scratch == rn_bits {
7709                return Err(synth_core::Error::synthesis(format!(
7710                    "ADD #imm: cannot lower #{imm:#x} for Rd==Rn==R12 — no free scratch \
7711                     register (R12 is the reserved encoder scratch and aliases Rn here)"
7712                )));
7713            }
7714
7715            let lo16 = imm & 0xFFFF;
7716            let hi16 = (imm >> 16) & 0xFFFF;
7717
7718            let mut bytes = self.encode_thumb32_movw_raw(scratch, lo16)?;
7719            if hi16 != 0 {
7720                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(scratch, hi16)?);
7721            }
7722            bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rn_bits, scratch)?);
7723            Ok(bytes)
7724        }
7725    }
7726
7727    // === Raw encoding helpers for POPCNT (take register numbers directly) ===
7728
7729    /// Encode Thumb-2 32-bit MOVW (16-bit immediate) - raw version
7730    ///
7731    /// # Contract (Verus-style)
7732    /// ```text
7733    /// requires rd <= 14, imm16 <= 0xFFFF
7734    /// ensures result.len() == 4
7735    /// ```
7736    fn encode_thumb32_movw_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
7737        reg_bits_checked(rd)?;
7738        encoding_contracts::verify_imm16(imm16);
7739        // MOVW Rd, #imm16
7740        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7741        let imm16 = imm16 & 0xFFFF;
7742        let imm4 = (imm16 >> 12) & 0xF;
7743        let i_bit = (imm16 >> 11) & 1;
7744        let imm3 = (imm16 >> 8) & 0x7;
7745        let imm8 = imm16 & 0xFF;
7746
7747        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7748        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
7749
7750        let mut bytes = hw1.to_le_bytes().to_vec();
7751        bytes.extend_from_slice(&hw2.to_le_bytes());
7752        encoding_contracts::verify_thumb32(&bytes);
7753        Ok(bytes)
7754    }
7755
7756    /// Encode Thumb-2 32-bit MOVT (move top 16 bits) - raw version
7757    ///
7758    /// # Contract (Verus-style)
7759    /// ```text
7760    /// requires rd <= 14, imm16 <= 0xFFFF
7761    /// ensures result.len() == 4
7762    /// ```
7763    fn encode_thumb32_movt_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
7764        reg_bits_checked(rd)?;
7765        encoding_contracts::verify_imm16(imm16);
7766        // MOVT Rd, #imm16
7767        // 1111 0 i 10 1 1 0 0 imm4 | 0 imm3 Rd imm8
7768        let imm16 = imm16 & 0xFFFF;
7769        let imm4 = (imm16 >> 12) & 0xF;
7770        let i_bit = (imm16 >> 11) & 1;
7771        let imm3 = (imm16 >> 8) & 0x7;
7772        let imm8 = imm16 & 0xFF;
7773
7774        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
7775        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
7776
7777        let mut bytes = hw1.to_le_bytes().to_vec();
7778        bytes.extend_from_slice(&hw2.to_le_bytes());
7779        encoding_contracts::verify_thumb32(&bytes);
7780        Ok(bytes)
7781    }
7782
7783    /// Encode Thumb-2 32-bit LSR (logical shift right) with immediate - raw version
7784    fn encode_thumb32_lsr_raw(&self, rd: u32, rm: u32, shift: u32) -> Result<Vec<u8>> {
7785        // MOV.W Rd, Rm, LSR #imm
7786        // EA4F 0 imm3 Rd imm2 01 Rm
7787        let imm5 = shift & 0x1F;
7788        let imm2 = imm5 & 0x3;
7789        let imm3 = (imm5 >> 2) & 0x7;
7790
7791        let hw1: u16 = 0xEA4F;
7792        let hw2: u16 = ((imm3 << 12) | (rd << 8) | (imm2 << 6) | (0b01 << 4) | rm) as u16;
7793
7794        let mut bytes = hw1.to_le_bytes().to_vec();
7795        bytes.extend_from_slice(&hw2.to_le_bytes());
7796        Ok(bytes)
7797    }
7798
7799    /// Encode Thumb-2 32-bit AND (register) - raw version
7800    fn encode_thumb32_and_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7801        // AND.W Rd, Rn, Rm
7802        // EA00 Rn | 0 Rd 00 00 Rm
7803        let hw1: u16 = (0xEA00 | rn) as u16;
7804        let hw2: u16 = ((rd << 8) | rm) as u16;
7805
7806        let mut bytes = hw1.to_le_bytes().to_vec();
7807        bytes.extend_from_slice(&hw2.to_le_bytes());
7808        Ok(bytes)
7809    }
7810
7811    /// Encode Thumb-2 32-bit AND with immediate - raw version
7812    fn encode_thumb32_and_imm_raw(&self, rd: u32, rn: u32, imm: u32) -> Result<Vec<u8>> {
7813        // AND.W Rd, Rn, #<modified_immediate>
7814        // For small immediates (0-255), the encoding is simpler
7815        // F0 00 Rn | 0 imm3 Rd imm8
7816        let i_bit = (imm >> 11) & 1;
7817        let imm3 = (imm >> 8) & 0x7;
7818        let imm8 = imm & 0xFF;
7819
7820        let hw1: u16 = (0xF000 | (i_bit << 10) | rn) as u16;
7821        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
7822
7823        let mut bytes = hw1.to_le_bytes().to_vec();
7824        bytes.extend_from_slice(&hw2.to_le_bytes());
7825        Ok(bytes)
7826    }
7827
7828    /// Encode Thumb-2 32-bit SUB (register) - raw version
7829    fn encode_thumb32_sub_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7830        // SUB.W Rd, Rn, Rm
7831        // EBA0 Rn | 0 Rd 00 00 Rm
7832        let hw1: u16 = (0xEBA0 | rn) as u16;
7833        let hw2: u16 = ((rd << 8) | rm) as u16;
7834
7835        let mut bytes = hw1.to_le_bytes().to_vec();
7836        bytes.extend_from_slice(&hw2.to_le_bytes());
7837        Ok(bytes)
7838    }
7839
7840    /// Encode Thumb-2 32-bit ADD (register) - raw version
7841    fn encode_thumb32_add_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7842        // ADD.W Rd, Rn, Rm
7843        // EB00 Rn | 0 Rd 00 00 Rm
7844        let hw1: u16 = (0xEB00 | rn) as u16;
7845        let hw2: u16 = ((rd << 8) | rm) as u16;
7846
7847        let mut bytes = hw1.to_le_bytes().to_vec();
7848        bytes.extend_from_slice(&hw2.to_le_bytes());
7849        Ok(bytes)
7850    }
7851
7852    /// Encode Thumb-2 32-bit ADDS (register, flag-setting) - raw version.
7853    /// Used as the high-register fallback for `ArmOp::Adds` (i64 low-word add)
7854    /// so R8-R11 pair operands don't overflow the 16-bit field — #178/#180.
7855    fn encode_thumb32_adds_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7856        // ADDS.W Rd, Rn, Rm (T3, S=1): EB10 Rn | 0 Rd 00 00 Rm
7857        let hw1: u16 = (0xEB10 | rn) as u16;
7858        let hw2: u16 = ((rd << 8) | rm) as u16;
7859        let mut bytes = hw1.to_le_bytes().to_vec();
7860        bytes.extend_from_slice(&hw2.to_le_bytes());
7861        Ok(bytes)
7862    }
7863
7864    /// Encode Thumb-2 32-bit SUBS (register, flag-setting) - raw version.
7865    /// High-register fallback for `ArmOp::Subs` (i64 low-word subtract) — #178/#180.
7866    fn encode_thumb32_subs_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7867        // SUBS.W Rd, Rn, Rm (T3, S=1): EBB0 Rn | 0 Rd 00 00 Rm
7868        let hw1: u16 = (0xEBB0 | rn) as u16;
7869        let hw2: u16 = ((rd << 8) | rm) as u16;
7870        let mut bytes = hw1.to_le_bytes().to_vec();
7871        bytes.extend_from_slice(&hw2.to_le_bytes());
7872        Ok(bytes)
7873    }
7874
7875    /// Encode a sequence of ARM instructions
7876    pub fn encode_sequence(&self, ops: &[ArmOp]) -> Result<Vec<u8>> {
7877        let mut code = Vec::new();
7878
7879        for op in ops {
7880            let encoded = self.encode(op)?;
7881            code.extend_from_slice(&encoded);
7882        }
7883
7884        Ok(code)
7885    }
7886}
7887
7888/// Convert register to bit encoding (0-15)
7889/// Reverse of the ARMv7-M `ThumbExpandImm`: given a 32-bit immediate, return the
7890/// 12-bit `i:imm3:imm8` field if it is a representable modified immediate, else
7891/// `None` (the caller must materialize the value into a register). This is the
7892/// shared correct path for the data-processing immediate encoders — without it
7893/// they pack raw bits and silently mis-encode any value `> 0xFF` that isn't a
7894/// modified immediate (the silent-miscompile class behind #251/#253/#255).
7895fn try_thumb_expand_imm(value: u32) -> Option<u32> {
7896    // i:imm3 = 0000 → 8-bit value, zero-extended (00000000 00000000 00000000 XY).
7897    if value <= 0xFF {
7898        return Some(value);
7899    }
7900    let b0 = value & 0xFF; // byte 0
7901    let b1 = (value >> 8) & 0xFF; // byte 1
7902    // 0x00XY00XY (i:imm3 = 0001) — XY in bytes 0 and 2
7903    if value == (b0 << 16) | b0 {
7904        return Some(0x100 | b0);
7905    }
7906    // 0xXY00XY00 (i:imm3 = 0010) — XY in bytes 1 and 3
7907    if value == (b1 << 24) | (b1 << 8) {
7908        return Some(0x200 | b1);
7909    }
7910    // 0xXYXYXYXY (i:imm3 = 0011) — XY in all four bytes
7911    if value == (b0 << 24) | (b0 << 16) | (b0 << 8) | b0 {
7912        return Some(0x300 | b0);
7913    }
7914    // An 8-bit value with bit 7 set, rotated right by 8..=31. `rotate_left(rot)`
7915    // undoes the encoded right rotation; if the result is `1bbbbbbb` (0x80..=0xFF)
7916    // the value is representable. imm12[11:7] = rot, imm12[6:0] = low 7 bits.
7917    for rot in 8..=31u32 {
7918        let unrot = value.rotate_left(rot);
7919        if (0x80..=0xFF).contains(&unrot) {
7920            return Some((rot << 7) | (unrot & 0x7F));
7921        }
7922    }
7923    None
7924}
7925
7926/// Guard a Thumb-2 `LDR/STR Rd, [Rn, #imm12]` offset. The imm12 form supports
7927/// `0..=4095`; a larger offset must be materialized into a register by the
7928/// selector (register-offset addressing). Returning `Err` rather than silently
7929/// masking `offset & 0xFFF` closes the wrong-address miscompile class (#259,
7930/// the load/store sibling of #253/#255).
7931fn check_ldst_imm12(offset: u32) -> Result<()> {
7932    if offset > 0xFFF {
7933        Err(synth_core::Error::synthesis(
7934            "load/store immediate offset > 0xFFF (4095) — materialize the offset into a register",
7935        ))
7936    } else {
7937        Ok(())
7938    }
7939}
7940
7941fn reg_to_bits(reg: &Reg) -> u32 {
7942    match reg {
7943        Reg::R0 => 0,
7944        Reg::R1 => 1,
7945        Reg::R2 => 2,
7946        Reg::R3 => 3,
7947        Reg::R4 => 4,
7948        Reg::R5 => 5,
7949        Reg::R6 => 6,
7950        Reg::R7 => 7,
7951        Reg::R8 => 8,
7952        Reg::R9 => 9,
7953        Reg::R10 => 10,
7954        Reg::R11 => 11,
7955        Reg::R12 => 12,
7956        Reg::SP => 13,
7957        Reg::LR => 14,
7958        Reg::PC => 15,
7959    }
7960}
7961
7962// ======================================================================
7963// #610 — i64 fixed-ABI expansion wrappers.
7964//
7965// The hand-written multi-instruction i64 cores (rotl/rotr and the div/rem
7966// shift-subtract loops) compute in FIXED low registers. Before #610 the
7967// div/rem arms ignored their operand fields outright (hardcoded R0:R1 /
7968// R2:R3 in, result to R0:R1) and the rot arms used R3/R4 scratch that
7969// collided with selector-assigned registers — then restored the saved
7970// scratch OVER the result (`POP {R4}` with rd_lo == R4), so the op
7971// returned the caller's stale register: 0 for every input under qemu.
7972//
7973// These wrappers make each core honor its register parameters:
7974//   1. save R0-R3,
7975//   2. marshal the operand registers into the core's fixed input regs via
7976//      the stack (permutation-safe: every source is read before any fixed
7977//      register is written),
7978//   3. run the fixed-reg core (self-preserving for R4+; R12 is encoder
7979//      scratch and never allocatable, #212),
7980//   4. MOV the result pair from R0:R1 into the selector's rd pair,
7981//   5. restore R0-R3, skipping any register the result now occupies.
7982//
7983// All emitted lengths are register-independent so the optimized path's
7984// byte-size estimator (`estimate_arm_byte_size`, pinned by the
7985// estimator↔encoder agreement oracle #498/#511) stays a constant per op.
7986// ======================================================================
7987
7988/// Steps 1+2: `PUSH {R0-R3}`, then marshal `srcs` (operand registers, any of
7989/// R0-R12) into `R0..R<n>` via individual stack pushes. Sources are all read
7990/// before any destination register is written, so arbitrary source/target
7991/// permutations (including operands living in R0-R3) are safe.
7992fn emit_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
7993    debug_assert!(srcs.len() <= 4);
7994    // PUSH {R0-R3} — save the caller-visible low registers.
7995    bytes.extend_from_slice(&0xB40Fu16.to_le_bytes());
7996    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
7997    for src in srcs.iter().rev() {
7998        let rt = reg_to_bits(src) as u16;
7999        bytes.extend_from_slice(&0xF84Du16.to_le_bytes());
8000        bytes.extend_from_slice(&((rt << 12) | 0x0D04).to_le_bytes());
8001    }
8002    // POP {Ri} — Ri := srcs[i].
8003    for i in 0..srcs.len() as u16 {
8004        bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes());
8005    }
8006}
8007
8008/// Steps 4+5: move the core's R0:R1 result into the selector's rd pair, then
8009/// restore the R0-R3 saved by [`emit_i64_fixed_abi_entry`], skipping any
8010/// register the result now lives in (its saved caller word is discarded).
8011fn emit_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8012    let lo = reg_to_bits(rdlo);
8013    let hi = reg_to_bits(rdhi);
8014    if lo == 1 && hi == 0 {
8015        // A fully swapped pair would clobber one half in either MOV order.
8016        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8017        return Err(synth_core::Error::synthesis(
8018            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8019        ));
8020    }
8021    let mov16 = |bytes: &mut Vec<u8>, rd: u32, rm: u32| {
8022        let d = ((rd >> 3) & 1) as u16;
8023        bytes.extend_from_slice(
8024            &(0x4600u16 | (d << 7) | ((rm as u16) << 3) | ((rd & 7) as u16)).to_le_bytes(),
8025        );
8026    };
8027    if hi == 0 {
8028        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8029        mov16(bytes, lo, 0);
8030        mov16(bytes, hi, 1);
8031    } else {
8032        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8033        mov16(bytes, hi, 1);
8034        mov16(bytes, lo, 0);
8035    }
8036    for i in 0..4u32 {
8037        if i == lo || i == hi {
8038            // The result lives here — drop the saved caller word.
8039            bytes.extend_from_slice(&0xB001u16.to_le_bytes()); // ADD SP, #4
8040        } else {
8041            bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes()); // POP {Ri}
8042        }
8043    }
8044    Ok(())
8045}
8046
8047/// WASM `i64.div_*` / `i64.rem_*` by zero must trap, matching the i32 path's
8048/// cmp/bne/udf guard. Emitted after marshaling, when the divisor pair is in
8049/// R2:R3: `ORRS R12, R2, R3` — `BNE` over a `UDF #0` when nonzero.
8050fn emit_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8051    bytes.extend_from_slice(&0xEA52u16.to_le_bytes()); // ORRS.W R12, R2, R3
8052    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8053    bytes.extend_from_slice(&0xD100u16.to_le_bytes()); // BNE.N +0 (skip the UDF)
8054    bytes.extend_from_slice(&0xDE00u16.to_le_bytes()); // UDF #0 — divide by zero
8055}
8056
8057/// WASM `i64.div_s(INT64_MIN, -1)` must trap (Core §4.3.2 `idiv_s`: the
8058/// quotient +2^63 is unrepresentable), matching the i32 path's overflow
8059/// guard — #633: without it the core negated INT64_MIN onto itself and
8060/// silently returned INT64_MIN. Emitted after marshaling, when the dividend
8061/// pair is in R0:R1 and the divisor pair in R2:R3; R12 is encoder scratch.
8062///
8063/// div_s ONLY — `i64.rem_s(INT64_MIN, -1)` is defined as 0 and must NOT
8064/// trap (`irem_s`), so the I64RemS arm never calls this. 22 bytes,
8065/// register-independent (estimator contract, #498/#511).
8066fn emit_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8067    // AND.W R12, R2, R3 — R12 == 0xFFFFFFFF iff divisor == -1
8068    bytes.extend_from_slice(&0xEA02u16.to_le_bytes());
8069    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8070    // CMN.W R12, #1 — EQ iff both divisor words are all-ones
8071    bytes.extend_from_slice(&0xF11Cu16.to_le_bytes());
8072    bytes.extend_from_slice(&0x0F01u16.to_le_bytes());
8073    // BNE .no_trap
8074    bytes.extend_from_slice(&0xD105u16.to_le_bytes());
8075    // CMP R0, #0 — dividend lo word of INT64_MIN
8076    bytes.extend_from_slice(&0x2800u16.to_le_bytes());
8077    // BNE .no_trap
8078    bytes.extend_from_slice(&0xD103u16.to_le_bytes());
8079    // CMP.W R1, #0x80000000 — dividend hi word of INT64_MIN
8080    bytes.extend_from_slice(&0xF1B1u16.to_le_bytes());
8081    bytes.extend_from_slice(&0x4F00u16.to_le_bytes());
8082    // BNE .no_trap
8083    bytes.extend_from_slice(&0xD100u16.to_le_bytes());
8084    // UDF #0 — signed-division overflow
8085    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
8086    // .no_trap:
8087}
8088
8089// ======================================================================
8090// #615 — A32 (ARM-mode) twins of the #610 i64 fixed-ABI wrappers above.
8091// Identical register contract, A32 encodings: the multi-instruction i64
8092// cores (rotl/rotr, div/rem) compute in fixed low registers (value/dividend
8093// R0:R1, amount R2 / divisor R2:R3, result to R0:R1); the wrappers marshal
8094// the selector-assigned operand registers in and the result out, saving and
8095// restoring the caller-visible R0-R3 around the core.
8096// ======================================================================
8097
8098/// A32 steps 1+2: `STMDB SP!, {R0-R3}`, then marshal `srcs` into `R0..R<n>`
8099/// via individual stack pushes (`STR src, [SP, #-4]!` in reverse order, then
8100/// `LDR Ri, [SP], #4`). Every source is read before any fixed register is
8101/// written, so arbitrary source/target permutations are safe.
8102fn emit_a32_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8103    debug_assert!(srcs.len() <= 4);
8104    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8105    // PUSH {R0-R3} — save the caller-visible low registers.
8106    w(bytes, 0xE92D_000F);
8107    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8108    for src in srcs.iter().rev() {
8109        w(bytes, 0xE52D_0004 | (reg_to_bits(src) << 12));
8110    }
8111    // LDR Ri, [SP], #4 — Ri := srcs[i].
8112    for i in 0..srcs.len() as u32 {
8113        w(bytes, 0xE49D_0004 | (i << 12));
8114    }
8115}
8116
8117/// A32 steps 4+5: move the core's R0:R1 result into the selector's rd pair,
8118/// then restore the R0-R3 saved by [`emit_a32_i64_fixed_abi_entry`], skipping
8119/// any register the result now lives in (its saved caller word is discarded).
8120fn emit_a32_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8121    let lo = reg_to_bits(rdlo);
8122    let hi = reg_to_bits(rdhi);
8123    if lo == 1 && hi == 0 {
8124        // A fully swapped pair would clobber one half in either MOV order.
8125        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8126        return Err(synth_core::Error::synthesis(
8127            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8128        ));
8129    }
8130    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8131    let mov = |bytes: &mut Vec<u8>, rd: u32, rm: u32| w(bytes, 0xE1A0_0000 | (rd << 12) | rm);
8132    if hi == 0 {
8133        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8134        mov(bytes, lo, 0);
8135        mov(bytes, hi, 1);
8136    } else {
8137        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8138        mov(bytes, hi, 1);
8139        mov(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            w(bytes, 0xE28D_D004); // ADD SP, SP, #4
8145        } else {
8146            w(bytes, 0xE49D_0004 | (i << 12)); // LDR Ri, [SP], #4
8147        }
8148    }
8149    Ok(())
8150}
8151
8152/// A32 zero-divisor trap, emitted after marshaling when the divisor pair is
8153/// in R2:R3: `ORRS R12, R2, R3` sets Z iff the divisor is zero; `BNE` skips a
8154/// `UDF #0` (WASM div/rem-by-zero must trap, matching the Thumb-2 twin).
8155fn emit_a32_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8156    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8157    w(bytes, 0xE192_C003); // ORRS R12, R2, R3
8158    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8159    w(bytes, 0xE7F0_00F0); // UDF #0 — divide by zero
8160}
8161
8162/// A32 twin of [`emit_i64_divs_overflow_trap`] (#633): trap on
8163/// `i64.div_s(INT64_MIN, -1)`. Conditional execution replaces the Thumb
8164/// branches — the CMPEQ chain leaves EQ set only when divisor == -1 AND
8165/// dividend == INT64_MIN. div_s only; rem_s must keep returning 0.
8166fn emit_a32_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8167    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8168    w(bytes, 0xE002_C003); // AND   R12, R2, R3 (== 0xFFFFFFFF iff divisor == -1)
8169    w(bytes, 0xE37C_0001); // CMN   R12, #1     (EQ iff divisor == -1)
8170    w(bytes, 0x0350_0000); // CMPEQ R0, #0      (EQ iff also dividend lo == 0)
8171    w(bytes, 0x0351_0102); // CMPEQ R1, #0x80000000 (EQ iff dividend == INT64_MIN)
8172    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8173    w(bytes, 0xE7F0_00F0); // UDF #0 — signed-division overflow
8174}
8175
8176/// Fallible form of the `verify_reg_bits` contract. PC (R15) is not a valid
8177/// data operand for the Thumb-2 encodings that use this guard (SDIV/UDIV/MLS/…
8178/// are UNPREDICTABLE with PC). Synth's own codegen never emits PC there, but
8179/// the encoder must stay *total* over arbitrary `ArmOp` inputs — the fuzz
8180/// harness (`encoder_no_panic`) requires Ok-or-Err, never a panic. Pre-fix, the
8181/// `debug_assert` in `verify_reg_bits` aborted under `-Cdebug-assertions`.
8182/// Returns a typed Err instead. See #185.
8183fn reg_bits_checked(bits: u32) -> Result<()> {
8184    if bits > 14 {
8185        return Err(synth_core::Error::synthesis(format!(
8186            "register bits {bits} (PC/R15) is not a valid operand for this Thumb-2 encoding"
8187        )));
8188    }
8189    Ok(())
8190}
8191
8192/// Try to encode a 32-bit value as an ARM rotated immediate (imm8 ROR 2*rot4).
8193/// Returns Some((encoded_bits, 1)) if representable, None otherwise.
8194fn try_encode_rotated_imm(val: u32) -> Option<(u32, u32)> {
8195    if val == 0 {
8196        return Some((0, 1));
8197    }
8198    for rot in 0..16u32 {
8199        let shift = rot * 2;
8200        // Rotate left by shift (undo the ROR) to see if result fits in 8 bits
8201        let unrotated = val.rotate_left(shift);
8202        if unrotated <= 0xFF {
8203            // Encoded as: rot4(4 bits) | imm8(8 bits) = rotate_imm << 8 | imm8
8204            return Some(((rot << 8) | unrotated, 1));
8205        }
8206    }
8207    None
8208}
8209
8210/// Encode operand2 field and return (bits, immediate_flag).
8211/// For ARM32 mode, immediates use the rotated-immediate encoding (imm8 ROR 2*rot4).
8212/// Panics if an immediate value cannot be represented. Callers that need large
8213/// immediates should use MOVW/MOVT instead of Operand2::Imm.
8214fn encode_operand2(op2: &Operand2) -> Result<(u32, u32)> {
8215    match op2 {
8216        Operand2::Imm(val) => {
8217            let uval = *val as u32;
8218            // Attempt rotated-immediate encoding (ARM32 Operand2)
8219            if let Some(encoded) = try_encode_rotated_imm(uval) {
8220                Ok(encoded)
8221            } else {
8222                // #378-class honesty: an immediate that can't be expressed as an
8223                // ARM32 rotated immediate is an INTERNAL selector bug — large
8224                // constants must be materialized via MOVW/MOVT, not passed here.
8225                // FAIL HONESTLY with an Err rather than silently masking to
8226                // `uval & 0xFF` and emitting a WRONG immediate. The encoder is
8227                // Ok-or-Err, never corrupt (#180/#185); a loud Err is also why
8228                // this is an Err and not a panic (the `encoder_no_panic` fuzz
8229                // contract — malformed/oversized input must degrade, not crash).
8230                Err(synth_core::Error::synthesis(format!(
8231                    "encode_operand2: immediate {uval:#x} ({val}) is not an ARM32 \
8232                     rotated immediate — the selector must materialize large \
8233                     constants via MOVW/MOVT"
8234                )))
8235            }
8236        }
8237
8238        Operand2::Reg(reg) => {
8239            let reg_bits = reg_to_bits(reg);
8240            Ok((reg_bits, 0)) // I=0 for register
8241        }
8242
8243        Operand2::RegShift {
8244            rm,
8245            shift: _,
8246            amount,
8247        } => {
8248            // Simplified encoding with shift
8249            let rm_bits = reg_to_bits(rm);
8250            let shift_bits = (*amount & 0x1F) << 7;
8251            Ok((shift_bits | rm_bits, 0))
8252        }
8253    }
8254}
8255
8256/// Encode memory address to (base_reg, offset)
8257fn encode_mem_addr(addr: &MemAddr) -> (u32, u32) {
8258    let base_bits = reg_to_bits(&addr.base);
8259    let offset_bits = (addr.offset as u32) & 0xFFF; // 12-bit offset
8260    (base_bits, offset_bits)
8261}
8262
8263/// S-register number: S0=0, S1=1, ..., S31=31
8264fn vfp_sreg_to_num(reg: &VfpReg) -> Result<u32> {
8265    match reg {
8266        VfpReg::S0 => Ok(0),
8267        VfpReg::S1 => Ok(1),
8268        VfpReg::S2 => Ok(2),
8269        VfpReg::S3 => Ok(3),
8270        VfpReg::S4 => Ok(4),
8271        VfpReg::S5 => Ok(5),
8272        VfpReg::S6 => Ok(6),
8273        VfpReg::S7 => Ok(7),
8274        VfpReg::S8 => Ok(8),
8275        VfpReg::S9 => Ok(9),
8276        VfpReg::S10 => Ok(10),
8277        VfpReg::S11 => Ok(11),
8278        VfpReg::S12 => Ok(12),
8279        VfpReg::S13 => Ok(13),
8280        VfpReg::S14 => Ok(14),
8281        VfpReg::S15 => Ok(15),
8282        VfpReg::S16 => Ok(16),
8283        VfpReg::S17 => Ok(17),
8284        VfpReg::S18 => Ok(18),
8285        VfpReg::S19 => Ok(19),
8286        VfpReg::S20 => Ok(20),
8287        VfpReg::S21 => Ok(21),
8288        VfpReg::S22 => Ok(22),
8289        VfpReg::S23 => Ok(23),
8290        VfpReg::S24 => Ok(24),
8291        VfpReg::S25 => Ok(25),
8292        VfpReg::S26 => Ok(26),
8293        VfpReg::S27 => Ok(27),
8294        VfpReg::S28 => Ok(28),
8295        VfpReg::S29 => Ok(29),
8296        VfpReg::S30 => Ok(30),
8297        VfpReg::S31 => Ok(31),
8298        // D-registers are not used in F32 single-precision encodings
8299        _ => Err(synth_core::Error::SynthesisError(
8300            "D-register not supported in single-precision VFP encoding".to_string(),
8301        )),
8302    }
8303}
8304
8305/// D-register number: D0=0, D1=1, ..., D15=15
8306fn vfp_dreg_to_num(reg: &VfpReg) -> Result<u32> {
8307    match reg {
8308        VfpReg::D0 => Ok(0),
8309        VfpReg::D1 => Ok(1),
8310        VfpReg::D2 => Ok(2),
8311        VfpReg::D3 => Ok(3),
8312        VfpReg::D4 => Ok(4),
8313        VfpReg::D5 => Ok(5),
8314        VfpReg::D6 => Ok(6),
8315        VfpReg::D7 => Ok(7),
8316        VfpReg::D8 => Ok(8),
8317        VfpReg::D9 => Ok(9),
8318        VfpReg::D10 => Ok(10),
8319        VfpReg::D11 => Ok(11),
8320        VfpReg::D12 => Ok(12),
8321        VfpReg::D13 => Ok(13),
8322        VfpReg::D14 => Ok(14),
8323        VfpReg::D15 => Ok(15),
8324        // S-registers are not used in F64 double-precision encodings
8325        _ => Err(synth_core::Error::SynthesisError(
8326            "S-register not supported in double-precision VFP encoding".to_string(),
8327        )),
8328    }
8329}
8330
8331/// Split S-register into (Vx[3:0], qualifier_bit) for VFP encoding.
8332/// For an S-register number s: Vx = s >> 1, qualifier = s & 1.
8333/// The qualifier bit goes to D (bit 22), N (bit 7), or M (bit 5) depending on role.
8334fn encode_sreg(s: u32) -> (u32, u32) {
8335    (s >> 1, s & 1)
8336}
8337
8338/// Split D-register into (Vx[3:0], qualifier_bit) for VFP double-precision encoding.
8339/// For a D-register number d: Vx = d & 0xF, qualifier = (d >> 4) & 1.
8340/// For D0-D15, qualifier is always 0.
8341fn encode_dreg(d: u32) -> (u32, u32) {
8342    (d & 0xF, (d >> 4) & 1)
8343}
8344
8345/// Encode a VFP 3-register arithmetic instruction (VADD.F32, VSUB.F32, VMUL.F32, VDIV.F32).
8346/// Returns the full 32-bit instruction word.
8347///
8348/// VFP encoding: [cond 1110] [D opc1 Vn] [Vd 101 sz] [N opc2 M 0 Vm]
8349/// For single-precision (sz=0), coprocessor = 0xA (bits[11:8]).
8350fn encode_vfp_3reg(base: u32, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<u32> {
8351    let sd_num = vfp_sreg_to_num(sd)?;
8352    let sn_num = vfp_sreg_to_num(sn)?;
8353    let sm_num = vfp_sreg_to_num(sm)?;
8354    let (vd, d) = encode_sreg(sd_num);
8355    let (vn, n) = encode_sreg(sn_num);
8356    let (vm, m) = encode_sreg(sm_num);
8357
8358    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8359}
8360
8361/// Encode a VFP 2-register instruction (VNEG.F32, VABS.F32, VSQRT.F32).
8362/// Returns the full 32-bit instruction word.
8363fn encode_vfp_2reg(base: u32, sd: &VfpReg, sm: &VfpReg) -> Result<u32> {
8364    let sd_num = vfp_sreg_to_num(sd)?;
8365    let sm_num = vfp_sreg_to_num(sm)?;
8366    let (vd, d) = encode_sreg(sd_num);
8367    let (vm, m) = encode_sreg(sm_num);
8368
8369    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8370}
8371
8372/// Encode a VFP load/store (VLDR.F32 / VSTR.F32).
8373/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8374/// U bit (bit 23) controls add/subtract offset.
8375fn encode_vfp_ldst(base: u32, sd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8376    let sd_num = vfp_sreg_to_num(sd)?;
8377    let (vd, d) = encode_sreg(sd_num);
8378    let rn = reg_to_bits(&addr.base);
8379
8380    let offset = addr.offset;
8381    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8382    let abs_offset = offset.unsigned_abs();
8383    let imm8 = (abs_offset / 4) & 0xFF;
8384
8385    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8386}
8387
8388/// Encode VMOV between core register and S-register.
8389/// VMOV Sn, Rt: 0xEE00_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8390/// VMOV Rt, Sn: 0xEE10_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8391fn encode_vmov_core_sreg(to_sreg: bool, sreg: &VfpReg, core: &Reg) -> Result<u32> {
8392    let s_num = vfp_sreg_to_num(sreg)?;
8393    let (vn, n) = encode_sreg(s_num);
8394    let rt = reg_to_bits(core);
8395
8396    let base = if to_sreg { 0xEE000A10 } else { 0xEE100A10 };
8397    Ok(base | (vn << 16) | (rt << 12) | (n << 7))
8398}
8399
8400/// Encode a VFP 3-register double-precision instruction (VADD.F64, VSUB.F64, etc.).
8401/// For double-precision (sz=1), coprocessor = 0xB (bits[11:8]).
8402/// The base should have bit 8 = 1 for F64 (0xB suffix instead of 0xA).
8403fn encode_vfp_3reg_f64(base: u32, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<u32> {
8404    let dd_num = vfp_dreg_to_num(dd)?;
8405    let dn_num = vfp_dreg_to_num(dn)?;
8406    let dm_num = vfp_dreg_to_num(dm)?;
8407    let (vd, d) = encode_dreg(dd_num);
8408    let (vn, n) = encode_dreg(dn_num);
8409    let (vm, m) = encode_dreg(dm_num);
8410
8411    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8412}
8413
8414/// Encode a VFP 2-register double-precision instruction (VNEG.F64, VABS.F64, VSQRT.F64).
8415fn encode_vfp_2reg_f64(base: u32, dd: &VfpReg, dm: &VfpReg) -> Result<u32> {
8416    let dd_num = vfp_dreg_to_num(dd)?;
8417    let dm_num = vfp_dreg_to_num(dm)?;
8418    let (vd, d) = encode_dreg(dd_num);
8419    let (vm, m) = encode_dreg(dm_num);
8420
8421    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8422}
8423
8424/// Encode a VFP load/store for double-precision (VLDR.64 / VSTR.64).
8425/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8426fn encode_vfp_ldst_f64(base: u32, dd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8427    let dd_num = vfp_dreg_to_num(dd)?;
8428    let (vd, d) = encode_dreg(dd_num);
8429    let rn = reg_to_bits(&addr.base);
8430
8431    let offset = addr.offset;
8432    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8433    let abs_offset = offset.unsigned_abs();
8434    let imm8 = (abs_offset / 4) & 0xFF;
8435
8436    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8437}
8438
8439/// Encode VMOV between two core registers and a D-register.
8440/// VMOV Dm, Rt, Rt2: 0xEC40_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8441/// VMOV Rt, Rt2, Dm: 0xEC50_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8442fn encode_vmov_core_dreg(
8443    to_dreg: bool,
8444    dreg: &VfpReg,
8445    core_lo: &Reg,
8446    core_hi: &Reg,
8447) -> Result<u32> {
8448    let d_num = vfp_dreg_to_num(dreg)?;
8449    let (vm, m) = encode_dreg(d_num);
8450    let rt = reg_to_bits(core_lo);
8451    let rt2 = reg_to_bits(core_hi);
8452
8453    let base = if to_dreg { 0xEC400B10 } else { 0xEC500B10 };
8454    Ok(base | (rt2 << 16) | (rt << 12) | (m << 5) | vm)
8455}
8456
8457/// Emit a VFP 32-bit instruction as Thumb-2 bytes (two LE halfwords).
8458fn vfp_to_thumb_bytes(instr: u32) -> Vec<u8> {
8459    let hw1 = ((instr >> 16) & 0xFFFF) as u16;
8460    let hw2 = (instr & 0xFFFF) as u16;
8461    let mut bytes = hw1.to_le_bytes().to_vec();
8462    bytes.extend_from_slice(&hw2.to_le_bytes());
8463    bytes
8464}
8465
8466// ============================================================================
8467// Helium MVE encoding helpers
8468// ============================================================================
8469
8470/// Q-register number: Q0=0, Q1=1, ..., Q7=7
8471fn qreg_to_num(reg: &QReg) -> u32 {
8472    match reg {
8473        QReg::Q0 => 0,
8474        QReg::Q1 => 1,
8475        QReg::Q2 => 2,
8476        QReg::Q3 => 3,
8477        QReg::Q4 => 4,
8478        QReg::Q5 => 5,
8479        QReg::Q6 => 6,
8480        QReg::Q7 => 7,
8481    }
8482}
8483
8484/// MVE element size to encoding bits: S8=0b00, S16=0b01, S32=0b10
8485fn mve_size_bits(size: &MveSize) -> u32 {
8486    match size {
8487        MveSize::S8 => 0b00,
8488        MveSize::S16 => 0b01,
8489        MveSize::S32 => 0b10,
8490    }
8491}
8492
8493/// Encode MVE 3-register instruction.
8494/// Q-registers are encoded as D-register pairs: Q0=D0:D1, Q1=D2:D3, etc.
8495/// In NEON/MVE encoding, the Q-register uses D-register number = Qn * 2.
8496fn encode_mve_3reg(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
8497    let d = qreg_to_num(qd) * 2;
8498    let n = qreg_to_num(qn) * 2;
8499    let m = qreg_to_num(qm) * 2;
8500
8501    // Standard NEON/MVE 3-register encoding:
8502    // D bit (bit 22) = Vd[4], Vd[3:0] = bits [15:12]
8503    // N bit (bit 7)  = Vn[4], Vn[3:0] = bits [19:16]
8504    // M bit (bit 5)  = Vm[4], Vm[3:0] = bits [3:0]
8505    let vd = d & 0xF;
8506    let d_bit = (d >> 4) & 1;
8507    let vn = n & 0xF;
8508    let n_bit = (n >> 4) & 1;
8509    let vm = m & 0xF;
8510    let m_bit = (m >> 4) & 1;
8511
8512    base | (d_bit << 22) | (vn << 16) | (vd << 12) | (n_bit << 7) | (m_bit << 5) | vm
8513}
8514
8515/// Encode MVE 3-register bitwise instruction (VAND, VORR, VEOR, VBIC).
8516fn encode_mve_3reg_bitwise(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
8517    encode_mve_3reg(base, qd, qn, qm)
8518}
8519
8520/// Encode MVE VLDRW.32 Qd, [Rn, #offset]
8521/// Format: EC9x xxxx - contiguous load, word-sized elements
8522fn encode_mve_vldrw(qd: &QReg, addr: &MemAddr) -> u32 {
8523    let qd_enc = qreg_to_num(qd) * 2;
8524    let rn = reg_to_bits(&addr.base);
8525    let offset = addr.offset;
8526    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8527    let abs_offset = offset.unsigned_abs();
8528    let imm7 = (abs_offset / 4) & 0x7F; // 7-bit word-aligned offset
8529
8530    // VLDRW.32 Qd, [Rn, #imm]: ED10 xx80 variant
8531    0xED100E80
8532        | (u_bit << 23)
8533        | ((qd_enc >> 4) << 22)
8534        | (rn << 16)
8535        | ((qd_enc & 0xF) << 12)
8536        | (imm7 & 0x7F)
8537}
8538
8539/// Encode MVE VSTRW.32 Qd, [Rn, #offset]
8540fn encode_mve_vstrw(qd: &QReg, addr: &MemAddr) -> u32 {
8541    let qd_enc = qreg_to_num(qd) * 2;
8542    let rn = reg_to_bits(&addr.base);
8543    let offset = addr.offset;
8544    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8545    let abs_offset = offset.unsigned_abs();
8546    let imm7 = (abs_offset / 4) & 0x7F;
8547
8548    0xED000E80
8549        | (u_bit << 23)
8550        | ((qd_enc >> 4) << 22)
8551        | (rn << 16)
8552        | ((qd_enc & 0xF) << 12)
8553        | (imm7 & 0x7F)
8554}
8555
8556impl ArmEncoder {
8557    /// Encode MVE constant load: MOVW+MOVT+VMOV for each 32-bit word, then assemble Q-register
8558    fn encode_thumb_mve_const(&self, qd: &QReg, bytes: &[u8; 16]) -> Result<Vec<u8>> {
8559        let mut result = Vec::new();
8560        let qd_num = qreg_to_num(qd);
8561
8562        // Load each 32-bit word into R12 (temp) then VMOV into S-register
8563        for i in 0..4 {
8564            let word = u32::from_le_bytes([
8565                bytes[i * 4],
8566                bytes[i * 4 + 1],
8567                bytes[i * 4 + 2],
8568                bytes[i * 4 + 3],
8569            ]);
8570            let lo16 = word & 0xFFFF;
8571            let hi16 = (word >> 16) & 0xFFFF;
8572
8573            // MOVW R12, #lo16
8574            result.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
8575            // MOVT R12, #hi16
8576            if hi16 != 0 {
8577                result.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
8578            }
8579
8580            // VMOV Sn, R12 where Sn = Qd*4 + i
8581            let s_num = qd_num * 4 + i as u32;
8582            let (vn, n) = encode_sreg(s_num);
8583            let vmov: u32 = 0xEE000A10 | (vn << 16) | (12 << 12) | (n << 7);
8584            result.extend_from_slice(&vfp_to_thumb_bytes(vmov));
8585        }
8586
8587        Ok(result)
8588    }
8589
8590    /// Encode lane-wise f32 binary operation (VDIV, etc.) via S-register extraction
8591    fn encode_thumb_mve_lane_wise_f32_binop(
8592        &self,
8593        qd: &QReg,
8594        qn: &QReg,
8595        qm: &QReg,
8596        vfp_base: u32,
8597    ) -> Result<Vec<u8>> {
8598        let mut result = Vec::new();
8599        let qd_num = qreg_to_num(qd);
8600        let qn_num = qreg_to_num(qn);
8601        let qm_num = qreg_to_num(qm);
8602
8603        // For each lane 0..3: use S-registers directly (Q aliasing)
8604        for i in 0..4u32 {
8605            let sd = qd_num * 4 + i;
8606            let sn = qn_num * 4 + i;
8607            let sm = qm_num * 4 + i;
8608
8609            let (vd, d) = encode_sreg(sd);
8610            let (vn, n) = encode_sreg(sn);
8611            let (vm, m) = encode_sreg(sm);
8612
8613            let instr = vfp_base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
8614            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
8615        }
8616
8617        Ok(result)
8618    }
8619
8620    /// Encode lane-wise f32 VSQRT via S-register extraction
8621    fn encode_thumb_mve_lane_wise_f32_sqrt(&self, qd: &QReg, qm: &QReg) -> Result<Vec<u8>> {
8622        let mut result = Vec::new();
8623        let qd_num = qreg_to_num(qd);
8624        let qm_num = qreg_to_num(qm);
8625
8626        // VSQRT.F32 base: 0xEEB10AC0
8627        for i in 0..4u32 {
8628            let sd = qd_num * 4 + i;
8629            let sm = qm_num * 4 + i;
8630
8631            let (vd, d) = encode_sreg(sd);
8632            let (vm, m) = encode_sreg(sm);
8633
8634            let instr: u32 = 0xEEB10AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
8635            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
8636        }
8637
8638        Ok(result)
8639    }
8640}
8641
8642#[cfg(test)]
8643mod tests {
8644    use super::*;
8645
8646    #[test]
8647    fn test_encoder_creation() {
8648        let encoder_arm = ArmEncoder::new_arm32();
8649        assert!(!encoder_arm.thumb_mode);
8650
8651        let encoder_thumb = ArmEncoder::new_thumb2();
8652        assert!(encoder_thumb.thumb_mode);
8653    }
8654
8655    /// #204 WAKE-path regression: `SetCond` materialized 0/1 with the 16-bit
8656    /// `MOVS Rd,#imm` (T1), whose Rd field is 3 bits (R0–R7). For a high Rd
8657    /// (R8–R12) `rd_bits << 8` overflows bit 11, flipping the opcode MOVS→CMP
8658    /// (`0x2c00`), so the boolean was never written — gale's `has_waiter` kept a
8659    /// stale value and the binary-sem WAKE dispatch read garbage. High Rd must
8660    /// use the 32-bit `MOV.W` (T2). Verify the bytes, not the IR.
8661    /// #311: the SAME high-Rd MOVS→CMP transmutation as #204, but in the
8662    /// i64 comparison expansions (I64SetCond / I64SetCondZ) — missed by the
8663    /// #204 hardening. With rd=R8 the boolean died in the flags
8664    /// (`ite eq; cmpeq r0,#1; cmpne r0,#0`), so gale's packed-u64 select
8665    /// read a stale register on silicon. High Rd must take MOV.W / CMP.W.
8666    #[test]
8667    fn test_encode_i64setcond_high_reg_uses_mov_w_311() {
8668        use synth_synthesis::{ArmOp, Condition, Reg};
8669        let enc = ArmEncoder::new_thumb2();
8670        let bytes = enc
8671            .encode(&ArmOp::I64SetCond {
8672                rd: Reg::R8,
8673                rn_lo: Reg::R2,
8674                rn_hi: Reg::R3,
8675                rm_lo: Reg::R6,
8676                rm_hi: Reg::R7,
8677                cond: Condition::EQ,
8678            })
8679            .unwrap();
8680        // The 32-bit MOV.W immediate (T2) first halfword is 0xF04F; the
8681        // 16-bit transmuted forms would contain 0x2801/0x2800 (CMP r0,#1/#0).
8682        let halfwords: Vec<u16> = bytes
8683            .chunks(2)
8684            .map(|c| u16::from_le_bytes([c[0], c[1]]))
8685            .collect();
8686        assert!(
8687            halfwords.iter().filter(|&&h| h == 0xF04F).count() == 2,
8688            "high rd must use two MOV.W (T2) encodings, got {halfwords:04x?}"
8689        );
8690        assert!(
8691            !halfwords.contains(&0x2801) && !halfwords.contains(&0x2800),
8692            "no transmuted 16-bit CMP imm: {halfwords:04x?}"
8693        );
8694
8695        let bytes_z = enc
8696            .encode(&ArmOp::I64SetCondZ {
8697                rd: Reg::R8,
8698                rn_lo: Reg::R2,
8699                rn_hi: Reg::R3,
8700            })
8701            .unwrap();
8702        let hw_z: Vec<u16> = bytes_z
8703            .chunks(2)
8704            .map(|c| u16::from_le_bytes([c[0], c[1]]))
8705            .collect();
8706        assert!(
8707            hw_z.iter().filter(|&&h| h == 0xF04F).count() == 2,
8708            "SetCondZ high rd MOV.W: {hw_z:04x?}"
8709        );
8710        // CMP.W rd,#0 (T2) first halfword: 0xF1B0 | rd
8711        assert!(
8712            hw_z.contains(&(0xF1B0 | 8)),
8713            "SetCondZ high rd must use CMP.W: {hw_z:04x?}"
8714        );
8715    }
8716
8717    #[test]
8718    fn test_encode_setcond_high_reg_uses_mov_w_204() {
8719        use synth_synthesis::{ArmOp, Condition, Reg};
8720        let enc = ArmEncoder::new_thumb2();
8721        // R12 (high): must be ITE + MOV.W #1 + MOV.W #0, never a 16-bit MOVS/CMP.
8722        let hi = enc
8723            .encode(&ArmOp::SetCond {
8724                rd: Reg::R12,
8725                cond: Condition::NE,
8726            })
8727            .unwrap();
8728        assert_eq!(hi.len(), 10, "ITE(2) + MOV.W(4) + MOV.W(4): {hi:02x?}");
8729        // both value halfwords are MOV.W (0xF04F) — NOT the corrupt CMP (0x2c..).
8730        assert_eq!(&hi[2..4], &[0x4F, 0xF0], "then = MOV.W: {hi:02x?}");
8731        assert_eq!(&hi[6..8], &[0x4F, 0xF0], "else = MOV.W: {hi:02x?}");
8732        assert_eq!(hi[4] & 0x0F, 0x01, "then imm = #1");
8733        assert_eq!(hi[8] & 0x0F, 0x00, "else imm = #0");
8734        // Low Rd keeps the compact 16-bit MOVS form.
8735        let lo = enc
8736            .encode(&ArmOp::SetCond {
8737                rd: Reg::R0,
8738                cond: Condition::NE,
8739            })
8740            .unwrap();
8741        assert_eq!(lo.len(), 6, "ITE(2) + MOVS(2) + MOVS(2): {lo:02x?}");
8742        assert_eq!(lo[2..4], [0x01, 0x20], "then = MOVS R0,#1");
8743        assert_eq!(lo[4..6], [0x00, 0x20], "else = MOVS R0,#0");
8744    }
8745
8746    /// #209 Opt 1b: UMULL RdLo, RdHi, Rn, Rm encodes correctly on both ISAs.
8747    /// Thumb-2 T1: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm.
8748    /// A32:        cond 0000 1000 RdHi RdLo Rm 1001 Rn.
8749    #[test]
8750    fn test_encode_umull_209b() {
8751        use synth_synthesis::{ArmOp, Reg};
8752        let op = ArmOp::Umull {
8753            rdlo: Reg::R4,
8754            rdhi: Reg::R5,
8755            rn: Reg::R0,
8756            rm: Reg::R3,
8757        };
8758        // Thumb-2: hw1 = 0xFBA0 | 0 = 0xFBA0; hw2 = (4<<12)|(5<<8)|3 = 0x4503.
8759        let t = ArmEncoder::new_thumb2().encode(&op).unwrap();
8760        assert_eq!(
8761            t,
8762            vec![0xA0, 0xFB, 0x03, 0x45],
8763            "umull r4,r5,r0,r3 (T2): {t:02x?}"
8764        );
8765        // A32: 0xE0800090 | (5<<16) | (4<<12) | (3<<8) | 0 = 0xE0854390.
8766        let a = ArmEncoder::new_arm32().encode(&op).unwrap();
8767        assert_eq!(
8768            a,
8769            0xE085_4390u32.to_le_bytes().to_vec(),
8770            "umull (A32): {a:02x?}"
8771        );
8772    }
8773
8774    /// #206 regression: the ARM32 (A32) `Ldr`/`Str` encoders fed `addr` through
8775    /// `encode_mem_addr`, which returns only the 12-bit immediate — so a register
8776    /// offset (`[rn, rm, #off]`) was silently dropped to `[rn, #off]`, sending
8777    /// the access to the wrong runtime address (silent miscompile on the default
8778    /// `--target arm`). A register offset must materialize `ip = rn + rm` and
8779    /// load from `[ip, #off]`. Verify the bytes.
8780    #[test]
8781    fn test_encode_arm32_indexed_load_keeps_index_206() {
8782        use synth_synthesis::{ArmOp, MemAddr, Reg};
8783        let enc = ArmEncoder::new_arm32();
8784        // ldr r0, [r11, r1, #8]  must NOT collapse to a single immediate ldr.
8785        let bytes = enc
8786            .encode(&ArmOp::Ldr {
8787                rd: Reg::R0,
8788                addr: MemAddr::reg_imm(Reg::R11, Reg::R1, 8),
8789            })
8790            .unwrap();
8791        assert_eq!(
8792            bytes.len(),
8793            8,
8794            "expected ADD ip + LDR (2 words): {bytes:02x?}"
8795        );
8796        let add = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
8797        let ldr = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
8798        // ADD ip, r11, r1  = 0xE08BC001
8799        assert_eq!(add, 0xE08B_C001, "ADD ip,r11,r1: {add:#010x}");
8800        // LDR r0, [ip, #8] = 0xE59C0008
8801        assert_eq!(ldr, 0xE59C_0008, "LDR r0,[ip,#8]: {ldr:#010x}");
8802        // A bare immediate ldr (the bug) would be 0xE59B0008 (base=r11) — reject.
8803        assert_ne!(ldr, 0xE59B_0008, "index must not be dropped");
8804    }
8805
8806    /// #594 regression: `call_indirect` on the A32 path (`--target cortex-r5`)
8807    /// was encoded as a literal NOP (0xE1A00000) — the call never happened and
8808    /// the function silently returned the leftover table-index value. The A32
8809    /// encoder must emit a real dispatch expansion, since #642 guarded by an
8810    /// inline bounds check:
8811    /// `MOVW r12, #size; CMP idx, r12; BLO +1; UDF;
8812    ///  MOV r12, idx, LSL #2; LDR r12, [r11, r12]; BLX r12`.
8813    #[test]
8814    fn test_encode_arm32_call_indirect_is_real_call_594() {
8815        use synth_synthesis::{ArmOp, Reg};
8816        let enc = ArmEncoder::new_arm32();
8817        let bytes = enc
8818            .encode(&ArmOp::CallIndirect {
8819                rd: Reg::R0,
8820                type_idx: 0,
8821                table_index_reg: Reg::R0,
8822                table_size: 4,
8823            })
8824            .unwrap();
8825        assert_eq!(
8826            bytes.len(),
8827            28,
8828            "expected MOVW + CMP + BLO + UDF + MOV + LDR + BLX (7 words): {bytes:02x?}"
8829        );
8830        let words: Vec<u32> = bytes
8831            .chunks_exact(4)
8832            .map(|w| u32::from_le_bytes(w.try_into().unwrap()))
8833            .collect();
8834        // #642 bounds guard: MOVW r12, #4; CMP r0, r12; BLO +1; UDF
8835        assert_eq!(words[0], 0xE300_C004, "MOVW r12,#4: {:#010x}", words[0]);
8836        assert_eq!(words[1], 0xE150_000C, "CMP r0,r12: {:#010x}", words[1]);
8837        assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
8838        assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
8839        // MOV r12, r0, LSL #2 = 0xE1A0C100
8840        assert_eq!(
8841            words[4], 0xE1A0_C100,
8842            "MOV r12,r0,LSL#2: {:#010x}",
8843            words[4]
8844        );
8845        // LDR r12, [r11, r12] = 0xE79BC00C
8846        assert_eq!(
8847            words[5], 0xE79B_C00C,
8848            "LDR r12,[r11,r12]: {:#010x}",
8849            words[5]
8850        );
8851        // BLX r12 = 0xE12FFF3C
8852        assert_eq!(words[6], 0xE12F_FF3C, "BLX r12: {:#010x}", words[6]);
8853        // The bug: a single NOP word. Must never come back.
8854        assert!(
8855            !bytes
8856                .chunks_exact(4)
8857                .any(|w| w == 0xE1A0_0000u32.to_le_bytes()),
8858            "call_indirect must not contain a NOP (#594): {bytes:02x?}"
8859        );
8860
8861        // A non-R0 index register lands in the MOV's Rm and CMP's Rn fields.
8862        let bytes = enc
8863            .encode(&ArmOp::CallIndirect {
8864                rd: Reg::R0,
8865                type_idx: 0,
8866                table_index_reg: Reg::R4,
8867                table_size: 4,
8868            })
8869            .unwrap();
8870        let cmp = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
8871        assert_eq!(cmp, 0xE154_000C, "CMP r4,r12: {cmp:#010x}");
8872        let mov = u32::from_le_bytes(bytes[16..20].try_into().unwrap());
8873        assert_eq!(mov, 0xE1A0_C104, "MOV r12,r4,LSL#2: {mov:#010x}");
8874    }
8875
8876    /// #642: a table size above 16 bits must not be silently truncated by the
8877    /// MOVW — the A32 guard adds a MOVT for the high half.
8878    #[test]
8879    fn test_encode_arm32_call_indirect_wide_table_size_642() {
8880        use synth_synthesis::{ArmOp, Reg};
8881        let enc = ArmEncoder::new_arm32();
8882        let bytes = enc
8883            .encode(&ArmOp::CallIndirect {
8884                rd: Reg::R0,
8885                type_idx: 0,
8886                table_index_reg: Reg::R0,
8887                table_size: 0x0002_0003,
8888            })
8889            .unwrap();
8890        assert_eq!(bytes.len(), 32, "MOVT arm adds one word: {bytes:02x?}");
8891        let movw = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
8892        let movt = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
8893        assert_eq!(movw, 0xE300_C003, "MOVW r12,#3: {movw:#010x}");
8894        assert_eq!(movt, 0xE340_C002, "MOVT r12,#2: {movt:#010x}");
8895    }
8896
8897    /// #597 anchor (justified correctness RE-PIN of the #594-era freeze): the
8898    /// Thumb-2 `CallIndirect` expansion is `mov.w ip, rm, LSL #2; ldr.w ip,
8899    /// [r11, ip]; blx ip`.
8900    ///
8901    /// The #594 PR froze the then-current bytes `4F EA 20 0C ...` whose first
8902    /// word decodes as `mov.w ip, rm, ASR #32` — the intended `LSL #2` had
8903    /// its shift amount in the TYPE field (bits 5:4) instead of imm2 (bits
8904    /// 7:6), so the index was destroyed and every call_indirect dispatched
8905    /// table entry 0 (shipped miscompile, masked by index-0 probes). #597
8906    /// corrects the encoding; new bytes `4F EA 80 0C ...` were
8907    /// execution-validated under unicorn against the wasmtime oracle on a
8908    /// multi-entry table (indexes 0, 1, 3 —
8909    /// scripts/repro/call_indirect_597_differential.py) before this pin was
8910    /// replaced. Old pin: [4F EA 20 0C, 5B F8 0C C0, E0 47] (ASR #32 — must
8911    /// never come back).
8912    #[test]
8913    fn test_encode_thumb_call_indirect_lsl2_597() {
8914        use synth_synthesis::{ArmOp, Reg};
8915        let enc = ArmEncoder::new_thumb2();
8916        let bytes = enc
8917            .encode(&ArmOp::CallIndirect {
8918                rd: Reg::R0,
8919                type_idx: 0,
8920                table_index_reg: Reg::R0,
8921                table_size: 4,
8922            })
8923            .unwrap();
8924        assert_eq!(
8925            bytes,
8926            vec![
8927                // #642 bounds guard: movw ip,#4; cmp r0,ip; blo +1; udf #0
8928                0x40, 0xF2, 0x04, 0x0C, // movw ip, #4
8929                0x60, 0x45, // cmp r0, ip
8930                0x00, 0xD3, // blo .+4 (skip the udf)
8931                0x00, 0xDE, // udf #0 — OOB index trap (WASM §4.4.8)
8932                // #597-pinned dispatch
8933                0x4F, 0xEA, 0x80, 0x0C, // mov.w ip, r0, lsl #2
8934                0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
8935                0xE0, 0x47, // blx ip
8936            ],
8937            "Thumb-2 CallIndirect: bounds guard + mov.w/ldr.w/blx dispatch: {bytes:02x?}"
8938        );
8939        // The #597 bug bytes (ASR #32 dispatch first word) must never come back.
8940        assert!(
8941            !bytes.windows(4).any(|w| w == [0x4F, 0xEA, 0x20, 0x0C]),
8942            "mov.w ip, rm, ASR #32 — the #597 type-field bug"
8943        );
8944
8945        // A non-R0 index register lands in the mov.w's Rm field (hw2 bits 3:0)
8946        // and the cmp's Rn field.
8947        let bytes = enc
8948            .encode(&ArmOp::CallIndirect {
8949                rd: Reg::R0,
8950                type_idx: 0,
8951                table_index_reg: Reg::R4,
8952                table_size: 4,
8953            })
8954            .unwrap();
8955        assert_eq!(&bytes[4..6], &[0x64, 0x45], "cmp r4, ip: {bytes:02x?}");
8956        assert_eq!(
8957            &bytes[10..14],
8958            &[0x4F, 0xEA, 0x84, 0x0C],
8959            "mov.w ip, r4, LSL #2: {bytes:02x?}"
8960        );
8961    }
8962
8963    /// #642: the Thumb-2 bounds guard for a high-register index (R8 — the top
8964    /// of the allocatable pool) uses the high-reg-capable 16-bit CMP (T2) with
8965    /// the N bit set; a table size above 16 bits adds a MOVT.
8966    #[test]
8967    fn test_encode_thumb_call_indirect_guard_shapes_642() {
8968        use synth_synthesis::{ArmOp, Reg};
8969        let enc = ArmEncoder::new_thumb2();
8970        let bytes = enc
8971            .encode(&ArmOp::CallIndirect {
8972                rd: Reg::R0,
8973                type_idx: 0,
8974                table_index_reg: Reg::R8,
8975                table_size: 3,
8976            })
8977            .unwrap();
8978        // cmp r8, ip — T2: 0x4500 | N(1)<<7 | Rm(12)<<3 | Rn(0) = 0x45E0
8979        assert_eq!(&bytes[4..6], &[0xE0, 0x45], "cmp r8, ip: {bytes:02x?}");
8980
8981        let bytes = enc
8982            .encode(&ArmOp::CallIndirect {
8983                rd: Reg::R0,
8984                type_idx: 0,
8985                table_index_reg: Reg::R0,
8986                table_size: 0x0002_0003,
8987            })
8988            .unwrap();
8989        // movw ip,#3 then movt ip,#2 — the size must not be truncated.
8990        assert_eq!(
8991            &bytes[0..8],
8992            &[0x40, 0xF2, 0x03, 0x0C, 0xC0, 0xF2, 0x02, 0x0C],
8993            "movw ip,#3; movt ip,#2: {bytes:02x?}"
8994        );
8995    }
8996
8997    /// #178/#180 regression: the Thumb `Add`/`Adds`/`Subs` reg-forms used the
8998    /// 16-bit encoding unconditionally. For high registers (R12 base scratch,
8999    /// R8-R11 i64 pairs) the 3-bit register fields overflow and corrupt the
9000    /// operands — `add ip,ip,r0` came out as `adds r4,r5,r1` (0x186C), silently
9001    /// dropping the address operand and miscompiling every optimized memory
9002    /// access. High registers must use the 32-bit `.W` forms.
9003    #[test]
9004    fn test_encode_thumb_add_high_reg_uses_add_w_178_180() {
9005        let encoder = ArmEncoder::new_thumb2();
9006
9007        // add ip, ip, r0  — the exact MemLoad/MemStore base+addr op.
9008        let code = encoder
9009            .encode(&ArmOp::Add {
9010                rd: Reg::R12,
9011                rn: Reg::R12,
9012                op2: Operand2::Reg(Reg::R0),
9013            })
9014            .unwrap();
9015        // ADD.W ip, ip, r0 = EB0C 0C00 (little-endian halfwords).
9016        assert_eq!(
9017            code,
9018            vec![0x0C, 0xEB, 0x00, 0x0C],
9019            "high-reg Thumb ADD must be 32-bit ADD.W (EB0C 0C00), not corrupt 16-bit; got {code:02X?}"
9020        );
9021        // Must NOT be the buggy 16-bit 0x186C (`adds r4,r5,r1`).
9022        assert_ne!(code, vec![0x6C, 0x18], "regressed to corrupt 16-bit ADDS");
9023
9024        // Low-register add stays 16-bit (no regression for the common case).
9025        let lo = encoder
9026            .encode(&ArmOp::Add {
9027                rd: Reg::R1,
9028                rn: Reg::R2,
9029                op2: Operand2::Reg(Reg::R3),
9030            })
9031            .unwrap();
9032        assert_eq!(
9033            lo.len(),
9034            2,
9035            "low-reg ADD should remain 16-bit, got {lo:02X?}"
9036        );
9037    }
9038
9039    /// #178/#180 sibling: i64 low-word `Adds`/`Subs` can land in R8-R11 pairs;
9040    /// those must fall back to 32-bit ADDS.W/SUBS.W (flag-setting preserved).
9041    #[test]
9042    fn test_encode_thumb_adds_subs_high_reg_use_32bit_178_180() {
9043        let encoder = ArmEncoder::new_thumb2();
9044
9045        // adds r10, r10, r8  → ADDS.W = EB1A 0A08
9046        let adds = encoder
9047            .encode(&ArmOp::Adds {
9048                rd: Reg::R10,
9049                rn: Reg::R10,
9050                op2: Operand2::Reg(Reg::R8),
9051            })
9052            .unwrap();
9053        assert_eq!(
9054            adds,
9055            vec![0x1A, 0xEB, 0x08, 0x0A],
9056            "high-reg ADDS must be 32-bit ADDS.W (EB1A 0A08); got {adds:02X?}"
9057        );
9058
9059        // subs r10, r10, r8  → SUBS.W = EBBA 0A08
9060        let subs = encoder
9061            .encode(&ArmOp::Subs {
9062                rd: Reg::R10,
9063                rn: Reg::R10,
9064                op2: Operand2::Reg(Reg::R8),
9065            })
9066            .unwrap();
9067        assert_eq!(
9068            subs,
9069            vec![0xBA, 0xEB, 0x08, 0x0A],
9070            "high-reg SUBS must be 32-bit SUBS.W (EBBA 0A08); got {subs:02X?}"
9071        );
9072    }
9073
9074    /// #184 (sibling of #180): 16-bit CMN (T1) only encodes R0-R7. High registers
9075    /// must use 32-bit CMN.W, not the corrupt truncated 16-bit form.
9076    #[test]
9077    fn test_encode_thumb_cmn_high_reg_uses_cmn_w_184() {
9078        let encoder = ArmEncoder::new_thumb2();
9079
9080        // cmn r10, r8  → CMN.W = EB1A 0F08 (ADD.W S=1, Rd=PC discarded).
9081        let cmn = encoder
9082            .encode(&ArmOp::Cmn {
9083                rn: Reg::R10,
9084                op2: Operand2::Reg(Reg::R8),
9085            })
9086            .unwrap();
9087        assert_eq!(
9088            cmn,
9089            vec![0x1A, 0xEB, 0x08, 0x0F],
9090            "high-reg CMN must be 32-bit CMN.W (EB1A 0F08); got {cmn:02X?}"
9091        );
9092
9093        // Low registers stay 16-bit: cmn r1, r2 = 0x42D1.
9094        let lo = encoder
9095            .encode(&ArmOp::Cmn {
9096                rn: Reg::R1,
9097                op2: Operand2::Reg(Reg::R2),
9098            })
9099            .unwrap();
9100        assert_eq!(
9101            lo.len(),
9102            2,
9103            "low-reg CMN should remain 16-bit, got {lo:02X?}"
9104        );
9105        assert_eq!(lo, vec![0xD1, 0x42], "low-reg CMN bytes wrong: {lo:02X?}");
9106    }
9107
9108    /// #185 regression: feeding PC (R15) as a data operand to a Thumb-2 op that
9109    /// guards its registers must return Err, not panic under debug-assertions.
9110    /// (Synth never emits PC here; the fuzz harness requires encode() be total.)
9111    #[test]
9112    fn test_encode_pc_operand_returns_err_not_panic_185() {
9113        let encoder = ArmEncoder::new_thumb2();
9114        for op in [
9115            ArmOp::Sdiv {
9116                rd: Reg::PC,
9117                rn: Reg::R0,
9118                rm: Reg::R1,
9119            },
9120            ArmOp::Udiv {
9121                rd: Reg::R0,
9122                rn: Reg::PC,
9123                rm: Reg::R1,
9124            },
9125            ArmOp::Sdiv {
9126                rd: Reg::R0,
9127                rn: Reg::R1,
9128                rm: Reg::PC,
9129            },
9130        ] {
9131            let r = encoder.encode(&op);
9132            assert!(
9133                r.is_err(),
9134                "encode({op:?}) must return Err for a PC operand, got {r:?}"
9135            );
9136        }
9137        // Valid registers still encode fine (no false rejection).
9138        assert!(
9139            encoder
9140                .encode(&ArmOp::Sdiv {
9141                    rd: Reg::R0,
9142                    rn: Reg::R1,
9143                    rm: Reg::R2
9144                })
9145                .is_ok()
9146        );
9147    }
9148
9149    #[test]
9150    fn test_encode_nop_arm32() {
9151        let encoder = ArmEncoder::new_arm32();
9152        let code = encoder.encode(&ArmOp::Nop).unwrap();
9153
9154        assert_eq!(code.len(), 4); // ARM32 instructions are 4 bytes
9155        assert_eq!(code, vec![0x00, 0x00, 0xA0, 0xE1]); // MOV R0, R0
9156    }
9157
9158    #[test]
9159    fn test_encode_nop_thumb() {
9160        let encoder = ArmEncoder::new_thumb2();
9161        let code = encoder.encode(&ArmOp::Nop).unwrap();
9162
9163        assert_eq!(code.len(), 2); // Thumb instructions are 2 bytes
9164        assert_eq!(code, vec![0x00, 0xBF]); // NOP
9165    }
9166
9167    #[test]
9168    fn test_encode_mov_immediate_arm32() {
9169        let encoder = ArmEncoder::new_arm32();
9170        let op = ArmOp::Mov {
9171            rd: Reg::R0,
9172            op2: Operand2::Imm(42),
9173        };
9174
9175        let code = encoder.encode(&op).unwrap();
9176        assert_eq!(code.len(), 4);
9177
9178        // Verify it's a MOV instruction (bits should have immediate flag set)
9179        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9180        assert_eq!(instr & 0x0E000000, 0x02000000); // Check I bit is set
9181    }
9182
9183    #[test]
9184    fn test_encode_add_registers_arm32() {
9185        let encoder = ArmEncoder::new_arm32();
9186        let op = ArmOp::Add {
9187            rd: Reg::R0,
9188            rn: Reg::R1,
9189            op2: Operand2::Reg(Reg::R2),
9190        };
9191
9192        let code = encoder.encode(&op).unwrap();
9193        assert_eq!(code.len(), 4);
9194
9195        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9196        // Verify it's an ADD instruction with correct opcode
9197        assert_eq!(instr & 0x0FE00000, 0x00800000);
9198    }
9199
9200    /// #350 — `encode_thumb32_add_imm` must lower an out-of-range immediate
9201    /// (> 0xFFF) to a legal MOVW(/MOVT) + ADD.W-register sequence instead of
9202    /// erroring. The small-imm fast path (imm <= 0xFFF) stays byte-identical.
9203    #[test]
9204    fn test_encode_add_imm_large_350() {
9205        let enc = ArmEncoder::new_thumb2();
9206
9207        // --- Fast path unchanged: imm <= 0xFFF is a single 4-byte ADD.W ---
9208        let small = enc
9209            .encode_thumb32_add_imm(&Reg::R0, &Reg::R1, 0x123)
9210            .unwrap();
9211        assert_eq!(small.len(), 4, "small imm must stay a single instruction");
9212
9213        // helper: decode a Thumb-2 MOVW/MOVT halfword pair back to its imm16
9214        fn movx_imm16(b: &[u8]) -> u32 {
9215            let hw1 = u16::from_le_bytes([b[0], b[1]]) as u32;
9216            let hw2 = u16::from_le_bytes([b[2], b[3]]) as u32;
9217            let imm4 = hw1 & 0xF;
9218            let i = (hw1 >> 10) & 1;
9219            let imm3 = (hw2 >> 12) & 0x7;
9220            let imm8 = hw2 & 0xFF;
9221            (imm4 << 12) | (i << 11) | (imm3 << 8) | imm8
9222        }
9223        fn movx_rd(b: &[u8]) -> u32 {
9224            (u16::from_le_bytes([b[2], b[3]]) as u32 >> 8) & 0xF
9225        }
9226
9227        // --- rd != rn: scratch is rd. imm = 70000 = 0x11170 needs MOVW+MOVT. ---
9228        // 0x11170: lo16 = 0x1170, hi16 = 0x0001
9229        let seq = enc
9230            .encode_thumb32_add_imm(&Reg::R12, &Reg::R0, 70000)
9231            .unwrap();
9232        assert_eq!(seq.len(), 12, "MOVW + MOVT + ADD = 12 bytes");
9233        // MOVW r12, #0x1170
9234        assert_eq!(u16::from_le_bytes([seq[0], seq[1]]) & 0xFBF0, 0xF240);
9235        assert_eq!(movx_rd(&seq[0..4]), 12);
9236        assert_eq!(movx_imm16(&seq[0..4]), 0x1170);
9237        // MOVT r12, #0x0001
9238        assert_eq!(u16::from_le_bytes([seq[4], seq[5]]) & 0xFBF0, 0xF2C0);
9239        assert_eq!(movx_rd(&seq[4..8]), 12);
9240        assert_eq!(movx_imm16(&seq[4..8]), 0x0001);
9241        // ADD.W r12, r0, r12  (EB00 | rn=0 ; rd=12, rm=12)
9242        let add1 = u16::from_le_bytes([seq[8], seq[9]]) as u32;
9243        let add2 = u16::from_le_bytes([seq[10], seq[11]]) as u32;
9244        assert_eq!(add1 & 0xFFF0, 0xEB00);
9245        assert_eq!(add1 & 0xF, 0); // rn = r0
9246        assert_eq!((add2 >> 8) & 0xF, 12); // rd = r12
9247        assert_eq!(add2 & 0xF, 12); // rm = scratch = r12
9248        // The materialized scratch must reconstruct exactly 70000.
9249        assert_eq!(
9250            (movx_imm16(&seq[4..8]) << 16) | movx_imm16(&seq[0..4]),
9251            70000
9252        );
9253
9254        // --- imm <= 0xFFFF: MOVT is skipped (MOVW + ADD = 8 bytes). ---
9255        let seq16 = enc
9256            .encode_thumb32_add_imm(&Reg::R3, &Reg::R0, 0xABCD)
9257            .unwrap();
9258        assert_eq!(seq16.len(), 8, "imm <= 0xFFFF skips MOVT");
9259        assert_eq!(movx_imm16(&seq16[0..4]), 0xABCD);
9260        assert_eq!(movx_rd(&seq16[0..4]), 3); // scratch = rd = r3
9261
9262        // --- rd == rn (in-place add): scratch must be R12, not rd. ---
9263        // imm = 0x12345: lo16 = 0x2345, hi16 = 0x0001
9264        let inplace = enc
9265            .encode_thumb32_add_imm(&Reg::R5, &Reg::R5, 0x12345)
9266            .unwrap();
9267        assert_eq!(inplace.len(), 12);
9268        assert_eq!(movx_rd(&inplace[0..4]), 12, "rd==rn must use R12 scratch");
9269        assert_eq!(
9270            (movx_imm16(&inplace[4..8]) << 16) | movx_imm16(&inplace[0..4]),
9271            0x12345
9272        );
9273        // ADD.W r5, r5, r12 — rm must be the scratch (12), never rn.
9274        let ip_add2 = u16::from_le_bytes([inplace[10], inplace[11]]) as u32;
9275        assert_eq!(ip_add2 & 0xF, 12);
9276        assert_eq!((ip_add2 >> 8) & 0xF, 5);
9277    }
9278
9279    /// #350 follow-up — the `encoder_no_panic` fuzz harness drives the encoder
9280    /// with ARBITRARY registers, including the one case the in-place lowering
9281    /// cannot serve: rd==rn==R12. There the scratch (R12, the reserved encoder
9282    /// register) would alias Rn and clobber it before the ADD reads it. The
9283    /// encoder contract (#180/#185) is Ok-or-Err, never a panic — so this must
9284    /// return Err, not assert. (Real codegen never emits rd==rn==R12 because R12
9285    /// is non-allocatable; this guards only the fuzz/adversarial path.)
9286    #[test]
9287    fn test_encode_add_imm_large_rd_rn_r12_errs_not_panics_350() {
9288        let enc = ArmEncoder::new_thumb2();
9289        // Out-of-range imm with rd==rn==R12: no free scratch -> Err.
9290        let r = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 70000);
9291        assert!(
9292            r.is_err(),
9293            "rd==rn==R12 with out-of-range imm must Err (no free scratch), got {r:?}"
9294        );
9295        // Small imm with rd==rn==R12 still takes the single-instruction fast path
9296        // (no scratch needed) and must succeed — the guard is scoped to the
9297        // out-of-range lowering only.
9298        let small = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 0x10);
9299        assert!(small.is_ok(), "small imm needs no scratch, must stay Ok");
9300    }
9301
9302    /// #378 — `encode_operand2` (ARM32 data-processing operand) must FAIL
9303    /// HONESTLY on an immediate that is not a valid rotated immediate, rather
9304    /// than silently masking it to `imm & 0xFF` and emitting a WRONG
9305    /// instruction. `0x1FF` has 9 set bits, so it cannot come from rotating an
9306    /// 8-bit imm8 — non-encodable. Real codegen materializes large constants via
9307    /// MOVW/MOVT; this guards the encoder's Ok-or-Err contract (#180/#185)
9308    /// directly. It is an Err (not a panic) so the `encoder_no_panic` fuzz
9309    /// harness — which drives arbitrary operands — still passes.
9310    #[test]
9311    fn test_encode_operand2_non_rotatable_imm_errs_not_masks_378() {
9312        let enc = ArmEncoder::new_arm32();
9313        let bad = enc.encode(&ArmOp::Add {
9314            rd: Reg::R0,
9315            rn: Reg::R1,
9316            op2: Operand2::Imm(0x1FF),
9317        });
9318        assert!(
9319            bad.is_err(),
9320            "non-rotatable ARM32 immediate 0x1FF must Err (was silently masked \
9321             to 0xFF), got {bad:?}"
9322        );
9323        // A representable rotated immediate still encodes fine (regression guard).
9324        let ok = enc.encode(&ArmOp::Add {
9325            rd: Reg::R0,
9326            rn: Reg::R1,
9327            op2: Operand2::Imm(0xFF),
9328        });
9329        assert!(
9330            ok.is_ok(),
9331            "0xFF is a valid rotated immediate, must stay Ok"
9332        );
9333    }
9334
9335    #[test]
9336    fn test_encode_ldr_arm32() {
9337        let encoder = ArmEncoder::new_arm32();
9338        let op = ArmOp::Ldr {
9339            rd: Reg::R0,
9340            addr: MemAddr::imm(Reg::R1, 4),
9341        };
9342
9343        let code = encoder.encode(&op).unwrap();
9344        assert_eq!(code.len(), 4);
9345
9346        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9347        // Verify load bit is set
9348        assert_eq!(instr & 0x00100000, 0x00100000);
9349    }
9350
9351    #[test]
9352    fn test_encode_str_arm32() {
9353        let encoder = ArmEncoder::new_arm32();
9354        let op = ArmOp::Str {
9355            rd: Reg::R0,
9356            addr: MemAddr::imm(Reg::SP, 0),
9357        };
9358
9359        let code = encoder.encode(&op).unwrap();
9360        assert_eq!(code.len(), 4);
9361    }
9362
9363    #[test]
9364    fn test_encode_branch_arm32() {
9365        let encoder = ArmEncoder::new_arm32();
9366        let op = ArmOp::Bl {
9367            label: "main".to_string(),
9368        };
9369
9370        let code = encoder.encode(&op).unwrap();
9371        assert_eq!(code.len(), 4);
9372
9373        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9374        // Verify BL opcode
9375        assert_eq!(instr & 0x0F000000, 0x0B000000);
9376    }
9377
9378    /// Regression test for #167 + #174: the Thumb-2 BL relocatable placeholder
9379    /// must carry a -4 addend so an R_ARM_THM_CALL nets to exactly the symbol S.
9380    /// The correct encoding is what `gas` emits for `bl <extern>`: f7ff fffe
9381    /// (hw1=0xF7FF, hw2=0xFFFE), little-endian bytes FF F7 FE FF.
9382    ///   - 0xD000 (J1=J2=0) → ~+0x600000 garbage addend: `bl c0000c` / truncated
9383    ///     to fit (#167).
9384    ///   - 0xF800 (addend 0) → lands at S+4, one instruction past the callee
9385    ///     entry (#174).
9386    ///   - 0xFFFE (addend -4) → lands at S. Correct.
9387    #[test]
9388    fn test_encode_thumb_bl_placeholder_addend_167_174() {
9389        let encoder = ArmEncoder::new_thumb2();
9390        let op = ArmOp::Bl {
9391            label: "callee".to_string(),
9392        };
9393
9394        let code = encoder.encode(&op).unwrap();
9395        assert_eq!(code.len(), 4, "Thumb-2 BL is 32-bit");
9396
9397        let hw1 = u16::from_le_bytes([code[0], code[1]]);
9398        let hw2 = u16::from_le_bytes([code[2], code[3]]);
9399        assert_eq!(hw1, 0xF7FF, "BL first halfword (matches gas `bl <extern>`)");
9400        assert_eq!(
9401            hw2, 0xFFFE,
9402            "BL second halfword must be 0xFFFE (-4 addend → nets to S), not 0xF800 (→ S+4, #174) or 0xD000 (#167)"
9403        );
9404        assert_ne!(hw2, 0xF800, "0xF800 (addend 0) lands at S+4 (#174)");
9405        assert_ne!(hw2, 0xD000, "0xD000 bakes in a ~+0x600000 addend (#167)");
9406    }
9407
9408    #[test]
9409    fn test_encode_sequence() {
9410        let encoder = ArmEncoder::new_arm32();
9411        let ops = vec![
9412            ArmOp::Mov {
9413                rd: Reg::R0,
9414                op2: Operand2::Imm(42),
9415            },
9416            ArmOp::Mov {
9417                rd: Reg::R1,
9418                op2: Operand2::Imm(10),
9419            },
9420            ArmOp::Add {
9421                rd: Reg::R2,
9422                rn: Reg::R0,
9423                op2: Operand2::Reg(Reg::R1),
9424            },
9425        ];
9426
9427        let code = encoder.encode_sequence(&ops).unwrap();
9428        assert_eq!(code.len(), 12); // 3 instructions * 4 bytes
9429    }
9430
9431    #[test]
9432    fn test_reg_to_bits() {
9433        assert_eq!(reg_to_bits(&Reg::R0), 0);
9434        assert_eq!(reg_to_bits(&Reg::R7), 7);
9435        assert_eq!(reg_to_bits(&Reg::SP), 13);
9436        assert_eq!(reg_to_bits(&Reg::LR), 14);
9437        assert_eq!(reg_to_bits(&Reg::PC), 15);
9438    }
9439
9440    #[test]
9441    fn test_encode_bitwise_operations() {
9442        let encoder = ArmEncoder::new_arm32();
9443
9444        let and_op = ArmOp::And {
9445            rd: Reg::R0,
9446            rn: Reg::R1,
9447            op2: Operand2::Reg(Reg::R2),
9448        };
9449        let and_code = encoder.encode(&and_op).unwrap();
9450        assert_eq!(and_code.len(), 4);
9451
9452        let orr_op = ArmOp::Orr {
9453            rd: Reg::R0,
9454            rn: Reg::R1,
9455            op2: Operand2::Reg(Reg::R2),
9456        };
9457        let orr_code = encoder.encode(&orr_op).unwrap();
9458        assert_eq!(orr_code.len(), 4);
9459
9460        let eor_op = ArmOp::Eor {
9461            rd: Reg::R0,
9462            rn: Reg::R1,
9463            op2: Operand2::Reg(Reg::R2),
9464        };
9465        let eor_code = encoder.encode(&eor_op).unwrap();
9466        assert_eq!(eor_code.len(), 4);
9467    }
9468
9469    // === Thumb-2 32-bit encoding tests ===
9470
9471    #[test]
9472    fn test_encode_sdiv_thumb2() {
9473        let encoder = ArmEncoder::new_thumb2();
9474        let op = ArmOp::Sdiv {
9475            rd: Reg::R0,
9476            rn: Reg::R1,
9477            rm: Reg::R2,
9478        };
9479
9480        let code = encoder.encode(&op).unwrap();
9481        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9482
9483        // SDIV R0, R1, R2: 0xFB91 0xF0F2
9484        // First halfword: 0xFB90 | Rn(1) = 0xFB91
9485        // Second halfword: 0xF0F0 | Rd(0)<<8 | Rm(2) = 0xF0F2
9486        // Little-endian: [0x91, 0xFB, 0xF2, 0xF0]
9487        assert_eq!(code[0], 0x91);
9488        assert_eq!(code[1], 0xFB);
9489        assert_eq!(code[2], 0xF2);
9490        assert_eq!(code[3], 0xF0);
9491    }
9492
9493    #[test]
9494    fn test_encode_udiv_thumb2() {
9495        let encoder = ArmEncoder::new_thumb2();
9496        let op = ArmOp::Udiv {
9497            rd: Reg::R0,
9498            rn: Reg::R1,
9499            rm: Reg::R2,
9500        };
9501
9502        let code = encoder.encode(&op).unwrap();
9503        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9504
9505        // UDIV R0, R1, R2: 0xFBB1 0xF0F2
9506        // Little-endian: [0xB1, 0xFB, 0xF2, 0xF0]
9507        assert_eq!(code[0], 0xB1);
9508        assert_eq!(code[1], 0xFB);
9509        assert_eq!(code[2], 0xF2);
9510        assert_eq!(code[3], 0xF0);
9511    }
9512
9513    #[test]
9514    fn test_encode_mul_thumb2() {
9515        let encoder = ArmEncoder::new_thumb2();
9516        let op = ArmOp::Mul {
9517            rd: Reg::R0,
9518            rn: Reg::R1,
9519            rm: Reg::R2,
9520        };
9521
9522        let code = encoder.encode(&op).unwrap();
9523        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9524    }
9525
9526    #[test]
9527    fn test_encode_and_thumb2() {
9528        let encoder = ArmEncoder::new_thumb2();
9529        let op = ArmOp::And {
9530            rd: Reg::R0,
9531            rn: Reg::R1,
9532            op2: Operand2::Reg(Reg::R2),
9533        };
9534
9535        let code = encoder.encode(&op).unwrap();
9536        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9537    }
9538
9539    #[test]
9540    fn test_encode_lsl_thumb2_low_regs() {
9541        let encoder = ArmEncoder::new_thumb2();
9542        let op = ArmOp::Lsl {
9543            rd: Reg::R0,
9544            rn: Reg::R1,
9545            shift: 5,
9546        };
9547
9548        let code = encoder.encode(&op).unwrap();
9549        assert_eq!(code.len(), 2); // 16-bit for low registers
9550    }
9551
9552    #[test]
9553    fn test_encode_clz_thumb2() {
9554        let encoder = ArmEncoder::new_thumb2();
9555        let op = ArmOp::Clz {
9556            rd: Reg::R0,
9557            rm: Reg::R1,
9558        };
9559
9560        let code = encoder.encode(&op).unwrap();
9561        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9562    }
9563
9564    #[test]
9565    fn test_encode_bx_thumb2() {
9566        let encoder = ArmEncoder::new_thumb2();
9567        let op = ArmOp::Bx { rm: Reg::LR };
9568
9569        let code = encoder.encode(&op).unwrap();
9570        assert_eq!(code.len(), 2); // 16-bit instruction
9571
9572        // BX LR: 0x4770
9573        assert_eq!(code, vec![0x70, 0x47]);
9574    }
9575
9576    // ========================================================================
9577    // f32 pseudo-op encoding tests
9578    // ========================================================================
9579
9580    #[test]
9581    fn test_encode_f32_abs_arm32() {
9582        let encoder = ArmEncoder::new_arm32();
9583        let op = ArmOp::F32Abs {
9584            sd: VfpReg::S0,
9585            sm: VfpReg::S2,
9586        };
9587        let code = encoder.encode(&op).unwrap();
9588        assert_eq!(code.len(), 4); // Single VFP instruction
9589    }
9590
9591    #[test]
9592    fn test_encode_f32_neg_arm32() {
9593        let encoder = ArmEncoder::new_arm32();
9594        let op = ArmOp::F32Neg {
9595            sd: VfpReg::S0,
9596            sm: VfpReg::S2,
9597        };
9598        let code = encoder.encode(&op).unwrap();
9599        assert_eq!(code.len(), 4);
9600    }
9601
9602    #[test]
9603    fn test_encode_f32_sqrt_arm32() {
9604        let encoder = ArmEncoder::new_arm32();
9605        let op = ArmOp::F32Sqrt {
9606            sd: VfpReg::S0,
9607            sm: VfpReg::S2,
9608        };
9609        let code = encoder.encode(&op).unwrap();
9610        assert_eq!(code.len(), 4);
9611    }
9612
9613    #[test]
9614    fn test_encode_f32_ceil_arm32() {
9615        let encoder = ArmEncoder::new_arm32();
9616        let op = ArmOp::F32Ceil {
9617            sd: VfpReg::S0,
9618            sm: VfpReg::S2,
9619        };
9620        let code = encoder.encode(&op).unwrap();
9621        // VMRS + BIC + ORR + VMSR + VCVT.S32.F32 + VMRS + BIC + VMSR + VCVT.F32.S32
9622        assert_eq!(code.len(), 36);
9623    }
9624
9625    #[test]
9626    fn test_encode_f32_floor_thumb2() {
9627        let encoder = ArmEncoder::new_thumb2();
9628        let op = ArmOp::F32Floor {
9629            sd: VfpReg::S0,
9630            sm: VfpReg::S2,
9631        };
9632        let code = encoder.encode(&op).unwrap();
9633        // VMRS + BIC.W + ORR.W + VMSR + VCVT + VMRS + BIC.W + VMSR + VCVT.F32.S32
9634        assert_eq!(code.len(), 36);
9635    }
9636
9637    #[test]
9638    fn test_encode_f32_min_arm32() {
9639        let encoder = ArmEncoder::new_arm32();
9640        let op = ArmOp::F32Min {
9641            sd: VfpReg::S0,
9642            sn: VfpReg::S2,
9643            sm: VfpReg::S4,
9644        };
9645        let code = encoder.encode(&op).unwrap();
9646        assert_eq!(code.len(), 16); // VMOV + VCMP + VMRS + conditional VMOV
9647    }
9648
9649    #[test]
9650    fn test_encode_f32_max_thumb2() {
9651        let encoder = ArmEncoder::new_thumb2();
9652        let op = ArmOp::F32Max {
9653            sd: VfpReg::S0,
9654            sn: VfpReg::S2,
9655            sm: VfpReg::S4,
9656        };
9657        let code = encoder.encode(&op).unwrap();
9658        // VMOV(4) + VCMP(4) + VMRS(4) + IT(2) + VMOV(4) = 18
9659        assert_eq!(code.len(), 18);
9660    }
9661
9662    #[test]
9663    fn test_encode_f32_copysign_arm32() {
9664        let encoder = ArmEncoder::new_arm32();
9665        let op = ArmOp::F32Copysign {
9666            sd: VfpReg::S0,
9667            sn: VfpReg::S2,
9668            sm: VfpReg::S4,
9669        };
9670        let code = encoder.encode(&op).unwrap();
9671        // VMOV + VMOV + AND + BIC + ORR + VMOV = 6 * 4 = 24
9672        assert_eq!(code.len(), 24);
9673    }
9674
9675    // ========================================================================
9676    // f64 encoding tests
9677    // ========================================================================
9678
9679    #[test]
9680    fn test_encode_f64_add_arm32() {
9681        let encoder = ArmEncoder::new_arm32();
9682        let op = ArmOp::F64Add {
9683            dd: VfpReg::D0,
9684            dn: VfpReg::D1,
9685            dm: VfpReg::D2,
9686        };
9687        let code = encoder.encode(&op).unwrap();
9688        assert_eq!(code.len(), 4);
9689        // VADD.F64 D0, D1, D2: check coprocessor is cp11 (0xB)
9690        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9691        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
9692    }
9693
9694    #[test]
9695    fn test_encode_f64_sub_thumb2() {
9696        let encoder = ArmEncoder::new_thumb2();
9697        let op = ArmOp::F64Sub {
9698            dd: VfpReg::D0,
9699            dn: VfpReg::D1,
9700            dm: VfpReg::D2,
9701        };
9702        let code = encoder.encode(&op).unwrap();
9703        assert_eq!(code.len(), 4); // 32-bit VFP as two Thumb halfwords
9704    }
9705
9706    #[test]
9707    fn test_encode_f64_mul_arm32() {
9708        let encoder = ArmEncoder::new_arm32();
9709        let op = ArmOp::F64Mul {
9710            dd: VfpReg::D0,
9711            dn: VfpReg::D1,
9712            dm: VfpReg::D2,
9713        };
9714        let code = encoder.encode(&op).unwrap();
9715        assert_eq!(code.len(), 4);
9716    }
9717
9718    #[test]
9719    fn test_encode_f64_div_arm32() {
9720        let encoder = ArmEncoder::new_arm32();
9721        let op = ArmOp::F64Div {
9722            dd: VfpReg::D0,
9723            dn: VfpReg::D1,
9724            dm: VfpReg::D2,
9725        };
9726        let code = encoder.encode(&op).unwrap();
9727        assert_eq!(code.len(), 4);
9728    }
9729
9730    #[test]
9731    fn test_encode_f64_abs_arm32() {
9732        let encoder = ArmEncoder::new_arm32();
9733        let op = ArmOp::F64Abs {
9734            dd: VfpReg::D0,
9735            dm: VfpReg::D2,
9736        };
9737        let code = encoder.encode(&op).unwrap();
9738        assert_eq!(code.len(), 4);
9739    }
9740
9741    #[test]
9742    fn test_encode_f64_neg_arm32() {
9743        let encoder = ArmEncoder::new_arm32();
9744        let op = ArmOp::F64Neg {
9745            dd: VfpReg::D0,
9746            dm: VfpReg::D2,
9747        };
9748        let code = encoder.encode(&op).unwrap();
9749        assert_eq!(code.len(), 4);
9750    }
9751
9752    #[test]
9753    fn test_encode_f64_sqrt_arm32() {
9754        let encoder = ArmEncoder::new_arm32();
9755        let op = ArmOp::F64Sqrt {
9756            dd: VfpReg::D0,
9757            dm: VfpReg::D2,
9758        };
9759        let code = encoder.encode(&op).unwrap();
9760        assert_eq!(code.len(), 4);
9761    }
9762
9763    #[test]
9764    fn test_encode_f64_load_arm32() {
9765        let encoder = ArmEncoder::new_arm32();
9766        let op = ArmOp::F64Load {
9767            dd: VfpReg::D0,
9768            addr: MemAddr::imm(Reg::R0, 8),
9769        };
9770        let code = encoder.encode(&op).unwrap();
9771        assert_eq!(code.len(), 4);
9772        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9773        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11 for F64
9774        assert_eq!(instr & 0xFF, 2); // offset 8 / 4 = 2
9775    }
9776
9777    #[test]
9778    fn test_encode_f64_store_thumb2() {
9779        let encoder = ArmEncoder::new_thumb2();
9780        let op = ArmOp::F64Store {
9781            dd: VfpReg::D0,
9782            addr: MemAddr::imm(Reg::SP, 0),
9783        };
9784        let code = encoder.encode(&op).unwrap();
9785        assert_eq!(code.len(), 4);
9786    }
9787
9788    #[test]
9789    fn test_encode_f64_compare_arm32() {
9790        let encoder = ArmEncoder::new_arm32();
9791        let op = ArmOp::F64Eq {
9792            rd: Reg::R0,
9793            dn: VfpReg::D0,
9794            dm: VfpReg::D1,
9795        };
9796        let code = encoder.encode(&op).unwrap();
9797        assert_eq!(code.len(), 16); // VCMP + VMRS + MOV #0 + MOVcond #1
9798    }
9799
9800    #[test]
9801    fn test_encode_f64_compare_thumb2() {
9802        let encoder = ArmEncoder::new_thumb2();
9803        let op = ArmOp::F64Lt {
9804            rd: Reg::R0,
9805            dn: VfpReg::D0,
9806            dm: VfpReg::D1,
9807        };
9808        let code = encoder.encode(&op).unwrap();
9809        // VCMP(4) + VMRS(4) + MOVS(2) + IT(2) + MOV(2) = 14
9810        assert_eq!(code.len(), 14);
9811    }
9812
9813    #[test]
9814    fn test_encode_f64_const_arm32() {
9815        let encoder = ArmEncoder::new_arm32();
9816        let op = ArmOp::F64Const {
9817            dd: VfpReg::D0,
9818            value: 3.125,
9819        };
9820        let code = encoder.encode(&op).unwrap();
9821        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
9822        assert_eq!(code.len(), 20);
9823    }
9824
9825    #[test]
9826    fn test_encode_f64_const_thumb2() {
9827        let encoder = ArmEncoder::new_thumb2();
9828        let op = ArmOp::F64Const {
9829            dd: VfpReg::D0,
9830            value: 2.5,
9831        };
9832        let code = encoder.encode(&op).unwrap();
9833        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
9834        assert_eq!(code.len(), 20);
9835    }
9836
9837    #[test]
9838    fn test_encode_f64_convert_i32s_arm32() {
9839        let encoder = ArmEncoder::new_arm32();
9840        let op = ArmOp::F64ConvertI32S {
9841            dd: VfpReg::D0,
9842            rm: Reg::R0,
9843        };
9844        let code = encoder.encode(&op).unwrap();
9845        // VMOV(4) + VCVT(4) = 8
9846        assert_eq!(code.len(), 8);
9847    }
9848
9849    #[test]
9850    fn test_encode_f64_promote_f32_arm32() {
9851        let encoder = ArmEncoder::new_arm32();
9852        let op = ArmOp::F64PromoteF32 {
9853            dd: VfpReg::D0,
9854            sm: VfpReg::S0,
9855        };
9856        let code = encoder.encode(&op).unwrap();
9857        assert_eq!(code.len(), 4); // Single VCVT.F64.F32 instruction
9858    }
9859
9860    #[test]
9861    fn test_encode_f64_promote_f32_thumb2() {
9862        let encoder = ArmEncoder::new_thumb2();
9863        let op = ArmOp::F64PromoteF32 {
9864            dd: VfpReg::D0,
9865            sm: VfpReg::S0,
9866        };
9867        let code = encoder.encode(&op).unwrap();
9868        assert_eq!(code.len(), 4);
9869    }
9870
9871    #[test]
9872    fn test_encode_i32_trunc_f64s_arm32() {
9873        let encoder = ArmEncoder::new_arm32();
9874        let op = ArmOp::I32TruncF64S {
9875            rd: Reg::R0,
9876            dm: VfpReg::D0,
9877        };
9878        let code = encoder.encode(&op).unwrap();
9879        // VCVT(4) + VMOV(4) = 8
9880        assert_eq!(code.len(), 8);
9881    }
9882
9883    #[test]
9884    fn test_encode_f64_reinterpret_i64_arm32() {
9885        let encoder = ArmEncoder::new_arm32();
9886        let op = ArmOp::F64ReinterpretI64 {
9887            dd: VfpReg::D0,
9888            rmlo: Reg::R0,
9889            rmhi: Reg::R1,
9890        };
9891        let code = encoder.encode(&op).unwrap();
9892        assert_eq!(code.len(), 4); // Single VMOV instruction
9893    }
9894
9895    #[test]
9896    fn test_encode_i64_reinterpret_f64_thumb2() {
9897        let encoder = ArmEncoder::new_thumb2();
9898        let op = ArmOp::I64ReinterpretF64 {
9899            rdlo: Reg::R0,
9900            rdhi: Reg::R1,
9901            dm: VfpReg::D0,
9902        };
9903        let code = encoder.encode(&op).unwrap();
9904        assert_eq!(code.len(), 4);
9905    }
9906
9907    #[test]
9908    fn test_encode_f64_trunc_thumb2() {
9909        let encoder = ArmEncoder::new_thumb2();
9910        let op = ArmOp::F64Trunc {
9911            dd: VfpReg::D0,
9912            dm: VfpReg::D1,
9913        };
9914        let code = encoder.encode(&op).unwrap();
9915        // Two VFP instructions via Thumb encoding
9916        assert_eq!(code.len(), 8);
9917    }
9918
9919    #[test]
9920    fn test_encode_f64_min_arm32() {
9921        let encoder = ArmEncoder::new_arm32();
9922        let op = ArmOp::F64Min {
9923            dd: VfpReg::D0,
9924            dn: VfpReg::D1,
9925            dm: VfpReg::D2,
9926        };
9927        let code = encoder.encode(&op).unwrap();
9928        // VMOV + VCMP + VMRS + conditional VMOV = 16
9929        assert_eq!(code.len(), 16);
9930    }
9931
9932    #[test]
9933    fn test_f64_cp11_encoding() {
9934        // Verify that F64 instructions use coprocessor 11 (0xB), not 10 (0xA)
9935        let encoder = ArmEncoder::new_arm32();
9936
9937        // F64Add
9938        let code = encoder
9939            .encode(&ArmOp::F64Add {
9940                dd: VfpReg::D0,
9941                dn: VfpReg::D0,
9942                dm: VfpReg::D0,
9943            })
9944            .unwrap();
9945        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9946        assert_eq!((instr >> 8) & 0xF, 0xB, "F64 should use cp11");
9947
9948        // F32Add for comparison
9949        let code = encoder
9950            .encode(&ArmOp::F32Add {
9951                sd: VfpReg::S0,
9952                sn: VfpReg::S0,
9953                sm: VfpReg::S0,
9954            })
9955            .unwrap();
9956        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9957        assert_eq!((instr >> 8) & 0xF, 0xA, "F32 should use cp10");
9958    }
9959
9960    #[test]
9961    fn test_dreg_encoding_higher_registers() {
9962        let encoder = ArmEncoder::new_arm32();
9963
9964        // Test with D15 (highest register)
9965        let op = ArmOp::F64Add {
9966            dd: VfpReg::D15,
9967            dn: VfpReg::D14,
9968            dm: VfpReg::D13,
9969        };
9970        let code = encoder.encode(&op).unwrap();
9971        assert_eq!(code.len(), 4);
9972
9973        // Verify the register encoding worked (instruction is valid)
9974        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9975        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
9976    }
9977
9978    // ========================================================================
9979    // Control flow encoding tests
9980    // ========================================================================
9981
9982    #[test]
9983    fn test_encode_label_emits_no_bytes() {
9984        let encoder = ArmEncoder::new_thumb2();
9985        let op = ArmOp::Label {
9986            name: ".Lblock_end_0".to_string(),
9987        };
9988        let code = encoder.encode(&op).unwrap();
9989        assert!(code.is_empty(), "Label should emit zero bytes");
9990
9991        let encoder32 = ArmEncoder::new_arm32();
9992        let code32 = encoder32.encode(&op).unwrap();
9993        assert!(
9994            code32.is_empty(),
9995            "Label should emit zero bytes in ARM32 too"
9996        );
9997    }
9998
9999    #[test]
10000    fn test_encode_bcc_eq_thumb2() {
10001        use synth_synthesis::Condition;
10002        let encoder = ArmEncoder::new_thumb2();
10003        let op = ArmOp::Bcc {
10004            cond: Condition::EQ,
10005            label: "target".to_string(),
10006        };
10007        let code = encoder.encode(&op).unwrap();
10008        assert_eq!(code.len(), 2); // 16-bit conditional branch
10009
10010        // BEQ with offset 0: 0xD000 in little-endian
10011        assert_eq!(code, vec![0x00, 0xD0]);
10012    }
10013
10014    #[test]
10015    fn test_encode_bcc_ne_thumb2() {
10016        use synth_synthesis::Condition;
10017        let encoder = ArmEncoder::new_thumb2();
10018        let op = ArmOp::Bcc {
10019            cond: Condition::NE,
10020            label: "target".to_string(),
10021        };
10022        let code = encoder.encode(&op).unwrap();
10023        assert_eq!(code.len(), 2);
10024
10025        // BNE with offset 0: 0xD100 in little-endian
10026        assert_eq!(code, vec![0x00, 0xD1]);
10027    }
10028
10029    #[test]
10030    fn test_encode_bcc_arm32() {
10031        use synth_synthesis::Condition;
10032        let encoder = ArmEncoder::new_arm32();
10033        let op = ArmOp::Bcc {
10034            cond: Condition::EQ,
10035            label: "target".to_string(),
10036        };
10037        let code = encoder.encode(&op).unwrap();
10038        assert_eq!(code.len(), 4); // 32-bit ARM instruction
10039
10040        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10041        // BEQ: cond=0x0, opcode=0xA, offset=0
10042        assert_eq!(instr & 0xF0000000, 0x00000000); // EQ condition
10043        assert_eq!(instr & 0x0F000000, 0x0A000000); // Branch opcode
10044    }
10045
10046    #[test]
10047    fn test_encode_udf_thumb2() {
10048        let encoder = ArmEncoder::new_thumb2();
10049        let op = ArmOp::Udf { imm: 0 };
10050        let code = encoder.encode(&op).unwrap();
10051        assert_eq!(code.len(), 2); // 16-bit
10052
10053        // UDF #0: 0xDE00 in little-endian
10054        assert_eq!(code, vec![0x00, 0xDE]);
10055    }
10056
10057    /// #610: the i64 rot/div/rem expansions must land the result in the
10058    /// selector-assigned rd pair and leave R0-R3 preserved (restored from the
10059    /// fixed-ABI wrapper's save area) — pre-#610 the rot expansion's own
10060    /// `POP {R4}` restored stale scratch OVER the result (rd_lo == R4) and
10061    /// the div/rem expansions ignored their register fields outright.
10062    #[test]
10063    fn test_610_i64_rot_expansion_ends_with_rd_movs_and_restore() {
10064        let encoder = ArmEncoder::new_thumb2();
10065        for op in [
10066            ArmOp::I64Rotl {
10067                rdlo: Reg::R4,
10068                rdhi: Reg::R5,
10069                rnlo: Reg::R0,
10070                rnhi: Reg::R1,
10071                shift: Reg::R2,
10072            },
10073            ArmOp::I64Rotr {
10074                rdlo: Reg::R4,
10075                rdhi: Reg::R5,
10076                rnlo: Reg::R0,
10077                rnhi: Reg::R1,
10078                shift: Reg::R2,
10079            },
10080        ] {
10081            let code = encoder.encode(&op).unwrap();
10082            assert_eq!(code.len(), 102, "register-independent size (estimator pin)");
10083            // Tail: MOV r5, r1 (0x460D); MOV r4, r0 (0x4604); POP {r0..r3}
10084            // (rd pair r4:r5 does not overlap the save area — all 4 restored).
10085            let tail: Vec<u16> = code[code.len() - 12..]
10086                .chunks(2)
10087                .map(|c| u16::from_le_bytes([c[0], c[1]]))
10088                .collect();
10089            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
10090        }
10091    }
10092
10093    /// #610: div/rem expansions honor rd and carry the divide-by-zero trap
10094    /// guard (`ORRS R12, R2, R3; BNE +0; UDF #0`) after operand marshaling.
10095    #[test]
10096    fn test_610_i64_div_rem_expansion_guard_and_rd() {
10097        let encoder = ArmEncoder::new_thumb2();
10098        let mk = |which: u8| {
10099            let (rdlo, rdhi, rnlo, rnhi, rmlo, rmhi) =
10100                (Reg::R4, Reg::R5, Reg::R0, Reg::R1, Reg::R2, Reg::R3);
10101            match which {
10102                0 => ArmOp::I64DivU {
10103                    rdlo,
10104                    rdhi,
10105                    rnlo,
10106                    rnhi,
10107                    rmlo,
10108                    rmhi,
10109                    elide_zero_guard: false,
10110                },
10111                1 => ArmOp::I64RemU {
10112                    rdlo,
10113                    rdhi,
10114                    rnlo,
10115                    rnhi,
10116                    rmlo,
10117                    rmhi,
10118                    elide_zero_guard: false,
10119                },
10120                2 => ArmOp::I64DivS {
10121                    rdlo,
10122                    rdhi,
10123                    rnlo,
10124                    rnhi,
10125                    rmlo,
10126                    rmhi,
10127                    elide_zero_guard: false,
10128                    elide_overflow_guard: false,
10129                },
10130                _ => ArmOp::I64RemS {
10131                    rdlo,
10132                    rdhi,
10133                    rnlo,
10134                    rnhi,
10135                    rmlo,
10136                    rmhi,
10137                    elide_zero_guard: false,
10138                },
10139            }
10140        };
10141        for which in 0..4u8 {
10142            let code = encoder.encode(&mk(which)).unwrap();
10143            // Zero-divisor trap guard right after the 26-byte marshal prologue.
10144            let guard: Vec<u16> = code[26..34]
10145                .chunks(2)
10146                .map(|c| u16::from_le_bytes([c[0], c[1]]))
10147                .collect();
10148            assert_eq!(
10149                guard,
10150                vec![0xEA52, 0x0C03, 0xD100, 0xDE00],
10151                "ORRS R12,R2,R3; BNE +0; UDF #0"
10152            );
10153            // Tail: result into rd pair (r5:r4), then restore all of R0-R3.
10154            let tail: Vec<u16> = code[code.len() - 12..]
10155                .chunks(2)
10156                .map(|c| u16::from_le_bytes([c[0], c[1]]))
10157                .collect();
10158            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
10159        }
10160    }
10161
10162    /// #610: when rd overlaps R0-R3 the restore must SKIP the result
10163    /// registers (drop the saved caller word) instead of popping over them.
10164    #[test]
10165    fn test_610_i64_divu_rd_in_r0_r1_skips_restore() {
10166        let encoder = ArmEncoder::new_thumb2();
10167        let code = encoder
10168            .encode(&ArmOp::I64DivU {
10169                rdlo: Reg::R0,
10170                rdhi: Reg::R1,
10171                rnlo: Reg::R0,
10172                rnhi: Reg::R1,
10173                rmlo: Reg::R2,
10174                rmhi: Reg::R3,
10175                elide_zero_guard: false,
10176            })
10177            .unwrap();
10178        let tail: Vec<u16> = code[code.len() - 12..]
10179            .chunks(2)
10180            .map(|c| u16::from_le_bytes([c[0], c[1]]))
10181            .collect();
10182        // MOV r1,r1 / MOV r0,r0 (no-ops, size-stable), ADD SP,#4 twice
10183        // (discard saved r0/r1 — the result lives there), POP {r2}, POP {r3}.
10184        assert_eq!(tail, vec![0x4609, 0x4600, 0xB001, 0xB001, 0xBC04, 0xBC08]);
10185    }
10186
10187    /// #610: a fully swapped rd pair (rd_lo=R1, rd_hi=R0) cannot be
10188    /// materialized by two MOVs in either order — must be a loud Err, never
10189    /// silent corruption. (Selector pairs are consecutive, so unreachable.)
10190    #[test]
10191    fn test_610_i64_swapped_rd_pair_rejected() {
10192        let encoder = ArmEncoder::new_thumb2();
10193        let result = encoder.encode(&ArmOp::I64RemU {
10194            rdlo: Reg::R1,
10195            rdhi: Reg::R0,
10196            rnlo: Reg::R2,
10197            rnhi: Reg::R3,
10198            rmlo: Reg::R4,
10199            rmhi: Reg::R5,
10200            elide_zero_guard: false,
10201        });
10202        assert!(result.is_err(), "swapped rd pair must be rejected loudly");
10203    }
10204
10205    /// #632: the I64Popcnt expansion's own scratch restore (`POP {R3,R4,R5}`)
10206    /// must not clobber the result. Pre-fix the total was materialized with
10207    /// `ADDS rd, R4, R5` BEFORE the pop, so any allocator-assigned
10208    /// rd ∈ {R3,R4,R5} received stale stack garbage. Post-fix the count is
10209    /// carried across the restore in R12 (never allocatable, never restored)
10210    /// and moved into rd only after the pop — structurally rd-independent.
10211    #[test]
10212    fn test_632_i64_popcnt_result_survives_scratch_restore() {
10213        let encoder = ArmEncoder::new_thumb2();
10214        // Every allocatable rd, including the restore set {R3,R4,R5} and R8.
10215        for rd in [
10216            Reg::R0,
10217            Reg::R2,
10218            Reg::R3,
10219            Reg::R4,
10220            Reg::R5,
10221            Reg::R6,
10222            Reg::R8,
10223        ] {
10224            let code = encoder
10225                .encode(&ArmOp::I64Popcnt {
10226                    rd,
10227                    rnlo: Reg::R6,
10228                    rnhi: Reg::R7,
10229                })
10230                .unwrap();
10231            assert_eq!(code.len(), 180, "register-independent size (estimator pin)");
10232            let hw: Vec<u16> = code
10233                .chunks(2)
10234                .map(|c| u16::from_le_bytes([c[0], c[1]]))
10235                .collect();
10236            let pop = hw
10237                .iter()
10238                .position(|&h| h == 0xBC38)
10239                .expect("POP {R3,R4,R5} present");
10240            // Immediately before the POP: ADD.W R12, R4, R5 (the total lives
10241            // in R12, which the POP cannot touch).
10242            assert_eq!(
10243                &hw[pop - 2..pop],
10244                &[0xEB04, 0x0C05],
10245                "total must be carried in R12 across the restore"
10246            );
10247            // Immediately after the POP: MOV rd, R12.
10248            let rd_bits = match rd {
10249                Reg::R8 => 8u16,
10250                Reg::R6 => 6,
10251                Reg::R5 => 5,
10252                Reg::R4 => 4,
10253                Reg::R3 => 3,
10254                Reg::R2 => 2,
10255                _ => 0,
10256            };
10257            let expect_mov = 0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7);
10258            assert_eq!(hw[pop + 1], expect_mov, "MOV rd, R12 after the restore");
10259            // No write into rd between the PUSH and the POP (the old
10260            // pre-restore ADDS is gone).
10261            assert!(
10262                !hw[..pop].contains(&(0x1800 | (5 << 6) | (4 << 3) | rd_bits)),
10263                "no ADDS rd, R4, R5 before the restore pop"
10264            );
10265        }
10266    }
10267
10268    /// #632 audit: the entry marshal must be permutation-safe. Pre-fix
10269    /// `MOV R4, rnlo; MOV R5, rnhi` read a clobbered R4 when the operand
10270    /// pair lived at (R3, R4). Post-fix rnlo routes through R12.
10271    #[test]
10272    fn test_632_i64_popcnt_marshal_pair_at_r3_r4() {
10273        let encoder = ArmEncoder::new_thumb2();
10274        let code = encoder
10275            .encode(&ArmOp::I64Popcnt {
10276                rd: Reg::R0,
10277                rnlo: Reg::R3,
10278                rnhi: Reg::R4,
10279            })
10280            .unwrap();
10281        let hw: Vec<u16> = code
10282            .chunks(2)
10283            .map(|c| u16::from_le_bytes([c[0], c[1]]))
10284            .collect();
10285        // PUSH {R3,R4,R5}; MOV R12, R3; MOV R5, R4 (rnhi read BEFORE any
10286        // write to R4); MOV R4, R12.
10287        assert_eq!(hw[0], 0xB438);
10288        assert_eq!(hw[1], 0x4600 | (1 << 7) | (3 << 3) | 4, "MOV R12, rnlo");
10289        assert_eq!(hw[2], 0x4600 | (4 << 3) | 5, "MOV R5, rnhi");
10290        assert_eq!(hw[3], 0x4664, "MOV R4, R12");
10291    }
10292
10293    /// #632: A32 twin — same structural fix on the ARM-mode path
10294    /// (`--target cortex-r5`): total carried in R12 across the restore.
10295    #[test]
10296    fn test_632_a32_i64_popcnt_result_survives_scratch_restore() {
10297        let encoder = ArmEncoder::new_arm32();
10298        for rd in [Reg::R0, Reg::R3, Reg::R4, Reg::R5, Reg::R8] {
10299            let code = encoder
10300                .encode(&ArmOp::I64Popcnt {
10301                    rd,
10302                    rnlo: Reg::R6,
10303                    rnhi: Reg::R7,
10304                })
10305                .unwrap();
10306            let words: Vec<u32> = code
10307                .chunks(4)
10308                .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
10309                .collect();
10310            let pop = words
10311                .iter()
10312                .position(|&w| w == 0xE8BD_0038)
10313                .expect("POP {R3,R4,R5} present");
10314            assert_eq!(words[pop - 1], 0xE084_C005, "ADD R12, R4, R5 before POP");
10315            let rd_bits = match rd {
10316                Reg::R8 => 8u32,
10317                Reg::R5 => 5,
10318                Reg::R4 => 4,
10319                Reg::R3 => 3,
10320                _ => 0,
10321            };
10322            assert_eq!(
10323                words[pop + 1],
10324                0xE1A0_0000 | (rd_bits << 12) | 12,
10325                "MOV rd, R12 after the restore"
10326            );
10327        }
10328    }
10329
10330    /// #633: I64DivS must carry the INT64_MIN/-1 overflow guard (mirroring
10331    /// the i32 path) right after the zero-divisor guard — dividend in R0:R1,
10332    /// divisor in R2:R3 on the #610/#613 fixed-ABI wrapper path.
10333    #[test]
10334    fn test_633_i64_divs_overflow_guard_emitted() {
10335        let encoder = ArmEncoder::new_thumb2();
10336        let code = encoder
10337            .encode(&ArmOp::I64DivS {
10338                rdlo: Reg::R4,
10339                rdhi: Reg::R5,
10340                rnlo: Reg::R0,
10341                rnhi: Reg::R1,
10342                rmlo: Reg::R2,
10343                rmhi: Reg::R3,
10344                elide_zero_guard: false,
10345                elide_overflow_guard: false,
10346            })
10347            .unwrap();
10348        // 26-byte marshal + 8-byte zero-trap, then the 22-byte overflow guard.
10349        let guard: Vec<u16> = code[34..56]
10350            .chunks(2)
10351            .map(|c| u16::from_le_bytes([c[0], c[1]]))
10352            .collect();
10353        assert_eq!(
10354            guard,
10355            vec![
10356                0xEA02, 0x0C03, // AND.W R12, R2, R3
10357                0xF11C, 0x0F01, // CMN.W R12, #1
10358                0xD105, // BNE .no_trap
10359                0x2800, // CMP R0, #0
10360                0xD103, // BNE .no_trap
10361                0xF1B1, 0x4F00, // CMP.W R1, #0x80000000
10362                0xD100, // BNE .no_trap
10363                0xDE00, // UDF #0 — signed-division overflow
10364            ],
10365            "INT64_MIN/-1 overflow guard after the zero-divisor guard"
10366        );
10367    }
10368
10369    /// #633 fix-guard twin: I64RemS must NOT carry the overflow guard —
10370    /// rem_s(INT64_MIN, -1) is defined as 0 and must not trap. Exactly one
10371    /// UDF (the zero-divisor trap) in the whole expansion.
10372    #[test]
10373    fn test_633_i64_rems_has_no_overflow_guard() {
10374        let encoder = ArmEncoder::new_thumb2();
10375        for (is_rem_s, op) in [
10376            (
10377                true,
10378                ArmOp::I64RemS {
10379                    rdlo: Reg::R4,
10380                    rdhi: Reg::R5,
10381                    rnlo: Reg::R0,
10382                    rnhi: Reg::R1,
10383                    rmlo: Reg::R2,
10384                    rmhi: Reg::R3,
10385                    elide_zero_guard: false,
10386                },
10387            ),
10388            (
10389                false,
10390                ArmOp::I64DivS {
10391                    rdlo: Reg::R4,
10392                    rdhi: Reg::R5,
10393                    rnlo: Reg::R0,
10394                    rnhi: Reg::R1,
10395                    rmlo: Reg::R2,
10396                    rmhi: Reg::R3,
10397                    elide_zero_guard: false,
10398                    elide_overflow_guard: false,
10399                },
10400            ),
10401        ] {
10402            let code = encoder.encode(&op).unwrap();
10403            let udfs = code
10404                .chunks(2)
10405                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
10406                .count();
10407            let want = if is_rem_s { 1 } else { 2 };
10408            assert_eq!(
10409                udfs, want,
10410                "rem_s: zero-trap only; div_s: zero-trap + overflow trap"
10411            );
10412        }
10413    }
10414
10415    /// #494 phase 2b: `elide_zero_guard` drops EXACTLY the 8-byte fused
10416    /// zero-trap (`ORRS.W R12,R2,R3; BNE; UDF #0`) and nothing else — the
10417    /// rest of the expansion is byte-identical (splice check).
10418    #[test]
10419    fn test_494_i64_zero_guard_elision_is_exact_splice() {
10420        let encoder = ArmEncoder::new_thumb2();
10421        let mk = |elide_zero_guard: bool| {
10422            encoder
10423                .encode(&ArmOp::I64DivU {
10424                    rdlo: Reg::R4,
10425                    rdhi: Reg::R5,
10426                    rnlo: Reg::R0,
10427                    rnhi: Reg::R1,
10428                    rmlo: Reg::R2,
10429                    rmhi: Reg::R3,
10430                    elide_zero_guard,
10431                })
10432                .unwrap()
10433        };
10434        let full = mk(false);
10435        let elided = mk(true);
10436        assert_eq!(full.len(), elided.len() + 8, "zero guard is 8 bytes");
10437        // Marshal prologue (26 B) unchanged, guard (8 B) gone, tail identical.
10438        assert_eq!(&full[..26], &elided[..26]);
10439        assert_eq!(
10440            &full[26..34],
10441            &[0x52, 0xEA, 0x03, 0x0C, 0x00, 0xD1, 0x00, 0xDE],
10442            "the spliced-out bytes are exactly ORRS.W; BNE; UDF #0"
10443        );
10444        assert_eq!(&full[34..], &elided[26..]);
10445    }
10446
10447    /// #494 phase 2b two-guard distinction (the #633/#634 synergy): a
10448    /// divisor-nonzero fact elides ONLY the zero guard — the INT64_MIN/-1
10449    /// OVERFLOW guard is a separate obligation and must survive
10450    /// `elide_zero_guard: true`. Pinned on div_s in all flag states.
10451    #[test]
10452    fn test_494_i64_divs_overflow_guard_retained_when_only_zero_elided() {
10453        let encoder = ArmEncoder::new_thumb2();
10454        let mk = |zero: bool, ovf: bool| {
10455            encoder
10456                .encode(&ArmOp::I64DivS {
10457                    rdlo: Reg::R4,
10458                    rdhi: Reg::R5,
10459                    rnlo: Reg::R0,
10460                    rnhi: Reg::R1,
10461                    rmlo: Reg::R2,
10462                    rmhi: Reg::R3,
10463                    elide_zero_guard: zero,
10464                    elide_overflow_guard: ovf,
10465                })
10466                .unwrap()
10467        };
10468        let udf_count = |code: &[u8]| {
10469            code.chunks(2)
10470                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
10471                .count()
10472        };
10473        let full = mk(false, false);
10474        let zero_only = mk(true, false);
10475        let both = mk(true, true);
10476        assert_eq!(udf_count(&full), 2, "baseline: zero trap + overflow trap");
10477        assert_eq!(
10478            udf_count(&zero_only),
10479            1,
10480            "divisor-nonzero elides the zero trap ONLY — the #633 overflow \
10481             guard must be retained"
10482        );
10483        // The retained guard is the 22-byte overflow sequence, now right
10484        // after the 26-byte marshal prologue.
10485        let guard: Vec<u16> = zero_only[26..48]
10486            .chunks(2)
10487            .map(|c| u16::from_le_bytes([c[0], c[1]]))
10488            .collect();
10489        assert_eq!(
10490            guard,
10491            vec![
10492                0xEA02, 0x0C03, 0xF11C, 0x0F01, 0xD105, 0x2800, 0xD103, 0xF1B1, 0x4F00, 0xD100,
10493                0xDE00,
10494            ],
10495            "the surviving guard is the INT64_MIN/-1 overflow trap"
10496        );
10497        assert_eq!(full.len(), zero_only.len() + 8);
10498        assert_eq!(zero_only.len(), both.len() + 22);
10499        assert_eq!(udf_count(&both), 0, "both obligations discharged ⇒ no UDF");
10500    }
10501
10502    /// #494 phase 2b A32 twin: zero-guard elision is an exact 12-byte splice
10503    /// and the A32 overflow guard survives a zero-only elision.
10504    #[test]
10505    fn test_494_a32_i64_guard_elision() {
10506        let encoder = ArmEncoder::new_arm32();
10507        let mk = |zero: bool, ovf: bool| {
10508            encoder
10509                .encode(&ArmOp::I64DivS {
10510                    rdlo: Reg::R4,
10511                    rdhi: Reg::R5,
10512                    rnlo: Reg::R0,
10513                    rnhi: Reg::R1,
10514                    rmlo: Reg::R2,
10515                    rmhi: Reg::R3,
10516                    elide_zero_guard: zero,
10517                    elide_overflow_guard: ovf,
10518                })
10519                .unwrap()
10520        };
10521        let full = mk(false, false);
10522        let zero_only = mk(true, false);
10523        let both = mk(true, true);
10524        // A32 zero guard = 3 words (ORRS/BNE/UDF), overflow guard = 6 words.
10525        assert_eq!(full.len(), zero_only.len() + 12);
10526        assert_eq!(zero_only.len(), both.len() + 24);
10527        let udf_count = |code: &[u8]| {
10528            code.chunks(4)
10529                .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
10530                .count()
10531        };
10532        assert_eq!(udf_count(&full), 2);
10533        assert_eq!(
10534            udf_count(&zero_only),
10535            1,
10536            "A32: overflow guard retained under zero-only elision"
10537        );
10538        assert_eq!(udf_count(&both), 0);
10539    }
10540
10541    /// #633: A32 twin — the conditional-execution overflow guard on the
10542    /// ARM-mode I64DivS, and its absence from I64RemS.
10543    #[test]
10544    fn test_633_a32_i64_divs_overflow_guard() {
10545        let encoder = ArmEncoder::new_arm32();
10546        let mk_divs = ArmOp::I64DivS {
10547            rdlo: Reg::R4,
10548            rdhi: Reg::R5,
10549            rnlo: Reg::R0,
10550            rnhi: Reg::R1,
10551            rmlo: Reg::R2,
10552            rmhi: Reg::R3,
10553            elide_zero_guard: false,
10554            elide_overflow_guard: false,
10555        };
10556        let code = encoder.encode(&mk_divs).unwrap();
10557        let words: Vec<u32> = code
10558            .chunks(4)
10559            .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
10560            .collect();
10561        let guard = [
10562            0xE002_C003u32, // AND   R12, R2, R3
10563            0xE37C_0001,    // CMN   R12, #1
10564            0x0350_0000,    // CMPEQ R0, #0
10565            0x0351_0102,    // CMPEQ R1, #0x80000000
10566            0x1A00_0000,    // BNE +1 insn
10567            0xE7F0_00F0,    // UDF #0
10568        ];
10569        assert!(
10570            words.windows(6).any(|w| w == guard),
10571            "A32 I64DivS carries the INT64_MIN/-1 overflow guard"
10572        );
10573        let rems = encoder
10574            .encode(&ArmOp::I64RemS {
10575                rdlo: Reg::R4,
10576                rdhi: Reg::R5,
10577                rnlo: Reg::R0,
10578                rnhi: Reg::R1,
10579                rmlo: Reg::R2,
10580                rmhi: Reg::R3,
10581                elide_zero_guard: false,
10582            })
10583            .unwrap();
10584        let rems_udfs = rems
10585            .chunks(4)
10586            .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
10587            .count();
10588        assert_eq!(rems_udfs, 1, "A32 I64RemS keeps only the zero-divisor trap");
10589    }
10590
10591    #[test]
10592    fn test_encode_nop_thumb2() {
10593        let encoder = ArmEncoder::new_thumb2();
10594        let op = ArmOp::Nop;
10595        let code = encoder.encode(&op).unwrap();
10596        assert_eq!(code.len(), 2); // 16-bit
10597
10598        // NOP: 0xBF00 in little-endian
10599        assert_eq!(code, vec![0x00, 0xBF]);
10600    }
10601
10602    // =========================================================================
10603    // i64 Thumb-2 encoding tests
10604    // =========================================================================
10605
10606    #[test]
10607    fn test_encode_i64_add_thumb2() {
10608        let encoder = ArmEncoder::new_thumb2();
10609        let op = ArmOp::I64Add {
10610            rdlo: Reg::R0,
10611            rdhi: Reg::R1,
10612            rnlo: Reg::R0,
10613            rnhi: Reg::R1,
10614            rmlo: Reg::R2,
10615            rmhi: Reg::R3,
10616        };
10617        let code = encoder.encode(&op).unwrap();
10618        // Should emit ADDS (2 bytes) + ADC.W (4 bytes) = 6 bytes
10619        assert_eq!(code.len(), 6, "I64Add should be 6 bytes (ADDS + ADC.W)");
10620    }
10621
10622    #[test]
10623    fn test_encode_i64_sub_thumb2() {
10624        let encoder = ArmEncoder::new_thumb2();
10625        let op = ArmOp::I64Sub {
10626            rdlo: Reg::R0,
10627            rdhi: Reg::R1,
10628            rnlo: Reg::R0,
10629            rnhi: Reg::R1,
10630            rmlo: Reg::R2,
10631            rmhi: Reg::R3,
10632        };
10633        let code = encoder.encode(&op).unwrap();
10634        // Should emit SUBS (2 bytes) + SBC.W (4 bytes) = 6 bytes
10635        assert_eq!(code.len(), 6, "I64Sub should be 6 bytes (SUBS + SBC.W)");
10636    }
10637
10638    #[test]
10639    fn test_encode_i64_and_thumb2() {
10640        let encoder = ArmEncoder::new_thumb2();
10641        let op = ArmOp::I64And {
10642            rdlo: Reg::R0,
10643            rdhi: Reg::R1,
10644            rnlo: Reg::R0,
10645            rnhi: Reg::R1,
10646            rmlo: Reg::R2,
10647            rmhi: Reg::R3,
10648        };
10649        let code = encoder.encode(&op).unwrap();
10650        // AND.W (4 bytes) + AND.W (4 bytes) = 8 bytes
10651        assert!(code.len() >= 4, "I64And should emit at least 4 bytes");
10652    }
10653
10654    #[test]
10655    fn test_encode_i64_or_thumb2() {
10656        let encoder = ArmEncoder::new_thumb2();
10657        let op = ArmOp::I64Or {
10658            rdlo: Reg::R0,
10659            rdhi: Reg::R1,
10660            rnlo: Reg::R0,
10661            rnhi: Reg::R1,
10662            rmlo: Reg::R2,
10663            rmhi: Reg::R3,
10664        };
10665        let code = encoder.encode(&op).unwrap();
10666        assert!(code.len() >= 4, "I64Or should emit at least 4 bytes");
10667    }
10668
10669    #[test]
10670    fn test_encode_i64_xor_thumb2() {
10671        let encoder = ArmEncoder::new_thumb2();
10672        let op = ArmOp::I64Xor {
10673            rdlo: Reg::R0,
10674            rdhi: Reg::R1,
10675            rnlo: Reg::R0,
10676            rnhi: Reg::R1,
10677            rmlo: Reg::R2,
10678            rmhi: Reg::R3,
10679        };
10680        let code = encoder.encode(&op).unwrap();
10681        assert!(code.len() >= 4, "I64Xor should emit at least 4 bytes");
10682    }
10683
10684    #[test]
10685    fn test_encode_i64_const_small_thumb2() {
10686        let encoder = ArmEncoder::new_thumb2();
10687        // Small constant: only needs MOVW for each half
10688        let op = ArmOp::I64Const {
10689            rdlo: Reg::R0,
10690            rdhi: Reg::R1,
10691            value: 42,
10692        };
10693        let code = encoder.encode(&op).unwrap();
10694        // MOVW R0, #42 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes minimum
10695        assert!(code.len() >= 8, "I64Const should emit at least 8 bytes");
10696    }
10697
10698    #[test]
10699    fn test_encode_i64_const_large_thumb2() {
10700        let encoder = ArmEncoder::new_thumb2();
10701        // Large constant: needs MOVW+MOVT for each half
10702        let op = ArmOp::I64Const {
10703            rdlo: Reg::R0,
10704            rdhi: Reg::R1,
10705            value: 0x1234_5678_9ABC_DEF0_u64 as i64,
10706        };
10707        let code = encoder.encode(&op).unwrap();
10708        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
10709        assert_eq!(
10710            code.len(),
10711            16,
10712            "I64Const with large value should be 16 bytes"
10713        );
10714    }
10715
10716    #[test]
10717    fn test_encode_i64_extend_i32_s_thumb2() {
10718        let encoder = ArmEncoder::new_thumb2();
10719        let op = ArmOp::I64ExtendI32S {
10720            rdlo: Reg::R0,
10721            rdhi: Reg::R1,
10722            rn: Reg::R0,
10723        };
10724        let code = encoder.encode(&op).unwrap();
10725        // When rdlo == rn, only ASR (4 bytes) is emitted
10726        assert_eq!(
10727            code.len(),
10728            4,
10729            "I64ExtendI32S (same reg) should be 4 bytes (ASR only)"
10730        );
10731    }
10732
10733    #[test]
10734    fn test_encode_i64_extend_i32_s_diff_reg_thumb2() {
10735        let encoder = ArmEncoder::new_thumb2();
10736        let op = ArmOp::I64ExtendI32S {
10737            rdlo: Reg::R0,
10738            rdhi: Reg::R1,
10739            rn: Reg::R2,
10740        };
10741        let code = encoder.encode(&op).unwrap();
10742        // MOV rdlo, rn (2 bytes for low regs) + ASR rdhi, rdlo, #31 (4 bytes) = 6 bytes
10743        assert!(
10744            code.len() >= 6,
10745            "I64ExtendI32S (diff reg) should be at least 6 bytes"
10746        );
10747    }
10748
10749    #[test]
10750    fn test_encode_i64_extend_i32_u_thumb2() {
10751        let encoder = ArmEncoder::new_thumb2();
10752        let op = ArmOp::I64ExtendI32U {
10753            rdlo: Reg::R0,
10754            rdhi: Reg::R1,
10755            rn: Reg::R0,
10756        };
10757        let code = encoder.encode(&op).unwrap();
10758        // When rdlo == rn, only MOV rdhi, #0 (2 bytes) is emitted
10759        assert_eq!(
10760            code.len(),
10761            2,
10762            "I64ExtendI32U (same reg) should be 2 bytes (MOV #0 only)"
10763        );
10764    }
10765
10766    #[test]
10767    fn test_encode_i32_wrap_i64_nop_thumb2() {
10768        let encoder = ArmEncoder::new_thumb2();
10769        // When rd == rnlo, should be a NOP
10770        let op = ArmOp::I32WrapI64 {
10771            rd: Reg::R0,
10772            rnlo: Reg::R0,
10773        };
10774        let code = encoder.encode(&op).unwrap();
10775        assert_eq!(code.len(), 2, "I32WrapI64 same reg should be NOP (2 bytes)");
10776        assert_eq!(code, vec![0x00, 0xBF]); // NOP
10777    }
10778
10779    #[test]
10780    fn test_encode_i32_wrap_i64_diff_reg_thumb2() {
10781        let encoder = ArmEncoder::new_thumb2();
10782        let op = ArmOp::I32WrapI64 {
10783            rd: Reg::R2,
10784            rnlo: Reg::R0,
10785        };
10786        let code = encoder.encode(&op).unwrap();
10787        // MOV R2, R0 (2 or 4 bytes)
10788        assert!(
10789            code.len() >= 2,
10790            "I32WrapI64 diff reg should emit at least 2 bytes"
10791        );
10792    }
10793
10794    #[test]
10795    fn test_encode_i64_eqz_thumb2() {
10796        let encoder = ArmEncoder::new_thumb2();
10797        let op = ArmOp::I64Eqz {
10798            rd: Reg::R0,
10799            rnlo: Reg::R0,
10800            rnhi: Reg::R1,
10801        };
10802        let code = encoder.encode(&op).unwrap();
10803        // Delegates to I64SetCondZ which is already encoded
10804        assert!(
10805            code.len() >= 6,
10806            "I64Eqz should emit at least 6 bytes for ORR+ITE+MOV+MOV"
10807        );
10808    }
10809
10810    #[test]
10811    fn test_encode_i64_eq_thumb2() {
10812        let encoder = ArmEncoder::new_thumb2();
10813        let op = ArmOp::I64Eq {
10814            rd: Reg::R0,
10815            rnlo: Reg::R0,
10816            rnhi: Reg::R1,
10817            rmlo: Reg::R2,
10818            rmhi: Reg::R3,
10819        };
10820        let code = encoder.encode(&op).unwrap();
10821        // Delegates to I64SetCond EQ: CMP lo + IT EQ + CMPEQ hi + ITE EQ + MOV 1 + MOV 0
10822        assert!(code.len() >= 10, "I64Eq should emit at least 10 bytes");
10823    }
10824
10825    #[test]
10826    fn test_encode_i64_ldr_thumb2() {
10827        let encoder = ArmEncoder::new_thumb2();
10828        let op = ArmOp::I64Ldr {
10829            rdlo: Reg::R0,
10830            rdhi: Reg::R1,
10831            addr: MemAddr::imm(Reg::SP, 0),
10832        };
10833        let code = encoder.encode(&op).unwrap();
10834        // Two LDR instructions (lo at offset, hi at offset+4)
10835        assert!(code.len() >= 4, "I64Ldr should emit at least 4 bytes");
10836    }
10837
10838    #[test]
10839    fn test_372_i64_ldr_indexed_materializes_address() {
10840        // #372: a memory i64.load carries an index register (R11 + addr + off).
10841        // The encoder must materialize `ip = base + index` (ADD.W) and load via
10842        // `[ip,#off]` — NOT drop the index. A frame (non-indexed) i64.load must
10843        // stay byte-identical (plain `[base,#off]`, no ADD).
10844        let encoder = ArmEncoder::new_thumb2();
10845        let indexed = encoder
10846            .encode(&ArmOp::I64Ldr {
10847                rdlo: Reg::R0,
10848                rdhi: Reg::R1,
10849                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
10850            })
10851            .unwrap();
10852        // ADD.W ip, fp, r0 = eb0b 0c00 (byte-verified vs arm-none-eabi-as).
10853        assert_eq!(
10854            &indexed[0..4],
10855            &[0x0b, 0xeb, 0x00, 0x0c],
10856            "indexed I64Ldr must start with ADD.W ip, base, index"
10857        );
10858        let frame = encoder
10859            .encode(&ArmOp::I64Ldr {
10860                rdlo: Reg::R0,
10861                rdhi: Reg::R1,
10862                addr: MemAddr::imm(Reg::SP, 8),
10863            })
10864            .unwrap();
10865        // No index -> no ADD.W prefix (byte-identical frame access).
10866        assert_ne!(
10867            &frame[0..2],
10868            &[0x0b, 0xeb],
10869            "frame (non-indexed) I64Ldr must NOT emit an ADD.W"
10870        );
10871    }
10872
10873    #[test]
10874    fn test_382_i64_ldst_large_offset_materializes_not_skips() {
10875        // #382: an indexed i64.load/store whose static offset > 0xFFF must
10876        // MATERIALIZE the offset into the base — NOT return Err (skip the fn).
10877        // Sequence for reg_imm(R11, R0, 5000): MOVW ip,#5000 ; ADD ip,r0,ip ;
10878        // ADD ip,ip,fp ; LDR/STR halves at [ip,#0] / [ip,#4]. Byte-verified tail
10879        // vs arm-none-eabi-as.
10880        let encoder = ArmEncoder::new_thumb2();
10881        // 0x1388 > 0xFFF (MemAddr is not Copy, so build it per use).
10882
10883        let ld = encoder
10884            .encode(&ArmOp::I64Ldr {
10885                rdlo: Reg::R0,
10886                rdhi: Reg::R1,
10887                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
10888            })
10889            .expect("large-offset i64.load must lower, not skip");
10890        // MOVW ip,#0x1388 (4) + ADD ip,r0,ip (4) + ADD ip,ip,fp (4) + 2 LDR (8).
10891        assert_eq!(ld.len(), 20, "expected MOVW + 2×ADD + 2×LDR");
10892        // Must NOT be the small-offset `ADD.W ip, fp, r0` (0x0b 0xeb) prefix —
10893        // that path can only reach imm12 offsets.
10894        assert_ne!(
10895            &ld[0..2],
10896            &[0x0b, 0xeb],
10897            "must materialize the large offset"
10898        );
10899        // Effective base built in ip, then halves at [ip,#0] / [ip,#4].
10900        assert_eq!(
10901            &ld[4..20],
10902            &[
10903                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
10904                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
10905                0xdc, 0xf8, 0x00, 0x00, // LDR.W r0, [ip, #0]
10906                0xdc, 0xf8, 0x04, 0x10, // LDR.W r1, [ip, #4]
10907            ],
10908            "large-offset i64.load must fold offset into ip and access [ip,#0]/[ip,#4]"
10909        );
10910
10911        // Store: same base materialization, STR halves.
10912        let st = encoder
10913            .encode(&ArmOp::I64Str {
10914                rdlo: Reg::R2,
10915                rdhi: Reg::R3,
10916                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
10917            })
10918            .expect("large-offset i64.store must lower, not skip");
10919        assert_eq!(st.len(), 20);
10920        assert_eq!(
10921            &st[4..20],
10922            &[
10923                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
10924                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
10925                0xcc, 0xf8, 0x00, 0x20, // STR.W r2, [ip, #0]
10926                0xcc, 0xf8, 0x04, 0x30, // STR.W r3, [ip, #4]
10927            ],
10928            "large-offset i64.store must fold offset into ip and access [ip,#0]/[ip,#4]"
10929        );
10930
10931        // Small-offset (imm12) indexed access stays byte-identical (#372): the
10932        // effective base is a single `ADD.W ip, fp, r0` and the halves keep the
10933        // folded immediates — NO extra MOVW/ADD.
10934        let small = encoder
10935            .encode(&ArmOp::I64Ldr {
10936                rdlo: Reg::R0,
10937                rdhi: Reg::R1,
10938                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 8),
10939            })
10940            .unwrap();
10941        assert_eq!(
10942            &small[0..4],
10943            &[0x0b, 0xeb, 0x00, 0x0c],
10944            "small-offset indexed i64 must keep the single ADD.W ip, fp, r0"
10945        );
10946        assert_eq!(small.len(), 12, "ADD.W + 2×LDR.W (offset folded in imm12)");
10947    }
10948
10949    #[test]
10950    fn test_encode_i64_str_thumb2() {
10951        let encoder = ArmEncoder::new_thumb2();
10952        let op = ArmOp::I64Str {
10953            rdlo: Reg::R0,
10954            rdhi: Reg::R1,
10955            addr: MemAddr::imm(Reg::SP, 0),
10956        };
10957        let code = encoder.encode(&op).unwrap();
10958        // Two STR instructions (lo at offset, hi at offset+4)
10959        assert!(code.len() >= 4, "I64Str should emit at least 4 bytes");
10960    }
10961
10962    #[test]
10963    fn test_encode_i64_all_comparisons_thumb2() {
10964        let encoder = ArmEncoder::new_thumb2();
10965
10966        let ops = vec![
10967            ArmOp::I64Ne {
10968                rd: Reg::R0,
10969                rnlo: Reg::R0,
10970                rnhi: Reg::R1,
10971                rmlo: Reg::R2,
10972                rmhi: Reg::R3,
10973            },
10974            ArmOp::I64LtS {
10975                rd: Reg::R0,
10976                rnlo: Reg::R0,
10977                rnhi: Reg::R1,
10978                rmlo: Reg::R2,
10979                rmhi: Reg::R3,
10980            },
10981            ArmOp::I64LtU {
10982                rd: Reg::R0,
10983                rnlo: Reg::R0,
10984                rnhi: Reg::R1,
10985                rmlo: Reg::R2,
10986                rmhi: Reg::R3,
10987            },
10988            ArmOp::I64LeS {
10989                rd: Reg::R0,
10990                rnlo: Reg::R0,
10991                rnhi: Reg::R1,
10992                rmlo: Reg::R2,
10993                rmhi: Reg::R3,
10994            },
10995            ArmOp::I64LeU {
10996                rd: Reg::R0,
10997                rnlo: Reg::R0,
10998                rnhi: Reg::R1,
10999                rmlo: Reg::R2,
11000                rmhi: Reg::R3,
11001            },
11002            ArmOp::I64GtS {
11003                rd: Reg::R0,
11004                rnlo: Reg::R0,
11005                rnhi: Reg::R1,
11006                rmlo: Reg::R2,
11007                rmhi: Reg::R3,
11008            },
11009            ArmOp::I64GtU {
11010                rd: Reg::R0,
11011                rnlo: Reg::R0,
11012                rnhi: Reg::R1,
11013                rmlo: Reg::R2,
11014                rmhi: Reg::R3,
11015            },
11016            ArmOp::I64GeS {
11017                rd: Reg::R0,
11018                rnlo: Reg::R0,
11019                rnhi: Reg::R1,
11020                rmlo: Reg::R2,
11021                rmhi: Reg::R3,
11022            },
11023            ArmOp::I64GeU {
11024                rd: Reg::R0,
11025                rnlo: Reg::R0,
11026                rnhi: Reg::R1,
11027                rmlo: Reg::R2,
11028                rmhi: Reg::R3,
11029            },
11030        ];
11031
11032        for op in &ops {
11033            let code = encoder.encode(op).unwrap();
11034            assert!(
11035                code.len() >= 8,
11036                "i64 comparison {:?} should emit at least 8 bytes, got {}",
11037                op,
11038                code.len()
11039            );
11040        }
11041    }
11042
11043    #[test]
11044    fn test_encode_i64_const_zero_thumb2() {
11045        let encoder = ArmEncoder::new_thumb2();
11046        let op = ArmOp::I64Const {
11047            rdlo: Reg::R0,
11048            rdhi: Reg::R1,
11049            value: 0,
11050        };
11051        let code = encoder.encode(&op).unwrap();
11052        // MOVW R0, #0 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes
11053        assert_eq!(code.len(), 8, "I64Const(0) should be 8 bytes");
11054    }
11055
11056    #[test]
11057    fn test_encode_i64_const_negative_one_thumb2() {
11058        let encoder = ArmEncoder::new_thumb2();
11059        let op = ArmOp::I64Const {
11060            rdlo: Reg::R0,
11061            rdhi: Reg::R1,
11062            value: -1, // 0xFFFF_FFFF_FFFF_FFFF
11063        };
11064        let code = encoder.encode(&op).unwrap();
11065        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
11066        assert_eq!(code.len(), 16, "I64Const(-1) should be 16 bytes");
11067    }
11068
11069    // =========================================================================
11070    // Sub-word load/store encoding tests
11071    // =========================================================================
11072
11073    #[test]
11074    fn test_encode_ldrb_arm32() {
11075        let encoder = ArmEncoder::new_arm32();
11076        let op = ArmOp::Ldrb {
11077            rd: Reg::R0,
11078            addr: MemAddr::imm(Reg::R1, 4),
11079        };
11080        let code = encoder.encode(&op).unwrap();
11081        assert_eq!(code.len(), 4, "ARM32 LDRB should be 4 bytes");
11082        // LDRB R0, [R1, #4] = 0xE5D10004
11083        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11084        assert_eq!(encoded, 0xE5D10004, "Should encode LDRB R0, [R1, #4]");
11085    }
11086
11087    #[test]
11088    fn test_encode_strb_arm32() {
11089        let encoder = ArmEncoder::new_arm32();
11090        let op = ArmOp::Strb {
11091            rd: Reg::R0,
11092            addr: MemAddr::imm(Reg::R1, 0),
11093        };
11094        let code = encoder.encode(&op).unwrap();
11095        assert_eq!(code.len(), 4, "ARM32 STRB should be 4 bytes");
11096        // STRB R0, [R1, #0] = 0xE5C10000
11097        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11098        assert_eq!(encoded, 0xE5C10000, "Should encode STRB R0, [R1, #0]");
11099    }
11100
11101    #[test]
11102    fn test_encode_ldrh_arm32() {
11103        let encoder = ArmEncoder::new_arm32();
11104        let op = ArmOp::Ldrh {
11105            rd: Reg::R0,
11106            addr: MemAddr::imm(Reg::R1, 2),
11107        };
11108        let code = encoder.encode(&op).unwrap();
11109        assert_eq!(code.len(), 4, "ARM32 LDRH should be 4 bytes");
11110    }
11111
11112    #[test]
11113    fn test_encode_strh_arm32() {
11114        let encoder = ArmEncoder::new_arm32();
11115        let op = ArmOp::Strh {
11116            rd: Reg::R0,
11117            addr: MemAddr::imm(Reg::R1, 0),
11118        };
11119        let code = encoder.encode(&op).unwrap();
11120        assert_eq!(code.len(), 4, "ARM32 STRH should be 4 bytes");
11121    }
11122
11123    #[test]
11124    fn test_encode_ldrsb_arm32() {
11125        let encoder = ArmEncoder::new_arm32();
11126        let op = ArmOp::Ldrsb {
11127            rd: Reg::R0,
11128            addr: MemAddr::imm(Reg::R1, 0),
11129        };
11130        let code = encoder.encode(&op).unwrap();
11131        assert_eq!(code.len(), 4, "ARM32 LDRSB should be 4 bytes");
11132    }
11133
11134    #[test]
11135    fn test_encode_ldrsh_arm32() {
11136        let encoder = ArmEncoder::new_arm32();
11137        let op = ArmOp::Ldrsh {
11138            rd: Reg::R0,
11139            addr: MemAddr::imm(Reg::R1, 0),
11140        };
11141        let code = encoder.encode(&op).unwrap();
11142        assert_eq!(code.len(), 4, "ARM32 LDRSH should be 4 bytes");
11143    }
11144
11145    #[test]
11146    fn test_encode_ldrb_thumb2_16bit() {
11147        let encoder = ArmEncoder::new_thumb2();
11148        let op = ArmOp::Ldrb {
11149            rd: Reg::R0,
11150            addr: MemAddr::imm(Reg::R1, 4),
11151        };
11152        let code = encoder.encode(&op).unwrap();
11153        // Low registers + small offset -> 16-bit encoding
11154        assert_eq!(
11155            code.len(),
11156            2,
11157            "Thumb-2 LDRB with small offset should be 16-bit"
11158        );
11159    }
11160
11161    #[test]
11162    fn test_encode_ldrb_thumb2_32bit() {
11163        let encoder = ArmEncoder::new_thumb2();
11164        let op = ArmOp::Ldrb {
11165            rd: Reg::R0,
11166            addr: MemAddr::imm(Reg::R1, 100), // offset > 31 needs 32-bit
11167        };
11168        let code = encoder.encode(&op).unwrap();
11169        assert_eq!(
11170            code.len(),
11171            4,
11172            "Thumb-2 LDRB with large offset should be 32-bit"
11173        );
11174    }
11175
11176    #[test]
11177    fn test_encode_strb_thumb2_16bit() {
11178        let encoder = ArmEncoder::new_thumb2();
11179        let op = ArmOp::Strb {
11180            rd: Reg::R0,
11181            addr: MemAddr::imm(Reg::R1, 10),
11182        };
11183        let code = encoder.encode(&op).unwrap();
11184        assert_eq!(
11185            code.len(),
11186            2,
11187            "Thumb-2 STRB with small offset should be 16-bit"
11188        );
11189    }
11190
11191    #[test]
11192    fn test_encode_ldrh_thumb2_16bit() {
11193        let encoder = ArmEncoder::new_thumb2();
11194        let op = ArmOp::Ldrh {
11195            rd: Reg::R0,
11196            addr: MemAddr::imm(Reg::R1, 4), // offset aligned to 2, <= 62
11197        };
11198        let code = encoder.encode(&op).unwrap();
11199        assert_eq!(
11200            code.len(),
11201            2,
11202            "Thumb-2 LDRH with small aligned offset should be 16-bit"
11203        );
11204    }
11205
11206    #[test]
11207    fn test_encode_strh_thumb2_16bit() {
11208        let encoder = ArmEncoder::new_thumb2();
11209        let op = ArmOp::Strh {
11210            rd: Reg::R0,
11211            addr: MemAddr::imm(Reg::R1, 4),
11212        };
11213        let code = encoder.encode(&op).unwrap();
11214        assert_eq!(
11215            code.len(),
11216            2,
11217            "Thumb-2 STRH with small aligned offset should be 16-bit"
11218        );
11219    }
11220
11221    #[test]
11222    fn test_encode_ldrsb_thumb2() {
11223        let encoder = ArmEncoder::new_thumb2();
11224        let op = ArmOp::Ldrsb {
11225            rd: Reg::R0,
11226            addr: MemAddr::imm(Reg::R1, 0),
11227        };
11228        let code = encoder.encode(&op).unwrap();
11229        // LDRSB has no 16-bit immediate form, always 32-bit
11230        assert_eq!(code.len(), 4, "Thumb-2 LDRSB should be 32-bit");
11231    }
11232
11233    #[test]
11234    fn test_encode_ldrsh_thumb2() {
11235        let encoder = ArmEncoder::new_thumb2();
11236        let op = ArmOp::Ldrsh {
11237            rd: Reg::R0,
11238            addr: MemAddr::imm(Reg::R1, 0),
11239        };
11240        let code = encoder.encode(&op).unwrap();
11241        assert_eq!(code.len(), 4, "Thumb-2 LDRSH should be 32-bit");
11242    }
11243
11244    #[test]
11245    fn test_encode_memory_size_thumb2() {
11246        let encoder = ArmEncoder::new_thumb2();
11247        let op = ArmOp::MemorySize { rd: Reg::R0 };
11248        let code = encoder.encode(&op).unwrap();
11249        // R0 and R10 are not both low registers, so this needs careful handling
11250        assert!(!code.is_empty(), "MemorySize should produce code");
11251    }
11252
11253    #[test]
11254    fn test_encode_memory_grow_thumb2() {
11255        let encoder = ArmEncoder::new_thumb2();
11256        let op = ArmOp::MemoryGrow {
11257            rd: Reg::R0,
11258            rn: Reg::R0,
11259        };
11260        let code = encoder.encode(&op).unwrap();
11261        assert_eq!(code.len(), 4, "MemoryGrow (MVN) should be 32-bit Thumb-2");
11262    }
11263
11264    #[test]
11265    fn test_encode_subword_reg_offset_thumb2() {
11266        let encoder = ArmEncoder::new_thumb2();
11267
11268        // LDRB with register offset
11269        let op = ArmOp::Ldrb {
11270            rd: Reg::R0,
11271            addr: MemAddr::reg(Reg::R1, Reg::R2),
11272        };
11273        let code = encoder.encode(&op).unwrap();
11274        assert_eq!(
11275            code.len(),
11276            4,
11277            "Thumb-2 LDRB with reg offset should be 32-bit"
11278        );
11279
11280        // STRB with register offset
11281        let op = ArmOp::Strb {
11282            rd: Reg::R0,
11283            addr: MemAddr::reg(Reg::R1, Reg::R2),
11284        };
11285        let code = encoder.encode(&op).unwrap();
11286        assert_eq!(
11287            code.len(),
11288            4,
11289            "Thumb-2 STRB with reg offset should be 32-bit"
11290        );
11291
11292        // LDRH with register offset
11293        let op = ArmOp::Ldrh {
11294            rd: Reg::R0,
11295            addr: MemAddr::reg(Reg::R1, Reg::R2),
11296        };
11297        let code = encoder.encode(&op).unwrap();
11298        assert_eq!(
11299            code.len(),
11300            4,
11301            "Thumb-2 LDRH with reg offset should be 32-bit"
11302        );
11303
11304        // STRH with register offset
11305        let op = ArmOp::Strh {
11306            rd: Reg::R0,
11307            addr: MemAddr::reg(Reg::R1, Reg::R2),
11308        };
11309        let code = encoder.encode(&op).unwrap();
11310        assert_eq!(
11311            code.len(),
11312            4,
11313            "Thumb-2 STRH with reg offset should be 32-bit"
11314        );
11315    }
11316
11317    #[test]
11318    fn test_encode_subword_reg_imm_offset_thumb2() {
11319        let encoder = ArmEncoder::new_thumb2();
11320
11321        // LDRB with both register and immediate offset
11322        let op = ArmOp::Ldrb {
11323            rd: Reg::R0,
11324            addr: MemAddr::reg_imm(Reg::R1, Reg::R2, 4),
11325        };
11326        let code = encoder.encode(&op).unwrap();
11327        // ADD R12, R2, #4 (4 bytes) + LDRB R0, [R1, R12] (4 bytes) = 8 bytes
11328        assert_eq!(
11329            code.len(),
11330            8,
11331            "Thumb-2 LDRB with reg+imm offset should be 8 bytes"
11332        );
11333    }
11334
11335    // ========================================================================
11336    // Helium MVE encoding tests
11337    // ========================================================================
11338
11339    #[test]
11340    fn test_encode_mve_addi32_thumb2() {
11341        let encoder = ArmEncoder::new_thumb2();
11342        let op = ArmOp::MveAddI {
11343            qd: QReg::Q0,
11344            qn: QReg::Q1,
11345            qm: QReg::Q2,
11346            size: MveSize::S32,
11347        };
11348        let code = encoder.encode(&op).unwrap();
11349        assert_eq!(
11350            code.len(),
11351            4,
11352            "MVE VADD.I32 should be 4 bytes (Thumb-2 32-bit)"
11353        );
11354    }
11355
11356    #[test]
11357    fn test_encode_mve_subi16_thumb2() {
11358        let encoder = ArmEncoder::new_thumb2();
11359        let op = ArmOp::MveSubI {
11360            qd: QReg::Q0,
11361            qn: QReg::Q1,
11362            qm: QReg::Q2,
11363            size: MveSize::S16,
11364        };
11365        let code = encoder.encode(&op).unwrap();
11366        assert_eq!(code.len(), 4, "MVE VSUB.I16 should be 4 bytes");
11367    }
11368
11369    #[test]
11370    fn test_encode_mve_muli8_thumb2() {
11371        let encoder = ArmEncoder::new_thumb2();
11372        let op = ArmOp::MveMulI {
11373            qd: QReg::Q0,
11374            qn: QReg::Q1,
11375            qm: QReg::Q2,
11376            size: MveSize::S8,
11377        };
11378        let code = encoder.encode(&op).unwrap();
11379        assert_eq!(code.len(), 4, "MVE VMUL.I8 should be 4 bytes");
11380    }
11381
11382    #[test]
11383    fn test_encode_mve_bitwise_thumb2() {
11384        let encoder = ArmEncoder::new_thumb2();
11385
11386        let ops = vec![
11387            ArmOp::MveAnd {
11388                qd: QReg::Q0,
11389                qn: QReg::Q1,
11390                qm: QReg::Q2,
11391            },
11392            ArmOp::MveOrr {
11393                qd: QReg::Q0,
11394                qn: QReg::Q1,
11395                qm: QReg::Q2,
11396            },
11397            ArmOp::MveEor {
11398                qd: QReg::Q0,
11399                qn: QReg::Q1,
11400                qm: QReg::Q2,
11401            },
11402            ArmOp::MveBic {
11403                qd: QReg::Q0,
11404                qn: QReg::Q1,
11405                qm: QReg::Q2,
11406            },
11407        ];
11408        for op in ops {
11409            let code = encoder.encode(&op).unwrap();
11410            assert_eq!(code.len(), 4, "MVE bitwise op should be 4 bytes");
11411        }
11412    }
11413
11414    #[test]
11415    fn test_encode_mve_mvn_thumb2() {
11416        let encoder = ArmEncoder::new_thumb2();
11417        let op = ArmOp::MveMvn {
11418            qd: QReg::Q0,
11419            qm: QReg::Q1,
11420        };
11421        let code = encoder.encode(&op).unwrap();
11422        assert_eq!(code.len(), 4, "MVE VMVN should be 4 bytes");
11423    }
11424
11425    #[test]
11426    fn test_encode_mve_load_store_thumb2() {
11427        let encoder = ArmEncoder::new_thumb2();
11428
11429        let load = ArmOp::MveLoad {
11430            qd: QReg::Q0,
11431            addr: MemAddr::imm(Reg::R0, 16),
11432        };
11433        let code = encoder.encode(&load).unwrap();
11434        assert_eq!(code.len(), 4, "MVE VLDRW.32 should be 4 bytes");
11435
11436        let store = ArmOp::MveStore {
11437            qd: QReg::Q1,
11438            addr: MemAddr::imm(Reg::R1, 0),
11439        };
11440        let code = encoder.encode(&store).unwrap();
11441        assert_eq!(code.len(), 4, "MVE VSTRW.32 should be 4 bytes");
11442    }
11443
11444    #[test]
11445    fn test_encode_mve_const_thumb2() {
11446        let encoder = ArmEncoder::new_thumb2();
11447        let op = ArmOp::MveConst {
11448            qd: QReg::Q0,
11449            bytes: [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0],
11450        };
11451        let code = encoder.encode(&op).unwrap();
11452        // Should be 4 words of (MOVW R12 + VMOV Sn) = 4 * (4+4) = 32 bytes min
11453        // Some words with hi16=0 skip MOVT, so length varies
11454        assert!(
11455            code.len() >= 24,
11456            "MVE const should produce multiple instructions"
11457        );
11458    }
11459
11460    #[test]
11461    fn test_encode_mve_dup_thumb2() {
11462        let encoder = ArmEncoder::new_thumb2();
11463        let op = ArmOp::MveDup {
11464            qd: QReg::Q0,
11465            rn: Reg::R0,
11466            size: MveSize::S32,
11467        };
11468        let code = encoder.encode(&op).unwrap();
11469        assert_eq!(code.len(), 4, "MVE VDUP.32 should be 4 bytes");
11470    }
11471
11472    #[test]
11473    fn test_encode_mve_extract_lane_thumb2() {
11474        let encoder = ArmEncoder::new_thumb2();
11475        let op = ArmOp::MveExtractLane {
11476            rd: Reg::R0,
11477            qn: QReg::Q1,
11478            lane: 2,
11479            size: MveSize::S32,
11480        };
11481        let code = encoder.encode(&op).unwrap();
11482        assert_eq!(code.len(), 4, "MVE extract lane should be 4 bytes");
11483    }
11484
11485    #[test]
11486    fn test_encode_mve_insert_lane_thumb2() {
11487        let encoder = ArmEncoder::new_thumb2();
11488        let op = ArmOp::MveInsertLane {
11489            qd: QReg::Q0,
11490            rn: Reg::R1,
11491            lane: 3,
11492            size: MveSize::S32,
11493        };
11494        let code = encoder.encode(&op).unwrap();
11495        assert_eq!(code.len(), 4, "MVE insert lane should be 4 bytes");
11496    }
11497
11498    #[test]
11499    fn test_encode_mve_addf32_thumb2() {
11500        let encoder = ArmEncoder::new_thumb2();
11501        let op = ArmOp::MveAddF32 {
11502            qd: QReg::Q0,
11503            qn: QReg::Q1,
11504            qm: QReg::Q2,
11505        };
11506        let code = encoder.encode(&op).unwrap();
11507        assert_eq!(code.len(), 4, "MVE VADD.F32 should be 4 bytes");
11508    }
11509
11510    #[test]
11511    fn test_encode_mve_divf32_thumb2() {
11512        let encoder = ArmEncoder::new_thumb2();
11513        let op = ArmOp::MveDivF32 {
11514            qd: QReg::Q0,
11515            qn: QReg::Q1,
11516            qm: QReg::Q2,
11517        };
11518        let code = encoder.encode(&op).unwrap();
11519        // Lane-wise: 4 x VDIV.F32 = 4 x 4 = 16 bytes
11520        assert_eq!(
11521            code.len(),
11522            16,
11523            "MVE VDIV.F32 (lane-wise) should be 16 bytes"
11524        );
11525    }
11526
11527    #[test]
11528    fn test_encode_mve_sqrtf32_thumb2() {
11529        let encoder = ArmEncoder::new_thumb2();
11530        let op = ArmOp::MveSqrtF32 {
11531            qd: QReg::Q0,
11532            qm: QReg::Q1,
11533        };
11534        let code = encoder.encode(&op).unwrap();
11535        // Lane-wise: 4 x VSQRT.F32 = 4 x 4 = 16 bytes
11536        assert_eq!(
11537            code.len(),
11538            16,
11539            "MVE VSQRT.F32 (lane-wise) should be 16 bytes"
11540        );
11541    }
11542
11543    #[test]
11544    fn test_encode_mve_negf32_thumb2() {
11545        let encoder = ArmEncoder::new_thumb2();
11546        let op = ArmOp::MveNegF32 {
11547            qd: QReg::Q0,
11548            qm: QReg::Q1,
11549        };
11550        let code = encoder.encode(&op).unwrap();
11551        assert_eq!(code.len(), 4, "MVE VNEG.F32 should be 4 bytes");
11552    }
11553
11554    #[test]
11555    fn test_encode_mve_absf32_thumb2() {
11556        let encoder = ArmEncoder::new_thumb2();
11557        let op = ArmOp::MveAbsF32 {
11558            qd: QReg::Q0,
11559            qm: QReg::Q1,
11560        };
11561        let code = encoder.encode(&op).unwrap();
11562        assert_eq!(code.len(), 4, "MVE VABS.F32 should be 4 bytes");
11563    }
11564
11565    /// VCR-RA-001 / immediate-folding precondition: pins the Thumb-2 `AND`
11566    /// immediate encoding for the byte range and documents its bound.
11567    ///
11568    /// The `And { Operand2::Imm }` encoder packs the low 12 bits straight into
11569    /// the `i:imm3:imm8` field WITHOUT applying ThumbExpandImm (the modified-
11570    /// immediate expansion). For `imm <= 0xFF` (e.g. gale's int8 clamps
11571    /// `#0x7e` / `#0x7f`) that is correct — `i:imm3 = 0000` means "imm8
11572    /// zero-extended". So `and r2, r0, #0x7e` encodes to the canonical
11573    /// `00 f0 7e 02`. For `imm >= 0x100` the field would need a true
11574    /// ThumbExpandImm pattern (rotation / replication), which is NOT
11575    /// implemented here — so **immediate folding must gate on `imm <= 0xFF`**
11576    /// until the encoder is hardened to ThumbExpandImm/Ok-or-Err (the
11577    /// "encoder must be Ok-or-Err, never silently wrong" principle, #180/#185).
11578    /// This bound covers the measured `flat_flight` waste (#209).
11579    #[test]
11580    fn and_immediate_encodes_correctly_in_byte_range_documents_fold_bound() {
11581        let encoder = ArmEncoder::new_thumb2();
11582        let op = ArmOp::And {
11583            rd: Reg::R2,
11584            rn: Reg::R0,
11585            op2: Operand2::Imm(0x7e),
11586        };
11587        let code = encoder.encode(&op).unwrap();
11588        assert_eq!(
11589            code,
11590            vec![0x00, 0xf0, 0x7e, 0x02],
11591            "and r2, r0, #0x7e must encode to the canonical AND.W T1 (imm8=0x7e)"
11592        );
11593    }
11594
11595    /// #255: the shared ThumbExpandImm reverse-encoder underpinning the
11596    /// data-processing immediate fix. Encodable modified immediates round-trip to
11597    /// the expected `i:imm3:imm8` field; a genuinely non-modified value is `None`
11598    /// (caller must materialize into a register). Note `1000 = 0xFA ror 30` *is*
11599    /// representable (field 0xF7A) — the old encoder mis-encoded it (raw 0x3E8);
11600    /// this encodes it correctly.
11601    #[test]
11602    fn try_thumb_expand_imm_encodes_modified_immediates() {
11603        assert_eq!(try_thumb_expand_imm(0x7e), Some(0x07e)); // zero-extended byte
11604        assert_eq!(try_thumb_expand_imm(0xff), Some(0x0ff));
11605        assert_eq!(try_thumb_expand_imm(0x0001_0001), Some(0x101)); // 0x00XY00XY
11606        assert_eq!(try_thumb_expand_imm(0xff00_ff00), Some(0x2ff)); // 0xXY00XY00
11607        assert_eq!(try_thumb_expand_imm(0xffff_ffff), Some(0x3ff)); // 0xXYXYXYXY
11608        assert_eq!(try_thumb_expand_imm(0x100), Some(0xf80)); // 0x80 ror 31
11609        assert_eq!(try_thumb_expand_imm(0x8000_0000), Some(0x400)); // 0x80 ror 8
11610        assert_eq!(try_thumb_expand_imm(1000), Some(0xf7a)); // 0xFA ror 30
11611        // Genuinely unrepresentable (bits too far apart for an 8-bit window).
11612        assert_eq!(try_thumb_expand_imm(0x101), None);
11613        assert_eq!(try_thumb_expand_imm(0x12345), None);
11614    }
11615
11616    /// #255: CMP/ADDS/SUBS encode any valid modified immediate correctly, and
11617    /// ERROR (not silently mis-encode) on a genuinely unrepresentable one,
11618    /// forcing the selector to materialize into a register — closing the
11619    /// silent-miscompile class of #251/#253.
11620    #[test]
11621    fn cmp_adds_subs_immediate_error_on_non_modified_imm() {
11622        let encoder = ArmEncoder::new_thumb2();
11623        // cmp r0, #0xff → valid → Ok; cmp r0, #1000 → valid (0xFA ror 30) → Ok.
11624        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 0xff).is_ok());
11625        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 1000).is_ok());
11626        // cmp r0, #0x101 → NOT a modified immediate → Err (materialize-reg).
11627        assert!(
11628            encoder.encode_thumb32_cmp_imm(&Reg::R0, 0x101).is_err(),
11629            "cmp #0x101 must error, not compare the wrong constant"
11630        );
11631        assert!(
11632            encoder
11633                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x101)
11634                .is_err()
11635        );
11636        assert!(
11637            encoder
11638                .encode_thumb32_subs(&Reg::R0, &Reg::R0, 0x101)
11639                .is_err()
11640        );
11641        // ...but a valid modified immediate still encodes.
11642        assert!(
11643            encoder
11644                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x80)
11645                .is_ok()
11646        );
11647    }
11648
11649    /// #257: MLA (multiply-accumulate) encodes as MLS without the bit-4 op flag.
11650    /// `mla r2, r3, r4, r8` (rd=r2, rn=r3, rm=r4, ra=r8) → Thumb-2 `03 fb 04 82`.
11651    #[test]
11652    fn mla_thumb2_encodes_correctly() {
11653        let encoder = ArmEncoder::new_thumb2();
11654        let code = encoder
11655            .encode(&ArmOp::Mla {
11656                rd: Reg::R2,
11657                rn: Reg::R3,
11658                rm: Reg::R4,
11659                ra: Reg::R8,
11660            })
11661            .unwrap();
11662        // hw1 = 0xFB03, hw2 = (8<<12)|(2<<8)|4 = 0x8204
11663        assert_eq!(code, vec![0x03, 0xfb, 0x04, 0x82]);
11664    }
11665
11666    /// #259: LDR/STR (and sub-word) immediate-offset encoders truncated
11667    /// `offset & 0xFFF`, silently targeting the wrong address for offset >= 4096.
11668    /// They now error (the selector must use register-offset addressing) — the
11669    /// load/store sibling of the #253/#255 class. Offsets <= 4095 still encode.
11670    #[test]
11671    fn ldst_imm12_offset_errors_when_out_of_range() {
11672        let encoder = ArmEncoder::new_thumb2();
11673        // offset 0xFFF (4095): valid → Ok; ldr r0, [r1, #4095].
11674        assert!(
11675            encoder
11676                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0xFFF)
11677                .is_ok()
11678        );
11679        // offset 0x1000 (4096): out of imm12 range → Err (not & 0xFFF → #0).
11680        assert!(
11681            encoder
11682                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0x1000)
11683                .is_err(),
11684            "ldr offset 4096 must error, not wrap to 0"
11685        );
11686        assert!(
11687            encoder
11688                .encode_thumb32_str(&Reg::R0, &Reg::R1, 0x1000)
11689                .is_err()
11690        );
11691        assert!(
11692            encoder
11693                .encode_thumb32_ldrb_imm(&Reg::R0, &Reg::R1, 5000)
11694                .is_err()
11695        );
11696        assert!(
11697            encoder
11698                .encode_thumb32_strh_imm(&Reg::R0, &Reg::R1, 5000)
11699                .is_err()
11700        );
11701    }
11702
11703    /// Latent miscompile fix: ADD/SUB with a >0xFF immediate (e.g.
11704    /// `add sp, sp, #frame` for a >=256-byte frame) used ADD.W (T3), whose
11705    /// `i:imm3:imm8` is a ThumbExpandImm modified immediate — so `#256` silently
11706    /// encoded as `#0` (stack corruption). Use ADDW/SUBW (T4), a PLAIN 12-bit
11707    /// immediate, for 0x100..=0xFFF; keep T3 for <=0xFF (bit-identical); error
11708    /// beyond 4095.
11709    #[test]
11710    fn add_sub_large_immediate_use_addw_subw_not_misencoded() {
11711        let encoder = ArmEncoder::new_thumb2();
11712        // add sp, sp, #256  →  ADDW (T4) SP, SP, #256  =  0d f2 00 1d
11713        assert_eq!(
11714            encoder
11715                .encode(&ArmOp::Add {
11716                    rd: Reg::SP,
11717                    rn: Reg::SP,
11718                    op2: Operand2::Imm(256),
11719                })
11720                .unwrap(),
11721            vec![0x0d, 0xf2, 0x00, 0x1d],
11722            "add sp,sp,#256 must be ADDW (plain imm12), not a mis-encoded ADD.W"
11723        );
11724        // sub sp, sp, #256  →  SUBW (T4) SP, SP, #256  =  ad f2 00 1d
11725        assert_eq!(
11726            encoder
11727                .encode(&ArmOp::Sub {
11728                    rd: Reg::SP,
11729                    rn: Reg::SP,
11730                    op2: Operand2::Imm(256),
11731                })
11732                .unwrap(),
11733            vec![0xad, 0xf2, 0x00, 0x1d],
11734        );
11735        // > 4095 has no single-instruction encoding → error, not silent wrong.
11736        assert!(
11737            encoder
11738                .encode(&ArmOp::Add {
11739                    rd: Reg::SP,
11740                    rn: Reg::SP,
11741                    op2: Operand2::Imm(5000),
11742                })
11743                .is_err(),
11744            "add #5000 must error (no single ADDW), not mis-encode"
11745        );
11746    }
11747
11748    /// Closes the data-proc immediate class: AND and CMN now go through
11749    /// `try_thumb_expand_imm` like ORR/EOR/CMP — correct for any modified
11750    /// immediate, `Err` (not raw-pack / NOP) on an un-encodable one. The byte
11751    /// range stays bit-identical (`and r2,r0,#0x7e` is unchanged).
11752    #[test]
11753    fn and_cmn_immediate_thumb_expand_else_error() {
11754        let encoder = ArmEncoder::new_thumb2();
11755        // byte range unchanged (bit-identical with the pre-retrofit encoding)
11756        assert_eq!(
11757            encoder
11758                .encode(&ArmOp::And {
11759                    rd: Reg::R2,
11760                    rn: Reg::R0,
11761                    op2: Operand2::Imm(0x7e),
11762                })
11763                .unwrap(),
11764            vec![0x00, 0xf0, 0x7e, 0x02],
11765        );
11766        // a valid replicated modified immediate now encodes (was silently wrong)
11767        assert!(
11768            encoder
11769                .encode(&ArmOp::And {
11770                    rd: Reg::R2,
11771                    rn: Reg::R0,
11772                    op2: Operand2::Imm(0xff00ff00u32 as i32),
11773                })
11774                .is_ok()
11775        );
11776        // a genuinely un-encodable immediate errors (AND was raw-pack; CMN NOP)
11777        assert!(
11778            encoder
11779                .encode(&ArmOp::And {
11780                    rd: Reg::R2,
11781                    rn: Reg::R0,
11782                    op2: Operand2::Imm(0x101),
11783                })
11784                .is_err()
11785        );
11786        assert!(
11787            encoder
11788                .encode(&ArmOp::Cmn {
11789                    rn: Reg::R0,
11790                    op2: Operand2::Imm(0x101),
11791                })
11792                .is_err(),
11793            "CMN #0x101 must error, not emit a NOP"
11794        );
11795    }
11796
11797    /// VCR-RA-001: ORR/EOR with a small immediate must encode the real
11798    /// instruction (not a silent `0xBF00` NOP). Pins the byte range and the
11799    /// Ok-or-Err bound that makes future Or/Eor immediate folding safe.
11800    #[test]
11801    fn orr_eor_immediate_encode_in_byte_range_else_error() {
11802        let encoder = ArmEncoder::new_thumb2();
11803        // orr r2, r0, #0x7e  →  ORR.W T1, imm8=0x7e
11804        assert_eq!(
11805            encoder
11806                .encode(&ArmOp::Orr {
11807                    rd: Reg::R2,
11808                    rn: Reg::R0,
11809                    op2: Operand2::Imm(0x7e),
11810                })
11811                .unwrap(),
11812            vec![0x40, 0xf0, 0x7e, 0x02],
11813        );
11814        // eor r2, r0, #0x7e  →  EOR.W T1, imm8=0x7e
11815        assert_eq!(
11816            encoder
11817                .encode(&ArmOp::Eor {
11818                    rd: Reg::R2,
11819                    rn: Reg::R0,
11820                    op2: Operand2::Imm(0x7e),
11821                })
11822                .unwrap(),
11823            vec![0x80, 0xf0, 0x7e, 0x02],
11824        );
11825        // Out-of-range immediates error rather than silently mis-encode / NOP.
11826        assert!(
11827            encoder
11828                .encode(&ArmOp::Orr {
11829                    rd: Reg::R2,
11830                    rn: Reg::R0,
11831                    op2: Operand2::Imm(0x140),
11832                })
11833                .is_err(),
11834            "ORR #0x140 must error, not emit a NOP"
11835        );
11836    }
11837
11838    #[test]
11839    fn test_encode_mve_different_qregs() {
11840        let encoder = ArmEncoder::new_thumb2();
11841
11842        // Test that different Q-register numbers produce different encodings
11843        let op1 = ArmOp::MveAddI {
11844            qd: QReg::Q0,
11845            qn: QReg::Q0,
11846            qm: QReg::Q0,
11847            size: MveSize::S32,
11848        };
11849        let op2 = ArmOp::MveAddI {
11850            qd: QReg::Q3,
11851            qn: QReg::Q5,
11852            qm: QReg::Q7,
11853            size: MveSize::S32,
11854        };
11855        let code1 = encoder.encode(&op1).unwrap();
11856        let code2 = encoder.encode(&op2).unwrap();
11857        assert_ne!(
11858            code1, code2,
11859            "Different Q-registers should produce different encodings"
11860        );
11861    }
11862
11863    #[test]
11864    fn test_encode_mve_arm32_loud_err() {
11865        // #615: MVE (Helium) is Thumb-2-only. The ARM32 encoder used to emit
11866        // a silent NOP here (dropping the vector op); it must now be a typed
11867        // Err so a broken "MVE implies Thumb" invariant fails loudly.
11868        let encoder = ArmEncoder::new_arm32();
11869        let op = ArmOp::MveAddI {
11870            qd: QReg::Q0,
11871            qn: QReg::Q1,
11872            qm: QReg::Q2,
11873            size: MveSize::S32,
11874        };
11875        let err = encoder
11876            .encode(&op)
11877            .expect_err("ARM32 MVE must be a loud Err, not a silent NOP (#615)");
11878        assert!(
11879            err.to_string().contains("Thumb-2 only"),
11880            "unexpected error message: {err}"
11881        );
11882    }
11883}