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    /// MOV r12, idx, LSL #2   ; table byte offset
131    /// LDR r12, [r11, r12]    ; load function pointer
132    /// BLX r12                ; indirect call
133    /// ```
134    ///
135    /// Bounds and type-signature checks are not emitted — parity with the
136    /// Thumb-2 path (tracked separately, see #594's note).
137    fn encode_arm_call_indirect(table_index_reg: &Reg) -> Vec<u8> {
138        let idx = reg_to_bits(table_index_reg);
139        let mut bytes = Vec::with_capacity(12);
140        // MOV r12, idx, LSL #2 — data-processing MOV, register op2 with
141        // imm5=2/LSL: cond=E, opcode=1101, S=0, Rd=r12.
142        let mov: u32 = 0xE1A0C000 | (2 << 7) | idx;
143        bytes.extend_from_slice(&mov.to_le_bytes());
144        // LDR r12, [r11, r12] — register offset, P=1 U=1 B=0 W=0 L=1.
145        let ldr: u32 = 0xE79BC00C;
146        bytes.extend_from_slice(&ldr.to_le_bytes());
147        // BLX r12 — cond=E, 0001 0010 1111 1111 1111 0011, Rm=r12.
148        let blx: u32 = 0xE12FFF3C;
149        bytes.extend_from_slice(&blx.to_le_bytes());
150        bytes
151    }
152
153    /// #615: A32 (ARM-mode) expansions for the multi-instruction ops that the
154    /// Thumb-2 encoder expands but the A32 arm previously encoded as a single
155    /// literal NOP (`0xE1A00000`) — i64 mul / shifts / rotates / comparisons /
156    /// eqz, plus i64 const/load/store/extend/wrap and the i32 SetCond /
157    /// SelectMove pseudo-ops. Each expansion mirrors its Thumb-2 twin's
158    /// register contract and semantics exactly (A32 conditional execution
159    /// replaces the IT blocks). Returns `Ok(None)` for ops this helper does
160    /// not handle; the caller's match encodes or loudly rejects those.
161    fn encode_arm_expanded(&self, op: &ArmOp) -> Result<Option<Vec<u8>>> {
162        use synth_synthesis::Condition;
163
164        /// A32 condition-field bits (instruction bits [31:28]).
165        fn cond_bits(cond: &Condition) -> u32 {
166            match cond {
167                Condition::EQ => 0x0,
168                Condition::NE => 0x1,
169                Condition::HS => 0x2, // CS: unsigned >=
170                Condition::LO => 0x3, // CC: unsigned <
171                Condition::HI => 0x8, // unsigned >
172                Condition::LS => 0x9, // unsigned <=
173                Condition::GE => 0xA,
174                Condition::LT => 0xB,
175                Condition::GT => 0xC,
176                Condition::LE => 0xD,
177            }
178        }
179        fn w(b: &mut Vec<u8>, word: u32) {
180            b.extend_from_slice(&word.to_le_bytes());
181        }
182        /// MOV<cond> rd, #imm (rotated-immediate form; only 0/1 used here).
183        fn mov_cond_imm(b: &mut Vec<u8>, cond: u32, rd: u32, imm: u32) {
184            w(b, (cond << 28) | 0x03A0_0000 | (rd << 12) | imm);
185        }
186        /// After a flag-setting pair: MOV<cond> rd,#1 ; MOV<!cond> rd,#0.
187        fn set_cond(b: &mut Vec<u8>, cond: &Condition, rd: u32) {
188            mov_cond_imm(b, cond_bits(cond), rd, 1);
189            mov_cond_imm(b, cond_bits(&cond.invert()), rd, 0);
190        }
191        /// CMP rn, rm (register form).
192        fn cmp_reg(b: &mut Vec<u8>, rn: u32, rm: u32) {
193            w(b, 0xE150_0000 | (rn << 16) | rm);
194        }
195        /// SBCS rd, rn, rm — the 64-bit compare idiom's high-word subtract.
196        fn sbcs(b: &mut Vec<u8>, rd: u32, rn: u32, rm: u32) {
197            w(b, 0xE0D0_0000 | (rn << 16) | (rd << 12) | rm);
198        }
199        /// MOVW rd, #imm16.
200        fn movw(b: &mut Vec<u8>, rd: u32, v: u32) {
201            w(
202                b,
203                0xE300_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
204            );
205        }
206        /// MOVT rd, #imm16.
207        fn movt(b: &mut Vec<u8>, rd: u32, v: u32) {
208            w(
209                b,
210                0xE340_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
211            );
212        }
213        /// Register-controlled shift: MOV rd, rn, <LSL|LSR|ASR> rs.
214        /// `ty`: 0=LSL, 1=LSR, 2=ASR. A32 uses the bottom byte of rs;
215        /// amounts of 32 or more yield 0 (LSL/LSR) or all-sign (ASR) — same
216        /// semantics the Thumb-2 expansions rely on.
217        fn shift_reg(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, rs: u32) {
218            w(b, 0xE1A0_0010 | (rd << 12) | (rs << 8) | (ty << 5) | rn);
219        }
220        const LSL: u32 = 0;
221        const LSR: u32 = 1;
222        const ASR: u32 = 2;
223        /// Immediate-shift move: MOV rd, rn, <LSL|LSR|ASR> #imm.
224        fn shift_imm(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, imm: u32) {
225            w(
226                b,
227                0xE1A0_0000 | (rd << 12) | ((imm & 0x1F) << 7) | (ty << 5) | rn,
228            );
229        }
230        /// Data-processing register form: `base | rn<<16 | rd<<12 | rm`.
231        /// `base` carries cond/opcode/S (e.g. 0xE090_0000 = ADDS).
232        fn dp_reg(b: &mut Vec<u8>, base: u32, rd: u32, rn: u32, rm: u32) {
233            w(b, base | (rn << 16) | (rd << 12) | rm);
234        }
235        /// ORR rd, rd, rm, LSR #31 — the carry-propagation idiom of the
236        /// shift-subtract division loop (bring rm's MSB into rd's bit 0).
237        fn orr_lsr31(b: &mut Vec<u8>, rd: u32, rm: u32) {
238            w(
239                b,
240                0xE180_0000 | (rd << 16) | (rd << 12) | (31 << 7) | (1 << 5) | rm,
241            );
242        }
243        /// 64-bit two's-complement negate of the lo:hi pair (MVN/MVN/ADDS/ADC).
244        fn negate64(b: &mut Vec<u8>, lo: u32, hi: u32) {
245            w(b, 0xE1E0_0000 | (lo << 12) | lo); //           MVN  lo, lo
246            w(b, 0xE1E0_0000 | (hi << 12) | hi); //           MVN  hi, hi
247            w(b, 0xE290_0001 | (lo << 16) | (lo << 12)); //   ADDS lo, lo, #1
248            w(b, 0xE2A0_0000 | (hi << 16) | (hi << 12)); //   ADC  hi, hi, #0
249        }
250        /// TST x, x ; BPL +4-instructions — the "skip the negate64 when the
251        /// sign bit is clear" guard of the signed div/rem arms.
252        fn skip_negate_if_positive(b: &mut Vec<u8>, x: u32) {
253            w(b, 0xE110_0000 | (x << 16) | x); // TST x, x
254            w(b, 0x5A00_0003); //                 BPL +4 insns (past negate64)
255        }
256        /// The 64-iteration shift-subtract division loop — A32 transcription
257        /// of the Thumb-2 #610 core: dividend R0:R1, divisor R2:R3, quotient
258        /// R4:R5, remainder R6:R7, loop counter in `counter` (R12 or R8).
259        fn div_loop(b: &mut Vec<u8>, counter: u32) {
260            w(b, 0xE3A0_0040 | (counter << 12)); // MOV counter, #64
261            let loop_start = b.len();
262            // quotient <<= 1
263            shift_imm(b, LSL, 5, 5, 1);
264            orr_lsr31(b, 5, 4);
265            shift_imm(b, LSL, 4, 4, 1);
266            // remainder <<= 1, OR in dividend MSB
267            shift_imm(b, LSL, 7, 7, 1);
268            orr_lsr31(b, 7, 6);
269            shift_imm(b, LSL, 6, 6, 1);
270            orr_lsr31(b, 6, 1);
271            // dividend <<= 1
272            shift_imm(b, LSL, 1, 1, 1);
273            orr_lsr31(b, 1, 0);
274            shift_imm(b, LSL, 0, 0, 1);
275            // if remainder >= divisor (64-bit unsigned): subtract, set q bit
276            w(b, 0xE157_0003); // CMP R7, R3      (high words)
277            w(b, 0x8A00_0002); // BHI .subtract   (+2 insns)
278            w(b, 0x3A00_0004); // BLO .next       (+4 insns)
279            w(b, 0xE156_0002); // CMP R6, R2      (low words, highs equal)
280            w(b, 0x3A00_0002); // BLO .next       (+2 insns)
281            w(b, 0xE056_6002); // .subtract: SUBS R6, R6, R2
282            w(b, 0xE0C7_7003); //            SBC  R7, R7, R3
283            w(b, 0xE384_4001); //            ORR  R4, R4, #1
284            // .next: decrement and loop
285            w(b, 0xE250_0001 | (counter << 16) | (counter << 12)); // SUBS counter, #1
286            let diff = (loop_start as i64) - (b.len() as i64 + 8);
287            w(b, 0x1A00_0000 | (((diff / 4) as u32) & 0x00FF_FFFF)); // BNE loop
288        }
289        /// 32-bit population count on working register `x` — A32 transcription
290        /// of the Thumb-2 I64Popcnt per-word core (mul-based fold): `c` is the
291        /// constant register, R12 the shifted temp. Both are clobbered.
292        fn popcnt_word(b: &mut Vec<u8>, x: u32, c: u32) {
293            // x = x - ((x >> 1) & 0x55555555)
294            shift_imm(b, LSR, 12, x, 1);
295            movw(b, c, 0x5555);
296            movt(b, c, 0x5555);
297            dp_reg(b, 0xE000_0000, 12, 12, c); // AND R12, R12, c
298            dp_reg(b, 0xE040_0000, x, x, 12); //  SUB x, x, R12
299            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
300            movw(b, c, 0x3333);
301            movt(b, c, 0x3333);
302            dp_reg(b, 0xE000_0000, 12, x, c); //  AND R12, x, c
303            shift_imm(b, LSR, x, x, 2);
304            dp_reg(b, 0xE000_0000, x, x, c); //   AND x, x, c
305            dp_reg(b, 0xE080_0000, x, x, 12); //  ADD x, x, R12
306            // x = (x + (x >> 4)) & 0x0F0F0F0F
307            shift_imm(b, LSR, 12, x, 4);
308            dp_reg(b, 0xE080_0000, x, x, 12); //  ADD x, x, R12
309            movw(b, c, 0x0F0F);
310            movt(b, c, 0x0F0F);
311            dp_reg(b, 0xE000_0000, x, x, c); //   AND x, x, c
312            // x = (x * 0x01010101) >> 24
313            movw(b, c, 0x0101);
314            movt(b, c, 0x0101);
315            w(b, 0xE000_0090 | (x << 16) | (c << 8) | x); // MUL x, x, c
316            shift_imm(b, LSR, x, x, 24);
317        }
318
319        let mut b: Vec<u8> = Vec::new();
320        match op {
321            // SetCond: materialize a flags-predicate as 0/1 — the A32 twin of
322            // the Thumb `ITE cond; MOV rd,#1; MOV rd,#0`.
323            ArmOp::SetCond { rd, cond } => {
324                set_cond(&mut b, cond, reg_to_bits(rd));
325            }
326
327            // SelectMove: conditional register move (Thumb: IT cond; MOV).
328            ArmOp::SelectMove { rd, rm, cond } => {
329                w(
330                    &mut b,
331                    (cond_bits(cond) << 28)
332                        | 0x01A0_0000
333                        | (reg_to_bits(rd) << 12)
334                        | reg_to_bits(rm),
335                );
336            }
337
338            // I64SetCond: compare two i64 register pairs, 0/1 into rd.
339            // EQ/NE: CMP lo,lo; CMPEQ hi,hi (only if lows equal); set.
340            // Ordered: CMP lo,lo; SBCS rd,hi,hi; set — with the same
341            // operand-swap + condition mapping as the Thumb-2 arm.
342            ArmOp::I64SetCond {
343                rd,
344                rn_lo,
345                rn_hi,
346                rm_lo,
347                rm_hi,
348                cond,
349            } => {
350                let rd_b = reg_to_bits(rd);
351                let (n_lo, n_hi, m_lo, m_hi) = (
352                    reg_to_bits(rn_lo),
353                    reg_to_bits(rn_hi),
354                    reg_to_bits(rm_lo),
355                    reg_to_bits(rm_hi),
356                );
357                match cond {
358                    Condition::EQ | Condition::NE => {
359                        cmp_reg(&mut b, n_lo, m_lo);
360                        // CMP<EQ> rn_hi, rm_hi — compare highs only if lows equal.
361                        w(&mut b, 0x0150_0000 | (n_hi << 16) | m_hi);
362                        set_cond(&mut b, cond, rd_b);
363                    }
364                    // (swap operands?, condition after SBCS) per the Thumb arm:
365                    // LT/GE/LO/HS compare (rn, rm); GT/LE/HI/LS swap to (rm, rn).
366                    Condition::LT => {
367                        cmp_reg(&mut b, n_lo, m_lo);
368                        sbcs(&mut b, rd_b, n_hi, m_hi);
369                        set_cond(&mut b, &Condition::LT, rd_b);
370                    }
371                    Condition::GE => {
372                        cmp_reg(&mut b, n_lo, m_lo);
373                        sbcs(&mut b, rd_b, n_hi, m_hi);
374                        set_cond(&mut b, &Condition::GE, rd_b);
375                    }
376                    Condition::GT => {
377                        cmp_reg(&mut b, m_lo, n_lo);
378                        sbcs(&mut b, rd_b, m_hi, n_hi);
379                        set_cond(&mut b, &Condition::LT, rd_b);
380                    }
381                    Condition::LE => {
382                        cmp_reg(&mut b, m_lo, n_lo);
383                        sbcs(&mut b, rd_b, m_hi, n_hi);
384                        set_cond(&mut b, &Condition::GE, rd_b);
385                    }
386                    Condition::LO => {
387                        cmp_reg(&mut b, n_lo, m_lo);
388                        sbcs(&mut b, rd_b, n_hi, m_hi);
389                        set_cond(&mut b, &Condition::LO, rd_b);
390                    }
391                    Condition::HS => {
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::HS, rd_b);
395                    }
396                    Condition::HI => {
397                        cmp_reg(&mut b, m_lo, n_lo);
398                        sbcs(&mut b, rd_b, m_hi, n_hi);
399                        set_cond(&mut b, &Condition::LO, rd_b);
400                    }
401                    Condition::LS => {
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::HS, rd_b);
405                    }
406                }
407            }
408
409            // I64SetCondZ: ORRS rd, lo, hi sets Z iff the pair is zero.
410            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
411                let rd_b = reg_to_bits(rd);
412                w(
413                    &mut b,
414                    0xE190_0000 | (reg_to_bits(rn_lo) << 16) | (rd_b << 12) | reg_to_bits(rn_hi),
415                );
416                set_cond(&mut b, &Condition::EQ, rd_b);
417            }
418
419            // i64 comparison wrappers: delegate to I64SetCond/Z, mirroring the
420            // Thumb-2 delegation arms.
421            ArmOp::I64Eqz { rd, rnlo, rnhi } => {
422                return self
423                    .encode_arm(&ArmOp::I64SetCondZ {
424                        rd: *rd,
425                        rn_lo: *rnlo,
426                        rn_hi: *rnhi,
427                    })
428                    .map(Some);
429            }
430            ArmOp::I64Eq {
431                rd,
432                rnlo,
433                rnhi,
434                rmlo,
435                rmhi,
436            }
437            | ArmOp::I64Ne {
438                rd,
439                rnlo,
440                rnhi,
441                rmlo,
442                rmhi,
443            }
444            | ArmOp::I64LtS {
445                rd,
446                rnlo,
447                rnhi,
448                rmlo,
449                rmhi,
450            }
451            | ArmOp::I64LtU {
452                rd,
453                rnlo,
454                rnhi,
455                rmlo,
456                rmhi,
457            }
458            | ArmOp::I64LeS {
459                rd,
460                rnlo,
461                rnhi,
462                rmlo,
463                rmhi,
464            }
465            | ArmOp::I64LeU {
466                rd,
467                rnlo,
468                rnhi,
469                rmlo,
470                rmhi,
471            }
472            | ArmOp::I64GtS {
473                rd,
474                rnlo,
475                rnhi,
476                rmlo,
477                rmhi,
478            }
479            | ArmOp::I64GtU {
480                rd,
481                rnlo,
482                rnhi,
483                rmlo,
484                rmhi,
485            }
486            | ArmOp::I64GeS {
487                rd,
488                rnlo,
489                rnhi,
490                rmlo,
491                rmhi,
492            }
493            | ArmOp::I64GeU {
494                rd,
495                rnlo,
496                rnhi,
497                rmlo,
498                rmhi,
499            } => {
500                let cond = match op {
501                    ArmOp::I64Eq { .. } => Condition::EQ,
502                    ArmOp::I64Ne { .. } => Condition::NE,
503                    ArmOp::I64LtS { .. } => Condition::LT,
504                    ArmOp::I64LtU { .. } => Condition::LO,
505                    ArmOp::I64LeS { .. } => Condition::LE,
506                    ArmOp::I64LeU { .. } => Condition::LS,
507                    ArmOp::I64GtS { .. } => Condition::GT,
508                    ArmOp::I64GtU { .. } => Condition::HI,
509                    ArmOp::I64GeS { .. } => Condition::GE,
510                    _ => Condition::HS,
511                };
512                return self
513                    .encode_arm(&ArmOp::I64SetCond {
514                        rd: *rd,
515                        rn_lo: *rnlo,
516                        rn_hi: *rnhi,
517                        rm_lo: *rmlo,
518                        rm_hi: *rmhi,
519                        cond,
520                    })
521                    .map(Some);
522            }
523
524            // I64Mul: cross products into R12, then UMULL — same sequence and
525            // ordering as the Thumb-2 arm (R12 is encoder scratch, #212).
526            ArmOp::I64Mul {
527                rd_lo,
528                rd_hi,
529                rn_lo,
530                rn_hi,
531                rm_lo,
532                rm_hi,
533            } => {
534                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
535                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
536                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
537                // MUL R12, rn_lo, rm_hi   (R12 = a_lo * b_hi)
538                w(&mut b, 0xE000_0090 | (12 << 16) | (mh << 8) | nl);
539                // MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
540                w(
541                    &mut b,
542                    0xE020_0090 | (12 << 16) | (12 << 12) | (ml << 8) | nh,
543                );
544                // UMULL rd_lo, rd_hi, rn_lo, rm_lo
545                w(
546                    &mut b,
547                    0xE080_0090 | (dh << 16) | (dl << 12) | (ml << 8) | nl,
548                );
549                // ADD rd_hi, rd_hi, R12
550                w(&mut b, 0xE080_0000 | (dh << 16) | (dh << 12) | 12);
551            }
552
553            // I64Shl / I64ShrU / I64ShrS: same small/large-shift structure as
554            // the Thumb-2 arms (rm_hi is the scratch register; amounts are
555            // masked to 6 bits; register-controlled shifts >= 32 yield 0,
556            // which the small path relies on for n = 0).
557            ArmOp::I64Shl {
558                rd_lo,
559                rd_hi,
560                rn_lo,
561                rn_hi,
562                rm_lo,
563                rm_hi,
564            } => {
565                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
566                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
567                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
568                w(&mut b, 0xE200_003F | (ml << 16) | (ml << 12)); // AND  ml, ml, #63
569                w(&mut b, 0xE250_0020 | (ml << 16) | (mh << 12)); // SUBS mh, ml, #32
570                w(&mut b, 0x5A00_0005); //                            BPL  .large
571                w(&mut b, 0xE260_0020 | (ml << 16) | (mh << 12)); // RSB  mh, ml, #32
572                shift_reg(&mut b, LSR, mh, nl, mh); //               mh = lo >> (32-n)
573                shift_reg(&mut b, LSL, dh, nh, ml); //               dh = hi << n
574                w(&mut b, 0xE180_0000 | (dh << 16) | (dh << 12) | mh); // ORR dh, dh, mh
575                shift_reg(&mut b, LSL, dl, nl, ml); //               dl = lo << n
576                w(&mut b, 0xEA00_0001); //                            B    .done
577                shift_reg(&mut b, LSL, dh, nl, mh); //               .large: dh = lo << (n-32)
578                w(&mut b, 0xE3A0_0000 | (dl << 12)); //              MOV  dl, #0
579            }
580            ArmOp::I64ShrU {
581                rd_lo,
582                rd_hi,
583                rn_lo,
584                rn_hi,
585                rm_lo,
586                rm_hi,
587            } => {
588                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
589                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
590                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
591                w(&mut b, 0xE200_003F | (ml << 16) | (ml << 12)); // AND  ml, ml, #63
592                w(&mut b, 0xE250_0020 | (ml << 16) | (mh << 12)); // SUBS mh, ml, #32
593                w(&mut b, 0x5A00_0005); //                            BPL  .large
594                w(&mut b, 0xE260_0020 | (ml << 16) | (mh << 12)); // RSB  mh, ml, #32
595                shift_reg(&mut b, LSL, mh, nh, mh); //               mh = hi << (32-n)
596                shift_reg(&mut b, LSR, dl, nl, ml); //               dl = lo >> n
597                w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | mh); // ORR dl, dl, mh
598                shift_reg(&mut b, LSR, dh, nh, ml); //               dh = hi >> n
599                w(&mut b, 0xEA00_0001); //                            B    .done
600                shift_reg(&mut b, LSR, dl, nh, mh); //               .large: dl = hi >> (n-32)
601                w(&mut b, 0xE3A0_0000 | (dh << 12)); //              MOV  dh, #0
602            }
603            ArmOp::I64ShrS {
604                rd_lo,
605                rd_hi,
606                rn_lo,
607                rn_hi,
608                rm_lo,
609                rm_hi,
610            } => {
611                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
612                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
613                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
614                w(&mut b, 0xE200_003F | (ml << 16) | (ml << 12)); // AND  ml, ml, #63
615                w(&mut b, 0xE250_0020 | (ml << 16) | (mh << 12)); // SUBS mh, ml, #32
616                w(&mut b, 0x5A00_0005); //                            BPL  .large
617                w(&mut b, 0xE260_0020 | (ml << 16) | (mh << 12)); // RSB  mh, ml, #32
618                shift_reg(&mut b, LSL, mh, nh, mh); //               mh = hi << (32-n)
619                shift_reg(&mut b, LSR, dl, nl, ml); //               dl = lo >> n
620                w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | mh); // ORR dl, dl, mh
621                shift_reg(&mut b, ASR, dh, nh, ml); //               dh = hi >> n (arith)
622                w(&mut b, 0xEA00_0001); //                            B    .done
623                shift_reg(&mut b, ASR, dl, nh, mh); //               .large: dl = hi >> (n-32)
624                w(&mut b, 0xE1A0_0040 | (dh << 12) | (31 << 7) | nh); // ASR dh, nh, #31
625            }
626
627            // I64Rotl / I64Rotr: the #610 fixed-ABI wrapper (A32 form) around
628            // the same fixed-register core as the Thumb-2 arms — value in
629            // R0:R1, amount in R2, scratch R3 + R12.
630            ArmOp::I64Rotl {
631                rdlo,
632                rdhi,
633                rnlo,
634                rnhi,
635                shift,
636            } => {
637                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
638                for word in [
639                    0xE202_203Fu32, // AND  R2, R2, #63   (mask amount mod 64)
640                    0xE252_3020,    // SUBS R3, R2, #32   (R3 = n-32, sets N)
641                    0x5A00_0007,    // BPL  .large        (n >= 32)
642                    // --- small rotation (n < 32) ---
643                    0xE262_3020, // RSB  R3, R2, #32   (R3 = 32-n)
644                    0xE1A0_C330, // LSR  R12, R0, R3   (lo >> (32-n))
645                    0xE1A0_3331, // LSR  R3, R1, R3    (hi >> (32-n))
646                    0xE1A0_1211, // LSL  R1, R1, R2    (hi << n)
647                    0xE181_100C, // ORR  R1, R1, R12   (new_hi)
648                    0xE1A0_0210, // LSL  R0, R0, R2    (lo << n)
649                    0xE180_0003, // ORR  R0, R0, R3    (new_lo)
650                    0xEA00_0007, // B    .done
651                    // --- large rotation (n >= 32), R3 = m = n-32 ---
652                    0xE263_2020, // RSB  R2, R3, #32   (R2 = 32-m = 64-n)
653                    0xE1A0_C231, // LSR  R12, R1, R2   (hi >> (64-n))
654                    0xE1A0_2230, // LSR  R2, R0, R2    (lo >> (64-n))
655                    0xE1A0_0310, // LSL  R0, R0, R3    (lo << m)
656                    0xE1A0_1311, // LSL  R1, R1, R3    (hi << m)
657                    0xE180_C00C, // ORR  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
658                    0xE181_0002, // ORR  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
659                    0xE1A0_100C, // MOV  R1, R12       (new_hi into place)
660                ] {
661                    w(&mut b, word);
662                }
663                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
664            }
665            ArmOp::I64Rotr {
666                rdlo,
667                rdhi,
668                rnlo,
669                rnhi,
670                shift,
671            } => {
672                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
673                for word in [
674                    0xE202_203Fu32, // AND  R2, R2, #63   (mask amount mod 64)
675                    0xE252_3020,    // SUBS R3, R2, #32   (R3 = n-32, sets N)
676                    0x5A00_0007,    // BPL  .large        (n >= 32)
677                    // --- small rotation (n < 32) ---
678                    0xE262_3020, // RSB  R3, R2, #32   (R3 = 32-n)
679                    0xE1A0_C311, // LSL  R12, R1, R3   (hi << (32-n))
680                    0xE1A0_3310, // LSL  R3, R0, R3    (lo << (32-n))
681                    0xE1A0_0230, // LSR  R0, R0, R2    (lo >> n)
682                    0xE180_000C, // ORR  R0, R0, R12   (new_lo)
683                    0xE1A0_1231, // LSR  R1, R1, R2    (hi >> n)
684                    0xE181_1003, // ORR  R1, R1, R3    (new_hi)
685                    0xEA00_0007, // B    .done
686                    // --- large rotation (n >= 32), R3 = m = n-32 ---
687                    0xE263_2020, // RSB  R2, R3, #32   (R2 = 32-m = 64-n)
688                    0xE1A0_C210, // LSL  R12, R0, R2   (lo << (64-n))
689                    0xE1A0_2211, // LSL  R2, R1, R2    (hi << (64-n))
690                    0xE1A0_1331, // LSR  R1, R1, R3    (hi >> m)
691                    0xE181_C00C, // ORR  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
692                    0xE1A0_1330, // LSR  R1, R0, R3    (lo >> m)
693                    0xE181_1002, // ORR  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
694                    0xE1A0_000C, // MOV  R0, R12       (new_lo into place)
695                ] {
696                    w(&mut b, word);
697                }
698                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
699            }
700
701            // I64Clz: CLZ(hi), or 32 + CLZ(lo) when hi == 0. Conditional
702            // execution replaces the Thumb branches; like the Thumb arm, the
703            // high word of the result pair (rnhi) is cleared last.
704            ArmOp::I64Clz { rd, rnlo, rnhi } => {
705                let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
706                w(&mut b, 0xE350_0000 | (hi << 16)); //              CMP   rnhi, #0
707                w(&mut b, 0x116F_0F10 | (rd_b << 12) | hi); //       CLZNE rd, rnhi
708                w(&mut b, 0x016F_0F10 | (rd_b << 12) | lo); //       CLZEQ rd, rnlo
709                w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
710                w(&mut b, 0xE3A0_0000 | (hi << 12)); //              MOV   rnhi, #0
711            }
712
713            // I64Ctz: CLZ(RBIT(lo)), or 32 + CLZ(RBIT(hi)) when lo == 0.
714            // RBIT/CLZ leave the flags intact, so the CMP's Z survives to the
715            // conditional ADD.
716            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
717                let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
718                w(&mut b, 0xE350_0000 | (lo << 16)); //              CMP    rnlo, #0
719                w(&mut b, 0x16FF_0F30 | (rd_b << 12) | lo); //       RBITNE rd, rnlo
720                w(&mut b, 0x06FF_0F30 | (rd_b << 12) | hi); //       RBITEQ rd, rnhi
721                w(&mut b, 0xE16F_0F10 | (rd_b << 12) | rd_b); //     CLZ    rd, rd
722                w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
723                w(&mut b, 0xE3A0_0000 | (hi << 12)); //              MOV    rnhi, #0
724            }
725
726            // I64Const: MOVW/MOVT per half (MOVT elided when the half fits in
727            // 16 bits, mirroring the Thumb-2 arm).
728            ArmOp::I64Const { rdlo, rdhi, value } => {
729                let lo32 = *value as u32;
730                let hi32 = (*value >> 32) as u32;
731                movw(&mut b, reg_to_bits(rdlo), lo32 & 0xFFFF);
732                if lo32 > 0xFFFF {
733                    movt(&mut b, reg_to_bits(rdlo), lo32 >> 16);
734                }
735                movw(&mut b, reg_to_bits(rdhi), hi32 & 0xFFFF);
736                if hi32 > 0xFFFF {
737                    movt(&mut b, reg_to_bits(rdhi), hi32 >> 16);
738                }
739            }
740
741            // I64Ldr / I64Str: two word accesses at [base, #off] / #off+4.
742            // A register offset is materialized into IP once (the #206/#372
743            // hazard: dropping it would read the wrong address).
744            ArmOp::I64Ldr { rdlo, rdhi, addr } | ArmOp::I64Str { rdlo, rdhi, addr } => {
745                let base = if let Some(rm) = addr.offset_reg {
746                    // ADD ip, base, rm
747                    w(
748                        &mut b,
749                        0xE080_0000
750                            | (reg_to_bits(&addr.base) << 16)
751                            | (12 << 12)
752                            | reg_to_bits(&rm),
753                    );
754                    12
755                } else {
756                    reg_to_bits(&addr.base)
757                };
758                if addr.offset < 0 || addr.offset > 0xFFB {
759                    return Err(synth_core::Error::synthesis(format!(
760                        "i64 load/store offset {} out of the A32 imm12 range (0..=4091) — materialize the offset into a register",
761                        addr.offset
762                    )));
763                }
764                let off = addr.offset as u32;
765                let opc: u32 = if matches!(op, ArmOp::I64Ldr { .. }) {
766                    0xE590_0000 // LDR
767                } else {
768                    0xE580_0000 // STR
769                };
770                w(&mut b, opc | (base << 16) | (reg_to_bits(rdlo) << 12) | off);
771                w(
772                    &mut b,
773                    opc | (base << 16) | (reg_to_bits(rdhi) << 12) | (off + 4),
774                );
775            }
776
777            // I64ExtendI32S: rdlo = rn; rdhi = rdlo >> 31 (arithmetic).
778            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
779                if rdlo != rn {
780                    w(
781                        &mut b,
782                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
783                    );
784                }
785                w(
786                    &mut b,
787                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
788                );
789            }
790
791            // I64ExtendI32U: rdlo = rn; rdhi = 0.
792            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
793                if rdlo != rn {
794                    w(
795                        &mut b,
796                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
797                    );
798                }
799                w(&mut b, 0xE3A0_0000 | (reg_to_bits(rdhi) << 12));
800            }
801
802            // I64Extend8S / I64Extend16S: SXTB/SXTH then sign-fill the high word.
803            ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
804                w(
805                    &mut b,
806                    0xE6AF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
807                );
808                w(
809                    &mut b,
810                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
811                );
812            }
813            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
814                w(
815                    &mut b,
816                    0xE6BF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
817                );
818                w(
819                    &mut b,
820                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
821                );
822            }
823            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
824                if rdlo != rnlo {
825                    w(
826                        &mut b,
827                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
828                    );
829                }
830                w(
831                    &mut b,
832                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rnlo),
833                );
834            }
835
836            // I32WrapI64: take the low word. When rd == rnlo this is a genuine
837            // no-op (the one case where a NOP word is the correct encoding).
838            ArmOp::I32WrapI64 { rd, rnlo } => {
839                w(
840                    &mut b,
841                    0xE1A0_0000 | (reg_to_bits(rd) << 12) | reg_to_bits(rnlo),
842                );
843            }
844
845            // I64Add / I64Sub: the classic pair — ADDS lo + ADC hi (SUBS/SBC).
846            // The selector emits these as separate Adds/Adc ops; the fused
847            // variants are verification-constructed, but they encode for real.
848            ArmOp::I64Add {
849                rdlo,
850                rdhi,
851                rnlo,
852                rnhi,
853                rmlo,
854                rmhi,
855            } => {
856                dp_reg(
857                    &mut b,
858                    0xE090_0000, // ADDS
859                    reg_to_bits(rdlo),
860                    reg_to_bits(rnlo),
861                    reg_to_bits(rmlo),
862                );
863                dp_reg(
864                    &mut b,
865                    0xE0A0_0000, // ADC
866                    reg_to_bits(rdhi),
867                    reg_to_bits(rnhi),
868                    reg_to_bits(rmhi),
869                );
870            }
871            ArmOp::I64Sub {
872                rdlo,
873                rdhi,
874                rnlo,
875                rnhi,
876                rmlo,
877                rmhi,
878            } => {
879                dp_reg(
880                    &mut b,
881                    0xE050_0000, // SUBS
882                    reg_to_bits(rdlo),
883                    reg_to_bits(rnlo),
884                    reg_to_bits(rmlo),
885                );
886                dp_reg(
887                    &mut b,
888                    0xE0C0_0000, // SBC
889                    reg_to_bits(rdhi),
890                    reg_to_bits(rnhi),
891                    reg_to_bits(rmhi),
892                );
893            }
894
895            // I64And / I64Or / I64Xor: two independent word ops.
896            ArmOp::I64And {
897                rdlo,
898                rdhi,
899                rnlo,
900                rnhi,
901                rmlo,
902                rmhi,
903            }
904            | ArmOp::I64Or {
905                rdlo,
906                rdhi,
907                rnlo,
908                rnhi,
909                rmlo,
910                rmhi,
911            }
912            | ArmOp::I64Xor {
913                rdlo,
914                rdhi,
915                rnlo,
916                rnhi,
917                rmlo,
918                rmhi,
919            } => {
920                let base = match op {
921                    ArmOp::I64And { .. } => 0xE000_0000, // AND
922                    ArmOp::I64Or { .. } => 0xE180_0000,  // ORR
923                    _ => 0xE020_0000,                    // EOR
924                };
925                dp_reg(
926                    &mut b,
927                    base,
928                    reg_to_bits(rdlo),
929                    reg_to_bits(rnlo),
930                    reg_to_bits(rmlo),
931                );
932                dp_reg(
933                    &mut b,
934                    base,
935                    reg_to_bits(rdhi),
936                    reg_to_bits(rnhi),
937                    reg_to_bits(rmhi),
938                );
939            }
940
941            // I64DivU: binary long division — A32 transcription of the Thumb-2
942            // #610/#613 arm (fixed-ABI marshal, zero-divisor trap, 64-round
943            // shift-subtract core, quotient to R0:R1, result to rd pair).
944            ArmOp::I64DivU {
945                rdlo,
946                rdhi,
947                rnlo,
948                rnhi,
949                rmlo,
950                rmhi,
951            } => {
952                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
953                emit_a32_i64_divisor_zero_trap(&mut b);
954                w(&mut b, 0xE92D_00F0); // PUSH {R4-R7}
955                for r in 4..8u32 {
956                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
957                }
958                div_loop(&mut b, 12); // counter in R12 (encoder scratch)
959                w(&mut b, 0xE1A0_0004); // MOV R0, R4 (quotient lo)
960                w(&mut b, 0xE1A0_1005); // MOV R1, R5 (quotient hi)
961                w(&mut b, 0xE8BD_00F0); // POP {R4-R7}
962                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
963            }
964
965            // I64DivS: sign-extract, unsigned core, conditional negate —
966            // A32 transcription of the Thumb-2 arm.
967            ArmOp::I64DivS {
968                rdlo,
969                rdhi,
970                rnlo,
971                rnhi,
972                rmlo,
973                rmhi,
974            } => {
975                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
976                emit_a32_i64_divisor_zero_trap(&mut b);
977                // #633: INT64_MIN / -1 overflows — trap like the i32 path
978                // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
979                emit_a32_i64_divs_overflow_trap(&mut b);
980                w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
981                w(&mut b, 0xE021_9003); // EOR R9, R1, R3 (result sign in MSB)
982                skip_negate_if_positive(&mut b, 1);
983                negate64(&mut b, 0, 1);
984                skip_negate_if_positive(&mut b, 3);
985                negate64(&mut b, 2, 3);
986                for r in 4..8u32 {
987                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
988                }
989                div_loop(&mut b, 8); // counter in R8 (saved above)
990                w(&mut b, 0xE1A0_0004); // MOV R0, R4
991                w(&mut b, 0xE1A0_1005); // MOV R1, R5
992                skip_negate_if_positive(&mut b, 9);
993                negate64(&mut b, 0, 1);
994                w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
995                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
996            }
997
998            // I64RemU: same core as I64DivU, returns the remainder (R6:R7).
999            ArmOp::I64RemU {
1000                rdlo,
1001                rdhi,
1002                rnlo,
1003                rnhi,
1004                rmlo,
1005                rmhi,
1006            } => {
1007                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1008                emit_a32_i64_divisor_zero_trap(&mut b);
1009                w(&mut b, 0xE92D_01F0); // PUSH {R4-R8}
1010                for r in 4..8u32 {
1011                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1012                }
1013                div_loop(&mut b, 8);
1014                w(&mut b, 0xE1A0_0006); // MOV R0, R6 (remainder lo)
1015                w(&mut b, 0xE1A0_1007); // MOV R1, R7 (remainder hi)
1016                w(&mut b, 0xE8BD_01F0); // POP {R4-R8}
1017                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1018            }
1019
1020            // I64RemS: remainder takes the DIVIDEND's sign (WASM semantics).
1021            ArmOp::I64RemS {
1022                rdlo,
1023                rdhi,
1024                rnlo,
1025                rnhi,
1026                rmlo,
1027                rmhi,
1028            } => {
1029                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1030                emit_a32_i64_divisor_zero_trap(&mut b);
1031                w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1032                w(&mut b, 0xE1A0_9001); // MOV R9, R1 (dividend sign)
1033                skip_negate_if_positive(&mut b, 1);
1034                negate64(&mut b, 0, 1);
1035                skip_negate_if_positive(&mut b, 3);
1036                negate64(&mut b, 2, 3);
1037                for r in 4..8u32 {
1038                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1039                }
1040                div_loop(&mut b, 8);
1041                w(&mut b, 0xE1A0_0006); // MOV R0, R6
1042                w(&mut b, 0xE1A0_1007); // MOV R1, R7
1043                skip_negate_if_positive(&mut b, 9);
1044                negate64(&mut b, 0, 1);
1045                w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1046                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1047            }
1048
1049            // Popcnt (i32): bit-twiddle expansion (no native A32 popcount),
1050            // mirroring the Thumb-2 arm's register contract (R11 + R12 as
1051            // scratch, shift-add fold, final AND #0x3F).
1052            ArmOp::Popcnt { rd, rm } => {
1053                let rd_b = reg_to_bits(rd);
1054                if rd != rm {
1055                    w(&mut b, 0xE1A0_0000 | (rd_b << 12) | reg_to_bits(rm)); // MOV rd, rm
1056                }
1057                // x = x - ((x >> 1) & 0x55555555)
1058                movw(&mut b, 12, 0x5555);
1059                movt(&mut b, 12, 0x5555);
1060                shift_imm(&mut b, LSR, 11, rd_b, 1);
1061                dp_reg(&mut b, 0xE000_0000, 11, 11, 12); // AND R11, R11, R12
1062                dp_reg(&mut b, 0xE040_0000, rd_b, rd_b, 11); // SUB rd, rd, R11
1063                // x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
1064                movw(&mut b, 12, 0x3333);
1065                movt(&mut b, 12, 0x3333);
1066                dp_reg(&mut b, 0xE000_0000, 11, rd_b, 12); // AND R11, rd, R12
1067                shift_imm(&mut b, LSR, rd_b, rd_b, 2);
1068                dp_reg(&mut b, 0xE000_0000, rd_b, rd_b, 12); // AND rd, rd, R12
1069                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11); // ADD rd, rd, R11
1070                // x = (x + (x >> 4)) & 0x0F0F0F0F
1071                shift_imm(&mut b, LSR, 11, rd_b, 4);
1072                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11); // ADD rd, rd, R11
1073                movw(&mut b, 12, 0x0F0F);
1074                movt(&mut b, 12, 0x0F0F);
1075                dp_reg(&mut b, 0xE000_0000, rd_b, rd_b, 12); // AND rd, rd, R12
1076                // x += x >> 8; x += x >> 16; x &= 0x3F
1077                shift_imm(&mut b, LSR, 11, rd_b, 8);
1078                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11);
1079                shift_imm(&mut b, LSR, 11, rd_b, 16);
1080                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11);
1081                w(&mut b, 0xE200_003F | (rd_b << 16) | (rd_b << 12)); // AND rd, rd, #63
1082            }
1083
1084            // I64Popcnt: POPCNT(lo) + POPCNT(hi) — A32 transcription of the
1085            // Thumb-2 arm (R3/R4/R5 saved, mul-based per-word fold, high
1086            // result word rnhi cleared last, mirroring the Thumb contract).
1087            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
1088                let hi = reg_to_bits(rnhi);
1089                w(&mut b, 0xE92D_0038); // PUSH {R3, R4, R5}
1090                // #632 audit: route rnlo through R12 so a pair living at
1091                // (R3,R4) cannot read a clobbered R4 (sources read before any
1092                // scratch register they could occupy is written).
1093                w(&mut b, 0xE1A0_C000 | reg_to_bits(rnlo)); // MOV R12, rnlo
1094                w(&mut b, 0xE1A0_5000 | hi); //                MOV R5, rnhi
1095                w(&mut b, 0xE1A0_400C); //                     MOV R4, R12
1096                popcnt_word(&mut b, 4, 3);
1097                popcnt_word(&mut b, 5, 3);
1098                // #632: carry the count across the scratch restore in R12 —
1099                // rd is allocator-assigned and can land inside {R3,R4,R5};
1100                // the old `ADD rd, R4, R5` before the POP was destroyed by
1101                // the restore. R12 is never allocatable and never restored.
1102                dp_reg(&mut b, 0xE080_0000, 12, 4, 5); // ADD R12, R4, R5
1103                w(&mut b, 0xE8BD_0038); // POP {R3, R4, R5}
1104                w(&mut b, 0xE1A0_0000 | (reg_to_bits(rd) << 12) | 12); // MOV rd, R12
1105                w(&mut b, 0xE3A0_0000 | (hi << 12)); // MOV rnhi, #0 (i64 hi word)
1106            }
1107
1108            _ => return Ok(None),
1109        }
1110        Ok(Some(b))
1111    }
1112
1113    fn encode_arm(&self, op: &ArmOp) -> Result<Vec<u8>> {
1114        // #615: A32 multi-instruction expansions (i64 arithmetic/shift/rotate/
1115        // compare, SetCond/SelectMove, popcnt, ...). These ops were literal
1116        // NOPs on the A32 path — user-reachable via `--target cortex-r5` —
1117        // so the value silently vanished. Mirror of the #594 CallIndirect
1118        // early-return: if the expansion helper covers the op, its bytes are
1119        // the encoding.
1120        if let Some(bytes) = self.encode_arm_expanded(op)? {
1121            return Ok(bytes);
1122        }
1123        // #206: ARM32 register-offset loads/stores. `encode_mem_addr` only
1124        // returns the 12-bit immediate, so the immediate-form arms below
1125        // silently DROP `addr.offset_reg` — a runtime address index vanished,
1126        // turning `ldr rd,[rn,rm,#off]` into `ldr rd,[rn,#off]` (the access went
1127        // to the wrong address). Compute the effective base into IP and re-encode
1128        // against `[ip, #off]`, which is uniform for word/byte/halfword/signed.
1129        if let Some(bytes) = self.encode_arm_reg_offset_mem(op)? {
1130            return Ok(bytes);
1131        }
1132        // #594: call_indirect was encoded as a literal NOP on the A32 path
1133        // (`--target cortex-r5`) — the call never happened and the function
1134        // silently returned garbage. Emit the same three-instruction expansion
1135        // as the Thumb-2 path (R11 = function-pointer table base, R12 scratch):
1136        //   MOV r12, idx, LSL #2 ; LDR r12, [r11, r12] ; BLX r12
1137        if let ArmOp::CallIndirect {
1138            table_index_reg, ..
1139        } = op
1140        {
1141            return Ok(Self::encode_arm_call_indirect(table_index_reg));
1142        }
1143        let instr: u32 = match op {
1144            // Data processing instructions
1145            ArmOp::Add { rd, rn, op2 } => {
1146                let rd_bits = reg_to_bits(rd);
1147                let rn_bits = reg_to_bits(rn);
1148                let (op2_bits, i_flag) = encode_operand2(op2)?;
1149
1150                // ADD encoding: cond(4) | 00 | I(1) | 0100 | S(1) | Rn(4) | Rd(4) | operand2(12)
1151                0xE0800000 // condition=always(E), opcode=ADD(0100), S=0
1152                    | (i_flag << 25)
1153                    | (rn_bits << 16)
1154                    | (rd_bits << 12)
1155                    | op2_bits
1156            }
1157
1158            ArmOp::Sub { rd, rn, op2 } => {
1159                let rd_bits = reg_to_bits(rd);
1160                let rn_bits = reg_to_bits(rn);
1161                let (op2_bits, i_flag) = encode_operand2(op2)?;
1162
1163                // SUB encoding: opcode=0010
1164                0xE0400000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1165            }
1166
1167            // i64 support: ADDS, ADC, SUBS, SBC for ARM32
1168            ArmOp::Adds { rd, rn, op2 } => {
1169                let rd_bits = reg_to_bits(rd);
1170                let rn_bits = reg_to_bits(rn);
1171                let (op2_bits, i_flag) = encode_operand2(op2)?;
1172
1173                // ADDS encoding: opcode=0100, S=1
1174                0xE0900000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1175            }
1176
1177            ArmOp::Adc { rd, rn, op2 } => {
1178                let rd_bits = reg_to_bits(rd);
1179                let rn_bits = reg_to_bits(rn);
1180                let (op2_bits, i_flag) = encode_operand2(op2)?;
1181
1182                // ADC encoding: opcode=0101
1183                0xE0A00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1184            }
1185
1186            ArmOp::Subs { rd, rn, op2 } => {
1187                let rd_bits = reg_to_bits(rd);
1188                let rn_bits = reg_to_bits(rn);
1189                let (op2_bits, i_flag) = encode_operand2(op2)?;
1190
1191                // SUBS encoding: opcode=0010, S=1
1192                0xE0500000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1193            }
1194
1195            ArmOp::Sbc { rd, rn, op2 } => {
1196                let rd_bits = reg_to_bits(rd);
1197                let rn_bits = reg_to_bits(rn);
1198                let (op2_bits, i_flag) = encode_operand2(op2)?;
1199
1200                // SBC encoding: opcode=0110
1201                0xE0C00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1202            }
1203
1204            ArmOp::Mul { rd, rn, rm } => {
1205                let rd_bits = reg_to_bits(rd);
1206                let rn_bits = reg_to_bits(rn);
1207                let rm_bits = reg_to_bits(rm);
1208
1209                // MUL encoding: cond(4) | 000000 | A(1) | S(1) | Rd(4) | Rn(4) | Rs(4) | 1001 | Rm(4)
1210                0xE0000090 | (rd_bits << 16) | (rn_bits << 8) | rm_bits
1211            }
1212
1213            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
1214                let rdlo_bits = reg_to_bits(rdlo);
1215                let rdhi_bits = reg_to_bits(rdhi);
1216                let rn_bits = reg_to_bits(rn);
1217                let rm_bits = reg_to_bits(rm);
1218
1219                // UMULL encoding: cond(4) | 0000 1000 | RdHi(4) | RdLo(4) | Rm(4) | 1001 | Rn(4)
1220                0xE0800090 | (rdhi_bits << 16) | (rdlo_bits << 12) | (rm_bits << 8) | rn_bits
1221            }
1222
1223            ArmOp::Sdiv { rd, rn, rm } => {
1224                let rd_bits = reg_to_bits(rd);
1225                let rn_bits = reg_to_bits(rn);
1226                let rm_bits = reg_to_bits(rm);
1227
1228                // SDIV encoding: cond(4) | 01110001 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1229                // ARMv7-M and above
1230                0xE710F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1231            }
1232
1233            ArmOp::Udiv { rd, rn, rm } => {
1234                let rd_bits = reg_to_bits(rd);
1235                let rn_bits = reg_to_bits(rn);
1236                let rm_bits = reg_to_bits(rm);
1237
1238                // UDIV encoding: cond(4) | 01110011 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1239                // ARMv7-M and above
1240                0xE730F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1241            }
1242
1243            ArmOp::Mls { rd, rn, rm, ra } => {
1244                let rd_bits = reg_to_bits(rd);
1245                let rn_bits = reg_to_bits(rn);
1246                let rm_bits = reg_to_bits(rm);
1247                let ra_bits = reg_to_bits(ra);
1248
1249                // MLS encoding: cond(4) | 00000110 | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1250                // Rd = Ra - (Rn * Rm)
1251                0xE0600090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1252            }
1253
1254            ArmOp::Mla { rd, rn, rm, ra } => {
1255                let rd_bits = reg_to_bits(rd);
1256                let rn_bits = reg_to_bits(rn);
1257                let rm_bits = reg_to_bits(rm);
1258                let ra_bits = reg_to_bits(ra);
1259
1260                // MLA encoding: cond(4) | 0000001 S | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1261                // Rd = Ra + (Rn * Rm). Base 0xE0200090 (S=0).
1262                0xE0200090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1263            }
1264
1265            ArmOp::And { rd, rn, op2 } => {
1266                let rd_bits = reg_to_bits(rd);
1267                let rn_bits = reg_to_bits(rn);
1268                let (op2_bits, i_flag) = encode_operand2(op2)?;
1269
1270                // AND encoding: opcode=0000
1271                0xE0000000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1272            }
1273
1274            ArmOp::Orr { rd, rn, op2 } => {
1275                let rd_bits = reg_to_bits(rd);
1276                let rn_bits = reg_to_bits(rn);
1277                let (op2_bits, i_flag) = encode_operand2(op2)?;
1278
1279                // ORR encoding: opcode=1100
1280                0xE1800000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1281            }
1282
1283            ArmOp::Eor { rd, rn, op2 } => {
1284                let rd_bits = reg_to_bits(rd);
1285                let rn_bits = reg_to_bits(rn);
1286                let (op2_bits, i_flag) = encode_operand2(op2)?;
1287
1288                // EOR encoding: opcode=0001
1289                0xE0200000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1290            }
1291
1292            // Shift instructions
1293            ArmOp::Lsl { rd, rn, shift } => {
1294                let rd_bits = reg_to_bits(rd);
1295                let rn_bits = reg_to_bits(rn);
1296                let shift_bits = *shift & 0x1F;
1297
1298                // LSL encoding: MOV with shift
1299                0xE1A00000 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1300            }
1301
1302            ArmOp::Lsr { rd, rn, shift } => {
1303                let rd_bits = reg_to_bits(rd);
1304                let rn_bits = reg_to_bits(rn);
1305                let shift_bits = *shift & 0x1F;
1306
1307                // LSR encoding
1308                0xE1A00020 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1309            }
1310
1311            ArmOp::Asr { rd, rn, shift } => {
1312                let rd_bits = reg_to_bits(rd);
1313                let rn_bits = reg_to_bits(rn);
1314                let shift_bits = *shift & 0x1F;
1315
1316                // ASR encoding
1317                0xE1A00040 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1318            }
1319
1320            ArmOp::Ror { rd, rn, shift } => {
1321                let rd_bits = reg_to_bits(rd);
1322                let rn_bits = reg_to_bits(rn);
1323                let shift_bits = *shift & 0x1F;
1324
1325                // ROR encoding: MOV with ROR shift
1326                0xE1A00060 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1327            }
1328
1329            // Register-based shifts (ARM32)
1330            // LSL Rd, Rn, Rm: cond 0001101S 0000 Rd Rs 0001 Rn
1331            ArmOp::LslReg { rd, rn, rm } => {
1332                let rd_bits = reg_to_bits(rd);
1333                let rn_bits = reg_to_bits(rn);
1334                let rm_bits = reg_to_bits(rm);
1335                0xE1A00010 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1336            }
1337            ArmOp::LsrReg { rd, rn, rm } => {
1338                let rd_bits = reg_to_bits(rd);
1339                let rn_bits = reg_to_bits(rn);
1340                let rm_bits = reg_to_bits(rm);
1341                0xE1A00030 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1342            }
1343            ArmOp::AsrReg { rd, rn, rm } => {
1344                let rd_bits = reg_to_bits(rd);
1345                let rn_bits = reg_to_bits(rn);
1346                let rm_bits = reg_to_bits(rm);
1347                0xE1A00050 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1348            }
1349            ArmOp::RorReg { rd, rn, rm } => {
1350                let rd_bits = reg_to_bits(rd);
1351                let rn_bits = reg_to_bits(rn);
1352                let rm_bits = reg_to_bits(rm);
1353                0xE1A00070 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1354            }
1355
1356            // RSB (Reverse Subtract): Rd = imm - Rn
1357            ArmOp::Rsb { rd, rn, imm } => {
1358                let rd_bits = reg_to_bits(rd);
1359                let rn_bits = reg_to_bits(rn);
1360                // RSB encoding: cond(4) | 00 1 0011 S | Rn(4) | Rd(4) | imm12
1361                // Opcode for RSB = 0011, I=1 (immediate), S=0
1362                0xE2600000 | (rn_bits << 16) | (rd_bits << 12) | (*imm & 0xFF)
1363            }
1364
1365            // Bit manipulation instructions
1366            ArmOp::Clz { rd, rm } => {
1367                let rd_bits = reg_to_bits(rd);
1368                let rm_bits = reg_to_bits(rm);
1369
1370                // CLZ encoding: cond(4) | 00010110 | 1111 | Rd(4) | 1111 | 0001 | Rm(4)
1371                // ARMv5T and above
1372                0xE16F0F10 | (rd_bits << 12) | rm_bits
1373            }
1374
1375            ArmOp::Rbit { rd, rm } => {
1376                let rd_bits = reg_to_bits(rd);
1377                let rm_bits = reg_to_bits(rm);
1378
1379                // RBIT encoding: cond(4) | 01101111 | 1111 | Rd(4) | 1111 | 0011 | Rm(4)
1380                // ARMv6T2 and above
1381                0xE6FF0F30 | (rd_bits << 12) | rm_bits
1382            }
1383
1384            ArmOp::Sxtb { rd, rm } => {
1385                let rd_bits = reg_to_bits(rd);
1386                let rm_bits = reg_to_bits(rm);
1387
1388                // SXTB encoding: cond(4) | 01101010 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1389                // ARMv6 and above. rotate=00 for no rotation
1390                0xE6AF0070 | (rd_bits << 12) | rm_bits
1391            }
1392
1393            ArmOp::Sxth { rd, rm } => {
1394                let rd_bits = reg_to_bits(rd);
1395                let rm_bits = reg_to_bits(rm);
1396
1397                // SXTH encoding: cond(4) | 01101011 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1398                // ARMv6 and above. rotate=00 for no rotation
1399                0xE6BF0070 | (rd_bits << 12) | rm_bits
1400            }
1401
1402            ArmOp::Uxtb { rd, rm } => {
1403                let rd_bits = reg_to_bits(rd);
1404                let rm_bits = reg_to_bits(rm);
1405                // UXTB encoding: cond | 01101110 1111 Rd rotate 00 0111 Rm (rotate=00)
1406                0xE6EF0070 | (rd_bits << 12) | rm_bits
1407            }
1408
1409            ArmOp::Uxth { rd, rm } => {
1410                let rd_bits = reg_to_bits(rd);
1411                let rm_bits = reg_to_bits(rm);
1412                // UXTH encoding: cond | 01101111 1111 Rd rotate 00 0111 Rm (rotate=00)
1413                0xE6FF0070 | (rd_bits << 12) | rm_bits
1414            }
1415
1416            // Move instructions
1417            ArmOp::Mov { rd, op2 } => {
1418                let rd_bits = reg_to_bits(rd);
1419                let (op2_bits, i_flag) = encode_operand2(op2)?;
1420
1421                // MOV encoding: opcode=1101
1422                0xE1A00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1423            }
1424
1425            ArmOp::Mvn { rd, op2 } => {
1426                let rd_bits = reg_to_bits(rd);
1427                let (op2_bits, i_flag) = encode_operand2(op2)?;
1428
1429                // MVN encoding: opcode=1111
1430                0xE1E00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1431            }
1432
1433            // MOVW - Move Wide (ARM32)
1434            // Encoding: cond(4) | 0011 0000 | imm4(4) | Rd(4) | imm12(12)
1435            ArmOp::Movw { rd, imm16 } => {
1436                let rd_bits = reg_to_bits(rd);
1437                let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1438                let imm12 = (*imm16 as u32) & 0xFFF;
1439                0xE3000000 | (imm4 << 16) | (rd_bits << 12) | imm12
1440            }
1441
1442            // MOVT - Move Top (ARM32)
1443            // Encoding: cond(4) | 0011 0100 | imm4(4) | Rd(4) | imm12(12)
1444            ArmOp::Movt { rd, imm16 } => {
1445                let rd_bits = reg_to_bits(rd);
1446                let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1447                let imm12 = (*imm16 as u32) & 0xFFF;
1448                0xE3400000 | (imm4 << 16) | (rd_bits << 12) | imm12
1449            }
1450
1451            // #237: symbol-relative MOVW/MOVT (ARM mode) — addend in place, the
1452            // backend records the MOVW_ABS/MOVT_ABS relocation against `symbol`.
1453            ArmOp::MovwSym { rd, addend, .. } => {
1454                let rd_bits = reg_to_bits(rd);
1455                let v = (*addend as u32) & 0xffff;
1456                0xE3000000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1457            }
1458            ArmOp::MovtSym { rd, addend, .. } => {
1459                let rd_bits = reg_to_bits(rd);
1460                let v = ((*addend as u32) >> 16) & 0xffff;
1461                0xE3400000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1462            }
1463
1464            // #345: LdrSym is the Thumb-2 literal-pool address load. A32 mode is
1465            // not used for relocatable native-pointer objects; fail loudly rather
1466            // than miscompile if it is ever reached here.
1467            ArmOp::LdrSym { .. } => {
1468                return Err(synth_core::Error::synthesis(
1469                    "LdrSym (literal-pool address load) is Thumb-2-only",
1470                ));
1471            }
1472
1473            // Compare
1474            ArmOp::Cmp { rn, op2 } => {
1475                let rn_bits = reg_to_bits(rn);
1476                let (op2_bits, i_flag) = encode_operand2(op2)?;
1477
1478                // CMP encoding: opcode=1010, S=1
1479                0xE1500000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1480            }
1481
1482            // Compare Negative (CMN) - computes Rn + op2 and sets flags
1483            ArmOp::Cmn { rn, op2 } => {
1484                let rn_bits = reg_to_bits(rn);
1485                let (op2_bits, i_flag) = encode_operand2(op2)?;
1486
1487                // CMN encoding: opcode=1011, S=1
1488                0xE1700000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1489            }
1490
1491            // Load/Store
1492            ArmOp::Ldr { rd, addr } => {
1493                let rd_bits = reg_to_bits(rd);
1494                let (base_bits, offset_bits) = encode_mem_addr(addr);
1495
1496                // LDR encoding: cond(4) | 01 | I(1) | P(1) | U(1) | B(1) | W(1) | L(1) | Rn(4) | Rd(4) | offset(12)
1497                // P=1 (pre-indexed), U=1 (add offset), L=1 (load)
1498                0xE5900000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1499            }
1500
1501            ArmOp::Str { rd, addr } => {
1502                let rd_bits = reg_to_bits(rd);
1503                let (base_bits, offset_bits) = encode_mem_addr(addr);
1504
1505                // STR encoding: L=0 (store)
1506                0xE5800000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1507            }
1508
1509            // Sub-word loads (ARM32 encoding)
1510            ArmOp::Ldrb { rd, addr } => {
1511                let rd_bits = reg_to_bits(rd);
1512                let (base_bits, offset_bits) = encode_mem_addr(addr);
1513                // LDRB: LDR with B=1 (byte): cond|01|I|P|U|1|W|L|Rn|Rd|offset
1514                0xE5D00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1515            }
1516
1517            ArmOp::Ldrsb { rd, addr } => {
1518                let rd_bits = reg_to_bits(rd);
1519                let (base_bits, offset_bits) = encode_mem_addr(addr);
1520                // LDRSB (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1101|imm4L
1521                // Simplified with immediate offset
1522                let offset_val = offset_bits & 0xFF;
1523                let imm4h = (offset_val >> 4) & 0xF;
1524                let imm4l = offset_val & 0xF;
1525                0xE1D000D0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1526            }
1527
1528            ArmOp::Ldrh { rd, addr } => {
1529                let rd_bits = reg_to_bits(rd);
1530                let (base_bits, offset_bits) = encode_mem_addr(addr);
1531                // LDRH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1011|imm4L
1532                let offset_val = offset_bits & 0xFF;
1533                let imm4h = (offset_val >> 4) & 0xF;
1534                let imm4l = offset_val & 0xF;
1535                0xE1D000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1536            }
1537
1538            ArmOp::Ldrsh { rd, addr } => {
1539                let rd_bits = reg_to_bits(rd);
1540                let (base_bits, offset_bits) = encode_mem_addr(addr);
1541                // LDRSH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1111|imm4L
1542                let offset_val = offset_bits & 0xFF;
1543                let imm4h = (offset_val >> 4) & 0xF;
1544                let imm4l = offset_val & 0xF;
1545                0xE1D000F0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1546            }
1547
1548            // Sub-word stores (ARM32 encoding)
1549            ArmOp::Strb { rd, addr } => {
1550                let rd_bits = reg_to_bits(rd);
1551                let (base_bits, offset_bits) = encode_mem_addr(addr);
1552                // STRB: STR with B=1 (byte): cond|01|I|P|U|1|W|0|Rn|Rd|offset
1553                0xE5C00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1554            }
1555
1556            ArmOp::Strh { rd, addr } => {
1557                let rd_bits = reg_to_bits(rd);
1558                let (base_bits, offset_bits) = encode_mem_addr(addr);
1559                // STRH (misc store): cond|000|P|U|1|W|0|Rn|Rd|imm4H|1011|imm4L
1560                let offset_val = offset_bits & 0xFF;
1561                let imm4h = (offset_val >> 4) & 0xF;
1562                let imm4l = offset_val & 0xF;
1563                0xE1C000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1564            }
1565
1566            // Memory management (ARM32 encoding)
1567            ArmOp::MemorySize { rd } => {
1568                let rd_bits = reg_to_bits(rd);
1569                // MOV rd, R10, LSR #16  (memory size in bytes / 65536 = pages)
1570                // cond|000|1101|S|0000|Rd|shift5|type|0|Rm
1571                // LSR #16: shift5=10000, type=01
1572                0xE1A00820 | (rd_bits << 12) | 0x0A // Rm=R10, shift=16, LSR
1573            }
1574
1575            ArmOp::MemoryGrow { rd, .. } => {
1576                let rd_bits = reg_to_bits(rd);
1577                // On embedded, always fail: MOV rd, #-1
1578                0xE3E00000 | (rd_bits << 12) // MVN rd, #0 = MOV rd, #-1
1579            }
1580
1581            // Label pseudo-instruction: emits no machine code
1582            ArmOp::Label { .. } => {
1583                return Ok(Vec::new());
1584            }
1585
1586            // Branch instructions
1587            ArmOp::B { label: _ } => {
1588                // B encoding: cond(4) | 1010 | offset(24)
1589                // Simplified: branch to offset 0 (will be patched by linker/resolver)
1590                0xEA000000
1591            }
1592
1593            // Conditional branch to label (generic)
1594            ArmOp::Bcc { cond, label: _ } => {
1595                use synth_synthesis::Condition;
1596                let cond_bits: u32 = match cond {
1597                    Condition::EQ => 0x0,
1598                    Condition::NE => 0x1,
1599                    Condition::HS => 0x2,
1600                    Condition::LO => 0x3,
1601                    Condition::HI => 0x8,
1602                    Condition::LS => 0x9,
1603                    Condition::GE => 0xA,
1604                    Condition::LT => 0xB,
1605                    Condition::GT => 0xC,
1606                    Condition::LE => 0xD,
1607                };
1608                // B<cond> with offset 0 (will be patched)
1609                (cond_bits << 28) | 0x0A000000
1610            }
1611
1612            // BHS (Branch if Higher or Same) - used for bounds checking
1613            ArmOp::Bhs { label: _ } => {
1614                // BHS encoding: cond(2=HS) | 1010 | offset(24)
1615                0x2A000000 // BHS with offset 0
1616            }
1617
1618            // BLO (Branch if Lower) - complementary to BHS
1619            ArmOp::Blo { label: _ } => {
1620                // BLO encoding: cond(3=LO) | 1010 | offset(24)
1621                0x3A000000 // BLO with offset 0
1622            }
1623
1624            // Branch with numeric offset (in instructions)
1625            // ARM32 B instruction: offset is in instructions, stored as words
1626            // The offset is relative to PC+8 (due to ARM pipeline)
1627            ArmOp::BOffset { offset } => {
1628                // B encoding: cond(4) | 1010 | offset(24)
1629                // Offset is signed, in words (4-byte units)
1630                // ARM adds PC+8 to the offset, so we need to adjust:
1631                // target = PC + 8 + (offset * 4)
1632                // For backward branch of N instructions: offset = -(N + 2)
1633                // wrapping_sub keeps the encoder total under fuzzing (#186): an
1634                // extreme i32::MIN offset would otherwise overflow-panic; for any
1635                // real branch offset this is identical to `- 2`.
1636                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1637                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1638                0xEA000000 | offset_bits
1639            }
1640
1641            // Conditional branch with numeric offset
1642            ArmOp::BCondOffset { cond, offset } => {
1643                use synth_synthesis::Condition;
1644                let cond_bits: u32 = match cond {
1645                    Condition::EQ => 0x0,
1646                    Condition::NE => 0x1,
1647                    Condition::HS => 0x2,
1648                    Condition::LO => 0x3,
1649                    Condition::HI => 0x8,
1650                    Condition::LS => 0x9,
1651                    Condition::GE => 0xA,
1652                    Condition::LT => 0xB,
1653                    Condition::GT => 0xC,
1654                    Condition::LE => 0xD,
1655                };
1656                // B<cond> encoding: cond(4) | 1010 | offset(24)
1657                // wrapping_sub: total under fuzzing (#186), identical for real offsets.
1658                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1659                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1660                (cond_bits << 28) | 0x0A000000 | offset_bits
1661            }
1662
1663            ArmOp::Bl { label: _ } => {
1664                // BL encoding: cond(4) | 1011 | offset(24)
1665                0xEB000000
1666            }
1667
1668            ArmOp::Bx { rm } => {
1669                let rm_bits = reg_to_bits(rm);
1670
1671                // BX encoding: cond(4) | 000100101111111111110001 | Rm(4)
1672                0xE12FFF10 | rm_bits
1673            }
1674
1675            ArmOp::Blx { rm } => {
1676                let rm_bits = reg_to_bits(rm);
1677
1678                // BLX (register) encoding: cond(4) | 000100101111111111110011 | Rm(4)
1679                0xE12FFF30 | rm_bits
1680            }
1681
1682            ArmOp::Push { regs } => {
1683                // STMDB SP!, {regs} encoding: cond(4) | 100100 | 10 | 1101 | register_list(16)
1684                let mut reg_list: u32 = 0;
1685                for r in regs {
1686                    reg_list |= 1 << reg_to_bits(r);
1687                }
1688                0xE92D0000 | reg_list
1689            }
1690
1691            ArmOp::Pop { regs } => {
1692                // LDMIA SP!, {regs} encoding: cond(4) | 100010 | 11 | 1101 | register_list(16)
1693                let mut reg_list: u32 = 0;
1694                for r in regs {
1695                    reg_list |= 1 << reg_to_bits(r);
1696                }
1697                0xE8BD0000 | reg_list
1698            }
1699
1700            ArmOp::Nop => {
1701                // NOP encoding: MOV R0, R0
1702                0xE1A00000
1703            }
1704
1705            ArmOp::Udf { imm } => {
1706                // UDF (Undefined) encoding in ARM: 0xE7F000F0 | (imm12_hi << 8) | imm4_lo
1707                // We only use imm8, so split into imm4_hi and imm4_lo
1708                let imm8 = *imm as u32;
1709                0xE7F000F0 | ((imm8 & 0xF0) << 4) | (imm8 & 0x0F)
1710            }
1711
1712            // #615: handled by the `encode_arm_expanded` early return at the
1713            // top of this function — a real MOV{cond}/MOV pair now, never a
1714            // silent NOP again.
1715            ArmOp::Popcnt { .. } | ArmOp::SetCond { .. } | ArmOp::SelectMove { .. } => {
1716                unreachable!("handled by encode_arm_expanded (#615)")
1717            }
1718
1719            // Verification-only pseudo-ops: `synth-verify`'s ArmSemantics
1720            // models these, but NO codegen path constructs them (the selector
1721            // lowers select/locals/globals/br_table/call to real instruction
1722            // sequences before the encoder). Encoding one as a NOP silently
1723            // dropped the operation (#615 class); a typed Err keeps the
1724            // encoder total (Ok-or-Err, the `encoder_no_panic` contract)
1725            // while making any future reachability LOUD.
1726            ArmOp::Select { .. }
1727            | ArmOp::LocalGet { .. }
1728            | ArmOp::LocalSet { .. }
1729            | ArmOp::LocalTee { .. }
1730            | ArmOp::GlobalGet { .. }
1731            | ArmOp::GlobalSet { .. }
1732            | ArmOp::BrTable { .. }
1733            | ArmOp::Call { .. } => {
1734                return Err(synth_core::Error::synthesis(format!(
1735                    "verification-only pseudo-op {op:?} reached the A32 encoder — \
1736                     codegen lowers it before encoding; refusing to emit a silent NOP (#615)"
1737                )));
1738            }
1739
1740            // #594: CallIndirect is expanded to a real multi-instruction
1741            // sequence by the early return at the top of this function —
1742            // it must NEVER fall through to a silent NOP again.
1743            ArmOp::CallIndirect { .. } => {
1744                unreachable!("CallIndirect handled by encode_arm_call_indirect (#594)")
1745            }
1746
1747            // #615: every i64 op (and I32WrapI64) is expanded to a real A32
1748            // multi-instruction sequence by `encode_arm_expanded` — the
1749            // "encode as NOP for now" era ended with the value silently
1750            // vanishing on `--target cortex-r5`.
1751            ArmOp::I64Add { .. }
1752            | ArmOp::I64Sub { .. }
1753            | ArmOp::I64DivS { .. }
1754            | ArmOp::I64DivU { .. }
1755            | ArmOp::I64RemS { .. }
1756            | ArmOp::I64RemU { .. }
1757            | ArmOp::I64Clz { .. }
1758            | ArmOp::I64Ctz { .. }
1759            | ArmOp::I64Popcnt { .. }
1760            | ArmOp::I64And { .. }
1761            | ArmOp::I64Or { .. }
1762            | ArmOp::I64Xor { .. }
1763            | ArmOp::I64Eqz { .. }
1764            | ArmOp::I64Eq { .. }
1765            | ArmOp::I64Ne { .. }
1766            | ArmOp::I64LtS { .. }
1767            | ArmOp::I64LtU { .. }
1768            | ArmOp::I64LeS { .. }
1769            | ArmOp::I64LeU { .. }
1770            | ArmOp::I64GtS { .. }
1771            | ArmOp::I64GtU { .. }
1772            | ArmOp::I64GeS { .. }
1773            | ArmOp::I64GeU { .. }
1774            | ArmOp::I64Const { .. }
1775            | ArmOp::I64Ldr { .. }
1776            | ArmOp::I64Str { .. }
1777            | ArmOp::I64ExtendI32S { .. }
1778            | ArmOp::I64ExtendI32U { .. }
1779            | ArmOp::I64Extend8S { .. }
1780            | ArmOp::I64Extend16S { .. }
1781            | ArmOp::I64Extend32S { .. }
1782            | ArmOp::I32WrapI64 { .. } => {
1783                unreachable!("handled by encode_arm_expanded (#615)")
1784            }
1785
1786            // f32 VFP single-precision instructions
1787            ArmOp::F32Add { sd, sn, sm } => encode_vfp_3reg(0xEE300A00, sd, sn, sm)?,
1788            ArmOp::F32Sub { sd, sn, sm } => encode_vfp_3reg(0xEE300A40, sd, sn, sm)?,
1789            ArmOp::F32Mul { sd, sn, sm } => encode_vfp_3reg(0xEE200A00, sd, sn, sm)?,
1790            ArmOp::F32Div { sd, sn, sm } => encode_vfp_3reg(0xEE800A00, sd, sn, sm)?,
1791            ArmOp::F32Abs { sd, sm } => encode_vfp_2reg(0xEEB00AC0, sd, sm)?,
1792            ArmOp::F32Neg { sd, sm } => encode_vfp_2reg(0xEEB10A40, sd, sm)?,
1793            ArmOp::F32Sqrt { sd, sm } => encode_vfp_2reg(0xEEB10AC0, sd, sm)?,
1794
1795            // f32 pseudo-ops — multi-instruction sequences
1796            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
1797            ArmOp::F32Ceil { sd, sm } => {
1798                return self.encode_arm_f32_rounding(sd, sm, 0b01); // Round toward +Inf
1799            }
1800            ArmOp::F32Floor { sd, sm } => {
1801                return self.encode_arm_f32_rounding(sd, sm, 0b10); // Round toward -Inf
1802            }
1803            ArmOp::F32Trunc { sd, sm } => {
1804                return self.encode_arm_f32_rounding(sd, sm, 0b11); // VCVT toward zero
1805            }
1806            ArmOp::F32Nearest { sd, sm } => {
1807                return self.encode_arm_f32_rounding(sd, sm, 0b00); // VCVT to nearest
1808            }
1809            ArmOp::F32Min { sd, sn, sm } => {
1810                return self.encode_arm_f32_minmax(sd, sn, sm, true);
1811            }
1812            ArmOp::F32Max { sd, sn, sm } => {
1813                return self.encode_arm_f32_minmax(sd, sn, sm, false);
1814            }
1815            ArmOp::F32Copysign { sd, sn, sm } => {
1816                return self.encode_arm_f32_copysign(sd, sn, sm);
1817            }
1818
1819            // f32 comparisons — multi-instruction: VCMP + VMRS + conditional MOV
1820            ArmOp::F32Eq { rd, sn, sm } => {
1821                return self.encode_arm_f32_compare(rd, sn, sm, 0x0); // EQ
1822            }
1823            ArmOp::F32Ne { rd, sn, sm } => {
1824                return self.encode_arm_f32_compare(rd, sn, sm, 0x1); // NE
1825            }
1826            ArmOp::F32Lt { rd, sn, sm } => {
1827                return self.encode_arm_f32_compare(rd, sn, sm, 0x4); // MI (less than)
1828            }
1829            ArmOp::F32Le { rd, sn, sm } => {
1830                return self.encode_arm_f32_compare(rd, sn, sm, 0x9); // LS (less or same)
1831            }
1832            ArmOp::F32Gt { rd, sn, sm } => {
1833                return self.encode_arm_f32_compare(rd, sn, sm, 0xC); // GT
1834            }
1835            ArmOp::F32Ge { rd, sn, sm } => {
1836                return self.encode_arm_f32_compare(rd, sn, sm, 0xA); // GE
1837            }
1838
1839            // f32 const — multi-instruction: MOVW + MOVT + VMOV
1840            ArmOp::F32Const { sd, value } => {
1841                return self.encode_arm_f32_const(sd, *value);
1842            }
1843
1844            ArmOp::F32Load { sd, addr } => encode_vfp_ldst(0xED900A00, sd, addr)?,
1845            ArmOp::F32Store { sd, addr } => encode_vfp_ldst(0xED800A00, sd, addr)?,
1846
1847            // f32 conversions — multi-instruction sequences
1848            ArmOp::F32ConvertI32S { sd, rm } => {
1849                return self.encode_arm_f32_convert_i32(sd, rm, true);
1850            }
1851            ArmOp::F32ConvertI32U { sd, rm } => {
1852                return self.encode_arm_f32_convert_i32(sd, rm, false);
1853            }
1854            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
1855                return Err(synth_core::Error::synthesis(
1856                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
1857                ));
1858            }
1859            ArmOp::F32ReinterpretI32 { sd, rm } => encode_vmov_core_sreg(true, sd, rm)?,
1860            ArmOp::I32ReinterpretF32 { rd, sm } => encode_vmov_core_sreg(false, sm, rd)?,
1861            ArmOp::I32TruncF32S { rd, sm } => {
1862                return self.encode_arm_i32_trunc_f32(rd, sm, true);
1863            }
1864            ArmOp::I32TruncF32U { rd, sm } => {
1865                return self.encode_arm_i32_trunc_f32(rd, sm, false);
1866            }
1867
1868            // f64 VFP double-precision instructions (ARM32)
1869            // F64 arithmetic: same as F32 but with sz=1 (bit 8 = 1, cp11 = 0xB)
1870            ArmOp::F64Add { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B00, dd, dn, dm)?,
1871            ArmOp::F64Sub { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B40, dd, dn, dm)?,
1872            ArmOp::F64Mul { dd, dn, dm } => encode_vfp_3reg_f64(0xEE200B00, dd, dn, dm)?,
1873            ArmOp::F64Div { dd, dn, dm } => encode_vfp_3reg_f64(0xEE800B00, dd, dn, dm)?,
1874            ArmOp::F64Abs { dd, dm } => encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?,
1875            ArmOp::F64Neg { dd, dm } => encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?,
1876            ArmOp::F64Sqrt { dd, dm } => encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?,
1877
1878            // f64 pseudo-ops
1879            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
1880            ArmOp::F64Ceil { dd, dm } => {
1881                return self.encode_arm_f64_rounding(dd, dm, 0b01);
1882            }
1883            ArmOp::F64Floor { dd, dm } => {
1884                return self.encode_arm_f64_rounding(dd, dm, 0b10);
1885            }
1886            ArmOp::F64Trunc { dd, dm } => {
1887                return self.encode_arm_f64_rounding(dd, dm, 0b11);
1888            }
1889            ArmOp::F64Nearest { dd, dm } => {
1890                return self.encode_arm_f64_rounding(dd, dm, 0b00);
1891            }
1892            ArmOp::F64Min { dd, dn, dm } => {
1893                return self.encode_arm_f64_minmax(dd, dn, dm, true);
1894            }
1895            ArmOp::F64Max { dd, dn, dm } => {
1896                return self.encode_arm_f64_minmax(dd, dn, dm, false);
1897            }
1898            ArmOp::F64Copysign { dd, dn, dm } => {
1899                return self.encode_arm_f64_copysign(dd, dn, dm);
1900            }
1901
1902            // f64 comparisons
1903            ArmOp::F64Eq { rd, dn, dm } => {
1904                return self.encode_arm_f64_compare(rd, dn, dm, 0x0);
1905            }
1906            ArmOp::F64Ne { rd, dn, dm } => {
1907                return self.encode_arm_f64_compare(rd, dn, dm, 0x1);
1908            }
1909            ArmOp::F64Lt { rd, dn, dm } => {
1910                return self.encode_arm_f64_compare(rd, dn, dm, 0x4);
1911            }
1912            ArmOp::F64Le { rd, dn, dm } => {
1913                return self.encode_arm_f64_compare(rd, dn, dm, 0x9);
1914            }
1915            ArmOp::F64Gt { rd, dn, dm } => {
1916                return self.encode_arm_f64_compare(rd, dn, dm, 0xC);
1917            }
1918            ArmOp::F64Ge { rd, dn, dm } => {
1919                return self.encode_arm_f64_compare(rd, dn, dm, 0xA);
1920            }
1921
1922            ArmOp::F64Const { dd, value } => {
1923                return self.encode_arm_f64_const(dd, *value);
1924            }
1925
1926            ArmOp::F64Load { dd, addr } => encode_vfp_ldst_f64(0xED900B00, dd, addr)?,
1927            ArmOp::F64Store { dd, addr } => encode_vfp_ldst_f64(0xED800B00, dd, addr)?,
1928
1929            ArmOp::F64ConvertI32S { dd, rm } => {
1930                return self.encode_arm_f64_convert_i32(dd, rm, true);
1931            }
1932            ArmOp::F64ConvertI32U { dd, rm } => {
1933                return self.encode_arm_f64_convert_i32(dd, rm, false);
1934            }
1935            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
1936                return Err(synth_core::Error::synthesis(
1937                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
1938                ));
1939            }
1940            ArmOp::F64PromoteF32 { dd, sm } => {
1941                return self.encode_arm_f64_promote_f32(dd, sm);
1942            }
1943            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => {
1944                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?
1945            }
1946            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => {
1947                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?
1948            }
1949            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
1950                return Err(synth_core::Error::synthesis(
1951                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
1952                ));
1953            }
1954            ArmOp::I32TruncF64S { rd, dm } => {
1955                return self.encode_arm_i32_trunc_f64(rd, dm, true);
1956            }
1957            ArmOp::I32TruncF64U { rd, dm } => {
1958                return self.encode_arm_i32_trunc_f64(rd, dm, false);
1959            }
1960            // #615: multi-instruction i64 sequences — expanded to real A32 by
1961            // `encode_arm_expanded`, no longer "Thumb-2 only" NOPs.
1962            ArmOp::I64SetCond { .. }
1963            | ArmOp::I64SetCondZ { .. }
1964            | ArmOp::I64Mul { .. }
1965            | ArmOp::I64Shl { .. }
1966            | ArmOp::I64ShrS { .. }
1967            | ArmOp::I64ShrU { .. }
1968            | ArmOp::I64Rotl { .. }
1969            | ArmOp::I64Rotr { .. } => {
1970                unreachable!("handled by encode_arm_expanded (#615)")
1971            }
1972
1973            // MVE instructions — Thumb-2 only (Cortex-M55 is always Thumb-2)
1974            ArmOp::MveLoad { .. }
1975            | ArmOp::MveStore { .. }
1976            | ArmOp::MveConst { .. }
1977            | ArmOp::MveAnd { .. }
1978            | ArmOp::MveOrr { .. }
1979            | ArmOp::MveEor { .. }
1980            | ArmOp::MveMvn { .. }
1981            | ArmOp::MveBic { .. }
1982            | ArmOp::MveAddI { .. }
1983            | ArmOp::MveSubI { .. }
1984            | ArmOp::MveMulI { .. }
1985            | ArmOp::MveNegI { .. }
1986            | ArmOp::MveCmpEqI { .. }
1987            | ArmOp::MveCmpNeI { .. }
1988            | ArmOp::MveCmpLtS { .. }
1989            | ArmOp::MveCmpLtU { .. }
1990            | ArmOp::MveCmpGtS { .. }
1991            | ArmOp::MveCmpGtU { .. }
1992            | ArmOp::MveCmpLeS { .. }
1993            | ArmOp::MveCmpLeU { .. }
1994            | ArmOp::MveCmpGeS { .. }
1995            | ArmOp::MveCmpGeU { .. }
1996            | ArmOp::MveDup { .. }
1997            | ArmOp::MveExtractLane { .. }
1998            | ArmOp::MveInsertLane { .. }
1999            | ArmOp::MveAddF32 { .. }
2000            | ArmOp::MveSubF32 { .. }
2001            | ArmOp::MveMulF32 { .. }
2002            | ArmOp::MveNegF32 { .. }
2003            | ArmOp::MveAbsF32 { .. }
2004            | ArmOp::MveCmpEqF32 { .. }
2005            | ArmOp::MveCmpNeF32 { .. }
2006            | ArmOp::MveCmpLtF32 { .. }
2007            | ArmOp::MveCmpLeF32 { .. }
2008            | ArmOp::MveCmpGtF32 { .. }
2009            | ArmOp::MveCmpGeF32 { .. }
2010            | ArmOp::MveDupF32 { .. }
2011            | ArmOp::MveExtractLaneF32 { .. }
2012            | ArmOp::MveReplaceLaneF32 { .. }
2013            | ArmOp::MveDivF32 { .. }
2014            | ArmOp::MveSqrtF32 { .. } => {
2015                // MVE (Helium) is a Thumb-2-only extension (Cortex-M55); there
2016                // is no A32 encoding. The selector only emits MVE ops for
2017                // Thumb targets — a NOP here silently dropped the vector op
2018                // if that invariant ever broke (#615 class). Err keeps the
2019                // encoder total and the failure loud.
2020                return Err(synth_core::Error::synthesis(format!(
2021                    "MVE op {op:?} has no A32 (ARM-mode) encoding — MVE is Thumb-2 only (#615)"
2022                )));
2023            }
2024        };
2025
2026        // ARM32 instructions are little-endian
2027        Ok(instr.to_le_bytes().to_vec())
2028    }
2029
2030    // === ARM32 VFP multi-instruction helpers ===
2031
2032    /// Encode F32 comparison as ARM32: VCMP.F32 + VMRS + MOV rd,#0 + MOVcond rd,#1
2033    fn encode_arm_f32_compare(
2034        &self,
2035        rd: &Reg,
2036        sn: &VfpReg,
2037        sm: &VfpReg,
2038        cond_code: u32,
2039    ) -> Result<Vec<u8>> {
2040        let mut bytes = Vec::new();
2041
2042        // VCMP.F32 Sn, Sm: 0xEEB40A40 with Sn in Vd position, Sm in Vm position
2043        let sn_num = vfp_sreg_to_num(sn)?;
2044        let sm_num = vfp_sreg_to_num(sm)?;
2045        let (vd, d) = encode_sreg(sn_num);
2046        let (vm, m) = encode_sreg(sm_num);
2047        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2048        bytes.extend_from_slice(&vcmp.to_le_bytes());
2049
2050        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
2051        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2052
2053        // MOV rd, #0: 0xE3A0_0000 | (rd << 12)
2054        let rd_bits = reg_to_bits(rd);
2055        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2056        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2057
2058        // MOVcond rd, #1: cond(4) | 0011 1010 0000 rd(4) 0000 0000 0001
2059        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2060        bytes.extend_from_slice(&mov_one.to_le_bytes());
2061
2062        Ok(bytes)
2063    }
2064
2065    /// Encode F32 constant load as ARM32: MOVW Rt,#lo16 + MOVT Rt,#hi16 + VMOV Sd,Rt
2066    fn encode_arm_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
2067        let mut bytes = Vec::new();
2068        let bits = value.to_bits();
2069
2070        // Use R12 as temp register for constant loading
2071        let rt: u32 = 12; // R12/IP
2072
2073        // MOVW R12, #lo16: 0xE300_C000 | (imm4 << 16) | imm12
2074        let lo16 = bits & 0xFFFF;
2075        let movw = 0xE3000000 | (rt << 12) | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2076        bytes.extend_from_slice(&movw.to_le_bytes());
2077
2078        // MOVT R12, #hi16: 0xE340_C000 | (imm4 << 16) | imm12
2079        let hi16 = (bits >> 16) & 0xFFFF;
2080        let movt = 0xE3400000 | (rt << 12) | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2081        bytes.extend_from_slice(&movt.to_le_bytes());
2082
2083        // VMOV Sd, R12
2084        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
2085        bytes.extend_from_slice(&vmov.to_le_bytes());
2086
2087        Ok(bytes)
2088    }
2089
2090    /// Encode VMOV + VCVT.F32.S32/U32 as ARM32
2091    fn encode_arm_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2092        let mut bytes = Vec::new();
2093
2094        // VMOV Sd, Rm — move integer to VFP register
2095        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
2096        bytes.extend_from_slice(&vmov.to_le_bytes());
2097
2098        // VCVT.F32.S32 Sd, Sd (signed) or VCVT.F32.U32 Sd, Sd (unsigned)
2099        // Base: 0xEEB80A40 (signed) or 0xEEB80AC0 (unsigned)
2100        let sd_num = vfp_sreg_to_num(sd)?;
2101        let (vd, d) = encode_sreg(sd_num);
2102        let (vm, m) = encode_sreg(sd_num); // same register as source
2103        let base = if signed { 0xEEB80A40 } else { 0xEEB80AC0 };
2104        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2105        bytes.extend_from_slice(&vcvt.to_le_bytes());
2106
2107        Ok(bytes)
2108    }
2109
2110    /// Encode F32 rounding pseudo-op as ARM32 via VCVT to integer and back.
2111    /// mode: 0b00=nearest, 0b01=floor(-Inf), 0b10=ceil(+Inf), 0b11=trunc(zero)
2112    /// Strategy: VCVT.S32.F32 Sd, Sm (toward zero), then VCVT.F32.S32 Sd, Sd
2113    /// For ceil/floor/nearest, we use VCVTR (round toward mode) + convert back.
2114    /// Simplified: convert to int (toward zero for trunc) then back to float.
2115    /// Encode F32 rounding as ARM32.
2116    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2117    ///
2118    /// For trunc (mode=0b11): uses VCVTR.S32.F32 (always rounds toward zero).
2119    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant
2120    /// which honours FPSCR rmode), then restores FPSCR.
2121    fn encode_arm_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2122        let mut bytes = Vec::new();
2123        let sm_num = vfp_sreg_to_num(sm)?;
2124        let sd_num = vfp_sreg_to_num(sd)?;
2125        let (vd_s, d_s) = encode_sreg(sd_num);
2126        let (vm_s, m_s) = encode_sreg(sm_num);
2127
2128        if mode == 0b11 {
2129            // Trunc (toward zero): VCVTR.S32.F32 — the "R" variant always truncates.
2130            // 0xEEBD0AC0: bit[7]=1 => round toward zero regardless of FPSCR
2131            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2132            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2133        } else {
2134            // ceil/floor/nearest: manipulate FPSCR rounding mode
2135            let rt: u32 = 12; // R12/IP as temp
2136
2137            // VMRS R12, FPSCR
2138            let vmrs = 0xEEF10A10 | (rt << 12);
2139            bytes.extend_from_slice(&vmrs.to_le_bytes());
2140
2141            // BIC R12, R12, #(3 << 22) — clear RMode bits [23:22]
2142            // 3<<22 = 0x00C00000. ARM rotated imm: 0x03 ror 10 (rotation=5, imm8=0x03)
2143            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2144            bytes.extend_from_slice(&bic.to_le_bytes());
2145
2146            // ORR R12, R12, #(mode << 22) — set desired rounding mode
2147            if mode != 0 {
2148                // mode<<22: rotation=5, imm8=mode
2149                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2150                bytes.extend_from_slice(&orr.to_le_bytes());
2151            }
2152
2153            // VMSR FPSCR, R12
2154            let vmsr = 0xEEE10A10 | (rt << 12);
2155            bytes.extend_from_slice(&vmsr.to_le_bytes());
2156
2157            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rounding mode
2158            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2159            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2160
2161            // Restore FPSCR: clear rmode bits back to nearest (default)
2162            bytes.extend_from_slice(&vmrs.to_le_bytes());
2163            bytes.extend_from_slice(&bic.to_le_bytes());
2164            bytes.extend_from_slice(&vmsr.to_le_bytes());
2165        }
2166
2167        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
2168        let (vd2, d2) = encode_sreg(sd_num);
2169        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
2170        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2171
2172        Ok(bytes)
2173    }
2174
2175    /// Encode F32 min/max as ARM32: VCMP + VMRS + conditional VMOV
2176    fn encode_arm_f32_minmax(
2177        &self,
2178        sd: &VfpReg,
2179        sn: &VfpReg,
2180        sm: &VfpReg,
2181        is_min: bool,
2182    ) -> Result<Vec<u8>> {
2183        let mut bytes = Vec::new();
2184        let sn_num = vfp_sreg_to_num(sn)?;
2185        let sm_num = vfp_sreg_to_num(sm)?;
2186        let sd_num = vfp_sreg_to_num(sd)?;
2187
2188        // VMOV Sd, Sn (start with first operand)
2189        let (vd, d) = encode_sreg(sd_num);
2190        let (vn, n) = encode_sreg(sn_num);
2191        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2192        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2193
2194        // VCMP.F32 Sn, Sm
2195        let (vm, m) = encode_sreg(sm_num);
2196        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2197        bytes.extend_from_slice(&vcmp.to_le_bytes());
2198
2199        // VMRS APSR_nzcv, FPSCR
2200        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2201
2202        // For min: if Sn > Sm (GT), use Sm. Condition = GT (0xC)
2203        // For max: if Sn < Sm (MI/LT), use Sm. Condition = MI (0x4)
2204        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2205
2206        // VMOV{cond} Sd, Sm — conditional VMOV
2207        let vmov_cond = (cond << 28) | 0x0EB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2208        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2209
2210        Ok(bytes)
2211    }
2212
2213    /// Encode F32 copysign as ARM32: extract sign from Sm, magnitude from Sn
2214    fn encode_arm_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2215        let mut bytes = Vec::new();
2216
2217        // VMOV R12, Sm (get sign source bits)
2218        let vmov_sm = encode_vmov_core_sreg(false, sm, &Reg::R12)?;
2219        bytes.extend_from_slice(&vmov_sm.to_le_bytes());
2220
2221        // VMOV R0, Sn (get magnitude source bits) — use R0 as temp
2222        let vmov_sn = encode_vmov_core_sreg(false, sn, &Reg::R0)?;
2223        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2224
2225        // AND R12, R12, #0x80000000 (keep only sign bit)
2226        // Thumb-2 constant 0x80000000 needs special encoding; in ARM32 use rotated imm
2227        // 0x80000000 = 0x02 rotated right by 2 (rotation=1, imm8=0x02)
2228        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2229        bytes.extend_from_slice(&and_sign.to_le_bytes());
2230
2231        // BIC R0, R0, #0x80000000 (clear sign bit from magnitude)
2232        // R0 = register 0, so Rn and Rd fields are 0
2233        let bic_sign = 0xE3C00000u32 | (1 << 8) | 0x02;
2234        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2235
2236        // ORR R0, R0, R12 (combine sign + magnitude)
2237        // R0 = register 0, so Rn and Rd fields are 0
2238        let orr = 0xE1800000u32 | 12;
2239        bytes.extend_from_slice(&orr.to_le_bytes());
2240
2241        // VMOV Sd, R0
2242        let vmov_result = encode_vmov_core_sreg(true, sd, &Reg::R0)?;
2243        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2244
2245        Ok(bytes)
2246    }
2247
2248    /// Encode F64 comparison as ARM32: VCMP.F64 + VMRS + MOV rd,#0 + MOVcond rd,#1
2249    fn encode_arm_f64_compare(
2250        &self,
2251        rd: &Reg,
2252        dn: &VfpReg,
2253        dm: &VfpReg,
2254        cond_code: u32,
2255    ) -> Result<Vec<u8>> {
2256        let mut bytes = Vec::new();
2257
2258        // VCMP.F64 Dn, Dm: 0xEEB40B40 with Dn in Vd position, Dm in Vm position
2259        let dn_num = vfp_dreg_to_num(dn)?;
2260        let dm_num = vfp_dreg_to_num(dm)?;
2261        let (vd, d) = encode_dreg(dn_num);
2262        let (vm, m) = encode_dreg(dm_num);
2263        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2264        bytes.extend_from_slice(&vcmp.to_le_bytes());
2265
2266        // VMRS APSR_nzcv, FPSCR
2267        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2268
2269        // MOV rd, #0
2270        let rd_bits = reg_to_bits(rd);
2271        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2272        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2273
2274        // MOVcond rd, #1
2275        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2276        bytes.extend_from_slice(&mov_one.to_le_bytes());
2277
2278        Ok(bytes)
2279    }
2280
2281    /// Encode F64 constant load as ARM32: MOVW + MOVT + MOVW + MOVT + VMOV
2282    fn encode_arm_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
2283        let mut bytes = Vec::new();
2284        let bits = value.to_bits();
2285        let lo32 = bits as u32;
2286        let hi32 = (bits >> 32) as u32;
2287
2288        // Load low 32 bits into R0 (Rd field = 0 for R0)
2289        let lo16 = lo32 & 0xFFFF;
2290        let movw_r0 = 0xE3000000 | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2291        bytes.extend_from_slice(&movw_r0.to_le_bytes());
2292        let hi16 = (lo32 >> 16) & 0xFFFF;
2293        let movt_r0 = 0xE3400000 | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2294        bytes.extend_from_slice(&movt_r0.to_le_bytes());
2295
2296        // Load high 32 bits into R12
2297        let lo16 = hi32 & 0xFFFF;
2298        let movw_r12 = 0xE3000000 | ((lo16 >> 12) << 16) | (12 << 12) | (lo16 & 0xFFF);
2299        bytes.extend_from_slice(&movw_r12.to_le_bytes());
2300        let hi16 = (hi32 >> 16) & 0xFFFF;
2301        let movt_r12 = 0xE3400000 | ((hi16 >> 12) << 16) | (12 << 12) | (hi16 & 0xFFF);
2302        bytes.extend_from_slice(&movt_r12.to_le_bytes());
2303
2304        // VMOV Dd, R0, R12
2305        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
2306        bytes.extend_from_slice(&vmov.to_le_bytes());
2307
2308        Ok(bytes)
2309    }
2310
2311    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as ARM32
2312    fn encode_arm_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2313        let mut bytes = Vec::new();
2314
2315        // Use S0 as intermediate: VMOV S0, Rm
2316        let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
2317        bytes.extend_from_slice(&vmov.to_le_bytes());
2318
2319        // VCVT.F64.S32 Dd, S0 (signed) or VCVT.F64.U32 Dd, S0 (unsigned)
2320        // Base: 0xEEB80B40 (signed) or 0xEEB80BC0 (unsigned)
2321        let dd_num = vfp_dreg_to_num(dd)?;
2322        let (vd, d) = encode_dreg(dd_num);
2323        let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
2324        // S0 is register 0: Vm=0, M=0
2325        let vcvt = base | (d << 22) | (vd << 12);
2326        bytes.extend_from_slice(&vcvt.to_le_bytes());
2327
2328        Ok(bytes)
2329    }
2330
2331    /// Encode VCVT.F64.F32 Dd, Sm as ARM32 (f32 to f64 promotion)
2332    fn encode_arm_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2333        let dd_num = vfp_dreg_to_num(dd)?;
2334        let sm_num = vfp_sreg_to_num(sm)?;
2335        let (vd, d) = encode_dreg(dd_num);
2336        let (vm, m) = encode_sreg(sm_num);
2337
2338        // VCVT.F64.F32 Dd, Sm: 0xEEB70AC0
2339        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
2340        Ok(vcvt.to_le_bytes().to_vec())
2341    }
2342
2343    /// Encode VCVT.S32/U32.F64 Sd, Dm + VMOV Rd, Sd as ARM32
2344    fn encode_arm_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2345        let mut bytes = Vec::new();
2346        let dm_num = vfp_dreg_to_num(dm)?;
2347        let (vm, m) = encode_dreg(dm_num);
2348
2349        // VCVT.S32.F64 S0, Dm (toward zero) or VCVT.U32.F64 S0, Dm
2350        // S0: Vd=0, D=0
2351        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
2352        let vcvt = base | (m << 5) | vm;
2353        bytes.extend_from_slice(&vcvt.to_le_bytes());
2354
2355        // VMOV Rd, S0
2356        let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
2357        bytes.extend_from_slice(&vmov.to_le_bytes());
2358
2359        Ok(bytes)
2360    }
2361
2362    /// Encode F64 rounding pseudo-op as ARM32 via VCVT to integer and back.
2363    /// Encode F64 rounding as ARM32.
2364    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2365    ///
2366    /// For trunc: uses VCVTR.S32.F64 (always truncates).
2367    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F64 (non-R variant),
2368    /// then restores FPSCR.
2369    fn encode_arm_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2370        let mut bytes = Vec::new();
2371        let dm_num = vfp_dreg_to_num(dm)?;
2372        let dd_num = vfp_dreg_to_num(dd)?;
2373        let (vm, m) = encode_dreg(dm_num);
2374        let (vd, d) = encode_dreg(dd_num);
2375
2376        if mode == 0b11 {
2377            // Trunc (toward zero): VCVTR.S32.F64 — bit[7]=1, always truncates
2378            let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
2379            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2380        } else {
2381            // ceil/floor/nearest: manipulate FPSCR rounding mode
2382            let rt: u32 = 12;
2383
2384            // VMRS R12, FPSCR
2385            let vmrs = 0xEEF10A10 | (rt << 12);
2386            bytes.extend_from_slice(&vmrs.to_le_bytes());
2387
2388            // BIC R12, R12, #(3 << 22)
2389            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2390            bytes.extend_from_slice(&bic.to_le_bytes());
2391
2392            // ORR R12, R12, #(mode << 22)
2393            if mode != 0 {
2394                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2395                bytes.extend_from_slice(&orr.to_le_bytes());
2396            }
2397
2398            // VMSR FPSCR, R12
2399            let vmsr = 0xEEE10A10 | (rt << 12);
2400            bytes.extend_from_slice(&vmsr.to_le_bytes());
2401
2402            // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0), uses FPSCR rmode
2403            let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
2404            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2405
2406            // Restore FPSCR
2407            bytes.extend_from_slice(&vmrs.to_le_bytes());
2408            bytes.extend_from_slice(&bic.to_le_bytes());
2409            bytes.extend_from_slice(&vmsr.to_le_bytes());
2410        }
2411
2412        // VCVT.F64.S32 Dd, S0 (convert back to double)
2413        let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
2414        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2415
2416        Ok(bytes)
2417    }
2418
2419    /// Encode F64 min/max as ARM32: VMOV + VCMP + VMRS + conditional VMOV
2420    fn encode_arm_f64_minmax(
2421        &self,
2422        dd: &VfpReg,
2423        dn: &VfpReg,
2424        dm: &VfpReg,
2425        is_min: bool,
2426    ) -> Result<Vec<u8>> {
2427        let mut bytes = Vec::new();
2428        let dn_num = vfp_dreg_to_num(dn)?;
2429        let dm_num = vfp_dreg_to_num(dm)?;
2430        let dd_num = vfp_dreg_to_num(dd)?;
2431
2432        // VMOV.F64 Dd, Dn (start with first operand)
2433        let (vd, d) = encode_dreg(dd_num);
2434        let (vn, n) = encode_dreg(dn_num);
2435        let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2436        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2437
2438        // VCMP.F64 Dn, Dm
2439        let (vm, m) = encode_dreg(dm_num);
2440        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2441        bytes.extend_from_slice(&vcmp.to_le_bytes());
2442
2443        // VMRS APSR_nzcv, FPSCR
2444        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2445
2446        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2447        let vmov_cond = (cond << 28) | 0x0EB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2448        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2449
2450        Ok(bytes)
2451    }
2452
2453    /// Encode F64 copysign as ARM32
2454    fn encode_arm_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
2455        let mut bytes = Vec::new();
2456
2457        // VMOV R0, R12, Dm (get sign source bits)
2458        let vmov_dm = encode_vmov_core_dreg(false, dm, &Reg::R0, &Reg::R12)?;
2459        bytes.extend_from_slice(&vmov_dm.to_le_bytes());
2460
2461        // VMOV R1, R2, Dn (get magnitude source bits)
2462        // We use R1 (lo) and R2 (hi) for the magnitude
2463        let vmov_dn = encode_vmov_core_dreg(false, dn, &Reg::R1, &Reg::R2)?;
2464        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2465
2466        // AND R12, R12, #0x80000000 (keep only sign bit from hi word)
2467        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2468        bytes.extend_from_slice(&and_sign.to_le_bytes());
2469
2470        // BIC R2, R2, #0x80000000 (clear sign bit from magnitude hi word)
2471        let bic_sign = 0xE3C00000u32 | (2 << 16) | (2 << 12) | (1 << 8) | 0x02;
2472        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2473
2474        // ORR R2, R2, R12 (combine sign + magnitude)
2475        let orr = 0xE1800000u32 | (2 << 16) | (2 << 12) | 12;
2476        bytes.extend_from_slice(&orr.to_le_bytes());
2477
2478        // VMOV Dd, R1, R2
2479        let vmov_result = encode_vmov_core_dreg(true, dd, &Reg::R1, &Reg::R2)?;
2480        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2481
2482        Ok(bytes)
2483    }
2484
2485    /// Encode VCVT.S32/U32.F32 + VMOV as ARM32
2486    fn encode_arm_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2487        let mut bytes = Vec::new();
2488
2489        // VCVT.S32.F32 Sd, Sm (toward zero) or VCVT.U32.F32 Sd, Sm
2490        // We use Sm as both source and destination for the intermediate result
2491        let sm_num = vfp_sreg_to_num(sm)?;
2492        let (vd, d) = encode_sreg(sm_num);
2493        let (vm, m) = encode_sreg(sm_num);
2494        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
2495        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2496        bytes.extend_from_slice(&vcvt.to_le_bytes());
2497
2498        // VMOV Rd, Sm — move result back to core register
2499        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
2500        bytes.extend_from_slice(&vmov.to_le_bytes());
2501
2502        Ok(bytes)
2503    }
2504
2505    /// Encode an ARM instruction in Thumb-2 mode (16-bit or 32-bit instructions)
2506    fn encode_thumb(&self, op: &ArmOp) -> Result<Vec<u8>> {
2507        // Thumb-2 supports both 16-bit and 32-bit instructions
2508        // 32-bit instructions are encoded as two 16-bit halfwords (big-endian order)
2509        match op {
2510            // === 16-bit Thumb encodings ===
2511            ArmOp::Add { rd, rn, op2 } => {
2512                let rd_bits = reg_to_bits(rd) as u16;
2513                let rn_bits = reg_to_bits(rn) as u16;
2514
2515                if let Operand2::Reg(rm) = op2 {
2516                    let rm_bits = reg_to_bits(rm) as u16;
2517                    // 16-bit ADDS only has 3-bit register fields (R0-R7). For
2518                    // high registers (e.g. R12, the MemLoad/MemStore base
2519                    // scratch) the bits overflow into adjacent fields, silently
2520                    // corrupting the operands — issue #178/#180: `add ip,ip,r0`
2521                    // was emitted as `adds r4,r5,r1`. Guard on all three regs
2522                    // being low and fall back to 32-bit ADD.W otherwise, exactly
2523                    // as the Sub handler below does.
2524                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2525                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2526                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2527                        Ok(instr.to_le_bytes().to_vec())
2528                    } else {
2529                        // ADD.W Rd, Rn, Rm (32-bit) for high registers
2530                        self.encode_thumb32_add_reg_raw(
2531                            rd_bits as u32,
2532                            rn_bits as u32,
2533                            rm_bits as u32,
2534                        )
2535                    }
2536                } else if let Operand2::Imm(imm) = op2 {
2537                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2538                        // ADDS Rd, Rn, #imm3 (16-bit): 0001 110 imm3 Rn Rd
2539                        let instr: u16 = 0x1C00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2540                        Ok(instr.to_le_bytes().to_vec())
2541                    } else {
2542                        // Use 32-bit ADD for larger immediates
2543                        self.encode_thumb32_add(rd, rn, *imm as u32)
2544                    }
2545                } else {
2546                    // Fallback to 32-bit encoding
2547                    self.encode_thumb32_add(rd, rn, 0)
2548                }
2549            }
2550
2551            ArmOp::Sub { rd, rn, op2 } => {
2552                let rd_bits = reg_to_bits(rd) as u16;
2553                let rn_bits = reg_to_bits(rn) as u16;
2554
2555                if let Operand2::Reg(rm) = op2 {
2556                    let rm_bits = reg_to_bits(rm) as u16;
2557                    // 16-bit SUBS can only use low registers (R0-R7)
2558                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2559                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2560                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2561                        Ok(instr.to_le_bytes().to_vec())
2562                    } else {
2563                        // Use 32-bit SUB.W for high registers
2564                        self.encode_thumb32_sub_reg_raw(
2565                            rd_bits as u32,
2566                            rn_bits as u32,
2567                            rm_bits as u32,
2568                        )
2569                    }
2570                } else if let Operand2::Imm(imm) = op2 {
2571                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2572                        // SUBS Rd, Rn, #imm3 (16-bit): 0001 111 imm3 Rn Rd
2573                        let instr: u16 = 0x1E00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2574                        Ok(instr.to_le_bytes().to_vec())
2575                    } else {
2576                        self.encode_thumb32_sub(rd, rn, *imm as u32)
2577                    }
2578                } else {
2579                    self.encode_thumb32_sub(rd, rn, 0)
2580                }
2581            }
2582
2583            ArmOp::Mov { rd, op2 } => {
2584                let rd_bits = reg_to_bits(rd) as u16;
2585
2586                if let Operand2::Imm(imm) = op2 {
2587                    if *imm <= 255 && rd_bits < 8 {
2588                        // MOVS Rd, #imm8 (16-bit): 0010 0 Rd imm8
2589                        let imm_bits = (*imm as u16) & 0xFF;
2590                        let instr: u16 = 0x2000 | (rd_bits << 8) | imm_bits;
2591                        Ok(instr.to_le_bytes().to_vec())
2592                    } else {
2593                        // Use 32-bit MOVW for larger immediates
2594                        self.encode_thumb32_movw(rd, *imm as u32)
2595                    }
2596                } else if let Operand2::Reg(rm) = op2 {
2597                    let rm_bits = reg_to_bits(rm) as u16;
2598                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
2599                    // D = Rd[3], Rd[2:0] in lower bits
2600                    let d_bit = (rd_bits >> 3) & 1;
2601                    let instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
2602                    Ok(instr.to_le_bytes().to_vec())
2603                } else {
2604                    let instr: u16 = 0xBF00; // NOP fallback
2605                    Ok(instr.to_le_bytes().to_vec())
2606                }
2607            }
2608
2609            ArmOp::Push { regs } => {
2610                // Thumb-2 PUSH encoding:
2611                // If all regs in R0-R7 + LR, use 16-bit: 1011 010 M rrrrrrrr
2612                // Otherwise use 32-bit: STMDB SP!, {regs} = 1110 1001 0010 1101 | 0M0 reglist(13)
2613                let mut reg_list: u16 = 0;
2614                let mut need_32bit = false;
2615                for r in regs {
2616                    let bit = reg_to_bits(r);
2617                    if bit >= 8 && *r != Reg::LR {
2618                        need_32bit = true;
2619                    }
2620                    reg_list |= 1 << bit;
2621                }
2622                if !need_32bit {
2623                    // 16-bit PUSH: 1011 010 M rrrrrrrr
2624                    let m_bit = if reg_list & (1 << 14) != 0 {
2625                        1u16
2626                    } else {
2627                        0u16
2628                    };
2629                    let low_regs = reg_list & 0xFF;
2630                    let instr: u16 = 0xB400 | (m_bit << 8) | low_regs;
2631                    Ok(instr.to_le_bytes().to_vec())
2632                } else {
2633                    // 32-bit STMDB SP!, {regs}: E92D | reglist(16)
2634                    let hw1: u16 = 0xE92D;
2635                    let hw2: u16 = reg_list;
2636                    let mut bytes = hw1.to_le_bytes().to_vec();
2637                    bytes.extend_from_slice(&hw2.to_le_bytes());
2638                    Ok(bytes)
2639                }
2640            }
2641
2642            ArmOp::Pop { regs } => {
2643                // Thumb-2 POP encoding:
2644                // If all regs in R0-R7 + PC, use 16-bit: 1011 110 P rrrrrrrr
2645                // Otherwise use 32-bit: LDMIA SP!, {regs} = 1110 1000 1011 1101 | PM0 reglist(13)
2646                let mut reg_list: u16 = 0;
2647                let mut need_32bit = false;
2648                for r in regs {
2649                    let bit = reg_to_bits(r);
2650                    if bit >= 8 && *r != Reg::PC {
2651                        need_32bit = true;
2652                    }
2653                    reg_list |= 1 << bit;
2654                }
2655                if !need_32bit {
2656                    // 16-bit POP: 1011 110 P rrrrrrrr
2657                    let p_bit = if reg_list & (1 << 15) != 0 {
2658                        1u16
2659                    } else {
2660                        0u16
2661                    };
2662                    let low_regs = reg_list & 0xFF;
2663                    let instr: u16 = 0xBC00 | (p_bit << 8) | low_regs;
2664                    Ok(instr.to_le_bytes().to_vec())
2665                } else {
2666                    // 32-bit LDMIA SP!, {regs}: E8BD | reglist(16)
2667                    let hw1: u16 = 0xE8BD;
2668                    let hw2: u16 = reg_list;
2669                    let mut bytes = hw1.to_le_bytes().to_vec();
2670                    bytes.extend_from_slice(&hw2.to_le_bytes());
2671                    Ok(bytes)
2672                }
2673            }
2674
2675            ArmOp::Nop => {
2676                let instr: u16 = 0xBF00; // NOP in Thumb-2
2677                Ok(instr.to_le_bytes().to_vec())
2678            }
2679
2680            ArmOp::Udf { imm } => {
2681                // UDF (Undefined) in Thumb-2: 16-bit encoding is 0xDE00 | imm8
2682                // This triggers UsageFault/HardFault, used for WASM traps
2683                let instr: u16 = 0xDE00 | (*imm as u16);
2684                let bytes = instr.to_le_bytes().to_vec();
2685                encoding_contracts::verify_thumb16(&bytes);
2686                Ok(bytes)
2687            }
2688
2689            // i64 support: ADDS, ADC, SUBS, SBC for register pair arithmetic
2690            // ADDS sets flags (carry), ADC uses carry from previous ADDS
2691            ArmOp::Adds { rd, rn, op2 } => {
2692                let rd_bits = reg_to_bits(rd) as u16;
2693                let rn_bits = reg_to_bits(rn) as u16;
2694
2695                if let Operand2::Reg(rm) = op2 {
2696                    let rm_bits = reg_to_bits(rm) as u16;
2697                    // 16-bit ADDS is R0-R7 only; i64 pair allocation can place
2698                    // operands in R8-R11, which would overflow the 3-bit fields
2699                    // and corrupt the operands (#178/#180 class). Guard and fall
2700                    // back to 32-bit ADDS.W for high registers.
2701                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2702                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2703                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2704                        Ok(instr.to_le_bytes().to_vec())
2705                    } else {
2706                        self.encode_thumb32_adds_reg_raw(
2707                            rd_bits as u32,
2708                            rn_bits as u32,
2709                            rm_bits as u32,
2710                        )
2711                    }
2712                } else {
2713                    // 32-bit Thumb-2 ADDS with immediate
2714                    self.encode_thumb32_adds(rd, rn, 0)
2715                }
2716            }
2717
2718            // ADC: Add with Carry (Thumb-2 32-bit)
2719            // ADC.W Rd, Rn, Rm: EB40 Rn | 00 Rd 00 Rm
2720            ArmOp::Adc { rd, rn, op2 } => {
2721                let rd_bits = reg_to_bits(rd);
2722                let rn_bits = reg_to_bits(rn);
2723
2724                if let Operand2::Reg(rm) = op2 {
2725                    let rm_bits = reg_to_bits(rm);
2726                    // ADC.W Rd, Rn, Rm (T2): 1110 1011 0100 Rn | 0 000 Rd 00 00 Rm
2727                    let hw1: u16 = (0xEB40 | rn_bits) as u16;
2728                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2729
2730                    let mut bytes = hw1.to_le_bytes().to_vec();
2731                    bytes.extend_from_slice(&hw2.to_le_bytes());
2732                    Ok(bytes)
2733                } else {
2734                    // ADC with immediate - use 32-bit encoding
2735                    let hw1: u16 = (0xF140 | rn_bits) as u16;
2736                    let hw2: u16 = (rd_bits << 8) as u16;
2737                    let mut bytes = hw1.to_le_bytes().to_vec();
2738                    bytes.extend_from_slice(&hw2.to_le_bytes());
2739                    Ok(bytes)
2740                }
2741            }
2742
2743            // SUBS sets flags (borrow), SBC uses borrow from previous SUBS
2744            ArmOp::Subs { rd, rn, op2 } => {
2745                let rd_bits = reg_to_bits(rd) as u16;
2746                let rn_bits = reg_to_bits(rn) as u16;
2747
2748                if let Operand2::Reg(rm) = op2 {
2749                    let rm_bits = reg_to_bits(rm) as u16;
2750                    // 16-bit SUBS is R0-R7 only; high-register i64 pair operands
2751                    // would overflow the 3-bit fields (#178/#180 class). Guard
2752                    // and fall back to 32-bit SUBS.W for high registers.
2753                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2754                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2755                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2756                        Ok(instr.to_le_bytes().to_vec())
2757                    } else {
2758                        self.encode_thumb32_subs_reg_raw(
2759                            rd_bits as u32,
2760                            rn_bits as u32,
2761                            rm_bits as u32,
2762                        )
2763                    }
2764                } else {
2765                    // 32-bit Thumb-2 SUBS with immediate
2766                    self.encode_thumb32_subs(rd, rn, 0)
2767                }
2768            }
2769
2770            // SBC: Subtract with Carry (Thumb-2 32-bit)
2771            // SBC.W Rd, Rn, Rm: EB60 Rn | 00 Rd 00 Rm
2772            ArmOp::Sbc { rd, rn, op2 } => {
2773                let rd_bits = reg_to_bits(rd);
2774                let rn_bits = reg_to_bits(rn);
2775
2776                if let Operand2::Reg(rm) = op2 {
2777                    let rm_bits = reg_to_bits(rm);
2778                    // SBC.W Rd, Rn, Rm (T2): 1110 1011 0110 Rn | 0 000 Rd 00 00 Rm
2779                    let hw1: u16 = (0xEB60 | rn_bits) as u16;
2780                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2781
2782                    let mut bytes = hw1.to_le_bytes().to_vec();
2783                    bytes.extend_from_slice(&hw2.to_le_bytes());
2784                    Ok(bytes)
2785                } else {
2786                    // SBC with immediate - use 32-bit encoding
2787                    let hw1: u16 = (0xF160 | rn_bits) as u16;
2788                    let hw2: u16 = (rd_bits << 8) as u16;
2789                    let mut bytes = hw1.to_le_bytes().to_vec();
2790                    bytes.extend_from_slice(&hw2.to_le_bytes());
2791                    Ok(bytes)
2792                }
2793            }
2794
2795            // === 32-bit Thumb-2 encodings ===
2796
2797            // SDIV: 11111011 1001 Rn 1111 Rd 1111 Rm
2798            ArmOp::Sdiv { rd, rn, rm } => {
2799                let rd_bits = reg_to_bits(rd);
2800                let rn_bits = reg_to_bits(rn);
2801                let rm_bits = reg_to_bits(rm);
2802                reg_bits_checked(rd_bits)?;
2803                reg_bits_checked(rn_bits)?;
2804                reg_bits_checked(rm_bits)?;
2805
2806                // Thumb-2 SDIV: FB90 F0F0 | Rn<<16 | Rd<<8 | Rm
2807                // First halfword: 1111 1011 1001 Rn = 0xFB90 | Rn
2808                // Second halfword: 1111 Rd 1111 Rm = 0xF0F0 | Rd<<8 | Rm
2809                let hw1: u16 = (0xFB90 | rn_bits) as u16;
2810                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
2811
2812                // Thumb-2 32-bit instructions: first halfword, then second halfword (little-endian each)
2813                let mut bytes = hw1.to_le_bytes().to_vec();
2814                bytes.extend_from_slice(&hw2.to_le_bytes());
2815                encoding_contracts::verify_thumb32(&bytes);
2816                Ok(bytes)
2817            }
2818
2819            // UDIV: 11111011 1011 Rn 1111 Rd 1111 Rm
2820            ArmOp::Udiv { rd, rn, rm } => {
2821                let rd_bits = reg_to_bits(rd);
2822                let rn_bits = reg_to_bits(rn);
2823                let rm_bits = reg_to_bits(rm);
2824                reg_bits_checked(rd_bits)?;
2825                reg_bits_checked(rn_bits)?;
2826                reg_bits_checked(rm_bits)?;
2827
2828                // Thumb-2 UDIV: FBB0 F0F0 | Rn<<16 | Rd<<8 | Rm
2829                let hw1: u16 = (0xFBB0 | rn_bits) as u16;
2830                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
2831
2832                let mut bytes = hw1.to_le_bytes().to_vec();
2833                bytes.extend_from_slice(&hw2.to_le_bytes());
2834                encoding_contracts::verify_thumb32(&bytes);
2835                Ok(bytes)
2836            }
2837
2838            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
2839                let rdlo_bits = reg_to_bits(rdlo);
2840                let rdhi_bits = reg_to_bits(rdhi);
2841                let rn_bits = reg_to_bits(rn);
2842                let rm_bits = reg_to_bits(rm);
2843                reg_bits_checked(rdlo_bits)?;
2844                reg_bits_checked(rdhi_bits)?;
2845                reg_bits_checked(rn_bits)?;
2846                reg_bits_checked(rm_bits)?;
2847
2848                // Thumb-2 UMULL: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm
2849                let hw1: u16 = (0xFBA0 | rn_bits) as u16;
2850                let hw2: u16 = ((rdlo_bits << 12) | (rdhi_bits << 8) | rm_bits) as u16;
2851
2852                let mut bytes = hw1.to_le_bytes().to_vec();
2853                bytes.extend_from_slice(&hw2.to_le_bytes());
2854                encoding_contracts::verify_thumb32(&bytes);
2855                Ok(bytes)
2856            }
2857
2858            // MUL (Thumb-2 32-bit): MUL Rd, Rn, Rm
2859            ArmOp::Mul { rd, rn, rm } => {
2860                let rd_bits = reg_to_bits(rd);
2861                let rn_bits = reg_to_bits(rn);
2862                let rm_bits = reg_to_bits(rm);
2863
2864                // Thumb-2 MUL: FB00 F000 | Rn | Rd<<8 | Rm
2865                // 11111011 0000 Rn | 1111 Rd 0000 Rm
2866                let hw1: u16 = (0xFB00 | rn_bits) as u16;
2867                let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
2868
2869                let mut bytes = hw1.to_le_bytes().to_vec();
2870                bytes.extend_from_slice(&hw2.to_le_bytes());
2871                Ok(bytes)
2872            }
2873
2874            // MLS: Rd = Ra - Rn * Rm
2875            ArmOp::Mls { rd, rn, rm, ra } => {
2876                let rd_bits = reg_to_bits(rd);
2877                let rn_bits = reg_to_bits(rn);
2878                let rm_bits = reg_to_bits(rm);
2879                let ra_bits = reg_to_bits(ra);
2880
2881                // Thumb-2 MLS: FB00 Rn | Ra Rd 0001 Rm
2882                // 11111011 0000 Rn | Ra Rd 0001 Rm
2883                let hw1: u16 = (0xFB00 | rn_bits) as u16;
2884                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | 0x10 | rm_bits) as u16;
2885
2886                let mut bytes = hw1.to_le_bytes().to_vec();
2887                bytes.extend_from_slice(&hw2.to_le_bytes());
2888                Ok(bytes)
2889            }
2890
2891            ArmOp::Mla { rd, rn, rm, ra } => {
2892                let rd_bits = reg_to_bits(rd);
2893                let rn_bits = reg_to_bits(rn);
2894                let rm_bits = reg_to_bits(rm);
2895                let ra_bits = reg_to_bits(ra);
2896
2897                // Thumb-2 MLA: FB00 Rn | Ra Rd 0000 Rm — same as MLS without the
2898                // bit-4 (0x10) op flag. rd = ra + rn*rm.
2899                let hw1: u16 = (0xFB00 | rn_bits) as u16;
2900                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | rm_bits) as u16;
2901
2902                let mut bytes = hw1.to_le_bytes().to_vec();
2903                bytes.extend_from_slice(&hw2.to_le_bytes());
2904                Ok(bytes)
2905            }
2906
2907            // AND (Thumb-2 32-bit)
2908            ArmOp::And { rd, rn, op2 } => {
2909                if let Operand2::Reg(rm) = op2 {
2910                    let rd_bits = reg_to_bits(rd);
2911                    let rn_bits = reg_to_bits(rn);
2912                    let rm_bits = reg_to_bits(rm);
2913
2914                    // Thumb-2 AND register: EA00 Rn | 0 Rd 00 00 Rm
2915                    let hw1: u16 = (0xEA00 | rn_bits) as u16;
2916                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2917
2918                    let mut bytes = hw1.to_le_bytes().to_vec();
2919                    bytes.extend_from_slice(&hw2.to_le_bytes());
2920                    Ok(bytes)
2921                } else if let Operand2::Imm(imm) = op2 {
2922                    let rd_bits = reg_to_bits(rd);
2923                    let rn_bits = reg_to_bits(rn);
2924
2925                    // Thumb-2 AND.W immediate T1: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8.
2926                    // The i:imm3:imm8 field is a ThumbExpandImm modified immediate —
2927                    // encode it correctly (or error on an un-encodable value)
2928                    // rather than packing raw bits, closing the silent-miscompile
2929                    // class for AND alongside ORR/EOR (#251) / ADD/SUB (#253) /
2930                    // CMP (#255).
2931                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
2932                        synth_core::Error::synthesis(
2933                            "AND immediate is not a valid ThumbExpandImm — materialize into a register",
2934                        )
2935                    })?;
2936                    let i_bit = (field >> 11) & 1;
2937                    let imm3 = (field >> 8) & 0x7;
2938                    let imm8 = field & 0xFF;
2939
2940                    let hw1: u16 = (0xF000 | (i_bit << 10) | rn_bits) as u16;
2941                    let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
2942
2943                    let mut bytes = hw1.to_le_bytes().to_vec();
2944                    bytes.extend_from_slice(&hw2.to_le_bytes());
2945                    Ok(bytes)
2946                } else {
2947                    // RegShift variant - fallback to NOP
2948                    let instr: u16 = 0xBF00;
2949                    Ok(instr.to_le_bytes().to_vec())
2950                }
2951            }
2952
2953            // ORR (Thumb-2 32-bit)
2954            ArmOp::Orr { rd, rn, op2 } => {
2955                if let Operand2::Reg(rm) = op2 {
2956                    let rd_bits = reg_to_bits(rd);
2957                    let rn_bits = reg_to_bits(rn);
2958                    let rm_bits = reg_to_bits(rm);
2959
2960                    // Thumb-2 ORR: EA40 Rn | 0 Rd 00 00 Rm
2961                    let hw1: u16 = (0xEA40 | rn_bits) as u16;
2962                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2963
2964                    let mut bytes = hw1.to_le_bytes().to_vec();
2965                    bytes.extend_from_slice(&hw2.to_le_bytes());
2966                    Ok(bytes)
2967                } else if let Operand2::Imm(imm) = op2 {
2968                    // ORR.W immediate T1: 11110 i 0 0010 S Rn | 0 imm3 Rd imm8.
2969                    // Only the zero-extended byte form (imm <= 0xFF) is encoded;
2970                    // larger modified immediates need ThumbExpandImm — return an
2971                    // error rather than silently emit a NOP (Ok-or-Err, #180/#185).
2972                    let imm_val = *imm as u32;
2973                    if imm_val > 0xFF {
2974                        return Err(synth_core::Error::synthesis(
2975                            "ORR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
2976                        ));
2977                    }
2978                    let rd_bits = reg_to_bits(rd);
2979                    let rn_bits = reg_to_bits(rn);
2980                    let hw1: u16 = (0xF040 | rn_bits) as u16;
2981                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
2982                    let mut bytes = hw1.to_le_bytes().to_vec();
2983                    bytes.extend_from_slice(&hw2.to_le_bytes());
2984                    Ok(bytes)
2985                } else {
2986                    let instr: u16 = 0xBF00;
2987                    Ok(instr.to_le_bytes().to_vec())
2988                }
2989            }
2990
2991            // EOR (Thumb-2 32-bit)
2992            ArmOp::Eor { rd, rn, op2 } => {
2993                if let Operand2::Reg(rm) = op2 {
2994                    let rd_bits = reg_to_bits(rd);
2995                    let rn_bits = reg_to_bits(rn);
2996                    let rm_bits = reg_to_bits(rm);
2997
2998                    // Thumb-2 EOR: EA80 Rn | 0 Rd 00 00 Rm
2999                    let hw1: u16 = (0xEA80 | rn_bits) as u16;
3000                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3001
3002                    let mut bytes = hw1.to_le_bytes().to_vec();
3003                    bytes.extend_from_slice(&hw2.to_le_bytes());
3004                    Ok(bytes)
3005                } else if let Operand2::Imm(imm) = op2 {
3006                    // EOR.W immediate T1: 11110 i 0 0100 S Rn | 0 imm3 Rd imm8.
3007                    // Byte form only (imm <= 0xFF); larger needs ThumbExpandImm —
3008                    // error, not a silent NOP (Ok-or-Err, #180/#185).
3009                    let imm_val = *imm as u32;
3010                    if imm_val > 0xFF {
3011                        return Err(synth_core::Error::synthesis(
3012                            "EOR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3013                        ));
3014                    }
3015                    let rd_bits = reg_to_bits(rd);
3016                    let rn_bits = reg_to_bits(rn);
3017                    let hw1: u16 = (0xF080 | rn_bits) as u16;
3018                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3019                    let mut bytes = hw1.to_le_bytes().to_vec();
3020                    bytes.extend_from_slice(&hw2.to_le_bytes());
3021                    Ok(bytes)
3022                } else {
3023                    let instr: u16 = 0xBF00;
3024                    Ok(instr.to_le_bytes().to_vec())
3025                }
3026            }
3027
3028            // Shift operations (16-bit for low registers)
3029            ArmOp::Lsl { rd, rn, shift } => {
3030                let rd_bits = reg_to_bits(rd) as u16;
3031                let rn_bits = reg_to_bits(rn) as u16;
3032                let shift_bits = (*shift as u16) & 0x1F;
3033
3034                if rd_bits < 8 && rn_bits < 8 {
3035                    // LSLS Rd, Rm, #imm5 (16-bit): 0000 0 imm5 Rm Rd
3036                    let instr: u16 = (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3037                    Ok(instr.to_le_bytes().to_vec())
3038                } else {
3039                    // Use 32-bit encoding for high registers
3040                    self.encode_thumb32_shift(rd, rn, *shift, 0b00) // LSL type
3041                }
3042            }
3043
3044            ArmOp::Lsr { rd, rn, shift } => {
3045                let rd_bits = reg_to_bits(rd) as u16;
3046                let rn_bits = reg_to_bits(rn) as u16;
3047                let shift_bits = (*shift as u16) & 0x1F;
3048
3049                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3050                    // LSRS Rd, Rm, #imm5 (16-bit): 0000 1 imm5 Rm Rd
3051                    let instr: u16 = 0x0800 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3052                    Ok(instr.to_le_bytes().to_vec())
3053                } else {
3054                    self.encode_thumb32_shift(rd, rn, *shift, 0b01) // LSR type
3055                }
3056            }
3057
3058            ArmOp::Asr { rd, rn, shift } => {
3059                let rd_bits = reg_to_bits(rd) as u16;
3060                let rn_bits = reg_to_bits(rn) as u16;
3061                let shift_bits = (*shift as u16) & 0x1F;
3062
3063                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3064                    // ASRS Rd, Rm, #imm5 (16-bit): 0001 0 imm5 Rm Rd
3065                    let instr: u16 = 0x1000 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3066                    Ok(instr.to_le_bytes().to_vec())
3067                } else {
3068                    self.encode_thumb32_shift(rd, rn, *shift, 0b10) // ASR type
3069                }
3070            }
3071
3072            ArmOp::Ror { rd, rn, shift } => {
3073                // ROR doesn't have a 16-bit immediate form, use 32-bit
3074                self.encode_thumb32_shift(rd, rn, *shift, 0b11) // ROR type
3075            }
3076
3077            // Register-based shifts (Thumb-2 32-bit)
3078            // Encoding: 11111010 0xxS Rn 1111 Rd 0000 Rm
3079            // xx = shift type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
3080            ArmOp::LslReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b00),
3081            ArmOp::LsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b01),
3082            ArmOp::AsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b10),
3083            ArmOp::RorReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b11),
3084
3085            // RSB (Reverse Subtract): Rd = imm - Rn
3086            // Thumb-2 T2 encoding: 11110 i 0 1110 S Rn | 0 imm3 Rd imm8
3087            ArmOp::Rsb { rd, rn, imm } => {
3088                let rd_bits = reg_to_bits(rd);
3089                let rn_bits = reg_to_bits(rn);
3090                let imm_val = *imm;
3091
3092                let i_bit = (imm_val >> 11) & 1;
3093                let imm3 = (imm_val >> 8) & 0x7;
3094                let imm8 = imm_val & 0xFF;
3095
3096                // hw1: 11110 i 01110 0 Rn  (S=0)
3097                let hw1: u16 = (0xF1C0 | (i_bit << 10) | rn_bits) as u16;
3098                // hw2: 0 imm3 Rd imm8
3099                let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3100
3101                let mut bytes = hw1.to_le_bytes().to_vec();
3102                bytes.extend_from_slice(&hw2.to_le_bytes());
3103                Ok(bytes)
3104            }
3105
3106            // CLZ (Thumb-2 32-bit)
3107            ArmOp::Clz { rd, rm } => {
3108                let rd_bits = reg_to_bits(rd);
3109                let rm_bits = reg_to_bits(rm);
3110
3111                // Thumb-2 CLZ: FAB0 Rm | F8 Rd Rm
3112                // 11111010 1011 Rm | 1111 1000 Rd Rm
3113                let hw1: u16 = (0xFAB0 | rm_bits) as u16;
3114                let hw2: u16 = (0xF080 | (rd_bits << 8) | rm_bits) as u16;
3115
3116                let mut bytes = hw1.to_le_bytes().to_vec();
3117                bytes.extend_from_slice(&hw2.to_le_bytes());
3118                Ok(bytes)
3119            }
3120
3121            // RBIT (Thumb-2 32-bit)
3122            ArmOp::Rbit { rd, rm } => {
3123                let rd_bits = reg_to_bits(rd);
3124                let rm_bits = reg_to_bits(rm);
3125
3126                // Thumb-2 RBIT: FA90 Rm | F0 Rd A0 Rm
3127                // 11111010 1001 Rm | 1111 Rd 1010 Rm
3128                let hw1: u16 = (0xFA90 | rm_bits) as u16;
3129                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rm_bits) as u16;
3130
3131                let mut bytes = hw1.to_le_bytes().to_vec();
3132                bytes.extend_from_slice(&hw2.to_le_bytes());
3133                Ok(bytes)
3134            }
3135
3136            // SXTB (16-bit for low registers)
3137            ArmOp::Sxtb { rd, rm } => {
3138                let rd_bits = reg_to_bits(rd) as u16;
3139                let rm_bits = reg_to_bits(rm) as u16;
3140
3141                if rd_bits < 8 && rm_bits < 8 {
3142                    // SXTB Rd, Rm (16-bit): 1011 0010 01 Rm Rd
3143                    let instr: u16 = 0xB240 | (rm_bits << 3) | rd_bits;
3144                    Ok(instr.to_le_bytes().to_vec())
3145                } else {
3146                    // Thumb-2 SXTB.W: FA4F F(rd)80 (rm)
3147                    // 11111010 0100 1111 | 1111 Rd 10 rotate Rm
3148                    let rd_bits32 = rd_bits as u32;
3149                    let rm_bits32 = rm_bits as u32;
3150                    let hw1: u16 = 0xFA4F;
3151                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3152                    let mut bytes = hw1.to_le_bytes().to_vec();
3153                    bytes.extend_from_slice(&hw2.to_le_bytes());
3154                    Ok(bytes)
3155                }
3156            }
3157
3158            // SXTH (16-bit for low registers)
3159            ArmOp::Sxth { rd, rm } => {
3160                let rd_bits = reg_to_bits(rd) as u16;
3161                let rm_bits = reg_to_bits(rm) as u16;
3162
3163                if rd_bits < 8 && rm_bits < 8 {
3164                    // SXTH Rd, Rm (16-bit): 1011 0010 00 Rm Rd
3165                    let instr: u16 = 0xB200 | (rm_bits << 3) | rd_bits;
3166                    Ok(instr.to_le_bytes().to_vec())
3167                } else {
3168                    // Thumb-2 SXTH.W: FA0F F(rd)80 (rm)
3169                    // 11111010 0000 1111 | 1111 Rd 10 rotate Rm
3170                    let rd_bits32 = rd_bits as u32;
3171                    let rm_bits32 = rm_bits as u32;
3172                    let hw1: u16 = 0xFA0F;
3173                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3174                    let mut bytes = hw1.to_le_bytes().to_vec();
3175                    bytes.extend_from_slice(&hw2.to_le_bytes());
3176                    Ok(bytes)
3177                }
3178            }
3179
3180            // UXTB Rd,Rm — zero-extend byte (rd = rm & 0xff)
3181            ArmOp::Uxtb { rd, rm } => {
3182                let rd_bits = reg_to_bits(rd) as u16;
3183                let rm_bits = reg_to_bits(rm) as u16;
3184                if rd_bits < 8 && rm_bits < 8 {
3185                    // UXTB Rd, Rm (16-bit): 1011 0010 11 Rm Rd
3186                    let instr: u16 = 0xB2C0 | (rm_bits << 3) | rd_bits;
3187                    Ok(instr.to_le_bytes().to_vec())
3188                } else {
3189                    // Thumb-2 UXTB.W: FA5F F(rd)80 (rm)
3190                    let hw1: u16 = 0xFA5F;
3191                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3192                    let mut bytes = hw1.to_le_bytes().to_vec();
3193                    bytes.extend_from_slice(&hw2.to_le_bytes());
3194                    Ok(bytes)
3195                }
3196            }
3197
3198            // UXTH Rd,Rm — zero-extend halfword (rd = rm & 0xffff)
3199            ArmOp::Uxth { rd, rm } => {
3200                let rd_bits = reg_to_bits(rd) as u16;
3201                let rm_bits = reg_to_bits(rm) as u16;
3202                if rd_bits < 8 && rm_bits < 8 {
3203                    // UXTH Rd, Rm (16-bit): 1011 0010 10 Rm Rd
3204                    let instr: u16 = 0xB280 | (rm_bits << 3) | rd_bits;
3205                    Ok(instr.to_le_bytes().to_vec())
3206                } else {
3207                    // Thumb-2 UXTH.W: FA1F F(rd)80 (rm)
3208                    let hw1: u16 = 0xFA1F;
3209                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3210                    let mut bytes = hw1.to_le_bytes().to_vec();
3211                    bytes.extend_from_slice(&hw2.to_le_bytes());
3212                    Ok(bytes)
3213                }
3214            }
3215
3216            // CMP (can be 16-bit for low registers)
3217            ArmOp::Cmp { rn, op2 } => {
3218                let rn_bits = reg_to_bits(rn) as u16;
3219
3220                if let Operand2::Imm(imm) = op2 {
3221                    // Only use 16-bit encoding for non-negative immediates 0-255
3222                    // Negative immediates must use 32-bit encoding
3223                    if *imm >= 0 && *imm <= 255 && rn_bits < 8 {
3224                        // CMP Rn, #imm8 (16-bit): 0010 1 Rn imm8
3225                        let instr: u16 = 0x2800 | (rn_bits << 8) | (*imm as u16 & 0xFF);
3226                        Ok(instr.to_le_bytes().to_vec())
3227                    } else {
3228                        self.encode_thumb32_cmp_imm(rn, *imm as u32)
3229                    }
3230                } else if let Operand2::Reg(rm) = op2 {
3231                    let rm_bits = reg_to_bits(rm) as u16;
3232                    if rn_bits < 8 && rm_bits < 8 {
3233                        // CMP Rn, Rm (16-bit low): 0100 0010 10 Rm Rn
3234                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
3235                        Ok(instr.to_le_bytes().to_vec())
3236                    } else {
3237                        // CMP Rn, Rm (16-bit high): 0100 0101 N Rm Rn[2:0]
3238                        let n_bit = (rn_bits >> 3) & 1;
3239                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
3240                        Ok(instr.to_le_bytes().to_vec())
3241                    }
3242                } else {
3243                    let instr: u16 = 0xBF00;
3244                    Ok(instr.to_le_bytes().to_vec())
3245                }
3246            }
3247
3248            // CMN (Compare Negative) - computes Rn + op2 and sets flags
3249            // CMN Rn, #1 sets Z flag if Rn == -1 (since -1 + 1 = 0)
3250            ArmOp::Cmn { rn, op2 } => {
3251                let rn_bits = reg_to_bits(rn) as u16;
3252
3253                if let Operand2::Imm(imm) = op2 {
3254                    // CMN.W Rn, #imm (32-bit): i:imm3:imm8 is a ThumbExpandImm
3255                    // modified immediate (the field sits in imm3=hw2[14:12],
3256                    // imm8=hw2[7:0], i=hw1[10]). Encode it correctly, or error on
3257                    // an un-encodable value — replacing the old silent `0xBF00`
3258                    // NOP (the last of the silent-miscompile data-proc encoders).
3259                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3260                        synth_core::Error::synthesis(
3261                            "CMN immediate is not a valid ThumbExpandImm — materialize into a register",
3262                        )
3263                    })?;
3264                    let i_bit = (field >> 11) & 1;
3265                    let imm3 = (field >> 8) & 0x7;
3266                    let imm8 = field & 0xFF;
3267                    let hw1: u16 = (0xF110 | (i_bit << 10) as u16) | rn_bits;
3268                    let hw2: u16 = (imm3 << 12) as u16 | 0x0F00 | imm8 as u16;
3269                    let mut bytes = hw1.to_le_bytes().to_vec();
3270                    bytes.extend_from_slice(&hw2.to_le_bytes());
3271                    Ok(bytes)
3272                } else if let Operand2::Reg(rm) = op2 {
3273                    let rm_bits = reg_to_bits(rm) as u16;
3274                    // 16-bit CMN (T1) only encodes R0-R7; high registers overflow
3275                    // the 3-bit fields and corrupt the operands (#184, the #180
3276                    // class). CMN has no high-register 16-bit form, so fall back
3277                    // to 32-bit CMN.W (T2): EB10 Rn | 0F00 Rm (ADD.W with S=1 and
3278                    // Rd discarded as PC/1111).
3279                    if rn_bits < 8 && rm_bits < 8 {
3280                        // CMN Rn, Rm (16-bit): 0100 0010 11 Rm Rn
3281                        let instr: u16 = 0x42C0 | (rm_bits << 3) | rn_bits;
3282                        Ok(instr.to_le_bytes().to_vec())
3283                    } else {
3284                        let hw1: u16 = 0xEB10 | rn_bits;
3285                        let hw2: u16 = 0x0F00 | rm_bits;
3286                        let mut bytes = hw1.to_le_bytes().to_vec();
3287                        bytes.extend_from_slice(&hw2.to_le_bytes());
3288                        Ok(bytes)
3289                    }
3290                } else {
3291                    Ok(vec![0xBF, 0x00])
3292                }
3293            }
3294
3295            // LDR (can be 16-bit for simple cases)
3296            ArmOp::Ldr { rd, addr } => {
3297                let rd_bits = reg_to_bits(rd);
3298                let base_bits = reg_to_bits(&addr.base);
3299
3300                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3301                if let Some(offset_reg) = &addr.offset_reg {
3302                    let rm_bits = reg_to_bits(offset_reg);
3303
3304                    // If there's also an immediate offset, we need to ADD it first
3305                    if addr.offset != 0 {
3306                        // Use R12 (IP) as scratch to avoid clobbering the address register
3307                        // ADD R12, Rm, #offset; LDR Rd, [base, R12]
3308                        let scratch = Reg::R12;
3309                        let mut bytes =
3310                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3311                        bytes.extend(self.encode_thumb32_ldr_reg(rd, &addr.base, &scratch)?);
3312                        return Ok(bytes);
3313                    }
3314
3315                    // Simple register offset: LDR Rd, [Rn, Rm]
3316                    // 16-bit: only if Rd, Rn, Rm < R8
3317                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3318                        // LDR Rd, [Rn, Rm] (16-bit): 0101 100 Rm Rn Rd
3319                        let instr: u16 = 0x5800
3320                            | ((rm_bits as u16) << 6)
3321                            | ((base_bits as u16) << 3)
3322                            | (rd_bits as u16);
3323                        return Ok(instr.to_le_bytes().to_vec());
3324                    }
3325
3326                    // 32-bit register offset
3327                    return self.encode_thumb32_ldr_reg(rd, &addr.base, offset_reg);
3328                }
3329
3330                // Immediate offset mode [base, #imm]
3331                let offset = addr.offset as u32;
3332
3333                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3334                    // LDR Rd, [Rn, #imm5*4] (16-bit): 0110 1 imm5 Rn Rd
3335                    let imm5 = (offset >> 2) as u16;
3336                    let instr: u16 =
3337                        0x6800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3338                    Ok(instr.to_le_bytes().to_vec())
3339                } else {
3340                    self.encode_thumb32_ldr(rd, &addr.base, offset)
3341                }
3342            }
3343
3344            // STR (can be 16-bit for simple cases)
3345            ArmOp::Str { rd, addr } => {
3346                let rd_bits = reg_to_bits(rd);
3347                let base_bits = reg_to_bits(&addr.base);
3348
3349                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3350                if let Some(offset_reg) = &addr.offset_reg {
3351                    let rm_bits = reg_to_bits(offset_reg);
3352
3353                    // If there's also an immediate offset, we need to ADD it first
3354                    if addr.offset != 0 {
3355                        // Use R12 (IP) as scratch to avoid clobbering the address register
3356                        // ADD R12, Rm, #offset; STR Rd, [base, R12]
3357                        let scratch = Reg::R12;
3358                        let mut bytes =
3359                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3360                        bytes.extend(self.encode_thumb32_str_reg(rd, &addr.base, &scratch)?);
3361                        return Ok(bytes);
3362                    }
3363
3364                    // Simple register offset: STR Rd, [Rn, Rm]
3365                    // 16-bit: only if Rd, Rn, Rm < R8
3366                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3367                        // STR Rd, [Rn, Rm] (16-bit): 0101 000 Rm Rn Rd
3368                        let instr: u16 = 0x5000
3369                            | ((rm_bits as u16) << 6)
3370                            | ((base_bits as u16) << 3)
3371                            | (rd_bits as u16);
3372                        return Ok(instr.to_le_bytes().to_vec());
3373                    }
3374
3375                    // 32-bit register offset
3376                    return self.encode_thumb32_str_reg(rd, &addr.base, offset_reg);
3377                }
3378
3379                // Immediate offset mode [base, #imm]
3380                let offset = addr.offset as u32;
3381
3382                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3383                    // STR Rd, [Rn, #imm5*4] (16-bit): 0110 0 imm5 Rn Rd
3384                    let imm5 = (offset >> 2) as u16;
3385                    let instr: u16 =
3386                        0x6000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3387                    Ok(instr.to_le_bytes().to_vec())
3388                } else {
3389                    self.encode_thumb32_str(rd, &addr.base, offset)
3390                }
3391            }
3392
3393            // LDRB (Thumb-2)
3394            ArmOp::Ldrb { rd, addr } => {
3395                let rd_bits = reg_to_bits(rd);
3396                let base_bits = reg_to_bits(&addr.base);
3397
3398                if let Some(offset_reg) = &addr.offset_reg {
3399                    if addr.offset != 0 {
3400                        let scratch = Reg::R12;
3401                        let mut bytes =
3402                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3403                        bytes.extend(self.encode_thumb32_ldrb_reg(rd, &addr.base, &scratch)?);
3404                        return Ok(bytes);
3405                    }
3406                    return self.encode_thumb32_ldrb_reg(rd, &addr.base, offset_reg);
3407                }
3408
3409                let offset = addr.offset as u32;
3410                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3411                    // LDRB Rd, [Rn, #imm5] (16-bit): 0111 1 imm5 Rn Rd
3412                    let instr: u16 = 0x7800
3413                        | ((offset as u16) << 6)
3414                        | ((base_bits as u16) << 3)
3415                        | (rd_bits as u16);
3416                    Ok(instr.to_le_bytes().to_vec())
3417                } else {
3418                    self.encode_thumb32_ldrb_imm(rd, &addr.base, offset)
3419                }
3420            }
3421
3422            // LDRSB (Thumb-2)
3423            ArmOp::Ldrsb { rd, addr } => {
3424                let rd_bits = reg_to_bits(rd);
3425                let base_bits = reg_to_bits(&addr.base);
3426
3427                if let Some(offset_reg) = &addr.offset_reg {
3428                    if addr.offset != 0 {
3429                        let scratch = Reg::R12;
3430                        let mut bytes =
3431                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3432                        bytes.extend(self.encode_thumb32_ldrsb_reg(rd, &addr.base, &scratch)?);
3433                        return Ok(bytes);
3434                    }
3435                    return self.encode_thumb32_ldrsb_reg(rd, &addr.base, offset_reg);
3436                }
3437
3438                let offset = addr.offset as u32;
3439                // LDRSB has no 16-bit immediate form (only register)
3440                // For 16-bit reg form: only if Rd, Rn, Rm < R8
3441                if rd_bits < 8 && base_bits < 8 && offset == 0 {
3442                    // No immediate 16-bit encoding for LDRSB; use 32-bit
3443                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3444                } else {
3445                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3446                }
3447            }
3448
3449            // LDRH (Thumb-2)
3450            ArmOp::Ldrh { rd, addr } => {
3451                let rd_bits = reg_to_bits(rd);
3452                let base_bits = reg_to_bits(&addr.base);
3453
3454                if let Some(offset_reg) = &addr.offset_reg {
3455                    if addr.offset != 0 {
3456                        let scratch = Reg::R12;
3457                        let mut bytes =
3458                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3459                        bytes.extend(self.encode_thumb32_ldrh_reg(rd, &addr.base, &scratch)?);
3460                        return Ok(bytes);
3461                    }
3462                    return self.encode_thumb32_ldrh_reg(rd, &addr.base, offset_reg);
3463                }
3464
3465                let offset = addr.offset as u32;
3466                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3467                    // LDRH Rd, [Rn, #imm5*2] (16-bit): 1000 1 imm5 Rn Rd
3468                    let imm5 = (offset >> 1) as u16;
3469                    let instr: u16 =
3470                        0x8800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3471                    Ok(instr.to_le_bytes().to_vec())
3472                } else {
3473                    self.encode_thumb32_ldrh_imm(rd, &addr.base, offset)
3474                }
3475            }
3476
3477            // LDRSH (Thumb-2)
3478            ArmOp::Ldrsh { rd, addr } => {
3479                if let Some(offset_reg) = &addr.offset_reg {
3480                    if addr.offset != 0 {
3481                        let scratch = Reg::R12;
3482                        let mut bytes =
3483                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3484                        bytes.extend(self.encode_thumb32_ldrsh_reg(rd, &addr.base, &scratch)?);
3485                        return Ok(bytes);
3486                    }
3487                    return self.encode_thumb32_ldrsh_reg(rd, &addr.base, offset_reg);
3488                }
3489
3490                let offset = addr.offset as u32;
3491                self.encode_thumb32_ldrsh_imm(rd, &addr.base, offset)
3492            }
3493
3494            // STRB (Thumb-2)
3495            ArmOp::Strb { rd, addr } => {
3496                let rd_bits = reg_to_bits(rd);
3497                let base_bits = reg_to_bits(&addr.base);
3498
3499                if let Some(offset_reg) = &addr.offset_reg {
3500                    if addr.offset != 0 {
3501                        let scratch = Reg::R12;
3502                        let mut bytes =
3503                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3504                        bytes.extend(self.encode_thumb32_strb_reg(rd, &addr.base, &scratch)?);
3505                        return Ok(bytes);
3506                    }
3507                    return self.encode_thumb32_strb_reg(rd, &addr.base, offset_reg);
3508                }
3509
3510                let offset = addr.offset as u32;
3511                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3512                    // STRB Rd, [Rn, #imm5] (16-bit): 0111 0 imm5 Rn Rd
3513                    let instr: u16 = 0x7000
3514                        | ((offset as u16) << 6)
3515                        | ((base_bits as u16) << 3)
3516                        | (rd_bits as u16);
3517                    Ok(instr.to_le_bytes().to_vec())
3518                } else {
3519                    self.encode_thumb32_strb_imm(rd, &addr.base, offset)
3520                }
3521            }
3522
3523            // STRH (Thumb-2)
3524            ArmOp::Strh { rd, addr } => {
3525                let rd_bits = reg_to_bits(rd);
3526                let base_bits = reg_to_bits(&addr.base);
3527
3528                if let Some(offset_reg) = &addr.offset_reg {
3529                    if addr.offset != 0 {
3530                        let scratch = Reg::R12;
3531                        let mut bytes =
3532                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3533                        bytes.extend(self.encode_thumb32_strh_reg(rd, &addr.base, &scratch)?);
3534                        return Ok(bytes);
3535                    }
3536                    return self.encode_thumb32_strh_reg(rd, &addr.base, offset_reg);
3537                }
3538
3539                let offset = addr.offset as u32;
3540                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3541                    // STRH Rd, [Rn, #imm5*2] (16-bit): 1000 0 imm5 Rn Rd
3542                    let imm5 = (offset >> 1) as u16;
3543                    let instr: u16 =
3544                        0x8000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3545                    Ok(instr.to_le_bytes().to_vec())
3546                } else {
3547                    self.encode_thumb32_strh_imm(rd, &addr.base, offset)
3548                }
3549            }
3550
3551            // MemorySize (Thumb-2)
3552            ArmOp::MemorySize { rd } => {
3553                // LSR rd, R10, #16 — memory size in bytes / 65536 = pages
3554                // Thumb-2 16-bit: LSRS Rd, Rm, #imm5 — 0000 1 imm5 Rm Rd
3555                let rd_bits = reg_to_bits(rd);
3556                let r10_bits = reg_to_bits(&Reg::R10);
3557                if rd_bits < 8 && r10_bits < 8 {
3558                    let instr: u16 =
3559                        0x0800 | (16u16 << 6) | ((r10_bits as u16) << 3) | (rd_bits as u16);
3560                    Ok(instr.to_le_bytes().to_vec())
3561                } else {
3562                    // Thumb-2 32-bit LSR: 1110 1010 010 0 1111 | 0 imm3 Rd imm2 01 Rm
3563                    let imm5: u32 = 16;
3564                    let imm3 = (imm5 >> 2) & 0x7;
3565                    let imm2 = imm5 & 0x3;
3566                    let hw1: u16 = 0xEA4F;
3567                    let hw2: u16 =
3568                        ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | 0x10 | r10_bits) as u16;
3569                    let mut bytes = hw1.to_le_bytes().to_vec();
3570                    bytes.extend_from_slice(&hw2.to_le_bytes());
3571                    Ok(bytes)
3572                }
3573            }
3574
3575            // MemoryGrow (Thumb-2)
3576            ArmOp::MemoryGrow { rd, .. } => {
3577                // On embedded with fixed memory, always return -1 (failure)
3578                // MVN rd, #0 → MOV rd, #-1
3579                // Thumb-2 32-bit: MVN: 1111 0 i 0 0 0 1 1 0 1111 | 0 imm3 Rd imm8
3580                let rd_bits = reg_to_bits(rd);
3581                let hw1: u16 = 0xF06F; // MVN with i=0
3582                let hw2: u16 = (rd_bits << 8) as u16; // imm8=0 → ~0 = 0xFFFFFFFF = -1
3583                let mut bytes = hw1.to_le_bytes().to_vec();
3584                bytes.extend_from_slice(&hw2.to_le_bytes());
3585                Ok(bytes)
3586            }
3587
3588            // BX (16-bit)
3589            ArmOp::Bx { rm } => {
3590                let rm_bits = reg_to_bits(rm) as u16;
3591                // BX Rm (16-bit): 0100 0111 0 Rm 000
3592                let instr: u16 = 0x4700 | (rm_bits << 3);
3593                Ok(instr.to_le_bytes().to_vec())
3594            }
3595
3596            // BLX (16-bit) - Branch with Link and Exchange
3597            // BLX Rm: 0100 0111 1 Rm 000
3598            ArmOp::Blx { rm } => {
3599                let rm_bits = reg_to_bits(rm) as u16;
3600                let instr: u16 = 0x4780 | (rm_bits << 3);
3601                Ok(instr.to_le_bytes().to_vec())
3602            }
3603
3604            // CallIndirect - indirect function call via table lookup
3605            // table_index_reg contains the table index
3606            // Generates: LSL R12, idx, #2; LDR R12, [R12, table_base]; BLX R12
3607            ArmOp::CallIndirect {
3608                rd: _,
3609                type_idx: _,
3610                table_index_reg,
3611            } => {
3612                let idx_reg = reg_to_bits(table_index_reg);
3613                let mut bytes = Vec::new();
3614
3615                // For now, we generate code that:
3616                // 1. Multiplies index by 4 (function pointer size)
3617                // 2. Loads function pointer from table (assumes table base in R11)
3618                // 3. Calls the function via BLX
3619                //
3620                // Table base setup must be done by caller/runtime.
3621                // This is a simplified implementation - full support needs:
3622                // - Table base address resolution
3623                // - Type signature checking
3624                // - Bounds checking
3625
3626                // LSL R12, idx_reg, #2 (multiply index by 4)
3627                // Thumb-2 MOV with shift: 11101010 010 S 1111 | 0 imm3 Rd imm2 type Rm
3628                // LSL: type=00 (bits 5:4), imm5=2 -> imm3=000, imm2=10 (bits 7:6)
3629                // #597: the shift amount was previously shifted into bits 5:4 —
3630                // the TYPE field — encoding `mov.w ip, rm, ASR #32`, which
3631                // destroyed the index and dispatched table entry 0 for every
3632                // call. imm2 lives at bits 7:6.
3633                let hw1: u16 = 0xEA4F_u16; // MOV.W R12, Rm, LSL #2
3634                let hw2: u16 = ((0x0C00 | (0b10 << 6)) | idx_reg) as u16;
3635                bytes.extend_from_slice(&hw1.to_le_bytes());
3636                bytes.extend_from_slice(&hw2.to_le_bytes());
3637
3638                // LDR R12, [R11, R12] - load function pointer
3639                // Thumb-2 LDR (register): 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
3640                // Rn=R11, Rt=R12, Rm=R12, imm2=00 (no shift)
3641                let ldr_hw1: u16 = 0xF85B; // LDR.W Rt, [R11, Rm]
3642                let ldr_hw2: u16 = 0xC00C; // Rt=R12, imm2=00, Rm=R12
3643                bytes.extend_from_slice(&ldr_hw1.to_le_bytes());
3644                bytes.extend_from_slice(&ldr_hw2.to_le_bytes());
3645
3646                // BLX R12 (call function indirectly)
3647                // BLX Rm (16-bit): 0100 0111 1 Rm 000
3648                let blx: u16 = 0x47E0; // BLX R12
3649                bytes.extend_from_slice(&blx.to_le_bytes());
3650
3651                Ok(bytes)
3652            }
3653
3654            // Label pseudo-instruction: emits no machine code
3655            ArmOp::Label { .. } => Ok(Vec::new()),
3656
3657            // Conditional branch to label (generic) - offset 0, will be patched
3658            ArmOp::Bcc { cond, label: _ } => {
3659                use synth_synthesis::Condition;
3660                let cond_bits: u16 = match cond {
3661                    Condition::EQ => 0x0,
3662                    Condition::NE => 0x1,
3663                    Condition::HS => 0x2,
3664                    Condition::LO => 0x3,
3665                    Condition::HI => 0x8,
3666                    Condition::LS => 0x9,
3667                    Condition::GE => 0xA,
3668                    Condition::LT => 0xB,
3669                    Condition::GT => 0xC,
3670                    Condition::LE => 0xD,
3671                };
3672                // 16-bit B<cond> with offset 0: 1101 cond imm8
3673                let instr: u16 = 0xD000 | (cond_bits << 8);
3674                Ok(instr.to_le_bytes().to_vec())
3675            }
3676
3677            // Branch instructions
3678            ArmOp::B { label: _ } => {
3679                // Simplified: B.N with offset 0
3680                // For real usage, would need label resolution
3681                let instr: u16 = 0xE000; // B.N #0
3682                Ok(instr.to_le_bytes().to_vec())
3683            }
3684
3685            // BHS (Branch if Higher or Same) - used for bounds checking
3686            // Condition code: 0x2 (C set)
3687            ArmOp::Bhs { label: _ } => {
3688                // 16-bit B<cond> with offset 0: 1101 cond imm8
3689                // cond = 0x2 (HS)
3690                let instr: u16 = 0xD200; // BHS.N #0
3691                Ok(instr.to_le_bytes().to_vec())
3692            }
3693
3694            // BLO (Branch if Lower) - complementary to BHS
3695            // Condition code: 0x3 (C clear)
3696            ArmOp::Blo { label: _ } => {
3697                // 16-bit B<cond> with offset 0: 1101 cond imm8
3698                // cond = 0x3 (LO)
3699                let instr: u16 = 0xD300; // BLO.N #0
3700                Ok(instr.to_le_bytes().to_vec())
3701            }
3702
3703            // Branch with numeric offset (Thumb-2)
3704            // Thumb-2 B.W instruction: 32-bit with +-16MB range
3705            ArmOp::BOffset { offset } => {
3706                // offset is already the halfword displacement: (target - branch - 4) / 2
3707                // This is the raw encoded value, accounting for variable-length instructions
3708                let halfword_offset = *offset;
3709
3710                // 16-bit B.N encoding: 1110 0 imm11 (11-bit signed halfword offset)
3711                // Range: -1024 to +1022 halfwords
3712                if (-1024..=1022).contains(&halfword_offset) {
3713                    // 16-bit B.N encoding: 1110 0 imm11
3714                    let imm11 = (halfword_offset as u16) & 0x7FF;
3715                    let instr: u16 = 0xE000 | imm11;
3716                    Ok(instr.to_le_bytes().to_vec())
3717                } else {
3718                    // 32-bit B.W encoding for larger offsets
3719                    // First halfword: 1111 0 S imm10
3720                    // Second halfword: 10 J1 0 J2 imm11
3721                    // Total offset = SignExtend(S:I1:I2:imm10:imm11:0)
3722                    // where I1 = NOT(J1 XOR S), I2 = NOT(J2 XOR S)
3723
3724                    // The B.W (T4) encoding packs the signed offset as:
3725                    //   S:I1:I2:imm10:imm11:0  (25-bit signed, halfword-aligned)
3726                    // where J1 = NOT(I1 XOR S), J2 = NOT(I2 XOR S)
3727                    // Input halfword_offset already equals (target - PC - 4) / 2,
3728                    // so the full byte offset = halfword_offset << 1.
3729                    // The encoding fields split that 25-bit signed value (including the
3730                    // implicit trailing zero) as: S | imm10 | imm11
3731                    // with I1 = bit 23 and I2 = bit 22 of the signed offset.
3732                    let signed_offset = halfword_offset << 1; // byte offset
3733                    let s = if signed_offset < 0 { 1u32 } else { 0u32 };
3734                    let uoffset = signed_offset as u32;
3735                    let imm10 = (uoffset >> 12) & 0x3FF; // bits [21:12]
3736                    let imm11 = (uoffset >> 1) & 0x7FF; // bits [11:1]
3737                    let i1 = (uoffset >> 23) & 1; // bit 23
3738                    let i2 = (uoffset >> 22) & 1; // bit 22
3739                    let j1 = (!(i1 ^ s)) & 1; // J1 = NOT(I1 XOR S)
3740                    let j2 = (!(i2 ^ s)) & 1; // J2 = NOT(I2 XOR S)
3741
3742                    let hw1: u16 = (0xF000 | (s << 10) | imm10) as u16;
3743                    let hw2: u16 = (0x9000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
3744
3745                    let mut bytes = hw1.to_le_bytes().to_vec();
3746                    bytes.extend_from_slice(&hw2.to_le_bytes());
3747                    Ok(bytes)
3748                }
3749            }
3750
3751            // Conditional branch with numeric offset (Thumb-2)
3752            ArmOp::BCondOffset { cond, offset } => {
3753                use synth_synthesis::Condition;
3754                let cond_bits: u16 = match cond {
3755                    Condition::EQ => 0x0,
3756                    Condition::NE => 0x1,
3757                    Condition::HS => 0x2,
3758                    Condition::LO => 0x3,
3759                    Condition::HI => 0x8,
3760                    Condition::LS => 0x9,
3761                    Condition::GE => 0xA,
3762                    Condition::LT => 0xB,
3763                    Condition::GT => 0xC,
3764                    Condition::LE => 0xD,
3765                };
3766
3767                // offset is already the halfword displacement: (target - branch - 4) / 2
3768                // This is the raw imm8 value for 16-bit B<cond> encoding
3769                let halfword_offset = *offset;
3770
3771                // 16-bit B<cond> encoding: 1101 cond imm8
3772                // Range: -256 to +254 halfwords (imm8 is sign-extended and shifted left 1)
3773                if (-128..=127).contains(&halfword_offset) {
3774                    let imm8 = (halfword_offset as u16) & 0xFF;
3775                    let instr: u16 = 0xD000 | (cond_bits << 8) | imm8;
3776                    Ok(instr.to_le_bytes().to_vec())
3777                } else {
3778                    // 32-bit B<cond>.W for larger offsets
3779                    // First halfword: 1111 0 S cond imm6
3780                    // Second halfword: 10 J1 0 J2 imm11
3781                    let offset = halfword_offset >> 1;
3782                    let s = if offset < 0 { 1u32 } else { 0u32 };
3783                    let imm6 = ((offset >> 11) as u32) & 0x3F;
3784                    let imm11 = (offset as u32) & 0x7FF;
3785                    let j1 = if s == 1 { 1 } else { 0 };
3786                    let j2 = if s == 1 { 1 } else { 0 };
3787
3788                    let hw1: u16 = (0xF000 | (s << 10) | ((cond_bits as u32) << 6) | imm6) as u16;
3789                    let hw2: u16 = (0x8000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
3790
3791                    let mut bytes = hw1.to_le_bytes().to_vec();
3792                    bytes.extend_from_slice(&hw2.to_le_bytes());
3793                    Ok(bytes)
3794                }
3795            }
3796
3797            ArmOp::Bl { label: _ } => {
3798                // BL is always 32-bit in Thumb-2, encoded here as a relocatable
3799                // placeholder; an R_ARM_THM_CALL relocation patches the target
3800                // (see arm_backend.rs). The placeholder must carry an embedded
3801                // addend of -4 so the relocation nets to exactly the symbol S.
3802                //
3803                // Thumb BL computes `target = (P + 4) + signed_offset`. Under
3804                // R_ARM_THM_CALL the linker resolves using the in-place addend;
3805                // a 0xF800 placeholder (addend 0) lands at S+4 — every call one
3806                // instruction past the callee entry (#174). The correct
3807                // placeholder is what `gas` emits for `bl <extern>`:
3808                //   f7ff fffe  ->  `bl <self>`  (S=1, J1=J2=1, imm = -4 addend),
3809                // i.e. hw1=0xF7FF, hw2=0xFFFE. This nets to S, not S+4.
3810                // (The earlier 0xD000 was worse still — a ~+0x600000 addend,
3811                // the garbage `bl c0000c` and "truncated to fit" of #167.)
3812                let hw1: u16 = 0xF7FF;
3813                let hw2: u16 = 0xFFFE;
3814                let mut bytes = hw1.to_le_bytes().to_vec();
3815                bytes.extend_from_slice(&hw2.to_le_bytes());
3816                Ok(bytes)
3817            }
3818
3819            // MVN
3820            ArmOp::Mvn { rd, op2 } => {
3821                if let Operand2::Reg(rm) = op2 {
3822                    let rd_bits = reg_to_bits(rd) as u16;
3823                    let rm_bits = reg_to_bits(rm) as u16;
3824
3825                    if rd_bits < 8 && rm_bits < 8 {
3826                        // MVNS Rd, Rm (16-bit): 0100 0011 11 Rm Rd
3827                        let instr: u16 = 0x43C0 | (rm_bits << 3) | rd_bits;
3828                        Ok(instr.to_le_bytes().to_vec())
3829                    } else {
3830                        // 32-bit MVN
3831                        let hw1: u16 = 0xEA6F_u16;
3832                        let hw2: u16 = ((reg_to_bits(rd) << 8) | reg_to_bits(rm)) as u16;
3833                        let mut bytes = hw1.to_le_bytes().to_vec();
3834                        bytes.extend_from_slice(&hw2.to_le_bytes());
3835                        Ok(bytes)
3836                    }
3837                } else {
3838                    let instr: u16 = 0xBF00;
3839                    Ok(instr.to_le_bytes().to_vec())
3840                }
3841            }
3842
3843            // MOVW - Move Wide (Thumb-2 32-bit)
3844            ArmOp::Movw { rd, imm16 } => {
3845                self.encode_thumb32_movw_raw(reg_to_bits(rd), *imm16 as u32)
3846            }
3847
3848            // MOVT - Move Top (Thumb-2 32-bit)
3849            ArmOp::Movt { rd, imm16 } => {
3850                self.encode_thumb32_movt_raw(reg_to_bits(rd), *imm16 as u32)
3851            }
3852
3853            // #237: symbol-relative MOVW/MOVT. Encode the addend's low/high 16
3854            // bits in place; the backend records an R_ARM_MOVW_ABS_NC /
3855            // R_ARM_MOVT_ABS relocation against `symbol`, so the linker adds the
3856            // symbol's final address to the in-place addend (REL semantics).
3857            ArmOp::MovwSym { rd, addend, .. } => {
3858                self.encode_thumb32_movw_raw(reg_to_bits(rd), (*addend as u32) & 0xffff)
3859            }
3860            ArmOp::MovtSym { rd, addend, .. } => {
3861                self.encode_thumb32_movt_raw(reg_to_bits(rd), ((*addend as u32) >> 16) & 0xffff)
3862            }
3863
3864            // #345: literal-pool address load — emit a PLACEHOLDER `LDR.W rd,
3865            // [pc, #0]` (U=1, imm12=0). The backend (arm_backend.rs) places the
3866            // 4-byte pool word at the end of the function, records the R_ARM_ABS32
3867            // relocation against `symbol+addend`, and patches the imm12 with the
3868            // real PC-relative distance once the pool offset is known.
3869            // Encoding T2: 1111 1000 1101 1111 | Rt(4) imm12(12), with the literal
3870            // base = Align(PC,4) and PC = address of this instruction + 4.
3871            ArmOp::LdrSym { rd, .. } => {
3872                let rt = reg_to_bits(rd) as u16;
3873                let hw1: u16 = 0xF8DF; // LDR.W (literal), U=1
3874                let hw2: u16 = rt << 12; // imm12 = 0 placeholder
3875                let mut bytes = Vec::with_capacity(4);
3876                bytes.extend_from_slice(&hw1.to_le_bytes());
3877                bytes.extend_from_slice(&hw2.to_le_bytes());
3878                Ok(bytes)
3879            }
3880
3881            // SetCond: Materialize condition flag into register (0 or 1)
3882            // Strategy: ITE <cond>; MOV Rd, #1; MOV Rd, #0
3883            // IMPORTANT: Must use ITE (If-Then-Else) because 16-bit Thumb MOV
3884            // always sets flags (MOVS). We need to evaluate the condition BEFORE
3885            // any MOV instruction clobbers the flags from CMP.
3886            ArmOp::SetCond { rd, cond } => {
3887                let rd_bits = reg_to_bits(rd) as u16;
3888
3889                // Condition code encoding for IT block
3890                use synth_synthesis::Condition;
3891                let cond_bits: u16 = match cond {
3892                    Condition::EQ => 0x0,
3893                    Condition::NE => 0x1,
3894                    Condition::LT => 0xB,
3895                    Condition::LE => 0xD,
3896                    Condition::GT => 0xC,
3897                    Condition::GE => 0xA,
3898                    Condition::LO => 0x3, // CC/LO (unsigned <)
3899                    Condition::LS => 0x9, // LS (unsigned <=)
3900                    Condition::HI => 0x8, // HI (unsigned >)
3901                    Condition::HS => 0x2, // CS/HS (unsigned >=)
3902                };
3903
3904                // ITE <cond>: encodes If-Then-Else block
3905                // The mask field depends on firstcond[0]:
3906                // - If firstcond[0] = 0: mask = 0xC for TE pattern (ITE EQ = BF0C)
3907                // - If firstcond[0] = 1: mask = 0x4 for TE pattern (ITE NE = BF14)
3908                let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
3909                let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
3910
3911                // Materialize 0/1 into Rd. The 16-bit MOVS (T1) encodes Rd in a
3912                // 3-bit field (bits[10:8]) — only R0–R7. For a high register
3913                // (R8–R12) `rd_bits << 8` overflows into bit 11 and silently
3914                // turns MOVS into CMP (00100 → 00101), corrupting the result
3915                // (this mis-materialized gale's `has_waiter`, so its `local.set`
3916                // stored a stale register → the binary-sem WAKE dispatch read
3917                // garbage). Use the 32-bit MOV.W (T2) for high registers, which
3918                // has a 4-bit Rd field. MOV.W with S=0 doesn't set flags, which
3919                // is fine inside the ITE (the materialized value is the result;
3920                // the flags are not consumed afterwards).
3921                let mut bytes = ite_instr.to_le_bytes().to_vec();
3922                let push_mov = |bytes: &mut Vec<u8>, imm: u16| {
3923                    if rd_bits <= 7 {
3924                        let m: u16 = 0x2000 | (rd_bits << 8) | imm; // 16-bit MOVS Rd,#imm
3925                        bytes.extend_from_slice(&m.to_le_bytes());
3926                    } else {
3927                        // 32-bit MOV.W Rd, #imm (T2): F04F | (Rd<<8) | imm8
3928                        let hw1: u16 = 0xF04F;
3929                        let hw2: u16 = (rd_bits << 8) | imm;
3930                        bytes.extend_from_slice(&hw1.to_le_bytes());
3931                        bytes.extend_from_slice(&hw2.to_le_bytes());
3932                    }
3933                };
3934                push_mov(&mut bytes, 1); // Then branch (condition true)  → 1
3935                push_mov(&mut bytes, 0); // Else branch (condition false) → 0
3936                Ok(bytes)
3937            }
3938
3939            // I64SetCond: Compare two i64 register pairs, result 0/1 in rd
3940            // EQ/NE: CMP lo,lo; IT EQ; CMPEQ hi,hi; ITE <cond>; MOV 1; MOV 0
3941            // LT: CMP lo,lo; SBCS rd,hi,hi; ITE LT; MOV 1; MOV 0
3942            // GT: CMP lo,lo (swapped); SBCS rd,hi,hi (swapped); ITE LT; MOV 1; MOV 0
3943            ArmOp::I64SetCond {
3944                rd,
3945                rn_lo,
3946                rn_hi,
3947                rm_lo,
3948                rm_hi,
3949                cond,
3950            } => {
3951                use synth_synthesis::Condition;
3952                let rd_bits = reg_to_bits(rd) as u16;
3953                let mut bytes = Vec::new();
3954
3955                // Helper: encode CMP Rn, Rm (16-bit)
3956                let encode_cmp_reg = |rn: &synth_synthesis::Reg,
3957                                      rm: &synth_synthesis::Reg|
3958                 -> Vec<u8> {
3959                    let rn_bits = reg_to_bits(rn) as u16;
3960                    let rm_bits = reg_to_bits(rm) as u16;
3961                    if rn_bits < 8 && rm_bits < 8 {
3962                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
3963                        instr.to_le_bytes().to_vec()
3964                    } else {
3965                        let n_bit = (rn_bits >> 3) & 1;
3966                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
3967                        instr.to_le_bytes().to_vec()
3968                    }
3969                };
3970
3971                // Helper: encode ITE <cond> (2 bytes)
3972                let encode_ite = |cond_bits: u16| -> Vec<u8> {
3973                    let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
3974                    let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
3975                    ite_instr.to_le_bytes().to_vec()
3976                };
3977
3978                // Helper: encode SetCond (ITE + MOV #1 + MOV #0) for given condition
3979                let encode_setcond = |cond_bits: u16, rd_bits: u16| -> Vec<u8> {
3980                    let mut b = encode_ite(cond_bits);
3981                    if rd_bits < 8 {
3982                        let mov_one: u16 = 0x2001 | (rd_bits << 8);
3983                        let mov_zero: u16 = 0x2000 | (rd_bits << 8);
3984                        b.extend_from_slice(&mov_one.to_le_bytes());
3985                        b.extend_from_slice(&mov_zero.to_le_bytes());
3986                    } else {
3987                        // #311: rd >= R8 — the 16-bit MOV imm8 form has a 3-bit
3988                        // rd field; rd_bits<<8 overflows into bit 11 and
3989                        // TRANSMUTES the MOV into CMP (0x2001|0x0800 = 0x2801 =
3990                        // CMP r0,#1): the boolean dies in the flags and the
3991                        // consumer reads a stale register. Use the 32-bit
3992                        // MOV.W (T2: F04F 0000|rd<<8|imm8) — IT-legal,
3993                        // flag-preserving. Same class as H-CODE-9 / #180.
3994                        for imm in [1u16, 0u16] {
3995                            let hw1: u16 = 0xF04F;
3996                            let hw2: u16 = (rd_bits << 8) | imm;
3997                            b.extend_from_slice(&hw1.to_le_bytes());
3998                            b.extend_from_slice(&hw2.to_le_bytes());
3999                        }
4000                    }
4001                    b
4002                };
4003
4004                match cond {
4005                    Condition::EQ | Condition::NE => {
4006                        // CMP rn_lo, rm_lo (compare low words)
4007                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4008
4009                        // IT EQ (execute next instruction only if Z=1)
4010                        let it_eq: u16 = 0xBF08; // IT EQ: cond=0000, mask=1000
4011                        bytes.extend_from_slice(&it_eq.to_le_bytes());
4012
4013                        // CMPEQ rn_hi, rm_hi (compare high words, only if low equal)
4014                        bytes.extend_from_slice(&encode_cmp_reg(rn_hi, rm_hi));
4015
4016                        // ITE <cond>; MOV rd, #1; MOV rd, #0
4017                        let cond_bits: u16 = match cond {
4018                            Condition::EQ => 0x0,
4019                            Condition::NE => 0x1,
4020                            _ => unreachable!(),
4021                        };
4022                        bytes.extend_from_slice(&encode_setcond(cond_bits, rd_bits));
4023                    }
4024
4025                    Condition::LT => {
4026                        // CMP rn_lo, rm_lo (sets C flag for borrow)
4027                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4028
4029                        // SBCS rd, rn_hi, rm_hi (subtract with carry, sets N,V flags)
4030                        // SBCS.W Rd, Rn, Rm: EB70 Rn | 0000 Rd 0000 Rm
4031                        let rn_hi_bits = reg_to_bits(rn_hi);
4032                        let rm_hi_bits = reg_to_bits(rm_hi);
4033                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4034                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4035                        bytes.extend_from_slice(&hw1.to_le_bytes());
4036                        bytes.extend_from_slice(&hw2.to_le_bytes());
4037
4038                        // ITE LT; MOV rd, #1; MOV rd, #0
4039                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4040                    }
4041
4042                    Condition::GT => {
4043                        // GT(a,b) = LT(b,a): swap operands
4044                        // CMP rm_lo, rn_lo (swapped)
4045                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4046
4047                        // SBCS rd, rm_hi, rn_hi (swapped)
4048                        let rm_hi_bits = reg_to_bits(rm_hi);
4049                        let rn_hi_bits = reg_to_bits(rn_hi);
4050                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4051                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4052                        bytes.extend_from_slice(&hw1.to_le_bytes());
4053                        bytes.extend_from_slice(&hw2.to_le_bytes());
4054
4055                        // ITE LT; MOV rd, #1; MOV rd, #0
4056                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4057                    }
4058
4059                    Condition::LE => {
4060                        // LE(a,b) = !GT(a,b): use GT logic but invert result
4061                        // GT(a,b) = LT(b,a): so we do CMP(b,a) and check LT, then invert
4062                        // CMP rm_lo, rn_lo (swapped, same as GT)
4063                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4064
4065                        // SBCS rd, rm_hi, rn_hi (swapped)
4066                        let rm_hi_bits = reg_to_bits(rm_hi);
4067                        let rn_hi_bits = reg_to_bits(rn_hi);
4068                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4069                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4070                        bytes.extend_from_slice(&hw1.to_le_bytes());
4071                        bytes.extend_from_slice(&hw2.to_le_bytes());
4072
4073                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT, so inverting GT result)
4074                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4075                    }
4076
4077                    Condition::GE => {
4078                        // GE(a,b) = !LT(a,b): use LT logic but invert result
4079                        // CMP rn_lo, rm_lo (same as LT)
4080                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4081
4082                        // SBCS rd, rn_hi, rm_hi (same as LT)
4083                        let rn_hi_bits = reg_to_bits(rn_hi);
4084                        let rm_hi_bits = reg_to_bits(rm_hi);
4085                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4086                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4087                        bytes.extend_from_slice(&hw1.to_le_bytes());
4088                        bytes.extend_from_slice(&hw2.to_le_bytes());
4089
4090                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT)
4091                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4092                    }
4093
4094                    // Unsigned comparisons - same instruction sequence, different conditions
4095                    Condition::LO => {
4096                        // LO (unsigned LT): CMP lo, SBCS hi, check C=0
4097                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4098                        let rn_hi_bits = reg_to_bits(rn_hi);
4099                        let rm_hi_bits = reg_to_bits(rm_hi);
4100                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4101                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4102                        bytes.extend_from_slice(&hw1.to_le_bytes());
4103                        bytes.extend_from_slice(&hw2.to_le_bytes());
4104                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4105                    }
4106
4107                    Condition::HI => {
4108                        // HI (unsigned GT): swap operands and check LO
4109                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4110                        let rm_hi_bits = reg_to_bits(rm_hi);
4111                        let rn_hi_bits = reg_to_bits(rn_hi);
4112                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4113                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4114                        bytes.extend_from_slice(&hw1.to_le_bytes());
4115                        bytes.extend_from_slice(&hw2.to_le_bytes());
4116                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4117                    }
4118
4119                    Condition::LS => {
4120                        // LS (unsigned LE): !(a > b) = !(HI), so do HI and invert
4121                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4122                        let rm_hi_bits = reg_to_bits(rm_hi);
4123                        let rn_hi_bits = reg_to_bits(rn_hi);
4124                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4125                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4126                        bytes.extend_from_slice(&hw1.to_le_bytes());
4127                        bytes.extend_from_slice(&hw2.to_le_bytes());
4128                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4129                    }
4130
4131                    Condition::HS => {
4132                        // HS (unsigned GE): !(a < b) = !(LO)
4133                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4134                        let rn_hi_bits = reg_to_bits(rn_hi);
4135                        let rm_hi_bits = reg_to_bits(rm_hi);
4136                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4137                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4138                        bytes.extend_from_slice(&hw1.to_le_bytes());
4139                        bytes.extend_from_slice(&hw2.to_le_bytes());
4140                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4141                    }
4142                }
4143
4144                Ok(bytes)
4145            }
4146
4147            // I64SetCondZ: Test if i64 register pair is zero, result 0/1 in rd
4148            // ORR.W rd, rn_lo, rn_hi; CMP rd, #0; ITE EQ; MOV 1; MOV 0
4149            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
4150                let rd_bits = reg_to_bits(rd);
4151                let rn_lo_bits = reg_to_bits(rn_lo);
4152                let rn_hi_bits = reg_to_bits(rn_hi);
4153                let mut bytes = Vec::new();
4154
4155                // ORR.W rd, rn_lo, rn_hi: EA40 rn_lo | 0000 rd 0000 rn_hi
4156                let hw1: u16 = (0xEA40 | rn_lo_bits) as u16;
4157                let hw2: u16 = ((rd_bits << 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                // CMP rd, #0 — 16-bit form only for r0-r7 (3-bit rd field);
4162                // high registers take CMP.W (T2: F1B0|rn 0F00|imm8). This was
4163                // H-CODE-9: rd_bits<<8 overflowing the field compared the
4164                // WRONG register. Same hardening as the #311 SetCond fix.
4165                if rd_bits < 8 {
4166                    let cmp_instr: u16 = 0x2800 | ((rd_bits as u16) << 8);
4167                    bytes.extend_from_slice(&cmp_instr.to_le_bytes());
4168                } else {
4169                    let hw1: u16 = 0xF1B0 | (rd_bits as u16);
4170                    let hw2: u16 = 0x0F00;
4171                    bytes.extend_from_slice(&hw1.to_le_bytes());
4172                    bytes.extend_from_slice(&hw2.to_le_bytes());
4173                }
4174
4175                // ITE EQ; MOV rd, #1; MOV rd, #0 (32-bit MOV.W for rd >= R8,
4176                // #311 — see I64SetCond)
4177                let mask = 0xC_u16; // ITE EQ mask: firstcond[0]=0, mask=0xC
4178                let ite_instr: u16 = 0xBF00 | mask;
4179                bytes.extend_from_slice(&ite_instr.to_le_bytes());
4180                if rd_bits < 8 {
4181                    let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
4182                    let mov_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
4183                    bytes.extend_from_slice(&mov_one.to_le_bytes());
4184                    bytes.extend_from_slice(&mov_zero.to_le_bytes());
4185                } else {
4186                    for imm in [1u16, 0u16] {
4187                        let hw1: u16 = 0xF04F;
4188                        let hw2: u16 = ((rd_bits as u16) << 8) | imm;
4189                        bytes.extend_from_slice(&hw1.to_le_bytes());
4190                        bytes.extend_from_slice(&hw2.to_le_bytes());
4191                    }
4192                }
4193
4194                Ok(bytes)
4195            }
4196
4197            // I64Mul: 64-bit multiply using UMULL + MLA cross products
4198            // Formula: result = (a_lo * b_lo) + ((a_lo * b_hi + a_hi * b_lo) << 32)
4199            // Uses R12 as scratch register
4200            ArmOp::I64Mul {
4201                rd_lo,
4202                rd_hi,
4203                rn_lo,
4204                rn_hi,
4205                rm_lo,
4206                rm_hi,
4207            } => {
4208                let rd_lo_bits = reg_to_bits(rd_lo);
4209                let rd_hi_bits = reg_to_bits(rd_hi);
4210                let rn_lo_bits = reg_to_bits(rn_lo);
4211                let rn_hi_bits = reg_to_bits(rn_hi);
4212                let rm_lo_bits = reg_to_bits(rm_lo);
4213                let rm_hi_bits = reg_to_bits(rm_hi);
4214                let r12: u32 = 12; // IP scratch register
4215                let mut bytes = Vec::new();
4216
4217                // 1. MUL R12, rn_lo, rm_hi  (R12 = a_lo * b_hi)
4218                // Thumb-2 MUL: hw1=0xFB00|Rn, hw2=0xF000|(Rd<<8)|Rm
4219                let hw1: u16 = (0xFB00 | rn_lo_bits) as u16;
4220                let hw2: u16 = (0xF000 | (r12 << 8) | rm_hi_bits) as u16;
4221                bytes.extend_from_slice(&hw1.to_le_bytes());
4222                bytes.extend_from_slice(&hw2.to_le_bytes());
4223
4224                // 2. MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
4225                // Thumb-2 MLA: hw1=0xFB00|Rn, hw2=(Ra<<12)|(Rd<<8)|Rm
4226                let hw1: u16 = (0xFB00 | rn_hi_bits) as u16;
4227                let hw2: u16 = ((r12 << 12) | (r12 << 8) | rm_lo_bits) as u16;
4228                bytes.extend_from_slice(&hw1.to_le_bytes());
4229                bytes.extend_from_slice(&hw2.to_le_bytes());
4230
4231                // 3. UMULL rd_lo, rd_hi, rn_lo, rm_lo  (rd_lo:rd_hi = a_lo * b_lo)
4232                // Thumb-2 UMULL: hw1=0xFBA0|Rn, hw2=(RdLo<<12)|(RdHi<<8)|Rm
4233                let hw1: u16 = (0xFBA0 | rn_lo_bits) as u16;
4234                let hw2: u16 = ((rd_lo_bits << 12) | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4235                bytes.extend_from_slice(&hw1.to_le_bytes());
4236                bytes.extend_from_slice(&hw2.to_le_bytes());
4237
4238                // 4. ADD rd_hi, R12  (rd_hi += cross products)
4239                // 16-bit high reg ADD: 01000100 D Rm Rdn[2:0]
4240                let d_bit = (rd_hi_bits >> 3) & 1;
4241                let add_instr: u16 =
4242                    (0x4400 | (d_bit << 7) | (r12 << 3) | (rd_hi_bits & 0x7)) as u16;
4243                bytes.extend_from_slice(&add_instr.to_le_bytes());
4244
4245                Ok(bytes)
4246            }
4247
4248            // I64Shl: 64-bit shift left with branch for n<32 vs n>=32
4249            // rm_hi (R3) is used as temp register
4250            ArmOp::I64Shl {
4251                rd_lo,
4252                rd_hi,
4253                rn_lo,
4254                rn_hi,
4255                rm_lo,
4256                rm_hi,
4257            } => {
4258                let rd_lo_bits = reg_to_bits(rd_lo);
4259                let rd_hi_bits = reg_to_bits(rd_hi);
4260                let rn_lo_bits = reg_to_bits(rn_lo);
4261                let rn_hi_bits = reg_to_bits(rn_hi);
4262                let rm_lo_bits = reg_to_bits(rm_lo);
4263                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4264                let mut bytes = Vec::new();
4265
4266                // AND.W rm_lo, rm_lo, #63  (mask shift amount to 6 bits)
4267                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4268                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4269                bytes.extend_from_slice(&hw1.to_le_bytes());
4270                bytes.extend_from_slice(&hw2.to_le_bytes());
4271
4272                // SUBS.W rm_hi, rm_lo, #32  (rm_hi = n-32, sets flags)
4273                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4274                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4275                bytes.extend_from_slice(&hw1.to_le_bytes());
4276                bytes.extend_from_slice(&hw2.to_le_bytes());
4277
4278                // BPL .large (branch if n >= 32, offset = +10 halfwords)
4279                let bpl: u16 = 0xD50A;
4280                bytes.extend_from_slice(&bpl.to_le_bytes());
4281
4282                // --- Small shift (n < 32) ---
4283                // RSB.W rm_hi, rm_lo, #32  (rm_hi = 32-n)
4284                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4285                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4286                bytes.extend_from_slice(&hw1.to_le_bytes());
4287                bytes.extend_from_slice(&hw2.to_le_bytes());
4288
4289                // LSR.W rm_hi, rn_lo, rm_hi  (rm_hi = lo >> (32-n), overflow bits)
4290                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4291                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4292                bytes.extend_from_slice(&hw1.to_le_bytes());
4293                bytes.extend_from_slice(&hw2.to_le_bytes());
4294
4295                // LSL.W rd_hi, rn_hi, rm_lo  (hi <<= n)
4296                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4297                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4298                bytes.extend_from_slice(&hw1.to_le_bytes());
4299                bytes.extend_from_slice(&hw2.to_le_bytes());
4300
4301                // ORR.W rd_hi, rd_hi, rm_hi  (hi |= overflow bits from lo)
4302                let hw1: u16 = (0xEA40 | rd_hi_bits) as u16;
4303                let hw2: u16 = ((rd_hi_bits << 8) | rm_hi_bits) as u16;
4304                bytes.extend_from_slice(&hw1.to_le_bytes());
4305                bytes.extend_from_slice(&hw2.to_le_bytes());
4306
4307                // LSL.W rd_lo, rn_lo, rm_lo  (lo <<= n)
4308                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4309                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4310                bytes.extend_from_slice(&hw1.to_le_bytes());
4311                bytes.extend_from_slice(&hw2.to_le_bytes());
4312
4313                // B .done (skip large shift: +2 halfwords)
4314                let b_done: u16 = 0xE002;
4315                bytes.extend_from_slice(&b_done.to_le_bytes());
4316
4317                // --- Large shift (n >= 32) ---
4318                // LSL.W rd_hi, rn_lo, rm_hi  (hi = lo << (n-32))
4319                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4320                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_hi_bits) as u16;
4321                bytes.extend_from_slice(&hw1.to_le_bytes());
4322                bytes.extend_from_slice(&hw2.to_le_bytes());
4323
4324                // MOV rd_lo, #0
4325                let mov_zero: u16 = 0x2000 | ((rd_lo_bits as u16) << 8);
4326                bytes.extend_from_slice(&mov_zero.to_le_bytes());
4327
4328                Ok(bytes) // Total: 38 bytes
4329            }
4330
4331            // I64ShrU: 64-bit logical shift right with branch for n<32 vs n>=32
4332            ArmOp::I64ShrU {
4333                rd_lo,
4334                rd_hi,
4335                rn_lo,
4336                rn_hi,
4337                rm_lo,
4338                rm_hi,
4339            } => {
4340                let rd_lo_bits = reg_to_bits(rd_lo);
4341                let rd_hi_bits = reg_to_bits(rd_hi);
4342                let rn_lo_bits = reg_to_bits(rn_lo);
4343                let rn_hi_bits = reg_to_bits(rn_hi);
4344                let rm_lo_bits = reg_to_bits(rm_lo);
4345                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4346                let mut bytes = Vec::new();
4347
4348                // AND.W rm_lo, rm_lo, #63
4349                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4350                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4351                bytes.extend_from_slice(&hw1.to_le_bytes());
4352                bytes.extend_from_slice(&hw2.to_le_bytes());
4353
4354                // SUBS.W rm_hi, rm_lo, #32
4355                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4356                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4357                bytes.extend_from_slice(&hw1.to_le_bytes());
4358                bytes.extend_from_slice(&hw2.to_le_bytes());
4359
4360                // BPL .large (+10 halfwords)
4361                let bpl: u16 = 0xD50A;
4362                bytes.extend_from_slice(&bpl.to_le_bytes());
4363
4364                // --- Small shift (n < 32) ---
4365                // RSB.W rm_hi, rm_lo, #32  (rm_hi = 32-n)
4366                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4367                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4368                bytes.extend_from_slice(&hw1.to_le_bytes());
4369                bytes.extend_from_slice(&hw2.to_le_bytes());
4370
4371                // LSL.W rm_hi, rn_hi, rm_hi  (rm_hi = hi << (32-n), bits flowing to lo)
4372                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4373                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4374                bytes.extend_from_slice(&hw1.to_le_bytes());
4375                bytes.extend_from_slice(&hw2.to_le_bytes());
4376
4377                // LSR.W rd_lo, rn_lo, rm_lo  (lo >>= n)
4378                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4379                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4380                bytes.extend_from_slice(&hw1.to_le_bytes());
4381                bytes.extend_from_slice(&hw2.to_le_bytes());
4382
4383                // ORR.W rd_lo, rd_lo, rm_hi  (lo |= overflow from hi)
4384                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4385                let hw2: u16 = ((rd_lo_bits << 8) | rm_hi_bits) as u16;
4386                bytes.extend_from_slice(&hw1.to_le_bytes());
4387                bytes.extend_from_slice(&hw2.to_le_bytes());
4388
4389                // LSR.W rd_hi, rn_hi, rm_lo  (hi >>= n, logical)
4390                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4391                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4392                bytes.extend_from_slice(&hw1.to_le_bytes());
4393                bytes.extend_from_slice(&hw2.to_le_bytes());
4394
4395                // B .done (+2 halfwords)
4396                let b_done: u16 = 0xE002;
4397                bytes.extend_from_slice(&b_done.to_le_bytes());
4398
4399                // --- Large shift (n >= 32) ---
4400                // LSR.W rd_lo, rn_hi, rm_hi  (lo = hi >> (n-32))
4401                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4402                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_hi_bits) as u16;
4403                bytes.extend_from_slice(&hw1.to_le_bytes());
4404                bytes.extend_from_slice(&hw2.to_le_bytes());
4405
4406                // MOV rd_hi, #0
4407                let mov_zero: u16 = 0x2000 | ((rd_hi_bits as u16) << 8);
4408                bytes.extend_from_slice(&mov_zero.to_le_bytes());
4409
4410                Ok(bytes) // Total: 38 bytes
4411            }
4412
4413            // I64ShrS: 64-bit arithmetic shift right with branch for n<32 vs n>=32
4414            ArmOp::I64ShrS {
4415                rd_lo,
4416                rd_hi,
4417                rn_lo,
4418                rn_hi,
4419                rm_lo,
4420                rm_hi,
4421            } => {
4422                let rd_lo_bits = reg_to_bits(rd_lo);
4423                let rd_hi_bits = reg_to_bits(rd_hi);
4424                let rn_lo_bits = reg_to_bits(rn_lo);
4425                let rn_hi_bits = reg_to_bits(rn_hi);
4426                let rm_lo_bits = reg_to_bits(rm_lo);
4427                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4428                let mut bytes = Vec::new();
4429
4430                // AND.W rm_lo, rm_lo, #63
4431                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4432                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4433                bytes.extend_from_slice(&hw1.to_le_bytes());
4434                bytes.extend_from_slice(&hw2.to_le_bytes());
4435
4436                // SUBS.W rm_hi, rm_lo, #32
4437                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4438                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4439                bytes.extend_from_slice(&hw1.to_le_bytes());
4440                bytes.extend_from_slice(&hw2.to_le_bytes());
4441
4442                // BPL .large (+10 halfwords)
4443                let bpl: u16 = 0xD50A;
4444                bytes.extend_from_slice(&bpl.to_le_bytes());
4445
4446                // --- Small shift (n < 32) ---
4447                // RSB.W rm_hi, rm_lo, #32
4448                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4449                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4450                bytes.extend_from_slice(&hw1.to_le_bytes());
4451                bytes.extend_from_slice(&hw2.to_le_bytes());
4452
4453                // LSL.W rm_hi, rn_hi, rm_hi  (rm_hi = hi << (32-n), bits flowing to lo)
4454                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4455                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4456                bytes.extend_from_slice(&hw1.to_le_bytes());
4457                bytes.extend_from_slice(&hw2.to_le_bytes());
4458
4459                // LSR.W rd_lo, rn_lo, rm_lo  (lo >>= n, logical for lo word)
4460                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4461                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4462                bytes.extend_from_slice(&hw1.to_le_bytes());
4463                bytes.extend_from_slice(&hw2.to_le_bytes());
4464
4465                // ORR.W rd_lo, rd_lo, rm_hi  (lo |= overflow from hi)
4466                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4467                let hw2: u16 = ((rd_lo_bits << 8) | rm_hi_bits) as u16;
4468                bytes.extend_from_slice(&hw1.to_le_bytes());
4469                bytes.extend_from_slice(&hw2.to_le_bytes());
4470
4471                // ASR.W rd_hi, rn_hi, rm_lo  (hi >>= n, arithmetic/sign-extending)
4472                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
4473                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4474                bytes.extend_from_slice(&hw1.to_le_bytes());
4475                bytes.extend_from_slice(&hw2.to_le_bytes());
4476
4477                // B .done (+3 halfwords, large shift is 8 bytes)
4478                let b_done: u16 = 0xE003;
4479                bytes.extend_from_slice(&b_done.to_le_bytes());
4480
4481                // --- Large shift (n >= 32) ---
4482                // ASR.W rd_lo, rn_hi, rm_hi  (lo = hi >>> (n-32))
4483                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
4484                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_hi_bits) as u16;
4485                bytes.extend_from_slice(&hw1.to_le_bytes());
4486                bytes.extend_from_slice(&hw2.to_le_bytes());
4487
4488                // ASR.W rd_hi, rn_hi, #31  (hi = sign extension, all 0s or all 1s)
4489                // Thumb-2 ASR immediate: hw1=0xEA4F, hw2=imm3:Rd:imm2:10:Rm
4490                // imm5=31=11111 → imm3=111, imm2=11
4491                let hw1: u16 = 0xEA4F;
4492                let hw2: u16 = (0x7000 | (rd_hi_bits << 8) | 0x00E0 | rn_hi_bits) as u16;
4493                bytes.extend_from_slice(&hw1.to_le_bytes());
4494                bytes.extend_from_slice(&hw2.to_le_bytes());
4495
4496                Ok(bytes) // Total: 40 bytes
4497            }
4498
4499            // I64Rotl: 64-bit rotate left (#610 rewrite).
4500            // For n < 32: new_hi = (hi << n) | (lo >> (32-n)), new_lo = (lo << n) | (hi >> (32-n))
4501            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
4502            //
4503            // Fixed-reg core: value in R0:R1, amount in R2, scratch R3 + R12
4504            // (all four saved/marshaled by the #610 fixed-ABI wrapper; the
4505            // pre-#610 expansion wrote through the selector's registers with
4506            // colliding R3/R4 scratch and restored the saved R4 OVER the
4507            // result). Relies on ARM register-shift semantics: amounts >= 32
4508            // yield 0 for LSL/LSR, which makes n = 0 and n = 32 exact.
4509            ArmOp::I64Rotl {
4510                rdlo,
4511                rdhi,
4512                rnlo,
4513                rnhi,
4514                shift,
4515            } => {
4516                let mut bytes = Vec::new();
4517                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
4518
4519                let core: [u16; 35] = [
4520                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
4521                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
4522                    0xD50E, //         BPL    .large        (n >= 32)
4523                    // --- small rotation (n < 32) ---
4524                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
4525                    0xFA20, 0xFC03, // LSR.W  R12, R0, R3   (lo >> (32-n))
4526                    0xFA21, 0xF303, // LSR.W  R3, R1, R3    (hi >> (32-n))
4527                    0xFA01, 0xF102, // LSL.W  R1, R1, R2    (hi << n)
4528                    0xEA41, 0x010C, // ORR.W  R1, R1, R12   (new_hi)
4529                    0xFA00, 0xF002, // LSL.W  R0, R0, R2    (lo << n)
4530                    0xEA40, 0x0003, // ORR.W  R0, R0, R3    (new_lo)
4531                    0xE00E, //         B      .done
4532                    // --- large rotation (n >= 32), R3 = m = n-32 ---
4533                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
4534                    0xFA21, 0xFC02, // LSR.W  R12, R1, R2   (hi >> (64-n))
4535                    0xFA20, 0xF202, // LSR.W  R2, R0, R2    (lo >> (64-n))
4536                    0xFA00, 0xF003, // LSL.W  R0, R0, R3    (lo << m)
4537                    0xFA01, 0xF103, // LSL.W  R1, R1, R3    (hi << m)
4538                    0xEA40, 0x0C0C, // ORR.W  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
4539                    0xEA41, 0x0002, // ORR.W  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
4540                    0x4661, //         MOV    R1, R12       (new_hi into place)
4541                            // .done: result in R0:R1
4542                ];
4543                for hw in core {
4544                    bytes.extend_from_slice(&hw.to_le_bytes());
4545                }
4546
4547                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
4548                Ok(bytes) // Total: 102 bytes
4549            }
4550
4551            // I64Rotr: 64-bit rotate right (#610 rewrite).
4552            // For n < 32: new_lo = (lo >> n) | (hi << (32-n)), new_hi = (hi >> n) | (lo << (32-n))
4553            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
4554            //
4555            // Same fixed-reg core contract as I64Rotl: value in R0:R1, amount
4556            // in R2, scratch R3 + R12, all covered by the fixed-ABI wrapper.
4557            ArmOp::I64Rotr {
4558                rdlo,
4559                rdhi,
4560                rnlo,
4561                rnhi,
4562                shift,
4563            } => {
4564                let mut bytes = Vec::new();
4565                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
4566
4567                let core: [u16; 35] = [
4568                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
4569                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
4570                    0xD50E, //         BPL    .large        (n >= 32)
4571                    // --- small rotation (n < 32) ---
4572                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
4573                    0xFA01, 0xFC03, // LSL.W  R12, R1, R3   (hi << (32-n))
4574                    0xFA00, 0xF303, // LSL.W  R3, R0, R3    (lo << (32-n))
4575                    0xFA20, 0xF002, // LSR.W  R0, R0, R2    (lo >> n)
4576                    0xEA40, 0x000C, // ORR.W  R0, R0, R12   (new_lo)
4577                    0xFA21, 0xF102, // LSR.W  R1, R1, R2    (hi >> n)
4578                    0xEA41, 0x0103, // ORR.W  R1, R1, R3    (new_hi)
4579                    0xE00E, //         B      .done
4580                    // --- large rotation (n >= 32), R3 = m = n-32 ---
4581                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
4582                    0xFA00, 0xFC02, // LSL.W  R12, R0, R2   (lo << (64-n))
4583                    0xFA01, 0xF202, // LSL.W  R2, R1, R2    (hi << (64-n))
4584                    0xFA21, 0xF103, // LSR.W  R1, R1, R3    (hi >> m)
4585                    0xEA41, 0x0C0C, // ORR.W  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
4586                    0xFA20, 0xF103, // LSR.W  R1, R0, R3    (lo >> m)
4587                    0xEA41, 0x0102, // ORR.W  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
4588                    0x4660, //         MOV    R0, R12       (new_lo into place)
4589                            // .done: result in R0:R1
4590                ];
4591                for hw in core {
4592                    bytes.extend_from_slice(&hw.to_le_bytes());
4593                }
4594
4595                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
4596                Ok(bytes) // Total: 102 bytes
4597            }
4598
4599            // I64Clz: Count leading zeros in 64-bit value
4600            // If hi != 0: result = CLZ(hi)
4601            // If hi == 0: result = 32 + CLZ(lo)
4602            //
4603            // Layout (using CMP+BNE approach for consistency):
4604            // 0: CMP.W rnhi, #0 (4 bytes)
4605            // 4: BEQ .hi_zero (2 bytes) - branch forward to offset 14
4606            // 6: CLZ.W rd, rnhi (4 bytes)
4607            // 10: B .done (2 bytes) - branch forward to offset 22
4608            // 12: NOP (2 bytes) - padding for alignment
4609            // 14: .hi_zero: CLZ.W rd, rnlo (4 bytes)
4610            // 18: ADD.W rd, rd, #32 (4 bytes)
4611            // 22: .done
4612            ArmOp::I64Clz { rd, rnlo, rnhi } => {
4613                let rd_bits = reg_to_bits(rd);
4614                let rn_lo_bits = reg_to_bits(rnlo);
4615                let rn_hi_bits = reg_to_bits(rnhi);
4616                let mut bytes = Vec::new();
4617
4618                // CMP.W rnhi, #0 (4 bytes at offset 0)
4619                let hw1: u16 = (0xF1B0 | rn_hi_bits) as u16;
4620                let hw2: u16 = 0x0F00;
4621                bytes.extend_from_slice(&hw1.to_le_bytes());
4622                bytes.extend_from_slice(&hw2.to_le_bytes());
4623
4624                // BEQ .hi_zero (2 bytes at offset 4)
4625                // PC = 4 + 4 = 8, target = 14, offset = 6, imm8 = 3
4626                let beq: u16 = 0xD003;
4627                bytes.extend_from_slice(&beq.to_le_bytes());
4628
4629                // CLZ.W rd, rnhi (4 bytes at offset 6)
4630                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4631                let hw1: u16 = (0xFAB0 | rn_hi_bits) as u16;
4632                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_hi_bits) as u16;
4633                bytes.extend_from_slice(&hw1.to_le_bytes());
4634                bytes.extend_from_slice(&hw2.to_le_bytes());
4635
4636                // B .done (2 bytes at offset 10)
4637                // PC = 10 + 4 = 14, target = 22, offset = 8, imm11 = 4
4638                let b_done: u16 = 0xE004;
4639                bytes.extend_from_slice(&b_done.to_le_bytes());
4640
4641                // NOP (2 bytes at offset 12) - padding
4642                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
4643
4644                // .hi_zero: (offset 14)
4645                // CLZ.W rd, rnlo (4 bytes)
4646                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4647                let hw1: u16 = (0xFAB0 | rn_lo_bits) as u16;
4648                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_lo_bits) as u16;
4649                bytes.extend_from_slice(&hw1.to_le_bytes());
4650                bytes.extend_from_slice(&hw2.to_le_bytes());
4651
4652                // ADD.W rd, rd, #32 (4 bytes at offset 18)
4653                let hw1: u16 = (0xF100 | rd_bits) as u16;
4654                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
4655                bytes.extend_from_slice(&hw1.to_le_bytes());
4656                bytes.extend_from_slice(&hw2.to_le_bytes());
4657
4658                // .done: (offset 22)
4659                // i64.clz returns i64, so clear high word: MOV rnhi, #0 (2 bytes)
4660                // MOVS Rn, #0: 0010 0 Rn 00000000
4661                let mov0: u16 = (0x2000 | (rn_hi_bits << 8)) as u16;
4662                bytes.extend_from_slice(&mov0.to_le_bytes());
4663
4664                Ok(bytes)
4665            }
4666
4667            // I64Ctz: Count trailing zeros in 64-bit value
4668            // If lo != 0: result = CTZ(lo) = CLZ(RBIT(lo))
4669            // If lo == 0: result = 32 + CTZ(hi) = 32 + CLZ(RBIT(hi))
4670            //
4671            // Layout:
4672            // 0: CMP.W rnlo, #0 (4 bytes)
4673            // 4: BEQ .lo_zero (2 bytes) - branch to offset 18
4674            // 6: RBIT.W rd, rnlo (4 bytes)
4675            // 10: CLZ.W rd, rd (4 bytes)
4676            // 14: B .done (2 bytes) - branch to offset 30
4677            // 16: NOP (2 bytes) - padding
4678            // 18: .lo_zero: RBIT.W rd, rnhi (4 bytes)
4679            // 22: CLZ.W rd, rd (4 bytes)
4680            // 26: ADD.W rd, rd, #32 (4 bytes)
4681            // 30: .done
4682            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
4683                let rd_bits = reg_to_bits(rd);
4684                let rn_lo_bits = reg_to_bits(rnlo);
4685                let rn_hi_bits = reg_to_bits(rnhi);
4686                let mut bytes = Vec::new();
4687
4688                // CMP.W rnlo, #0 (4 bytes at offset 0)
4689                let hw1: u16 = (0xF1B0 | rn_lo_bits) as u16;
4690                let hw2: u16 = 0x0F00;
4691                bytes.extend_from_slice(&hw1.to_le_bytes());
4692                bytes.extend_from_slice(&hw2.to_le_bytes());
4693
4694                // BEQ .lo_zero (2 bytes at offset 4)
4695                // PC = 4 + 4 = 8, target = 18, offset = 10, imm8 = 5
4696                let beq: u16 = 0xD005;
4697                bytes.extend_from_slice(&beq.to_le_bytes());
4698
4699                // RBIT.W rd, rnlo (4 bytes at offset 6)
4700                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
4701                let hw1: u16 = (0xFA90 | rn_lo_bits) as u16;
4702                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_lo_bits) as u16;
4703                bytes.extend_from_slice(&hw1.to_le_bytes());
4704                bytes.extend_from_slice(&hw2.to_le_bytes());
4705
4706                // CLZ.W rd, rd (4 bytes at offset 10)
4707                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4708                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
4709                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
4710                bytes.extend_from_slice(&hw1.to_le_bytes());
4711                bytes.extend_from_slice(&hw2.to_le_bytes());
4712
4713                // B .done (2 bytes at offset 14)
4714                // PC = 14 + 4 = 18, target = 30, offset = 12, imm11 = 6
4715                let b_done: u16 = 0xE006;
4716                bytes.extend_from_slice(&b_done.to_le_bytes());
4717
4718                // NOP (2 bytes at offset 16) - padding
4719                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
4720
4721                // .lo_zero: (offset 18)
4722                // RBIT.W rd, rnhi (4 bytes)
4723                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
4724                let hw1: u16 = (0xFA90 | rn_hi_bits) as u16;
4725                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_hi_bits) as u16;
4726                bytes.extend_from_slice(&hw1.to_le_bytes());
4727                bytes.extend_from_slice(&hw2.to_le_bytes());
4728
4729                // CLZ.W rd, rd (4 bytes at offset 22)
4730                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4731                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
4732                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
4733                bytes.extend_from_slice(&hw1.to_le_bytes());
4734                bytes.extend_from_slice(&hw2.to_le_bytes());
4735
4736                // ADD.W rd, rd, #32 (4 bytes at offset 26)
4737                let hw1: u16 = (0xF100 | rd_bits) as u16;
4738                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
4739                bytes.extend_from_slice(&hw1.to_le_bytes());
4740                bytes.extend_from_slice(&hw2.to_le_bytes());
4741
4742                // .done: (offset 30)
4743                // i64.ctz returns i64, so clear high word: MOV rnhi, #0 (2 bytes)
4744                let mov0: u16 = (0x2000 | (rn_hi_bits << 8)) as u16;
4745                bytes.extend_from_slice(&mov0.to_le_bytes());
4746
4747                Ok(bytes)
4748            }
4749
4750            // I64Popcnt: Population count of 64-bit value
4751            // result = POPCNT(lo) + POPCNT(hi)
4752            // Using SIMD-style parallel bit counting algorithm
4753            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
4754                let rd_bits = reg_to_bits(rd);
4755                let rn_lo_bits = reg_to_bits(rnlo);
4756                let rn_hi_bits = reg_to_bits(rnhi);
4757                let r12: u32 = 12; // IP scratch
4758                let r3: u32 = 3; // Scratch for hi popcnt result
4759                let mut bytes = Vec::new();
4760
4761                // PUSH {R3, R4, R5} - save scratch registers
4762                bytes.extend_from_slice(&0xB438u16.to_le_bytes());
4763
4764                // Strategy: compute popcnt(lo) -> R4, popcnt(hi) -> R5, add them -> rd
4765                // Using lookup table approach for each byte would be too large
4766                // Using shift-and-add approach instead
4767
4768                // For simplicity and correctness, use the efficient parallel algorithm
4769                // but implement it as a series of inline operations
4770
4771                // Marshal the operand pair into the fixed scratch regs, routing
4772                // rnlo through R12 (#632 audit): writing R4 first corrupted the
4773                // rnhi read for a pair living at (R3,R4) — every source is read
4774                // before any scratch register it could occupy is written.
4775                // MOV R12, rnlo
4776                let mov: u16 = (0x4600 | (1 << 7) | (rn_lo_bits << 3) | 4) as u16;
4777                bytes.extend_from_slice(&mov.to_le_bytes());
4778                // MOV R5, rnhi (R4 untouched so far; rnhi == R5 is a no-op)
4779                let mov: u16 = (0x4600 | (rn_hi_bits << 3) | 5) as u16;
4780                bytes.extend_from_slice(&mov.to_le_bytes());
4781                // MOV R4, R12
4782                bytes.extend_from_slice(&0x4664u16.to_le_bytes());
4783
4784                // --- POPCNT for R4 (lo word) ---
4785                // Step 1: x = x - ((x >> 1) & 0x55555555)
4786                // LSR.W R12, R4, #1
4787                let hw1: u16 = 0xEA4F;
4788                let hw2: u16 = ((r12 << 8) | 0x50 | 4) as u16;
4789                bytes.extend_from_slice(&hw1.to_le_bytes());
4790                bytes.extend_from_slice(&hw2.to_le_bytes());
4791
4792                // Load 0x55555555 into R3 using MOVW/MOVT
4793                // MOVW R3, #0x5555
4794                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
4795                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
4796                // MOVT R3, #0x5555
4797                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
4798                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
4799
4800                // AND.W R12, R12, R3
4801                let hw1: u16 = (0xEA00 | r12) as u16;
4802                let hw2: u16 = ((r12 << 8) | r3) as u16;
4803                bytes.extend_from_slice(&hw1.to_le_bytes());
4804                bytes.extend_from_slice(&hw2.to_le_bytes());
4805
4806                // SUB.W R4, R4, R12
4807                let hw1: u16 = (0xEBA0 | 4) as u16;
4808                let hw2: u16 = ((4 << 8) | r12) as u16;
4809                bytes.extend_from_slice(&hw1.to_le_bytes());
4810                bytes.extend_from_slice(&hw2.to_le_bytes());
4811
4812                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
4813                // Load 0x33333333 into R3
4814                // MOVW R3, #0x3333
4815                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
4816                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
4817                // MOVT R3, #0x3333
4818                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
4819                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
4820
4821                // AND.W R12, R4, R3
4822                let hw1: u16 = (0xEA00 | 4) as u16;
4823                let hw2: u16 = ((r12 << 8) | r3) as u16;
4824                bytes.extend_from_slice(&hw1.to_le_bytes());
4825                bytes.extend_from_slice(&hw2.to_le_bytes());
4826
4827                // LSR.W R4, R4, #2
4828                let hw1: u16 = 0xEA4F;
4829                let hw2: u16 = ((4 << 8) | 0x90 | 4) as u16;
4830                bytes.extend_from_slice(&hw1.to_le_bytes());
4831                bytes.extend_from_slice(&hw2.to_le_bytes());
4832
4833                // AND.W R4, R4, R3
4834                let hw1: u16 = (0xEA00 | 4) as u16;
4835                let hw2: u16 = ((4 << 8) | r3) as u16;
4836                bytes.extend_from_slice(&hw1.to_le_bytes());
4837                bytes.extend_from_slice(&hw2.to_le_bytes());
4838
4839                // ADD.W R4, R4, R12
4840                let hw1: u16 = (0xEB00 | 4) as u16;
4841                let hw2: u16 = ((4 << 8) | r12) as u16;
4842                bytes.extend_from_slice(&hw1.to_le_bytes());
4843                bytes.extend_from_slice(&hw2.to_le_bytes());
4844
4845                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
4846                // LSR.W R12, R4, #4
4847                // hw2 = (imm3 << 12) | (Rd << 8) | (imm2 << 6) | (type << 4) | Rm
4848                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
4849                let hw1: u16 = 0xEA4F;
4850                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 4) as u16;
4851                bytes.extend_from_slice(&hw1.to_le_bytes());
4852                bytes.extend_from_slice(&hw2.to_le_bytes());
4853
4854                // ADD.W R4, R4, R12
4855                let hw1: u16 = (0xEB00 | 4) as u16;
4856                let hw2: u16 = ((4 << 8) | r12) as u16;
4857                bytes.extend_from_slice(&hw1.to_le_bytes());
4858                bytes.extend_from_slice(&hw2.to_le_bytes());
4859
4860                // Load 0x0F0F0F0F into R3
4861                // MOVW R3, #0x0F0F (imm4=0, i=1, imm3=7, imm8=0x0F)
4862                // hw1 = 11110 1 10 0100 0000 = 0xF640
4863                // hw2 = 0 111 0011 00001111 = 0x730F
4864                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
4865                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
4866                // MOVT R3, #0x0F0F
4867                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
4868                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
4869
4870                // AND.W R4, R4, R3
4871                let hw1: u16 = (0xEA00 | 4) as u16;
4872                let hw2: u16 = ((4 << 8) | r3) as u16;
4873                bytes.extend_from_slice(&hw1.to_le_bytes());
4874                bytes.extend_from_slice(&hw2.to_le_bytes());
4875
4876                // Step 4: x = x * 0x01010101 >> 24
4877                // Load 0x01010101 into R3
4878                // MOVW R3, #0x0101
4879                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
4880                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
4881                // MOVT R3, #0x0101
4882                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
4883                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
4884
4885                // MUL R4, R4, R3
4886                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
4887                let hw1: u16 = (0xFB00 | 4) as u16;
4888                let hw2: u16 = (0xF000 | (4 << 8) | r3) as u16;
4889                bytes.extend_from_slice(&hw1.to_le_bytes());
4890                bytes.extend_from_slice(&hw2.to_le_bytes());
4891
4892                // LSR.W R4, R4, #24
4893                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
4894                let hw1: u16 = 0xEA4F;
4895                let hw2: u16 = (0x6000 | (4 << 8) | 0x10 | 4) as u16;
4896                bytes.extend_from_slice(&hw1.to_le_bytes());
4897                bytes.extend_from_slice(&hw2.to_le_bytes());
4898
4899                // --- POPCNT for R5 (hi word) - same algorithm ---
4900                // Step 1
4901                let hw1: u16 = 0xEA4F;
4902                let hw2: u16 = ((r12 << 8) | 0x50 | 5) as u16;
4903                bytes.extend_from_slice(&hw1.to_le_bytes());
4904                bytes.extend_from_slice(&hw2.to_le_bytes());
4905
4906                // Load 0x55555555 into R3
4907                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
4908                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
4909                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
4910                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
4911
4912                let hw1: u16 = (0xEA00 | r12) as u16;
4913                let hw2: u16 = ((r12 << 8) | r3) as u16;
4914                bytes.extend_from_slice(&hw1.to_le_bytes());
4915                bytes.extend_from_slice(&hw2.to_le_bytes());
4916
4917                let hw1: u16 = (0xEBA0 | 5) as u16;
4918                let hw2: u16 = ((5 << 8) | r12) as u16;
4919                bytes.extend_from_slice(&hw1.to_le_bytes());
4920                bytes.extend_from_slice(&hw2.to_le_bytes());
4921
4922                // Step 2
4923                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
4924                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
4925                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
4926                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
4927
4928                let hw1: u16 = (0xEA00 | 5) 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                let hw1: u16 = 0xEA4F;
4934                let hw2: u16 = ((5 << 8) | 0x90 | 5) as u16;
4935                bytes.extend_from_slice(&hw1.to_le_bytes());
4936                bytes.extend_from_slice(&hw2.to_le_bytes());
4937
4938                let hw1: u16 = (0xEA00 | 5) as u16;
4939                let hw2: u16 = ((5 << 8) | r3) as u16;
4940                bytes.extend_from_slice(&hw1.to_le_bytes());
4941                bytes.extend_from_slice(&hw2.to_le_bytes());
4942
4943                let hw1: u16 = (0xEB00 | 5) as u16;
4944                let hw2: u16 = ((5 << 8) | r12) as u16;
4945                bytes.extend_from_slice(&hw1.to_le_bytes());
4946                bytes.extend_from_slice(&hw2.to_le_bytes());
4947
4948                // Step 3: LSR.W R12, R5, #4
4949                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
4950                let hw1: u16 = 0xEA4F;
4951                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 5) as u16;
4952                bytes.extend_from_slice(&hw1.to_le_bytes());
4953                bytes.extend_from_slice(&hw2.to_le_bytes());
4954
4955                let hw1: u16 = (0xEB00 | 5) as u16;
4956                let hw2: u16 = ((5 << 8) | r12) as u16;
4957                bytes.extend_from_slice(&hw1.to_le_bytes());
4958                bytes.extend_from_slice(&hw2.to_le_bytes());
4959
4960                // Load 0x0F0F0F0F into R3 (for hi-word)
4961                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
4962                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
4963                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
4964                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
4965
4966                let hw1: u16 = (0xEA00 | 5) as u16;
4967                let hw2: u16 = ((5 << 8) | r3) as u16;
4968                bytes.extend_from_slice(&hw1.to_le_bytes());
4969                bytes.extend_from_slice(&hw2.to_le_bytes());
4970
4971                // Step 4
4972                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
4973                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
4974                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
4975                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
4976
4977                // MUL R5, R5, R3
4978                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
4979                let hw1: u16 = (0xFB00 | 5) as u16;
4980                let hw2: u16 = (0xF000 | (5 << 8) | r3) as u16;
4981                bytes.extend_from_slice(&hw1.to_le_bytes());
4982                bytes.extend_from_slice(&hw2.to_le_bytes());
4983
4984                // LSR.W R5, R5, #24
4985                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
4986                let hw1: u16 = 0xEA4F;
4987                let hw2: u16 = (0x6000 | (5 << 8) | 0x10 | 5) as u16;
4988                bytes.extend_from_slice(&hw1.to_le_bytes());
4989                bytes.extend_from_slice(&hw2.to_le_bytes());
4990
4991                // #632: the count must be carried ACROSS the scratch restore
4992                // in a register the POP cannot touch. rd is allocator-assigned
4993                // (any of R0-R8) and can land inside the {R3,R4,R5} restore set
4994                // — the old `ADDS rd, R4, R5; POP {R3,R4,R5}` destroyed the
4995                // result one instruction after computing it (0 for every input
4996                // under qemu). R12 is encoder scratch: never allocatable (#212)
4997                // and never in a restore set, so no choice of rd can collide.
4998                // ADD.W R12, R4, R5
4999                bytes.extend_from_slice(&0xEB04u16.to_le_bytes());
5000                bytes.extend_from_slice(&0x0C05u16.to_le_bytes());
5001
5002                // POP {R3, R4, R5}
5003                bytes.extend_from_slice(&0xBC38u16.to_le_bytes());
5004
5005                // MOV rd, R12 — after the restore. The 4-bit Rd (D:rd) form is
5006                // also total over rd = R8, where the old ADDS T1 3-bit field
5007                // silently corrupted the encoding (#178/#180 class).
5008                let mov: u16 =
5009                    (0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7)) as u16;
5010                bytes.extend_from_slice(&mov.to_le_bytes());
5011
5012                // i64.popcnt returns i64, so clear high word: MOV.W rnhi, #0
5013                // (T2, 4 bytes — total over rnhi = R8, where the old 16-bit
5014                // MOVS encoding overflowed its 3-bit field into CMP R0, #0).
5015                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5016                bytes.extend_from_slice(&(((rn_hi_bits & 0xF) << 8) as u16).to_le_bytes());
5017
5018                Ok(bytes)
5019            }
5020
5021            // I64Extend8S: Sign-extend low 8 bits to 64 bits
5022            // Result: rdlo = sign_extend_8(rnlo), rdhi = rdlo >> 31
5023            ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
5024                let rdlo_bits = reg_to_bits(rdlo);
5025                let rdhi_bits = reg_to_bits(rdhi);
5026                let rnlo_bits = reg_to_bits(rnlo);
5027                let mut bytes = Vec::new();
5028
5029                // SXTB.W rdlo, rnlo (sign-extend byte to 32-bit)
5030                // SXTB T2: hw1 = 0xFA4F, hw2 = 0xF0<Rd><Rm>
5031                let hw1: u16 = 0xFA4F_u16;
5032                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5033                bytes.extend_from_slice(&hw1.to_le_bytes());
5034                bytes.extend_from_slice(&hw2.to_le_bytes());
5035
5036                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5037                // ASR (immediate): hw1 = 0xEA4F, hw2 = imm3:Rd:imm2:type:Rm
5038                // For imm5=31: imm3=111, imm2=11, type=10 (ASR)
5039                // hw2 = (7 << 12) | (rdhi << 8) | (3 << 6) | (2 << 4) | rdlo
5040                let hw1: u16 = 0xEA4F;
5041                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5042                bytes.extend_from_slice(&hw1.to_le_bytes());
5043                bytes.extend_from_slice(&hw2.to_le_bytes());
5044
5045                Ok(bytes)
5046            }
5047
5048            // I64Extend16S: Sign-extend low 16 bits to 64 bits
5049            // Result: rdlo = sign_extend_16(rnlo), rdhi = rdlo >> 31
5050            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
5051                let rdlo_bits = reg_to_bits(rdlo);
5052                let rdhi_bits = reg_to_bits(rdhi);
5053                let rnlo_bits = reg_to_bits(rnlo);
5054                let mut bytes = Vec::new();
5055
5056                // SXTH.W rdlo, rnlo (sign-extend halfword to 32-bit)
5057                // SXTH T2: hw1 = 0xFA0F, hw2 = 0xF0<Rd><Rm>
5058                let hw1: u16 = 0xFA0F_u16;
5059                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5060                bytes.extend_from_slice(&hw1.to_le_bytes());
5061                bytes.extend_from_slice(&hw2.to_le_bytes());
5062
5063                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5064                let hw1: u16 = 0xEA4F;
5065                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5066                bytes.extend_from_slice(&hw1.to_le_bytes());
5067                bytes.extend_from_slice(&hw2.to_le_bytes());
5068
5069                Ok(bytes)
5070            }
5071
5072            // I64Extend32S: Sign-extend low 32 bits to 64 bits
5073            // Result: rdlo = rnlo, rdhi = rnlo >> 31
5074            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
5075                let rdlo_bits = reg_to_bits(rdlo);
5076                let rdhi_bits = reg_to_bits(rdhi);
5077                let rnlo_bits = reg_to_bits(rnlo);
5078                let mut bytes = Vec::new();
5079
5080                // MOV rdlo, rnlo (if different)
5081                if rdlo_bits != rnlo_bits {
5082                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5083                    let d_bit = ((rdlo_bits >> 3) & 1) as u16;
5084                    let mov: u16 = 0x4600
5085                        | (d_bit << 7)
5086                        | ((rnlo_bits as u16) << 3)
5087                        | ((rdlo_bits & 0x7) as u16);
5088                    bytes.extend_from_slice(&mov.to_le_bytes());
5089                }
5090
5091                // ASR.W rdhi, rnlo, #31 (sign-extend to high word)
5092                let hw1: u16 = 0xEA4F;
5093                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rnlo_bits) as u16;
5094                bytes.extend_from_slice(&hw1.to_le_bytes());
5095                bytes.extend_from_slice(&hw2.to_le_bytes());
5096
5097                Ok(bytes)
5098            }
5099
5100            // SelectMove: IT <cond>; MOV{cond} rd, rm
5101            // Conditional move: only execute MOV if condition is true
5102            ArmOp::SelectMove { rd, rm, cond } => {
5103                let rd_bits = reg_to_bits(rd) as u16;
5104                let rm_bits = reg_to_bits(rm) as u16;
5105
5106                // Condition code encoding for IT block
5107                use synth_synthesis::Condition;
5108                let cond_bits: u16 = match cond {
5109                    Condition::EQ => 0x0, // Equal
5110                    Condition::NE => 0x1, // Not equal
5111                    Condition::HS => 0x2, // Higher or same (unsigned >=)
5112                    Condition::LO => 0x3, // Lower (unsigned <)
5113                    Condition::HI => 0x8, // Higher (unsigned >)
5114                    Condition::LS => 0x9, // Lower or same (unsigned <=)
5115                    Condition::GE => 0xA, // Greater or equal (signed)
5116                    Condition::LT => 0xB, // Less than (signed)
5117                    Condition::GT => 0xC, // Greater than (signed)
5118                    Condition::LE => 0xD, // Less or equal (signed)
5119                };
5120
5121                // IT <cond>: single Then block (mask = 0x8 for T only)
5122                // IT instruction: 1011 1111 firstcond mask
5123                let it_instr: u16 = 0xBF00 | (cond_bits << 4) | 0x8;
5124
5125                // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5126                // This MOV will only execute if condition is true due to IT block
5127                let d_bit = (rd_bits >> 3) & 1;
5128                let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5129
5130                // Emit: IT <cond>, MOV rd, rm
5131                let mut bytes = it_instr.to_le_bytes().to_vec();
5132                bytes.extend_from_slice(&mov_instr.to_le_bytes());
5133                Ok(bytes)
5134            }
5135
5136            // Popcnt: Population count (count set bits)
5137            // ARM Cortex-M has no native POPCNT, so we implement the bit manipulation algorithm:
5138            // x = x - ((x >> 1) & 0x55555555);
5139            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
5140            // x = (x + (x >> 4)) & 0x0F0F0F0F;
5141            // x = x + (x >> 8);
5142            // x = x + (x >> 16);
5143            // return x & 0x3F;
5144            //
5145            // Uses rd as working register and R12 as scratch for constants
5146            ArmOp::Popcnt { rd, rm } => {
5147                let mut bytes = Vec::new();
5148
5149                // First, move rm to rd if they're different
5150                if rd != rm {
5151                    let rd_bits = reg_to_bits(rd) as u16;
5152                    let rm_bits = reg_to_bits(rm) as u16;
5153                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5154                    let d_bit = (rd_bits >> 3) & 1;
5155                    let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5156                    bytes.extend_from_slice(&mov_instr.to_le_bytes());
5157                }
5158
5159                // Step 1: x = x - ((x >> 1) & 0x55555555)
5160                // Load 0x55555555 into R12
5161                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x5555)?);
5162                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x5555)?);
5163
5164                // R12_temp = rd >> 1
5165                // We need a second scratch register. Use R11.
5166                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 1)?);
5167
5168                // R11 = R11 & R12 (R11 = (x >> 1) & 0x55555555)
5169                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(11, 11, 12)?);
5170
5171                // rd = rd - R11
5172                bytes.extend_from_slice(&self.encode_thumb32_sub_reg_raw(
5173                    reg_to_bits(rd),
5174                    reg_to_bits(rd),
5175                    11,
5176                )?);
5177
5178                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5179                // Load 0x33333333 into R12
5180                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x3333)?);
5181                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x3333)?);
5182
5183                // R11 = rd & R12
5184                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5185                    11,
5186                    reg_to_bits(rd),
5187                    12,
5188                )?);
5189
5190                // rd = rd >> 2
5191                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(
5192                    reg_to_bits(rd),
5193                    reg_to_bits(rd),
5194                    2,
5195                )?);
5196
5197                // rd = rd & R12
5198                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5199                    reg_to_bits(rd),
5200                    reg_to_bits(rd),
5201                    12,
5202                )?);
5203
5204                // rd = rd + R11
5205                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5206                    reg_to_bits(rd),
5207                    reg_to_bits(rd),
5208                    11,
5209                )?);
5210
5211                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5212                // R11 = rd >> 4
5213                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 4)?);
5214
5215                // rd = rd + R11
5216                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5217                    reg_to_bits(rd),
5218                    reg_to_bits(rd),
5219                    11,
5220                )?);
5221
5222                // Load 0x0F0F0F0F into R12
5223                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x0F0F)?);
5224                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x0F0F)?);
5225
5226                // rd = rd & R12
5227                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5228                    reg_to_bits(rd),
5229                    reg_to_bits(rd),
5230                    12,
5231                )?);
5232
5233                // Step 4: x = x + (x >> 8)
5234                // R11 = rd >> 8
5235                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 8)?);
5236
5237                // rd = rd + R11
5238                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5239                    reg_to_bits(rd),
5240                    reg_to_bits(rd),
5241                    11,
5242                )?);
5243
5244                // Step 5: x = x + (x >> 16)
5245                // R11 = rd >> 16
5246                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 16)?);
5247
5248                // rd = rd + R11
5249                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5250                    reg_to_bits(rd),
5251                    reg_to_bits(rd),
5252                    11,
5253                )?);
5254
5255                // Step 6: return x & 0x3F
5256                // AND with 0x3F (small immediate, can use BIC or AND with immediate)
5257                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5258                    reg_to_bits(rd),
5259                    reg_to_bits(rd),
5260                    0x3F,
5261                )?);
5262
5263                Ok(bytes)
5264            }
5265
5266            // I64DivU: 64-bit unsigned division using binary long division
5267            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = quotient
5268            // Uses: R4-R7, R12 as loop counter (avoid R8 for Renode compatibility)
5269            //
5270            // #610: the fixed-ABI wrapper marshals the selector-assigned
5271            // operand registers into the core's fixed regs and lands the
5272            // result in rd — pre-#610 this arm IGNORED its register fields,
5273            // so the selector read its rd pair (e.g. R4:R5) after the core's
5274            // own POP restored the stale caller values over it: 0 for every
5275            // input. A zero divisor now traps (UDF #0), per WASM semantics.
5276            ArmOp::I64DivU {
5277                rdlo,
5278                rdhi,
5279                rnlo,
5280                rnhi,
5281                rmlo,
5282                rmhi,
5283            } => {
5284                let mut bytes = Vec::new();
5285                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5286                emit_i64_divisor_zero_trap(&mut bytes);
5287
5288                // PUSH {R4-R7} - save scratch registers (NO LR — this is inline code)
5289                // 16-bit PUSH: 1011 010 M rrrrrrrr where M=0 (no LR), r=R4-R7 = 0xF0
5290                // Encoding: 1011 0100 1111 0000 = 0xB4F0
5291                bytes.extend_from_slice(&0xB4F0u16.to_le_bytes());
5292
5293                // Initialize quotient (R4:R5) = 0
5294                bytes.extend_from_slice(&0x2400u16.to_le_bytes()); // MOV R4, #0
5295                bytes.extend_from_slice(&0x2500u16.to_le_bytes()); // MOV R5, #0
5296
5297                // Initialize remainder (R6:R7) = 0
5298                bytes.extend_from_slice(&0x2600u16.to_le_bytes()); // MOV R6, #0
5299                bytes.extend_from_slice(&0x2700u16.to_le_bytes()); // MOV R7, #0
5300
5301                // Initialize loop counter R12 = 64 (use R12 scratch instead of R8)
5302                // MOV.W R12, #64: F04F 0C40
5303                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5304                bytes.extend_from_slice(&0x0C40u16.to_le_bytes());
5305
5306                // Loop start
5307                let loop_start = bytes.len();
5308
5309                // === Loop body: process one bit ===
5310
5311                // 1. Shift quotient R4:R5 left by 1
5312                // LSLS R5, R5, #1 (16-bit: 0000 0010 1010 1101 = 0x006D -> actually 0x002D for LSL R5,R5,#1)
5313                // LSL Rd, Rm, #imm5: 000 00 imm5 Rm Rd = 000 00 00001 101 101 = 0x006D
5314                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5315                // Get carry from R4 into R5: ORR R5, R5, R4 LSR #31
5316                // Thumb-2 ORR with shifted register: EA45 75D4 = ORR.W R5, R5, R4, LSR #31
5317                // 11101010 010 S Rn | 0 imm3 Rd imm2 type Rm
5318                // type=01 (LSR), imm5=31 (imm3=111, imm2=11)
5319                bytes.extend_from_slice(&0xEA45u16.to_le_bytes());
5320                bytes.extend_from_slice(&0x75D4u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5321                // LSLS R4, R4, #1: 000 00 00001 100 100 = 0x0064
5322                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5323
5324                // 2. Shift remainder R6:R7 left by 1, OR in MSB of dividend R1
5325                // LSLS R7, R7, #1
5326                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5327                // ORR.W R7, R7, R6, LSR #31
5328                bytes.extend_from_slice(&0xEA47u16.to_le_bytes());
5329                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5330                // LSLS R6, R6, #1
5331                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5332                // ORR.W R6, R6, R1, LSR #31 (bring in MSB of dividend high)
5333                bytes.extend_from_slice(&0xEA46u16.to_le_bytes());
5334                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5335
5336                // 3. Shift dividend R0:R1 left by 1
5337                // LSLS R1, R1, #1
5338                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5339                // ORR.W R1, R1, R0, LSR #31
5340                bytes.extend_from_slice(&0xEA41u16.to_le_bytes());
5341                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5342                // LSLS R0, R0, #1
5343                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5344
5345                // 4. Compare remainder >= divisor (64-bit unsigned comparison)
5346                // Compare high words first: CMP R7, R3
5347                // CMP Rn, Rm encoding: 0x4280 | (Rm << 3) | Rn
5348                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3 (16-bit)
5349                // BHI means R7 > R3 (unsigned) - definitely subtract
5350                // BLO means R7 < R3 - definitely don't subtract
5351                // BEQ means need to check low words
5352
5353                // If high > divisor high: branch to subtract (forward +offset)
5354                // BHI.N +6 (skip CMP, skip BLO, do subtract)
5355                // BHI: 1101 1000 offset8 where cond=1000 (HI)
5356                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4 (to subtract block)
5357
5358                // If high < divisor high: branch past subtract
5359                // BLO.N +10 (skip to decrement)
5360                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BLO/BCC +12 (past subtract)
5361
5362                // High words equal, compare low: CMP R6, R2
5363                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2 (16-bit)
5364                // BLO/BCC past subtract (skip SUBS+SBC.W+ORR.W = 10 bytes = 4 halfwords from PC+4)
5365                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords (past subtract)
5366
5367                // === Subtract block: remainder -= divisor, quotient |= 1 ===
5368                // SUBS R6, R6, R2
5369                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2 (16-bit)
5370                // SBC R7, R7, R3 (with borrow)
5371                // Thumb-2 SBC.W: EB67 0703 = SBC.W R7, R7, R3
5372                bytes.extend_from_slice(&0xEB67u16.to_le_bytes());
5373                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5374                // ORR R4, R4, #1 (set bit 0 of quotient low)
5375                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5376                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5377
5378                // === Decrement counter and loop ===
5379                // SUBS.W R12, R12, #1 (decrement loop counter)
5380                // SUBS.W R12, R12, #1: F1BC 0C01
5381                bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
5382                bytes.extend_from_slice(&0x0C01u16.to_le_bytes());
5383
5384                // BNE back to loop_start
5385                let branch_offset_bytes = bytes.len() - loop_start + 4; // +4 for pipeline
5386                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5387                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5388                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5389
5390                // === Loop done, move quotient to R0:R1 ===
5391                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5392                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5393
5394                // POP {R4-R7} - restore scratch registers (NO PC — inline code continues)
5395                // 16-bit POP: 1011 110 P rrrrrrrr where P=0 (no PC), r=R4-R7 = 0xF0
5396                // Encoding: 1011 1100 1111 0000 = 0xBCF0
5397                bytes.extend_from_slice(&0xBCF0u16.to_le_bytes());
5398
5399                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5400                Ok(bytes)
5401            }
5402
5403            // I64DivS: 64-bit signed division
5404            // Converts to unsigned, divides, then applies sign
5405            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5406            //   ->  R0:R1 = quotient (signed)
5407            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5408            ArmOp::I64DivS {
5409                rdlo,
5410                rdhi,
5411                rnlo,
5412                rnhi,
5413                rmlo,
5414                rmhi,
5415            } => {
5416                let mut bytes = Vec::new();
5417                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5418                emit_i64_divisor_zero_trap(&mut bytes);
5419                // #633: INT64_MIN / -1 overflows — trap like the i32 path
5420                // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
5421                emit_i64_divs_overflow_trap(&mut bytes);
5422
5423                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5424                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5425                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5426
5427                // Save result sign in R9: R9 = R1 XOR R3 (sign bit = MSB)
5428                // EOR.W R9, R1, R3
5429                bytes.extend_from_slice(&0xEA81u16.to_le_bytes());
5430                bytes.extend_from_slice(&0x0903u16.to_le_bytes());
5431
5432                // If dividend negative (R1 MSB set), negate it
5433                // TST R1, R1 (check sign)
5434                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5435                // BPL skip_neg_dividend (+10 bytes = 5 halfwords)
5436                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5437
5438                // Negate R0:R1 (64-bit): RSBS R0, R0, #0; SBC R1, R1, R1 LSL #1
5439                // Actually: MVN R0, R0; MVN R1, R1; ADDS R0, R0, #1; ADC R1, R1, #0
5440                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5441                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5442                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5443                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5444                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5445
5446                // If divisor negative (R3 MSB set), negate it
5447                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5448                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5449
5450                // Negate R2:R3
5451                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
5452                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
5453                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
5454                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
5455                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
5456
5457                // === Now do unsigned division (same as I64DivU) ===
5458                // Initialize quotient (R4:R5) = 0
5459                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5460                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5461                // Initialize remainder (R6:R7) = 0
5462                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5463                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5464                // Initialize loop counter R8 = 64
5465                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5466                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5467
5468                let loop_start = bytes.len();
5469
5470                // Shift quotient left
5471                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5472                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5473                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5474                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5475
5476                // Shift remainder left, OR in MSB of dividend
5477                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5478                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5479                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5480                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5481                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5482                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5483
5484                // Shift dividend left
5485                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5486                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5487                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5488                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5489
5490                // Compare and conditionally subtract
5491                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5492                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5493                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5494                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5495                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5496
5497                // Subtract and set quotient bit
5498                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5499                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5500                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5501                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5502                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5503
5504                // Decrement and loop
5505                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
5506                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
5507
5508                let branch_offset_bytes = bytes.len() - loop_start + 4;
5509                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5510                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5511                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5512
5513                // Move quotient to R0:R1
5514                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5515                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5516
5517                // If result should be negative (R9 MSB set), negate R0:R1
5518                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9 (check MSB)
5519                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
5520                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8 (skip negation)
5521
5522                // Negate result R0:R1
5523                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5524                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5525                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5526                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5527                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5528
5529                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
5530                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
5531                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5532
5533                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5534                Ok(bytes)
5535            }
5536
5537            // I64RemU: 64-bit unsigned remainder using binary long division
5538            // Same algorithm as I64DivU but returns remainder instead of quotient
5539            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = remainder
5540            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5541            ArmOp::I64RemU {
5542                rdlo,
5543                rdhi,
5544                rnlo,
5545                rnhi,
5546                rmlo,
5547                rmhi,
5548            } => {
5549                let mut bytes = Vec::new();
5550                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5551                emit_i64_divisor_zero_trap(&mut bytes);
5552
5553                // PUSH {R4-R8} - save scratch registers (NO LR — inline code)
5554                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5555                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
5556
5557                // Initialize quotient (R4:R5) = 0 (computed but not returned)
5558                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5559                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5560                // Initialize remainder (R6:R7) = 0
5561                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5562                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5563                // Initialize loop counter R8 = 64
5564                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5565                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5566
5567                let loop_start = bytes.len();
5568
5569                // Shift quotient left (not needed for result, but keeps algorithm same)
5570                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5571                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5572                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5573                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5574
5575                // Shift remainder left, OR in MSB of dividend
5576                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5577                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5578                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5579                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5580                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5581                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5582
5583                // Shift dividend left
5584                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5585                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5586                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5587                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5588
5589                // Compare and conditionally subtract
5590                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5591                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5592                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5593                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5594                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5595
5596                // Subtract and set quotient bit
5597                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5598                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5599                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5600                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5601                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5602
5603                // Decrement and loop
5604                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
5605                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
5606
5607                let branch_offset_bytes = bytes.len() - loop_start + 4;
5608                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5609                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5610                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5611
5612                // Move REMAINDER to R0:R1 (difference from I64DivU)
5613                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
5614                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
5615
5616                // POP {R4-R8} - restore scratch registers (NO PC — inline code continues)
5617                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
5618                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
5619
5620                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5621                Ok(bytes)
5622            }
5623
5624            // I64RemS: 64-bit signed remainder
5625            // Remainder sign follows dividend sign (not quotient rule)
5626            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5627            //   ->  R0:R1 = remainder (signed, same sign as dividend)
5628            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5629            ArmOp::I64RemS {
5630                rdlo,
5631                rdhi,
5632                rnlo,
5633                rnhi,
5634                rmlo,
5635                rmhi,
5636            } => {
5637                let mut bytes = Vec::new();
5638                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5639                emit_i64_divisor_zero_trap(&mut bytes);
5640
5641                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5642                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5643                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5644
5645                // Save dividend sign in R9 (remainder sign = dividend sign)
5646                // MOV R9, R1 (just need the sign bit)
5647                bytes.extend_from_slice(&0x4689u16.to_le_bytes()); // MOV R9, R1
5648
5649                // If dividend negative (R1 MSB set), negate it
5650                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5651                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5652
5653                // Negate R0:R1
5654                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5655                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5656                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5657                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5658                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5659
5660                // If divisor negative (R3 MSB set), negate it
5661                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5662                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5663
5664                // Negate R2:R3
5665                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
5666                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
5667                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
5668                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
5669                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
5670
5671                // === Unsigned division algorithm ===
5672                // Initialize quotient (R4:R5) = 0
5673                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5674                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5675                // Initialize remainder (R6:R7) = 0
5676                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5677                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5678                // Initialize loop counter R8 = 64
5679                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5680                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5681
5682                let loop_start = bytes.len();
5683
5684                // Shift quotient left
5685                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5686                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5687                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5688                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5689
5690                // Shift remainder left, OR in MSB of dividend
5691                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5692                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5693                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5694                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5695                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5696                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5697
5698                // Shift dividend left
5699                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5700                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5701                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5702                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5703
5704                // Compare and conditionally subtract
5705                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5706                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5707                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5708                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5709                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5710
5711                // Subtract and set quotient bit
5712                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5713                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5714                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5715                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5716                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5717
5718                // Decrement and loop
5719                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
5720                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
5721
5722                let branch_offset_bytes = bytes.len() - loop_start + 4;
5723                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5724                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5725                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5726
5727                // Move remainder to R0:R1
5728                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
5729                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
5730
5731                // If original dividend was negative (R9 MSB set), negate remainder
5732                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9
5733                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
5734                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5735
5736                // Negate result R0:R1
5737                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5738                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5739                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5740                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5741                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5742
5743                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
5744                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
5745                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5746
5747                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5748                Ok(bytes)
5749            }
5750
5751            // === F32 VFP single-precision Thumb-2 encodings ===
5752            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
5753            ArmOp::F32Add { sd, sn, sm } => {
5754                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A00, sd, sn, sm)?))
5755            }
5756            ArmOp::F32Sub { sd, sn, sm } => {
5757                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A40, sd, sn, sm)?))
5758            }
5759            ArmOp::F32Mul { sd, sn, sm } => {
5760                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE200A00, sd, sn, sm)?))
5761            }
5762            ArmOp::F32Div { sd, sn, sm } => {
5763                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE800A00, sd, sn, sm)?))
5764            }
5765            ArmOp::F32Abs { sd, sm } => {
5766                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB00AC0, sd, sm)?))
5767            }
5768            ArmOp::F32Neg { sd, sm } => {
5769                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10A40, sd, sm)?))
5770            }
5771            ArmOp::F32Sqrt { sd, sm } => {
5772                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10AC0, sd, sm)?))
5773            }
5774
5775            // f32 pseudo-ops — multi-instruction sequences
5776            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
5777            ArmOp::F32Ceil { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b01),
5778            ArmOp::F32Floor { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b10),
5779            ArmOp::F32Trunc { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b11),
5780            ArmOp::F32Nearest { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b00),
5781            ArmOp::F32Min { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, true),
5782            ArmOp::F32Max { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, false),
5783            ArmOp::F32Copysign { sd, sn, sm } => self.encode_thumb_f32_copysign(sd, sn, sm),
5784
5785            // f32 comparisons — VCMP + VMRS + MOV #0 + IT + MOV #1
5786            ArmOp::F32Eq { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x0),
5787            ArmOp::F32Ne { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x1),
5788            ArmOp::F32Lt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x4),
5789            ArmOp::F32Le { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x9),
5790            ArmOp::F32Gt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xC),
5791            ArmOp::F32Ge { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xA),
5792
5793            ArmOp::F32Const { sd, value } => self.encode_thumb_f32_const(sd, *value),
5794
5795            ArmOp::F32Load { sd, addr } => {
5796                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED900A00, sd, addr)?))
5797            }
5798            ArmOp::F32Store { sd, addr } => {
5799                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED800A00, sd, addr)?))
5800            }
5801
5802            ArmOp::F32ConvertI32S { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, true),
5803            ArmOp::F32ConvertI32U { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, false),
5804            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
5805                Err(synth_core::Error::synthesis(
5806                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
5807                ))
5808            }
5809            ArmOp::F32ReinterpretI32 { sd, rm } => {
5810                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(true, sd, rm)?))
5811            }
5812            ArmOp::I32ReinterpretF32 { rd, sm } => {
5813                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(false, sm, rd)?))
5814            }
5815            ArmOp::I32TruncF32S { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, true),
5816            ArmOp::I32TruncF32U { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, false),
5817
5818            // === F64 VFP double-precision Thumb-2 encodings ===
5819            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
5820            ArmOp::F64Add { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
5821                0xEE300B00, dd, dn, dm,
5822            )?)),
5823            ArmOp::F64Sub { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
5824                0xEE300B40, dd, dn, dm,
5825            )?)),
5826            ArmOp::F64Mul { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
5827                0xEE200B00, dd, dn, dm,
5828            )?)),
5829            ArmOp::F64Div { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
5830                0xEE800B00, dd, dn, dm,
5831            )?)),
5832            ArmOp::F64Abs { dd, dm } => {
5833                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?))
5834            }
5835            ArmOp::F64Neg { dd, dm } => {
5836                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?))
5837            }
5838            ArmOp::F64Sqrt { dd, dm } => {
5839                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?))
5840            }
5841
5842            // f64 pseudo-ops
5843            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
5844            ArmOp::F64Ceil { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b01),
5845            ArmOp::F64Floor { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b10),
5846            ArmOp::F64Trunc { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b11),
5847            ArmOp::F64Nearest { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b00),
5848            ArmOp::F64Min { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, true),
5849            ArmOp::F64Max { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, false),
5850            ArmOp::F64Copysign { dd, dn, dm } => self.encode_thumb_f64_copysign(dd, dn, dm),
5851
5852            // f64 comparisons
5853            ArmOp::F64Eq { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x0),
5854            ArmOp::F64Ne { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x1),
5855            ArmOp::F64Lt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x4),
5856            ArmOp::F64Le { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x9),
5857            ArmOp::F64Gt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xC),
5858            ArmOp::F64Ge { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xA),
5859
5860            ArmOp::F64Const { dd, value } => self.encode_thumb_f64_const(dd, *value),
5861
5862            ArmOp::F64Load { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
5863                0xED900B00, dd, addr,
5864            )?)),
5865            ArmOp::F64Store { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
5866                0xED800B00, dd, addr,
5867            )?)),
5868
5869            ArmOp::F64ConvertI32S { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, true),
5870            ArmOp::F64ConvertI32U { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, false),
5871            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
5872                Err(synth_core::Error::synthesis(
5873                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
5874                ))
5875            }
5876            ArmOp::F64PromoteF32 { dd, sm } => self.encode_thumb_f64_promote_f32(dd, sm),
5877            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => Ok(vfp_to_thumb_bytes(
5878                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?,
5879            )),
5880            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => Ok(vfp_to_thumb_bytes(
5881                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?,
5882            )),
5883            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
5884                Err(synth_core::Error::synthesis(
5885                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
5886                ))
5887            }
5888            ArmOp::I32TruncF64S { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, true),
5889            ArmOp::I32TruncF64U { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, false),
5890
5891            // ===== i64 operations: encode as multi-instruction Thumb-2 sequences =====
5892
5893            // I64Add: ADDS rdlo, rnlo, rmlo; ADC.W rdhi, rnhi, rmhi
5894            ArmOp::I64Add {
5895                rdlo,
5896                rdhi,
5897                rnlo,
5898                rnhi,
5899                rmlo,
5900                rmhi,
5901            } => {
5902                let mut bytes = Vec::new();
5903                // ADDS rdlo, rnlo, rmlo (16-bit)
5904                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adds {
5905                    rd: *rdlo,
5906                    rn: *rnlo,
5907                    op2: Operand2::Reg(*rmlo),
5908                })?);
5909                // ADC.W rdhi, rnhi, rmhi (32-bit)
5910                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adc {
5911                    rd: *rdhi,
5912                    rn: *rnhi,
5913                    op2: Operand2::Reg(*rmhi),
5914                })?);
5915                Ok(bytes)
5916            }
5917
5918            // I64Sub: SUBS rdlo, rnlo, rmlo; SBC.W rdhi, rnhi, rmhi
5919            ArmOp::I64Sub {
5920                rdlo,
5921                rdhi,
5922                rnlo,
5923                rnhi,
5924                rmlo,
5925                rmhi,
5926            } => {
5927                let mut bytes = Vec::new();
5928                // SUBS rdlo, rnlo, rmlo (16-bit)
5929                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Subs {
5930                    rd: *rdlo,
5931                    rn: *rnlo,
5932                    op2: Operand2::Reg(*rmlo),
5933                })?);
5934                // SBC.W rdhi, rnhi, rmhi (32-bit)
5935                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Sbc {
5936                    rd: *rdhi,
5937                    rn: *rnhi,
5938                    op2: Operand2::Reg(*rmhi),
5939                })?);
5940                Ok(bytes)
5941            }
5942
5943            // I64And: AND rdlo, rnlo, rmlo; AND rdhi, rnhi, rmhi
5944            ArmOp::I64And {
5945                rdlo,
5946                rdhi,
5947                rnlo,
5948                rnhi,
5949                rmlo,
5950                rmhi,
5951            } => {
5952                let mut bytes = Vec::new();
5953                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
5954                    rd: *rdlo,
5955                    rn: *rnlo,
5956                    op2: Operand2::Reg(*rmlo),
5957                })?);
5958                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
5959                    rd: *rdhi,
5960                    rn: *rnhi,
5961                    op2: Operand2::Reg(*rmhi),
5962                })?);
5963                Ok(bytes)
5964            }
5965
5966            // I64Or: ORR rdlo, rnlo, rmlo; ORR rdhi, rnhi, rmhi
5967            ArmOp::I64Or {
5968                rdlo,
5969                rdhi,
5970                rnlo,
5971                rnhi,
5972                rmlo,
5973                rmhi,
5974            } => {
5975                let mut bytes = Vec::new();
5976                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
5977                    rd: *rdlo,
5978                    rn: *rnlo,
5979                    op2: Operand2::Reg(*rmlo),
5980                })?);
5981                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
5982                    rd: *rdhi,
5983                    rn: *rnhi,
5984                    op2: Operand2::Reg(*rmhi),
5985                })?);
5986                Ok(bytes)
5987            }
5988
5989            // I64Xor: EOR rdlo, rnlo, rmlo; EOR rdhi, rnhi, rmhi
5990            ArmOp::I64Xor {
5991                rdlo,
5992                rdhi,
5993                rnlo,
5994                rnhi,
5995                rmlo,
5996                rmhi,
5997            } => {
5998                let mut bytes = Vec::new();
5999                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6000                    rd: *rdlo,
6001                    rn: *rnlo,
6002                    op2: Operand2::Reg(*rmlo),
6003                })?);
6004                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6005                    rd: *rdhi,
6006                    rn: *rnhi,
6007                    op2: Operand2::Reg(*rmhi),
6008                })?);
6009                Ok(bytes)
6010            }
6011
6012            // I64Eqz: ORR scratch, lo, hi; ITE EQ; MOV rd, #1; MOV rd, #0
6013            ArmOp::I64Eqz { rd, rnlo, rnhi } => self.encode_thumb(&ArmOp::I64SetCondZ {
6014                rd: *rd,
6015                rn_lo: *rnlo,
6016                rn_hi: *rnhi,
6017            }),
6018
6019            // I64 comparisons: delegate to I64SetCond
6020            ArmOp::I64Eq {
6021                rd,
6022                rnlo,
6023                rnhi,
6024                rmlo,
6025                rmhi,
6026            } => self.encode_thumb(&ArmOp::I64SetCond {
6027                rd: *rd,
6028                rn_lo: *rnlo,
6029                rn_hi: *rnhi,
6030                rm_lo: *rmlo,
6031                rm_hi: *rmhi,
6032                cond: synth_synthesis::Condition::EQ,
6033            }),
6034
6035            ArmOp::I64Ne {
6036                rd,
6037                rnlo,
6038                rnhi,
6039                rmlo,
6040                rmhi,
6041            } => self.encode_thumb(&ArmOp::I64SetCond {
6042                rd: *rd,
6043                rn_lo: *rnlo,
6044                rn_hi: *rnhi,
6045                rm_lo: *rmlo,
6046                rm_hi: *rmhi,
6047                cond: synth_synthesis::Condition::NE,
6048            }),
6049
6050            ArmOp::I64LtS {
6051                rd,
6052                rnlo,
6053                rnhi,
6054                rmlo,
6055                rmhi,
6056            } => self.encode_thumb(&ArmOp::I64SetCond {
6057                rd: *rd,
6058                rn_lo: *rnlo,
6059                rn_hi: *rnhi,
6060                rm_lo: *rmlo,
6061                rm_hi: *rmhi,
6062                cond: synth_synthesis::Condition::LT,
6063            }),
6064
6065            ArmOp::I64LtU {
6066                rd,
6067                rnlo,
6068                rnhi,
6069                rmlo,
6070                rmhi,
6071            } => self.encode_thumb(&ArmOp::I64SetCond {
6072                rd: *rd,
6073                rn_lo: *rnlo,
6074                rn_hi: *rnhi,
6075                rm_lo: *rmlo,
6076                rm_hi: *rmhi,
6077                cond: synth_synthesis::Condition::LO,
6078            }),
6079
6080            ArmOp::I64LeS {
6081                rd,
6082                rnlo,
6083                rnhi,
6084                rmlo,
6085                rmhi,
6086            } => self.encode_thumb(&ArmOp::I64SetCond {
6087                rd: *rd,
6088                rn_lo: *rnlo,
6089                rn_hi: *rnhi,
6090                rm_lo: *rmlo,
6091                rm_hi: *rmhi,
6092                cond: synth_synthesis::Condition::LE,
6093            }),
6094
6095            ArmOp::I64LeU {
6096                rd,
6097                rnlo,
6098                rnhi,
6099                rmlo,
6100                rmhi,
6101            } => self.encode_thumb(&ArmOp::I64SetCond {
6102                rd: *rd,
6103                rn_lo: *rnlo,
6104                rn_hi: *rnhi,
6105                rm_lo: *rmlo,
6106                rm_hi: *rmhi,
6107                cond: synth_synthesis::Condition::LS,
6108            }),
6109
6110            ArmOp::I64GtS {
6111                rd,
6112                rnlo,
6113                rnhi,
6114                rmlo,
6115                rmhi,
6116            } => self.encode_thumb(&ArmOp::I64SetCond {
6117                rd: *rd,
6118                rn_lo: *rnlo,
6119                rn_hi: *rnhi,
6120                rm_lo: *rmlo,
6121                rm_hi: *rmhi,
6122                cond: synth_synthesis::Condition::GT,
6123            }),
6124
6125            ArmOp::I64GtU {
6126                rd,
6127                rnlo,
6128                rnhi,
6129                rmlo,
6130                rmhi,
6131            } => self.encode_thumb(&ArmOp::I64SetCond {
6132                rd: *rd,
6133                rn_lo: *rnlo,
6134                rn_hi: *rnhi,
6135                rm_lo: *rmlo,
6136                rm_hi: *rmhi,
6137                cond: synth_synthesis::Condition::HI,
6138            }),
6139
6140            ArmOp::I64GeS {
6141                rd,
6142                rnlo,
6143                rnhi,
6144                rmlo,
6145                rmhi,
6146            } => self.encode_thumb(&ArmOp::I64SetCond {
6147                rd: *rd,
6148                rn_lo: *rnlo,
6149                rn_hi: *rnhi,
6150                rm_lo: *rmlo,
6151                rm_hi: *rmhi,
6152                cond: synth_synthesis::Condition::GE,
6153            }),
6154
6155            ArmOp::I64GeU {
6156                rd,
6157                rnlo,
6158                rnhi,
6159                rmlo,
6160                rmhi,
6161            } => self.encode_thumb(&ArmOp::I64SetCond {
6162                rd: *rd,
6163                rn_lo: *rnlo,
6164                rn_hi: *rnhi,
6165                rm_lo: *rmlo,
6166                rm_hi: *rmhi,
6167                cond: synth_synthesis::Condition::HS,
6168            }),
6169
6170            // I64Const: MOVW rdlo, lo16; MOVT rdlo, hi16; MOVW rdhi, lo16_hi; MOVT rdhi, hi16_hi
6171            ArmOp::I64Const { rdlo, rdhi, value } => {
6172                let lo32 = *value as u32;
6173                let hi32 = (*value >> 32) as u32;
6174                let mut bytes = Vec::new();
6175                // Load low 32 bits into rdlo
6176                bytes.extend_from_slice(
6177                    &self.encode_thumb32_movw_raw(reg_to_bits(rdlo), lo32 & 0xFFFF)?,
6178                );
6179                if lo32 > 0xFFFF {
6180                    bytes.extend_from_slice(
6181                        &self.encode_thumb32_movt_raw(reg_to_bits(rdlo), lo32 >> 16)?,
6182                    );
6183                }
6184                // Load high 32 bits into rdhi
6185                bytes.extend_from_slice(
6186                    &self.encode_thumb32_movw_raw(reg_to_bits(rdhi), hi32 & 0xFFFF)?,
6187                );
6188                if hi32 > 0xFFFF {
6189                    bytes.extend_from_slice(
6190                        &self.encode_thumb32_movt_raw(reg_to_bits(rdhi), hi32 >> 16)?,
6191                    );
6192                }
6193                Ok(bytes)
6194            }
6195
6196            // I64Ldr: LDR rdlo, [base, offset]; LDR rdhi, [base, offset+4]
6197            ArmOp::I64Ldr { rdlo, rdhi, addr } => {
6198                let mut bytes = Vec::new();
6199                // #372/#382: a memory `i64.load` carries an index register
6200                // (`reg_imm(R11, addr_reg, offset)` = R11 + addr + offset). The
6201                // immediate `encode_thumb32_ldr` below uses only base+offset and
6202                // would SILENTLY DROP `offset_reg` — the #206 defect, here for
6203                // i64. `i64_effective_base` materializes the effective base into
6204                // `ip` (and, when `offset+4 > 0xFFF`, folds the offset in too so
6205                // the function is NOT skipped — #382), returning the residual
6206                // imm12 for the two halves. Frame i64 loads (no `offset_reg`, e.g.
6207                // a spilled local at `[SP, #off]`) keep the plain `[base,#off]`
6208                // form unchanged — so existing output is byte-identical.
6209                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6210                bytes.extend_from_slice(&self.encode_thumb32_ldr(rdlo, &base, offset)?);
6211                bytes.extend_from_slice(&self.encode_thumb32_ldr(
6212                    rdhi,
6213                    &base,
6214                    offset.wrapping_add(4),
6215                )?);
6216                Ok(bytes)
6217            }
6218
6219            // I64Str: STR rdlo, [base, offset]; STR rdhi, [base, offset+4]
6220            ArmOp::I64Str { rdlo, rdhi, addr } => {
6221                let mut bytes = Vec::new();
6222                // #372/#382: same index-materialization + large-offset fold as
6223                // I64Ldr (see above).
6224                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6225                bytes.extend_from_slice(&self.encode_thumb32_str(rdlo, &base, offset)?);
6226                bytes.extend_from_slice(&self.encode_thumb32_str(
6227                    rdhi,
6228                    &base,
6229                    offset.wrapping_add(4),
6230                )?);
6231                Ok(bytes)
6232            }
6233
6234            // I64ExtendI32S: MOV rdlo, rn; ASR rdhi, rdlo, #31 (sign-extend)
6235            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
6236                let mut bytes = Vec::new();
6237                if rdlo != rn {
6238                    // MOV rdlo, rn (16-bit)
6239                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6240                        rd: *rdlo,
6241                        op2: Operand2::Reg(*rn),
6242                    })?);
6243                }
6244                // ASR rdhi, rdlo, #31 (sign-extend: fill high word with sign bit)
6245                bytes.extend_from_slice(
6246                    &self.encode_thumb32_shift(rdhi, rdlo, 31, 0b10)?, // ASR type
6247                );
6248                Ok(bytes)
6249            }
6250
6251            // I64ExtendI32U: MOV rdlo, rn; MOV rdhi, #0
6252            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
6253                let mut bytes = Vec::new();
6254                if rdlo != rn {
6255                    // MOV rdlo, rn
6256                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6257                        rd: *rdlo,
6258                        op2: Operand2::Reg(*rn),
6259                    })?);
6260                }
6261                // MOV rdhi, #0 (16-bit: MOVS Rd, #0)
6262                let rdhi_bits = reg_to_bits(rdhi) as u16;
6263                let instr: u16 = 0x2000 | (rdhi_bits << 8);
6264                bytes.extend_from_slice(&instr.to_le_bytes());
6265                Ok(bytes)
6266            }
6267
6268            // I32WrapI64: MOV rd, rnlo (just take low 32 bits)
6269            ArmOp::I32WrapI64 { rd, rnlo } => {
6270                if rd == rnlo {
6271                    // No-op: already in the right register
6272                    let instr: u16 = 0xBF00; // NOP
6273                    Ok(instr.to_le_bytes().to_vec())
6274                } else {
6275                    // MOV rd, rnlo
6276                    self.encode_thumb(&ArmOp::Mov {
6277                        rd: *rd,
6278                        op2: Operand2::Reg(*rnlo),
6279                    })
6280                }
6281            }
6282
6283            // ===== Helium MVE operations (Thumb-2 encoding) =====
6284            ArmOp::MveLoad { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vldrw(qd, addr))),
6285            ArmOp::MveStore { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vstrw(qd, addr))),
6286            ArmOp::MveConst { qd, bytes } => self.encode_thumb_mve_const(qd, bytes),
6287            ArmOp::MveAnd { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6288                0xEF000150, qd, qn, qm,
6289            ))),
6290            ArmOp::MveOrr { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6291                0xEF200150, qd, qn, qm,
6292            ))),
6293            ArmOp::MveEor { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6294                0xFF000150, qd, qn, qm,
6295            ))),
6296            ArmOp::MveMvn { qd, qm } => {
6297                // VMVN Qd, Qm: 0xFFB005C0 | Qd<<12 | Qm
6298                let qd_enc = qreg_to_num(qd);
6299                let qm_enc = qreg_to_num(qm);
6300                let instr: u32 = 0xFFB005C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6301                Ok(vfp_to_thumb_bytes(instr))
6302            }
6303            ArmOp::MveBic { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6304                0xEF100150, qd, qn, qm,
6305            ))),
6306            ArmOp::MveAddI { qd, qn, qm, size } => {
6307                let sz = mve_size_bits(size);
6308                let base: u32 = 0xEF000840 | (sz << 20);
6309                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6310            }
6311            ArmOp::MveSubI { qd, qn, qm, size } => {
6312                let sz = mve_size_bits(size);
6313                let base: u32 = 0xFF000840 | (sz << 20);
6314                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6315            }
6316            ArmOp::MveMulI { qd, qn, qm, size } => {
6317                let sz = mve_size_bits(size);
6318                let base: u32 = 0xEF000950 | (sz << 20);
6319                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6320            }
6321            ArmOp::MveNegI { qd, qm, size } => {
6322                let sz = mve_size_bits(size);
6323                // VNEG.Sx Qd, Qm
6324                let qd_enc = qreg_to_num(qd);
6325                let qm_enc = qreg_to_num(qm);
6326                let base: u32 = 0xFFB103C0 | (sz << 18);
6327                let instr = base | ((qd_enc * 2) << 12) | (qm_enc * 2);
6328                Ok(vfp_to_thumb_bytes(instr))
6329            }
6330            ArmOp::MveDup { qd, rn, size } => {
6331                let sz = mve_size_bits(size);
6332                let qd_enc = qreg_to_num(qd);
6333                let rn_bits = reg_to_bits(rn);
6334                // VDUP.sz Qd, Rn: EEA0 0B10 variant
6335                // size encoding: 00=32, 01=16, 10=8
6336                let be = match sz {
6337                    0 => 0b00u32, // 8-bit
6338                    1 => 0b01,    // 16-bit
6339                    _ => 0b00,    // 32-bit (default)
6340                };
6341                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12) | (be << 5);
6342                Ok(vfp_to_thumb_bytes(instr))
6343            }
6344            ArmOp::MveExtractLane { rd, qn, lane, size } => {
6345                let qn_enc = qreg_to_num(qn);
6346                let rd_bits = reg_to_bits(rd);
6347                // VMOV.sz Rd, Dn[x] — extract from Q-register lane
6348                // For 32-bit: VMOV Rd, Dn — where Dn is the appropriate D-register
6349                let d_reg = qn_enc * 2 + ((*lane as u32) >> 1);
6350                let lane_in_d = (*lane as u32) & 1;
6351                let _sz = mve_size_bits(size);
6352                // VMOV Rd, Dn[x]: EE10 0B10 for 32-bit
6353                let instr: u32 = 0xEE100B10 | (d_reg << 16) | (rd_bits << 12) | (lane_in_d << 21);
6354                Ok(vfp_to_thumb_bytes(instr))
6355            }
6356            ArmOp::MveInsertLane { qd, rn, lane, size } => {
6357                let qd_enc = qreg_to_num(qd);
6358                let rn_bits = reg_to_bits(rn);
6359                let d_reg = qd_enc * 2 + ((*lane as u32) >> 1);
6360                let lane_in_d = (*lane as u32) & 1;
6361                let _sz = mve_size_bits(size);
6362                // VMOV Dn[x], Rn: EE00 0B10 for 32-bit
6363                let instr: u32 = 0xEE000B10 | (d_reg << 16) | (rn_bits << 12) | (lane_in_d << 21);
6364                Ok(vfp_to_thumb_bytes(instr))
6365            }
6366
6367            // MVE float comparisons — emit VCMP + VPSEL sequence (simplified: just VCMP)
6368            ArmOp::MveCmpEqI { qd, qn, qm, size }
6369            | ArmOp::MveCmpNeI { qd, qn, qm, size }
6370            | ArmOp::MveCmpLtS { qd, qn, qm, size }
6371            | ArmOp::MveCmpLtU { qd, qn, qm, size }
6372            | ArmOp::MveCmpGtS { qd, qn, qm, size }
6373            | ArmOp::MveCmpGtU { qd, qn, qm, size }
6374            | ArmOp::MveCmpLeS { qd, qn, qm, size }
6375            | ArmOp::MveCmpLeU { qd, qn, qm, size }
6376            | ArmOp::MveCmpGeS { qd, qn, qm, size }
6377            | ArmOp::MveCmpGeU { qd, qn, qm, size } => {
6378                // Encode as VADD (placeholder encoding — real implementation
6379                // would use VCMP + VPSEL pair)
6380                let sz = mve_size_bits(size);
6381                let base: u32 = 0xEF000840 | (sz << 20);
6382                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6383            }
6384
6385            // f32x4 MVE arithmetic
6386            ArmOp::MveAddF32 { qd, qn, qm } => {
6387                // VADD.F32 Qd, Qn, Qm (MVE): 0xEF000D40
6388                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6389            }
6390            ArmOp::MveSubF32 { qd, qn, qm } => {
6391                // VSUB.F32 Qd, Qn, Qm (MVE): 0xEF200D40
6392                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF200D40, qd, qn, qm)))
6393            }
6394            ArmOp::MveMulF32 { qd, qn, qm } => {
6395                // VMUL.F32 Qd, Qn, Qm (MVE): 0xFF000D50
6396                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xFF000D50, qd, qn, qm)))
6397            }
6398            ArmOp::MveNegF32 { qd, qm } => {
6399                let qd_enc = qreg_to_num(qd);
6400                let qm_enc = qreg_to_num(qm);
6401                // VNEG.F32 Qd, Qm: FFB907C0
6402                let instr: u32 = 0xFFB907C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6403                Ok(vfp_to_thumb_bytes(instr))
6404            }
6405            ArmOp::MveAbsF32 { qd, qm } => {
6406                let qd_enc = qreg_to_num(qd);
6407                let qm_enc = qreg_to_num(qm);
6408                // VABS.F32 Qd, Qm: FFB90740
6409                let instr: u32 = 0xFFB90740 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6410                Ok(vfp_to_thumb_bytes(instr))
6411            }
6412            ArmOp::MveCmpEqF32 { qd, qn, qm }
6413            | ArmOp::MveCmpNeF32 { qd, qn, qm }
6414            | ArmOp::MveCmpLtF32 { qd, qn, qm }
6415            | ArmOp::MveCmpLeF32 { qd, qn, qm }
6416            | ArmOp::MveCmpGtF32 { qd, qn, qm }
6417            | ArmOp::MveCmpGeF32 { qd, qn, qm } => {
6418                // Placeholder: encode as VADD.F32 (real impl needs VCMP.F32 + VPSEL)
6419                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6420            }
6421            ArmOp::MveDupF32 { qd, rn } => {
6422                let qd_enc = qreg_to_num(qd);
6423                let rn_bits = reg_to_bits(rn);
6424                // VDUP.32 Qd, Rn (same encoding as integer VDUP.32)
6425                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12);
6426                Ok(vfp_to_thumb_bytes(instr))
6427            }
6428            ArmOp::MveExtractLaneF32 { rd, qn, lane } => {
6429                let qn_enc = qreg_to_num(qn);
6430                let rd_bits = reg_to_bits(rd);
6431                // VMOV Rd, Sn where Sn = Q*4 + lane
6432                let s_num = qn_enc * 4 + (*lane as u32);
6433                let (vn, n) = encode_sreg(s_num);
6434                let instr: u32 = 0xEE100A10 | (vn << 16) | (rd_bits << 12) | (n << 7);
6435                Ok(vfp_to_thumb_bytes(instr))
6436            }
6437            ArmOp::MveReplaceLaneF32 { qd, rn, lane } => {
6438                let qd_enc = qreg_to_num(qd);
6439                let rn_bits = reg_to_bits(rn);
6440                // VMOV Sn, Rn where Sn = Q*4 + lane
6441                let s_num = qd_enc * 4 + (*lane as u32);
6442                let (vn, n) = encode_sreg(s_num);
6443                let instr: u32 = 0xEE000A10 | (vn << 16) | (rn_bits << 12) | (n << 7);
6444                Ok(vfp_to_thumb_bytes(instr))
6445            }
6446            ArmOp::MveDivF32 { qd, qn, qm } => {
6447                // Lane-wise: extract 4 S-regs, VDIV, insert back
6448                self.encode_thumb_mve_lane_wise_f32_binop(qd, qn, qm, 0xEE800A00)
6449            }
6450            ArmOp::MveSqrtF32 { qd, qm } => {
6451                // Lane-wise: extract 4 S-regs, VSQRT, insert back
6452                self.encode_thumb_mve_lane_wise_f32_sqrt(qd, qm)
6453            }
6454
6455            // Catch-all for any remaining ops
6456            _ => {
6457                let instr: u16 = 0xBF00; // NOP
6458                Ok(instr.to_le_bytes().to_vec())
6459            }
6460        }
6461    }
6462
6463    // === Thumb-2 VFP multi-instruction helpers ===
6464
6465    /// Encode F32 comparison as Thumb-2: VCMP.F32 + VMRS + MOVS rd,#0 + IT + MOV rd,#1
6466    fn encode_thumb_f32_compare(
6467        &self,
6468        rd: &Reg,
6469        sn: &VfpReg,
6470        sm: &VfpReg,
6471        cond_code: u32,
6472    ) -> Result<Vec<u8>> {
6473        let mut bytes = Vec::new();
6474        let rd_bits = reg_to_bits(rd);
6475
6476        // VCMP.F32 Sn, Sm
6477        let sn_num = vfp_sreg_to_num(sn)?;
6478        let sm_num = vfp_sreg_to_num(sm)?;
6479        let (vd, d) = encode_sreg(sn_num);
6480        let (vm, m) = encode_sreg(sm_num);
6481        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
6482        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
6483
6484        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
6485        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
6486
6487        // MOVS Rd, #0 (16-bit): 0010 0 Rd(3) 0000 0000
6488        if rd_bits < 8 {
6489            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
6490            bytes.extend_from_slice(&movs_zero.to_le_bytes());
6491        } else {
6492            // MOV.W Rd, #0 (32-bit Thumb-2)
6493            let hw1: u16 = 0xF04F;
6494            let hw2: u16 = (rd_bits as u16) << 8;
6495            bytes.extend_from_slice(&hw1.to_le_bytes());
6496            bytes.extend_from_slice(&hw2.to_le_bytes());
6497        }
6498
6499        // IT<cond> — If-Then for conditional MOV
6500        // IT encoding: 1011 1111 cond(4) mask(4)
6501        // mask = 0x8 for single "then" (IT)
6502        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
6503        bytes.extend_from_slice(&it.to_le_bytes());
6504
6505        // MOV Rd, #1 (16-bit, conditional due to IT): 0010 0 Rd(3) 0000 0001
6506        if rd_bits < 8 {
6507            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
6508            bytes.extend_from_slice(&mov_one.to_le_bytes());
6509        } else {
6510            // MOV.W Rd, #1 (32-bit)
6511            let hw1: u16 = 0xF04F;
6512            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
6513            bytes.extend_from_slice(&hw1.to_le_bytes());
6514            bytes.extend_from_slice(&hw2.to_le_bytes());
6515        }
6516
6517        Ok(bytes)
6518    }
6519
6520    /// Encode F32 constant load as Thumb-2: MOVW + MOVT + VMOV
6521    fn encode_thumb_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
6522        let mut bytes = Vec::new();
6523        let bits = value.to_bits();
6524        let rt: u32 = 12; // R12/IP as temp
6525
6526        // MOVW R12, #lo16
6527        // Thumb-2 MOVW: 11110 i 10 0100 imm4 | 0 imm3 Rd imm8
6528        let lo16 = bits & 0xFFFF;
6529        let imm4 = (lo16 >> 12) & 0xF;
6530        let i_bit = (lo16 >> 11) & 1;
6531        let imm3 = (lo16 >> 8) & 0x7;
6532        let imm8 = lo16 & 0xFF;
6533        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
6534        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
6535        bytes.extend_from_slice(&hw1.to_le_bytes());
6536        bytes.extend_from_slice(&hw2.to_le_bytes());
6537
6538        // MOVT R12, #hi16
6539        let hi16 = (bits >> 16) & 0xFFFF;
6540        let imm4 = (hi16 >> 12) & 0xF;
6541        let i_bit = (hi16 >> 11) & 1;
6542        let imm3 = (hi16 >> 8) & 0x7;
6543        let imm8 = hi16 & 0xFF;
6544        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
6545        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
6546        bytes.extend_from_slice(&hw1.to_le_bytes());
6547        bytes.extend_from_slice(&hw2.to_le_bytes());
6548
6549        // VMOV Sd, R12
6550        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
6551        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6552
6553        Ok(bytes)
6554    }
6555
6556    /// Encode VMOV + VCVT.F32.xS32 as Thumb-2
6557    fn encode_thumb_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
6558        let mut bytes = Vec::new();
6559
6560        // VMOV Sd, Rm
6561        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
6562        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6563
6564        // VCVT.F32.S32/U32 Sd, Sd
6565        let sd_num = vfp_sreg_to_num(sd)?;
6566        let (vd, d) = encode_sreg(sd_num);
6567        let (vm, m) = encode_sreg(sd_num);
6568        let base = if signed { 0xEEB80A40 } else { 0xEEB80AC0 };
6569        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
6570        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
6571
6572        Ok(bytes)
6573    }
6574
6575    /// Encode F32 rounding pseudo-op as Thumb-2 via VCVT to integer and back
6576    /// Encode F32 rounding as Thumb-2.
6577    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
6578    ///
6579    /// For trunc: uses VCVTR.S32.F32 (always truncates).
6580    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant),
6581    /// then restores FPSCR.
6582    fn encode_thumb_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
6583        let mut bytes = Vec::new();
6584        let sm_num = vfp_sreg_to_num(sm)?;
6585        let sd_num = vfp_sreg_to_num(sd)?;
6586        let (vd_s, d_s) = encode_sreg(sd_num);
6587        let (vm_s, m_s) = encode_sreg(sm_num);
6588
6589        if mode == 0b11 {
6590            // Trunc (toward zero): VCVTR.S32.F32 — bit[7]=1, always truncates
6591            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
6592            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
6593        } else {
6594            // ceil/floor/nearest: manipulate FPSCR rounding mode
6595            let rt: u32 = 12; // R12/IP as temp
6596
6597            // VMRS R12, FPSCR
6598            let vmrs = 0xEEF10A10 | (rt << 12);
6599            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
6600
6601            // BIC.W R12, R12, #(3 << 22) — clear RMode bits [23:22]
6602            // Thumb-2 modified immediate for 3<<22 = 0x00C00000:
6603            // BIC.W encoding: 11110 i 0 0001 S Rn | 0 imm3 Rd imm8
6604            // 0x00C00000 = 0x03 shifted left by 22 => Thumb mod-imm: i=0, imm3=0b101, imm8=0x03
6605            let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF); // BIC, Rn=R12
6606            let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
6607            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
6608            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
6609
6610            // ORR.W R12, R12, #(mode << 22)
6611            if mode != 0 {
6612                let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF); // ORR, Rn=R12
6613                let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
6614                bytes.extend_from_slice(&orr_hw1.to_le_bytes());
6615                bytes.extend_from_slice(&orr_hw2.to_le_bytes());
6616            }
6617
6618            // VMSR FPSCR, R12
6619            let vmsr = 0xEEE10A10 | (rt << 12);
6620            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
6621
6622            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rmode
6623            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
6624            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
6625
6626            // Restore FPSCR: clear rmode bits back to nearest (default)
6627            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
6628            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
6629            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
6630            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
6631        }
6632
6633        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
6634        let (vd2, d2) = encode_sreg(sd_num);
6635        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
6636        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
6637
6638        Ok(bytes)
6639    }
6640
6641    /// Encode F32 min/max as Thumb-2: VMOV + VCMP + VMRS + IT + VMOV
6642    fn encode_thumb_f32_minmax(
6643        &self,
6644        sd: &VfpReg,
6645        sn: &VfpReg,
6646        sm: &VfpReg,
6647        is_min: bool,
6648    ) -> Result<Vec<u8>> {
6649        let mut bytes = Vec::new();
6650        let sn_num = vfp_sreg_to_num(sn)?;
6651        let sm_num = vfp_sreg_to_num(sm)?;
6652        let sd_num = vfp_sreg_to_num(sd)?;
6653
6654        // VMOV.F32 Sd, Sn
6655        let (vd, d) = encode_sreg(sd_num);
6656        let (vn, n) = encode_sreg(sn_num);
6657        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
6658        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sn));
6659
6660        // VCMP.F32 Sn, Sm
6661        let (vm, m) = encode_sreg(sm_num);
6662        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
6663        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
6664
6665        // VMRS APSR_nzcv, FPSCR
6666        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
6667
6668        // IT GT (for min) or IT MI (for max)
6669        let cond: u16 = if is_min { 0xC } else { 0x4 };
6670        let it: u16 = 0xBF00 | (cond << 4) | 0x8;
6671        bytes.extend_from_slice(&it.to_le_bytes());
6672
6673        // VMOV{cond}.F32 Sd, Sm — conditional VMOV in IT block
6674        let vmov_sm = 0xEEB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
6675        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sm));
6676
6677        Ok(bytes)
6678    }
6679
6680    /// Encode F32 copysign as Thumb-2
6681    fn encode_thumb_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
6682        let mut bytes = Vec::new();
6683
6684        // VMOV R12, Sm (get sign source bits)
6685        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
6686            false,
6687            sm,
6688            &Reg::R12,
6689        )?));
6690
6691        // VMOV R0, Sn (get magnitude source bits)
6692        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
6693            false,
6694            sn,
6695            &Reg::R0,
6696        )?));
6697
6698        // AND.W R12, R12, #0x80000000
6699        // Thumb-2 modified immediate: 0x80000000 = constant 0x80 with rotation
6700        // Using T1 encoding: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8
6701        // 0x80000000: i=0, imm3=0b001, imm8=0x00 (rotation=4, value=0x80)
6702        // Actually encoding #0x80000000 as modified constant:
6703        // bit pattern 1 followed by 31 zeros: enc = 0b0100_00000000 = 0x0100? No.
6704        // ARM modified immediate: abcdefgh rotated. 0x80000000 = 0x80 ROR 2 = enc 0x0102
6705        // Actually: value = abcdefgh ROR (2*rot). 0x80 = 10000000, ROR 2 gives 0x20000000.
6706        // For 0x80000000: 0x02 ROR 2 = 0x80000000. So imm12 = (1<<8) | 0x02 = 0x102
6707        let hw1: u16 = 0xF000 | 12; // AND.W R12, R12, #modified_const (i=0, Rn=R12)
6708        let hw2: u16 = (0x1 << 12) | (12 << 8) | 0x02; // imm3=1, Rd=R12, imm8=0x02
6709        bytes.extend_from_slice(&hw1.to_le_bytes());
6710        bytes.extend_from_slice(&hw2.to_le_bytes());
6711
6712        // BIC.W R0, R0, #0x80000000 (R0 = register 0, fields are zero)
6713        let hw1: u16 = 0xF020; // BIC.W R0, R0, #modified_const (i=0, Rn=R0)
6714        let hw2: u16 = (0x1 << 12) | 0x02; // imm3=1, Rd=R0, imm8=0x02
6715        bytes.extend_from_slice(&hw1.to_le_bytes());
6716        bytes.extend_from_slice(&hw2.to_le_bytes());
6717
6718        // ORR.W R0, R0, R12 (R0 = register 0)
6719        let hw1: u16 = 0xEA40; // ORR.W R0, R0, R12 (Rn=R0)
6720        let hw2: u16 = 12; // Rd=R0, Rm=R12
6721        bytes.extend_from_slice(&hw1.to_le_bytes());
6722        bytes.extend_from_slice(&hw2.to_le_bytes());
6723
6724        // VMOV Sd, R0
6725        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
6726            true,
6727            sd,
6728            &Reg::R0,
6729        )?));
6730
6731        Ok(bytes)
6732    }
6733
6734    /// Encode F64 comparison as Thumb-2: VCMP.F64 + VMRS + MOV #0 + IT + MOV #1
6735    fn encode_thumb_f64_compare(
6736        &self,
6737        rd: &Reg,
6738        dn: &VfpReg,
6739        dm: &VfpReg,
6740        cond_code: u32,
6741    ) -> Result<Vec<u8>> {
6742        let mut bytes = Vec::new();
6743        let rd_bits = reg_to_bits(rd);
6744
6745        // VCMP.F64 Dn, Dm
6746        let dn_num = vfp_dreg_to_num(dn)?;
6747        let dm_num = vfp_dreg_to_num(dm)?;
6748        let (vd, d) = encode_dreg(dn_num);
6749        let (vm, m) = encode_dreg(dm_num);
6750        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
6751        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
6752
6753        // VMRS APSR_nzcv, FPSCR
6754        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
6755
6756        // MOVS Rd, #0
6757        if rd_bits < 8 {
6758            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
6759            bytes.extend_from_slice(&movs_zero.to_le_bytes());
6760        } else {
6761            let hw1: u16 = 0xF04F;
6762            let hw2: u16 = (rd_bits as u16) << 8;
6763            bytes.extend_from_slice(&hw1.to_le_bytes());
6764            bytes.extend_from_slice(&hw2.to_le_bytes());
6765        }
6766
6767        // IT<cond>
6768        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
6769        bytes.extend_from_slice(&it.to_le_bytes());
6770
6771        // MOV Rd, #1
6772        if rd_bits < 8 {
6773            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
6774            bytes.extend_from_slice(&mov_one.to_le_bytes());
6775        } else {
6776            let hw1: u16 = 0xF04F;
6777            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
6778            bytes.extend_from_slice(&hw1.to_le_bytes());
6779            bytes.extend_from_slice(&hw2.to_le_bytes());
6780        }
6781
6782        Ok(bytes)
6783    }
6784
6785    /// Encode F64 constant load as Thumb-2: MOVW+MOVT (lo32 into R0) + MOVW+MOVT (hi32 into R12) + VMOV Dd, R0, R12
6786    fn encode_thumb_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
6787        let mut bytes = Vec::new();
6788        let bits = value.to_bits();
6789        let lo32 = bits as u32;
6790        let hi32 = (bits >> 32) as u32;
6791
6792        // MOVW R0, #lo16(lo32)
6793        let lo16 = lo32 & 0xFFFF;
6794        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(0, lo16)?);
6795
6796        // MOVT R0, #hi16(lo32)
6797        let hi16 = (lo32 >> 16) & 0xFFFF;
6798        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(0, hi16)?);
6799
6800        // MOVW R12, #lo16(hi32)
6801        let lo16 = hi32 & 0xFFFF;
6802        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
6803
6804        // MOVT R12, #hi16(hi32)
6805        let hi16 = (hi32 >> 16) & 0xFFFF;
6806        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
6807
6808        // VMOV Dd, R0, R12
6809        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
6810        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6811
6812        Ok(bytes)
6813    }
6814
6815    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as Thumb-2
6816    fn encode_thumb_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
6817        let mut bytes = Vec::new();
6818
6819        // VMOV S0, Rm
6820        let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
6821        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6822
6823        // VCVT.F64.S32 Dd, S0 or VCVT.F64.U32 Dd, S0
6824        let dd_num = vfp_dreg_to_num(dd)?;
6825        let (vd, d) = encode_dreg(dd_num);
6826        let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
6827        let vcvt = base | (d << 22) | (vd << 12);
6828        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
6829
6830        Ok(bytes)
6831    }
6832
6833    /// Encode VCVT.F64.F32 Dd, Sm as Thumb-2
6834    fn encode_thumb_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
6835        let dd_num = vfp_dreg_to_num(dd)?;
6836        let sm_num = vfp_sreg_to_num(sm)?;
6837        let (vd, d) = encode_dreg(dd_num);
6838        let (vm, m) = encode_sreg(sm_num);
6839
6840        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
6841        Ok(vfp_to_thumb_bytes(vcvt))
6842    }
6843
6844    /// Encode VCVT.S32/U32.F64 S0, Dm + VMOV Rd, S0 as Thumb-2
6845    fn encode_thumb_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
6846        let mut bytes = Vec::new();
6847        let dm_num = vfp_dreg_to_num(dm)?;
6848        let (vm, m) = encode_dreg(dm_num);
6849
6850        // VCVT.S32.F64 S0, Dm or VCVT.U32.F64 S0, Dm
6851        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
6852        let vcvt = base | (m << 5) | vm;
6853        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
6854
6855        // VMOV Rd, S0
6856        let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
6857        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6858
6859        Ok(bytes)
6860    }
6861
6862    /// Encode F64 rounding pseudo-op as Thumb-2 via VCVT to integer and back
6863    /// Encode F64 rounding as Thumb-2.
6864    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
6865    fn encode_thumb_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
6866        let mut bytes = Vec::new();
6867        let dm_num = vfp_dreg_to_num(dm)?;
6868        let dd_num = vfp_dreg_to_num(dd)?;
6869        let (vm, m) = encode_dreg(dm_num);
6870        let (vd, d) = encode_dreg(dd_num);
6871
6872        if mode == 0b11 {
6873            // Trunc: VCVTR.S32.F64 — bit[7]=1, always truncates
6874            let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
6875            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
6876        } else {
6877            let rt: u32 = 12;
6878
6879            // VMRS R12, FPSCR
6880            let vmrs = 0xEEF10A10 | (rt << 12);
6881            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
6882
6883            // BIC.W R12, R12, #(3 << 22)
6884            let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF);
6885            let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
6886            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
6887            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
6888
6889            // ORR.W R12, R12, #(mode << 22)
6890            if mode != 0 {
6891                let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF);
6892                let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
6893                bytes.extend_from_slice(&orr_hw1.to_le_bytes());
6894                bytes.extend_from_slice(&orr_hw2.to_le_bytes());
6895            }
6896
6897            // VMSR FPSCR, R12
6898            let vmsr = 0xEEE10A10 | (rt << 12);
6899            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
6900
6901            // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0)
6902            let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
6903            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
6904
6905            // Restore FPSCR
6906            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
6907            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
6908            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
6909            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
6910        }
6911
6912        // VCVT.F64.S32 Dd, S0
6913        let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
6914        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
6915
6916        Ok(bytes)
6917    }
6918
6919    /// Encode F64 min/max as Thumb-2
6920    fn encode_thumb_f64_minmax(
6921        &self,
6922        dd: &VfpReg,
6923        dn: &VfpReg,
6924        dm: &VfpReg,
6925        is_min: bool,
6926    ) -> Result<Vec<u8>> {
6927        let mut bytes = Vec::new();
6928        let dn_num = vfp_dreg_to_num(dn)?;
6929        let dm_num = vfp_dreg_to_num(dm)?;
6930        let dd_num = vfp_dreg_to_num(dd)?;
6931
6932        // VMOV.F64 Dd, Dn
6933        let (vd, d) = encode_dreg(dd_num);
6934        let (vn, n) = encode_dreg(dn_num);
6935        let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
6936        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_dn));
6937
6938        // VCMP.F64 Dn, Dm
6939        let (vm, m) = encode_dreg(dm_num);
6940        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
6941        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
6942
6943        // VMRS APSR_nzcv, FPSCR
6944        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
6945
6946        // IT GT (for min) or IT MI (for max)
6947        let cond: u16 = if is_min { 0xC } else { 0x4 };
6948        let it: u16 = 0xBF00 | (cond << 4) | 0x8;
6949        bytes.extend_from_slice(&it.to_le_bytes());
6950
6951        // VMOV{cond}.F64 Dd, Dm
6952        let vmov_dm = 0xEEB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
6953        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_dm));
6954
6955        Ok(bytes)
6956    }
6957
6958    /// Encode F64 copysign as Thumb-2
6959    fn encode_thumb_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
6960        let mut bytes = Vec::new();
6961
6962        // VMOV R0, R12, Dm (get sign source)
6963        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_dreg(
6964            false,
6965            dm,
6966            &Reg::R0,
6967            &Reg::R12,
6968        )?));
6969
6970        // VMOV R1, R2, Dn (get magnitude source)
6971        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_dreg(
6972            false,
6973            dn,
6974            &Reg::R1,
6975            &Reg::R2,
6976        )?));
6977
6978        // AND.W R12, R12, #0x80000000 (i=0, Rn=R12)
6979        let hw1: u16 = 0xF000 | 12;
6980        let hw2: u16 = (0x1 << 12) | (12 << 8) | 0x02;
6981        bytes.extend_from_slice(&hw1.to_le_bytes());
6982        bytes.extend_from_slice(&hw2.to_le_bytes());
6983
6984        // BIC.W R2, R2, #0x80000000 (i=0, Rn=R2)
6985        let hw1: u16 = 0xF020 | 2;
6986        let hw2: u16 = (0x1 << 12) | (2 << 8) | 0x02;
6987        bytes.extend_from_slice(&hw1.to_le_bytes());
6988        bytes.extend_from_slice(&hw2.to_le_bytes());
6989
6990        // ORR.W R2, R2, R12
6991        let hw1: u16 = 0xEA40 | 2;
6992        let hw2: u16 = (2 << 8) | 12;
6993        bytes.extend_from_slice(&hw1.to_le_bytes());
6994        bytes.extend_from_slice(&hw2.to_le_bytes());
6995
6996        // VMOV Dd, R1, R2
6997        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_dreg(
6998            true,
6999            dd,
7000            &Reg::R1,
7001            &Reg::R2,
7002        )?));
7003
7004        Ok(bytes)
7005    }
7006
7007    /// Encode VCVT.S32/U32.F32 + VMOV as Thumb-2
7008    fn encode_thumb_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7009        let mut bytes = Vec::new();
7010
7011        let sm_num = vfp_sreg_to_num(sm)?;
7012        let (vd, d) = encode_sreg(sm_num);
7013        let (vm, m) = encode_sreg(sm_num);
7014        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
7015        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7016        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7017
7018        // VMOV Rd, Sm
7019        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
7020        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7021
7022        Ok(bytes)
7023    }
7024
7025    // === Thumb-2 32-bit encoding helpers ===
7026
7027    /// Encode Thumb-2 32-bit ADD with immediate
7028    fn encode_thumb32_add(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7029        let rd_bits = reg_to_bits(rd);
7030        let rn_bits = reg_to_bits(rn);
7031
7032        // The `i:imm3:imm8` field is split the same way for both forms.
7033        let i_bit = (imm >> 11) & 1;
7034        let imm3 = (imm >> 8) & 0x7;
7035        let imm8 = imm & 0xFF;
7036
7037        let hw1_base = if imm <= 0xFF {
7038            // ADD.W (T3): the field is a ThumbExpandImm modified immediate. For
7039            // imm <= 0xFF (i:imm3 = 0000) it is the zero-extended byte, which is
7040            // correct — keep this form so existing encodings stay bit-identical.
7041            0xF100
7042        } else if imm <= 0xFFF {
7043            // ADDW (T4): a PLAIN 12-bit immediate (0..4095) — no ThumbExpandImm.
7044            // This is what makes `add sp, sp, #frame` correct for frame sizes
7045            // >= 256, which ADD.W (T3) would silently mis-encode (e.g. #256 -> #0).
7046            0xF200
7047        } else {
7048            return Err(synth_core::Error::synthesis(
7049                "ADD immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7050            ));
7051        };
7052
7053        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7054        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7055
7056        let mut bytes = hw1.to_le_bytes().to_vec();
7057        bytes.extend_from_slice(&hw2.to_le_bytes());
7058        Ok(bytes)
7059    }
7060
7061    /// Encode Thumb-2 32-bit SUB with immediate
7062    fn encode_thumb32_sub(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7063        let rd_bits = reg_to_bits(rd);
7064        let rn_bits = reg_to_bits(rn);
7065
7066        let i_bit = (imm >> 11) & 1;
7067        let imm3 = (imm >> 8) & 0x7;
7068        let imm8 = imm & 0xFF;
7069
7070        let hw1_base = if imm <= 0xFF {
7071            // SUB.W (T3) modified immediate — correct for the zero-extended byte
7072            // (imm <= 0xFF). Kept bit-identical for existing encodings.
7073            0xF1A0
7074        } else if imm <= 0xFFF {
7075            // SUBW (T4): plain 12-bit immediate (0..4095). Makes
7076            // `sub sp, sp, #frame` correct for frame sizes >= 256.
7077            0xF2A0
7078        } else {
7079            return Err(synth_core::Error::synthesis(
7080                "SUB immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7081            ));
7082        };
7083
7084        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7085        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7086
7087        let mut bytes = hw1.to_le_bytes().to_vec();
7088        bytes.extend_from_slice(&hw2.to_le_bytes());
7089        Ok(bytes)
7090    }
7091
7092    /// Encode Thumb-2 32-bit ADDS with immediate (sets flags)
7093    fn encode_thumb32_adds(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7094        let rd_bits = reg_to_bits(rd);
7095        let rn_bits = reg_to_bits(rn);
7096
7097        // ADDS.W (flag-setting) has only the modified-immediate form — error on
7098        // an un-encodable value rather than silently add the wrong constant.
7099        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7100            synth_core::Error::synthesis(
7101                "ADDS immediate is not a valid ThumbExpandImm — materialize into a register",
7102            )
7103        })?;
7104        let i_bit = (field >> 11) & 1;
7105        let imm3 = (field >> 8) & 0x7;
7106        let imm8 = field & 0xFF;
7107
7108        // ADDS.W Rd, Rn, #imm (with S=1)
7109        // First halfword: 1111 0 i 0 1000 1 Rn = F110 | i<<10 | Rn
7110        let hw1: u16 = (0xF110 | (i_bit << 10) | rn_bits) as u16;
7111        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7112
7113        let mut bytes = hw1.to_le_bytes().to_vec();
7114        bytes.extend_from_slice(&hw2.to_le_bytes());
7115        Ok(bytes)
7116    }
7117
7118    /// Encode Thumb-2 32-bit SUBS with immediate (sets flags)
7119    fn encode_thumb32_subs(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7120        let rd_bits = reg_to_bits(rd);
7121        let rn_bits = reg_to_bits(rn);
7122
7123        // SUBS.W (flag-setting) has only the modified-immediate form — error on
7124        // an un-encodable value rather than silently subtract the wrong constant.
7125        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7126            synth_core::Error::synthesis(
7127                "SUBS immediate is not a valid ThumbExpandImm — materialize into a register",
7128            )
7129        })?;
7130        let i_bit = (field >> 11) & 1;
7131        let imm3 = (field >> 8) & 0x7;
7132        let imm8 = field & 0xFF;
7133
7134        // SUBS.W Rd, Rn, #imm (with S=1)
7135        // First halfword: 1111 0 i 0 1101 1 Rn = F1B0 | i<<10 | Rn
7136        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7137        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7138
7139        let mut bytes = hw1.to_le_bytes().to_vec();
7140        bytes.extend_from_slice(&hw2.to_le_bytes());
7141        Ok(bytes)
7142    }
7143
7144    /// Encode Thumb-2 32-bit MOVW (16-bit immediate)
7145    ///
7146    /// # Contract (Verus-style)
7147    /// ```text
7148    /// requires rd <= R14
7149    /// ensures result.len() == 4
7150    /// ensures (imm & 0xFFFF) can be reconstructed from the encoding
7151    /// ```
7152    fn encode_thumb32_movw(&self, rd: &Reg, imm: u32) -> Result<Vec<u8>> {
7153        let rd_bits = reg_to_bits(rd);
7154        reg_bits_checked(rd_bits)?;
7155        let imm16 = imm & 0xFFFF;
7156
7157        // MOVW Rd, #imm16
7158        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7159        let imm4 = (imm16 >> 12) & 0xF;
7160        let i_bit = (imm16 >> 11) & 1;
7161        let imm3 = (imm16 >> 8) & 0x7;
7162        let imm8 = imm16 & 0xFF;
7163
7164        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7165        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7166
7167        let mut bytes = hw1.to_le_bytes().to_vec();
7168        bytes.extend_from_slice(&hw2.to_le_bytes());
7169        encoding_contracts::verify_thumb32(&bytes);
7170        Ok(bytes)
7171    }
7172
7173    /// Encode Thumb-2 32-bit shift with immediate
7174    ///
7175    /// # Contract (Verus-style)
7176    /// ```text
7177    /// requires rd <= R14, rm <= R14
7178    /// ensures result.len() == 4
7179    /// ```
7180    fn encode_thumb32_shift(
7181        &self,
7182        rd: &Reg,
7183        rm: &Reg,
7184        shift: u32,
7185        shift_type: u8,
7186    ) -> Result<Vec<u8>> {
7187        let rd_bits = reg_to_bits(rd);
7188        let rm_bits = reg_to_bits(rm);
7189        reg_bits_checked(rd_bits)?;
7190        reg_bits_checked(rm_bits)?;
7191        let imm5 = shift & 0x1F;
7192        let imm2 = imm5 & 0x3;
7193        let imm3 = (imm5 >> 2) & 0x7;
7194
7195        // MOV.W Rd, Rm, <shift> #imm
7196        // EA4F 0 imm3 Rd imm2 type Rm
7197        let hw1: u16 = 0xEA4F;
7198        let hw2: u16 =
7199            ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | ((shift_type as u32) << 4) | rm_bits)
7200                as u16;
7201
7202        let mut bytes = hw1.to_le_bytes().to_vec();
7203        bytes.extend_from_slice(&hw2.to_le_bytes());
7204        Ok(bytes)
7205    }
7206
7207    /// Encode Thumb-2 32-bit shift by register
7208    /// Encoding: 11111010 0xx0 Rn | 1111 Rd 0000 Rm
7209    /// shift_type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
7210    fn encode_thumb32_shift_reg(
7211        &self,
7212        rd: &Reg,
7213        rn: &Reg,
7214        rm: &Reg,
7215        shift_type: u8,
7216    ) -> Result<Vec<u8>> {
7217        let rd_bits = reg_to_bits(rd);
7218        let rn_bits = reg_to_bits(rn);
7219        let rm_bits = reg_to_bits(rm);
7220
7221        // hw1: 1111 1010 0xx0 Rn
7222        let hw1: u16 = (0xFA00 | ((shift_type as u32) << 5) | rn_bits) as u16;
7223        // hw2: 1111 Rd 0000 Rm
7224        let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
7225
7226        let mut bytes = hw1.to_le_bytes().to_vec();
7227        bytes.extend_from_slice(&hw2.to_le_bytes());
7228        Ok(bytes)
7229    }
7230
7231    /// Encode Thumb-2 32-bit CMP with immediate
7232    fn encode_thumb32_cmp_imm(&self, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7233        let rn_bits = reg_to_bits(rn);
7234
7235        // CMP.W has only the modified-immediate form (no plain-imm12 like ADDW),
7236        // so an un-encodable immediate MUST be materialized into a register by
7237        // the selector. Error rather than silently compare the wrong constant.
7238        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7239            synth_core::Error::synthesis(
7240                "CMP immediate is not a valid ThumbExpandImm — materialize into a register",
7241            )
7242        })?;
7243        let i_bit = (field >> 11) & 1;
7244        let imm3 = (field >> 8) & 0x7;
7245        let imm8 = field & 0xFF;
7246
7247        // CMP.W Rn, #imm
7248        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7249        let hw2: u16 = ((imm3 << 12) | 0x0F00 | imm8) as u16;
7250
7251        let mut bytes = hw1.to_le_bytes().to_vec();
7252        bytes.extend_from_slice(&hw2.to_le_bytes());
7253        Ok(bytes)
7254    }
7255
7256    /// #372/#382: resolve the base register AND residual immediate offset for an
7257    /// `I64Ldr`/`I64Str` whose address may carry an index register. Returns
7258    /// `(base, low_offset)`; the caller accesses the halves at `[base,
7259    /// #low_offset]` and `[base, #low_offset + 4]`.
7260    ///
7261    /// - Frame access (no `offset_reg`, e.g. a spilled local at `[SP, #off]`):
7262    ///   returns `(addr.base, off)` and emits NOTHING — byte-identical.
7263    /// - Memory access (`reg_imm(R11, addr, offset)` = `R11 + addr + offset`)
7264    ///   with `offset + 4 <= 0xFFF`: emits `ADD.W ip, base, index` and returns
7265    ///   `(ip, offset)`, folding `offset`/`offset+4` into the halves' imm12.
7266    ///   Byte-identical to the pre-#382 (#372) behavior.
7267    /// - Memory access with `offset + 4 > 0xFFF`: the imm12 form cannot hold the
7268    ///   high half's offset, so `encode_thumb32_ldr`'s `check_ldst_imm12` (#259)
7269    ///   rightly refused it and the WHOLE function was skipped (#382). Instead
7270    ///   MATERIALIZE the offset into the base: `ADD ip, index, #offset` (against
7271    ///   the read-only INDEX register, so `encode_thumb32_add_imm` never trips its
7272    ///   `rd==rn==R12` alias trap), then `ADD.W ip, ip, base` (+ R11), and return
7273    ///   `(ip, 0)` so the halves use `[ip, #0]` / `[ip, #4]`.
7274    ///
7275    /// The effective address is fully materialized into `ip` BEFORE the halves
7276    /// are accessed, so an `rdlo` aliasing the index register is safe.
7277    fn i64_effective_base(&self, bytes: &mut Vec<u8>, addr: &MemAddr) -> Result<(Reg, u32)> {
7278        let offset = if addr.offset < 0 {
7279            0u32
7280        } else {
7281            addr.offset as u32
7282        };
7283        match addr.offset_reg {
7284            Some(idx) => {
7285                let ip = Reg::R12;
7286                if offset.wrapping_add(4) > 0xFFF {
7287                    // Large static offset (#382): fold it (and R11) into ip so the
7288                    // imm12 halves stay in range instead of skipping the function.
7289                    // ADD ip, index, #offset  (index != ip → no add_imm alias trap)
7290                    bytes.extend_from_slice(&self.encode_thumb32_add_imm(&ip, &idx, offset)?);
7291                    // ADD.W ip, ip, base  (+ R11)
7292                    bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
7293                        reg_to_bits(&ip),
7294                        reg_to_bits(&ip),
7295                        reg_to_bits(&addr.base),
7296                    )?);
7297                    Ok((ip, 0))
7298                } else {
7299                    // ADD.W ip, addr.base, idx  (Thumb-2, byte-verified vs as)
7300                    let hw1: u16 = 0xEB00 | reg_to_bits(&addr.base) as u16;
7301                    let hw2: u16 = 0x0C00 | reg_to_bits(&idx) as u16;
7302                    bytes.extend_from_slice(&hw1.to_le_bytes());
7303                    bytes.extend_from_slice(&hw2.to_le_bytes());
7304                    Ok((ip, offset))
7305                }
7306            }
7307            None => Ok((addr.base, offset)),
7308        }
7309    }
7310
7311    /// Encode Thumb-2 32-bit LDR
7312    fn encode_thumb32_ldr(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7313        let rd_bits = reg_to_bits(rd);
7314        let base_bits = reg_to_bits(base);
7315
7316        // LDR.W Rd, [Rn, #imm12]
7317        check_ldst_imm12(offset)?;
7318        let hw1: u16 = (0xF8D0 | base_bits) as u16;
7319        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7320
7321        let mut bytes = hw1.to_le_bytes().to_vec();
7322        bytes.extend_from_slice(&hw2.to_le_bytes());
7323        Ok(bytes)
7324    }
7325
7326    /// Encode Thumb-2 32-bit STR
7327    fn encode_thumb32_str(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7328        let rd_bits = reg_to_bits(rd);
7329        let base_bits = reg_to_bits(base);
7330
7331        // STR.W Rd, [Rn, #imm12]
7332        check_ldst_imm12(offset)?;
7333        let hw1: u16 = (0xF8C0 | base_bits) as u16;
7334        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7335
7336        let mut bytes = hw1.to_le_bytes().to_vec();
7337        bytes.extend_from_slice(&hw2.to_le_bytes());
7338        Ok(bytes)
7339    }
7340
7341    /// Encode Thumb-2 32-bit LDR with register offset: LDR.W Rd, [Rn, Rm]
7342    fn encode_thumb32_ldr_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7343        let rd_bits = reg_to_bits(rd);
7344        let base_bits = reg_to_bits(base);
7345        let rm_bits = reg_to_bits(offset_reg);
7346
7347        // LDR.W Rd, [Rn, Rm, LSL #0]
7348        // Encoding: 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
7349        // imm2 = 00 for no shift (LSL #0)
7350        let hw1: u16 = (0xF850 | base_bits) as u16;
7351        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7352
7353        let mut bytes = hw1.to_le_bytes().to_vec();
7354        bytes.extend_from_slice(&hw2.to_le_bytes());
7355        Ok(bytes)
7356    }
7357
7358    /// Encode Thumb-2 32-bit STR with register offset: STR.W Rd, [Rn, Rm]
7359    fn encode_thumb32_str_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7360        let rd_bits = reg_to_bits(rd);
7361        let base_bits = reg_to_bits(base);
7362        let rm_bits = reg_to_bits(offset_reg);
7363
7364        // STR.W Rd, [Rn, Rm, LSL #0]
7365        // Encoding: 1111 1000 0100 Rn | Rt 0000 00 imm2 Rm
7366        // imm2 = 00 for no shift (LSL #0)
7367        let hw1: u16 = (0xF840 | base_bits) as u16;
7368        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7369
7370        let mut bytes = hw1.to_le_bytes().to_vec();
7371        bytes.extend_from_slice(&hw2.to_le_bytes());
7372        Ok(bytes)
7373    }
7374
7375    // === Sub-word load/store Thumb-2 encoding helpers ===
7376
7377    /// Encode Thumb-2 32-bit LDRB with immediate: LDRB.W Rd, [Rn, #imm12]
7378    fn encode_thumb32_ldrb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7379        let rd_bits = reg_to_bits(rd);
7380        let base_bits = reg_to_bits(base);
7381        // LDRB.W Rd, [Rn, #imm12]: 1111 1000 1001 Rn | Rt imm12
7382        check_ldst_imm12(offset)?;
7383        let hw1: u16 = (0xF890 | base_bits) as u16;
7384        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7385        let mut bytes = hw1.to_le_bytes().to_vec();
7386        bytes.extend_from_slice(&hw2.to_le_bytes());
7387        Ok(bytes)
7388    }
7389
7390    /// Encode Thumb-2 32-bit LDRB with register: LDRB.W Rd, [Rn, Rm]
7391    fn encode_thumb32_ldrb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7392        let rd_bits = reg_to_bits(rd);
7393        let base_bits = reg_to_bits(base);
7394        let rm_bits = reg_to_bits(offset_reg);
7395        // LDRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0001 Rn | Rt 0000 00 imm2 Rm
7396        let hw1: u16 = (0xF810 | base_bits) as u16;
7397        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7398        let mut bytes = hw1.to_le_bytes().to_vec();
7399        bytes.extend_from_slice(&hw2.to_le_bytes());
7400        Ok(bytes)
7401    }
7402
7403    /// Encode Thumb-2 32-bit LDRSB with immediate: LDRSB.W Rd, [Rn, #imm12]
7404    fn encode_thumb32_ldrsb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7405        let rd_bits = reg_to_bits(rd);
7406        let base_bits = reg_to_bits(base);
7407        // LDRSB.W Rd, [Rn, #imm12]: 1111 1001 1001 Rn | Rt imm12
7408        check_ldst_imm12(offset)?;
7409        let hw1: u16 = (0xF990 | base_bits) as u16;
7410        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7411        let mut bytes = hw1.to_le_bytes().to_vec();
7412        bytes.extend_from_slice(&hw2.to_le_bytes());
7413        Ok(bytes)
7414    }
7415
7416    /// Encode Thumb-2 32-bit LDRSB with register: LDRSB.W Rd, [Rn, Rm]
7417    fn encode_thumb32_ldrsb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7418        let rd_bits = reg_to_bits(rd);
7419        let base_bits = reg_to_bits(base);
7420        let rm_bits = reg_to_bits(offset_reg);
7421        // LDRSB.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0001 Rn | Rt 0000 00 imm2 Rm
7422        let hw1: u16 = (0xF910 | base_bits) as u16;
7423        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7424        let mut bytes = hw1.to_le_bytes().to_vec();
7425        bytes.extend_from_slice(&hw2.to_le_bytes());
7426        Ok(bytes)
7427    }
7428
7429    /// Encode Thumb-2 32-bit LDRH with immediate: LDRH.W Rd, [Rn, #imm12]
7430    fn encode_thumb32_ldrh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7431        let rd_bits = reg_to_bits(rd);
7432        let base_bits = reg_to_bits(base);
7433        // LDRH.W Rd, [Rn, #imm12]: 1111 1000 1011 Rn | Rt imm12
7434        check_ldst_imm12(offset)?;
7435        let hw1: u16 = (0xF8B0 | base_bits) as u16;
7436        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7437        let mut bytes = hw1.to_le_bytes().to_vec();
7438        bytes.extend_from_slice(&hw2.to_le_bytes());
7439        Ok(bytes)
7440    }
7441
7442    /// Encode Thumb-2 32-bit LDRH with register: LDRH.W Rd, [Rn, Rm]
7443    fn encode_thumb32_ldrh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7444        let rd_bits = reg_to_bits(rd);
7445        let base_bits = reg_to_bits(base);
7446        let rm_bits = reg_to_bits(offset_reg);
7447        // LDRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0011 Rn | Rt 0000 00 imm2 Rm
7448        let hw1: u16 = (0xF830 | base_bits) as u16;
7449        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7450        let mut bytes = hw1.to_le_bytes().to_vec();
7451        bytes.extend_from_slice(&hw2.to_le_bytes());
7452        Ok(bytes)
7453    }
7454
7455    /// Encode Thumb-2 32-bit LDRSH with immediate: LDRSH.W Rd, [Rn, #imm12]
7456    fn encode_thumb32_ldrsh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7457        let rd_bits = reg_to_bits(rd);
7458        let base_bits = reg_to_bits(base);
7459        // LDRSH.W Rd, [Rn, #imm12]: 1111 1001 1011 Rn | Rt imm12
7460        check_ldst_imm12(offset)?;
7461        let hw1: u16 = (0xF9B0 | base_bits) as u16;
7462        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7463        let mut bytes = hw1.to_le_bytes().to_vec();
7464        bytes.extend_from_slice(&hw2.to_le_bytes());
7465        Ok(bytes)
7466    }
7467
7468    /// Encode Thumb-2 32-bit LDRSH with register: LDRSH.W Rd, [Rn, Rm]
7469    fn encode_thumb32_ldrsh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7470        let rd_bits = reg_to_bits(rd);
7471        let base_bits = reg_to_bits(base);
7472        let rm_bits = reg_to_bits(offset_reg);
7473        // LDRSH.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0011 Rn | Rt 0000 00 imm2 Rm
7474        let hw1: u16 = (0xF930 | base_bits) as u16;
7475        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7476        let mut bytes = hw1.to_le_bytes().to_vec();
7477        bytes.extend_from_slice(&hw2.to_le_bytes());
7478        Ok(bytes)
7479    }
7480
7481    /// Encode Thumb-2 32-bit STRB with immediate: STRB.W Rd, [Rn, #imm12]
7482    fn encode_thumb32_strb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7483        let rd_bits = reg_to_bits(rd);
7484        let base_bits = reg_to_bits(base);
7485        // STRB.W Rd, [Rn, #imm12]: 1111 1000 1000 Rn | Rt imm12
7486        check_ldst_imm12(offset)?;
7487        let hw1: u16 = (0xF880 | base_bits) as u16;
7488        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7489        let mut bytes = hw1.to_le_bytes().to_vec();
7490        bytes.extend_from_slice(&hw2.to_le_bytes());
7491        Ok(bytes)
7492    }
7493
7494    /// Encode Thumb-2 32-bit STRB with register: STRB.W Rd, [Rn, Rm]
7495    fn encode_thumb32_strb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7496        let rd_bits = reg_to_bits(rd);
7497        let base_bits = reg_to_bits(base);
7498        let rm_bits = reg_to_bits(offset_reg);
7499        // STRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0000 Rn | Rt 0000 00 imm2 Rm
7500        let hw1: u16 = (0xF800 | base_bits) as u16;
7501        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7502        let mut bytes = hw1.to_le_bytes().to_vec();
7503        bytes.extend_from_slice(&hw2.to_le_bytes());
7504        Ok(bytes)
7505    }
7506
7507    /// Encode Thumb-2 32-bit STRH with immediate: STRH.W Rd, [Rn, #imm12]
7508    fn encode_thumb32_strh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7509        let rd_bits = reg_to_bits(rd);
7510        let base_bits = reg_to_bits(base);
7511        // STRH.W Rd, [Rn, #imm12]: 1111 1000 1010 Rn | Rt imm12
7512        check_ldst_imm12(offset)?;
7513        let hw1: u16 = (0xF8A0 | base_bits) as u16;
7514        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7515        let mut bytes = hw1.to_le_bytes().to_vec();
7516        bytes.extend_from_slice(&hw2.to_le_bytes());
7517        Ok(bytes)
7518    }
7519
7520    /// Encode Thumb-2 32-bit STRH with register: STRH.W Rd, [Rn, Rm]
7521    fn encode_thumb32_strh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7522        let rd_bits = reg_to_bits(rd);
7523        let base_bits = reg_to_bits(base);
7524        let rm_bits = reg_to_bits(offset_reg);
7525        // STRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0010 Rn | Rt 0000 00 imm2 Rm
7526        let hw1: u16 = (0xF820 | base_bits) as u16;
7527        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7528        let mut bytes = hw1.to_le_bytes().to_vec();
7529        bytes.extend_from_slice(&hw2.to_le_bytes());
7530        Ok(bytes)
7531    }
7532
7533    /// Encode Thumb-2 32-bit ADD with immediate: ADD.W Rd, Rn, #imm
7534    fn encode_thumb32_add_imm(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7535        let rd_bits = reg_to_bits(rd);
7536        let rn_bits = reg_to_bits(rn);
7537
7538        // For small immediates, use ADD.W Rd, Rn, #imm12
7539        // Encoding: 1111 0 i 0 1 0 0 0 S Rn | 0 imm3 Rd imm8
7540        // S = 0 (don't update flags)
7541        // The 12-bit immediate is encoded as: i:imm3:imm8
7542        // For simplicity, we only support imm <= 0xFFF (direct encoding)
7543        if imm <= 0xFFF {
7544            let i_bit = (imm >> 11) & 1;
7545            let imm3 = (imm >> 8) & 0x7;
7546            let imm8 = imm & 0xFF;
7547
7548            let hw1: u16 = (0xF100 | (i_bit << 10) | rn_bits) as u16;
7549            let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7550
7551            let mut bytes = hw1.to_le_bytes().to_vec();
7552            bytes.extend_from_slice(&hw2.to_le_bytes());
7553            Ok(bytes)
7554        } else {
7555            // Out-of-range immediate (> 0xFFF): materialize it into a scratch
7556            // register, then ADD.W Rd, Rn, scratch. This is the #180/#185
7557            // "encoder must produce a legal sequence, not assert" class — see #350.
7558            //
7559            // Scratch choice (must NEVER equal Rn, or Rn would be clobbered before
7560            // the ADD reads it):
7561            //   - rd != rn  => use rd itself (rn is untouched, since rd != rn).
7562            //   - rd == rn  => use R12/IP (the reserved encoder scratch). rd/rn are
7563            //                  never R12 (R12 is non-allocatable), so it can't alias.
7564            //
7565            // The materialized value is the same whether or not MOVT is emitted, so
7566            // the byte length depends only on `imm` (and rd==rn) — the size probe and
7567            // the final emit therefore agree (mandatory: the function is encoded twice).
7568            let scratch: u32 = if rd_bits == rn_bits {
7569                12 // R12/IP — in-place add, can't use rd because rd == rn
7570            } else {
7571                rd_bits // rn is preserved because rd != rn
7572            };
7573            // Invariant: the scratch must never alias Rn (would clobber it before
7574            // the ADD reads it). Unreachable in real codegen (rd/rn are never R12,
7575            // which is reserved encoder scratch), but the encoder is also driven by
7576            // the `encoder_no_panic` fuzz harness with ARBITRARY registers — incl.
7577            // rd==rn==R12, which makes scratch (R12) alias Rn. The encoder contract
7578            // (#180/#185) is Ok-or-Err, never a panic, so return a typed error
7579            // instead of asserting. #350 follow-up.
7580            if scratch == rn_bits {
7581                return Err(synth_core::Error::synthesis(format!(
7582                    "ADD #imm: cannot lower #{imm:#x} for Rd==Rn==R12 — no free scratch \
7583                     register (R12 is the reserved encoder scratch and aliases Rn here)"
7584                )));
7585            }
7586
7587            let lo16 = imm & 0xFFFF;
7588            let hi16 = (imm >> 16) & 0xFFFF;
7589
7590            let mut bytes = self.encode_thumb32_movw_raw(scratch, lo16)?;
7591            if hi16 != 0 {
7592                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(scratch, hi16)?);
7593            }
7594            bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rn_bits, scratch)?);
7595            Ok(bytes)
7596        }
7597    }
7598
7599    // === Raw encoding helpers for POPCNT (take register numbers directly) ===
7600
7601    /// Encode Thumb-2 32-bit MOVW (16-bit immediate) - raw version
7602    ///
7603    /// # Contract (Verus-style)
7604    /// ```text
7605    /// requires rd <= 14, imm16 <= 0xFFFF
7606    /// ensures result.len() == 4
7607    /// ```
7608    fn encode_thumb32_movw_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
7609        reg_bits_checked(rd)?;
7610        encoding_contracts::verify_imm16(imm16);
7611        // MOVW Rd, #imm16
7612        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7613        let imm16 = imm16 & 0xFFFF;
7614        let imm4 = (imm16 >> 12) & 0xF;
7615        let i_bit = (imm16 >> 11) & 1;
7616        let imm3 = (imm16 >> 8) & 0x7;
7617        let imm8 = imm16 & 0xFF;
7618
7619        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7620        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
7621
7622        let mut bytes = hw1.to_le_bytes().to_vec();
7623        bytes.extend_from_slice(&hw2.to_le_bytes());
7624        encoding_contracts::verify_thumb32(&bytes);
7625        Ok(bytes)
7626    }
7627
7628    /// Encode Thumb-2 32-bit MOVT (move top 16 bits) - raw version
7629    ///
7630    /// # Contract (Verus-style)
7631    /// ```text
7632    /// requires rd <= 14, imm16 <= 0xFFFF
7633    /// ensures result.len() == 4
7634    /// ```
7635    fn encode_thumb32_movt_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
7636        reg_bits_checked(rd)?;
7637        encoding_contracts::verify_imm16(imm16);
7638        // MOVT Rd, #imm16
7639        // 1111 0 i 10 1 1 0 0 imm4 | 0 imm3 Rd imm8
7640        let imm16 = imm16 & 0xFFFF;
7641        let imm4 = (imm16 >> 12) & 0xF;
7642        let i_bit = (imm16 >> 11) & 1;
7643        let imm3 = (imm16 >> 8) & 0x7;
7644        let imm8 = imm16 & 0xFF;
7645
7646        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
7647        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
7648
7649        let mut bytes = hw1.to_le_bytes().to_vec();
7650        bytes.extend_from_slice(&hw2.to_le_bytes());
7651        encoding_contracts::verify_thumb32(&bytes);
7652        Ok(bytes)
7653    }
7654
7655    /// Encode Thumb-2 32-bit LSR (logical shift right) with immediate - raw version
7656    fn encode_thumb32_lsr_raw(&self, rd: u32, rm: u32, shift: u32) -> Result<Vec<u8>> {
7657        // MOV.W Rd, Rm, LSR #imm
7658        // EA4F 0 imm3 Rd imm2 01 Rm
7659        let imm5 = shift & 0x1F;
7660        let imm2 = imm5 & 0x3;
7661        let imm3 = (imm5 >> 2) & 0x7;
7662
7663        let hw1: u16 = 0xEA4F;
7664        let hw2: u16 = ((imm3 << 12) | (rd << 8) | (imm2 << 6) | (0b01 << 4) | rm) as u16;
7665
7666        let mut bytes = hw1.to_le_bytes().to_vec();
7667        bytes.extend_from_slice(&hw2.to_le_bytes());
7668        Ok(bytes)
7669    }
7670
7671    /// Encode Thumb-2 32-bit AND (register) - raw version
7672    fn encode_thumb32_and_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7673        // AND.W Rd, Rn, Rm
7674        // EA00 Rn | 0 Rd 00 00 Rm
7675        let hw1: u16 = (0xEA00 | rn) as u16;
7676        let hw2: u16 = ((rd << 8) | rm) as u16;
7677
7678        let mut bytes = hw1.to_le_bytes().to_vec();
7679        bytes.extend_from_slice(&hw2.to_le_bytes());
7680        Ok(bytes)
7681    }
7682
7683    /// Encode Thumb-2 32-bit AND with immediate - raw version
7684    fn encode_thumb32_and_imm_raw(&self, rd: u32, rn: u32, imm: u32) -> Result<Vec<u8>> {
7685        // AND.W Rd, Rn, #<modified_immediate>
7686        // For small immediates (0-255), the encoding is simpler
7687        // F0 00 Rn | 0 imm3 Rd imm8
7688        let i_bit = (imm >> 11) & 1;
7689        let imm3 = (imm >> 8) & 0x7;
7690        let imm8 = imm & 0xFF;
7691
7692        let hw1: u16 = (0xF000 | (i_bit << 10) | rn) as u16;
7693        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
7694
7695        let mut bytes = hw1.to_le_bytes().to_vec();
7696        bytes.extend_from_slice(&hw2.to_le_bytes());
7697        Ok(bytes)
7698    }
7699
7700    /// Encode Thumb-2 32-bit SUB (register) - raw version
7701    fn encode_thumb32_sub_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7702        // SUB.W Rd, Rn, Rm
7703        // EBA0 Rn | 0 Rd 00 00 Rm
7704        let hw1: u16 = (0xEBA0 | rn) as u16;
7705        let hw2: u16 = ((rd << 8) | rm) as u16;
7706
7707        let mut bytes = hw1.to_le_bytes().to_vec();
7708        bytes.extend_from_slice(&hw2.to_le_bytes());
7709        Ok(bytes)
7710    }
7711
7712    /// Encode Thumb-2 32-bit ADD (register) - raw version
7713    fn encode_thumb32_add_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7714        // ADD.W Rd, Rn, Rm
7715        // EB00 Rn | 0 Rd 00 00 Rm
7716        let hw1: u16 = (0xEB00 | rn) as u16;
7717        let hw2: u16 = ((rd << 8) | rm) as u16;
7718
7719        let mut bytes = hw1.to_le_bytes().to_vec();
7720        bytes.extend_from_slice(&hw2.to_le_bytes());
7721        Ok(bytes)
7722    }
7723
7724    /// Encode Thumb-2 32-bit ADDS (register, flag-setting) - raw version.
7725    /// Used as the high-register fallback for `ArmOp::Adds` (i64 low-word add)
7726    /// so R8-R11 pair operands don't overflow the 16-bit field — #178/#180.
7727    fn encode_thumb32_adds_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7728        // ADDS.W Rd, Rn, Rm (T3, S=1): EB10 Rn | 0 Rd 00 00 Rm
7729        let hw1: u16 = (0xEB10 | rn) as u16;
7730        let hw2: u16 = ((rd << 8) | rm) as u16;
7731        let mut bytes = hw1.to_le_bytes().to_vec();
7732        bytes.extend_from_slice(&hw2.to_le_bytes());
7733        Ok(bytes)
7734    }
7735
7736    /// Encode Thumb-2 32-bit SUBS (register, flag-setting) - raw version.
7737    /// High-register fallback for `ArmOp::Subs` (i64 low-word subtract) — #178/#180.
7738    fn encode_thumb32_subs_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7739        // SUBS.W Rd, Rn, Rm (T3, S=1): EBB0 Rn | 0 Rd 00 00 Rm
7740        let hw1: u16 = (0xEBB0 | rn) as u16;
7741        let hw2: u16 = ((rd << 8) | rm) as u16;
7742        let mut bytes = hw1.to_le_bytes().to_vec();
7743        bytes.extend_from_slice(&hw2.to_le_bytes());
7744        Ok(bytes)
7745    }
7746
7747    /// Encode a sequence of ARM instructions
7748    pub fn encode_sequence(&self, ops: &[ArmOp]) -> Result<Vec<u8>> {
7749        let mut code = Vec::new();
7750
7751        for op in ops {
7752            let encoded = self.encode(op)?;
7753            code.extend_from_slice(&encoded);
7754        }
7755
7756        Ok(code)
7757    }
7758}
7759
7760/// Convert register to bit encoding (0-15)
7761/// Reverse of the ARMv7-M `ThumbExpandImm`: given a 32-bit immediate, return the
7762/// 12-bit `i:imm3:imm8` field if it is a representable modified immediate, else
7763/// `None` (the caller must materialize the value into a register). This is the
7764/// shared correct path for the data-processing immediate encoders — without it
7765/// they pack raw bits and silently mis-encode any value `> 0xFF` that isn't a
7766/// modified immediate (the silent-miscompile class behind #251/#253/#255).
7767fn try_thumb_expand_imm(value: u32) -> Option<u32> {
7768    // i:imm3 = 0000 → 8-bit value, zero-extended (00000000 00000000 00000000 XY).
7769    if value <= 0xFF {
7770        return Some(value);
7771    }
7772    let b0 = value & 0xFF; // byte 0
7773    let b1 = (value >> 8) & 0xFF; // byte 1
7774    // 0x00XY00XY (i:imm3 = 0001) — XY in bytes 0 and 2
7775    if value == (b0 << 16) | b0 {
7776        return Some(0x100 | b0);
7777    }
7778    // 0xXY00XY00 (i:imm3 = 0010) — XY in bytes 1 and 3
7779    if value == (b1 << 24) | (b1 << 8) {
7780        return Some(0x200 | b1);
7781    }
7782    // 0xXYXYXYXY (i:imm3 = 0011) — XY in all four bytes
7783    if value == (b0 << 24) | (b0 << 16) | (b0 << 8) | b0 {
7784        return Some(0x300 | b0);
7785    }
7786    // An 8-bit value with bit 7 set, rotated right by 8..=31. `rotate_left(rot)`
7787    // undoes the encoded right rotation; if the result is `1bbbbbbb` (0x80..=0xFF)
7788    // the value is representable. imm12[11:7] = rot, imm12[6:0] = low 7 bits.
7789    for rot in 8..=31u32 {
7790        let unrot = value.rotate_left(rot);
7791        if (0x80..=0xFF).contains(&unrot) {
7792            return Some((rot << 7) | (unrot & 0x7F));
7793        }
7794    }
7795    None
7796}
7797
7798/// Guard a Thumb-2 `LDR/STR Rd, [Rn, #imm12]` offset. The imm12 form supports
7799/// `0..=4095`; a larger offset must be materialized into a register by the
7800/// selector (register-offset addressing). Returning `Err` rather than silently
7801/// masking `offset & 0xFFF` closes the wrong-address miscompile class (#259,
7802/// the load/store sibling of #253/#255).
7803fn check_ldst_imm12(offset: u32) -> Result<()> {
7804    if offset > 0xFFF {
7805        Err(synth_core::Error::synthesis(
7806            "load/store immediate offset > 0xFFF (4095) — materialize the offset into a register",
7807        ))
7808    } else {
7809        Ok(())
7810    }
7811}
7812
7813fn reg_to_bits(reg: &Reg) -> u32 {
7814    match reg {
7815        Reg::R0 => 0,
7816        Reg::R1 => 1,
7817        Reg::R2 => 2,
7818        Reg::R3 => 3,
7819        Reg::R4 => 4,
7820        Reg::R5 => 5,
7821        Reg::R6 => 6,
7822        Reg::R7 => 7,
7823        Reg::R8 => 8,
7824        Reg::R9 => 9,
7825        Reg::R10 => 10,
7826        Reg::R11 => 11,
7827        Reg::R12 => 12,
7828        Reg::SP => 13,
7829        Reg::LR => 14,
7830        Reg::PC => 15,
7831    }
7832}
7833
7834// ======================================================================
7835// #610 — i64 fixed-ABI expansion wrappers.
7836//
7837// The hand-written multi-instruction i64 cores (rotl/rotr and the div/rem
7838// shift-subtract loops) compute in FIXED low registers. Before #610 the
7839// div/rem arms ignored their operand fields outright (hardcoded R0:R1 /
7840// R2:R3 in, result to R0:R1) and the rot arms used R3/R4 scratch that
7841// collided with selector-assigned registers — then restored the saved
7842// scratch OVER the result (`POP {R4}` with rd_lo == R4), so the op
7843// returned the caller's stale register: 0 for every input under qemu.
7844//
7845// These wrappers make each core honor its register parameters:
7846//   1. save R0-R3,
7847//   2. marshal the operand registers into the core's fixed input regs via
7848//      the stack (permutation-safe: every source is read before any fixed
7849//      register is written),
7850//   3. run the fixed-reg core (self-preserving for R4+; R12 is encoder
7851//      scratch and never allocatable, #212),
7852//   4. MOV the result pair from R0:R1 into the selector's rd pair,
7853//   5. restore R0-R3, skipping any register the result now occupies.
7854//
7855// All emitted lengths are register-independent so the optimized path's
7856// byte-size estimator (`estimate_arm_byte_size`, pinned by the
7857// estimator↔encoder agreement oracle #498/#511) stays a constant per op.
7858// ======================================================================
7859
7860/// Steps 1+2: `PUSH {R0-R3}`, then marshal `srcs` (operand registers, any of
7861/// R0-R12) into `R0..R<n>` via individual stack pushes. Sources are all read
7862/// before any destination register is written, so arbitrary source/target
7863/// permutations (including operands living in R0-R3) are safe.
7864fn emit_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
7865    debug_assert!(srcs.len() <= 4);
7866    // PUSH {R0-R3} — save the caller-visible low registers.
7867    bytes.extend_from_slice(&0xB40Fu16.to_le_bytes());
7868    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
7869    for src in srcs.iter().rev() {
7870        let rt = reg_to_bits(src) as u16;
7871        bytes.extend_from_slice(&0xF84Du16.to_le_bytes());
7872        bytes.extend_from_slice(&((rt << 12) | 0x0D04).to_le_bytes());
7873    }
7874    // POP {Ri} — Ri := srcs[i].
7875    for i in 0..srcs.len() as u16 {
7876        bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes());
7877    }
7878}
7879
7880/// Steps 4+5: move the core's R0:R1 result into the selector's rd pair, then
7881/// restore the R0-R3 saved by [`emit_i64_fixed_abi_entry`], skipping any
7882/// register the result now lives in (its saved caller word is discarded).
7883fn emit_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
7884    let lo = reg_to_bits(rdlo);
7885    let hi = reg_to_bits(rdhi);
7886    if lo == 1 && hi == 0 {
7887        // A fully swapped pair would clobber one half in either MOV order.
7888        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
7889        return Err(synth_core::Error::synthesis(
7890            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
7891        ));
7892    }
7893    let mov16 = |bytes: &mut Vec<u8>, rd: u32, rm: u32| {
7894        let d = ((rd >> 3) & 1) as u16;
7895        bytes.extend_from_slice(
7896            &(0x4600u16 | (d << 7) | ((rm as u16) << 3) | ((rd & 7) as u16)).to_le_bytes(),
7897        );
7898    };
7899    if hi == 0 {
7900        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
7901        mov16(bytes, lo, 0);
7902        mov16(bytes, hi, 1);
7903    } else {
7904        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
7905        mov16(bytes, hi, 1);
7906        mov16(bytes, lo, 0);
7907    }
7908    for i in 0..4u32 {
7909        if i == lo || i == hi {
7910            // The result lives here — drop the saved caller word.
7911            bytes.extend_from_slice(&0xB001u16.to_le_bytes()); // ADD SP, #4
7912        } else {
7913            bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes()); // POP {Ri}
7914        }
7915    }
7916    Ok(())
7917}
7918
7919/// WASM `i64.div_*` / `i64.rem_*` by zero must trap, matching the i32 path's
7920/// cmp/bne/udf guard. Emitted after marshaling, when the divisor pair is in
7921/// R2:R3: `ORRS R12, R2, R3` — `BNE` over a `UDF #0` when nonzero.
7922fn emit_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
7923    bytes.extend_from_slice(&0xEA52u16.to_le_bytes()); // ORRS.W R12, R2, R3
7924    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
7925    bytes.extend_from_slice(&0xD100u16.to_le_bytes()); // BNE.N +0 (skip the UDF)
7926    bytes.extend_from_slice(&0xDE00u16.to_le_bytes()); // UDF #0 — divide by zero
7927}
7928
7929/// WASM `i64.div_s(INT64_MIN, -1)` must trap (Core §4.3.2 `idiv_s`: the
7930/// quotient +2^63 is unrepresentable), matching the i32 path's overflow
7931/// guard — #633: without it the core negated INT64_MIN onto itself and
7932/// silently returned INT64_MIN. Emitted after marshaling, when the dividend
7933/// pair is in R0:R1 and the divisor pair in R2:R3; R12 is encoder scratch.
7934///
7935/// div_s ONLY — `i64.rem_s(INT64_MIN, -1)` is defined as 0 and must NOT
7936/// trap (`irem_s`), so the I64RemS arm never calls this. 22 bytes,
7937/// register-independent (estimator contract, #498/#511).
7938fn emit_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
7939    // AND.W R12, R2, R3 — R12 == 0xFFFFFFFF iff divisor == -1
7940    bytes.extend_from_slice(&0xEA02u16.to_le_bytes());
7941    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
7942    // CMN.W R12, #1 — EQ iff both divisor words are all-ones
7943    bytes.extend_from_slice(&0xF11Cu16.to_le_bytes());
7944    bytes.extend_from_slice(&0x0F01u16.to_le_bytes());
7945    // BNE .no_trap
7946    bytes.extend_from_slice(&0xD105u16.to_le_bytes());
7947    // CMP R0, #0 — dividend lo word of INT64_MIN
7948    bytes.extend_from_slice(&0x2800u16.to_le_bytes());
7949    // BNE .no_trap
7950    bytes.extend_from_slice(&0xD103u16.to_le_bytes());
7951    // CMP.W R1, #0x80000000 — dividend hi word of INT64_MIN
7952    bytes.extend_from_slice(&0xF1B1u16.to_le_bytes());
7953    bytes.extend_from_slice(&0x4F00u16.to_le_bytes());
7954    // BNE .no_trap
7955    bytes.extend_from_slice(&0xD100u16.to_le_bytes());
7956    // UDF #0 — signed-division overflow
7957    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
7958    // .no_trap:
7959}
7960
7961// ======================================================================
7962// #615 — A32 (ARM-mode) twins of the #610 i64 fixed-ABI wrappers above.
7963// Identical register contract, A32 encodings: the multi-instruction i64
7964// cores (rotl/rotr, div/rem) compute in fixed low registers (value/dividend
7965// R0:R1, amount R2 / divisor R2:R3, result to R0:R1); the wrappers marshal
7966// the selector-assigned operand registers in and the result out, saving and
7967// restoring the caller-visible R0-R3 around the core.
7968// ======================================================================
7969
7970/// A32 steps 1+2: `STMDB SP!, {R0-R3}`, then marshal `srcs` into `R0..R<n>`
7971/// via individual stack pushes (`STR src, [SP, #-4]!` in reverse order, then
7972/// `LDR Ri, [SP], #4`). Every source is read before any fixed register is
7973/// written, so arbitrary source/target permutations are safe.
7974fn emit_a32_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
7975    debug_assert!(srcs.len() <= 4);
7976    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
7977    // PUSH {R0-R3} — save the caller-visible low registers.
7978    w(bytes, 0xE92D_000F);
7979    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
7980    for src in srcs.iter().rev() {
7981        w(bytes, 0xE52D_0004 | (reg_to_bits(src) << 12));
7982    }
7983    // LDR Ri, [SP], #4 — Ri := srcs[i].
7984    for i in 0..srcs.len() as u32 {
7985        w(bytes, 0xE49D_0004 | (i << 12));
7986    }
7987}
7988
7989/// A32 steps 4+5: move the core's R0:R1 result into the selector's rd pair,
7990/// then restore the R0-R3 saved by [`emit_a32_i64_fixed_abi_entry`], skipping
7991/// any register the result now lives in (its saved caller word is discarded).
7992fn emit_a32_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
7993    let lo = reg_to_bits(rdlo);
7994    let hi = reg_to_bits(rdhi);
7995    if lo == 1 && hi == 0 {
7996        // A fully swapped pair would clobber one half in either MOV order.
7997        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
7998        return Err(synth_core::Error::synthesis(
7999            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8000        ));
8001    }
8002    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8003    let mov = |bytes: &mut Vec<u8>, rd: u32, rm: u32| w(bytes, 0xE1A0_0000 | (rd << 12) | rm);
8004    if hi == 0 {
8005        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8006        mov(bytes, lo, 0);
8007        mov(bytes, hi, 1);
8008    } else {
8009        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8010        mov(bytes, hi, 1);
8011        mov(bytes, lo, 0);
8012    }
8013    for i in 0..4u32 {
8014        if i == lo || i == hi {
8015            // The result lives here — drop the saved caller word.
8016            w(bytes, 0xE28D_D004); // ADD SP, SP, #4
8017        } else {
8018            w(bytes, 0xE49D_0004 | (i << 12)); // LDR Ri, [SP], #4
8019        }
8020    }
8021    Ok(())
8022}
8023
8024/// A32 zero-divisor trap, emitted after marshaling when the divisor pair is
8025/// in R2:R3: `ORRS R12, R2, R3` sets Z iff the divisor is zero; `BNE` skips a
8026/// `UDF #0` (WASM div/rem-by-zero must trap, matching the Thumb-2 twin).
8027fn emit_a32_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8028    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8029    w(bytes, 0xE192_C003); // ORRS R12, R2, R3
8030    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8031    w(bytes, 0xE7F0_00F0); // UDF #0 — divide by zero
8032}
8033
8034/// A32 twin of [`emit_i64_divs_overflow_trap`] (#633): trap on
8035/// `i64.div_s(INT64_MIN, -1)`. Conditional execution replaces the Thumb
8036/// branches — the CMPEQ chain leaves EQ set only when divisor == -1 AND
8037/// dividend == INT64_MIN. div_s only; rem_s must keep returning 0.
8038fn emit_a32_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8039    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8040    w(bytes, 0xE002_C003); // AND   R12, R2, R3 (== 0xFFFFFFFF iff divisor == -1)
8041    w(bytes, 0xE37C_0001); // CMN   R12, #1     (EQ iff divisor == -1)
8042    w(bytes, 0x0350_0000); // CMPEQ R0, #0      (EQ iff also dividend lo == 0)
8043    w(bytes, 0x0351_0102); // CMPEQ R1, #0x80000000 (EQ iff dividend == INT64_MIN)
8044    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8045    w(bytes, 0xE7F0_00F0); // UDF #0 — signed-division overflow
8046}
8047
8048/// Fallible form of the `verify_reg_bits` contract. PC (R15) is not a valid
8049/// data operand for the Thumb-2 encodings that use this guard (SDIV/UDIV/MLS/…
8050/// are UNPREDICTABLE with PC). Synth's own codegen never emits PC there, but
8051/// the encoder must stay *total* over arbitrary `ArmOp` inputs — the fuzz
8052/// harness (`encoder_no_panic`) requires Ok-or-Err, never a panic. Pre-fix, the
8053/// `debug_assert` in `verify_reg_bits` aborted under `-Cdebug-assertions`.
8054/// Returns a typed Err instead. See #185.
8055fn reg_bits_checked(bits: u32) -> Result<()> {
8056    if bits > 14 {
8057        return Err(synth_core::Error::synthesis(format!(
8058            "register bits {bits} (PC/R15) is not a valid operand for this Thumb-2 encoding"
8059        )));
8060    }
8061    Ok(())
8062}
8063
8064/// Try to encode a 32-bit value as an ARM rotated immediate (imm8 ROR 2*rot4).
8065/// Returns Some((encoded_bits, 1)) if representable, None otherwise.
8066fn try_encode_rotated_imm(val: u32) -> Option<(u32, u32)> {
8067    if val == 0 {
8068        return Some((0, 1));
8069    }
8070    for rot in 0..16u32 {
8071        let shift = rot * 2;
8072        // Rotate left by shift (undo the ROR) to see if result fits in 8 bits
8073        let unrotated = val.rotate_left(shift);
8074        if unrotated <= 0xFF {
8075            // Encoded as: rot4(4 bits) | imm8(8 bits) = rotate_imm << 8 | imm8
8076            return Some(((rot << 8) | unrotated, 1));
8077        }
8078    }
8079    None
8080}
8081
8082/// Encode operand2 field and return (bits, immediate_flag).
8083/// For ARM32 mode, immediates use the rotated-immediate encoding (imm8 ROR 2*rot4).
8084/// Panics if an immediate value cannot be represented. Callers that need large
8085/// immediates should use MOVW/MOVT instead of Operand2::Imm.
8086fn encode_operand2(op2: &Operand2) -> Result<(u32, u32)> {
8087    match op2 {
8088        Operand2::Imm(val) => {
8089            let uval = *val as u32;
8090            // Attempt rotated-immediate encoding (ARM32 Operand2)
8091            if let Some(encoded) = try_encode_rotated_imm(uval) {
8092                Ok(encoded)
8093            } else {
8094                // #378-class honesty: an immediate that can't be expressed as an
8095                // ARM32 rotated immediate is an INTERNAL selector bug — large
8096                // constants must be materialized via MOVW/MOVT, not passed here.
8097                // FAIL HONESTLY with an Err rather than silently masking to
8098                // `uval & 0xFF` and emitting a WRONG immediate. The encoder is
8099                // Ok-or-Err, never corrupt (#180/#185); a loud Err is also why
8100                // this is an Err and not a panic (the `encoder_no_panic` fuzz
8101                // contract — malformed/oversized input must degrade, not crash).
8102                Err(synth_core::Error::synthesis(format!(
8103                    "encode_operand2: immediate {uval:#x} ({val}) is not an ARM32 \
8104                     rotated immediate — the selector must materialize large \
8105                     constants via MOVW/MOVT"
8106                )))
8107            }
8108        }
8109
8110        Operand2::Reg(reg) => {
8111            let reg_bits = reg_to_bits(reg);
8112            Ok((reg_bits, 0)) // I=0 for register
8113        }
8114
8115        Operand2::RegShift {
8116            rm,
8117            shift: _,
8118            amount,
8119        } => {
8120            // Simplified encoding with shift
8121            let rm_bits = reg_to_bits(rm);
8122            let shift_bits = (*amount & 0x1F) << 7;
8123            Ok((shift_bits | rm_bits, 0))
8124        }
8125    }
8126}
8127
8128/// Encode memory address to (base_reg, offset)
8129fn encode_mem_addr(addr: &MemAddr) -> (u32, u32) {
8130    let base_bits = reg_to_bits(&addr.base);
8131    let offset_bits = (addr.offset as u32) & 0xFFF; // 12-bit offset
8132    (base_bits, offset_bits)
8133}
8134
8135/// S-register number: S0=0, S1=1, ..., S31=31
8136fn vfp_sreg_to_num(reg: &VfpReg) -> Result<u32> {
8137    match reg {
8138        VfpReg::S0 => Ok(0),
8139        VfpReg::S1 => Ok(1),
8140        VfpReg::S2 => Ok(2),
8141        VfpReg::S3 => Ok(3),
8142        VfpReg::S4 => Ok(4),
8143        VfpReg::S5 => Ok(5),
8144        VfpReg::S6 => Ok(6),
8145        VfpReg::S7 => Ok(7),
8146        VfpReg::S8 => Ok(8),
8147        VfpReg::S9 => Ok(9),
8148        VfpReg::S10 => Ok(10),
8149        VfpReg::S11 => Ok(11),
8150        VfpReg::S12 => Ok(12),
8151        VfpReg::S13 => Ok(13),
8152        VfpReg::S14 => Ok(14),
8153        VfpReg::S15 => Ok(15),
8154        VfpReg::S16 => Ok(16),
8155        VfpReg::S17 => Ok(17),
8156        VfpReg::S18 => Ok(18),
8157        VfpReg::S19 => Ok(19),
8158        VfpReg::S20 => Ok(20),
8159        VfpReg::S21 => Ok(21),
8160        VfpReg::S22 => Ok(22),
8161        VfpReg::S23 => Ok(23),
8162        VfpReg::S24 => Ok(24),
8163        VfpReg::S25 => Ok(25),
8164        VfpReg::S26 => Ok(26),
8165        VfpReg::S27 => Ok(27),
8166        VfpReg::S28 => Ok(28),
8167        VfpReg::S29 => Ok(29),
8168        VfpReg::S30 => Ok(30),
8169        VfpReg::S31 => Ok(31),
8170        // D-registers are not used in F32 single-precision encodings
8171        _ => Err(synth_core::Error::SynthesisError(
8172            "D-register not supported in single-precision VFP encoding".to_string(),
8173        )),
8174    }
8175}
8176
8177/// D-register number: D0=0, D1=1, ..., D15=15
8178fn vfp_dreg_to_num(reg: &VfpReg) -> Result<u32> {
8179    match reg {
8180        VfpReg::D0 => Ok(0),
8181        VfpReg::D1 => Ok(1),
8182        VfpReg::D2 => Ok(2),
8183        VfpReg::D3 => Ok(3),
8184        VfpReg::D4 => Ok(4),
8185        VfpReg::D5 => Ok(5),
8186        VfpReg::D6 => Ok(6),
8187        VfpReg::D7 => Ok(7),
8188        VfpReg::D8 => Ok(8),
8189        VfpReg::D9 => Ok(9),
8190        VfpReg::D10 => Ok(10),
8191        VfpReg::D11 => Ok(11),
8192        VfpReg::D12 => Ok(12),
8193        VfpReg::D13 => Ok(13),
8194        VfpReg::D14 => Ok(14),
8195        VfpReg::D15 => Ok(15),
8196        // S-registers are not used in F64 double-precision encodings
8197        _ => Err(synth_core::Error::SynthesisError(
8198            "S-register not supported in double-precision VFP encoding".to_string(),
8199        )),
8200    }
8201}
8202
8203/// Split S-register into (Vx[3:0], qualifier_bit) for VFP encoding.
8204/// For an S-register number s: Vx = s >> 1, qualifier = s & 1.
8205/// The qualifier bit goes to D (bit 22), N (bit 7), or M (bit 5) depending on role.
8206fn encode_sreg(s: u32) -> (u32, u32) {
8207    (s >> 1, s & 1)
8208}
8209
8210/// Split D-register into (Vx[3:0], qualifier_bit) for VFP double-precision encoding.
8211/// For a D-register number d: Vx = d & 0xF, qualifier = (d >> 4) & 1.
8212/// For D0-D15, qualifier is always 0.
8213fn encode_dreg(d: u32) -> (u32, u32) {
8214    (d & 0xF, (d >> 4) & 1)
8215}
8216
8217/// Encode a VFP 3-register arithmetic instruction (VADD.F32, VSUB.F32, VMUL.F32, VDIV.F32).
8218/// Returns the full 32-bit instruction word.
8219///
8220/// VFP encoding: [cond 1110] [D opc1 Vn] [Vd 101 sz] [N opc2 M 0 Vm]
8221/// For single-precision (sz=0), coprocessor = 0xA (bits[11:8]).
8222fn encode_vfp_3reg(base: u32, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<u32> {
8223    let sd_num = vfp_sreg_to_num(sd)?;
8224    let sn_num = vfp_sreg_to_num(sn)?;
8225    let sm_num = vfp_sreg_to_num(sm)?;
8226    let (vd, d) = encode_sreg(sd_num);
8227    let (vn, n) = encode_sreg(sn_num);
8228    let (vm, m) = encode_sreg(sm_num);
8229
8230    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8231}
8232
8233/// Encode a VFP 2-register instruction (VNEG.F32, VABS.F32, VSQRT.F32).
8234/// Returns the full 32-bit instruction word.
8235fn encode_vfp_2reg(base: u32, sd: &VfpReg, sm: &VfpReg) -> Result<u32> {
8236    let sd_num = vfp_sreg_to_num(sd)?;
8237    let sm_num = vfp_sreg_to_num(sm)?;
8238    let (vd, d) = encode_sreg(sd_num);
8239    let (vm, m) = encode_sreg(sm_num);
8240
8241    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8242}
8243
8244/// Encode a VFP load/store (VLDR.F32 / VSTR.F32).
8245/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8246/// U bit (bit 23) controls add/subtract offset.
8247fn encode_vfp_ldst(base: u32, sd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8248    let sd_num = vfp_sreg_to_num(sd)?;
8249    let (vd, d) = encode_sreg(sd_num);
8250    let rn = reg_to_bits(&addr.base);
8251
8252    let offset = addr.offset;
8253    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8254    let abs_offset = offset.unsigned_abs();
8255    let imm8 = (abs_offset / 4) & 0xFF;
8256
8257    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8258}
8259
8260/// Encode VMOV between core register and S-register.
8261/// VMOV Sn, Rt: 0xEE00_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8262/// VMOV Rt, Sn: 0xEE10_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8263fn encode_vmov_core_sreg(to_sreg: bool, sreg: &VfpReg, core: &Reg) -> Result<u32> {
8264    let s_num = vfp_sreg_to_num(sreg)?;
8265    let (vn, n) = encode_sreg(s_num);
8266    let rt = reg_to_bits(core);
8267
8268    let base = if to_sreg { 0xEE000A10 } else { 0xEE100A10 };
8269    Ok(base | (vn << 16) | (rt << 12) | (n << 7))
8270}
8271
8272/// Encode a VFP 3-register double-precision instruction (VADD.F64, VSUB.F64, etc.).
8273/// For double-precision (sz=1), coprocessor = 0xB (bits[11:8]).
8274/// The base should have bit 8 = 1 for F64 (0xB suffix instead of 0xA).
8275fn encode_vfp_3reg_f64(base: u32, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<u32> {
8276    let dd_num = vfp_dreg_to_num(dd)?;
8277    let dn_num = vfp_dreg_to_num(dn)?;
8278    let dm_num = vfp_dreg_to_num(dm)?;
8279    let (vd, d) = encode_dreg(dd_num);
8280    let (vn, n) = encode_dreg(dn_num);
8281    let (vm, m) = encode_dreg(dm_num);
8282
8283    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8284}
8285
8286/// Encode a VFP 2-register double-precision instruction (VNEG.F64, VABS.F64, VSQRT.F64).
8287fn encode_vfp_2reg_f64(base: u32, dd: &VfpReg, dm: &VfpReg) -> Result<u32> {
8288    let dd_num = vfp_dreg_to_num(dd)?;
8289    let dm_num = vfp_dreg_to_num(dm)?;
8290    let (vd, d) = encode_dreg(dd_num);
8291    let (vm, m) = encode_dreg(dm_num);
8292
8293    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8294}
8295
8296/// Encode a VFP load/store for double-precision (VLDR.64 / VSTR.64).
8297/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8298fn encode_vfp_ldst_f64(base: u32, dd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8299    let dd_num = vfp_dreg_to_num(dd)?;
8300    let (vd, d) = encode_dreg(dd_num);
8301    let rn = reg_to_bits(&addr.base);
8302
8303    let offset = addr.offset;
8304    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8305    let abs_offset = offset.unsigned_abs();
8306    let imm8 = (abs_offset / 4) & 0xFF;
8307
8308    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8309}
8310
8311/// Encode VMOV between two core registers and a D-register.
8312/// VMOV Dm, Rt, Rt2: 0xEC40_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8313/// VMOV Rt, Rt2, Dm: 0xEC50_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8314fn encode_vmov_core_dreg(
8315    to_dreg: bool,
8316    dreg: &VfpReg,
8317    core_lo: &Reg,
8318    core_hi: &Reg,
8319) -> Result<u32> {
8320    let d_num = vfp_dreg_to_num(dreg)?;
8321    let (vm, m) = encode_dreg(d_num);
8322    let rt = reg_to_bits(core_lo);
8323    let rt2 = reg_to_bits(core_hi);
8324
8325    let base = if to_dreg { 0xEC400B10 } else { 0xEC500B10 };
8326    Ok(base | (rt2 << 16) | (rt << 12) | (m << 5) | vm)
8327}
8328
8329/// Emit a VFP 32-bit instruction as Thumb-2 bytes (two LE halfwords).
8330fn vfp_to_thumb_bytes(instr: u32) -> Vec<u8> {
8331    let hw1 = ((instr >> 16) & 0xFFFF) as u16;
8332    let hw2 = (instr & 0xFFFF) as u16;
8333    let mut bytes = hw1.to_le_bytes().to_vec();
8334    bytes.extend_from_slice(&hw2.to_le_bytes());
8335    bytes
8336}
8337
8338// ============================================================================
8339// Helium MVE encoding helpers
8340// ============================================================================
8341
8342/// Q-register number: Q0=0, Q1=1, ..., Q7=7
8343fn qreg_to_num(reg: &QReg) -> u32 {
8344    match reg {
8345        QReg::Q0 => 0,
8346        QReg::Q1 => 1,
8347        QReg::Q2 => 2,
8348        QReg::Q3 => 3,
8349        QReg::Q4 => 4,
8350        QReg::Q5 => 5,
8351        QReg::Q6 => 6,
8352        QReg::Q7 => 7,
8353    }
8354}
8355
8356/// MVE element size to encoding bits: S8=0b00, S16=0b01, S32=0b10
8357fn mve_size_bits(size: &MveSize) -> u32 {
8358    match size {
8359        MveSize::S8 => 0b00,
8360        MveSize::S16 => 0b01,
8361        MveSize::S32 => 0b10,
8362    }
8363}
8364
8365/// Encode MVE 3-register instruction.
8366/// Q-registers are encoded as D-register pairs: Q0=D0:D1, Q1=D2:D3, etc.
8367/// In NEON/MVE encoding, the Q-register uses D-register number = Qn * 2.
8368fn encode_mve_3reg(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
8369    let d = qreg_to_num(qd) * 2;
8370    let n = qreg_to_num(qn) * 2;
8371    let m = qreg_to_num(qm) * 2;
8372
8373    // Standard NEON/MVE 3-register encoding:
8374    // D bit (bit 22) = Vd[4], Vd[3:0] = bits [15:12]
8375    // N bit (bit 7)  = Vn[4], Vn[3:0] = bits [19:16]
8376    // M bit (bit 5)  = Vm[4], Vm[3:0] = bits [3:0]
8377    let vd = d & 0xF;
8378    let d_bit = (d >> 4) & 1;
8379    let vn = n & 0xF;
8380    let n_bit = (n >> 4) & 1;
8381    let vm = m & 0xF;
8382    let m_bit = (m >> 4) & 1;
8383
8384    base | (d_bit << 22) | (vn << 16) | (vd << 12) | (n_bit << 7) | (m_bit << 5) | vm
8385}
8386
8387/// Encode MVE 3-register bitwise instruction (VAND, VORR, VEOR, VBIC).
8388fn encode_mve_3reg_bitwise(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
8389    encode_mve_3reg(base, qd, qn, qm)
8390}
8391
8392/// Encode MVE VLDRW.32 Qd, [Rn, #offset]
8393/// Format: EC9x xxxx - contiguous load, word-sized elements
8394fn encode_mve_vldrw(qd: &QReg, addr: &MemAddr) -> u32 {
8395    let qd_enc = qreg_to_num(qd) * 2;
8396    let rn = reg_to_bits(&addr.base);
8397    let offset = addr.offset;
8398    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8399    let abs_offset = offset.unsigned_abs();
8400    let imm7 = (abs_offset / 4) & 0x7F; // 7-bit word-aligned offset
8401
8402    // VLDRW.32 Qd, [Rn, #imm]: ED10 xx80 variant
8403    0xED100E80
8404        | (u_bit << 23)
8405        | ((qd_enc >> 4) << 22)
8406        | (rn << 16)
8407        | ((qd_enc & 0xF) << 12)
8408        | (imm7 & 0x7F)
8409}
8410
8411/// Encode MVE VSTRW.32 Qd, [Rn, #offset]
8412fn encode_mve_vstrw(qd: &QReg, addr: &MemAddr) -> u32 {
8413    let qd_enc = qreg_to_num(qd) * 2;
8414    let rn = reg_to_bits(&addr.base);
8415    let offset = addr.offset;
8416    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8417    let abs_offset = offset.unsigned_abs();
8418    let imm7 = (abs_offset / 4) & 0x7F;
8419
8420    0xED000E80
8421        | (u_bit << 23)
8422        | ((qd_enc >> 4) << 22)
8423        | (rn << 16)
8424        | ((qd_enc & 0xF) << 12)
8425        | (imm7 & 0x7F)
8426}
8427
8428impl ArmEncoder {
8429    /// Encode MVE constant load: MOVW+MOVT+VMOV for each 32-bit word, then assemble Q-register
8430    fn encode_thumb_mve_const(&self, qd: &QReg, bytes: &[u8; 16]) -> Result<Vec<u8>> {
8431        let mut result = Vec::new();
8432        let qd_num = qreg_to_num(qd);
8433
8434        // Load each 32-bit word into R12 (temp) then VMOV into S-register
8435        for i in 0..4 {
8436            let word = u32::from_le_bytes([
8437                bytes[i * 4],
8438                bytes[i * 4 + 1],
8439                bytes[i * 4 + 2],
8440                bytes[i * 4 + 3],
8441            ]);
8442            let lo16 = word & 0xFFFF;
8443            let hi16 = (word >> 16) & 0xFFFF;
8444
8445            // MOVW R12, #lo16
8446            result.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
8447            // MOVT R12, #hi16
8448            if hi16 != 0 {
8449                result.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
8450            }
8451
8452            // VMOV Sn, R12 where Sn = Qd*4 + i
8453            let s_num = qd_num * 4 + i as u32;
8454            let (vn, n) = encode_sreg(s_num);
8455            let vmov: u32 = 0xEE000A10 | (vn << 16) | (12 << 12) | (n << 7);
8456            result.extend_from_slice(&vfp_to_thumb_bytes(vmov));
8457        }
8458
8459        Ok(result)
8460    }
8461
8462    /// Encode lane-wise f32 binary operation (VDIV, etc.) via S-register extraction
8463    fn encode_thumb_mve_lane_wise_f32_binop(
8464        &self,
8465        qd: &QReg,
8466        qn: &QReg,
8467        qm: &QReg,
8468        vfp_base: u32,
8469    ) -> Result<Vec<u8>> {
8470        let mut result = Vec::new();
8471        let qd_num = qreg_to_num(qd);
8472        let qn_num = qreg_to_num(qn);
8473        let qm_num = qreg_to_num(qm);
8474
8475        // For each lane 0..3: use S-registers directly (Q aliasing)
8476        for i in 0..4u32 {
8477            let sd = qd_num * 4 + i;
8478            let sn = qn_num * 4 + i;
8479            let sm = qm_num * 4 + i;
8480
8481            let (vd, d) = encode_sreg(sd);
8482            let (vn, n) = encode_sreg(sn);
8483            let (vm, m) = encode_sreg(sm);
8484
8485            let instr = vfp_base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
8486            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
8487        }
8488
8489        Ok(result)
8490    }
8491
8492    /// Encode lane-wise f32 VSQRT via S-register extraction
8493    fn encode_thumb_mve_lane_wise_f32_sqrt(&self, qd: &QReg, qm: &QReg) -> Result<Vec<u8>> {
8494        let mut result = Vec::new();
8495        let qd_num = qreg_to_num(qd);
8496        let qm_num = qreg_to_num(qm);
8497
8498        // VSQRT.F32 base: 0xEEB10AC0
8499        for i in 0..4u32 {
8500            let sd = qd_num * 4 + i;
8501            let sm = qm_num * 4 + i;
8502
8503            let (vd, d) = encode_sreg(sd);
8504            let (vm, m) = encode_sreg(sm);
8505
8506            let instr: u32 = 0xEEB10AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
8507            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
8508        }
8509
8510        Ok(result)
8511    }
8512}
8513
8514#[cfg(test)]
8515mod tests {
8516    use super::*;
8517
8518    #[test]
8519    fn test_encoder_creation() {
8520        let encoder_arm = ArmEncoder::new_arm32();
8521        assert!(!encoder_arm.thumb_mode);
8522
8523        let encoder_thumb = ArmEncoder::new_thumb2();
8524        assert!(encoder_thumb.thumb_mode);
8525    }
8526
8527    /// #204 WAKE-path regression: `SetCond` materialized 0/1 with the 16-bit
8528    /// `MOVS Rd,#imm` (T1), whose Rd field is 3 bits (R0–R7). For a high Rd
8529    /// (R8–R12) `rd_bits << 8` overflows bit 11, flipping the opcode MOVS→CMP
8530    /// (`0x2c00`), so the boolean was never written — gale's `has_waiter` kept a
8531    /// stale value and the binary-sem WAKE dispatch read garbage. High Rd must
8532    /// use the 32-bit `MOV.W` (T2). Verify the bytes, not the IR.
8533    /// #311: the SAME high-Rd MOVS→CMP transmutation as #204, but in the
8534    /// i64 comparison expansions (I64SetCond / I64SetCondZ) — missed by the
8535    /// #204 hardening. With rd=R8 the boolean died in the flags
8536    /// (`ite eq; cmpeq r0,#1; cmpne r0,#0`), so gale's packed-u64 select
8537    /// read a stale register on silicon. High Rd must take MOV.W / CMP.W.
8538    #[test]
8539    fn test_encode_i64setcond_high_reg_uses_mov_w_311() {
8540        use synth_synthesis::{ArmOp, Condition, Reg};
8541        let enc = ArmEncoder::new_thumb2();
8542        let bytes = enc
8543            .encode(&ArmOp::I64SetCond {
8544                rd: Reg::R8,
8545                rn_lo: Reg::R2,
8546                rn_hi: Reg::R3,
8547                rm_lo: Reg::R6,
8548                rm_hi: Reg::R7,
8549                cond: Condition::EQ,
8550            })
8551            .unwrap();
8552        // The 32-bit MOV.W immediate (T2) first halfword is 0xF04F; the
8553        // 16-bit transmuted forms would contain 0x2801/0x2800 (CMP r0,#1/#0).
8554        let halfwords: Vec<u16> = bytes
8555            .chunks(2)
8556            .map(|c| u16::from_le_bytes([c[0], c[1]]))
8557            .collect();
8558        assert!(
8559            halfwords.iter().filter(|&&h| h == 0xF04F).count() == 2,
8560            "high rd must use two MOV.W (T2) encodings, got {halfwords:04x?}"
8561        );
8562        assert!(
8563            !halfwords.contains(&0x2801) && !halfwords.contains(&0x2800),
8564            "no transmuted 16-bit CMP imm: {halfwords:04x?}"
8565        );
8566
8567        let bytes_z = enc
8568            .encode(&ArmOp::I64SetCondZ {
8569                rd: Reg::R8,
8570                rn_lo: Reg::R2,
8571                rn_hi: Reg::R3,
8572            })
8573            .unwrap();
8574        let hw_z: Vec<u16> = bytes_z
8575            .chunks(2)
8576            .map(|c| u16::from_le_bytes([c[0], c[1]]))
8577            .collect();
8578        assert!(
8579            hw_z.iter().filter(|&&h| h == 0xF04F).count() == 2,
8580            "SetCondZ high rd MOV.W: {hw_z:04x?}"
8581        );
8582        // CMP.W rd,#0 (T2) first halfword: 0xF1B0 | rd
8583        assert!(
8584            hw_z.contains(&(0xF1B0 | 8)),
8585            "SetCondZ high rd must use CMP.W: {hw_z:04x?}"
8586        );
8587    }
8588
8589    #[test]
8590    fn test_encode_setcond_high_reg_uses_mov_w_204() {
8591        use synth_synthesis::{ArmOp, Condition, Reg};
8592        let enc = ArmEncoder::new_thumb2();
8593        // R12 (high): must be ITE + MOV.W #1 + MOV.W #0, never a 16-bit MOVS/CMP.
8594        let hi = enc
8595            .encode(&ArmOp::SetCond {
8596                rd: Reg::R12,
8597                cond: Condition::NE,
8598            })
8599            .unwrap();
8600        assert_eq!(hi.len(), 10, "ITE(2) + MOV.W(4) + MOV.W(4): {hi:02x?}");
8601        // both value halfwords are MOV.W (0xF04F) — NOT the corrupt CMP (0x2c..).
8602        assert_eq!(&hi[2..4], &[0x4F, 0xF0], "then = MOV.W: {hi:02x?}");
8603        assert_eq!(&hi[6..8], &[0x4F, 0xF0], "else = MOV.W: {hi:02x?}");
8604        assert_eq!(hi[4] & 0x0F, 0x01, "then imm = #1");
8605        assert_eq!(hi[8] & 0x0F, 0x00, "else imm = #0");
8606        // Low Rd keeps the compact 16-bit MOVS form.
8607        let lo = enc
8608            .encode(&ArmOp::SetCond {
8609                rd: Reg::R0,
8610                cond: Condition::NE,
8611            })
8612            .unwrap();
8613        assert_eq!(lo.len(), 6, "ITE(2) + MOVS(2) + MOVS(2): {lo:02x?}");
8614        assert_eq!(lo[2..4], [0x01, 0x20], "then = MOVS R0,#1");
8615        assert_eq!(lo[4..6], [0x00, 0x20], "else = MOVS R0,#0");
8616    }
8617
8618    /// #209 Opt 1b: UMULL RdLo, RdHi, Rn, Rm encodes correctly on both ISAs.
8619    /// Thumb-2 T1: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm.
8620    /// A32:        cond 0000 1000 RdHi RdLo Rm 1001 Rn.
8621    #[test]
8622    fn test_encode_umull_209b() {
8623        use synth_synthesis::{ArmOp, Reg};
8624        let op = ArmOp::Umull {
8625            rdlo: Reg::R4,
8626            rdhi: Reg::R5,
8627            rn: Reg::R0,
8628            rm: Reg::R3,
8629        };
8630        // Thumb-2: hw1 = 0xFBA0 | 0 = 0xFBA0; hw2 = (4<<12)|(5<<8)|3 = 0x4503.
8631        let t = ArmEncoder::new_thumb2().encode(&op).unwrap();
8632        assert_eq!(
8633            t,
8634            vec![0xA0, 0xFB, 0x03, 0x45],
8635            "umull r4,r5,r0,r3 (T2): {t:02x?}"
8636        );
8637        // A32: 0xE0800090 | (5<<16) | (4<<12) | (3<<8) | 0 = 0xE0854390.
8638        let a = ArmEncoder::new_arm32().encode(&op).unwrap();
8639        assert_eq!(
8640            a,
8641            0xE085_4390u32.to_le_bytes().to_vec(),
8642            "umull (A32): {a:02x?}"
8643        );
8644    }
8645
8646    /// #206 regression: the ARM32 (A32) `Ldr`/`Str` encoders fed `addr` through
8647    /// `encode_mem_addr`, which returns only the 12-bit immediate — so a register
8648    /// offset (`[rn, rm, #off]`) was silently dropped to `[rn, #off]`, sending
8649    /// the access to the wrong runtime address (silent miscompile on the default
8650    /// `--target arm`). A register offset must materialize `ip = rn + rm` and
8651    /// load from `[ip, #off]`. Verify the bytes.
8652    #[test]
8653    fn test_encode_arm32_indexed_load_keeps_index_206() {
8654        use synth_synthesis::{ArmOp, MemAddr, Reg};
8655        let enc = ArmEncoder::new_arm32();
8656        // ldr r0, [r11, r1, #8]  must NOT collapse to a single immediate ldr.
8657        let bytes = enc
8658            .encode(&ArmOp::Ldr {
8659                rd: Reg::R0,
8660                addr: MemAddr::reg_imm(Reg::R11, Reg::R1, 8),
8661            })
8662            .unwrap();
8663        assert_eq!(
8664            bytes.len(),
8665            8,
8666            "expected ADD ip + LDR (2 words): {bytes:02x?}"
8667        );
8668        let add = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
8669        let ldr = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
8670        // ADD ip, r11, r1  = 0xE08BC001
8671        assert_eq!(add, 0xE08B_C001, "ADD ip,r11,r1: {add:#010x}");
8672        // LDR r0, [ip, #8] = 0xE59C0008
8673        assert_eq!(ldr, 0xE59C_0008, "LDR r0,[ip,#8]: {ldr:#010x}");
8674        // A bare immediate ldr (the bug) would be 0xE59B0008 (base=r11) — reject.
8675        assert_ne!(ldr, 0xE59B_0008, "index must not be dropped");
8676    }
8677
8678    /// #594 regression: `call_indirect` on the A32 path (`--target cortex-r5`)
8679    /// was encoded as a literal NOP (0xE1A00000) — the call never happened and
8680    /// the function silently returned the leftover table-index value. The A32
8681    /// encoder must emit the same three-instruction expansion as Thumb-2:
8682    /// `MOV r12, idx, LSL #2; LDR r12, [r11, r12]; BLX r12`.
8683    #[test]
8684    fn test_encode_arm32_call_indirect_is_real_call_594() {
8685        use synth_synthesis::{ArmOp, Reg};
8686        let enc = ArmEncoder::new_arm32();
8687        let bytes = enc
8688            .encode(&ArmOp::CallIndirect {
8689                rd: Reg::R0,
8690                type_idx: 0,
8691                table_index_reg: Reg::R0,
8692            })
8693            .unwrap();
8694        assert_eq!(
8695            bytes.len(),
8696            12,
8697            "expected MOV + LDR + BLX (3 words): {bytes:02x?}"
8698        );
8699        let mov = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
8700        let ldr = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
8701        let blx = u32::from_le_bytes(bytes[8..12].try_into().unwrap());
8702        // MOV r12, r0, LSL #2 = 0xE1A0C100
8703        assert_eq!(mov, 0xE1A0_C100, "MOV r12,r0,LSL#2: {mov:#010x}");
8704        // LDR r12, [r11, r12] = 0xE79BC00C
8705        assert_eq!(ldr, 0xE79B_C00C, "LDR r12,[r11,r12]: {ldr:#010x}");
8706        // BLX r12 = 0xE12FFF3C
8707        assert_eq!(blx, 0xE12F_FF3C, "BLX r12: {blx:#010x}");
8708        // The bug: a single NOP word. Must never come back.
8709        assert!(
8710            !bytes
8711                .chunks_exact(4)
8712                .any(|w| w == 0xE1A0_0000u32.to_le_bytes()),
8713            "call_indirect must not contain a NOP (#594): {bytes:02x?}"
8714        );
8715
8716        // A non-R0 index register lands in the MOV's Rm field.
8717        let bytes = enc
8718            .encode(&ArmOp::CallIndirect {
8719                rd: Reg::R0,
8720                type_idx: 0,
8721                table_index_reg: Reg::R4,
8722            })
8723            .unwrap();
8724        let mov = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
8725        assert_eq!(mov, 0xE1A0_C104, "MOV r12,r4,LSL#2: {mov:#010x}");
8726    }
8727
8728    /// #597 anchor (justified correctness RE-PIN of the #594-era freeze): the
8729    /// Thumb-2 `CallIndirect` expansion is `mov.w ip, rm, LSL #2; ldr.w ip,
8730    /// [r11, ip]; blx ip`.
8731    ///
8732    /// The #594 PR froze the then-current bytes `4F EA 20 0C ...` whose first
8733    /// word decodes as `mov.w ip, rm, ASR #32` — the intended `LSL #2` had
8734    /// its shift amount in the TYPE field (bits 5:4) instead of imm2 (bits
8735    /// 7:6), so the index was destroyed and every call_indirect dispatched
8736    /// table entry 0 (shipped miscompile, masked by index-0 probes). #597
8737    /// corrects the encoding; new bytes `4F EA 80 0C ...` were
8738    /// execution-validated under unicorn against the wasmtime oracle on a
8739    /// multi-entry table (indexes 0, 1, 3 —
8740    /// scripts/repro/call_indirect_597_differential.py) before this pin was
8741    /// replaced. Old pin: [4F EA 20 0C, 5B F8 0C C0, E0 47] (ASR #32 — must
8742    /// never come back).
8743    #[test]
8744    fn test_encode_thumb_call_indirect_lsl2_597() {
8745        use synth_synthesis::{ArmOp, Reg};
8746        let enc = ArmEncoder::new_thumb2();
8747        let bytes = enc
8748            .encode(&ArmOp::CallIndirect {
8749                rd: Reg::R0,
8750                type_idx: 0,
8751                table_index_reg: Reg::R0,
8752            })
8753            .unwrap();
8754        assert_eq!(
8755            bytes,
8756            vec![0x4F, 0xEA, 0x80, 0x0C, 0x5B, 0xF8, 0x0C, 0xC0, 0xE0, 0x47],
8757            "Thumb-2 CallIndirect: mov.w ip,r0,LSL#2; ldr.w ip,[r11,ip]; blx ip: {bytes:02x?}"
8758        );
8759        // The #597 bug bytes (ASR #32 first word) must never come back.
8760        assert_ne!(
8761            &bytes[0..4],
8762            &[0x4F, 0xEA, 0x20, 0x0C],
8763            "mov.w ip, rm, ASR #32 — the #597 type-field bug"
8764        );
8765
8766        // A non-R0 index register lands in the mov.w's Rm field (hw2 bits 3:0).
8767        let bytes = enc
8768            .encode(&ArmOp::CallIndirect {
8769                rd: Reg::R0,
8770                type_idx: 0,
8771                table_index_reg: Reg::R4,
8772            })
8773            .unwrap();
8774        assert_eq!(
8775            &bytes[0..4],
8776            &[0x4F, 0xEA, 0x84, 0x0C],
8777            "mov.w ip, r4, LSL #2: {bytes:02x?}"
8778        );
8779    }
8780
8781    /// #178/#180 regression: the Thumb `Add`/`Adds`/`Subs` reg-forms used the
8782    /// 16-bit encoding unconditionally. For high registers (R12 base scratch,
8783    /// R8-R11 i64 pairs) the 3-bit register fields overflow and corrupt the
8784    /// operands — `add ip,ip,r0` came out as `adds r4,r5,r1` (0x186C), silently
8785    /// dropping the address operand and miscompiling every optimized memory
8786    /// access. High registers must use the 32-bit `.W` forms.
8787    #[test]
8788    fn test_encode_thumb_add_high_reg_uses_add_w_178_180() {
8789        let encoder = ArmEncoder::new_thumb2();
8790
8791        // add ip, ip, r0  — the exact MemLoad/MemStore base+addr op.
8792        let code = encoder
8793            .encode(&ArmOp::Add {
8794                rd: Reg::R12,
8795                rn: Reg::R12,
8796                op2: Operand2::Reg(Reg::R0),
8797            })
8798            .unwrap();
8799        // ADD.W ip, ip, r0 = EB0C 0C00 (little-endian halfwords).
8800        assert_eq!(
8801            code,
8802            vec![0x0C, 0xEB, 0x00, 0x0C],
8803            "high-reg Thumb ADD must be 32-bit ADD.W (EB0C 0C00), not corrupt 16-bit; got {code:02X?}"
8804        );
8805        // Must NOT be the buggy 16-bit 0x186C (`adds r4,r5,r1`).
8806        assert_ne!(code, vec![0x6C, 0x18], "regressed to corrupt 16-bit ADDS");
8807
8808        // Low-register add stays 16-bit (no regression for the common case).
8809        let lo = encoder
8810            .encode(&ArmOp::Add {
8811                rd: Reg::R1,
8812                rn: Reg::R2,
8813                op2: Operand2::Reg(Reg::R3),
8814            })
8815            .unwrap();
8816        assert_eq!(
8817            lo.len(),
8818            2,
8819            "low-reg ADD should remain 16-bit, got {lo:02X?}"
8820        );
8821    }
8822
8823    /// #178/#180 sibling: i64 low-word `Adds`/`Subs` can land in R8-R11 pairs;
8824    /// those must fall back to 32-bit ADDS.W/SUBS.W (flag-setting preserved).
8825    #[test]
8826    fn test_encode_thumb_adds_subs_high_reg_use_32bit_178_180() {
8827        let encoder = ArmEncoder::new_thumb2();
8828
8829        // adds r10, r10, r8  → ADDS.W = EB1A 0A08
8830        let adds = encoder
8831            .encode(&ArmOp::Adds {
8832                rd: Reg::R10,
8833                rn: Reg::R10,
8834                op2: Operand2::Reg(Reg::R8),
8835            })
8836            .unwrap();
8837        assert_eq!(
8838            adds,
8839            vec![0x1A, 0xEB, 0x08, 0x0A],
8840            "high-reg ADDS must be 32-bit ADDS.W (EB1A 0A08); got {adds:02X?}"
8841        );
8842
8843        // subs r10, r10, r8  → SUBS.W = EBBA 0A08
8844        let subs = encoder
8845            .encode(&ArmOp::Subs {
8846                rd: Reg::R10,
8847                rn: Reg::R10,
8848                op2: Operand2::Reg(Reg::R8),
8849            })
8850            .unwrap();
8851        assert_eq!(
8852            subs,
8853            vec![0xBA, 0xEB, 0x08, 0x0A],
8854            "high-reg SUBS must be 32-bit SUBS.W (EBBA 0A08); got {subs:02X?}"
8855        );
8856    }
8857
8858    /// #184 (sibling of #180): 16-bit CMN (T1) only encodes R0-R7. High registers
8859    /// must use 32-bit CMN.W, not the corrupt truncated 16-bit form.
8860    #[test]
8861    fn test_encode_thumb_cmn_high_reg_uses_cmn_w_184() {
8862        let encoder = ArmEncoder::new_thumb2();
8863
8864        // cmn r10, r8  → CMN.W = EB1A 0F08 (ADD.W S=1, Rd=PC discarded).
8865        let cmn = encoder
8866            .encode(&ArmOp::Cmn {
8867                rn: Reg::R10,
8868                op2: Operand2::Reg(Reg::R8),
8869            })
8870            .unwrap();
8871        assert_eq!(
8872            cmn,
8873            vec![0x1A, 0xEB, 0x08, 0x0F],
8874            "high-reg CMN must be 32-bit CMN.W (EB1A 0F08); got {cmn:02X?}"
8875        );
8876
8877        // Low registers stay 16-bit: cmn r1, r2 = 0x42D1.
8878        let lo = encoder
8879            .encode(&ArmOp::Cmn {
8880                rn: Reg::R1,
8881                op2: Operand2::Reg(Reg::R2),
8882            })
8883            .unwrap();
8884        assert_eq!(
8885            lo.len(),
8886            2,
8887            "low-reg CMN should remain 16-bit, got {lo:02X?}"
8888        );
8889        assert_eq!(lo, vec![0xD1, 0x42], "low-reg CMN bytes wrong: {lo:02X?}");
8890    }
8891
8892    /// #185 regression: feeding PC (R15) as a data operand to a Thumb-2 op that
8893    /// guards its registers must return Err, not panic under debug-assertions.
8894    /// (Synth never emits PC here; the fuzz harness requires encode() be total.)
8895    #[test]
8896    fn test_encode_pc_operand_returns_err_not_panic_185() {
8897        let encoder = ArmEncoder::new_thumb2();
8898        for op in [
8899            ArmOp::Sdiv {
8900                rd: Reg::PC,
8901                rn: Reg::R0,
8902                rm: Reg::R1,
8903            },
8904            ArmOp::Udiv {
8905                rd: Reg::R0,
8906                rn: Reg::PC,
8907                rm: Reg::R1,
8908            },
8909            ArmOp::Sdiv {
8910                rd: Reg::R0,
8911                rn: Reg::R1,
8912                rm: Reg::PC,
8913            },
8914        ] {
8915            let r = encoder.encode(&op);
8916            assert!(
8917                r.is_err(),
8918                "encode({op:?}) must return Err for a PC operand, got {r:?}"
8919            );
8920        }
8921        // Valid registers still encode fine (no false rejection).
8922        assert!(
8923            encoder
8924                .encode(&ArmOp::Sdiv {
8925                    rd: Reg::R0,
8926                    rn: Reg::R1,
8927                    rm: Reg::R2
8928                })
8929                .is_ok()
8930        );
8931    }
8932
8933    #[test]
8934    fn test_encode_nop_arm32() {
8935        let encoder = ArmEncoder::new_arm32();
8936        let code = encoder.encode(&ArmOp::Nop).unwrap();
8937
8938        assert_eq!(code.len(), 4); // ARM32 instructions are 4 bytes
8939        assert_eq!(code, vec![0x00, 0x00, 0xA0, 0xE1]); // MOV R0, R0
8940    }
8941
8942    #[test]
8943    fn test_encode_nop_thumb() {
8944        let encoder = ArmEncoder::new_thumb2();
8945        let code = encoder.encode(&ArmOp::Nop).unwrap();
8946
8947        assert_eq!(code.len(), 2); // Thumb instructions are 2 bytes
8948        assert_eq!(code, vec![0x00, 0xBF]); // NOP
8949    }
8950
8951    #[test]
8952    fn test_encode_mov_immediate_arm32() {
8953        let encoder = ArmEncoder::new_arm32();
8954        let op = ArmOp::Mov {
8955            rd: Reg::R0,
8956            op2: Operand2::Imm(42),
8957        };
8958
8959        let code = encoder.encode(&op).unwrap();
8960        assert_eq!(code.len(), 4);
8961
8962        // Verify it's a MOV instruction (bits should have immediate flag set)
8963        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
8964        assert_eq!(instr & 0x0E000000, 0x02000000); // Check I bit is set
8965    }
8966
8967    #[test]
8968    fn test_encode_add_registers_arm32() {
8969        let encoder = ArmEncoder::new_arm32();
8970        let op = ArmOp::Add {
8971            rd: Reg::R0,
8972            rn: Reg::R1,
8973            op2: Operand2::Reg(Reg::R2),
8974        };
8975
8976        let code = encoder.encode(&op).unwrap();
8977        assert_eq!(code.len(), 4);
8978
8979        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
8980        // Verify it's an ADD instruction with correct opcode
8981        assert_eq!(instr & 0x0FE00000, 0x00800000);
8982    }
8983
8984    /// #350 — `encode_thumb32_add_imm` must lower an out-of-range immediate
8985    /// (> 0xFFF) to a legal MOVW(/MOVT) + ADD.W-register sequence instead of
8986    /// erroring. The small-imm fast path (imm <= 0xFFF) stays byte-identical.
8987    #[test]
8988    fn test_encode_add_imm_large_350() {
8989        let enc = ArmEncoder::new_thumb2();
8990
8991        // --- Fast path unchanged: imm <= 0xFFF is a single 4-byte ADD.W ---
8992        let small = enc
8993            .encode_thumb32_add_imm(&Reg::R0, &Reg::R1, 0x123)
8994            .unwrap();
8995        assert_eq!(small.len(), 4, "small imm must stay a single instruction");
8996
8997        // helper: decode a Thumb-2 MOVW/MOVT halfword pair back to its imm16
8998        fn movx_imm16(b: &[u8]) -> u32 {
8999            let hw1 = u16::from_le_bytes([b[0], b[1]]) as u32;
9000            let hw2 = u16::from_le_bytes([b[2], b[3]]) as u32;
9001            let imm4 = hw1 & 0xF;
9002            let i = (hw1 >> 10) & 1;
9003            let imm3 = (hw2 >> 12) & 0x7;
9004            let imm8 = hw2 & 0xFF;
9005            (imm4 << 12) | (i << 11) | (imm3 << 8) | imm8
9006        }
9007        fn movx_rd(b: &[u8]) -> u32 {
9008            (u16::from_le_bytes([b[2], b[3]]) as u32 >> 8) & 0xF
9009        }
9010
9011        // --- rd != rn: scratch is rd. imm = 70000 = 0x11170 needs MOVW+MOVT. ---
9012        // 0x11170: lo16 = 0x1170, hi16 = 0x0001
9013        let seq = enc
9014            .encode_thumb32_add_imm(&Reg::R12, &Reg::R0, 70000)
9015            .unwrap();
9016        assert_eq!(seq.len(), 12, "MOVW + MOVT + ADD = 12 bytes");
9017        // MOVW r12, #0x1170
9018        assert_eq!(u16::from_le_bytes([seq[0], seq[1]]) & 0xFBF0, 0xF240);
9019        assert_eq!(movx_rd(&seq[0..4]), 12);
9020        assert_eq!(movx_imm16(&seq[0..4]), 0x1170);
9021        // MOVT r12, #0x0001
9022        assert_eq!(u16::from_le_bytes([seq[4], seq[5]]) & 0xFBF0, 0xF2C0);
9023        assert_eq!(movx_rd(&seq[4..8]), 12);
9024        assert_eq!(movx_imm16(&seq[4..8]), 0x0001);
9025        // ADD.W r12, r0, r12  (EB00 | rn=0 ; rd=12, rm=12)
9026        let add1 = u16::from_le_bytes([seq[8], seq[9]]) as u32;
9027        let add2 = u16::from_le_bytes([seq[10], seq[11]]) as u32;
9028        assert_eq!(add1 & 0xFFF0, 0xEB00);
9029        assert_eq!(add1 & 0xF, 0); // rn = r0
9030        assert_eq!((add2 >> 8) & 0xF, 12); // rd = r12
9031        assert_eq!(add2 & 0xF, 12); // rm = scratch = r12
9032        // The materialized scratch must reconstruct exactly 70000.
9033        assert_eq!(
9034            (movx_imm16(&seq[4..8]) << 16) | movx_imm16(&seq[0..4]),
9035            70000
9036        );
9037
9038        // --- imm <= 0xFFFF: MOVT is skipped (MOVW + ADD = 8 bytes). ---
9039        let seq16 = enc
9040            .encode_thumb32_add_imm(&Reg::R3, &Reg::R0, 0xABCD)
9041            .unwrap();
9042        assert_eq!(seq16.len(), 8, "imm <= 0xFFFF skips MOVT");
9043        assert_eq!(movx_imm16(&seq16[0..4]), 0xABCD);
9044        assert_eq!(movx_rd(&seq16[0..4]), 3); // scratch = rd = r3
9045
9046        // --- rd == rn (in-place add): scratch must be R12, not rd. ---
9047        // imm = 0x12345: lo16 = 0x2345, hi16 = 0x0001
9048        let inplace = enc
9049            .encode_thumb32_add_imm(&Reg::R5, &Reg::R5, 0x12345)
9050            .unwrap();
9051        assert_eq!(inplace.len(), 12);
9052        assert_eq!(movx_rd(&inplace[0..4]), 12, "rd==rn must use R12 scratch");
9053        assert_eq!(
9054            (movx_imm16(&inplace[4..8]) << 16) | movx_imm16(&inplace[0..4]),
9055            0x12345
9056        );
9057        // ADD.W r5, r5, r12 — rm must be the scratch (12), never rn.
9058        let ip_add2 = u16::from_le_bytes([inplace[10], inplace[11]]) as u32;
9059        assert_eq!(ip_add2 & 0xF, 12);
9060        assert_eq!((ip_add2 >> 8) & 0xF, 5);
9061    }
9062
9063    /// #350 follow-up — the `encoder_no_panic` fuzz harness drives the encoder
9064    /// with ARBITRARY registers, including the one case the in-place lowering
9065    /// cannot serve: rd==rn==R12. There the scratch (R12, the reserved encoder
9066    /// register) would alias Rn and clobber it before the ADD reads it. The
9067    /// encoder contract (#180/#185) is Ok-or-Err, never a panic — so this must
9068    /// return Err, not assert. (Real codegen never emits rd==rn==R12 because R12
9069    /// is non-allocatable; this guards only the fuzz/adversarial path.)
9070    #[test]
9071    fn test_encode_add_imm_large_rd_rn_r12_errs_not_panics_350() {
9072        let enc = ArmEncoder::new_thumb2();
9073        // Out-of-range imm with rd==rn==R12: no free scratch -> Err.
9074        let r = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 70000);
9075        assert!(
9076            r.is_err(),
9077            "rd==rn==R12 with out-of-range imm must Err (no free scratch), got {r:?}"
9078        );
9079        // Small imm with rd==rn==R12 still takes the single-instruction fast path
9080        // (no scratch needed) and must succeed — the guard is scoped to the
9081        // out-of-range lowering only.
9082        let small = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 0x10);
9083        assert!(small.is_ok(), "small imm needs no scratch, must stay Ok");
9084    }
9085
9086    /// #378 — `encode_operand2` (ARM32 data-processing operand) must FAIL
9087    /// HONESTLY on an immediate that is not a valid rotated immediate, rather
9088    /// than silently masking it to `imm & 0xFF` and emitting a WRONG
9089    /// instruction. `0x1FF` has 9 set bits, so it cannot come from rotating an
9090    /// 8-bit imm8 — non-encodable. Real codegen materializes large constants via
9091    /// MOVW/MOVT; this guards the encoder's Ok-or-Err contract (#180/#185)
9092    /// directly. It is an Err (not a panic) so the `encoder_no_panic` fuzz
9093    /// harness — which drives arbitrary operands — still passes.
9094    #[test]
9095    fn test_encode_operand2_non_rotatable_imm_errs_not_masks_378() {
9096        let enc = ArmEncoder::new_arm32();
9097        let bad = enc.encode(&ArmOp::Add {
9098            rd: Reg::R0,
9099            rn: Reg::R1,
9100            op2: Operand2::Imm(0x1FF),
9101        });
9102        assert!(
9103            bad.is_err(),
9104            "non-rotatable ARM32 immediate 0x1FF must Err (was silently masked \
9105             to 0xFF), got {bad:?}"
9106        );
9107        // A representable rotated immediate still encodes fine (regression guard).
9108        let ok = enc.encode(&ArmOp::Add {
9109            rd: Reg::R0,
9110            rn: Reg::R1,
9111            op2: Operand2::Imm(0xFF),
9112        });
9113        assert!(
9114            ok.is_ok(),
9115            "0xFF is a valid rotated immediate, must stay Ok"
9116        );
9117    }
9118
9119    #[test]
9120    fn test_encode_ldr_arm32() {
9121        let encoder = ArmEncoder::new_arm32();
9122        let op = ArmOp::Ldr {
9123            rd: Reg::R0,
9124            addr: MemAddr::imm(Reg::R1, 4),
9125        };
9126
9127        let code = encoder.encode(&op).unwrap();
9128        assert_eq!(code.len(), 4);
9129
9130        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9131        // Verify load bit is set
9132        assert_eq!(instr & 0x00100000, 0x00100000);
9133    }
9134
9135    #[test]
9136    fn test_encode_str_arm32() {
9137        let encoder = ArmEncoder::new_arm32();
9138        let op = ArmOp::Str {
9139            rd: Reg::R0,
9140            addr: MemAddr::imm(Reg::SP, 0),
9141        };
9142
9143        let code = encoder.encode(&op).unwrap();
9144        assert_eq!(code.len(), 4);
9145    }
9146
9147    #[test]
9148    fn test_encode_branch_arm32() {
9149        let encoder = ArmEncoder::new_arm32();
9150        let op = ArmOp::Bl {
9151            label: "main".to_string(),
9152        };
9153
9154        let code = encoder.encode(&op).unwrap();
9155        assert_eq!(code.len(), 4);
9156
9157        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9158        // Verify BL opcode
9159        assert_eq!(instr & 0x0F000000, 0x0B000000);
9160    }
9161
9162    /// Regression test for #167 + #174: the Thumb-2 BL relocatable placeholder
9163    /// must carry a -4 addend so an R_ARM_THM_CALL nets to exactly the symbol S.
9164    /// The correct encoding is what `gas` emits for `bl <extern>`: f7ff fffe
9165    /// (hw1=0xF7FF, hw2=0xFFFE), little-endian bytes FF F7 FE FF.
9166    ///   - 0xD000 (J1=J2=0) → ~+0x600000 garbage addend: `bl c0000c` / truncated
9167    ///     to fit (#167).
9168    ///   - 0xF800 (addend 0) → lands at S+4, one instruction past the callee
9169    ///     entry (#174).
9170    ///   - 0xFFFE (addend -4) → lands at S. Correct.
9171    #[test]
9172    fn test_encode_thumb_bl_placeholder_addend_167_174() {
9173        let encoder = ArmEncoder::new_thumb2();
9174        let op = ArmOp::Bl {
9175            label: "callee".to_string(),
9176        };
9177
9178        let code = encoder.encode(&op).unwrap();
9179        assert_eq!(code.len(), 4, "Thumb-2 BL is 32-bit");
9180
9181        let hw1 = u16::from_le_bytes([code[0], code[1]]);
9182        let hw2 = u16::from_le_bytes([code[2], code[3]]);
9183        assert_eq!(hw1, 0xF7FF, "BL first halfword (matches gas `bl <extern>`)");
9184        assert_eq!(
9185            hw2, 0xFFFE,
9186            "BL second halfword must be 0xFFFE (-4 addend → nets to S), not 0xF800 (→ S+4, #174) or 0xD000 (#167)"
9187        );
9188        assert_ne!(hw2, 0xF800, "0xF800 (addend 0) lands at S+4 (#174)");
9189        assert_ne!(hw2, 0xD000, "0xD000 bakes in a ~+0x600000 addend (#167)");
9190    }
9191
9192    #[test]
9193    fn test_encode_sequence() {
9194        let encoder = ArmEncoder::new_arm32();
9195        let ops = vec![
9196            ArmOp::Mov {
9197                rd: Reg::R0,
9198                op2: Operand2::Imm(42),
9199            },
9200            ArmOp::Mov {
9201                rd: Reg::R1,
9202                op2: Operand2::Imm(10),
9203            },
9204            ArmOp::Add {
9205                rd: Reg::R2,
9206                rn: Reg::R0,
9207                op2: Operand2::Reg(Reg::R1),
9208            },
9209        ];
9210
9211        let code = encoder.encode_sequence(&ops).unwrap();
9212        assert_eq!(code.len(), 12); // 3 instructions * 4 bytes
9213    }
9214
9215    #[test]
9216    fn test_reg_to_bits() {
9217        assert_eq!(reg_to_bits(&Reg::R0), 0);
9218        assert_eq!(reg_to_bits(&Reg::R7), 7);
9219        assert_eq!(reg_to_bits(&Reg::SP), 13);
9220        assert_eq!(reg_to_bits(&Reg::LR), 14);
9221        assert_eq!(reg_to_bits(&Reg::PC), 15);
9222    }
9223
9224    #[test]
9225    fn test_encode_bitwise_operations() {
9226        let encoder = ArmEncoder::new_arm32();
9227
9228        let and_op = ArmOp::And {
9229            rd: Reg::R0,
9230            rn: Reg::R1,
9231            op2: Operand2::Reg(Reg::R2),
9232        };
9233        let and_code = encoder.encode(&and_op).unwrap();
9234        assert_eq!(and_code.len(), 4);
9235
9236        let orr_op = ArmOp::Orr {
9237            rd: Reg::R0,
9238            rn: Reg::R1,
9239            op2: Operand2::Reg(Reg::R2),
9240        };
9241        let orr_code = encoder.encode(&orr_op).unwrap();
9242        assert_eq!(orr_code.len(), 4);
9243
9244        let eor_op = ArmOp::Eor {
9245            rd: Reg::R0,
9246            rn: Reg::R1,
9247            op2: Operand2::Reg(Reg::R2),
9248        };
9249        let eor_code = encoder.encode(&eor_op).unwrap();
9250        assert_eq!(eor_code.len(), 4);
9251    }
9252
9253    // === Thumb-2 32-bit encoding tests ===
9254
9255    #[test]
9256    fn test_encode_sdiv_thumb2() {
9257        let encoder = ArmEncoder::new_thumb2();
9258        let op = ArmOp::Sdiv {
9259            rd: Reg::R0,
9260            rn: Reg::R1,
9261            rm: Reg::R2,
9262        };
9263
9264        let code = encoder.encode(&op).unwrap();
9265        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9266
9267        // SDIV R0, R1, R2: 0xFB91 0xF0F2
9268        // First halfword: 0xFB90 | Rn(1) = 0xFB91
9269        // Second halfword: 0xF0F0 | Rd(0)<<8 | Rm(2) = 0xF0F2
9270        // Little-endian: [0x91, 0xFB, 0xF2, 0xF0]
9271        assert_eq!(code[0], 0x91);
9272        assert_eq!(code[1], 0xFB);
9273        assert_eq!(code[2], 0xF2);
9274        assert_eq!(code[3], 0xF0);
9275    }
9276
9277    #[test]
9278    fn test_encode_udiv_thumb2() {
9279        let encoder = ArmEncoder::new_thumb2();
9280        let op = ArmOp::Udiv {
9281            rd: Reg::R0,
9282            rn: Reg::R1,
9283            rm: Reg::R2,
9284        };
9285
9286        let code = encoder.encode(&op).unwrap();
9287        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9288
9289        // UDIV R0, R1, R2: 0xFBB1 0xF0F2
9290        // Little-endian: [0xB1, 0xFB, 0xF2, 0xF0]
9291        assert_eq!(code[0], 0xB1);
9292        assert_eq!(code[1], 0xFB);
9293        assert_eq!(code[2], 0xF2);
9294        assert_eq!(code[3], 0xF0);
9295    }
9296
9297    #[test]
9298    fn test_encode_mul_thumb2() {
9299        let encoder = ArmEncoder::new_thumb2();
9300        let op = ArmOp::Mul {
9301            rd: Reg::R0,
9302            rn: Reg::R1,
9303            rm: Reg::R2,
9304        };
9305
9306        let code = encoder.encode(&op).unwrap();
9307        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9308    }
9309
9310    #[test]
9311    fn test_encode_and_thumb2() {
9312        let encoder = ArmEncoder::new_thumb2();
9313        let op = ArmOp::And {
9314            rd: Reg::R0,
9315            rn: Reg::R1,
9316            op2: Operand2::Reg(Reg::R2),
9317        };
9318
9319        let code = encoder.encode(&op).unwrap();
9320        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9321    }
9322
9323    #[test]
9324    fn test_encode_lsl_thumb2_low_regs() {
9325        let encoder = ArmEncoder::new_thumb2();
9326        let op = ArmOp::Lsl {
9327            rd: Reg::R0,
9328            rn: Reg::R1,
9329            shift: 5,
9330        };
9331
9332        let code = encoder.encode(&op).unwrap();
9333        assert_eq!(code.len(), 2); // 16-bit for low registers
9334    }
9335
9336    #[test]
9337    fn test_encode_clz_thumb2() {
9338        let encoder = ArmEncoder::new_thumb2();
9339        let op = ArmOp::Clz {
9340            rd: Reg::R0,
9341            rm: Reg::R1,
9342        };
9343
9344        let code = encoder.encode(&op).unwrap();
9345        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9346    }
9347
9348    #[test]
9349    fn test_encode_bx_thumb2() {
9350        let encoder = ArmEncoder::new_thumb2();
9351        let op = ArmOp::Bx { rm: Reg::LR };
9352
9353        let code = encoder.encode(&op).unwrap();
9354        assert_eq!(code.len(), 2); // 16-bit instruction
9355
9356        // BX LR: 0x4770
9357        assert_eq!(code, vec![0x70, 0x47]);
9358    }
9359
9360    // ========================================================================
9361    // f32 pseudo-op encoding tests
9362    // ========================================================================
9363
9364    #[test]
9365    fn test_encode_f32_abs_arm32() {
9366        let encoder = ArmEncoder::new_arm32();
9367        let op = ArmOp::F32Abs {
9368            sd: VfpReg::S0,
9369            sm: VfpReg::S2,
9370        };
9371        let code = encoder.encode(&op).unwrap();
9372        assert_eq!(code.len(), 4); // Single VFP instruction
9373    }
9374
9375    #[test]
9376    fn test_encode_f32_neg_arm32() {
9377        let encoder = ArmEncoder::new_arm32();
9378        let op = ArmOp::F32Neg {
9379            sd: VfpReg::S0,
9380            sm: VfpReg::S2,
9381        };
9382        let code = encoder.encode(&op).unwrap();
9383        assert_eq!(code.len(), 4);
9384    }
9385
9386    #[test]
9387    fn test_encode_f32_sqrt_arm32() {
9388        let encoder = ArmEncoder::new_arm32();
9389        let op = ArmOp::F32Sqrt {
9390            sd: VfpReg::S0,
9391            sm: VfpReg::S2,
9392        };
9393        let code = encoder.encode(&op).unwrap();
9394        assert_eq!(code.len(), 4);
9395    }
9396
9397    #[test]
9398    fn test_encode_f32_ceil_arm32() {
9399        let encoder = ArmEncoder::new_arm32();
9400        let op = ArmOp::F32Ceil {
9401            sd: VfpReg::S0,
9402            sm: VfpReg::S2,
9403        };
9404        let code = encoder.encode(&op).unwrap();
9405        // VMRS + BIC + ORR + VMSR + VCVT.S32.F32 + VMRS + BIC + VMSR + VCVT.F32.S32
9406        assert_eq!(code.len(), 36);
9407    }
9408
9409    #[test]
9410    fn test_encode_f32_floor_thumb2() {
9411        let encoder = ArmEncoder::new_thumb2();
9412        let op = ArmOp::F32Floor {
9413            sd: VfpReg::S0,
9414            sm: VfpReg::S2,
9415        };
9416        let code = encoder.encode(&op).unwrap();
9417        // VMRS + BIC.W + ORR.W + VMSR + VCVT + VMRS + BIC.W + VMSR + VCVT.F32.S32
9418        assert_eq!(code.len(), 36);
9419    }
9420
9421    #[test]
9422    fn test_encode_f32_min_arm32() {
9423        let encoder = ArmEncoder::new_arm32();
9424        let op = ArmOp::F32Min {
9425            sd: VfpReg::S0,
9426            sn: VfpReg::S2,
9427            sm: VfpReg::S4,
9428        };
9429        let code = encoder.encode(&op).unwrap();
9430        assert_eq!(code.len(), 16); // VMOV + VCMP + VMRS + conditional VMOV
9431    }
9432
9433    #[test]
9434    fn test_encode_f32_max_thumb2() {
9435        let encoder = ArmEncoder::new_thumb2();
9436        let op = ArmOp::F32Max {
9437            sd: VfpReg::S0,
9438            sn: VfpReg::S2,
9439            sm: VfpReg::S4,
9440        };
9441        let code = encoder.encode(&op).unwrap();
9442        // VMOV(4) + VCMP(4) + VMRS(4) + IT(2) + VMOV(4) = 18
9443        assert_eq!(code.len(), 18);
9444    }
9445
9446    #[test]
9447    fn test_encode_f32_copysign_arm32() {
9448        let encoder = ArmEncoder::new_arm32();
9449        let op = ArmOp::F32Copysign {
9450            sd: VfpReg::S0,
9451            sn: VfpReg::S2,
9452            sm: VfpReg::S4,
9453        };
9454        let code = encoder.encode(&op).unwrap();
9455        // VMOV + VMOV + AND + BIC + ORR + VMOV = 6 * 4 = 24
9456        assert_eq!(code.len(), 24);
9457    }
9458
9459    // ========================================================================
9460    // f64 encoding tests
9461    // ========================================================================
9462
9463    #[test]
9464    fn test_encode_f64_add_arm32() {
9465        let encoder = ArmEncoder::new_arm32();
9466        let op = ArmOp::F64Add {
9467            dd: VfpReg::D0,
9468            dn: VfpReg::D1,
9469            dm: VfpReg::D2,
9470        };
9471        let code = encoder.encode(&op).unwrap();
9472        assert_eq!(code.len(), 4);
9473        // VADD.F64 D0, D1, D2: check coprocessor is cp11 (0xB)
9474        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9475        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
9476    }
9477
9478    #[test]
9479    fn test_encode_f64_sub_thumb2() {
9480        let encoder = ArmEncoder::new_thumb2();
9481        let op = ArmOp::F64Sub {
9482            dd: VfpReg::D0,
9483            dn: VfpReg::D1,
9484            dm: VfpReg::D2,
9485        };
9486        let code = encoder.encode(&op).unwrap();
9487        assert_eq!(code.len(), 4); // 32-bit VFP as two Thumb halfwords
9488    }
9489
9490    #[test]
9491    fn test_encode_f64_mul_arm32() {
9492        let encoder = ArmEncoder::new_arm32();
9493        let op = ArmOp::F64Mul {
9494            dd: VfpReg::D0,
9495            dn: VfpReg::D1,
9496            dm: VfpReg::D2,
9497        };
9498        let code = encoder.encode(&op).unwrap();
9499        assert_eq!(code.len(), 4);
9500    }
9501
9502    #[test]
9503    fn test_encode_f64_div_arm32() {
9504        let encoder = ArmEncoder::new_arm32();
9505        let op = ArmOp::F64Div {
9506            dd: VfpReg::D0,
9507            dn: VfpReg::D1,
9508            dm: VfpReg::D2,
9509        };
9510        let code = encoder.encode(&op).unwrap();
9511        assert_eq!(code.len(), 4);
9512    }
9513
9514    #[test]
9515    fn test_encode_f64_abs_arm32() {
9516        let encoder = ArmEncoder::new_arm32();
9517        let op = ArmOp::F64Abs {
9518            dd: VfpReg::D0,
9519            dm: VfpReg::D2,
9520        };
9521        let code = encoder.encode(&op).unwrap();
9522        assert_eq!(code.len(), 4);
9523    }
9524
9525    #[test]
9526    fn test_encode_f64_neg_arm32() {
9527        let encoder = ArmEncoder::new_arm32();
9528        let op = ArmOp::F64Neg {
9529            dd: VfpReg::D0,
9530            dm: VfpReg::D2,
9531        };
9532        let code = encoder.encode(&op).unwrap();
9533        assert_eq!(code.len(), 4);
9534    }
9535
9536    #[test]
9537    fn test_encode_f64_sqrt_arm32() {
9538        let encoder = ArmEncoder::new_arm32();
9539        let op = ArmOp::F64Sqrt {
9540            dd: VfpReg::D0,
9541            dm: VfpReg::D2,
9542        };
9543        let code = encoder.encode(&op).unwrap();
9544        assert_eq!(code.len(), 4);
9545    }
9546
9547    #[test]
9548    fn test_encode_f64_load_arm32() {
9549        let encoder = ArmEncoder::new_arm32();
9550        let op = ArmOp::F64Load {
9551            dd: VfpReg::D0,
9552            addr: MemAddr::imm(Reg::R0, 8),
9553        };
9554        let code = encoder.encode(&op).unwrap();
9555        assert_eq!(code.len(), 4);
9556        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9557        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11 for F64
9558        assert_eq!(instr & 0xFF, 2); // offset 8 / 4 = 2
9559    }
9560
9561    #[test]
9562    fn test_encode_f64_store_thumb2() {
9563        let encoder = ArmEncoder::new_thumb2();
9564        let op = ArmOp::F64Store {
9565            dd: VfpReg::D0,
9566            addr: MemAddr::imm(Reg::SP, 0),
9567        };
9568        let code = encoder.encode(&op).unwrap();
9569        assert_eq!(code.len(), 4);
9570    }
9571
9572    #[test]
9573    fn test_encode_f64_compare_arm32() {
9574        let encoder = ArmEncoder::new_arm32();
9575        let op = ArmOp::F64Eq {
9576            rd: Reg::R0,
9577            dn: VfpReg::D0,
9578            dm: VfpReg::D1,
9579        };
9580        let code = encoder.encode(&op).unwrap();
9581        assert_eq!(code.len(), 16); // VCMP + VMRS + MOV #0 + MOVcond #1
9582    }
9583
9584    #[test]
9585    fn test_encode_f64_compare_thumb2() {
9586        let encoder = ArmEncoder::new_thumb2();
9587        let op = ArmOp::F64Lt {
9588            rd: Reg::R0,
9589            dn: VfpReg::D0,
9590            dm: VfpReg::D1,
9591        };
9592        let code = encoder.encode(&op).unwrap();
9593        // VCMP(4) + VMRS(4) + MOVS(2) + IT(2) + MOV(2) = 14
9594        assert_eq!(code.len(), 14);
9595    }
9596
9597    #[test]
9598    fn test_encode_f64_const_arm32() {
9599        let encoder = ArmEncoder::new_arm32();
9600        let op = ArmOp::F64Const {
9601            dd: VfpReg::D0,
9602            value: 3.125,
9603        };
9604        let code = encoder.encode(&op).unwrap();
9605        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
9606        assert_eq!(code.len(), 20);
9607    }
9608
9609    #[test]
9610    fn test_encode_f64_const_thumb2() {
9611        let encoder = ArmEncoder::new_thumb2();
9612        let op = ArmOp::F64Const {
9613            dd: VfpReg::D0,
9614            value: 2.5,
9615        };
9616        let code = encoder.encode(&op).unwrap();
9617        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
9618        assert_eq!(code.len(), 20);
9619    }
9620
9621    #[test]
9622    fn test_encode_f64_convert_i32s_arm32() {
9623        let encoder = ArmEncoder::new_arm32();
9624        let op = ArmOp::F64ConvertI32S {
9625            dd: VfpReg::D0,
9626            rm: Reg::R0,
9627        };
9628        let code = encoder.encode(&op).unwrap();
9629        // VMOV(4) + VCVT(4) = 8
9630        assert_eq!(code.len(), 8);
9631    }
9632
9633    #[test]
9634    fn test_encode_f64_promote_f32_arm32() {
9635        let encoder = ArmEncoder::new_arm32();
9636        let op = ArmOp::F64PromoteF32 {
9637            dd: VfpReg::D0,
9638            sm: VfpReg::S0,
9639        };
9640        let code = encoder.encode(&op).unwrap();
9641        assert_eq!(code.len(), 4); // Single VCVT.F64.F32 instruction
9642    }
9643
9644    #[test]
9645    fn test_encode_f64_promote_f32_thumb2() {
9646        let encoder = ArmEncoder::new_thumb2();
9647        let op = ArmOp::F64PromoteF32 {
9648            dd: VfpReg::D0,
9649            sm: VfpReg::S0,
9650        };
9651        let code = encoder.encode(&op).unwrap();
9652        assert_eq!(code.len(), 4);
9653    }
9654
9655    #[test]
9656    fn test_encode_i32_trunc_f64s_arm32() {
9657        let encoder = ArmEncoder::new_arm32();
9658        let op = ArmOp::I32TruncF64S {
9659            rd: Reg::R0,
9660            dm: VfpReg::D0,
9661        };
9662        let code = encoder.encode(&op).unwrap();
9663        // VCVT(4) + VMOV(4) = 8
9664        assert_eq!(code.len(), 8);
9665    }
9666
9667    #[test]
9668    fn test_encode_f64_reinterpret_i64_arm32() {
9669        let encoder = ArmEncoder::new_arm32();
9670        let op = ArmOp::F64ReinterpretI64 {
9671            dd: VfpReg::D0,
9672            rmlo: Reg::R0,
9673            rmhi: Reg::R1,
9674        };
9675        let code = encoder.encode(&op).unwrap();
9676        assert_eq!(code.len(), 4); // Single VMOV instruction
9677    }
9678
9679    #[test]
9680    fn test_encode_i64_reinterpret_f64_thumb2() {
9681        let encoder = ArmEncoder::new_thumb2();
9682        let op = ArmOp::I64ReinterpretF64 {
9683            rdlo: Reg::R0,
9684            rdhi: Reg::R1,
9685            dm: VfpReg::D0,
9686        };
9687        let code = encoder.encode(&op).unwrap();
9688        assert_eq!(code.len(), 4);
9689    }
9690
9691    #[test]
9692    fn test_encode_f64_trunc_thumb2() {
9693        let encoder = ArmEncoder::new_thumb2();
9694        let op = ArmOp::F64Trunc {
9695            dd: VfpReg::D0,
9696            dm: VfpReg::D1,
9697        };
9698        let code = encoder.encode(&op).unwrap();
9699        // Two VFP instructions via Thumb encoding
9700        assert_eq!(code.len(), 8);
9701    }
9702
9703    #[test]
9704    fn test_encode_f64_min_arm32() {
9705        let encoder = ArmEncoder::new_arm32();
9706        let op = ArmOp::F64Min {
9707            dd: VfpReg::D0,
9708            dn: VfpReg::D1,
9709            dm: VfpReg::D2,
9710        };
9711        let code = encoder.encode(&op).unwrap();
9712        // VMOV + VCMP + VMRS + conditional VMOV = 16
9713        assert_eq!(code.len(), 16);
9714    }
9715
9716    #[test]
9717    fn test_f64_cp11_encoding() {
9718        // Verify that F64 instructions use coprocessor 11 (0xB), not 10 (0xA)
9719        let encoder = ArmEncoder::new_arm32();
9720
9721        // F64Add
9722        let code = encoder
9723            .encode(&ArmOp::F64Add {
9724                dd: VfpReg::D0,
9725                dn: VfpReg::D0,
9726                dm: VfpReg::D0,
9727            })
9728            .unwrap();
9729        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9730        assert_eq!((instr >> 8) & 0xF, 0xB, "F64 should use cp11");
9731
9732        // F32Add for comparison
9733        let code = encoder
9734            .encode(&ArmOp::F32Add {
9735                sd: VfpReg::S0,
9736                sn: VfpReg::S0,
9737                sm: VfpReg::S0,
9738            })
9739            .unwrap();
9740        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9741        assert_eq!((instr >> 8) & 0xF, 0xA, "F32 should use cp10");
9742    }
9743
9744    #[test]
9745    fn test_dreg_encoding_higher_registers() {
9746        let encoder = ArmEncoder::new_arm32();
9747
9748        // Test with D15 (highest register)
9749        let op = ArmOp::F64Add {
9750            dd: VfpReg::D15,
9751            dn: VfpReg::D14,
9752            dm: VfpReg::D13,
9753        };
9754        let code = encoder.encode(&op).unwrap();
9755        assert_eq!(code.len(), 4);
9756
9757        // Verify the register encoding worked (instruction is valid)
9758        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9759        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
9760    }
9761
9762    // ========================================================================
9763    // Control flow encoding tests
9764    // ========================================================================
9765
9766    #[test]
9767    fn test_encode_label_emits_no_bytes() {
9768        let encoder = ArmEncoder::new_thumb2();
9769        let op = ArmOp::Label {
9770            name: ".Lblock_end_0".to_string(),
9771        };
9772        let code = encoder.encode(&op).unwrap();
9773        assert!(code.is_empty(), "Label should emit zero bytes");
9774
9775        let encoder32 = ArmEncoder::new_arm32();
9776        let code32 = encoder32.encode(&op).unwrap();
9777        assert!(
9778            code32.is_empty(),
9779            "Label should emit zero bytes in ARM32 too"
9780        );
9781    }
9782
9783    #[test]
9784    fn test_encode_bcc_eq_thumb2() {
9785        use synth_synthesis::Condition;
9786        let encoder = ArmEncoder::new_thumb2();
9787        let op = ArmOp::Bcc {
9788            cond: Condition::EQ,
9789            label: "target".to_string(),
9790        };
9791        let code = encoder.encode(&op).unwrap();
9792        assert_eq!(code.len(), 2); // 16-bit conditional branch
9793
9794        // BEQ with offset 0: 0xD000 in little-endian
9795        assert_eq!(code, vec![0x00, 0xD0]);
9796    }
9797
9798    #[test]
9799    fn test_encode_bcc_ne_thumb2() {
9800        use synth_synthesis::Condition;
9801        let encoder = ArmEncoder::new_thumb2();
9802        let op = ArmOp::Bcc {
9803            cond: Condition::NE,
9804            label: "target".to_string(),
9805        };
9806        let code = encoder.encode(&op).unwrap();
9807        assert_eq!(code.len(), 2);
9808
9809        // BNE with offset 0: 0xD100 in little-endian
9810        assert_eq!(code, vec![0x00, 0xD1]);
9811    }
9812
9813    #[test]
9814    fn test_encode_bcc_arm32() {
9815        use synth_synthesis::Condition;
9816        let encoder = ArmEncoder::new_arm32();
9817        let op = ArmOp::Bcc {
9818            cond: Condition::EQ,
9819            label: "target".to_string(),
9820        };
9821        let code = encoder.encode(&op).unwrap();
9822        assert_eq!(code.len(), 4); // 32-bit ARM instruction
9823
9824        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9825        // BEQ: cond=0x0, opcode=0xA, offset=0
9826        assert_eq!(instr & 0xF0000000, 0x00000000); // EQ condition
9827        assert_eq!(instr & 0x0F000000, 0x0A000000); // Branch opcode
9828    }
9829
9830    #[test]
9831    fn test_encode_udf_thumb2() {
9832        let encoder = ArmEncoder::new_thumb2();
9833        let op = ArmOp::Udf { imm: 0 };
9834        let code = encoder.encode(&op).unwrap();
9835        assert_eq!(code.len(), 2); // 16-bit
9836
9837        // UDF #0: 0xDE00 in little-endian
9838        assert_eq!(code, vec![0x00, 0xDE]);
9839    }
9840
9841    /// #610: the i64 rot/div/rem expansions must land the result in the
9842    /// selector-assigned rd pair and leave R0-R3 preserved (restored from the
9843    /// fixed-ABI wrapper's save area) — pre-#610 the rot expansion's own
9844    /// `POP {R4}` restored stale scratch OVER the result (rd_lo == R4) and
9845    /// the div/rem expansions ignored their register fields outright.
9846    #[test]
9847    fn test_610_i64_rot_expansion_ends_with_rd_movs_and_restore() {
9848        let encoder = ArmEncoder::new_thumb2();
9849        for op in [
9850            ArmOp::I64Rotl {
9851                rdlo: Reg::R4,
9852                rdhi: Reg::R5,
9853                rnlo: Reg::R0,
9854                rnhi: Reg::R1,
9855                shift: Reg::R2,
9856            },
9857            ArmOp::I64Rotr {
9858                rdlo: Reg::R4,
9859                rdhi: Reg::R5,
9860                rnlo: Reg::R0,
9861                rnhi: Reg::R1,
9862                shift: Reg::R2,
9863            },
9864        ] {
9865            let code = encoder.encode(&op).unwrap();
9866            assert_eq!(code.len(), 102, "register-independent size (estimator pin)");
9867            // Tail: MOV r5, r1 (0x460D); MOV r4, r0 (0x4604); POP {r0..r3}
9868            // (rd pair r4:r5 does not overlap the save area — all 4 restored).
9869            let tail: Vec<u16> = code[code.len() - 12..]
9870                .chunks(2)
9871                .map(|c| u16::from_le_bytes([c[0], c[1]]))
9872                .collect();
9873            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
9874        }
9875    }
9876
9877    /// #610: div/rem expansions honor rd and carry the divide-by-zero trap
9878    /// guard (`ORRS R12, R2, R3; BNE +0; UDF #0`) after operand marshaling.
9879    #[test]
9880    fn test_610_i64_div_rem_expansion_guard_and_rd() {
9881        let encoder = ArmEncoder::new_thumb2();
9882        let mk = |which: u8| {
9883            let (rdlo, rdhi, rnlo, rnhi, rmlo, rmhi) =
9884                (Reg::R4, Reg::R5, Reg::R0, Reg::R1, Reg::R2, Reg::R3);
9885            match which {
9886                0 => ArmOp::I64DivU {
9887                    rdlo,
9888                    rdhi,
9889                    rnlo,
9890                    rnhi,
9891                    rmlo,
9892                    rmhi,
9893                },
9894                1 => ArmOp::I64RemU {
9895                    rdlo,
9896                    rdhi,
9897                    rnlo,
9898                    rnhi,
9899                    rmlo,
9900                    rmhi,
9901                },
9902                2 => ArmOp::I64DivS {
9903                    rdlo,
9904                    rdhi,
9905                    rnlo,
9906                    rnhi,
9907                    rmlo,
9908                    rmhi,
9909                },
9910                _ => ArmOp::I64RemS {
9911                    rdlo,
9912                    rdhi,
9913                    rnlo,
9914                    rnhi,
9915                    rmlo,
9916                    rmhi,
9917                },
9918            }
9919        };
9920        for which in 0..4u8 {
9921            let code = encoder.encode(&mk(which)).unwrap();
9922            // Zero-divisor trap guard right after the 26-byte marshal prologue.
9923            let guard: Vec<u16> = code[26..34]
9924                .chunks(2)
9925                .map(|c| u16::from_le_bytes([c[0], c[1]]))
9926                .collect();
9927            assert_eq!(
9928                guard,
9929                vec![0xEA52, 0x0C03, 0xD100, 0xDE00],
9930                "ORRS R12,R2,R3; BNE +0; UDF #0"
9931            );
9932            // Tail: result into rd pair (r5:r4), then restore all of R0-R3.
9933            let tail: Vec<u16> = code[code.len() - 12..]
9934                .chunks(2)
9935                .map(|c| u16::from_le_bytes([c[0], c[1]]))
9936                .collect();
9937            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
9938        }
9939    }
9940
9941    /// #610: when rd overlaps R0-R3 the restore must SKIP the result
9942    /// registers (drop the saved caller word) instead of popping over them.
9943    #[test]
9944    fn test_610_i64_divu_rd_in_r0_r1_skips_restore() {
9945        let encoder = ArmEncoder::new_thumb2();
9946        let code = encoder
9947            .encode(&ArmOp::I64DivU {
9948                rdlo: Reg::R0,
9949                rdhi: Reg::R1,
9950                rnlo: Reg::R0,
9951                rnhi: Reg::R1,
9952                rmlo: Reg::R2,
9953                rmhi: Reg::R3,
9954            })
9955            .unwrap();
9956        let tail: Vec<u16> = code[code.len() - 12..]
9957            .chunks(2)
9958            .map(|c| u16::from_le_bytes([c[0], c[1]]))
9959            .collect();
9960        // MOV r1,r1 / MOV r0,r0 (no-ops, size-stable), ADD SP,#4 twice
9961        // (discard saved r0/r1 — the result lives there), POP {r2}, POP {r3}.
9962        assert_eq!(tail, vec![0x4609, 0x4600, 0xB001, 0xB001, 0xBC04, 0xBC08]);
9963    }
9964
9965    /// #610: a fully swapped rd pair (rd_lo=R1, rd_hi=R0) cannot be
9966    /// materialized by two MOVs in either order — must be a loud Err, never
9967    /// silent corruption. (Selector pairs are consecutive, so unreachable.)
9968    #[test]
9969    fn test_610_i64_swapped_rd_pair_rejected() {
9970        let encoder = ArmEncoder::new_thumb2();
9971        let result = encoder.encode(&ArmOp::I64RemU {
9972            rdlo: Reg::R1,
9973            rdhi: Reg::R0,
9974            rnlo: Reg::R2,
9975            rnhi: Reg::R3,
9976            rmlo: Reg::R4,
9977            rmhi: Reg::R5,
9978        });
9979        assert!(result.is_err(), "swapped rd pair must be rejected loudly");
9980    }
9981
9982    /// #632: the I64Popcnt expansion's own scratch restore (`POP {R3,R4,R5}`)
9983    /// must not clobber the result. Pre-fix the total was materialized with
9984    /// `ADDS rd, R4, R5` BEFORE the pop, so any allocator-assigned
9985    /// rd ∈ {R3,R4,R5} received stale stack garbage. Post-fix the count is
9986    /// carried across the restore in R12 (never allocatable, never restored)
9987    /// and moved into rd only after the pop — structurally rd-independent.
9988    #[test]
9989    fn test_632_i64_popcnt_result_survives_scratch_restore() {
9990        let encoder = ArmEncoder::new_thumb2();
9991        // Every allocatable rd, including the restore set {R3,R4,R5} and R8.
9992        for rd in [
9993            Reg::R0,
9994            Reg::R2,
9995            Reg::R3,
9996            Reg::R4,
9997            Reg::R5,
9998            Reg::R6,
9999            Reg::R8,
10000        ] {
10001            let code = encoder
10002                .encode(&ArmOp::I64Popcnt {
10003                    rd,
10004                    rnlo: Reg::R6,
10005                    rnhi: Reg::R7,
10006                })
10007                .unwrap();
10008            assert_eq!(code.len(), 180, "register-independent size (estimator pin)");
10009            let hw: Vec<u16> = code
10010                .chunks(2)
10011                .map(|c| u16::from_le_bytes([c[0], c[1]]))
10012                .collect();
10013            let pop = hw
10014                .iter()
10015                .position(|&h| h == 0xBC38)
10016                .expect("POP {R3,R4,R5} present");
10017            // Immediately before the POP: ADD.W R12, R4, R5 (the total lives
10018            // in R12, which the POP cannot touch).
10019            assert_eq!(
10020                &hw[pop - 2..pop],
10021                &[0xEB04, 0x0C05],
10022                "total must be carried in R12 across the restore"
10023            );
10024            // Immediately after the POP: MOV rd, R12.
10025            let rd_bits = match rd {
10026                Reg::R8 => 8u16,
10027                Reg::R6 => 6,
10028                Reg::R5 => 5,
10029                Reg::R4 => 4,
10030                Reg::R3 => 3,
10031                Reg::R2 => 2,
10032                _ => 0,
10033            };
10034            let expect_mov = 0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7);
10035            assert_eq!(hw[pop + 1], expect_mov, "MOV rd, R12 after the restore");
10036            // No write into rd between the PUSH and the POP (the old
10037            // pre-restore ADDS is gone).
10038            assert!(
10039                !hw[..pop].contains(&(0x1800 | (5 << 6) | (4 << 3) | rd_bits)),
10040                "no ADDS rd, R4, R5 before the restore pop"
10041            );
10042        }
10043    }
10044
10045    /// #632 audit: the entry marshal must be permutation-safe. Pre-fix
10046    /// `MOV R4, rnlo; MOV R5, rnhi` read a clobbered R4 when the operand
10047    /// pair lived at (R3, R4). Post-fix rnlo routes through R12.
10048    #[test]
10049    fn test_632_i64_popcnt_marshal_pair_at_r3_r4() {
10050        let encoder = ArmEncoder::new_thumb2();
10051        let code = encoder
10052            .encode(&ArmOp::I64Popcnt {
10053                rd: Reg::R0,
10054                rnlo: Reg::R3,
10055                rnhi: Reg::R4,
10056            })
10057            .unwrap();
10058        let hw: Vec<u16> = code
10059            .chunks(2)
10060            .map(|c| u16::from_le_bytes([c[0], c[1]]))
10061            .collect();
10062        // PUSH {R3,R4,R5}; MOV R12, R3; MOV R5, R4 (rnhi read BEFORE any
10063        // write to R4); MOV R4, R12.
10064        assert_eq!(hw[0], 0xB438);
10065        assert_eq!(hw[1], 0x4600 | (1 << 7) | (3 << 3) | 4, "MOV R12, rnlo");
10066        assert_eq!(hw[2], 0x4600 | (4 << 3) | 5, "MOV R5, rnhi");
10067        assert_eq!(hw[3], 0x4664, "MOV R4, R12");
10068    }
10069
10070    /// #632: A32 twin — same structural fix on the ARM-mode path
10071    /// (`--target cortex-r5`): total carried in R12 across the restore.
10072    #[test]
10073    fn test_632_a32_i64_popcnt_result_survives_scratch_restore() {
10074        let encoder = ArmEncoder::new_arm32();
10075        for rd in [Reg::R0, Reg::R3, Reg::R4, Reg::R5, Reg::R8] {
10076            let code = encoder
10077                .encode(&ArmOp::I64Popcnt {
10078                    rd,
10079                    rnlo: Reg::R6,
10080                    rnhi: Reg::R7,
10081                })
10082                .unwrap();
10083            let words: Vec<u32> = code
10084                .chunks(4)
10085                .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
10086                .collect();
10087            let pop = words
10088                .iter()
10089                .position(|&w| w == 0xE8BD_0038)
10090                .expect("POP {R3,R4,R5} present");
10091            assert_eq!(words[pop - 1], 0xE084_C005, "ADD R12, R4, R5 before POP");
10092            let rd_bits = match rd {
10093                Reg::R8 => 8u32,
10094                Reg::R5 => 5,
10095                Reg::R4 => 4,
10096                Reg::R3 => 3,
10097                _ => 0,
10098            };
10099            assert_eq!(
10100                words[pop + 1],
10101                0xE1A0_0000 | (rd_bits << 12) | 12,
10102                "MOV rd, R12 after the restore"
10103            );
10104        }
10105    }
10106
10107    /// #633: I64DivS must carry the INT64_MIN/-1 overflow guard (mirroring
10108    /// the i32 path) right after the zero-divisor guard — dividend in R0:R1,
10109    /// divisor in R2:R3 on the #610/#613 fixed-ABI wrapper path.
10110    #[test]
10111    fn test_633_i64_divs_overflow_guard_emitted() {
10112        let encoder = ArmEncoder::new_thumb2();
10113        let code = encoder
10114            .encode(&ArmOp::I64DivS {
10115                rdlo: Reg::R4,
10116                rdhi: Reg::R5,
10117                rnlo: Reg::R0,
10118                rnhi: Reg::R1,
10119                rmlo: Reg::R2,
10120                rmhi: Reg::R3,
10121            })
10122            .unwrap();
10123        // 26-byte marshal + 8-byte zero-trap, then the 22-byte overflow guard.
10124        let guard: Vec<u16> = code[34..56]
10125            .chunks(2)
10126            .map(|c| u16::from_le_bytes([c[0], c[1]]))
10127            .collect();
10128        assert_eq!(
10129            guard,
10130            vec![
10131                0xEA02, 0x0C03, // AND.W R12, R2, R3
10132                0xF11C, 0x0F01, // CMN.W R12, #1
10133                0xD105, // BNE .no_trap
10134                0x2800, // CMP R0, #0
10135                0xD103, // BNE .no_trap
10136                0xF1B1, 0x4F00, // CMP.W R1, #0x80000000
10137                0xD100, // BNE .no_trap
10138                0xDE00, // UDF #0 — signed-division overflow
10139            ],
10140            "INT64_MIN/-1 overflow guard after the zero-divisor guard"
10141        );
10142    }
10143
10144    /// #633 fix-guard twin: I64RemS must NOT carry the overflow guard —
10145    /// rem_s(INT64_MIN, -1) is defined as 0 and must not trap. Exactly one
10146    /// UDF (the zero-divisor trap) in the whole expansion.
10147    #[test]
10148    fn test_633_i64_rems_has_no_overflow_guard() {
10149        let encoder = ArmEncoder::new_thumb2();
10150        for (is_rem_s, op) in [
10151            (
10152                true,
10153                ArmOp::I64RemS {
10154                    rdlo: Reg::R4,
10155                    rdhi: Reg::R5,
10156                    rnlo: Reg::R0,
10157                    rnhi: Reg::R1,
10158                    rmlo: Reg::R2,
10159                    rmhi: Reg::R3,
10160                },
10161            ),
10162            (
10163                false,
10164                ArmOp::I64DivS {
10165                    rdlo: Reg::R4,
10166                    rdhi: Reg::R5,
10167                    rnlo: Reg::R0,
10168                    rnhi: Reg::R1,
10169                    rmlo: Reg::R2,
10170                    rmhi: Reg::R3,
10171                },
10172            ),
10173        ] {
10174            let code = encoder.encode(&op).unwrap();
10175            let udfs = code
10176                .chunks(2)
10177                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
10178                .count();
10179            let want = if is_rem_s { 1 } else { 2 };
10180            assert_eq!(
10181                udfs, want,
10182                "rem_s: zero-trap only; div_s: zero-trap + overflow trap"
10183            );
10184        }
10185    }
10186
10187    /// #633: A32 twin — the conditional-execution overflow guard on the
10188    /// ARM-mode I64DivS, and its absence from I64RemS.
10189    #[test]
10190    fn test_633_a32_i64_divs_overflow_guard() {
10191        let encoder = ArmEncoder::new_arm32();
10192        let mk_divs = ArmOp::I64DivS {
10193            rdlo: Reg::R4,
10194            rdhi: Reg::R5,
10195            rnlo: Reg::R0,
10196            rnhi: Reg::R1,
10197            rmlo: Reg::R2,
10198            rmhi: Reg::R3,
10199        };
10200        let code = encoder.encode(&mk_divs).unwrap();
10201        let words: Vec<u32> = code
10202            .chunks(4)
10203            .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
10204            .collect();
10205        let guard = [
10206            0xE002_C003u32, // AND   R12, R2, R3
10207            0xE37C_0001,    // CMN   R12, #1
10208            0x0350_0000,    // CMPEQ R0, #0
10209            0x0351_0102,    // CMPEQ R1, #0x80000000
10210            0x1A00_0000,    // BNE +1 insn
10211            0xE7F0_00F0,    // UDF #0
10212        ];
10213        assert!(
10214            words.windows(6).any(|w| w == guard),
10215            "A32 I64DivS carries the INT64_MIN/-1 overflow guard"
10216        );
10217        let rems = encoder
10218            .encode(&ArmOp::I64RemS {
10219                rdlo: Reg::R4,
10220                rdhi: Reg::R5,
10221                rnlo: Reg::R0,
10222                rnhi: Reg::R1,
10223                rmlo: Reg::R2,
10224                rmhi: Reg::R3,
10225            })
10226            .unwrap();
10227        let rems_udfs = rems
10228            .chunks(4)
10229            .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
10230            .count();
10231        assert_eq!(rems_udfs, 1, "A32 I64RemS keeps only the zero-divisor trap");
10232    }
10233
10234    #[test]
10235    fn test_encode_nop_thumb2() {
10236        let encoder = ArmEncoder::new_thumb2();
10237        let op = ArmOp::Nop;
10238        let code = encoder.encode(&op).unwrap();
10239        assert_eq!(code.len(), 2); // 16-bit
10240
10241        // NOP: 0xBF00 in little-endian
10242        assert_eq!(code, vec![0x00, 0xBF]);
10243    }
10244
10245    // =========================================================================
10246    // i64 Thumb-2 encoding tests
10247    // =========================================================================
10248
10249    #[test]
10250    fn test_encode_i64_add_thumb2() {
10251        let encoder = ArmEncoder::new_thumb2();
10252        let op = ArmOp::I64Add {
10253            rdlo: Reg::R0,
10254            rdhi: Reg::R1,
10255            rnlo: Reg::R0,
10256            rnhi: Reg::R1,
10257            rmlo: Reg::R2,
10258            rmhi: Reg::R3,
10259        };
10260        let code = encoder.encode(&op).unwrap();
10261        // Should emit ADDS (2 bytes) + ADC.W (4 bytes) = 6 bytes
10262        assert_eq!(code.len(), 6, "I64Add should be 6 bytes (ADDS + ADC.W)");
10263    }
10264
10265    #[test]
10266    fn test_encode_i64_sub_thumb2() {
10267        let encoder = ArmEncoder::new_thumb2();
10268        let op = ArmOp::I64Sub {
10269            rdlo: Reg::R0,
10270            rdhi: Reg::R1,
10271            rnlo: Reg::R0,
10272            rnhi: Reg::R1,
10273            rmlo: Reg::R2,
10274            rmhi: Reg::R3,
10275        };
10276        let code = encoder.encode(&op).unwrap();
10277        // Should emit SUBS (2 bytes) + SBC.W (4 bytes) = 6 bytes
10278        assert_eq!(code.len(), 6, "I64Sub should be 6 bytes (SUBS + SBC.W)");
10279    }
10280
10281    #[test]
10282    fn test_encode_i64_and_thumb2() {
10283        let encoder = ArmEncoder::new_thumb2();
10284        let op = ArmOp::I64And {
10285            rdlo: Reg::R0,
10286            rdhi: Reg::R1,
10287            rnlo: Reg::R0,
10288            rnhi: Reg::R1,
10289            rmlo: Reg::R2,
10290            rmhi: Reg::R3,
10291        };
10292        let code = encoder.encode(&op).unwrap();
10293        // AND.W (4 bytes) + AND.W (4 bytes) = 8 bytes
10294        assert!(code.len() >= 4, "I64And should emit at least 4 bytes");
10295    }
10296
10297    #[test]
10298    fn test_encode_i64_or_thumb2() {
10299        let encoder = ArmEncoder::new_thumb2();
10300        let op = ArmOp::I64Or {
10301            rdlo: Reg::R0,
10302            rdhi: Reg::R1,
10303            rnlo: Reg::R0,
10304            rnhi: Reg::R1,
10305            rmlo: Reg::R2,
10306            rmhi: Reg::R3,
10307        };
10308        let code = encoder.encode(&op).unwrap();
10309        assert!(code.len() >= 4, "I64Or should emit at least 4 bytes");
10310    }
10311
10312    #[test]
10313    fn test_encode_i64_xor_thumb2() {
10314        let encoder = ArmEncoder::new_thumb2();
10315        let op = ArmOp::I64Xor {
10316            rdlo: Reg::R0,
10317            rdhi: Reg::R1,
10318            rnlo: Reg::R0,
10319            rnhi: Reg::R1,
10320            rmlo: Reg::R2,
10321            rmhi: Reg::R3,
10322        };
10323        let code = encoder.encode(&op).unwrap();
10324        assert!(code.len() >= 4, "I64Xor should emit at least 4 bytes");
10325    }
10326
10327    #[test]
10328    fn test_encode_i64_const_small_thumb2() {
10329        let encoder = ArmEncoder::new_thumb2();
10330        // Small constant: only needs MOVW for each half
10331        let op = ArmOp::I64Const {
10332            rdlo: Reg::R0,
10333            rdhi: Reg::R1,
10334            value: 42,
10335        };
10336        let code = encoder.encode(&op).unwrap();
10337        // MOVW R0, #42 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes minimum
10338        assert!(code.len() >= 8, "I64Const should emit at least 8 bytes");
10339    }
10340
10341    #[test]
10342    fn test_encode_i64_const_large_thumb2() {
10343        let encoder = ArmEncoder::new_thumb2();
10344        // Large constant: needs MOVW+MOVT for each half
10345        let op = ArmOp::I64Const {
10346            rdlo: Reg::R0,
10347            rdhi: Reg::R1,
10348            value: 0x1234_5678_9ABC_DEF0_u64 as i64,
10349        };
10350        let code = encoder.encode(&op).unwrap();
10351        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
10352        assert_eq!(
10353            code.len(),
10354            16,
10355            "I64Const with large value should be 16 bytes"
10356        );
10357    }
10358
10359    #[test]
10360    fn test_encode_i64_extend_i32_s_thumb2() {
10361        let encoder = ArmEncoder::new_thumb2();
10362        let op = ArmOp::I64ExtendI32S {
10363            rdlo: Reg::R0,
10364            rdhi: Reg::R1,
10365            rn: Reg::R0,
10366        };
10367        let code = encoder.encode(&op).unwrap();
10368        // When rdlo == rn, only ASR (4 bytes) is emitted
10369        assert_eq!(
10370            code.len(),
10371            4,
10372            "I64ExtendI32S (same reg) should be 4 bytes (ASR only)"
10373        );
10374    }
10375
10376    #[test]
10377    fn test_encode_i64_extend_i32_s_diff_reg_thumb2() {
10378        let encoder = ArmEncoder::new_thumb2();
10379        let op = ArmOp::I64ExtendI32S {
10380            rdlo: Reg::R0,
10381            rdhi: Reg::R1,
10382            rn: Reg::R2,
10383        };
10384        let code = encoder.encode(&op).unwrap();
10385        // MOV rdlo, rn (2 bytes for low regs) + ASR rdhi, rdlo, #31 (4 bytes) = 6 bytes
10386        assert!(
10387            code.len() >= 6,
10388            "I64ExtendI32S (diff reg) should be at least 6 bytes"
10389        );
10390    }
10391
10392    #[test]
10393    fn test_encode_i64_extend_i32_u_thumb2() {
10394        let encoder = ArmEncoder::new_thumb2();
10395        let op = ArmOp::I64ExtendI32U {
10396            rdlo: Reg::R0,
10397            rdhi: Reg::R1,
10398            rn: Reg::R0,
10399        };
10400        let code = encoder.encode(&op).unwrap();
10401        // When rdlo == rn, only MOV rdhi, #0 (2 bytes) is emitted
10402        assert_eq!(
10403            code.len(),
10404            2,
10405            "I64ExtendI32U (same reg) should be 2 bytes (MOV #0 only)"
10406        );
10407    }
10408
10409    #[test]
10410    fn test_encode_i32_wrap_i64_nop_thumb2() {
10411        let encoder = ArmEncoder::new_thumb2();
10412        // When rd == rnlo, should be a NOP
10413        let op = ArmOp::I32WrapI64 {
10414            rd: Reg::R0,
10415            rnlo: Reg::R0,
10416        };
10417        let code = encoder.encode(&op).unwrap();
10418        assert_eq!(code.len(), 2, "I32WrapI64 same reg should be NOP (2 bytes)");
10419        assert_eq!(code, vec![0x00, 0xBF]); // NOP
10420    }
10421
10422    #[test]
10423    fn test_encode_i32_wrap_i64_diff_reg_thumb2() {
10424        let encoder = ArmEncoder::new_thumb2();
10425        let op = ArmOp::I32WrapI64 {
10426            rd: Reg::R2,
10427            rnlo: Reg::R0,
10428        };
10429        let code = encoder.encode(&op).unwrap();
10430        // MOV R2, R0 (2 or 4 bytes)
10431        assert!(
10432            code.len() >= 2,
10433            "I32WrapI64 diff reg should emit at least 2 bytes"
10434        );
10435    }
10436
10437    #[test]
10438    fn test_encode_i64_eqz_thumb2() {
10439        let encoder = ArmEncoder::new_thumb2();
10440        let op = ArmOp::I64Eqz {
10441            rd: Reg::R0,
10442            rnlo: Reg::R0,
10443            rnhi: Reg::R1,
10444        };
10445        let code = encoder.encode(&op).unwrap();
10446        // Delegates to I64SetCondZ which is already encoded
10447        assert!(
10448            code.len() >= 6,
10449            "I64Eqz should emit at least 6 bytes for ORR+ITE+MOV+MOV"
10450        );
10451    }
10452
10453    #[test]
10454    fn test_encode_i64_eq_thumb2() {
10455        let encoder = ArmEncoder::new_thumb2();
10456        let op = ArmOp::I64Eq {
10457            rd: Reg::R0,
10458            rnlo: Reg::R0,
10459            rnhi: Reg::R1,
10460            rmlo: Reg::R2,
10461            rmhi: Reg::R3,
10462        };
10463        let code = encoder.encode(&op).unwrap();
10464        // Delegates to I64SetCond EQ: CMP lo + IT EQ + CMPEQ hi + ITE EQ + MOV 1 + MOV 0
10465        assert!(code.len() >= 10, "I64Eq should emit at least 10 bytes");
10466    }
10467
10468    #[test]
10469    fn test_encode_i64_ldr_thumb2() {
10470        let encoder = ArmEncoder::new_thumb2();
10471        let op = ArmOp::I64Ldr {
10472            rdlo: Reg::R0,
10473            rdhi: Reg::R1,
10474            addr: MemAddr::imm(Reg::SP, 0),
10475        };
10476        let code = encoder.encode(&op).unwrap();
10477        // Two LDR instructions (lo at offset, hi at offset+4)
10478        assert!(code.len() >= 4, "I64Ldr should emit at least 4 bytes");
10479    }
10480
10481    #[test]
10482    fn test_372_i64_ldr_indexed_materializes_address() {
10483        // #372: a memory i64.load carries an index register (R11 + addr + off).
10484        // The encoder must materialize `ip = base + index` (ADD.W) and load via
10485        // `[ip,#off]` — NOT drop the index. A frame (non-indexed) i64.load must
10486        // stay byte-identical (plain `[base,#off]`, no ADD).
10487        let encoder = ArmEncoder::new_thumb2();
10488        let indexed = encoder
10489            .encode(&ArmOp::I64Ldr {
10490                rdlo: Reg::R0,
10491                rdhi: Reg::R1,
10492                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
10493            })
10494            .unwrap();
10495        // ADD.W ip, fp, r0 = eb0b 0c00 (byte-verified vs arm-none-eabi-as).
10496        assert_eq!(
10497            &indexed[0..4],
10498            &[0x0b, 0xeb, 0x00, 0x0c],
10499            "indexed I64Ldr must start with ADD.W ip, base, index"
10500        );
10501        let frame = encoder
10502            .encode(&ArmOp::I64Ldr {
10503                rdlo: Reg::R0,
10504                rdhi: Reg::R1,
10505                addr: MemAddr::imm(Reg::SP, 8),
10506            })
10507            .unwrap();
10508        // No index -> no ADD.W prefix (byte-identical frame access).
10509        assert_ne!(
10510            &frame[0..2],
10511            &[0x0b, 0xeb],
10512            "frame (non-indexed) I64Ldr must NOT emit an ADD.W"
10513        );
10514    }
10515
10516    #[test]
10517    fn test_382_i64_ldst_large_offset_materializes_not_skips() {
10518        // #382: an indexed i64.load/store whose static offset > 0xFFF must
10519        // MATERIALIZE the offset into the base — NOT return Err (skip the fn).
10520        // Sequence for reg_imm(R11, R0, 5000): MOVW ip,#5000 ; ADD ip,r0,ip ;
10521        // ADD ip,ip,fp ; LDR/STR halves at [ip,#0] / [ip,#4]. Byte-verified tail
10522        // vs arm-none-eabi-as.
10523        let encoder = ArmEncoder::new_thumb2();
10524        // 0x1388 > 0xFFF (MemAddr is not Copy, so build it per use).
10525
10526        let ld = encoder
10527            .encode(&ArmOp::I64Ldr {
10528                rdlo: Reg::R0,
10529                rdhi: Reg::R1,
10530                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
10531            })
10532            .expect("large-offset i64.load must lower, not skip");
10533        // MOVW ip,#0x1388 (4) + ADD ip,r0,ip (4) + ADD ip,ip,fp (4) + 2 LDR (8).
10534        assert_eq!(ld.len(), 20, "expected MOVW + 2×ADD + 2×LDR");
10535        // Must NOT be the small-offset `ADD.W ip, fp, r0` (0x0b 0xeb) prefix —
10536        // that path can only reach imm12 offsets.
10537        assert_ne!(
10538            &ld[0..2],
10539            &[0x0b, 0xeb],
10540            "must materialize the large offset"
10541        );
10542        // Effective base built in ip, then halves at [ip,#0] / [ip,#4].
10543        assert_eq!(
10544            &ld[4..20],
10545            &[
10546                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
10547                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
10548                0xdc, 0xf8, 0x00, 0x00, // LDR.W r0, [ip, #0]
10549                0xdc, 0xf8, 0x04, 0x10, // LDR.W r1, [ip, #4]
10550            ],
10551            "large-offset i64.load must fold offset into ip and access [ip,#0]/[ip,#4]"
10552        );
10553
10554        // Store: same base materialization, STR halves.
10555        let st = encoder
10556            .encode(&ArmOp::I64Str {
10557                rdlo: Reg::R2,
10558                rdhi: Reg::R3,
10559                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
10560            })
10561            .expect("large-offset i64.store must lower, not skip");
10562        assert_eq!(st.len(), 20);
10563        assert_eq!(
10564            &st[4..20],
10565            &[
10566                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
10567                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
10568                0xcc, 0xf8, 0x00, 0x20, // STR.W r2, [ip, #0]
10569                0xcc, 0xf8, 0x04, 0x30, // STR.W r3, [ip, #4]
10570            ],
10571            "large-offset i64.store must fold offset into ip and access [ip,#0]/[ip,#4]"
10572        );
10573
10574        // Small-offset (imm12) indexed access stays byte-identical (#372): the
10575        // effective base is a single `ADD.W ip, fp, r0` and the halves keep the
10576        // folded immediates — NO extra MOVW/ADD.
10577        let small = encoder
10578            .encode(&ArmOp::I64Ldr {
10579                rdlo: Reg::R0,
10580                rdhi: Reg::R1,
10581                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 8),
10582            })
10583            .unwrap();
10584        assert_eq!(
10585            &small[0..4],
10586            &[0x0b, 0xeb, 0x00, 0x0c],
10587            "small-offset indexed i64 must keep the single ADD.W ip, fp, r0"
10588        );
10589        assert_eq!(small.len(), 12, "ADD.W + 2×LDR.W (offset folded in imm12)");
10590    }
10591
10592    #[test]
10593    fn test_encode_i64_str_thumb2() {
10594        let encoder = ArmEncoder::new_thumb2();
10595        let op = ArmOp::I64Str {
10596            rdlo: Reg::R0,
10597            rdhi: Reg::R1,
10598            addr: MemAddr::imm(Reg::SP, 0),
10599        };
10600        let code = encoder.encode(&op).unwrap();
10601        // Two STR instructions (lo at offset, hi at offset+4)
10602        assert!(code.len() >= 4, "I64Str should emit at least 4 bytes");
10603    }
10604
10605    #[test]
10606    fn test_encode_i64_all_comparisons_thumb2() {
10607        let encoder = ArmEncoder::new_thumb2();
10608
10609        let ops = vec![
10610            ArmOp::I64Ne {
10611                rd: Reg::R0,
10612                rnlo: Reg::R0,
10613                rnhi: Reg::R1,
10614                rmlo: Reg::R2,
10615                rmhi: Reg::R3,
10616            },
10617            ArmOp::I64LtS {
10618                rd: Reg::R0,
10619                rnlo: Reg::R0,
10620                rnhi: Reg::R1,
10621                rmlo: Reg::R2,
10622                rmhi: Reg::R3,
10623            },
10624            ArmOp::I64LtU {
10625                rd: Reg::R0,
10626                rnlo: Reg::R0,
10627                rnhi: Reg::R1,
10628                rmlo: Reg::R2,
10629                rmhi: Reg::R3,
10630            },
10631            ArmOp::I64LeS {
10632                rd: Reg::R0,
10633                rnlo: Reg::R0,
10634                rnhi: Reg::R1,
10635                rmlo: Reg::R2,
10636                rmhi: Reg::R3,
10637            },
10638            ArmOp::I64LeU {
10639                rd: Reg::R0,
10640                rnlo: Reg::R0,
10641                rnhi: Reg::R1,
10642                rmlo: Reg::R2,
10643                rmhi: Reg::R3,
10644            },
10645            ArmOp::I64GtS {
10646                rd: Reg::R0,
10647                rnlo: Reg::R0,
10648                rnhi: Reg::R1,
10649                rmlo: Reg::R2,
10650                rmhi: Reg::R3,
10651            },
10652            ArmOp::I64GtU {
10653                rd: Reg::R0,
10654                rnlo: Reg::R0,
10655                rnhi: Reg::R1,
10656                rmlo: Reg::R2,
10657                rmhi: Reg::R3,
10658            },
10659            ArmOp::I64GeS {
10660                rd: Reg::R0,
10661                rnlo: Reg::R0,
10662                rnhi: Reg::R1,
10663                rmlo: Reg::R2,
10664                rmhi: Reg::R3,
10665            },
10666            ArmOp::I64GeU {
10667                rd: Reg::R0,
10668                rnlo: Reg::R0,
10669                rnhi: Reg::R1,
10670                rmlo: Reg::R2,
10671                rmhi: Reg::R3,
10672            },
10673        ];
10674
10675        for op in &ops {
10676            let code = encoder.encode(op).unwrap();
10677            assert!(
10678                code.len() >= 8,
10679                "i64 comparison {:?} should emit at least 8 bytes, got {}",
10680                op,
10681                code.len()
10682            );
10683        }
10684    }
10685
10686    #[test]
10687    fn test_encode_i64_const_zero_thumb2() {
10688        let encoder = ArmEncoder::new_thumb2();
10689        let op = ArmOp::I64Const {
10690            rdlo: Reg::R0,
10691            rdhi: Reg::R1,
10692            value: 0,
10693        };
10694        let code = encoder.encode(&op).unwrap();
10695        // MOVW R0, #0 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes
10696        assert_eq!(code.len(), 8, "I64Const(0) should be 8 bytes");
10697    }
10698
10699    #[test]
10700    fn test_encode_i64_const_negative_one_thumb2() {
10701        let encoder = ArmEncoder::new_thumb2();
10702        let op = ArmOp::I64Const {
10703            rdlo: Reg::R0,
10704            rdhi: Reg::R1,
10705            value: -1, // 0xFFFF_FFFF_FFFF_FFFF
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!(code.len(), 16, "I64Const(-1) should be 16 bytes");
10710    }
10711
10712    // =========================================================================
10713    // Sub-word load/store encoding tests
10714    // =========================================================================
10715
10716    #[test]
10717    fn test_encode_ldrb_arm32() {
10718        let encoder = ArmEncoder::new_arm32();
10719        let op = ArmOp::Ldrb {
10720            rd: Reg::R0,
10721            addr: MemAddr::imm(Reg::R1, 4),
10722        };
10723        let code = encoder.encode(&op).unwrap();
10724        assert_eq!(code.len(), 4, "ARM32 LDRB should be 4 bytes");
10725        // LDRB R0, [R1, #4] = 0xE5D10004
10726        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10727        assert_eq!(encoded, 0xE5D10004, "Should encode LDRB R0, [R1, #4]");
10728    }
10729
10730    #[test]
10731    fn test_encode_strb_arm32() {
10732        let encoder = ArmEncoder::new_arm32();
10733        let op = ArmOp::Strb {
10734            rd: Reg::R0,
10735            addr: MemAddr::imm(Reg::R1, 0),
10736        };
10737        let code = encoder.encode(&op).unwrap();
10738        assert_eq!(code.len(), 4, "ARM32 STRB should be 4 bytes");
10739        // STRB R0, [R1, #0] = 0xE5C10000
10740        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10741        assert_eq!(encoded, 0xE5C10000, "Should encode STRB R0, [R1, #0]");
10742    }
10743
10744    #[test]
10745    fn test_encode_ldrh_arm32() {
10746        let encoder = ArmEncoder::new_arm32();
10747        let op = ArmOp::Ldrh {
10748            rd: Reg::R0,
10749            addr: MemAddr::imm(Reg::R1, 2),
10750        };
10751        let code = encoder.encode(&op).unwrap();
10752        assert_eq!(code.len(), 4, "ARM32 LDRH should be 4 bytes");
10753    }
10754
10755    #[test]
10756    fn test_encode_strh_arm32() {
10757        let encoder = ArmEncoder::new_arm32();
10758        let op = ArmOp::Strh {
10759            rd: Reg::R0,
10760            addr: MemAddr::imm(Reg::R1, 0),
10761        };
10762        let code = encoder.encode(&op).unwrap();
10763        assert_eq!(code.len(), 4, "ARM32 STRH should be 4 bytes");
10764    }
10765
10766    #[test]
10767    fn test_encode_ldrsb_arm32() {
10768        let encoder = ArmEncoder::new_arm32();
10769        let op = ArmOp::Ldrsb {
10770            rd: Reg::R0,
10771            addr: MemAddr::imm(Reg::R1, 0),
10772        };
10773        let code = encoder.encode(&op).unwrap();
10774        assert_eq!(code.len(), 4, "ARM32 LDRSB should be 4 bytes");
10775    }
10776
10777    #[test]
10778    fn test_encode_ldrsh_arm32() {
10779        let encoder = ArmEncoder::new_arm32();
10780        let op = ArmOp::Ldrsh {
10781            rd: Reg::R0,
10782            addr: MemAddr::imm(Reg::R1, 0),
10783        };
10784        let code = encoder.encode(&op).unwrap();
10785        assert_eq!(code.len(), 4, "ARM32 LDRSH should be 4 bytes");
10786    }
10787
10788    #[test]
10789    fn test_encode_ldrb_thumb2_16bit() {
10790        let encoder = ArmEncoder::new_thumb2();
10791        let op = ArmOp::Ldrb {
10792            rd: Reg::R0,
10793            addr: MemAddr::imm(Reg::R1, 4),
10794        };
10795        let code = encoder.encode(&op).unwrap();
10796        // Low registers + small offset -> 16-bit encoding
10797        assert_eq!(
10798            code.len(),
10799            2,
10800            "Thumb-2 LDRB with small offset should be 16-bit"
10801        );
10802    }
10803
10804    #[test]
10805    fn test_encode_ldrb_thumb2_32bit() {
10806        let encoder = ArmEncoder::new_thumb2();
10807        let op = ArmOp::Ldrb {
10808            rd: Reg::R0,
10809            addr: MemAddr::imm(Reg::R1, 100), // offset > 31 needs 32-bit
10810        };
10811        let code = encoder.encode(&op).unwrap();
10812        assert_eq!(
10813            code.len(),
10814            4,
10815            "Thumb-2 LDRB with large offset should be 32-bit"
10816        );
10817    }
10818
10819    #[test]
10820    fn test_encode_strb_thumb2_16bit() {
10821        let encoder = ArmEncoder::new_thumb2();
10822        let op = ArmOp::Strb {
10823            rd: Reg::R0,
10824            addr: MemAddr::imm(Reg::R1, 10),
10825        };
10826        let code = encoder.encode(&op).unwrap();
10827        assert_eq!(
10828            code.len(),
10829            2,
10830            "Thumb-2 STRB with small offset should be 16-bit"
10831        );
10832    }
10833
10834    #[test]
10835    fn test_encode_ldrh_thumb2_16bit() {
10836        let encoder = ArmEncoder::new_thumb2();
10837        let op = ArmOp::Ldrh {
10838            rd: Reg::R0,
10839            addr: MemAddr::imm(Reg::R1, 4), // offset aligned to 2, <= 62
10840        };
10841        let code = encoder.encode(&op).unwrap();
10842        assert_eq!(
10843            code.len(),
10844            2,
10845            "Thumb-2 LDRH with small aligned offset should be 16-bit"
10846        );
10847    }
10848
10849    #[test]
10850    fn test_encode_strh_thumb2_16bit() {
10851        let encoder = ArmEncoder::new_thumb2();
10852        let op = ArmOp::Strh {
10853            rd: Reg::R0,
10854            addr: MemAddr::imm(Reg::R1, 4),
10855        };
10856        let code = encoder.encode(&op).unwrap();
10857        assert_eq!(
10858            code.len(),
10859            2,
10860            "Thumb-2 STRH with small aligned offset should be 16-bit"
10861        );
10862    }
10863
10864    #[test]
10865    fn test_encode_ldrsb_thumb2() {
10866        let encoder = ArmEncoder::new_thumb2();
10867        let op = ArmOp::Ldrsb {
10868            rd: Reg::R0,
10869            addr: MemAddr::imm(Reg::R1, 0),
10870        };
10871        let code = encoder.encode(&op).unwrap();
10872        // LDRSB has no 16-bit immediate form, always 32-bit
10873        assert_eq!(code.len(), 4, "Thumb-2 LDRSB should be 32-bit");
10874    }
10875
10876    #[test]
10877    fn test_encode_ldrsh_thumb2() {
10878        let encoder = ArmEncoder::new_thumb2();
10879        let op = ArmOp::Ldrsh {
10880            rd: Reg::R0,
10881            addr: MemAddr::imm(Reg::R1, 0),
10882        };
10883        let code = encoder.encode(&op).unwrap();
10884        assert_eq!(code.len(), 4, "Thumb-2 LDRSH should be 32-bit");
10885    }
10886
10887    #[test]
10888    fn test_encode_memory_size_thumb2() {
10889        let encoder = ArmEncoder::new_thumb2();
10890        let op = ArmOp::MemorySize { rd: Reg::R0 };
10891        let code = encoder.encode(&op).unwrap();
10892        // R0 and R10 are not both low registers, so this needs careful handling
10893        assert!(!code.is_empty(), "MemorySize should produce code");
10894    }
10895
10896    #[test]
10897    fn test_encode_memory_grow_thumb2() {
10898        let encoder = ArmEncoder::new_thumb2();
10899        let op = ArmOp::MemoryGrow {
10900            rd: Reg::R0,
10901            rn: Reg::R0,
10902        };
10903        let code = encoder.encode(&op).unwrap();
10904        assert_eq!(code.len(), 4, "MemoryGrow (MVN) should be 32-bit Thumb-2");
10905    }
10906
10907    #[test]
10908    fn test_encode_subword_reg_offset_thumb2() {
10909        let encoder = ArmEncoder::new_thumb2();
10910
10911        // LDRB with register offset
10912        let op = ArmOp::Ldrb {
10913            rd: Reg::R0,
10914            addr: MemAddr::reg(Reg::R1, Reg::R2),
10915        };
10916        let code = encoder.encode(&op).unwrap();
10917        assert_eq!(
10918            code.len(),
10919            4,
10920            "Thumb-2 LDRB with reg offset should be 32-bit"
10921        );
10922
10923        // STRB with register offset
10924        let op = ArmOp::Strb {
10925            rd: Reg::R0,
10926            addr: MemAddr::reg(Reg::R1, Reg::R2),
10927        };
10928        let code = encoder.encode(&op).unwrap();
10929        assert_eq!(
10930            code.len(),
10931            4,
10932            "Thumb-2 STRB with reg offset should be 32-bit"
10933        );
10934
10935        // LDRH with register offset
10936        let op = ArmOp::Ldrh {
10937            rd: Reg::R0,
10938            addr: MemAddr::reg(Reg::R1, Reg::R2),
10939        };
10940        let code = encoder.encode(&op).unwrap();
10941        assert_eq!(
10942            code.len(),
10943            4,
10944            "Thumb-2 LDRH with reg offset should be 32-bit"
10945        );
10946
10947        // STRH with register offset
10948        let op = ArmOp::Strh {
10949            rd: Reg::R0,
10950            addr: MemAddr::reg(Reg::R1, Reg::R2),
10951        };
10952        let code = encoder.encode(&op).unwrap();
10953        assert_eq!(
10954            code.len(),
10955            4,
10956            "Thumb-2 STRH with reg offset should be 32-bit"
10957        );
10958    }
10959
10960    #[test]
10961    fn test_encode_subword_reg_imm_offset_thumb2() {
10962        let encoder = ArmEncoder::new_thumb2();
10963
10964        // LDRB with both register and immediate offset
10965        let op = ArmOp::Ldrb {
10966            rd: Reg::R0,
10967            addr: MemAddr::reg_imm(Reg::R1, Reg::R2, 4),
10968        };
10969        let code = encoder.encode(&op).unwrap();
10970        // ADD R12, R2, #4 (4 bytes) + LDRB R0, [R1, R12] (4 bytes) = 8 bytes
10971        assert_eq!(
10972            code.len(),
10973            8,
10974            "Thumb-2 LDRB with reg+imm offset should be 8 bytes"
10975        );
10976    }
10977
10978    // ========================================================================
10979    // Helium MVE encoding tests
10980    // ========================================================================
10981
10982    #[test]
10983    fn test_encode_mve_addi32_thumb2() {
10984        let encoder = ArmEncoder::new_thumb2();
10985        let op = ArmOp::MveAddI {
10986            qd: QReg::Q0,
10987            qn: QReg::Q1,
10988            qm: QReg::Q2,
10989            size: MveSize::S32,
10990        };
10991        let code = encoder.encode(&op).unwrap();
10992        assert_eq!(
10993            code.len(),
10994            4,
10995            "MVE VADD.I32 should be 4 bytes (Thumb-2 32-bit)"
10996        );
10997    }
10998
10999    #[test]
11000    fn test_encode_mve_subi16_thumb2() {
11001        let encoder = ArmEncoder::new_thumb2();
11002        let op = ArmOp::MveSubI {
11003            qd: QReg::Q0,
11004            qn: QReg::Q1,
11005            qm: QReg::Q2,
11006            size: MveSize::S16,
11007        };
11008        let code = encoder.encode(&op).unwrap();
11009        assert_eq!(code.len(), 4, "MVE VSUB.I16 should be 4 bytes");
11010    }
11011
11012    #[test]
11013    fn test_encode_mve_muli8_thumb2() {
11014        let encoder = ArmEncoder::new_thumb2();
11015        let op = ArmOp::MveMulI {
11016            qd: QReg::Q0,
11017            qn: QReg::Q1,
11018            qm: QReg::Q2,
11019            size: MveSize::S8,
11020        };
11021        let code = encoder.encode(&op).unwrap();
11022        assert_eq!(code.len(), 4, "MVE VMUL.I8 should be 4 bytes");
11023    }
11024
11025    #[test]
11026    fn test_encode_mve_bitwise_thumb2() {
11027        let encoder = ArmEncoder::new_thumb2();
11028
11029        let ops = vec![
11030            ArmOp::MveAnd {
11031                qd: QReg::Q0,
11032                qn: QReg::Q1,
11033                qm: QReg::Q2,
11034            },
11035            ArmOp::MveOrr {
11036                qd: QReg::Q0,
11037                qn: QReg::Q1,
11038                qm: QReg::Q2,
11039            },
11040            ArmOp::MveEor {
11041                qd: QReg::Q0,
11042                qn: QReg::Q1,
11043                qm: QReg::Q2,
11044            },
11045            ArmOp::MveBic {
11046                qd: QReg::Q0,
11047                qn: QReg::Q1,
11048                qm: QReg::Q2,
11049            },
11050        ];
11051        for op in ops {
11052            let code = encoder.encode(&op).unwrap();
11053            assert_eq!(code.len(), 4, "MVE bitwise op should be 4 bytes");
11054        }
11055    }
11056
11057    #[test]
11058    fn test_encode_mve_mvn_thumb2() {
11059        let encoder = ArmEncoder::new_thumb2();
11060        let op = ArmOp::MveMvn {
11061            qd: QReg::Q0,
11062            qm: QReg::Q1,
11063        };
11064        let code = encoder.encode(&op).unwrap();
11065        assert_eq!(code.len(), 4, "MVE VMVN should be 4 bytes");
11066    }
11067
11068    #[test]
11069    fn test_encode_mve_load_store_thumb2() {
11070        let encoder = ArmEncoder::new_thumb2();
11071
11072        let load = ArmOp::MveLoad {
11073            qd: QReg::Q0,
11074            addr: MemAddr::imm(Reg::R0, 16),
11075        };
11076        let code = encoder.encode(&load).unwrap();
11077        assert_eq!(code.len(), 4, "MVE VLDRW.32 should be 4 bytes");
11078
11079        let store = ArmOp::MveStore {
11080            qd: QReg::Q1,
11081            addr: MemAddr::imm(Reg::R1, 0),
11082        };
11083        let code = encoder.encode(&store).unwrap();
11084        assert_eq!(code.len(), 4, "MVE VSTRW.32 should be 4 bytes");
11085    }
11086
11087    #[test]
11088    fn test_encode_mve_const_thumb2() {
11089        let encoder = ArmEncoder::new_thumb2();
11090        let op = ArmOp::MveConst {
11091            qd: QReg::Q0,
11092            bytes: [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0],
11093        };
11094        let code = encoder.encode(&op).unwrap();
11095        // Should be 4 words of (MOVW R12 + VMOV Sn) = 4 * (4+4) = 32 bytes min
11096        // Some words with hi16=0 skip MOVT, so length varies
11097        assert!(
11098            code.len() >= 24,
11099            "MVE const should produce multiple instructions"
11100        );
11101    }
11102
11103    #[test]
11104    fn test_encode_mve_dup_thumb2() {
11105        let encoder = ArmEncoder::new_thumb2();
11106        let op = ArmOp::MveDup {
11107            qd: QReg::Q0,
11108            rn: Reg::R0,
11109            size: MveSize::S32,
11110        };
11111        let code = encoder.encode(&op).unwrap();
11112        assert_eq!(code.len(), 4, "MVE VDUP.32 should be 4 bytes");
11113    }
11114
11115    #[test]
11116    fn test_encode_mve_extract_lane_thumb2() {
11117        let encoder = ArmEncoder::new_thumb2();
11118        let op = ArmOp::MveExtractLane {
11119            rd: Reg::R0,
11120            qn: QReg::Q1,
11121            lane: 2,
11122            size: MveSize::S32,
11123        };
11124        let code = encoder.encode(&op).unwrap();
11125        assert_eq!(code.len(), 4, "MVE extract lane should be 4 bytes");
11126    }
11127
11128    #[test]
11129    fn test_encode_mve_insert_lane_thumb2() {
11130        let encoder = ArmEncoder::new_thumb2();
11131        let op = ArmOp::MveInsertLane {
11132            qd: QReg::Q0,
11133            rn: Reg::R1,
11134            lane: 3,
11135            size: MveSize::S32,
11136        };
11137        let code = encoder.encode(&op).unwrap();
11138        assert_eq!(code.len(), 4, "MVE insert lane should be 4 bytes");
11139    }
11140
11141    #[test]
11142    fn test_encode_mve_addf32_thumb2() {
11143        let encoder = ArmEncoder::new_thumb2();
11144        let op = ArmOp::MveAddF32 {
11145            qd: QReg::Q0,
11146            qn: QReg::Q1,
11147            qm: QReg::Q2,
11148        };
11149        let code = encoder.encode(&op).unwrap();
11150        assert_eq!(code.len(), 4, "MVE VADD.F32 should be 4 bytes");
11151    }
11152
11153    #[test]
11154    fn test_encode_mve_divf32_thumb2() {
11155        let encoder = ArmEncoder::new_thumb2();
11156        let op = ArmOp::MveDivF32 {
11157            qd: QReg::Q0,
11158            qn: QReg::Q1,
11159            qm: QReg::Q2,
11160        };
11161        let code = encoder.encode(&op).unwrap();
11162        // Lane-wise: 4 x VDIV.F32 = 4 x 4 = 16 bytes
11163        assert_eq!(
11164            code.len(),
11165            16,
11166            "MVE VDIV.F32 (lane-wise) should be 16 bytes"
11167        );
11168    }
11169
11170    #[test]
11171    fn test_encode_mve_sqrtf32_thumb2() {
11172        let encoder = ArmEncoder::new_thumb2();
11173        let op = ArmOp::MveSqrtF32 {
11174            qd: QReg::Q0,
11175            qm: QReg::Q1,
11176        };
11177        let code = encoder.encode(&op).unwrap();
11178        // Lane-wise: 4 x VSQRT.F32 = 4 x 4 = 16 bytes
11179        assert_eq!(
11180            code.len(),
11181            16,
11182            "MVE VSQRT.F32 (lane-wise) should be 16 bytes"
11183        );
11184    }
11185
11186    #[test]
11187    fn test_encode_mve_negf32_thumb2() {
11188        let encoder = ArmEncoder::new_thumb2();
11189        let op = ArmOp::MveNegF32 {
11190            qd: QReg::Q0,
11191            qm: QReg::Q1,
11192        };
11193        let code = encoder.encode(&op).unwrap();
11194        assert_eq!(code.len(), 4, "MVE VNEG.F32 should be 4 bytes");
11195    }
11196
11197    #[test]
11198    fn test_encode_mve_absf32_thumb2() {
11199        let encoder = ArmEncoder::new_thumb2();
11200        let op = ArmOp::MveAbsF32 {
11201            qd: QReg::Q0,
11202            qm: QReg::Q1,
11203        };
11204        let code = encoder.encode(&op).unwrap();
11205        assert_eq!(code.len(), 4, "MVE VABS.F32 should be 4 bytes");
11206    }
11207
11208    /// VCR-RA-001 / immediate-folding precondition: pins the Thumb-2 `AND`
11209    /// immediate encoding for the byte range and documents its bound.
11210    ///
11211    /// The `And { Operand2::Imm }` encoder packs the low 12 bits straight into
11212    /// the `i:imm3:imm8` field WITHOUT applying ThumbExpandImm (the modified-
11213    /// immediate expansion). For `imm <= 0xFF` (e.g. gale's int8 clamps
11214    /// `#0x7e` / `#0x7f`) that is correct — `i:imm3 = 0000` means "imm8
11215    /// zero-extended". So `and r2, r0, #0x7e` encodes to the canonical
11216    /// `00 f0 7e 02`. For `imm >= 0x100` the field would need a true
11217    /// ThumbExpandImm pattern (rotation / replication), which is NOT
11218    /// implemented here — so **immediate folding must gate on `imm <= 0xFF`**
11219    /// until the encoder is hardened to ThumbExpandImm/Ok-or-Err (the
11220    /// "encoder must be Ok-or-Err, never silently wrong" principle, #180/#185).
11221    /// This bound covers the measured `flat_flight` waste (#209).
11222    #[test]
11223    fn and_immediate_encodes_correctly_in_byte_range_documents_fold_bound() {
11224        let encoder = ArmEncoder::new_thumb2();
11225        let op = ArmOp::And {
11226            rd: Reg::R2,
11227            rn: Reg::R0,
11228            op2: Operand2::Imm(0x7e),
11229        };
11230        let code = encoder.encode(&op).unwrap();
11231        assert_eq!(
11232            code,
11233            vec![0x00, 0xf0, 0x7e, 0x02],
11234            "and r2, r0, #0x7e must encode to the canonical AND.W T1 (imm8=0x7e)"
11235        );
11236    }
11237
11238    /// #255: the shared ThumbExpandImm reverse-encoder underpinning the
11239    /// data-processing immediate fix. Encodable modified immediates round-trip to
11240    /// the expected `i:imm3:imm8` field; a genuinely non-modified value is `None`
11241    /// (caller must materialize into a register). Note `1000 = 0xFA ror 30` *is*
11242    /// representable (field 0xF7A) — the old encoder mis-encoded it (raw 0x3E8);
11243    /// this encodes it correctly.
11244    #[test]
11245    fn try_thumb_expand_imm_encodes_modified_immediates() {
11246        assert_eq!(try_thumb_expand_imm(0x7e), Some(0x07e)); // zero-extended byte
11247        assert_eq!(try_thumb_expand_imm(0xff), Some(0x0ff));
11248        assert_eq!(try_thumb_expand_imm(0x0001_0001), Some(0x101)); // 0x00XY00XY
11249        assert_eq!(try_thumb_expand_imm(0xff00_ff00), Some(0x2ff)); // 0xXY00XY00
11250        assert_eq!(try_thumb_expand_imm(0xffff_ffff), Some(0x3ff)); // 0xXYXYXYXY
11251        assert_eq!(try_thumb_expand_imm(0x100), Some(0xf80)); // 0x80 ror 31
11252        assert_eq!(try_thumb_expand_imm(0x8000_0000), Some(0x400)); // 0x80 ror 8
11253        assert_eq!(try_thumb_expand_imm(1000), Some(0xf7a)); // 0xFA ror 30
11254        // Genuinely unrepresentable (bits too far apart for an 8-bit window).
11255        assert_eq!(try_thumb_expand_imm(0x101), None);
11256        assert_eq!(try_thumb_expand_imm(0x12345), None);
11257    }
11258
11259    /// #255: CMP/ADDS/SUBS encode any valid modified immediate correctly, and
11260    /// ERROR (not silently mis-encode) on a genuinely unrepresentable one,
11261    /// forcing the selector to materialize into a register — closing the
11262    /// silent-miscompile class of #251/#253.
11263    #[test]
11264    fn cmp_adds_subs_immediate_error_on_non_modified_imm() {
11265        let encoder = ArmEncoder::new_thumb2();
11266        // cmp r0, #0xff → valid → Ok; cmp r0, #1000 → valid (0xFA ror 30) → Ok.
11267        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 0xff).is_ok());
11268        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 1000).is_ok());
11269        // cmp r0, #0x101 → NOT a modified immediate → Err (materialize-reg).
11270        assert!(
11271            encoder.encode_thumb32_cmp_imm(&Reg::R0, 0x101).is_err(),
11272            "cmp #0x101 must error, not compare the wrong constant"
11273        );
11274        assert!(
11275            encoder
11276                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x101)
11277                .is_err()
11278        );
11279        assert!(
11280            encoder
11281                .encode_thumb32_subs(&Reg::R0, &Reg::R0, 0x101)
11282                .is_err()
11283        );
11284        // ...but a valid modified immediate still encodes.
11285        assert!(
11286            encoder
11287                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x80)
11288                .is_ok()
11289        );
11290    }
11291
11292    /// #257: MLA (multiply-accumulate) encodes as MLS without the bit-4 op flag.
11293    /// `mla r2, r3, r4, r8` (rd=r2, rn=r3, rm=r4, ra=r8) → Thumb-2 `03 fb 04 82`.
11294    #[test]
11295    fn mla_thumb2_encodes_correctly() {
11296        let encoder = ArmEncoder::new_thumb2();
11297        let code = encoder
11298            .encode(&ArmOp::Mla {
11299                rd: Reg::R2,
11300                rn: Reg::R3,
11301                rm: Reg::R4,
11302                ra: Reg::R8,
11303            })
11304            .unwrap();
11305        // hw1 = 0xFB03, hw2 = (8<<12)|(2<<8)|4 = 0x8204
11306        assert_eq!(code, vec![0x03, 0xfb, 0x04, 0x82]);
11307    }
11308
11309    /// #259: LDR/STR (and sub-word) immediate-offset encoders truncated
11310    /// `offset & 0xFFF`, silently targeting the wrong address for offset >= 4096.
11311    /// They now error (the selector must use register-offset addressing) — the
11312    /// load/store sibling of the #253/#255 class. Offsets <= 4095 still encode.
11313    #[test]
11314    fn ldst_imm12_offset_errors_when_out_of_range() {
11315        let encoder = ArmEncoder::new_thumb2();
11316        // offset 0xFFF (4095): valid → Ok; ldr r0, [r1, #4095].
11317        assert!(
11318            encoder
11319                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0xFFF)
11320                .is_ok()
11321        );
11322        // offset 0x1000 (4096): out of imm12 range → Err (not & 0xFFF → #0).
11323        assert!(
11324            encoder
11325                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0x1000)
11326                .is_err(),
11327            "ldr offset 4096 must error, not wrap to 0"
11328        );
11329        assert!(
11330            encoder
11331                .encode_thumb32_str(&Reg::R0, &Reg::R1, 0x1000)
11332                .is_err()
11333        );
11334        assert!(
11335            encoder
11336                .encode_thumb32_ldrb_imm(&Reg::R0, &Reg::R1, 5000)
11337                .is_err()
11338        );
11339        assert!(
11340            encoder
11341                .encode_thumb32_strh_imm(&Reg::R0, &Reg::R1, 5000)
11342                .is_err()
11343        );
11344    }
11345
11346    /// Latent miscompile fix: ADD/SUB with a >0xFF immediate (e.g.
11347    /// `add sp, sp, #frame` for a >=256-byte frame) used ADD.W (T3), whose
11348    /// `i:imm3:imm8` is a ThumbExpandImm modified immediate — so `#256` silently
11349    /// encoded as `#0` (stack corruption). Use ADDW/SUBW (T4), a PLAIN 12-bit
11350    /// immediate, for 0x100..=0xFFF; keep T3 for <=0xFF (bit-identical); error
11351    /// beyond 4095.
11352    #[test]
11353    fn add_sub_large_immediate_use_addw_subw_not_misencoded() {
11354        let encoder = ArmEncoder::new_thumb2();
11355        // add sp, sp, #256  →  ADDW (T4) SP, SP, #256  =  0d f2 00 1d
11356        assert_eq!(
11357            encoder
11358                .encode(&ArmOp::Add {
11359                    rd: Reg::SP,
11360                    rn: Reg::SP,
11361                    op2: Operand2::Imm(256),
11362                })
11363                .unwrap(),
11364            vec![0x0d, 0xf2, 0x00, 0x1d],
11365            "add sp,sp,#256 must be ADDW (plain imm12), not a mis-encoded ADD.W"
11366        );
11367        // sub sp, sp, #256  →  SUBW (T4) SP, SP, #256  =  ad f2 00 1d
11368        assert_eq!(
11369            encoder
11370                .encode(&ArmOp::Sub {
11371                    rd: Reg::SP,
11372                    rn: Reg::SP,
11373                    op2: Operand2::Imm(256),
11374                })
11375                .unwrap(),
11376            vec![0xad, 0xf2, 0x00, 0x1d],
11377        );
11378        // > 4095 has no single-instruction encoding → error, not silent wrong.
11379        assert!(
11380            encoder
11381                .encode(&ArmOp::Add {
11382                    rd: Reg::SP,
11383                    rn: Reg::SP,
11384                    op2: Operand2::Imm(5000),
11385                })
11386                .is_err(),
11387            "add #5000 must error (no single ADDW), not mis-encode"
11388        );
11389    }
11390
11391    /// Closes the data-proc immediate class: AND and CMN now go through
11392    /// `try_thumb_expand_imm` like ORR/EOR/CMP — correct for any modified
11393    /// immediate, `Err` (not raw-pack / NOP) on an un-encodable one. The byte
11394    /// range stays bit-identical (`and r2,r0,#0x7e` is unchanged).
11395    #[test]
11396    fn and_cmn_immediate_thumb_expand_else_error() {
11397        let encoder = ArmEncoder::new_thumb2();
11398        // byte range unchanged (bit-identical with the pre-retrofit encoding)
11399        assert_eq!(
11400            encoder
11401                .encode(&ArmOp::And {
11402                    rd: Reg::R2,
11403                    rn: Reg::R0,
11404                    op2: Operand2::Imm(0x7e),
11405                })
11406                .unwrap(),
11407            vec![0x00, 0xf0, 0x7e, 0x02],
11408        );
11409        // a valid replicated modified immediate now encodes (was silently wrong)
11410        assert!(
11411            encoder
11412                .encode(&ArmOp::And {
11413                    rd: Reg::R2,
11414                    rn: Reg::R0,
11415                    op2: Operand2::Imm(0xff00ff00u32 as i32),
11416                })
11417                .is_ok()
11418        );
11419        // a genuinely un-encodable immediate errors (AND was raw-pack; CMN NOP)
11420        assert!(
11421            encoder
11422                .encode(&ArmOp::And {
11423                    rd: Reg::R2,
11424                    rn: Reg::R0,
11425                    op2: Operand2::Imm(0x101),
11426                })
11427                .is_err()
11428        );
11429        assert!(
11430            encoder
11431                .encode(&ArmOp::Cmn {
11432                    rn: Reg::R0,
11433                    op2: Operand2::Imm(0x101),
11434                })
11435                .is_err(),
11436            "CMN #0x101 must error, not emit a NOP"
11437        );
11438    }
11439
11440    /// VCR-RA-001: ORR/EOR with a small immediate must encode the real
11441    /// instruction (not a silent `0xBF00` NOP). Pins the byte range and the
11442    /// Ok-or-Err bound that makes future Or/Eor immediate folding safe.
11443    #[test]
11444    fn orr_eor_immediate_encode_in_byte_range_else_error() {
11445        let encoder = ArmEncoder::new_thumb2();
11446        // orr r2, r0, #0x7e  →  ORR.W T1, imm8=0x7e
11447        assert_eq!(
11448            encoder
11449                .encode(&ArmOp::Orr {
11450                    rd: Reg::R2,
11451                    rn: Reg::R0,
11452                    op2: Operand2::Imm(0x7e),
11453                })
11454                .unwrap(),
11455            vec![0x40, 0xf0, 0x7e, 0x02],
11456        );
11457        // eor r2, r0, #0x7e  →  EOR.W T1, imm8=0x7e
11458        assert_eq!(
11459            encoder
11460                .encode(&ArmOp::Eor {
11461                    rd: Reg::R2,
11462                    rn: Reg::R0,
11463                    op2: Operand2::Imm(0x7e),
11464                })
11465                .unwrap(),
11466            vec![0x80, 0xf0, 0x7e, 0x02],
11467        );
11468        // Out-of-range immediates error rather than silently mis-encode / NOP.
11469        assert!(
11470            encoder
11471                .encode(&ArmOp::Orr {
11472                    rd: Reg::R2,
11473                    rn: Reg::R0,
11474                    op2: Operand2::Imm(0x140),
11475                })
11476                .is_err(),
11477            "ORR #0x140 must error, not emit a NOP"
11478        );
11479    }
11480
11481    #[test]
11482    fn test_encode_mve_different_qregs() {
11483        let encoder = ArmEncoder::new_thumb2();
11484
11485        // Test that different Q-register numbers produce different encodings
11486        let op1 = ArmOp::MveAddI {
11487            qd: QReg::Q0,
11488            qn: QReg::Q0,
11489            qm: QReg::Q0,
11490            size: MveSize::S32,
11491        };
11492        let op2 = ArmOp::MveAddI {
11493            qd: QReg::Q3,
11494            qn: QReg::Q5,
11495            qm: QReg::Q7,
11496            size: MveSize::S32,
11497        };
11498        let code1 = encoder.encode(&op1).unwrap();
11499        let code2 = encoder.encode(&op2).unwrap();
11500        assert_ne!(
11501            code1, code2,
11502            "Different Q-registers should produce different encodings"
11503        );
11504    }
11505
11506    #[test]
11507    fn test_encode_mve_arm32_loud_err() {
11508        // #615: MVE (Helium) is Thumb-2-only. The ARM32 encoder used to emit
11509        // a silent NOP here (dropping the vector op); it must now be a typed
11510        // Err so a broken "MVE implies Thumb" invariant fails loudly.
11511        let encoder = ArmEncoder::new_arm32();
11512        let op = ArmOp::MveAddI {
11513            qd: QReg::Q0,
11514            qn: QReg::Q1,
11515            qm: QReg::Q2,
11516            size: MveSize::S32,
11517        };
11518        let err = encoder
11519            .encode(&op)
11520            .expect_err("ARM32 MVE must be a loud Err, not a silent NOP (#615)");
11521        assert!(
11522            err.to_string().contains("Thumb-2 only"),
11523            "unexpected error message: {err}"
11524        );
11525    }
11526}