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                w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
978                w(&mut b, 0xE021_9003); // EOR R9, R1, R3 (result sign in MSB)
979                skip_negate_if_positive(&mut b, 1);
980                negate64(&mut b, 0, 1);
981                skip_negate_if_positive(&mut b, 3);
982                negate64(&mut b, 2, 3);
983                for r in 4..8u32 {
984                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
985                }
986                div_loop(&mut b, 8); // counter in R8 (saved above)
987                w(&mut b, 0xE1A0_0004); // MOV R0, R4
988                w(&mut b, 0xE1A0_1005); // MOV R1, R5
989                skip_negate_if_positive(&mut b, 9);
990                negate64(&mut b, 0, 1);
991                w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
992                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
993            }
994
995            // I64RemU: same core as I64DivU, returns the remainder (R6:R7).
996            ArmOp::I64RemU {
997                rdlo,
998                rdhi,
999                rnlo,
1000                rnhi,
1001                rmlo,
1002                rmhi,
1003            } => {
1004                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1005                emit_a32_i64_divisor_zero_trap(&mut b);
1006                w(&mut b, 0xE92D_01F0); // PUSH {R4-R8}
1007                for r in 4..8u32 {
1008                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1009                }
1010                div_loop(&mut b, 8);
1011                w(&mut b, 0xE1A0_0006); // MOV R0, R6 (remainder lo)
1012                w(&mut b, 0xE1A0_1007); // MOV R1, R7 (remainder hi)
1013                w(&mut b, 0xE8BD_01F0); // POP {R4-R8}
1014                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1015            }
1016
1017            // I64RemS: remainder takes the DIVIDEND's sign (WASM semantics).
1018            ArmOp::I64RemS {
1019                rdlo,
1020                rdhi,
1021                rnlo,
1022                rnhi,
1023                rmlo,
1024                rmhi,
1025            } => {
1026                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1027                emit_a32_i64_divisor_zero_trap(&mut b);
1028                w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1029                w(&mut b, 0xE1A0_9001); // MOV R9, R1 (dividend sign)
1030                skip_negate_if_positive(&mut b, 1);
1031                negate64(&mut b, 0, 1);
1032                skip_negate_if_positive(&mut b, 3);
1033                negate64(&mut b, 2, 3);
1034                for r in 4..8u32 {
1035                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1036                }
1037                div_loop(&mut b, 8);
1038                w(&mut b, 0xE1A0_0006); // MOV R0, R6
1039                w(&mut b, 0xE1A0_1007); // MOV R1, R7
1040                skip_negate_if_positive(&mut b, 9);
1041                negate64(&mut b, 0, 1);
1042                w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1043                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1044            }
1045
1046            // Popcnt (i32): bit-twiddle expansion (no native A32 popcount),
1047            // mirroring the Thumb-2 arm's register contract (R11 + R12 as
1048            // scratch, shift-add fold, final AND #0x3F).
1049            ArmOp::Popcnt { rd, rm } => {
1050                let rd_b = reg_to_bits(rd);
1051                if rd != rm {
1052                    w(&mut b, 0xE1A0_0000 | (rd_b << 12) | reg_to_bits(rm)); // MOV rd, rm
1053                }
1054                // x = x - ((x >> 1) & 0x55555555)
1055                movw(&mut b, 12, 0x5555);
1056                movt(&mut b, 12, 0x5555);
1057                shift_imm(&mut b, LSR, 11, rd_b, 1);
1058                dp_reg(&mut b, 0xE000_0000, 11, 11, 12); // AND R11, R11, R12
1059                dp_reg(&mut b, 0xE040_0000, rd_b, rd_b, 11); // SUB rd, rd, R11
1060                // x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
1061                movw(&mut b, 12, 0x3333);
1062                movt(&mut b, 12, 0x3333);
1063                dp_reg(&mut b, 0xE000_0000, 11, rd_b, 12); // AND R11, rd, R12
1064                shift_imm(&mut b, LSR, rd_b, rd_b, 2);
1065                dp_reg(&mut b, 0xE000_0000, rd_b, rd_b, 12); // AND rd, rd, R12
1066                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11); // ADD rd, rd, R11
1067                // x = (x + (x >> 4)) & 0x0F0F0F0F
1068                shift_imm(&mut b, LSR, 11, rd_b, 4);
1069                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11); // ADD rd, rd, R11
1070                movw(&mut b, 12, 0x0F0F);
1071                movt(&mut b, 12, 0x0F0F);
1072                dp_reg(&mut b, 0xE000_0000, rd_b, rd_b, 12); // AND rd, rd, R12
1073                // x += x >> 8; x += x >> 16; x &= 0x3F
1074                shift_imm(&mut b, LSR, 11, rd_b, 8);
1075                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11);
1076                shift_imm(&mut b, LSR, 11, rd_b, 16);
1077                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11);
1078                w(&mut b, 0xE200_003F | (rd_b << 16) | (rd_b << 12)); // AND rd, rd, #63
1079            }
1080
1081            // I64Popcnt: POPCNT(lo) + POPCNT(hi) — A32 transcription of the
1082            // Thumb-2 arm (R3/R4/R5 saved, mul-based per-word fold, high
1083            // result word rnhi cleared last, mirroring the Thumb contract).
1084            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
1085                let hi = reg_to_bits(rnhi);
1086                w(&mut b, 0xE92D_0038); // PUSH {R3, R4, R5}
1087                w(&mut b, 0xE1A0_4000 | reg_to_bits(rnlo)); // MOV R4, rnlo
1088                w(&mut b, 0xE1A0_5000 | hi); //                MOV R5, rnhi
1089                popcnt_word(&mut b, 4, 3);
1090                popcnt_word(&mut b, 5, 3);
1091                dp_reg(&mut b, 0xE080_0000, reg_to_bits(rd), 4, 5); // ADD rd, R4, R5
1092                w(&mut b, 0xE8BD_0038); // POP {R3, R4, R5}
1093                w(&mut b, 0xE3A0_0000 | (hi << 12)); // MOV rnhi, #0 (i64 hi word)
1094            }
1095
1096            _ => return Ok(None),
1097        }
1098        Ok(Some(b))
1099    }
1100
1101    fn encode_arm(&self, op: &ArmOp) -> Result<Vec<u8>> {
1102        // #615: A32 multi-instruction expansions (i64 arithmetic/shift/rotate/
1103        // compare, SetCond/SelectMove, popcnt, ...). These ops were literal
1104        // NOPs on the A32 path — user-reachable via `--target cortex-r5` —
1105        // so the value silently vanished. Mirror of the #594 CallIndirect
1106        // early-return: if the expansion helper covers the op, its bytes are
1107        // the encoding.
1108        if let Some(bytes) = self.encode_arm_expanded(op)? {
1109            return Ok(bytes);
1110        }
1111        // #206: ARM32 register-offset loads/stores. `encode_mem_addr` only
1112        // returns the 12-bit immediate, so the immediate-form arms below
1113        // silently DROP `addr.offset_reg` — a runtime address index vanished,
1114        // turning `ldr rd,[rn,rm,#off]` into `ldr rd,[rn,#off]` (the access went
1115        // to the wrong address). Compute the effective base into IP and re-encode
1116        // against `[ip, #off]`, which is uniform for word/byte/halfword/signed.
1117        if let Some(bytes) = self.encode_arm_reg_offset_mem(op)? {
1118            return Ok(bytes);
1119        }
1120        // #594: call_indirect was encoded as a literal NOP on the A32 path
1121        // (`--target cortex-r5`) — the call never happened and the function
1122        // silently returned garbage. Emit the same three-instruction expansion
1123        // as the Thumb-2 path (R11 = function-pointer table base, R12 scratch):
1124        //   MOV r12, idx, LSL #2 ; LDR r12, [r11, r12] ; BLX r12
1125        if let ArmOp::CallIndirect {
1126            table_index_reg, ..
1127        } = op
1128        {
1129            return Ok(Self::encode_arm_call_indirect(table_index_reg));
1130        }
1131        let instr: u32 = match op {
1132            // Data processing instructions
1133            ArmOp::Add { rd, rn, op2 } => {
1134                let rd_bits = reg_to_bits(rd);
1135                let rn_bits = reg_to_bits(rn);
1136                let (op2_bits, i_flag) = encode_operand2(op2)?;
1137
1138                // ADD encoding: cond(4) | 00 | I(1) | 0100 | S(1) | Rn(4) | Rd(4) | operand2(12)
1139                0xE0800000 // condition=always(E), opcode=ADD(0100), S=0
1140                    | (i_flag << 25)
1141                    | (rn_bits << 16)
1142                    | (rd_bits << 12)
1143                    | op2_bits
1144            }
1145
1146            ArmOp::Sub { rd, rn, op2 } => {
1147                let rd_bits = reg_to_bits(rd);
1148                let rn_bits = reg_to_bits(rn);
1149                let (op2_bits, i_flag) = encode_operand2(op2)?;
1150
1151                // SUB encoding: opcode=0010
1152                0xE0400000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1153            }
1154
1155            // i64 support: ADDS, ADC, SUBS, SBC for ARM32
1156            ArmOp::Adds { rd, rn, op2 } => {
1157                let rd_bits = reg_to_bits(rd);
1158                let rn_bits = reg_to_bits(rn);
1159                let (op2_bits, i_flag) = encode_operand2(op2)?;
1160
1161                // ADDS encoding: opcode=0100, S=1
1162                0xE0900000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1163            }
1164
1165            ArmOp::Adc { rd, rn, op2 } => {
1166                let rd_bits = reg_to_bits(rd);
1167                let rn_bits = reg_to_bits(rn);
1168                let (op2_bits, i_flag) = encode_operand2(op2)?;
1169
1170                // ADC encoding: opcode=0101
1171                0xE0A00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1172            }
1173
1174            ArmOp::Subs { rd, rn, op2 } => {
1175                let rd_bits = reg_to_bits(rd);
1176                let rn_bits = reg_to_bits(rn);
1177                let (op2_bits, i_flag) = encode_operand2(op2)?;
1178
1179                // SUBS encoding: opcode=0010, S=1
1180                0xE0500000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1181            }
1182
1183            ArmOp::Sbc { rd, rn, op2 } => {
1184                let rd_bits = reg_to_bits(rd);
1185                let rn_bits = reg_to_bits(rn);
1186                let (op2_bits, i_flag) = encode_operand2(op2)?;
1187
1188                // SBC encoding: opcode=0110
1189                0xE0C00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1190            }
1191
1192            ArmOp::Mul { rd, rn, rm } => {
1193                let rd_bits = reg_to_bits(rd);
1194                let rn_bits = reg_to_bits(rn);
1195                let rm_bits = reg_to_bits(rm);
1196
1197                // MUL encoding: cond(4) | 000000 | A(1) | S(1) | Rd(4) | Rn(4) | Rs(4) | 1001 | Rm(4)
1198                0xE0000090 | (rd_bits << 16) | (rn_bits << 8) | rm_bits
1199            }
1200
1201            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
1202                let rdlo_bits = reg_to_bits(rdlo);
1203                let rdhi_bits = reg_to_bits(rdhi);
1204                let rn_bits = reg_to_bits(rn);
1205                let rm_bits = reg_to_bits(rm);
1206
1207                // UMULL encoding: cond(4) | 0000 1000 | RdHi(4) | RdLo(4) | Rm(4) | 1001 | Rn(4)
1208                0xE0800090 | (rdhi_bits << 16) | (rdlo_bits << 12) | (rm_bits << 8) | rn_bits
1209            }
1210
1211            ArmOp::Sdiv { rd, rn, rm } => {
1212                let rd_bits = reg_to_bits(rd);
1213                let rn_bits = reg_to_bits(rn);
1214                let rm_bits = reg_to_bits(rm);
1215
1216                // SDIV encoding: cond(4) | 01110001 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1217                // ARMv7-M and above
1218                0xE710F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1219            }
1220
1221            ArmOp::Udiv { rd, rn, rm } => {
1222                let rd_bits = reg_to_bits(rd);
1223                let rn_bits = reg_to_bits(rn);
1224                let rm_bits = reg_to_bits(rm);
1225
1226                // UDIV encoding: cond(4) | 01110011 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1227                // ARMv7-M and above
1228                0xE730F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1229            }
1230
1231            ArmOp::Mls { rd, rn, rm, ra } => {
1232                let rd_bits = reg_to_bits(rd);
1233                let rn_bits = reg_to_bits(rn);
1234                let rm_bits = reg_to_bits(rm);
1235                let ra_bits = reg_to_bits(ra);
1236
1237                // MLS encoding: cond(4) | 00000110 | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1238                // Rd = Ra - (Rn * Rm)
1239                0xE0600090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1240            }
1241
1242            ArmOp::Mla { rd, rn, rm, ra } => {
1243                let rd_bits = reg_to_bits(rd);
1244                let rn_bits = reg_to_bits(rn);
1245                let rm_bits = reg_to_bits(rm);
1246                let ra_bits = reg_to_bits(ra);
1247
1248                // MLA encoding: cond(4) | 0000001 S | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1249                // Rd = Ra + (Rn * Rm). Base 0xE0200090 (S=0).
1250                0xE0200090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1251            }
1252
1253            ArmOp::And { rd, rn, op2 } => {
1254                let rd_bits = reg_to_bits(rd);
1255                let rn_bits = reg_to_bits(rn);
1256                let (op2_bits, i_flag) = encode_operand2(op2)?;
1257
1258                // AND encoding: opcode=0000
1259                0xE0000000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1260            }
1261
1262            ArmOp::Orr { rd, rn, op2 } => {
1263                let rd_bits = reg_to_bits(rd);
1264                let rn_bits = reg_to_bits(rn);
1265                let (op2_bits, i_flag) = encode_operand2(op2)?;
1266
1267                // ORR encoding: opcode=1100
1268                0xE1800000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1269            }
1270
1271            ArmOp::Eor { rd, rn, op2 } => {
1272                let rd_bits = reg_to_bits(rd);
1273                let rn_bits = reg_to_bits(rn);
1274                let (op2_bits, i_flag) = encode_operand2(op2)?;
1275
1276                // EOR encoding: opcode=0001
1277                0xE0200000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1278            }
1279
1280            // Shift instructions
1281            ArmOp::Lsl { rd, rn, shift } => {
1282                let rd_bits = reg_to_bits(rd);
1283                let rn_bits = reg_to_bits(rn);
1284                let shift_bits = *shift & 0x1F;
1285
1286                // LSL encoding: MOV with shift
1287                0xE1A00000 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1288            }
1289
1290            ArmOp::Lsr { rd, rn, shift } => {
1291                let rd_bits = reg_to_bits(rd);
1292                let rn_bits = reg_to_bits(rn);
1293                let shift_bits = *shift & 0x1F;
1294
1295                // LSR encoding
1296                0xE1A00020 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1297            }
1298
1299            ArmOp::Asr { rd, rn, shift } => {
1300                let rd_bits = reg_to_bits(rd);
1301                let rn_bits = reg_to_bits(rn);
1302                let shift_bits = *shift & 0x1F;
1303
1304                // ASR encoding
1305                0xE1A00040 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1306            }
1307
1308            ArmOp::Ror { rd, rn, shift } => {
1309                let rd_bits = reg_to_bits(rd);
1310                let rn_bits = reg_to_bits(rn);
1311                let shift_bits = *shift & 0x1F;
1312
1313                // ROR encoding: MOV with ROR shift
1314                0xE1A00060 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1315            }
1316
1317            // Register-based shifts (ARM32)
1318            // LSL Rd, Rn, Rm: cond 0001101S 0000 Rd Rs 0001 Rn
1319            ArmOp::LslReg { rd, rn, rm } => {
1320                let rd_bits = reg_to_bits(rd);
1321                let rn_bits = reg_to_bits(rn);
1322                let rm_bits = reg_to_bits(rm);
1323                0xE1A00010 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1324            }
1325            ArmOp::LsrReg { rd, rn, rm } => {
1326                let rd_bits = reg_to_bits(rd);
1327                let rn_bits = reg_to_bits(rn);
1328                let rm_bits = reg_to_bits(rm);
1329                0xE1A00030 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1330            }
1331            ArmOp::AsrReg { 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                0xE1A00050 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1336            }
1337            ArmOp::RorReg { 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                0xE1A00070 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1342            }
1343
1344            // RSB (Reverse Subtract): Rd = imm - Rn
1345            ArmOp::Rsb { rd, rn, imm } => {
1346                let rd_bits = reg_to_bits(rd);
1347                let rn_bits = reg_to_bits(rn);
1348                // RSB encoding: cond(4) | 00 1 0011 S | Rn(4) | Rd(4) | imm12
1349                // Opcode for RSB = 0011, I=1 (immediate), S=0
1350                0xE2600000 | (rn_bits << 16) | (rd_bits << 12) | (*imm & 0xFF)
1351            }
1352
1353            // Bit manipulation instructions
1354            ArmOp::Clz { rd, rm } => {
1355                let rd_bits = reg_to_bits(rd);
1356                let rm_bits = reg_to_bits(rm);
1357
1358                // CLZ encoding: cond(4) | 00010110 | 1111 | Rd(4) | 1111 | 0001 | Rm(4)
1359                // ARMv5T and above
1360                0xE16F0F10 | (rd_bits << 12) | rm_bits
1361            }
1362
1363            ArmOp::Rbit { rd, rm } => {
1364                let rd_bits = reg_to_bits(rd);
1365                let rm_bits = reg_to_bits(rm);
1366
1367                // RBIT encoding: cond(4) | 01101111 | 1111 | Rd(4) | 1111 | 0011 | Rm(4)
1368                // ARMv6T2 and above
1369                0xE6FF0F30 | (rd_bits << 12) | rm_bits
1370            }
1371
1372            ArmOp::Sxtb { rd, rm } => {
1373                let rd_bits = reg_to_bits(rd);
1374                let rm_bits = reg_to_bits(rm);
1375
1376                // SXTB encoding: cond(4) | 01101010 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1377                // ARMv6 and above. rotate=00 for no rotation
1378                0xE6AF0070 | (rd_bits << 12) | rm_bits
1379            }
1380
1381            ArmOp::Sxth { rd, rm } => {
1382                let rd_bits = reg_to_bits(rd);
1383                let rm_bits = reg_to_bits(rm);
1384
1385                // SXTH encoding: cond(4) | 01101011 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1386                // ARMv6 and above. rotate=00 for no rotation
1387                0xE6BF0070 | (rd_bits << 12) | rm_bits
1388            }
1389
1390            ArmOp::Uxtb { rd, rm } => {
1391                let rd_bits = reg_to_bits(rd);
1392                let rm_bits = reg_to_bits(rm);
1393                // UXTB encoding: cond | 01101110 1111 Rd rotate 00 0111 Rm (rotate=00)
1394                0xE6EF0070 | (rd_bits << 12) | rm_bits
1395            }
1396
1397            ArmOp::Uxth { rd, rm } => {
1398                let rd_bits = reg_to_bits(rd);
1399                let rm_bits = reg_to_bits(rm);
1400                // UXTH encoding: cond | 01101111 1111 Rd rotate 00 0111 Rm (rotate=00)
1401                0xE6FF0070 | (rd_bits << 12) | rm_bits
1402            }
1403
1404            // Move instructions
1405            ArmOp::Mov { rd, op2 } => {
1406                let rd_bits = reg_to_bits(rd);
1407                let (op2_bits, i_flag) = encode_operand2(op2)?;
1408
1409                // MOV encoding: opcode=1101
1410                0xE1A00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1411            }
1412
1413            ArmOp::Mvn { rd, op2 } => {
1414                let rd_bits = reg_to_bits(rd);
1415                let (op2_bits, i_flag) = encode_operand2(op2)?;
1416
1417                // MVN encoding: opcode=1111
1418                0xE1E00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1419            }
1420
1421            // MOVW - Move Wide (ARM32)
1422            // Encoding: cond(4) | 0011 0000 | imm4(4) | Rd(4) | imm12(12)
1423            ArmOp::Movw { rd, imm16 } => {
1424                let rd_bits = reg_to_bits(rd);
1425                let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1426                let imm12 = (*imm16 as u32) & 0xFFF;
1427                0xE3000000 | (imm4 << 16) | (rd_bits << 12) | imm12
1428            }
1429
1430            // MOVT - Move Top (ARM32)
1431            // Encoding: cond(4) | 0011 0100 | imm4(4) | Rd(4) | imm12(12)
1432            ArmOp::Movt { rd, imm16 } => {
1433                let rd_bits = reg_to_bits(rd);
1434                let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1435                let imm12 = (*imm16 as u32) & 0xFFF;
1436                0xE3400000 | (imm4 << 16) | (rd_bits << 12) | imm12
1437            }
1438
1439            // #237: symbol-relative MOVW/MOVT (ARM mode) — addend in place, the
1440            // backend records the MOVW_ABS/MOVT_ABS relocation against `symbol`.
1441            ArmOp::MovwSym { rd, addend, .. } => {
1442                let rd_bits = reg_to_bits(rd);
1443                let v = (*addend as u32) & 0xffff;
1444                0xE3000000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1445            }
1446            ArmOp::MovtSym { rd, addend, .. } => {
1447                let rd_bits = reg_to_bits(rd);
1448                let v = ((*addend as u32) >> 16) & 0xffff;
1449                0xE3400000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1450            }
1451
1452            // #345: LdrSym is the Thumb-2 literal-pool address load. A32 mode is
1453            // not used for relocatable native-pointer objects; fail loudly rather
1454            // than miscompile if it is ever reached here.
1455            ArmOp::LdrSym { .. } => {
1456                return Err(synth_core::Error::synthesis(
1457                    "LdrSym (literal-pool address load) is Thumb-2-only",
1458                ));
1459            }
1460
1461            // Compare
1462            ArmOp::Cmp { rn, op2 } => {
1463                let rn_bits = reg_to_bits(rn);
1464                let (op2_bits, i_flag) = encode_operand2(op2)?;
1465
1466                // CMP encoding: opcode=1010, S=1
1467                0xE1500000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1468            }
1469
1470            // Compare Negative (CMN) - computes Rn + op2 and sets flags
1471            ArmOp::Cmn { rn, op2 } => {
1472                let rn_bits = reg_to_bits(rn);
1473                let (op2_bits, i_flag) = encode_operand2(op2)?;
1474
1475                // CMN encoding: opcode=1011, S=1
1476                0xE1700000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1477            }
1478
1479            // Load/Store
1480            ArmOp::Ldr { rd, addr } => {
1481                let rd_bits = reg_to_bits(rd);
1482                let (base_bits, offset_bits) = encode_mem_addr(addr);
1483
1484                // LDR encoding: cond(4) | 01 | I(1) | P(1) | U(1) | B(1) | W(1) | L(1) | Rn(4) | Rd(4) | offset(12)
1485                // P=1 (pre-indexed), U=1 (add offset), L=1 (load)
1486                0xE5900000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1487            }
1488
1489            ArmOp::Str { rd, addr } => {
1490                let rd_bits = reg_to_bits(rd);
1491                let (base_bits, offset_bits) = encode_mem_addr(addr);
1492
1493                // STR encoding: L=0 (store)
1494                0xE5800000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1495            }
1496
1497            // Sub-word loads (ARM32 encoding)
1498            ArmOp::Ldrb { rd, addr } => {
1499                let rd_bits = reg_to_bits(rd);
1500                let (base_bits, offset_bits) = encode_mem_addr(addr);
1501                // LDRB: LDR with B=1 (byte): cond|01|I|P|U|1|W|L|Rn|Rd|offset
1502                0xE5D00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1503            }
1504
1505            ArmOp::Ldrsb { rd, addr } => {
1506                let rd_bits = reg_to_bits(rd);
1507                let (base_bits, offset_bits) = encode_mem_addr(addr);
1508                // LDRSB (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1101|imm4L
1509                // Simplified with immediate offset
1510                let offset_val = offset_bits & 0xFF;
1511                let imm4h = (offset_val >> 4) & 0xF;
1512                let imm4l = offset_val & 0xF;
1513                0xE1D000D0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1514            }
1515
1516            ArmOp::Ldrh { rd, addr } => {
1517                let rd_bits = reg_to_bits(rd);
1518                let (base_bits, offset_bits) = encode_mem_addr(addr);
1519                // LDRH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1011|imm4L
1520                let offset_val = offset_bits & 0xFF;
1521                let imm4h = (offset_val >> 4) & 0xF;
1522                let imm4l = offset_val & 0xF;
1523                0xE1D000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1524            }
1525
1526            ArmOp::Ldrsh { rd, addr } => {
1527                let rd_bits = reg_to_bits(rd);
1528                let (base_bits, offset_bits) = encode_mem_addr(addr);
1529                // LDRSH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1111|imm4L
1530                let offset_val = offset_bits & 0xFF;
1531                let imm4h = (offset_val >> 4) & 0xF;
1532                let imm4l = offset_val & 0xF;
1533                0xE1D000F0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1534            }
1535
1536            // Sub-word stores (ARM32 encoding)
1537            ArmOp::Strb { rd, addr } => {
1538                let rd_bits = reg_to_bits(rd);
1539                let (base_bits, offset_bits) = encode_mem_addr(addr);
1540                // STRB: STR with B=1 (byte): cond|01|I|P|U|1|W|0|Rn|Rd|offset
1541                0xE5C00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1542            }
1543
1544            ArmOp::Strh { rd, addr } => {
1545                let rd_bits = reg_to_bits(rd);
1546                let (base_bits, offset_bits) = encode_mem_addr(addr);
1547                // STRH (misc store): cond|000|P|U|1|W|0|Rn|Rd|imm4H|1011|imm4L
1548                let offset_val = offset_bits & 0xFF;
1549                let imm4h = (offset_val >> 4) & 0xF;
1550                let imm4l = offset_val & 0xF;
1551                0xE1C000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1552            }
1553
1554            // Memory management (ARM32 encoding)
1555            ArmOp::MemorySize { rd } => {
1556                let rd_bits = reg_to_bits(rd);
1557                // MOV rd, R10, LSR #16  (memory size in bytes / 65536 = pages)
1558                // cond|000|1101|S|0000|Rd|shift5|type|0|Rm
1559                // LSR #16: shift5=10000, type=01
1560                0xE1A00820 | (rd_bits << 12) | 0x0A // Rm=R10, shift=16, LSR
1561            }
1562
1563            ArmOp::MemoryGrow { rd, .. } => {
1564                let rd_bits = reg_to_bits(rd);
1565                // On embedded, always fail: MOV rd, #-1
1566                0xE3E00000 | (rd_bits << 12) // MVN rd, #0 = MOV rd, #-1
1567            }
1568
1569            // Label pseudo-instruction: emits no machine code
1570            ArmOp::Label { .. } => {
1571                return Ok(Vec::new());
1572            }
1573
1574            // Branch instructions
1575            ArmOp::B { label: _ } => {
1576                // B encoding: cond(4) | 1010 | offset(24)
1577                // Simplified: branch to offset 0 (will be patched by linker/resolver)
1578                0xEA000000
1579            }
1580
1581            // Conditional branch to label (generic)
1582            ArmOp::Bcc { cond, label: _ } => {
1583                use synth_synthesis::Condition;
1584                let cond_bits: u32 = match cond {
1585                    Condition::EQ => 0x0,
1586                    Condition::NE => 0x1,
1587                    Condition::HS => 0x2,
1588                    Condition::LO => 0x3,
1589                    Condition::HI => 0x8,
1590                    Condition::LS => 0x9,
1591                    Condition::GE => 0xA,
1592                    Condition::LT => 0xB,
1593                    Condition::GT => 0xC,
1594                    Condition::LE => 0xD,
1595                };
1596                // B<cond> with offset 0 (will be patched)
1597                (cond_bits << 28) | 0x0A000000
1598            }
1599
1600            // BHS (Branch if Higher or Same) - used for bounds checking
1601            ArmOp::Bhs { label: _ } => {
1602                // BHS encoding: cond(2=HS) | 1010 | offset(24)
1603                0x2A000000 // BHS with offset 0
1604            }
1605
1606            // BLO (Branch if Lower) - complementary to BHS
1607            ArmOp::Blo { label: _ } => {
1608                // BLO encoding: cond(3=LO) | 1010 | offset(24)
1609                0x3A000000 // BLO with offset 0
1610            }
1611
1612            // Branch with numeric offset (in instructions)
1613            // ARM32 B instruction: offset is in instructions, stored as words
1614            // The offset is relative to PC+8 (due to ARM pipeline)
1615            ArmOp::BOffset { offset } => {
1616                // B encoding: cond(4) | 1010 | offset(24)
1617                // Offset is signed, in words (4-byte units)
1618                // ARM adds PC+8 to the offset, so we need to adjust:
1619                // target = PC + 8 + (offset * 4)
1620                // For backward branch of N instructions: offset = -(N + 2)
1621                // wrapping_sub keeps the encoder total under fuzzing (#186): an
1622                // extreme i32::MIN offset would otherwise overflow-panic; for any
1623                // real branch offset this is identical to `- 2`.
1624                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1625                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1626                0xEA000000 | offset_bits
1627            }
1628
1629            // Conditional branch with numeric offset
1630            ArmOp::BCondOffset { cond, offset } => {
1631                use synth_synthesis::Condition;
1632                let cond_bits: u32 = match cond {
1633                    Condition::EQ => 0x0,
1634                    Condition::NE => 0x1,
1635                    Condition::HS => 0x2,
1636                    Condition::LO => 0x3,
1637                    Condition::HI => 0x8,
1638                    Condition::LS => 0x9,
1639                    Condition::GE => 0xA,
1640                    Condition::LT => 0xB,
1641                    Condition::GT => 0xC,
1642                    Condition::LE => 0xD,
1643                };
1644                // B<cond> encoding: cond(4) | 1010 | offset(24)
1645                // wrapping_sub: total under fuzzing (#186), identical for real offsets.
1646                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1647                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1648                (cond_bits << 28) | 0x0A000000 | offset_bits
1649            }
1650
1651            ArmOp::Bl { label: _ } => {
1652                // BL encoding: cond(4) | 1011 | offset(24)
1653                0xEB000000
1654            }
1655
1656            ArmOp::Bx { rm } => {
1657                let rm_bits = reg_to_bits(rm);
1658
1659                // BX encoding: cond(4) | 000100101111111111110001 | Rm(4)
1660                0xE12FFF10 | rm_bits
1661            }
1662
1663            ArmOp::Blx { rm } => {
1664                let rm_bits = reg_to_bits(rm);
1665
1666                // BLX (register) encoding: cond(4) | 000100101111111111110011 | Rm(4)
1667                0xE12FFF30 | rm_bits
1668            }
1669
1670            ArmOp::Push { regs } => {
1671                // STMDB SP!, {regs} encoding: cond(4) | 100100 | 10 | 1101 | register_list(16)
1672                let mut reg_list: u32 = 0;
1673                for r in regs {
1674                    reg_list |= 1 << reg_to_bits(r);
1675                }
1676                0xE92D0000 | reg_list
1677            }
1678
1679            ArmOp::Pop { regs } => {
1680                // LDMIA SP!, {regs} encoding: cond(4) | 100010 | 11 | 1101 | register_list(16)
1681                let mut reg_list: u32 = 0;
1682                for r in regs {
1683                    reg_list |= 1 << reg_to_bits(r);
1684                }
1685                0xE8BD0000 | reg_list
1686            }
1687
1688            ArmOp::Nop => {
1689                // NOP encoding: MOV R0, R0
1690                0xE1A00000
1691            }
1692
1693            ArmOp::Udf { imm } => {
1694                // UDF (Undefined) encoding in ARM: 0xE7F000F0 | (imm12_hi << 8) | imm4_lo
1695                // We only use imm8, so split into imm4_hi and imm4_lo
1696                let imm8 = *imm as u32;
1697                0xE7F000F0 | ((imm8 & 0xF0) << 4) | (imm8 & 0x0F)
1698            }
1699
1700            // #615: handled by the `encode_arm_expanded` early return at the
1701            // top of this function — a real MOV{cond}/MOV pair now, never a
1702            // silent NOP again.
1703            ArmOp::Popcnt { .. } | ArmOp::SetCond { .. } | ArmOp::SelectMove { .. } => {
1704                unreachable!("handled by encode_arm_expanded (#615)")
1705            }
1706
1707            // Verification-only pseudo-ops: `synth-verify`'s ArmSemantics
1708            // models these, but NO codegen path constructs them (the selector
1709            // lowers select/locals/globals/br_table/call to real instruction
1710            // sequences before the encoder). Encoding one as a NOP silently
1711            // dropped the operation (#615 class); a typed Err keeps the
1712            // encoder total (Ok-or-Err, the `encoder_no_panic` contract)
1713            // while making any future reachability LOUD.
1714            ArmOp::Select { .. }
1715            | ArmOp::LocalGet { .. }
1716            | ArmOp::LocalSet { .. }
1717            | ArmOp::LocalTee { .. }
1718            | ArmOp::GlobalGet { .. }
1719            | ArmOp::GlobalSet { .. }
1720            | ArmOp::BrTable { .. }
1721            | ArmOp::Call { .. } => {
1722                return Err(synth_core::Error::synthesis(format!(
1723                    "verification-only pseudo-op {op:?} reached the A32 encoder — \
1724                     codegen lowers it before encoding; refusing to emit a silent NOP (#615)"
1725                )));
1726            }
1727
1728            // #594: CallIndirect is expanded to a real multi-instruction
1729            // sequence by the early return at the top of this function —
1730            // it must NEVER fall through to a silent NOP again.
1731            ArmOp::CallIndirect { .. } => {
1732                unreachable!("CallIndirect handled by encode_arm_call_indirect (#594)")
1733            }
1734
1735            // #615: every i64 op (and I32WrapI64) is expanded to a real A32
1736            // multi-instruction sequence by `encode_arm_expanded` — the
1737            // "encode as NOP for now" era ended with the value silently
1738            // vanishing on `--target cortex-r5`.
1739            ArmOp::I64Add { .. }
1740            | ArmOp::I64Sub { .. }
1741            | ArmOp::I64DivS { .. }
1742            | ArmOp::I64DivU { .. }
1743            | ArmOp::I64RemS { .. }
1744            | ArmOp::I64RemU { .. }
1745            | ArmOp::I64Clz { .. }
1746            | ArmOp::I64Ctz { .. }
1747            | ArmOp::I64Popcnt { .. }
1748            | ArmOp::I64And { .. }
1749            | ArmOp::I64Or { .. }
1750            | ArmOp::I64Xor { .. }
1751            | ArmOp::I64Eqz { .. }
1752            | ArmOp::I64Eq { .. }
1753            | ArmOp::I64Ne { .. }
1754            | ArmOp::I64LtS { .. }
1755            | ArmOp::I64LtU { .. }
1756            | ArmOp::I64LeS { .. }
1757            | ArmOp::I64LeU { .. }
1758            | ArmOp::I64GtS { .. }
1759            | ArmOp::I64GtU { .. }
1760            | ArmOp::I64GeS { .. }
1761            | ArmOp::I64GeU { .. }
1762            | ArmOp::I64Const { .. }
1763            | ArmOp::I64Ldr { .. }
1764            | ArmOp::I64Str { .. }
1765            | ArmOp::I64ExtendI32S { .. }
1766            | ArmOp::I64ExtendI32U { .. }
1767            | ArmOp::I64Extend8S { .. }
1768            | ArmOp::I64Extend16S { .. }
1769            | ArmOp::I64Extend32S { .. }
1770            | ArmOp::I32WrapI64 { .. } => {
1771                unreachable!("handled by encode_arm_expanded (#615)")
1772            }
1773
1774            // f32 VFP single-precision instructions
1775            ArmOp::F32Add { sd, sn, sm } => encode_vfp_3reg(0xEE300A00, sd, sn, sm)?,
1776            ArmOp::F32Sub { sd, sn, sm } => encode_vfp_3reg(0xEE300A40, sd, sn, sm)?,
1777            ArmOp::F32Mul { sd, sn, sm } => encode_vfp_3reg(0xEE200A00, sd, sn, sm)?,
1778            ArmOp::F32Div { sd, sn, sm } => encode_vfp_3reg(0xEE800A00, sd, sn, sm)?,
1779            ArmOp::F32Abs { sd, sm } => encode_vfp_2reg(0xEEB00AC0, sd, sm)?,
1780            ArmOp::F32Neg { sd, sm } => encode_vfp_2reg(0xEEB10A40, sd, sm)?,
1781            ArmOp::F32Sqrt { sd, sm } => encode_vfp_2reg(0xEEB10AC0, sd, sm)?,
1782
1783            // f32 pseudo-ops — multi-instruction sequences
1784            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
1785            ArmOp::F32Ceil { sd, sm } => {
1786                return self.encode_arm_f32_rounding(sd, sm, 0b01); // Round toward +Inf
1787            }
1788            ArmOp::F32Floor { sd, sm } => {
1789                return self.encode_arm_f32_rounding(sd, sm, 0b10); // Round toward -Inf
1790            }
1791            ArmOp::F32Trunc { sd, sm } => {
1792                return self.encode_arm_f32_rounding(sd, sm, 0b11); // VCVT toward zero
1793            }
1794            ArmOp::F32Nearest { sd, sm } => {
1795                return self.encode_arm_f32_rounding(sd, sm, 0b00); // VCVT to nearest
1796            }
1797            ArmOp::F32Min { sd, sn, sm } => {
1798                return self.encode_arm_f32_minmax(sd, sn, sm, true);
1799            }
1800            ArmOp::F32Max { sd, sn, sm } => {
1801                return self.encode_arm_f32_minmax(sd, sn, sm, false);
1802            }
1803            ArmOp::F32Copysign { sd, sn, sm } => {
1804                return self.encode_arm_f32_copysign(sd, sn, sm);
1805            }
1806
1807            // f32 comparisons — multi-instruction: VCMP + VMRS + conditional MOV
1808            ArmOp::F32Eq { rd, sn, sm } => {
1809                return self.encode_arm_f32_compare(rd, sn, sm, 0x0); // EQ
1810            }
1811            ArmOp::F32Ne { rd, sn, sm } => {
1812                return self.encode_arm_f32_compare(rd, sn, sm, 0x1); // NE
1813            }
1814            ArmOp::F32Lt { rd, sn, sm } => {
1815                return self.encode_arm_f32_compare(rd, sn, sm, 0x4); // MI (less than)
1816            }
1817            ArmOp::F32Le { rd, sn, sm } => {
1818                return self.encode_arm_f32_compare(rd, sn, sm, 0x9); // LS (less or same)
1819            }
1820            ArmOp::F32Gt { rd, sn, sm } => {
1821                return self.encode_arm_f32_compare(rd, sn, sm, 0xC); // GT
1822            }
1823            ArmOp::F32Ge { rd, sn, sm } => {
1824                return self.encode_arm_f32_compare(rd, sn, sm, 0xA); // GE
1825            }
1826
1827            // f32 const — multi-instruction: MOVW + MOVT + VMOV
1828            ArmOp::F32Const { sd, value } => {
1829                return self.encode_arm_f32_const(sd, *value);
1830            }
1831
1832            ArmOp::F32Load { sd, addr } => encode_vfp_ldst(0xED900A00, sd, addr)?,
1833            ArmOp::F32Store { sd, addr } => encode_vfp_ldst(0xED800A00, sd, addr)?,
1834
1835            // f32 conversions — multi-instruction sequences
1836            ArmOp::F32ConvertI32S { sd, rm } => {
1837                return self.encode_arm_f32_convert_i32(sd, rm, true);
1838            }
1839            ArmOp::F32ConvertI32U { sd, rm } => {
1840                return self.encode_arm_f32_convert_i32(sd, rm, false);
1841            }
1842            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
1843                return Err(synth_core::Error::synthesis(
1844                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
1845                ));
1846            }
1847            ArmOp::F32ReinterpretI32 { sd, rm } => encode_vmov_core_sreg(true, sd, rm)?,
1848            ArmOp::I32ReinterpretF32 { rd, sm } => encode_vmov_core_sreg(false, sm, rd)?,
1849            ArmOp::I32TruncF32S { rd, sm } => {
1850                return self.encode_arm_i32_trunc_f32(rd, sm, true);
1851            }
1852            ArmOp::I32TruncF32U { rd, sm } => {
1853                return self.encode_arm_i32_trunc_f32(rd, sm, false);
1854            }
1855
1856            // f64 VFP double-precision instructions (ARM32)
1857            // F64 arithmetic: same as F32 but with sz=1 (bit 8 = 1, cp11 = 0xB)
1858            ArmOp::F64Add { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B00, dd, dn, dm)?,
1859            ArmOp::F64Sub { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B40, dd, dn, dm)?,
1860            ArmOp::F64Mul { dd, dn, dm } => encode_vfp_3reg_f64(0xEE200B00, dd, dn, dm)?,
1861            ArmOp::F64Div { dd, dn, dm } => encode_vfp_3reg_f64(0xEE800B00, dd, dn, dm)?,
1862            ArmOp::F64Abs { dd, dm } => encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?,
1863            ArmOp::F64Neg { dd, dm } => encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?,
1864            ArmOp::F64Sqrt { dd, dm } => encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?,
1865
1866            // f64 pseudo-ops
1867            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
1868            ArmOp::F64Ceil { dd, dm } => {
1869                return self.encode_arm_f64_rounding(dd, dm, 0b01);
1870            }
1871            ArmOp::F64Floor { dd, dm } => {
1872                return self.encode_arm_f64_rounding(dd, dm, 0b10);
1873            }
1874            ArmOp::F64Trunc { dd, dm } => {
1875                return self.encode_arm_f64_rounding(dd, dm, 0b11);
1876            }
1877            ArmOp::F64Nearest { dd, dm } => {
1878                return self.encode_arm_f64_rounding(dd, dm, 0b00);
1879            }
1880            ArmOp::F64Min { dd, dn, dm } => {
1881                return self.encode_arm_f64_minmax(dd, dn, dm, true);
1882            }
1883            ArmOp::F64Max { dd, dn, dm } => {
1884                return self.encode_arm_f64_minmax(dd, dn, dm, false);
1885            }
1886            ArmOp::F64Copysign { dd, dn, dm } => {
1887                return self.encode_arm_f64_copysign(dd, dn, dm);
1888            }
1889
1890            // f64 comparisons
1891            ArmOp::F64Eq { rd, dn, dm } => {
1892                return self.encode_arm_f64_compare(rd, dn, dm, 0x0);
1893            }
1894            ArmOp::F64Ne { rd, dn, dm } => {
1895                return self.encode_arm_f64_compare(rd, dn, dm, 0x1);
1896            }
1897            ArmOp::F64Lt { rd, dn, dm } => {
1898                return self.encode_arm_f64_compare(rd, dn, dm, 0x4);
1899            }
1900            ArmOp::F64Le { rd, dn, dm } => {
1901                return self.encode_arm_f64_compare(rd, dn, dm, 0x9);
1902            }
1903            ArmOp::F64Gt { rd, dn, dm } => {
1904                return self.encode_arm_f64_compare(rd, dn, dm, 0xC);
1905            }
1906            ArmOp::F64Ge { rd, dn, dm } => {
1907                return self.encode_arm_f64_compare(rd, dn, dm, 0xA);
1908            }
1909
1910            ArmOp::F64Const { dd, value } => {
1911                return self.encode_arm_f64_const(dd, *value);
1912            }
1913
1914            ArmOp::F64Load { dd, addr } => encode_vfp_ldst_f64(0xED900B00, dd, addr)?,
1915            ArmOp::F64Store { dd, addr } => encode_vfp_ldst_f64(0xED800B00, dd, addr)?,
1916
1917            ArmOp::F64ConvertI32S { dd, rm } => {
1918                return self.encode_arm_f64_convert_i32(dd, rm, true);
1919            }
1920            ArmOp::F64ConvertI32U { dd, rm } => {
1921                return self.encode_arm_f64_convert_i32(dd, rm, false);
1922            }
1923            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
1924                return Err(synth_core::Error::synthesis(
1925                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
1926                ));
1927            }
1928            ArmOp::F64PromoteF32 { dd, sm } => {
1929                return self.encode_arm_f64_promote_f32(dd, sm);
1930            }
1931            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => {
1932                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?
1933            }
1934            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => {
1935                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?
1936            }
1937            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
1938                return Err(synth_core::Error::synthesis(
1939                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
1940                ));
1941            }
1942            ArmOp::I32TruncF64S { rd, dm } => {
1943                return self.encode_arm_i32_trunc_f64(rd, dm, true);
1944            }
1945            ArmOp::I32TruncF64U { rd, dm } => {
1946                return self.encode_arm_i32_trunc_f64(rd, dm, false);
1947            }
1948            // #615: multi-instruction i64 sequences — expanded to real A32 by
1949            // `encode_arm_expanded`, no longer "Thumb-2 only" NOPs.
1950            ArmOp::I64SetCond { .. }
1951            | ArmOp::I64SetCondZ { .. }
1952            | ArmOp::I64Mul { .. }
1953            | ArmOp::I64Shl { .. }
1954            | ArmOp::I64ShrS { .. }
1955            | ArmOp::I64ShrU { .. }
1956            | ArmOp::I64Rotl { .. }
1957            | ArmOp::I64Rotr { .. } => {
1958                unreachable!("handled by encode_arm_expanded (#615)")
1959            }
1960
1961            // MVE instructions — Thumb-2 only (Cortex-M55 is always Thumb-2)
1962            ArmOp::MveLoad { .. }
1963            | ArmOp::MveStore { .. }
1964            | ArmOp::MveConst { .. }
1965            | ArmOp::MveAnd { .. }
1966            | ArmOp::MveOrr { .. }
1967            | ArmOp::MveEor { .. }
1968            | ArmOp::MveMvn { .. }
1969            | ArmOp::MveBic { .. }
1970            | ArmOp::MveAddI { .. }
1971            | ArmOp::MveSubI { .. }
1972            | ArmOp::MveMulI { .. }
1973            | ArmOp::MveNegI { .. }
1974            | ArmOp::MveCmpEqI { .. }
1975            | ArmOp::MveCmpNeI { .. }
1976            | ArmOp::MveCmpLtS { .. }
1977            | ArmOp::MveCmpLtU { .. }
1978            | ArmOp::MveCmpGtS { .. }
1979            | ArmOp::MveCmpGtU { .. }
1980            | ArmOp::MveCmpLeS { .. }
1981            | ArmOp::MveCmpLeU { .. }
1982            | ArmOp::MveCmpGeS { .. }
1983            | ArmOp::MveCmpGeU { .. }
1984            | ArmOp::MveDup { .. }
1985            | ArmOp::MveExtractLane { .. }
1986            | ArmOp::MveInsertLane { .. }
1987            | ArmOp::MveAddF32 { .. }
1988            | ArmOp::MveSubF32 { .. }
1989            | ArmOp::MveMulF32 { .. }
1990            | ArmOp::MveNegF32 { .. }
1991            | ArmOp::MveAbsF32 { .. }
1992            | ArmOp::MveCmpEqF32 { .. }
1993            | ArmOp::MveCmpNeF32 { .. }
1994            | ArmOp::MveCmpLtF32 { .. }
1995            | ArmOp::MveCmpLeF32 { .. }
1996            | ArmOp::MveCmpGtF32 { .. }
1997            | ArmOp::MveCmpGeF32 { .. }
1998            | ArmOp::MveDupF32 { .. }
1999            | ArmOp::MveExtractLaneF32 { .. }
2000            | ArmOp::MveReplaceLaneF32 { .. }
2001            | ArmOp::MveDivF32 { .. }
2002            | ArmOp::MveSqrtF32 { .. } => {
2003                // MVE (Helium) is a Thumb-2-only extension (Cortex-M55); there
2004                // is no A32 encoding. The selector only emits MVE ops for
2005                // Thumb targets — a NOP here silently dropped the vector op
2006                // if that invariant ever broke (#615 class). Err keeps the
2007                // encoder total and the failure loud.
2008                return Err(synth_core::Error::synthesis(format!(
2009                    "MVE op {op:?} has no A32 (ARM-mode) encoding — MVE is Thumb-2 only (#615)"
2010                )));
2011            }
2012        };
2013
2014        // ARM32 instructions are little-endian
2015        Ok(instr.to_le_bytes().to_vec())
2016    }
2017
2018    // === ARM32 VFP multi-instruction helpers ===
2019
2020    /// Encode F32 comparison as ARM32: VCMP.F32 + VMRS + MOV rd,#0 + MOVcond rd,#1
2021    fn encode_arm_f32_compare(
2022        &self,
2023        rd: &Reg,
2024        sn: &VfpReg,
2025        sm: &VfpReg,
2026        cond_code: u32,
2027    ) -> Result<Vec<u8>> {
2028        let mut bytes = Vec::new();
2029
2030        // VCMP.F32 Sn, Sm: 0xEEB40A40 with Sn in Vd position, Sm in Vm position
2031        let sn_num = vfp_sreg_to_num(sn)?;
2032        let sm_num = vfp_sreg_to_num(sm)?;
2033        let (vd, d) = encode_sreg(sn_num);
2034        let (vm, m) = encode_sreg(sm_num);
2035        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2036        bytes.extend_from_slice(&vcmp.to_le_bytes());
2037
2038        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
2039        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2040
2041        // MOV rd, #0: 0xE3A0_0000 | (rd << 12)
2042        let rd_bits = reg_to_bits(rd);
2043        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2044        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2045
2046        // MOVcond rd, #1: cond(4) | 0011 1010 0000 rd(4) 0000 0000 0001
2047        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2048        bytes.extend_from_slice(&mov_one.to_le_bytes());
2049
2050        Ok(bytes)
2051    }
2052
2053    /// Encode F32 constant load as ARM32: MOVW Rt,#lo16 + MOVT Rt,#hi16 + VMOV Sd,Rt
2054    fn encode_arm_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
2055        let mut bytes = Vec::new();
2056        let bits = value.to_bits();
2057
2058        // Use R12 as temp register for constant loading
2059        let rt: u32 = 12; // R12/IP
2060
2061        // MOVW R12, #lo16: 0xE300_C000 | (imm4 << 16) | imm12
2062        let lo16 = bits & 0xFFFF;
2063        let movw = 0xE3000000 | (rt << 12) | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2064        bytes.extend_from_slice(&movw.to_le_bytes());
2065
2066        // MOVT R12, #hi16: 0xE340_C000 | (imm4 << 16) | imm12
2067        let hi16 = (bits >> 16) & 0xFFFF;
2068        let movt = 0xE3400000 | (rt << 12) | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2069        bytes.extend_from_slice(&movt.to_le_bytes());
2070
2071        // VMOV Sd, R12
2072        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
2073        bytes.extend_from_slice(&vmov.to_le_bytes());
2074
2075        Ok(bytes)
2076    }
2077
2078    /// Encode VMOV + VCVT.F32.S32/U32 as ARM32
2079    fn encode_arm_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2080        let mut bytes = Vec::new();
2081
2082        // VMOV Sd, Rm — move integer to VFP register
2083        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
2084        bytes.extend_from_slice(&vmov.to_le_bytes());
2085
2086        // VCVT.F32.S32 Sd, Sd (signed) or VCVT.F32.U32 Sd, Sd (unsigned)
2087        // Base: 0xEEB80A40 (signed) or 0xEEB80AC0 (unsigned)
2088        let sd_num = vfp_sreg_to_num(sd)?;
2089        let (vd, d) = encode_sreg(sd_num);
2090        let (vm, m) = encode_sreg(sd_num); // same register as source
2091        let base = if signed { 0xEEB80A40 } else { 0xEEB80AC0 };
2092        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2093        bytes.extend_from_slice(&vcvt.to_le_bytes());
2094
2095        Ok(bytes)
2096    }
2097
2098    /// Encode F32 rounding pseudo-op as ARM32 via VCVT to integer and back.
2099    /// mode: 0b00=nearest, 0b01=floor(-Inf), 0b10=ceil(+Inf), 0b11=trunc(zero)
2100    /// Strategy: VCVT.S32.F32 Sd, Sm (toward zero), then VCVT.F32.S32 Sd, Sd
2101    /// For ceil/floor/nearest, we use VCVTR (round toward mode) + convert back.
2102    /// Simplified: convert to int (toward zero for trunc) then back to float.
2103    /// Encode F32 rounding as ARM32.
2104    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2105    ///
2106    /// For trunc (mode=0b11): uses VCVTR.S32.F32 (always rounds toward zero).
2107    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant
2108    /// which honours FPSCR rmode), then restores FPSCR.
2109    fn encode_arm_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2110        let mut bytes = Vec::new();
2111        let sm_num = vfp_sreg_to_num(sm)?;
2112        let sd_num = vfp_sreg_to_num(sd)?;
2113        let (vd_s, d_s) = encode_sreg(sd_num);
2114        let (vm_s, m_s) = encode_sreg(sm_num);
2115
2116        if mode == 0b11 {
2117            // Trunc (toward zero): VCVTR.S32.F32 — the "R" variant always truncates.
2118            // 0xEEBD0AC0: bit[7]=1 => round toward zero regardless of FPSCR
2119            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2120            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2121        } else {
2122            // ceil/floor/nearest: manipulate FPSCR rounding mode
2123            let rt: u32 = 12; // R12/IP as temp
2124
2125            // VMRS R12, FPSCR
2126            let vmrs = 0xEEF10A10 | (rt << 12);
2127            bytes.extend_from_slice(&vmrs.to_le_bytes());
2128
2129            // BIC R12, R12, #(3 << 22) — clear RMode bits [23:22]
2130            // 3<<22 = 0x00C00000. ARM rotated imm: 0x03 ror 10 (rotation=5, imm8=0x03)
2131            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2132            bytes.extend_from_slice(&bic.to_le_bytes());
2133
2134            // ORR R12, R12, #(mode << 22) — set desired rounding mode
2135            if mode != 0 {
2136                // mode<<22: rotation=5, imm8=mode
2137                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2138                bytes.extend_from_slice(&orr.to_le_bytes());
2139            }
2140
2141            // VMSR FPSCR, R12
2142            let vmsr = 0xEEE10A10 | (rt << 12);
2143            bytes.extend_from_slice(&vmsr.to_le_bytes());
2144
2145            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rounding mode
2146            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2147            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2148
2149            // Restore FPSCR: clear rmode bits back to nearest (default)
2150            bytes.extend_from_slice(&vmrs.to_le_bytes());
2151            bytes.extend_from_slice(&bic.to_le_bytes());
2152            bytes.extend_from_slice(&vmsr.to_le_bytes());
2153        }
2154
2155        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
2156        let (vd2, d2) = encode_sreg(sd_num);
2157        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
2158        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2159
2160        Ok(bytes)
2161    }
2162
2163    /// Encode F32 min/max as ARM32: VCMP + VMRS + conditional VMOV
2164    fn encode_arm_f32_minmax(
2165        &self,
2166        sd: &VfpReg,
2167        sn: &VfpReg,
2168        sm: &VfpReg,
2169        is_min: bool,
2170    ) -> Result<Vec<u8>> {
2171        let mut bytes = Vec::new();
2172        let sn_num = vfp_sreg_to_num(sn)?;
2173        let sm_num = vfp_sreg_to_num(sm)?;
2174        let sd_num = vfp_sreg_to_num(sd)?;
2175
2176        // VMOV Sd, Sn (start with first operand)
2177        let (vd, d) = encode_sreg(sd_num);
2178        let (vn, n) = encode_sreg(sn_num);
2179        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2180        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2181
2182        // VCMP.F32 Sn, Sm
2183        let (vm, m) = encode_sreg(sm_num);
2184        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2185        bytes.extend_from_slice(&vcmp.to_le_bytes());
2186
2187        // VMRS APSR_nzcv, FPSCR
2188        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2189
2190        // For min: if Sn > Sm (GT), use Sm. Condition = GT (0xC)
2191        // For max: if Sn < Sm (MI/LT), use Sm. Condition = MI (0x4)
2192        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2193
2194        // VMOV{cond} Sd, Sm — conditional VMOV
2195        let vmov_cond = (cond << 28) | 0x0EB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2196        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2197
2198        Ok(bytes)
2199    }
2200
2201    /// Encode F32 copysign as ARM32: extract sign from Sm, magnitude from Sn
2202    fn encode_arm_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2203        let mut bytes = Vec::new();
2204
2205        // VMOV R12, Sm (get sign source bits)
2206        let vmov_sm = encode_vmov_core_sreg(false, sm, &Reg::R12)?;
2207        bytes.extend_from_slice(&vmov_sm.to_le_bytes());
2208
2209        // VMOV R0, Sn (get magnitude source bits) — use R0 as temp
2210        let vmov_sn = encode_vmov_core_sreg(false, sn, &Reg::R0)?;
2211        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2212
2213        // AND R12, R12, #0x80000000 (keep only sign bit)
2214        // Thumb-2 constant 0x80000000 needs special encoding; in ARM32 use rotated imm
2215        // 0x80000000 = 0x02 rotated right by 2 (rotation=1, imm8=0x02)
2216        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2217        bytes.extend_from_slice(&and_sign.to_le_bytes());
2218
2219        // BIC R0, R0, #0x80000000 (clear sign bit from magnitude)
2220        // R0 = register 0, so Rn and Rd fields are 0
2221        let bic_sign = 0xE3C00000u32 | (1 << 8) | 0x02;
2222        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2223
2224        // ORR R0, R0, R12 (combine sign + magnitude)
2225        // R0 = register 0, so Rn and Rd fields are 0
2226        let orr = 0xE1800000u32 | 12;
2227        bytes.extend_from_slice(&orr.to_le_bytes());
2228
2229        // VMOV Sd, R0
2230        let vmov_result = encode_vmov_core_sreg(true, sd, &Reg::R0)?;
2231        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2232
2233        Ok(bytes)
2234    }
2235
2236    /// Encode F64 comparison as ARM32: VCMP.F64 + VMRS + MOV rd,#0 + MOVcond rd,#1
2237    fn encode_arm_f64_compare(
2238        &self,
2239        rd: &Reg,
2240        dn: &VfpReg,
2241        dm: &VfpReg,
2242        cond_code: u32,
2243    ) -> Result<Vec<u8>> {
2244        let mut bytes = Vec::new();
2245
2246        // VCMP.F64 Dn, Dm: 0xEEB40B40 with Dn in Vd position, Dm in Vm position
2247        let dn_num = vfp_dreg_to_num(dn)?;
2248        let dm_num = vfp_dreg_to_num(dm)?;
2249        let (vd, d) = encode_dreg(dn_num);
2250        let (vm, m) = encode_dreg(dm_num);
2251        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2252        bytes.extend_from_slice(&vcmp.to_le_bytes());
2253
2254        // VMRS APSR_nzcv, FPSCR
2255        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2256
2257        // MOV rd, #0
2258        let rd_bits = reg_to_bits(rd);
2259        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2260        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2261
2262        // MOVcond rd, #1
2263        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2264        bytes.extend_from_slice(&mov_one.to_le_bytes());
2265
2266        Ok(bytes)
2267    }
2268
2269    /// Encode F64 constant load as ARM32: MOVW + MOVT + MOVW + MOVT + VMOV
2270    fn encode_arm_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
2271        let mut bytes = Vec::new();
2272        let bits = value.to_bits();
2273        let lo32 = bits as u32;
2274        let hi32 = (bits >> 32) as u32;
2275
2276        // Load low 32 bits into R0 (Rd field = 0 for R0)
2277        let lo16 = lo32 & 0xFFFF;
2278        let movw_r0 = 0xE3000000 | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2279        bytes.extend_from_slice(&movw_r0.to_le_bytes());
2280        let hi16 = (lo32 >> 16) & 0xFFFF;
2281        let movt_r0 = 0xE3400000 | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2282        bytes.extend_from_slice(&movt_r0.to_le_bytes());
2283
2284        // Load high 32 bits into R12
2285        let lo16 = hi32 & 0xFFFF;
2286        let movw_r12 = 0xE3000000 | ((lo16 >> 12) << 16) | (12 << 12) | (lo16 & 0xFFF);
2287        bytes.extend_from_slice(&movw_r12.to_le_bytes());
2288        let hi16 = (hi32 >> 16) & 0xFFFF;
2289        let movt_r12 = 0xE3400000 | ((hi16 >> 12) << 16) | (12 << 12) | (hi16 & 0xFFF);
2290        bytes.extend_from_slice(&movt_r12.to_le_bytes());
2291
2292        // VMOV Dd, R0, R12
2293        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
2294        bytes.extend_from_slice(&vmov.to_le_bytes());
2295
2296        Ok(bytes)
2297    }
2298
2299    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as ARM32
2300    fn encode_arm_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2301        let mut bytes = Vec::new();
2302
2303        // Use S0 as intermediate: VMOV S0, Rm
2304        let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
2305        bytes.extend_from_slice(&vmov.to_le_bytes());
2306
2307        // VCVT.F64.S32 Dd, S0 (signed) or VCVT.F64.U32 Dd, S0 (unsigned)
2308        // Base: 0xEEB80B40 (signed) or 0xEEB80BC0 (unsigned)
2309        let dd_num = vfp_dreg_to_num(dd)?;
2310        let (vd, d) = encode_dreg(dd_num);
2311        let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
2312        // S0 is register 0: Vm=0, M=0
2313        let vcvt = base | (d << 22) | (vd << 12);
2314        bytes.extend_from_slice(&vcvt.to_le_bytes());
2315
2316        Ok(bytes)
2317    }
2318
2319    /// Encode VCVT.F64.F32 Dd, Sm as ARM32 (f32 to f64 promotion)
2320    fn encode_arm_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2321        let dd_num = vfp_dreg_to_num(dd)?;
2322        let sm_num = vfp_sreg_to_num(sm)?;
2323        let (vd, d) = encode_dreg(dd_num);
2324        let (vm, m) = encode_sreg(sm_num);
2325
2326        // VCVT.F64.F32 Dd, Sm: 0xEEB70AC0
2327        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
2328        Ok(vcvt.to_le_bytes().to_vec())
2329    }
2330
2331    /// Encode VCVT.S32/U32.F64 Sd, Dm + VMOV Rd, Sd as ARM32
2332    fn encode_arm_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2333        let mut bytes = Vec::new();
2334        let dm_num = vfp_dreg_to_num(dm)?;
2335        let (vm, m) = encode_dreg(dm_num);
2336
2337        // VCVT.S32.F64 S0, Dm (toward zero) or VCVT.U32.F64 S0, Dm
2338        // S0: Vd=0, D=0
2339        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
2340        let vcvt = base | (m << 5) | vm;
2341        bytes.extend_from_slice(&vcvt.to_le_bytes());
2342
2343        // VMOV Rd, S0
2344        let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
2345        bytes.extend_from_slice(&vmov.to_le_bytes());
2346
2347        Ok(bytes)
2348    }
2349
2350    /// Encode F64 rounding pseudo-op as ARM32 via VCVT to integer and back.
2351    /// Encode F64 rounding as ARM32.
2352    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2353    ///
2354    /// For trunc: uses VCVTR.S32.F64 (always truncates).
2355    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F64 (non-R variant),
2356    /// then restores FPSCR.
2357    fn encode_arm_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2358        let mut bytes = Vec::new();
2359        let dm_num = vfp_dreg_to_num(dm)?;
2360        let dd_num = vfp_dreg_to_num(dd)?;
2361        let (vm, m) = encode_dreg(dm_num);
2362        let (vd, d) = encode_dreg(dd_num);
2363
2364        if mode == 0b11 {
2365            // Trunc (toward zero): VCVTR.S32.F64 — bit[7]=1, always truncates
2366            let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
2367            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2368        } else {
2369            // ceil/floor/nearest: manipulate FPSCR rounding mode
2370            let rt: u32 = 12;
2371
2372            // VMRS R12, FPSCR
2373            let vmrs = 0xEEF10A10 | (rt << 12);
2374            bytes.extend_from_slice(&vmrs.to_le_bytes());
2375
2376            // BIC R12, R12, #(3 << 22)
2377            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2378            bytes.extend_from_slice(&bic.to_le_bytes());
2379
2380            // ORR R12, R12, #(mode << 22)
2381            if mode != 0 {
2382                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2383                bytes.extend_from_slice(&orr.to_le_bytes());
2384            }
2385
2386            // VMSR FPSCR, R12
2387            let vmsr = 0xEEE10A10 | (rt << 12);
2388            bytes.extend_from_slice(&vmsr.to_le_bytes());
2389
2390            // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0), uses FPSCR rmode
2391            let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
2392            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2393
2394            // Restore FPSCR
2395            bytes.extend_from_slice(&vmrs.to_le_bytes());
2396            bytes.extend_from_slice(&bic.to_le_bytes());
2397            bytes.extend_from_slice(&vmsr.to_le_bytes());
2398        }
2399
2400        // VCVT.F64.S32 Dd, S0 (convert back to double)
2401        let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
2402        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2403
2404        Ok(bytes)
2405    }
2406
2407    /// Encode F64 min/max as ARM32: VMOV + VCMP + VMRS + conditional VMOV
2408    fn encode_arm_f64_minmax(
2409        &self,
2410        dd: &VfpReg,
2411        dn: &VfpReg,
2412        dm: &VfpReg,
2413        is_min: bool,
2414    ) -> Result<Vec<u8>> {
2415        let mut bytes = Vec::new();
2416        let dn_num = vfp_dreg_to_num(dn)?;
2417        let dm_num = vfp_dreg_to_num(dm)?;
2418        let dd_num = vfp_dreg_to_num(dd)?;
2419
2420        // VMOV.F64 Dd, Dn (start with first operand)
2421        let (vd, d) = encode_dreg(dd_num);
2422        let (vn, n) = encode_dreg(dn_num);
2423        let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2424        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2425
2426        // VCMP.F64 Dn, Dm
2427        let (vm, m) = encode_dreg(dm_num);
2428        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2429        bytes.extend_from_slice(&vcmp.to_le_bytes());
2430
2431        // VMRS APSR_nzcv, FPSCR
2432        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2433
2434        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2435        let vmov_cond = (cond << 28) | 0x0EB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2436        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2437
2438        Ok(bytes)
2439    }
2440
2441    /// Encode F64 copysign as ARM32
2442    fn encode_arm_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
2443        let mut bytes = Vec::new();
2444
2445        // VMOV R0, R12, Dm (get sign source bits)
2446        let vmov_dm = encode_vmov_core_dreg(false, dm, &Reg::R0, &Reg::R12)?;
2447        bytes.extend_from_slice(&vmov_dm.to_le_bytes());
2448
2449        // VMOV R1, R2, Dn (get magnitude source bits)
2450        // We use R1 (lo) and R2 (hi) for the magnitude
2451        let vmov_dn = encode_vmov_core_dreg(false, dn, &Reg::R1, &Reg::R2)?;
2452        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2453
2454        // AND R12, R12, #0x80000000 (keep only sign bit from hi word)
2455        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2456        bytes.extend_from_slice(&and_sign.to_le_bytes());
2457
2458        // BIC R2, R2, #0x80000000 (clear sign bit from magnitude hi word)
2459        let bic_sign = 0xE3C00000u32 | (2 << 16) | (2 << 12) | (1 << 8) | 0x02;
2460        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2461
2462        // ORR R2, R2, R12 (combine sign + magnitude)
2463        let orr = 0xE1800000u32 | (2 << 16) | (2 << 12) | 12;
2464        bytes.extend_from_slice(&orr.to_le_bytes());
2465
2466        // VMOV Dd, R1, R2
2467        let vmov_result = encode_vmov_core_dreg(true, dd, &Reg::R1, &Reg::R2)?;
2468        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2469
2470        Ok(bytes)
2471    }
2472
2473    /// Encode VCVT.S32/U32.F32 + VMOV as ARM32
2474    fn encode_arm_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2475        let mut bytes = Vec::new();
2476
2477        // VCVT.S32.F32 Sd, Sm (toward zero) or VCVT.U32.F32 Sd, Sm
2478        // We use Sm as both source and destination for the intermediate result
2479        let sm_num = vfp_sreg_to_num(sm)?;
2480        let (vd, d) = encode_sreg(sm_num);
2481        let (vm, m) = encode_sreg(sm_num);
2482        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
2483        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2484        bytes.extend_from_slice(&vcvt.to_le_bytes());
2485
2486        // VMOV Rd, Sm — move result back to core register
2487        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
2488        bytes.extend_from_slice(&vmov.to_le_bytes());
2489
2490        Ok(bytes)
2491    }
2492
2493    /// Encode an ARM instruction in Thumb-2 mode (16-bit or 32-bit instructions)
2494    fn encode_thumb(&self, op: &ArmOp) -> Result<Vec<u8>> {
2495        // Thumb-2 supports both 16-bit and 32-bit instructions
2496        // 32-bit instructions are encoded as two 16-bit halfwords (big-endian order)
2497        match op {
2498            // === 16-bit Thumb encodings ===
2499            ArmOp::Add { rd, rn, op2 } => {
2500                let rd_bits = reg_to_bits(rd) as u16;
2501                let rn_bits = reg_to_bits(rn) as u16;
2502
2503                if let Operand2::Reg(rm) = op2 {
2504                    let rm_bits = reg_to_bits(rm) as u16;
2505                    // 16-bit ADDS only has 3-bit register fields (R0-R7). For
2506                    // high registers (e.g. R12, the MemLoad/MemStore base
2507                    // scratch) the bits overflow into adjacent fields, silently
2508                    // corrupting the operands — issue #178/#180: `add ip,ip,r0`
2509                    // was emitted as `adds r4,r5,r1`. Guard on all three regs
2510                    // being low and fall back to 32-bit ADD.W otherwise, exactly
2511                    // as the Sub handler below does.
2512                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2513                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2514                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2515                        Ok(instr.to_le_bytes().to_vec())
2516                    } else {
2517                        // ADD.W Rd, Rn, Rm (32-bit) for high registers
2518                        self.encode_thumb32_add_reg_raw(
2519                            rd_bits as u32,
2520                            rn_bits as u32,
2521                            rm_bits as u32,
2522                        )
2523                    }
2524                } else if let Operand2::Imm(imm) = op2 {
2525                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2526                        // ADDS Rd, Rn, #imm3 (16-bit): 0001 110 imm3 Rn Rd
2527                        let instr: u16 = 0x1C00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2528                        Ok(instr.to_le_bytes().to_vec())
2529                    } else {
2530                        // Use 32-bit ADD for larger immediates
2531                        self.encode_thumb32_add(rd, rn, *imm as u32)
2532                    }
2533                } else {
2534                    // Fallback to 32-bit encoding
2535                    self.encode_thumb32_add(rd, rn, 0)
2536                }
2537            }
2538
2539            ArmOp::Sub { rd, rn, op2 } => {
2540                let rd_bits = reg_to_bits(rd) as u16;
2541                let rn_bits = reg_to_bits(rn) as u16;
2542
2543                if let Operand2::Reg(rm) = op2 {
2544                    let rm_bits = reg_to_bits(rm) as u16;
2545                    // 16-bit SUBS can only use low registers (R0-R7)
2546                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2547                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2548                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2549                        Ok(instr.to_le_bytes().to_vec())
2550                    } else {
2551                        // Use 32-bit SUB.W for high registers
2552                        self.encode_thumb32_sub_reg_raw(
2553                            rd_bits as u32,
2554                            rn_bits as u32,
2555                            rm_bits as u32,
2556                        )
2557                    }
2558                } else if let Operand2::Imm(imm) = op2 {
2559                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2560                        // SUBS Rd, Rn, #imm3 (16-bit): 0001 111 imm3 Rn Rd
2561                        let instr: u16 = 0x1E00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2562                        Ok(instr.to_le_bytes().to_vec())
2563                    } else {
2564                        self.encode_thumb32_sub(rd, rn, *imm as u32)
2565                    }
2566                } else {
2567                    self.encode_thumb32_sub(rd, rn, 0)
2568                }
2569            }
2570
2571            ArmOp::Mov { rd, op2 } => {
2572                let rd_bits = reg_to_bits(rd) as u16;
2573
2574                if let Operand2::Imm(imm) = op2 {
2575                    if *imm <= 255 && rd_bits < 8 {
2576                        // MOVS Rd, #imm8 (16-bit): 0010 0 Rd imm8
2577                        let imm_bits = (*imm as u16) & 0xFF;
2578                        let instr: u16 = 0x2000 | (rd_bits << 8) | imm_bits;
2579                        Ok(instr.to_le_bytes().to_vec())
2580                    } else {
2581                        // Use 32-bit MOVW for larger immediates
2582                        self.encode_thumb32_movw(rd, *imm as u32)
2583                    }
2584                } else if let Operand2::Reg(rm) = op2 {
2585                    let rm_bits = reg_to_bits(rm) as u16;
2586                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
2587                    // D = Rd[3], Rd[2:0] in lower bits
2588                    let d_bit = (rd_bits >> 3) & 1;
2589                    let instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
2590                    Ok(instr.to_le_bytes().to_vec())
2591                } else {
2592                    let instr: u16 = 0xBF00; // NOP fallback
2593                    Ok(instr.to_le_bytes().to_vec())
2594                }
2595            }
2596
2597            ArmOp::Push { regs } => {
2598                // Thumb-2 PUSH encoding:
2599                // If all regs in R0-R7 + LR, use 16-bit: 1011 010 M rrrrrrrr
2600                // Otherwise use 32-bit: STMDB SP!, {regs} = 1110 1001 0010 1101 | 0M0 reglist(13)
2601                let mut reg_list: u16 = 0;
2602                let mut need_32bit = false;
2603                for r in regs {
2604                    let bit = reg_to_bits(r);
2605                    if bit >= 8 && *r != Reg::LR {
2606                        need_32bit = true;
2607                    }
2608                    reg_list |= 1 << bit;
2609                }
2610                if !need_32bit {
2611                    // 16-bit PUSH: 1011 010 M rrrrrrrr
2612                    let m_bit = if reg_list & (1 << 14) != 0 {
2613                        1u16
2614                    } else {
2615                        0u16
2616                    };
2617                    let low_regs = reg_list & 0xFF;
2618                    let instr: u16 = 0xB400 | (m_bit << 8) | low_regs;
2619                    Ok(instr.to_le_bytes().to_vec())
2620                } else {
2621                    // 32-bit STMDB SP!, {regs}: E92D | reglist(16)
2622                    let hw1: u16 = 0xE92D;
2623                    let hw2: u16 = reg_list;
2624                    let mut bytes = hw1.to_le_bytes().to_vec();
2625                    bytes.extend_from_slice(&hw2.to_le_bytes());
2626                    Ok(bytes)
2627                }
2628            }
2629
2630            ArmOp::Pop { regs } => {
2631                // Thumb-2 POP encoding:
2632                // If all regs in R0-R7 + PC, use 16-bit: 1011 110 P rrrrrrrr
2633                // Otherwise use 32-bit: LDMIA SP!, {regs} = 1110 1000 1011 1101 | PM0 reglist(13)
2634                let mut reg_list: u16 = 0;
2635                let mut need_32bit = false;
2636                for r in regs {
2637                    let bit = reg_to_bits(r);
2638                    if bit >= 8 && *r != Reg::PC {
2639                        need_32bit = true;
2640                    }
2641                    reg_list |= 1 << bit;
2642                }
2643                if !need_32bit {
2644                    // 16-bit POP: 1011 110 P rrrrrrrr
2645                    let p_bit = if reg_list & (1 << 15) != 0 {
2646                        1u16
2647                    } else {
2648                        0u16
2649                    };
2650                    let low_regs = reg_list & 0xFF;
2651                    let instr: u16 = 0xBC00 | (p_bit << 8) | low_regs;
2652                    Ok(instr.to_le_bytes().to_vec())
2653                } else {
2654                    // 32-bit LDMIA SP!, {regs}: E8BD | reglist(16)
2655                    let hw1: u16 = 0xE8BD;
2656                    let hw2: u16 = reg_list;
2657                    let mut bytes = hw1.to_le_bytes().to_vec();
2658                    bytes.extend_from_slice(&hw2.to_le_bytes());
2659                    Ok(bytes)
2660                }
2661            }
2662
2663            ArmOp::Nop => {
2664                let instr: u16 = 0xBF00; // NOP in Thumb-2
2665                Ok(instr.to_le_bytes().to_vec())
2666            }
2667
2668            ArmOp::Udf { imm } => {
2669                // UDF (Undefined) in Thumb-2: 16-bit encoding is 0xDE00 | imm8
2670                // This triggers UsageFault/HardFault, used for WASM traps
2671                let instr: u16 = 0xDE00 | (*imm as u16);
2672                let bytes = instr.to_le_bytes().to_vec();
2673                encoding_contracts::verify_thumb16(&bytes);
2674                Ok(bytes)
2675            }
2676
2677            // i64 support: ADDS, ADC, SUBS, SBC for register pair arithmetic
2678            // ADDS sets flags (carry), ADC uses carry from previous ADDS
2679            ArmOp::Adds { rd, rn, op2 } => {
2680                let rd_bits = reg_to_bits(rd) as u16;
2681                let rn_bits = reg_to_bits(rn) as u16;
2682
2683                if let Operand2::Reg(rm) = op2 {
2684                    let rm_bits = reg_to_bits(rm) as u16;
2685                    // 16-bit ADDS is R0-R7 only; i64 pair allocation can place
2686                    // operands in R8-R11, which would overflow the 3-bit fields
2687                    // and corrupt the operands (#178/#180 class). Guard and fall
2688                    // back to 32-bit ADDS.W for high registers.
2689                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2690                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2691                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2692                        Ok(instr.to_le_bytes().to_vec())
2693                    } else {
2694                        self.encode_thumb32_adds_reg_raw(
2695                            rd_bits as u32,
2696                            rn_bits as u32,
2697                            rm_bits as u32,
2698                        )
2699                    }
2700                } else {
2701                    // 32-bit Thumb-2 ADDS with immediate
2702                    self.encode_thumb32_adds(rd, rn, 0)
2703                }
2704            }
2705
2706            // ADC: Add with Carry (Thumb-2 32-bit)
2707            // ADC.W Rd, Rn, Rm: EB40 Rn | 00 Rd 00 Rm
2708            ArmOp::Adc { rd, rn, op2 } => {
2709                let rd_bits = reg_to_bits(rd);
2710                let rn_bits = reg_to_bits(rn);
2711
2712                if let Operand2::Reg(rm) = op2 {
2713                    let rm_bits = reg_to_bits(rm);
2714                    // ADC.W Rd, Rn, Rm (T2): 1110 1011 0100 Rn | 0 000 Rd 00 00 Rm
2715                    let hw1: u16 = (0xEB40 | rn_bits) as u16;
2716                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2717
2718                    let mut bytes = hw1.to_le_bytes().to_vec();
2719                    bytes.extend_from_slice(&hw2.to_le_bytes());
2720                    Ok(bytes)
2721                } else {
2722                    // ADC with immediate - use 32-bit encoding
2723                    let hw1: u16 = (0xF140 | rn_bits) as u16;
2724                    let hw2: u16 = (rd_bits << 8) as u16;
2725                    let mut bytes = hw1.to_le_bytes().to_vec();
2726                    bytes.extend_from_slice(&hw2.to_le_bytes());
2727                    Ok(bytes)
2728                }
2729            }
2730
2731            // SUBS sets flags (borrow), SBC uses borrow from previous SUBS
2732            ArmOp::Subs { rd, rn, op2 } => {
2733                let rd_bits = reg_to_bits(rd) as u16;
2734                let rn_bits = reg_to_bits(rn) as u16;
2735
2736                if let Operand2::Reg(rm) = op2 {
2737                    let rm_bits = reg_to_bits(rm) as u16;
2738                    // 16-bit SUBS is R0-R7 only; high-register i64 pair operands
2739                    // would overflow the 3-bit fields (#178/#180 class). Guard
2740                    // and fall back to 32-bit SUBS.W for high registers.
2741                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2742                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2743                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2744                        Ok(instr.to_le_bytes().to_vec())
2745                    } else {
2746                        self.encode_thumb32_subs_reg_raw(
2747                            rd_bits as u32,
2748                            rn_bits as u32,
2749                            rm_bits as u32,
2750                        )
2751                    }
2752                } else {
2753                    // 32-bit Thumb-2 SUBS with immediate
2754                    self.encode_thumb32_subs(rd, rn, 0)
2755                }
2756            }
2757
2758            // SBC: Subtract with Carry (Thumb-2 32-bit)
2759            // SBC.W Rd, Rn, Rm: EB60 Rn | 00 Rd 00 Rm
2760            ArmOp::Sbc { rd, rn, op2 } => {
2761                let rd_bits = reg_to_bits(rd);
2762                let rn_bits = reg_to_bits(rn);
2763
2764                if let Operand2::Reg(rm) = op2 {
2765                    let rm_bits = reg_to_bits(rm);
2766                    // SBC.W Rd, Rn, Rm (T2): 1110 1011 0110 Rn | 0 000 Rd 00 00 Rm
2767                    let hw1: u16 = (0xEB60 | rn_bits) as u16;
2768                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2769
2770                    let mut bytes = hw1.to_le_bytes().to_vec();
2771                    bytes.extend_from_slice(&hw2.to_le_bytes());
2772                    Ok(bytes)
2773                } else {
2774                    // SBC with immediate - use 32-bit encoding
2775                    let hw1: u16 = (0xF160 | rn_bits) as u16;
2776                    let hw2: u16 = (rd_bits << 8) as u16;
2777                    let mut bytes = hw1.to_le_bytes().to_vec();
2778                    bytes.extend_from_slice(&hw2.to_le_bytes());
2779                    Ok(bytes)
2780                }
2781            }
2782
2783            // === 32-bit Thumb-2 encodings ===
2784
2785            // SDIV: 11111011 1001 Rn 1111 Rd 1111 Rm
2786            ArmOp::Sdiv { rd, rn, rm } => {
2787                let rd_bits = reg_to_bits(rd);
2788                let rn_bits = reg_to_bits(rn);
2789                let rm_bits = reg_to_bits(rm);
2790                reg_bits_checked(rd_bits)?;
2791                reg_bits_checked(rn_bits)?;
2792                reg_bits_checked(rm_bits)?;
2793
2794                // Thumb-2 SDIV: FB90 F0F0 | Rn<<16 | Rd<<8 | Rm
2795                // First halfword: 1111 1011 1001 Rn = 0xFB90 | Rn
2796                // Second halfword: 1111 Rd 1111 Rm = 0xF0F0 | Rd<<8 | Rm
2797                let hw1: u16 = (0xFB90 | rn_bits) as u16;
2798                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
2799
2800                // Thumb-2 32-bit instructions: first halfword, then second halfword (little-endian each)
2801                let mut bytes = hw1.to_le_bytes().to_vec();
2802                bytes.extend_from_slice(&hw2.to_le_bytes());
2803                encoding_contracts::verify_thumb32(&bytes);
2804                Ok(bytes)
2805            }
2806
2807            // UDIV: 11111011 1011 Rn 1111 Rd 1111 Rm
2808            ArmOp::Udiv { rd, rn, rm } => {
2809                let rd_bits = reg_to_bits(rd);
2810                let rn_bits = reg_to_bits(rn);
2811                let rm_bits = reg_to_bits(rm);
2812                reg_bits_checked(rd_bits)?;
2813                reg_bits_checked(rn_bits)?;
2814                reg_bits_checked(rm_bits)?;
2815
2816                // Thumb-2 UDIV: FBB0 F0F0 | Rn<<16 | Rd<<8 | Rm
2817                let hw1: u16 = (0xFBB0 | rn_bits) as u16;
2818                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
2819
2820                let mut bytes = hw1.to_le_bytes().to_vec();
2821                bytes.extend_from_slice(&hw2.to_le_bytes());
2822                encoding_contracts::verify_thumb32(&bytes);
2823                Ok(bytes)
2824            }
2825
2826            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
2827                let rdlo_bits = reg_to_bits(rdlo);
2828                let rdhi_bits = reg_to_bits(rdhi);
2829                let rn_bits = reg_to_bits(rn);
2830                let rm_bits = reg_to_bits(rm);
2831                reg_bits_checked(rdlo_bits)?;
2832                reg_bits_checked(rdhi_bits)?;
2833                reg_bits_checked(rn_bits)?;
2834                reg_bits_checked(rm_bits)?;
2835
2836                // Thumb-2 UMULL: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm
2837                let hw1: u16 = (0xFBA0 | rn_bits) as u16;
2838                let hw2: u16 = ((rdlo_bits << 12) | (rdhi_bits << 8) | rm_bits) as u16;
2839
2840                let mut bytes = hw1.to_le_bytes().to_vec();
2841                bytes.extend_from_slice(&hw2.to_le_bytes());
2842                encoding_contracts::verify_thumb32(&bytes);
2843                Ok(bytes)
2844            }
2845
2846            // MUL (Thumb-2 32-bit): MUL Rd, Rn, Rm
2847            ArmOp::Mul { rd, rn, rm } => {
2848                let rd_bits = reg_to_bits(rd);
2849                let rn_bits = reg_to_bits(rn);
2850                let rm_bits = reg_to_bits(rm);
2851
2852                // Thumb-2 MUL: FB00 F000 | Rn | Rd<<8 | Rm
2853                // 11111011 0000 Rn | 1111 Rd 0000 Rm
2854                let hw1: u16 = (0xFB00 | rn_bits) as u16;
2855                let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
2856
2857                let mut bytes = hw1.to_le_bytes().to_vec();
2858                bytes.extend_from_slice(&hw2.to_le_bytes());
2859                Ok(bytes)
2860            }
2861
2862            // MLS: Rd = Ra - Rn * Rm
2863            ArmOp::Mls { rd, rn, rm, ra } => {
2864                let rd_bits = reg_to_bits(rd);
2865                let rn_bits = reg_to_bits(rn);
2866                let rm_bits = reg_to_bits(rm);
2867                let ra_bits = reg_to_bits(ra);
2868
2869                // Thumb-2 MLS: FB00 Rn | Ra Rd 0001 Rm
2870                // 11111011 0000 Rn | Ra Rd 0001 Rm
2871                let hw1: u16 = (0xFB00 | rn_bits) as u16;
2872                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | 0x10 | rm_bits) as u16;
2873
2874                let mut bytes = hw1.to_le_bytes().to_vec();
2875                bytes.extend_from_slice(&hw2.to_le_bytes());
2876                Ok(bytes)
2877            }
2878
2879            ArmOp::Mla { rd, rn, rm, ra } => {
2880                let rd_bits = reg_to_bits(rd);
2881                let rn_bits = reg_to_bits(rn);
2882                let rm_bits = reg_to_bits(rm);
2883                let ra_bits = reg_to_bits(ra);
2884
2885                // Thumb-2 MLA: FB00 Rn | Ra Rd 0000 Rm — same as MLS without the
2886                // bit-4 (0x10) op flag. rd = ra + rn*rm.
2887                let hw1: u16 = (0xFB00 | rn_bits) as u16;
2888                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | rm_bits) as u16;
2889
2890                let mut bytes = hw1.to_le_bytes().to_vec();
2891                bytes.extend_from_slice(&hw2.to_le_bytes());
2892                Ok(bytes)
2893            }
2894
2895            // AND (Thumb-2 32-bit)
2896            ArmOp::And { rd, rn, op2 } => {
2897                if let Operand2::Reg(rm) = op2 {
2898                    let rd_bits = reg_to_bits(rd);
2899                    let rn_bits = reg_to_bits(rn);
2900                    let rm_bits = reg_to_bits(rm);
2901
2902                    // Thumb-2 AND register: EA00 Rn | 0 Rd 00 00 Rm
2903                    let hw1: u16 = (0xEA00 | rn_bits) as u16;
2904                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2905
2906                    let mut bytes = hw1.to_le_bytes().to_vec();
2907                    bytes.extend_from_slice(&hw2.to_le_bytes());
2908                    Ok(bytes)
2909                } else if let Operand2::Imm(imm) = op2 {
2910                    let rd_bits = reg_to_bits(rd);
2911                    let rn_bits = reg_to_bits(rn);
2912
2913                    // Thumb-2 AND.W immediate T1: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8.
2914                    // The i:imm3:imm8 field is a ThumbExpandImm modified immediate —
2915                    // encode it correctly (or error on an un-encodable value)
2916                    // rather than packing raw bits, closing the silent-miscompile
2917                    // class for AND alongside ORR/EOR (#251) / ADD/SUB (#253) /
2918                    // CMP (#255).
2919                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
2920                        synth_core::Error::synthesis(
2921                            "AND immediate is not a valid ThumbExpandImm — materialize into a register",
2922                        )
2923                    })?;
2924                    let i_bit = (field >> 11) & 1;
2925                    let imm3 = (field >> 8) & 0x7;
2926                    let imm8 = field & 0xFF;
2927
2928                    let hw1: u16 = (0xF000 | (i_bit << 10) | rn_bits) as u16;
2929                    let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
2930
2931                    let mut bytes = hw1.to_le_bytes().to_vec();
2932                    bytes.extend_from_slice(&hw2.to_le_bytes());
2933                    Ok(bytes)
2934                } else {
2935                    // RegShift variant - fallback to NOP
2936                    let instr: u16 = 0xBF00;
2937                    Ok(instr.to_le_bytes().to_vec())
2938                }
2939            }
2940
2941            // ORR (Thumb-2 32-bit)
2942            ArmOp::Orr { rd, rn, op2 } => {
2943                if let Operand2::Reg(rm) = op2 {
2944                    let rd_bits = reg_to_bits(rd);
2945                    let rn_bits = reg_to_bits(rn);
2946                    let rm_bits = reg_to_bits(rm);
2947
2948                    // Thumb-2 ORR: EA40 Rn | 0 Rd 00 00 Rm
2949                    let hw1: u16 = (0xEA40 | rn_bits) as u16;
2950                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2951
2952                    let mut bytes = hw1.to_le_bytes().to_vec();
2953                    bytes.extend_from_slice(&hw2.to_le_bytes());
2954                    Ok(bytes)
2955                } else if let Operand2::Imm(imm) = op2 {
2956                    // ORR.W immediate T1: 11110 i 0 0010 S Rn | 0 imm3 Rd imm8.
2957                    // Only the zero-extended byte form (imm <= 0xFF) is encoded;
2958                    // larger modified immediates need ThumbExpandImm — return an
2959                    // error rather than silently emit a NOP (Ok-or-Err, #180/#185).
2960                    let imm_val = *imm as u32;
2961                    if imm_val > 0xFF {
2962                        return Err(synth_core::Error::synthesis(
2963                            "ORR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
2964                        ));
2965                    }
2966                    let rd_bits = reg_to_bits(rd);
2967                    let rn_bits = reg_to_bits(rn);
2968                    let hw1: u16 = (0xF040 | rn_bits) as u16;
2969                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
2970                    let mut bytes = hw1.to_le_bytes().to_vec();
2971                    bytes.extend_from_slice(&hw2.to_le_bytes());
2972                    Ok(bytes)
2973                } else {
2974                    let instr: u16 = 0xBF00;
2975                    Ok(instr.to_le_bytes().to_vec())
2976                }
2977            }
2978
2979            // EOR (Thumb-2 32-bit)
2980            ArmOp::Eor { rd, rn, op2 } => {
2981                if let Operand2::Reg(rm) = op2 {
2982                    let rd_bits = reg_to_bits(rd);
2983                    let rn_bits = reg_to_bits(rn);
2984                    let rm_bits = reg_to_bits(rm);
2985
2986                    // Thumb-2 EOR: EA80 Rn | 0 Rd 00 00 Rm
2987                    let hw1: u16 = (0xEA80 | rn_bits) as u16;
2988                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2989
2990                    let mut bytes = hw1.to_le_bytes().to_vec();
2991                    bytes.extend_from_slice(&hw2.to_le_bytes());
2992                    Ok(bytes)
2993                } else if let Operand2::Imm(imm) = op2 {
2994                    // EOR.W immediate T1: 11110 i 0 0100 S Rn | 0 imm3 Rd imm8.
2995                    // Byte form only (imm <= 0xFF); larger needs ThumbExpandImm —
2996                    // error, not a silent NOP (Ok-or-Err, #180/#185).
2997                    let imm_val = *imm as u32;
2998                    if imm_val > 0xFF {
2999                        return Err(synth_core::Error::synthesis(
3000                            "EOR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3001                        ));
3002                    }
3003                    let rd_bits = reg_to_bits(rd);
3004                    let rn_bits = reg_to_bits(rn);
3005                    let hw1: u16 = (0xF080 | rn_bits) as u16;
3006                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3007                    let mut bytes = hw1.to_le_bytes().to_vec();
3008                    bytes.extend_from_slice(&hw2.to_le_bytes());
3009                    Ok(bytes)
3010                } else {
3011                    let instr: u16 = 0xBF00;
3012                    Ok(instr.to_le_bytes().to_vec())
3013                }
3014            }
3015
3016            // Shift operations (16-bit for low registers)
3017            ArmOp::Lsl { rd, rn, shift } => {
3018                let rd_bits = reg_to_bits(rd) as u16;
3019                let rn_bits = reg_to_bits(rn) as u16;
3020                let shift_bits = (*shift as u16) & 0x1F;
3021
3022                if rd_bits < 8 && rn_bits < 8 {
3023                    // LSLS Rd, Rm, #imm5 (16-bit): 0000 0 imm5 Rm Rd
3024                    let instr: u16 = (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3025                    Ok(instr.to_le_bytes().to_vec())
3026                } else {
3027                    // Use 32-bit encoding for high registers
3028                    self.encode_thumb32_shift(rd, rn, *shift, 0b00) // LSL type
3029                }
3030            }
3031
3032            ArmOp::Lsr { rd, rn, shift } => {
3033                let rd_bits = reg_to_bits(rd) as u16;
3034                let rn_bits = reg_to_bits(rn) as u16;
3035                let shift_bits = (*shift as u16) & 0x1F;
3036
3037                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3038                    // LSRS Rd, Rm, #imm5 (16-bit): 0000 1 imm5 Rm Rd
3039                    let instr: u16 = 0x0800 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3040                    Ok(instr.to_le_bytes().to_vec())
3041                } else {
3042                    self.encode_thumb32_shift(rd, rn, *shift, 0b01) // LSR type
3043                }
3044            }
3045
3046            ArmOp::Asr { rd, rn, shift } => {
3047                let rd_bits = reg_to_bits(rd) as u16;
3048                let rn_bits = reg_to_bits(rn) as u16;
3049                let shift_bits = (*shift as u16) & 0x1F;
3050
3051                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3052                    // ASRS Rd, Rm, #imm5 (16-bit): 0001 0 imm5 Rm Rd
3053                    let instr: u16 = 0x1000 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3054                    Ok(instr.to_le_bytes().to_vec())
3055                } else {
3056                    self.encode_thumb32_shift(rd, rn, *shift, 0b10) // ASR type
3057                }
3058            }
3059
3060            ArmOp::Ror { rd, rn, shift } => {
3061                // ROR doesn't have a 16-bit immediate form, use 32-bit
3062                self.encode_thumb32_shift(rd, rn, *shift, 0b11) // ROR type
3063            }
3064
3065            // Register-based shifts (Thumb-2 32-bit)
3066            // Encoding: 11111010 0xxS Rn 1111 Rd 0000 Rm
3067            // xx = shift type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
3068            ArmOp::LslReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b00),
3069            ArmOp::LsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b01),
3070            ArmOp::AsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b10),
3071            ArmOp::RorReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b11),
3072
3073            // RSB (Reverse Subtract): Rd = imm - Rn
3074            // Thumb-2 T2 encoding: 11110 i 0 1110 S Rn | 0 imm3 Rd imm8
3075            ArmOp::Rsb { rd, rn, imm } => {
3076                let rd_bits = reg_to_bits(rd);
3077                let rn_bits = reg_to_bits(rn);
3078                let imm_val = *imm;
3079
3080                let i_bit = (imm_val >> 11) & 1;
3081                let imm3 = (imm_val >> 8) & 0x7;
3082                let imm8 = imm_val & 0xFF;
3083
3084                // hw1: 11110 i 01110 0 Rn  (S=0)
3085                let hw1: u16 = (0xF1C0 | (i_bit << 10) | rn_bits) as u16;
3086                // hw2: 0 imm3 Rd imm8
3087                let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3088
3089                let mut bytes = hw1.to_le_bytes().to_vec();
3090                bytes.extend_from_slice(&hw2.to_le_bytes());
3091                Ok(bytes)
3092            }
3093
3094            // CLZ (Thumb-2 32-bit)
3095            ArmOp::Clz { rd, rm } => {
3096                let rd_bits = reg_to_bits(rd);
3097                let rm_bits = reg_to_bits(rm);
3098
3099                // Thumb-2 CLZ: FAB0 Rm | F8 Rd Rm
3100                // 11111010 1011 Rm | 1111 1000 Rd Rm
3101                let hw1: u16 = (0xFAB0 | rm_bits) as u16;
3102                let hw2: u16 = (0xF080 | (rd_bits << 8) | rm_bits) as u16;
3103
3104                let mut bytes = hw1.to_le_bytes().to_vec();
3105                bytes.extend_from_slice(&hw2.to_le_bytes());
3106                Ok(bytes)
3107            }
3108
3109            // RBIT (Thumb-2 32-bit)
3110            ArmOp::Rbit { rd, rm } => {
3111                let rd_bits = reg_to_bits(rd);
3112                let rm_bits = reg_to_bits(rm);
3113
3114                // Thumb-2 RBIT: FA90 Rm | F0 Rd A0 Rm
3115                // 11111010 1001 Rm | 1111 Rd 1010 Rm
3116                let hw1: u16 = (0xFA90 | rm_bits) as u16;
3117                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rm_bits) as u16;
3118
3119                let mut bytes = hw1.to_le_bytes().to_vec();
3120                bytes.extend_from_slice(&hw2.to_le_bytes());
3121                Ok(bytes)
3122            }
3123
3124            // SXTB (16-bit for low registers)
3125            ArmOp::Sxtb { rd, rm } => {
3126                let rd_bits = reg_to_bits(rd) as u16;
3127                let rm_bits = reg_to_bits(rm) as u16;
3128
3129                if rd_bits < 8 && rm_bits < 8 {
3130                    // SXTB Rd, Rm (16-bit): 1011 0010 01 Rm Rd
3131                    let instr: u16 = 0xB240 | (rm_bits << 3) | rd_bits;
3132                    Ok(instr.to_le_bytes().to_vec())
3133                } else {
3134                    // Thumb-2 SXTB.W: FA4F F(rd)80 (rm)
3135                    // 11111010 0100 1111 | 1111 Rd 10 rotate Rm
3136                    let rd_bits32 = rd_bits as u32;
3137                    let rm_bits32 = rm_bits as u32;
3138                    let hw1: u16 = 0xFA4F;
3139                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3140                    let mut bytes = hw1.to_le_bytes().to_vec();
3141                    bytes.extend_from_slice(&hw2.to_le_bytes());
3142                    Ok(bytes)
3143                }
3144            }
3145
3146            // SXTH (16-bit for low registers)
3147            ArmOp::Sxth { rd, rm } => {
3148                let rd_bits = reg_to_bits(rd) as u16;
3149                let rm_bits = reg_to_bits(rm) as u16;
3150
3151                if rd_bits < 8 && rm_bits < 8 {
3152                    // SXTH Rd, Rm (16-bit): 1011 0010 00 Rm Rd
3153                    let instr: u16 = 0xB200 | (rm_bits << 3) | rd_bits;
3154                    Ok(instr.to_le_bytes().to_vec())
3155                } else {
3156                    // Thumb-2 SXTH.W: FA0F F(rd)80 (rm)
3157                    // 11111010 0000 1111 | 1111 Rd 10 rotate Rm
3158                    let rd_bits32 = rd_bits as u32;
3159                    let rm_bits32 = rm_bits as u32;
3160                    let hw1: u16 = 0xFA0F;
3161                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3162                    let mut bytes = hw1.to_le_bytes().to_vec();
3163                    bytes.extend_from_slice(&hw2.to_le_bytes());
3164                    Ok(bytes)
3165                }
3166            }
3167
3168            // UXTB Rd,Rm — zero-extend byte (rd = rm & 0xff)
3169            ArmOp::Uxtb { rd, rm } => {
3170                let rd_bits = reg_to_bits(rd) as u16;
3171                let rm_bits = reg_to_bits(rm) as u16;
3172                if rd_bits < 8 && rm_bits < 8 {
3173                    // UXTB Rd, Rm (16-bit): 1011 0010 11 Rm Rd
3174                    let instr: u16 = 0xB2C0 | (rm_bits << 3) | rd_bits;
3175                    Ok(instr.to_le_bytes().to_vec())
3176                } else {
3177                    // Thumb-2 UXTB.W: FA5F F(rd)80 (rm)
3178                    let hw1: u16 = 0xFA5F;
3179                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3180                    let mut bytes = hw1.to_le_bytes().to_vec();
3181                    bytes.extend_from_slice(&hw2.to_le_bytes());
3182                    Ok(bytes)
3183                }
3184            }
3185
3186            // UXTH Rd,Rm — zero-extend halfword (rd = rm & 0xffff)
3187            ArmOp::Uxth { rd, rm } => {
3188                let rd_bits = reg_to_bits(rd) as u16;
3189                let rm_bits = reg_to_bits(rm) as u16;
3190                if rd_bits < 8 && rm_bits < 8 {
3191                    // UXTH Rd, Rm (16-bit): 1011 0010 10 Rm Rd
3192                    let instr: u16 = 0xB280 | (rm_bits << 3) | rd_bits;
3193                    Ok(instr.to_le_bytes().to_vec())
3194                } else {
3195                    // Thumb-2 UXTH.W: FA1F F(rd)80 (rm)
3196                    let hw1: u16 = 0xFA1F;
3197                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3198                    let mut bytes = hw1.to_le_bytes().to_vec();
3199                    bytes.extend_from_slice(&hw2.to_le_bytes());
3200                    Ok(bytes)
3201                }
3202            }
3203
3204            // CMP (can be 16-bit for low registers)
3205            ArmOp::Cmp { rn, op2 } => {
3206                let rn_bits = reg_to_bits(rn) as u16;
3207
3208                if let Operand2::Imm(imm) = op2 {
3209                    // Only use 16-bit encoding for non-negative immediates 0-255
3210                    // Negative immediates must use 32-bit encoding
3211                    if *imm >= 0 && *imm <= 255 && rn_bits < 8 {
3212                        // CMP Rn, #imm8 (16-bit): 0010 1 Rn imm8
3213                        let instr: u16 = 0x2800 | (rn_bits << 8) | (*imm as u16 & 0xFF);
3214                        Ok(instr.to_le_bytes().to_vec())
3215                    } else {
3216                        self.encode_thumb32_cmp_imm(rn, *imm as u32)
3217                    }
3218                } else if let Operand2::Reg(rm) = op2 {
3219                    let rm_bits = reg_to_bits(rm) as u16;
3220                    if rn_bits < 8 && rm_bits < 8 {
3221                        // CMP Rn, Rm (16-bit low): 0100 0010 10 Rm Rn
3222                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
3223                        Ok(instr.to_le_bytes().to_vec())
3224                    } else {
3225                        // CMP Rn, Rm (16-bit high): 0100 0101 N Rm Rn[2:0]
3226                        let n_bit = (rn_bits >> 3) & 1;
3227                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
3228                        Ok(instr.to_le_bytes().to_vec())
3229                    }
3230                } else {
3231                    let instr: u16 = 0xBF00;
3232                    Ok(instr.to_le_bytes().to_vec())
3233                }
3234            }
3235
3236            // CMN (Compare Negative) - computes Rn + op2 and sets flags
3237            // CMN Rn, #1 sets Z flag if Rn == -1 (since -1 + 1 = 0)
3238            ArmOp::Cmn { rn, op2 } => {
3239                let rn_bits = reg_to_bits(rn) as u16;
3240
3241                if let Operand2::Imm(imm) = op2 {
3242                    // CMN.W Rn, #imm (32-bit): i:imm3:imm8 is a ThumbExpandImm
3243                    // modified immediate (the field sits in imm3=hw2[14:12],
3244                    // imm8=hw2[7:0], i=hw1[10]). Encode it correctly, or error on
3245                    // an un-encodable value — replacing the old silent `0xBF00`
3246                    // NOP (the last of the silent-miscompile data-proc encoders).
3247                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3248                        synth_core::Error::synthesis(
3249                            "CMN immediate is not a valid ThumbExpandImm — materialize into a register",
3250                        )
3251                    })?;
3252                    let i_bit = (field >> 11) & 1;
3253                    let imm3 = (field >> 8) & 0x7;
3254                    let imm8 = field & 0xFF;
3255                    let hw1: u16 = (0xF110 | (i_bit << 10) as u16) | rn_bits;
3256                    let hw2: u16 = (imm3 << 12) as u16 | 0x0F00 | imm8 as u16;
3257                    let mut bytes = hw1.to_le_bytes().to_vec();
3258                    bytes.extend_from_slice(&hw2.to_le_bytes());
3259                    Ok(bytes)
3260                } else if let Operand2::Reg(rm) = op2 {
3261                    let rm_bits = reg_to_bits(rm) as u16;
3262                    // 16-bit CMN (T1) only encodes R0-R7; high registers overflow
3263                    // the 3-bit fields and corrupt the operands (#184, the #180
3264                    // class). CMN has no high-register 16-bit form, so fall back
3265                    // to 32-bit CMN.W (T2): EB10 Rn | 0F00 Rm (ADD.W with S=1 and
3266                    // Rd discarded as PC/1111).
3267                    if rn_bits < 8 && rm_bits < 8 {
3268                        // CMN Rn, Rm (16-bit): 0100 0010 11 Rm Rn
3269                        let instr: u16 = 0x42C0 | (rm_bits << 3) | rn_bits;
3270                        Ok(instr.to_le_bytes().to_vec())
3271                    } else {
3272                        let hw1: u16 = 0xEB10 | rn_bits;
3273                        let hw2: u16 = 0x0F00 | rm_bits;
3274                        let mut bytes = hw1.to_le_bytes().to_vec();
3275                        bytes.extend_from_slice(&hw2.to_le_bytes());
3276                        Ok(bytes)
3277                    }
3278                } else {
3279                    Ok(vec![0xBF, 0x00])
3280                }
3281            }
3282
3283            // LDR (can be 16-bit for simple cases)
3284            ArmOp::Ldr { rd, addr } => {
3285                let rd_bits = reg_to_bits(rd);
3286                let base_bits = reg_to_bits(&addr.base);
3287
3288                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3289                if let Some(offset_reg) = &addr.offset_reg {
3290                    let rm_bits = reg_to_bits(offset_reg);
3291
3292                    // If there's also an immediate offset, we need to ADD it first
3293                    if addr.offset != 0 {
3294                        // Use R12 (IP) as scratch to avoid clobbering the address register
3295                        // ADD R12, Rm, #offset; LDR Rd, [base, R12]
3296                        let scratch = Reg::R12;
3297                        let mut bytes =
3298                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3299                        bytes.extend(self.encode_thumb32_ldr_reg(rd, &addr.base, &scratch)?);
3300                        return Ok(bytes);
3301                    }
3302
3303                    // Simple register offset: LDR Rd, [Rn, Rm]
3304                    // 16-bit: only if Rd, Rn, Rm < R8
3305                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3306                        // LDR Rd, [Rn, Rm] (16-bit): 0101 100 Rm Rn Rd
3307                        let instr: u16 = 0x5800
3308                            | ((rm_bits as u16) << 6)
3309                            | ((base_bits as u16) << 3)
3310                            | (rd_bits as u16);
3311                        return Ok(instr.to_le_bytes().to_vec());
3312                    }
3313
3314                    // 32-bit register offset
3315                    return self.encode_thumb32_ldr_reg(rd, &addr.base, offset_reg);
3316                }
3317
3318                // Immediate offset mode [base, #imm]
3319                let offset = addr.offset as u32;
3320
3321                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3322                    // LDR Rd, [Rn, #imm5*4] (16-bit): 0110 1 imm5 Rn Rd
3323                    let imm5 = (offset >> 2) as u16;
3324                    let instr: u16 =
3325                        0x6800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3326                    Ok(instr.to_le_bytes().to_vec())
3327                } else {
3328                    self.encode_thumb32_ldr(rd, &addr.base, offset)
3329                }
3330            }
3331
3332            // STR (can be 16-bit for simple cases)
3333            ArmOp::Str { rd, addr } => {
3334                let rd_bits = reg_to_bits(rd);
3335                let base_bits = reg_to_bits(&addr.base);
3336
3337                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3338                if let Some(offset_reg) = &addr.offset_reg {
3339                    let rm_bits = reg_to_bits(offset_reg);
3340
3341                    // If there's also an immediate offset, we need to ADD it first
3342                    if addr.offset != 0 {
3343                        // Use R12 (IP) as scratch to avoid clobbering the address register
3344                        // ADD R12, Rm, #offset; STR Rd, [base, R12]
3345                        let scratch = Reg::R12;
3346                        let mut bytes =
3347                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3348                        bytes.extend(self.encode_thumb32_str_reg(rd, &addr.base, &scratch)?);
3349                        return Ok(bytes);
3350                    }
3351
3352                    // Simple register offset: STR Rd, [Rn, Rm]
3353                    // 16-bit: only if Rd, Rn, Rm < R8
3354                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3355                        // STR Rd, [Rn, Rm] (16-bit): 0101 000 Rm Rn Rd
3356                        let instr: u16 = 0x5000
3357                            | ((rm_bits as u16) << 6)
3358                            | ((base_bits as u16) << 3)
3359                            | (rd_bits as u16);
3360                        return Ok(instr.to_le_bytes().to_vec());
3361                    }
3362
3363                    // 32-bit register offset
3364                    return self.encode_thumb32_str_reg(rd, &addr.base, offset_reg);
3365                }
3366
3367                // Immediate offset mode [base, #imm]
3368                let offset = addr.offset as u32;
3369
3370                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3371                    // STR Rd, [Rn, #imm5*4] (16-bit): 0110 0 imm5 Rn Rd
3372                    let imm5 = (offset >> 2) as u16;
3373                    let instr: u16 =
3374                        0x6000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3375                    Ok(instr.to_le_bytes().to_vec())
3376                } else {
3377                    self.encode_thumb32_str(rd, &addr.base, offset)
3378                }
3379            }
3380
3381            // LDRB (Thumb-2)
3382            ArmOp::Ldrb { rd, addr } => {
3383                let rd_bits = reg_to_bits(rd);
3384                let base_bits = reg_to_bits(&addr.base);
3385
3386                if let Some(offset_reg) = &addr.offset_reg {
3387                    if addr.offset != 0 {
3388                        let scratch = Reg::R12;
3389                        let mut bytes =
3390                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3391                        bytes.extend(self.encode_thumb32_ldrb_reg(rd, &addr.base, &scratch)?);
3392                        return Ok(bytes);
3393                    }
3394                    return self.encode_thumb32_ldrb_reg(rd, &addr.base, offset_reg);
3395                }
3396
3397                let offset = addr.offset as u32;
3398                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3399                    // LDRB Rd, [Rn, #imm5] (16-bit): 0111 1 imm5 Rn Rd
3400                    let instr: u16 = 0x7800
3401                        | ((offset as u16) << 6)
3402                        | ((base_bits as u16) << 3)
3403                        | (rd_bits as u16);
3404                    Ok(instr.to_le_bytes().to_vec())
3405                } else {
3406                    self.encode_thumb32_ldrb_imm(rd, &addr.base, offset)
3407                }
3408            }
3409
3410            // LDRSB (Thumb-2)
3411            ArmOp::Ldrsb { rd, addr } => {
3412                let rd_bits = reg_to_bits(rd);
3413                let base_bits = reg_to_bits(&addr.base);
3414
3415                if let Some(offset_reg) = &addr.offset_reg {
3416                    if addr.offset != 0 {
3417                        let scratch = Reg::R12;
3418                        let mut bytes =
3419                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3420                        bytes.extend(self.encode_thumb32_ldrsb_reg(rd, &addr.base, &scratch)?);
3421                        return Ok(bytes);
3422                    }
3423                    return self.encode_thumb32_ldrsb_reg(rd, &addr.base, offset_reg);
3424                }
3425
3426                let offset = addr.offset as u32;
3427                // LDRSB has no 16-bit immediate form (only register)
3428                // For 16-bit reg form: only if Rd, Rn, Rm < R8
3429                if rd_bits < 8 && base_bits < 8 && offset == 0 {
3430                    // No immediate 16-bit encoding for LDRSB; use 32-bit
3431                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3432                } else {
3433                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3434                }
3435            }
3436
3437            // LDRH (Thumb-2)
3438            ArmOp::Ldrh { rd, addr } => {
3439                let rd_bits = reg_to_bits(rd);
3440                let base_bits = reg_to_bits(&addr.base);
3441
3442                if let Some(offset_reg) = &addr.offset_reg {
3443                    if addr.offset != 0 {
3444                        let scratch = Reg::R12;
3445                        let mut bytes =
3446                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3447                        bytes.extend(self.encode_thumb32_ldrh_reg(rd, &addr.base, &scratch)?);
3448                        return Ok(bytes);
3449                    }
3450                    return self.encode_thumb32_ldrh_reg(rd, &addr.base, offset_reg);
3451                }
3452
3453                let offset = addr.offset as u32;
3454                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3455                    // LDRH Rd, [Rn, #imm5*2] (16-bit): 1000 1 imm5 Rn Rd
3456                    let imm5 = (offset >> 1) as u16;
3457                    let instr: u16 =
3458                        0x8800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3459                    Ok(instr.to_le_bytes().to_vec())
3460                } else {
3461                    self.encode_thumb32_ldrh_imm(rd, &addr.base, offset)
3462                }
3463            }
3464
3465            // LDRSH (Thumb-2)
3466            ArmOp::Ldrsh { rd, addr } => {
3467                if let Some(offset_reg) = &addr.offset_reg {
3468                    if addr.offset != 0 {
3469                        let scratch = Reg::R12;
3470                        let mut bytes =
3471                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3472                        bytes.extend(self.encode_thumb32_ldrsh_reg(rd, &addr.base, &scratch)?);
3473                        return Ok(bytes);
3474                    }
3475                    return self.encode_thumb32_ldrsh_reg(rd, &addr.base, offset_reg);
3476                }
3477
3478                let offset = addr.offset as u32;
3479                self.encode_thumb32_ldrsh_imm(rd, &addr.base, offset)
3480            }
3481
3482            // STRB (Thumb-2)
3483            ArmOp::Strb { rd, addr } => {
3484                let rd_bits = reg_to_bits(rd);
3485                let base_bits = reg_to_bits(&addr.base);
3486
3487                if let Some(offset_reg) = &addr.offset_reg {
3488                    if addr.offset != 0 {
3489                        let scratch = Reg::R12;
3490                        let mut bytes =
3491                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3492                        bytes.extend(self.encode_thumb32_strb_reg(rd, &addr.base, &scratch)?);
3493                        return Ok(bytes);
3494                    }
3495                    return self.encode_thumb32_strb_reg(rd, &addr.base, offset_reg);
3496                }
3497
3498                let offset = addr.offset as u32;
3499                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3500                    // STRB Rd, [Rn, #imm5] (16-bit): 0111 0 imm5 Rn Rd
3501                    let instr: u16 = 0x7000
3502                        | ((offset as u16) << 6)
3503                        | ((base_bits as u16) << 3)
3504                        | (rd_bits as u16);
3505                    Ok(instr.to_le_bytes().to_vec())
3506                } else {
3507                    self.encode_thumb32_strb_imm(rd, &addr.base, offset)
3508                }
3509            }
3510
3511            // STRH (Thumb-2)
3512            ArmOp::Strh { rd, addr } => {
3513                let rd_bits = reg_to_bits(rd);
3514                let base_bits = reg_to_bits(&addr.base);
3515
3516                if let Some(offset_reg) = &addr.offset_reg {
3517                    if addr.offset != 0 {
3518                        let scratch = Reg::R12;
3519                        let mut bytes =
3520                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3521                        bytes.extend(self.encode_thumb32_strh_reg(rd, &addr.base, &scratch)?);
3522                        return Ok(bytes);
3523                    }
3524                    return self.encode_thumb32_strh_reg(rd, &addr.base, offset_reg);
3525                }
3526
3527                let offset = addr.offset as u32;
3528                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3529                    // STRH Rd, [Rn, #imm5*2] (16-bit): 1000 0 imm5 Rn Rd
3530                    let imm5 = (offset >> 1) as u16;
3531                    let instr: u16 =
3532                        0x8000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3533                    Ok(instr.to_le_bytes().to_vec())
3534                } else {
3535                    self.encode_thumb32_strh_imm(rd, &addr.base, offset)
3536                }
3537            }
3538
3539            // MemorySize (Thumb-2)
3540            ArmOp::MemorySize { rd } => {
3541                // LSR rd, R10, #16 — memory size in bytes / 65536 = pages
3542                // Thumb-2 16-bit: LSRS Rd, Rm, #imm5 — 0000 1 imm5 Rm Rd
3543                let rd_bits = reg_to_bits(rd);
3544                let r10_bits = reg_to_bits(&Reg::R10);
3545                if rd_bits < 8 && r10_bits < 8 {
3546                    let instr: u16 =
3547                        0x0800 | (16u16 << 6) | ((r10_bits as u16) << 3) | (rd_bits as u16);
3548                    Ok(instr.to_le_bytes().to_vec())
3549                } else {
3550                    // Thumb-2 32-bit LSR: 1110 1010 010 0 1111 | 0 imm3 Rd imm2 01 Rm
3551                    let imm5: u32 = 16;
3552                    let imm3 = (imm5 >> 2) & 0x7;
3553                    let imm2 = imm5 & 0x3;
3554                    let hw1: u16 = 0xEA4F;
3555                    let hw2: u16 =
3556                        ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | 0x10 | r10_bits) as u16;
3557                    let mut bytes = hw1.to_le_bytes().to_vec();
3558                    bytes.extend_from_slice(&hw2.to_le_bytes());
3559                    Ok(bytes)
3560                }
3561            }
3562
3563            // MemoryGrow (Thumb-2)
3564            ArmOp::MemoryGrow { rd, .. } => {
3565                // On embedded with fixed memory, always return -1 (failure)
3566                // MVN rd, #0 → MOV rd, #-1
3567                // Thumb-2 32-bit: MVN: 1111 0 i 0 0 0 1 1 0 1111 | 0 imm3 Rd imm8
3568                let rd_bits = reg_to_bits(rd);
3569                let hw1: u16 = 0xF06F; // MVN with i=0
3570                let hw2: u16 = (rd_bits << 8) as u16; // imm8=0 → ~0 = 0xFFFFFFFF = -1
3571                let mut bytes = hw1.to_le_bytes().to_vec();
3572                bytes.extend_from_slice(&hw2.to_le_bytes());
3573                Ok(bytes)
3574            }
3575
3576            // BX (16-bit)
3577            ArmOp::Bx { rm } => {
3578                let rm_bits = reg_to_bits(rm) as u16;
3579                // BX Rm (16-bit): 0100 0111 0 Rm 000
3580                let instr: u16 = 0x4700 | (rm_bits << 3);
3581                Ok(instr.to_le_bytes().to_vec())
3582            }
3583
3584            // BLX (16-bit) - Branch with Link and Exchange
3585            // BLX Rm: 0100 0111 1 Rm 000
3586            ArmOp::Blx { rm } => {
3587                let rm_bits = reg_to_bits(rm) as u16;
3588                let instr: u16 = 0x4780 | (rm_bits << 3);
3589                Ok(instr.to_le_bytes().to_vec())
3590            }
3591
3592            // CallIndirect - indirect function call via table lookup
3593            // table_index_reg contains the table index
3594            // Generates: LSL R12, idx, #2; LDR R12, [R12, table_base]; BLX R12
3595            ArmOp::CallIndirect {
3596                rd: _,
3597                type_idx: _,
3598                table_index_reg,
3599            } => {
3600                let idx_reg = reg_to_bits(table_index_reg);
3601                let mut bytes = Vec::new();
3602
3603                // For now, we generate code that:
3604                // 1. Multiplies index by 4 (function pointer size)
3605                // 2. Loads function pointer from table (assumes table base in R11)
3606                // 3. Calls the function via BLX
3607                //
3608                // Table base setup must be done by caller/runtime.
3609                // This is a simplified implementation - full support needs:
3610                // - Table base address resolution
3611                // - Type signature checking
3612                // - Bounds checking
3613
3614                // LSL R12, idx_reg, #2 (multiply index by 4)
3615                // Thumb-2 MOV with shift: 11101010 010 S 1111 | 0 imm3 Rd imm2 type Rm
3616                // LSL: type=00 (bits 5:4), imm5=2 -> imm3=000, imm2=10 (bits 7:6)
3617                // #597: the shift amount was previously shifted into bits 5:4 —
3618                // the TYPE field — encoding `mov.w ip, rm, ASR #32`, which
3619                // destroyed the index and dispatched table entry 0 for every
3620                // call. imm2 lives at bits 7:6.
3621                let hw1: u16 = 0xEA4F_u16; // MOV.W R12, Rm, LSL #2
3622                let hw2: u16 = ((0x0C00 | (0b10 << 6)) | idx_reg) as u16;
3623                bytes.extend_from_slice(&hw1.to_le_bytes());
3624                bytes.extend_from_slice(&hw2.to_le_bytes());
3625
3626                // LDR R12, [R11, R12] - load function pointer
3627                // Thumb-2 LDR (register): 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
3628                // Rn=R11, Rt=R12, Rm=R12, imm2=00 (no shift)
3629                let ldr_hw1: u16 = 0xF85B; // LDR.W Rt, [R11, Rm]
3630                let ldr_hw2: u16 = 0xC00C; // Rt=R12, imm2=00, Rm=R12
3631                bytes.extend_from_slice(&ldr_hw1.to_le_bytes());
3632                bytes.extend_from_slice(&ldr_hw2.to_le_bytes());
3633
3634                // BLX R12 (call function indirectly)
3635                // BLX Rm (16-bit): 0100 0111 1 Rm 000
3636                let blx: u16 = 0x47E0; // BLX R12
3637                bytes.extend_from_slice(&blx.to_le_bytes());
3638
3639                Ok(bytes)
3640            }
3641
3642            // Label pseudo-instruction: emits no machine code
3643            ArmOp::Label { .. } => Ok(Vec::new()),
3644
3645            // Conditional branch to label (generic) - offset 0, will be patched
3646            ArmOp::Bcc { cond, label: _ } => {
3647                use synth_synthesis::Condition;
3648                let cond_bits: u16 = match cond {
3649                    Condition::EQ => 0x0,
3650                    Condition::NE => 0x1,
3651                    Condition::HS => 0x2,
3652                    Condition::LO => 0x3,
3653                    Condition::HI => 0x8,
3654                    Condition::LS => 0x9,
3655                    Condition::GE => 0xA,
3656                    Condition::LT => 0xB,
3657                    Condition::GT => 0xC,
3658                    Condition::LE => 0xD,
3659                };
3660                // 16-bit B<cond> with offset 0: 1101 cond imm8
3661                let instr: u16 = 0xD000 | (cond_bits << 8);
3662                Ok(instr.to_le_bytes().to_vec())
3663            }
3664
3665            // Branch instructions
3666            ArmOp::B { label: _ } => {
3667                // Simplified: B.N with offset 0
3668                // For real usage, would need label resolution
3669                let instr: u16 = 0xE000; // B.N #0
3670                Ok(instr.to_le_bytes().to_vec())
3671            }
3672
3673            // BHS (Branch if Higher or Same) - used for bounds checking
3674            // Condition code: 0x2 (C set)
3675            ArmOp::Bhs { label: _ } => {
3676                // 16-bit B<cond> with offset 0: 1101 cond imm8
3677                // cond = 0x2 (HS)
3678                let instr: u16 = 0xD200; // BHS.N #0
3679                Ok(instr.to_le_bytes().to_vec())
3680            }
3681
3682            // BLO (Branch if Lower) - complementary to BHS
3683            // Condition code: 0x3 (C clear)
3684            ArmOp::Blo { label: _ } => {
3685                // 16-bit B<cond> with offset 0: 1101 cond imm8
3686                // cond = 0x3 (LO)
3687                let instr: u16 = 0xD300; // BLO.N #0
3688                Ok(instr.to_le_bytes().to_vec())
3689            }
3690
3691            // Branch with numeric offset (Thumb-2)
3692            // Thumb-2 B.W instruction: 32-bit with +-16MB range
3693            ArmOp::BOffset { offset } => {
3694                // offset is already the halfword displacement: (target - branch - 4) / 2
3695                // This is the raw encoded value, accounting for variable-length instructions
3696                let halfword_offset = *offset;
3697
3698                // 16-bit B.N encoding: 1110 0 imm11 (11-bit signed halfword offset)
3699                // Range: -1024 to +1022 halfwords
3700                if (-1024..=1022).contains(&halfword_offset) {
3701                    // 16-bit B.N encoding: 1110 0 imm11
3702                    let imm11 = (halfword_offset as u16) & 0x7FF;
3703                    let instr: u16 = 0xE000 | imm11;
3704                    Ok(instr.to_le_bytes().to_vec())
3705                } else {
3706                    // 32-bit B.W encoding for larger offsets
3707                    // First halfword: 1111 0 S imm10
3708                    // Second halfword: 10 J1 0 J2 imm11
3709                    // Total offset = SignExtend(S:I1:I2:imm10:imm11:0)
3710                    // where I1 = NOT(J1 XOR S), I2 = NOT(J2 XOR S)
3711
3712                    // The B.W (T4) encoding packs the signed offset as:
3713                    //   S:I1:I2:imm10:imm11:0  (25-bit signed, halfword-aligned)
3714                    // where J1 = NOT(I1 XOR S), J2 = NOT(I2 XOR S)
3715                    // Input halfword_offset already equals (target - PC - 4) / 2,
3716                    // so the full byte offset = halfword_offset << 1.
3717                    // The encoding fields split that 25-bit signed value (including the
3718                    // implicit trailing zero) as: S | imm10 | imm11
3719                    // with I1 = bit 23 and I2 = bit 22 of the signed offset.
3720                    let signed_offset = halfword_offset << 1; // byte offset
3721                    let s = if signed_offset < 0 { 1u32 } else { 0u32 };
3722                    let uoffset = signed_offset as u32;
3723                    let imm10 = (uoffset >> 12) & 0x3FF; // bits [21:12]
3724                    let imm11 = (uoffset >> 1) & 0x7FF; // bits [11:1]
3725                    let i1 = (uoffset >> 23) & 1; // bit 23
3726                    let i2 = (uoffset >> 22) & 1; // bit 22
3727                    let j1 = (!(i1 ^ s)) & 1; // J1 = NOT(I1 XOR S)
3728                    let j2 = (!(i2 ^ s)) & 1; // J2 = NOT(I2 XOR S)
3729
3730                    let hw1: u16 = (0xF000 | (s << 10) | imm10) as u16;
3731                    let hw2: u16 = (0x9000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
3732
3733                    let mut bytes = hw1.to_le_bytes().to_vec();
3734                    bytes.extend_from_slice(&hw2.to_le_bytes());
3735                    Ok(bytes)
3736                }
3737            }
3738
3739            // Conditional branch with numeric offset (Thumb-2)
3740            ArmOp::BCondOffset { cond, offset } => {
3741                use synth_synthesis::Condition;
3742                let cond_bits: u16 = match cond {
3743                    Condition::EQ => 0x0,
3744                    Condition::NE => 0x1,
3745                    Condition::HS => 0x2,
3746                    Condition::LO => 0x3,
3747                    Condition::HI => 0x8,
3748                    Condition::LS => 0x9,
3749                    Condition::GE => 0xA,
3750                    Condition::LT => 0xB,
3751                    Condition::GT => 0xC,
3752                    Condition::LE => 0xD,
3753                };
3754
3755                // offset is already the halfword displacement: (target - branch - 4) / 2
3756                // This is the raw imm8 value for 16-bit B<cond> encoding
3757                let halfword_offset = *offset;
3758
3759                // 16-bit B<cond> encoding: 1101 cond imm8
3760                // Range: -256 to +254 halfwords (imm8 is sign-extended and shifted left 1)
3761                if (-128..=127).contains(&halfword_offset) {
3762                    let imm8 = (halfword_offset as u16) & 0xFF;
3763                    let instr: u16 = 0xD000 | (cond_bits << 8) | imm8;
3764                    Ok(instr.to_le_bytes().to_vec())
3765                } else {
3766                    // 32-bit B<cond>.W for larger offsets
3767                    // First halfword: 1111 0 S cond imm6
3768                    // Second halfword: 10 J1 0 J2 imm11
3769                    let offset = halfword_offset >> 1;
3770                    let s = if offset < 0 { 1u32 } else { 0u32 };
3771                    let imm6 = ((offset >> 11) as u32) & 0x3F;
3772                    let imm11 = (offset as u32) & 0x7FF;
3773                    let j1 = if s == 1 { 1 } else { 0 };
3774                    let j2 = if s == 1 { 1 } else { 0 };
3775
3776                    let hw1: u16 = (0xF000 | (s << 10) | ((cond_bits as u32) << 6) | imm6) as u16;
3777                    let hw2: u16 = (0x8000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
3778
3779                    let mut bytes = hw1.to_le_bytes().to_vec();
3780                    bytes.extend_from_slice(&hw2.to_le_bytes());
3781                    Ok(bytes)
3782                }
3783            }
3784
3785            ArmOp::Bl { label: _ } => {
3786                // BL is always 32-bit in Thumb-2, encoded here as a relocatable
3787                // placeholder; an R_ARM_THM_CALL relocation patches the target
3788                // (see arm_backend.rs). The placeholder must carry an embedded
3789                // addend of -4 so the relocation nets to exactly the symbol S.
3790                //
3791                // Thumb BL computes `target = (P + 4) + signed_offset`. Under
3792                // R_ARM_THM_CALL the linker resolves using the in-place addend;
3793                // a 0xF800 placeholder (addend 0) lands at S+4 — every call one
3794                // instruction past the callee entry (#174). The correct
3795                // placeholder is what `gas` emits for `bl <extern>`:
3796                //   f7ff fffe  ->  `bl <self>`  (S=1, J1=J2=1, imm = -4 addend),
3797                // i.e. hw1=0xF7FF, hw2=0xFFFE. This nets to S, not S+4.
3798                // (The earlier 0xD000 was worse still — a ~+0x600000 addend,
3799                // the garbage `bl c0000c` and "truncated to fit" of #167.)
3800                let hw1: u16 = 0xF7FF;
3801                let hw2: u16 = 0xFFFE;
3802                let mut bytes = hw1.to_le_bytes().to_vec();
3803                bytes.extend_from_slice(&hw2.to_le_bytes());
3804                Ok(bytes)
3805            }
3806
3807            // MVN
3808            ArmOp::Mvn { rd, op2 } => {
3809                if let Operand2::Reg(rm) = op2 {
3810                    let rd_bits = reg_to_bits(rd) as u16;
3811                    let rm_bits = reg_to_bits(rm) as u16;
3812
3813                    if rd_bits < 8 && rm_bits < 8 {
3814                        // MVNS Rd, Rm (16-bit): 0100 0011 11 Rm Rd
3815                        let instr: u16 = 0x43C0 | (rm_bits << 3) | rd_bits;
3816                        Ok(instr.to_le_bytes().to_vec())
3817                    } else {
3818                        // 32-bit MVN
3819                        let hw1: u16 = 0xEA6F_u16;
3820                        let hw2: u16 = ((reg_to_bits(rd) << 8) | reg_to_bits(rm)) as u16;
3821                        let mut bytes = hw1.to_le_bytes().to_vec();
3822                        bytes.extend_from_slice(&hw2.to_le_bytes());
3823                        Ok(bytes)
3824                    }
3825                } else {
3826                    let instr: u16 = 0xBF00;
3827                    Ok(instr.to_le_bytes().to_vec())
3828                }
3829            }
3830
3831            // MOVW - Move Wide (Thumb-2 32-bit)
3832            ArmOp::Movw { rd, imm16 } => {
3833                self.encode_thumb32_movw_raw(reg_to_bits(rd), *imm16 as u32)
3834            }
3835
3836            // MOVT - Move Top (Thumb-2 32-bit)
3837            ArmOp::Movt { rd, imm16 } => {
3838                self.encode_thumb32_movt_raw(reg_to_bits(rd), *imm16 as u32)
3839            }
3840
3841            // #237: symbol-relative MOVW/MOVT. Encode the addend's low/high 16
3842            // bits in place; the backend records an R_ARM_MOVW_ABS_NC /
3843            // R_ARM_MOVT_ABS relocation against `symbol`, so the linker adds the
3844            // symbol's final address to the in-place addend (REL semantics).
3845            ArmOp::MovwSym { rd, addend, .. } => {
3846                self.encode_thumb32_movw_raw(reg_to_bits(rd), (*addend as u32) & 0xffff)
3847            }
3848            ArmOp::MovtSym { rd, addend, .. } => {
3849                self.encode_thumb32_movt_raw(reg_to_bits(rd), ((*addend as u32) >> 16) & 0xffff)
3850            }
3851
3852            // #345: literal-pool address load — emit a PLACEHOLDER `LDR.W rd,
3853            // [pc, #0]` (U=1, imm12=0). The backend (arm_backend.rs) places the
3854            // 4-byte pool word at the end of the function, records the R_ARM_ABS32
3855            // relocation against `symbol+addend`, and patches the imm12 with the
3856            // real PC-relative distance once the pool offset is known.
3857            // Encoding T2: 1111 1000 1101 1111 | Rt(4) imm12(12), with the literal
3858            // base = Align(PC,4) and PC = address of this instruction + 4.
3859            ArmOp::LdrSym { rd, .. } => {
3860                let rt = reg_to_bits(rd) as u16;
3861                let hw1: u16 = 0xF8DF; // LDR.W (literal), U=1
3862                let hw2: u16 = rt << 12; // imm12 = 0 placeholder
3863                let mut bytes = Vec::with_capacity(4);
3864                bytes.extend_from_slice(&hw1.to_le_bytes());
3865                bytes.extend_from_slice(&hw2.to_le_bytes());
3866                Ok(bytes)
3867            }
3868
3869            // SetCond: Materialize condition flag into register (0 or 1)
3870            // Strategy: ITE <cond>; MOV Rd, #1; MOV Rd, #0
3871            // IMPORTANT: Must use ITE (If-Then-Else) because 16-bit Thumb MOV
3872            // always sets flags (MOVS). We need to evaluate the condition BEFORE
3873            // any MOV instruction clobbers the flags from CMP.
3874            ArmOp::SetCond { rd, cond } => {
3875                let rd_bits = reg_to_bits(rd) as u16;
3876
3877                // Condition code encoding for IT block
3878                use synth_synthesis::Condition;
3879                let cond_bits: u16 = match cond {
3880                    Condition::EQ => 0x0,
3881                    Condition::NE => 0x1,
3882                    Condition::LT => 0xB,
3883                    Condition::LE => 0xD,
3884                    Condition::GT => 0xC,
3885                    Condition::GE => 0xA,
3886                    Condition::LO => 0x3, // CC/LO (unsigned <)
3887                    Condition::LS => 0x9, // LS (unsigned <=)
3888                    Condition::HI => 0x8, // HI (unsigned >)
3889                    Condition::HS => 0x2, // CS/HS (unsigned >=)
3890                };
3891
3892                // ITE <cond>: encodes If-Then-Else block
3893                // The mask field depends on firstcond[0]:
3894                // - If firstcond[0] = 0: mask = 0xC for TE pattern (ITE EQ = BF0C)
3895                // - If firstcond[0] = 1: mask = 0x4 for TE pattern (ITE NE = BF14)
3896                let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
3897                let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
3898
3899                // Materialize 0/1 into Rd. The 16-bit MOVS (T1) encodes Rd in a
3900                // 3-bit field (bits[10:8]) — only R0–R7. For a high register
3901                // (R8–R12) `rd_bits << 8` overflows into bit 11 and silently
3902                // turns MOVS into CMP (00100 → 00101), corrupting the result
3903                // (this mis-materialized gale's `has_waiter`, so its `local.set`
3904                // stored a stale register → the binary-sem WAKE dispatch read
3905                // garbage). Use the 32-bit MOV.W (T2) for high registers, which
3906                // has a 4-bit Rd field. MOV.W with S=0 doesn't set flags, which
3907                // is fine inside the ITE (the materialized value is the result;
3908                // the flags are not consumed afterwards).
3909                let mut bytes = ite_instr.to_le_bytes().to_vec();
3910                let push_mov = |bytes: &mut Vec<u8>, imm: u16| {
3911                    if rd_bits <= 7 {
3912                        let m: u16 = 0x2000 | (rd_bits << 8) | imm; // 16-bit MOVS Rd,#imm
3913                        bytes.extend_from_slice(&m.to_le_bytes());
3914                    } else {
3915                        // 32-bit MOV.W Rd, #imm (T2): F04F | (Rd<<8) | imm8
3916                        let hw1: u16 = 0xF04F;
3917                        let hw2: u16 = (rd_bits << 8) | imm;
3918                        bytes.extend_from_slice(&hw1.to_le_bytes());
3919                        bytes.extend_from_slice(&hw2.to_le_bytes());
3920                    }
3921                };
3922                push_mov(&mut bytes, 1); // Then branch (condition true)  → 1
3923                push_mov(&mut bytes, 0); // Else branch (condition false) → 0
3924                Ok(bytes)
3925            }
3926
3927            // I64SetCond: Compare two i64 register pairs, result 0/1 in rd
3928            // EQ/NE: CMP lo,lo; IT EQ; CMPEQ hi,hi; ITE <cond>; MOV 1; MOV 0
3929            // LT: CMP lo,lo; SBCS rd,hi,hi; ITE LT; MOV 1; MOV 0
3930            // GT: CMP lo,lo (swapped); SBCS rd,hi,hi (swapped); ITE LT; MOV 1; MOV 0
3931            ArmOp::I64SetCond {
3932                rd,
3933                rn_lo,
3934                rn_hi,
3935                rm_lo,
3936                rm_hi,
3937                cond,
3938            } => {
3939                use synth_synthesis::Condition;
3940                let rd_bits = reg_to_bits(rd) as u16;
3941                let mut bytes = Vec::new();
3942
3943                // Helper: encode CMP Rn, Rm (16-bit)
3944                let encode_cmp_reg = |rn: &synth_synthesis::Reg,
3945                                      rm: &synth_synthesis::Reg|
3946                 -> Vec<u8> {
3947                    let rn_bits = reg_to_bits(rn) as u16;
3948                    let rm_bits = reg_to_bits(rm) as u16;
3949                    if rn_bits < 8 && rm_bits < 8 {
3950                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
3951                        instr.to_le_bytes().to_vec()
3952                    } else {
3953                        let n_bit = (rn_bits >> 3) & 1;
3954                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
3955                        instr.to_le_bytes().to_vec()
3956                    }
3957                };
3958
3959                // Helper: encode ITE <cond> (2 bytes)
3960                let encode_ite = |cond_bits: u16| -> Vec<u8> {
3961                    let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
3962                    let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
3963                    ite_instr.to_le_bytes().to_vec()
3964                };
3965
3966                // Helper: encode SetCond (ITE + MOV #1 + MOV #0) for given condition
3967                let encode_setcond = |cond_bits: u16, rd_bits: u16| -> Vec<u8> {
3968                    let mut b = encode_ite(cond_bits);
3969                    if rd_bits < 8 {
3970                        let mov_one: u16 = 0x2001 | (rd_bits << 8);
3971                        let mov_zero: u16 = 0x2000 | (rd_bits << 8);
3972                        b.extend_from_slice(&mov_one.to_le_bytes());
3973                        b.extend_from_slice(&mov_zero.to_le_bytes());
3974                    } else {
3975                        // #311: rd >= R8 — the 16-bit MOV imm8 form has a 3-bit
3976                        // rd field; rd_bits<<8 overflows into bit 11 and
3977                        // TRANSMUTES the MOV into CMP (0x2001|0x0800 = 0x2801 =
3978                        // CMP r0,#1): the boolean dies in the flags and the
3979                        // consumer reads a stale register. Use the 32-bit
3980                        // MOV.W (T2: F04F 0000|rd<<8|imm8) — IT-legal,
3981                        // flag-preserving. Same class as H-CODE-9 / #180.
3982                        for imm in [1u16, 0u16] {
3983                            let hw1: u16 = 0xF04F;
3984                            let hw2: u16 = (rd_bits << 8) | imm;
3985                            b.extend_from_slice(&hw1.to_le_bytes());
3986                            b.extend_from_slice(&hw2.to_le_bytes());
3987                        }
3988                    }
3989                    b
3990                };
3991
3992                match cond {
3993                    Condition::EQ | Condition::NE => {
3994                        // CMP rn_lo, rm_lo (compare low words)
3995                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
3996
3997                        // IT EQ (execute next instruction only if Z=1)
3998                        let it_eq: u16 = 0xBF08; // IT EQ: cond=0000, mask=1000
3999                        bytes.extend_from_slice(&it_eq.to_le_bytes());
4000
4001                        // CMPEQ rn_hi, rm_hi (compare high words, only if low equal)
4002                        bytes.extend_from_slice(&encode_cmp_reg(rn_hi, rm_hi));
4003
4004                        // ITE <cond>; MOV rd, #1; MOV rd, #0
4005                        let cond_bits: u16 = match cond {
4006                            Condition::EQ => 0x0,
4007                            Condition::NE => 0x1,
4008                            _ => unreachable!(),
4009                        };
4010                        bytes.extend_from_slice(&encode_setcond(cond_bits, rd_bits));
4011                    }
4012
4013                    Condition::LT => {
4014                        // CMP rn_lo, rm_lo (sets C flag for borrow)
4015                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4016
4017                        // SBCS rd, rn_hi, rm_hi (subtract with carry, sets N,V flags)
4018                        // SBCS.W Rd, Rn, Rm: EB70 Rn | 0000 Rd 0000 Rm
4019                        let rn_hi_bits = reg_to_bits(rn_hi);
4020                        let rm_hi_bits = reg_to_bits(rm_hi);
4021                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4022                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4023                        bytes.extend_from_slice(&hw1.to_le_bytes());
4024                        bytes.extend_from_slice(&hw2.to_le_bytes());
4025
4026                        // ITE LT; MOV rd, #1; MOV rd, #0
4027                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4028                    }
4029
4030                    Condition::GT => {
4031                        // GT(a,b) = LT(b,a): swap operands
4032                        // CMP rm_lo, rn_lo (swapped)
4033                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4034
4035                        // SBCS rd, rm_hi, rn_hi (swapped)
4036                        let rm_hi_bits = reg_to_bits(rm_hi);
4037                        let rn_hi_bits = reg_to_bits(rn_hi);
4038                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4039                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4040                        bytes.extend_from_slice(&hw1.to_le_bytes());
4041                        bytes.extend_from_slice(&hw2.to_le_bytes());
4042
4043                        // ITE LT; MOV rd, #1; MOV rd, #0
4044                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4045                    }
4046
4047                    Condition::LE => {
4048                        // LE(a,b) = !GT(a,b): use GT logic but invert result
4049                        // GT(a,b) = LT(b,a): so we do CMP(b,a) and check LT, then invert
4050                        // CMP rm_lo, rn_lo (swapped, same as GT)
4051                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4052
4053                        // SBCS rd, rm_hi, rn_hi (swapped)
4054                        let rm_hi_bits = reg_to_bits(rm_hi);
4055                        let rn_hi_bits = reg_to_bits(rn_hi);
4056                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4057                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4058                        bytes.extend_from_slice(&hw1.to_le_bytes());
4059                        bytes.extend_from_slice(&hw2.to_le_bytes());
4060
4061                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT, so inverting GT result)
4062                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4063                    }
4064
4065                    Condition::GE => {
4066                        // GE(a,b) = !LT(a,b): use LT logic but invert result
4067                        // CMP rn_lo, rm_lo (same as LT)
4068                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4069
4070                        // SBCS rd, rn_hi, rm_hi (same as LT)
4071                        let rn_hi_bits = reg_to_bits(rn_hi);
4072                        let rm_hi_bits = reg_to_bits(rm_hi);
4073                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4074                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4075                        bytes.extend_from_slice(&hw1.to_le_bytes());
4076                        bytes.extend_from_slice(&hw2.to_le_bytes());
4077
4078                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT)
4079                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4080                    }
4081
4082                    // Unsigned comparisons - same instruction sequence, different conditions
4083                    Condition::LO => {
4084                        // LO (unsigned LT): CMP lo, SBCS hi, check C=0
4085                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4086                        let rn_hi_bits = reg_to_bits(rn_hi);
4087                        let rm_hi_bits = reg_to_bits(rm_hi);
4088                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4089                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4090                        bytes.extend_from_slice(&hw1.to_le_bytes());
4091                        bytes.extend_from_slice(&hw2.to_le_bytes());
4092                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4093                    }
4094
4095                    Condition::HI => {
4096                        // HI (unsigned GT): swap operands and check LO
4097                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4098                        let rm_hi_bits = reg_to_bits(rm_hi);
4099                        let rn_hi_bits = reg_to_bits(rn_hi);
4100                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4101                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_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::LS => {
4108                        // LS (unsigned LE): !(a > b) = !(HI), so do HI and invert
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(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4117                    }
4118
4119                    Condition::HS => {
4120                        // HS (unsigned GE): !(a < b) = !(LO)
4121                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4122                        let rn_hi_bits = reg_to_bits(rn_hi);
4123                        let rm_hi_bits = reg_to_bits(rm_hi);
4124                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4125                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_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
4132                Ok(bytes)
4133            }
4134
4135            // I64SetCondZ: Test if i64 register pair is zero, result 0/1 in rd
4136            // ORR.W rd, rn_lo, rn_hi; CMP rd, #0; ITE EQ; MOV 1; MOV 0
4137            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
4138                let rd_bits = reg_to_bits(rd);
4139                let rn_lo_bits = reg_to_bits(rn_lo);
4140                let rn_hi_bits = reg_to_bits(rn_hi);
4141                let mut bytes = Vec::new();
4142
4143                // ORR.W rd, rn_lo, rn_hi: EA40 rn_lo | 0000 rd 0000 rn_hi
4144                let hw1: u16 = (0xEA40 | rn_lo_bits) as u16;
4145                let hw2: u16 = ((rd_bits << 8) | rn_hi_bits) as u16;
4146                bytes.extend_from_slice(&hw1.to_le_bytes());
4147                bytes.extend_from_slice(&hw2.to_le_bytes());
4148
4149                // CMP rd, #0 — 16-bit form only for r0-r7 (3-bit rd field);
4150                // high registers take CMP.W (T2: F1B0|rn 0F00|imm8). This was
4151                // H-CODE-9: rd_bits<<8 overflowing the field compared the
4152                // WRONG register. Same hardening as the #311 SetCond fix.
4153                if rd_bits < 8 {
4154                    let cmp_instr: u16 = 0x2800 | ((rd_bits as u16) << 8);
4155                    bytes.extend_from_slice(&cmp_instr.to_le_bytes());
4156                } else {
4157                    let hw1: u16 = 0xF1B0 | (rd_bits as u16);
4158                    let hw2: u16 = 0x0F00;
4159                    bytes.extend_from_slice(&hw1.to_le_bytes());
4160                    bytes.extend_from_slice(&hw2.to_le_bytes());
4161                }
4162
4163                // ITE EQ; MOV rd, #1; MOV rd, #0 (32-bit MOV.W for rd >= R8,
4164                // #311 — see I64SetCond)
4165                let mask = 0xC_u16; // ITE EQ mask: firstcond[0]=0, mask=0xC
4166                let ite_instr: u16 = 0xBF00 | mask;
4167                bytes.extend_from_slice(&ite_instr.to_le_bytes());
4168                if rd_bits < 8 {
4169                    let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
4170                    let mov_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
4171                    bytes.extend_from_slice(&mov_one.to_le_bytes());
4172                    bytes.extend_from_slice(&mov_zero.to_le_bytes());
4173                } else {
4174                    for imm in [1u16, 0u16] {
4175                        let hw1: u16 = 0xF04F;
4176                        let hw2: u16 = ((rd_bits as u16) << 8) | imm;
4177                        bytes.extend_from_slice(&hw1.to_le_bytes());
4178                        bytes.extend_from_slice(&hw2.to_le_bytes());
4179                    }
4180                }
4181
4182                Ok(bytes)
4183            }
4184
4185            // I64Mul: 64-bit multiply using UMULL + MLA cross products
4186            // Formula: result = (a_lo * b_lo) + ((a_lo * b_hi + a_hi * b_lo) << 32)
4187            // Uses R12 as scratch register
4188            ArmOp::I64Mul {
4189                rd_lo,
4190                rd_hi,
4191                rn_lo,
4192                rn_hi,
4193                rm_lo,
4194                rm_hi,
4195            } => {
4196                let rd_lo_bits = reg_to_bits(rd_lo);
4197                let rd_hi_bits = reg_to_bits(rd_hi);
4198                let rn_lo_bits = reg_to_bits(rn_lo);
4199                let rn_hi_bits = reg_to_bits(rn_hi);
4200                let rm_lo_bits = reg_to_bits(rm_lo);
4201                let rm_hi_bits = reg_to_bits(rm_hi);
4202                let r12: u32 = 12; // IP scratch register
4203                let mut bytes = Vec::new();
4204
4205                // 1. MUL R12, rn_lo, rm_hi  (R12 = a_lo * b_hi)
4206                // Thumb-2 MUL: hw1=0xFB00|Rn, hw2=0xF000|(Rd<<8)|Rm
4207                let hw1: u16 = (0xFB00 | rn_lo_bits) as u16;
4208                let hw2: u16 = (0xF000 | (r12 << 8) | rm_hi_bits) as u16;
4209                bytes.extend_from_slice(&hw1.to_le_bytes());
4210                bytes.extend_from_slice(&hw2.to_le_bytes());
4211
4212                // 2. MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
4213                // Thumb-2 MLA: hw1=0xFB00|Rn, hw2=(Ra<<12)|(Rd<<8)|Rm
4214                let hw1: u16 = (0xFB00 | rn_hi_bits) as u16;
4215                let hw2: u16 = ((r12 << 12) | (r12 << 8) | rm_lo_bits) as u16;
4216                bytes.extend_from_slice(&hw1.to_le_bytes());
4217                bytes.extend_from_slice(&hw2.to_le_bytes());
4218
4219                // 3. UMULL rd_lo, rd_hi, rn_lo, rm_lo  (rd_lo:rd_hi = a_lo * b_lo)
4220                // Thumb-2 UMULL: hw1=0xFBA0|Rn, hw2=(RdLo<<12)|(RdHi<<8)|Rm
4221                let hw1: u16 = (0xFBA0 | rn_lo_bits) as u16;
4222                let hw2: u16 = ((rd_lo_bits << 12) | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4223                bytes.extend_from_slice(&hw1.to_le_bytes());
4224                bytes.extend_from_slice(&hw2.to_le_bytes());
4225
4226                // 4. ADD rd_hi, R12  (rd_hi += cross products)
4227                // 16-bit high reg ADD: 01000100 D Rm Rdn[2:0]
4228                let d_bit = (rd_hi_bits >> 3) & 1;
4229                let add_instr: u16 =
4230                    (0x4400 | (d_bit << 7) | (r12 << 3) | (rd_hi_bits & 0x7)) as u16;
4231                bytes.extend_from_slice(&add_instr.to_le_bytes());
4232
4233                Ok(bytes)
4234            }
4235
4236            // I64Shl: 64-bit shift left with branch for n<32 vs n>=32
4237            // rm_hi (R3) is used as temp register
4238            ArmOp::I64Shl {
4239                rd_lo,
4240                rd_hi,
4241                rn_lo,
4242                rn_hi,
4243                rm_lo,
4244                rm_hi,
4245            } => {
4246                let rd_lo_bits = reg_to_bits(rd_lo);
4247                let rd_hi_bits = reg_to_bits(rd_hi);
4248                let rn_lo_bits = reg_to_bits(rn_lo);
4249                let rn_hi_bits = reg_to_bits(rn_hi);
4250                let rm_lo_bits = reg_to_bits(rm_lo);
4251                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4252                let mut bytes = Vec::new();
4253
4254                // AND.W rm_lo, rm_lo, #63  (mask shift amount to 6 bits)
4255                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4256                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4257                bytes.extend_from_slice(&hw1.to_le_bytes());
4258                bytes.extend_from_slice(&hw2.to_le_bytes());
4259
4260                // SUBS.W rm_hi, rm_lo, #32  (rm_hi = n-32, sets flags)
4261                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4262                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4263                bytes.extend_from_slice(&hw1.to_le_bytes());
4264                bytes.extend_from_slice(&hw2.to_le_bytes());
4265
4266                // BPL .large (branch if n >= 32, offset = +10 halfwords)
4267                let bpl: u16 = 0xD50A;
4268                bytes.extend_from_slice(&bpl.to_le_bytes());
4269
4270                // --- Small shift (n < 32) ---
4271                // RSB.W rm_hi, rm_lo, #32  (rm_hi = 32-n)
4272                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4273                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4274                bytes.extend_from_slice(&hw1.to_le_bytes());
4275                bytes.extend_from_slice(&hw2.to_le_bytes());
4276
4277                // LSR.W rm_hi, rn_lo, rm_hi  (rm_hi = lo >> (32-n), overflow bits)
4278                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4279                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4280                bytes.extend_from_slice(&hw1.to_le_bytes());
4281                bytes.extend_from_slice(&hw2.to_le_bytes());
4282
4283                // LSL.W rd_hi, rn_hi, rm_lo  (hi <<= n)
4284                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4285                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4286                bytes.extend_from_slice(&hw1.to_le_bytes());
4287                bytes.extend_from_slice(&hw2.to_le_bytes());
4288
4289                // ORR.W rd_hi, rd_hi, rm_hi  (hi |= overflow bits from lo)
4290                let hw1: u16 = (0xEA40 | rd_hi_bits) as u16;
4291                let hw2: u16 = ((rd_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_lo, rn_lo, rm_lo  (lo <<= n)
4296                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4297                let hw2: u16 = (0xF000 | (rd_lo_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                // B .done (skip large shift: +2 halfwords)
4302                let b_done: u16 = 0xE002;
4303                bytes.extend_from_slice(&b_done.to_le_bytes());
4304
4305                // --- Large shift (n >= 32) ---
4306                // LSL.W rd_hi, rn_lo, rm_hi  (hi = lo << (n-32))
4307                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4308                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_hi_bits) as u16;
4309                bytes.extend_from_slice(&hw1.to_le_bytes());
4310                bytes.extend_from_slice(&hw2.to_le_bytes());
4311
4312                // MOV rd_lo, #0
4313                let mov_zero: u16 = 0x2000 | ((rd_lo_bits as u16) << 8);
4314                bytes.extend_from_slice(&mov_zero.to_le_bytes());
4315
4316                Ok(bytes) // Total: 38 bytes
4317            }
4318
4319            // I64ShrU: 64-bit logical shift right with branch for n<32 vs n>=32
4320            ArmOp::I64ShrU {
4321                rd_lo,
4322                rd_hi,
4323                rn_lo,
4324                rn_hi,
4325                rm_lo,
4326                rm_hi,
4327            } => {
4328                let rd_lo_bits = reg_to_bits(rd_lo);
4329                let rd_hi_bits = reg_to_bits(rd_hi);
4330                let rn_lo_bits = reg_to_bits(rn_lo);
4331                let rn_hi_bits = reg_to_bits(rn_hi);
4332                let rm_lo_bits = reg_to_bits(rm_lo);
4333                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4334                let mut bytes = Vec::new();
4335
4336                // AND.W rm_lo, rm_lo, #63
4337                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4338                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4339                bytes.extend_from_slice(&hw1.to_le_bytes());
4340                bytes.extend_from_slice(&hw2.to_le_bytes());
4341
4342                // SUBS.W rm_hi, rm_lo, #32
4343                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4344                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4345                bytes.extend_from_slice(&hw1.to_le_bytes());
4346                bytes.extend_from_slice(&hw2.to_le_bytes());
4347
4348                // BPL .large (+10 halfwords)
4349                let bpl: u16 = 0xD50A;
4350                bytes.extend_from_slice(&bpl.to_le_bytes());
4351
4352                // --- Small shift (n < 32) ---
4353                // RSB.W rm_hi, rm_lo, #32  (rm_hi = 32-n)
4354                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4355                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4356                bytes.extend_from_slice(&hw1.to_le_bytes());
4357                bytes.extend_from_slice(&hw2.to_le_bytes());
4358
4359                // LSL.W rm_hi, rn_hi, rm_hi  (rm_hi = hi << (32-n), bits flowing to lo)
4360                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4361                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4362                bytes.extend_from_slice(&hw1.to_le_bytes());
4363                bytes.extend_from_slice(&hw2.to_le_bytes());
4364
4365                // LSR.W rd_lo, rn_lo, rm_lo  (lo >>= n)
4366                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4367                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4368                bytes.extend_from_slice(&hw1.to_le_bytes());
4369                bytes.extend_from_slice(&hw2.to_le_bytes());
4370
4371                // ORR.W rd_lo, rd_lo, rm_hi  (lo |= overflow from hi)
4372                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4373                let hw2: u16 = ((rd_lo_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_hi, rn_hi, rm_lo  (hi >>= n, logical)
4378                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4379                let hw2: u16 = (0xF000 | (rd_hi_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                // B .done (+2 halfwords)
4384                let b_done: u16 = 0xE002;
4385                bytes.extend_from_slice(&b_done.to_le_bytes());
4386
4387                // --- Large shift (n >= 32) ---
4388                // LSR.W rd_lo, rn_hi, rm_hi  (lo = hi >> (n-32))
4389                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4390                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_hi_bits) as u16;
4391                bytes.extend_from_slice(&hw1.to_le_bytes());
4392                bytes.extend_from_slice(&hw2.to_le_bytes());
4393
4394                // MOV rd_hi, #0
4395                let mov_zero: u16 = 0x2000 | ((rd_hi_bits as u16) << 8);
4396                bytes.extend_from_slice(&mov_zero.to_le_bytes());
4397
4398                Ok(bytes) // Total: 38 bytes
4399            }
4400
4401            // I64ShrS: 64-bit arithmetic shift right with branch for n<32 vs n>=32
4402            ArmOp::I64ShrS {
4403                rd_lo,
4404                rd_hi,
4405                rn_lo,
4406                rn_hi,
4407                rm_lo,
4408                rm_hi,
4409            } => {
4410                let rd_lo_bits = reg_to_bits(rd_lo);
4411                let rd_hi_bits = reg_to_bits(rd_hi);
4412                let rn_lo_bits = reg_to_bits(rn_lo);
4413                let rn_hi_bits = reg_to_bits(rn_hi);
4414                let rm_lo_bits = reg_to_bits(rm_lo);
4415                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4416                let mut bytes = Vec::new();
4417
4418                // AND.W rm_lo, rm_lo, #63
4419                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4420                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4421                bytes.extend_from_slice(&hw1.to_le_bytes());
4422                bytes.extend_from_slice(&hw2.to_le_bytes());
4423
4424                // SUBS.W rm_hi, rm_lo, #32
4425                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4426                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4427                bytes.extend_from_slice(&hw1.to_le_bytes());
4428                bytes.extend_from_slice(&hw2.to_le_bytes());
4429
4430                // BPL .large (+10 halfwords)
4431                let bpl: u16 = 0xD50A;
4432                bytes.extend_from_slice(&bpl.to_le_bytes());
4433
4434                // --- Small shift (n < 32) ---
4435                // RSB.W rm_hi, rm_lo, #32
4436                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4437                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4438                bytes.extend_from_slice(&hw1.to_le_bytes());
4439                bytes.extend_from_slice(&hw2.to_le_bytes());
4440
4441                // LSL.W rm_hi, rn_hi, rm_hi  (rm_hi = hi << (32-n), bits flowing to lo)
4442                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4443                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4444                bytes.extend_from_slice(&hw1.to_le_bytes());
4445                bytes.extend_from_slice(&hw2.to_le_bytes());
4446
4447                // LSR.W rd_lo, rn_lo, rm_lo  (lo >>= n, logical for lo word)
4448                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4449                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4450                bytes.extend_from_slice(&hw1.to_le_bytes());
4451                bytes.extend_from_slice(&hw2.to_le_bytes());
4452
4453                // ORR.W rd_lo, rd_lo, rm_hi  (lo |= overflow from hi)
4454                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4455                let hw2: u16 = ((rd_lo_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                // ASR.W rd_hi, rn_hi, rm_lo  (hi >>= n, arithmetic/sign-extending)
4460                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
4461                let hw2: u16 = (0xF000 | (rd_hi_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                // B .done (+3 halfwords, large shift is 8 bytes)
4466                let b_done: u16 = 0xE003;
4467                bytes.extend_from_slice(&b_done.to_le_bytes());
4468
4469                // --- Large shift (n >= 32) ---
4470                // ASR.W rd_lo, rn_hi, rm_hi  (lo = hi >>> (n-32))
4471                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
4472                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_hi_bits) as u16;
4473                bytes.extend_from_slice(&hw1.to_le_bytes());
4474                bytes.extend_from_slice(&hw2.to_le_bytes());
4475
4476                // ASR.W rd_hi, rn_hi, #31  (hi = sign extension, all 0s or all 1s)
4477                // Thumb-2 ASR immediate: hw1=0xEA4F, hw2=imm3:Rd:imm2:10:Rm
4478                // imm5=31=11111 → imm3=111, imm2=11
4479                let hw1: u16 = 0xEA4F;
4480                let hw2: u16 = (0x7000 | (rd_hi_bits << 8) | 0x00E0 | rn_hi_bits) as u16;
4481                bytes.extend_from_slice(&hw1.to_le_bytes());
4482                bytes.extend_from_slice(&hw2.to_le_bytes());
4483
4484                Ok(bytes) // Total: 40 bytes
4485            }
4486
4487            // I64Rotl: 64-bit rotate left (#610 rewrite).
4488            // For n < 32: new_hi = (hi << n) | (lo >> (32-n)), new_lo = (lo << n) | (hi >> (32-n))
4489            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
4490            //
4491            // Fixed-reg core: value in R0:R1, amount in R2, scratch R3 + R12
4492            // (all four saved/marshaled by the #610 fixed-ABI wrapper; the
4493            // pre-#610 expansion wrote through the selector's registers with
4494            // colliding R3/R4 scratch and restored the saved R4 OVER the
4495            // result). Relies on ARM register-shift semantics: amounts >= 32
4496            // yield 0 for LSL/LSR, which makes n = 0 and n = 32 exact.
4497            ArmOp::I64Rotl {
4498                rdlo,
4499                rdhi,
4500                rnlo,
4501                rnhi,
4502                shift,
4503            } => {
4504                let mut bytes = Vec::new();
4505                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
4506
4507                let core: [u16; 35] = [
4508                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
4509                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
4510                    0xD50E, //         BPL    .large        (n >= 32)
4511                    // --- small rotation (n < 32) ---
4512                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
4513                    0xFA20, 0xFC03, // LSR.W  R12, R0, R3   (lo >> (32-n))
4514                    0xFA21, 0xF303, // LSR.W  R3, R1, R3    (hi >> (32-n))
4515                    0xFA01, 0xF102, // LSL.W  R1, R1, R2    (hi << n)
4516                    0xEA41, 0x010C, // ORR.W  R1, R1, R12   (new_hi)
4517                    0xFA00, 0xF002, // LSL.W  R0, R0, R2    (lo << n)
4518                    0xEA40, 0x0003, // ORR.W  R0, R0, R3    (new_lo)
4519                    0xE00E, //         B      .done
4520                    // --- large rotation (n >= 32), R3 = m = n-32 ---
4521                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
4522                    0xFA21, 0xFC02, // LSR.W  R12, R1, R2   (hi >> (64-n))
4523                    0xFA20, 0xF202, // LSR.W  R2, R0, R2    (lo >> (64-n))
4524                    0xFA00, 0xF003, // LSL.W  R0, R0, R3    (lo << m)
4525                    0xFA01, 0xF103, // LSL.W  R1, R1, R3    (hi << m)
4526                    0xEA40, 0x0C0C, // ORR.W  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
4527                    0xEA41, 0x0002, // ORR.W  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
4528                    0x4661, //         MOV    R1, R12       (new_hi into place)
4529                            // .done: result in R0:R1
4530                ];
4531                for hw in core {
4532                    bytes.extend_from_slice(&hw.to_le_bytes());
4533                }
4534
4535                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
4536                Ok(bytes) // Total: 102 bytes
4537            }
4538
4539            // I64Rotr: 64-bit rotate right (#610 rewrite).
4540            // For n < 32: new_lo = (lo >> n) | (hi << (32-n)), new_hi = (hi >> n) | (lo << (32-n))
4541            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
4542            //
4543            // Same fixed-reg core contract as I64Rotl: value in R0:R1, amount
4544            // in R2, scratch R3 + R12, all covered by the fixed-ABI wrapper.
4545            ArmOp::I64Rotr {
4546                rdlo,
4547                rdhi,
4548                rnlo,
4549                rnhi,
4550                shift,
4551            } => {
4552                let mut bytes = Vec::new();
4553                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
4554
4555                let core: [u16; 35] = [
4556                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
4557                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
4558                    0xD50E, //         BPL    .large        (n >= 32)
4559                    // --- small rotation (n < 32) ---
4560                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
4561                    0xFA01, 0xFC03, // LSL.W  R12, R1, R3   (hi << (32-n))
4562                    0xFA00, 0xF303, // LSL.W  R3, R0, R3    (lo << (32-n))
4563                    0xFA20, 0xF002, // LSR.W  R0, R0, R2    (lo >> n)
4564                    0xEA40, 0x000C, // ORR.W  R0, R0, R12   (new_lo)
4565                    0xFA21, 0xF102, // LSR.W  R1, R1, R2    (hi >> n)
4566                    0xEA41, 0x0103, // ORR.W  R1, R1, R3    (new_hi)
4567                    0xE00E, //         B      .done
4568                    // --- large rotation (n >= 32), R3 = m = n-32 ---
4569                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
4570                    0xFA00, 0xFC02, // LSL.W  R12, R0, R2   (lo << (64-n))
4571                    0xFA01, 0xF202, // LSL.W  R2, R1, R2    (hi << (64-n))
4572                    0xFA21, 0xF103, // LSR.W  R1, R1, R3    (hi >> m)
4573                    0xEA41, 0x0C0C, // ORR.W  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
4574                    0xFA20, 0xF103, // LSR.W  R1, R0, R3    (lo >> m)
4575                    0xEA41, 0x0102, // ORR.W  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
4576                    0x4660, //         MOV    R0, R12       (new_lo into place)
4577                            // .done: result in R0:R1
4578                ];
4579                for hw in core {
4580                    bytes.extend_from_slice(&hw.to_le_bytes());
4581                }
4582
4583                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
4584                Ok(bytes) // Total: 102 bytes
4585            }
4586
4587            // I64Clz: Count leading zeros in 64-bit value
4588            // If hi != 0: result = CLZ(hi)
4589            // If hi == 0: result = 32 + CLZ(lo)
4590            //
4591            // Layout (using CMP+BNE approach for consistency):
4592            // 0: CMP.W rnhi, #0 (4 bytes)
4593            // 4: BEQ .hi_zero (2 bytes) - branch forward to offset 14
4594            // 6: CLZ.W rd, rnhi (4 bytes)
4595            // 10: B .done (2 bytes) - branch forward to offset 22
4596            // 12: NOP (2 bytes) - padding for alignment
4597            // 14: .hi_zero: CLZ.W rd, rnlo (4 bytes)
4598            // 18: ADD.W rd, rd, #32 (4 bytes)
4599            // 22: .done
4600            ArmOp::I64Clz { rd, rnlo, rnhi } => {
4601                let rd_bits = reg_to_bits(rd);
4602                let rn_lo_bits = reg_to_bits(rnlo);
4603                let rn_hi_bits = reg_to_bits(rnhi);
4604                let mut bytes = Vec::new();
4605
4606                // CMP.W rnhi, #0 (4 bytes at offset 0)
4607                let hw1: u16 = (0xF1B0 | rn_hi_bits) as u16;
4608                let hw2: u16 = 0x0F00;
4609                bytes.extend_from_slice(&hw1.to_le_bytes());
4610                bytes.extend_from_slice(&hw2.to_le_bytes());
4611
4612                // BEQ .hi_zero (2 bytes at offset 4)
4613                // PC = 4 + 4 = 8, target = 14, offset = 6, imm8 = 3
4614                let beq: u16 = 0xD003;
4615                bytes.extend_from_slice(&beq.to_le_bytes());
4616
4617                // CLZ.W rd, rnhi (4 bytes at offset 6)
4618                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4619                let hw1: u16 = (0xFAB0 | rn_hi_bits) as u16;
4620                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_hi_bits) as u16;
4621                bytes.extend_from_slice(&hw1.to_le_bytes());
4622                bytes.extend_from_slice(&hw2.to_le_bytes());
4623
4624                // B .done (2 bytes at offset 10)
4625                // PC = 10 + 4 = 14, target = 22, offset = 8, imm11 = 4
4626                let b_done: u16 = 0xE004;
4627                bytes.extend_from_slice(&b_done.to_le_bytes());
4628
4629                // NOP (2 bytes at offset 12) - padding
4630                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
4631
4632                // .hi_zero: (offset 14)
4633                // CLZ.W rd, rnlo (4 bytes)
4634                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4635                let hw1: u16 = (0xFAB0 | rn_lo_bits) as u16;
4636                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_lo_bits) as u16;
4637                bytes.extend_from_slice(&hw1.to_le_bytes());
4638                bytes.extend_from_slice(&hw2.to_le_bytes());
4639
4640                // ADD.W rd, rd, #32 (4 bytes at offset 18)
4641                let hw1: u16 = (0xF100 | rd_bits) as u16;
4642                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
4643                bytes.extend_from_slice(&hw1.to_le_bytes());
4644                bytes.extend_from_slice(&hw2.to_le_bytes());
4645
4646                // .done: (offset 22)
4647                // i64.clz returns i64, so clear high word: MOV rnhi, #0 (2 bytes)
4648                // MOVS Rn, #0: 0010 0 Rn 00000000
4649                let mov0: u16 = (0x2000 | (rn_hi_bits << 8)) as u16;
4650                bytes.extend_from_slice(&mov0.to_le_bytes());
4651
4652                Ok(bytes)
4653            }
4654
4655            // I64Ctz: Count trailing zeros in 64-bit value
4656            // If lo != 0: result = CTZ(lo) = CLZ(RBIT(lo))
4657            // If lo == 0: result = 32 + CTZ(hi) = 32 + CLZ(RBIT(hi))
4658            //
4659            // Layout:
4660            // 0: CMP.W rnlo, #0 (4 bytes)
4661            // 4: BEQ .lo_zero (2 bytes) - branch to offset 18
4662            // 6: RBIT.W rd, rnlo (4 bytes)
4663            // 10: CLZ.W rd, rd (4 bytes)
4664            // 14: B .done (2 bytes) - branch to offset 30
4665            // 16: NOP (2 bytes) - padding
4666            // 18: .lo_zero: RBIT.W rd, rnhi (4 bytes)
4667            // 22: CLZ.W rd, rd (4 bytes)
4668            // 26: ADD.W rd, rd, #32 (4 bytes)
4669            // 30: .done
4670            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
4671                let rd_bits = reg_to_bits(rd);
4672                let rn_lo_bits = reg_to_bits(rnlo);
4673                let rn_hi_bits = reg_to_bits(rnhi);
4674                let mut bytes = Vec::new();
4675
4676                // CMP.W rnlo, #0 (4 bytes at offset 0)
4677                let hw1: u16 = (0xF1B0 | rn_lo_bits) as u16;
4678                let hw2: u16 = 0x0F00;
4679                bytes.extend_from_slice(&hw1.to_le_bytes());
4680                bytes.extend_from_slice(&hw2.to_le_bytes());
4681
4682                // BEQ .lo_zero (2 bytes at offset 4)
4683                // PC = 4 + 4 = 8, target = 18, offset = 10, imm8 = 5
4684                let beq: u16 = 0xD005;
4685                bytes.extend_from_slice(&beq.to_le_bytes());
4686
4687                // RBIT.W rd, rnlo (4 bytes at offset 6)
4688                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
4689                let hw1: u16 = (0xFA90 | rn_lo_bits) as u16;
4690                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_lo_bits) as u16;
4691                bytes.extend_from_slice(&hw1.to_le_bytes());
4692                bytes.extend_from_slice(&hw2.to_le_bytes());
4693
4694                // CLZ.W rd, rd (4 bytes at offset 10)
4695                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4696                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
4697                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
4698                bytes.extend_from_slice(&hw1.to_le_bytes());
4699                bytes.extend_from_slice(&hw2.to_le_bytes());
4700
4701                // B .done (2 bytes at offset 14)
4702                // PC = 14 + 4 = 18, target = 30, offset = 12, imm11 = 6
4703                let b_done: u16 = 0xE006;
4704                bytes.extend_from_slice(&b_done.to_le_bytes());
4705
4706                // NOP (2 bytes at offset 16) - padding
4707                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
4708
4709                // .lo_zero: (offset 18)
4710                // RBIT.W rd, rnhi (4 bytes)
4711                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
4712                let hw1: u16 = (0xFA90 | rn_hi_bits) as u16;
4713                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_hi_bits) as u16;
4714                bytes.extend_from_slice(&hw1.to_le_bytes());
4715                bytes.extend_from_slice(&hw2.to_le_bytes());
4716
4717                // CLZ.W rd, rd (4 bytes at offset 22)
4718                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4719                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
4720                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
4721                bytes.extend_from_slice(&hw1.to_le_bytes());
4722                bytes.extend_from_slice(&hw2.to_le_bytes());
4723
4724                // ADD.W rd, rd, #32 (4 bytes at offset 26)
4725                let hw1: u16 = (0xF100 | rd_bits) as u16;
4726                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
4727                bytes.extend_from_slice(&hw1.to_le_bytes());
4728                bytes.extend_from_slice(&hw2.to_le_bytes());
4729
4730                // .done: (offset 30)
4731                // i64.ctz returns i64, so clear high word: MOV rnhi, #0 (2 bytes)
4732                let mov0: u16 = (0x2000 | (rn_hi_bits << 8)) as u16;
4733                bytes.extend_from_slice(&mov0.to_le_bytes());
4734
4735                Ok(bytes)
4736            }
4737
4738            // I64Popcnt: Population count of 64-bit value
4739            // result = POPCNT(lo) + POPCNT(hi)
4740            // Using SIMD-style parallel bit counting algorithm
4741            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
4742                let rd_bits = reg_to_bits(rd);
4743                let rn_lo_bits = reg_to_bits(rnlo);
4744                let rn_hi_bits = reg_to_bits(rnhi);
4745                let r12: u32 = 12; // IP scratch
4746                let r3: u32 = 3; // Scratch for hi popcnt result
4747                let mut bytes = Vec::new();
4748
4749                // PUSH {R3, R4, R5} - save scratch registers
4750                bytes.extend_from_slice(&0xB438u16.to_le_bytes());
4751
4752                // Strategy: compute popcnt(lo) -> R4, popcnt(hi) -> R5, add them -> rd
4753                // Using lookup table approach for each byte would be too large
4754                // Using shift-and-add approach instead
4755
4756                // For simplicity and correctness, use the efficient parallel algorithm
4757                // but implement it as a series of inline operations
4758
4759                // MOV R4, rnlo
4760                let d_bit: u32 = 0; // R4 < 8, so high bit is 0
4761                let mov: u16 = (0x4600 | (d_bit << 7) | (rn_lo_bits << 3) | (4 & 0x7)) as u16;
4762                bytes.extend_from_slice(&mov.to_le_bytes());
4763
4764                // MOV R5, rnhi
4765                let d_bit: u32 = 0; // R5 < 8, so high bit is 0
4766                let mov: u16 = (0x4600 | (d_bit << 7) | (rn_hi_bits << 3) | (5 & 0x7)) as u16;
4767                bytes.extend_from_slice(&mov.to_le_bytes());
4768
4769                // --- POPCNT for R4 (lo word) ---
4770                // Step 1: x = x - ((x >> 1) & 0x55555555)
4771                // LSR.W R12, R4, #1
4772                let hw1: u16 = 0xEA4F;
4773                let hw2: u16 = ((r12 << 8) | 0x50 | 4) as u16;
4774                bytes.extend_from_slice(&hw1.to_le_bytes());
4775                bytes.extend_from_slice(&hw2.to_le_bytes());
4776
4777                // Load 0x55555555 into R3 using MOVW/MOVT
4778                // MOVW R3, #0x5555
4779                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
4780                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
4781                // MOVT R3, #0x5555
4782                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
4783                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
4784
4785                // AND.W R12, R12, R3
4786                let hw1: u16 = (0xEA00 | r12) as u16;
4787                let hw2: u16 = ((r12 << 8) | r3) as u16;
4788                bytes.extend_from_slice(&hw1.to_le_bytes());
4789                bytes.extend_from_slice(&hw2.to_le_bytes());
4790
4791                // SUB.W R4, R4, R12
4792                let hw1: u16 = (0xEBA0 | 4) as u16;
4793                let hw2: u16 = ((4 << 8) | r12) as u16;
4794                bytes.extend_from_slice(&hw1.to_le_bytes());
4795                bytes.extend_from_slice(&hw2.to_le_bytes());
4796
4797                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
4798                // Load 0x33333333 into R3
4799                // MOVW R3, #0x3333
4800                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
4801                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
4802                // MOVT R3, #0x3333
4803                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
4804                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
4805
4806                // AND.W R12, R4, R3
4807                let hw1: u16 = (0xEA00 | 4) as u16;
4808                let hw2: u16 = ((r12 << 8) | r3) as u16;
4809                bytes.extend_from_slice(&hw1.to_le_bytes());
4810                bytes.extend_from_slice(&hw2.to_le_bytes());
4811
4812                // LSR.W R4, R4, #2
4813                let hw1: u16 = 0xEA4F;
4814                let hw2: u16 = ((4 << 8) | 0x90 | 4) as u16;
4815                bytes.extend_from_slice(&hw1.to_le_bytes());
4816                bytes.extend_from_slice(&hw2.to_le_bytes());
4817
4818                // AND.W R4, R4, R3
4819                let hw1: u16 = (0xEA00 | 4) as u16;
4820                let hw2: u16 = ((4 << 8) | r3) as u16;
4821                bytes.extend_from_slice(&hw1.to_le_bytes());
4822                bytes.extend_from_slice(&hw2.to_le_bytes());
4823
4824                // ADD.W R4, R4, R12
4825                let hw1: u16 = (0xEB00 | 4) as u16;
4826                let hw2: u16 = ((4 << 8) | r12) as u16;
4827                bytes.extend_from_slice(&hw1.to_le_bytes());
4828                bytes.extend_from_slice(&hw2.to_le_bytes());
4829
4830                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
4831                // LSR.W R12, R4, #4
4832                // hw2 = (imm3 << 12) | (Rd << 8) | (imm2 << 6) | (type << 4) | Rm
4833                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
4834                let hw1: u16 = 0xEA4F;
4835                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 4) 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                // Load 0x0F0F0F0F into R3
4846                // MOVW R3, #0x0F0F (imm4=0, i=1, imm3=7, imm8=0x0F)
4847                // hw1 = 11110 1 10 0100 0000 = 0xF640
4848                // hw2 = 0 111 0011 00001111 = 0x730F
4849                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
4850                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
4851                // MOVT R3, #0x0F0F
4852                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
4853                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
4854
4855                // AND.W R4, R4, R3
4856                let hw1: u16 = (0xEA00 | 4) as u16;
4857                let hw2: u16 = ((4 << 8) | r3) as u16;
4858                bytes.extend_from_slice(&hw1.to_le_bytes());
4859                bytes.extend_from_slice(&hw2.to_le_bytes());
4860
4861                // Step 4: x = x * 0x01010101 >> 24
4862                // Load 0x01010101 into R3
4863                // MOVW R3, #0x0101
4864                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
4865                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
4866                // MOVT R3, #0x0101
4867                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
4868                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
4869
4870                // MUL R4, R4, R3
4871                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
4872                let hw1: u16 = (0xFB00 | 4) as u16;
4873                let hw2: u16 = (0xF000 | (4 << 8) | r3) as u16;
4874                bytes.extend_from_slice(&hw1.to_le_bytes());
4875                bytes.extend_from_slice(&hw2.to_le_bytes());
4876
4877                // LSR.W R4, R4, #24
4878                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
4879                let hw1: u16 = 0xEA4F;
4880                let hw2: u16 = (0x6000 | (4 << 8) | 0x10 | 4) as u16;
4881                bytes.extend_from_slice(&hw1.to_le_bytes());
4882                bytes.extend_from_slice(&hw2.to_le_bytes());
4883
4884                // --- POPCNT for R5 (hi word) - same algorithm ---
4885                // Step 1
4886                let hw1: u16 = 0xEA4F;
4887                let hw2: u16 = ((r12 << 8) | 0x50 | 5) as u16;
4888                bytes.extend_from_slice(&hw1.to_le_bytes());
4889                bytes.extend_from_slice(&hw2.to_le_bytes());
4890
4891                // Load 0x55555555 into R3
4892                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
4893                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
4894                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
4895                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
4896
4897                let hw1: u16 = (0xEA00 | r12) as u16;
4898                let hw2: u16 = ((r12 << 8) | r3) as u16;
4899                bytes.extend_from_slice(&hw1.to_le_bytes());
4900                bytes.extend_from_slice(&hw2.to_le_bytes());
4901
4902                let hw1: u16 = (0xEBA0 | 5) as u16;
4903                let hw2: u16 = ((5 << 8) | r12) as u16;
4904                bytes.extend_from_slice(&hw1.to_le_bytes());
4905                bytes.extend_from_slice(&hw2.to_le_bytes());
4906
4907                // Step 2
4908                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
4909                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
4910                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
4911                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
4912
4913                let hw1: u16 = (0xEA00 | 5) as u16;
4914                let hw2: u16 = ((r12 << 8) | r3) as u16;
4915                bytes.extend_from_slice(&hw1.to_le_bytes());
4916                bytes.extend_from_slice(&hw2.to_le_bytes());
4917
4918                let hw1: u16 = 0xEA4F;
4919                let hw2: u16 = ((5 << 8) | 0x90 | 5) as u16;
4920                bytes.extend_from_slice(&hw1.to_le_bytes());
4921                bytes.extend_from_slice(&hw2.to_le_bytes());
4922
4923                let hw1: u16 = (0xEA00 | 5) as u16;
4924                let hw2: u16 = ((5 << 8) | r3) as u16;
4925                bytes.extend_from_slice(&hw1.to_le_bytes());
4926                bytes.extend_from_slice(&hw2.to_le_bytes());
4927
4928                let hw1: u16 = (0xEB00 | 5) as u16;
4929                let hw2: u16 = ((5 << 8) | r12) as u16;
4930                bytes.extend_from_slice(&hw1.to_le_bytes());
4931                bytes.extend_from_slice(&hw2.to_le_bytes());
4932
4933                // Step 3: LSR.W R12, R5, #4
4934                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
4935                let hw1: u16 = 0xEA4F;
4936                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 5) as u16;
4937                bytes.extend_from_slice(&hw1.to_le_bytes());
4938                bytes.extend_from_slice(&hw2.to_le_bytes());
4939
4940                let hw1: u16 = (0xEB00 | 5) as u16;
4941                let hw2: u16 = ((5 << 8) | r12) as u16;
4942                bytes.extend_from_slice(&hw1.to_le_bytes());
4943                bytes.extend_from_slice(&hw2.to_le_bytes());
4944
4945                // Load 0x0F0F0F0F into R3 (for hi-word)
4946                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
4947                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
4948                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
4949                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
4950
4951                let hw1: u16 = (0xEA00 | 5) as u16;
4952                let hw2: u16 = ((5 << 8) | r3) as u16;
4953                bytes.extend_from_slice(&hw1.to_le_bytes());
4954                bytes.extend_from_slice(&hw2.to_le_bytes());
4955
4956                // Step 4
4957                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
4958                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
4959                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
4960                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
4961
4962                // MUL R5, R5, R3
4963                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
4964                let hw1: u16 = (0xFB00 | 5) as u16;
4965                let hw2: u16 = (0xF000 | (5 << 8) | r3) as u16;
4966                bytes.extend_from_slice(&hw1.to_le_bytes());
4967                bytes.extend_from_slice(&hw2.to_le_bytes());
4968
4969                // LSR.W R5, R5, #24
4970                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
4971                let hw1: u16 = 0xEA4F;
4972                let hw2: u16 = (0x6000 | (5 << 8) | 0x10 | 5) as u16;
4973                bytes.extend_from_slice(&hw1.to_le_bytes());
4974                bytes.extend_from_slice(&hw2.to_le_bytes());
4975
4976                // ADD rd, R4, R5 (combine lo and hi counts)
4977                // ADDS Rd, Rn, Rm (T1): 0001 100 Rm Rn Rd = 0x1800 | (Rm<<6) | (Rn<<3) | Rd
4978                let rd_bits_u16 = rd_bits as u16;
4979                let instr: u16 = 0x1800 | (5 << 6) | (4 << 3) | rd_bits_u16;
4980                bytes.extend_from_slice(&instr.to_le_bytes());
4981
4982                // POP {R3, R4, R5}
4983                bytes.extend_from_slice(&0xBC38u16.to_le_bytes());
4984
4985                // i64.popcnt returns i64, so clear high word: MOV rnhi, #0 (2 bytes)
4986                let mov0: u16 = (0x2000 | (rn_hi_bits << 8)) as u16;
4987                bytes.extend_from_slice(&mov0.to_le_bytes());
4988
4989                Ok(bytes)
4990            }
4991
4992            // I64Extend8S: Sign-extend low 8 bits to 64 bits
4993            // Result: rdlo = sign_extend_8(rnlo), rdhi = rdlo >> 31
4994            ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
4995                let rdlo_bits = reg_to_bits(rdlo);
4996                let rdhi_bits = reg_to_bits(rdhi);
4997                let rnlo_bits = reg_to_bits(rnlo);
4998                let mut bytes = Vec::new();
4999
5000                // SXTB.W rdlo, rnlo (sign-extend byte to 32-bit)
5001                // SXTB T2: hw1 = 0xFA4F, hw2 = 0xF0<Rd><Rm>
5002                let hw1: u16 = 0xFA4F_u16;
5003                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5004                bytes.extend_from_slice(&hw1.to_le_bytes());
5005                bytes.extend_from_slice(&hw2.to_le_bytes());
5006
5007                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5008                // ASR (immediate): hw1 = 0xEA4F, hw2 = imm3:Rd:imm2:type:Rm
5009                // For imm5=31: imm3=111, imm2=11, type=10 (ASR)
5010                // hw2 = (7 << 12) | (rdhi << 8) | (3 << 6) | (2 << 4) | rdlo
5011                let hw1: u16 = 0xEA4F;
5012                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5013                bytes.extend_from_slice(&hw1.to_le_bytes());
5014                bytes.extend_from_slice(&hw2.to_le_bytes());
5015
5016                Ok(bytes)
5017            }
5018
5019            // I64Extend16S: Sign-extend low 16 bits to 64 bits
5020            // Result: rdlo = sign_extend_16(rnlo), rdhi = rdlo >> 31
5021            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
5022                let rdlo_bits = reg_to_bits(rdlo);
5023                let rdhi_bits = reg_to_bits(rdhi);
5024                let rnlo_bits = reg_to_bits(rnlo);
5025                let mut bytes = Vec::new();
5026
5027                // SXTH.W rdlo, rnlo (sign-extend halfword to 32-bit)
5028                // SXTH T2: hw1 = 0xFA0F, hw2 = 0xF0<Rd><Rm>
5029                let hw1: u16 = 0xFA0F_u16;
5030                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5031                bytes.extend_from_slice(&hw1.to_le_bytes());
5032                bytes.extend_from_slice(&hw2.to_le_bytes());
5033
5034                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5035                let hw1: u16 = 0xEA4F;
5036                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5037                bytes.extend_from_slice(&hw1.to_le_bytes());
5038                bytes.extend_from_slice(&hw2.to_le_bytes());
5039
5040                Ok(bytes)
5041            }
5042
5043            // I64Extend32S: Sign-extend low 32 bits to 64 bits
5044            // Result: rdlo = rnlo, rdhi = rnlo >> 31
5045            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
5046                let rdlo_bits = reg_to_bits(rdlo);
5047                let rdhi_bits = reg_to_bits(rdhi);
5048                let rnlo_bits = reg_to_bits(rnlo);
5049                let mut bytes = Vec::new();
5050
5051                // MOV rdlo, rnlo (if different)
5052                if rdlo_bits != rnlo_bits {
5053                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5054                    let d_bit = ((rdlo_bits >> 3) & 1) as u16;
5055                    let mov: u16 = 0x4600
5056                        | (d_bit << 7)
5057                        | ((rnlo_bits as u16) << 3)
5058                        | ((rdlo_bits & 0x7) as u16);
5059                    bytes.extend_from_slice(&mov.to_le_bytes());
5060                }
5061
5062                // ASR.W rdhi, rnlo, #31 (sign-extend to high word)
5063                let hw1: u16 = 0xEA4F;
5064                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rnlo_bits) as u16;
5065                bytes.extend_from_slice(&hw1.to_le_bytes());
5066                bytes.extend_from_slice(&hw2.to_le_bytes());
5067
5068                Ok(bytes)
5069            }
5070
5071            // SelectMove: IT <cond>; MOV{cond} rd, rm
5072            // Conditional move: only execute MOV if condition is true
5073            ArmOp::SelectMove { rd, rm, cond } => {
5074                let rd_bits = reg_to_bits(rd) as u16;
5075                let rm_bits = reg_to_bits(rm) as u16;
5076
5077                // Condition code encoding for IT block
5078                use synth_synthesis::Condition;
5079                let cond_bits: u16 = match cond {
5080                    Condition::EQ => 0x0, // Equal
5081                    Condition::NE => 0x1, // Not equal
5082                    Condition::HS => 0x2, // Higher or same (unsigned >=)
5083                    Condition::LO => 0x3, // Lower (unsigned <)
5084                    Condition::HI => 0x8, // Higher (unsigned >)
5085                    Condition::LS => 0x9, // Lower or same (unsigned <=)
5086                    Condition::GE => 0xA, // Greater or equal (signed)
5087                    Condition::LT => 0xB, // Less than (signed)
5088                    Condition::GT => 0xC, // Greater than (signed)
5089                    Condition::LE => 0xD, // Less or equal (signed)
5090                };
5091
5092                // IT <cond>: single Then block (mask = 0x8 for T only)
5093                // IT instruction: 1011 1111 firstcond mask
5094                let it_instr: u16 = 0xBF00 | (cond_bits << 4) | 0x8;
5095
5096                // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5097                // This MOV will only execute if condition is true due to IT block
5098                let d_bit = (rd_bits >> 3) & 1;
5099                let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5100
5101                // Emit: IT <cond>, MOV rd, rm
5102                let mut bytes = it_instr.to_le_bytes().to_vec();
5103                bytes.extend_from_slice(&mov_instr.to_le_bytes());
5104                Ok(bytes)
5105            }
5106
5107            // Popcnt: Population count (count set bits)
5108            // ARM Cortex-M has no native POPCNT, so we implement the bit manipulation algorithm:
5109            // x = x - ((x >> 1) & 0x55555555);
5110            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
5111            // x = (x + (x >> 4)) & 0x0F0F0F0F;
5112            // x = x + (x >> 8);
5113            // x = x + (x >> 16);
5114            // return x & 0x3F;
5115            //
5116            // Uses rd as working register and R12 as scratch for constants
5117            ArmOp::Popcnt { rd, rm } => {
5118                let mut bytes = Vec::new();
5119
5120                // First, move rm to rd if they're different
5121                if rd != rm {
5122                    let rd_bits = reg_to_bits(rd) as u16;
5123                    let rm_bits = reg_to_bits(rm) as u16;
5124                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5125                    let d_bit = (rd_bits >> 3) & 1;
5126                    let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5127                    bytes.extend_from_slice(&mov_instr.to_le_bytes());
5128                }
5129
5130                // Step 1: x = x - ((x >> 1) & 0x55555555)
5131                // Load 0x55555555 into R12
5132                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x5555)?);
5133                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x5555)?);
5134
5135                // R12_temp = rd >> 1
5136                // We need a second scratch register. Use R11.
5137                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 1)?);
5138
5139                // R11 = R11 & R12 (R11 = (x >> 1) & 0x55555555)
5140                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(11, 11, 12)?);
5141
5142                // rd = rd - R11
5143                bytes.extend_from_slice(&self.encode_thumb32_sub_reg_raw(
5144                    reg_to_bits(rd),
5145                    reg_to_bits(rd),
5146                    11,
5147                )?);
5148
5149                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5150                // Load 0x33333333 into R12
5151                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x3333)?);
5152                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x3333)?);
5153
5154                // R11 = rd & R12
5155                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5156                    11,
5157                    reg_to_bits(rd),
5158                    12,
5159                )?);
5160
5161                // rd = rd >> 2
5162                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(
5163                    reg_to_bits(rd),
5164                    reg_to_bits(rd),
5165                    2,
5166                )?);
5167
5168                // rd = rd & R12
5169                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5170                    reg_to_bits(rd),
5171                    reg_to_bits(rd),
5172                    12,
5173                )?);
5174
5175                // rd = rd + R11
5176                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5177                    reg_to_bits(rd),
5178                    reg_to_bits(rd),
5179                    11,
5180                )?);
5181
5182                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5183                // R11 = rd >> 4
5184                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 4)?);
5185
5186                // rd = rd + R11
5187                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5188                    reg_to_bits(rd),
5189                    reg_to_bits(rd),
5190                    11,
5191                )?);
5192
5193                // Load 0x0F0F0F0F into R12
5194                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x0F0F)?);
5195                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x0F0F)?);
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                // Step 4: x = x + (x >> 8)
5205                // R11 = rd >> 8
5206                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 8)?);
5207
5208                // rd = rd + R11
5209                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5210                    reg_to_bits(rd),
5211                    reg_to_bits(rd),
5212                    11,
5213                )?);
5214
5215                // Step 5: x = x + (x >> 16)
5216                // R11 = rd >> 16
5217                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 16)?);
5218
5219                // rd = rd + R11
5220                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5221                    reg_to_bits(rd),
5222                    reg_to_bits(rd),
5223                    11,
5224                )?);
5225
5226                // Step 6: return x & 0x3F
5227                // AND with 0x3F (small immediate, can use BIC or AND with immediate)
5228                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5229                    reg_to_bits(rd),
5230                    reg_to_bits(rd),
5231                    0x3F,
5232                )?);
5233
5234                Ok(bytes)
5235            }
5236
5237            // I64DivU: 64-bit unsigned division using binary long division
5238            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = quotient
5239            // Uses: R4-R7, R12 as loop counter (avoid R8 for Renode compatibility)
5240            //
5241            // #610: the fixed-ABI wrapper marshals the selector-assigned
5242            // operand registers into the core's fixed regs and lands the
5243            // result in rd — pre-#610 this arm IGNORED its register fields,
5244            // so the selector read its rd pair (e.g. R4:R5) after the core's
5245            // own POP restored the stale caller values over it: 0 for every
5246            // input. A zero divisor now traps (UDF #0), per WASM semantics.
5247            ArmOp::I64DivU {
5248                rdlo,
5249                rdhi,
5250                rnlo,
5251                rnhi,
5252                rmlo,
5253                rmhi,
5254            } => {
5255                let mut bytes = Vec::new();
5256                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5257                emit_i64_divisor_zero_trap(&mut bytes);
5258
5259                // PUSH {R4-R7} - save scratch registers (NO LR — this is inline code)
5260                // 16-bit PUSH: 1011 010 M rrrrrrrr where M=0 (no LR), r=R4-R7 = 0xF0
5261                // Encoding: 1011 0100 1111 0000 = 0xB4F0
5262                bytes.extend_from_slice(&0xB4F0u16.to_le_bytes());
5263
5264                // Initialize quotient (R4:R5) = 0
5265                bytes.extend_from_slice(&0x2400u16.to_le_bytes()); // MOV R4, #0
5266                bytes.extend_from_slice(&0x2500u16.to_le_bytes()); // MOV R5, #0
5267
5268                // Initialize remainder (R6:R7) = 0
5269                bytes.extend_from_slice(&0x2600u16.to_le_bytes()); // MOV R6, #0
5270                bytes.extend_from_slice(&0x2700u16.to_le_bytes()); // MOV R7, #0
5271
5272                // Initialize loop counter R12 = 64 (use R12 scratch instead of R8)
5273                // MOV.W R12, #64: F04F 0C40
5274                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5275                bytes.extend_from_slice(&0x0C40u16.to_le_bytes());
5276
5277                // Loop start
5278                let loop_start = bytes.len();
5279
5280                // === Loop body: process one bit ===
5281
5282                // 1. Shift quotient R4:R5 left by 1
5283                // LSLS R5, R5, #1 (16-bit: 0000 0010 1010 1101 = 0x006D -> actually 0x002D for LSL R5,R5,#1)
5284                // LSL Rd, Rm, #imm5: 000 00 imm5 Rm Rd = 000 00 00001 101 101 = 0x006D
5285                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5286                // Get carry from R4 into R5: ORR R5, R5, R4 LSR #31
5287                // Thumb-2 ORR with shifted register: EA45 75D4 = ORR.W R5, R5, R4, LSR #31
5288                // 11101010 010 S Rn | 0 imm3 Rd imm2 type Rm
5289                // type=01 (LSR), imm5=31 (imm3=111, imm2=11)
5290                bytes.extend_from_slice(&0xEA45u16.to_le_bytes());
5291                bytes.extend_from_slice(&0x75D4u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5292                // LSLS R4, R4, #1: 000 00 00001 100 100 = 0x0064
5293                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5294
5295                // 2. Shift remainder R6:R7 left by 1, OR in MSB of dividend R1
5296                // LSLS R7, R7, #1
5297                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5298                // ORR.W R7, R7, R6, LSR #31
5299                bytes.extend_from_slice(&0xEA47u16.to_le_bytes());
5300                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5301                // LSLS R6, R6, #1
5302                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5303                // ORR.W R6, R6, R1, LSR #31 (bring in MSB of dividend high)
5304                bytes.extend_from_slice(&0xEA46u16.to_le_bytes());
5305                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5306
5307                // 3. Shift dividend R0:R1 left by 1
5308                // LSLS R1, R1, #1
5309                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5310                // ORR.W R1, R1, R0, LSR #31
5311                bytes.extend_from_slice(&0xEA41u16.to_le_bytes());
5312                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5313                // LSLS R0, R0, #1
5314                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5315
5316                // 4. Compare remainder >= divisor (64-bit unsigned comparison)
5317                // Compare high words first: CMP R7, R3
5318                // CMP Rn, Rm encoding: 0x4280 | (Rm << 3) | Rn
5319                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3 (16-bit)
5320                // BHI means R7 > R3 (unsigned) - definitely subtract
5321                // BLO means R7 < R3 - definitely don't subtract
5322                // BEQ means need to check low words
5323
5324                // If high > divisor high: branch to subtract (forward +offset)
5325                // BHI.N +6 (skip CMP, skip BLO, do subtract)
5326                // BHI: 1101 1000 offset8 where cond=1000 (HI)
5327                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4 (to subtract block)
5328
5329                // If high < divisor high: branch past subtract
5330                // BLO.N +10 (skip to decrement)
5331                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BLO/BCC +12 (past subtract)
5332
5333                // High words equal, compare low: CMP R6, R2
5334                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2 (16-bit)
5335                // BLO/BCC past subtract (skip SUBS+SBC.W+ORR.W = 10 bytes = 4 halfwords from PC+4)
5336                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords (past subtract)
5337
5338                // === Subtract block: remainder -= divisor, quotient |= 1 ===
5339                // SUBS R6, R6, R2
5340                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2 (16-bit)
5341                // SBC R7, R7, R3 (with borrow)
5342                // Thumb-2 SBC.W: EB67 0703 = SBC.W R7, R7, R3
5343                bytes.extend_from_slice(&0xEB67u16.to_le_bytes());
5344                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5345                // ORR R4, R4, #1 (set bit 0 of quotient low)
5346                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5347                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5348
5349                // === Decrement counter and loop ===
5350                // SUBS.W R12, R12, #1 (decrement loop counter)
5351                // SUBS.W R12, R12, #1: F1BC 0C01
5352                bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
5353                bytes.extend_from_slice(&0x0C01u16.to_le_bytes());
5354
5355                // BNE back to loop_start
5356                let branch_offset_bytes = bytes.len() - loop_start + 4; // +4 for pipeline
5357                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5358                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5359                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5360
5361                // === Loop done, move quotient to R0:R1 ===
5362                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5363                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5364
5365                // POP {R4-R7} - restore scratch registers (NO PC — inline code continues)
5366                // 16-bit POP: 1011 110 P rrrrrrrr where P=0 (no PC), r=R4-R7 = 0xF0
5367                // Encoding: 1011 1100 1111 0000 = 0xBCF0
5368                bytes.extend_from_slice(&0xBCF0u16.to_le_bytes());
5369
5370                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5371                Ok(bytes)
5372            }
5373
5374            // I64DivS: 64-bit signed division
5375            // Converts to unsigned, divides, then applies sign
5376            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5377            //   ->  R0:R1 = quotient (signed)
5378            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5379            ArmOp::I64DivS {
5380                rdlo,
5381                rdhi,
5382                rnlo,
5383                rnhi,
5384                rmlo,
5385                rmhi,
5386            } => {
5387                let mut bytes = Vec::new();
5388                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5389                emit_i64_divisor_zero_trap(&mut bytes);
5390
5391                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5392                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5393                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5394
5395                // Save result sign in R9: R9 = R1 XOR R3 (sign bit = MSB)
5396                // EOR.W R9, R1, R3
5397                bytes.extend_from_slice(&0xEA81u16.to_le_bytes());
5398                bytes.extend_from_slice(&0x0903u16.to_le_bytes());
5399
5400                // If dividend negative (R1 MSB set), negate it
5401                // TST R1, R1 (check sign)
5402                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5403                // BPL skip_neg_dividend (+10 bytes = 5 halfwords)
5404                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5405
5406                // Negate R0:R1 (64-bit): RSBS R0, R0, #0; SBC R1, R1, R1 LSL #1
5407                // Actually: MVN R0, R0; MVN R1, R1; ADDS R0, R0, #1; ADC R1, R1, #0
5408                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5409                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5410                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5411                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5412                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5413
5414                // If divisor negative (R3 MSB set), negate it
5415                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5416                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5417
5418                // Negate R2:R3
5419                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
5420                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
5421                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
5422                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
5423                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
5424
5425                // === Now do unsigned division (same as I64DivU) ===
5426                // Initialize quotient (R4:R5) = 0
5427                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5428                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5429                // Initialize remainder (R6:R7) = 0
5430                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5431                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5432                // Initialize loop counter R8 = 64
5433                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5434                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5435
5436                let loop_start = bytes.len();
5437
5438                // Shift quotient left
5439                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5440                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5441                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5442                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5443
5444                // Shift remainder left, OR in MSB of dividend
5445                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5446                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5447                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5448                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5449                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5450                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5451
5452                // Shift dividend left
5453                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5454                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5455                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5456                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5457
5458                // Compare and conditionally subtract
5459                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5460                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5461                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5462                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5463                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5464
5465                // Subtract and set quotient bit
5466                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5467                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5468                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5469                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5470                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5471
5472                // Decrement and loop
5473                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
5474                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
5475
5476                let branch_offset_bytes = bytes.len() - loop_start + 4;
5477                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5478                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5479                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5480
5481                // Move quotient to R0:R1
5482                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5483                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5484
5485                // If result should be negative (R9 MSB set), negate R0:R1
5486                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9 (check MSB)
5487                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
5488                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8 (skip negation)
5489
5490                // Negate result R0:R1
5491                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5492                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5493                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5494                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5495                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5496
5497                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
5498                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
5499                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5500
5501                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5502                Ok(bytes)
5503            }
5504
5505            // I64RemU: 64-bit unsigned remainder using binary long division
5506            // Same algorithm as I64DivU but returns remainder instead of quotient
5507            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = remainder
5508            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5509            ArmOp::I64RemU {
5510                rdlo,
5511                rdhi,
5512                rnlo,
5513                rnhi,
5514                rmlo,
5515                rmhi,
5516            } => {
5517                let mut bytes = Vec::new();
5518                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5519                emit_i64_divisor_zero_trap(&mut bytes);
5520
5521                // PUSH {R4-R8} - save scratch registers (NO LR — inline code)
5522                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5523                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
5524
5525                // Initialize quotient (R4:R5) = 0 (computed but not returned)
5526                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5527                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5528                // Initialize remainder (R6:R7) = 0
5529                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5530                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5531                // Initialize loop counter R8 = 64
5532                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5533                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5534
5535                let loop_start = bytes.len();
5536
5537                // Shift quotient left (not needed for result, but keeps algorithm same)
5538                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5539                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5540                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5541                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5542
5543                // Shift remainder left, OR in MSB of dividend
5544                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5545                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5546                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5547                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5548                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5549                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5550
5551                // Shift dividend left
5552                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5553                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5554                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5555                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5556
5557                // Compare and conditionally subtract
5558                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5559                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5560                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5561                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5562                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5563
5564                // Subtract and set quotient bit
5565                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5566                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5567                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5568                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5569                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5570
5571                // Decrement and loop
5572                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
5573                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
5574
5575                let branch_offset_bytes = bytes.len() - loop_start + 4;
5576                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5577                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5578                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5579
5580                // Move REMAINDER to R0:R1 (difference from I64DivU)
5581                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
5582                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
5583
5584                // POP {R4-R8} - restore scratch registers (NO PC — inline code continues)
5585                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
5586                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
5587
5588                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5589                Ok(bytes)
5590            }
5591
5592            // I64RemS: 64-bit signed remainder
5593            // Remainder sign follows dividend sign (not quotient rule)
5594            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5595            //   ->  R0:R1 = remainder (signed, same sign as dividend)
5596            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5597            ArmOp::I64RemS {
5598                rdlo,
5599                rdhi,
5600                rnlo,
5601                rnhi,
5602                rmlo,
5603                rmhi,
5604            } => {
5605                let mut bytes = Vec::new();
5606                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5607                emit_i64_divisor_zero_trap(&mut bytes);
5608
5609                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5610                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5611                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5612
5613                // Save dividend sign in R9 (remainder sign = dividend sign)
5614                // MOV R9, R1 (just need the sign bit)
5615                bytes.extend_from_slice(&0x4689u16.to_le_bytes()); // MOV R9, R1
5616
5617                // If dividend negative (R1 MSB set), negate it
5618                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5619                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5620
5621                // Negate R0:R1
5622                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5623                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5624                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5625                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5626                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5627
5628                // If divisor negative (R3 MSB set), negate it
5629                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5630                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5631
5632                // Negate R2:R3
5633                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
5634                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
5635                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
5636                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
5637                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
5638
5639                // === Unsigned division algorithm ===
5640                // Initialize quotient (R4:R5) = 0
5641                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5642                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5643                // Initialize remainder (R6:R7) = 0
5644                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5645                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5646                // Initialize loop counter R8 = 64
5647                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5648                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5649
5650                let loop_start = bytes.len();
5651
5652                // Shift quotient left
5653                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5654                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5655                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5656                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5657
5658                // Shift remainder left, OR in MSB of dividend
5659                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5660                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5661                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5662                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5663                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5664                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5665
5666                // Shift dividend left
5667                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5668                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5669                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5670                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5671
5672                // Compare and conditionally subtract
5673                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5674                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5675                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5676                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5677                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5678
5679                // Subtract and set quotient bit
5680                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5681                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5682                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5683                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5684                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5685
5686                // Decrement and loop
5687                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
5688                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
5689
5690                let branch_offset_bytes = bytes.len() - loop_start + 4;
5691                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5692                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5693                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5694
5695                // Move remainder to R0:R1
5696                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
5697                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
5698
5699                // If original dividend was negative (R9 MSB set), negate remainder
5700                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9
5701                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
5702                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5703
5704                // Negate result R0:R1
5705                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5706                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5707                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5708                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5709                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5710
5711                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
5712                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
5713                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5714
5715                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5716                Ok(bytes)
5717            }
5718
5719            // === F32 VFP single-precision Thumb-2 encodings ===
5720            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
5721            ArmOp::F32Add { sd, sn, sm } => {
5722                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A00, sd, sn, sm)?))
5723            }
5724            ArmOp::F32Sub { sd, sn, sm } => {
5725                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A40, sd, sn, sm)?))
5726            }
5727            ArmOp::F32Mul { sd, sn, sm } => {
5728                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE200A00, sd, sn, sm)?))
5729            }
5730            ArmOp::F32Div { sd, sn, sm } => {
5731                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE800A00, sd, sn, sm)?))
5732            }
5733            ArmOp::F32Abs { sd, sm } => {
5734                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB00AC0, sd, sm)?))
5735            }
5736            ArmOp::F32Neg { sd, sm } => {
5737                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10A40, sd, sm)?))
5738            }
5739            ArmOp::F32Sqrt { sd, sm } => {
5740                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10AC0, sd, sm)?))
5741            }
5742
5743            // f32 pseudo-ops — multi-instruction sequences
5744            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
5745            ArmOp::F32Ceil { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b01),
5746            ArmOp::F32Floor { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b10),
5747            ArmOp::F32Trunc { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b11),
5748            ArmOp::F32Nearest { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b00),
5749            ArmOp::F32Min { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, true),
5750            ArmOp::F32Max { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, false),
5751            ArmOp::F32Copysign { sd, sn, sm } => self.encode_thumb_f32_copysign(sd, sn, sm),
5752
5753            // f32 comparisons — VCMP + VMRS + MOV #0 + IT + MOV #1
5754            ArmOp::F32Eq { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x0),
5755            ArmOp::F32Ne { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x1),
5756            ArmOp::F32Lt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x4),
5757            ArmOp::F32Le { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x9),
5758            ArmOp::F32Gt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xC),
5759            ArmOp::F32Ge { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xA),
5760
5761            ArmOp::F32Const { sd, value } => self.encode_thumb_f32_const(sd, *value),
5762
5763            ArmOp::F32Load { sd, addr } => {
5764                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED900A00, sd, addr)?))
5765            }
5766            ArmOp::F32Store { sd, addr } => {
5767                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED800A00, sd, addr)?))
5768            }
5769
5770            ArmOp::F32ConvertI32S { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, true),
5771            ArmOp::F32ConvertI32U { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, false),
5772            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
5773                Err(synth_core::Error::synthesis(
5774                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
5775                ))
5776            }
5777            ArmOp::F32ReinterpretI32 { sd, rm } => {
5778                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(true, sd, rm)?))
5779            }
5780            ArmOp::I32ReinterpretF32 { rd, sm } => {
5781                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(false, sm, rd)?))
5782            }
5783            ArmOp::I32TruncF32S { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, true),
5784            ArmOp::I32TruncF32U { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, false),
5785
5786            // === F64 VFP double-precision Thumb-2 encodings ===
5787            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
5788            ArmOp::F64Add { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
5789                0xEE300B00, dd, dn, dm,
5790            )?)),
5791            ArmOp::F64Sub { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
5792                0xEE300B40, dd, dn, dm,
5793            )?)),
5794            ArmOp::F64Mul { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
5795                0xEE200B00, dd, dn, dm,
5796            )?)),
5797            ArmOp::F64Div { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
5798                0xEE800B00, dd, dn, dm,
5799            )?)),
5800            ArmOp::F64Abs { dd, dm } => {
5801                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?))
5802            }
5803            ArmOp::F64Neg { dd, dm } => {
5804                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?))
5805            }
5806            ArmOp::F64Sqrt { dd, dm } => {
5807                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?))
5808            }
5809
5810            // f64 pseudo-ops
5811            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
5812            ArmOp::F64Ceil { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b01),
5813            ArmOp::F64Floor { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b10),
5814            ArmOp::F64Trunc { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b11),
5815            ArmOp::F64Nearest { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b00),
5816            ArmOp::F64Min { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, true),
5817            ArmOp::F64Max { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, false),
5818            ArmOp::F64Copysign { dd, dn, dm } => self.encode_thumb_f64_copysign(dd, dn, dm),
5819
5820            // f64 comparisons
5821            ArmOp::F64Eq { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x0),
5822            ArmOp::F64Ne { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x1),
5823            ArmOp::F64Lt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x4),
5824            ArmOp::F64Le { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x9),
5825            ArmOp::F64Gt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xC),
5826            ArmOp::F64Ge { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xA),
5827
5828            ArmOp::F64Const { dd, value } => self.encode_thumb_f64_const(dd, *value),
5829
5830            ArmOp::F64Load { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
5831                0xED900B00, dd, addr,
5832            )?)),
5833            ArmOp::F64Store { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
5834                0xED800B00, dd, addr,
5835            )?)),
5836
5837            ArmOp::F64ConvertI32S { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, true),
5838            ArmOp::F64ConvertI32U { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, false),
5839            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
5840                Err(synth_core::Error::synthesis(
5841                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
5842                ))
5843            }
5844            ArmOp::F64PromoteF32 { dd, sm } => self.encode_thumb_f64_promote_f32(dd, sm),
5845            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => Ok(vfp_to_thumb_bytes(
5846                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?,
5847            )),
5848            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => Ok(vfp_to_thumb_bytes(
5849                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?,
5850            )),
5851            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
5852                Err(synth_core::Error::synthesis(
5853                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
5854                ))
5855            }
5856            ArmOp::I32TruncF64S { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, true),
5857            ArmOp::I32TruncF64U { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, false),
5858
5859            // ===== i64 operations: encode as multi-instruction Thumb-2 sequences =====
5860
5861            // I64Add: ADDS rdlo, rnlo, rmlo; ADC.W rdhi, rnhi, rmhi
5862            ArmOp::I64Add {
5863                rdlo,
5864                rdhi,
5865                rnlo,
5866                rnhi,
5867                rmlo,
5868                rmhi,
5869            } => {
5870                let mut bytes = Vec::new();
5871                // ADDS rdlo, rnlo, rmlo (16-bit)
5872                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adds {
5873                    rd: *rdlo,
5874                    rn: *rnlo,
5875                    op2: Operand2::Reg(*rmlo),
5876                })?);
5877                // ADC.W rdhi, rnhi, rmhi (32-bit)
5878                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adc {
5879                    rd: *rdhi,
5880                    rn: *rnhi,
5881                    op2: Operand2::Reg(*rmhi),
5882                })?);
5883                Ok(bytes)
5884            }
5885
5886            // I64Sub: SUBS rdlo, rnlo, rmlo; SBC.W rdhi, rnhi, rmhi
5887            ArmOp::I64Sub {
5888                rdlo,
5889                rdhi,
5890                rnlo,
5891                rnhi,
5892                rmlo,
5893                rmhi,
5894            } => {
5895                let mut bytes = Vec::new();
5896                // SUBS rdlo, rnlo, rmlo (16-bit)
5897                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Subs {
5898                    rd: *rdlo,
5899                    rn: *rnlo,
5900                    op2: Operand2::Reg(*rmlo),
5901                })?);
5902                // SBC.W rdhi, rnhi, rmhi (32-bit)
5903                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Sbc {
5904                    rd: *rdhi,
5905                    rn: *rnhi,
5906                    op2: Operand2::Reg(*rmhi),
5907                })?);
5908                Ok(bytes)
5909            }
5910
5911            // I64And: AND rdlo, rnlo, rmlo; AND rdhi, rnhi, rmhi
5912            ArmOp::I64And {
5913                rdlo,
5914                rdhi,
5915                rnlo,
5916                rnhi,
5917                rmlo,
5918                rmhi,
5919            } => {
5920                let mut bytes = Vec::new();
5921                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
5922                    rd: *rdlo,
5923                    rn: *rnlo,
5924                    op2: Operand2::Reg(*rmlo),
5925                })?);
5926                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
5927                    rd: *rdhi,
5928                    rn: *rnhi,
5929                    op2: Operand2::Reg(*rmhi),
5930                })?);
5931                Ok(bytes)
5932            }
5933
5934            // I64Or: ORR rdlo, rnlo, rmlo; ORR rdhi, rnhi, rmhi
5935            ArmOp::I64Or {
5936                rdlo,
5937                rdhi,
5938                rnlo,
5939                rnhi,
5940                rmlo,
5941                rmhi,
5942            } => {
5943                let mut bytes = Vec::new();
5944                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
5945                    rd: *rdlo,
5946                    rn: *rnlo,
5947                    op2: Operand2::Reg(*rmlo),
5948                })?);
5949                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
5950                    rd: *rdhi,
5951                    rn: *rnhi,
5952                    op2: Operand2::Reg(*rmhi),
5953                })?);
5954                Ok(bytes)
5955            }
5956
5957            // I64Xor: EOR rdlo, rnlo, rmlo; EOR rdhi, rnhi, rmhi
5958            ArmOp::I64Xor {
5959                rdlo,
5960                rdhi,
5961                rnlo,
5962                rnhi,
5963                rmlo,
5964                rmhi,
5965            } => {
5966                let mut bytes = Vec::new();
5967                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
5968                    rd: *rdlo,
5969                    rn: *rnlo,
5970                    op2: Operand2::Reg(*rmlo),
5971                })?);
5972                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
5973                    rd: *rdhi,
5974                    rn: *rnhi,
5975                    op2: Operand2::Reg(*rmhi),
5976                })?);
5977                Ok(bytes)
5978            }
5979
5980            // I64Eqz: ORR scratch, lo, hi; ITE EQ; MOV rd, #1; MOV rd, #0
5981            ArmOp::I64Eqz { rd, rnlo, rnhi } => self.encode_thumb(&ArmOp::I64SetCondZ {
5982                rd: *rd,
5983                rn_lo: *rnlo,
5984                rn_hi: *rnhi,
5985            }),
5986
5987            // I64 comparisons: delegate to I64SetCond
5988            ArmOp::I64Eq {
5989                rd,
5990                rnlo,
5991                rnhi,
5992                rmlo,
5993                rmhi,
5994            } => self.encode_thumb(&ArmOp::I64SetCond {
5995                rd: *rd,
5996                rn_lo: *rnlo,
5997                rn_hi: *rnhi,
5998                rm_lo: *rmlo,
5999                rm_hi: *rmhi,
6000                cond: synth_synthesis::Condition::EQ,
6001            }),
6002
6003            ArmOp::I64Ne {
6004                rd,
6005                rnlo,
6006                rnhi,
6007                rmlo,
6008                rmhi,
6009            } => self.encode_thumb(&ArmOp::I64SetCond {
6010                rd: *rd,
6011                rn_lo: *rnlo,
6012                rn_hi: *rnhi,
6013                rm_lo: *rmlo,
6014                rm_hi: *rmhi,
6015                cond: synth_synthesis::Condition::NE,
6016            }),
6017
6018            ArmOp::I64LtS {
6019                rd,
6020                rnlo,
6021                rnhi,
6022                rmlo,
6023                rmhi,
6024            } => self.encode_thumb(&ArmOp::I64SetCond {
6025                rd: *rd,
6026                rn_lo: *rnlo,
6027                rn_hi: *rnhi,
6028                rm_lo: *rmlo,
6029                rm_hi: *rmhi,
6030                cond: synth_synthesis::Condition::LT,
6031            }),
6032
6033            ArmOp::I64LtU {
6034                rd,
6035                rnlo,
6036                rnhi,
6037                rmlo,
6038                rmhi,
6039            } => self.encode_thumb(&ArmOp::I64SetCond {
6040                rd: *rd,
6041                rn_lo: *rnlo,
6042                rn_hi: *rnhi,
6043                rm_lo: *rmlo,
6044                rm_hi: *rmhi,
6045                cond: synth_synthesis::Condition::LO,
6046            }),
6047
6048            ArmOp::I64LeS {
6049                rd,
6050                rnlo,
6051                rnhi,
6052                rmlo,
6053                rmhi,
6054            } => self.encode_thumb(&ArmOp::I64SetCond {
6055                rd: *rd,
6056                rn_lo: *rnlo,
6057                rn_hi: *rnhi,
6058                rm_lo: *rmlo,
6059                rm_hi: *rmhi,
6060                cond: synth_synthesis::Condition::LE,
6061            }),
6062
6063            ArmOp::I64LeU {
6064                rd,
6065                rnlo,
6066                rnhi,
6067                rmlo,
6068                rmhi,
6069            } => self.encode_thumb(&ArmOp::I64SetCond {
6070                rd: *rd,
6071                rn_lo: *rnlo,
6072                rn_hi: *rnhi,
6073                rm_lo: *rmlo,
6074                rm_hi: *rmhi,
6075                cond: synth_synthesis::Condition::LS,
6076            }),
6077
6078            ArmOp::I64GtS {
6079                rd,
6080                rnlo,
6081                rnhi,
6082                rmlo,
6083                rmhi,
6084            } => self.encode_thumb(&ArmOp::I64SetCond {
6085                rd: *rd,
6086                rn_lo: *rnlo,
6087                rn_hi: *rnhi,
6088                rm_lo: *rmlo,
6089                rm_hi: *rmhi,
6090                cond: synth_synthesis::Condition::GT,
6091            }),
6092
6093            ArmOp::I64GtU {
6094                rd,
6095                rnlo,
6096                rnhi,
6097                rmlo,
6098                rmhi,
6099            } => self.encode_thumb(&ArmOp::I64SetCond {
6100                rd: *rd,
6101                rn_lo: *rnlo,
6102                rn_hi: *rnhi,
6103                rm_lo: *rmlo,
6104                rm_hi: *rmhi,
6105                cond: synth_synthesis::Condition::HI,
6106            }),
6107
6108            ArmOp::I64GeS {
6109                rd,
6110                rnlo,
6111                rnhi,
6112                rmlo,
6113                rmhi,
6114            } => self.encode_thumb(&ArmOp::I64SetCond {
6115                rd: *rd,
6116                rn_lo: *rnlo,
6117                rn_hi: *rnhi,
6118                rm_lo: *rmlo,
6119                rm_hi: *rmhi,
6120                cond: synth_synthesis::Condition::GE,
6121            }),
6122
6123            ArmOp::I64GeU {
6124                rd,
6125                rnlo,
6126                rnhi,
6127                rmlo,
6128                rmhi,
6129            } => self.encode_thumb(&ArmOp::I64SetCond {
6130                rd: *rd,
6131                rn_lo: *rnlo,
6132                rn_hi: *rnhi,
6133                rm_lo: *rmlo,
6134                rm_hi: *rmhi,
6135                cond: synth_synthesis::Condition::HS,
6136            }),
6137
6138            // I64Const: MOVW rdlo, lo16; MOVT rdlo, hi16; MOVW rdhi, lo16_hi; MOVT rdhi, hi16_hi
6139            ArmOp::I64Const { rdlo, rdhi, value } => {
6140                let lo32 = *value as u32;
6141                let hi32 = (*value >> 32) as u32;
6142                let mut bytes = Vec::new();
6143                // Load low 32 bits into rdlo
6144                bytes.extend_from_slice(
6145                    &self.encode_thumb32_movw_raw(reg_to_bits(rdlo), lo32 & 0xFFFF)?,
6146                );
6147                if lo32 > 0xFFFF {
6148                    bytes.extend_from_slice(
6149                        &self.encode_thumb32_movt_raw(reg_to_bits(rdlo), lo32 >> 16)?,
6150                    );
6151                }
6152                // Load high 32 bits into rdhi
6153                bytes.extend_from_slice(
6154                    &self.encode_thumb32_movw_raw(reg_to_bits(rdhi), hi32 & 0xFFFF)?,
6155                );
6156                if hi32 > 0xFFFF {
6157                    bytes.extend_from_slice(
6158                        &self.encode_thumb32_movt_raw(reg_to_bits(rdhi), hi32 >> 16)?,
6159                    );
6160                }
6161                Ok(bytes)
6162            }
6163
6164            // I64Ldr: LDR rdlo, [base, offset]; LDR rdhi, [base, offset+4]
6165            ArmOp::I64Ldr { rdlo, rdhi, addr } => {
6166                let mut bytes = Vec::new();
6167                // #372/#382: a memory `i64.load` carries an index register
6168                // (`reg_imm(R11, addr_reg, offset)` = R11 + addr + offset). The
6169                // immediate `encode_thumb32_ldr` below uses only base+offset and
6170                // would SILENTLY DROP `offset_reg` — the #206 defect, here for
6171                // i64. `i64_effective_base` materializes the effective base into
6172                // `ip` (and, when `offset+4 > 0xFFF`, folds the offset in too so
6173                // the function is NOT skipped — #382), returning the residual
6174                // imm12 for the two halves. Frame i64 loads (no `offset_reg`, e.g.
6175                // a spilled local at `[SP, #off]`) keep the plain `[base,#off]`
6176                // form unchanged — so existing output is byte-identical.
6177                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6178                bytes.extend_from_slice(&self.encode_thumb32_ldr(rdlo, &base, offset)?);
6179                bytes.extend_from_slice(&self.encode_thumb32_ldr(
6180                    rdhi,
6181                    &base,
6182                    offset.wrapping_add(4),
6183                )?);
6184                Ok(bytes)
6185            }
6186
6187            // I64Str: STR rdlo, [base, offset]; STR rdhi, [base, offset+4]
6188            ArmOp::I64Str { rdlo, rdhi, addr } => {
6189                let mut bytes = Vec::new();
6190                // #372/#382: same index-materialization + large-offset fold as
6191                // I64Ldr (see above).
6192                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6193                bytes.extend_from_slice(&self.encode_thumb32_str(rdlo, &base, offset)?);
6194                bytes.extend_from_slice(&self.encode_thumb32_str(
6195                    rdhi,
6196                    &base,
6197                    offset.wrapping_add(4),
6198                )?);
6199                Ok(bytes)
6200            }
6201
6202            // I64ExtendI32S: MOV rdlo, rn; ASR rdhi, rdlo, #31 (sign-extend)
6203            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
6204                let mut bytes = Vec::new();
6205                if rdlo != rn {
6206                    // MOV rdlo, rn (16-bit)
6207                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6208                        rd: *rdlo,
6209                        op2: Operand2::Reg(*rn),
6210                    })?);
6211                }
6212                // ASR rdhi, rdlo, #31 (sign-extend: fill high word with sign bit)
6213                bytes.extend_from_slice(
6214                    &self.encode_thumb32_shift(rdhi, rdlo, 31, 0b10)?, // ASR type
6215                );
6216                Ok(bytes)
6217            }
6218
6219            // I64ExtendI32U: MOV rdlo, rn; MOV rdhi, #0
6220            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
6221                let mut bytes = Vec::new();
6222                if rdlo != rn {
6223                    // MOV rdlo, rn
6224                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6225                        rd: *rdlo,
6226                        op2: Operand2::Reg(*rn),
6227                    })?);
6228                }
6229                // MOV rdhi, #0 (16-bit: MOVS Rd, #0)
6230                let rdhi_bits = reg_to_bits(rdhi) as u16;
6231                let instr: u16 = 0x2000 | (rdhi_bits << 8);
6232                bytes.extend_from_slice(&instr.to_le_bytes());
6233                Ok(bytes)
6234            }
6235
6236            // I32WrapI64: MOV rd, rnlo (just take low 32 bits)
6237            ArmOp::I32WrapI64 { rd, rnlo } => {
6238                if rd == rnlo {
6239                    // No-op: already in the right register
6240                    let instr: u16 = 0xBF00; // NOP
6241                    Ok(instr.to_le_bytes().to_vec())
6242                } else {
6243                    // MOV rd, rnlo
6244                    self.encode_thumb(&ArmOp::Mov {
6245                        rd: *rd,
6246                        op2: Operand2::Reg(*rnlo),
6247                    })
6248                }
6249            }
6250
6251            // ===== Helium MVE operations (Thumb-2 encoding) =====
6252            ArmOp::MveLoad { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vldrw(qd, addr))),
6253            ArmOp::MveStore { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vstrw(qd, addr))),
6254            ArmOp::MveConst { qd, bytes } => self.encode_thumb_mve_const(qd, bytes),
6255            ArmOp::MveAnd { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6256                0xEF000150, qd, qn, qm,
6257            ))),
6258            ArmOp::MveOrr { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6259                0xEF200150, qd, qn, qm,
6260            ))),
6261            ArmOp::MveEor { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6262                0xFF000150, qd, qn, qm,
6263            ))),
6264            ArmOp::MveMvn { qd, qm } => {
6265                // VMVN Qd, Qm: 0xFFB005C0 | Qd<<12 | Qm
6266                let qd_enc = qreg_to_num(qd);
6267                let qm_enc = qreg_to_num(qm);
6268                let instr: u32 = 0xFFB005C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6269                Ok(vfp_to_thumb_bytes(instr))
6270            }
6271            ArmOp::MveBic { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6272                0xEF100150, qd, qn, qm,
6273            ))),
6274            ArmOp::MveAddI { qd, qn, qm, size } => {
6275                let sz = mve_size_bits(size);
6276                let base: u32 = 0xEF000840 | (sz << 20);
6277                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6278            }
6279            ArmOp::MveSubI { qd, qn, qm, size } => {
6280                let sz = mve_size_bits(size);
6281                let base: u32 = 0xFF000840 | (sz << 20);
6282                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6283            }
6284            ArmOp::MveMulI { qd, qn, qm, size } => {
6285                let sz = mve_size_bits(size);
6286                let base: u32 = 0xEF000950 | (sz << 20);
6287                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6288            }
6289            ArmOp::MveNegI { qd, qm, size } => {
6290                let sz = mve_size_bits(size);
6291                // VNEG.Sx Qd, Qm
6292                let qd_enc = qreg_to_num(qd);
6293                let qm_enc = qreg_to_num(qm);
6294                let base: u32 = 0xFFB103C0 | (sz << 18);
6295                let instr = base | ((qd_enc * 2) << 12) | (qm_enc * 2);
6296                Ok(vfp_to_thumb_bytes(instr))
6297            }
6298            ArmOp::MveDup { qd, rn, size } => {
6299                let sz = mve_size_bits(size);
6300                let qd_enc = qreg_to_num(qd);
6301                let rn_bits = reg_to_bits(rn);
6302                // VDUP.sz Qd, Rn: EEA0 0B10 variant
6303                // size encoding: 00=32, 01=16, 10=8
6304                let be = match sz {
6305                    0 => 0b00u32, // 8-bit
6306                    1 => 0b01,    // 16-bit
6307                    _ => 0b00,    // 32-bit (default)
6308                };
6309                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12) | (be << 5);
6310                Ok(vfp_to_thumb_bytes(instr))
6311            }
6312            ArmOp::MveExtractLane { rd, qn, lane, size } => {
6313                let qn_enc = qreg_to_num(qn);
6314                let rd_bits = reg_to_bits(rd);
6315                // VMOV.sz Rd, Dn[x] — extract from Q-register lane
6316                // For 32-bit: VMOV Rd, Dn — where Dn is the appropriate D-register
6317                let d_reg = qn_enc * 2 + ((*lane as u32) >> 1);
6318                let lane_in_d = (*lane as u32) & 1;
6319                let _sz = mve_size_bits(size);
6320                // VMOV Rd, Dn[x]: EE10 0B10 for 32-bit
6321                let instr: u32 = 0xEE100B10 | (d_reg << 16) | (rd_bits << 12) | (lane_in_d << 21);
6322                Ok(vfp_to_thumb_bytes(instr))
6323            }
6324            ArmOp::MveInsertLane { qd, rn, lane, size } => {
6325                let qd_enc = qreg_to_num(qd);
6326                let rn_bits = reg_to_bits(rn);
6327                let d_reg = qd_enc * 2 + ((*lane as u32) >> 1);
6328                let lane_in_d = (*lane as u32) & 1;
6329                let _sz = mve_size_bits(size);
6330                // VMOV Dn[x], Rn: EE00 0B10 for 32-bit
6331                let instr: u32 = 0xEE000B10 | (d_reg << 16) | (rn_bits << 12) | (lane_in_d << 21);
6332                Ok(vfp_to_thumb_bytes(instr))
6333            }
6334
6335            // MVE float comparisons — emit VCMP + VPSEL sequence (simplified: just VCMP)
6336            ArmOp::MveCmpEqI { qd, qn, qm, size }
6337            | ArmOp::MveCmpNeI { qd, qn, qm, size }
6338            | ArmOp::MveCmpLtS { qd, qn, qm, size }
6339            | ArmOp::MveCmpLtU { qd, qn, qm, size }
6340            | ArmOp::MveCmpGtS { qd, qn, qm, size }
6341            | ArmOp::MveCmpGtU { qd, qn, qm, size }
6342            | ArmOp::MveCmpLeS { qd, qn, qm, size }
6343            | ArmOp::MveCmpLeU { qd, qn, qm, size }
6344            | ArmOp::MveCmpGeS { qd, qn, qm, size }
6345            | ArmOp::MveCmpGeU { qd, qn, qm, size } => {
6346                // Encode as VADD (placeholder encoding — real implementation
6347                // would use VCMP + VPSEL pair)
6348                let sz = mve_size_bits(size);
6349                let base: u32 = 0xEF000840 | (sz << 20);
6350                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6351            }
6352
6353            // f32x4 MVE arithmetic
6354            ArmOp::MveAddF32 { qd, qn, qm } => {
6355                // VADD.F32 Qd, Qn, Qm (MVE): 0xEF000D40
6356                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6357            }
6358            ArmOp::MveSubF32 { qd, qn, qm } => {
6359                // VSUB.F32 Qd, Qn, Qm (MVE): 0xEF200D40
6360                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF200D40, qd, qn, qm)))
6361            }
6362            ArmOp::MveMulF32 { qd, qn, qm } => {
6363                // VMUL.F32 Qd, Qn, Qm (MVE): 0xFF000D50
6364                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xFF000D50, qd, qn, qm)))
6365            }
6366            ArmOp::MveNegF32 { qd, qm } => {
6367                let qd_enc = qreg_to_num(qd);
6368                let qm_enc = qreg_to_num(qm);
6369                // VNEG.F32 Qd, Qm: FFB907C0
6370                let instr: u32 = 0xFFB907C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6371                Ok(vfp_to_thumb_bytes(instr))
6372            }
6373            ArmOp::MveAbsF32 { qd, qm } => {
6374                let qd_enc = qreg_to_num(qd);
6375                let qm_enc = qreg_to_num(qm);
6376                // VABS.F32 Qd, Qm: FFB90740
6377                let instr: u32 = 0xFFB90740 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6378                Ok(vfp_to_thumb_bytes(instr))
6379            }
6380            ArmOp::MveCmpEqF32 { qd, qn, qm }
6381            | ArmOp::MveCmpNeF32 { qd, qn, qm }
6382            | ArmOp::MveCmpLtF32 { qd, qn, qm }
6383            | ArmOp::MveCmpLeF32 { qd, qn, qm }
6384            | ArmOp::MveCmpGtF32 { qd, qn, qm }
6385            | ArmOp::MveCmpGeF32 { qd, qn, qm } => {
6386                // Placeholder: encode as VADD.F32 (real impl needs VCMP.F32 + VPSEL)
6387                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6388            }
6389            ArmOp::MveDupF32 { qd, rn } => {
6390                let qd_enc = qreg_to_num(qd);
6391                let rn_bits = reg_to_bits(rn);
6392                // VDUP.32 Qd, Rn (same encoding as integer VDUP.32)
6393                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12);
6394                Ok(vfp_to_thumb_bytes(instr))
6395            }
6396            ArmOp::MveExtractLaneF32 { rd, qn, lane } => {
6397                let qn_enc = qreg_to_num(qn);
6398                let rd_bits = reg_to_bits(rd);
6399                // VMOV Rd, Sn where Sn = Q*4 + lane
6400                let s_num = qn_enc * 4 + (*lane as u32);
6401                let (vn, n) = encode_sreg(s_num);
6402                let instr: u32 = 0xEE100A10 | (vn << 16) | (rd_bits << 12) | (n << 7);
6403                Ok(vfp_to_thumb_bytes(instr))
6404            }
6405            ArmOp::MveReplaceLaneF32 { qd, rn, lane } => {
6406                let qd_enc = qreg_to_num(qd);
6407                let rn_bits = reg_to_bits(rn);
6408                // VMOV Sn, Rn where Sn = Q*4 + lane
6409                let s_num = qd_enc * 4 + (*lane as u32);
6410                let (vn, n) = encode_sreg(s_num);
6411                let instr: u32 = 0xEE000A10 | (vn << 16) | (rn_bits << 12) | (n << 7);
6412                Ok(vfp_to_thumb_bytes(instr))
6413            }
6414            ArmOp::MveDivF32 { qd, qn, qm } => {
6415                // Lane-wise: extract 4 S-regs, VDIV, insert back
6416                self.encode_thumb_mve_lane_wise_f32_binop(qd, qn, qm, 0xEE800A00)
6417            }
6418            ArmOp::MveSqrtF32 { qd, qm } => {
6419                // Lane-wise: extract 4 S-regs, VSQRT, insert back
6420                self.encode_thumb_mve_lane_wise_f32_sqrt(qd, qm)
6421            }
6422
6423            // Catch-all for any remaining ops
6424            _ => {
6425                let instr: u16 = 0xBF00; // NOP
6426                Ok(instr.to_le_bytes().to_vec())
6427            }
6428        }
6429    }
6430
6431    // === Thumb-2 VFP multi-instruction helpers ===
6432
6433    /// Encode F32 comparison as Thumb-2: VCMP.F32 + VMRS + MOVS rd,#0 + IT + MOV rd,#1
6434    fn encode_thumb_f32_compare(
6435        &self,
6436        rd: &Reg,
6437        sn: &VfpReg,
6438        sm: &VfpReg,
6439        cond_code: u32,
6440    ) -> Result<Vec<u8>> {
6441        let mut bytes = Vec::new();
6442        let rd_bits = reg_to_bits(rd);
6443
6444        // VCMP.F32 Sn, Sm
6445        let sn_num = vfp_sreg_to_num(sn)?;
6446        let sm_num = vfp_sreg_to_num(sm)?;
6447        let (vd, d) = encode_sreg(sn_num);
6448        let (vm, m) = encode_sreg(sm_num);
6449        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
6450        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
6451
6452        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
6453        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
6454
6455        // MOVS Rd, #0 (16-bit): 0010 0 Rd(3) 0000 0000
6456        if rd_bits < 8 {
6457            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
6458            bytes.extend_from_slice(&movs_zero.to_le_bytes());
6459        } else {
6460            // MOV.W Rd, #0 (32-bit Thumb-2)
6461            let hw1: u16 = 0xF04F;
6462            let hw2: u16 = (rd_bits as u16) << 8;
6463            bytes.extend_from_slice(&hw1.to_le_bytes());
6464            bytes.extend_from_slice(&hw2.to_le_bytes());
6465        }
6466
6467        // IT<cond> — If-Then for conditional MOV
6468        // IT encoding: 1011 1111 cond(4) mask(4)
6469        // mask = 0x8 for single "then" (IT)
6470        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
6471        bytes.extend_from_slice(&it.to_le_bytes());
6472
6473        // MOV Rd, #1 (16-bit, conditional due to IT): 0010 0 Rd(3) 0000 0001
6474        if rd_bits < 8 {
6475            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
6476            bytes.extend_from_slice(&mov_one.to_le_bytes());
6477        } else {
6478            // MOV.W Rd, #1 (32-bit)
6479            let hw1: u16 = 0xF04F;
6480            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
6481            bytes.extend_from_slice(&hw1.to_le_bytes());
6482            bytes.extend_from_slice(&hw2.to_le_bytes());
6483        }
6484
6485        Ok(bytes)
6486    }
6487
6488    /// Encode F32 constant load as Thumb-2: MOVW + MOVT + VMOV
6489    fn encode_thumb_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
6490        let mut bytes = Vec::new();
6491        let bits = value.to_bits();
6492        let rt: u32 = 12; // R12/IP as temp
6493
6494        // MOVW R12, #lo16
6495        // Thumb-2 MOVW: 11110 i 10 0100 imm4 | 0 imm3 Rd imm8
6496        let lo16 = bits & 0xFFFF;
6497        let imm4 = (lo16 >> 12) & 0xF;
6498        let i_bit = (lo16 >> 11) & 1;
6499        let imm3 = (lo16 >> 8) & 0x7;
6500        let imm8 = lo16 & 0xFF;
6501        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
6502        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
6503        bytes.extend_from_slice(&hw1.to_le_bytes());
6504        bytes.extend_from_slice(&hw2.to_le_bytes());
6505
6506        // MOVT R12, #hi16
6507        let hi16 = (bits >> 16) & 0xFFFF;
6508        let imm4 = (hi16 >> 12) & 0xF;
6509        let i_bit = (hi16 >> 11) & 1;
6510        let imm3 = (hi16 >> 8) & 0x7;
6511        let imm8 = hi16 & 0xFF;
6512        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
6513        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
6514        bytes.extend_from_slice(&hw1.to_le_bytes());
6515        bytes.extend_from_slice(&hw2.to_le_bytes());
6516
6517        // VMOV Sd, R12
6518        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
6519        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6520
6521        Ok(bytes)
6522    }
6523
6524    /// Encode VMOV + VCVT.F32.xS32 as Thumb-2
6525    fn encode_thumb_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
6526        let mut bytes = Vec::new();
6527
6528        // VMOV Sd, Rm
6529        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
6530        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6531
6532        // VCVT.F32.S32/U32 Sd, Sd
6533        let sd_num = vfp_sreg_to_num(sd)?;
6534        let (vd, d) = encode_sreg(sd_num);
6535        let (vm, m) = encode_sreg(sd_num);
6536        let base = if signed { 0xEEB80A40 } else { 0xEEB80AC0 };
6537        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
6538        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
6539
6540        Ok(bytes)
6541    }
6542
6543    /// Encode F32 rounding pseudo-op as Thumb-2 via VCVT to integer and back
6544    /// Encode F32 rounding as Thumb-2.
6545    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
6546    ///
6547    /// For trunc: uses VCVTR.S32.F32 (always truncates).
6548    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant),
6549    /// then restores FPSCR.
6550    fn encode_thumb_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
6551        let mut bytes = Vec::new();
6552        let sm_num = vfp_sreg_to_num(sm)?;
6553        let sd_num = vfp_sreg_to_num(sd)?;
6554        let (vd_s, d_s) = encode_sreg(sd_num);
6555        let (vm_s, m_s) = encode_sreg(sm_num);
6556
6557        if mode == 0b11 {
6558            // Trunc (toward zero): VCVTR.S32.F32 — bit[7]=1, always truncates
6559            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
6560            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
6561        } else {
6562            // ceil/floor/nearest: manipulate FPSCR rounding mode
6563            let rt: u32 = 12; // R12/IP as temp
6564
6565            // VMRS R12, FPSCR
6566            let vmrs = 0xEEF10A10 | (rt << 12);
6567            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
6568
6569            // BIC.W R12, R12, #(3 << 22) — clear RMode bits [23:22]
6570            // Thumb-2 modified immediate for 3<<22 = 0x00C00000:
6571            // BIC.W encoding: 11110 i 0 0001 S Rn | 0 imm3 Rd imm8
6572            // 0x00C00000 = 0x03 shifted left by 22 => Thumb mod-imm: i=0, imm3=0b101, imm8=0x03
6573            let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF); // BIC, Rn=R12
6574            let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
6575            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
6576            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
6577
6578            // ORR.W R12, R12, #(mode << 22)
6579            if mode != 0 {
6580                let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF); // ORR, Rn=R12
6581                let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
6582                bytes.extend_from_slice(&orr_hw1.to_le_bytes());
6583                bytes.extend_from_slice(&orr_hw2.to_le_bytes());
6584            }
6585
6586            // VMSR FPSCR, R12
6587            let vmsr = 0xEEE10A10 | (rt << 12);
6588            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
6589
6590            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rmode
6591            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
6592            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
6593
6594            // Restore FPSCR: clear rmode bits back to nearest (default)
6595            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
6596            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
6597            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
6598            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
6599        }
6600
6601        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
6602        let (vd2, d2) = encode_sreg(sd_num);
6603        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
6604        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
6605
6606        Ok(bytes)
6607    }
6608
6609    /// Encode F32 min/max as Thumb-2: VMOV + VCMP + VMRS + IT + VMOV
6610    fn encode_thumb_f32_minmax(
6611        &self,
6612        sd: &VfpReg,
6613        sn: &VfpReg,
6614        sm: &VfpReg,
6615        is_min: bool,
6616    ) -> Result<Vec<u8>> {
6617        let mut bytes = Vec::new();
6618        let sn_num = vfp_sreg_to_num(sn)?;
6619        let sm_num = vfp_sreg_to_num(sm)?;
6620        let sd_num = vfp_sreg_to_num(sd)?;
6621
6622        // VMOV.F32 Sd, Sn
6623        let (vd, d) = encode_sreg(sd_num);
6624        let (vn, n) = encode_sreg(sn_num);
6625        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
6626        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sn));
6627
6628        // VCMP.F32 Sn, Sm
6629        let (vm, m) = encode_sreg(sm_num);
6630        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
6631        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
6632
6633        // VMRS APSR_nzcv, FPSCR
6634        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
6635
6636        // IT GT (for min) or IT MI (for max)
6637        let cond: u16 = if is_min { 0xC } else { 0x4 };
6638        let it: u16 = 0xBF00 | (cond << 4) | 0x8;
6639        bytes.extend_from_slice(&it.to_le_bytes());
6640
6641        // VMOV{cond}.F32 Sd, Sm — conditional VMOV in IT block
6642        let vmov_sm = 0xEEB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
6643        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sm));
6644
6645        Ok(bytes)
6646    }
6647
6648    /// Encode F32 copysign as Thumb-2
6649    fn encode_thumb_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
6650        let mut bytes = Vec::new();
6651
6652        // VMOV R12, Sm (get sign source bits)
6653        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
6654            false,
6655            sm,
6656            &Reg::R12,
6657        )?));
6658
6659        // VMOV R0, Sn (get magnitude source bits)
6660        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
6661            false,
6662            sn,
6663            &Reg::R0,
6664        )?));
6665
6666        // AND.W R12, R12, #0x80000000
6667        // Thumb-2 modified immediate: 0x80000000 = constant 0x80 with rotation
6668        // Using T1 encoding: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8
6669        // 0x80000000: i=0, imm3=0b001, imm8=0x00 (rotation=4, value=0x80)
6670        // Actually encoding #0x80000000 as modified constant:
6671        // bit pattern 1 followed by 31 zeros: enc = 0b0100_00000000 = 0x0100? No.
6672        // ARM modified immediate: abcdefgh rotated. 0x80000000 = 0x80 ROR 2 = enc 0x0102
6673        // Actually: value = abcdefgh ROR (2*rot). 0x80 = 10000000, ROR 2 gives 0x20000000.
6674        // For 0x80000000: 0x02 ROR 2 = 0x80000000. So imm12 = (1<<8) | 0x02 = 0x102
6675        let hw1: u16 = 0xF000 | 12; // AND.W R12, R12, #modified_const (i=0, Rn=R12)
6676        let hw2: u16 = (0x1 << 12) | (12 << 8) | 0x02; // imm3=1, Rd=R12, imm8=0x02
6677        bytes.extend_from_slice(&hw1.to_le_bytes());
6678        bytes.extend_from_slice(&hw2.to_le_bytes());
6679
6680        // BIC.W R0, R0, #0x80000000 (R0 = register 0, fields are zero)
6681        let hw1: u16 = 0xF020; // BIC.W R0, R0, #modified_const (i=0, Rn=R0)
6682        let hw2: u16 = (0x1 << 12) | 0x02; // imm3=1, Rd=R0, imm8=0x02
6683        bytes.extend_from_slice(&hw1.to_le_bytes());
6684        bytes.extend_from_slice(&hw2.to_le_bytes());
6685
6686        // ORR.W R0, R0, R12 (R0 = register 0)
6687        let hw1: u16 = 0xEA40; // ORR.W R0, R0, R12 (Rn=R0)
6688        let hw2: u16 = 12; // Rd=R0, Rm=R12
6689        bytes.extend_from_slice(&hw1.to_le_bytes());
6690        bytes.extend_from_slice(&hw2.to_le_bytes());
6691
6692        // VMOV Sd, R0
6693        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
6694            true,
6695            sd,
6696            &Reg::R0,
6697        )?));
6698
6699        Ok(bytes)
6700    }
6701
6702    /// Encode F64 comparison as Thumb-2: VCMP.F64 + VMRS + MOV #0 + IT + MOV #1
6703    fn encode_thumb_f64_compare(
6704        &self,
6705        rd: &Reg,
6706        dn: &VfpReg,
6707        dm: &VfpReg,
6708        cond_code: u32,
6709    ) -> Result<Vec<u8>> {
6710        let mut bytes = Vec::new();
6711        let rd_bits = reg_to_bits(rd);
6712
6713        // VCMP.F64 Dn, Dm
6714        let dn_num = vfp_dreg_to_num(dn)?;
6715        let dm_num = vfp_dreg_to_num(dm)?;
6716        let (vd, d) = encode_dreg(dn_num);
6717        let (vm, m) = encode_dreg(dm_num);
6718        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
6719        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
6720
6721        // VMRS APSR_nzcv, FPSCR
6722        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
6723
6724        // MOVS Rd, #0
6725        if rd_bits < 8 {
6726            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
6727            bytes.extend_from_slice(&movs_zero.to_le_bytes());
6728        } else {
6729            let hw1: u16 = 0xF04F;
6730            let hw2: u16 = (rd_bits as u16) << 8;
6731            bytes.extend_from_slice(&hw1.to_le_bytes());
6732            bytes.extend_from_slice(&hw2.to_le_bytes());
6733        }
6734
6735        // IT<cond>
6736        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
6737        bytes.extend_from_slice(&it.to_le_bytes());
6738
6739        // MOV Rd, #1
6740        if rd_bits < 8 {
6741            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
6742            bytes.extend_from_slice(&mov_one.to_le_bytes());
6743        } else {
6744            let hw1: u16 = 0xF04F;
6745            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
6746            bytes.extend_from_slice(&hw1.to_le_bytes());
6747            bytes.extend_from_slice(&hw2.to_le_bytes());
6748        }
6749
6750        Ok(bytes)
6751    }
6752
6753    /// Encode F64 constant load as Thumb-2: MOVW+MOVT (lo32 into R0) + MOVW+MOVT (hi32 into R12) + VMOV Dd, R0, R12
6754    fn encode_thumb_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
6755        let mut bytes = Vec::new();
6756        let bits = value.to_bits();
6757        let lo32 = bits as u32;
6758        let hi32 = (bits >> 32) as u32;
6759
6760        // MOVW R0, #lo16(lo32)
6761        let lo16 = lo32 & 0xFFFF;
6762        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(0, lo16)?);
6763
6764        // MOVT R0, #hi16(lo32)
6765        let hi16 = (lo32 >> 16) & 0xFFFF;
6766        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(0, hi16)?);
6767
6768        // MOVW R12, #lo16(hi32)
6769        let lo16 = hi32 & 0xFFFF;
6770        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
6771
6772        // MOVT R12, #hi16(hi32)
6773        let hi16 = (hi32 >> 16) & 0xFFFF;
6774        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
6775
6776        // VMOV Dd, R0, R12
6777        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
6778        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6779
6780        Ok(bytes)
6781    }
6782
6783    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as Thumb-2
6784    fn encode_thumb_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
6785        let mut bytes = Vec::new();
6786
6787        // VMOV S0, Rm
6788        let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
6789        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6790
6791        // VCVT.F64.S32 Dd, S0 or VCVT.F64.U32 Dd, S0
6792        let dd_num = vfp_dreg_to_num(dd)?;
6793        let (vd, d) = encode_dreg(dd_num);
6794        let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
6795        let vcvt = base | (d << 22) | (vd << 12);
6796        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
6797
6798        Ok(bytes)
6799    }
6800
6801    /// Encode VCVT.F64.F32 Dd, Sm as Thumb-2
6802    fn encode_thumb_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
6803        let dd_num = vfp_dreg_to_num(dd)?;
6804        let sm_num = vfp_sreg_to_num(sm)?;
6805        let (vd, d) = encode_dreg(dd_num);
6806        let (vm, m) = encode_sreg(sm_num);
6807
6808        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
6809        Ok(vfp_to_thumb_bytes(vcvt))
6810    }
6811
6812    /// Encode VCVT.S32/U32.F64 S0, Dm + VMOV Rd, S0 as Thumb-2
6813    fn encode_thumb_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
6814        let mut bytes = Vec::new();
6815        let dm_num = vfp_dreg_to_num(dm)?;
6816        let (vm, m) = encode_dreg(dm_num);
6817
6818        // VCVT.S32.F64 S0, Dm or VCVT.U32.F64 S0, Dm
6819        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
6820        let vcvt = base | (m << 5) | vm;
6821        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
6822
6823        // VMOV Rd, S0
6824        let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
6825        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6826
6827        Ok(bytes)
6828    }
6829
6830    /// Encode F64 rounding pseudo-op as Thumb-2 via VCVT to integer and back
6831    /// Encode F64 rounding as Thumb-2.
6832    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
6833    fn encode_thumb_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
6834        let mut bytes = Vec::new();
6835        let dm_num = vfp_dreg_to_num(dm)?;
6836        let dd_num = vfp_dreg_to_num(dd)?;
6837        let (vm, m) = encode_dreg(dm_num);
6838        let (vd, d) = encode_dreg(dd_num);
6839
6840        if mode == 0b11 {
6841            // Trunc: VCVTR.S32.F64 — bit[7]=1, always truncates
6842            let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
6843            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
6844        } else {
6845            let rt: u32 = 12;
6846
6847            // VMRS R12, FPSCR
6848            let vmrs = 0xEEF10A10 | (rt << 12);
6849            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
6850
6851            // BIC.W R12, R12, #(3 << 22)
6852            let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF);
6853            let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
6854            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
6855            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
6856
6857            // ORR.W R12, R12, #(mode << 22)
6858            if mode != 0 {
6859                let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF);
6860                let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
6861                bytes.extend_from_slice(&orr_hw1.to_le_bytes());
6862                bytes.extend_from_slice(&orr_hw2.to_le_bytes());
6863            }
6864
6865            // VMSR FPSCR, R12
6866            let vmsr = 0xEEE10A10 | (rt << 12);
6867            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
6868
6869            // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0)
6870            let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
6871            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
6872
6873            // Restore FPSCR
6874            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
6875            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
6876            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
6877            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
6878        }
6879
6880        // VCVT.F64.S32 Dd, S0
6881        let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
6882        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
6883
6884        Ok(bytes)
6885    }
6886
6887    /// Encode F64 min/max as Thumb-2
6888    fn encode_thumb_f64_minmax(
6889        &self,
6890        dd: &VfpReg,
6891        dn: &VfpReg,
6892        dm: &VfpReg,
6893        is_min: bool,
6894    ) -> Result<Vec<u8>> {
6895        let mut bytes = Vec::new();
6896        let dn_num = vfp_dreg_to_num(dn)?;
6897        let dm_num = vfp_dreg_to_num(dm)?;
6898        let dd_num = vfp_dreg_to_num(dd)?;
6899
6900        // VMOV.F64 Dd, Dn
6901        let (vd, d) = encode_dreg(dd_num);
6902        let (vn, n) = encode_dreg(dn_num);
6903        let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
6904        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_dn));
6905
6906        // VCMP.F64 Dn, Dm
6907        let (vm, m) = encode_dreg(dm_num);
6908        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
6909        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
6910
6911        // VMRS APSR_nzcv, FPSCR
6912        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
6913
6914        // IT GT (for min) or IT MI (for max)
6915        let cond: u16 = if is_min { 0xC } else { 0x4 };
6916        let it: u16 = 0xBF00 | (cond << 4) | 0x8;
6917        bytes.extend_from_slice(&it.to_le_bytes());
6918
6919        // VMOV{cond}.F64 Dd, Dm
6920        let vmov_dm = 0xEEB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
6921        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_dm));
6922
6923        Ok(bytes)
6924    }
6925
6926    /// Encode F64 copysign as Thumb-2
6927    fn encode_thumb_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
6928        let mut bytes = Vec::new();
6929
6930        // VMOV R0, R12, Dm (get sign source)
6931        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_dreg(
6932            false,
6933            dm,
6934            &Reg::R0,
6935            &Reg::R12,
6936        )?));
6937
6938        // VMOV R1, R2, Dn (get magnitude source)
6939        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_dreg(
6940            false,
6941            dn,
6942            &Reg::R1,
6943            &Reg::R2,
6944        )?));
6945
6946        // AND.W R12, R12, #0x80000000 (i=0, Rn=R12)
6947        let hw1: u16 = 0xF000 | 12;
6948        let hw2: u16 = (0x1 << 12) | (12 << 8) | 0x02;
6949        bytes.extend_from_slice(&hw1.to_le_bytes());
6950        bytes.extend_from_slice(&hw2.to_le_bytes());
6951
6952        // BIC.W R2, R2, #0x80000000 (i=0, Rn=R2)
6953        let hw1: u16 = 0xF020 | 2;
6954        let hw2: u16 = (0x1 << 12) | (2 << 8) | 0x02;
6955        bytes.extend_from_slice(&hw1.to_le_bytes());
6956        bytes.extend_from_slice(&hw2.to_le_bytes());
6957
6958        // ORR.W R2, R2, R12
6959        let hw1: u16 = 0xEA40 | 2;
6960        let hw2: u16 = (2 << 8) | 12;
6961        bytes.extend_from_slice(&hw1.to_le_bytes());
6962        bytes.extend_from_slice(&hw2.to_le_bytes());
6963
6964        // VMOV Dd, R1, R2
6965        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_dreg(
6966            true,
6967            dd,
6968            &Reg::R1,
6969            &Reg::R2,
6970        )?));
6971
6972        Ok(bytes)
6973    }
6974
6975    /// Encode VCVT.S32/U32.F32 + VMOV as Thumb-2
6976    fn encode_thumb_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
6977        let mut bytes = Vec::new();
6978
6979        let sm_num = vfp_sreg_to_num(sm)?;
6980        let (vd, d) = encode_sreg(sm_num);
6981        let (vm, m) = encode_sreg(sm_num);
6982        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
6983        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
6984        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
6985
6986        // VMOV Rd, Sm
6987        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
6988        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6989
6990        Ok(bytes)
6991    }
6992
6993    // === Thumb-2 32-bit encoding helpers ===
6994
6995    /// Encode Thumb-2 32-bit ADD with immediate
6996    fn encode_thumb32_add(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
6997        let rd_bits = reg_to_bits(rd);
6998        let rn_bits = reg_to_bits(rn);
6999
7000        // The `i:imm3:imm8` field is split the same way for both forms.
7001        let i_bit = (imm >> 11) & 1;
7002        let imm3 = (imm >> 8) & 0x7;
7003        let imm8 = imm & 0xFF;
7004
7005        let hw1_base = if imm <= 0xFF {
7006            // ADD.W (T3): the field is a ThumbExpandImm modified immediate. For
7007            // imm <= 0xFF (i:imm3 = 0000) it is the zero-extended byte, which is
7008            // correct — keep this form so existing encodings stay bit-identical.
7009            0xF100
7010        } else if imm <= 0xFFF {
7011            // ADDW (T4): a PLAIN 12-bit immediate (0..4095) — no ThumbExpandImm.
7012            // This is what makes `add sp, sp, #frame` correct for frame sizes
7013            // >= 256, which ADD.W (T3) would silently mis-encode (e.g. #256 -> #0).
7014            0xF200
7015        } else {
7016            return Err(synth_core::Error::synthesis(
7017                "ADD immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7018            ));
7019        };
7020
7021        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7022        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7023
7024        let mut bytes = hw1.to_le_bytes().to_vec();
7025        bytes.extend_from_slice(&hw2.to_le_bytes());
7026        Ok(bytes)
7027    }
7028
7029    /// Encode Thumb-2 32-bit SUB with immediate
7030    fn encode_thumb32_sub(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7031        let rd_bits = reg_to_bits(rd);
7032        let rn_bits = reg_to_bits(rn);
7033
7034        let i_bit = (imm >> 11) & 1;
7035        let imm3 = (imm >> 8) & 0x7;
7036        let imm8 = imm & 0xFF;
7037
7038        let hw1_base = if imm <= 0xFF {
7039            // SUB.W (T3) modified immediate — correct for the zero-extended byte
7040            // (imm <= 0xFF). Kept bit-identical for existing encodings.
7041            0xF1A0
7042        } else if imm <= 0xFFF {
7043            // SUBW (T4): plain 12-bit immediate (0..4095). Makes
7044            // `sub sp, sp, #frame` correct for frame sizes >= 256.
7045            0xF2A0
7046        } else {
7047            return Err(synth_core::Error::synthesis(
7048                "SUB immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7049            ));
7050        };
7051
7052        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7053        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7054
7055        let mut bytes = hw1.to_le_bytes().to_vec();
7056        bytes.extend_from_slice(&hw2.to_le_bytes());
7057        Ok(bytes)
7058    }
7059
7060    /// Encode Thumb-2 32-bit ADDS with immediate (sets flags)
7061    fn encode_thumb32_adds(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7062        let rd_bits = reg_to_bits(rd);
7063        let rn_bits = reg_to_bits(rn);
7064
7065        // ADDS.W (flag-setting) has only the modified-immediate form — error on
7066        // an un-encodable value rather than silently add the wrong constant.
7067        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7068            synth_core::Error::synthesis(
7069                "ADDS immediate is not a valid ThumbExpandImm — materialize into a register",
7070            )
7071        })?;
7072        let i_bit = (field >> 11) & 1;
7073        let imm3 = (field >> 8) & 0x7;
7074        let imm8 = field & 0xFF;
7075
7076        // ADDS.W Rd, Rn, #imm (with S=1)
7077        // First halfword: 1111 0 i 0 1000 1 Rn = F110 | i<<10 | Rn
7078        let hw1: u16 = (0xF110 | (i_bit << 10) | rn_bits) as u16;
7079        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7080
7081        let mut bytes = hw1.to_le_bytes().to_vec();
7082        bytes.extend_from_slice(&hw2.to_le_bytes());
7083        Ok(bytes)
7084    }
7085
7086    /// Encode Thumb-2 32-bit SUBS with immediate (sets flags)
7087    fn encode_thumb32_subs(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7088        let rd_bits = reg_to_bits(rd);
7089        let rn_bits = reg_to_bits(rn);
7090
7091        // SUBS.W (flag-setting) has only the modified-immediate form — error on
7092        // an un-encodable value rather than silently subtract the wrong constant.
7093        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7094            synth_core::Error::synthesis(
7095                "SUBS immediate is not a valid ThumbExpandImm — materialize into a register",
7096            )
7097        })?;
7098        let i_bit = (field >> 11) & 1;
7099        let imm3 = (field >> 8) & 0x7;
7100        let imm8 = field & 0xFF;
7101
7102        // SUBS.W Rd, Rn, #imm (with S=1)
7103        // First halfword: 1111 0 i 0 1101 1 Rn = F1B0 | i<<10 | Rn
7104        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7105        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7106
7107        let mut bytes = hw1.to_le_bytes().to_vec();
7108        bytes.extend_from_slice(&hw2.to_le_bytes());
7109        Ok(bytes)
7110    }
7111
7112    /// Encode Thumb-2 32-bit MOVW (16-bit immediate)
7113    ///
7114    /// # Contract (Verus-style)
7115    /// ```text
7116    /// requires rd <= R14
7117    /// ensures result.len() == 4
7118    /// ensures (imm & 0xFFFF) can be reconstructed from the encoding
7119    /// ```
7120    fn encode_thumb32_movw(&self, rd: &Reg, imm: u32) -> Result<Vec<u8>> {
7121        let rd_bits = reg_to_bits(rd);
7122        reg_bits_checked(rd_bits)?;
7123        let imm16 = imm & 0xFFFF;
7124
7125        // MOVW Rd, #imm16
7126        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7127        let imm4 = (imm16 >> 12) & 0xF;
7128        let i_bit = (imm16 >> 11) & 1;
7129        let imm3 = (imm16 >> 8) & 0x7;
7130        let imm8 = imm16 & 0xFF;
7131
7132        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7133        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7134
7135        let mut bytes = hw1.to_le_bytes().to_vec();
7136        bytes.extend_from_slice(&hw2.to_le_bytes());
7137        encoding_contracts::verify_thumb32(&bytes);
7138        Ok(bytes)
7139    }
7140
7141    /// Encode Thumb-2 32-bit shift with immediate
7142    ///
7143    /// # Contract (Verus-style)
7144    /// ```text
7145    /// requires rd <= R14, rm <= R14
7146    /// ensures result.len() == 4
7147    /// ```
7148    fn encode_thumb32_shift(
7149        &self,
7150        rd: &Reg,
7151        rm: &Reg,
7152        shift: u32,
7153        shift_type: u8,
7154    ) -> Result<Vec<u8>> {
7155        let rd_bits = reg_to_bits(rd);
7156        let rm_bits = reg_to_bits(rm);
7157        reg_bits_checked(rd_bits)?;
7158        reg_bits_checked(rm_bits)?;
7159        let imm5 = shift & 0x1F;
7160        let imm2 = imm5 & 0x3;
7161        let imm3 = (imm5 >> 2) & 0x7;
7162
7163        // MOV.W Rd, Rm, <shift> #imm
7164        // EA4F 0 imm3 Rd imm2 type Rm
7165        let hw1: u16 = 0xEA4F;
7166        let hw2: u16 =
7167            ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | ((shift_type as u32) << 4) | rm_bits)
7168                as u16;
7169
7170        let mut bytes = hw1.to_le_bytes().to_vec();
7171        bytes.extend_from_slice(&hw2.to_le_bytes());
7172        Ok(bytes)
7173    }
7174
7175    /// Encode Thumb-2 32-bit shift by register
7176    /// Encoding: 11111010 0xx0 Rn | 1111 Rd 0000 Rm
7177    /// shift_type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
7178    fn encode_thumb32_shift_reg(
7179        &self,
7180        rd: &Reg,
7181        rn: &Reg,
7182        rm: &Reg,
7183        shift_type: u8,
7184    ) -> Result<Vec<u8>> {
7185        let rd_bits = reg_to_bits(rd);
7186        let rn_bits = reg_to_bits(rn);
7187        let rm_bits = reg_to_bits(rm);
7188
7189        // hw1: 1111 1010 0xx0 Rn
7190        let hw1: u16 = (0xFA00 | ((shift_type as u32) << 5) | rn_bits) as u16;
7191        // hw2: 1111 Rd 0000 Rm
7192        let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
7193
7194        let mut bytes = hw1.to_le_bytes().to_vec();
7195        bytes.extend_from_slice(&hw2.to_le_bytes());
7196        Ok(bytes)
7197    }
7198
7199    /// Encode Thumb-2 32-bit CMP with immediate
7200    fn encode_thumb32_cmp_imm(&self, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7201        let rn_bits = reg_to_bits(rn);
7202
7203        // CMP.W has only the modified-immediate form (no plain-imm12 like ADDW),
7204        // so an un-encodable immediate MUST be materialized into a register by
7205        // the selector. Error rather than silently compare the wrong constant.
7206        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7207            synth_core::Error::synthesis(
7208                "CMP immediate is not a valid ThumbExpandImm — materialize into a register",
7209            )
7210        })?;
7211        let i_bit = (field >> 11) & 1;
7212        let imm3 = (field >> 8) & 0x7;
7213        let imm8 = field & 0xFF;
7214
7215        // CMP.W Rn, #imm
7216        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7217        let hw2: u16 = ((imm3 << 12) | 0x0F00 | imm8) as u16;
7218
7219        let mut bytes = hw1.to_le_bytes().to_vec();
7220        bytes.extend_from_slice(&hw2.to_le_bytes());
7221        Ok(bytes)
7222    }
7223
7224    /// #372/#382: resolve the base register AND residual immediate offset for an
7225    /// `I64Ldr`/`I64Str` whose address may carry an index register. Returns
7226    /// `(base, low_offset)`; the caller accesses the halves at `[base,
7227    /// #low_offset]` and `[base, #low_offset + 4]`.
7228    ///
7229    /// - Frame access (no `offset_reg`, e.g. a spilled local at `[SP, #off]`):
7230    ///   returns `(addr.base, off)` and emits NOTHING — byte-identical.
7231    /// - Memory access (`reg_imm(R11, addr, offset)` = `R11 + addr + offset`)
7232    ///   with `offset + 4 <= 0xFFF`: emits `ADD.W ip, base, index` and returns
7233    ///   `(ip, offset)`, folding `offset`/`offset+4` into the halves' imm12.
7234    ///   Byte-identical to the pre-#382 (#372) behavior.
7235    /// - Memory access with `offset + 4 > 0xFFF`: the imm12 form cannot hold the
7236    ///   high half's offset, so `encode_thumb32_ldr`'s `check_ldst_imm12` (#259)
7237    ///   rightly refused it and the WHOLE function was skipped (#382). Instead
7238    ///   MATERIALIZE the offset into the base: `ADD ip, index, #offset` (against
7239    ///   the read-only INDEX register, so `encode_thumb32_add_imm` never trips its
7240    ///   `rd==rn==R12` alias trap), then `ADD.W ip, ip, base` (+ R11), and return
7241    ///   `(ip, 0)` so the halves use `[ip, #0]` / `[ip, #4]`.
7242    ///
7243    /// The effective address is fully materialized into `ip` BEFORE the halves
7244    /// are accessed, so an `rdlo` aliasing the index register is safe.
7245    fn i64_effective_base(&self, bytes: &mut Vec<u8>, addr: &MemAddr) -> Result<(Reg, u32)> {
7246        let offset = if addr.offset < 0 {
7247            0u32
7248        } else {
7249            addr.offset as u32
7250        };
7251        match addr.offset_reg {
7252            Some(idx) => {
7253                let ip = Reg::R12;
7254                if offset.wrapping_add(4) > 0xFFF {
7255                    // Large static offset (#382): fold it (and R11) into ip so the
7256                    // imm12 halves stay in range instead of skipping the function.
7257                    // ADD ip, index, #offset  (index != ip → no add_imm alias trap)
7258                    bytes.extend_from_slice(&self.encode_thumb32_add_imm(&ip, &idx, offset)?);
7259                    // ADD.W ip, ip, base  (+ R11)
7260                    bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
7261                        reg_to_bits(&ip),
7262                        reg_to_bits(&ip),
7263                        reg_to_bits(&addr.base),
7264                    )?);
7265                    Ok((ip, 0))
7266                } else {
7267                    // ADD.W ip, addr.base, idx  (Thumb-2, byte-verified vs as)
7268                    let hw1: u16 = 0xEB00 | reg_to_bits(&addr.base) as u16;
7269                    let hw2: u16 = 0x0C00 | reg_to_bits(&idx) as u16;
7270                    bytes.extend_from_slice(&hw1.to_le_bytes());
7271                    bytes.extend_from_slice(&hw2.to_le_bytes());
7272                    Ok((ip, offset))
7273                }
7274            }
7275            None => Ok((addr.base, offset)),
7276        }
7277    }
7278
7279    /// Encode Thumb-2 32-bit LDR
7280    fn encode_thumb32_ldr(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7281        let rd_bits = reg_to_bits(rd);
7282        let base_bits = reg_to_bits(base);
7283
7284        // LDR.W Rd, [Rn, #imm12]
7285        check_ldst_imm12(offset)?;
7286        let hw1: u16 = (0xF8D0 | base_bits) as u16;
7287        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7288
7289        let mut bytes = hw1.to_le_bytes().to_vec();
7290        bytes.extend_from_slice(&hw2.to_le_bytes());
7291        Ok(bytes)
7292    }
7293
7294    /// Encode Thumb-2 32-bit STR
7295    fn encode_thumb32_str(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7296        let rd_bits = reg_to_bits(rd);
7297        let base_bits = reg_to_bits(base);
7298
7299        // STR.W Rd, [Rn, #imm12]
7300        check_ldst_imm12(offset)?;
7301        let hw1: u16 = (0xF8C0 | base_bits) as u16;
7302        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7303
7304        let mut bytes = hw1.to_le_bytes().to_vec();
7305        bytes.extend_from_slice(&hw2.to_le_bytes());
7306        Ok(bytes)
7307    }
7308
7309    /// Encode Thumb-2 32-bit LDR with register offset: LDR.W Rd, [Rn, Rm]
7310    fn encode_thumb32_ldr_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7311        let rd_bits = reg_to_bits(rd);
7312        let base_bits = reg_to_bits(base);
7313        let rm_bits = reg_to_bits(offset_reg);
7314
7315        // LDR.W Rd, [Rn, Rm, LSL #0]
7316        // Encoding: 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
7317        // imm2 = 00 for no shift (LSL #0)
7318        let hw1: u16 = (0xF850 | base_bits) as u16;
7319        let hw2: u16 = ((rd_bits << 12) | rm_bits) 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 with register offset: STR.W Rd, [Rn, Rm]
7327    fn encode_thumb32_str_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7328        let rd_bits = reg_to_bits(rd);
7329        let base_bits = reg_to_bits(base);
7330        let rm_bits = reg_to_bits(offset_reg);
7331
7332        // STR.W Rd, [Rn, Rm, LSL #0]
7333        // Encoding: 1111 1000 0100 Rn | Rt 0000 00 imm2 Rm
7334        // imm2 = 00 for no shift (LSL #0)
7335        let hw1: u16 = (0xF840 | base_bits) as u16;
7336        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7337
7338        let mut bytes = hw1.to_le_bytes().to_vec();
7339        bytes.extend_from_slice(&hw2.to_le_bytes());
7340        Ok(bytes)
7341    }
7342
7343    // === Sub-word load/store Thumb-2 encoding helpers ===
7344
7345    /// Encode Thumb-2 32-bit LDRB with immediate: LDRB.W Rd, [Rn, #imm12]
7346    fn encode_thumb32_ldrb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7347        let rd_bits = reg_to_bits(rd);
7348        let base_bits = reg_to_bits(base);
7349        // LDRB.W Rd, [Rn, #imm12]: 1111 1000 1001 Rn | Rt imm12
7350        check_ldst_imm12(offset)?;
7351        let hw1: u16 = (0xF890 | base_bits) as u16;
7352        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
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 LDRB with register: LDRB.W Rd, [Rn, Rm]
7359    fn encode_thumb32_ldrb_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        // LDRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0001 Rn | Rt 0000 00 imm2 Rm
7364        let hw1: u16 = (0xF810 | base_bits) as u16;
7365        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7366        let mut bytes = hw1.to_le_bytes().to_vec();
7367        bytes.extend_from_slice(&hw2.to_le_bytes());
7368        Ok(bytes)
7369    }
7370
7371    /// Encode Thumb-2 32-bit LDRSB with immediate: LDRSB.W Rd, [Rn, #imm12]
7372    fn encode_thumb32_ldrsb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7373        let rd_bits = reg_to_bits(rd);
7374        let base_bits = reg_to_bits(base);
7375        // LDRSB.W Rd, [Rn, #imm12]: 1111 1001 1001 Rn | Rt imm12
7376        check_ldst_imm12(offset)?;
7377        let hw1: u16 = (0xF990 | base_bits) as u16;
7378        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7379        let mut bytes = hw1.to_le_bytes().to_vec();
7380        bytes.extend_from_slice(&hw2.to_le_bytes());
7381        Ok(bytes)
7382    }
7383
7384    /// Encode Thumb-2 32-bit LDRSB with register: LDRSB.W Rd, [Rn, Rm]
7385    fn encode_thumb32_ldrsb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7386        let rd_bits = reg_to_bits(rd);
7387        let base_bits = reg_to_bits(base);
7388        let rm_bits = reg_to_bits(offset_reg);
7389        // LDRSB.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0001 Rn | Rt 0000 00 imm2 Rm
7390        let hw1: u16 = (0xF910 | base_bits) as u16;
7391        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7392        let mut bytes = hw1.to_le_bytes().to_vec();
7393        bytes.extend_from_slice(&hw2.to_le_bytes());
7394        Ok(bytes)
7395    }
7396
7397    /// Encode Thumb-2 32-bit LDRH with immediate: LDRH.W Rd, [Rn, #imm12]
7398    fn encode_thumb32_ldrh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7399        let rd_bits = reg_to_bits(rd);
7400        let base_bits = reg_to_bits(base);
7401        // LDRH.W Rd, [Rn, #imm12]: 1111 1000 1011 Rn | Rt imm12
7402        check_ldst_imm12(offset)?;
7403        let hw1: u16 = (0xF8B0 | base_bits) as u16;
7404        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7405        let mut bytes = hw1.to_le_bytes().to_vec();
7406        bytes.extend_from_slice(&hw2.to_le_bytes());
7407        Ok(bytes)
7408    }
7409
7410    /// Encode Thumb-2 32-bit LDRH with register: LDRH.W Rd, [Rn, Rm]
7411    fn encode_thumb32_ldrh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7412        let rd_bits = reg_to_bits(rd);
7413        let base_bits = reg_to_bits(base);
7414        let rm_bits = reg_to_bits(offset_reg);
7415        // LDRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0011 Rn | Rt 0000 00 imm2 Rm
7416        let hw1: u16 = (0xF830 | base_bits) as u16;
7417        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7418        let mut bytes = hw1.to_le_bytes().to_vec();
7419        bytes.extend_from_slice(&hw2.to_le_bytes());
7420        Ok(bytes)
7421    }
7422
7423    /// Encode Thumb-2 32-bit LDRSH with immediate: LDRSH.W Rd, [Rn, #imm12]
7424    fn encode_thumb32_ldrsh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7425        let rd_bits = reg_to_bits(rd);
7426        let base_bits = reg_to_bits(base);
7427        // LDRSH.W Rd, [Rn, #imm12]: 1111 1001 1011 Rn | Rt imm12
7428        check_ldst_imm12(offset)?;
7429        let hw1: u16 = (0xF9B0 | base_bits) as u16;
7430        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7431        let mut bytes = hw1.to_le_bytes().to_vec();
7432        bytes.extend_from_slice(&hw2.to_le_bytes());
7433        Ok(bytes)
7434    }
7435
7436    /// Encode Thumb-2 32-bit LDRSH with register: LDRSH.W Rd, [Rn, Rm]
7437    fn encode_thumb32_ldrsh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7438        let rd_bits = reg_to_bits(rd);
7439        let base_bits = reg_to_bits(base);
7440        let rm_bits = reg_to_bits(offset_reg);
7441        // LDRSH.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0011 Rn | Rt 0000 00 imm2 Rm
7442        let hw1: u16 = (0xF930 | base_bits) as u16;
7443        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7444        let mut bytes = hw1.to_le_bytes().to_vec();
7445        bytes.extend_from_slice(&hw2.to_le_bytes());
7446        Ok(bytes)
7447    }
7448
7449    /// Encode Thumb-2 32-bit STRB with immediate: STRB.W Rd, [Rn, #imm12]
7450    fn encode_thumb32_strb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7451        let rd_bits = reg_to_bits(rd);
7452        let base_bits = reg_to_bits(base);
7453        // STRB.W Rd, [Rn, #imm12]: 1111 1000 1000 Rn | Rt imm12
7454        check_ldst_imm12(offset)?;
7455        let hw1: u16 = (0xF880 | base_bits) as u16;
7456        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7457        let mut bytes = hw1.to_le_bytes().to_vec();
7458        bytes.extend_from_slice(&hw2.to_le_bytes());
7459        Ok(bytes)
7460    }
7461
7462    /// Encode Thumb-2 32-bit STRB with register: STRB.W Rd, [Rn, Rm]
7463    fn encode_thumb32_strb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7464        let rd_bits = reg_to_bits(rd);
7465        let base_bits = reg_to_bits(base);
7466        let rm_bits = reg_to_bits(offset_reg);
7467        // STRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0000 Rn | Rt 0000 00 imm2 Rm
7468        let hw1: u16 = (0xF800 | base_bits) as u16;
7469        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7470        let mut bytes = hw1.to_le_bytes().to_vec();
7471        bytes.extend_from_slice(&hw2.to_le_bytes());
7472        Ok(bytes)
7473    }
7474
7475    /// Encode Thumb-2 32-bit STRH with immediate: STRH.W Rd, [Rn, #imm12]
7476    fn encode_thumb32_strh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7477        let rd_bits = reg_to_bits(rd);
7478        let base_bits = reg_to_bits(base);
7479        // STRH.W Rd, [Rn, #imm12]: 1111 1000 1010 Rn | Rt imm12
7480        check_ldst_imm12(offset)?;
7481        let hw1: u16 = (0xF8A0 | base_bits) as u16;
7482        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7483        let mut bytes = hw1.to_le_bytes().to_vec();
7484        bytes.extend_from_slice(&hw2.to_le_bytes());
7485        Ok(bytes)
7486    }
7487
7488    /// Encode Thumb-2 32-bit STRH with register: STRH.W Rd, [Rn, Rm]
7489    fn encode_thumb32_strh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7490        let rd_bits = reg_to_bits(rd);
7491        let base_bits = reg_to_bits(base);
7492        let rm_bits = reg_to_bits(offset_reg);
7493        // STRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0010 Rn | Rt 0000 00 imm2 Rm
7494        let hw1: u16 = (0xF820 | base_bits) as u16;
7495        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7496        let mut bytes = hw1.to_le_bytes().to_vec();
7497        bytes.extend_from_slice(&hw2.to_le_bytes());
7498        Ok(bytes)
7499    }
7500
7501    /// Encode Thumb-2 32-bit ADD with immediate: ADD.W Rd, Rn, #imm
7502    fn encode_thumb32_add_imm(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7503        let rd_bits = reg_to_bits(rd);
7504        let rn_bits = reg_to_bits(rn);
7505
7506        // For small immediates, use ADD.W Rd, Rn, #imm12
7507        // Encoding: 1111 0 i 0 1 0 0 0 S Rn | 0 imm3 Rd imm8
7508        // S = 0 (don't update flags)
7509        // The 12-bit immediate is encoded as: i:imm3:imm8
7510        // For simplicity, we only support imm <= 0xFFF (direct encoding)
7511        if imm <= 0xFFF {
7512            let i_bit = (imm >> 11) & 1;
7513            let imm3 = (imm >> 8) & 0x7;
7514            let imm8 = imm & 0xFF;
7515
7516            let hw1: u16 = (0xF100 | (i_bit << 10) | rn_bits) as u16;
7517            let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7518
7519            let mut bytes = hw1.to_le_bytes().to_vec();
7520            bytes.extend_from_slice(&hw2.to_le_bytes());
7521            Ok(bytes)
7522        } else {
7523            // Out-of-range immediate (> 0xFFF): materialize it into a scratch
7524            // register, then ADD.W Rd, Rn, scratch. This is the #180/#185
7525            // "encoder must produce a legal sequence, not assert" class — see #350.
7526            //
7527            // Scratch choice (must NEVER equal Rn, or Rn would be clobbered before
7528            // the ADD reads it):
7529            //   - rd != rn  => use rd itself (rn is untouched, since rd != rn).
7530            //   - rd == rn  => use R12/IP (the reserved encoder scratch). rd/rn are
7531            //                  never R12 (R12 is non-allocatable), so it can't alias.
7532            //
7533            // The materialized value is the same whether or not MOVT is emitted, so
7534            // the byte length depends only on `imm` (and rd==rn) — the size probe and
7535            // the final emit therefore agree (mandatory: the function is encoded twice).
7536            let scratch: u32 = if rd_bits == rn_bits {
7537                12 // R12/IP — in-place add, can't use rd because rd == rn
7538            } else {
7539                rd_bits // rn is preserved because rd != rn
7540            };
7541            // Invariant: the scratch must never alias Rn (would clobber it before
7542            // the ADD reads it). Unreachable in real codegen (rd/rn are never R12,
7543            // which is reserved encoder scratch), but the encoder is also driven by
7544            // the `encoder_no_panic` fuzz harness with ARBITRARY registers — incl.
7545            // rd==rn==R12, which makes scratch (R12) alias Rn. The encoder contract
7546            // (#180/#185) is Ok-or-Err, never a panic, so return a typed error
7547            // instead of asserting. #350 follow-up.
7548            if scratch == rn_bits {
7549                return Err(synth_core::Error::synthesis(format!(
7550                    "ADD #imm: cannot lower #{imm:#x} for Rd==Rn==R12 — no free scratch \
7551                     register (R12 is the reserved encoder scratch and aliases Rn here)"
7552                )));
7553            }
7554
7555            let lo16 = imm & 0xFFFF;
7556            let hi16 = (imm >> 16) & 0xFFFF;
7557
7558            let mut bytes = self.encode_thumb32_movw_raw(scratch, lo16)?;
7559            if hi16 != 0 {
7560                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(scratch, hi16)?);
7561            }
7562            bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rn_bits, scratch)?);
7563            Ok(bytes)
7564        }
7565    }
7566
7567    // === Raw encoding helpers for POPCNT (take register numbers directly) ===
7568
7569    /// Encode Thumb-2 32-bit MOVW (16-bit immediate) - raw version
7570    ///
7571    /// # Contract (Verus-style)
7572    /// ```text
7573    /// requires rd <= 14, imm16 <= 0xFFFF
7574    /// ensures result.len() == 4
7575    /// ```
7576    fn encode_thumb32_movw_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
7577        reg_bits_checked(rd)?;
7578        encoding_contracts::verify_imm16(imm16);
7579        // MOVW Rd, #imm16
7580        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7581        let imm16 = imm16 & 0xFFFF;
7582        let imm4 = (imm16 >> 12) & 0xF;
7583        let i_bit = (imm16 >> 11) & 1;
7584        let imm3 = (imm16 >> 8) & 0x7;
7585        let imm8 = imm16 & 0xFF;
7586
7587        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7588        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
7589
7590        let mut bytes = hw1.to_le_bytes().to_vec();
7591        bytes.extend_from_slice(&hw2.to_le_bytes());
7592        encoding_contracts::verify_thumb32(&bytes);
7593        Ok(bytes)
7594    }
7595
7596    /// Encode Thumb-2 32-bit MOVT (move top 16 bits) - raw version
7597    ///
7598    /// # Contract (Verus-style)
7599    /// ```text
7600    /// requires rd <= 14, imm16 <= 0xFFFF
7601    /// ensures result.len() == 4
7602    /// ```
7603    fn encode_thumb32_movt_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
7604        reg_bits_checked(rd)?;
7605        encoding_contracts::verify_imm16(imm16);
7606        // MOVT Rd, #imm16
7607        // 1111 0 i 10 1 1 0 0 imm4 | 0 imm3 Rd imm8
7608        let imm16 = imm16 & 0xFFFF;
7609        let imm4 = (imm16 >> 12) & 0xF;
7610        let i_bit = (imm16 >> 11) & 1;
7611        let imm3 = (imm16 >> 8) & 0x7;
7612        let imm8 = imm16 & 0xFF;
7613
7614        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
7615        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
7616
7617        let mut bytes = hw1.to_le_bytes().to_vec();
7618        bytes.extend_from_slice(&hw2.to_le_bytes());
7619        encoding_contracts::verify_thumb32(&bytes);
7620        Ok(bytes)
7621    }
7622
7623    /// Encode Thumb-2 32-bit LSR (logical shift right) with immediate - raw version
7624    fn encode_thumb32_lsr_raw(&self, rd: u32, rm: u32, shift: u32) -> Result<Vec<u8>> {
7625        // MOV.W Rd, Rm, LSR #imm
7626        // EA4F 0 imm3 Rd imm2 01 Rm
7627        let imm5 = shift & 0x1F;
7628        let imm2 = imm5 & 0x3;
7629        let imm3 = (imm5 >> 2) & 0x7;
7630
7631        let hw1: u16 = 0xEA4F;
7632        let hw2: u16 = ((imm3 << 12) | (rd << 8) | (imm2 << 6) | (0b01 << 4) | rm) as u16;
7633
7634        let mut bytes = hw1.to_le_bytes().to_vec();
7635        bytes.extend_from_slice(&hw2.to_le_bytes());
7636        Ok(bytes)
7637    }
7638
7639    /// Encode Thumb-2 32-bit AND (register) - raw version
7640    fn encode_thumb32_and_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7641        // AND.W Rd, Rn, Rm
7642        // EA00 Rn | 0 Rd 00 00 Rm
7643        let hw1: u16 = (0xEA00 | rn) as u16;
7644        let hw2: u16 = ((rd << 8) | rm) as u16;
7645
7646        let mut bytes = hw1.to_le_bytes().to_vec();
7647        bytes.extend_from_slice(&hw2.to_le_bytes());
7648        Ok(bytes)
7649    }
7650
7651    /// Encode Thumb-2 32-bit AND with immediate - raw version
7652    fn encode_thumb32_and_imm_raw(&self, rd: u32, rn: u32, imm: u32) -> Result<Vec<u8>> {
7653        // AND.W Rd, Rn, #<modified_immediate>
7654        // For small immediates (0-255), the encoding is simpler
7655        // F0 00 Rn | 0 imm3 Rd imm8
7656        let i_bit = (imm >> 11) & 1;
7657        let imm3 = (imm >> 8) & 0x7;
7658        let imm8 = imm & 0xFF;
7659
7660        let hw1: u16 = (0xF000 | (i_bit << 10) | rn) as u16;
7661        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
7662
7663        let mut bytes = hw1.to_le_bytes().to_vec();
7664        bytes.extend_from_slice(&hw2.to_le_bytes());
7665        Ok(bytes)
7666    }
7667
7668    /// Encode Thumb-2 32-bit SUB (register) - raw version
7669    fn encode_thumb32_sub_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7670        // SUB.W Rd, Rn, Rm
7671        // EBA0 Rn | 0 Rd 00 00 Rm
7672        let hw1: u16 = (0xEBA0 | rn) as u16;
7673        let hw2: u16 = ((rd << 8) | rm) as u16;
7674
7675        let mut bytes = hw1.to_le_bytes().to_vec();
7676        bytes.extend_from_slice(&hw2.to_le_bytes());
7677        Ok(bytes)
7678    }
7679
7680    /// Encode Thumb-2 32-bit ADD (register) - raw version
7681    fn encode_thumb32_add_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7682        // ADD.W Rd, Rn, Rm
7683        // EB00 Rn | 0 Rd 00 00 Rm
7684        let hw1: u16 = (0xEB00 | rn) as u16;
7685        let hw2: u16 = ((rd << 8) | rm) as u16;
7686
7687        let mut bytes = hw1.to_le_bytes().to_vec();
7688        bytes.extend_from_slice(&hw2.to_le_bytes());
7689        Ok(bytes)
7690    }
7691
7692    /// Encode Thumb-2 32-bit ADDS (register, flag-setting) - raw version.
7693    /// Used as the high-register fallback for `ArmOp::Adds` (i64 low-word add)
7694    /// so R8-R11 pair operands don't overflow the 16-bit field — #178/#180.
7695    fn encode_thumb32_adds_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7696        // ADDS.W Rd, Rn, Rm (T3, S=1): EB10 Rn | 0 Rd 00 00 Rm
7697        let hw1: u16 = (0xEB10 | rn) as u16;
7698        let hw2: u16 = ((rd << 8) | rm) as u16;
7699        let mut bytes = hw1.to_le_bytes().to_vec();
7700        bytes.extend_from_slice(&hw2.to_le_bytes());
7701        Ok(bytes)
7702    }
7703
7704    /// Encode Thumb-2 32-bit SUBS (register, flag-setting) - raw version.
7705    /// High-register fallback for `ArmOp::Subs` (i64 low-word subtract) — #178/#180.
7706    fn encode_thumb32_subs_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7707        // SUBS.W Rd, Rn, Rm (T3, S=1): EBB0 Rn | 0 Rd 00 00 Rm
7708        let hw1: u16 = (0xEBB0 | rn) as u16;
7709        let hw2: u16 = ((rd << 8) | rm) as u16;
7710        let mut bytes = hw1.to_le_bytes().to_vec();
7711        bytes.extend_from_slice(&hw2.to_le_bytes());
7712        Ok(bytes)
7713    }
7714
7715    /// Encode a sequence of ARM instructions
7716    pub fn encode_sequence(&self, ops: &[ArmOp]) -> Result<Vec<u8>> {
7717        let mut code = Vec::new();
7718
7719        for op in ops {
7720            let encoded = self.encode(op)?;
7721            code.extend_from_slice(&encoded);
7722        }
7723
7724        Ok(code)
7725    }
7726}
7727
7728/// Convert register to bit encoding (0-15)
7729/// Reverse of the ARMv7-M `ThumbExpandImm`: given a 32-bit immediate, return the
7730/// 12-bit `i:imm3:imm8` field if it is a representable modified immediate, else
7731/// `None` (the caller must materialize the value into a register). This is the
7732/// shared correct path for the data-processing immediate encoders — without it
7733/// they pack raw bits and silently mis-encode any value `> 0xFF` that isn't a
7734/// modified immediate (the silent-miscompile class behind #251/#253/#255).
7735fn try_thumb_expand_imm(value: u32) -> Option<u32> {
7736    // i:imm3 = 0000 → 8-bit value, zero-extended (00000000 00000000 00000000 XY).
7737    if value <= 0xFF {
7738        return Some(value);
7739    }
7740    let b0 = value & 0xFF; // byte 0
7741    let b1 = (value >> 8) & 0xFF; // byte 1
7742    // 0x00XY00XY (i:imm3 = 0001) — XY in bytes 0 and 2
7743    if value == (b0 << 16) | b0 {
7744        return Some(0x100 | b0);
7745    }
7746    // 0xXY00XY00 (i:imm3 = 0010) — XY in bytes 1 and 3
7747    if value == (b1 << 24) | (b1 << 8) {
7748        return Some(0x200 | b1);
7749    }
7750    // 0xXYXYXYXY (i:imm3 = 0011) — XY in all four bytes
7751    if value == (b0 << 24) | (b0 << 16) | (b0 << 8) | b0 {
7752        return Some(0x300 | b0);
7753    }
7754    // An 8-bit value with bit 7 set, rotated right by 8..=31. `rotate_left(rot)`
7755    // undoes the encoded right rotation; if the result is `1bbbbbbb` (0x80..=0xFF)
7756    // the value is representable. imm12[11:7] = rot, imm12[6:0] = low 7 bits.
7757    for rot in 8..=31u32 {
7758        let unrot = value.rotate_left(rot);
7759        if (0x80..=0xFF).contains(&unrot) {
7760            return Some((rot << 7) | (unrot & 0x7F));
7761        }
7762    }
7763    None
7764}
7765
7766/// Guard a Thumb-2 `LDR/STR Rd, [Rn, #imm12]` offset. The imm12 form supports
7767/// `0..=4095`; a larger offset must be materialized into a register by the
7768/// selector (register-offset addressing). Returning `Err` rather than silently
7769/// masking `offset & 0xFFF` closes the wrong-address miscompile class (#259,
7770/// the load/store sibling of #253/#255).
7771fn check_ldst_imm12(offset: u32) -> Result<()> {
7772    if offset > 0xFFF {
7773        Err(synth_core::Error::synthesis(
7774            "load/store immediate offset > 0xFFF (4095) — materialize the offset into a register",
7775        ))
7776    } else {
7777        Ok(())
7778    }
7779}
7780
7781fn reg_to_bits(reg: &Reg) -> u32 {
7782    match reg {
7783        Reg::R0 => 0,
7784        Reg::R1 => 1,
7785        Reg::R2 => 2,
7786        Reg::R3 => 3,
7787        Reg::R4 => 4,
7788        Reg::R5 => 5,
7789        Reg::R6 => 6,
7790        Reg::R7 => 7,
7791        Reg::R8 => 8,
7792        Reg::R9 => 9,
7793        Reg::R10 => 10,
7794        Reg::R11 => 11,
7795        Reg::R12 => 12,
7796        Reg::SP => 13,
7797        Reg::LR => 14,
7798        Reg::PC => 15,
7799    }
7800}
7801
7802// ======================================================================
7803// #610 — i64 fixed-ABI expansion wrappers.
7804//
7805// The hand-written multi-instruction i64 cores (rotl/rotr and the div/rem
7806// shift-subtract loops) compute in FIXED low registers. Before #610 the
7807// div/rem arms ignored their operand fields outright (hardcoded R0:R1 /
7808// R2:R3 in, result to R0:R1) and the rot arms used R3/R4 scratch that
7809// collided with selector-assigned registers — then restored the saved
7810// scratch OVER the result (`POP {R4}` with rd_lo == R4), so the op
7811// returned the caller's stale register: 0 for every input under qemu.
7812//
7813// These wrappers make each core honor its register parameters:
7814//   1. save R0-R3,
7815//   2. marshal the operand registers into the core's fixed input regs via
7816//      the stack (permutation-safe: every source is read before any fixed
7817//      register is written),
7818//   3. run the fixed-reg core (self-preserving for R4+; R12 is encoder
7819//      scratch and never allocatable, #212),
7820//   4. MOV the result pair from R0:R1 into the selector's rd pair,
7821//   5. restore R0-R3, skipping any register the result now occupies.
7822//
7823// All emitted lengths are register-independent so the optimized path's
7824// byte-size estimator (`estimate_arm_byte_size`, pinned by the
7825// estimator↔encoder agreement oracle #498/#511) stays a constant per op.
7826// ======================================================================
7827
7828/// Steps 1+2: `PUSH {R0-R3}`, then marshal `srcs` (operand registers, any of
7829/// R0-R12) into `R0..R<n>` via individual stack pushes. Sources are all read
7830/// before any destination register is written, so arbitrary source/target
7831/// permutations (including operands living in R0-R3) are safe.
7832fn emit_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
7833    debug_assert!(srcs.len() <= 4);
7834    // PUSH {R0-R3} — save the caller-visible low registers.
7835    bytes.extend_from_slice(&0xB40Fu16.to_le_bytes());
7836    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
7837    for src in srcs.iter().rev() {
7838        let rt = reg_to_bits(src) as u16;
7839        bytes.extend_from_slice(&0xF84Du16.to_le_bytes());
7840        bytes.extend_from_slice(&((rt << 12) | 0x0D04).to_le_bytes());
7841    }
7842    // POP {Ri} — Ri := srcs[i].
7843    for i in 0..srcs.len() as u16 {
7844        bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes());
7845    }
7846}
7847
7848/// Steps 4+5: move the core's R0:R1 result into the selector's rd pair, then
7849/// restore the R0-R3 saved by [`emit_i64_fixed_abi_entry`], skipping any
7850/// register the result now lives in (its saved caller word is discarded).
7851fn emit_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
7852    let lo = reg_to_bits(rdlo);
7853    let hi = reg_to_bits(rdhi);
7854    if lo == 1 && hi == 0 {
7855        // A fully swapped pair would clobber one half in either MOV order.
7856        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
7857        return Err(synth_core::Error::synthesis(
7858            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
7859        ));
7860    }
7861    let mov16 = |bytes: &mut Vec<u8>, rd: u32, rm: u32| {
7862        let d = ((rd >> 3) & 1) as u16;
7863        bytes.extend_from_slice(
7864            &(0x4600u16 | (d << 7) | ((rm as u16) << 3) | ((rd & 7) as u16)).to_le_bytes(),
7865        );
7866    };
7867    if hi == 0 {
7868        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
7869        mov16(bytes, lo, 0);
7870        mov16(bytes, hi, 1);
7871    } else {
7872        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
7873        mov16(bytes, hi, 1);
7874        mov16(bytes, lo, 0);
7875    }
7876    for i in 0..4u32 {
7877        if i == lo || i == hi {
7878            // The result lives here — drop the saved caller word.
7879            bytes.extend_from_slice(&0xB001u16.to_le_bytes()); // ADD SP, #4
7880        } else {
7881            bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes()); // POP {Ri}
7882        }
7883    }
7884    Ok(())
7885}
7886
7887/// WASM `i64.div_*` / `i64.rem_*` by zero must trap, matching the i32 path's
7888/// cmp/bne/udf guard. Emitted after marshaling, when the divisor pair is in
7889/// R2:R3: `ORRS R12, R2, R3` — `BNE` over a `UDF #0` when nonzero.
7890fn emit_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
7891    bytes.extend_from_slice(&0xEA52u16.to_le_bytes()); // ORRS.W R12, R2, R3
7892    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
7893    bytes.extend_from_slice(&0xD100u16.to_le_bytes()); // BNE.N +0 (skip the UDF)
7894    bytes.extend_from_slice(&0xDE00u16.to_le_bytes()); // UDF #0 — divide by zero
7895}
7896
7897// ======================================================================
7898// #615 — A32 (ARM-mode) twins of the #610 i64 fixed-ABI wrappers above.
7899// Identical register contract, A32 encodings: the multi-instruction i64
7900// cores (rotl/rotr, div/rem) compute in fixed low registers (value/dividend
7901// R0:R1, amount R2 / divisor R2:R3, result to R0:R1); the wrappers marshal
7902// the selector-assigned operand registers in and the result out, saving and
7903// restoring the caller-visible R0-R3 around the core.
7904// ======================================================================
7905
7906/// A32 steps 1+2: `STMDB SP!, {R0-R3}`, then marshal `srcs` into `R0..R<n>`
7907/// via individual stack pushes (`STR src, [SP, #-4]!` in reverse order, then
7908/// `LDR Ri, [SP], #4`). Every source is read before any fixed register is
7909/// written, so arbitrary source/target permutations are safe.
7910fn emit_a32_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
7911    debug_assert!(srcs.len() <= 4);
7912    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
7913    // PUSH {R0-R3} — save the caller-visible low registers.
7914    w(bytes, 0xE92D_000F);
7915    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
7916    for src in srcs.iter().rev() {
7917        w(bytes, 0xE52D_0004 | (reg_to_bits(src) << 12));
7918    }
7919    // LDR Ri, [SP], #4 — Ri := srcs[i].
7920    for i in 0..srcs.len() as u32 {
7921        w(bytes, 0xE49D_0004 | (i << 12));
7922    }
7923}
7924
7925/// A32 steps 4+5: move the core's R0:R1 result into the selector's rd pair,
7926/// then restore the R0-R3 saved by [`emit_a32_i64_fixed_abi_entry`], skipping
7927/// any register the result now lives in (its saved caller word is discarded).
7928fn emit_a32_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
7929    let lo = reg_to_bits(rdlo);
7930    let hi = reg_to_bits(rdhi);
7931    if lo == 1 && hi == 0 {
7932        // A fully swapped pair would clobber one half in either MOV order.
7933        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
7934        return Err(synth_core::Error::synthesis(
7935            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
7936        ));
7937    }
7938    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
7939    let mov = |bytes: &mut Vec<u8>, rd: u32, rm: u32| w(bytes, 0xE1A0_0000 | (rd << 12) | rm);
7940    if hi == 0 {
7941        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
7942        mov(bytes, lo, 0);
7943        mov(bytes, hi, 1);
7944    } else {
7945        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
7946        mov(bytes, hi, 1);
7947        mov(bytes, lo, 0);
7948    }
7949    for i in 0..4u32 {
7950        if i == lo || i == hi {
7951            // The result lives here — drop the saved caller word.
7952            w(bytes, 0xE28D_D004); // ADD SP, SP, #4
7953        } else {
7954            w(bytes, 0xE49D_0004 | (i << 12)); // LDR Ri, [SP], #4
7955        }
7956    }
7957    Ok(())
7958}
7959
7960/// A32 zero-divisor trap, emitted after marshaling when the divisor pair is
7961/// in R2:R3: `ORRS R12, R2, R3` sets Z iff the divisor is zero; `BNE` skips a
7962/// `UDF #0` (WASM div/rem-by-zero must trap, matching the Thumb-2 twin).
7963fn emit_a32_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
7964    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
7965    w(bytes, 0xE192_C003); // ORRS R12, R2, R3
7966    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
7967    w(bytes, 0xE7F0_00F0); // UDF #0 — divide by zero
7968}
7969
7970/// Fallible form of the `verify_reg_bits` contract. PC (R15) is not a valid
7971/// data operand for the Thumb-2 encodings that use this guard (SDIV/UDIV/MLS/…
7972/// are UNPREDICTABLE with PC). Synth's own codegen never emits PC there, but
7973/// the encoder must stay *total* over arbitrary `ArmOp` inputs — the fuzz
7974/// harness (`encoder_no_panic`) requires Ok-or-Err, never a panic. Pre-fix, the
7975/// `debug_assert` in `verify_reg_bits` aborted under `-Cdebug-assertions`.
7976/// Returns a typed Err instead. See #185.
7977fn reg_bits_checked(bits: u32) -> Result<()> {
7978    if bits > 14 {
7979        return Err(synth_core::Error::synthesis(format!(
7980            "register bits {bits} (PC/R15) is not a valid operand for this Thumb-2 encoding"
7981        )));
7982    }
7983    Ok(())
7984}
7985
7986/// Try to encode a 32-bit value as an ARM rotated immediate (imm8 ROR 2*rot4).
7987/// Returns Some((encoded_bits, 1)) if representable, None otherwise.
7988fn try_encode_rotated_imm(val: u32) -> Option<(u32, u32)> {
7989    if val == 0 {
7990        return Some((0, 1));
7991    }
7992    for rot in 0..16u32 {
7993        let shift = rot * 2;
7994        // Rotate left by shift (undo the ROR) to see if result fits in 8 bits
7995        let unrotated = val.rotate_left(shift);
7996        if unrotated <= 0xFF {
7997            // Encoded as: rot4(4 bits) | imm8(8 bits) = rotate_imm << 8 | imm8
7998            return Some(((rot << 8) | unrotated, 1));
7999        }
8000    }
8001    None
8002}
8003
8004/// Encode operand2 field and return (bits, immediate_flag).
8005/// For ARM32 mode, immediates use the rotated-immediate encoding (imm8 ROR 2*rot4).
8006/// Panics if an immediate value cannot be represented. Callers that need large
8007/// immediates should use MOVW/MOVT instead of Operand2::Imm.
8008fn encode_operand2(op2: &Operand2) -> Result<(u32, u32)> {
8009    match op2 {
8010        Operand2::Imm(val) => {
8011            let uval = *val as u32;
8012            // Attempt rotated-immediate encoding (ARM32 Operand2)
8013            if let Some(encoded) = try_encode_rotated_imm(uval) {
8014                Ok(encoded)
8015            } else {
8016                // #378-class honesty: an immediate that can't be expressed as an
8017                // ARM32 rotated immediate is an INTERNAL selector bug — large
8018                // constants must be materialized via MOVW/MOVT, not passed here.
8019                // FAIL HONESTLY with an Err rather than silently masking to
8020                // `uval & 0xFF` and emitting a WRONG immediate. The encoder is
8021                // Ok-or-Err, never corrupt (#180/#185); a loud Err is also why
8022                // this is an Err and not a panic (the `encoder_no_panic` fuzz
8023                // contract — malformed/oversized input must degrade, not crash).
8024                Err(synth_core::Error::synthesis(format!(
8025                    "encode_operand2: immediate {uval:#x} ({val}) is not an ARM32 \
8026                     rotated immediate — the selector must materialize large \
8027                     constants via MOVW/MOVT"
8028                )))
8029            }
8030        }
8031
8032        Operand2::Reg(reg) => {
8033            let reg_bits = reg_to_bits(reg);
8034            Ok((reg_bits, 0)) // I=0 for register
8035        }
8036
8037        Operand2::RegShift {
8038            rm,
8039            shift: _,
8040            amount,
8041        } => {
8042            // Simplified encoding with shift
8043            let rm_bits = reg_to_bits(rm);
8044            let shift_bits = (*amount & 0x1F) << 7;
8045            Ok((shift_bits | rm_bits, 0))
8046        }
8047    }
8048}
8049
8050/// Encode memory address to (base_reg, offset)
8051fn encode_mem_addr(addr: &MemAddr) -> (u32, u32) {
8052    let base_bits = reg_to_bits(&addr.base);
8053    let offset_bits = (addr.offset as u32) & 0xFFF; // 12-bit offset
8054    (base_bits, offset_bits)
8055}
8056
8057/// S-register number: S0=0, S1=1, ..., S31=31
8058fn vfp_sreg_to_num(reg: &VfpReg) -> Result<u32> {
8059    match reg {
8060        VfpReg::S0 => Ok(0),
8061        VfpReg::S1 => Ok(1),
8062        VfpReg::S2 => Ok(2),
8063        VfpReg::S3 => Ok(3),
8064        VfpReg::S4 => Ok(4),
8065        VfpReg::S5 => Ok(5),
8066        VfpReg::S6 => Ok(6),
8067        VfpReg::S7 => Ok(7),
8068        VfpReg::S8 => Ok(8),
8069        VfpReg::S9 => Ok(9),
8070        VfpReg::S10 => Ok(10),
8071        VfpReg::S11 => Ok(11),
8072        VfpReg::S12 => Ok(12),
8073        VfpReg::S13 => Ok(13),
8074        VfpReg::S14 => Ok(14),
8075        VfpReg::S15 => Ok(15),
8076        VfpReg::S16 => Ok(16),
8077        VfpReg::S17 => Ok(17),
8078        VfpReg::S18 => Ok(18),
8079        VfpReg::S19 => Ok(19),
8080        VfpReg::S20 => Ok(20),
8081        VfpReg::S21 => Ok(21),
8082        VfpReg::S22 => Ok(22),
8083        VfpReg::S23 => Ok(23),
8084        VfpReg::S24 => Ok(24),
8085        VfpReg::S25 => Ok(25),
8086        VfpReg::S26 => Ok(26),
8087        VfpReg::S27 => Ok(27),
8088        VfpReg::S28 => Ok(28),
8089        VfpReg::S29 => Ok(29),
8090        VfpReg::S30 => Ok(30),
8091        VfpReg::S31 => Ok(31),
8092        // D-registers are not used in F32 single-precision encodings
8093        _ => Err(synth_core::Error::SynthesisError(
8094            "D-register not supported in single-precision VFP encoding".to_string(),
8095        )),
8096    }
8097}
8098
8099/// D-register number: D0=0, D1=1, ..., D15=15
8100fn vfp_dreg_to_num(reg: &VfpReg) -> Result<u32> {
8101    match reg {
8102        VfpReg::D0 => Ok(0),
8103        VfpReg::D1 => Ok(1),
8104        VfpReg::D2 => Ok(2),
8105        VfpReg::D3 => Ok(3),
8106        VfpReg::D4 => Ok(4),
8107        VfpReg::D5 => Ok(5),
8108        VfpReg::D6 => Ok(6),
8109        VfpReg::D7 => Ok(7),
8110        VfpReg::D8 => Ok(8),
8111        VfpReg::D9 => Ok(9),
8112        VfpReg::D10 => Ok(10),
8113        VfpReg::D11 => Ok(11),
8114        VfpReg::D12 => Ok(12),
8115        VfpReg::D13 => Ok(13),
8116        VfpReg::D14 => Ok(14),
8117        VfpReg::D15 => Ok(15),
8118        // S-registers are not used in F64 double-precision encodings
8119        _ => Err(synth_core::Error::SynthesisError(
8120            "S-register not supported in double-precision VFP encoding".to_string(),
8121        )),
8122    }
8123}
8124
8125/// Split S-register into (Vx[3:0], qualifier_bit) for VFP encoding.
8126/// For an S-register number s: Vx = s >> 1, qualifier = s & 1.
8127/// The qualifier bit goes to D (bit 22), N (bit 7), or M (bit 5) depending on role.
8128fn encode_sreg(s: u32) -> (u32, u32) {
8129    (s >> 1, s & 1)
8130}
8131
8132/// Split D-register into (Vx[3:0], qualifier_bit) for VFP double-precision encoding.
8133/// For a D-register number d: Vx = d & 0xF, qualifier = (d >> 4) & 1.
8134/// For D0-D15, qualifier is always 0.
8135fn encode_dreg(d: u32) -> (u32, u32) {
8136    (d & 0xF, (d >> 4) & 1)
8137}
8138
8139/// Encode a VFP 3-register arithmetic instruction (VADD.F32, VSUB.F32, VMUL.F32, VDIV.F32).
8140/// Returns the full 32-bit instruction word.
8141///
8142/// VFP encoding: [cond 1110] [D opc1 Vn] [Vd 101 sz] [N opc2 M 0 Vm]
8143/// For single-precision (sz=0), coprocessor = 0xA (bits[11:8]).
8144fn encode_vfp_3reg(base: u32, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<u32> {
8145    let sd_num = vfp_sreg_to_num(sd)?;
8146    let sn_num = vfp_sreg_to_num(sn)?;
8147    let sm_num = vfp_sreg_to_num(sm)?;
8148    let (vd, d) = encode_sreg(sd_num);
8149    let (vn, n) = encode_sreg(sn_num);
8150    let (vm, m) = encode_sreg(sm_num);
8151
8152    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8153}
8154
8155/// Encode a VFP 2-register instruction (VNEG.F32, VABS.F32, VSQRT.F32).
8156/// Returns the full 32-bit instruction word.
8157fn encode_vfp_2reg(base: u32, sd: &VfpReg, sm: &VfpReg) -> Result<u32> {
8158    let sd_num = vfp_sreg_to_num(sd)?;
8159    let sm_num = vfp_sreg_to_num(sm)?;
8160    let (vd, d) = encode_sreg(sd_num);
8161    let (vm, m) = encode_sreg(sm_num);
8162
8163    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8164}
8165
8166/// Encode a VFP load/store (VLDR.F32 / VSTR.F32).
8167/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8168/// U bit (bit 23) controls add/subtract offset.
8169fn encode_vfp_ldst(base: u32, sd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8170    let sd_num = vfp_sreg_to_num(sd)?;
8171    let (vd, d) = encode_sreg(sd_num);
8172    let rn = reg_to_bits(&addr.base);
8173
8174    let offset = addr.offset;
8175    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8176    let abs_offset = offset.unsigned_abs();
8177    let imm8 = (abs_offset / 4) & 0xFF;
8178
8179    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8180}
8181
8182/// Encode VMOV between core register and S-register.
8183/// VMOV Sn, Rt: 0xEE00_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8184/// VMOV Rt, Sn: 0xEE10_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8185fn encode_vmov_core_sreg(to_sreg: bool, sreg: &VfpReg, core: &Reg) -> Result<u32> {
8186    let s_num = vfp_sreg_to_num(sreg)?;
8187    let (vn, n) = encode_sreg(s_num);
8188    let rt = reg_to_bits(core);
8189
8190    let base = if to_sreg { 0xEE000A10 } else { 0xEE100A10 };
8191    Ok(base | (vn << 16) | (rt << 12) | (n << 7))
8192}
8193
8194/// Encode a VFP 3-register double-precision instruction (VADD.F64, VSUB.F64, etc.).
8195/// For double-precision (sz=1), coprocessor = 0xB (bits[11:8]).
8196/// The base should have bit 8 = 1 for F64 (0xB suffix instead of 0xA).
8197fn encode_vfp_3reg_f64(base: u32, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<u32> {
8198    let dd_num = vfp_dreg_to_num(dd)?;
8199    let dn_num = vfp_dreg_to_num(dn)?;
8200    let dm_num = vfp_dreg_to_num(dm)?;
8201    let (vd, d) = encode_dreg(dd_num);
8202    let (vn, n) = encode_dreg(dn_num);
8203    let (vm, m) = encode_dreg(dm_num);
8204
8205    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8206}
8207
8208/// Encode a VFP 2-register double-precision instruction (VNEG.F64, VABS.F64, VSQRT.F64).
8209fn encode_vfp_2reg_f64(base: u32, dd: &VfpReg, dm: &VfpReg) -> Result<u32> {
8210    let dd_num = vfp_dreg_to_num(dd)?;
8211    let dm_num = vfp_dreg_to_num(dm)?;
8212    let (vd, d) = encode_dreg(dd_num);
8213    let (vm, m) = encode_dreg(dm_num);
8214
8215    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8216}
8217
8218/// Encode a VFP load/store for double-precision (VLDR.64 / VSTR.64).
8219/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8220fn encode_vfp_ldst_f64(base: u32, dd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8221    let dd_num = vfp_dreg_to_num(dd)?;
8222    let (vd, d) = encode_dreg(dd_num);
8223    let rn = reg_to_bits(&addr.base);
8224
8225    let offset = addr.offset;
8226    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8227    let abs_offset = offset.unsigned_abs();
8228    let imm8 = (abs_offset / 4) & 0xFF;
8229
8230    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8231}
8232
8233/// Encode VMOV between two core registers and a D-register.
8234/// VMOV Dm, Rt, Rt2: 0xEC40_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8235/// VMOV Rt, Rt2, Dm: 0xEC50_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8236fn encode_vmov_core_dreg(
8237    to_dreg: bool,
8238    dreg: &VfpReg,
8239    core_lo: &Reg,
8240    core_hi: &Reg,
8241) -> Result<u32> {
8242    let d_num = vfp_dreg_to_num(dreg)?;
8243    let (vm, m) = encode_dreg(d_num);
8244    let rt = reg_to_bits(core_lo);
8245    let rt2 = reg_to_bits(core_hi);
8246
8247    let base = if to_dreg { 0xEC400B10 } else { 0xEC500B10 };
8248    Ok(base | (rt2 << 16) | (rt << 12) | (m << 5) | vm)
8249}
8250
8251/// Emit a VFP 32-bit instruction as Thumb-2 bytes (two LE halfwords).
8252fn vfp_to_thumb_bytes(instr: u32) -> Vec<u8> {
8253    let hw1 = ((instr >> 16) & 0xFFFF) as u16;
8254    let hw2 = (instr & 0xFFFF) as u16;
8255    let mut bytes = hw1.to_le_bytes().to_vec();
8256    bytes.extend_from_slice(&hw2.to_le_bytes());
8257    bytes
8258}
8259
8260// ============================================================================
8261// Helium MVE encoding helpers
8262// ============================================================================
8263
8264/// Q-register number: Q0=0, Q1=1, ..., Q7=7
8265fn qreg_to_num(reg: &QReg) -> u32 {
8266    match reg {
8267        QReg::Q0 => 0,
8268        QReg::Q1 => 1,
8269        QReg::Q2 => 2,
8270        QReg::Q3 => 3,
8271        QReg::Q4 => 4,
8272        QReg::Q5 => 5,
8273        QReg::Q6 => 6,
8274        QReg::Q7 => 7,
8275    }
8276}
8277
8278/// MVE element size to encoding bits: S8=0b00, S16=0b01, S32=0b10
8279fn mve_size_bits(size: &MveSize) -> u32 {
8280    match size {
8281        MveSize::S8 => 0b00,
8282        MveSize::S16 => 0b01,
8283        MveSize::S32 => 0b10,
8284    }
8285}
8286
8287/// Encode MVE 3-register instruction.
8288/// Q-registers are encoded as D-register pairs: Q0=D0:D1, Q1=D2:D3, etc.
8289/// In NEON/MVE encoding, the Q-register uses D-register number = Qn * 2.
8290fn encode_mve_3reg(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
8291    let d = qreg_to_num(qd) * 2;
8292    let n = qreg_to_num(qn) * 2;
8293    let m = qreg_to_num(qm) * 2;
8294
8295    // Standard NEON/MVE 3-register encoding:
8296    // D bit (bit 22) = Vd[4], Vd[3:0] = bits [15:12]
8297    // N bit (bit 7)  = Vn[4], Vn[3:0] = bits [19:16]
8298    // M bit (bit 5)  = Vm[4], Vm[3:0] = bits [3:0]
8299    let vd = d & 0xF;
8300    let d_bit = (d >> 4) & 1;
8301    let vn = n & 0xF;
8302    let n_bit = (n >> 4) & 1;
8303    let vm = m & 0xF;
8304    let m_bit = (m >> 4) & 1;
8305
8306    base | (d_bit << 22) | (vn << 16) | (vd << 12) | (n_bit << 7) | (m_bit << 5) | vm
8307}
8308
8309/// Encode MVE 3-register bitwise instruction (VAND, VORR, VEOR, VBIC).
8310fn encode_mve_3reg_bitwise(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
8311    encode_mve_3reg(base, qd, qn, qm)
8312}
8313
8314/// Encode MVE VLDRW.32 Qd, [Rn, #offset]
8315/// Format: EC9x xxxx - contiguous load, word-sized elements
8316fn encode_mve_vldrw(qd: &QReg, addr: &MemAddr) -> u32 {
8317    let qd_enc = qreg_to_num(qd) * 2;
8318    let rn = reg_to_bits(&addr.base);
8319    let offset = addr.offset;
8320    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8321    let abs_offset = offset.unsigned_abs();
8322    let imm7 = (abs_offset / 4) & 0x7F; // 7-bit word-aligned offset
8323
8324    // VLDRW.32 Qd, [Rn, #imm]: ED10 xx80 variant
8325    0xED100E80
8326        | (u_bit << 23)
8327        | ((qd_enc >> 4) << 22)
8328        | (rn << 16)
8329        | ((qd_enc & 0xF) << 12)
8330        | (imm7 & 0x7F)
8331}
8332
8333/// Encode MVE VSTRW.32 Qd, [Rn, #offset]
8334fn encode_mve_vstrw(qd: &QReg, addr: &MemAddr) -> u32 {
8335    let qd_enc = qreg_to_num(qd) * 2;
8336    let rn = reg_to_bits(&addr.base);
8337    let offset = addr.offset;
8338    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8339    let abs_offset = offset.unsigned_abs();
8340    let imm7 = (abs_offset / 4) & 0x7F;
8341
8342    0xED000E80
8343        | (u_bit << 23)
8344        | ((qd_enc >> 4) << 22)
8345        | (rn << 16)
8346        | ((qd_enc & 0xF) << 12)
8347        | (imm7 & 0x7F)
8348}
8349
8350impl ArmEncoder {
8351    /// Encode MVE constant load: MOVW+MOVT+VMOV for each 32-bit word, then assemble Q-register
8352    fn encode_thumb_mve_const(&self, qd: &QReg, bytes: &[u8; 16]) -> Result<Vec<u8>> {
8353        let mut result = Vec::new();
8354        let qd_num = qreg_to_num(qd);
8355
8356        // Load each 32-bit word into R12 (temp) then VMOV into S-register
8357        for i in 0..4 {
8358            let word = u32::from_le_bytes([
8359                bytes[i * 4],
8360                bytes[i * 4 + 1],
8361                bytes[i * 4 + 2],
8362                bytes[i * 4 + 3],
8363            ]);
8364            let lo16 = word & 0xFFFF;
8365            let hi16 = (word >> 16) & 0xFFFF;
8366
8367            // MOVW R12, #lo16
8368            result.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
8369            // MOVT R12, #hi16
8370            if hi16 != 0 {
8371                result.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
8372            }
8373
8374            // VMOV Sn, R12 where Sn = Qd*4 + i
8375            let s_num = qd_num * 4 + i as u32;
8376            let (vn, n) = encode_sreg(s_num);
8377            let vmov: u32 = 0xEE000A10 | (vn << 16) | (12 << 12) | (n << 7);
8378            result.extend_from_slice(&vfp_to_thumb_bytes(vmov));
8379        }
8380
8381        Ok(result)
8382    }
8383
8384    /// Encode lane-wise f32 binary operation (VDIV, etc.) via S-register extraction
8385    fn encode_thumb_mve_lane_wise_f32_binop(
8386        &self,
8387        qd: &QReg,
8388        qn: &QReg,
8389        qm: &QReg,
8390        vfp_base: u32,
8391    ) -> Result<Vec<u8>> {
8392        let mut result = Vec::new();
8393        let qd_num = qreg_to_num(qd);
8394        let qn_num = qreg_to_num(qn);
8395        let qm_num = qreg_to_num(qm);
8396
8397        // For each lane 0..3: use S-registers directly (Q aliasing)
8398        for i in 0..4u32 {
8399            let sd = qd_num * 4 + i;
8400            let sn = qn_num * 4 + i;
8401            let sm = qm_num * 4 + i;
8402
8403            let (vd, d) = encode_sreg(sd);
8404            let (vn, n) = encode_sreg(sn);
8405            let (vm, m) = encode_sreg(sm);
8406
8407            let instr = vfp_base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
8408            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
8409        }
8410
8411        Ok(result)
8412    }
8413
8414    /// Encode lane-wise f32 VSQRT via S-register extraction
8415    fn encode_thumb_mve_lane_wise_f32_sqrt(&self, qd: &QReg, qm: &QReg) -> Result<Vec<u8>> {
8416        let mut result = Vec::new();
8417        let qd_num = qreg_to_num(qd);
8418        let qm_num = qreg_to_num(qm);
8419
8420        // VSQRT.F32 base: 0xEEB10AC0
8421        for i in 0..4u32 {
8422            let sd = qd_num * 4 + i;
8423            let sm = qm_num * 4 + i;
8424
8425            let (vd, d) = encode_sreg(sd);
8426            let (vm, m) = encode_sreg(sm);
8427
8428            let instr: u32 = 0xEEB10AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
8429            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
8430        }
8431
8432        Ok(result)
8433    }
8434}
8435
8436#[cfg(test)]
8437mod tests {
8438    use super::*;
8439
8440    #[test]
8441    fn test_encoder_creation() {
8442        let encoder_arm = ArmEncoder::new_arm32();
8443        assert!(!encoder_arm.thumb_mode);
8444
8445        let encoder_thumb = ArmEncoder::new_thumb2();
8446        assert!(encoder_thumb.thumb_mode);
8447    }
8448
8449    /// #204 WAKE-path regression: `SetCond` materialized 0/1 with the 16-bit
8450    /// `MOVS Rd,#imm` (T1), whose Rd field is 3 bits (R0–R7). For a high Rd
8451    /// (R8–R12) `rd_bits << 8` overflows bit 11, flipping the opcode MOVS→CMP
8452    /// (`0x2c00`), so the boolean was never written — gale's `has_waiter` kept a
8453    /// stale value and the binary-sem WAKE dispatch read garbage. High Rd must
8454    /// use the 32-bit `MOV.W` (T2). Verify the bytes, not the IR.
8455    /// #311: the SAME high-Rd MOVS→CMP transmutation as #204, but in the
8456    /// i64 comparison expansions (I64SetCond / I64SetCondZ) — missed by the
8457    /// #204 hardening. With rd=R8 the boolean died in the flags
8458    /// (`ite eq; cmpeq r0,#1; cmpne r0,#0`), so gale's packed-u64 select
8459    /// read a stale register on silicon. High Rd must take MOV.W / CMP.W.
8460    #[test]
8461    fn test_encode_i64setcond_high_reg_uses_mov_w_311() {
8462        use synth_synthesis::{ArmOp, Condition, Reg};
8463        let enc = ArmEncoder::new_thumb2();
8464        let bytes = enc
8465            .encode(&ArmOp::I64SetCond {
8466                rd: Reg::R8,
8467                rn_lo: Reg::R2,
8468                rn_hi: Reg::R3,
8469                rm_lo: Reg::R6,
8470                rm_hi: Reg::R7,
8471                cond: Condition::EQ,
8472            })
8473            .unwrap();
8474        // The 32-bit MOV.W immediate (T2) first halfword is 0xF04F; the
8475        // 16-bit transmuted forms would contain 0x2801/0x2800 (CMP r0,#1/#0).
8476        let halfwords: Vec<u16> = bytes
8477            .chunks(2)
8478            .map(|c| u16::from_le_bytes([c[0], c[1]]))
8479            .collect();
8480        assert!(
8481            halfwords.iter().filter(|&&h| h == 0xF04F).count() == 2,
8482            "high rd must use two MOV.W (T2) encodings, got {halfwords:04x?}"
8483        );
8484        assert!(
8485            !halfwords.contains(&0x2801) && !halfwords.contains(&0x2800),
8486            "no transmuted 16-bit CMP imm: {halfwords:04x?}"
8487        );
8488
8489        let bytes_z = enc
8490            .encode(&ArmOp::I64SetCondZ {
8491                rd: Reg::R8,
8492                rn_lo: Reg::R2,
8493                rn_hi: Reg::R3,
8494            })
8495            .unwrap();
8496        let hw_z: Vec<u16> = bytes_z
8497            .chunks(2)
8498            .map(|c| u16::from_le_bytes([c[0], c[1]]))
8499            .collect();
8500        assert!(
8501            hw_z.iter().filter(|&&h| h == 0xF04F).count() == 2,
8502            "SetCondZ high rd MOV.W: {hw_z:04x?}"
8503        );
8504        // CMP.W rd,#0 (T2) first halfword: 0xF1B0 | rd
8505        assert!(
8506            hw_z.contains(&(0xF1B0 | 8)),
8507            "SetCondZ high rd must use CMP.W: {hw_z:04x?}"
8508        );
8509    }
8510
8511    #[test]
8512    fn test_encode_setcond_high_reg_uses_mov_w_204() {
8513        use synth_synthesis::{ArmOp, Condition, Reg};
8514        let enc = ArmEncoder::new_thumb2();
8515        // R12 (high): must be ITE + MOV.W #1 + MOV.W #0, never a 16-bit MOVS/CMP.
8516        let hi = enc
8517            .encode(&ArmOp::SetCond {
8518                rd: Reg::R12,
8519                cond: Condition::NE,
8520            })
8521            .unwrap();
8522        assert_eq!(hi.len(), 10, "ITE(2) + MOV.W(4) + MOV.W(4): {hi:02x?}");
8523        // both value halfwords are MOV.W (0xF04F) — NOT the corrupt CMP (0x2c..).
8524        assert_eq!(&hi[2..4], &[0x4F, 0xF0], "then = MOV.W: {hi:02x?}");
8525        assert_eq!(&hi[6..8], &[0x4F, 0xF0], "else = MOV.W: {hi:02x?}");
8526        assert_eq!(hi[4] & 0x0F, 0x01, "then imm = #1");
8527        assert_eq!(hi[8] & 0x0F, 0x00, "else imm = #0");
8528        // Low Rd keeps the compact 16-bit MOVS form.
8529        let lo = enc
8530            .encode(&ArmOp::SetCond {
8531                rd: Reg::R0,
8532                cond: Condition::NE,
8533            })
8534            .unwrap();
8535        assert_eq!(lo.len(), 6, "ITE(2) + MOVS(2) + MOVS(2): {lo:02x?}");
8536        assert_eq!(lo[2..4], [0x01, 0x20], "then = MOVS R0,#1");
8537        assert_eq!(lo[4..6], [0x00, 0x20], "else = MOVS R0,#0");
8538    }
8539
8540    /// #209 Opt 1b: UMULL RdLo, RdHi, Rn, Rm encodes correctly on both ISAs.
8541    /// Thumb-2 T1: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm.
8542    /// A32:        cond 0000 1000 RdHi RdLo Rm 1001 Rn.
8543    #[test]
8544    fn test_encode_umull_209b() {
8545        use synth_synthesis::{ArmOp, Reg};
8546        let op = ArmOp::Umull {
8547            rdlo: Reg::R4,
8548            rdhi: Reg::R5,
8549            rn: Reg::R0,
8550            rm: Reg::R3,
8551        };
8552        // Thumb-2: hw1 = 0xFBA0 | 0 = 0xFBA0; hw2 = (4<<12)|(5<<8)|3 = 0x4503.
8553        let t = ArmEncoder::new_thumb2().encode(&op).unwrap();
8554        assert_eq!(
8555            t,
8556            vec![0xA0, 0xFB, 0x03, 0x45],
8557            "umull r4,r5,r0,r3 (T2): {t:02x?}"
8558        );
8559        // A32: 0xE0800090 | (5<<16) | (4<<12) | (3<<8) | 0 = 0xE0854390.
8560        let a = ArmEncoder::new_arm32().encode(&op).unwrap();
8561        assert_eq!(
8562            a,
8563            0xE085_4390u32.to_le_bytes().to_vec(),
8564            "umull (A32): {a:02x?}"
8565        );
8566    }
8567
8568    /// #206 regression: the ARM32 (A32) `Ldr`/`Str` encoders fed `addr` through
8569    /// `encode_mem_addr`, which returns only the 12-bit immediate — so a register
8570    /// offset (`[rn, rm, #off]`) was silently dropped to `[rn, #off]`, sending
8571    /// the access to the wrong runtime address (silent miscompile on the default
8572    /// `--target arm`). A register offset must materialize `ip = rn + rm` and
8573    /// load from `[ip, #off]`. Verify the bytes.
8574    #[test]
8575    fn test_encode_arm32_indexed_load_keeps_index_206() {
8576        use synth_synthesis::{ArmOp, MemAddr, Reg};
8577        let enc = ArmEncoder::new_arm32();
8578        // ldr r0, [r11, r1, #8]  must NOT collapse to a single immediate ldr.
8579        let bytes = enc
8580            .encode(&ArmOp::Ldr {
8581                rd: Reg::R0,
8582                addr: MemAddr::reg_imm(Reg::R11, Reg::R1, 8),
8583            })
8584            .unwrap();
8585        assert_eq!(
8586            bytes.len(),
8587            8,
8588            "expected ADD ip + LDR (2 words): {bytes:02x?}"
8589        );
8590        let add = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
8591        let ldr = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
8592        // ADD ip, r11, r1  = 0xE08BC001
8593        assert_eq!(add, 0xE08B_C001, "ADD ip,r11,r1: {add:#010x}");
8594        // LDR r0, [ip, #8] = 0xE59C0008
8595        assert_eq!(ldr, 0xE59C_0008, "LDR r0,[ip,#8]: {ldr:#010x}");
8596        // A bare immediate ldr (the bug) would be 0xE59B0008 (base=r11) — reject.
8597        assert_ne!(ldr, 0xE59B_0008, "index must not be dropped");
8598    }
8599
8600    /// #594 regression: `call_indirect` on the A32 path (`--target cortex-r5`)
8601    /// was encoded as a literal NOP (0xE1A00000) — the call never happened and
8602    /// the function silently returned the leftover table-index value. The A32
8603    /// encoder must emit the same three-instruction expansion as Thumb-2:
8604    /// `MOV r12, idx, LSL #2; LDR r12, [r11, r12]; BLX r12`.
8605    #[test]
8606    fn test_encode_arm32_call_indirect_is_real_call_594() {
8607        use synth_synthesis::{ArmOp, Reg};
8608        let enc = ArmEncoder::new_arm32();
8609        let bytes = enc
8610            .encode(&ArmOp::CallIndirect {
8611                rd: Reg::R0,
8612                type_idx: 0,
8613                table_index_reg: Reg::R0,
8614            })
8615            .unwrap();
8616        assert_eq!(
8617            bytes.len(),
8618            12,
8619            "expected MOV + LDR + BLX (3 words): {bytes:02x?}"
8620        );
8621        let mov = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
8622        let ldr = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
8623        let blx = u32::from_le_bytes(bytes[8..12].try_into().unwrap());
8624        // MOV r12, r0, LSL #2 = 0xE1A0C100
8625        assert_eq!(mov, 0xE1A0_C100, "MOV r12,r0,LSL#2: {mov:#010x}");
8626        // LDR r12, [r11, r12] = 0xE79BC00C
8627        assert_eq!(ldr, 0xE79B_C00C, "LDR r12,[r11,r12]: {ldr:#010x}");
8628        // BLX r12 = 0xE12FFF3C
8629        assert_eq!(blx, 0xE12F_FF3C, "BLX r12: {blx:#010x}");
8630        // The bug: a single NOP word. Must never come back.
8631        assert!(
8632            !bytes
8633                .chunks_exact(4)
8634                .any(|w| w == 0xE1A0_0000u32.to_le_bytes()),
8635            "call_indirect must not contain a NOP (#594): {bytes:02x?}"
8636        );
8637
8638        // A non-R0 index register lands in the MOV's Rm field.
8639        let bytes = enc
8640            .encode(&ArmOp::CallIndirect {
8641                rd: Reg::R0,
8642                type_idx: 0,
8643                table_index_reg: Reg::R4,
8644            })
8645            .unwrap();
8646        let mov = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
8647        assert_eq!(mov, 0xE1A0_C104, "MOV r12,r4,LSL#2: {mov:#010x}");
8648    }
8649
8650    /// #597 anchor (justified correctness RE-PIN of the #594-era freeze): the
8651    /// Thumb-2 `CallIndirect` expansion is `mov.w ip, rm, LSL #2; ldr.w ip,
8652    /// [r11, ip]; blx ip`.
8653    ///
8654    /// The #594 PR froze the then-current bytes `4F EA 20 0C ...` whose first
8655    /// word decodes as `mov.w ip, rm, ASR #32` — the intended `LSL #2` had
8656    /// its shift amount in the TYPE field (bits 5:4) instead of imm2 (bits
8657    /// 7:6), so the index was destroyed and every call_indirect dispatched
8658    /// table entry 0 (shipped miscompile, masked by index-0 probes). #597
8659    /// corrects the encoding; new bytes `4F EA 80 0C ...` were
8660    /// execution-validated under unicorn against the wasmtime oracle on a
8661    /// multi-entry table (indexes 0, 1, 3 —
8662    /// scripts/repro/call_indirect_597_differential.py) before this pin was
8663    /// replaced. Old pin: [4F EA 20 0C, 5B F8 0C C0, E0 47] (ASR #32 — must
8664    /// never come back).
8665    #[test]
8666    fn test_encode_thumb_call_indirect_lsl2_597() {
8667        use synth_synthesis::{ArmOp, Reg};
8668        let enc = ArmEncoder::new_thumb2();
8669        let bytes = enc
8670            .encode(&ArmOp::CallIndirect {
8671                rd: Reg::R0,
8672                type_idx: 0,
8673                table_index_reg: Reg::R0,
8674            })
8675            .unwrap();
8676        assert_eq!(
8677            bytes,
8678            vec![0x4F, 0xEA, 0x80, 0x0C, 0x5B, 0xF8, 0x0C, 0xC0, 0xE0, 0x47],
8679            "Thumb-2 CallIndirect: mov.w ip,r0,LSL#2; ldr.w ip,[r11,ip]; blx ip: {bytes:02x?}"
8680        );
8681        // The #597 bug bytes (ASR #32 first word) must never come back.
8682        assert_ne!(
8683            &bytes[0..4],
8684            &[0x4F, 0xEA, 0x20, 0x0C],
8685            "mov.w ip, rm, ASR #32 — the #597 type-field bug"
8686        );
8687
8688        // A non-R0 index register lands in the mov.w's Rm field (hw2 bits 3:0).
8689        let bytes = enc
8690            .encode(&ArmOp::CallIndirect {
8691                rd: Reg::R0,
8692                type_idx: 0,
8693                table_index_reg: Reg::R4,
8694            })
8695            .unwrap();
8696        assert_eq!(
8697            &bytes[0..4],
8698            &[0x4F, 0xEA, 0x84, 0x0C],
8699            "mov.w ip, r4, LSL #2: {bytes:02x?}"
8700        );
8701    }
8702
8703    /// #178/#180 regression: the Thumb `Add`/`Adds`/`Subs` reg-forms used the
8704    /// 16-bit encoding unconditionally. For high registers (R12 base scratch,
8705    /// R8-R11 i64 pairs) the 3-bit register fields overflow and corrupt the
8706    /// operands — `add ip,ip,r0` came out as `adds r4,r5,r1` (0x186C), silently
8707    /// dropping the address operand and miscompiling every optimized memory
8708    /// access. High registers must use the 32-bit `.W` forms.
8709    #[test]
8710    fn test_encode_thumb_add_high_reg_uses_add_w_178_180() {
8711        let encoder = ArmEncoder::new_thumb2();
8712
8713        // add ip, ip, r0  — the exact MemLoad/MemStore base+addr op.
8714        let code = encoder
8715            .encode(&ArmOp::Add {
8716                rd: Reg::R12,
8717                rn: Reg::R12,
8718                op2: Operand2::Reg(Reg::R0),
8719            })
8720            .unwrap();
8721        // ADD.W ip, ip, r0 = EB0C 0C00 (little-endian halfwords).
8722        assert_eq!(
8723            code,
8724            vec![0x0C, 0xEB, 0x00, 0x0C],
8725            "high-reg Thumb ADD must be 32-bit ADD.W (EB0C 0C00), not corrupt 16-bit; got {code:02X?}"
8726        );
8727        // Must NOT be the buggy 16-bit 0x186C (`adds r4,r5,r1`).
8728        assert_ne!(code, vec![0x6C, 0x18], "regressed to corrupt 16-bit ADDS");
8729
8730        // Low-register add stays 16-bit (no regression for the common case).
8731        let lo = encoder
8732            .encode(&ArmOp::Add {
8733                rd: Reg::R1,
8734                rn: Reg::R2,
8735                op2: Operand2::Reg(Reg::R3),
8736            })
8737            .unwrap();
8738        assert_eq!(
8739            lo.len(),
8740            2,
8741            "low-reg ADD should remain 16-bit, got {lo:02X?}"
8742        );
8743    }
8744
8745    /// #178/#180 sibling: i64 low-word `Adds`/`Subs` can land in R8-R11 pairs;
8746    /// those must fall back to 32-bit ADDS.W/SUBS.W (flag-setting preserved).
8747    #[test]
8748    fn test_encode_thumb_adds_subs_high_reg_use_32bit_178_180() {
8749        let encoder = ArmEncoder::new_thumb2();
8750
8751        // adds r10, r10, r8  → ADDS.W = EB1A 0A08
8752        let adds = encoder
8753            .encode(&ArmOp::Adds {
8754                rd: Reg::R10,
8755                rn: Reg::R10,
8756                op2: Operand2::Reg(Reg::R8),
8757            })
8758            .unwrap();
8759        assert_eq!(
8760            adds,
8761            vec![0x1A, 0xEB, 0x08, 0x0A],
8762            "high-reg ADDS must be 32-bit ADDS.W (EB1A 0A08); got {adds:02X?}"
8763        );
8764
8765        // subs r10, r10, r8  → SUBS.W = EBBA 0A08
8766        let subs = encoder
8767            .encode(&ArmOp::Subs {
8768                rd: Reg::R10,
8769                rn: Reg::R10,
8770                op2: Operand2::Reg(Reg::R8),
8771            })
8772            .unwrap();
8773        assert_eq!(
8774            subs,
8775            vec![0xBA, 0xEB, 0x08, 0x0A],
8776            "high-reg SUBS must be 32-bit SUBS.W (EBBA 0A08); got {subs:02X?}"
8777        );
8778    }
8779
8780    /// #184 (sibling of #180): 16-bit CMN (T1) only encodes R0-R7. High registers
8781    /// must use 32-bit CMN.W, not the corrupt truncated 16-bit form.
8782    #[test]
8783    fn test_encode_thumb_cmn_high_reg_uses_cmn_w_184() {
8784        let encoder = ArmEncoder::new_thumb2();
8785
8786        // cmn r10, r8  → CMN.W = EB1A 0F08 (ADD.W S=1, Rd=PC discarded).
8787        let cmn = encoder
8788            .encode(&ArmOp::Cmn {
8789                rn: Reg::R10,
8790                op2: Operand2::Reg(Reg::R8),
8791            })
8792            .unwrap();
8793        assert_eq!(
8794            cmn,
8795            vec![0x1A, 0xEB, 0x08, 0x0F],
8796            "high-reg CMN must be 32-bit CMN.W (EB1A 0F08); got {cmn:02X?}"
8797        );
8798
8799        // Low registers stay 16-bit: cmn r1, r2 = 0x42D1.
8800        let lo = encoder
8801            .encode(&ArmOp::Cmn {
8802                rn: Reg::R1,
8803                op2: Operand2::Reg(Reg::R2),
8804            })
8805            .unwrap();
8806        assert_eq!(
8807            lo.len(),
8808            2,
8809            "low-reg CMN should remain 16-bit, got {lo:02X?}"
8810        );
8811        assert_eq!(lo, vec![0xD1, 0x42], "low-reg CMN bytes wrong: {lo:02X?}");
8812    }
8813
8814    /// #185 regression: feeding PC (R15) as a data operand to a Thumb-2 op that
8815    /// guards its registers must return Err, not panic under debug-assertions.
8816    /// (Synth never emits PC here; the fuzz harness requires encode() be total.)
8817    #[test]
8818    fn test_encode_pc_operand_returns_err_not_panic_185() {
8819        let encoder = ArmEncoder::new_thumb2();
8820        for op in [
8821            ArmOp::Sdiv {
8822                rd: Reg::PC,
8823                rn: Reg::R0,
8824                rm: Reg::R1,
8825            },
8826            ArmOp::Udiv {
8827                rd: Reg::R0,
8828                rn: Reg::PC,
8829                rm: Reg::R1,
8830            },
8831            ArmOp::Sdiv {
8832                rd: Reg::R0,
8833                rn: Reg::R1,
8834                rm: Reg::PC,
8835            },
8836        ] {
8837            let r = encoder.encode(&op);
8838            assert!(
8839                r.is_err(),
8840                "encode({op:?}) must return Err for a PC operand, got {r:?}"
8841            );
8842        }
8843        // Valid registers still encode fine (no false rejection).
8844        assert!(
8845            encoder
8846                .encode(&ArmOp::Sdiv {
8847                    rd: Reg::R0,
8848                    rn: Reg::R1,
8849                    rm: Reg::R2
8850                })
8851                .is_ok()
8852        );
8853    }
8854
8855    #[test]
8856    fn test_encode_nop_arm32() {
8857        let encoder = ArmEncoder::new_arm32();
8858        let code = encoder.encode(&ArmOp::Nop).unwrap();
8859
8860        assert_eq!(code.len(), 4); // ARM32 instructions are 4 bytes
8861        assert_eq!(code, vec![0x00, 0x00, 0xA0, 0xE1]); // MOV R0, R0
8862    }
8863
8864    #[test]
8865    fn test_encode_nop_thumb() {
8866        let encoder = ArmEncoder::new_thumb2();
8867        let code = encoder.encode(&ArmOp::Nop).unwrap();
8868
8869        assert_eq!(code.len(), 2); // Thumb instructions are 2 bytes
8870        assert_eq!(code, vec![0x00, 0xBF]); // NOP
8871    }
8872
8873    #[test]
8874    fn test_encode_mov_immediate_arm32() {
8875        let encoder = ArmEncoder::new_arm32();
8876        let op = ArmOp::Mov {
8877            rd: Reg::R0,
8878            op2: Operand2::Imm(42),
8879        };
8880
8881        let code = encoder.encode(&op).unwrap();
8882        assert_eq!(code.len(), 4);
8883
8884        // Verify it's a MOV instruction (bits should have immediate flag set)
8885        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
8886        assert_eq!(instr & 0x0E000000, 0x02000000); // Check I bit is set
8887    }
8888
8889    #[test]
8890    fn test_encode_add_registers_arm32() {
8891        let encoder = ArmEncoder::new_arm32();
8892        let op = ArmOp::Add {
8893            rd: Reg::R0,
8894            rn: Reg::R1,
8895            op2: Operand2::Reg(Reg::R2),
8896        };
8897
8898        let code = encoder.encode(&op).unwrap();
8899        assert_eq!(code.len(), 4);
8900
8901        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
8902        // Verify it's an ADD instruction with correct opcode
8903        assert_eq!(instr & 0x0FE00000, 0x00800000);
8904    }
8905
8906    /// #350 — `encode_thumb32_add_imm` must lower an out-of-range immediate
8907    /// (> 0xFFF) to a legal MOVW(/MOVT) + ADD.W-register sequence instead of
8908    /// erroring. The small-imm fast path (imm <= 0xFFF) stays byte-identical.
8909    #[test]
8910    fn test_encode_add_imm_large_350() {
8911        let enc = ArmEncoder::new_thumb2();
8912
8913        // --- Fast path unchanged: imm <= 0xFFF is a single 4-byte ADD.W ---
8914        let small = enc
8915            .encode_thumb32_add_imm(&Reg::R0, &Reg::R1, 0x123)
8916            .unwrap();
8917        assert_eq!(small.len(), 4, "small imm must stay a single instruction");
8918
8919        // helper: decode a Thumb-2 MOVW/MOVT halfword pair back to its imm16
8920        fn movx_imm16(b: &[u8]) -> u32 {
8921            let hw1 = u16::from_le_bytes([b[0], b[1]]) as u32;
8922            let hw2 = u16::from_le_bytes([b[2], b[3]]) as u32;
8923            let imm4 = hw1 & 0xF;
8924            let i = (hw1 >> 10) & 1;
8925            let imm3 = (hw2 >> 12) & 0x7;
8926            let imm8 = hw2 & 0xFF;
8927            (imm4 << 12) | (i << 11) | (imm3 << 8) | imm8
8928        }
8929        fn movx_rd(b: &[u8]) -> u32 {
8930            (u16::from_le_bytes([b[2], b[3]]) as u32 >> 8) & 0xF
8931        }
8932
8933        // --- rd != rn: scratch is rd. imm = 70000 = 0x11170 needs MOVW+MOVT. ---
8934        // 0x11170: lo16 = 0x1170, hi16 = 0x0001
8935        let seq = enc
8936            .encode_thumb32_add_imm(&Reg::R12, &Reg::R0, 70000)
8937            .unwrap();
8938        assert_eq!(seq.len(), 12, "MOVW + MOVT + ADD = 12 bytes");
8939        // MOVW r12, #0x1170
8940        assert_eq!(u16::from_le_bytes([seq[0], seq[1]]) & 0xFBF0, 0xF240);
8941        assert_eq!(movx_rd(&seq[0..4]), 12);
8942        assert_eq!(movx_imm16(&seq[0..4]), 0x1170);
8943        // MOVT r12, #0x0001
8944        assert_eq!(u16::from_le_bytes([seq[4], seq[5]]) & 0xFBF0, 0xF2C0);
8945        assert_eq!(movx_rd(&seq[4..8]), 12);
8946        assert_eq!(movx_imm16(&seq[4..8]), 0x0001);
8947        // ADD.W r12, r0, r12  (EB00 | rn=0 ; rd=12, rm=12)
8948        let add1 = u16::from_le_bytes([seq[8], seq[9]]) as u32;
8949        let add2 = u16::from_le_bytes([seq[10], seq[11]]) as u32;
8950        assert_eq!(add1 & 0xFFF0, 0xEB00);
8951        assert_eq!(add1 & 0xF, 0); // rn = r0
8952        assert_eq!((add2 >> 8) & 0xF, 12); // rd = r12
8953        assert_eq!(add2 & 0xF, 12); // rm = scratch = r12
8954        // The materialized scratch must reconstruct exactly 70000.
8955        assert_eq!(
8956            (movx_imm16(&seq[4..8]) << 16) | movx_imm16(&seq[0..4]),
8957            70000
8958        );
8959
8960        // --- imm <= 0xFFFF: MOVT is skipped (MOVW + ADD = 8 bytes). ---
8961        let seq16 = enc
8962            .encode_thumb32_add_imm(&Reg::R3, &Reg::R0, 0xABCD)
8963            .unwrap();
8964        assert_eq!(seq16.len(), 8, "imm <= 0xFFFF skips MOVT");
8965        assert_eq!(movx_imm16(&seq16[0..4]), 0xABCD);
8966        assert_eq!(movx_rd(&seq16[0..4]), 3); // scratch = rd = r3
8967
8968        // --- rd == rn (in-place add): scratch must be R12, not rd. ---
8969        // imm = 0x12345: lo16 = 0x2345, hi16 = 0x0001
8970        let inplace = enc
8971            .encode_thumb32_add_imm(&Reg::R5, &Reg::R5, 0x12345)
8972            .unwrap();
8973        assert_eq!(inplace.len(), 12);
8974        assert_eq!(movx_rd(&inplace[0..4]), 12, "rd==rn must use R12 scratch");
8975        assert_eq!(
8976            (movx_imm16(&inplace[4..8]) << 16) | movx_imm16(&inplace[0..4]),
8977            0x12345
8978        );
8979        // ADD.W r5, r5, r12 — rm must be the scratch (12), never rn.
8980        let ip_add2 = u16::from_le_bytes([inplace[10], inplace[11]]) as u32;
8981        assert_eq!(ip_add2 & 0xF, 12);
8982        assert_eq!((ip_add2 >> 8) & 0xF, 5);
8983    }
8984
8985    /// #350 follow-up — the `encoder_no_panic` fuzz harness drives the encoder
8986    /// with ARBITRARY registers, including the one case the in-place lowering
8987    /// cannot serve: rd==rn==R12. There the scratch (R12, the reserved encoder
8988    /// register) would alias Rn and clobber it before the ADD reads it. The
8989    /// encoder contract (#180/#185) is Ok-or-Err, never a panic — so this must
8990    /// return Err, not assert. (Real codegen never emits rd==rn==R12 because R12
8991    /// is non-allocatable; this guards only the fuzz/adversarial path.)
8992    #[test]
8993    fn test_encode_add_imm_large_rd_rn_r12_errs_not_panics_350() {
8994        let enc = ArmEncoder::new_thumb2();
8995        // Out-of-range imm with rd==rn==R12: no free scratch -> Err.
8996        let r = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 70000);
8997        assert!(
8998            r.is_err(),
8999            "rd==rn==R12 with out-of-range imm must Err (no free scratch), got {r:?}"
9000        );
9001        // Small imm with rd==rn==R12 still takes the single-instruction fast path
9002        // (no scratch needed) and must succeed — the guard is scoped to the
9003        // out-of-range lowering only.
9004        let small = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 0x10);
9005        assert!(small.is_ok(), "small imm needs no scratch, must stay Ok");
9006    }
9007
9008    /// #378 — `encode_operand2` (ARM32 data-processing operand) must FAIL
9009    /// HONESTLY on an immediate that is not a valid rotated immediate, rather
9010    /// than silently masking it to `imm & 0xFF` and emitting a WRONG
9011    /// instruction. `0x1FF` has 9 set bits, so it cannot come from rotating an
9012    /// 8-bit imm8 — non-encodable. Real codegen materializes large constants via
9013    /// MOVW/MOVT; this guards the encoder's Ok-or-Err contract (#180/#185)
9014    /// directly. It is an Err (not a panic) so the `encoder_no_panic` fuzz
9015    /// harness — which drives arbitrary operands — still passes.
9016    #[test]
9017    fn test_encode_operand2_non_rotatable_imm_errs_not_masks_378() {
9018        let enc = ArmEncoder::new_arm32();
9019        let bad = enc.encode(&ArmOp::Add {
9020            rd: Reg::R0,
9021            rn: Reg::R1,
9022            op2: Operand2::Imm(0x1FF),
9023        });
9024        assert!(
9025            bad.is_err(),
9026            "non-rotatable ARM32 immediate 0x1FF must Err (was silently masked \
9027             to 0xFF), got {bad:?}"
9028        );
9029        // A representable rotated immediate still encodes fine (regression guard).
9030        let ok = enc.encode(&ArmOp::Add {
9031            rd: Reg::R0,
9032            rn: Reg::R1,
9033            op2: Operand2::Imm(0xFF),
9034        });
9035        assert!(
9036            ok.is_ok(),
9037            "0xFF is a valid rotated immediate, must stay Ok"
9038        );
9039    }
9040
9041    #[test]
9042    fn test_encode_ldr_arm32() {
9043        let encoder = ArmEncoder::new_arm32();
9044        let op = ArmOp::Ldr {
9045            rd: Reg::R0,
9046            addr: MemAddr::imm(Reg::R1, 4),
9047        };
9048
9049        let code = encoder.encode(&op).unwrap();
9050        assert_eq!(code.len(), 4);
9051
9052        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9053        // Verify load bit is set
9054        assert_eq!(instr & 0x00100000, 0x00100000);
9055    }
9056
9057    #[test]
9058    fn test_encode_str_arm32() {
9059        let encoder = ArmEncoder::new_arm32();
9060        let op = ArmOp::Str {
9061            rd: Reg::R0,
9062            addr: MemAddr::imm(Reg::SP, 0),
9063        };
9064
9065        let code = encoder.encode(&op).unwrap();
9066        assert_eq!(code.len(), 4);
9067    }
9068
9069    #[test]
9070    fn test_encode_branch_arm32() {
9071        let encoder = ArmEncoder::new_arm32();
9072        let op = ArmOp::Bl {
9073            label: "main".to_string(),
9074        };
9075
9076        let code = encoder.encode(&op).unwrap();
9077        assert_eq!(code.len(), 4);
9078
9079        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9080        // Verify BL opcode
9081        assert_eq!(instr & 0x0F000000, 0x0B000000);
9082    }
9083
9084    /// Regression test for #167 + #174: the Thumb-2 BL relocatable placeholder
9085    /// must carry a -4 addend so an R_ARM_THM_CALL nets to exactly the symbol S.
9086    /// The correct encoding is what `gas` emits for `bl <extern>`: f7ff fffe
9087    /// (hw1=0xF7FF, hw2=0xFFFE), little-endian bytes FF F7 FE FF.
9088    ///   - 0xD000 (J1=J2=0) → ~+0x600000 garbage addend: `bl c0000c` / truncated
9089    ///     to fit (#167).
9090    ///   - 0xF800 (addend 0) → lands at S+4, one instruction past the callee
9091    ///     entry (#174).
9092    ///   - 0xFFFE (addend -4) → lands at S. Correct.
9093    #[test]
9094    fn test_encode_thumb_bl_placeholder_addend_167_174() {
9095        let encoder = ArmEncoder::new_thumb2();
9096        let op = ArmOp::Bl {
9097            label: "callee".to_string(),
9098        };
9099
9100        let code = encoder.encode(&op).unwrap();
9101        assert_eq!(code.len(), 4, "Thumb-2 BL is 32-bit");
9102
9103        let hw1 = u16::from_le_bytes([code[0], code[1]]);
9104        let hw2 = u16::from_le_bytes([code[2], code[3]]);
9105        assert_eq!(hw1, 0xF7FF, "BL first halfword (matches gas `bl <extern>`)");
9106        assert_eq!(
9107            hw2, 0xFFFE,
9108            "BL second halfword must be 0xFFFE (-4 addend → nets to S), not 0xF800 (→ S+4, #174) or 0xD000 (#167)"
9109        );
9110        assert_ne!(hw2, 0xF800, "0xF800 (addend 0) lands at S+4 (#174)");
9111        assert_ne!(hw2, 0xD000, "0xD000 bakes in a ~+0x600000 addend (#167)");
9112    }
9113
9114    #[test]
9115    fn test_encode_sequence() {
9116        let encoder = ArmEncoder::new_arm32();
9117        let ops = vec![
9118            ArmOp::Mov {
9119                rd: Reg::R0,
9120                op2: Operand2::Imm(42),
9121            },
9122            ArmOp::Mov {
9123                rd: Reg::R1,
9124                op2: Operand2::Imm(10),
9125            },
9126            ArmOp::Add {
9127                rd: Reg::R2,
9128                rn: Reg::R0,
9129                op2: Operand2::Reg(Reg::R1),
9130            },
9131        ];
9132
9133        let code = encoder.encode_sequence(&ops).unwrap();
9134        assert_eq!(code.len(), 12); // 3 instructions * 4 bytes
9135    }
9136
9137    #[test]
9138    fn test_reg_to_bits() {
9139        assert_eq!(reg_to_bits(&Reg::R0), 0);
9140        assert_eq!(reg_to_bits(&Reg::R7), 7);
9141        assert_eq!(reg_to_bits(&Reg::SP), 13);
9142        assert_eq!(reg_to_bits(&Reg::LR), 14);
9143        assert_eq!(reg_to_bits(&Reg::PC), 15);
9144    }
9145
9146    #[test]
9147    fn test_encode_bitwise_operations() {
9148        let encoder = ArmEncoder::new_arm32();
9149
9150        let and_op = ArmOp::And {
9151            rd: Reg::R0,
9152            rn: Reg::R1,
9153            op2: Operand2::Reg(Reg::R2),
9154        };
9155        let and_code = encoder.encode(&and_op).unwrap();
9156        assert_eq!(and_code.len(), 4);
9157
9158        let orr_op = ArmOp::Orr {
9159            rd: Reg::R0,
9160            rn: Reg::R1,
9161            op2: Operand2::Reg(Reg::R2),
9162        };
9163        let orr_code = encoder.encode(&orr_op).unwrap();
9164        assert_eq!(orr_code.len(), 4);
9165
9166        let eor_op = ArmOp::Eor {
9167            rd: Reg::R0,
9168            rn: Reg::R1,
9169            op2: Operand2::Reg(Reg::R2),
9170        };
9171        let eor_code = encoder.encode(&eor_op).unwrap();
9172        assert_eq!(eor_code.len(), 4);
9173    }
9174
9175    // === Thumb-2 32-bit encoding tests ===
9176
9177    #[test]
9178    fn test_encode_sdiv_thumb2() {
9179        let encoder = ArmEncoder::new_thumb2();
9180        let op = ArmOp::Sdiv {
9181            rd: Reg::R0,
9182            rn: Reg::R1,
9183            rm: Reg::R2,
9184        };
9185
9186        let code = encoder.encode(&op).unwrap();
9187        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9188
9189        // SDIV R0, R1, R2: 0xFB91 0xF0F2
9190        // First halfword: 0xFB90 | Rn(1) = 0xFB91
9191        // Second halfword: 0xF0F0 | Rd(0)<<8 | Rm(2) = 0xF0F2
9192        // Little-endian: [0x91, 0xFB, 0xF2, 0xF0]
9193        assert_eq!(code[0], 0x91);
9194        assert_eq!(code[1], 0xFB);
9195        assert_eq!(code[2], 0xF2);
9196        assert_eq!(code[3], 0xF0);
9197    }
9198
9199    #[test]
9200    fn test_encode_udiv_thumb2() {
9201        let encoder = ArmEncoder::new_thumb2();
9202        let op = ArmOp::Udiv {
9203            rd: Reg::R0,
9204            rn: Reg::R1,
9205            rm: Reg::R2,
9206        };
9207
9208        let code = encoder.encode(&op).unwrap();
9209        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9210
9211        // UDIV R0, R1, R2: 0xFBB1 0xF0F2
9212        // Little-endian: [0xB1, 0xFB, 0xF2, 0xF0]
9213        assert_eq!(code[0], 0xB1);
9214        assert_eq!(code[1], 0xFB);
9215        assert_eq!(code[2], 0xF2);
9216        assert_eq!(code[3], 0xF0);
9217    }
9218
9219    #[test]
9220    fn test_encode_mul_thumb2() {
9221        let encoder = ArmEncoder::new_thumb2();
9222        let op = ArmOp::Mul {
9223            rd: Reg::R0,
9224            rn: Reg::R1,
9225            rm: Reg::R2,
9226        };
9227
9228        let code = encoder.encode(&op).unwrap();
9229        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9230    }
9231
9232    #[test]
9233    fn test_encode_and_thumb2() {
9234        let encoder = ArmEncoder::new_thumb2();
9235        let op = ArmOp::And {
9236            rd: Reg::R0,
9237            rn: Reg::R1,
9238            op2: Operand2::Reg(Reg::R2),
9239        };
9240
9241        let code = encoder.encode(&op).unwrap();
9242        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9243    }
9244
9245    #[test]
9246    fn test_encode_lsl_thumb2_low_regs() {
9247        let encoder = ArmEncoder::new_thumb2();
9248        let op = ArmOp::Lsl {
9249            rd: Reg::R0,
9250            rn: Reg::R1,
9251            shift: 5,
9252        };
9253
9254        let code = encoder.encode(&op).unwrap();
9255        assert_eq!(code.len(), 2); // 16-bit for low registers
9256    }
9257
9258    #[test]
9259    fn test_encode_clz_thumb2() {
9260        let encoder = ArmEncoder::new_thumb2();
9261        let op = ArmOp::Clz {
9262            rd: Reg::R0,
9263            rm: Reg::R1,
9264        };
9265
9266        let code = encoder.encode(&op).unwrap();
9267        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9268    }
9269
9270    #[test]
9271    fn test_encode_bx_thumb2() {
9272        let encoder = ArmEncoder::new_thumb2();
9273        let op = ArmOp::Bx { rm: Reg::LR };
9274
9275        let code = encoder.encode(&op).unwrap();
9276        assert_eq!(code.len(), 2); // 16-bit instruction
9277
9278        // BX LR: 0x4770
9279        assert_eq!(code, vec![0x70, 0x47]);
9280    }
9281
9282    // ========================================================================
9283    // f32 pseudo-op encoding tests
9284    // ========================================================================
9285
9286    #[test]
9287    fn test_encode_f32_abs_arm32() {
9288        let encoder = ArmEncoder::new_arm32();
9289        let op = ArmOp::F32Abs {
9290            sd: VfpReg::S0,
9291            sm: VfpReg::S2,
9292        };
9293        let code = encoder.encode(&op).unwrap();
9294        assert_eq!(code.len(), 4); // Single VFP instruction
9295    }
9296
9297    #[test]
9298    fn test_encode_f32_neg_arm32() {
9299        let encoder = ArmEncoder::new_arm32();
9300        let op = ArmOp::F32Neg {
9301            sd: VfpReg::S0,
9302            sm: VfpReg::S2,
9303        };
9304        let code = encoder.encode(&op).unwrap();
9305        assert_eq!(code.len(), 4);
9306    }
9307
9308    #[test]
9309    fn test_encode_f32_sqrt_arm32() {
9310        let encoder = ArmEncoder::new_arm32();
9311        let op = ArmOp::F32Sqrt {
9312            sd: VfpReg::S0,
9313            sm: VfpReg::S2,
9314        };
9315        let code = encoder.encode(&op).unwrap();
9316        assert_eq!(code.len(), 4);
9317    }
9318
9319    #[test]
9320    fn test_encode_f32_ceil_arm32() {
9321        let encoder = ArmEncoder::new_arm32();
9322        let op = ArmOp::F32Ceil {
9323            sd: VfpReg::S0,
9324            sm: VfpReg::S2,
9325        };
9326        let code = encoder.encode(&op).unwrap();
9327        // VMRS + BIC + ORR + VMSR + VCVT.S32.F32 + VMRS + BIC + VMSR + VCVT.F32.S32
9328        assert_eq!(code.len(), 36);
9329    }
9330
9331    #[test]
9332    fn test_encode_f32_floor_thumb2() {
9333        let encoder = ArmEncoder::new_thumb2();
9334        let op = ArmOp::F32Floor {
9335            sd: VfpReg::S0,
9336            sm: VfpReg::S2,
9337        };
9338        let code = encoder.encode(&op).unwrap();
9339        // VMRS + BIC.W + ORR.W + VMSR + VCVT + VMRS + BIC.W + VMSR + VCVT.F32.S32
9340        assert_eq!(code.len(), 36);
9341    }
9342
9343    #[test]
9344    fn test_encode_f32_min_arm32() {
9345        let encoder = ArmEncoder::new_arm32();
9346        let op = ArmOp::F32Min {
9347            sd: VfpReg::S0,
9348            sn: VfpReg::S2,
9349            sm: VfpReg::S4,
9350        };
9351        let code = encoder.encode(&op).unwrap();
9352        assert_eq!(code.len(), 16); // VMOV + VCMP + VMRS + conditional VMOV
9353    }
9354
9355    #[test]
9356    fn test_encode_f32_max_thumb2() {
9357        let encoder = ArmEncoder::new_thumb2();
9358        let op = ArmOp::F32Max {
9359            sd: VfpReg::S0,
9360            sn: VfpReg::S2,
9361            sm: VfpReg::S4,
9362        };
9363        let code = encoder.encode(&op).unwrap();
9364        // VMOV(4) + VCMP(4) + VMRS(4) + IT(2) + VMOV(4) = 18
9365        assert_eq!(code.len(), 18);
9366    }
9367
9368    #[test]
9369    fn test_encode_f32_copysign_arm32() {
9370        let encoder = ArmEncoder::new_arm32();
9371        let op = ArmOp::F32Copysign {
9372            sd: VfpReg::S0,
9373            sn: VfpReg::S2,
9374            sm: VfpReg::S4,
9375        };
9376        let code = encoder.encode(&op).unwrap();
9377        // VMOV + VMOV + AND + BIC + ORR + VMOV = 6 * 4 = 24
9378        assert_eq!(code.len(), 24);
9379    }
9380
9381    // ========================================================================
9382    // f64 encoding tests
9383    // ========================================================================
9384
9385    #[test]
9386    fn test_encode_f64_add_arm32() {
9387        let encoder = ArmEncoder::new_arm32();
9388        let op = ArmOp::F64Add {
9389            dd: VfpReg::D0,
9390            dn: VfpReg::D1,
9391            dm: VfpReg::D2,
9392        };
9393        let code = encoder.encode(&op).unwrap();
9394        assert_eq!(code.len(), 4);
9395        // VADD.F64 D0, D1, D2: check coprocessor is cp11 (0xB)
9396        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9397        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
9398    }
9399
9400    #[test]
9401    fn test_encode_f64_sub_thumb2() {
9402        let encoder = ArmEncoder::new_thumb2();
9403        let op = ArmOp::F64Sub {
9404            dd: VfpReg::D0,
9405            dn: VfpReg::D1,
9406            dm: VfpReg::D2,
9407        };
9408        let code = encoder.encode(&op).unwrap();
9409        assert_eq!(code.len(), 4); // 32-bit VFP as two Thumb halfwords
9410    }
9411
9412    #[test]
9413    fn test_encode_f64_mul_arm32() {
9414        let encoder = ArmEncoder::new_arm32();
9415        let op = ArmOp::F64Mul {
9416            dd: VfpReg::D0,
9417            dn: VfpReg::D1,
9418            dm: VfpReg::D2,
9419        };
9420        let code = encoder.encode(&op).unwrap();
9421        assert_eq!(code.len(), 4);
9422    }
9423
9424    #[test]
9425    fn test_encode_f64_div_arm32() {
9426        let encoder = ArmEncoder::new_arm32();
9427        let op = ArmOp::F64Div {
9428            dd: VfpReg::D0,
9429            dn: VfpReg::D1,
9430            dm: VfpReg::D2,
9431        };
9432        let code = encoder.encode(&op).unwrap();
9433        assert_eq!(code.len(), 4);
9434    }
9435
9436    #[test]
9437    fn test_encode_f64_abs_arm32() {
9438        let encoder = ArmEncoder::new_arm32();
9439        let op = ArmOp::F64Abs {
9440            dd: VfpReg::D0,
9441            dm: VfpReg::D2,
9442        };
9443        let code = encoder.encode(&op).unwrap();
9444        assert_eq!(code.len(), 4);
9445    }
9446
9447    #[test]
9448    fn test_encode_f64_neg_arm32() {
9449        let encoder = ArmEncoder::new_arm32();
9450        let op = ArmOp::F64Neg {
9451            dd: VfpReg::D0,
9452            dm: VfpReg::D2,
9453        };
9454        let code = encoder.encode(&op).unwrap();
9455        assert_eq!(code.len(), 4);
9456    }
9457
9458    #[test]
9459    fn test_encode_f64_sqrt_arm32() {
9460        let encoder = ArmEncoder::new_arm32();
9461        let op = ArmOp::F64Sqrt {
9462            dd: VfpReg::D0,
9463            dm: VfpReg::D2,
9464        };
9465        let code = encoder.encode(&op).unwrap();
9466        assert_eq!(code.len(), 4);
9467    }
9468
9469    #[test]
9470    fn test_encode_f64_load_arm32() {
9471        let encoder = ArmEncoder::new_arm32();
9472        let op = ArmOp::F64Load {
9473            dd: VfpReg::D0,
9474            addr: MemAddr::imm(Reg::R0, 8),
9475        };
9476        let code = encoder.encode(&op).unwrap();
9477        assert_eq!(code.len(), 4);
9478        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9479        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11 for F64
9480        assert_eq!(instr & 0xFF, 2); // offset 8 / 4 = 2
9481    }
9482
9483    #[test]
9484    fn test_encode_f64_store_thumb2() {
9485        let encoder = ArmEncoder::new_thumb2();
9486        let op = ArmOp::F64Store {
9487            dd: VfpReg::D0,
9488            addr: MemAddr::imm(Reg::SP, 0),
9489        };
9490        let code = encoder.encode(&op).unwrap();
9491        assert_eq!(code.len(), 4);
9492    }
9493
9494    #[test]
9495    fn test_encode_f64_compare_arm32() {
9496        let encoder = ArmEncoder::new_arm32();
9497        let op = ArmOp::F64Eq {
9498            rd: Reg::R0,
9499            dn: VfpReg::D0,
9500            dm: VfpReg::D1,
9501        };
9502        let code = encoder.encode(&op).unwrap();
9503        assert_eq!(code.len(), 16); // VCMP + VMRS + MOV #0 + MOVcond #1
9504    }
9505
9506    #[test]
9507    fn test_encode_f64_compare_thumb2() {
9508        let encoder = ArmEncoder::new_thumb2();
9509        let op = ArmOp::F64Lt {
9510            rd: Reg::R0,
9511            dn: VfpReg::D0,
9512            dm: VfpReg::D1,
9513        };
9514        let code = encoder.encode(&op).unwrap();
9515        // VCMP(4) + VMRS(4) + MOVS(2) + IT(2) + MOV(2) = 14
9516        assert_eq!(code.len(), 14);
9517    }
9518
9519    #[test]
9520    fn test_encode_f64_const_arm32() {
9521        let encoder = ArmEncoder::new_arm32();
9522        let op = ArmOp::F64Const {
9523            dd: VfpReg::D0,
9524            value: 3.125,
9525        };
9526        let code = encoder.encode(&op).unwrap();
9527        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
9528        assert_eq!(code.len(), 20);
9529    }
9530
9531    #[test]
9532    fn test_encode_f64_const_thumb2() {
9533        let encoder = ArmEncoder::new_thumb2();
9534        let op = ArmOp::F64Const {
9535            dd: VfpReg::D0,
9536            value: 2.5,
9537        };
9538        let code = encoder.encode(&op).unwrap();
9539        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
9540        assert_eq!(code.len(), 20);
9541    }
9542
9543    #[test]
9544    fn test_encode_f64_convert_i32s_arm32() {
9545        let encoder = ArmEncoder::new_arm32();
9546        let op = ArmOp::F64ConvertI32S {
9547            dd: VfpReg::D0,
9548            rm: Reg::R0,
9549        };
9550        let code = encoder.encode(&op).unwrap();
9551        // VMOV(4) + VCVT(4) = 8
9552        assert_eq!(code.len(), 8);
9553    }
9554
9555    #[test]
9556    fn test_encode_f64_promote_f32_arm32() {
9557        let encoder = ArmEncoder::new_arm32();
9558        let op = ArmOp::F64PromoteF32 {
9559            dd: VfpReg::D0,
9560            sm: VfpReg::S0,
9561        };
9562        let code = encoder.encode(&op).unwrap();
9563        assert_eq!(code.len(), 4); // Single VCVT.F64.F32 instruction
9564    }
9565
9566    #[test]
9567    fn test_encode_f64_promote_f32_thumb2() {
9568        let encoder = ArmEncoder::new_thumb2();
9569        let op = ArmOp::F64PromoteF32 {
9570            dd: VfpReg::D0,
9571            sm: VfpReg::S0,
9572        };
9573        let code = encoder.encode(&op).unwrap();
9574        assert_eq!(code.len(), 4);
9575    }
9576
9577    #[test]
9578    fn test_encode_i32_trunc_f64s_arm32() {
9579        let encoder = ArmEncoder::new_arm32();
9580        let op = ArmOp::I32TruncF64S {
9581            rd: Reg::R0,
9582            dm: VfpReg::D0,
9583        };
9584        let code = encoder.encode(&op).unwrap();
9585        // VCVT(4) + VMOV(4) = 8
9586        assert_eq!(code.len(), 8);
9587    }
9588
9589    #[test]
9590    fn test_encode_f64_reinterpret_i64_arm32() {
9591        let encoder = ArmEncoder::new_arm32();
9592        let op = ArmOp::F64ReinterpretI64 {
9593            dd: VfpReg::D0,
9594            rmlo: Reg::R0,
9595            rmhi: Reg::R1,
9596        };
9597        let code = encoder.encode(&op).unwrap();
9598        assert_eq!(code.len(), 4); // Single VMOV instruction
9599    }
9600
9601    #[test]
9602    fn test_encode_i64_reinterpret_f64_thumb2() {
9603        let encoder = ArmEncoder::new_thumb2();
9604        let op = ArmOp::I64ReinterpretF64 {
9605            rdlo: Reg::R0,
9606            rdhi: Reg::R1,
9607            dm: VfpReg::D0,
9608        };
9609        let code = encoder.encode(&op).unwrap();
9610        assert_eq!(code.len(), 4);
9611    }
9612
9613    #[test]
9614    fn test_encode_f64_trunc_thumb2() {
9615        let encoder = ArmEncoder::new_thumb2();
9616        let op = ArmOp::F64Trunc {
9617            dd: VfpReg::D0,
9618            dm: VfpReg::D1,
9619        };
9620        let code = encoder.encode(&op).unwrap();
9621        // Two VFP instructions via Thumb encoding
9622        assert_eq!(code.len(), 8);
9623    }
9624
9625    #[test]
9626    fn test_encode_f64_min_arm32() {
9627        let encoder = ArmEncoder::new_arm32();
9628        let op = ArmOp::F64Min {
9629            dd: VfpReg::D0,
9630            dn: VfpReg::D1,
9631            dm: VfpReg::D2,
9632        };
9633        let code = encoder.encode(&op).unwrap();
9634        // VMOV + VCMP + VMRS + conditional VMOV = 16
9635        assert_eq!(code.len(), 16);
9636    }
9637
9638    #[test]
9639    fn test_f64_cp11_encoding() {
9640        // Verify that F64 instructions use coprocessor 11 (0xB), not 10 (0xA)
9641        let encoder = ArmEncoder::new_arm32();
9642
9643        // F64Add
9644        let code = encoder
9645            .encode(&ArmOp::F64Add {
9646                dd: VfpReg::D0,
9647                dn: VfpReg::D0,
9648                dm: VfpReg::D0,
9649            })
9650            .unwrap();
9651        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9652        assert_eq!((instr >> 8) & 0xF, 0xB, "F64 should use cp11");
9653
9654        // F32Add for comparison
9655        let code = encoder
9656            .encode(&ArmOp::F32Add {
9657                sd: VfpReg::S0,
9658                sn: VfpReg::S0,
9659                sm: VfpReg::S0,
9660            })
9661            .unwrap();
9662        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9663        assert_eq!((instr >> 8) & 0xF, 0xA, "F32 should use cp10");
9664    }
9665
9666    #[test]
9667    fn test_dreg_encoding_higher_registers() {
9668        let encoder = ArmEncoder::new_arm32();
9669
9670        // Test with D15 (highest register)
9671        let op = ArmOp::F64Add {
9672            dd: VfpReg::D15,
9673            dn: VfpReg::D14,
9674            dm: VfpReg::D13,
9675        };
9676        let code = encoder.encode(&op).unwrap();
9677        assert_eq!(code.len(), 4);
9678
9679        // Verify the register encoding worked (instruction is valid)
9680        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9681        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
9682    }
9683
9684    // ========================================================================
9685    // Control flow encoding tests
9686    // ========================================================================
9687
9688    #[test]
9689    fn test_encode_label_emits_no_bytes() {
9690        let encoder = ArmEncoder::new_thumb2();
9691        let op = ArmOp::Label {
9692            name: ".Lblock_end_0".to_string(),
9693        };
9694        let code = encoder.encode(&op).unwrap();
9695        assert!(code.is_empty(), "Label should emit zero bytes");
9696
9697        let encoder32 = ArmEncoder::new_arm32();
9698        let code32 = encoder32.encode(&op).unwrap();
9699        assert!(
9700            code32.is_empty(),
9701            "Label should emit zero bytes in ARM32 too"
9702        );
9703    }
9704
9705    #[test]
9706    fn test_encode_bcc_eq_thumb2() {
9707        use synth_synthesis::Condition;
9708        let encoder = ArmEncoder::new_thumb2();
9709        let op = ArmOp::Bcc {
9710            cond: Condition::EQ,
9711            label: "target".to_string(),
9712        };
9713        let code = encoder.encode(&op).unwrap();
9714        assert_eq!(code.len(), 2); // 16-bit conditional branch
9715
9716        // BEQ with offset 0: 0xD000 in little-endian
9717        assert_eq!(code, vec![0x00, 0xD0]);
9718    }
9719
9720    #[test]
9721    fn test_encode_bcc_ne_thumb2() {
9722        use synth_synthesis::Condition;
9723        let encoder = ArmEncoder::new_thumb2();
9724        let op = ArmOp::Bcc {
9725            cond: Condition::NE,
9726            label: "target".to_string(),
9727        };
9728        let code = encoder.encode(&op).unwrap();
9729        assert_eq!(code.len(), 2);
9730
9731        // BNE with offset 0: 0xD100 in little-endian
9732        assert_eq!(code, vec![0x00, 0xD1]);
9733    }
9734
9735    #[test]
9736    fn test_encode_bcc_arm32() {
9737        use synth_synthesis::Condition;
9738        let encoder = ArmEncoder::new_arm32();
9739        let op = ArmOp::Bcc {
9740            cond: Condition::EQ,
9741            label: "target".to_string(),
9742        };
9743        let code = encoder.encode(&op).unwrap();
9744        assert_eq!(code.len(), 4); // 32-bit ARM instruction
9745
9746        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9747        // BEQ: cond=0x0, opcode=0xA, offset=0
9748        assert_eq!(instr & 0xF0000000, 0x00000000); // EQ condition
9749        assert_eq!(instr & 0x0F000000, 0x0A000000); // Branch opcode
9750    }
9751
9752    #[test]
9753    fn test_encode_udf_thumb2() {
9754        let encoder = ArmEncoder::new_thumb2();
9755        let op = ArmOp::Udf { imm: 0 };
9756        let code = encoder.encode(&op).unwrap();
9757        assert_eq!(code.len(), 2); // 16-bit
9758
9759        // UDF #0: 0xDE00 in little-endian
9760        assert_eq!(code, vec![0x00, 0xDE]);
9761    }
9762
9763    /// #610: the i64 rot/div/rem expansions must land the result in the
9764    /// selector-assigned rd pair and leave R0-R3 preserved (restored from the
9765    /// fixed-ABI wrapper's save area) — pre-#610 the rot expansion's own
9766    /// `POP {R4}` restored stale scratch OVER the result (rd_lo == R4) and
9767    /// the div/rem expansions ignored their register fields outright.
9768    #[test]
9769    fn test_610_i64_rot_expansion_ends_with_rd_movs_and_restore() {
9770        let encoder = ArmEncoder::new_thumb2();
9771        for op in [
9772            ArmOp::I64Rotl {
9773                rdlo: Reg::R4,
9774                rdhi: Reg::R5,
9775                rnlo: Reg::R0,
9776                rnhi: Reg::R1,
9777                shift: Reg::R2,
9778            },
9779            ArmOp::I64Rotr {
9780                rdlo: Reg::R4,
9781                rdhi: Reg::R5,
9782                rnlo: Reg::R0,
9783                rnhi: Reg::R1,
9784                shift: Reg::R2,
9785            },
9786        ] {
9787            let code = encoder.encode(&op).unwrap();
9788            assert_eq!(code.len(), 102, "register-independent size (estimator pin)");
9789            // Tail: MOV r5, r1 (0x460D); MOV r4, r0 (0x4604); POP {r0..r3}
9790            // (rd pair r4:r5 does not overlap the save area — all 4 restored).
9791            let tail: Vec<u16> = code[code.len() - 12..]
9792                .chunks(2)
9793                .map(|c| u16::from_le_bytes([c[0], c[1]]))
9794                .collect();
9795            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
9796        }
9797    }
9798
9799    /// #610: div/rem expansions honor rd and carry the divide-by-zero trap
9800    /// guard (`ORRS R12, R2, R3; BNE +0; UDF #0`) after operand marshaling.
9801    #[test]
9802    fn test_610_i64_div_rem_expansion_guard_and_rd() {
9803        let encoder = ArmEncoder::new_thumb2();
9804        let mk = |which: u8| {
9805            let (rdlo, rdhi, rnlo, rnhi, rmlo, rmhi) =
9806                (Reg::R4, Reg::R5, Reg::R0, Reg::R1, Reg::R2, Reg::R3);
9807            match which {
9808                0 => ArmOp::I64DivU {
9809                    rdlo,
9810                    rdhi,
9811                    rnlo,
9812                    rnhi,
9813                    rmlo,
9814                    rmhi,
9815                },
9816                1 => ArmOp::I64RemU {
9817                    rdlo,
9818                    rdhi,
9819                    rnlo,
9820                    rnhi,
9821                    rmlo,
9822                    rmhi,
9823                },
9824                2 => ArmOp::I64DivS {
9825                    rdlo,
9826                    rdhi,
9827                    rnlo,
9828                    rnhi,
9829                    rmlo,
9830                    rmhi,
9831                },
9832                _ => ArmOp::I64RemS {
9833                    rdlo,
9834                    rdhi,
9835                    rnlo,
9836                    rnhi,
9837                    rmlo,
9838                    rmhi,
9839                },
9840            }
9841        };
9842        for which in 0..4u8 {
9843            let code = encoder.encode(&mk(which)).unwrap();
9844            // Zero-divisor trap guard right after the 26-byte marshal prologue.
9845            let guard: Vec<u16> = code[26..34]
9846                .chunks(2)
9847                .map(|c| u16::from_le_bytes([c[0], c[1]]))
9848                .collect();
9849            assert_eq!(
9850                guard,
9851                vec![0xEA52, 0x0C03, 0xD100, 0xDE00],
9852                "ORRS R12,R2,R3; BNE +0; UDF #0"
9853            );
9854            // Tail: result into rd pair (r5:r4), then restore all of R0-R3.
9855            let tail: Vec<u16> = code[code.len() - 12..]
9856                .chunks(2)
9857                .map(|c| u16::from_le_bytes([c[0], c[1]]))
9858                .collect();
9859            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
9860        }
9861    }
9862
9863    /// #610: when rd overlaps R0-R3 the restore must SKIP the result
9864    /// registers (drop the saved caller word) instead of popping over them.
9865    #[test]
9866    fn test_610_i64_divu_rd_in_r0_r1_skips_restore() {
9867        let encoder = ArmEncoder::new_thumb2();
9868        let code = encoder
9869            .encode(&ArmOp::I64DivU {
9870                rdlo: Reg::R0,
9871                rdhi: Reg::R1,
9872                rnlo: Reg::R0,
9873                rnhi: Reg::R1,
9874                rmlo: Reg::R2,
9875                rmhi: Reg::R3,
9876            })
9877            .unwrap();
9878        let tail: Vec<u16> = code[code.len() - 12..]
9879            .chunks(2)
9880            .map(|c| u16::from_le_bytes([c[0], c[1]]))
9881            .collect();
9882        // MOV r1,r1 / MOV r0,r0 (no-ops, size-stable), ADD SP,#4 twice
9883        // (discard saved r0/r1 — the result lives there), POP {r2}, POP {r3}.
9884        assert_eq!(tail, vec![0x4609, 0x4600, 0xB001, 0xB001, 0xBC04, 0xBC08]);
9885    }
9886
9887    /// #610: a fully swapped rd pair (rd_lo=R1, rd_hi=R0) cannot be
9888    /// materialized by two MOVs in either order — must be a loud Err, never
9889    /// silent corruption. (Selector pairs are consecutive, so unreachable.)
9890    #[test]
9891    fn test_610_i64_swapped_rd_pair_rejected() {
9892        let encoder = ArmEncoder::new_thumb2();
9893        let result = encoder.encode(&ArmOp::I64RemU {
9894            rdlo: Reg::R1,
9895            rdhi: Reg::R0,
9896            rnlo: Reg::R2,
9897            rnhi: Reg::R3,
9898            rmlo: Reg::R4,
9899            rmhi: Reg::R5,
9900        });
9901        assert!(result.is_err(), "swapped rd pair must be rejected loudly");
9902    }
9903
9904    #[test]
9905    fn test_encode_nop_thumb2() {
9906        let encoder = ArmEncoder::new_thumb2();
9907        let op = ArmOp::Nop;
9908        let code = encoder.encode(&op).unwrap();
9909        assert_eq!(code.len(), 2); // 16-bit
9910
9911        // NOP: 0xBF00 in little-endian
9912        assert_eq!(code, vec![0x00, 0xBF]);
9913    }
9914
9915    // =========================================================================
9916    // i64 Thumb-2 encoding tests
9917    // =========================================================================
9918
9919    #[test]
9920    fn test_encode_i64_add_thumb2() {
9921        let encoder = ArmEncoder::new_thumb2();
9922        let op = ArmOp::I64Add {
9923            rdlo: Reg::R0,
9924            rdhi: Reg::R1,
9925            rnlo: Reg::R0,
9926            rnhi: Reg::R1,
9927            rmlo: Reg::R2,
9928            rmhi: Reg::R3,
9929        };
9930        let code = encoder.encode(&op).unwrap();
9931        // Should emit ADDS (2 bytes) + ADC.W (4 bytes) = 6 bytes
9932        assert_eq!(code.len(), 6, "I64Add should be 6 bytes (ADDS + ADC.W)");
9933    }
9934
9935    #[test]
9936    fn test_encode_i64_sub_thumb2() {
9937        let encoder = ArmEncoder::new_thumb2();
9938        let op = ArmOp::I64Sub {
9939            rdlo: Reg::R0,
9940            rdhi: Reg::R1,
9941            rnlo: Reg::R0,
9942            rnhi: Reg::R1,
9943            rmlo: Reg::R2,
9944            rmhi: Reg::R3,
9945        };
9946        let code = encoder.encode(&op).unwrap();
9947        // Should emit SUBS (2 bytes) + SBC.W (4 bytes) = 6 bytes
9948        assert_eq!(code.len(), 6, "I64Sub should be 6 bytes (SUBS + SBC.W)");
9949    }
9950
9951    #[test]
9952    fn test_encode_i64_and_thumb2() {
9953        let encoder = ArmEncoder::new_thumb2();
9954        let op = ArmOp::I64And {
9955            rdlo: Reg::R0,
9956            rdhi: Reg::R1,
9957            rnlo: Reg::R0,
9958            rnhi: Reg::R1,
9959            rmlo: Reg::R2,
9960            rmhi: Reg::R3,
9961        };
9962        let code = encoder.encode(&op).unwrap();
9963        // AND.W (4 bytes) + AND.W (4 bytes) = 8 bytes
9964        assert!(code.len() >= 4, "I64And should emit at least 4 bytes");
9965    }
9966
9967    #[test]
9968    fn test_encode_i64_or_thumb2() {
9969        let encoder = ArmEncoder::new_thumb2();
9970        let op = ArmOp::I64Or {
9971            rdlo: Reg::R0,
9972            rdhi: Reg::R1,
9973            rnlo: Reg::R0,
9974            rnhi: Reg::R1,
9975            rmlo: Reg::R2,
9976            rmhi: Reg::R3,
9977        };
9978        let code = encoder.encode(&op).unwrap();
9979        assert!(code.len() >= 4, "I64Or should emit at least 4 bytes");
9980    }
9981
9982    #[test]
9983    fn test_encode_i64_xor_thumb2() {
9984        let encoder = ArmEncoder::new_thumb2();
9985        let op = ArmOp::I64Xor {
9986            rdlo: Reg::R0,
9987            rdhi: Reg::R1,
9988            rnlo: Reg::R0,
9989            rnhi: Reg::R1,
9990            rmlo: Reg::R2,
9991            rmhi: Reg::R3,
9992        };
9993        let code = encoder.encode(&op).unwrap();
9994        assert!(code.len() >= 4, "I64Xor should emit at least 4 bytes");
9995    }
9996
9997    #[test]
9998    fn test_encode_i64_const_small_thumb2() {
9999        let encoder = ArmEncoder::new_thumb2();
10000        // Small constant: only needs MOVW for each half
10001        let op = ArmOp::I64Const {
10002            rdlo: Reg::R0,
10003            rdhi: Reg::R1,
10004            value: 42,
10005        };
10006        let code = encoder.encode(&op).unwrap();
10007        // MOVW R0, #42 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes minimum
10008        assert!(code.len() >= 8, "I64Const should emit at least 8 bytes");
10009    }
10010
10011    #[test]
10012    fn test_encode_i64_const_large_thumb2() {
10013        let encoder = ArmEncoder::new_thumb2();
10014        // Large constant: needs MOVW+MOVT for each half
10015        let op = ArmOp::I64Const {
10016            rdlo: Reg::R0,
10017            rdhi: Reg::R1,
10018            value: 0x1234_5678_9ABC_DEF0_u64 as i64,
10019        };
10020        let code = encoder.encode(&op).unwrap();
10021        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
10022        assert_eq!(
10023            code.len(),
10024            16,
10025            "I64Const with large value should be 16 bytes"
10026        );
10027    }
10028
10029    #[test]
10030    fn test_encode_i64_extend_i32_s_thumb2() {
10031        let encoder = ArmEncoder::new_thumb2();
10032        let op = ArmOp::I64ExtendI32S {
10033            rdlo: Reg::R0,
10034            rdhi: Reg::R1,
10035            rn: Reg::R0,
10036        };
10037        let code = encoder.encode(&op).unwrap();
10038        // When rdlo == rn, only ASR (4 bytes) is emitted
10039        assert_eq!(
10040            code.len(),
10041            4,
10042            "I64ExtendI32S (same reg) should be 4 bytes (ASR only)"
10043        );
10044    }
10045
10046    #[test]
10047    fn test_encode_i64_extend_i32_s_diff_reg_thumb2() {
10048        let encoder = ArmEncoder::new_thumb2();
10049        let op = ArmOp::I64ExtendI32S {
10050            rdlo: Reg::R0,
10051            rdhi: Reg::R1,
10052            rn: Reg::R2,
10053        };
10054        let code = encoder.encode(&op).unwrap();
10055        // MOV rdlo, rn (2 bytes for low regs) + ASR rdhi, rdlo, #31 (4 bytes) = 6 bytes
10056        assert!(
10057            code.len() >= 6,
10058            "I64ExtendI32S (diff reg) should be at least 6 bytes"
10059        );
10060    }
10061
10062    #[test]
10063    fn test_encode_i64_extend_i32_u_thumb2() {
10064        let encoder = ArmEncoder::new_thumb2();
10065        let op = ArmOp::I64ExtendI32U {
10066            rdlo: Reg::R0,
10067            rdhi: Reg::R1,
10068            rn: Reg::R0,
10069        };
10070        let code = encoder.encode(&op).unwrap();
10071        // When rdlo == rn, only MOV rdhi, #0 (2 bytes) is emitted
10072        assert_eq!(
10073            code.len(),
10074            2,
10075            "I64ExtendI32U (same reg) should be 2 bytes (MOV #0 only)"
10076        );
10077    }
10078
10079    #[test]
10080    fn test_encode_i32_wrap_i64_nop_thumb2() {
10081        let encoder = ArmEncoder::new_thumb2();
10082        // When rd == rnlo, should be a NOP
10083        let op = ArmOp::I32WrapI64 {
10084            rd: Reg::R0,
10085            rnlo: Reg::R0,
10086        };
10087        let code = encoder.encode(&op).unwrap();
10088        assert_eq!(code.len(), 2, "I32WrapI64 same reg should be NOP (2 bytes)");
10089        assert_eq!(code, vec![0x00, 0xBF]); // NOP
10090    }
10091
10092    #[test]
10093    fn test_encode_i32_wrap_i64_diff_reg_thumb2() {
10094        let encoder = ArmEncoder::new_thumb2();
10095        let op = ArmOp::I32WrapI64 {
10096            rd: Reg::R2,
10097            rnlo: Reg::R0,
10098        };
10099        let code = encoder.encode(&op).unwrap();
10100        // MOV R2, R0 (2 or 4 bytes)
10101        assert!(
10102            code.len() >= 2,
10103            "I32WrapI64 diff reg should emit at least 2 bytes"
10104        );
10105    }
10106
10107    #[test]
10108    fn test_encode_i64_eqz_thumb2() {
10109        let encoder = ArmEncoder::new_thumb2();
10110        let op = ArmOp::I64Eqz {
10111            rd: Reg::R0,
10112            rnlo: Reg::R0,
10113            rnhi: Reg::R1,
10114        };
10115        let code = encoder.encode(&op).unwrap();
10116        // Delegates to I64SetCondZ which is already encoded
10117        assert!(
10118            code.len() >= 6,
10119            "I64Eqz should emit at least 6 bytes for ORR+ITE+MOV+MOV"
10120        );
10121    }
10122
10123    #[test]
10124    fn test_encode_i64_eq_thumb2() {
10125        let encoder = ArmEncoder::new_thumb2();
10126        let op = ArmOp::I64Eq {
10127            rd: Reg::R0,
10128            rnlo: Reg::R0,
10129            rnhi: Reg::R1,
10130            rmlo: Reg::R2,
10131            rmhi: Reg::R3,
10132        };
10133        let code = encoder.encode(&op).unwrap();
10134        // Delegates to I64SetCond EQ: CMP lo + IT EQ + CMPEQ hi + ITE EQ + MOV 1 + MOV 0
10135        assert!(code.len() >= 10, "I64Eq should emit at least 10 bytes");
10136    }
10137
10138    #[test]
10139    fn test_encode_i64_ldr_thumb2() {
10140        let encoder = ArmEncoder::new_thumb2();
10141        let op = ArmOp::I64Ldr {
10142            rdlo: Reg::R0,
10143            rdhi: Reg::R1,
10144            addr: MemAddr::imm(Reg::SP, 0),
10145        };
10146        let code = encoder.encode(&op).unwrap();
10147        // Two LDR instructions (lo at offset, hi at offset+4)
10148        assert!(code.len() >= 4, "I64Ldr should emit at least 4 bytes");
10149    }
10150
10151    #[test]
10152    fn test_372_i64_ldr_indexed_materializes_address() {
10153        // #372: a memory i64.load carries an index register (R11 + addr + off).
10154        // The encoder must materialize `ip = base + index` (ADD.W) and load via
10155        // `[ip,#off]` — NOT drop the index. A frame (non-indexed) i64.load must
10156        // stay byte-identical (plain `[base,#off]`, no ADD).
10157        let encoder = ArmEncoder::new_thumb2();
10158        let indexed = encoder
10159            .encode(&ArmOp::I64Ldr {
10160                rdlo: Reg::R0,
10161                rdhi: Reg::R1,
10162                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
10163            })
10164            .unwrap();
10165        // ADD.W ip, fp, r0 = eb0b 0c00 (byte-verified vs arm-none-eabi-as).
10166        assert_eq!(
10167            &indexed[0..4],
10168            &[0x0b, 0xeb, 0x00, 0x0c],
10169            "indexed I64Ldr must start with ADD.W ip, base, index"
10170        );
10171        let frame = encoder
10172            .encode(&ArmOp::I64Ldr {
10173                rdlo: Reg::R0,
10174                rdhi: Reg::R1,
10175                addr: MemAddr::imm(Reg::SP, 8),
10176            })
10177            .unwrap();
10178        // No index -> no ADD.W prefix (byte-identical frame access).
10179        assert_ne!(
10180            &frame[0..2],
10181            &[0x0b, 0xeb],
10182            "frame (non-indexed) I64Ldr must NOT emit an ADD.W"
10183        );
10184    }
10185
10186    #[test]
10187    fn test_382_i64_ldst_large_offset_materializes_not_skips() {
10188        // #382: an indexed i64.load/store whose static offset > 0xFFF must
10189        // MATERIALIZE the offset into the base — NOT return Err (skip the fn).
10190        // Sequence for reg_imm(R11, R0, 5000): MOVW ip,#5000 ; ADD ip,r0,ip ;
10191        // ADD ip,ip,fp ; LDR/STR halves at [ip,#0] / [ip,#4]. Byte-verified tail
10192        // vs arm-none-eabi-as.
10193        let encoder = ArmEncoder::new_thumb2();
10194        // 0x1388 > 0xFFF (MemAddr is not Copy, so build it per use).
10195
10196        let ld = encoder
10197            .encode(&ArmOp::I64Ldr {
10198                rdlo: Reg::R0,
10199                rdhi: Reg::R1,
10200                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
10201            })
10202            .expect("large-offset i64.load must lower, not skip");
10203        // MOVW ip,#0x1388 (4) + ADD ip,r0,ip (4) + ADD ip,ip,fp (4) + 2 LDR (8).
10204        assert_eq!(ld.len(), 20, "expected MOVW + 2×ADD + 2×LDR");
10205        // Must NOT be the small-offset `ADD.W ip, fp, r0` (0x0b 0xeb) prefix —
10206        // that path can only reach imm12 offsets.
10207        assert_ne!(
10208            &ld[0..2],
10209            &[0x0b, 0xeb],
10210            "must materialize the large offset"
10211        );
10212        // Effective base built in ip, then halves at [ip,#0] / [ip,#4].
10213        assert_eq!(
10214            &ld[4..20],
10215            &[
10216                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
10217                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
10218                0xdc, 0xf8, 0x00, 0x00, // LDR.W r0, [ip, #0]
10219                0xdc, 0xf8, 0x04, 0x10, // LDR.W r1, [ip, #4]
10220            ],
10221            "large-offset i64.load must fold offset into ip and access [ip,#0]/[ip,#4]"
10222        );
10223
10224        // Store: same base materialization, STR halves.
10225        let st = encoder
10226            .encode(&ArmOp::I64Str {
10227                rdlo: Reg::R2,
10228                rdhi: Reg::R3,
10229                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
10230            })
10231            .expect("large-offset i64.store must lower, not skip");
10232        assert_eq!(st.len(), 20);
10233        assert_eq!(
10234            &st[4..20],
10235            &[
10236                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
10237                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
10238                0xcc, 0xf8, 0x00, 0x20, // STR.W r2, [ip, #0]
10239                0xcc, 0xf8, 0x04, 0x30, // STR.W r3, [ip, #4]
10240            ],
10241            "large-offset i64.store must fold offset into ip and access [ip,#0]/[ip,#4]"
10242        );
10243
10244        // Small-offset (imm12) indexed access stays byte-identical (#372): the
10245        // effective base is a single `ADD.W ip, fp, r0` and the halves keep the
10246        // folded immediates — NO extra MOVW/ADD.
10247        let small = encoder
10248            .encode(&ArmOp::I64Ldr {
10249                rdlo: Reg::R0,
10250                rdhi: Reg::R1,
10251                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 8),
10252            })
10253            .unwrap();
10254        assert_eq!(
10255            &small[0..4],
10256            &[0x0b, 0xeb, 0x00, 0x0c],
10257            "small-offset indexed i64 must keep the single ADD.W ip, fp, r0"
10258        );
10259        assert_eq!(small.len(), 12, "ADD.W + 2×LDR.W (offset folded in imm12)");
10260    }
10261
10262    #[test]
10263    fn test_encode_i64_str_thumb2() {
10264        let encoder = ArmEncoder::new_thumb2();
10265        let op = ArmOp::I64Str {
10266            rdlo: Reg::R0,
10267            rdhi: Reg::R1,
10268            addr: MemAddr::imm(Reg::SP, 0),
10269        };
10270        let code = encoder.encode(&op).unwrap();
10271        // Two STR instructions (lo at offset, hi at offset+4)
10272        assert!(code.len() >= 4, "I64Str should emit at least 4 bytes");
10273    }
10274
10275    #[test]
10276    fn test_encode_i64_all_comparisons_thumb2() {
10277        let encoder = ArmEncoder::new_thumb2();
10278
10279        let ops = vec![
10280            ArmOp::I64Ne {
10281                rd: Reg::R0,
10282                rnlo: Reg::R0,
10283                rnhi: Reg::R1,
10284                rmlo: Reg::R2,
10285                rmhi: Reg::R3,
10286            },
10287            ArmOp::I64LtS {
10288                rd: Reg::R0,
10289                rnlo: Reg::R0,
10290                rnhi: Reg::R1,
10291                rmlo: Reg::R2,
10292                rmhi: Reg::R3,
10293            },
10294            ArmOp::I64LtU {
10295                rd: Reg::R0,
10296                rnlo: Reg::R0,
10297                rnhi: Reg::R1,
10298                rmlo: Reg::R2,
10299                rmhi: Reg::R3,
10300            },
10301            ArmOp::I64LeS {
10302                rd: Reg::R0,
10303                rnlo: Reg::R0,
10304                rnhi: Reg::R1,
10305                rmlo: Reg::R2,
10306                rmhi: Reg::R3,
10307            },
10308            ArmOp::I64LeU {
10309                rd: Reg::R0,
10310                rnlo: Reg::R0,
10311                rnhi: Reg::R1,
10312                rmlo: Reg::R2,
10313                rmhi: Reg::R3,
10314            },
10315            ArmOp::I64GtS {
10316                rd: Reg::R0,
10317                rnlo: Reg::R0,
10318                rnhi: Reg::R1,
10319                rmlo: Reg::R2,
10320                rmhi: Reg::R3,
10321            },
10322            ArmOp::I64GtU {
10323                rd: Reg::R0,
10324                rnlo: Reg::R0,
10325                rnhi: Reg::R1,
10326                rmlo: Reg::R2,
10327                rmhi: Reg::R3,
10328            },
10329            ArmOp::I64GeS {
10330                rd: Reg::R0,
10331                rnlo: Reg::R0,
10332                rnhi: Reg::R1,
10333                rmlo: Reg::R2,
10334                rmhi: Reg::R3,
10335            },
10336            ArmOp::I64GeU {
10337                rd: Reg::R0,
10338                rnlo: Reg::R0,
10339                rnhi: Reg::R1,
10340                rmlo: Reg::R2,
10341                rmhi: Reg::R3,
10342            },
10343        ];
10344
10345        for op in &ops {
10346            let code = encoder.encode(op).unwrap();
10347            assert!(
10348                code.len() >= 8,
10349                "i64 comparison {:?} should emit at least 8 bytes, got {}",
10350                op,
10351                code.len()
10352            );
10353        }
10354    }
10355
10356    #[test]
10357    fn test_encode_i64_const_zero_thumb2() {
10358        let encoder = ArmEncoder::new_thumb2();
10359        let op = ArmOp::I64Const {
10360            rdlo: Reg::R0,
10361            rdhi: Reg::R1,
10362            value: 0,
10363        };
10364        let code = encoder.encode(&op).unwrap();
10365        // MOVW R0, #0 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes
10366        assert_eq!(code.len(), 8, "I64Const(0) should be 8 bytes");
10367    }
10368
10369    #[test]
10370    fn test_encode_i64_const_negative_one_thumb2() {
10371        let encoder = ArmEncoder::new_thumb2();
10372        let op = ArmOp::I64Const {
10373            rdlo: Reg::R0,
10374            rdhi: Reg::R1,
10375            value: -1, // 0xFFFF_FFFF_FFFF_FFFF
10376        };
10377        let code = encoder.encode(&op).unwrap();
10378        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
10379        assert_eq!(code.len(), 16, "I64Const(-1) should be 16 bytes");
10380    }
10381
10382    // =========================================================================
10383    // Sub-word load/store encoding tests
10384    // =========================================================================
10385
10386    #[test]
10387    fn test_encode_ldrb_arm32() {
10388        let encoder = ArmEncoder::new_arm32();
10389        let op = ArmOp::Ldrb {
10390            rd: Reg::R0,
10391            addr: MemAddr::imm(Reg::R1, 4),
10392        };
10393        let code = encoder.encode(&op).unwrap();
10394        assert_eq!(code.len(), 4, "ARM32 LDRB should be 4 bytes");
10395        // LDRB R0, [R1, #4] = 0xE5D10004
10396        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10397        assert_eq!(encoded, 0xE5D10004, "Should encode LDRB R0, [R1, #4]");
10398    }
10399
10400    #[test]
10401    fn test_encode_strb_arm32() {
10402        let encoder = ArmEncoder::new_arm32();
10403        let op = ArmOp::Strb {
10404            rd: Reg::R0,
10405            addr: MemAddr::imm(Reg::R1, 0),
10406        };
10407        let code = encoder.encode(&op).unwrap();
10408        assert_eq!(code.len(), 4, "ARM32 STRB should be 4 bytes");
10409        // STRB R0, [R1, #0] = 0xE5C10000
10410        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10411        assert_eq!(encoded, 0xE5C10000, "Should encode STRB R0, [R1, #0]");
10412    }
10413
10414    #[test]
10415    fn test_encode_ldrh_arm32() {
10416        let encoder = ArmEncoder::new_arm32();
10417        let op = ArmOp::Ldrh {
10418            rd: Reg::R0,
10419            addr: MemAddr::imm(Reg::R1, 2),
10420        };
10421        let code = encoder.encode(&op).unwrap();
10422        assert_eq!(code.len(), 4, "ARM32 LDRH should be 4 bytes");
10423    }
10424
10425    #[test]
10426    fn test_encode_strh_arm32() {
10427        let encoder = ArmEncoder::new_arm32();
10428        let op = ArmOp::Strh {
10429            rd: Reg::R0,
10430            addr: MemAddr::imm(Reg::R1, 0),
10431        };
10432        let code = encoder.encode(&op).unwrap();
10433        assert_eq!(code.len(), 4, "ARM32 STRH should be 4 bytes");
10434    }
10435
10436    #[test]
10437    fn test_encode_ldrsb_arm32() {
10438        let encoder = ArmEncoder::new_arm32();
10439        let op = ArmOp::Ldrsb {
10440            rd: Reg::R0,
10441            addr: MemAddr::imm(Reg::R1, 0),
10442        };
10443        let code = encoder.encode(&op).unwrap();
10444        assert_eq!(code.len(), 4, "ARM32 LDRSB should be 4 bytes");
10445    }
10446
10447    #[test]
10448    fn test_encode_ldrsh_arm32() {
10449        let encoder = ArmEncoder::new_arm32();
10450        let op = ArmOp::Ldrsh {
10451            rd: Reg::R0,
10452            addr: MemAddr::imm(Reg::R1, 0),
10453        };
10454        let code = encoder.encode(&op).unwrap();
10455        assert_eq!(code.len(), 4, "ARM32 LDRSH should be 4 bytes");
10456    }
10457
10458    #[test]
10459    fn test_encode_ldrb_thumb2_16bit() {
10460        let encoder = ArmEncoder::new_thumb2();
10461        let op = ArmOp::Ldrb {
10462            rd: Reg::R0,
10463            addr: MemAddr::imm(Reg::R1, 4),
10464        };
10465        let code = encoder.encode(&op).unwrap();
10466        // Low registers + small offset -> 16-bit encoding
10467        assert_eq!(
10468            code.len(),
10469            2,
10470            "Thumb-2 LDRB with small offset should be 16-bit"
10471        );
10472    }
10473
10474    #[test]
10475    fn test_encode_ldrb_thumb2_32bit() {
10476        let encoder = ArmEncoder::new_thumb2();
10477        let op = ArmOp::Ldrb {
10478            rd: Reg::R0,
10479            addr: MemAddr::imm(Reg::R1, 100), // offset > 31 needs 32-bit
10480        };
10481        let code = encoder.encode(&op).unwrap();
10482        assert_eq!(
10483            code.len(),
10484            4,
10485            "Thumb-2 LDRB with large offset should be 32-bit"
10486        );
10487    }
10488
10489    #[test]
10490    fn test_encode_strb_thumb2_16bit() {
10491        let encoder = ArmEncoder::new_thumb2();
10492        let op = ArmOp::Strb {
10493            rd: Reg::R0,
10494            addr: MemAddr::imm(Reg::R1, 10),
10495        };
10496        let code = encoder.encode(&op).unwrap();
10497        assert_eq!(
10498            code.len(),
10499            2,
10500            "Thumb-2 STRB with small offset should be 16-bit"
10501        );
10502    }
10503
10504    #[test]
10505    fn test_encode_ldrh_thumb2_16bit() {
10506        let encoder = ArmEncoder::new_thumb2();
10507        let op = ArmOp::Ldrh {
10508            rd: Reg::R0,
10509            addr: MemAddr::imm(Reg::R1, 4), // offset aligned to 2, <= 62
10510        };
10511        let code = encoder.encode(&op).unwrap();
10512        assert_eq!(
10513            code.len(),
10514            2,
10515            "Thumb-2 LDRH with small aligned offset should be 16-bit"
10516        );
10517    }
10518
10519    #[test]
10520    fn test_encode_strh_thumb2_16bit() {
10521        let encoder = ArmEncoder::new_thumb2();
10522        let op = ArmOp::Strh {
10523            rd: Reg::R0,
10524            addr: MemAddr::imm(Reg::R1, 4),
10525        };
10526        let code = encoder.encode(&op).unwrap();
10527        assert_eq!(
10528            code.len(),
10529            2,
10530            "Thumb-2 STRH with small aligned offset should be 16-bit"
10531        );
10532    }
10533
10534    #[test]
10535    fn test_encode_ldrsb_thumb2() {
10536        let encoder = ArmEncoder::new_thumb2();
10537        let op = ArmOp::Ldrsb {
10538            rd: Reg::R0,
10539            addr: MemAddr::imm(Reg::R1, 0),
10540        };
10541        let code = encoder.encode(&op).unwrap();
10542        // LDRSB has no 16-bit immediate form, always 32-bit
10543        assert_eq!(code.len(), 4, "Thumb-2 LDRSB should be 32-bit");
10544    }
10545
10546    #[test]
10547    fn test_encode_ldrsh_thumb2() {
10548        let encoder = ArmEncoder::new_thumb2();
10549        let op = ArmOp::Ldrsh {
10550            rd: Reg::R0,
10551            addr: MemAddr::imm(Reg::R1, 0),
10552        };
10553        let code = encoder.encode(&op).unwrap();
10554        assert_eq!(code.len(), 4, "Thumb-2 LDRSH should be 32-bit");
10555    }
10556
10557    #[test]
10558    fn test_encode_memory_size_thumb2() {
10559        let encoder = ArmEncoder::new_thumb2();
10560        let op = ArmOp::MemorySize { rd: Reg::R0 };
10561        let code = encoder.encode(&op).unwrap();
10562        // R0 and R10 are not both low registers, so this needs careful handling
10563        assert!(!code.is_empty(), "MemorySize should produce code");
10564    }
10565
10566    #[test]
10567    fn test_encode_memory_grow_thumb2() {
10568        let encoder = ArmEncoder::new_thumb2();
10569        let op = ArmOp::MemoryGrow {
10570            rd: Reg::R0,
10571            rn: Reg::R0,
10572        };
10573        let code = encoder.encode(&op).unwrap();
10574        assert_eq!(code.len(), 4, "MemoryGrow (MVN) should be 32-bit Thumb-2");
10575    }
10576
10577    #[test]
10578    fn test_encode_subword_reg_offset_thumb2() {
10579        let encoder = ArmEncoder::new_thumb2();
10580
10581        // LDRB with register offset
10582        let op = ArmOp::Ldrb {
10583            rd: Reg::R0,
10584            addr: MemAddr::reg(Reg::R1, Reg::R2),
10585        };
10586        let code = encoder.encode(&op).unwrap();
10587        assert_eq!(
10588            code.len(),
10589            4,
10590            "Thumb-2 LDRB with reg offset should be 32-bit"
10591        );
10592
10593        // STRB with register offset
10594        let op = ArmOp::Strb {
10595            rd: Reg::R0,
10596            addr: MemAddr::reg(Reg::R1, Reg::R2),
10597        };
10598        let code = encoder.encode(&op).unwrap();
10599        assert_eq!(
10600            code.len(),
10601            4,
10602            "Thumb-2 STRB with reg offset should be 32-bit"
10603        );
10604
10605        // LDRH with register offset
10606        let op = ArmOp::Ldrh {
10607            rd: Reg::R0,
10608            addr: MemAddr::reg(Reg::R1, Reg::R2),
10609        };
10610        let code = encoder.encode(&op).unwrap();
10611        assert_eq!(
10612            code.len(),
10613            4,
10614            "Thumb-2 LDRH with reg offset should be 32-bit"
10615        );
10616
10617        // STRH with register offset
10618        let op = ArmOp::Strh {
10619            rd: Reg::R0,
10620            addr: MemAddr::reg(Reg::R1, Reg::R2),
10621        };
10622        let code = encoder.encode(&op).unwrap();
10623        assert_eq!(
10624            code.len(),
10625            4,
10626            "Thumb-2 STRH with reg offset should be 32-bit"
10627        );
10628    }
10629
10630    #[test]
10631    fn test_encode_subword_reg_imm_offset_thumb2() {
10632        let encoder = ArmEncoder::new_thumb2();
10633
10634        // LDRB with both register and immediate offset
10635        let op = ArmOp::Ldrb {
10636            rd: Reg::R0,
10637            addr: MemAddr::reg_imm(Reg::R1, Reg::R2, 4),
10638        };
10639        let code = encoder.encode(&op).unwrap();
10640        // ADD R12, R2, #4 (4 bytes) + LDRB R0, [R1, R12] (4 bytes) = 8 bytes
10641        assert_eq!(
10642            code.len(),
10643            8,
10644            "Thumb-2 LDRB with reg+imm offset should be 8 bytes"
10645        );
10646    }
10647
10648    // ========================================================================
10649    // Helium MVE encoding tests
10650    // ========================================================================
10651
10652    #[test]
10653    fn test_encode_mve_addi32_thumb2() {
10654        let encoder = ArmEncoder::new_thumb2();
10655        let op = ArmOp::MveAddI {
10656            qd: QReg::Q0,
10657            qn: QReg::Q1,
10658            qm: QReg::Q2,
10659            size: MveSize::S32,
10660        };
10661        let code = encoder.encode(&op).unwrap();
10662        assert_eq!(
10663            code.len(),
10664            4,
10665            "MVE VADD.I32 should be 4 bytes (Thumb-2 32-bit)"
10666        );
10667    }
10668
10669    #[test]
10670    fn test_encode_mve_subi16_thumb2() {
10671        let encoder = ArmEncoder::new_thumb2();
10672        let op = ArmOp::MveSubI {
10673            qd: QReg::Q0,
10674            qn: QReg::Q1,
10675            qm: QReg::Q2,
10676            size: MveSize::S16,
10677        };
10678        let code = encoder.encode(&op).unwrap();
10679        assert_eq!(code.len(), 4, "MVE VSUB.I16 should be 4 bytes");
10680    }
10681
10682    #[test]
10683    fn test_encode_mve_muli8_thumb2() {
10684        let encoder = ArmEncoder::new_thumb2();
10685        let op = ArmOp::MveMulI {
10686            qd: QReg::Q0,
10687            qn: QReg::Q1,
10688            qm: QReg::Q2,
10689            size: MveSize::S8,
10690        };
10691        let code = encoder.encode(&op).unwrap();
10692        assert_eq!(code.len(), 4, "MVE VMUL.I8 should be 4 bytes");
10693    }
10694
10695    #[test]
10696    fn test_encode_mve_bitwise_thumb2() {
10697        let encoder = ArmEncoder::new_thumb2();
10698
10699        let ops = vec![
10700            ArmOp::MveAnd {
10701                qd: QReg::Q0,
10702                qn: QReg::Q1,
10703                qm: QReg::Q2,
10704            },
10705            ArmOp::MveOrr {
10706                qd: QReg::Q0,
10707                qn: QReg::Q1,
10708                qm: QReg::Q2,
10709            },
10710            ArmOp::MveEor {
10711                qd: QReg::Q0,
10712                qn: QReg::Q1,
10713                qm: QReg::Q2,
10714            },
10715            ArmOp::MveBic {
10716                qd: QReg::Q0,
10717                qn: QReg::Q1,
10718                qm: QReg::Q2,
10719            },
10720        ];
10721        for op in ops {
10722            let code = encoder.encode(&op).unwrap();
10723            assert_eq!(code.len(), 4, "MVE bitwise op should be 4 bytes");
10724        }
10725    }
10726
10727    #[test]
10728    fn test_encode_mve_mvn_thumb2() {
10729        let encoder = ArmEncoder::new_thumb2();
10730        let op = ArmOp::MveMvn {
10731            qd: QReg::Q0,
10732            qm: QReg::Q1,
10733        };
10734        let code = encoder.encode(&op).unwrap();
10735        assert_eq!(code.len(), 4, "MVE VMVN should be 4 bytes");
10736    }
10737
10738    #[test]
10739    fn test_encode_mve_load_store_thumb2() {
10740        let encoder = ArmEncoder::new_thumb2();
10741
10742        let load = ArmOp::MveLoad {
10743            qd: QReg::Q0,
10744            addr: MemAddr::imm(Reg::R0, 16),
10745        };
10746        let code = encoder.encode(&load).unwrap();
10747        assert_eq!(code.len(), 4, "MVE VLDRW.32 should be 4 bytes");
10748
10749        let store = ArmOp::MveStore {
10750            qd: QReg::Q1,
10751            addr: MemAddr::imm(Reg::R1, 0),
10752        };
10753        let code = encoder.encode(&store).unwrap();
10754        assert_eq!(code.len(), 4, "MVE VSTRW.32 should be 4 bytes");
10755    }
10756
10757    #[test]
10758    fn test_encode_mve_const_thumb2() {
10759        let encoder = ArmEncoder::new_thumb2();
10760        let op = ArmOp::MveConst {
10761            qd: QReg::Q0,
10762            bytes: [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0],
10763        };
10764        let code = encoder.encode(&op).unwrap();
10765        // Should be 4 words of (MOVW R12 + VMOV Sn) = 4 * (4+4) = 32 bytes min
10766        // Some words with hi16=0 skip MOVT, so length varies
10767        assert!(
10768            code.len() >= 24,
10769            "MVE const should produce multiple instructions"
10770        );
10771    }
10772
10773    #[test]
10774    fn test_encode_mve_dup_thumb2() {
10775        let encoder = ArmEncoder::new_thumb2();
10776        let op = ArmOp::MveDup {
10777            qd: QReg::Q0,
10778            rn: Reg::R0,
10779            size: MveSize::S32,
10780        };
10781        let code = encoder.encode(&op).unwrap();
10782        assert_eq!(code.len(), 4, "MVE VDUP.32 should be 4 bytes");
10783    }
10784
10785    #[test]
10786    fn test_encode_mve_extract_lane_thumb2() {
10787        let encoder = ArmEncoder::new_thumb2();
10788        let op = ArmOp::MveExtractLane {
10789            rd: Reg::R0,
10790            qn: QReg::Q1,
10791            lane: 2,
10792            size: MveSize::S32,
10793        };
10794        let code = encoder.encode(&op).unwrap();
10795        assert_eq!(code.len(), 4, "MVE extract lane should be 4 bytes");
10796    }
10797
10798    #[test]
10799    fn test_encode_mve_insert_lane_thumb2() {
10800        let encoder = ArmEncoder::new_thumb2();
10801        let op = ArmOp::MveInsertLane {
10802            qd: QReg::Q0,
10803            rn: Reg::R1,
10804            lane: 3,
10805            size: MveSize::S32,
10806        };
10807        let code = encoder.encode(&op).unwrap();
10808        assert_eq!(code.len(), 4, "MVE insert lane should be 4 bytes");
10809    }
10810
10811    #[test]
10812    fn test_encode_mve_addf32_thumb2() {
10813        let encoder = ArmEncoder::new_thumb2();
10814        let op = ArmOp::MveAddF32 {
10815            qd: QReg::Q0,
10816            qn: QReg::Q1,
10817            qm: QReg::Q2,
10818        };
10819        let code = encoder.encode(&op).unwrap();
10820        assert_eq!(code.len(), 4, "MVE VADD.F32 should be 4 bytes");
10821    }
10822
10823    #[test]
10824    fn test_encode_mve_divf32_thumb2() {
10825        let encoder = ArmEncoder::new_thumb2();
10826        let op = ArmOp::MveDivF32 {
10827            qd: QReg::Q0,
10828            qn: QReg::Q1,
10829            qm: QReg::Q2,
10830        };
10831        let code = encoder.encode(&op).unwrap();
10832        // Lane-wise: 4 x VDIV.F32 = 4 x 4 = 16 bytes
10833        assert_eq!(
10834            code.len(),
10835            16,
10836            "MVE VDIV.F32 (lane-wise) should be 16 bytes"
10837        );
10838    }
10839
10840    #[test]
10841    fn test_encode_mve_sqrtf32_thumb2() {
10842        let encoder = ArmEncoder::new_thumb2();
10843        let op = ArmOp::MveSqrtF32 {
10844            qd: QReg::Q0,
10845            qm: QReg::Q1,
10846        };
10847        let code = encoder.encode(&op).unwrap();
10848        // Lane-wise: 4 x VSQRT.F32 = 4 x 4 = 16 bytes
10849        assert_eq!(
10850            code.len(),
10851            16,
10852            "MVE VSQRT.F32 (lane-wise) should be 16 bytes"
10853        );
10854    }
10855
10856    #[test]
10857    fn test_encode_mve_negf32_thumb2() {
10858        let encoder = ArmEncoder::new_thumb2();
10859        let op = ArmOp::MveNegF32 {
10860            qd: QReg::Q0,
10861            qm: QReg::Q1,
10862        };
10863        let code = encoder.encode(&op).unwrap();
10864        assert_eq!(code.len(), 4, "MVE VNEG.F32 should be 4 bytes");
10865    }
10866
10867    #[test]
10868    fn test_encode_mve_absf32_thumb2() {
10869        let encoder = ArmEncoder::new_thumb2();
10870        let op = ArmOp::MveAbsF32 {
10871            qd: QReg::Q0,
10872            qm: QReg::Q1,
10873        };
10874        let code = encoder.encode(&op).unwrap();
10875        assert_eq!(code.len(), 4, "MVE VABS.F32 should be 4 bytes");
10876    }
10877
10878    /// VCR-RA-001 / immediate-folding precondition: pins the Thumb-2 `AND`
10879    /// immediate encoding for the byte range and documents its bound.
10880    ///
10881    /// The `And { Operand2::Imm }` encoder packs the low 12 bits straight into
10882    /// the `i:imm3:imm8` field WITHOUT applying ThumbExpandImm (the modified-
10883    /// immediate expansion). For `imm <= 0xFF` (e.g. gale's int8 clamps
10884    /// `#0x7e` / `#0x7f`) that is correct — `i:imm3 = 0000` means "imm8
10885    /// zero-extended". So `and r2, r0, #0x7e` encodes to the canonical
10886    /// `00 f0 7e 02`. For `imm >= 0x100` the field would need a true
10887    /// ThumbExpandImm pattern (rotation / replication), which is NOT
10888    /// implemented here — so **immediate folding must gate on `imm <= 0xFF`**
10889    /// until the encoder is hardened to ThumbExpandImm/Ok-or-Err (the
10890    /// "encoder must be Ok-or-Err, never silently wrong" principle, #180/#185).
10891    /// This bound covers the measured `flat_flight` waste (#209).
10892    #[test]
10893    fn and_immediate_encodes_correctly_in_byte_range_documents_fold_bound() {
10894        let encoder = ArmEncoder::new_thumb2();
10895        let op = ArmOp::And {
10896            rd: Reg::R2,
10897            rn: Reg::R0,
10898            op2: Operand2::Imm(0x7e),
10899        };
10900        let code = encoder.encode(&op).unwrap();
10901        assert_eq!(
10902            code,
10903            vec![0x00, 0xf0, 0x7e, 0x02],
10904            "and r2, r0, #0x7e must encode to the canonical AND.W T1 (imm8=0x7e)"
10905        );
10906    }
10907
10908    /// #255: the shared ThumbExpandImm reverse-encoder underpinning the
10909    /// data-processing immediate fix. Encodable modified immediates round-trip to
10910    /// the expected `i:imm3:imm8` field; a genuinely non-modified value is `None`
10911    /// (caller must materialize into a register). Note `1000 = 0xFA ror 30` *is*
10912    /// representable (field 0xF7A) — the old encoder mis-encoded it (raw 0x3E8);
10913    /// this encodes it correctly.
10914    #[test]
10915    fn try_thumb_expand_imm_encodes_modified_immediates() {
10916        assert_eq!(try_thumb_expand_imm(0x7e), Some(0x07e)); // zero-extended byte
10917        assert_eq!(try_thumb_expand_imm(0xff), Some(0x0ff));
10918        assert_eq!(try_thumb_expand_imm(0x0001_0001), Some(0x101)); // 0x00XY00XY
10919        assert_eq!(try_thumb_expand_imm(0xff00_ff00), Some(0x2ff)); // 0xXY00XY00
10920        assert_eq!(try_thumb_expand_imm(0xffff_ffff), Some(0x3ff)); // 0xXYXYXYXY
10921        assert_eq!(try_thumb_expand_imm(0x100), Some(0xf80)); // 0x80 ror 31
10922        assert_eq!(try_thumb_expand_imm(0x8000_0000), Some(0x400)); // 0x80 ror 8
10923        assert_eq!(try_thumb_expand_imm(1000), Some(0xf7a)); // 0xFA ror 30
10924        // Genuinely unrepresentable (bits too far apart for an 8-bit window).
10925        assert_eq!(try_thumb_expand_imm(0x101), None);
10926        assert_eq!(try_thumb_expand_imm(0x12345), None);
10927    }
10928
10929    /// #255: CMP/ADDS/SUBS encode any valid modified immediate correctly, and
10930    /// ERROR (not silently mis-encode) on a genuinely unrepresentable one,
10931    /// forcing the selector to materialize into a register — closing the
10932    /// silent-miscompile class of #251/#253.
10933    #[test]
10934    fn cmp_adds_subs_immediate_error_on_non_modified_imm() {
10935        let encoder = ArmEncoder::new_thumb2();
10936        // cmp r0, #0xff → valid → Ok; cmp r0, #1000 → valid (0xFA ror 30) → Ok.
10937        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 0xff).is_ok());
10938        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 1000).is_ok());
10939        // cmp r0, #0x101 → NOT a modified immediate → Err (materialize-reg).
10940        assert!(
10941            encoder.encode_thumb32_cmp_imm(&Reg::R0, 0x101).is_err(),
10942            "cmp #0x101 must error, not compare the wrong constant"
10943        );
10944        assert!(
10945            encoder
10946                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x101)
10947                .is_err()
10948        );
10949        assert!(
10950            encoder
10951                .encode_thumb32_subs(&Reg::R0, &Reg::R0, 0x101)
10952                .is_err()
10953        );
10954        // ...but a valid modified immediate still encodes.
10955        assert!(
10956            encoder
10957                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x80)
10958                .is_ok()
10959        );
10960    }
10961
10962    /// #257: MLA (multiply-accumulate) encodes as MLS without the bit-4 op flag.
10963    /// `mla r2, r3, r4, r8` (rd=r2, rn=r3, rm=r4, ra=r8) → Thumb-2 `03 fb 04 82`.
10964    #[test]
10965    fn mla_thumb2_encodes_correctly() {
10966        let encoder = ArmEncoder::new_thumb2();
10967        let code = encoder
10968            .encode(&ArmOp::Mla {
10969                rd: Reg::R2,
10970                rn: Reg::R3,
10971                rm: Reg::R4,
10972                ra: Reg::R8,
10973            })
10974            .unwrap();
10975        // hw1 = 0xFB03, hw2 = (8<<12)|(2<<8)|4 = 0x8204
10976        assert_eq!(code, vec![0x03, 0xfb, 0x04, 0x82]);
10977    }
10978
10979    /// #259: LDR/STR (and sub-word) immediate-offset encoders truncated
10980    /// `offset & 0xFFF`, silently targeting the wrong address for offset >= 4096.
10981    /// They now error (the selector must use register-offset addressing) — the
10982    /// load/store sibling of the #253/#255 class. Offsets <= 4095 still encode.
10983    #[test]
10984    fn ldst_imm12_offset_errors_when_out_of_range() {
10985        let encoder = ArmEncoder::new_thumb2();
10986        // offset 0xFFF (4095): valid → Ok; ldr r0, [r1, #4095].
10987        assert!(
10988            encoder
10989                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0xFFF)
10990                .is_ok()
10991        );
10992        // offset 0x1000 (4096): out of imm12 range → Err (not & 0xFFF → #0).
10993        assert!(
10994            encoder
10995                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0x1000)
10996                .is_err(),
10997            "ldr offset 4096 must error, not wrap to 0"
10998        );
10999        assert!(
11000            encoder
11001                .encode_thumb32_str(&Reg::R0, &Reg::R1, 0x1000)
11002                .is_err()
11003        );
11004        assert!(
11005            encoder
11006                .encode_thumb32_ldrb_imm(&Reg::R0, &Reg::R1, 5000)
11007                .is_err()
11008        );
11009        assert!(
11010            encoder
11011                .encode_thumb32_strh_imm(&Reg::R0, &Reg::R1, 5000)
11012                .is_err()
11013        );
11014    }
11015
11016    /// Latent miscompile fix: ADD/SUB with a >0xFF immediate (e.g.
11017    /// `add sp, sp, #frame` for a >=256-byte frame) used ADD.W (T3), whose
11018    /// `i:imm3:imm8` is a ThumbExpandImm modified immediate — so `#256` silently
11019    /// encoded as `#0` (stack corruption). Use ADDW/SUBW (T4), a PLAIN 12-bit
11020    /// immediate, for 0x100..=0xFFF; keep T3 for <=0xFF (bit-identical); error
11021    /// beyond 4095.
11022    #[test]
11023    fn add_sub_large_immediate_use_addw_subw_not_misencoded() {
11024        let encoder = ArmEncoder::new_thumb2();
11025        // add sp, sp, #256  →  ADDW (T4) SP, SP, #256  =  0d f2 00 1d
11026        assert_eq!(
11027            encoder
11028                .encode(&ArmOp::Add {
11029                    rd: Reg::SP,
11030                    rn: Reg::SP,
11031                    op2: Operand2::Imm(256),
11032                })
11033                .unwrap(),
11034            vec![0x0d, 0xf2, 0x00, 0x1d],
11035            "add sp,sp,#256 must be ADDW (plain imm12), not a mis-encoded ADD.W"
11036        );
11037        // sub sp, sp, #256  →  SUBW (T4) SP, SP, #256  =  ad f2 00 1d
11038        assert_eq!(
11039            encoder
11040                .encode(&ArmOp::Sub {
11041                    rd: Reg::SP,
11042                    rn: Reg::SP,
11043                    op2: Operand2::Imm(256),
11044                })
11045                .unwrap(),
11046            vec![0xad, 0xf2, 0x00, 0x1d],
11047        );
11048        // > 4095 has no single-instruction encoding → error, not silent wrong.
11049        assert!(
11050            encoder
11051                .encode(&ArmOp::Add {
11052                    rd: Reg::SP,
11053                    rn: Reg::SP,
11054                    op2: Operand2::Imm(5000),
11055                })
11056                .is_err(),
11057            "add #5000 must error (no single ADDW), not mis-encode"
11058        );
11059    }
11060
11061    /// Closes the data-proc immediate class: AND and CMN now go through
11062    /// `try_thumb_expand_imm` like ORR/EOR/CMP — correct for any modified
11063    /// immediate, `Err` (not raw-pack / NOP) on an un-encodable one. The byte
11064    /// range stays bit-identical (`and r2,r0,#0x7e` is unchanged).
11065    #[test]
11066    fn and_cmn_immediate_thumb_expand_else_error() {
11067        let encoder = ArmEncoder::new_thumb2();
11068        // byte range unchanged (bit-identical with the pre-retrofit encoding)
11069        assert_eq!(
11070            encoder
11071                .encode(&ArmOp::And {
11072                    rd: Reg::R2,
11073                    rn: Reg::R0,
11074                    op2: Operand2::Imm(0x7e),
11075                })
11076                .unwrap(),
11077            vec![0x00, 0xf0, 0x7e, 0x02],
11078        );
11079        // a valid replicated modified immediate now encodes (was silently wrong)
11080        assert!(
11081            encoder
11082                .encode(&ArmOp::And {
11083                    rd: Reg::R2,
11084                    rn: Reg::R0,
11085                    op2: Operand2::Imm(0xff00ff00u32 as i32),
11086                })
11087                .is_ok()
11088        );
11089        // a genuinely un-encodable immediate errors (AND was raw-pack; CMN NOP)
11090        assert!(
11091            encoder
11092                .encode(&ArmOp::And {
11093                    rd: Reg::R2,
11094                    rn: Reg::R0,
11095                    op2: Operand2::Imm(0x101),
11096                })
11097                .is_err()
11098        );
11099        assert!(
11100            encoder
11101                .encode(&ArmOp::Cmn {
11102                    rn: Reg::R0,
11103                    op2: Operand2::Imm(0x101),
11104                })
11105                .is_err(),
11106            "CMN #0x101 must error, not emit a NOP"
11107        );
11108    }
11109
11110    /// VCR-RA-001: ORR/EOR with a small immediate must encode the real
11111    /// instruction (not a silent `0xBF00` NOP). Pins the byte range and the
11112    /// Ok-or-Err bound that makes future Or/Eor immediate folding safe.
11113    #[test]
11114    fn orr_eor_immediate_encode_in_byte_range_else_error() {
11115        let encoder = ArmEncoder::new_thumb2();
11116        // orr r2, r0, #0x7e  →  ORR.W T1, imm8=0x7e
11117        assert_eq!(
11118            encoder
11119                .encode(&ArmOp::Orr {
11120                    rd: Reg::R2,
11121                    rn: Reg::R0,
11122                    op2: Operand2::Imm(0x7e),
11123                })
11124                .unwrap(),
11125            vec![0x40, 0xf0, 0x7e, 0x02],
11126        );
11127        // eor r2, r0, #0x7e  →  EOR.W T1, imm8=0x7e
11128        assert_eq!(
11129            encoder
11130                .encode(&ArmOp::Eor {
11131                    rd: Reg::R2,
11132                    rn: Reg::R0,
11133                    op2: Operand2::Imm(0x7e),
11134                })
11135                .unwrap(),
11136            vec![0x80, 0xf0, 0x7e, 0x02],
11137        );
11138        // Out-of-range immediates error rather than silently mis-encode / NOP.
11139        assert!(
11140            encoder
11141                .encode(&ArmOp::Orr {
11142                    rd: Reg::R2,
11143                    rn: Reg::R0,
11144                    op2: Operand2::Imm(0x140),
11145                })
11146                .is_err(),
11147            "ORR #0x140 must error, not emit a NOP"
11148        );
11149    }
11150
11151    #[test]
11152    fn test_encode_mve_different_qregs() {
11153        let encoder = ArmEncoder::new_thumb2();
11154
11155        // Test that different Q-register numbers produce different encodings
11156        let op1 = ArmOp::MveAddI {
11157            qd: QReg::Q0,
11158            qn: QReg::Q0,
11159            qm: QReg::Q0,
11160            size: MveSize::S32,
11161        };
11162        let op2 = ArmOp::MveAddI {
11163            qd: QReg::Q3,
11164            qn: QReg::Q5,
11165            qm: QReg::Q7,
11166            size: MveSize::S32,
11167        };
11168        let code1 = encoder.encode(&op1).unwrap();
11169        let code2 = encoder.encode(&op2).unwrap();
11170        assert_ne!(
11171            code1, code2,
11172            "Different Q-registers should produce different encodings"
11173        );
11174    }
11175
11176    #[test]
11177    fn test_encode_mve_arm32_loud_err() {
11178        // #615: MVE (Helium) is Thumb-2-only. The ARM32 encoder used to emit
11179        // a silent NOP here (dropping the vector op); it must now be a typed
11180        // Err so a broken "MVE implies Thumb" invariant fails loudly.
11181        let encoder = ArmEncoder::new_arm32();
11182        let op = ArmOp::MveAddI {
11183            qd: QReg::Q0,
11184            qn: QReg::Q1,
11185            qm: QReg::Q2,
11186            size: MveSize::S32,
11187        };
11188        let err = encoder
11189            .encode(&op)
11190            .expect_err("ARM32 MVE must be a loud Err, not a silent NOP (#615)");
11191        assert!(
11192            err.to_string().contains("Thumb-2 only"),
11193            "unexpected error message: {err}"
11194        );
11195    }
11196}