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                elide_zero_guard,
952            } => {
953                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
954                // #494 phase 2b: elided only under a certificate-discharged
955                // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
956                if !elide_zero_guard {
957                    emit_a32_i64_divisor_zero_trap(&mut b);
958                }
959                w(&mut b, 0xE92D_00F0); // PUSH {R4-R7}
960                for r in 4..8u32 {
961                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
962                }
963                div_loop(&mut b, 12); // counter in R12 (encoder scratch)
964                w(&mut b, 0xE1A0_0004); // MOV R0, R4 (quotient lo)
965                w(&mut b, 0xE1A0_1005); // MOV R1, R5 (quotient hi)
966                w(&mut b, 0xE8BD_00F0); // POP {R4-R7}
967                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
968            }
969
970            // I64DivS: sign-extract, unsigned core, conditional negate —
971            // A32 transcription of the Thumb-2 arm.
972            ArmOp::I64DivS {
973                rdlo,
974                rdhi,
975                rnlo,
976                rnhi,
977                rmlo,
978                rmhi,
979                elide_zero_guard,
980                elide_overflow_guard,
981            } => {
982                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
983                // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
984                // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
985                // the #633 overflow guard falls ONLY to
986                // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
987                // divisor-nonzero fact alone must keep it.
988                if !elide_zero_guard {
989                    emit_a32_i64_divisor_zero_trap(&mut b);
990                }
991                if !elide_overflow_guard {
992                    // #633: INT64_MIN / -1 overflows — trap like the i32 path
993                    // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
994                    emit_a32_i64_divs_overflow_trap(&mut b);
995                }
996                w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
997                w(&mut b, 0xE021_9003); // EOR R9, R1, R3 (result sign in MSB)
998                skip_negate_if_positive(&mut b, 1);
999                negate64(&mut b, 0, 1);
1000                skip_negate_if_positive(&mut b, 3);
1001                negate64(&mut b, 2, 3);
1002                for r in 4..8u32 {
1003                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1004                }
1005                div_loop(&mut b, 8); // counter in R8 (saved above)
1006                w(&mut b, 0xE1A0_0004); // MOV R0, R4
1007                w(&mut b, 0xE1A0_1005); // MOV R1, R5
1008                skip_negate_if_positive(&mut b, 9);
1009                negate64(&mut b, 0, 1);
1010                w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1011                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1012            }
1013
1014            // I64RemU: same core as I64DivU, returns the remainder (R6:R7).
1015            ArmOp::I64RemU {
1016                rdlo,
1017                rdhi,
1018                rnlo,
1019                rnhi,
1020                rmlo,
1021                rmhi,
1022                elide_zero_guard,
1023            } => {
1024                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1025                if !elide_zero_guard {
1026                    emit_a32_i64_divisor_zero_trap(&mut b);
1027                }
1028                w(&mut b, 0xE92D_01F0); // PUSH {R4-R8}
1029                for r in 4..8u32 {
1030                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1031                }
1032                div_loop(&mut b, 8);
1033                w(&mut b, 0xE1A0_0006); // MOV R0, R6 (remainder lo)
1034                w(&mut b, 0xE1A0_1007); // MOV R1, R7 (remainder hi)
1035                w(&mut b, 0xE8BD_01F0); // POP {R4-R8}
1036                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1037            }
1038
1039            // I64RemS: remainder takes the DIVIDEND's sign (WASM semantics).
1040            ArmOp::I64RemS {
1041                rdlo,
1042                rdhi,
1043                rnlo,
1044                rnhi,
1045                rmlo,
1046                rmhi,
1047                elide_zero_guard,
1048            } => {
1049                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1050                if !elide_zero_guard {
1051                    emit_a32_i64_divisor_zero_trap(&mut b);
1052                }
1053                w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1054                w(&mut b, 0xE1A0_9001); // MOV R9, R1 (dividend sign)
1055                skip_negate_if_positive(&mut b, 1);
1056                negate64(&mut b, 0, 1);
1057                skip_negate_if_positive(&mut b, 3);
1058                negate64(&mut b, 2, 3);
1059                for r in 4..8u32 {
1060                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1061                }
1062                div_loop(&mut b, 8);
1063                w(&mut b, 0xE1A0_0006); // MOV R0, R6
1064                w(&mut b, 0xE1A0_1007); // MOV R1, R7
1065                skip_negate_if_positive(&mut b, 9);
1066                negate64(&mut b, 0, 1);
1067                w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1068                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1069            }
1070
1071            // Popcnt (i32): bit-twiddle expansion (no native A32 popcount),
1072            // mirroring the Thumb-2 arm's register contract (R11 + R12 as
1073            // scratch, shift-add fold, final AND #0x3F).
1074            ArmOp::Popcnt { rd, rm } => {
1075                let rd_b = reg_to_bits(rd);
1076                if rd != rm {
1077                    w(&mut b, 0xE1A0_0000 | (rd_b << 12) | reg_to_bits(rm)); // MOV rd, rm
1078                }
1079                // x = x - ((x >> 1) & 0x55555555)
1080                movw(&mut b, 12, 0x5555);
1081                movt(&mut b, 12, 0x5555);
1082                shift_imm(&mut b, LSR, 11, rd_b, 1);
1083                dp_reg(&mut b, 0xE000_0000, 11, 11, 12); // AND R11, R11, R12
1084                dp_reg(&mut b, 0xE040_0000, rd_b, rd_b, 11); // SUB rd, rd, R11
1085                // x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
1086                movw(&mut b, 12, 0x3333);
1087                movt(&mut b, 12, 0x3333);
1088                dp_reg(&mut b, 0xE000_0000, 11, rd_b, 12); // AND R11, rd, R12
1089                shift_imm(&mut b, LSR, rd_b, rd_b, 2);
1090                dp_reg(&mut b, 0xE000_0000, rd_b, rd_b, 12); // AND rd, rd, R12
1091                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11); // ADD rd, rd, R11
1092                // x = (x + (x >> 4)) & 0x0F0F0F0F
1093                shift_imm(&mut b, LSR, 11, rd_b, 4);
1094                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11); // ADD rd, rd, R11
1095                movw(&mut b, 12, 0x0F0F);
1096                movt(&mut b, 12, 0x0F0F);
1097                dp_reg(&mut b, 0xE000_0000, rd_b, rd_b, 12); // AND rd, rd, R12
1098                // x += x >> 8; x += x >> 16; x &= 0x3F
1099                shift_imm(&mut b, LSR, 11, rd_b, 8);
1100                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11);
1101                shift_imm(&mut b, LSR, 11, rd_b, 16);
1102                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11);
1103                w(&mut b, 0xE200_003F | (rd_b << 16) | (rd_b << 12)); // AND rd, rd, #63
1104            }
1105
1106            // I64Popcnt: POPCNT(lo) + POPCNT(hi) — A32 transcription of the
1107            // Thumb-2 arm (R3/R4/R5 saved, mul-based per-word fold, high
1108            // result word rnhi cleared last, mirroring the Thumb contract).
1109            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
1110                let hi = reg_to_bits(rnhi);
1111                w(&mut b, 0xE92D_0038); // PUSH {R3, R4, R5}
1112                // #632 audit: route rnlo through R12 so a pair living at
1113                // (R3,R4) cannot read a clobbered R4 (sources read before any
1114                // scratch register they could occupy is written).
1115                w(&mut b, 0xE1A0_C000 | reg_to_bits(rnlo)); // MOV R12, rnlo
1116                w(&mut b, 0xE1A0_5000 | hi); //                MOV R5, rnhi
1117                w(&mut b, 0xE1A0_400C); //                     MOV R4, R12
1118                popcnt_word(&mut b, 4, 3);
1119                popcnt_word(&mut b, 5, 3);
1120                // #632: carry the count across the scratch restore in R12 —
1121                // rd is allocator-assigned and can land inside {R3,R4,R5};
1122                // the old `ADD rd, R4, R5` before the POP was destroyed by
1123                // the restore. R12 is never allocatable and never restored.
1124                dp_reg(&mut b, 0xE080_0000, 12, 4, 5); // ADD R12, R4, R5
1125                w(&mut b, 0xE8BD_0038); // POP {R3, R4, R5}
1126                w(&mut b, 0xE1A0_0000 | (reg_to_bits(rd) << 12) | 12); // MOV rd, R12
1127                w(&mut b, 0xE3A0_0000 | (hi << 12)); // MOV rnhi, #0 (i64 hi word)
1128            }
1129
1130            _ => return Ok(None),
1131        }
1132        Ok(Some(b))
1133    }
1134
1135    fn encode_arm(&self, op: &ArmOp) -> Result<Vec<u8>> {
1136        // #615: A32 multi-instruction expansions (i64 arithmetic/shift/rotate/
1137        // compare, SetCond/SelectMove, popcnt, ...). These ops were literal
1138        // NOPs on the A32 path — user-reachable via `--target cortex-r5` —
1139        // so the value silently vanished. Mirror of the #594 CallIndirect
1140        // early-return: if the expansion helper covers the op, its bytes are
1141        // the encoding.
1142        if let Some(bytes) = self.encode_arm_expanded(op)? {
1143            return Ok(bytes);
1144        }
1145        // #206: ARM32 register-offset loads/stores. `encode_mem_addr` only
1146        // returns the 12-bit immediate, so the immediate-form arms below
1147        // silently DROP `addr.offset_reg` — a runtime address index vanished,
1148        // turning `ldr rd,[rn,rm,#off]` into `ldr rd,[rn,#off]` (the access went
1149        // to the wrong address). Compute the effective base into IP and re-encode
1150        // against `[ip, #off]`, which is uniform for word/byte/halfword/signed.
1151        if let Some(bytes) = self.encode_arm_reg_offset_mem(op)? {
1152            return Ok(bytes);
1153        }
1154        // #594: call_indirect was encoded as a literal NOP on the A32 path
1155        // (`--target cortex-r5`) — the call never happened and the function
1156        // silently returned garbage. Emit the same three-instruction expansion
1157        // as the Thumb-2 path (R11 = function-pointer table base, R12 scratch):
1158        //   MOV r12, idx, LSL #2 ; LDR r12, [r11, r12] ; BLX r12
1159        if let ArmOp::CallIndirect {
1160            table_index_reg, ..
1161        } = op
1162        {
1163            return Ok(Self::encode_arm_call_indirect(table_index_reg));
1164        }
1165        let instr: u32 = match op {
1166            // Data processing instructions
1167            ArmOp::Add { rd, rn, op2 } => {
1168                let rd_bits = reg_to_bits(rd);
1169                let rn_bits = reg_to_bits(rn);
1170                let (op2_bits, i_flag) = encode_operand2(op2)?;
1171
1172                // ADD encoding: cond(4) | 00 | I(1) | 0100 | S(1) | Rn(4) | Rd(4) | operand2(12)
1173                0xE0800000 // condition=always(E), opcode=ADD(0100), S=0
1174                    | (i_flag << 25)
1175                    | (rn_bits << 16)
1176                    | (rd_bits << 12)
1177                    | op2_bits
1178            }
1179
1180            ArmOp::Sub { rd, rn, op2 } => {
1181                let rd_bits = reg_to_bits(rd);
1182                let rn_bits = reg_to_bits(rn);
1183                let (op2_bits, i_flag) = encode_operand2(op2)?;
1184
1185                // SUB encoding: opcode=0010
1186                0xE0400000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1187            }
1188
1189            // i64 support: ADDS, ADC, SUBS, SBC for ARM32
1190            ArmOp::Adds { rd, rn, op2 } => {
1191                let rd_bits = reg_to_bits(rd);
1192                let rn_bits = reg_to_bits(rn);
1193                let (op2_bits, i_flag) = encode_operand2(op2)?;
1194
1195                // ADDS encoding: opcode=0100, S=1
1196                0xE0900000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1197            }
1198
1199            ArmOp::Adc { rd, rn, op2 } => {
1200                let rd_bits = reg_to_bits(rd);
1201                let rn_bits = reg_to_bits(rn);
1202                let (op2_bits, i_flag) = encode_operand2(op2)?;
1203
1204                // ADC encoding: opcode=0101
1205                0xE0A00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1206            }
1207
1208            ArmOp::Subs { rd, rn, op2 } => {
1209                let rd_bits = reg_to_bits(rd);
1210                let rn_bits = reg_to_bits(rn);
1211                let (op2_bits, i_flag) = encode_operand2(op2)?;
1212
1213                // SUBS encoding: opcode=0010, S=1
1214                0xE0500000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1215            }
1216
1217            ArmOp::Sbc { rd, rn, op2 } => {
1218                let rd_bits = reg_to_bits(rd);
1219                let rn_bits = reg_to_bits(rn);
1220                let (op2_bits, i_flag) = encode_operand2(op2)?;
1221
1222                // SBC encoding: opcode=0110
1223                0xE0C00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1224            }
1225
1226            ArmOp::Mul { rd, rn, rm } => {
1227                let rd_bits = reg_to_bits(rd);
1228                let rn_bits = reg_to_bits(rn);
1229                let rm_bits = reg_to_bits(rm);
1230
1231                // MUL encoding: cond(4) | 000000 | A(1) | S(1) | Rd(4) | Rn(4) | Rs(4) | 1001 | Rm(4)
1232                0xE0000090 | (rd_bits << 16) | (rn_bits << 8) | rm_bits
1233            }
1234
1235            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
1236                let rdlo_bits = reg_to_bits(rdlo);
1237                let rdhi_bits = reg_to_bits(rdhi);
1238                let rn_bits = reg_to_bits(rn);
1239                let rm_bits = reg_to_bits(rm);
1240
1241                // UMULL encoding: cond(4) | 0000 1000 | RdHi(4) | RdLo(4) | Rm(4) | 1001 | Rn(4)
1242                0xE0800090 | (rdhi_bits << 16) | (rdlo_bits << 12) | (rm_bits << 8) | rn_bits
1243            }
1244
1245            ArmOp::Sdiv { rd, rn, rm } => {
1246                let rd_bits = reg_to_bits(rd);
1247                let rn_bits = reg_to_bits(rn);
1248                let rm_bits = reg_to_bits(rm);
1249
1250                // SDIV encoding: cond(4) | 01110001 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1251                // ARMv7-M and above
1252                0xE710F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1253            }
1254
1255            ArmOp::Udiv { rd, rn, rm } => {
1256                let rd_bits = reg_to_bits(rd);
1257                let rn_bits = reg_to_bits(rn);
1258                let rm_bits = reg_to_bits(rm);
1259
1260                // UDIV encoding: cond(4) | 01110011 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1261                // ARMv7-M and above
1262                0xE730F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1263            }
1264
1265            ArmOp::Mls { rd, rn, rm, ra } => {
1266                let rd_bits = reg_to_bits(rd);
1267                let rn_bits = reg_to_bits(rn);
1268                let rm_bits = reg_to_bits(rm);
1269                let ra_bits = reg_to_bits(ra);
1270
1271                // MLS encoding: cond(4) | 00000110 | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1272                // Rd = Ra - (Rn * Rm)
1273                0xE0600090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1274            }
1275
1276            ArmOp::Mla { rd, rn, rm, ra } => {
1277                let rd_bits = reg_to_bits(rd);
1278                let rn_bits = reg_to_bits(rn);
1279                let rm_bits = reg_to_bits(rm);
1280                let ra_bits = reg_to_bits(ra);
1281
1282                // MLA encoding: cond(4) | 0000001 S | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1283                // Rd = Ra + (Rn * Rm). Base 0xE0200090 (S=0).
1284                0xE0200090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1285            }
1286
1287            ArmOp::And { rd, rn, op2 } => {
1288                let rd_bits = reg_to_bits(rd);
1289                let rn_bits = reg_to_bits(rn);
1290                let (op2_bits, i_flag) = encode_operand2(op2)?;
1291
1292                // AND encoding: opcode=0000
1293                0xE0000000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1294            }
1295
1296            ArmOp::Orr { rd, rn, op2 } => {
1297                let rd_bits = reg_to_bits(rd);
1298                let rn_bits = reg_to_bits(rn);
1299                let (op2_bits, i_flag) = encode_operand2(op2)?;
1300
1301                // ORR encoding: opcode=1100
1302                0xE1800000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1303            }
1304
1305            ArmOp::Eor { rd, rn, op2 } => {
1306                let rd_bits = reg_to_bits(rd);
1307                let rn_bits = reg_to_bits(rn);
1308                let (op2_bits, i_flag) = encode_operand2(op2)?;
1309
1310                // EOR encoding: opcode=0001
1311                0xE0200000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1312            }
1313
1314            // Shift instructions
1315            ArmOp::Lsl { rd, rn, shift } => {
1316                let rd_bits = reg_to_bits(rd);
1317                let rn_bits = reg_to_bits(rn);
1318                let shift_bits = *shift & 0x1F;
1319
1320                // LSL encoding: MOV with shift
1321                0xE1A00000 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1322            }
1323
1324            ArmOp::Lsr { rd, rn, shift } => {
1325                let rd_bits = reg_to_bits(rd);
1326                let rn_bits = reg_to_bits(rn);
1327                let shift_bits = *shift & 0x1F;
1328
1329                // LSR encoding
1330                0xE1A00020 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1331            }
1332
1333            ArmOp::Asr { rd, rn, shift } => {
1334                let rd_bits = reg_to_bits(rd);
1335                let rn_bits = reg_to_bits(rn);
1336                let shift_bits = *shift & 0x1F;
1337
1338                // ASR encoding
1339                0xE1A00040 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1340            }
1341
1342            ArmOp::Ror { rd, rn, shift } => {
1343                let rd_bits = reg_to_bits(rd);
1344                let rn_bits = reg_to_bits(rn);
1345                let shift_bits = *shift & 0x1F;
1346
1347                // ROR encoding: MOV with ROR shift
1348                0xE1A00060 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1349            }
1350
1351            // Register-based shifts (ARM32)
1352            // LSL Rd, Rn, Rm: cond 0001101S 0000 Rd Rs 0001 Rn
1353            ArmOp::LslReg { rd, rn, rm } => {
1354                let rd_bits = reg_to_bits(rd);
1355                let rn_bits = reg_to_bits(rn);
1356                let rm_bits = reg_to_bits(rm);
1357                0xE1A00010 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1358            }
1359            ArmOp::LsrReg { rd, rn, rm } => {
1360                let rd_bits = reg_to_bits(rd);
1361                let rn_bits = reg_to_bits(rn);
1362                let rm_bits = reg_to_bits(rm);
1363                0xE1A00030 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1364            }
1365            ArmOp::AsrReg { rd, rn, rm } => {
1366                let rd_bits = reg_to_bits(rd);
1367                let rn_bits = reg_to_bits(rn);
1368                let rm_bits = reg_to_bits(rm);
1369                0xE1A00050 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1370            }
1371            ArmOp::RorReg { rd, rn, rm } => {
1372                let rd_bits = reg_to_bits(rd);
1373                let rn_bits = reg_to_bits(rn);
1374                let rm_bits = reg_to_bits(rm);
1375                0xE1A00070 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1376            }
1377
1378            // RSB (Reverse Subtract): Rd = imm - Rn
1379            ArmOp::Rsb { rd, rn, imm } => {
1380                let rd_bits = reg_to_bits(rd);
1381                let rn_bits = reg_to_bits(rn);
1382                // RSB encoding: cond(4) | 00 1 0011 S | Rn(4) | Rd(4) | imm12
1383                // Opcode for RSB = 0011, I=1 (immediate), S=0
1384                0xE2600000 | (rn_bits << 16) | (rd_bits << 12) | (*imm & 0xFF)
1385            }
1386
1387            // Bit manipulation instructions
1388            ArmOp::Clz { rd, rm } => {
1389                let rd_bits = reg_to_bits(rd);
1390                let rm_bits = reg_to_bits(rm);
1391
1392                // CLZ encoding: cond(4) | 00010110 | 1111 | Rd(4) | 1111 | 0001 | Rm(4)
1393                // ARMv5T and above
1394                0xE16F0F10 | (rd_bits << 12) | rm_bits
1395            }
1396
1397            ArmOp::Rbit { rd, rm } => {
1398                let rd_bits = reg_to_bits(rd);
1399                let rm_bits = reg_to_bits(rm);
1400
1401                // RBIT encoding: cond(4) | 01101111 | 1111 | Rd(4) | 1111 | 0011 | Rm(4)
1402                // ARMv6T2 and above
1403                0xE6FF0F30 | (rd_bits << 12) | rm_bits
1404            }
1405
1406            ArmOp::Sxtb { rd, rm } => {
1407                let rd_bits = reg_to_bits(rd);
1408                let rm_bits = reg_to_bits(rm);
1409
1410                // SXTB encoding: cond(4) | 01101010 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1411                // ARMv6 and above. rotate=00 for no rotation
1412                0xE6AF0070 | (rd_bits << 12) | rm_bits
1413            }
1414
1415            ArmOp::Sxth { rd, rm } => {
1416                let rd_bits = reg_to_bits(rd);
1417                let rm_bits = reg_to_bits(rm);
1418
1419                // SXTH encoding: cond(4) | 01101011 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1420                // ARMv6 and above. rotate=00 for no rotation
1421                0xE6BF0070 | (rd_bits << 12) | rm_bits
1422            }
1423
1424            ArmOp::Uxtb { rd, rm } => {
1425                let rd_bits = reg_to_bits(rd);
1426                let rm_bits = reg_to_bits(rm);
1427                // UXTB encoding: cond | 01101110 1111 Rd rotate 00 0111 Rm (rotate=00)
1428                0xE6EF0070 | (rd_bits << 12) | rm_bits
1429            }
1430
1431            ArmOp::Uxth { rd, rm } => {
1432                let rd_bits = reg_to_bits(rd);
1433                let rm_bits = reg_to_bits(rm);
1434                // UXTH encoding: cond | 01101111 1111 Rd rotate 00 0111 Rm (rotate=00)
1435                0xE6FF0070 | (rd_bits << 12) | rm_bits
1436            }
1437
1438            // Move instructions
1439            ArmOp::Mov { rd, op2 } => {
1440                let rd_bits = reg_to_bits(rd);
1441                let (op2_bits, i_flag) = encode_operand2(op2)?;
1442
1443                // MOV encoding: opcode=1101
1444                0xE1A00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1445            }
1446
1447            ArmOp::Mvn { rd, op2 } => {
1448                let rd_bits = reg_to_bits(rd);
1449                let (op2_bits, i_flag) = encode_operand2(op2)?;
1450
1451                // MVN encoding: opcode=1111
1452                0xE1E00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1453            }
1454
1455            // MOVW - Move Wide (ARM32)
1456            // Encoding: cond(4) | 0011 0000 | imm4(4) | Rd(4) | imm12(12)
1457            ArmOp::Movw { rd, imm16 } => {
1458                let rd_bits = reg_to_bits(rd);
1459                let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1460                let imm12 = (*imm16 as u32) & 0xFFF;
1461                0xE3000000 | (imm4 << 16) | (rd_bits << 12) | imm12
1462            }
1463
1464            // MOVT - Move Top (ARM32)
1465            // Encoding: cond(4) | 0011 0100 | imm4(4) | Rd(4) | imm12(12)
1466            ArmOp::Movt { rd, imm16 } => {
1467                let rd_bits = reg_to_bits(rd);
1468                let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1469                let imm12 = (*imm16 as u32) & 0xFFF;
1470                0xE3400000 | (imm4 << 16) | (rd_bits << 12) | imm12
1471            }
1472
1473            // #237: symbol-relative MOVW/MOVT (ARM mode) — addend in place, the
1474            // backend records the MOVW_ABS/MOVT_ABS relocation against `symbol`.
1475            ArmOp::MovwSym { rd, addend, .. } => {
1476                let rd_bits = reg_to_bits(rd);
1477                let v = (*addend as u32) & 0xffff;
1478                0xE3000000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1479            }
1480            ArmOp::MovtSym { rd, addend, .. } => {
1481                let rd_bits = reg_to_bits(rd);
1482                let v = ((*addend as u32) >> 16) & 0xffff;
1483                0xE3400000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1484            }
1485
1486            // #345: LdrSym is the Thumb-2 literal-pool address load. A32 mode is
1487            // not used for relocatable native-pointer objects; fail loudly rather
1488            // than miscompile if it is ever reached here.
1489            ArmOp::LdrSym { .. } => {
1490                return Err(synth_core::Error::synthesis(
1491                    "LdrSym (literal-pool address load) is Thumb-2-only",
1492                ));
1493            }
1494
1495            // Compare
1496            ArmOp::Cmp { rn, op2 } => {
1497                let rn_bits = reg_to_bits(rn);
1498                let (op2_bits, i_flag) = encode_operand2(op2)?;
1499
1500                // CMP encoding: opcode=1010, S=1
1501                0xE1500000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1502            }
1503
1504            // Compare Negative (CMN) - computes Rn + op2 and sets flags
1505            ArmOp::Cmn { rn, op2 } => {
1506                let rn_bits = reg_to_bits(rn);
1507                let (op2_bits, i_flag) = encode_operand2(op2)?;
1508
1509                // CMN encoding: opcode=1011, S=1
1510                0xE1700000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1511            }
1512
1513            // Load/Store
1514            ArmOp::Ldr { rd, addr } => {
1515                let rd_bits = reg_to_bits(rd);
1516                let (base_bits, offset_bits) = encode_mem_addr(addr);
1517
1518                // LDR encoding: cond(4) | 01 | I(1) | P(1) | U(1) | B(1) | W(1) | L(1) | Rn(4) | Rd(4) | offset(12)
1519                // P=1 (pre-indexed), U=1 (add offset), L=1 (load)
1520                0xE5900000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1521            }
1522
1523            ArmOp::Str { rd, addr } => {
1524                let rd_bits = reg_to_bits(rd);
1525                let (base_bits, offset_bits) = encode_mem_addr(addr);
1526
1527                // STR encoding: L=0 (store)
1528                0xE5800000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1529            }
1530
1531            // Sub-word loads (ARM32 encoding)
1532            ArmOp::Ldrb { rd, addr } => {
1533                let rd_bits = reg_to_bits(rd);
1534                let (base_bits, offset_bits) = encode_mem_addr(addr);
1535                // LDRB: LDR with B=1 (byte): cond|01|I|P|U|1|W|L|Rn|Rd|offset
1536                0xE5D00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1537            }
1538
1539            ArmOp::Ldrsb { rd, addr } => {
1540                let rd_bits = reg_to_bits(rd);
1541                let (base_bits, offset_bits) = encode_mem_addr(addr);
1542                // LDRSB (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1101|imm4L
1543                // Simplified with immediate offset
1544                let offset_val = offset_bits & 0xFF;
1545                let imm4h = (offset_val >> 4) & 0xF;
1546                let imm4l = offset_val & 0xF;
1547                0xE1D000D0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1548            }
1549
1550            ArmOp::Ldrh { rd, addr } => {
1551                let rd_bits = reg_to_bits(rd);
1552                let (base_bits, offset_bits) = encode_mem_addr(addr);
1553                // LDRH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1011|imm4L
1554                let offset_val = offset_bits & 0xFF;
1555                let imm4h = (offset_val >> 4) & 0xF;
1556                let imm4l = offset_val & 0xF;
1557                0xE1D000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1558            }
1559
1560            ArmOp::Ldrsh { rd, addr } => {
1561                let rd_bits = reg_to_bits(rd);
1562                let (base_bits, offset_bits) = encode_mem_addr(addr);
1563                // LDRSH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1111|imm4L
1564                let offset_val = offset_bits & 0xFF;
1565                let imm4h = (offset_val >> 4) & 0xF;
1566                let imm4l = offset_val & 0xF;
1567                0xE1D000F0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1568            }
1569
1570            // Sub-word stores (ARM32 encoding)
1571            ArmOp::Strb { rd, addr } => {
1572                let rd_bits = reg_to_bits(rd);
1573                let (base_bits, offset_bits) = encode_mem_addr(addr);
1574                // STRB: STR with B=1 (byte): cond|01|I|P|U|1|W|0|Rn|Rd|offset
1575                0xE5C00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1576            }
1577
1578            ArmOp::Strh { rd, addr } => {
1579                let rd_bits = reg_to_bits(rd);
1580                let (base_bits, offset_bits) = encode_mem_addr(addr);
1581                // STRH (misc store): cond|000|P|U|1|W|0|Rn|Rd|imm4H|1011|imm4L
1582                let offset_val = offset_bits & 0xFF;
1583                let imm4h = (offset_val >> 4) & 0xF;
1584                let imm4l = offset_val & 0xF;
1585                0xE1C000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1586            }
1587
1588            // Memory management (ARM32 encoding)
1589            ArmOp::MemorySize { rd } => {
1590                let rd_bits = reg_to_bits(rd);
1591                // MOV rd, R10, LSR #16  (memory size in bytes / 65536 = pages)
1592                // cond|000|1101|S|0000|Rd|shift5|type|0|Rm
1593                // LSR #16: shift5=10000, type=01
1594                0xE1A00820 | (rd_bits << 12) | 0x0A // Rm=R10, shift=16, LSR
1595            }
1596
1597            ArmOp::MemoryGrow { rd, .. } => {
1598                let rd_bits = reg_to_bits(rd);
1599                // On embedded, always fail: MOV rd, #-1
1600                0xE3E00000 | (rd_bits << 12) // MVN rd, #0 = MOV rd, #-1
1601            }
1602
1603            // Label pseudo-instruction: emits no machine code
1604            ArmOp::Label { .. } => {
1605                return Ok(Vec::new());
1606            }
1607
1608            // Branch instructions
1609            ArmOp::B { label: _ } => {
1610                // B encoding: cond(4) | 1010 | offset(24)
1611                // Simplified: branch to offset 0 (will be patched by linker/resolver)
1612                0xEA000000
1613            }
1614
1615            // Conditional branch to label (generic)
1616            ArmOp::Bcc { cond, label: _ } => {
1617                use synth_synthesis::Condition;
1618                let cond_bits: u32 = match cond {
1619                    Condition::EQ => 0x0,
1620                    Condition::NE => 0x1,
1621                    Condition::HS => 0x2,
1622                    Condition::LO => 0x3,
1623                    Condition::HI => 0x8,
1624                    Condition::LS => 0x9,
1625                    Condition::GE => 0xA,
1626                    Condition::LT => 0xB,
1627                    Condition::GT => 0xC,
1628                    Condition::LE => 0xD,
1629                };
1630                // B<cond> with offset 0 (will be patched)
1631                (cond_bits << 28) | 0x0A000000
1632            }
1633
1634            // BHS (Branch if Higher or Same) - used for bounds checking
1635            ArmOp::Bhs { label: _ } => {
1636                // BHS encoding: cond(2=HS) | 1010 | offset(24)
1637                0x2A000000 // BHS with offset 0
1638            }
1639
1640            // BLO (Branch if Lower) - complementary to BHS
1641            ArmOp::Blo { label: _ } => {
1642                // BLO encoding: cond(3=LO) | 1010 | offset(24)
1643                0x3A000000 // BLO with offset 0
1644            }
1645
1646            // Branch with numeric offset (in instructions)
1647            // ARM32 B instruction: offset is in instructions, stored as words
1648            // The offset is relative to PC+8 (due to ARM pipeline)
1649            ArmOp::BOffset { offset } => {
1650                // B encoding: cond(4) | 1010 | offset(24)
1651                // Offset is signed, in words (4-byte units)
1652                // ARM adds PC+8 to the offset, so we need to adjust:
1653                // target = PC + 8 + (offset * 4)
1654                // For backward branch of N instructions: offset = -(N + 2)
1655                // wrapping_sub keeps the encoder total under fuzzing (#186): an
1656                // extreme i32::MIN offset would otherwise overflow-panic; for any
1657                // real branch offset this is identical to `- 2`.
1658                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1659                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1660                0xEA000000 | offset_bits
1661            }
1662
1663            // Conditional branch with numeric offset
1664            ArmOp::BCondOffset { cond, offset } => {
1665                use synth_synthesis::Condition;
1666                let cond_bits: u32 = match cond {
1667                    Condition::EQ => 0x0,
1668                    Condition::NE => 0x1,
1669                    Condition::HS => 0x2,
1670                    Condition::LO => 0x3,
1671                    Condition::HI => 0x8,
1672                    Condition::LS => 0x9,
1673                    Condition::GE => 0xA,
1674                    Condition::LT => 0xB,
1675                    Condition::GT => 0xC,
1676                    Condition::LE => 0xD,
1677                };
1678                // B<cond> encoding: cond(4) | 1010 | offset(24)
1679                // wrapping_sub: total under fuzzing (#186), identical for real offsets.
1680                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1681                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1682                (cond_bits << 28) | 0x0A000000 | offset_bits
1683            }
1684
1685            ArmOp::Bl { label: _ } => {
1686                // BL encoding: cond(4) | 1011 | offset(24)
1687                0xEB000000
1688            }
1689
1690            ArmOp::Bx { rm } => {
1691                let rm_bits = reg_to_bits(rm);
1692
1693                // BX encoding: cond(4) | 000100101111111111110001 | Rm(4)
1694                0xE12FFF10 | rm_bits
1695            }
1696
1697            ArmOp::Blx { rm } => {
1698                let rm_bits = reg_to_bits(rm);
1699
1700                // BLX (register) encoding: cond(4) | 000100101111111111110011 | Rm(4)
1701                0xE12FFF30 | rm_bits
1702            }
1703
1704            ArmOp::Push { regs } => {
1705                // STMDB SP!, {regs} encoding: cond(4) | 100100 | 10 | 1101 | register_list(16)
1706                let mut reg_list: u32 = 0;
1707                for r in regs {
1708                    reg_list |= 1 << reg_to_bits(r);
1709                }
1710                0xE92D0000 | reg_list
1711            }
1712
1713            ArmOp::Pop { regs } => {
1714                // LDMIA SP!, {regs} encoding: cond(4) | 100010 | 11 | 1101 | register_list(16)
1715                let mut reg_list: u32 = 0;
1716                for r in regs {
1717                    reg_list |= 1 << reg_to_bits(r);
1718                }
1719                0xE8BD0000 | reg_list
1720            }
1721
1722            ArmOp::Nop => {
1723                // NOP encoding: MOV R0, R0
1724                0xE1A00000
1725            }
1726
1727            ArmOp::Udf { imm } => {
1728                // UDF (Undefined) encoding in ARM: 0xE7F000F0 | (imm12_hi << 8) | imm4_lo
1729                // We only use imm8, so split into imm4_hi and imm4_lo
1730                let imm8 = *imm as u32;
1731                0xE7F000F0 | ((imm8 & 0xF0) << 4) | (imm8 & 0x0F)
1732            }
1733
1734            // #615: handled by the `encode_arm_expanded` early return at the
1735            // top of this function — a real MOV{cond}/MOV pair now, never a
1736            // silent NOP again.
1737            ArmOp::Popcnt { .. } | ArmOp::SetCond { .. } | ArmOp::SelectMove { .. } => {
1738                unreachable!("handled by encode_arm_expanded (#615)")
1739            }
1740
1741            // Verification-only pseudo-ops: `synth-verify`'s ArmSemantics
1742            // models these, but NO codegen path constructs them (the selector
1743            // lowers select/locals/globals/br_table/call to real instruction
1744            // sequences before the encoder). Encoding one as a NOP silently
1745            // dropped the operation (#615 class); a typed Err keeps the
1746            // encoder total (Ok-or-Err, the `encoder_no_panic` contract)
1747            // while making any future reachability LOUD.
1748            ArmOp::Select { .. }
1749            | ArmOp::LocalGet { .. }
1750            | ArmOp::LocalSet { .. }
1751            | ArmOp::LocalTee { .. }
1752            | ArmOp::GlobalGet { .. }
1753            | ArmOp::GlobalSet { .. }
1754            | ArmOp::BrTable { .. }
1755            | ArmOp::Call { .. } => {
1756                return Err(synth_core::Error::synthesis(format!(
1757                    "verification-only pseudo-op {op:?} reached the A32 encoder — \
1758                     codegen lowers it before encoding; refusing to emit a silent NOP (#615)"
1759                )));
1760            }
1761
1762            // #594: CallIndirect is expanded to a real multi-instruction
1763            // sequence by the early return at the top of this function —
1764            // it must NEVER fall through to a silent NOP again.
1765            ArmOp::CallIndirect { .. } => {
1766                unreachable!("CallIndirect handled by encode_arm_call_indirect (#594)")
1767            }
1768
1769            // #615: every i64 op (and I32WrapI64) is expanded to a real A32
1770            // multi-instruction sequence by `encode_arm_expanded` — the
1771            // "encode as NOP for now" era ended with the value silently
1772            // vanishing on `--target cortex-r5`.
1773            ArmOp::I64Add { .. }
1774            | ArmOp::I64Sub { .. }
1775            | ArmOp::I64DivS { .. }
1776            | ArmOp::I64DivU { .. }
1777            | ArmOp::I64RemS { .. }
1778            | ArmOp::I64RemU { .. }
1779            | ArmOp::I64Clz { .. }
1780            | ArmOp::I64Ctz { .. }
1781            | ArmOp::I64Popcnt { .. }
1782            | ArmOp::I64And { .. }
1783            | ArmOp::I64Or { .. }
1784            | ArmOp::I64Xor { .. }
1785            | ArmOp::I64Eqz { .. }
1786            | ArmOp::I64Eq { .. }
1787            | ArmOp::I64Ne { .. }
1788            | ArmOp::I64LtS { .. }
1789            | ArmOp::I64LtU { .. }
1790            | ArmOp::I64LeS { .. }
1791            | ArmOp::I64LeU { .. }
1792            | ArmOp::I64GtS { .. }
1793            | ArmOp::I64GtU { .. }
1794            | ArmOp::I64GeS { .. }
1795            | ArmOp::I64GeU { .. }
1796            | ArmOp::I64Const { .. }
1797            | ArmOp::I64Ldr { .. }
1798            | ArmOp::I64Str { .. }
1799            | ArmOp::I64ExtendI32S { .. }
1800            | ArmOp::I64ExtendI32U { .. }
1801            | ArmOp::I64Extend8S { .. }
1802            | ArmOp::I64Extend16S { .. }
1803            | ArmOp::I64Extend32S { .. }
1804            | ArmOp::I32WrapI64 { .. } => {
1805                unreachable!("handled by encode_arm_expanded (#615)")
1806            }
1807
1808            // f32 VFP single-precision instructions
1809            ArmOp::F32Add { sd, sn, sm } => encode_vfp_3reg(0xEE300A00, sd, sn, sm)?,
1810            ArmOp::F32Sub { sd, sn, sm } => encode_vfp_3reg(0xEE300A40, sd, sn, sm)?,
1811            ArmOp::F32Mul { sd, sn, sm } => encode_vfp_3reg(0xEE200A00, sd, sn, sm)?,
1812            ArmOp::F32Div { sd, sn, sm } => encode_vfp_3reg(0xEE800A00, sd, sn, sm)?,
1813            ArmOp::F32Abs { sd, sm } => encode_vfp_2reg(0xEEB00AC0, sd, sm)?,
1814            ArmOp::F32Neg { sd, sm } => encode_vfp_2reg(0xEEB10A40, sd, sm)?,
1815            ArmOp::F32Sqrt { sd, sm } => encode_vfp_2reg(0xEEB10AC0, sd, sm)?,
1816
1817            // f32 pseudo-ops — multi-instruction sequences
1818            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
1819            ArmOp::F32Ceil { sd, sm } => {
1820                return self.encode_arm_f32_rounding(sd, sm, 0b01); // Round toward +Inf
1821            }
1822            ArmOp::F32Floor { sd, sm } => {
1823                return self.encode_arm_f32_rounding(sd, sm, 0b10); // Round toward -Inf
1824            }
1825            ArmOp::F32Trunc { sd, sm } => {
1826                return self.encode_arm_f32_rounding(sd, sm, 0b11); // VCVT toward zero
1827            }
1828            ArmOp::F32Nearest { sd, sm } => {
1829                return self.encode_arm_f32_rounding(sd, sm, 0b00); // VCVT to nearest
1830            }
1831            ArmOp::F32Min { sd, sn, sm } => {
1832                return self.encode_arm_f32_minmax(sd, sn, sm, true);
1833            }
1834            ArmOp::F32Max { sd, sn, sm } => {
1835                return self.encode_arm_f32_minmax(sd, sn, sm, false);
1836            }
1837            ArmOp::F32Copysign { sd, sn, sm } => {
1838                return self.encode_arm_f32_copysign(sd, sn, sm);
1839            }
1840
1841            // f32 comparisons — multi-instruction: VCMP + VMRS + conditional MOV
1842            ArmOp::F32Eq { rd, sn, sm } => {
1843                return self.encode_arm_f32_compare(rd, sn, sm, 0x0); // EQ
1844            }
1845            ArmOp::F32Ne { rd, sn, sm } => {
1846                return self.encode_arm_f32_compare(rd, sn, sm, 0x1); // NE
1847            }
1848            ArmOp::F32Lt { rd, sn, sm } => {
1849                return self.encode_arm_f32_compare(rd, sn, sm, 0x4); // MI (less than)
1850            }
1851            ArmOp::F32Le { rd, sn, sm } => {
1852                return self.encode_arm_f32_compare(rd, sn, sm, 0x9); // LS (less or same)
1853            }
1854            ArmOp::F32Gt { rd, sn, sm } => {
1855                return self.encode_arm_f32_compare(rd, sn, sm, 0xC); // GT
1856            }
1857            ArmOp::F32Ge { rd, sn, sm } => {
1858                return self.encode_arm_f32_compare(rd, sn, sm, 0xA); // GE
1859            }
1860
1861            // f32 const — multi-instruction: MOVW + MOVT + VMOV
1862            ArmOp::F32Const { sd, value } => {
1863                return self.encode_arm_f32_const(sd, *value);
1864            }
1865
1866            ArmOp::F32Load { sd, addr } => encode_vfp_ldst(0xED900A00, sd, addr)?,
1867            ArmOp::F32Store { sd, addr } => encode_vfp_ldst(0xED800A00, sd, addr)?,
1868
1869            // f32 conversions — multi-instruction sequences
1870            ArmOp::F32ConvertI32S { sd, rm } => {
1871                return self.encode_arm_f32_convert_i32(sd, rm, true);
1872            }
1873            ArmOp::F32ConvertI32U { sd, rm } => {
1874                return self.encode_arm_f32_convert_i32(sd, rm, false);
1875            }
1876            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
1877                return Err(synth_core::Error::synthesis(
1878                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
1879                ));
1880            }
1881            ArmOp::F32ReinterpretI32 { sd, rm } => encode_vmov_core_sreg(true, sd, rm)?,
1882            ArmOp::I32ReinterpretF32 { rd, sm } => encode_vmov_core_sreg(false, sm, rd)?,
1883            ArmOp::I32TruncF32S { rd, sm } => {
1884                return self.encode_arm_i32_trunc_f32(rd, sm, true);
1885            }
1886            ArmOp::I32TruncF32U { rd, sm } => {
1887                return self.encode_arm_i32_trunc_f32(rd, sm, false);
1888            }
1889
1890            // f64 VFP double-precision instructions (ARM32)
1891            // F64 arithmetic: same as F32 but with sz=1 (bit 8 = 1, cp11 = 0xB)
1892            ArmOp::F64Add { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B00, dd, dn, dm)?,
1893            ArmOp::F64Sub { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B40, dd, dn, dm)?,
1894            ArmOp::F64Mul { dd, dn, dm } => encode_vfp_3reg_f64(0xEE200B00, dd, dn, dm)?,
1895            ArmOp::F64Div { dd, dn, dm } => encode_vfp_3reg_f64(0xEE800B00, dd, dn, dm)?,
1896            ArmOp::F64Abs { dd, dm } => encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?,
1897            ArmOp::F64Neg { dd, dm } => encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?,
1898            ArmOp::F64Sqrt { dd, dm } => encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?,
1899
1900            // f64 pseudo-ops
1901            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
1902            ArmOp::F64Ceil { dd, dm } => {
1903                return self.encode_arm_f64_rounding(dd, dm, 0b01);
1904            }
1905            ArmOp::F64Floor { dd, dm } => {
1906                return self.encode_arm_f64_rounding(dd, dm, 0b10);
1907            }
1908            ArmOp::F64Trunc { dd, dm } => {
1909                return self.encode_arm_f64_rounding(dd, dm, 0b11);
1910            }
1911            ArmOp::F64Nearest { dd, dm } => {
1912                return self.encode_arm_f64_rounding(dd, dm, 0b00);
1913            }
1914            ArmOp::F64Min { dd, dn, dm } => {
1915                return self.encode_arm_f64_minmax(dd, dn, dm, true);
1916            }
1917            ArmOp::F64Max { dd, dn, dm } => {
1918                return self.encode_arm_f64_minmax(dd, dn, dm, false);
1919            }
1920            ArmOp::F64Copysign { dd, dn, dm } => {
1921                return self.encode_arm_f64_copysign(dd, dn, dm);
1922            }
1923
1924            // f64 comparisons
1925            ArmOp::F64Eq { rd, dn, dm } => {
1926                return self.encode_arm_f64_compare(rd, dn, dm, 0x0);
1927            }
1928            ArmOp::F64Ne { rd, dn, dm } => {
1929                return self.encode_arm_f64_compare(rd, dn, dm, 0x1);
1930            }
1931            ArmOp::F64Lt { rd, dn, dm } => {
1932                return self.encode_arm_f64_compare(rd, dn, dm, 0x4);
1933            }
1934            ArmOp::F64Le { rd, dn, dm } => {
1935                return self.encode_arm_f64_compare(rd, dn, dm, 0x9);
1936            }
1937            ArmOp::F64Gt { rd, dn, dm } => {
1938                return self.encode_arm_f64_compare(rd, dn, dm, 0xC);
1939            }
1940            ArmOp::F64Ge { rd, dn, dm } => {
1941                return self.encode_arm_f64_compare(rd, dn, dm, 0xA);
1942            }
1943
1944            ArmOp::F64Const { dd, value } => {
1945                return self.encode_arm_f64_const(dd, *value);
1946            }
1947
1948            ArmOp::F64Load { dd, addr } => encode_vfp_ldst_f64(0xED900B00, dd, addr)?,
1949            ArmOp::F64Store { dd, addr } => encode_vfp_ldst_f64(0xED800B00, dd, addr)?,
1950
1951            ArmOp::F64ConvertI32S { dd, rm } => {
1952                return self.encode_arm_f64_convert_i32(dd, rm, true);
1953            }
1954            ArmOp::F64ConvertI32U { dd, rm } => {
1955                return self.encode_arm_f64_convert_i32(dd, rm, false);
1956            }
1957            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
1958                return Err(synth_core::Error::synthesis(
1959                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
1960                ));
1961            }
1962            ArmOp::F64PromoteF32 { dd, sm } => {
1963                return self.encode_arm_f64_promote_f32(dd, sm);
1964            }
1965            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => {
1966                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?
1967            }
1968            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => {
1969                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?
1970            }
1971            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
1972                return Err(synth_core::Error::synthesis(
1973                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
1974                ));
1975            }
1976            ArmOp::I32TruncF64S { rd, dm } => {
1977                return self.encode_arm_i32_trunc_f64(rd, dm, true);
1978            }
1979            ArmOp::I32TruncF64U { rd, dm } => {
1980                return self.encode_arm_i32_trunc_f64(rd, dm, false);
1981            }
1982            // #615: multi-instruction i64 sequences — expanded to real A32 by
1983            // `encode_arm_expanded`, no longer "Thumb-2 only" NOPs.
1984            ArmOp::I64SetCond { .. }
1985            | ArmOp::I64SetCondZ { .. }
1986            | ArmOp::I64Mul { .. }
1987            | ArmOp::I64Shl { .. }
1988            | ArmOp::I64ShrS { .. }
1989            | ArmOp::I64ShrU { .. }
1990            | ArmOp::I64Rotl { .. }
1991            | ArmOp::I64Rotr { .. } => {
1992                unreachable!("handled by encode_arm_expanded (#615)")
1993            }
1994
1995            // MVE instructions — Thumb-2 only (Cortex-M55 is always Thumb-2)
1996            ArmOp::MveLoad { .. }
1997            | ArmOp::MveStore { .. }
1998            | ArmOp::MveConst { .. }
1999            | ArmOp::MveAnd { .. }
2000            | ArmOp::MveOrr { .. }
2001            | ArmOp::MveEor { .. }
2002            | ArmOp::MveMvn { .. }
2003            | ArmOp::MveBic { .. }
2004            | ArmOp::MveAddI { .. }
2005            | ArmOp::MveSubI { .. }
2006            | ArmOp::MveMulI { .. }
2007            | ArmOp::MveNegI { .. }
2008            | ArmOp::MveCmpEqI { .. }
2009            | ArmOp::MveCmpNeI { .. }
2010            | ArmOp::MveCmpLtS { .. }
2011            | ArmOp::MveCmpLtU { .. }
2012            | ArmOp::MveCmpGtS { .. }
2013            | ArmOp::MveCmpGtU { .. }
2014            | ArmOp::MveCmpLeS { .. }
2015            | ArmOp::MveCmpLeU { .. }
2016            | ArmOp::MveCmpGeS { .. }
2017            | ArmOp::MveCmpGeU { .. }
2018            | ArmOp::MveDup { .. }
2019            | ArmOp::MveExtractLane { .. }
2020            | ArmOp::MveInsertLane { .. }
2021            | ArmOp::MveAddF32 { .. }
2022            | ArmOp::MveSubF32 { .. }
2023            | ArmOp::MveMulF32 { .. }
2024            | ArmOp::MveNegF32 { .. }
2025            | ArmOp::MveAbsF32 { .. }
2026            | ArmOp::MveCmpEqF32 { .. }
2027            | ArmOp::MveCmpNeF32 { .. }
2028            | ArmOp::MveCmpLtF32 { .. }
2029            | ArmOp::MveCmpLeF32 { .. }
2030            | ArmOp::MveCmpGtF32 { .. }
2031            | ArmOp::MveCmpGeF32 { .. }
2032            | ArmOp::MveDupF32 { .. }
2033            | ArmOp::MveExtractLaneF32 { .. }
2034            | ArmOp::MveReplaceLaneF32 { .. }
2035            | ArmOp::MveDivF32 { .. }
2036            | ArmOp::MveSqrtF32 { .. } => {
2037                // MVE (Helium) is a Thumb-2-only extension (Cortex-M55); there
2038                // is no A32 encoding. The selector only emits MVE ops for
2039                // Thumb targets — a NOP here silently dropped the vector op
2040                // if that invariant ever broke (#615 class). Err keeps the
2041                // encoder total and the failure loud.
2042                return Err(synth_core::Error::synthesis(format!(
2043                    "MVE op {op:?} has no A32 (ARM-mode) encoding — MVE is Thumb-2 only (#615)"
2044                )));
2045            }
2046        };
2047
2048        // ARM32 instructions are little-endian
2049        Ok(instr.to_le_bytes().to_vec())
2050    }
2051
2052    // === ARM32 VFP multi-instruction helpers ===
2053
2054    /// Encode F32 comparison as ARM32: VCMP.F32 + VMRS + MOV rd,#0 + MOVcond rd,#1
2055    fn encode_arm_f32_compare(
2056        &self,
2057        rd: &Reg,
2058        sn: &VfpReg,
2059        sm: &VfpReg,
2060        cond_code: u32,
2061    ) -> Result<Vec<u8>> {
2062        let mut bytes = Vec::new();
2063
2064        // VCMP.F32 Sn, Sm: 0xEEB40A40 with Sn in Vd position, Sm in Vm position
2065        let sn_num = vfp_sreg_to_num(sn)?;
2066        let sm_num = vfp_sreg_to_num(sm)?;
2067        let (vd, d) = encode_sreg(sn_num);
2068        let (vm, m) = encode_sreg(sm_num);
2069        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2070        bytes.extend_from_slice(&vcmp.to_le_bytes());
2071
2072        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
2073        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2074
2075        // MOV rd, #0: 0xE3A0_0000 | (rd << 12)
2076        let rd_bits = reg_to_bits(rd);
2077        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2078        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2079
2080        // MOVcond rd, #1: cond(4) | 0011 1010 0000 rd(4) 0000 0000 0001
2081        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2082        bytes.extend_from_slice(&mov_one.to_le_bytes());
2083
2084        Ok(bytes)
2085    }
2086
2087    /// Encode F32 constant load as ARM32: MOVW Rt,#lo16 + MOVT Rt,#hi16 + VMOV Sd,Rt
2088    fn encode_arm_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
2089        let mut bytes = Vec::new();
2090        let bits = value.to_bits();
2091
2092        // Use R12 as temp register for constant loading
2093        let rt: u32 = 12; // R12/IP
2094
2095        // MOVW R12, #lo16: 0xE300_C000 | (imm4 << 16) | imm12
2096        let lo16 = bits & 0xFFFF;
2097        let movw = 0xE3000000 | (rt << 12) | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2098        bytes.extend_from_slice(&movw.to_le_bytes());
2099
2100        // MOVT R12, #hi16: 0xE340_C000 | (imm4 << 16) | imm12
2101        let hi16 = (bits >> 16) & 0xFFFF;
2102        let movt = 0xE3400000 | (rt << 12) | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2103        bytes.extend_from_slice(&movt.to_le_bytes());
2104
2105        // VMOV Sd, R12
2106        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
2107        bytes.extend_from_slice(&vmov.to_le_bytes());
2108
2109        Ok(bytes)
2110    }
2111
2112    /// Encode VMOV + VCVT.F32.S32/U32 as ARM32
2113    fn encode_arm_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2114        let mut bytes = Vec::new();
2115
2116        // VMOV Sd, Rm — move integer to VFP register
2117        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
2118        bytes.extend_from_slice(&vmov.to_le_bytes());
2119
2120        // VCVT.F32.S32 Sd, Sd (signed) or VCVT.F32.U32 Sd, Sd (unsigned)
2121        // Base: 0xEEB80A40 (signed) or 0xEEB80AC0 (unsigned)
2122        let sd_num = vfp_sreg_to_num(sd)?;
2123        let (vd, d) = encode_sreg(sd_num);
2124        let (vm, m) = encode_sreg(sd_num); // same register as source
2125        let base = if signed { 0xEEB80A40 } else { 0xEEB80AC0 };
2126        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2127        bytes.extend_from_slice(&vcvt.to_le_bytes());
2128
2129        Ok(bytes)
2130    }
2131
2132    /// Encode F32 rounding pseudo-op as ARM32 via VCVT to integer and back.
2133    /// mode: 0b00=nearest, 0b01=floor(-Inf), 0b10=ceil(+Inf), 0b11=trunc(zero)
2134    /// Strategy: VCVT.S32.F32 Sd, Sm (toward zero), then VCVT.F32.S32 Sd, Sd
2135    /// For ceil/floor/nearest, we use VCVTR (round toward mode) + convert back.
2136    /// Simplified: convert to int (toward zero for trunc) then back to float.
2137    /// Encode F32 rounding as ARM32.
2138    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2139    ///
2140    /// For trunc (mode=0b11): uses VCVTR.S32.F32 (always rounds toward zero).
2141    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant
2142    /// which honours FPSCR rmode), then restores FPSCR.
2143    fn encode_arm_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2144        let mut bytes = Vec::new();
2145        let sm_num = vfp_sreg_to_num(sm)?;
2146        let sd_num = vfp_sreg_to_num(sd)?;
2147        let (vd_s, d_s) = encode_sreg(sd_num);
2148        let (vm_s, m_s) = encode_sreg(sm_num);
2149
2150        if mode == 0b11 {
2151            // Trunc (toward zero): VCVTR.S32.F32 — the "R" variant always truncates.
2152            // 0xEEBD0AC0: bit[7]=1 => round toward zero regardless of FPSCR
2153            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2154            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2155        } else {
2156            // ceil/floor/nearest: manipulate FPSCR rounding mode
2157            let rt: u32 = 12; // R12/IP as temp
2158
2159            // VMRS R12, FPSCR
2160            let vmrs = 0xEEF10A10 | (rt << 12);
2161            bytes.extend_from_slice(&vmrs.to_le_bytes());
2162
2163            // BIC R12, R12, #(3 << 22) — clear RMode bits [23:22]
2164            // 3<<22 = 0x00C00000. ARM rotated imm: 0x03 ror 10 (rotation=5, imm8=0x03)
2165            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2166            bytes.extend_from_slice(&bic.to_le_bytes());
2167
2168            // ORR R12, R12, #(mode << 22) — set desired rounding mode
2169            if mode != 0 {
2170                // mode<<22: rotation=5, imm8=mode
2171                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2172                bytes.extend_from_slice(&orr.to_le_bytes());
2173            }
2174
2175            // VMSR FPSCR, R12
2176            let vmsr = 0xEEE10A10 | (rt << 12);
2177            bytes.extend_from_slice(&vmsr.to_le_bytes());
2178
2179            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rounding mode
2180            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2181            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2182
2183            // Restore FPSCR: clear rmode bits back to nearest (default)
2184            bytes.extend_from_slice(&vmrs.to_le_bytes());
2185            bytes.extend_from_slice(&bic.to_le_bytes());
2186            bytes.extend_from_slice(&vmsr.to_le_bytes());
2187        }
2188
2189        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
2190        let (vd2, d2) = encode_sreg(sd_num);
2191        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
2192        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2193
2194        Ok(bytes)
2195    }
2196
2197    /// Encode F32 min/max as ARM32: VCMP + VMRS + conditional VMOV
2198    fn encode_arm_f32_minmax(
2199        &self,
2200        sd: &VfpReg,
2201        sn: &VfpReg,
2202        sm: &VfpReg,
2203        is_min: bool,
2204    ) -> Result<Vec<u8>> {
2205        let mut bytes = Vec::new();
2206        let sn_num = vfp_sreg_to_num(sn)?;
2207        let sm_num = vfp_sreg_to_num(sm)?;
2208        let sd_num = vfp_sreg_to_num(sd)?;
2209
2210        // VMOV Sd, Sn (start with first operand)
2211        let (vd, d) = encode_sreg(sd_num);
2212        let (vn, n) = encode_sreg(sn_num);
2213        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2214        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2215
2216        // VCMP.F32 Sn, Sm
2217        let (vm, m) = encode_sreg(sm_num);
2218        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2219        bytes.extend_from_slice(&vcmp.to_le_bytes());
2220
2221        // VMRS APSR_nzcv, FPSCR
2222        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2223
2224        // For min: if Sn > Sm (GT), use Sm. Condition = GT (0xC)
2225        // For max: if Sn < Sm (MI/LT), use Sm. Condition = MI (0x4)
2226        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2227
2228        // VMOV{cond} Sd, Sm — conditional VMOV
2229        let vmov_cond = (cond << 28) | 0x0EB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2230        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2231
2232        Ok(bytes)
2233    }
2234
2235    /// Encode F32 copysign as ARM32: extract sign from Sm, magnitude from Sn
2236    fn encode_arm_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2237        let mut bytes = Vec::new();
2238
2239        // VMOV R12, Sm (get sign source bits)
2240        let vmov_sm = encode_vmov_core_sreg(false, sm, &Reg::R12)?;
2241        bytes.extend_from_slice(&vmov_sm.to_le_bytes());
2242
2243        // VMOV R0, Sn (get magnitude source bits) — use R0 as temp
2244        let vmov_sn = encode_vmov_core_sreg(false, sn, &Reg::R0)?;
2245        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2246
2247        // AND R12, R12, #0x80000000 (keep only sign bit)
2248        // Thumb-2 constant 0x80000000 needs special encoding; in ARM32 use rotated imm
2249        // 0x80000000 = 0x02 rotated right by 2 (rotation=1, imm8=0x02)
2250        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2251        bytes.extend_from_slice(&and_sign.to_le_bytes());
2252
2253        // BIC R0, R0, #0x80000000 (clear sign bit from magnitude)
2254        // R0 = register 0, so Rn and Rd fields are 0
2255        let bic_sign = 0xE3C00000u32 | (1 << 8) | 0x02;
2256        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2257
2258        // ORR R0, R0, R12 (combine sign + magnitude)
2259        // R0 = register 0, so Rn and Rd fields are 0
2260        let orr = 0xE1800000u32 | 12;
2261        bytes.extend_from_slice(&orr.to_le_bytes());
2262
2263        // VMOV Sd, R0
2264        let vmov_result = encode_vmov_core_sreg(true, sd, &Reg::R0)?;
2265        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2266
2267        Ok(bytes)
2268    }
2269
2270    /// Encode F64 comparison as ARM32: VCMP.F64 + VMRS + MOV rd,#0 + MOVcond rd,#1
2271    fn encode_arm_f64_compare(
2272        &self,
2273        rd: &Reg,
2274        dn: &VfpReg,
2275        dm: &VfpReg,
2276        cond_code: u32,
2277    ) -> Result<Vec<u8>> {
2278        let mut bytes = Vec::new();
2279
2280        // VCMP.F64 Dn, Dm: 0xEEB40B40 with Dn in Vd position, Dm in Vm position
2281        let dn_num = vfp_dreg_to_num(dn)?;
2282        let dm_num = vfp_dreg_to_num(dm)?;
2283        let (vd, d) = encode_dreg(dn_num);
2284        let (vm, m) = encode_dreg(dm_num);
2285        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2286        bytes.extend_from_slice(&vcmp.to_le_bytes());
2287
2288        // VMRS APSR_nzcv, FPSCR
2289        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2290
2291        // MOV rd, #0
2292        let rd_bits = reg_to_bits(rd);
2293        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2294        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2295
2296        // MOVcond rd, #1
2297        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2298        bytes.extend_from_slice(&mov_one.to_le_bytes());
2299
2300        Ok(bytes)
2301    }
2302
2303    /// Encode F64 constant load as ARM32: MOVW + MOVT + MOVW + MOVT + VMOV
2304    fn encode_arm_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
2305        let mut bytes = Vec::new();
2306        let bits = value.to_bits();
2307        let lo32 = bits as u32;
2308        let hi32 = (bits >> 32) as u32;
2309
2310        // Load low 32 bits into R0 (Rd field = 0 for R0)
2311        let lo16 = lo32 & 0xFFFF;
2312        let movw_r0 = 0xE3000000 | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2313        bytes.extend_from_slice(&movw_r0.to_le_bytes());
2314        let hi16 = (lo32 >> 16) & 0xFFFF;
2315        let movt_r0 = 0xE3400000 | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2316        bytes.extend_from_slice(&movt_r0.to_le_bytes());
2317
2318        // Load high 32 bits into R12
2319        let lo16 = hi32 & 0xFFFF;
2320        let movw_r12 = 0xE3000000 | ((lo16 >> 12) << 16) | (12 << 12) | (lo16 & 0xFFF);
2321        bytes.extend_from_slice(&movw_r12.to_le_bytes());
2322        let hi16 = (hi32 >> 16) & 0xFFFF;
2323        let movt_r12 = 0xE3400000 | ((hi16 >> 12) << 16) | (12 << 12) | (hi16 & 0xFFF);
2324        bytes.extend_from_slice(&movt_r12.to_le_bytes());
2325
2326        // VMOV Dd, R0, R12
2327        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
2328        bytes.extend_from_slice(&vmov.to_le_bytes());
2329
2330        Ok(bytes)
2331    }
2332
2333    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as ARM32
2334    fn encode_arm_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2335        let mut bytes = Vec::new();
2336
2337        // Use S0 as intermediate: VMOV S0, Rm
2338        let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
2339        bytes.extend_from_slice(&vmov.to_le_bytes());
2340
2341        // VCVT.F64.S32 Dd, S0 (signed) or VCVT.F64.U32 Dd, S0 (unsigned)
2342        // Base: 0xEEB80B40 (signed) or 0xEEB80BC0 (unsigned)
2343        let dd_num = vfp_dreg_to_num(dd)?;
2344        let (vd, d) = encode_dreg(dd_num);
2345        let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
2346        // S0 is register 0: Vm=0, M=0
2347        let vcvt = base | (d << 22) | (vd << 12);
2348        bytes.extend_from_slice(&vcvt.to_le_bytes());
2349
2350        Ok(bytes)
2351    }
2352
2353    /// Encode VCVT.F64.F32 Dd, Sm as ARM32 (f32 to f64 promotion)
2354    fn encode_arm_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2355        let dd_num = vfp_dreg_to_num(dd)?;
2356        let sm_num = vfp_sreg_to_num(sm)?;
2357        let (vd, d) = encode_dreg(dd_num);
2358        let (vm, m) = encode_sreg(sm_num);
2359
2360        // VCVT.F64.F32 Dd, Sm: 0xEEB70AC0
2361        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
2362        Ok(vcvt.to_le_bytes().to_vec())
2363    }
2364
2365    /// Encode VCVT.S32/U32.F64 Sd, Dm + VMOV Rd, Sd as ARM32
2366    fn encode_arm_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2367        let mut bytes = Vec::new();
2368        let dm_num = vfp_dreg_to_num(dm)?;
2369        let (vm, m) = encode_dreg(dm_num);
2370
2371        // VCVT.S32.F64 S0, Dm (toward zero) or VCVT.U32.F64 S0, Dm
2372        // S0: Vd=0, D=0
2373        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
2374        let vcvt = base | (m << 5) | vm;
2375        bytes.extend_from_slice(&vcvt.to_le_bytes());
2376
2377        // VMOV Rd, S0
2378        let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
2379        bytes.extend_from_slice(&vmov.to_le_bytes());
2380
2381        Ok(bytes)
2382    }
2383
2384    /// Encode F64 rounding pseudo-op as ARM32 via VCVT to integer and back.
2385    /// Encode F64 rounding as ARM32.
2386    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2387    ///
2388    /// For trunc: uses VCVTR.S32.F64 (always truncates).
2389    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F64 (non-R variant),
2390    /// then restores FPSCR.
2391    fn encode_arm_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2392        let mut bytes = Vec::new();
2393        let dm_num = vfp_dreg_to_num(dm)?;
2394        let dd_num = vfp_dreg_to_num(dd)?;
2395        let (vm, m) = encode_dreg(dm_num);
2396        let (vd, d) = encode_dreg(dd_num);
2397
2398        if mode == 0b11 {
2399            // Trunc (toward zero): VCVTR.S32.F64 — bit[7]=1, always truncates
2400            let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
2401            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2402        } else {
2403            // ceil/floor/nearest: manipulate FPSCR rounding mode
2404            let rt: u32 = 12;
2405
2406            // VMRS R12, FPSCR
2407            let vmrs = 0xEEF10A10 | (rt << 12);
2408            bytes.extend_from_slice(&vmrs.to_le_bytes());
2409
2410            // BIC R12, R12, #(3 << 22)
2411            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2412            bytes.extend_from_slice(&bic.to_le_bytes());
2413
2414            // ORR R12, R12, #(mode << 22)
2415            if mode != 0 {
2416                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2417                bytes.extend_from_slice(&orr.to_le_bytes());
2418            }
2419
2420            // VMSR FPSCR, R12
2421            let vmsr = 0xEEE10A10 | (rt << 12);
2422            bytes.extend_from_slice(&vmsr.to_le_bytes());
2423
2424            // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0), uses FPSCR rmode
2425            let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
2426            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2427
2428            // Restore FPSCR
2429            bytes.extend_from_slice(&vmrs.to_le_bytes());
2430            bytes.extend_from_slice(&bic.to_le_bytes());
2431            bytes.extend_from_slice(&vmsr.to_le_bytes());
2432        }
2433
2434        // VCVT.F64.S32 Dd, S0 (convert back to double)
2435        let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
2436        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2437
2438        Ok(bytes)
2439    }
2440
2441    /// Encode F64 min/max as ARM32: VMOV + VCMP + VMRS + conditional VMOV
2442    fn encode_arm_f64_minmax(
2443        &self,
2444        dd: &VfpReg,
2445        dn: &VfpReg,
2446        dm: &VfpReg,
2447        is_min: bool,
2448    ) -> Result<Vec<u8>> {
2449        let mut bytes = Vec::new();
2450        let dn_num = vfp_dreg_to_num(dn)?;
2451        let dm_num = vfp_dreg_to_num(dm)?;
2452        let dd_num = vfp_dreg_to_num(dd)?;
2453
2454        // VMOV.F64 Dd, Dn (start with first operand)
2455        let (vd, d) = encode_dreg(dd_num);
2456        let (vn, n) = encode_dreg(dn_num);
2457        let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2458        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2459
2460        // VCMP.F64 Dn, Dm
2461        let (vm, m) = encode_dreg(dm_num);
2462        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2463        bytes.extend_from_slice(&vcmp.to_le_bytes());
2464
2465        // VMRS APSR_nzcv, FPSCR
2466        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2467
2468        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2469        let vmov_cond = (cond << 28) | 0x0EB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2470        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2471
2472        Ok(bytes)
2473    }
2474
2475    /// Encode F64 copysign as ARM32
2476    fn encode_arm_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
2477        let mut bytes = Vec::new();
2478
2479        // VMOV R0, R12, Dm (get sign source bits)
2480        let vmov_dm = encode_vmov_core_dreg(false, dm, &Reg::R0, &Reg::R12)?;
2481        bytes.extend_from_slice(&vmov_dm.to_le_bytes());
2482
2483        // VMOV R1, R2, Dn (get magnitude source bits)
2484        // We use R1 (lo) and R2 (hi) for the magnitude
2485        let vmov_dn = encode_vmov_core_dreg(false, dn, &Reg::R1, &Reg::R2)?;
2486        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2487
2488        // AND R12, R12, #0x80000000 (keep only sign bit from hi word)
2489        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2490        bytes.extend_from_slice(&and_sign.to_le_bytes());
2491
2492        // BIC R2, R2, #0x80000000 (clear sign bit from magnitude hi word)
2493        let bic_sign = 0xE3C00000u32 | (2 << 16) | (2 << 12) | (1 << 8) | 0x02;
2494        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2495
2496        // ORR R2, R2, R12 (combine sign + magnitude)
2497        let orr = 0xE1800000u32 | (2 << 16) | (2 << 12) | 12;
2498        bytes.extend_from_slice(&orr.to_le_bytes());
2499
2500        // VMOV Dd, R1, R2
2501        let vmov_result = encode_vmov_core_dreg(true, dd, &Reg::R1, &Reg::R2)?;
2502        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2503
2504        Ok(bytes)
2505    }
2506
2507    /// Encode VCVT.S32/U32.F32 + VMOV as ARM32
2508    fn encode_arm_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2509        let mut bytes = Vec::new();
2510
2511        // VCVT.S32.F32 Sd, Sm (toward zero) or VCVT.U32.F32 Sd, Sm
2512        // We use Sm as both source and destination for the intermediate result
2513        let sm_num = vfp_sreg_to_num(sm)?;
2514        let (vd, d) = encode_sreg(sm_num);
2515        let (vm, m) = encode_sreg(sm_num);
2516        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
2517        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2518        bytes.extend_from_slice(&vcvt.to_le_bytes());
2519
2520        // VMOV Rd, Sm — move result back to core register
2521        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
2522        bytes.extend_from_slice(&vmov.to_le_bytes());
2523
2524        Ok(bytes)
2525    }
2526
2527    /// Encode an ARM instruction in Thumb-2 mode (16-bit or 32-bit instructions)
2528    fn encode_thumb(&self, op: &ArmOp) -> Result<Vec<u8>> {
2529        // Thumb-2 supports both 16-bit and 32-bit instructions
2530        // 32-bit instructions are encoded as two 16-bit halfwords (big-endian order)
2531        match op {
2532            // === 16-bit Thumb encodings ===
2533            ArmOp::Add { rd, rn, op2 } => {
2534                let rd_bits = reg_to_bits(rd) as u16;
2535                let rn_bits = reg_to_bits(rn) as u16;
2536
2537                if let Operand2::Reg(rm) = op2 {
2538                    let rm_bits = reg_to_bits(rm) as u16;
2539                    // 16-bit ADDS only has 3-bit register fields (R0-R7). For
2540                    // high registers (e.g. R12, the MemLoad/MemStore base
2541                    // scratch) the bits overflow into adjacent fields, silently
2542                    // corrupting the operands — issue #178/#180: `add ip,ip,r0`
2543                    // was emitted as `adds r4,r5,r1`. Guard on all three regs
2544                    // being low and fall back to 32-bit ADD.W otherwise, exactly
2545                    // as the Sub handler below does.
2546                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2547                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2548                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2549                        Ok(instr.to_le_bytes().to_vec())
2550                    } else {
2551                        // ADD.W Rd, Rn, Rm (32-bit) for high registers
2552                        self.encode_thumb32_add_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                        // ADDS Rd, Rn, #imm3 (16-bit): 0001 110 imm3 Rn Rd
2561                        let instr: u16 = 0x1C00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2562                        Ok(instr.to_le_bytes().to_vec())
2563                    } else {
2564                        // Use 32-bit ADD for larger immediates
2565                        self.encode_thumb32_add(rd, rn, *imm as u32)
2566                    }
2567                } else {
2568                    // Fallback to 32-bit encoding
2569                    self.encode_thumb32_add(rd, rn, 0)
2570                }
2571            }
2572
2573            ArmOp::Sub { rd, rn, op2 } => {
2574                let rd_bits = reg_to_bits(rd) as u16;
2575                let rn_bits = reg_to_bits(rn) as u16;
2576
2577                if let Operand2::Reg(rm) = op2 {
2578                    let rm_bits = reg_to_bits(rm) as u16;
2579                    // 16-bit SUBS can only use low registers (R0-R7)
2580                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2581                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2582                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2583                        Ok(instr.to_le_bytes().to_vec())
2584                    } else {
2585                        // Use 32-bit SUB.W for high registers
2586                        self.encode_thumb32_sub_reg_raw(
2587                            rd_bits as u32,
2588                            rn_bits as u32,
2589                            rm_bits as u32,
2590                        )
2591                    }
2592                } else if let Operand2::Imm(imm) = op2 {
2593                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2594                        // SUBS Rd, Rn, #imm3 (16-bit): 0001 111 imm3 Rn Rd
2595                        let instr: u16 = 0x1E00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2596                        Ok(instr.to_le_bytes().to_vec())
2597                    } else {
2598                        self.encode_thumb32_sub(rd, rn, *imm as u32)
2599                    }
2600                } else {
2601                    self.encode_thumb32_sub(rd, rn, 0)
2602                }
2603            }
2604
2605            ArmOp::Mov { rd, op2 } => {
2606                let rd_bits = reg_to_bits(rd) as u16;
2607
2608                if let Operand2::Imm(imm) = op2 {
2609                    // #498: the old test here was the SIGNED `*imm <= 255`,
2610                    // so a negative immediate (e.g. -1) fell into the 16-bit
2611                    // MOVS arm and encoded the wrong VALUE (#(imm & 0xFF) =
2612                    // #0xFF). A positive imm above 0xFFFF was equally wrong:
2613                    // MOVW truncates to 16 bits. Split on the UNSIGNED value:
2614                    // imm8 → MOVS, imm16 → MOVW, anything wider (negative or
2615                    // >0xFFFF) → the full-value MOVW+MOVT pair. No emitter
2616                    // produces the wide shape today (both selectors
2617                    // materialize wide constants as explicit Movw/Movt or
2618                    // Movw+Mvn), so this is byte-identical on shipped paths —
2619                    // it retires the latent wrong-value encodings the
2620                    // `estimator_encoder_agreement` oracle had pinned.
2621                    let uimm = *imm as u32;
2622                    if uimm <= 255 && rd_bits < 8 {
2623                        // MOVS Rd, #imm8 (16-bit): 0010 0 Rd imm8
2624                        let imm_bits = (*imm as u16) & 0xFF;
2625                        let instr: u16 = 0x2000 | (rd_bits << 8) | imm_bits;
2626                        Ok(instr.to_le_bytes().to_vec())
2627                    } else if uimm <= 0xFFFF {
2628                        // Use 32-bit MOVW for 16-bit immediates
2629                        self.encode_thumb32_movw(rd, uimm)
2630                    } else {
2631                        // Full 32-bit value: MOVW low16 + MOVT high16
2632                        let mut bytes = self.encode_thumb32_movw(rd, uimm & 0xFFFF)?;
2633                        bytes.extend(self.encode_thumb32_movt_raw(reg_to_bits(rd), uimm >> 16)?);
2634                        Ok(bytes)
2635                    }
2636                } else if let Operand2::Reg(rm) = op2 {
2637                    let rm_bits = reg_to_bits(rm) as u16;
2638                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
2639                    // D = Rd[3], Rd[2:0] in lower bits
2640                    let d_bit = (rd_bits >> 3) & 1;
2641                    let instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
2642                    Ok(instr.to_le_bytes().to_vec())
2643                } else {
2644                    let instr: u16 = 0xBF00; // NOP fallback
2645                    Ok(instr.to_le_bytes().to_vec())
2646                }
2647            }
2648
2649            ArmOp::Push { regs } => {
2650                // Thumb-2 PUSH encoding:
2651                // If all regs in R0-R7 + LR, use 16-bit: 1011 010 M rrrrrrrr
2652                // Otherwise use 32-bit: STMDB SP!, {regs} = 1110 1001 0010 1101 | 0M0 reglist(13)
2653                let mut reg_list: u16 = 0;
2654                let mut need_32bit = false;
2655                for r in regs {
2656                    let bit = reg_to_bits(r);
2657                    if bit >= 8 && *r != Reg::LR {
2658                        need_32bit = true;
2659                    }
2660                    reg_list |= 1 << bit;
2661                }
2662                if !need_32bit {
2663                    // 16-bit PUSH: 1011 010 M rrrrrrrr
2664                    let m_bit = if reg_list & (1 << 14) != 0 {
2665                        1u16
2666                    } else {
2667                        0u16
2668                    };
2669                    let low_regs = reg_list & 0xFF;
2670                    let instr: u16 = 0xB400 | (m_bit << 8) | low_regs;
2671                    Ok(instr.to_le_bytes().to_vec())
2672                } else {
2673                    // 32-bit STMDB SP!, {regs}: E92D | reglist(16)
2674                    let hw1: u16 = 0xE92D;
2675                    let hw2: u16 = reg_list;
2676                    let mut bytes = hw1.to_le_bytes().to_vec();
2677                    bytes.extend_from_slice(&hw2.to_le_bytes());
2678                    Ok(bytes)
2679                }
2680            }
2681
2682            ArmOp::Pop { regs } => {
2683                // Thumb-2 POP encoding:
2684                // If all regs in R0-R7 + PC, use 16-bit: 1011 110 P rrrrrrrr
2685                // Otherwise use 32-bit: LDMIA SP!, {regs} = 1110 1000 1011 1101 | PM0 reglist(13)
2686                let mut reg_list: u16 = 0;
2687                let mut need_32bit = false;
2688                for r in regs {
2689                    let bit = reg_to_bits(r);
2690                    if bit >= 8 && *r != Reg::PC {
2691                        need_32bit = true;
2692                    }
2693                    reg_list |= 1 << bit;
2694                }
2695                if !need_32bit {
2696                    // 16-bit POP: 1011 110 P rrrrrrrr
2697                    let p_bit = if reg_list & (1 << 15) != 0 {
2698                        1u16
2699                    } else {
2700                        0u16
2701                    };
2702                    let low_regs = reg_list & 0xFF;
2703                    let instr: u16 = 0xBC00 | (p_bit << 8) | low_regs;
2704                    Ok(instr.to_le_bytes().to_vec())
2705                } else {
2706                    // 32-bit LDMIA SP!, {regs}: E8BD | reglist(16)
2707                    let hw1: u16 = 0xE8BD;
2708                    let hw2: u16 = reg_list;
2709                    let mut bytes = hw1.to_le_bytes().to_vec();
2710                    bytes.extend_from_slice(&hw2.to_le_bytes());
2711                    Ok(bytes)
2712                }
2713            }
2714
2715            ArmOp::Nop => {
2716                let instr: u16 = 0xBF00; // NOP in Thumb-2
2717                Ok(instr.to_le_bytes().to_vec())
2718            }
2719
2720            ArmOp::Udf { imm } => {
2721                // UDF (Undefined) in Thumb-2: 16-bit encoding is 0xDE00 | imm8
2722                // This triggers UsageFault/HardFault, used for WASM traps
2723                let instr: u16 = 0xDE00 | (*imm as u16);
2724                let bytes = instr.to_le_bytes().to_vec();
2725                encoding_contracts::verify_thumb16(&bytes);
2726                Ok(bytes)
2727            }
2728
2729            // i64 support: ADDS, ADC, SUBS, SBC for register pair arithmetic
2730            // ADDS sets flags (carry), ADC uses carry from previous ADDS
2731            ArmOp::Adds { rd, rn, op2 } => {
2732                let rd_bits = reg_to_bits(rd) as u16;
2733                let rn_bits = reg_to_bits(rn) as u16;
2734
2735                if let Operand2::Reg(rm) = op2 {
2736                    let rm_bits = reg_to_bits(rm) as u16;
2737                    // 16-bit ADDS is R0-R7 only; i64 pair allocation can place
2738                    // operands in R8-R11, which would overflow the 3-bit fields
2739                    // and corrupt the operands (#178/#180 class). Guard and fall
2740                    // back to 32-bit ADDS.W for high registers.
2741                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2742                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2743                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2744                        Ok(instr.to_le_bytes().to_vec())
2745                    } else {
2746                        self.encode_thumb32_adds_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 ADDS with immediate
2754                    self.encode_thumb32_adds(rd, rn, 0)
2755                }
2756            }
2757
2758            // ADC: Add with Carry (Thumb-2 32-bit)
2759            // ADC.W Rd, Rn, Rm: EB40 Rn | 00 Rd 00 Rm
2760            ArmOp::Adc { 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                    // ADC.W Rd, Rn, Rm (T2): 1110 1011 0100 Rn | 0 000 Rd 00 00 Rm
2767                    let hw1: u16 = (0xEB40 | 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                    // ADC with immediate - use 32-bit encoding
2775                    let hw1: u16 = (0xF140 | 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            // SUBS sets flags (borrow), SBC uses borrow from previous SUBS
2784            ArmOp::Subs { rd, rn, op2 } => {
2785                let rd_bits = reg_to_bits(rd) as u16;
2786                let rn_bits = reg_to_bits(rn) as u16;
2787
2788                if let Operand2::Reg(rm) = op2 {
2789                    let rm_bits = reg_to_bits(rm) as u16;
2790                    // 16-bit SUBS is R0-R7 only; high-register i64 pair operands
2791                    // would overflow the 3-bit fields (#178/#180 class). Guard
2792                    // and fall back to 32-bit SUBS.W for high registers.
2793                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2794                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2795                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2796                        Ok(instr.to_le_bytes().to_vec())
2797                    } else {
2798                        self.encode_thumb32_subs_reg_raw(
2799                            rd_bits as u32,
2800                            rn_bits as u32,
2801                            rm_bits as u32,
2802                        )
2803                    }
2804                } else {
2805                    // 32-bit Thumb-2 SUBS with immediate
2806                    self.encode_thumb32_subs(rd, rn, 0)
2807                }
2808            }
2809
2810            // SBC: Subtract with Carry (Thumb-2 32-bit)
2811            // SBC.W Rd, Rn, Rm: EB60 Rn | 00 Rd 00 Rm
2812            ArmOp::Sbc { rd, rn, op2 } => {
2813                let rd_bits = reg_to_bits(rd);
2814                let rn_bits = reg_to_bits(rn);
2815
2816                if let Operand2::Reg(rm) = op2 {
2817                    let rm_bits = reg_to_bits(rm);
2818                    // SBC.W Rd, Rn, Rm (T2): 1110 1011 0110 Rn | 0 000 Rd 00 00 Rm
2819                    let hw1: u16 = (0xEB60 | rn_bits) as u16;
2820                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2821
2822                    let mut bytes = hw1.to_le_bytes().to_vec();
2823                    bytes.extend_from_slice(&hw2.to_le_bytes());
2824                    Ok(bytes)
2825                } else {
2826                    // SBC with immediate - use 32-bit encoding
2827                    let hw1: u16 = (0xF160 | rn_bits) as u16;
2828                    let hw2: u16 = (rd_bits << 8) as u16;
2829                    let mut bytes = hw1.to_le_bytes().to_vec();
2830                    bytes.extend_from_slice(&hw2.to_le_bytes());
2831                    Ok(bytes)
2832                }
2833            }
2834
2835            // === 32-bit Thumb-2 encodings ===
2836
2837            // SDIV: 11111011 1001 Rn 1111 Rd 1111 Rm
2838            ArmOp::Sdiv { rd, rn, rm } => {
2839                let rd_bits = reg_to_bits(rd);
2840                let rn_bits = reg_to_bits(rn);
2841                let rm_bits = reg_to_bits(rm);
2842                reg_bits_checked(rd_bits)?;
2843                reg_bits_checked(rn_bits)?;
2844                reg_bits_checked(rm_bits)?;
2845
2846                // Thumb-2 SDIV: FB90 F0F0 | Rn<<16 | Rd<<8 | Rm
2847                // First halfword: 1111 1011 1001 Rn = 0xFB90 | Rn
2848                // Second halfword: 1111 Rd 1111 Rm = 0xF0F0 | Rd<<8 | Rm
2849                let hw1: u16 = (0xFB90 | rn_bits) as u16;
2850                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
2851
2852                // Thumb-2 32-bit instructions: first halfword, then second halfword (little-endian each)
2853                let mut bytes = hw1.to_le_bytes().to_vec();
2854                bytes.extend_from_slice(&hw2.to_le_bytes());
2855                encoding_contracts::verify_thumb32(&bytes);
2856                Ok(bytes)
2857            }
2858
2859            // UDIV: 11111011 1011 Rn 1111 Rd 1111 Rm
2860            ArmOp::Udiv { rd, rn, rm } => {
2861                let rd_bits = reg_to_bits(rd);
2862                let rn_bits = reg_to_bits(rn);
2863                let rm_bits = reg_to_bits(rm);
2864                reg_bits_checked(rd_bits)?;
2865                reg_bits_checked(rn_bits)?;
2866                reg_bits_checked(rm_bits)?;
2867
2868                // Thumb-2 UDIV: FBB0 F0F0 | Rn<<16 | Rd<<8 | Rm
2869                let hw1: u16 = (0xFBB0 | rn_bits) as u16;
2870                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
2871
2872                let mut bytes = hw1.to_le_bytes().to_vec();
2873                bytes.extend_from_slice(&hw2.to_le_bytes());
2874                encoding_contracts::verify_thumb32(&bytes);
2875                Ok(bytes)
2876            }
2877
2878            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
2879                let rdlo_bits = reg_to_bits(rdlo);
2880                let rdhi_bits = reg_to_bits(rdhi);
2881                let rn_bits = reg_to_bits(rn);
2882                let rm_bits = reg_to_bits(rm);
2883                reg_bits_checked(rdlo_bits)?;
2884                reg_bits_checked(rdhi_bits)?;
2885                reg_bits_checked(rn_bits)?;
2886                reg_bits_checked(rm_bits)?;
2887
2888                // Thumb-2 UMULL: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm
2889                let hw1: u16 = (0xFBA0 | rn_bits) as u16;
2890                let hw2: u16 = ((rdlo_bits << 12) | (rdhi_bits << 8) | rm_bits) as u16;
2891
2892                let mut bytes = hw1.to_le_bytes().to_vec();
2893                bytes.extend_from_slice(&hw2.to_le_bytes());
2894                encoding_contracts::verify_thumb32(&bytes);
2895                Ok(bytes)
2896            }
2897
2898            // MUL (Thumb-2 32-bit): MUL Rd, Rn, Rm
2899            ArmOp::Mul { rd, rn, rm } => {
2900                let rd_bits = reg_to_bits(rd);
2901                let rn_bits = reg_to_bits(rn);
2902                let rm_bits = reg_to_bits(rm);
2903
2904                // Thumb-2 MUL: FB00 F000 | Rn | Rd<<8 | Rm
2905                // 11111011 0000 Rn | 1111 Rd 0000 Rm
2906                let hw1: u16 = (0xFB00 | rn_bits) as u16;
2907                let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
2908
2909                let mut bytes = hw1.to_le_bytes().to_vec();
2910                bytes.extend_from_slice(&hw2.to_le_bytes());
2911                Ok(bytes)
2912            }
2913
2914            // MLS: Rd = Ra - Rn * Rm
2915            ArmOp::Mls { rd, rn, rm, ra } => {
2916                let rd_bits = reg_to_bits(rd);
2917                let rn_bits = reg_to_bits(rn);
2918                let rm_bits = reg_to_bits(rm);
2919                let ra_bits = reg_to_bits(ra);
2920
2921                // Thumb-2 MLS: FB00 Rn | Ra Rd 0001 Rm
2922                // 11111011 0000 Rn | Ra Rd 0001 Rm
2923                let hw1: u16 = (0xFB00 | rn_bits) as u16;
2924                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | 0x10 | rm_bits) as u16;
2925
2926                let mut bytes = hw1.to_le_bytes().to_vec();
2927                bytes.extend_from_slice(&hw2.to_le_bytes());
2928                Ok(bytes)
2929            }
2930
2931            ArmOp::Mla { rd, rn, rm, ra } => {
2932                let rd_bits = reg_to_bits(rd);
2933                let rn_bits = reg_to_bits(rn);
2934                let rm_bits = reg_to_bits(rm);
2935                let ra_bits = reg_to_bits(ra);
2936
2937                // Thumb-2 MLA: FB00 Rn | Ra Rd 0000 Rm — same as MLS without the
2938                // bit-4 (0x10) op flag. rd = ra + rn*rm.
2939                let hw1: u16 = (0xFB00 | rn_bits) as u16;
2940                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | rm_bits) as u16;
2941
2942                let mut bytes = hw1.to_le_bytes().to_vec();
2943                bytes.extend_from_slice(&hw2.to_le_bytes());
2944                Ok(bytes)
2945            }
2946
2947            // AND (Thumb-2 32-bit)
2948            ArmOp::And { rd, rn, op2 } => {
2949                if let Operand2::Reg(rm) = op2 {
2950                    let rd_bits = reg_to_bits(rd);
2951                    let rn_bits = reg_to_bits(rn);
2952                    let rm_bits = reg_to_bits(rm);
2953
2954                    // Thumb-2 AND register: EA00 Rn | 0 Rd 00 00 Rm
2955                    let hw1: u16 = (0xEA00 | rn_bits) as u16;
2956                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2957
2958                    let mut bytes = hw1.to_le_bytes().to_vec();
2959                    bytes.extend_from_slice(&hw2.to_le_bytes());
2960                    Ok(bytes)
2961                } else if let Operand2::Imm(imm) = op2 {
2962                    let rd_bits = reg_to_bits(rd);
2963                    let rn_bits = reg_to_bits(rn);
2964
2965                    // Thumb-2 AND.W immediate T1: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8.
2966                    // The i:imm3:imm8 field is a ThumbExpandImm modified immediate —
2967                    // encode it correctly (or error on an un-encodable value)
2968                    // rather than packing raw bits, closing the silent-miscompile
2969                    // class for AND alongside ORR/EOR (#251) / ADD/SUB (#253) /
2970                    // CMP (#255).
2971                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
2972                        synth_core::Error::synthesis(
2973                            "AND immediate is not a valid ThumbExpandImm — materialize into a register",
2974                        )
2975                    })?;
2976                    let i_bit = (field >> 11) & 1;
2977                    let imm3 = (field >> 8) & 0x7;
2978                    let imm8 = field & 0xFF;
2979
2980                    let hw1: u16 = (0xF000 | (i_bit << 10) | rn_bits) as u16;
2981                    let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
2982
2983                    let mut bytes = hw1.to_le_bytes().to_vec();
2984                    bytes.extend_from_slice(&hw2.to_le_bytes());
2985                    Ok(bytes)
2986                } else {
2987                    // RegShift variant - fallback to NOP
2988                    let instr: u16 = 0xBF00;
2989                    Ok(instr.to_le_bytes().to_vec())
2990                }
2991            }
2992
2993            // ORR (Thumb-2 32-bit)
2994            ArmOp::Orr { rd, rn, op2 } => {
2995                if let Operand2::Reg(rm) = op2 {
2996                    let rd_bits = reg_to_bits(rd);
2997                    let rn_bits = reg_to_bits(rn);
2998                    let rm_bits = reg_to_bits(rm);
2999
3000                    // Thumb-2 ORR: EA40 Rn | 0 Rd 00 00 Rm
3001                    let hw1: u16 = (0xEA40 | rn_bits) as u16;
3002                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3003
3004                    let mut bytes = hw1.to_le_bytes().to_vec();
3005                    bytes.extend_from_slice(&hw2.to_le_bytes());
3006                    Ok(bytes)
3007                } else if let Operand2::Imm(imm) = op2 {
3008                    // ORR.W immediate T1: 11110 i 0 0010 S Rn | 0 imm3 Rd imm8.
3009                    // Only the zero-extended byte form (imm <= 0xFF) is encoded;
3010                    // larger modified immediates need ThumbExpandImm — return an
3011                    // error rather than silently emit a NOP (Ok-or-Err, #180/#185).
3012                    let imm_val = *imm as u32;
3013                    if imm_val > 0xFF {
3014                        return Err(synth_core::Error::synthesis(
3015                            "ORR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3016                        ));
3017                    }
3018                    let rd_bits = reg_to_bits(rd);
3019                    let rn_bits = reg_to_bits(rn);
3020                    let hw1: u16 = (0xF040 | rn_bits) as u16;
3021                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3022                    let mut bytes = hw1.to_le_bytes().to_vec();
3023                    bytes.extend_from_slice(&hw2.to_le_bytes());
3024                    Ok(bytes)
3025                } else {
3026                    let instr: u16 = 0xBF00;
3027                    Ok(instr.to_le_bytes().to_vec())
3028                }
3029            }
3030
3031            // EOR (Thumb-2 32-bit)
3032            ArmOp::Eor { rd, rn, op2 } => {
3033                if let Operand2::Reg(rm) = op2 {
3034                    let rd_bits = reg_to_bits(rd);
3035                    let rn_bits = reg_to_bits(rn);
3036                    let rm_bits = reg_to_bits(rm);
3037
3038                    // Thumb-2 EOR: EA80 Rn | 0 Rd 00 00 Rm
3039                    let hw1: u16 = (0xEA80 | rn_bits) as u16;
3040                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3041
3042                    let mut bytes = hw1.to_le_bytes().to_vec();
3043                    bytes.extend_from_slice(&hw2.to_le_bytes());
3044                    Ok(bytes)
3045                } else if let Operand2::Imm(imm) = op2 {
3046                    // EOR.W immediate T1: 11110 i 0 0100 S Rn | 0 imm3 Rd imm8.
3047                    // Byte form only (imm <= 0xFF); larger needs ThumbExpandImm —
3048                    // error, not a silent NOP (Ok-or-Err, #180/#185).
3049                    let imm_val = *imm as u32;
3050                    if imm_val > 0xFF {
3051                        return Err(synth_core::Error::synthesis(
3052                            "EOR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3053                        ));
3054                    }
3055                    let rd_bits = reg_to_bits(rd);
3056                    let rn_bits = reg_to_bits(rn);
3057                    let hw1: u16 = (0xF080 | rn_bits) as u16;
3058                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3059                    let mut bytes = hw1.to_le_bytes().to_vec();
3060                    bytes.extend_from_slice(&hw2.to_le_bytes());
3061                    Ok(bytes)
3062                } else {
3063                    let instr: u16 = 0xBF00;
3064                    Ok(instr.to_le_bytes().to_vec())
3065                }
3066            }
3067
3068            // Shift operations (16-bit for low registers)
3069            ArmOp::Lsl { rd, rn, shift } => {
3070                let rd_bits = reg_to_bits(rd) as u16;
3071                let rn_bits = reg_to_bits(rn) as u16;
3072                let shift_bits = (*shift as u16) & 0x1F;
3073
3074                if rd_bits < 8 && rn_bits < 8 {
3075                    // LSLS Rd, Rm, #imm5 (16-bit): 0000 0 imm5 Rm Rd
3076                    let instr: u16 = (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3077                    Ok(instr.to_le_bytes().to_vec())
3078                } else {
3079                    // Use 32-bit encoding for high registers
3080                    self.encode_thumb32_shift(rd, rn, *shift, 0b00) // LSL type
3081                }
3082            }
3083
3084            ArmOp::Lsr { rd, rn, shift } => {
3085                let rd_bits = reg_to_bits(rd) as u16;
3086                let rn_bits = reg_to_bits(rn) as u16;
3087                let shift_bits = (*shift as u16) & 0x1F;
3088
3089                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3090                    // LSRS Rd, Rm, #imm5 (16-bit): 0000 1 imm5 Rm Rd
3091                    let instr: u16 = 0x0800 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3092                    Ok(instr.to_le_bytes().to_vec())
3093                } else {
3094                    self.encode_thumb32_shift(rd, rn, *shift, 0b01) // LSR type
3095                }
3096            }
3097
3098            ArmOp::Asr { rd, rn, shift } => {
3099                let rd_bits = reg_to_bits(rd) as u16;
3100                let rn_bits = reg_to_bits(rn) as u16;
3101                let shift_bits = (*shift as u16) & 0x1F;
3102
3103                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3104                    // ASRS Rd, Rm, #imm5 (16-bit): 0001 0 imm5 Rm Rd
3105                    let instr: u16 = 0x1000 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3106                    Ok(instr.to_le_bytes().to_vec())
3107                } else {
3108                    self.encode_thumb32_shift(rd, rn, *shift, 0b10) // ASR type
3109                }
3110            }
3111
3112            ArmOp::Ror { rd, rn, shift } => {
3113                // ROR doesn't have a 16-bit immediate form, use 32-bit
3114                self.encode_thumb32_shift(rd, rn, *shift, 0b11) // ROR type
3115            }
3116
3117            // Register-based shifts (Thumb-2 32-bit)
3118            // Encoding: 11111010 0xxS Rn 1111 Rd 0000 Rm
3119            // xx = shift type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
3120            ArmOp::LslReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b00),
3121            ArmOp::LsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b01),
3122            ArmOp::AsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b10),
3123            ArmOp::RorReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b11),
3124
3125            // RSB (Reverse Subtract): Rd = imm - Rn
3126            // Thumb-2 T2 encoding: 11110 i 0 1110 S Rn | 0 imm3 Rd imm8
3127            ArmOp::Rsb { rd, rn, imm } => {
3128                let rd_bits = reg_to_bits(rd);
3129                let rn_bits = reg_to_bits(rn);
3130                let imm_val = *imm;
3131
3132                let i_bit = (imm_val >> 11) & 1;
3133                let imm3 = (imm_val >> 8) & 0x7;
3134                let imm8 = imm_val & 0xFF;
3135
3136                // hw1: 11110 i 01110 0 Rn  (S=0)
3137                let hw1: u16 = (0xF1C0 | (i_bit << 10) | rn_bits) as u16;
3138                // hw2: 0 imm3 Rd imm8
3139                let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3140
3141                let mut bytes = hw1.to_le_bytes().to_vec();
3142                bytes.extend_from_slice(&hw2.to_le_bytes());
3143                Ok(bytes)
3144            }
3145
3146            // CLZ (Thumb-2 32-bit)
3147            ArmOp::Clz { rd, rm } => {
3148                let rd_bits = reg_to_bits(rd);
3149                let rm_bits = reg_to_bits(rm);
3150
3151                // Thumb-2 CLZ: FAB0 Rm | F8 Rd Rm
3152                // 11111010 1011 Rm | 1111 1000 Rd Rm
3153                let hw1: u16 = (0xFAB0 | rm_bits) as u16;
3154                let hw2: u16 = (0xF080 | (rd_bits << 8) | rm_bits) as u16;
3155
3156                let mut bytes = hw1.to_le_bytes().to_vec();
3157                bytes.extend_from_slice(&hw2.to_le_bytes());
3158                Ok(bytes)
3159            }
3160
3161            // RBIT (Thumb-2 32-bit)
3162            ArmOp::Rbit { rd, rm } => {
3163                let rd_bits = reg_to_bits(rd);
3164                let rm_bits = reg_to_bits(rm);
3165
3166                // Thumb-2 RBIT: FA90 Rm | F0 Rd A0 Rm
3167                // 11111010 1001 Rm | 1111 Rd 1010 Rm
3168                let hw1: u16 = (0xFA90 | rm_bits) as u16;
3169                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rm_bits) as u16;
3170
3171                let mut bytes = hw1.to_le_bytes().to_vec();
3172                bytes.extend_from_slice(&hw2.to_le_bytes());
3173                Ok(bytes)
3174            }
3175
3176            // SXTB (16-bit for low registers)
3177            ArmOp::Sxtb { rd, rm } => {
3178                let rd_bits = reg_to_bits(rd) as u16;
3179                let rm_bits = reg_to_bits(rm) as u16;
3180
3181                if rd_bits < 8 && rm_bits < 8 {
3182                    // SXTB Rd, Rm (16-bit): 1011 0010 01 Rm Rd
3183                    let instr: u16 = 0xB240 | (rm_bits << 3) | rd_bits;
3184                    Ok(instr.to_le_bytes().to_vec())
3185                } else {
3186                    // Thumb-2 SXTB.W: FA4F F(rd)80 (rm)
3187                    // 11111010 0100 1111 | 1111 Rd 10 rotate Rm
3188                    let rd_bits32 = rd_bits as u32;
3189                    let rm_bits32 = rm_bits as u32;
3190                    let hw1: u16 = 0xFA4F;
3191                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3192                    let mut bytes = hw1.to_le_bytes().to_vec();
3193                    bytes.extend_from_slice(&hw2.to_le_bytes());
3194                    Ok(bytes)
3195                }
3196            }
3197
3198            // SXTH (16-bit for low registers)
3199            ArmOp::Sxth { rd, rm } => {
3200                let rd_bits = reg_to_bits(rd) as u16;
3201                let rm_bits = reg_to_bits(rm) as u16;
3202
3203                if rd_bits < 8 && rm_bits < 8 {
3204                    // SXTH Rd, Rm (16-bit): 1011 0010 00 Rm Rd
3205                    let instr: u16 = 0xB200 | (rm_bits << 3) | rd_bits;
3206                    Ok(instr.to_le_bytes().to_vec())
3207                } else {
3208                    // Thumb-2 SXTH.W: FA0F F(rd)80 (rm)
3209                    // 11111010 0000 1111 | 1111 Rd 10 rotate Rm
3210                    let rd_bits32 = rd_bits as u32;
3211                    let rm_bits32 = rm_bits as u32;
3212                    let hw1: u16 = 0xFA0F;
3213                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3214                    let mut bytes = hw1.to_le_bytes().to_vec();
3215                    bytes.extend_from_slice(&hw2.to_le_bytes());
3216                    Ok(bytes)
3217                }
3218            }
3219
3220            // UXTB Rd,Rm — zero-extend byte (rd = rm & 0xff)
3221            ArmOp::Uxtb { rd, rm } => {
3222                let rd_bits = reg_to_bits(rd) as u16;
3223                let rm_bits = reg_to_bits(rm) as u16;
3224                if rd_bits < 8 && rm_bits < 8 {
3225                    // UXTB Rd, Rm (16-bit): 1011 0010 11 Rm Rd
3226                    let instr: u16 = 0xB2C0 | (rm_bits << 3) | rd_bits;
3227                    Ok(instr.to_le_bytes().to_vec())
3228                } else {
3229                    // Thumb-2 UXTB.W: FA5F F(rd)80 (rm)
3230                    let hw1: u16 = 0xFA5F;
3231                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3232                    let mut bytes = hw1.to_le_bytes().to_vec();
3233                    bytes.extend_from_slice(&hw2.to_le_bytes());
3234                    Ok(bytes)
3235                }
3236            }
3237
3238            // UXTH Rd,Rm — zero-extend halfword (rd = rm & 0xffff)
3239            ArmOp::Uxth { rd, rm } => {
3240                let rd_bits = reg_to_bits(rd) as u16;
3241                let rm_bits = reg_to_bits(rm) as u16;
3242                if rd_bits < 8 && rm_bits < 8 {
3243                    // UXTH Rd, Rm (16-bit): 1011 0010 10 Rm Rd
3244                    let instr: u16 = 0xB280 | (rm_bits << 3) | rd_bits;
3245                    Ok(instr.to_le_bytes().to_vec())
3246                } else {
3247                    // Thumb-2 UXTH.W: FA1F F(rd)80 (rm)
3248                    let hw1: u16 = 0xFA1F;
3249                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3250                    let mut bytes = hw1.to_le_bytes().to_vec();
3251                    bytes.extend_from_slice(&hw2.to_le_bytes());
3252                    Ok(bytes)
3253                }
3254            }
3255
3256            // CMP (can be 16-bit for low registers)
3257            ArmOp::Cmp { rn, op2 } => {
3258                let rn_bits = reg_to_bits(rn) as u16;
3259
3260                if let Operand2::Imm(imm) = op2 {
3261                    // Only use 16-bit encoding for non-negative immediates 0-255
3262                    // Negative immediates must use 32-bit encoding
3263                    if *imm >= 0 && *imm <= 255 && rn_bits < 8 {
3264                        // CMP Rn, #imm8 (16-bit): 0010 1 Rn imm8
3265                        let instr: u16 = 0x2800 | (rn_bits << 8) | (*imm as u16 & 0xFF);
3266                        Ok(instr.to_le_bytes().to_vec())
3267                    } else {
3268                        self.encode_thumb32_cmp_imm(rn, *imm as u32)
3269                    }
3270                } else if let Operand2::Reg(rm) = op2 {
3271                    let rm_bits = reg_to_bits(rm) as u16;
3272                    if rn_bits < 8 && rm_bits < 8 {
3273                        // CMP Rn, Rm (16-bit low): 0100 0010 10 Rm Rn
3274                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
3275                        Ok(instr.to_le_bytes().to_vec())
3276                    } else {
3277                        // CMP Rn, Rm (16-bit high): 0100 0101 N Rm Rn[2:0]
3278                        let n_bit = (rn_bits >> 3) & 1;
3279                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
3280                        Ok(instr.to_le_bytes().to_vec())
3281                    }
3282                } else {
3283                    let instr: u16 = 0xBF00;
3284                    Ok(instr.to_le_bytes().to_vec())
3285                }
3286            }
3287
3288            // CMN (Compare Negative) - computes Rn + op2 and sets flags
3289            // CMN Rn, #1 sets Z flag if Rn == -1 (since -1 + 1 = 0)
3290            ArmOp::Cmn { rn, op2 } => {
3291                let rn_bits = reg_to_bits(rn) as u16;
3292
3293                if let Operand2::Imm(imm) = op2 {
3294                    // CMN.W Rn, #imm (32-bit): i:imm3:imm8 is a ThumbExpandImm
3295                    // modified immediate (the field sits in imm3=hw2[14:12],
3296                    // imm8=hw2[7:0], i=hw1[10]). Encode it correctly, or error on
3297                    // an un-encodable value — replacing the old silent `0xBF00`
3298                    // NOP (the last of the silent-miscompile data-proc encoders).
3299                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3300                        synth_core::Error::synthesis(
3301                            "CMN immediate is not a valid ThumbExpandImm — materialize into a register",
3302                        )
3303                    })?;
3304                    let i_bit = (field >> 11) & 1;
3305                    let imm3 = (field >> 8) & 0x7;
3306                    let imm8 = field & 0xFF;
3307                    let hw1: u16 = (0xF110 | (i_bit << 10) as u16) | rn_bits;
3308                    let hw2: u16 = (imm3 << 12) as u16 | 0x0F00 | imm8 as u16;
3309                    let mut bytes = hw1.to_le_bytes().to_vec();
3310                    bytes.extend_from_slice(&hw2.to_le_bytes());
3311                    Ok(bytes)
3312                } else if let Operand2::Reg(rm) = op2 {
3313                    let rm_bits = reg_to_bits(rm) as u16;
3314                    // 16-bit CMN (T1) only encodes R0-R7; high registers overflow
3315                    // the 3-bit fields and corrupt the operands (#184, the #180
3316                    // class). CMN has no high-register 16-bit form, so fall back
3317                    // to 32-bit CMN.W (T2): EB10 Rn | 0F00 Rm (ADD.W with S=1 and
3318                    // Rd discarded as PC/1111).
3319                    if rn_bits < 8 && rm_bits < 8 {
3320                        // CMN Rn, Rm (16-bit): 0100 0010 11 Rm Rn
3321                        let instr: u16 = 0x42C0 | (rm_bits << 3) | rn_bits;
3322                        Ok(instr.to_le_bytes().to_vec())
3323                    } else {
3324                        let hw1: u16 = 0xEB10 | rn_bits;
3325                        let hw2: u16 = 0x0F00 | rm_bits;
3326                        let mut bytes = hw1.to_le_bytes().to_vec();
3327                        bytes.extend_from_slice(&hw2.to_le_bytes());
3328                        Ok(bytes)
3329                    }
3330                } else {
3331                    Ok(vec![0xBF, 0x00])
3332                }
3333            }
3334
3335            // LDR (can be 16-bit for simple cases)
3336            ArmOp::Ldr { rd, addr } => {
3337                let rd_bits = reg_to_bits(rd);
3338                let base_bits = reg_to_bits(&addr.base);
3339
3340                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3341                if let Some(offset_reg) = &addr.offset_reg {
3342                    let rm_bits = reg_to_bits(offset_reg);
3343
3344                    // If there's also an immediate offset, we need to ADD it first
3345                    if addr.offset != 0 {
3346                        // Use R12 (IP) as scratch to avoid clobbering the address register
3347                        // ADD R12, Rm, #offset; LDR Rd, [base, R12]
3348                        let scratch = Reg::R12;
3349                        let mut bytes =
3350                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3351                        bytes.extend(self.encode_thumb32_ldr_reg(rd, &addr.base, &scratch)?);
3352                        return Ok(bytes);
3353                    }
3354
3355                    // Simple register offset: LDR Rd, [Rn, Rm]
3356                    // 16-bit: only if Rd, Rn, Rm < R8
3357                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3358                        // LDR Rd, [Rn, Rm] (16-bit): 0101 100 Rm Rn Rd
3359                        let instr: u16 = 0x5800
3360                            | ((rm_bits as u16) << 6)
3361                            | ((base_bits as u16) << 3)
3362                            | (rd_bits as u16);
3363                        return Ok(instr.to_le_bytes().to_vec());
3364                    }
3365
3366                    // 32-bit register offset
3367                    return self.encode_thumb32_ldr_reg(rd, &addr.base, offset_reg);
3368                }
3369
3370                // Immediate offset mode [base, #imm]
3371                let offset = addr.offset as u32;
3372
3373                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3374                    // LDR Rd, [Rn, #imm5*4] (16-bit): 0110 1 imm5 Rn Rd
3375                    let imm5 = (offset >> 2) as u16;
3376                    let instr: u16 =
3377                        0x6800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3378                    Ok(instr.to_le_bytes().to_vec())
3379                } else {
3380                    self.encode_thumb32_ldr(rd, &addr.base, offset)
3381                }
3382            }
3383
3384            // STR (can be 16-bit for simple cases)
3385            ArmOp::Str { rd, addr } => {
3386                let rd_bits = reg_to_bits(rd);
3387                let base_bits = reg_to_bits(&addr.base);
3388
3389                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3390                if let Some(offset_reg) = &addr.offset_reg {
3391                    let rm_bits = reg_to_bits(offset_reg);
3392
3393                    // If there's also an immediate offset, we need to ADD it first
3394                    if addr.offset != 0 {
3395                        // Use R12 (IP) as scratch to avoid clobbering the address register
3396                        // ADD R12, Rm, #offset; STR Rd, [base, R12]
3397                        let scratch = Reg::R12;
3398                        let mut bytes =
3399                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3400                        bytes.extend(self.encode_thumb32_str_reg(rd, &addr.base, &scratch)?);
3401                        return Ok(bytes);
3402                    }
3403
3404                    // Simple register offset: STR Rd, [Rn, Rm]
3405                    // 16-bit: only if Rd, Rn, Rm < R8
3406                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3407                        // STR Rd, [Rn, Rm] (16-bit): 0101 000 Rm Rn Rd
3408                        let instr: u16 = 0x5000
3409                            | ((rm_bits as u16) << 6)
3410                            | ((base_bits as u16) << 3)
3411                            | (rd_bits as u16);
3412                        return Ok(instr.to_le_bytes().to_vec());
3413                    }
3414
3415                    // 32-bit register offset
3416                    return self.encode_thumb32_str_reg(rd, &addr.base, offset_reg);
3417                }
3418
3419                // Immediate offset mode [base, #imm]
3420                let offset = addr.offset as u32;
3421
3422                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3423                    // STR Rd, [Rn, #imm5*4] (16-bit): 0110 0 imm5 Rn Rd
3424                    let imm5 = (offset >> 2) as u16;
3425                    let instr: u16 =
3426                        0x6000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3427                    Ok(instr.to_le_bytes().to_vec())
3428                } else {
3429                    self.encode_thumb32_str(rd, &addr.base, offset)
3430                }
3431            }
3432
3433            // LDRB (Thumb-2)
3434            ArmOp::Ldrb { rd, addr } => {
3435                let rd_bits = reg_to_bits(rd);
3436                let base_bits = reg_to_bits(&addr.base);
3437
3438                if let Some(offset_reg) = &addr.offset_reg {
3439                    if addr.offset != 0 {
3440                        let scratch = Reg::R12;
3441                        let mut bytes =
3442                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3443                        bytes.extend(self.encode_thumb32_ldrb_reg(rd, &addr.base, &scratch)?);
3444                        return Ok(bytes);
3445                    }
3446                    return self.encode_thumb32_ldrb_reg(rd, &addr.base, offset_reg);
3447                }
3448
3449                let offset = addr.offset as u32;
3450                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3451                    // LDRB Rd, [Rn, #imm5] (16-bit): 0111 1 imm5 Rn Rd
3452                    let instr: u16 = 0x7800
3453                        | ((offset as u16) << 6)
3454                        | ((base_bits as u16) << 3)
3455                        | (rd_bits as u16);
3456                    Ok(instr.to_le_bytes().to_vec())
3457                } else {
3458                    self.encode_thumb32_ldrb_imm(rd, &addr.base, offset)
3459                }
3460            }
3461
3462            // LDRSB (Thumb-2)
3463            ArmOp::Ldrsb { rd, addr } => {
3464                let rd_bits = reg_to_bits(rd);
3465                let base_bits = reg_to_bits(&addr.base);
3466
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_ldrsb_reg(rd, &addr.base, &scratch)?);
3473                        return Ok(bytes);
3474                    }
3475                    return self.encode_thumb32_ldrsb_reg(rd, &addr.base, offset_reg);
3476                }
3477
3478                let offset = addr.offset as u32;
3479                // LDRSB has no 16-bit immediate form (only register)
3480                // For 16-bit reg form: only if Rd, Rn, Rm < R8
3481                if rd_bits < 8 && base_bits < 8 && offset == 0 {
3482                    // No immediate 16-bit encoding for LDRSB; use 32-bit
3483                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3484                } else {
3485                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3486                }
3487            }
3488
3489            // LDRH (Thumb-2)
3490            ArmOp::Ldrh { rd, addr } => {
3491                let rd_bits = reg_to_bits(rd);
3492                let base_bits = reg_to_bits(&addr.base);
3493
3494                if let Some(offset_reg) = &addr.offset_reg {
3495                    if addr.offset != 0 {
3496                        let scratch = Reg::R12;
3497                        let mut bytes =
3498                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3499                        bytes.extend(self.encode_thumb32_ldrh_reg(rd, &addr.base, &scratch)?);
3500                        return Ok(bytes);
3501                    }
3502                    return self.encode_thumb32_ldrh_reg(rd, &addr.base, offset_reg);
3503                }
3504
3505                let offset = addr.offset as u32;
3506                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3507                    // LDRH Rd, [Rn, #imm5*2] (16-bit): 1000 1 imm5 Rn Rd
3508                    let imm5 = (offset >> 1) as u16;
3509                    let instr: u16 =
3510                        0x8800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3511                    Ok(instr.to_le_bytes().to_vec())
3512                } else {
3513                    self.encode_thumb32_ldrh_imm(rd, &addr.base, offset)
3514                }
3515            }
3516
3517            // LDRSH (Thumb-2)
3518            ArmOp::Ldrsh { rd, addr } => {
3519                if let Some(offset_reg) = &addr.offset_reg {
3520                    if addr.offset != 0 {
3521                        let scratch = Reg::R12;
3522                        let mut bytes =
3523                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3524                        bytes.extend(self.encode_thumb32_ldrsh_reg(rd, &addr.base, &scratch)?);
3525                        return Ok(bytes);
3526                    }
3527                    return self.encode_thumb32_ldrsh_reg(rd, &addr.base, offset_reg);
3528                }
3529
3530                let offset = addr.offset as u32;
3531                self.encode_thumb32_ldrsh_imm(rd, &addr.base, offset)
3532            }
3533
3534            // STRB (Thumb-2)
3535            ArmOp::Strb { rd, addr } => {
3536                let rd_bits = reg_to_bits(rd);
3537                let base_bits = reg_to_bits(&addr.base);
3538
3539                if let Some(offset_reg) = &addr.offset_reg {
3540                    if addr.offset != 0 {
3541                        let scratch = Reg::R12;
3542                        let mut bytes =
3543                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3544                        bytes.extend(self.encode_thumb32_strb_reg(rd, &addr.base, &scratch)?);
3545                        return Ok(bytes);
3546                    }
3547                    return self.encode_thumb32_strb_reg(rd, &addr.base, offset_reg);
3548                }
3549
3550                let offset = addr.offset as u32;
3551                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3552                    // STRB Rd, [Rn, #imm5] (16-bit): 0111 0 imm5 Rn Rd
3553                    let instr: u16 = 0x7000
3554                        | ((offset as u16) << 6)
3555                        | ((base_bits as u16) << 3)
3556                        | (rd_bits as u16);
3557                    Ok(instr.to_le_bytes().to_vec())
3558                } else {
3559                    self.encode_thumb32_strb_imm(rd, &addr.base, offset)
3560                }
3561            }
3562
3563            // STRH (Thumb-2)
3564            ArmOp::Strh { rd, addr } => {
3565                let rd_bits = reg_to_bits(rd);
3566                let base_bits = reg_to_bits(&addr.base);
3567
3568                if let Some(offset_reg) = &addr.offset_reg {
3569                    if addr.offset != 0 {
3570                        let scratch = Reg::R12;
3571                        let mut bytes =
3572                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3573                        bytes.extend(self.encode_thumb32_strh_reg(rd, &addr.base, &scratch)?);
3574                        return Ok(bytes);
3575                    }
3576                    return self.encode_thumb32_strh_reg(rd, &addr.base, offset_reg);
3577                }
3578
3579                let offset = addr.offset as u32;
3580                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3581                    // STRH Rd, [Rn, #imm5*2] (16-bit): 1000 0 imm5 Rn Rd
3582                    let imm5 = (offset >> 1) as u16;
3583                    let instr: u16 =
3584                        0x8000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3585                    Ok(instr.to_le_bytes().to_vec())
3586                } else {
3587                    self.encode_thumb32_strh_imm(rd, &addr.base, offset)
3588                }
3589            }
3590
3591            // MemorySize (Thumb-2)
3592            ArmOp::MemorySize { rd } => {
3593                // LSR rd, R10, #16 — memory size in bytes / 65536 = pages
3594                // Thumb-2 16-bit: LSRS Rd, Rm, #imm5 — 0000 1 imm5 Rm Rd
3595                let rd_bits = reg_to_bits(rd);
3596                let r10_bits = reg_to_bits(&Reg::R10);
3597                if rd_bits < 8 && r10_bits < 8 {
3598                    let instr: u16 =
3599                        0x0800 | (16u16 << 6) | ((r10_bits as u16) << 3) | (rd_bits as u16);
3600                    Ok(instr.to_le_bytes().to_vec())
3601                } else {
3602                    // Thumb-2 32-bit LSR: 1110 1010 010 0 1111 | 0 imm3 Rd imm2 01 Rm
3603                    let imm5: u32 = 16;
3604                    let imm3 = (imm5 >> 2) & 0x7;
3605                    let imm2 = imm5 & 0x3;
3606                    let hw1: u16 = 0xEA4F;
3607                    let hw2: u16 =
3608                        ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | 0x10 | r10_bits) as u16;
3609                    let mut bytes = hw1.to_le_bytes().to_vec();
3610                    bytes.extend_from_slice(&hw2.to_le_bytes());
3611                    Ok(bytes)
3612                }
3613            }
3614
3615            // MemoryGrow (Thumb-2)
3616            ArmOp::MemoryGrow { rd, .. } => {
3617                // On embedded with fixed memory, always return -1 (failure)
3618                // MVN rd, #0 → MOV rd, #-1
3619                // Thumb-2 32-bit: MVN: 1111 0 i 0 0 0 1 1 0 1111 | 0 imm3 Rd imm8
3620                let rd_bits = reg_to_bits(rd);
3621                let hw1: u16 = 0xF06F; // MVN with i=0
3622                let hw2: u16 = (rd_bits << 8) as u16; // imm8=0 → ~0 = 0xFFFFFFFF = -1
3623                let mut bytes = hw1.to_le_bytes().to_vec();
3624                bytes.extend_from_slice(&hw2.to_le_bytes());
3625                Ok(bytes)
3626            }
3627
3628            // BX (16-bit)
3629            ArmOp::Bx { rm } => {
3630                let rm_bits = reg_to_bits(rm) as u16;
3631                // BX Rm (16-bit): 0100 0111 0 Rm 000
3632                let instr: u16 = 0x4700 | (rm_bits << 3);
3633                Ok(instr.to_le_bytes().to_vec())
3634            }
3635
3636            // BLX (16-bit) - Branch with Link and Exchange
3637            // BLX Rm: 0100 0111 1 Rm 000
3638            ArmOp::Blx { rm } => {
3639                let rm_bits = reg_to_bits(rm) as u16;
3640                let instr: u16 = 0x4780 | (rm_bits << 3);
3641                Ok(instr.to_le_bytes().to_vec())
3642            }
3643
3644            // CallIndirect - indirect function call via table lookup
3645            // table_index_reg contains the table index
3646            // Generates: LSL R12, idx, #2; LDR R12, [R12, table_base]; BLX R12
3647            ArmOp::CallIndirect {
3648                rd: _,
3649                type_idx: _,
3650                table_index_reg,
3651            } => {
3652                let idx_reg = reg_to_bits(table_index_reg);
3653                let mut bytes = Vec::new();
3654
3655                // For now, we generate code that:
3656                // 1. Multiplies index by 4 (function pointer size)
3657                // 2. Loads function pointer from table (assumes table base in R11)
3658                // 3. Calls the function via BLX
3659                //
3660                // Table base setup must be done by caller/runtime.
3661                // This is a simplified implementation - full support needs:
3662                // - Table base address resolution
3663                // - Type signature checking
3664                // - Bounds checking
3665
3666                // LSL R12, idx_reg, #2 (multiply index by 4)
3667                // Thumb-2 MOV with shift: 11101010 010 S 1111 | 0 imm3 Rd imm2 type Rm
3668                // LSL: type=00 (bits 5:4), imm5=2 -> imm3=000, imm2=10 (bits 7:6)
3669                // #597: the shift amount was previously shifted into bits 5:4 —
3670                // the TYPE field — encoding `mov.w ip, rm, ASR #32`, which
3671                // destroyed the index and dispatched table entry 0 for every
3672                // call. imm2 lives at bits 7:6.
3673                let hw1: u16 = 0xEA4F_u16; // MOV.W R12, Rm, LSL #2
3674                let hw2: u16 = ((0x0C00 | (0b10 << 6)) | idx_reg) as u16;
3675                bytes.extend_from_slice(&hw1.to_le_bytes());
3676                bytes.extend_from_slice(&hw2.to_le_bytes());
3677
3678                // LDR R12, [R11, R12] - load function pointer
3679                // Thumb-2 LDR (register): 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
3680                // Rn=R11, Rt=R12, Rm=R12, imm2=00 (no shift)
3681                let ldr_hw1: u16 = 0xF85B; // LDR.W Rt, [R11, Rm]
3682                let ldr_hw2: u16 = 0xC00C; // Rt=R12, imm2=00, Rm=R12
3683                bytes.extend_from_slice(&ldr_hw1.to_le_bytes());
3684                bytes.extend_from_slice(&ldr_hw2.to_le_bytes());
3685
3686                // BLX R12 (call function indirectly)
3687                // BLX Rm (16-bit): 0100 0111 1 Rm 000
3688                let blx: u16 = 0x47E0; // BLX R12
3689                bytes.extend_from_slice(&blx.to_le_bytes());
3690
3691                Ok(bytes)
3692            }
3693
3694            // Label pseudo-instruction: emits no machine code
3695            ArmOp::Label { .. } => Ok(Vec::new()),
3696
3697            // Conditional branch to label (generic) - offset 0, will be patched
3698            ArmOp::Bcc { cond, label: _ } => {
3699                use synth_synthesis::Condition;
3700                let cond_bits: u16 = match cond {
3701                    Condition::EQ => 0x0,
3702                    Condition::NE => 0x1,
3703                    Condition::HS => 0x2,
3704                    Condition::LO => 0x3,
3705                    Condition::HI => 0x8,
3706                    Condition::LS => 0x9,
3707                    Condition::GE => 0xA,
3708                    Condition::LT => 0xB,
3709                    Condition::GT => 0xC,
3710                    Condition::LE => 0xD,
3711                };
3712                // 16-bit B<cond> with offset 0: 1101 cond imm8
3713                let instr: u16 = 0xD000 | (cond_bits << 8);
3714                Ok(instr.to_le_bytes().to_vec())
3715            }
3716
3717            // Branch instructions
3718            ArmOp::B { label: _ } => {
3719                // Simplified: B.N with offset 0
3720                // For real usage, would need label resolution
3721                let instr: u16 = 0xE000; // B.N #0
3722                Ok(instr.to_le_bytes().to_vec())
3723            }
3724
3725            // BHS (Branch if Higher or Same) - used for bounds checking
3726            // Condition code: 0x2 (C set)
3727            ArmOp::Bhs { label: _ } => {
3728                // 16-bit B<cond> with offset 0: 1101 cond imm8
3729                // cond = 0x2 (HS)
3730                let instr: u16 = 0xD200; // BHS.N #0
3731                Ok(instr.to_le_bytes().to_vec())
3732            }
3733
3734            // BLO (Branch if Lower) - complementary to BHS
3735            // Condition code: 0x3 (C clear)
3736            ArmOp::Blo { label: _ } => {
3737                // 16-bit B<cond> with offset 0: 1101 cond imm8
3738                // cond = 0x3 (LO)
3739                let instr: u16 = 0xD300; // BLO.N #0
3740                Ok(instr.to_le_bytes().to_vec())
3741            }
3742
3743            // Branch with numeric offset (Thumb-2)
3744            // Thumb-2 B.W instruction: 32-bit with +-16MB range
3745            ArmOp::BOffset { offset } => {
3746                // offset is already the halfword displacement: (target - branch - 4) / 2
3747                // This is the raw encoded value, accounting for variable-length instructions
3748                let halfword_offset = *offset;
3749
3750                // 16-bit B.N encoding: 1110 0 imm11 (11-bit signed halfword offset)
3751                // Range: -1024 to +1022 halfwords
3752                if (-1024..=1022).contains(&halfword_offset) {
3753                    // 16-bit B.N encoding: 1110 0 imm11
3754                    let imm11 = (halfword_offset as u16) & 0x7FF;
3755                    let instr: u16 = 0xE000 | imm11;
3756                    Ok(instr.to_le_bytes().to_vec())
3757                } else {
3758                    // 32-bit B.W encoding for larger offsets
3759                    // First halfword: 1111 0 S imm10
3760                    // Second halfword: 10 J1 0 J2 imm11
3761                    // Total offset = SignExtend(S:I1:I2:imm10:imm11:0)
3762                    // where I1 = NOT(J1 XOR S), I2 = NOT(J2 XOR S)
3763
3764                    // The B.W (T4) encoding packs the signed offset as:
3765                    //   S:I1:I2:imm10:imm11:0  (25-bit signed, halfword-aligned)
3766                    // where J1 = NOT(I1 XOR S), J2 = NOT(I2 XOR S)
3767                    // Input halfword_offset already equals (target - PC - 4) / 2,
3768                    // so the full byte offset = halfword_offset << 1.
3769                    // The encoding fields split that 25-bit signed value (including the
3770                    // implicit trailing zero) as: S | imm10 | imm11
3771                    // with I1 = bit 23 and I2 = bit 22 of the signed offset.
3772                    let signed_offset = halfword_offset << 1; // byte offset
3773                    let s = if signed_offset < 0 { 1u32 } else { 0u32 };
3774                    let uoffset = signed_offset as u32;
3775                    let imm10 = (uoffset >> 12) & 0x3FF; // bits [21:12]
3776                    let imm11 = (uoffset >> 1) & 0x7FF; // bits [11:1]
3777                    let i1 = (uoffset >> 23) & 1; // bit 23
3778                    let i2 = (uoffset >> 22) & 1; // bit 22
3779                    let j1 = (!(i1 ^ s)) & 1; // J1 = NOT(I1 XOR S)
3780                    let j2 = (!(i2 ^ s)) & 1; // J2 = NOT(I2 XOR S)
3781
3782                    let hw1: u16 = (0xF000 | (s << 10) | imm10) as u16;
3783                    let hw2: u16 = (0x9000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
3784
3785                    let mut bytes = hw1.to_le_bytes().to_vec();
3786                    bytes.extend_from_slice(&hw2.to_le_bytes());
3787                    Ok(bytes)
3788                }
3789            }
3790
3791            // Conditional branch with numeric offset (Thumb-2)
3792            ArmOp::BCondOffset { cond, offset } => {
3793                use synth_synthesis::Condition;
3794                let cond_bits: u16 = match cond {
3795                    Condition::EQ => 0x0,
3796                    Condition::NE => 0x1,
3797                    Condition::HS => 0x2,
3798                    Condition::LO => 0x3,
3799                    Condition::HI => 0x8,
3800                    Condition::LS => 0x9,
3801                    Condition::GE => 0xA,
3802                    Condition::LT => 0xB,
3803                    Condition::GT => 0xC,
3804                    Condition::LE => 0xD,
3805                };
3806
3807                // offset is already the halfword displacement: (target - branch - 4) / 2
3808                // This is the raw imm8 value for 16-bit B<cond> encoding
3809                let halfword_offset = *offset;
3810
3811                // 16-bit B<cond> encoding: 1101 cond imm8
3812                // Range: -256 to +254 halfwords (imm8 is sign-extended and shifted left 1)
3813                if (-128..=127).contains(&halfword_offset) {
3814                    let imm8 = (halfword_offset as u16) & 0xFF;
3815                    let instr: u16 = 0xD000 | (cond_bits << 8) | imm8;
3816                    Ok(instr.to_le_bytes().to_vec())
3817                } else {
3818                    // 32-bit B<cond>.W for larger offsets
3819                    // First halfword: 1111 0 S cond imm6
3820                    // Second halfword: 10 J1 0 J2 imm11
3821                    let offset = halfword_offset >> 1;
3822                    let s = if offset < 0 { 1u32 } else { 0u32 };
3823                    let imm6 = ((offset >> 11) as u32) & 0x3F;
3824                    let imm11 = (offset as u32) & 0x7FF;
3825                    let j1 = if s == 1 { 1 } else { 0 };
3826                    let j2 = if s == 1 { 1 } else { 0 };
3827
3828                    let hw1: u16 = (0xF000 | (s << 10) | ((cond_bits as u32) << 6) | imm6) as u16;
3829                    let hw2: u16 = (0x8000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
3830
3831                    let mut bytes = hw1.to_le_bytes().to_vec();
3832                    bytes.extend_from_slice(&hw2.to_le_bytes());
3833                    Ok(bytes)
3834                }
3835            }
3836
3837            ArmOp::Bl { label: _ } => {
3838                // BL is always 32-bit in Thumb-2, encoded here as a relocatable
3839                // placeholder; an R_ARM_THM_CALL relocation patches the target
3840                // (see arm_backend.rs). The placeholder must carry an embedded
3841                // addend of -4 so the relocation nets to exactly the symbol S.
3842                //
3843                // Thumb BL computes `target = (P + 4) + signed_offset`. Under
3844                // R_ARM_THM_CALL the linker resolves using the in-place addend;
3845                // a 0xF800 placeholder (addend 0) lands at S+4 — every call one
3846                // instruction past the callee entry (#174). The correct
3847                // placeholder is what `gas` emits for `bl <extern>`:
3848                //   f7ff fffe  ->  `bl <self>`  (S=1, J1=J2=1, imm = -4 addend),
3849                // i.e. hw1=0xF7FF, hw2=0xFFFE. This nets to S, not S+4.
3850                // (The earlier 0xD000 was worse still — a ~+0x600000 addend,
3851                // the garbage `bl c0000c` and "truncated to fit" of #167.)
3852                let hw1: u16 = 0xF7FF;
3853                let hw2: u16 = 0xFFFE;
3854                let mut bytes = hw1.to_le_bytes().to_vec();
3855                bytes.extend_from_slice(&hw2.to_le_bytes());
3856                Ok(bytes)
3857            }
3858
3859            // MVN
3860            ArmOp::Mvn { rd, op2 } => {
3861                if let Operand2::Reg(rm) = op2 {
3862                    let rd_bits = reg_to_bits(rd) as u16;
3863                    let rm_bits = reg_to_bits(rm) as u16;
3864
3865                    if rd_bits < 8 && rm_bits < 8 {
3866                        // MVNS Rd, Rm (16-bit): 0100 0011 11 Rm Rd
3867                        let instr: u16 = 0x43C0 | (rm_bits << 3) | rd_bits;
3868                        Ok(instr.to_le_bytes().to_vec())
3869                    } else {
3870                        // 32-bit MVN
3871                        let hw1: u16 = 0xEA6F_u16;
3872                        let hw2: u16 = ((reg_to_bits(rd) << 8) | reg_to_bits(rm)) as u16;
3873                        let mut bytes = hw1.to_le_bytes().to_vec();
3874                        bytes.extend_from_slice(&hw2.to_le_bytes());
3875                        Ok(bytes)
3876                    }
3877                } else {
3878                    let instr: u16 = 0xBF00;
3879                    Ok(instr.to_le_bytes().to_vec())
3880                }
3881            }
3882
3883            // MOVW - Move Wide (Thumb-2 32-bit)
3884            ArmOp::Movw { rd, imm16 } => {
3885                self.encode_thumb32_movw_raw(reg_to_bits(rd), *imm16 as u32)
3886            }
3887
3888            // MOVT - Move Top (Thumb-2 32-bit)
3889            ArmOp::Movt { rd, imm16 } => {
3890                self.encode_thumb32_movt_raw(reg_to_bits(rd), *imm16 as u32)
3891            }
3892
3893            // #237: symbol-relative MOVW/MOVT. Encode the addend's low/high 16
3894            // bits in place; the backend records an R_ARM_MOVW_ABS_NC /
3895            // R_ARM_MOVT_ABS relocation against `symbol`, so the linker adds the
3896            // symbol's final address to the in-place addend (REL semantics).
3897            ArmOp::MovwSym { rd, addend, .. } => {
3898                self.encode_thumb32_movw_raw(reg_to_bits(rd), (*addend as u32) & 0xffff)
3899            }
3900            ArmOp::MovtSym { rd, addend, .. } => {
3901                self.encode_thumb32_movt_raw(reg_to_bits(rd), ((*addend as u32) >> 16) & 0xffff)
3902            }
3903
3904            // #345: literal-pool address load — emit a PLACEHOLDER `LDR.W rd,
3905            // [pc, #0]` (U=1, imm12=0). The backend (arm_backend.rs) places the
3906            // 4-byte pool word at the end of the function, records the R_ARM_ABS32
3907            // relocation against `symbol+addend`, and patches the imm12 with the
3908            // real PC-relative distance once the pool offset is known.
3909            // Encoding T2: 1111 1000 1101 1111 | Rt(4) imm12(12), with the literal
3910            // base = Align(PC,4) and PC = address of this instruction + 4.
3911            ArmOp::LdrSym { rd, .. } => {
3912                let rt = reg_to_bits(rd) as u16;
3913                let hw1: u16 = 0xF8DF; // LDR.W (literal), U=1
3914                let hw2: u16 = rt << 12; // imm12 = 0 placeholder
3915                let mut bytes = Vec::with_capacity(4);
3916                bytes.extend_from_slice(&hw1.to_le_bytes());
3917                bytes.extend_from_slice(&hw2.to_le_bytes());
3918                Ok(bytes)
3919            }
3920
3921            // SetCond: Materialize condition flag into register (0 or 1)
3922            // Strategy: ITE <cond>; MOV Rd, #1; MOV Rd, #0
3923            // IMPORTANT: Must use ITE (If-Then-Else) because 16-bit Thumb MOV
3924            // always sets flags (MOVS). We need to evaluate the condition BEFORE
3925            // any MOV instruction clobbers the flags from CMP.
3926            ArmOp::SetCond { rd, cond } => {
3927                let rd_bits = reg_to_bits(rd) as u16;
3928
3929                // Condition code encoding for IT block
3930                use synth_synthesis::Condition;
3931                let cond_bits: u16 = match cond {
3932                    Condition::EQ => 0x0,
3933                    Condition::NE => 0x1,
3934                    Condition::LT => 0xB,
3935                    Condition::LE => 0xD,
3936                    Condition::GT => 0xC,
3937                    Condition::GE => 0xA,
3938                    Condition::LO => 0x3, // CC/LO (unsigned <)
3939                    Condition::LS => 0x9, // LS (unsigned <=)
3940                    Condition::HI => 0x8, // HI (unsigned >)
3941                    Condition::HS => 0x2, // CS/HS (unsigned >=)
3942                };
3943
3944                // ITE <cond>: encodes If-Then-Else block
3945                // The mask field depends on firstcond[0]:
3946                // - If firstcond[0] = 0: mask = 0xC for TE pattern (ITE EQ = BF0C)
3947                // - If firstcond[0] = 1: mask = 0x4 for TE pattern (ITE NE = BF14)
3948                let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
3949                let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
3950
3951                // Materialize 0/1 into Rd. The 16-bit MOVS (T1) encodes Rd in a
3952                // 3-bit field (bits[10:8]) — only R0–R7. For a high register
3953                // (R8–R12) `rd_bits << 8` overflows into bit 11 and silently
3954                // turns MOVS into CMP (00100 → 00101), corrupting the result
3955                // (this mis-materialized gale's `has_waiter`, so its `local.set`
3956                // stored a stale register → the binary-sem WAKE dispatch read
3957                // garbage). Use the 32-bit MOV.W (T2) for high registers, which
3958                // has a 4-bit Rd field. MOV.W with S=0 doesn't set flags, which
3959                // is fine inside the ITE (the materialized value is the result;
3960                // the flags are not consumed afterwards).
3961                let mut bytes = ite_instr.to_le_bytes().to_vec();
3962                let push_mov = |bytes: &mut Vec<u8>, imm: u16| {
3963                    if rd_bits <= 7 {
3964                        let m: u16 = 0x2000 | (rd_bits << 8) | imm; // 16-bit MOVS Rd,#imm
3965                        bytes.extend_from_slice(&m.to_le_bytes());
3966                    } else {
3967                        // 32-bit MOV.W Rd, #imm (T2): F04F | (Rd<<8) | imm8
3968                        let hw1: u16 = 0xF04F;
3969                        let hw2: u16 = (rd_bits << 8) | imm;
3970                        bytes.extend_from_slice(&hw1.to_le_bytes());
3971                        bytes.extend_from_slice(&hw2.to_le_bytes());
3972                    }
3973                };
3974                push_mov(&mut bytes, 1); // Then branch (condition true)  → 1
3975                push_mov(&mut bytes, 0); // Else branch (condition false) → 0
3976                Ok(bytes)
3977            }
3978
3979            // I64SetCond: Compare two i64 register pairs, result 0/1 in rd
3980            // EQ/NE: CMP lo,lo; IT EQ; CMPEQ hi,hi; ITE <cond>; MOV 1; MOV 0
3981            // LT: CMP lo,lo; SBCS rd,hi,hi; ITE LT; MOV 1; MOV 0
3982            // GT: CMP lo,lo (swapped); SBCS rd,hi,hi (swapped); ITE LT; MOV 1; MOV 0
3983            ArmOp::I64SetCond {
3984                rd,
3985                rn_lo,
3986                rn_hi,
3987                rm_lo,
3988                rm_hi,
3989                cond,
3990            } => {
3991                use synth_synthesis::Condition;
3992                let rd_bits = reg_to_bits(rd) as u16;
3993                let mut bytes = Vec::new();
3994
3995                // Helper: encode CMP Rn, Rm (16-bit)
3996                let encode_cmp_reg = |rn: &synth_synthesis::Reg,
3997                                      rm: &synth_synthesis::Reg|
3998                 -> Vec<u8> {
3999                    let rn_bits = reg_to_bits(rn) as u16;
4000                    let rm_bits = reg_to_bits(rm) as u16;
4001                    if rn_bits < 8 && rm_bits < 8 {
4002                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
4003                        instr.to_le_bytes().to_vec()
4004                    } else {
4005                        let n_bit = (rn_bits >> 3) & 1;
4006                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
4007                        instr.to_le_bytes().to_vec()
4008                    }
4009                };
4010
4011                // Helper: encode ITE <cond> (2 bytes)
4012                let encode_ite = |cond_bits: u16| -> Vec<u8> {
4013                    let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4014                    let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4015                    ite_instr.to_le_bytes().to_vec()
4016                };
4017
4018                // Helper: encode SetCond (ITE + MOV #1 + MOV #0) for given condition
4019                let encode_setcond = |cond_bits: u16, rd_bits: u16| -> Vec<u8> {
4020                    let mut b = encode_ite(cond_bits);
4021                    if rd_bits < 8 {
4022                        let mov_one: u16 = 0x2001 | (rd_bits << 8);
4023                        let mov_zero: u16 = 0x2000 | (rd_bits << 8);
4024                        b.extend_from_slice(&mov_one.to_le_bytes());
4025                        b.extend_from_slice(&mov_zero.to_le_bytes());
4026                    } else {
4027                        // #311: rd >= R8 — the 16-bit MOV imm8 form has a 3-bit
4028                        // rd field; rd_bits<<8 overflows into bit 11 and
4029                        // TRANSMUTES the MOV into CMP (0x2001|0x0800 = 0x2801 =
4030                        // CMP r0,#1): the boolean dies in the flags and the
4031                        // consumer reads a stale register. Use the 32-bit
4032                        // MOV.W (T2: F04F 0000|rd<<8|imm8) — IT-legal,
4033                        // flag-preserving. Same class as H-CODE-9 / #180.
4034                        for imm in [1u16, 0u16] {
4035                            let hw1: u16 = 0xF04F;
4036                            let hw2: u16 = (rd_bits << 8) | imm;
4037                            b.extend_from_slice(&hw1.to_le_bytes());
4038                            b.extend_from_slice(&hw2.to_le_bytes());
4039                        }
4040                    }
4041                    b
4042                };
4043
4044                match cond {
4045                    Condition::EQ | Condition::NE => {
4046                        // CMP rn_lo, rm_lo (compare low words)
4047                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4048
4049                        // IT EQ (execute next instruction only if Z=1)
4050                        let it_eq: u16 = 0xBF08; // IT EQ: cond=0000, mask=1000
4051                        bytes.extend_from_slice(&it_eq.to_le_bytes());
4052
4053                        // CMPEQ rn_hi, rm_hi (compare high words, only if low equal)
4054                        bytes.extend_from_slice(&encode_cmp_reg(rn_hi, rm_hi));
4055
4056                        // ITE <cond>; MOV rd, #1; MOV rd, #0
4057                        let cond_bits: u16 = match cond {
4058                            Condition::EQ => 0x0,
4059                            Condition::NE => 0x1,
4060                            _ => unreachable!(),
4061                        };
4062                        bytes.extend_from_slice(&encode_setcond(cond_bits, rd_bits));
4063                    }
4064
4065                    Condition::LT => {
4066                        // CMP rn_lo, rm_lo (sets C flag for borrow)
4067                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4068
4069                        // SBCS rd, rn_hi, rm_hi (subtract with carry, sets N,V flags)
4070                        // SBCS.W Rd, Rn, Rm: EB70 Rn | 0000 Rd 0000 Rm
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 LT; MOV rd, #1; MOV rd, #0
4079                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4080                    }
4081
4082                    Condition::GT => {
4083                        // GT(a,b) = LT(b,a): swap operands
4084                        // CMP rm_lo, rn_lo (swapped)
4085                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4086
4087                        // SBCS rd, rm_hi, rn_hi (swapped)
4088                        let rm_hi_bits = reg_to_bits(rm_hi);
4089                        let rn_hi_bits = reg_to_bits(rn_hi);
4090                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4091                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4092                        bytes.extend_from_slice(&hw1.to_le_bytes());
4093                        bytes.extend_from_slice(&hw2.to_le_bytes());
4094
4095                        // ITE LT; MOV rd, #1; MOV rd, #0
4096                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4097                    }
4098
4099                    Condition::LE => {
4100                        // LE(a,b) = !GT(a,b): use GT logic but invert result
4101                        // GT(a,b) = LT(b,a): so we do CMP(b,a) and check LT, then invert
4102                        // CMP rm_lo, rn_lo (swapped, same as GT)
4103                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4104
4105                        // SBCS rd, rm_hi, rn_hi (swapped)
4106                        let rm_hi_bits = reg_to_bits(rm_hi);
4107                        let rn_hi_bits = reg_to_bits(rn_hi);
4108                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4109                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4110                        bytes.extend_from_slice(&hw1.to_le_bytes());
4111                        bytes.extend_from_slice(&hw2.to_le_bytes());
4112
4113                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT, so inverting GT result)
4114                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4115                    }
4116
4117                    Condition::GE => {
4118                        // GE(a,b) = !LT(a,b): use LT logic but invert result
4119                        // CMP rn_lo, rm_lo (same as LT)
4120                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4121
4122                        // SBCS rd, rn_hi, rm_hi (same as LT)
4123                        let rn_hi_bits = reg_to_bits(rn_hi);
4124                        let rm_hi_bits = reg_to_bits(rm_hi);
4125                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4126                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4127                        bytes.extend_from_slice(&hw1.to_le_bytes());
4128                        bytes.extend_from_slice(&hw2.to_le_bytes());
4129
4130                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT)
4131                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4132                    }
4133
4134                    // Unsigned comparisons - same instruction sequence, different conditions
4135                    Condition::LO => {
4136                        // LO (unsigned LT): CMP lo, SBCS hi, check C=0
4137                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4138                        let rn_hi_bits = reg_to_bits(rn_hi);
4139                        let rm_hi_bits = reg_to_bits(rm_hi);
4140                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4141                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4142                        bytes.extend_from_slice(&hw1.to_le_bytes());
4143                        bytes.extend_from_slice(&hw2.to_le_bytes());
4144                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4145                    }
4146
4147                    Condition::HI => {
4148                        // HI (unsigned GT): swap operands and check LO
4149                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4150                        let rm_hi_bits = reg_to_bits(rm_hi);
4151                        let rn_hi_bits = reg_to_bits(rn_hi);
4152                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4153                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4154                        bytes.extend_from_slice(&hw1.to_le_bytes());
4155                        bytes.extend_from_slice(&hw2.to_le_bytes());
4156                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4157                    }
4158
4159                    Condition::LS => {
4160                        // LS (unsigned LE): !(a > b) = !(HI), so do HI and invert
4161                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4162                        let rm_hi_bits = reg_to_bits(rm_hi);
4163                        let rn_hi_bits = reg_to_bits(rn_hi);
4164                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4165                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4166                        bytes.extend_from_slice(&hw1.to_le_bytes());
4167                        bytes.extend_from_slice(&hw2.to_le_bytes());
4168                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4169                    }
4170
4171                    Condition::HS => {
4172                        // HS (unsigned GE): !(a < b) = !(LO)
4173                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4174                        let rn_hi_bits = reg_to_bits(rn_hi);
4175                        let rm_hi_bits = reg_to_bits(rm_hi);
4176                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4177                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4178                        bytes.extend_from_slice(&hw1.to_le_bytes());
4179                        bytes.extend_from_slice(&hw2.to_le_bytes());
4180                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4181                    }
4182                }
4183
4184                Ok(bytes)
4185            }
4186
4187            // I64SetCondZ: Test if i64 register pair is zero, result 0/1 in rd
4188            // ORR.W rd, rn_lo, rn_hi; CMP rd, #0; ITE EQ; MOV 1; MOV 0
4189            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
4190                let rd_bits = reg_to_bits(rd);
4191                let rn_lo_bits = reg_to_bits(rn_lo);
4192                let rn_hi_bits = reg_to_bits(rn_hi);
4193                let mut bytes = Vec::new();
4194
4195                // ORR.W rd, rn_lo, rn_hi: EA40 rn_lo | 0000 rd 0000 rn_hi
4196                let hw1: u16 = (0xEA40 | rn_lo_bits) as u16;
4197                let hw2: u16 = ((rd_bits << 8) | rn_hi_bits) as u16;
4198                bytes.extend_from_slice(&hw1.to_le_bytes());
4199                bytes.extend_from_slice(&hw2.to_le_bytes());
4200
4201                // CMP rd, #0 — 16-bit form only for r0-r7 (3-bit rd field);
4202                // high registers take CMP.W (T2: F1B0|rn 0F00|imm8). This was
4203                // H-CODE-9: rd_bits<<8 overflowing the field compared the
4204                // WRONG register. Same hardening as the #311 SetCond fix.
4205                if rd_bits < 8 {
4206                    let cmp_instr: u16 = 0x2800 | ((rd_bits as u16) << 8);
4207                    bytes.extend_from_slice(&cmp_instr.to_le_bytes());
4208                } else {
4209                    let hw1: u16 = 0xF1B0 | (rd_bits as u16);
4210                    let hw2: u16 = 0x0F00;
4211                    bytes.extend_from_slice(&hw1.to_le_bytes());
4212                    bytes.extend_from_slice(&hw2.to_le_bytes());
4213                }
4214
4215                // ITE EQ; MOV rd, #1; MOV rd, #0 (32-bit MOV.W for rd >= R8,
4216                // #311 — see I64SetCond)
4217                let mask = 0xC_u16; // ITE EQ mask: firstcond[0]=0, mask=0xC
4218                let ite_instr: u16 = 0xBF00 | mask;
4219                bytes.extend_from_slice(&ite_instr.to_le_bytes());
4220                if rd_bits < 8 {
4221                    let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
4222                    let mov_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
4223                    bytes.extend_from_slice(&mov_one.to_le_bytes());
4224                    bytes.extend_from_slice(&mov_zero.to_le_bytes());
4225                } else {
4226                    for imm in [1u16, 0u16] {
4227                        let hw1: u16 = 0xF04F;
4228                        let hw2: u16 = ((rd_bits as u16) << 8) | imm;
4229                        bytes.extend_from_slice(&hw1.to_le_bytes());
4230                        bytes.extend_from_slice(&hw2.to_le_bytes());
4231                    }
4232                }
4233
4234                Ok(bytes)
4235            }
4236
4237            // I64Mul: 64-bit multiply using UMULL + MLA cross products
4238            // Formula: result = (a_lo * b_lo) + ((a_lo * b_hi + a_hi * b_lo) << 32)
4239            // Uses R12 as scratch register
4240            ArmOp::I64Mul {
4241                rd_lo,
4242                rd_hi,
4243                rn_lo,
4244                rn_hi,
4245                rm_lo,
4246                rm_hi,
4247            } => {
4248                let rd_lo_bits = reg_to_bits(rd_lo);
4249                let rd_hi_bits = reg_to_bits(rd_hi);
4250                let rn_lo_bits = reg_to_bits(rn_lo);
4251                let rn_hi_bits = reg_to_bits(rn_hi);
4252                let rm_lo_bits = reg_to_bits(rm_lo);
4253                let rm_hi_bits = reg_to_bits(rm_hi);
4254                let r12: u32 = 12; // IP scratch register
4255                let mut bytes = Vec::new();
4256
4257                // 1. MUL R12, rn_lo, rm_hi  (R12 = a_lo * b_hi)
4258                // Thumb-2 MUL: hw1=0xFB00|Rn, hw2=0xF000|(Rd<<8)|Rm
4259                let hw1: u16 = (0xFB00 | rn_lo_bits) as u16;
4260                let hw2: u16 = (0xF000 | (r12 << 8) | rm_hi_bits) as u16;
4261                bytes.extend_from_slice(&hw1.to_le_bytes());
4262                bytes.extend_from_slice(&hw2.to_le_bytes());
4263
4264                // 2. MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
4265                // Thumb-2 MLA: hw1=0xFB00|Rn, hw2=(Ra<<12)|(Rd<<8)|Rm
4266                let hw1: u16 = (0xFB00 | rn_hi_bits) as u16;
4267                let hw2: u16 = ((r12 << 12) | (r12 << 8) | rm_lo_bits) as u16;
4268                bytes.extend_from_slice(&hw1.to_le_bytes());
4269                bytes.extend_from_slice(&hw2.to_le_bytes());
4270
4271                // 3. UMULL rd_lo, rd_hi, rn_lo, rm_lo  (rd_lo:rd_hi = a_lo * b_lo)
4272                // Thumb-2 UMULL: hw1=0xFBA0|Rn, hw2=(RdLo<<12)|(RdHi<<8)|Rm
4273                let hw1: u16 = (0xFBA0 | rn_lo_bits) as u16;
4274                let hw2: u16 = ((rd_lo_bits << 12) | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4275                bytes.extend_from_slice(&hw1.to_le_bytes());
4276                bytes.extend_from_slice(&hw2.to_le_bytes());
4277
4278                // 4. ADD rd_hi, R12  (rd_hi += cross products)
4279                // 16-bit high reg ADD: 01000100 D Rm Rdn[2:0]
4280                let d_bit = (rd_hi_bits >> 3) & 1;
4281                let add_instr: u16 =
4282                    (0x4400 | (d_bit << 7) | (r12 << 3) | (rd_hi_bits & 0x7)) as u16;
4283                bytes.extend_from_slice(&add_instr.to_le_bytes());
4284
4285                Ok(bytes)
4286            }
4287
4288            // I64Shl: 64-bit shift left with branch for n<32 vs n>=32
4289            // rm_hi (R3) is used as temp register
4290            ArmOp::I64Shl {
4291                rd_lo,
4292                rd_hi,
4293                rn_lo,
4294                rn_hi,
4295                rm_lo,
4296                rm_hi,
4297            } => {
4298                let rd_lo_bits = reg_to_bits(rd_lo);
4299                let rd_hi_bits = reg_to_bits(rd_hi);
4300                let rn_lo_bits = reg_to_bits(rn_lo);
4301                let rn_hi_bits = reg_to_bits(rn_hi);
4302                let rm_lo_bits = reg_to_bits(rm_lo);
4303                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4304                let mut bytes = Vec::new();
4305
4306                // AND.W rm_lo, rm_lo, #63  (mask shift amount to 6 bits)
4307                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4308                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4309                bytes.extend_from_slice(&hw1.to_le_bytes());
4310                bytes.extend_from_slice(&hw2.to_le_bytes());
4311
4312                // SUBS.W rm_hi, rm_lo, #32  (rm_hi = n-32, sets flags)
4313                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4314                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4315                bytes.extend_from_slice(&hw1.to_le_bytes());
4316                bytes.extend_from_slice(&hw2.to_le_bytes());
4317
4318                // BPL .large (branch if n >= 32, offset = +10 halfwords)
4319                let bpl: u16 = 0xD50A;
4320                bytes.extend_from_slice(&bpl.to_le_bytes());
4321
4322                // --- Small shift (n < 32) ---
4323                // RSB.W rm_hi, rm_lo, #32  (rm_hi = 32-n)
4324                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4325                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4326                bytes.extend_from_slice(&hw1.to_le_bytes());
4327                bytes.extend_from_slice(&hw2.to_le_bytes());
4328
4329                // LSR.W rm_hi, rn_lo, rm_hi  (rm_hi = lo >> (32-n), overflow bits)
4330                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4331                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4332                bytes.extend_from_slice(&hw1.to_le_bytes());
4333                bytes.extend_from_slice(&hw2.to_le_bytes());
4334
4335                // LSL.W rd_hi, rn_hi, rm_lo  (hi <<= n)
4336                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4337                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4338                bytes.extend_from_slice(&hw1.to_le_bytes());
4339                bytes.extend_from_slice(&hw2.to_le_bytes());
4340
4341                // ORR.W rd_hi, rd_hi, rm_hi  (hi |= overflow bits from lo)
4342                let hw1: u16 = (0xEA40 | rd_hi_bits) as u16;
4343                let hw2: u16 = ((rd_hi_bits << 8) | rm_hi_bits) as u16;
4344                bytes.extend_from_slice(&hw1.to_le_bytes());
4345                bytes.extend_from_slice(&hw2.to_le_bytes());
4346
4347                // LSL.W rd_lo, rn_lo, rm_lo  (lo <<= n)
4348                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4349                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4350                bytes.extend_from_slice(&hw1.to_le_bytes());
4351                bytes.extend_from_slice(&hw2.to_le_bytes());
4352
4353                // B .done (skip large shift: +2 halfwords)
4354                let b_done: u16 = 0xE002;
4355                bytes.extend_from_slice(&b_done.to_le_bytes());
4356
4357                // --- Large shift (n >= 32) ---
4358                // LSL.W rd_hi, rn_lo, rm_hi  (hi = lo << (n-32))
4359                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4360                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_hi_bits) as u16;
4361                bytes.extend_from_slice(&hw1.to_le_bytes());
4362                bytes.extend_from_slice(&hw2.to_le_bytes());
4363
4364                // MOV rd_lo, #0
4365                let mov_zero: u16 = 0x2000 | ((rd_lo_bits as u16) << 8);
4366                bytes.extend_from_slice(&mov_zero.to_le_bytes());
4367
4368                Ok(bytes) // Total: 38 bytes
4369            }
4370
4371            // I64ShrU: 64-bit logical shift right with branch for n<32 vs n>=32
4372            ArmOp::I64ShrU {
4373                rd_lo,
4374                rd_hi,
4375                rn_lo,
4376                rn_hi,
4377                rm_lo,
4378                rm_hi,
4379            } => {
4380                let rd_lo_bits = reg_to_bits(rd_lo);
4381                let rd_hi_bits = reg_to_bits(rd_hi);
4382                let rn_lo_bits = reg_to_bits(rn_lo);
4383                let rn_hi_bits = reg_to_bits(rn_hi);
4384                let rm_lo_bits = reg_to_bits(rm_lo);
4385                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4386                let mut bytes = Vec::new();
4387
4388                // AND.W rm_lo, rm_lo, #63
4389                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4390                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4391                bytes.extend_from_slice(&hw1.to_le_bytes());
4392                bytes.extend_from_slice(&hw2.to_le_bytes());
4393
4394                // SUBS.W rm_hi, rm_lo, #32
4395                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4396                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4397                bytes.extend_from_slice(&hw1.to_le_bytes());
4398                bytes.extend_from_slice(&hw2.to_le_bytes());
4399
4400                // BPL .large (+10 halfwords)
4401                let bpl: u16 = 0xD50A;
4402                bytes.extend_from_slice(&bpl.to_le_bytes());
4403
4404                // --- Small shift (n < 32) ---
4405                // RSB.W rm_hi, rm_lo, #32  (rm_hi = 32-n)
4406                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4407                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4408                bytes.extend_from_slice(&hw1.to_le_bytes());
4409                bytes.extend_from_slice(&hw2.to_le_bytes());
4410
4411                // LSL.W rm_hi, rn_hi, rm_hi  (rm_hi = hi << (32-n), bits flowing to lo)
4412                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4413                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4414                bytes.extend_from_slice(&hw1.to_le_bytes());
4415                bytes.extend_from_slice(&hw2.to_le_bytes());
4416
4417                // LSR.W rd_lo, rn_lo, rm_lo  (lo >>= n)
4418                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4419                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4420                bytes.extend_from_slice(&hw1.to_le_bytes());
4421                bytes.extend_from_slice(&hw2.to_le_bytes());
4422
4423                // ORR.W rd_lo, rd_lo, rm_hi  (lo |= overflow from hi)
4424                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4425                let hw2: u16 = ((rd_lo_bits << 8) | rm_hi_bits) as u16;
4426                bytes.extend_from_slice(&hw1.to_le_bytes());
4427                bytes.extend_from_slice(&hw2.to_le_bytes());
4428
4429                // LSR.W rd_hi, rn_hi, rm_lo  (hi >>= n, logical)
4430                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4431                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4432                bytes.extend_from_slice(&hw1.to_le_bytes());
4433                bytes.extend_from_slice(&hw2.to_le_bytes());
4434
4435                // B .done (+2 halfwords)
4436                let b_done: u16 = 0xE002;
4437                bytes.extend_from_slice(&b_done.to_le_bytes());
4438
4439                // --- Large shift (n >= 32) ---
4440                // LSR.W rd_lo, rn_hi, rm_hi  (lo = hi >> (n-32))
4441                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4442                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_hi_bits) as u16;
4443                bytes.extend_from_slice(&hw1.to_le_bytes());
4444                bytes.extend_from_slice(&hw2.to_le_bytes());
4445
4446                // MOV rd_hi, #0
4447                let mov_zero: u16 = 0x2000 | ((rd_hi_bits as u16) << 8);
4448                bytes.extend_from_slice(&mov_zero.to_le_bytes());
4449
4450                Ok(bytes) // Total: 38 bytes
4451            }
4452
4453            // I64ShrS: 64-bit arithmetic shift right with branch for n<32 vs n>=32
4454            ArmOp::I64ShrS {
4455                rd_lo,
4456                rd_hi,
4457                rn_lo,
4458                rn_hi,
4459                rm_lo,
4460                rm_hi,
4461            } => {
4462                let rd_lo_bits = reg_to_bits(rd_lo);
4463                let rd_hi_bits = reg_to_bits(rd_hi);
4464                let rn_lo_bits = reg_to_bits(rn_lo);
4465                let rn_hi_bits = reg_to_bits(rn_hi);
4466                let rm_lo_bits = reg_to_bits(rm_lo);
4467                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4468                let mut bytes = Vec::new();
4469
4470                // AND.W rm_lo, rm_lo, #63
4471                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4472                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4473                bytes.extend_from_slice(&hw1.to_le_bytes());
4474                bytes.extend_from_slice(&hw2.to_le_bytes());
4475
4476                // SUBS.W rm_hi, rm_lo, #32
4477                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4478                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4479                bytes.extend_from_slice(&hw1.to_le_bytes());
4480                bytes.extend_from_slice(&hw2.to_le_bytes());
4481
4482                // BPL .large (+10 halfwords)
4483                let bpl: u16 = 0xD50A;
4484                bytes.extend_from_slice(&bpl.to_le_bytes());
4485
4486                // --- Small shift (n < 32) ---
4487                // RSB.W rm_hi, rm_lo, #32
4488                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4489                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4490                bytes.extend_from_slice(&hw1.to_le_bytes());
4491                bytes.extend_from_slice(&hw2.to_le_bytes());
4492
4493                // LSL.W rm_hi, rn_hi, rm_hi  (rm_hi = hi << (32-n), bits flowing to lo)
4494                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4495                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4496                bytes.extend_from_slice(&hw1.to_le_bytes());
4497                bytes.extend_from_slice(&hw2.to_le_bytes());
4498
4499                // LSR.W rd_lo, rn_lo, rm_lo  (lo >>= n, logical for lo word)
4500                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4501                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4502                bytes.extend_from_slice(&hw1.to_le_bytes());
4503                bytes.extend_from_slice(&hw2.to_le_bytes());
4504
4505                // ORR.W rd_lo, rd_lo, rm_hi  (lo |= overflow from hi)
4506                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4507                let hw2: u16 = ((rd_lo_bits << 8) | rm_hi_bits) as u16;
4508                bytes.extend_from_slice(&hw1.to_le_bytes());
4509                bytes.extend_from_slice(&hw2.to_le_bytes());
4510
4511                // ASR.W rd_hi, rn_hi, rm_lo  (hi >>= n, arithmetic/sign-extending)
4512                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
4513                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4514                bytes.extend_from_slice(&hw1.to_le_bytes());
4515                bytes.extend_from_slice(&hw2.to_le_bytes());
4516
4517                // B .done (+3 halfwords, large shift is 8 bytes)
4518                let b_done: u16 = 0xE003;
4519                bytes.extend_from_slice(&b_done.to_le_bytes());
4520
4521                // --- Large shift (n >= 32) ---
4522                // ASR.W rd_lo, rn_hi, rm_hi  (lo = hi >>> (n-32))
4523                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
4524                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_hi_bits) as u16;
4525                bytes.extend_from_slice(&hw1.to_le_bytes());
4526                bytes.extend_from_slice(&hw2.to_le_bytes());
4527
4528                // ASR.W rd_hi, rn_hi, #31  (hi = sign extension, all 0s or all 1s)
4529                // Thumb-2 ASR immediate: hw1=0xEA4F, hw2=imm3:Rd:imm2:10:Rm
4530                // imm5=31=11111 → imm3=111, imm2=11
4531                let hw1: u16 = 0xEA4F;
4532                let hw2: u16 = (0x7000 | (rd_hi_bits << 8) | 0x00E0 | rn_hi_bits) as u16;
4533                bytes.extend_from_slice(&hw1.to_le_bytes());
4534                bytes.extend_from_slice(&hw2.to_le_bytes());
4535
4536                Ok(bytes) // Total: 40 bytes
4537            }
4538
4539            // I64Rotl: 64-bit rotate left (#610 rewrite).
4540            // For n < 32: new_hi = (hi << n) | (lo >> (32-n)), new_lo = (lo << n) | (hi >> (32-n))
4541            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
4542            //
4543            // Fixed-reg core: value in R0:R1, amount in R2, scratch R3 + R12
4544            // (all four saved/marshaled by the #610 fixed-ABI wrapper; the
4545            // pre-#610 expansion wrote through the selector's registers with
4546            // colliding R3/R4 scratch and restored the saved R4 OVER the
4547            // result). Relies on ARM register-shift semantics: amounts >= 32
4548            // yield 0 for LSL/LSR, which makes n = 0 and n = 32 exact.
4549            ArmOp::I64Rotl {
4550                rdlo,
4551                rdhi,
4552                rnlo,
4553                rnhi,
4554                shift,
4555            } => {
4556                let mut bytes = Vec::new();
4557                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
4558
4559                let core: [u16; 35] = [
4560                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
4561                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
4562                    0xD50E, //         BPL    .large        (n >= 32)
4563                    // --- small rotation (n < 32) ---
4564                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
4565                    0xFA20, 0xFC03, // LSR.W  R12, R0, R3   (lo >> (32-n))
4566                    0xFA21, 0xF303, // LSR.W  R3, R1, R3    (hi >> (32-n))
4567                    0xFA01, 0xF102, // LSL.W  R1, R1, R2    (hi << n)
4568                    0xEA41, 0x010C, // ORR.W  R1, R1, R12   (new_hi)
4569                    0xFA00, 0xF002, // LSL.W  R0, R0, R2    (lo << n)
4570                    0xEA40, 0x0003, // ORR.W  R0, R0, R3    (new_lo)
4571                    0xE00E, //         B      .done
4572                    // --- large rotation (n >= 32), R3 = m = n-32 ---
4573                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
4574                    0xFA21, 0xFC02, // LSR.W  R12, R1, R2   (hi >> (64-n))
4575                    0xFA20, 0xF202, // LSR.W  R2, R0, R2    (lo >> (64-n))
4576                    0xFA00, 0xF003, // LSL.W  R0, R0, R3    (lo << m)
4577                    0xFA01, 0xF103, // LSL.W  R1, R1, R3    (hi << m)
4578                    0xEA40, 0x0C0C, // ORR.W  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
4579                    0xEA41, 0x0002, // ORR.W  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
4580                    0x4661, //         MOV    R1, R12       (new_hi into place)
4581                            // .done: result in R0:R1
4582                ];
4583                for hw in core {
4584                    bytes.extend_from_slice(&hw.to_le_bytes());
4585                }
4586
4587                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
4588                Ok(bytes) // Total: 102 bytes
4589            }
4590
4591            // I64Rotr: 64-bit rotate right (#610 rewrite).
4592            // For n < 32: new_lo = (lo >> n) | (hi << (32-n)), new_hi = (hi >> n) | (lo << (32-n))
4593            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
4594            //
4595            // Same fixed-reg core contract as I64Rotl: value in R0:R1, amount
4596            // in R2, scratch R3 + R12, all covered by the fixed-ABI wrapper.
4597            ArmOp::I64Rotr {
4598                rdlo,
4599                rdhi,
4600                rnlo,
4601                rnhi,
4602                shift,
4603            } => {
4604                let mut bytes = Vec::new();
4605                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
4606
4607                let core: [u16; 35] = [
4608                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
4609                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
4610                    0xD50E, //         BPL    .large        (n >= 32)
4611                    // --- small rotation (n < 32) ---
4612                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
4613                    0xFA01, 0xFC03, // LSL.W  R12, R1, R3   (hi << (32-n))
4614                    0xFA00, 0xF303, // LSL.W  R3, R0, R3    (lo << (32-n))
4615                    0xFA20, 0xF002, // LSR.W  R0, R0, R2    (lo >> n)
4616                    0xEA40, 0x000C, // ORR.W  R0, R0, R12   (new_lo)
4617                    0xFA21, 0xF102, // LSR.W  R1, R1, R2    (hi >> n)
4618                    0xEA41, 0x0103, // ORR.W  R1, R1, R3    (new_hi)
4619                    0xE00E, //         B      .done
4620                    // --- large rotation (n >= 32), R3 = m = n-32 ---
4621                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
4622                    0xFA00, 0xFC02, // LSL.W  R12, R0, R2   (lo << (64-n))
4623                    0xFA01, 0xF202, // LSL.W  R2, R1, R2    (hi << (64-n))
4624                    0xFA21, 0xF103, // LSR.W  R1, R1, R3    (hi >> m)
4625                    0xEA41, 0x0C0C, // ORR.W  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
4626                    0xFA20, 0xF103, // LSR.W  R1, R0, R3    (lo >> m)
4627                    0xEA41, 0x0102, // ORR.W  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
4628                    0x4660, //         MOV    R0, R12       (new_lo into place)
4629                            // .done: result in R0:R1
4630                ];
4631                for hw in core {
4632                    bytes.extend_from_slice(&hw.to_le_bytes());
4633                }
4634
4635                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
4636                Ok(bytes) // Total: 102 bytes
4637            }
4638
4639            // I64Clz: Count leading zeros in 64-bit value
4640            // If hi != 0: result = CLZ(hi)
4641            // If hi == 0: result = 32 + CLZ(lo)
4642            //
4643            // Layout (using CMP+BNE approach for consistency):
4644            // 0: CMP.W rnhi, #0 (4 bytes)
4645            // 4: BEQ .hi_zero (2 bytes) - branch forward to offset 14
4646            // 6: CLZ.W rd, rnhi (4 bytes)
4647            // 10: B .done (2 bytes) - branch forward to offset 22
4648            // 12: NOP (2 bytes) - padding for alignment
4649            // 14: .hi_zero: CLZ.W rd, rnlo (4 bytes)
4650            // 18: ADD.W rd, rd, #32 (4 bytes)
4651            // 22: .done
4652            ArmOp::I64Clz { rd, rnlo, rnhi } => {
4653                let rd_bits = reg_to_bits(rd);
4654                let rn_lo_bits = reg_to_bits(rnlo);
4655                let rn_hi_bits = reg_to_bits(rnhi);
4656                let mut bytes = Vec::new();
4657
4658                // CMP.W rnhi, #0 (4 bytes at offset 0)
4659                let hw1: u16 = (0xF1B0 | rn_hi_bits) as u16;
4660                let hw2: u16 = 0x0F00;
4661                bytes.extend_from_slice(&hw1.to_le_bytes());
4662                bytes.extend_from_slice(&hw2.to_le_bytes());
4663
4664                // BEQ .hi_zero (2 bytes at offset 4)
4665                // PC = 4 + 4 = 8, target = 14, offset = 6, imm8 = 3
4666                let beq: u16 = 0xD003;
4667                bytes.extend_from_slice(&beq.to_le_bytes());
4668
4669                // CLZ.W rd, rnhi (4 bytes at offset 6)
4670                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4671                let hw1: u16 = (0xFAB0 | rn_hi_bits) as u16;
4672                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_hi_bits) as u16;
4673                bytes.extend_from_slice(&hw1.to_le_bytes());
4674                bytes.extend_from_slice(&hw2.to_le_bytes());
4675
4676                // B .done (2 bytes at offset 10)
4677                // PC = 10 + 4 = 14, target = 22, offset = 8, imm11 = 4
4678                let b_done: u16 = 0xE004;
4679                bytes.extend_from_slice(&b_done.to_le_bytes());
4680
4681                // NOP (2 bytes at offset 12) - padding
4682                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
4683
4684                // .hi_zero: (offset 14)
4685                // CLZ.W rd, rnlo (4 bytes)
4686                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4687                let hw1: u16 = (0xFAB0 | rn_lo_bits) as u16;
4688                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_lo_bits) as u16;
4689                bytes.extend_from_slice(&hw1.to_le_bytes());
4690                bytes.extend_from_slice(&hw2.to_le_bytes());
4691
4692                // ADD.W rd, rd, #32 (4 bytes at offset 18)
4693                let hw1: u16 = (0xF100 | rd_bits) as u16;
4694                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
4695                bytes.extend_from_slice(&hw1.to_le_bytes());
4696                bytes.extend_from_slice(&hw2.to_le_bytes());
4697
4698                // .done: (offset 22)
4699                // i64.clz returns i64, so clear high word: MOV rnhi, #0 (2 bytes)
4700                // MOVS Rn, #0: 0010 0 Rn 00000000
4701                let mov0: u16 = (0x2000 | (rn_hi_bits << 8)) as u16;
4702                bytes.extend_from_slice(&mov0.to_le_bytes());
4703
4704                Ok(bytes)
4705            }
4706
4707            // I64Ctz: Count trailing zeros in 64-bit value
4708            // If lo != 0: result = CTZ(lo) = CLZ(RBIT(lo))
4709            // If lo == 0: result = 32 + CTZ(hi) = 32 + CLZ(RBIT(hi))
4710            //
4711            // Layout:
4712            // 0: CMP.W rnlo, #0 (4 bytes)
4713            // 4: BEQ .lo_zero (2 bytes) - branch to offset 18
4714            // 6: RBIT.W rd, rnlo (4 bytes)
4715            // 10: CLZ.W rd, rd (4 bytes)
4716            // 14: B .done (2 bytes) - branch to offset 30
4717            // 16: NOP (2 bytes) - padding
4718            // 18: .lo_zero: RBIT.W rd, rnhi (4 bytes)
4719            // 22: CLZ.W rd, rd (4 bytes)
4720            // 26: ADD.W rd, rd, #32 (4 bytes)
4721            // 30: .done
4722            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
4723                let rd_bits = reg_to_bits(rd);
4724                let rn_lo_bits = reg_to_bits(rnlo);
4725                let rn_hi_bits = reg_to_bits(rnhi);
4726                let mut bytes = Vec::new();
4727
4728                // CMP.W rnlo, #0 (4 bytes at offset 0)
4729                let hw1: u16 = (0xF1B0 | rn_lo_bits) as u16;
4730                let hw2: u16 = 0x0F00;
4731                bytes.extend_from_slice(&hw1.to_le_bytes());
4732                bytes.extend_from_slice(&hw2.to_le_bytes());
4733
4734                // BEQ .lo_zero (2 bytes at offset 4)
4735                // PC = 4 + 4 = 8, target = 18, offset = 10, imm8 = 5
4736                let beq: u16 = 0xD005;
4737                bytes.extend_from_slice(&beq.to_le_bytes());
4738
4739                // RBIT.W rd, rnlo (4 bytes at offset 6)
4740                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
4741                let hw1: u16 = (0xFA90 | rn_lo_bits) as u16;
4742                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_lo_bits) as u16;
4743                bytes.extend_from_slice(&hw1.to_le_bytes());
4744                bytes.extend_from_slice(&hw2.to_le_bytes());
4745
4746                // CLZ.W rd, rd (4 bytes at offset 10)
4747                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4748                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
4749                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
4750                bytes.extend_from_slice(&hw1.to_le_bytes());
4751                bytes.extend_from_slice(&hw2.to_le_bytes());
4752
4753                // B .done (2 bytes at offset 14)
4754                // PC = 14 + 4 = 18, target = 30, offset = 12, imm11 = 6
4755                let b_done: u16 = 0xE006;
4756                bytes.extend_from_slice(&b_done.to_le_bytes());
4757
4758                // NOP (2 bytes at offset 16) - padding
4759                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
4760
4761                // .lo_zero: (offset 18)
4762                // RBIT.W rd, rnhi (4 bytes)
4763                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
4764                let hw1: u16 = (0xFA90 | rn_hi_bits) as u16;
4765                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_hi_bits) as u16;
4766                bytes.extend_from_slice(&hw1.to_le_bytes());
4767                bytes.extend_from_slice(&hw2.to_le_bytes());
4768
4769                // CLZ.W rd, rd (4 bytes at offset 22)
4770                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4771                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
4772                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
4773                bytes.extend_from_slice(&hw1.to_le_bytes());
4774                bytes.extend_from_slice(&hw2.to_le_bytes());
4775
4776                // ADD.W rd, rd, #32 (4 bytes at offset 26)
4777                let hw1: u16 = (0xF100 | rd_bits) as u16;
4778                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
4779                bytes.extend_from_slice(&hw1.to_le_bytes());
4780                bytes.extend_from_slice(&hw2.to_le_bytes());
4781
4782                // .done: (offset 30)
4783                // i64.ctz returns i64, so clear high word: MOV rnhi, #0 (2 bytes)
4784                let mov0: u16 = (0x2000 | (rn_hi_bits << 8)) as u16;
4785                bytes.extend_from_slice(&mov0.to_le_bytes());
4786
4787                Ok(bytes)
4788            }
4789
4790            // I64Popcnt: Population count of 64-bit value
4791            // result = POPCNT(lo) + POPCNT(hi)
4792            // Using SIMD-style parallel bit counting algorithm
4793            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
4794                let rd_bits = reg_to_bits(rd);
4795                let rn_lo_bits = reg_to_bits(rnlo);
4796                let rn_hi_bits = reg_to_bits(rnhi);
4797                let r12: u32 = 12; // IP scratch
4798                let r3: u32 = 3; // Scratch for hi popcnt result
4799                let mut bytes = Vec::new();
4800
4801                // PUSH {R3, R4, R5} - save scratch registers
4802                bytes.extend_from_slice(&0xB438u16.to_le_bytes());
4803
4804                // Strategy: compute popcnt(lo) -> R4, popcnt(hi) -> R5, add them -> rd
4805                // Using lookup table approach for each byte would be too large
4806                // Using shift-and-add approach instead
4807
4808                // For simplicity and correctness, use the efficient parallel algorithm
4809                // but implement it as a series of inline operations
4810
4811                // Marshal the operand pair into the fixed scratch regs, routing
4812                // rnlo through R12 (#632 audit): writing R4 first corrupted the
4813                // rnhi read for a pair living at (R3,R4) — every source is read
4814                // before any scratch register it could occupy is written.
4815                // MOV R12, rnlo
4816                let mov: u16 = (0x4600 | (1 << 7) | (rn_lo_bits << 3) | 4) as u16;
4817                bytes.extend_from_slice(&mov.to_le_bytes());
4818                // MOV R5, rnhi (R4 untouched so far; rnhi == R5 is a no-op)
4819                let mov: u16 = (0x4600 | (rn_hi_bits << 3) | 5) as u16;
4820                bytes.extend_from_slice(&mov.to_le_bytes());
4821                // MOV R4, R12
4822                bytes.extend_from_slice(&0x4664u16.to_le_bytes());
4823
4824                // --- POPCNT for R4 (lo word) ---
4825                // Step 1: x = x - ((x >> 1) & 0x55555555)
4826                // LSR.W R12, R4, #1
4827                let hw1: u16 = 0xEA4F;
4828                let hw2: u16 = ((r12 << 8) | 0x50 | 4) as u16;
4829                bytes.extend_from_slice(&hw1.to_le_bytes());
4830                bytes.extend_from_slice(&hw2.to_le_bytes());
4831
4832                // Load 0x55555555 into R3 using MOVW/MOVT
4833                // MOVW R3, #0x5555
4834                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
4835                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
4836                // MOVT R3, #0x5555
4837                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
4838                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
4839
4840                // AND.W R12, R12, R3
4841                let hw1: u16 = (0xEA00 | r12) as u16;
4842                let hw2: u16 = ((r12 << 8) | r3) as u16;
4843                bytes.extend_from_slice(&hw1.to_le_bytes());
4844                bytes.extend_from_slice(&hw2.to_le_bytes());
4845
4846                // SUB.W R4, R4, R12
4847                let hw1: u16 = (0xEBA0 | 4) as u16;
4848                let hw2: u16 = ((4 << 8) | r12) as u16;
4849                bytes.extend_from_slice(&hw1.to_le_bytes());
4850                bytes.extend_from_slice(&hw2.to_le_bytes());
4851
4852                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
4853                // Load 0x33333333 into R3
4854                // MOVW R3, #0x3333
4855                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
4856                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
4857                // MOVT R3, #0x3333
4858                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
4859                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
4860
4861                // AND.W R12, R4, R3
4862                let hw1: u16 = (0xEA00 | 4) as u16;
4863                let hw2: u16 = ((r12 << 8) | r3) as u16;
4864                bytes.extend_from_slice(&hw1.to_le_bytes());
4865                bytes.extend_from_slice(&hw2.to_le_bytes());
4866
4867                // LSR.W R4, R4, #2
4868                let hw1: u16 = 0xEA4F;
4869                let hw2: u16 = ((4 << 8) | 0x90 | 4) as u16;
4870                bytes.extend_from_slice(&hw1.to_le_bytes());
4871                bytes.extend_from_slice(&hw2.to_le_bytes());
4872
4873                // AND.W R4, R4, R3
4874                let hw1: u16 = (0xEA00 | 4) as u16;
4875                let hw2: u16 = ((4 << 8) | r3) as u16;
4876                bytes.extend_from_slice(&hw1.to_le_bytes());
4877                bytes.extend_from_slice(&hw2.to_le_bytes());
4878
4879                // ADD.W R4, R4, R12
4880                let hw1: u16 = (0xEB00 | 4) as u16;
4881                let hw2: u16 = ((4 << 8) | r12) as u16;
4882                bytes.extend_from_slice(&hw1.to_le_bytes());
4883                bytes.extend_from_slice(&hw2.to_le_bytes());
4884
4885                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
4886                // LSR.W R12, R4, #4
4887                // hw2 = (imm3 << 12) | (Rd << 8) | (imm2 << 6) | (type << 4) | Rm
4888                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
4889                let hw1: u16 = 0xEA4F;
4890                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 4) as u16;
4891                bytes.extend_from_slice(&hw1.to_le_bytes());
4892                bytes.extend_from_slice(&hw2.to_le_bytes());
4893
4894                // ADD.W R4, R4, R12
4895                let hw1: u16 = (0xEB00 | 4) as u16;
4896                let hw2: u16 = ((4 << 8) | r12) as u16;
4897                bytes.extend_from_slice(&hw1.to_le_bytes());
4898                bytes.extend_from_slice(&hw2.to_le_bytes());
4899
4900                // Load 0x0F0F0F0F into R3
4901                // MOVW R3, #0x0F0F (imm4=0, i=1, imm3=7, imm8=0x0F)
4902                // hw1 = 11110 1 10 0100 0000 = 0xF640
4903                // hw2 = 0 111 0011 00001111 = 0x730F
4904                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
4905                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
4906                // MOVT R3, #0x0F0F
4907                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
4908                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
4909
4910                // AND.W R4, R4, R3
4911                let hw1: u16 = (0xEA00 | 4) as u16;
4912                let hw2: u16 = ((4 << 8) | r3) as u16;
4913                bytes.extend_from_slice(&hw1.to_le_bytes());
4914                bytes.extend_from_slice(&hw2.to_le_bytes());
4915
4916                // Step 4: x = x * 0x01010101 >> 24
4917                // Load 0x01010101 into R3
4918                // MOVW R3, #0x0101
4919                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
4920                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
4921                // MOVT R3, #0x0101
4922                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
4923                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
4924
4925                // MUL R4, R4, R3
4926                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
4927                let hw1: u16 = (0xFB00 | 4) as u16;
4928                let hw2: u16 = (0xF000 | (4 << 8) | r3) as u16;
4929                bytes.extend_from_slice(&hw1.to_le_bytes());
4930                bytes.extend_from_slice(&hw2.to_le_bytes());
4931
4932                // LSR.W R4, R4, #24
4933                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
4934                let hw1: u16 = 0xEA4F;
4935                let hw2: u16 = (0x6000 | (4 << 8) | 0x10 | 4) as u16;
4936                bytes.extend_from_slice(&hw1.to_le_bytes());
4937                bytes.extend_from_slice(&hw2.to_le_bytes());
4938
4939                // --- POPCNT for R5 (hi word) - same algorithm ---
4940                // Step 1
4941                let hw1: u16 = 0xEA4F;
4942                let hw2: u16 = ((r12 << 8) | 0x50 | 5) as u16;
4943                bytes.extend_from_slice(&hw1.to_le_bytes());
4944                bytes.extend_from_slice(&hw2.to_le_bytes());
4945
4946                // Load 0x55555555 into R3
4947                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
4948                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
4949                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
4950                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
4951
4952                let hw1: u16 = (0xEA00 | r12) as u16;
4953                let hw2: u16 = ((r12 << 8) | r3) as u16;
4954                bytes.extend_from_slice(&hw1.to_le_bytes());
4955                bytes.extend_from_slice(&hw2.to_le_bytes());
4956
4957                let hw1: u16 = (0xEBA0 | 5) as u16;
4958                let hw2: u16 = ((5 << 8) | r12) as u16;
4959                bytes.extend_from_slice(&hw1.to_le_bytes());
4960                bytes.extend_from_slice(&hw2.to_le_bytes());
4961
4962                // Step 2
4963                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
4964                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
4965                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
4966                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
4967
4968                let hw1: u16 = (0xEA00 | 5) as u16;
4969                let hw2: u16 = ((r12 << 8) | r3) as u16;
4970                bytes.extend_from_slice(&hw1.to_le_bytes());
4971                bytes.extend_from_slice(&hw2.to_le_bytes());
4972
4973                let hw1: u16 = 0xEA4F;
4974                let hw2: u16 = ((5 << 8) | 0x90 | 5) as u16;
4975                bytes.extend_from_slice(&hw1.to_le_bytes());
4976                bytes.extend_from_slice(&hw2.to_le_bytes());
4977
4978                let hw1: u16 = (0xEA00 | 5) as u16;
4979                let hw2: u16 = ((5 << 8) | r3) as u16;
4980                bytes.extend_from_slice(&hw1.to_le_bytes());
4981                bytes.extend_from_slice(&hw2.to_le_bytes());
4982
4983                let hw1: u16 = (0xEB00 | 5) as u16;
4984                let hw2: u16 = ((5 << 8) | r12) as u16;
4985                bytes.extend_from_slice(&hw1.to_le_bytes());
4986                bytes.extend_from_slice(&hw2.to_le_bytes());
4987
4988                // Step 3: LSR.W R12, R5, #4
4989                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
4990                let hw1: u16 = 0xEA4F;
4991                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 5) as u16;
4992                bytes.extend_from_slice(&hw1.to_le_bytes());
4993                bytes.extend_from_slice(&hw2.to_le_bytes());
4994
4995                let hw1: u16 = (0xEB00 | 5) as u16;
4996                let hw2: u16 = ((5 << 8) | r12) as u16;
4997                bytes.extend_from_slice(&hw1.to_le_bytes());
4998                bytes.extend_from_slice(&hw2.to_le_bytes());
4999
5000                // Load 0x0F0F0F0F into R3 (for hi-word)
5001                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5002                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5003                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5004                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5005
5006                let hw1: u16 = (0xEA00 | 5) as u16;
5007                let hw2: u16 = ((5 << 8) | r3) as u16;
5008                bytes.extend_from_slice(&hw1.to_le_bytes());
5009                bytes.extend_from_slice(&hw2.to_le_bytes());
5010
5011                // Step 4
5012                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5013                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5014                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5015                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5016
5017                // MUL R5, R5, R3
5018                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5019                let hw1: u16 = (0xFB00 | 5) as u16;
5020                let hw2: u16 = (0xF000 | (5 << 8) | r3) as u16;
5021                bytes.extend_from_slice(&hw1.to_le_bytes());
5022                bytes.extend_from_slice(&hw2.to_le_bytes());
5023
5024                // LSR.W R5, R5, #24
5025                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5026                let hw1: u16 = 0xEA4F;
5027                let hw2: u16 = (0x6000 | (5 << 8) | 0x10 | 5) as u16;
5028                bytes.extend_from_slice(&hw1.to_le_bytes());
5029                bytes.extend_from_slice(&hw2.to_le_bytes());
5030
5031                // #632: the count must be carried ACROSS the scratch restore
5032                // in a register the POP cannot touch. rd is allocator-assigned
5033                // (any of R0-R8) and can land inside the {R3,R4,R5} restore set
5034                // — the old `ADDS rd, R4, R5; POP {R3,R4,R5}` destroyed the
5035                // result one instruction after computing it (0 for every input
5036                // under qemu). R12 is encoder scratch: never allocatable (#212)
5037                // and never in a restore set, so no choice of rd can collide.
5038                // ADD.W R12, R4, R5
5039                bytes.extend_from_slice(&0xEB04u16.to_le_bytes());
5040                bytes.extend_from_slice(&0x0C05u16.to_le_bytes());
5041
5042                // POP {R3, R4, R5}
5043                bytes.extend_from_slice(&0xBC38u16.to_le_bytes());
5044
5045                // MOV rd, R12 — after the restore. The 4-bit Rd (D:rd) form is
5046                // also total over rd = R8, where the old ADDS T1 3-bit field
5047                // silently corrupted the encoding (#178/#180 class).
5048                let mov: u16 =
5049                    (0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7)) as u16;
5050                bytes.extend_from_slice(&mov.to_le_bytes());
5051
5052                // i64.popcnt returns i64, so clear high word: MOV.W rnhi, #0
5053                // (T2, 4 bytes — total over rnhi = R8, where the old 16-bit
5054                // MOVS encoding overflowed its 3-bit field into CMP R0, #0).
5055                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5056                bytes.extend_from_slice(&(((rn_hi_bits & 0xF) << 8) as u16).to_le_bytes());
5057
5058                Ok(bytes)
5059            }
5060
5061            // I64Extend8S: Sign-extend low 8 bits to 64 bits
5062            // Result: rdlo = sign_extend_8(rnlo), rdhi = rdlo >> 31
5063            ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
5064                let rdlo_bits = reg_to_bits(rdlo);
5065                let rdhi_bits = reg_to_bits(rdhi);
5066                let rnlo_bits = reg_to_bits(rnlo);
5067                let mut bytes = Vec::new();
5068
5069                // SXTB.W rdlo, rnlo (sign-extend byte to 32-bit)
5070                // SXTB T2: hw1 = 0xFA4F, hw2 = 0xF0<Rd><Rm>
5071                let hw1: u16 = 0xFA4F_u16;
5072                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5073                bytes.extend_from_slice(&hw1.to_le_bytes());
5074                bytes.extend_from_slice(&hw2.to_le_bytes());
5075
5076                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5077                // ASR (immediate): hw1 = 0xEA4F, hw2 = imm3:Rd:imm2:type:Rm
5078                // For imm5=31: imm3=111, imm2=11, type=10 (ASR)
5079                // hw2 = (7 << 12) | (rdhi << 8) | (3 << 6) | (2 << 4) | rdlo
5080                let hw1: u16 = 0xEA4F;
5081                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5082                bytes.extend_from_slice(&hw1.to_le_bytes());
5083                bytes.extend_from_slice(&hw2.to_le_bytes());
5084
5085                Ok(bytes)
5086            }
5087
5088            // I64Extend16S: Sign-extend low 16 bits to 64 bits
5089            // Result: rdlo = sign_extend_16(rnlo), rdhi = rdlo >> 31
5090            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
5091                let rdlo_bits = reg_to_bits(rdlo);
5092                let rdhi_bits = reg_to_bits(rdhi);
5093                let rnlo_bits = reg_to_bits(rnlo);
5094                let mut bytes = Vec::new();
5095
5096                // SXTH.W rdlo, rnlo (sign-extend halfword to 32-bit)
5097                // SXTH T2: hw1 = 0xFA0F, hw2 = 0xF0<Rd><Rm>
5098                let hw1: u16 = 0xFA0F_u16;
5099                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5100                bytes.extend_from_slice(&hw1.to_le_bytes());
5101                bytes.extend_from_slice(&hw2.to_le_bytes());
5102
5103                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5104                let hw1: u16 = 0xEA4F;
5105                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5106                bytes.extend_from_slice(&hw1.to_le_bytes());
5107                bytes.extend_from_slice(&hw2.to_le_bytes());
5108
5109                Ok(bytes)
5110            }
5111
5112            // I64Extend32S: Sign-extend low 32 bits to 64 bits
5113            // Result: rdlo = rnlo, rdhi = rnlo >> 31
5114            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
5115                let rdlo_bits = reg_to_bits(rdlo);
5116                let rdhi_bits = reg_to_bits(rdhi);
5117                let rnlo_bits = reg_to_bits(rnlo);
5118                let mut bytes = Vec::new();
5119
5120                // MOV rdlo, rnlo (if different)
5121                if rdlo_bits != rnlo_bits {
5122                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5123                    let d_bit = ((rdlo_bits >> 3) & 1) as u16;
5124                    let mov: u16 = 0x4600
5125                        | (d_bit << 7)
5126                        | ((rnlo_bits as u16) << 3)
5127                        | ((rdlo_bits & 0x7) as u16);
5128                    bytes.extend_from_slice(&mov.to_le_bytes());
5129                }
5130
5131                // ASR.W rdhi, rnlo, #31 (sign-extend to high word)
5132                let hw1: u16 = 0xEA4F;
5133                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rnlo_bits) as u16;
5134                bytes.extend_from_slice(&hw1.to_le_bytes());
5135                bytes.extend_from_slice(&hw2.to_le_bytes());
5136
5137                Ok(bytes)
5138            }
5139
5140            // SelectMove: IT <cond>; MOV{cond} rd, rm
5141            // Conditional move: only execute MOV if condition is true
5142            ArmOp::SelectMove { rd, rm, cond } => {
5143                let rd_bits = reg_to_bits(rd) as u16;
5144                let rm_bits = reg_to_bits(rm) as u16;
5145
5146                // Condition code encoding for IT block
5147                use synth_synthesis::Condition;
5148                let cond_bits: u16 = match cond {
5149                    Condition::EQ => 0x0, // Equal
5150                    Condition::NE => 0x1, // Not equal
5151                    Condition::HS => 0x2, // Higher or same (unsigned >=)
5152                    Condition::LO => 0x3, // Lower (unsigned <)
5153                    Condition::HI => 0x8, // Higher (unsigned >)
5154                    Condition::LS => 0x9, // Lower or same (unsigned <=)
5155                    Condition::GE => 0xA, // Greater or equal (signed)
5156                    Condition::LT => 0xB, // Less than (signed)
5157                    Condition::GT => 0xC, // Greater than (signed)
5158                    Condition::LE => 0xD, // Less or equal (signed)
5159                };
5160
5161                // IT <cond>: single Then block (mask = 0x8 for T only)
5162                // IT instruction: 1011 1111 firstcond mask
5163                let it_instr: u16 = 0xBF00 | (cond_bits << 4) | 0x8;
5164
5165                // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5166                // This MOV will only execute if condition is true due to IT block
5167                let d_bit = (rd_bits >> 3) & 1;
5168                let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5169
5170                // Emit: IT <cond>, MOV rd, rm
5171                let mut bytes = it_instr.to_le_bytes().to_vec();
5172                bytes.extend_from_slice(&mov_instr.to_le_bytes());
5173                Ok(bytes)
5174            }
5175
5176            // Popcnt: Population count (count set bits)
5177            // ARM Cortex-M has no native POPCNT, so we implement the bit manipulation algorithm:
5178            // x = x - ((x >> 1) & 0x55555555);
5179            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
5180            // x = (x + (x >> 4)) & 0x0F0F0F0F;
5181            // x = x + (x >> 8);
5182            // x = x + (x >> 16);
5183            // return x & 0x3F;
5184            //
5185            // Uses rd as working register and R12 as scratch for constants
5186            ArmOp::Popcnt { rd, rm } => {
5187                let mut bytes = Vec::new();
5188
5189                // First, move rm to rd if they're different
5190                if rd != rm {
5191                    let rd_bits = reg_to_bits(rd) as u16;
5192                    let rm_bits = reg_to_bits(rm) as u16;
5193                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5194                    let d_bit = (rd_bits >> 3) & 1;
5195                    let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5196                    bytes.extend_from_slice(&mov_instr.to_le_bytes());
5197                }
5198
5199                // Step 1: x = x - ((x >> 1) & 0x55555555)
5200                // Load 0x55555555 into R12
5201                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x5555)?);
5202                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x5555)?);
5203
5204                // R12_temp = rd >> 1
5205                // We need a second scratch register. Use R11.
5206                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 1)?);
5207
5208                // R11 = R11 & R12 (R11 = (x >> 1) & 0x55555555)
5209                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(11, 11, 12)?);
5210
5211                // rd = rd - R11
5212                bytes.extend_from_slice(&self.encode_thumb32_sub_reg_raw(
5213                    reg_to_bits(rd),
5214                    reg_to_bits(rd),
5215                    11,
5216                )?);
5217
5218                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5219                // Load 0x33333333 into R12
5220                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x3333)?);
5221                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x3333)?);
5222
5223                // R11 = rd & R12
5224                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5225                    11,
5226                    reg_to_bits(rd),
5227                    12,
5228                )?);
5229
5230                // rd = rd >> 2
5231                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(
5232                    reg_to_bits(rd),
5233                    reg_to_bits(rd),
5234                    2,
5235                )?);
5236
5237                // rd = rd & R12
5238                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5239                    reg_to_bits(rd),
5240                    reg_to_bits(rd),
5241                    12,
5242                )?);
5243
5244                // rd = rd + R11
5245                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5246                    reg_to_bits(rd),
5247                    reg_to_bits(rd),
5248                    11,
5249                )?);
5250
5251                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5252                // R11 = rd >> 4
5253                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 4)?);
5254
5255                // rd = rd + R11
5256                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5257                    reg_to_bits(rd),
5258                    reg_to_bits(rd),
5259                    11,
5260                )?);
5261
5262                // Load 0x0F0F0F0F into R12
5263                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x0F0F)?);
5264                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x0F0F)?);
5265
5266                // rd = rd & R12
5267                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5268                    reg_to_bits(rd),
5269                    reg_to_bits(rd),
5270                    12,
5271                )?);
5272
5273                // Step 4: x = x + (x >> 8)
5274                // R11 = rd >> 8
5275                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 8)?);
5276
5277                // rd = rd + R11
5278                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5279                    reg_to_bits(rd),
5280                    reg_to_bits(rd),
5281                    11,
5282                )?);
5283
5284                // Step 5: x = x + (x >> 16)
5285                // R11 = rd >> 16
5286                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 16)?);
5287
5288                // rd = rd + R11
5289                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5290                    reg_to_bits(rd),
5291                    reg_to_bits(rd),
5292                    11,
5293                )?);
5294
5295                // Step 6: return x & 0x3F
5296                // AND with 0x3F (small immediate, can use BIC or AND with immediate)
5297                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5298                    reg_to_bits(rd),
5299                    reg_to_bits(rd),
5300                    0x3F,
5301                )?);
5302
5303                Ok(bytes)
5304            }
5305
5306            // I64DivU: 64-bit unsigned division using binary long division
5307            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = quotient
5308            // Uses: R4-R7, R12 as loop counter (avoid R8 for Renode compatibility)
5309            //
5310            // #610: the fixed-ABI wrapper marshals the selector-assigned
5311            // operand registers into the core's fixed regs and lands the
5312            // result in rd — pre-#610 this arm IGNORED its register fields,
5313            // so the selector read its rd pair (e.g. R4:R5) after the core's
5314            // own POP restored the stale caller values over it: 0 for every
5315            // input. A zero divisor now traps (UDF #0), per WASM semantics.
5316            ArmOp::I64DivU {
5317                rdlo,
5318                rdhi,
5319                rnlo,
5320                rnhi,
5321                rmlo,
5322                rmhi,
5323                elide_zero_guard,
5324            } => {
5325                let mut bytes = Vec::new();
5326                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5327                // #494 phase 2b: elided only under a certificate-discharged
5328                // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
5329                if !elide_zero_guard {
5330                    emit_i64_divisor_zero_trap(&mut bytes);
5331                }
5332
5333                // PUSH {R4-R7} - save scratch registers (NO LR — this is inline code)
5334                // 16-bit PUSH: 1011 010 M rrrrrrrr where M=0 (no LR), r=R4-R7 = 0xF0
5335                // Encoding: 1011 0100 1111 0000 = 0xB4F0
5336                bytes.extend_from_slice(&0xB4F0u16.to_le_bytes());
5337
5338                // Initialize quotient (R4:R5) = 0
5339                bytes.extend_from_slice(&0x2400u16.to_le_bytes()); // MOV R4, #0
5340                bytes.extend_from_slice(&0x2500u16.to_le_bytes()); // MOV R5, #0
5341
5342                // Initialize remainder (R6:R7) = 0
5343                bytes.extend_from_slice(&0x2600u16.to_le_bytes()); // MOV R6, #0
5344                bytes.extend_from_slice(&0x2700u16.to_le_bytes()); // MOV R7, #0
5345
5346                // Initialize loop counter R12 = 64 (use R12 scratch instead of R8)
5347                // MOV.W R12, #64: F04F 0C40
5348                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5349                bytes.extend_from_slice(&0x0C40u16.to_le_bytes());
5350
5351                // Loop start
5352                let loop_start = bytes.len();
5353
5354                // === Loop body: process one bit ===
5355
5356                // 1. Shift quotient R4:R5 left by 1
5357                // LSLS R5, R5, #1 (16-bit: 0000 0010 1010 1101 = 0x006D -> actually 0x002D for LSL R5,R5,#1)
5358                // LSL Rd, Rm, #imm5: 000 00 imm5 Rm Rd = 000 00 00001 101 101 = 0x006D
5359                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5360                // Get carry from R4 into R5: ORR R5, R5, R4 LSR #31
5361                // Thumb-2 ORR with shifted register: EA45 75D4 = ORR.W R5, R5, R4, LSR #31
5362                // 11101010 010 S Rn | 0 imm3 Rd imm2 type Rm
5363                // type=01 (LSR), imm5=31 (imm3=111, imm2=11)
5364                bytes.extend_from_slice(&0xEA45u16.to_le_bytes());
5365                bytes.extend_from_slice(&0x75D4u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5366                // LSLS R4, R4, #1: 000 00 00001 100 100 = 0x0064
5367                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5368
5369                // 2. Shift remainder R6:R7 left by 1, OR in MSB of dividend R1
5370                // LSLS R7, R7, #1
5371                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5372                // ORR.W R7, R7, R6, LSR #31
5373                bytes.extend_from_slice(&0xEA47u16.to_le_bytes());
5374                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5375                // LSLS R6, R6, #1
5376                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5377                // ORR.W R6, R6, R1, LSR #31 (bring in MSB of dividend high)
5378                bytes.extend_from_slice(&0xEA46u16.to_le_bytes());
5379                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5380
5381                // 3. Shift dividend R0:R1 left by 1
5382                // LSLS R1, R1, #1
5383                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5384                // ORR.W R1, R1, R0, LSR #31
5385                bytes.extend_from_slice(&0xEA41u16.to_le_bytes());
5386                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5387                // LSLS R0, R0, #1
5388                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5389
5390                // 4. Compare remainder >= divisor (64-bit unsigned comparison)
5391                // Compare high words first: CMP R7, R3
5392                // CMP Rn, Rm encoding: 0x4280 | (Rm << 3) | Rn
5393                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3 (16-bit)
5394                // BHI means R7 > R3 (unsigned) - definitely subtract
5395                // BLO means R7 < R3 - definitely don't subtract
5396                // BEQ means need to check low words
5397
5398                // If high > divisor high: branch to subtract (forward +offset)
5399                // BHI.N +6 (skip CMP, skip BLO, do subtract)
5400                // BHI: 1101 1000 offset8 where cond=1000 (HI)
5401                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4 (to subtract block)
5402
5403                // If high < divisor high: branch past subtract
5404                // BLO.N +10 (skip to decrement)
5405                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BLO/BCC +12 (past subtract)
5406
5407                // High words equal, compare low: CMP R6, R2
5408                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2 (16-bit)
5409                // BLO/BCC past subtract (skip SUBS+SBC.W+ORR.W = 10 bytes = 4 halfwords from PC+4)
5410                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords (past subtract)
5411
5412                // === Subtract block: remainder -= divisor, quotient |= 1 ===
5413                // SUBS R6, R6, R2
5414                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2 (16-bit)
5415                // SBC R7, R7, R3 (with borrow)
5416                // Thumb-2 SBC.W: EB67 0703 = SBC.W R7, R7, R3
5417                bytes.extend_from_slice(&0xEB67u16.to_le_bytes());
5418                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5419                // ORR R4, R4, #1 (set bit 0 of quotient low)
5420                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5421                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5422
5423                // === Decrement counter and loop ===
5424                // SUBS.W R12, R12, #1 (decrement loop counter)
5425                // SUBS.W R12, R12, #1: F1BC 0C01
5426                bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
5427                bytes.extend_from_slice(&0x0C01u16.to_le_bytes());
5428
5429                // BNE back to loop_start
5430                let branch_offset_bytes = bytes.len() - loop_start + 4; // +4 for pipeline
5431                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5432                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5433                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5434
5435                // === Loop done, move quotient to R0:R1 ===
5436                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5437                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5438
5439                // POP {R4-R7} - restore scratch registers (NO PC — inline code continues)
5440                // 16-bit POP: 1011 110 P rrrrrrrr where P=0 (no PC), r=R4-R7 = 0xF0
5441                // Encoding: 1011 1100 1111 0000 = 0xBCF0
5442                bytes.extend_from_slice(&0xBCF0u16.to_le_bytes());
5443
5444                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5445                Ok(bytes)
5446            }
5447
5448            // I64DivS: 64-bit signed division
5449            // Converts to unsigned, divides, then applies sign
5450            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5451            //   ->  R0:R1 = quotient (signed)
5452            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5453            ArmOp::I64DivS {
5454                rdlo,
5455                rdhi,
5456                rnlo,
5457                rnhi,
5458                rmlo,
5459                rmhi,
5460                elide_zero_guard,
5461                elide_overflow_guard,
5462            } => {
5463                let mut bytes = Vec::new();
5464                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5465                // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
5466                // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
5467                // the #633 overflow guard falls ONLY to
5468                // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
5469                // divisor-nonzero fact alone must keep it.
5470                if !elide_zero_guard {
5471                    emit_i64_divisor_zero_trap(&mut bytes);
5472                }
5473                if !elide_overflow_guard {
5474                    // #633: INT64_MIN / -1 overflows — trap like the i32 path
5475                    // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
5476                    emit_i64_divs_overflow_trap(&mut bytes);
5477                }
5478
5479                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5480                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5481                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5482
5483                // Save result sign in R9: R9 = R1 XOR R3 (sign bit = MSB)
5484                // EOR.W R9, R1, R3
5485                bytes.extend_from_slice(&0xEA81u16.to_le_bytes());
5486                bytes.extend_from_slice(&0x0903u16.to_le_bytes());
5487
5488                // If dividend negative (R1 MSB set), negate it
5489                // TST R1, R1 (check sign)
5490                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5491                // BPL skip_neg_dividend (+10 bytes = 5 halfwords)
5492                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5493
5494                // Negate R0:R1 (64-bit): RSBS R0, R0, #0; SBC R1, R1, R1 LSL #1
5495                // Actually: MVN R0, R0; MVN R1, R1; ADDS R0, R0, #1; ADC R1, R1, #0
5496                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5497                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5498                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5499                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5500                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5501
5502                // If divisor negative (R3 MSB set), negate it
5503                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5504                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5505
5506                // Negate R2:R3
5507                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
5508                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
5509                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
5510                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
5511                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
5512
5513                // === Now do unsigned division (same as I64DivU) ===
5514                // Initialize quotient (R4:R5) = 0
5515                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5516                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5517                // Initialize remainder (R6:R7) = 0
5518                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5519                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5520                // Initialize loop counter R8 = 64
5521                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5522                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5523
5524                let loop_start = bytes.len();
5525
5526                // Shift quotient left
5527                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5528                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5529                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5530                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5531
5532                // Shift remainder left, OR in MSB of dividend
5533                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5534                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5535                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5536                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5537                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5538                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5539
5540                // Shift dividend left
5541                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5542                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5543                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5544                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5545
5546                // Compare and conditionally subtract
5547                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5548                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5549                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5550                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5551                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5552
5553                // Subtract and set quotient bit
5554                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5555                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5556                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5557                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5558                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5559
5560                // Decrement and loop
5561                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
5562                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
5563
5564                let branch_offset_bytes = bytes.len() - loop_start + 4;
5565                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5566                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5567                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5568
5569                // Move quotient to R0:R1
5570                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5571                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5572
5573                // If result should be negative (R9 MSB set), negate R0:R1
5574                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9 (check MSB)
5575                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
5576                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8 (skip negation)
5577
5578                // Negate result R0:R1
5579                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5580                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5581                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5582                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5583                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5584
5585                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
5586                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
5587                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5588
5589                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5590                Ok(bytes)
5591            }
5592
5593            // I64RemU: 64-bit unsigned remainder using binary long division
5594            // Same algorithm as I64DivU but returns remainder instead of quotient
5595            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = remainder
5596            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5597            ArmOp::I64RemU {
5598                rdlo,
5599                rdhi,
5600                rnlo,
5601                rnhi,
5602                rmlo,
5603                rmhi,
5604                elide_zero_guard,
5605            } => {
5606                let mut bytes = Vec::new();
5607                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5608                if !elide_zero_guard {
5609                    emit_i64_divisor_zero_trap(&mut bytes);
5610                }
5611
5612                // PUSH {R4-R8} - save scratch registers (NO LR — inline code)
5613                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5614                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
5615
5616                // Initialize quotient (R4:R5) = 0 (computed but not returned)
5617                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5618                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5619                // Initialize remainder (R6:R7) = 0
5620                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5621                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5622                // Initialize loop counter R8 = 64
5623                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5624                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5625
5626                let loop_start = bytes.len();
5627
5628                // Shift quotient left (not needed for result, but keeps algorithm same)
5629                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5630                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5631                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5632                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5633
5634                // Shift remainder left, OR in MSB of dividend
5635                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5636                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5637                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5638                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5639                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5640                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5641
5642                // Shift dividend left
5643                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5644                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5645                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5646                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5647
5648                // Compare and conditionally subtract
5649                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5650                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5651                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5652                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5653                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5654
5655                // Subtract and set quotient bit
5656                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5657                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5658                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5659                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5660                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5661
5662                // Decrement and loop
5663                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
5664                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
5665
5666                let branch_offset_bytes = bytes.len() - loop_start + 4;
5667                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5668                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5669                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5670
5671                // Move REMAINDER to R0:R1 (difference from I64DivU)
5672                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
5673                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
5674
5675                // POP {R4-R8} - restore scratch registers (NO PC — inline code continues)
5676                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
5677                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
5678
5679                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5680                Ok(bytes)
5681            }
5682
5683            // I64RemS: 64-bit signed remainder
5684            // Remainder sign follows dividend sign (not quotient rule)
5685            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5686            //   ->  R0:R1 = remainder (signed, same sign as dividend)
5687            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5688            ArmOp::I64RemS {
5689                rdlo,
5690                rdhi,
5691                rnlo,
5692                rnhi,
5693                rmlo,
5694                rmhi,
5695                elide_zero_guard,
5696            } => {
5697                let mut bytes = Vec::new();
5698                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5699                if !elide_zero_guard {
5700                    emit_i64_divisor_zero_trap(&mut bytes);
5701                }
5702
5703                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5704                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5705                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5706
5707                // Save dividend sign in R9 (remainder sign = dividend sign)
5708                // MOV R9, R1 (just need the sign bit)
5709                bytes.extend_from_slice(&0x4689u16.to_le_bytes()); // MOV R9, R1
5710
5711                // If dividend negative (R1 MSB set), negate it
5712                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5713                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5714
5715                // Negate R0:R1
5716                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5717                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5718                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5719                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5720                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5721
5722                // If divisor negative (R3 MSB set), negate it
5723                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5724                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5725
5726                // Negate R2:R3
5727                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
5728                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
5729                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
5730                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
5731                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
5732
5733                // === Unsigned division algorithm ===
5734                // Initialize quotient (R4:R5) = 0
5735                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5736                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5737                // Initialize remainder (R6:R7) = 0
5738                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5739                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5740                // Initialize loop counter R8 = 64
5741                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5742                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5743
5744                let loop_start = bytes.len();
5745
5746                // Shift quotient left
5747                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5748                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5749                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5750                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5751
5752                // Shift remainder left, OR in MSB of dividend
5753                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5754                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5755                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5756                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5757                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5758                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5759
5760                // Shift dividend left
5761                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5762                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5763                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5764                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5765
5766                // Compare and conditionally subtract
5767                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5768                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5769                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5770                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5771                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5772
5773                // Subtract and set quotient bit
5774                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5775                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5776                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5777                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5778                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5779
5780                // Decrement and loop
5781                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
5782                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
5783
5784                let branch_offset_bytes = bytes.len() - loop_start + 4;
5785                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5786                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5787                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5788
5789                // Move remainder to R0:R1
5790                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
5791                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
5792
5793                // If original dividend was negative (R9 MSB set), negate remainder
5794                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9
5795                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
5796                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5797
5798                // Negate result R0:R1
5799                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5800                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5801                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5802                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5803                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5804
5805                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
5806                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
5807                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5808
5809                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5810                Ok(bytes)
5811            }
5812
5813            // === F32 VFP single-precision Thumb-2 encodings ===
5814            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
5815            ArmOp::F32Add { sd, sn, sm } => {
5816                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A00, sd, sn, sm)?))
5817            }
5818            ArmOp::F32Sub { sd, sn, sm } => {
5819                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A40, sd, sn, sm)?))
5820            }
5821            ArmOp::F32Mul { sd, sn, sm } => {
5822                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE200A00, sd, sn, sm)?))
5823            }
5824            ArmOp::F32Div { sd, sn, sm } => {
5825                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE800A00, sd, sn, sm)?))
5826            }
5827            ArmOp::F32Abs { sd, sm } => {
5828                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB00AC0, sd, sm)?))
5829            }
5830            ArmOp::F32Neg { sd, sm } => {
5831                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10A40, sd, sm)?))
5832            }
5833            ArmOp::F32Sqrt { sd, sm } => {
5834                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10AC0, sd, sm)?))
5835            }
5836
5837            // f32 pseudo-ops — multi-instruction sequences
5838            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
5839            ArmOp::F32Ceil { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b01),
5840            ArmOp::F32Floor { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b10),
5841            ArmOp::F32Trunc { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b11),
5842            ArmOp::F32Nearest { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b00),
5843            ArmOp::F32Min { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, true),
5844            ArmOp::F32Max { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, false),
5845            ArmOp::F32Copysign { sd, sn, sm } => self.encode_thumb_f32_copysign(sd, sn, sm),
5846
5847            // f32 comparisons — VCMP + VMRS + MOV #0 + IT + MOV #1
5848            ArmOp::F32Eq { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x0),
5849            ArmOp::F32Ne { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x1),
5850            ArmOp::F32Lt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x4),
5851            ArmOp::F32Le { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x9),
5852            ArmOp::F32Gt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xC),
5853            ArmOp::F32Ge { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xA),
5854
5855            ArmOp::F32Const { sd, value } => self.encode_thumb_f32_const(sd, *value),
5856
5857            ArmOp::F32Load { sd, addr } => {
5858                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED900A00, sd, addr)?))
5859            }
5860            ArmOp::F32Store { sd, addr } => {
5861                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED800A00, sd, addr)?))
5862            }
5863
5864            ArmOp::F32ConvertI32S { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, true),
5865            ArmOp::F32ConvertI32U { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, false),
5866            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
5867                Err(synth_core::Error::synthesis(
5868                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
5869                ))
5870            }
5871            ArmOp::F32ReinterpretI32 { sd, rm } => {
5872                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(true, sd, rm)?))
5873            }
5874            ArmOp::I32ReinterpretF32 { rd, sm } => {
5875                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(false, sm, rd)?))
5876            }
5877            ArmOp::I32TruncF32S { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, true),
5878            ArmOp::I32TruncF32U { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, false),
5879
5880            // === F64 VFP double-precision Thumb-2 encodings ===
5881            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
5882            ArmOp::F64Add { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
5883                0xEE300B00, dd, dn, dm,
5884            )?)),
5885            ArmOp::F64Sub { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
5886                0xEE300B40, dd, dn, dm,
5887            )?)),
5888            ArmOp::F64Mul { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
5889                0xEE200B00, dd, dn, dm,
5890            )?)),
5891            ArmOp::F64Div { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
5892                0xEE800B00, dd, dn, dm,
5893            )?)),
5894            ArmOp::F64Abs { dd, dm } => {
5895                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?))
5896            }
5897            ArmOp::F64Neg { dd, dm } => {
5898                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?))
5899            }
5900            ArmOp::F64Sqrt { dd, dm } => {
5901                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?))
5902            }
5903
5904            // f64 pseudo-ops
5905            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
5906            ArmOp::F64Ceil { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b01),
5907            ArmOp::F64Floor { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b10),
5908            ArmOp::F64Trunc { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b11),
5909            ArmOp::F64Nearest { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b00),
5910            ArmOp::F64Min { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, true),
5911            ArmOp::F64Max { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, false),
5912            ArmOp::F64Copysign { dd, dn, dm } => self.encode_thumb_f64_copysign(dd, dn, dm),
5913
5914            // f64 comparisons
5915            ArmOp::F64Eq { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x0),
5916            ArmOp::F64Ne { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x1),
5917            ArmOp::F64Lt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x4),
5918            ArmOp::F64Le { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x9),
5919            ArmOp::F64Gt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xC),
5920            ArmOp::F64Ge { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xA),
5921
5922            ArmOp::F64Const { dd, value } => self.encode_thumb_f64_const(dd, *value),
5923
5924            ArmOp::F64Load { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
5925                0xED900B00, dd, addr,
5926            )?)),
5927            ArmOp::F64Store { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
5928                0xED800B00, dd, addr,
5929            )?)),
5930
5931            ArmOp::F64ConvertI32S { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, true),
5932            ArmOp::F64ConvertI32U { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, false),
5933            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
5934                Err(synth_core::Error::synthesis(
5935                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
5936                ))
5937            }
5938            ArmOp::F64PromoteF32 { dd, sm } => self.encode_thumb_f64_promote_f32(dd, sm),
5939            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => Ok(vfp_to_thumb_bytes(
5940                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?,
5941            )),
5942            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => Ok(vfp_to_thumb_bytes(
5943                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?,
5944            )),
5945            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
5946                Err(synth_core::Error::synthesis(
5947                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
5948                ))
5949            }
5950            ArmOp::I32TruncF64S { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, true),
5951            ArmOp::I32TruncF64U { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, false),
5952
5953            // ===== i64 operations: encode as multi-instruction Thumb-2 sequences =====
5954
5955            // I64Add: ADDS rdlo, rnlo, rmlo; ADC.W rdhi, rnhi, rmhi
5956            ArmOp::I64Add {
5957                rdlo,
5958                rdhi,
5959                rnlo,
5960                rnhi,
5961                rmlo,
5962                rmhi,
5963            } => {
5964                let mut bytes = Vec::new();
5965                // ADDS rdlo, rnlo, rmlo (16-bit)
5966                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adds {
5967                    rd: *rdlo,
5968                    rn: *rnlo,
5969                    op2: Operand2::Reg(*rmlo),
5970                })?);
5971                // ADC.W rdhi, rnhi, rmhi (32-bit)
5972                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adc {
5973                    rd: *rdhi,
5974                    rn: *rnhi,
5975                    op2: Operand2::Reg(*rmhi),
5976                })?);
5977                Ok(bytes)
5978            }
5979
5980            // I64Sub: SUBS rdlo, rnlo, rmlo; SBC.W rdhi, rnhi, rmhi
5981            ArmOp::I64Sub {
5982                rdlo,
5983                rdhi,
5984                rnlo,
5985                rnhi,
5986                rmlo,
5987                rmhi,
5988            } => {
5989                let mut bytes = Vec::new();
5990                // SUBS rdlo, rnlo, rmlo (16-bit)
5991                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Subs {
5992                    rd: *rdlo,
5993                    rn: *rnlo,
5994                    op2: Operand2::Reg(*rmlo),
5995                })?);
5996                // SBC.W rdhi, rnhi, rmhi (32-bit)
5997                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Sbc {
5998                    rd: *rdhi,
5999                    rn: *rnhi,
6000                    op2: Operand2::Reg(*rmhi),
6001                })?);
6002                Ok(bytes)
6003            }
6004
6005            // I64And: AND rdlo, rnlo, rmlo; AND rdhi, rnhi, rmhi
6006            ArmOp::I64And {
6007                rdlo,
6008                rdhi,
6009                rnlo,
6010                rnhi,
6011                rmlo,
6012                rmhi,
6013            } => {
6014                let mut bytes = Vec::new();
6015                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6016                    rd: *rdlo,
6017                    rn: *rnlo,
6018                    op2: Operand2::Reg(*rmlo),
6019                })?);
6020                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6021                    rd: *rdhi,
6022                    rn: *rnhi,
6023                    op2: Operand2::Reg(*rmhi),
6024                })?);
6025                Ok(bytes)
6026            }
6027
6028            // I64Or: ORR rdlo, rnlo, rmlo; ORR rdhi, rnhi, rmhi
6029            ArmOp::I64Or {
6030                rdlo,
6031                rdhi,
6032                rnlo,
6033                rnhi,
6034                rmlo,
6035                rmhi,
6036            } => {
6037                let mut bytes = Vec::new();
6038                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6039                    rd: *rdlo,
6040                    rn: *rnlo,
6041                    op2: Operand2::Reg(*rmlo),
6042                })?);
6043                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6044                    rd: *rdhi,
6045                    rn: *rnhi,
6046                    op2: Operand2::Reg(*rmhi),
6047                })?);
6048                Ok(bytes)
6049            }
6050
6051            // I64Xor: EOR rdlo, rnlo, rmlo; EOR rdhi, rnhi, rmhi
6052            ArmOp::I64Xor {
6053                rdlo,
6054                rdhi,
6055                rnlo,
6056                rnhi,
6057                rmlo,
6058                rmhi,
6059            } => {
6060                let mut bytes = Vec::new();
6061                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6062                    rd: *rdlo,
6063                    rn: *rnlo,
6064                    op2: Operand2::Reg(*rmlo),
6065                })?);
6066                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6067                    rd: *rdhi,
6068                    rn: *rnhi,
6069                    op2: Operand2::Reg(*rmhi),
6070                })?);
6071                Ok(bytes)
6072            }
6073
6074            // I64Eqz: ORR scratch, lo, hi; ITE EQ; MOV rd, #1; MOV rd, #0
6075            ArmOp::I64Eqz { rd, rnlo, rnhi } => self.encode_thumb(&ArmOp::I64SetCondZ {
6076                rd: *rd,
6077                rn_lo: *rnlo,
6078                rn_hi: *rnhi,
6079            }),
6080
6081            // I64 comparisons: delegate to I64SetCond
6082            ArmOp::I64Eq {
6083                rd,
6084                rnlo,
6085                rnhi,
6086                rmlo,
6087                rmhi,
6088            } => self.encode_thumb(&ArmOp::I64SetCond {
6089                rd: *rd,
6090                rn_lo: *rnlo,
6091                rn_hi: *rnhi,
6092                rm_lo: *rmlo,
6093                rm_hi: *rmhi,
6094                cond: synth_synthesis::Condition::EQ,
6095            }),
6096
6097            ArmOp::I64Ne {
6098                rd,
6099                rnlo,
6100                rnhi,
6101                rmlo,
6102                rmhi,
6103            } => self.encode_thumb(&ArmOp::I64SetCond {
6104                rd: *rd,
6105                rn_lo: *rnlo,
6106                rn_hi: *rnhi,
6107                rm_lo: *rmlo,
6108                rm_hi: *rmhi,
6109                cond: synth_synthesis::Condition::NE,
6110            }),
6111
6112            ArmOp::I64LtS {
6113                rd,
6114                rnlo,
6115                rnhi,
6116                rmlo,
6117                rmhi,
6118            } => self.encode_thumb(&ArmOp::I64SetCond {
6119                rd: *rd,
6120                rn_lo: *rnlo,
6121                rn_hi: *rnhi,
6122                rm_lo: *rmlo,
6123                rm_hi: *rmhi,
6124                cond: synth_synthesis::Condition::LT,
6125            }),
6126
6127            ArmOp::I64LtU {
6128                rd,
6129                rnlo,
6130                rnhi,
6131                rmlo,
6132                rmhi,
6133            } => self.encode_thumb(&ArmOp::I64SetCond {
6134                rd: *rd,
6135                rn_lo: *rnlo,
6136                rn_hi: *rnhi,
6137                rm_lo: *rmlo,
6138                rm_hi: *rmhi,
6139                cond: synth_synthesis::Condition::LO,
6140            }),
6141
6142            ArmOp::I64LeS {
6143                rd,
6144                rnlo,
6145                rnhi,
6146                rmlo,
6147                rmhi,
6148            } => self.encode_thumb(&ArmOp::I64SetCond {
6149                rd: *rd,
6150                rn_lo: *rnlo,
6151                rn_hi: *rnhi,
6152                rm_lo: *rmlo,
6153                rm_hi: *rmhi,
6154                cond: synth_synthesis::Condition::LE,
6155            }),
6156
6157            ArmOp::I64LeU {
6158                rd,
6159                rnlo,
6160                rnhi,
6161                rmlo,
6162                rmhi,
6163            } => self.encode_thumb(&ArmOp::I64SetCond {
6164                rd: *rd,
6165                rn_lo: *rnlo,
6166                rn_hi: *rnhi,
6167                rm_lo: *rmlo,
6168                rm_hi: *rmhi,
6169                cond: synth_synthesis::Condition::LS,
6170            }),
6171
6172            ArmOp::I64GtS {
6173                rd,
6174                rnlo,
6175                rnhi,
6176                rmlo,
6177                rmhi,
6178            } => self.encode_thumb(&ArmOp::I64SetCond {
6179                rd: *rd,
6180                rn_lo: *rnlo,
6181                rn_hi: *rnhi,
6182                rm_lo: *rmlo,
6183                rm_hi: *rmhi,
6184                cond: synth_synthesis::Condition::GT,
6185            }),
6186
6187            ArmOp::I64GtU {
6188                rd,
6189                rnlo,
6190                rnhi,
6191                rmlo,
6192                rmhi,
6193            } => self.encode_thumb(&ArmOp::I64SetCond {
6194                rd: *rd,
6195                rn_lo: *rnlo,
6196                rn_hi: *rnhi,
6197                rm_lo: *rmlo,
6198                rm_hi: *rmhi,
6199                cond: synth_synthesis::Condition::HI,
6200            }),
6201
6202            ArmOp::I64GeS {
6203                rd,
6204                rnlo,
6205                rnhi,
6206                rmlo,
6207                rmhi,
6208            } => self.encode_thumb(&ArmOp::I64SetCond {
6209                rd: *rd,
6210                rn_lo: *rnlo,
6211                rn_hi: *rnhi,
6212                rm_lo: *rmlo,
6213                rm_hi: *rmhi,
6214                cond: synth_synthesis::Condition::GE,
6215            }),
6216
6217            ArmOp::I64GeU {
6218                rd,
6219                rnlo,
6220                rnhi,
6221                rmlo,
6222                rmhi,
6223            } => self.encode_thumb(&ArmOp::I64SetCond {
6224                rd: *rd,
6225                rn_lo: *rnlo,
6226                rn_hi: *rnhi,
6227                rm_lo: *rmlo,
6228                rm_hi: *rmhi,
6229                cond: synth_synthesis::Condition::HS,
6230            }),
6231
6232            // I64Const: MOVW rdlo, lo16; MOVT rdlo, hi16; MOVW rdhi, lo16_hi; MOVT rdhi, hi16_hi
6233            ArmOp::I64Const { rdlo, rdhi, value } => {
6234                let lo32 = *value as u32;
6235                let hi32 = (*value >> 32) as u32;
6236                let mut bytes = Vec::new();
6237                // Load low 32 bits into rdlo
6238                bytes.extend_from_slice(
6239                    &self.encode_thumb32_movw_raw(reg_to_bits(rdlo), lo32 & 0xFFFF)?,
6240                );
6241                if lo32 > 0xFFFF {
6242                    bytes.extend_from_slice(
6243                        &self.encode_thumb32_movt_raw(reg_to_bits(rdlo), lo32 >> 16)?,
6244                    );
6245                }
6246                // Load high 32 bits into rdhi
6247                bytes.extend_from_slice(
6248                    &self.encode_thumb32_movw_raw(reg_to_bits(rdhi), hi32 & 0xFFFF)?,
6249                );
6250                if hi32 > 0xFFFF {
6251                    bytes.extend_from_slice(
6252                        &self.encode_thumb32_movt_raw(reg_to_bits(rdhi), hi32 >> 16)?,
6253                    );
6254                }
6255                Ok(bytes)
6256            }
6257
6258            // I64Ldr: LDR rdlo, [base, offset]; LDR rdhi, [base, offset+4]
6259            ArmOp::I64Ldr { rdlo, rdhi, addr } => {
6260                let mut bytes = Vec::new();
6261                // #372/#382: a memory `i64.load` carries an index register
6262                // (`reg_imm(R11, addr_reg, offset)` = R11 + addr + offset). The
6263                // immediate `encode_thumb32_ldr` below uses only base+offset and
6264                // would SILENTLY DROP `offset_reg` — the #206 defect, here for
6265                // i64. `i64_effective_base` materializes the effective base into
6266                // `ip` (and, when `offset+4 > 0xFFF`, folds the offset in too so
6267                // the function is NOT skipped — #382), returning the residual
6268                // imm12 for the two halves. Frame i64 loads (no `offset_reg`, e.g.
6269                // a spilled local at `[SP, #off]`) keep the plain `[base,#off]`
6270                // form unchanged — so existing output is byte-identical.
6271                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6272                bytes.extend_from_slice(&self.encode_thumb32_ldr(rdlo, &base, offset)?);
6273                bytes.extend_from_slice(&self.encode_thumb32_ldr(
6274                    rdhi,
6275                    &base,
6276                    offset.wrapping_add(4),
6277                )?);
6278                Ok(bytes)
6279            }
6280
6281            // I64Str: STR rdlo, [base, offset]; STR rdhi, [base, offset+4]
6282            ArmOp::I64Str { rdlo, rdhi, addr } => {
6283                let mut bytes = Vec::new();
6284                // #372/#382: same index-materialization + large-offset fold as
6285                // I64Ldr (see above).
6286                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6287                bytes.extend_from_slice(&self.encode_thumb32_str(rdlo, &base, offset)?);
6288                bytes.extend_from_slice(&self.encode_thumb32_str(
6289                    rdhi,
6290                    &base,
6291                    offset.wrapping_add(4),
6292                )?);
6293                Ok(bytes)
6294            }
6295
6296            // I64ExtendI32S: MOV rdlo, rn; ASR rdhi, rdlo, #31 (sign-extend)
6297            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
6298                let mut bytes = Vec::new();
6299                if rdlo != rn {
6300                    // MOV rdlo, rn (16-bit)
6301                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6302                        rd: *rdlo,
6303                        op2: Operand2::Reg(*rn),
6304                    })?);
6305                }
6306                // ASR rdhi, rdlo, #31 (sign-extend: fill high word with sign bit)
6307                bytes.extend_from_slice(
6308                    &self.encode_thumb32_shift(rdhi, rdlo, 31, 0b10)?, // ASR type
6309                );
6310                Ok(bytes)
6311            }
6312
6313            // I64ExtendI32U: MOV rdlo, rn; MOV rdhi, #0
6314            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
6315                let mut bytes = Vec::new();
6316                if rdlo != rn {
6317                    // MOV rdlo, rn
6318                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6319                        rd: *rdlo,
6320                        op2: Operand2::Reg(*rn),
6321                    })?);
6322                }
6323                // MOV rdhi, #0 (16-bit: MOVS Rd, #0)
6324                let rdhi_bits = reg_to_bits(rdhi) as u16;
6325                let instr: u16 = 0x2000 | (rdhi_bits << 8);
6326                bytes.extend_from_slice(&instr.to_le_bytes());
6327                Ok(bytes)
6328            }
6329
6330            // I32WrapI64: MOV rd, rnlo (just take low 32 bits)
6331            ArmOp::I32WrapI64 { rd, rnlo } => {
6332                if rd == rnlo {
6333                    // No-op: already in the right register
6334                    let instr: u16 = 0xBF00; // NOP
6335                    Ok(instr.to_le_bytes().to_vec())
6336                } else {
6337                    // MOV rd, rnlo
6338                    self.encode_thumb(&ArmOp::Mov {
6339                        rd: *rd,
6340                        op2: Operand2::Reg(*rnlo),
6341                    })
6342                }
6343            }
6344
6345            // ===== Helium MVE operations (Thumb-2 encoding) =====
6346            ArmOp::MveLoad { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vldrw(qd, addr))),
6347            ArmOp::MveStore { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vstrw(qd, addr))),
6348            ArmOp::MveConst { qd, bytes } => self.encode_thumb_mve_const(qd, bytes),
6349            ArmOp::MveAnd { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6350                0xEF000150, qd, qn, qm,
6351            ))),
6352            ArmOp::MveOrr { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6353                0xEF200150, qd, qn, qm,
6354            ))),
6355            ArmOp::MveEor { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6356                0xFF000150, qd, qn, qm,
6357            ))),
6358            ArmOp::MveMvn { qd, qm } => {
6359                // VMVN Qd, Qm: 0xFFB005C0 | Qd<<12 | Qm
6360                let qd_enc = qreg_to_num(qd);
6361                let qm_enc = qreg_to_num(qm);
6362                let instr: u32 = 0xFFB005C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6363                Ok(vfp_to_thumb_bytes(instr))
6364            }
6365            ArmOp::MveBic { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6366                0xEF100150, qd, qn, qm,
6367            ))),
6368            ArmOp::MveAddI { qd, qn, qm, size } => {
6369                let sz = mve_size_bits(size);
6370                let base: u32 = 0xEF000840 | (sz << 20);
6371                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6372            }
6373            ArmOp::MveSubI { qd, qn, qm, size } => {
6374                let sz = mve_size_bits(size);
6375                let base: u32 = 0xFF000840 | (sz << 20);
6376                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6377            }
6378            ArmOp::MveMulI { qd, qn, qm, size } => {
6379                let sz = mve_size_bits(size);
6380                let base: u32 = 0xEF000950 | (sz << 20);
6381                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6382            }
6383            ArmOp::MveNegI { qd, qm, size } => {
6384                let sz = mve_size_bits(size);
6385                // VNEG.Sx Qd, Qm
6386                let qd_enc = qreg_to_num(qd);
6387                let qm_enc = qreg_to_num(qm);
6388                let base: u32 = 0xFFB103C0 | (sz << 18);
6389                let instr = base | ((qd_enc * 2) << 12) | (qm_enc * 2);
6390                Ok(vfp_to_thumb_bytes(instr))
6391            }
6392            ArmOp::MveDup { qd, rn, size } => {
6393                let sz = mve_size_bits(size);
6394                let qd_enc = qreg_to_num(qd);
6395                let rn_bits = reg_to_bits(rn);
6396                // VDUP.sz Qd, Rn: EEA0 0B10 variant
6397                // size encoding: 00=32, 01=16, 10=8
6398                let be = match sz {
6399                    0 => 0b00u32, // 8-bit
6400                    1 => 0b01,    // 16-bit
6401                    _ => 0b00,    // 32-bit (default)
6402                };
6403                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12) | (be << 5);
6404                Ok(vfp_to_thumb_bytes(instr))
6405            }
6406            ArmOp::MveExtractLane { rd, qn, lane, size } => {
6407                let qn_enc = qreg_to_num(qn);
6408                let rd_bits = reg_to_bits(rd);
6409                // VMOV.sz Rd, Dn[x] — extract from Q-register lane
6410                // For 32-bit: VMOV Rd, Dn — where Dn is the appropriate D-register
6411                let d_reg = qn_enc * 2 + ((*lane as u32) >> 1);
6412                let lane_in_d = (*lane as u32) & 1;
6413                let _sz = mve_size_bits(size);
6414                // VMOV Rd, Dn[x]: EE10 0B10 for 32-bit
6415                let instr: u32 = 0xEE100B10 | (d_reg << 16) | (rd_bits << 12) | (lane_in_d << 21);
6416                Ok(vfp_to_thumb_bytes(instr))
6417            }
6418            ArmOp::MveInsertLane { qd, rn, lane, size } => {
6419                let qd_enc = qreg_to_num(qd);
6420                let rn_bits = reg_to_bits(rn);
6421                let d_reg = qd_enc * 2 + ((*lane as u32) >> 1);
6422                let lane_in_d = (*lane as u32) & 1;
6423                let _sz = mve_size_bits(size);
6424                // VMOV Dn[x], Rn: EE00 0B10 for 32-bit
6425                let instr: u32 = 0xEE000B10 | (d_reg << 16) | (rn_bits << 12) | (lane_in_d << 21);
6426                Ok(vfp_to_thumb_bytes(instr))
6427            }
6428
6429            // MVE float comparisons — emit VCMP + VPSEL sequence (simplified: just VCMP)
6430            ArmOp::MveCmpEqI { qd, qn, qm, size }
6431            | ArmOp::MveCmpNeI { qd, qn, qm, size }
6432            | ArmOp::MveCmpLtS { qd, qn, qm, size }
6433            | ArmOp::MveCmpLtU { qd, qn, qm, size }
6434            | ArmOp::MveCmpGtS { qd, qn, qm, size }
6435            | ArmOp::MveCmpGtU { qd, qn, qm, size }
6436            | ArmOp::MveCmpLeS { qd, qn, qm, size }
6437            | ArmOp::MveCmpLeU { qd, qn, qm, size }
6438            | ArmOp::MveCmpGeS { qd, qn, qm, size }
6439            | ArmOp::MveCmpGeU { qd, qn, qm, size } => {
6440                // Encode as VADD (placeholder encoding — real implementation
6441                // would use VCMP + VPSEL pair)
6442                let sz = mve_size_bits(size);
6443                let base: u32 = 0xEF000840 | (sz << 20);
6444                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6445            }
6446
6447            // f32x4 MVE arithmetic
6448            ArmOp::MveAddF32 { qd, qn, qm } => {
6449                // VADD.F32 Qd, Qn, Qm (MVE): 0xEF000D40
6450                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6451            }
6452            ArmOp::MveSubF32 { qd, qn, qm } => {
6453                // VSUB.F32 Qd, Qn, Qm (MVE): 0xEF200D40
6454                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF200D40, qd, qn, qm)))
6455            }
6456            ArmOp::MveMulF32 { qd, qn, qm } => {
6457                // VMUL.F32 Qd, Qn, Qm (MVE): 0xFF000D50
6458                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xFF000D50, qd, qn, qm)))
6459            }
6460            ArmOp::MveNegF32 { qd, qm } => {
6461                let qd_enc = qreg_to_num(qd);
6462                let qm_enc = qreg_to_num(qm);
6463                // VNEG.F32 Qd, Qm: FFB907C0
6464                let instr: u32 = 0xFFB907C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6465                Ok(vfp_to_thumb_bytes(instr))
6466            }
6467            ArmOp::MveAbsF32 { qd, qm } => {
6468                let qd_enc = qreg_to_num(qd);
6469                let qm_enc = qreg_to_num(qm);
6470                // VABS.F32 Qd, Qm: FFB90740
6471                let instr: u32 = 0xFFB90740 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6472                Ok(vfp_to_thumb_bytes(instr))
6473            }
6474            ArmOp::MveCmpEqF32 { qd, qn, qm }
6475            | ArmOp::MveCmpNeF32 { qd, qn, qm }
6476            | ArmOp::MveCmpLtF32 { qd, qn, qm }
6477            | ArmOp::MveCmpLeF32 { qd, qn, qm }
6478            | ArmOp::MveCmpGtF32 { qd, qn, qm }
6479            | ArmOp::MveCmpGeF32 { qd, qn, qm } => {
6480                // Placeholder: encode as VADD.F32 (real impl needs VCMP.F32 + VPSEL)
6481                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6482            }
6483            ArmOp::MveDupF32 { qd, rn } => {
6484                let qd_enc = qreg_to_num(qd);
6485                let rn_bits = reg_to_bits(rn);
6486                // VDUP.32 Qd, Rn (same encoding as integer VDUP.32)
6487                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12);
6488                Ok(vfp_to_thumb_bytes(instr))
6489            }
6490            ArmOp::MveExtractLaneF32 { rd, qn, lane } => {
6491                let qn_enc = qreg_to_num(qn);
6492                let rd_bits = reg_to_bits(rd);
6493                // VMOV Rd, Sn where Sn = Q*4 + lane
6494                let s_num = qn_enc * 4 + (*lane as u32);
6495                let (vn, n) = encode_sreg(s_num);
6496                let instr: u32 = 0xEE100A10 | (vn << 16) | (rd_bits << 12) | (n << 7);
6497                Ok(vfp_to_thumb_bytes(instr))
6498            }
6499            ArmOp::MveReplaceLaneF32 { qd, rn, lane } => {
6500                let qd_enc = qreg_to_num(qd);
6501                let rn_bits = reg_to_bits(rn);
6502                // VMOV Sn, Rn where Sn = Q*4 + lane
6503                let s_num = qd_enc * 4 + (*lane as u32);
6504                let (vn, n) = encode_sreg(s_num);
6505                let instr: u32 = 0xEE000A10 | (vn << 16) | (rn_bits << 12) | (n << 7);
6506                Ok(vfp_to_thumb_bytes(instr))
6507            }
6508            ArmOp::MveDivF32 { qd, qn, qm } => {
6509                // Lane-wise: extract 4 S-regs, VDIV, insert back
6510                self.encode_thumb_mve_lane_wise_f32_binop(qd, qn, qm, 0xEE800A00)
6511            }
6512            ArmOp::MveSqrtF32 { qd, qm } => {
6513                // Lane-wise: extract 4 S-regs, VSQRT, insert back
6514                self.encode_thumb_mve_lane_wise_f32_sqrt(qd, qm)
6515            }
6516
6517            // Catch-all for any remaining ops
6518            _ => {
6519                let instr: u16 = 0xBF00; // NOP
6520                Ok(instr.to_le_bytes().to_vec())
6521            }
6522        }
6523    }
6524
6525    // === Thumb-2 VFP multi-instruction helpers ===
6526
6527    /// Encode F32 comparison as Thumb-2: VCMP.F32 + VMRS + MOVS rd,#0 + IT + MOV rd,#1
6528    fn encode_thumb_f32_compare(
6529        &self,
6530        rd: &Reg,
6531        sn: &VfpReg,
6532        sm: &VfpReg,
6533        cond_code: u32,
6534    ) -> Result<Vec<u8>> {
6535        let mut bytes = Vec::new();
6536        let rd_bits = reg_to_bits(rd);
6537
6538        // VCMP.F32 Sn, Sm
6539        let sn_num = vfp_sreg_to_num(sn)?;
6540        let sm_num = vfp_sreg_to_num(sm)?;
6541        let (vd, d) = encode_sreg(sn_num);
6542        let (vm, m) = encode_sreg(sm_num);
6543        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
6544        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
6545
6546        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
6547        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
6548
6549        // MOVS Rd, #0 (16-bit): 0010 0 Rd(3) 0000 0000
6550        if rd_bits < 8 {
6551            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
6552            bytes.extend_from_slice(&movs_zero.to_le_bytes());
6553        } else {
6554            // MOV.W Rd, #0 (32-bit Thumb-2)
6555            let hw1: u16 = 0xF04F;
6556            let hw2: u16 = (rd_bits as u16) << 8;
6557            bytes.extend_from_slice(&hw1.to_le_bytes());
6558            bytes.extend_from_slice(&hw2.to_le_bytes());
6559        }
6560
6561        // IT<cond> — If-Then for conditional MOV
6562        // IT encoding: 1011 1111 cond(4) mask(4)
6563        // mask = 0x8 for single "then" (IT)
6564        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
6565        bytes.extend_from_slice(&it.to_le_bytes());
6566
6567        // MOV Rd, #1 (16-bit, conditional due to IT): 0010 0 Rd(3) 0000 0001
6568        if rd_bits < 8 {
6569            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
6570            bytes.extend_from_slice(&mov_one.to_le_bytes());
6571        } else {
6572            // MOV.W Rd, #1 (32-bit)
6573            let hw1: u16 = 0xF04F;
6574            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
6575            bytes.extend_from_slice(&hw1.to_le_bytes());
6576            bytes.extend_from_slice(&hw2.to_le_bytes());
6577        }
6578
6579        Ok(bytes)
6580    }
6581
6582    /// Encode F32 constant load as Thumb-2: MOVW + MOVT + VMOV
6583    fn encode_thumb_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
6584        let mut bytes = Vec::new();
6585        let bits = value.to_bits();
6586        let rt: u32 = 12; // R12/IP as temp
6587
6588        // MOVW R12, #lo16
6589        // Thumb-2 MOVW: 11110 i 10 0100 imm4 | 0 imm3 Rd imm8
6590        let lo16 = bits & 0xFFFF;
6591        let imm4 = (lo16 >> 12) & 0xF;
6592        let i_bit = (lo16 >> 11) & 1;
6593        let imm3 = (lo16 >> 8) & 0x7;
6594        let imm8 = lo16 & 0xFF;
6595        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
6596        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
6597        bytes.extend_from_slice(&hw1.to_le_bytes());
6598        bytes.extend_from_slice(&hw2.to_le_bytes());
6599
6600        // MOVT R12, #hi16
6601        let hi16 = (bits >> 16) & 0xFFFF;
6602        let imm4 = (hi16 >> 12) & 0xF;
6603        let i_bit = (hi16 >> 11) & 1;
6604        let imm3 = (hi16 >> 8) & 0x7;
6605        let imm8 = hi16 & 0xFF;
6606        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
6607        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
6608        bytes.extend_from_slice(&hw1.to_le_bytes());
6609        bytes.extend_from_slice(&hw2.to_le_bytes());
6610
6611        // VMOV Sd, R12
6612        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
6613        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6614
6615        Ok(bytes)
6616    }
6617
6618    /// Encode VMOV + VCVT.F32.xS32 as Thumb-2
6619    fn encode_thumb_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
6620        let mut bytes = Vec::new();
6621
6622        // VMOV Sd, Rm
6623        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
6624        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6625
6626        // VCVT.F32.S32/U32 Sd, Sd
6627        let sd_num = vfp_sreg_to_num(sd)?;
6628        let (vd, d) = encode_sreg(sd_num);
6629        let (vm, m) = encode_sreg(sd_num);
6630        let base = if signed { 0xEEB80A40 } else { 0xEEB80AC0 };
6631        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
6632        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
6633
6634        Ok(bytes)
6635    }
6636
6637    /// Encode F32 rounding pseudo-op as Thumb-2 via VCVT to integer and back
6638    /// Encode F32 rounding as Thumb-2.
6639    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
6640    ///
6641    /// For trunc: uses VCVTR.S32.F32 (always truncates).
6642    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant),
6643    /// then restores FPSCR.
6644    fn encode_thumb_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
6645        let mut bytes = Vec::new();
6646        let sm_num = vfp_sreg_to_num(sm)?;
6647        let sd_num = vfp_sreg_to_num(sd)?;
6648        let (vd_s, d_s) = encode_sreg(sd_num);
6649        let (vm_s, m_s) = encode_sreg(sm_num);
6650
6651        if mode == 0b11 {
6652            // Trunc (toward zero): VCVTR.S32.F32 — bit[7]=1, always truncates
6653            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
6654            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
6655        } else {
6656            // ceil/floor/nearest: manipulate FPSCR rounding mode
6657            let rt: u32 = 12; // R12/IP as temp
6658
6659            // VMRS R12, FPSCR
6660            let vmrs = 0xEEF10A10 | (rt << 12);
6661            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
6662
6663            // BIC.W R12, R12, #(3 << 22) — clear RMode bits [23:22]
6664            // Thumb-2 modified immediate for 3<<22 = 0x00C00000:
6665            // BIC.W encoding: 11110 i 0 0001 S Rn | 0 imm3 Rd imm8
6666            // 0x00C00000 = 0x03 shifted left by 22 => Thumb mod-imm: i=0, imm3=0b101, imm8=0x03
6667            let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF); // BIC, Rn=R12
6668            let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
6669            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
6670            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
6671
6672            // ORR.W R12, R12, #(mode << 22)
6673            if mode != 0 {
6674                let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF); // ORR, Rn=R12
6675                let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
6676                bytes.extend_from_slice(&orr_hw1.to_le_bytes());
6677                bytes.extend_from_slice(&orr_hw2.to_le_bytes());
6678            }
6679
6680            // VMSR FPSCR, R12
6681            let vmsr = 0xEEE10A10 | (rt << 12);
6682            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
6683
6684            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rmode
6685            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
6686            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
6687
6688            // Restore FPSCR: clear rmode bits back to nearest (default)
6689            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
6690            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
6691            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
6692            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
6693        }
6694
6695        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
6696        let (vd2, d2) = encode_sreg(sd_num);
6697        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
6698        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
6699
6700        Ok(bytes)
6701    }
6702
6703    /// Encode F32 min/max as Thumb-2: VMOV + VCMP + VMRS + IT + VMOV
6704    fn encode_thumb_f32_minmax(
6705        &self,
6706        sd: &VfpReg,
6707        sn: &VfpReg,
6708        sm: &VfpReg,
6709        is_min: bool,
6710    ) -> Result<Vec<u8>> {
6711        let mut bytes = Vec::new();
6712        let sn_num = vfp_sreg_to_num(sn)?;
6713        let sm_num = vfp_sreg_to_num(sm)?;
6714        let sd_num = vfp_sreg_to_num(sd)?;
6715
6716        // VMOV.F32 Sd, Sn
6717        let (vd, d) = encode_sreg(sd_num);
6718        let (vn, n) = encode_sreg(sn_num);
6719        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
6720        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sn));
6721
6722        // VCMP.F32 Sn, Sm
6723        let (vm, m) = encode_sreg(sm_num);
6724        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
6725        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
6726
6727        // VMRS APSR_nzcv, FPSCR
6728        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
6729
6730        // IT GT (for min) or IT MI (for max)
6731        let cond: u16 = if is_min { 0xC } else { 0x4 };
6732        let it: u16 = 0xBF00 | (cond << 4) | 0x8;
6733        bytes.extend_from_slice(&it.to_le_bytes());
6734
6735        // VMOV{cond}.F32 Sd, Sm — conditional VMOV in IT block
6736        let vmov_sm = 0xEEB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
6737        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sm));
6738
6739        Ok(bytes)
6740    }
6741
6742    /// Encode F32 copysign as Thumb-2
6743    fn encode_thumb_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
6744        let mut bytes = Vec::new();
6745
6746        // VMOV R12, Sm (get sign source bits)
6747        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
6748            false,
6749            sm,
6750            &Reg::R12,
6751        )?));
6752
6753        // VMOV R0, Sn (get magnitude source bits)
6754        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
6755            false,
6756            sn,
6757            &Reg::R0,
6758        )?));
6759
6760        // AND.W R12, R12, #0x80000000
6761        // Thumb-2 modified immediate: 0x80000000 = constant 0x80 with rotation
6762        // Using T1 encoding: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8
6763        // 0x80000000: i=0, imm3=0b001, imm8=0x00 (rotation=4, value=0x80)
6764        // Actually encoding #0x80000000 as modified constant:
6765        // bit pattern 1 followed by 31 zeros: enc = 0b0100_00000000 = 0x0100? No.
6766        // ARM modified immediate: abcdefgh rotated. 0x80000000 = 0x80 ROR 2 = enc 0x0102
6767        // Actually: value = abcdefgh ROR (2*rot). 0x80 = 10000000, ROR 2 gives 0x20000000.
6768        // For 0x80000000: 0x02 ROR 2 = 0x80000000. So imm12 = (1<<8) | 0x02 = 0x102
6769        let hw1: u16 = 0xF000 | 12; // AND.W R12, R12, #modified_const (i=0, Rn=R12)
6770        let hw2: u16 = (0x1 << 12) | (12 << 8) | 0x02; // imm3=1, Rd=R12, imm8=0x02
6771        bytes.extend_from_slice(&hw1.to_le_bytes());
6772        bytes.extend_from_slice(&hw2.to_le_bytes());
6773
6774        // BIC.W R0, R0, #0x80000000 (R0 = register 0, fields are zero)
6775        let hw1: u16 = 0xF020; // BIC.W R0, R0, #modified_const (i=0, Rn=R0)
6776        let hw2: u16 = (0x1 << 12) | 0x02; // imm3=1, Rd=R0, imm8=0x02
6777        bytes.extend_from_slice(&hw1.to_le_bytes());
6778        bytes.extend_from_slice(&hw2.to_le_bytes());
6779
6780        // ORR.W R0, R0, R12 (R0 = register 0)
6781        let hw1: u16 = 0xEA40; // ORR.W R0, R0, R12 (Rn=R0)
6782        let hw2: u16 = 12; // Rd=R0, Rm=R12
6783        bytes.extend_from_slice(&hw1.to_le_bytes());
6784        bytes.extend_from_slice(&hw2.to_le_bytes());
6785
6786        // VMOV Sd, R0
6787        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
6788            true,
6789            sd,
6790            &Reg::R0,
6791        )?));
6792
6793        Ok(bytes)
6794    }
6795
6796    /// Encode F64 comparison as Thumb-2: VCMP.F64 + VMRS + MOV #0 + IT + MOV #1
6797    fn encode_thumb_f64_compare(
6798        &self,
6799        rd: &Reg,
6800        dn: &VfpReg,
6801        dm: &VfpReg,
6802        cond_code: u32,
6803    ) -> Result<Vec<u8>> {
6804        let mut bytes = Vec::new();
6805        let rd_bits = reg_to_bits(rd);
6806
6807        // VCMP.F64 Dn, Dm
6808        let dn_num = vfp_dreg_to_num(dn)?;
6809        let dm_num = vfp_dreg_to_num(dm)?;
6810        let (vd, d) = encode_dreg(dn_num);
6811        let (vm, m) = encode_dreg(dm_num);
6812        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
6813        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
6814
6815        // VMRS APSR_nzcv, FPSCR
6816        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
6817
6818        // MOVS Rd, #0
6819        if rd_bits < 8 {
6820            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
6821            bytes.extend_from_slice(&movs_zero.to_le_bytes());
6822        } else {
6823            let hw1: u16 = 0xF04F;
6824            let hw2: u16 = (rd_bits as u16) << 8;
6825            bytes.extend_from_slice(&hw1.to_le_bytes());
6826            bytes.extend_from_slice(&hw2.to_le_bytes());
6827        }
6828
6829        // IT<cond>
6830        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
6831        bytes.extend_from_slice(&it.to_le_bytes());
6832
6833        // MOV Rd, #1
6834        if rd_bits < 8 {
6835            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
6836            bytes.extend_from_slice(&mov_one.to_le_bytes());
6837        } else {
6838            let hw1: u16 = 0xF04F;
6839            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
6840            bytes.extend_from_slice(&hw1.to_le_bytes());
6841            bytes.extend_from_slice(&hw2.to_le_bytes());
6842        }
6843
6844        Ok(bytes)
6845    }
6846
6847    /// Encode F64 constant load as Thumb-2: MOVW+MOVT (lo32 into R0) + MOVW+MOVT (hi32 into R12) + VMOV Dd, R0, R12
6848    fn encode_thumb_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
6849        let mut bytes = Vec::new();
6850        let bits = value.to_bits();
6851        let lo32 = bits as u32;
6852        let hi32 = (bits >> 32) as u32;
6853
6854        // MOVW R0, #lo16(lo32)
6855        let lo16 = lo32 & 0xFFFF;
6856        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(0, lo16)?);
6857
6858        // MOVT R0, #hi16(lo32)
6859        let hi16 = (lo32 >> 16) & 0xFFFF;
6860        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(0, hi16)?);
6861
6862        // MOVW R12, #lo16(hi32)
6863        let lo16 = hi32 & 0xFFFF;
6864        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
6865
6866        // MOVT R12, #hi16(hi32)
6867        let hi16 = (hi32 >> 16) & 0xFFFF;
6868        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
6869
6870        // VMOV Dd, R0, R12
6871        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
6872        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6873
6874        Ok(bytes)
6875    }
6876
6877    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as Thumb-2
6878    fn encode_thumb_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
6879        let mut bytes = Vec::new();
6880
6881        // VMOV S0, Rm
6882        let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
6883        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6884
6885        // VCVT.F64.S32 Dd, S0 or VCVT.F64.U32 Dd, S0
6886        let dd_num = vfp_dreg_to_num(dd)?;
6887        let (vd, d) = encode_dreg(dd_num);
6888        let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
6889        let vcvt = base | (d << 22) | (vd << 12);
6890        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
6891
6892        Ok(bytes)
6893    }
6894
6895    /// Encode VCVT.F64.F32 Dd, Sm as Thumb-2
6896    fn encode_thumb_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
6897        let dd_num = vfp_dreg_to_num(dd)?;
6898        let sm_num = vfp_sreg_to_num(sm)?;
6899        let (vd, d) = encode_dreg(dd_num);
6900        let (vm, m) = encode_sreg(sm_num);
6901
6902        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
6903        Ok(vfp_to_thumb_bytes(vcvt))
6904    }
6905
6906    /// Encode VCVT.S32/U32.F64 S0, Dm + VMOV Rd, S0 as Thumb-2
6907    fn encode_thumb_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
6908        let mut bytes = Vec::new();
6909        let dm_num = vfp_dreg_to_num(dm)?;
6910        let (vm, m) = encode_dreg(dm_num);
6911
6912        // VCVT.S32.F64 S0, Dm or VCVT.U32.F64 S0, Dm
6913        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
6914        let vcvt = base | (m << 5) | vm;
6915        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
6916
6917        // VMOV Rd, S0
6918        let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
6919        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6920
6921        Ok(bytes)
6922    }
6923
6924    /// Encode F64 rounding pseudo-op as Thumb-2 via VCVT to integer and back
6925    /// Encode F64 rounding as Thumb-2.
6926    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
6927    fn encode_thumb_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
6928        let mut bytes = Vec::new();
6929        let dm_num = vfp_dreg_to_num(dm)?;
6930        let dd_num = vfp_dreg_to_num(dd)?;
6931        let (vm, m) = encode_dreg(dm_num);
6932        let (vd, d) = encode_dreg(dd_num);
6933
6934        if mode == 0b11 {
6935            // Trunc: VCVTR.S32.F64 — bit[7]=1, always truncates
6936            let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
6937            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
6938        } else {
6939            let rt: u32 = 12;
6940
6941            // VMRS R12, FPSCR
6942            let vmrs = 0xEEF10A10 | (rt << 12);
6943            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
6944
6945            // BIC.W R12, R12, #(3 << 22)
6946            let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF);
6947            let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
6948            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
6949            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
6950
6951            // ORR.W R12, R12, #(mode << 22)
6952            if mode != 0 {
6953                let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF);
6954                let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
6955                bytes.extend_from_slice(&orr_hw1.to_le_bytes());
6956                bytes.extend_from_slice(&orr_hw2.to_le_bytes());
6957            }
6958
6959            // VMSR FPSCR, R12
6960            let vmsr = 0xEEE10A10 | (rt << 12);
6961            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
6962
6963            // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0)
6964            let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
6965            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
6966
6967            // Restore FPSCR
6968            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
6969            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
6970            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
6971            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
6972        }
6973
6974        // VCVT.F64.S32 Dd, S0
6975        let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
6976        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
6977
6978        Ok(bytes)
6979    }
6980
6981    /// Encode F64 min/max as Thumb-2
6982    fn encode_thumb_f64_minmax(
6983        &self,
6984        dd: &VfpReg,
6985        dn: &VfpReg,
6986        dm: &VfpReg,
6987        is_min: bool,
6988    ) -> Result<Vec<u8>> {
6989        let mut bytes = Vec::new();
6990        let dn_num = vfp_dreg_to_num(dn)?;
6991        let dm_num = vfp_dreg_to_num(dm)?;
6992        let dd_num = vfp_dreg_to_num(dd)?;
6993
6994        // VMOV.F64 Dd, Dn
6995        let (vd, d) = encode_dreg(dd_num);
6996        let (vn, n) = encode_dreg(dn_num);
6997        let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
6998        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_dn));
6999
7000        // VCMP.F64 Dn, Dm
7001        let (vm, m) = encode_dreg(dm_num);
7002        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7003        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7004
7005        // VMRS APSR_nzcv, FPSCR
7006        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7007
7008        // IT GT (for min) or IT MI (for max)
7009        let cond: u16 = if is_min { 0xC } else { 0x4 };
7010        let it: u16 = 0xBF00 | (cond << 4) | 0x8;
7011        bytes.extend_from_slice(&it.to_le_bytes());
7012
7013        // VMOV{cond}.F64 Dd, Dm
7014        let vmov_dm = 0xEEB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7015        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_dm));
7016
7017        Ok(bytes)
7018    }
7019
7020    /// Encode F64 copysign as Thumb-2
7021    fn encode_thumb_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7022        let mut bytes = Vec::new();
7023
7024        // VMOV R0, R12, Dm (get sign source)
7025        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_dreg(
7026            false,
7027            dm,
7028            &Reg::R0,
7029            &Reg::R12,
7030        )?));
7031
7032        // VMOV R1, R2, Dn (get magnitude source)
7033        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_dreg(
7034            false,
7035            dn,
7036            &Reg::R1,
7037            &Reg::R2,
7038        )?));
7039
7040        // AND.W R12, R12, #0x80000000 (i=0, Rn=R12)
7041        let hw1: u16 = 0xF000 | 12;
7042        let hw2: u16 = (0x1 << 12) | (12 << 8) | 0x02;
7043        bytes.extend_from_slice(&hw1.to_le_bytes());
7044        bytes.extend_from_slice(&hw2.to_le_bytes());
7045
7046        // BIC.W R2, R2, #0x80000000 (i=0, Rn=R2)
7047        let hw1: u16 = 0xF020 | 2;
7048        let hw2: u16 = (0x1 << 12) | (2 << 8) | 0x02;
7049        bytes.extend_from_slice(&hw1.to_le_bytes());
7050        bytes.extend_from_slice(&hw2.to_le_bytes());
7051
7052        // ORR.W R2, R2, R12
7053        let hw1: u16 = 0xEA40 | 2;
7054        let hw2: u16 = (2 << 8) | 12;
7055        bytes.extend_from_slice(&hw1.to_le_bytes());
7056        bytes.extend_from_slice(&hw2.to_le_bytes());
7057
7058        // VMOV Dd, R1, R2
7059        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_dreg(
7060            true,
7061            dd,
7062            &Reg::R1,
7063            &Reg::R2,
7064        )?));
7065
7066        Ok(bytes)
7067    }
7068
7069    /// Encode VCVT.S32/U32.F32 + VMOV as Thumb-2
7070    fn encode_thumb_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7071        let mut bytes = Vec::new();
7072
7073        let sm_num = vfp_sreg_to_num(sm)?;
7074        let (vd, d) = encode_sreg(sm_num);
7075        let (vm, m) = encode_sreg(sm_num);
7076        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
7077        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7078        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7079
7080        // VMOV Rd, Sm
7081        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
7082        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7083
7084        Ok(bytes)
7085    }
7086
7087    // === Thumb-2 32-bit encoding helpers ===
7088
7089    /// Encode Thumb-2 32-bit ADD with immediate
7090    fn encode_thumb32_add(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7091        let rd_bits = reg_to_bits(rd);
7092        let rn_bits = reg_to_bits(rn);
7093
7094        // The `i:imm3:imm8` field is split the same way for both forms.
7095        let i_bit = (imm >> 11) & 1;
7096        let imm3 = (imm >> 8) & 0x7;
7097        let imm8 = imm & 0xFF;
7098
7099        let hw1_base = if imm <= 0xFF {
7100            // ADD.W (T3): the field is a ThumbExpandImm modified immediate. For
7101            // imm <= 0xFF (i:imm3 = 0000) it is the zero-extended byte, which is
7102            // correct — keep this form so existing encodings stay bit-identical.
7103            0xF100
7104        } else if imm <= 0xFFF {
7105            // ADDW (T4): a PLAIN 12-bit immediate (0..4095) — no ThumbExpandImm.
7106            // This is what makes `add sp, sp, #frame` correct for frame sizes
7107            // >= 256, which ADD.W (T3) would silently mis-encode (e.g. #256 -> #0).
7108            0xF200
7109        } else {
7110            return Err(synth_core::Error::synthesis(
7111                "ADD immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7112            ));
7113        };
7114
7115        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7116        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7117
7118        let mut bytes = hw1.to_le_bytes().to_vec();
7119        bytes.extend_from_slice(&hw2.to_le_bytes());
7120        Ok(bytes)
7121    }
7122
7123    /// Encode Thumb-2 32-bit SUB with immediate
7124    fn encode_thumb32_sub(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7125        let rd_bits = reg_to_bits(rd);
7126        let rn_bits = reg_to_bits(rn);
7127
7128        let i_bit = (imm >> 11) & 1;
7129        let imm3 = (imm >> 8) & 0x7;
7130        let imm8 = imm & 0xFF;
7131
7132        let hw1_base = if imm <= 0xFF {
7133            // SUB.W (T3) modified immediate — correct for the zero-extended byte
7134            // (imm <= 0xFF). Kept bit-identical for existing encodings.
7135            0xF1A0
7136        } else if imm <= 0xFFF {
7137            // SUBW (T4): plain 12-bit immediate (0..4095). Makes
7138            // `sub sp, sp, #frame` correct for frame sizes >= 256.
7139            0xF2A0
7140        } else {
7141            return Err(synth_core::Error::synthesis(
7142                "SUB immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7143            ));
7144        };
7145
7146        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7147        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7148
7149        let mut bytes = hw1.to_le_bytes().to_vec();
7150        bytes.extend_from_slice(&hw2.to_le_bytes());
7151        Ok(bytes)
7152    }
7153
7154    /// Encode Thumb-2 32-bit ADDS with immediate (sets flags)
7155    fn encode_thumb32_adds(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7156        let rd_bits = reg_to_bits(rd);
7157        let rn_bits = reg_to_bits(rn);
7158
7159        // ADDS.W (flag-setting) has only the modified-immediate form — error on
7160        // an un-encodable value rather than silently add the wrong constant.
7161        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7162            synth_core::Error::synthesis(
7163                "ADDS immediate is not a valid ThumbExpandImm — materialize into a register",
7164            )
7165        })?;
7166        let i_bit = (field >> 11) & 1;
7167        let imm3 = (field >> 8) & 0x7;
7168        let imm8 = field & 0xFF;
7169
7170        // ADDS.W Rd, Rn, #imm (with S=1)
7171        // First halfword: 1111 0 i 0 1000 1 Rn = F110 | i<<10 | Rn
7172        let hw1: u16 = (0xF110 | (i_bit << 10) | rn_bits) as u16;
7173        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7174
7175        let mut bytes = hw1.to_le_bytes().to_vec();
7176        bytes.extend_from_slice(&hw2.to_le_bytes());
7177        Ok(bytes)
7178    }
7179
7180    /// Encode Thumb-2 32-bit SUBS with immediate (sets flags)
7181    fn encode_thumb32_subs(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7182        let rd_bits = reg_to_bits(rd);
7183        let rn_bits = reg_to_bits(rn);
7184
7185        // SUBS.W (flag-setting) has only the modified-immediate form — error on
7186        // an un-encodable value rather than silently subtract the wrong constant.
7187        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7188            synth_core::Error::synthesis(
7189                "SUBS immediate is not a valid ThumbExpandImm — materialize into a register",
7190            )
7191        })?;
7192        let i_bit = (field >> 11) & 1;
7193        let imm3 = (field >> 8) & 0x7;
7194        let imm8 = field & 0xFF;
7195
7196        // SUBS.W Rd, Rn, #imm (with S=1)
7197        // First halfword: 1111 0 i 0 1101 1 Rn = F1B0 | i<<10 | Rn
7198        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7199        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7200
7201        let mut bytes = hw1.to_le_bytes().to_vec();
7202        bytes.extend_from_slice(&hw2.to_le_bytes());
7203        Ok(bytes)
7204    }
7205
7206    /// Encode Thumb-2 32-bit MOVW (16-bit immediate)
7207    ///
7208    /// # Contract (Verus-style)
7209    /// ```text
7210    /// requires rd <= R14
7211    /// ensures result.len() == 4
7212    /// ensures (imm & 0xFFFF) can be reconstructed from the encoding
7213    /// ```
7214    fn encode_thumb32_movw(&self, rd: &Reg, imm: u32) -> Result<Vec<u8>> {
7215        let rd_bits = reg_to_bits(rd);
7216        reg_bits_checked(rd_bits)?;
7217        let imm16 = imm & 0xFFFF;
7218
7219        // MOVW Rd, #imm16
7220        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7221        let imm4 = (imm16 >> 12) & 0xF;
7222        let i_bit = (imm16 >> 11) & 1;
7223        let imm3 = (imm16 >> 8) & 0x7;
7224        let imm8 = imm16 & 0xFF;
7225
7226        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7227        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7228
7229        let mut bytes = hw1.to_le_bytes().to_vec();
7230        bytes.extend_from_slice(&hw2.to_le_bytes());
7231        encoding_contracts::verify_thumb32(&bytes);
7232        Ok(bytes)
7233    }
7234
7235    /// Encode Thumb-2 32-bit shift with immediate
7236    ///
7237    /// # Contract (Verus-style)
7238    /// ```text
7239    /// requires rd <= R14, rm <= R14
7240    /// ensures result.len() == 4
7241    /// ```
7242    fn encode_thumb32_shift(
7243        &self,
7244        rd: &Reg,
7245        rm: &Reg,
7246        shift: u32,
7247        shift_type: u8,
7248    ) -> Result<Vec<u8>> {
7249        let rd_bits = reg_to_bits(rd);
7250        let rm_bits = reg_to_bits(rm);
7251        reg_bits_checked(rd_bits)?;
7252        reg_bits_checked(rm_bits)?;
7253        let imm5 = shift & 0x1F;
7254        let imm2 = imm5 & 0x3;
7255        let imm3 = (imm5 >> 2) & 0x7;
7256
7257        // MOV.W Rd, Rm, <shift> #imm
7258        // EA4F 0 imm3 Rd imm2 type Rm
7259        let hw1: u16 = 0xEA4F;
7260        let hw2: u16 =
7261            ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | ((shift_type as u32) << 4) | rm_bits)
7262                as u16;
7263
7264        let mut bytes = hw1.to_le_bytes().to_vec();
7265        bytes.extend_from_slice(&hw2.to_le_bytes());
7266        Ok(bytes)
7267    }
7268
7269    /// Encode Thumb-2 32-bit shift by register
7270    /// Encoding: 11111010 0xx0 Rn | 1111 Rd 0000 Rm
7271    /// shift_type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
7272    fn encode_thumb32_shift_reg(
7273        &self,
7274        rd: &Reg,
7275        rn: &Reg,
7276        rm: &Reg,
7277        shift_type: u8,
7278    ) -> Result<Vec<u8>> {
7279        let rd_bits = reg_to_bits(rd);
7280        let rn_bits = reg_to_bits(rn);
7281        let rm_bits = reg_to_bits(rm);
7282
7283        // hw1: 1111 1010 0xx0 Rn
7284        let hw1: u16 = (0xFA00 | ((shift_type as u32) << 5) | rn_bits) as u16;
7285        // hw2: 1111 Rd 0000 Rm
7286        let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
7287
7288        let mut bytes = hw1.to_le_bytes().to_vec();
7289        bytes.extend_from_slice(&hw2.to_le_bytes());
7290        Ok(bytes)
7291    }
7292
7293    /// Encode Thumb-2 32-bit CMP with immediate
7294    fn encode_thumb32_cmp_imm(&self, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7295        let rn_bits = reg_to_bits(rn);
7296
7297        // CMP.W has only the modified-immediate form (no plain-imm12 like ADDW),
7298        // so an un-encodable immediate MUST be materialized into a register by
7299        // the selector. Error rather than silently compare the wrong constant.
7300        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7301            synth_core::Error::synthesis(
7302                "CMP immediate is not a valid ThumbExpandImm — materialize into a register",
7303            )
7304        })?;
7305        let i_bit = (field >> 11) & 1;
7306        let imm3 = (field >> 8) & 0x7;
7307        let imm8 = field & 0xFF;
7308
7309        // CMP.W Rn, #imm
7310        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7311        let hw2: u16 = ((imm3 << 12) | 0x0F00 | imm8) as u16;
7312
7313        let mut bytes = hw1.to_le_bytes().to_vec();
7314        bytes.extend_from_slice(&hw2.to_le_bytes());
7315        Ok(bytes)
7316    }
7317
7318    /// #372/#382: resolve the base register AND residual immediate offset for an
7319    /// `I64Ldr`/`I64Str` whose address may carry an index register. Returns
7320    /// `(base, low_offset)`; the caller accesses the halves at `[base,
7321    /// #low_offset]` and `[base, #low_offset + 4]`.
7322    ///
7323    /// - Frame access (no `offset_reg`, e.g. a spilled local at `[SP, #off]`):
7324    ///   returns `(addr.base, off)` and emits NOTHING — byte-identical.
7325    /// - Memory access (`reg_imm(R11, addr, offset)` = `R11 + addr + offset`)
7326    ///   with `offset + 4 <= 0xFFF`: emits `ADD.W ip, base, index` and returns
7327    ///   `(ip, offset)`, folding `offset`/`offset+4` into the halves' imm12.
7328    ///   Byte-identical to the pre-#382 (#372) behavior.
7329    /// - Memory access with `offset + 4 > 0xFFF`: the imm12 form cannot hold the
7330    ///   high half's offset, so `encode_thumb32_ldr`'s `check_ldst_imm12` (#259)
7331    ///   rightly refused it and the WHOLE function was skipped (#382). Instead
7332    ///   MATERIALIZE the offset into the base: `ADD ip, index, #offset` (against
7333    ///   the read-only INDEX register, so `encode_thumb32_add_imm` never trips its
7334    ///   `rd==rn==R12` alias trap), then `ADD.W ip, ip, base` (+ R11), and return
7335    ///   `(ip, 0)` so the halves use `[ip, #0]` / `[ip, #4]`.
7336    ///
7337    /// The effective address is fully materialized into `ip` BEFORE the halves
7338    /// are accessed, so an `rdlo` aliasing the index register is safe.
7339    fn i64_effective_base(&self, bytes: &mut Vec<u8>, addr: &MemAddr) -> Result<(Reg, u32)> {
7340        let offset = if addr.offset < 0 {
7341            0u32
7342        } else {
7343            addr.offset as u32
7344        };
7345        match addr.offset_reg {
7346            Some(idx) => {
7347                let ip = Reg::R12;
7348                if offset.wrapping_add(4) > 0xFFF {
7349                    // Large static offset (#382): fold it (and R11) into ip so the
7350                    // imm12 halves stay in range instead of skipping the function.
7351                    // ADD ip, index, #offset  (index != ip → no add_imm alias trap)
7352                    bytes.extend_from_slice(&self.encode_thumb32_add_imm(&ip, &idx, offset)?);
7353                    // ADD.W ip, ip, base  (+ R11)
7354                    bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
7355                        reg_to_bits(&ip),
7356                        reg_to_bits(&ip),
7357                        reg_to_bits(&addr.base),
7358                    )?);
7359                    Ok((ip, 0))
7360                } else {
7361                    // ADD.W ip, addr.base, idx  (Thumb-2, byte-verified vs as)
7362                    let hw1: u16 = 0xEB00 | reg_to_bits(&addr.base) as u16;
7363                    let hw2: u16 = 0x0C00 | reg_to_bits(&idx) as u16;
7364                    bytes.extend_from_slice(&hw1.to_le_bytes());
7365                    bytes.extend_from_slice(&hw2.to_le_bytes());
7366                    Ok((ip, offset))
7367                }
7368            }
7369            None => Ok((addr.base, offset)),
7370        }
7371    }
7372
7373    /// Encode Thumb-2 32-bit LDR
7374    fn encode_thumb32_ldr(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7375        let rd_bits = reg_to_bits(rd);
7376        let base_bits = reg_to_bits(base);
7377
7378        // LDR.W Rd, [Rn, #imm12]
7379        check_ldst_imm12(offset)?;
7380        let hw1: u16 = (0xF8D0 | base_bits) as u16;
7381        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7382
7383        let mut bytes = hw1.to_le_bytes().to_vec();
7384        bytes.extend_from_slice(&hw2.to_le_bytes());
7385        Ok(bytes)
7386    }
7387
7388    /// Encode Thumb-2 32-bit STR
7389    fn encode_thumb32_str(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7390        let rd_bits = reg_to_bits(rd);
7391        let base_bits = reg_to_bits(base);
7392
7393        // STR.W Rd, [Rn, #imm12]
7394        check_ldst_imm12(offset)?;
7395        let hw1: u16 = (0xF8C0 | base_bits) as u16;
7396        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7397
7398        let mut bytes = hw1.to_le_bytes().to_vec();
7399        bytes.extend_from_slice(&hw2.to_le_bytes());
7400        Ok(bytes)
7401    }
7402
7403    /// Encode Thumb-2 32-bit LDR with register offset: LDR.W Rd, [Rn, Rm]
7404    fn encode_thumb32_ldr_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7405        let rd_bits = reg_to_bits(rd);
7406        let base_bits = reg_to_bits(base);
7407        let rm_bits = reg_to_bits(offset_reg);
7408
7409        // LDR.W Rd, [Rn, Rm, LSL #0]
7410        // Encoding: 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
7411        // imm2 = 00 for no shift (LSL #0)
7412        let hw1: u16 = (0xF850 | base_bits) as u16;
7413        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7414
7415        let mut bytes = hw1.to_le_bytes().to_vec();
7416        bytes.extend_from_slice(&hw2.to_le_bytes());
7417        Ok(bytes)
7418    }
7419
7420    /// Encode Thumb-2 32-bit STR with register offset: STR.W Rd, [Rn, Rm]
7421    fn encode_thumb32_str_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7422        let rd_bits = reg_to_bits(rd);
7423        let base_bits = reg_to_bits(base);
7424        let rm_bits = reg_to_bits(offset_reg);
7425
7426        // STR.W Rd, [Rn, Rm, LSL #0]
7427        // Encoding: 1111 1000 0100 Rn | Rt 0000 00 imm2 Rm
7428        // imm2 = 00 for no shift (LSL #0)
7429        let hw1: u16 = (0xF840 | base_bits) as u16;
7430        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7431
7432        let mut bytes = hw1.to_le_bytes().to_vec();
7433        bytes.extend_from_slice(&hw2.to_le_bytes());
7434        Ok(bytes)
7435    }
7436
7437    // === Sub-word load/store Thumb-2 encoding helpers ===
7438
7439    /// Encode Thumb-2 32-bit LDRB with immediate: LDRB.W Rd, [Rn, #imm12]
7440    fn encode_thumb32_ldrb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7441        let rd_bits = reg_to_bits(rd);
7442        let base_bits = reg_to_bits(base);
7443        // LDRB.W Rd, [Rn, #imm12]: 1111 1000 1001 Rn | Rt imm12
7444        check_ldst_imm12(offset)?;
7445        let hw1: u16 = (0xF890 | base_bits) as u16;
7446        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7447        let mut bytes = hw1.to_le_bytes().to_vec();
7448        bytes.extend_from_slice(&hw2.to_le_bytes());
7449        Ok(bytes)
7450    }
7451
7452    /// Encode Thumb-2 32-bit LDRB with register: LDRB.W Rd, [Rn, Rm]
7453    fn encode_thumb32_ldrb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7454        let rd_bits = reg_to_bits(rd);
7455        let base_bits = reg_to_bits(base);
7456        let rm_bits = reg_to_bits(offset_reg);
7457        // LDRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0001 Rn | Rt 0000 00 imm2 Rm
7458        let hw1: u16 = (0xF810 | base_bits) as u16;
7459        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7460        let mut bytes = hw1.to_le_bytes().to_vec();
7461        bytes.extend_from_slice(&hw2.to_le_bytes());
7462        Ok(bytes)
7463    }
7464
7465    /// Encode Thumb-2 32-bit LDRSB with immediate: LDRSB.W Rd, [Rn, #imm12]
7466    fn encode_thumb32_ldrsb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7467        let rd_bits = reg_to_bits(rd);
7468        let base_bits = reg_to_bits(base);
7469        // LDRSB.W Rd, [Rn, #imm12]: 1111 1001 1001 Rn | Rt imm12
7470        check_ldst_imm12(offset)?;
7471        let hw1: u16 = (0xF990 | base_bits) as u16;
7472        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7473        let mut bytes = hw1.to_le_bytes().to_vec();
7474        bytes.extend_from_slice(&hw2.to_le_bytes());
7475        Ok(bytes)
7476    }
7477
7478    /// Encode Thumb-2 32-bit LDRSB with register: LDRSB.W Rd, [Rn, Rm]
7479    fn encode_thumb32_ldrsb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7480        let rd_bits = reg_to_bits(rd);
7481        let base_bits = reg_to_bits(base);
7482        let rm_bits = reg_to_bits(offset_reg);
7483        // LDRSB.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0001 Rn | Rt 0000 00 imm2 Rm
7484        let hw1: u16 = (0xF910 | base_bits) as u16;
7485        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7486        let mut bytes = hw1.to_le_bytes().to_vec();
7487        bytes.extend_from_slice(&hw2.to_le_bytes());
7488        Ok(bytes)
7489    }
7490
7491    /// Encode Thumb-2 32-bit LDRH with immediate: LDRH.W Rd, [Rn, #imm12]
7492    fn encode_thumb32_ldrh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7493        let rd_bits = reg_to_bits(rd);
7494        let base_bits = reg_to_bits(base);
7495        // LDRH.W Rd, [Rn, #imm12]: 1111 1000 1011 Rn | Rt imm12
7496        check_ldst_imm12(offset)?;
7497        let hw1: u16 = (0xF8B0 | base_bits) as u16;
7498        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7499        let mut bytes = hw1.to_le_bytes().to_vec();
7500        bytes.extend_from_slice(&hw2.to_le_bytes());
7501        Ok(bytes)
7502    }
7503
7504    /// Encode Thumb-2 32-bit LDRH with register: LDRH.W Rd, [Rn, Rm]
7505    fn encode_thumb32_ldrh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7506        let rd_bits = reg_to_bits(rd);
7507        let base_bits = reg_to_bits(base);
7508        let rm_bits = reg_to_bits(offset_reg);
7509        // LDRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0011 Rn | Rt 0000 00 imm2 Rm
7510        let hw1: u16 = (0xF830 | base_bits) as u16;
7511        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7512        let mut bytes = hw1.to_le_bytes().to_vec();
7513        bytes.extend_from_slice(&hw2.to_le_bytes());
7514        Ok(bytes)
7515    }
7516
7517    /// Encode Thumb-2 32-bit LDRSH with immediate: LDRSH.W Rd, [Rn, #imm12]
7518    fn encode_thumb32_ldrsh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7519        let rd_bits = reg_to_bits(rd);
7520        let base_bits = reg_to_bits(base);
7521        // LDRSH.W Rd, [Rn, #imm12]: 1111 1001 1011 Rn | Rt imm12
7522        check_ldst_imm12(offset)?;
7523        let hw1: u16 = (0xF9B0 | base_bits) as u16;
7524        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7525        let mut bytes = hw1.to_le_bytes().to_vec();
7526        bytes.extend_from_slice(&hw2.to_le_bytes());
7527        Ok(bytes)
7528    }
7529
7530    /// Encode Thumb-2 32-bit LDRSH with register: LDRSH.W Rd, [Rn, Rm]
7531    fn encode_thumb32_ldrsh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7532        let rd_bits = reg_to_bits(rd);
7533        let base_bits = reg_to_bits(base);
7534        let rm_bits = reg_to_bits(offset_reg);
7535        // LDRSH.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0011 Rn | Rt 0000 00 imm2 Rm
7536        let hw1: u16 = (0xF930 | base_bits) as u16;
7537        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7538        let mut bytes = hw1.to_le_bytes().to_vec();
7539        bytes.extend_from_slice(&hw2.to_le_bytes());
7540        Ok(bytes)
7541    }
7542
7543    /// Encode Thumb-2 32-bit STRB with immediate: STRB.W Rd, [Rn, #imm12]
7544    fn encode_thumb32_strb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7545        let rd_bits = reg_to_bits(rd);
7546        let base_bits = reg_to_bits(base);
7547        // STRB.W Rd, [Rn, #imm12]: 1111 1000 1000 Rn | Rt imm12
7548        check_ldst_imm12(offset)?;
7549        let hw1: u16 = (0xF880 | base_bits) as u16;
7550        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7551        let mut bytes = hw1.to_le_bytes().to_vec();
7552        bytes.extend_from_slice(&hw2.to_le_bytes());
7553        Ok(bytes)
7554    }
7555
7556    /// Encode Thumb-2 32-bit STRB with register: STRB.W Rd, [Rn, Rm]
7557    fn encode_thumb32_strb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7558        let rd_bits = reg_to_bits(rd);
7559        let base_bits = reg_to_bits(base);
7560        let rm_bits = reg_to_bits(offset_reg);
7561        // STRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0000 Rn | Rt 0000 00 imm2 Rm
7562        let hw1: u16 = (0xF800 | base_bits) as u16;
7563        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7564        let mut bytes = hw1.to_le_bytes().to_vec();
7565        bytes.extend_from_slice(&hw2.to_le_bytes());
7566        Ok(bytes)
7567    }
7568
7569    /// Encode Thumb-2 32-bit STRH with immediate: STRH.W Rd, [Rn, #imm12]
7570    fn encode_thumb32_strh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7571        let rd_bits = reg_to_bits(rd);
7572        let base_bits = reg_to_bits(base);
7573        // STRH.W Rd, [Rn, #imm12]: 1111 1000 1010 Rn | Rt imm12
7574        check_ldst_imm12(offset)?;
7575        let hw1: u16 = (0xF8A0 | base_bits) as u16;
7576        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7577        let mut bytes = hw1.to_le_bytes().to_vec();
7578        bytes.extend_from_slice(&hw2.to_le_bytes());
7579        Ok(bytes)
7580    }
7581
7582    /// Encode Thumb-2 32-bit STRH with register: STRH.W Rd, [Rn, Rm]
7583    fn encode_thumb32_strh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7584        let rd_bits = reg_to_bits(rd);
7585        let base_bits = reg_to_bits(base);
7586        let rm_bits = reg_to_bits(offset_reg);
7587        // STRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0010 Rn | Rt 0000 00 imm2 Rm
7588        let hw1: u16 = (0xF820 | base_bits) as u16;
7589        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7590        let mut bytes = hw1.to_le_bytes().to_vec();
7591        bytes.extend_from_slice(&hw2.to_le_bytes());
7592        Ok(bytes)
7593    }
7594
7595    /// Encode Thumb-2 32-bit ADD with immediate: ADD.W Rd, Rn, #imm
7596    fn encode_thumb32_add_imm(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7597        let rd_bits = reg_to_bits(rd);
7598        let rn_bits = reg_to_bits(rn);
7599
7600        // For small immediates, use ADD.W Rd, Rn, #imm12
7601        // Encoding: 1111 0 i 0 1 0 0 0 S Rn | 0 imm3 Rd imm8
7602        // S = 0 (don't update flags)
7603        // The 12-bit immediate is encoded as: i:imm3:imm8
7604        // For simplicity, we only support imm <= 0xFFF (direct encoding)
7605        if imm <= 0xFFF {
7606            let i_bit = (imm >> 11) & 1;
7607            let imm3 = (imm >> 8) & 0x7;
7608            let imm8 = imm & 0xFF;
7609
7610            let hw1: u16 = (0xF100 | (i_bit << 10) | rn_bits) as u16;
7611            let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7612
7613            let mut bytes = hw1.to_le_bytes().to_vec();
7614            bytes.extend_from_slice(&hw2.to_le_bytes());
7615            Ok(bytes)
7616        } else {
7617            // Out-of-range immediate (> 0xFFF): materialize it into a scratch
7618            // register, then ADD.W Rd, Rn, scratch. This is the #180/#185
7619            // "encoder must produce a legal sequence, not assert" class — see #350.
7620            //
7621            // Scratch choice (must NEVER equal Rn, or Rn would be clobbered before
7622            // the ADD reads it):
7623            //   - rd != rn  => use rd itself (rn is untouched, since rd != rn).
7624            //   - rd == rn  => use R12/IP (the reserved encoder scratch). rd/rn are
7625            //                  never R12 (R12 is non-allocatable), so it can't alias.
7626            //
7627            // The materialized value is the same whether or not MOVT is emitted, so
7628            // the byte length depends only on `imm` (and rd==rn) — the size probe and
7629            // the final emit therefore agree (mandatory: the function is encoded twice).
7630            let scratch: u32 = if rd_bits == rn_bits {
7631                12 // R12/IP — in-place add, can't use rd because rd == rn
7632            } else {
7633                rd_bits // rn is preserved because rd != rn
7634            };
7635            // Invariant: the scratch must never alias Rn (would clobber it before
7636            // the ADD reads it). Unreachable in real codegen (rd/rn are never R12,
7637            // which is reserved encoder scratch), but the encoder is also driven by
7638            // the `encoder_no_panic` fuzz harness with ARBITRARY registers — incl.
7639            // rd==rn==R12, which makes scratch (R12) alias Rn. The encoder contract
7640            // (#180/#185) is Ok-or-Err, never a panic, so return a typed error
7641            // instead of asserting. #350 follow-up.
7642            if scratch == rn_bits {
7643                return Err(synth_core::Error::synthesis(format!(
7644                    "ADD #imm: cannot lower #{imm:#x} for Rd==Rn==R12 — no free scratch \
7645                     register (R12 is the reserved encoder scratch and aliases Rn here)"
7646                )));
7647            }
7648
7649            let lo16 = imm & 0xFFFF;
7650            let hi16 = (imm >> 16) & 0xFFFF;
7651
7652            let mut bytes = self.encode_thumb32_movw_raw(scratch, lo16)?;
7653            if hi16 != 0 {
7654                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(scratch, hi16)?);
7655            }
7656            bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rn_bits, scratch)?);
7657            Ok(bytes)
7658        }
7659    }
7660
7661    // === Raw encoding helpers for POPCNT (take register numbers directly) ===
7662
7663    /// Encode Thumb-2 32-bit MOVW (16-bit immediate) - raw version
7664    ///
7665    /// # Contract (Verus-style)
7666    /// ```text
7667    /// requires rd <= 14, imm16 <= 0xFFFF
7668    /// ensures result.len() == 4
7669    /// ```
7670    fn encode_thumb32_movw_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
7671        reg_bits_checked(rd)?;
7672        encoding_contracts::verify_imm16(imm16);
7673        // MOVW Rd, #imm16
7674        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7675        let imm16 = imm16 & 0xFFFF;
7676        let imm4 = (imm16 >> 12) & 0xF;
7677        let i_bit = (imm16 >> 11) & 1;
7678        let imm3 = (imm16 >> 8) & 0x7;
7679        let imm8 = imm16 & 0xFF;
7680
7681        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7682        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
7683
7684        let mut bytes = hw1.to_le_bytes().to_vec();
7685        bytes.extend_from_slice(&hw2.to_le_bytes());
7686        encoding_contracts::verify_thumb32(&bytes);
7687        Ok(bytes)
7688    }
7689
7690    /// Encode Thumb-2 32-bit MOVT (move top 16 bits) - raw version
7691    ///
7692    /// # Contract (Verus-style)
7693    /// ```text
7694    /// requires rd <= 14, imm16 <= 0xFFFF
7695    /// ensures result.len() == 4
7696    /// ```
7697    fn encode_thumb32_movt_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
7698        reg_bits_checked(rd)?;
7699        encoding_contracts::verify_imm16(imm16);
7700        // MOVT Rd, #imm16
7701        // 1111 0 i 10 1 1 0 0 imm4 | 0 imm3 Rd imm8
7702        let imm16 = imm16 & 0xFFFF;
7703        let imm4 = (imm16 >> 12) & 0xF;
7704        let i_bit = (imm16 >> 11) & 1;
7705        let imm3 = (imm16 >> 8) & 0x7;
7706        let imm8 = imm16 & 0xFF;
7707
7708        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
7709        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
7710
7711        let mut bytes = hw1.to_le_bytes().to_vec();
7712        bytes.extend_from_slice(&hw2.to_le_bytes());
7713        encoding_contracts::verify_thumb32(&bytes);
7714        Ok(bytes)
7715    }
7716
7717    /// Encode Thumb-2 32-bit LSR (logical shift right) with immediate - raw version
7718    fn encode_thumb32_lsr_raw(&self, rd: u32, rm: u32, shift: u32) -> Result<Vec<u8>> {
7719        // MOV.W Rd, Rm, LSR #imm
7720        // EA4F 0 imm3 Rd imm2 01 Rm
7721        let imm5 = shift & 0x1F;
7722        let imm2 = imm5 & 0x3;
7723        let imm3 = (imm5 >> 2) & 0x7;
7724
7725        let hw1: u16 = 0xEA4F;
7726        let hw2: u16 = ((imm3 << 12) | (rd << 8) | (imm2 << 6) | (0b01 << 4) | rm) as u16;
7727
7728        let mut bytes = hw1.to_le_bytes().to_vec();
7729        bytes.extend_from_slice(&hw2.to_le_bytes());
7730        Ok(bytes)
7731    }
7732
7733    /// Encode Thumb-2 32-bit AND (register) - raw version
7734    fn encode_thumb32_and_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7735        // AND.W Rd, Rn, Rm
7736        // EA00 Rn | 0 Rd 00 00 Rm
7737        let hw1: u16 = (0xEA00 | rn) as u16;
7738        let hw2: u16 = ((rd << 8) | rm) as u16;
7739
7740        let mut bytes = hw1.to_le_bytes().to_vec();
7741        bytes.extend_from_slice(&hw2.to_le_bytes());
7742        Ok(bytes)
7743    }
7744
7745    /// Encode Thumb-2 32-bit AND with immediate - raw version
7746    fn encode_thumb32_and_imm_raw(&self, rd: u32, rn: u32, imm: u32) -> Result<Vec<u8>> {
7747        // AND.W Rd, Rn, #<modified_immediate>
7748        // For small immediates (0-255), the encoding is simpler
7749        // F0 00 Rn | 0 imm3 Rd imm8
7750        let i_bit = (imm >> 11) & 1;
7751        let imm3 = (imm >> 8) & 0x7;
7752        let imm8 = imm & 0xFF;
7753
7754        let hw1: u16 = (0xF000 | (i_bit << 10) | rn) as u16;
7755        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
7756
7757        let mut bytes = hw1.to_le_bytes().to_vec();
7758        bytes.extend_from_slice(&hw2.to_le_bytes());
7759        Ok(bytes)
7760    }
7761
7762    /// Encode Thumb-2 32-bit SUB (register) - raw version
7763    fn encode_thumb32_sub_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7764        // SUB.W Rd, Rn, Rm
7765        // EBA0 Rn | 0 Rd 00 00 Rm
7766        let hw1: u16 = (0xEBA0 | rn) as u16;
7767        let hw2: u16 = ((rd << 8) | rm) as u16;
7768
7769        let mut bytes = hw1.to_le_bytes().to_vec();
7770        bytes.extend_from_slice(&hw2.to_le_bytes());
7771        Ok(bytes)
7772    }
7773
7774    /// Encode Thumb-2 32-bit ADD (register) - raw version
7775    fn encode_thumb32_add_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7776        // ADD.W Rd, Rn, Rm
7777        // EB00 Rn | 0 Rd 00 00 Rm
7778        let hw1: u16 = (0xEB00 | rn) as u16;
7779        let hw2: u16 = ((rd << 8) | rm) as u16;
7780
7781        let mut bytes = hw1.to_le_bytes().to_vec();
7782        bytes.extend_from_slice(&hw2.to_le_bytes());
7783        Ok(bytes)
7784    }
7785
7786    /// Encode Thumb-2 32-bit ADDS (register, flag-setting) - raw version.
7787    /// Used as the high-register fallback for `ArmOp::Adds` (i64 low-word add)
7788    /// so R8-R11 pair operands don't overflow the 16-bit field — #178/#180.
7789    fn encode_thumb32_adds_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7790        // ADDS.W Rd, Rn, Rm (T3, S=1): EB10 Rn | 0 Rd 00 00 Rm
7791        let hw1: u16 = (0xEB10 | rn) as u16;
7792        let hw2: u16 = ((rd << 8) | rm) as u16;
7793        let mut bytes = hw1.to_le_bytes().to_vec();
7794        bytes.extend_from_slice(&hw2.to_le_bytes());
7795        Ok(bytes)
7796    }
7797
7798    /// Encode Thumb-2 32-bit SUBS (register, flag-setting) - raw version.
7799    /// High-register fallback for `ArmOp::Subs` (i64 low-word subtract) — #178/#180.
7800    fn encode_thumb32_subs_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
7801        // SUBS.W Rd, Rn, Rm (T3, S=1): EBB0 Rn | 0 Rd 00 00 Rm
7802        let hw1: u16 = (0xEBB0 | rn) as u16;
7803        let hw2: u16 = ((rd << 8) | rm) as u16;
7804        let mut bytes = hw1.to_le_bytes().to_vec();
7805        bytes.extend_from_slice(&hw2.to_le_bytes());
7806        Ok(bytes)
7807    }
7808
7809    /// Encode a sequence of ARM instructions
7810    pub fn encode_sequence(&self, ops: &[ArmOp]) -> Result<Vec<u8>> {
7811        let mut code = Vec::new();
7812
7813        for op in ops {
7814            let encoded = self.encode(op)?;
7815            code.extend_from_slice(&encoded);
7816        }
7817
7818        Ok(code)
7819    }
7820}
7821
7822/// Convert register to bit encoding (0-15)
7823/// Reverse of the ARMv7-M `ThumbExpandImm`: given a 32-bit immediate, return the
7824/// 12-bit `i:imm3:imm8` field if it is a representable modified immediate, else
7825/// `None` (the caller must materialize the value into a register). This is the
7826/// shared correct path for the data-processing immediate encoders — without it
7827/// they pack raw bits and silently mis-encode any value `> 0xFF` that isn't a
7828/// modified immediate (the silent-miscompile class behind #251/#253/#255).
7829fn try_thumb_expand_imm(value: u32) -> Option<u32> {
7830    // i:imm3 = 0000 → 8-bit value, zero-extended (00000000 00000000 00000000 XY).
7831    if value <= 0xFF {
7832        return Some(value);
7833    }
7834    let b0 = value & 0xFF; // byte 0
7835    let b1 = (value >> 8) & 0xFF; // byte 1
7836    // 0x00XY00XY (i:imm3 = 0001) — XY in bytes 0 and 2
7837    if value == (b0 << 16) | b0 {
7838        return Some(0x100 | b0);
7839    }
7840    // 0xXY00XY00 (i:imm3 = 0010) — XY in bytes 1 and 3
7841    if value == (b1 << 24) | (b1 << 8) {
7842        return Some(0x200 | b1);
7843    }
7844    // 0xXYXYXYXY (i:imm3 = 0011) — XY in all four bytes
7845    if value == (b0 << 24) | (b0 << 16) | (b0 << 8) | b0 {
7846        return Some(0x300 | b0);
7847    }
7848    // An 8-bit value with bit 7 set, rotated right by 8..=31. `rotate_left(rot)`
7849    // undoes the encoded right rotation; if the result is `1bbbbbbb` (0x80..=0xFF)
7850    // the value is representable. imm12[11:7] = rot, imm12[6:0] = low 7 bits.
7851    for rot in 8..=31u32 {
7852        let unrot = value.rotate_left(rot);
7853        if (0x80..=0xFF).contains(&unrot) {
7854            return Some((rot << 7) | (unrot & 0x7F));
7855        }
7856    }
7857    None
7858}
7859
7860/// Guard a Thumb-2 `LDR/STR Rd, [Rn, #imm12]` offset. The imm12 form supports
7861/// `0..=4095`; a larger offset must be materialized into a register by the
7862/// selector (register-offset addressing). Returning `Err` rather than silently
7863/// masking `offset & 0xFFF` closes the wrong-address miscompile class (#259,
7864/// the load/store sibling of #253/#255).
7865fn check_ldst_imm12(offset: u32) -> Result<()> {
7866    if offset > 0xFFF {
7867        Err(synth_core::Error::synthesis(
7868            "load/store immediate offset > 0xFFF (4095) — materialize the offset into a register",
7869        ))
7870    } else {
7871        Ok(())
7872    }
7873}
7874
7875fn reg_to_bits(reg: &Reg) -> u32 {
7876    match reg {
7877        Reg::R0 => 0,
7878        Reg::R1 => 1,
7879        Reg::R2 => 2,
7880        Reg::R3 => 3,
7881        Reg::R4 => 4,
7882        Reg::R5 => 5,
7883        Reg::R6 => 6,
7884        Reg::R7 => 7,
7885        Reg::R8 => 8,
7886        Reg::R9 => 9,
7887        Reg::R10 => 10,
7888        Reg::R11 => 11,
7889        Reg::R12 => 12,
7890        Reg::SP => 13,
7891        Reg::LR => 14,
7892        Reg::PC => 15,
7893    }
7894}
7895
7896// ======================================================================
7897// #610 — i64 fixed-ABI expansion wrappers.
7898//
7899// The hand-written multi-instruction i64 cores (rotl/rotr and the div/rem
7900// shift-subtract loops) compute in FIXED low registers. Before #610 the
7901// div/rem arms ignored their operand fields outright (hardcoded R0:R1 /
7902// R2:R3 in, result to R0:R1) and the rot arms used R3/R4 scratch that
7903// collided with selector-assigned registers — then restored the saved
7904// scratch OVER the result (`POP {R4}` with rd_lo == R4), so the op
7905// returned the caller's stale register: 0 for every input under qemu.
7906//
7907// These wrappers make each core honor its register parameters:
7908//   1. save R0-R3,
7909//   2. marshal the operand registers into the core's fixed input regs via
7910//      the stack (permutation-safe: every source is read before any fixed
7911//      register is written),
7912//   3. run the fixed-reg core (self-preserving for R4+; R12 is encoder
7913//      scratch and never allocatable, #212),
7914//   4. MOV the result pair from R0:R1 into the selector's rd pair,
7915//   5. restore R0-R3, skipping any register the result now occupies.
7916//
7917// All emitted lengths are register-independent so the optimized path's
7918// byte-size estimator (`estimate_arm_byte_size`, pinned by the
7919// estimator↔encoder agreement oracle #498/#511) stays a constant per op.
7920// ======================================================================
7921
7922/// Steps 1+2: `PUSH {R0-R3}`, then marshal `srcs` (operand registers, any of
7923/// R0-R12) into `R0..R<n>` via individual stack pushes. Sources are all read
7924/// before any destination register is written, so arbitrary source/target
7925/// permutations (including operands living in R0-R3) are safe.
7926fn emit_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
7927    debug_assert!(srcs.len() <= 4);
7928    // PUSH {R0-R3} — save the caller-visible low registers.
7929    bytes.extend_from_slice(&0xB40Fu16.to_le_bytes());
7930    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
7931    for src in srcs.iter().rev() {
7932        let rt = reg_to_bits(src) as u16;
7933        bytes.extend_from_slice(&0xF84Du16.to_le_bytes());
7934        bytes.extend_from_slice(&((rt << 12) | 0x0D04).to_le_bytes());
7935    }
7936    // POP {Ri} — Ri := srcs[i].
7937    for i in 0..srcs.len() as u16 {
7938        bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes());
7939    }
7940}
7941
7942/// Steps 4+5: move the core's R0:R1 result into the selector's rd pair, then
7943/// restore the R0-R3 saved by [`emit_i64_fixed_abi_entry`], skipping any
7944/// register the result now lives in (its saved caller word is discarded).
7945fn emit_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
7946    let lo = reg_to_bits(rdlo);
7947    let hi = reg_to_bits(rdhi);
7948    if lo == 1 && hi == 0 {
7949        // A fully swapped pair would clobber one half in either MOV order.
7950        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
7951        return Err(synth_core::Error::synthesis(
7952            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
7953        ));
7954    }
7955    let mov16 = |bytes: &mut Vec<u8>, rd: u32, rm: u32| {
7956        let d = ((rd >> 3) & 1) as u16;
7957        bytes.extend_from_slice(
7958            &(0x4600u16 | (d << 7) | ((rm as u16) << 3) | ((rd & 7) as u16)).to_le_bytes(),
7959        );
7960    };
7961    if hi == 0 {
7962        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
7963        mov16(bytes, lo, 0);
7964        mov16(bytes, hi, 1);
7965    } else {
7966        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
7967        mov16(bytes, hi, 1);
7968        mov16(bytes, lo, 0);
7969    }
7970    for i in 0..4u32 {
7971        if i == lo || i == hi {
7972            // The result lives here — drop the saved caller word.
7973            bytes.extend_from_slice(&0xB001u16.to_le_bytes()); // ADD SP, #4
7974        } else {
7975            bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes()); // POP {Ri}
7976        }
7977    }
7978    Ok(())
7979}
7980
7981/// WASM `i64.div_*` / `i64.rem_*` by zero must trap, matching the i32 path's
7982/// cmp/bne/udf guard. Emitted after marshaling, when the divisor pair is in
7983/// R2:R3: `ORRS R12, R2, R3` — `BNE` over a `UDF #0` when nonzero.
7984fn emit_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
7985    bytes.extend_from_slice(&0xEA52u16.to_le_bytes()); // ORRS.W R12, R2, R3
7986    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
7987    bytes.extend_from_slice(&0xD100u16.to_le_bytes()); // BNE.N +0 (skip the UDF)
7988    bytes.extend_from_slice(&0xDE00u16.to_le_bytes()); // UDF #0 — divide by zero
7989}
7990
7991/// WASM `i64.div_s(INT64_MIN, -1)` must trap (Core §4.3.2 `idiv_s`: the
7992/// quotient +2^63 is unrepresentable), matching the i32 path's overflow
7993/// guard — #633: without it the core negated INT64_MIN onto itself and
7994/// silently returned INT64_MIN. Emitted after marshaling, when the dividend
7995/// pair is in R0:R1 and the divisor pair in R2:R3; R12 is encoder scratch.
7996///
7997/// div_s ONLY — `i64.rem_s(INT64_MIN, -1)` is defined as 0 and must NOT
7998/// trap (`irem_s`), so the I64RemS arm never calls this. 22 bytes,
7999/// register-independent (estimator contract, #498/#511).
8000fn emit_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8001    // AND.W R12, R2, R3 — R12 == 0xFFFFFFFF iff divisor == -1
8002    bytes.extend_from_slice(&0xEA02u16.to_le_bytes());
8003    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8004    // CMN.W R12, #1 — EQ iff both divisor words are all-ones
8005    bytes.extend_from_slice(&0xF11Cu16.to_le_bytes());
8006    bytes.extend_from_slice(&0x0F01u16.to_le_bytes());
8007    // BNE .no_trap
8008    bytes.extend_from_slice(&0xD105u16.to_le_bytes());
8009    // CMP R0, #0 — dividend lo word of INT64_MIN
8010    bytes.extend_from_slice(&0x2800u16.to_le_bytes());
8011    // BNE .no_trap
8012    bytes.extend_from_slice(&0xD103u16.to_le_bytes());
8013    // CMP.W R1, #0x80000000 — dividend hi word of INT64_MIN
8014    bytes.extend_from_slice(&0xF1B1u16.to_le_bytes());
8015    bytes.extend_from_slice(&0x4F00u16.to_le_bytes());
8016    // BNE .no_trap
8017    bytes.extend_from_slice(&0xD100u16.to_le_bytes());
8018    // UDF #0 — signed-division overflow
8019    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
8020    // .no_trap:
8021}
8022
8023// ======================================================================
8024// #615 — A32 (ARM-mode) twins of the #610 i64 fixed-ABI wrappers above.
8025// Identical register contract, A32 encodings: the multi-instruction i64
8026// cores (rotl/rotr, div/rem) compute in fixed low registers (value/dividend
8027// R0:R1, amount R2 / divisor R2:R3, result to R0:R1); the wrappers marshal
8028// the selector-assigned operand registers in and the result out, saving and
8029// restoring the caller-visible R0-R3 around the core.
8030// ======================================================================
8031
8032/// A32 steps 1+2: `STMDB SP!, {R0-R3}`, then marshal `srcs` into `R0..R<n>`
8033/// via individual stack pushes (`STR src, [SP, #-4]!` in reverse order, then
8034/// `LDR Ri, [SP], #4`). Every source is read before any fixed register is
8035/// written, so arbitrary source/target permutations are safe.
8036fn emit_a32_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8037    debug_assert!(srcs.len() <= 4);
8038    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8039    // PUSH {R0-R3} — save the caller-visible low registers.
8040    w(bytes, 0xE92D_000F);
8041    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8042    for src in srcs.iter().rev() {
8043        w(bytes, 0xE52D_0004 | (reg_to_bits(src) << 12));
8044    }
8045    // LDR Ri, [SP], #4 — Ri := srcs[i].
8046    for i in 0..srcs.len() as u32 {
8047        w(bytes, 0xE49D_0004 | (i << 12));
8048    }
8049}
8050
8051/// A32 steps 4+5: move the core's R0:R1 result into the selector's rd pair,
8052/// then restore the R0-R3 saved by [`emit_a32_i64_fixed_abi_entry`], skipping
8053/// any register the result now lives in (its saved caller word is discarded).
8054fn emit_a32_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8055    let lo = reg_to_bits(rdlo);
8056    let hi = reg_to_bits(rdhi);
8057    if lo == 1 && hi == 0 {
8058        // A fully swapped pair would clobber one half in either MOV order.
8059        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8060        return Err(synth_core::Error::synthesis(
8061            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8062        ));
8063    }
8064    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8065    let mov = |bytes: &mut Vec<u8>, rd: u32, rm: u32| w(bytes, 0xE1A0_0000 | (rd << 12) | rm);
8066    if hi == 0 {
8067        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8068        mov(bytes, lo, 0);
8069        mov(bytes, hi, 1);
8070    } else {
8071        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8072        mov(bytes, hi, 1);
8073        mov(bytes, lo, 0);
8074    }
8075    for i in 0..4u32 {
8076        if i == lo || i == hi {
8077            // The result lives here — drop the saved caller word.
8078            w(bytes, 0xE28D_D004); // ADD SP, SP, #4
8079        } else {
8080            w(bytes, 0xE49D_0004 | (i << 12)); // LDR Ri, [SP], #4
8081        }
8082    }
8083    Ok(())
8084}
8085
8086/// A32 zero-divisor trap, emitted after marshaling when the divisor pair is
8087/// in R2:R3: `ORRS R12, R2, R3` sets Z iff the divisor is zero; `BNE` skips a
8088/// `UDF #0` (WASM div/rem-by-zero must trap, matching the Thumb-2 twin).
8089fn emit_a32_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8090    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8091    w(bytes, 0xE192_C003); // ORRS R12, R2, R3
8092    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8093    w(bytes, 0xE7F0_00F0); // UDF #0 — divide by zero
8094}
8095
8096/// A32 twin of [`emit_i64_divs_overflow_trap`] (#633): trap on
8097/// `i64.div_s(INT64_MIN, -1)`. Conditional execution replaces the Thumb
8098/// branches — the CMPEQ chain leaves EQ set only when divisor == -1 AND
8099/// dividend == INT64_MIN. div_s only; rem_s must keep returning 0.
8100fn emit_a32_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8101    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8102    w(bytes, 0xE002_C003); // AND   R12, R2, R3 (== 0xFFFFFFFF iff divisor == -1)
8103    w(bytes, 0xE37C_0001); // CMN   R12, #1     (EQ iff divisor == -1)
8104    w(bytes, 0x0350_0000); // CMPEQ R0, #0      (EQ iff also dividend lo == 0)
8105    w(bytes, 0x0351_0102); // CMPEQ R1, #0x80000000 (EQ iff dividend == INT64_MIN)
8106    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8107    w(bytes, 0xE7F0_00F0); // UDF #0 — signed-division overflow
8108}
8109
8110/// Fallible form of the `verify_reg_bits` contract. PC (R15) is not a valid
8111/// data operand for the Thumb-2 encodings that use this guard (SDIV/UDIV/MLS/…
8112/// are UNPREDICTABLE with PC). Synth's own codegen never emits PC there, but
8113/// the encoder must stay *total* over arbitrary `ArmOp` inputs — the fuzz
8114/// harness (`encoder_no_panic`) requires Ok-or-Err, never a panic. Pre-fix, the
8115/// `debug_assert` in `verify_reg_bits` aborted under `-Cdebug-assertions`.
8116/// Returns a typed Err instead. See #185.
8117fn reg_bits_checked(bits: u32) -> Result<()> {
8118    if bits > 14 {
8119        return Err(synth_core::Error::synthesis(format!(
8120            "register bits {bits} (PC/R15) is not a valid operand for this Thumb-2 encoding"
8121        )));
8122    }
8123    Ok(())
8124}
8125
8126/// Try to encode a 32-bit value as an ARM rotated immediate (imm8 ROR 2*rot4).
8127/// Returns Some((encoded_bits, 1)) if representable, None otherwise.
8128fn try_encode_rotated_imm(val: u32) -> Option<(u32, u32)> {
8129    if val == 0 {
8130        return Some((0, 1));
8131    }
8132    for rot in 0..16u32 {
8133        let shift = rot * 2;
8134        // Rotate left by shift (undo the ROR) to see if result fits in 8 bits
8135        let unrotated = val.rotate_left(shift);
8136        if unrotated <= 0xFF {
8137            // Encoded as: rot4(4 bits) | imm8(8 bits) = rotate_imm << 8 | imm8
8138            return Some(((rot << 8) | unrotated, 1));
8139        }
8140    }
8141    None
8142}
8143
8144/// Encode operand2 field and return (bits, immediate_flag).
8145/// For ARM32 mode, immediates use the rotated-immediate encoding (imm8 ROR 2*rot4).
8146/// Panics if an immediate value cannot be represented. Callers that need large
8147/// immediates should use MOVW/MOVT instead of Operand2::Imm.
8148fn encode_operand2(op2: &Operand2) -> Result<(u32, u32)> {
8149    match op2 {
8150        Operand2::Imm(val) => {
8151            let uval = *val as u32;
8152            // Attempt rotated-immediate encoding (ARM32 Operand2)
8153            if let Some(encoded) = try_encode_rotated_imm(uval) {
8154                Ok(encoded)
8155            } else {
8156                // #378-class honesty: an immediate that can't be expressed as an
8157                // ARM32 rotated immediate is an INTERNAL selector bug — large
8158                // constants must be materialized via MOVW/MOVT, not passed here.
8159                // FAIL HONESTLY with an Err rather than silently masking to
8160                // `uval & 0xFF` and emitting a WRONG immediate. The encoder is
8161                // Ok-or-Err, never corrupt (#180/#185); a loud Err is also why
8162                // this is an Err and not a panic (the `encoder_no_panic` fuzz
8163                // contract — malformed/oversized input must degrade, not crash).
8164                Err(synth_core::Error::synthesis(format!(
8165                    "encode_operand2: immediate {uval:#x} ({val}) is not an ARM32 \
8166                     rotated immediate — the selector must materialize large \
8167                     constants via MOVW/MOVT"
8168                )))
8169            }
8170        }
8171
8172        Operand2::Reg(reg) => {
8173            let reg_bits = reg_to_bits(reg);
8174            Ok((reg_bits, 0)) // I=0 for register
8175        }
8176
8177        Operand2::RegShift {
8178            rm,
8179            shift: _,
8180            amount,
8181        } => {
8182            // Simplified encoding with shift
8183            let rm_bits = reg_to_bits(rm);
8184            let shift_bits = (*amount & 0x1F) << 7;
8185            Ok((shift_bits | rm_bits, 0))
8186        }
8187    }
8188}
8189
8190/// Encode memory address to (base_reg, offset)
8191fn encode_mem_addr(addr: &MemAddr) -> (u32, u32) {
8192    let base_bits = reg_to_bits(&addr.base);
8193    let offset_bits = (addr.offset as u32) & 0xFFF; // 12-bit offset
8194    (base_bits, offset_bits)
8195}
8196
8197/// S-register number: S0=0, S1=1, ..., S31=31
8198fn vfp_sreg_to_num(reg: &VfpReg) -> Result<u32> {
8199    match reg {
8200        VfpReg::S0 => Ok(0),
8201        VfpReg::S1 => Ok(1),
8202        VfpReg::S2 => Ok(2),
8203        VfpReg::S3 => Ok(3),
8204        VfpReg::S4 => Ok(4),
8205        VfpReg::S5 => Ok(5),
8206        VfpReg::S6 => Ok(6),
8207        VfpReg::S7 => Ok(7),
8208        VfpReg::S8 => Ok(8),
8209        VfpReg::S9 => Ok(9),
8210        VfpReg::S10 => Ok(10),
8211        VfpReg::S11 => Ok(11),
8212        VfpReg::S12 => Ok(12),
8213        VfpReg::S13 => Ok(13),
8214        VfpReg::S14 => Ok(14),
8215        VfpReg::S15 => Ok(15),
8216        VfpReg::S16 => Ok(16),
8217        VfpReg::S17 => Ok(17),
8218        VfpReg::S18 => Ok(18),
8219        VfpReg::S19 => Ok(19),
8220        VfpReg::S20 => Ok(20),
8221        VfpReg::S21 => Ok(21),
8222        VfpReg::S22 => Ok(22),
8223        VfpReg::S23 => Ok(23),
8224        VfpReg::S24 => Ok(24),
8225        VfpReg::S25 => Ok(25),
8226        VfpReg::S26 => Ok(26),
8227        VfpReg::S27 => Ok(27),
8228        VfpReg::S28 => Ok(28),
8229        VfpReg::S29 => Ok(29),
8230        VfpReg::S30 => Ok(30),
8231        VfpReg::S31 => Ok(31),
8232        // D-registers are not used in F32 single-precision encodings
8233        _ => Err(synth_core::Error::SynthesisError(
8234            "D-register not supported in single-precision VFP encoding".to_string(),
8235        )),
8236    }
8237}
8238
8239/// D-register number: D0=0, D1=1, ..., D15=15
8240fn vfp_dreg_to_num(reg: &VfpReg) -> Result<u32> {
8241    match reg {
8242        VfpReg::D0 => Ok(0),
8243        VfpReg::D1 => Ok(1),
8244        VfpReg::D2 => Ok(2),
8245        VfpReg::D3 => Ok(3),
8246        VfpReg::D4 => Ok(4),
8247        VfpReg::D5 => Ok(5),
8248        VfpReg::D6 => Ok(6),
8249        VfpReg::D7 => Ok(7),
8250        VfpReg::D8 => Ok(8),
8251        VfpReg::D9 => Ok(9),
8252        VfpReg::D10 => Ok(10),
8253        VfpReg::D11 => Ok(11),
8254        VfpReg::D12 => Ok(12),
8255        VfpReg::D13 => Ok(13),
8256        VfpReg::D14 => Ok(14),
8257        VfpReg::D15 => Ok(15),
8258        // S-registers are not used in F64 double-precision encodings
8259        _ => Err(synth_core::Error::SynthesisError(
8260            "S-register not supported in double-precision VFP encoding".to_string(),
8261        )),
8262    }
8263}
8264
8265/// Split S-register into (Vx[3:0], qualifier_bit) for VFP encoding.
8266/// For an S-register number s: Vx = s >> 1, qualifier = s & 1.
8267/// The qualifier bit goes to D (bit 22), N (bit 7), or M (bit 5) depending on role.
8268fn encode_sreg(s: u32) -> (u32, u32) {
8269    (s >> 1, s & 1)
8270}
8271
8272/// Split D-register into (Vx[3:0], qualifier_bit) for VFP double-precision encoding.
8273/// For a D-register number d: Vx = d & 0xF, qualifier = (d >> 4) & 1.
8274/// For D0-D15, qualifier is always 0.
8275fn encode_dreg(d: u32) -> (u32, u32) {
8276    (d & 0xF, (d >> 4) & 1)
8277}
8278
8279/// Encode a VFP 3-register arithmetic instruction (VADD.F32, VSUB.F32, VMUL.F32, VDIV.F32).
8280/// Returns the full 32-bit instruction word.
8281///
8282/// VFP encoding: [cond 1110] [D opc1 Vn] [Vd 101 sz] [N opc2 M 0 Vm]
8283/// For single-precision (sz=0), coprocessor = 0xA (bits[11:8]).
8284fn encode_vfp_3reg(base: u32, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<u32> {
8285    let sd_num = vfp_sreg_to_num(sd)?;
8286    let sn_num = vfp_sreg_to_num(sn)?;
8287    let sm_num = vfp_sreg_to_num(sm)?;
8288    let (vd, d) = encode_sreg(sd_num);
8289    let (vn, n) = encode_sreg(sn_num);
8290    let (vm, m) = encode_sreg(sm_num);
8291
8292    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8293}
8294
8295/// Encode a VFP 2-register instruction (VNEG.F32, VABS.F32, VSQRT.F32).
8296/// Returns the full 32-bit instruction word.
8297fn encode_vfp_2reg(base: u32, sd: &VfpReg, sm: &VfpReg) -> Result<u32> {
8298    let sd_num = vfp_sreg_to_num(sd)?;
8299    let sm_num = vfp_sreg_to_num(sm)?;
8300    let (vd, d) = encode_sreg(sd_num);
8301    let (vm, m) = encode_sreg(sm_num);
8302
8303    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8304}
8305
8306/// Encode a VFP load/store (VLDR.F32 / VSTR.F32).
8307/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8308/// U bit (bit 23) controls add/subtract offset.
8309fn encode_vfp_ldst(base: u32, sd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8310    let sd_num = vfp_sreg_to_num(sd)?;
8311    let (vd, d) = encode_sreg(sd_num);
8312    let rn = reg_to_bits(&addr.base);
8313
8314    let offset = addr.offset;
8315    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8316    let abs_offset = offset.unsigned_abs();
8317    let imm8 = (abs_offset / 4) & 0xFF;
8318
8319    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8320}
8321
8322/// Encode VMOV between core register and S-register.
8323/// VMOV Sn, Rt: 0xEE00_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8324/// VMOV Rt, Sn: 0xEE10_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8325fn encode_vmov_core_sreg(to_sreg: bool, sreg: &VfpReg, core: &Reg) -> Result<u32> {
8326    let s_num = vfp_sreg_to_num(sreg)?;
8327    let (vn, n) = encode_sreg(s_num);
8328    let rt = reg_to_bits(core);
8329
8330    let base = if to_sreg { 0xEE000A10 } else { 0xEE100A10 };
8331    Ok(base | (vn << 16) | (rt << 12) | (n << 7))
8332}
8333
8334/// Encode a VFP 3-register double-precision instruction (VADD.F64, VSUB.F64, etc.).
8335/// For double-precision (sz=1), coprocessor = 0xB (bits[11:8]).
8336/// The base should have bit 8 = 1 for F64 (0xB suffix instead of 0xA).
8337fn encode_vfp_3reg_f64(base: u32, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<u32> {
8338    let dd_num = vfp_dreg_to_num(dd)?;
8339    let dn_num = vfp_dreg_to_num(dn)?;
8340    let dm_num = vfp_dreg_to_num(dm)?;
8341    let (vd, d) = encode_dreg(dd_num);
8342    let (vn, n) = encode_dreg(dn_num);
8343    let (vm, m) = encode_dreg(dm_num);
8344
8345    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8346}
8347
8348/// Encode a VFP 2-register double-precision instruction (VNEG.F64, VABS.F64, VSQRT.F64).
8349fn encode_vfp_2reg_f64(base: u32, dd: &VfpReg, dm: &VfpReg) -> Result<u32> {
8350    let dd_num = vfp_dreg_to_num(dd)?;
8351    let dm_num = vfp_dreg_to_num(dm)?;
8352    let (vd, d) = encode_dreg(dd_num);
8353    let (vm, m) = encode_dreg(dm_num);
8354
8355    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8356}
8357
8358/// Encode a VFP load/store for double-precision (VLDR.64 / VSTR.64).
8359/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8360fn encode_vfp_ldst_f64(base: u32, dd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8361    let dd_num = vfp_dreg_to_num(dd)?;
8362    let (vd, d) = encode_dreg(dd_num);
8363    let rn = reg_to_bits(&addr.base);
8364
8365    let offset = addr.offset;
8366    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8367    let abs_offset = offset.unsigned_abs();
8368    let imm8 = (abs_offset / 4) & 0xFF;
8369
8370    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8371}
8372
8373/// Encode VMOV between two core registers and a D-register.
8374/// VMOV Dm, Rt, Rt2: 0xEC40_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8375/// VMOV Rt, Rt2, Dm: 0xEC50_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8376fn encode_vmov_core_dreg(
8377    to_dreg: bool,
8378    dreg: &VfpReg,
8379    core_lo: &Reg,
8380    core_hi: &Reg,
8381) -> Result<u32> {
8382    let d_num = vfp_dreg_to_num(dreg)?;
8383    let (vm, m) = encode_dreg(d_num);
8384    let rt = reg_to_bits(core_lo);
8385    let rt2 = reg_to_bits(core_hi);
8386
8387    let base = if to_dreg { 0xEC400B10 } else { 0xEC500B10 };
8388    Ok(base | (rt2 << 16) | (rt << 12) | (m << 5) | vm)
8389}
8390
8391/// Emit a VFP 32-bit instruction as Thumb-2 bytes (two LE halfwords).
8392fn vfp_to_thumb_bytes(instr: u32) -> Vec<u8> {
8393    let hw1 = ((instr >> 16) & 0xFFFF) as u16;
8394    let hw2 = (instr & 0xFFFF) as u16;
8395    let mut bytes = hw1.to_le_bytes().to_vec();
8396    bytes.extend_from_slice(&hw2.to_le_bytes());
8397    bytes
8398}
8399
8400// ============================================================================
8401// Helium MVE encoding helpers
8402// ============================================================================
8403
8404/// Q-register number: Q0=0, Q1=1, ..., Q7=7
8405fn qreg_to_num(reg: &QReg) -> u32 {
8406    match reg {
8407        QReg::Q0 => 0,
8408        QReg::Q1 => 1,
8409        QReg::Q2 => 2,
8410        QReg::Q3 => 3,
8411        QReg::Q4 => 4,
8412        QReg::Q5 => 5,
8413        QReg::Q6 => 6,
8414        QReg::Q7 => 7,
8415    }
8416}
8417
8418/// MVE element size to encoding bits: S8=0b00, S16=0b01, S32=0b10
8419fn mve_size_bits(size: &MveSize) -> u32 {
8420    match size {
8421        MveSize::S8 => 0b00,
8422        MveSize::S16 => 0b01,
8423        MveSize::S32 => 0b10,
8424    }
8425}
8426
8427/// Encode MVE 3-register instruction.
8428/// Q-registers are encoded as D-register pairs: Q0=D0:D1, Q1=D2:D3, etc.
8429/// In NEON/MVE encoding, the Q-register uses D-register number = Qn * 2.
8430fn encode_mve_3reg(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
8431    let d = qreg_to_num(qd) * 2;
8432    let n = qreg_to_num(qn) * 2;
8433    let m = qreg_to_num(qm) * 2;
8434
8435    // Standard NEON/MVE 3-register encoding:
8436    // D bit (bit 22) = Vd[4], Vd[3:0] = bits [15:12]
8437    // N bit (bit 7)  = Vn[4], Vn[3:0] = bits [19:16]
8438    // M bit (bit 5)  = Vm[4], Vm[3:0] = bits [3:0]
8439    let vd = d & 0xF;
8440    let d_bit = (d >> 4) & 1;
8441    let vn = n & 0xF;
8442    let n_bit = (n >> 4) & 1;
8443    let vm = m & 0xF;
8444    let m_bit = (m >> 4) & 1;
8445
8446    base | (d_bit << 22) | (vn << 16) | (vd << 12) | (n_bit << 7) | (m_bit << 5) | vm
8447}
8448
8449/// Encode MVE 3-register bitwise instruction (VAND, VORR, VEOR, VBIC).
8450fn encode_mve_3reg_bitwise(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
8451    encode_mve_3reg(base, qd, qn, qm)
8452}
8453
8454/// Encode MVE VLDRW.32 Qd, [Rn, #offset]
8455/// Format: EC9x xxxx - contiguous load, word-sized elements
8456fn encode_mve_vldrw(qd: &QReg, addr: &MemAddr) -> u32 {
8457    let qd_enc = qreg_to_num(qd) * 2;
8458    let rn = reg_to_bits(&addr.base);
8459    let offset = addr.offset;
8460    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8461    let abs_offset = offset.unsigned_abs();
8462    let imm7 = (abs_offset / 4) & 0x7F; // 7-bit word-aligned offset
8463
8464    // VLDRW.32 Qd, [Rn, #imm]: ED10 xx80 variant
8465    0xED100E80
8466        | (u_bit << 23)
8467        | ((qd_enc >> 4) << 22)
8468        | (rn << 16)
8469        | ((qd_enc & 0xF) << 12)
8470        | (imm7 & 0x7F)
8471}
8472
8473/// Encode MVE VSTRW.32 Qd, [Rn, #offset]
8474fn encode_mve_vstrw(qd: &QReg, addr: &MemAddr) -> u32 {
8475    let qd_enc = qreg_to_num(qd) * 2;
8476    let rn = reg_to_bits(&addr.base);
8477    let offset = addr.offset;
8478    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8479    let abs_offset = offset.unsigned_abs();
8480    let imm7 = (abs_offset / 4) & 0x7F;
8481
8482    0xED000E80
8483        | (u_bit << 23)
8484        | ((qd_enc >> 4) << 22)
8485        | (rn << 16)
8486        | ((qd_enc & 0xF) << 12)
8487        | (imm7 & 0x7F)
8488}
8489
8490impl ArmEncoder {
8491    /// Encode MVE constant load: MOVW+MOVT+VMOV for each 32-bit word, then assemble Q-register
8492    fn encode_thumb_mve_const(&self, qd: &QReg, bytes: &[u8; 16]) -> Result<Vec<u8>> {
8493        let mut result = Vec::new();
8494        let qd_num = qreg_to_num(qd);
8495
8496        // Load each 32-bit word into R12 (temp) then VMOV into S-register
8497        for i in 0..4 {
8498            let word = u32::from_le_bytes([
8499                bytes[i * 4],
8500                bytes[i * 4 + 1],
8501                bytes[i * 4 + 2],
8502                bytes[i * 4 + 3],
8503            ]);
8504            let lo16 = word & 0xFFFF;
8505            let hi16 = (word >> 16) & 0xFFFF;
8506
8507            // MOVW R12, #lo16
8508            result.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
8509            // MOVT R12, #hi16
8510            if hi16 != 0 {
8511                result.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
8512            }
8513
8514            // VMOV Sn, R12 where Sn = Qd*4 + i
8515            let s_num = qd_num * 4 + i as u32;
8516            let (vn, n) = encode_sreg(s_num);
8517            let vmov: u32 = 0xEE000A10 | (vn << 16) | (12 << 12) | (n << 7);
8518            result.extend_from_slice(&vfp_to_thumb_bytes(vmov));
8519        }
8520
8521        Ok(result)
8522    }
8523
8524    /// Encode lane-wise f32 binary operation (VDIV, etc.) via S-register extraction
8525    fn encode_thumb_mve_lane_wise_f32_binop(
8526        &self,
8527        qd: &QReg,
8528        qn: &QReg,
8529        qm: &QReg,
8530        vfp_base: u32,
8531    ) -> Result<Vec<u8>> {
8532        let mut result = Vec::new();
8533        let qd_num = qreg_to_num(qd);
8534        let qn_num = qreg_to_num(qn);
8535        let qm_num = qreg_to_num(qm);
8536
8537        // For each lane 0..3: use S-registers directly (Q aliasing)
8538        for i in 0..4u32 {
8539            let sd = qd_num * 4 + i;
8540            let sn = qn_num * 4 + i;
8541            let sm = qm_num * 4 + i;
8542
8543            let (vd, d) = encode_sreg(sd);
8544            let (vn, n) = encode_sreg(sn);
8545            let (vm, m) = encode_sreg(sm);
8546
8547            let instr = vfp_base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
8548            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
8549        }
8550
8551        Ok(result)
8552    }
8553
8554    /// Encode lane-wise f32 VSQRT via S-register extraction
8555    fn encode_thumb_mve_lane_wise_f32_sqrt(&self, qd: &QReg, qm: &QReg) -> Result<Vec<u8>> {
8556        let mut result = Vec::new();
8557        let qd_num = qreg_to_num(qd);
8558        let qm_num = qreg_to_num(qm);
8559
8560        // VSQRT.F32 base: 0xEEB10AC0
8561        for i in 0..4u32 {
8562            let sd = qd_num * 4 + i;
8563            let sm = qm_num * 4 + i;
8564
8565            let (vd, d) = encode_sreg(sd);
8566            let (vm, m) = encode_sreg(sm);
8567
8568            let instr: u32 = 0xEEB10AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
8569            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
8570        }
8571
8572        Ok(result)
8573    }
8574}
8575
8576#[cfg(test)]
8577mod tests {
8578    use super::*;
8579
8580    #[test]
8581    fn test_encoder_creation() {
8582        let encoder_arm = ArmEncoder::new_arm32();
8583        assert!(!encoder_arm.thumb_mode);
8584
8585        let encoder_thumb = ArmEncoder::new_thumb2();
8586        assert!(encoder_thumb.thumb_mode);
8587    }
8588
8589    /// #204 WAKE-path regression: `SetCond` materialized 0/1 with the 16-bit
8590    /// `MOVS Rd,#imm` (T1), whose Rd field is 3 bits (R0–R7). For a high Rd
8591    /// (R8–R12) `rd_bits << 8` overflows bit 11, flipping the opcode MOVS→CMP
8592    /// (`0x2c00`), so the boolean was never written — gale's `has_waiter` kept a
8593    /// stale value and the binary-sem WAKE dispatch read garbage. High Rd must
8594    /// use the 32-bit `MOV.W` (T2). Verify the bytes, not the IR.
8595    /// #311: the SAME high-Rd MOVS→CMP transmutation as #204, but in the
8596    /// i64 comparison expansions (I64SetCond / I64SetCondZ) — missed by the
8597    /// #204 hardening. With rd=R8 the boolean died in the flags
8598    /// (`ite eq; cmpeq r0,#1; cmpne r0,#0`), so gale's packed-u64 select
8599    /// read a stale register on silicon. High Rd must take MOV.W / CMP.W.
8600    #[test]
8601    fn test_encode_i64setcond_high_reg_uses_mov_w_311() {
8602        use synth_synthesis::{ArmOp, Condition, Reg};
8603        let enc = ArmEncoder::new_thumb2();
8604        let bytes = enc
8605            .encode(&ArmOp::I64SetCond {
8606                rd: Reg::R8,
8607                rn_lo: Reg::R2,
8608                rn_hi: Reg::R3,
8609                rm_lo: Reg::R6,
8610                rm_hi: Reg::R7,
8611                cond: Condition::EQ,
8612            })
8613            .unwrap();
8614        // The 32-bit MOV.W immediate (T2) first halfword is 0xF04F; the
8615        // 16-bit transmuted forms would contain 0x2801/0x2800 (CMP r0,#1/#0).
8616        let halfwords: Vec<u16> = bytes
8617            .chunks(2)
8618            .map(|c| u16::from_le_bytes([c[0], c[1]]))
8619            .collect();
8620        assert!(
8621            halfwords.iter().filter(|&&h| h == 0xF04F).count() == 2,
8622            "high rd must use two MOV.W (T2) encodings, got {halfwords:04x?}"
8623        );
8624        assert!(
8625            !halfwords.contains(&0x2801) && !halfwords.contains(&0x2800),
8626            "no transmuted 16-bit CMP imm: {halfwords:04x?}"
8627        );
8628
8629        let bytes_z = enc
8630            .encode(&ArmOp::I64SetCondZ {
8631                rd: Reg::R8,
8632                rn_lo: Reg::R2,
8633                rn_hi: Reg::R3,
8634            })
8635            .unwrap();
8636        let hw_z: Vec<u16> = bytes_z
8637            .chunks(2)
8638            .map(|c| u16::from_le_bytes([c[0], c[1]]))
8639            .collect();
8640        assert!(
8641            hw_z.iter().filter(|&&h| h == 0xF04F).count() == 2,
8642            "SetCondZ high rd MOV.W: {hw_z:04x?}"
8643        );
8644        // CMP.W rd,#0 (T2) first halfword: 0xF1B0 | rd
8645        assert!(
8646            hw_z.contains(&(0xF1B0 | 8)),
8647            "SetCondZ high rd must use CMP.W: {hw_z:04x?}"
8648        );
8649    }
8650
8651    #[test]
8652    fn test_encode_setcond_high_reg_uses_mov_w_204() {
8653        use synth_synthesis::{ArmOp, Condition, Reg};
8654        let enc = ArmEncoder::new_thumb2();
8655        // R12 (high): must be ITE + MOV.W #1 + MOV.W #0, never a 16-bit MOVS/CMP.
8656        let hi = enc
8657            .encode(&ArmOp::SetCond {
8658                rd: Reg::R12,
8659                cond: Condition::NE,
8660            })
8661            .unwrap();
8662        assert_eq!(hi.len(), 10, "ITE(2) + MOV.W(4) + MOV.W(4): {hi:02x?}");
8663        // both value halfwords are MOV.W (0xF04F) — NOT the corrupt CMP (0x2c..).
8664        assert_eq!(&hi[2..4], &[0x4F, 0xF0], "then = MOV.W: {hi:02x?}");
8665        assert_eq!(&hi[6..8], &[0x4F, 0xF0], "else = MOV.W: {hi:02x?}");
8666        assert_eq!(hi[4] & 0x0F, 0x01, "then imm = #1");
8667        assert_eq!(hi[8] & 0x0F, 0x00, "else imm = #0");
8668        // Low Rd keeps the compact 16-bit MOVS form.
8669        let lo = enc
8670            .encode(&ArmOp::SetCond {
8671                rd: Reg::R0,
8672                cond: Condition::NE,
8673            })
8674            .unwrap();
8675        assert_eq!(lo.len(), 6, "ITE(2) + MOVS(2) + MOVS(2): {lo:02x?}");
8676        assert_eq!(lo[2..4], [0x01, 0x20], "then = MOVS R0,#1");
8677        assert_eq!(lo[4..6], [0x00, 0x20], "else = MOVS R0,#0");
8678    }
8679
8680    /// #209 Opt 1b: UMULL RdLo, RdHi, Rn, Rm encodes correctly on both ISAs.
8681    /// Thumb-2 T1: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm.
8682    /// A32:        cond 0000 1000 RdHi RdLo Rm 1001 Rn.
8683    #[test]
8684    fn test_encode_umull_209b() {
8685        use synth_synthesis::{ArmOp, Reg};
8686        let op = ArmOp::Umull {
8687            rdlo: Reg::R4,
8688            rdhi: Reg::R5,
8689            rn: Reg::R0,
8690            rm: Reg::R3,
8691        };
8692        // Thumb-2: hw1 = 0xFBA0 | 0 = 0xFBA0; hw2 = (4<<12)|(5<<8)|3 = 0x4503.
8693        let t = ArmEncoder::new_thumb2().encode(&op).unwrap();
8694        assert_eq!(
8695            t,
8696            vec![0xA0, 0xFB, 0x03, 0x45],
8697            "umull r4,r5,r0,r3 (T2): {t:02x?}"
8698        );
8699        // A32: 0xE0800090 | (5<<16) | (4<<12) | (3<<8) | 0 = 0xE0854390.
8700        let a = ArmEncoder::new_arm32().encode(&op).unwrap();
8701        assert_eq!(
8702            a,
8703            0xE085_4390u32.to_le_bytes().to_vec(),
8704            "umull (A32): {a:02x?}"
8705        );
8706    }
8707
8708    /// #206 regression: the ARM32 (A32) `Ldr`/`Str` encoders fed `addr` through
8709    /// `encode_mem_addr`, which returns only the 12-bit immediate — so a register
8710    /// offset (`[rn, rm, #off]`) was silently dropped to `[rn, #off]`, sending
8711    /// the access to the wrong runtime address (silent miscompile on the default
8712    /// `--target arm`). A register offset must materialize `ip = rn + rm` and
8713    /// load from `[ip, #off]`. Verify the bytes.
8714    #[test]
8715    fn test_encode_arm32_indexed_load_keeps_index_206() {
8716        use synth_synthesis::{ArmOp, MemAddr, Reg};
8717        let enc = ArmEncoder::new_arm32();
8718        // ldr r0, [r11, r1, #8]  must NOT collapse to a single immediate ldr.
8719        let bytes = enc
8720            .encode(&ArmOp::Ldr {
8721                rd: Reg::R0,
8722                addr: MemAddr::reg_imm(Reg::R11, Reg::R1, 8),
8723            })
8724            .unwrap();
8725        assert_eq!(
8726            bytes.len(),
8727            8,
8728            "expected ADD ip + LDR (2 words): {bytes:02x?}"
8729        );
8730        let add = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
8731        let ldr = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
8732        // ADD ip, r11, r1  = 0xE08BC001
8733        assert_eq!(add, 0xE08B_C001, "ADD ip,r11,r1: {add:#010x}");
8734        // LDR r0, [ip, #8] = 0xE59C0008
8735        assert_eq!(ldr, 0xE59C_0008, "LDR r0,[ip,#8]: {ldr:#010x}");
8736        // A bare immediate ldr (the bug) would be 0xE59B0008 (base=r11) — reject.
8737        assert_ne!(ldr, 0xE59B_0008, "index must not be dropped");
8738    }
8739
8740    /// #594 regression: `call_indirect` on the A32 path (`--target cortex-r5`)
8741    /// was encoded as a literal NOP (0xE1A00000) — the call never happened and
8742    /// the function silently returned the leftover table-index value. The A32
8743    /// encoder must emit the same three-instruction expansion as Thumb-2:
8744    /// `MOV r12, idx, LSL #2; LDR r12, [r11, r12]; BLX r12`.
8745    #[test]
8746    fn test_encode_arm32_call_indirect_is_real_call_594() {
8747        use synth_synthesis::{ArmOp, Reg};
8748        let enc = ArmEncoder::new_arm32();
8749        let bytes = enc
8750            .encode(&ArmOp::CallIndirect {
8751                rd: Reg::R0,
8752                type_idx: 0,
8753                table_index_reg: Reg::R0,
8754            })
8755            .unwrap();
8756        assert_eq!(
8757            bytes.len(),
8758            12,
8759            "expected MOV + LDR + BLX (3 words): {bytes:02x?}"
8760        );
8761        let mov = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
8762        let ldr = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
8763        let blx = u32::from_le_bytes(bytes[8..12].try_into().unwrap());
8764        // MOV r12, r0, LSL #2 = 0xE1A0C100
8765        assert_eq!(mov, 0xE1A0_C100, "MOV r12,r0,LSL#2: {mov:#010x}");
8766        // LDR r12, [r11, r12] = 0xE79BC00C
8767        assert_eq!(ldr, 0xE79B_C00C, "LDR r12,[r11,r12]: {ldr:#010x}");
8768        // BLX r12 = 0xE12FFF3C
8769        assert_eq!(blx, 0xE12F_FF3C, "BLX r12: {blx:#010x}");
8770        // The bug: a single NOP word. Must never come back.
8771        assert!(
8772            !bytes
8773                .chunks_exact(4)
8774                .any(|w| w == 0xE1A0_0000u32.to_le_bytes()),
8775            "call_indirect must not contain a NOP (#594): {bytes:02x?}"
8776        );
8777
8778        // A non-R0 index register lands in the MOV's Rm field.
8779        let bytes = enc
8780            .encode(&ArmOp::CallIndirect {
8781                rd: Reg::R0,
8782                type_idx: 0,
8783                table_index_reg: Reg::R4,
8784            })
8785            .unwrap();
8786        let mov = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
8787        assert_eq!(mov, 0xE1A0_C104, "MOV r12,r4,LSL#2: {mov:#010x}");
8788    }
8789
8790    /// #597 anchor (justified correctness RE-PIN of the #594-era freeze): the
8791    /// Thumb-2 `CallIndirect` expansion is `mov.w ip, rm, LSL #2; ldr.w ip,
8792    /// [r11, ip]; blx ip`.
8793    ///
8794    /// The #594 PR froze the then-current bytes `4F EA 20 0C ...` whose first
8795    /// word decodes as `mov.w ip, rm, ASR #32` — the intended `LSL #2` had
8796    /// its shift amount in the TYPE field (bits 5:4) instead of imm2 (bits
8797    /// 7:6), so the index was destroyed and every call_indirect dispatched
8798    /// table entry 0 (shipped miscompile, masked by index-0 probes). #597
8799    /// corrects the encoding; new bytes `4F EA 80 0C ...` were
8800    /// execution-validated under unicorn against the wasmtime oracle on a
8801    /// multi-entry table (indexes 0, 1, 3 —
8802    /// scripts/repro/call_indirect_597_differential.py) before this pin was
8803    /// replaced. Old pin: [4F EA 20 0C, 5B F8 0C C0, E0 47] (ASR #32 — must
8804    /// never come back).
8805    #[test]
8806    fn test_encode_thumb_call_indirect_lsl2_597() {
8807        use synth_synthesis::{ArmOp, Reg};
8808        let enc = ArmEncoder::new_thumb2();
8809        let bytes = enc
8810            .encode(&ArmOp::CallIndirect {
8811                rd: Reg::R0,
8812                type_idx: 0,
8813                table_index_reg: Reg::R0,
8814            })
8815            .unwrap();
8816        assert_eq!(
8817            bytes,
8818            vec![0x4F, 0xEA, 0x80, 0x0C, 0x5B, 0xF8, 0x0C, 0xC0, 0xE0, 0x47],
8819            "Thumb-2 CallIndirect: mov.w ip,r0,LSL#2; ldr.w ip,[r11,ip]; blx ip: {bytes:02x?}"
8820        );
8821        // The #597 bug bytes (ASR #32 first word) must never come back.
8822        assert_ne!(
8823            &bytes[0..4],
8824            &[0x4F, 0xEA, 0x20, 0x0C],
8825            "mov.w ip, rm, ASR #32 — the #597 type-field bug"
8826        );
8827
8828        // A non-R0 index register lands in the mov.w's Rm field (hw2 bits 3:0).
8829        let bytes = enc
8830            .encode(&ArmOp::CallIndirect {
8831                rd: Reg::R0,
8832                type_idx: 0,
8833                table_index_reg: Reg::R4,
8834            })
8835            .unwrap();
8836        assert_eq!(
8837            &bytes[0..4],
8838            &[0x4F, 0xEA, 0x84, 0x0C],
8839            "mov.w ip, r4, LSL #2: {bytes:02x?}"
8840        );
8841    }
8842
8843    /// #178/#180 regression: the Thumb `Add`/`Adds`/`Subs` reg-forms used the
8844    /// 16-bit encoding unconditionally. For high registers (R12 base scratch,
8845    /// R8-R11 i64 pairs) the 3-bit register fields overflow and corrupt the
8846    /// operands — `add ip,ip,r0` came out as `adds r4,r5,r1` (0x186C), silently
8847    /// dropping the address operand and miscompiling every optimized memory
8848    /// access. High registers must use the 32-bit `.W` forms.
8849    #[test]
8850    fn test_encode_thumb_add_high_reg_uses_add_w_178_180() {
8851        let encoder = ArmEncoder::new_thumb2();
8852
8853        // add ip, ip, r0  — the exact MemLoad/MemStore base+addr op.
8854        let code = encoder
8855            .encode(&ArmOp::Add {
8856                rd: Reg::R12,
8857                rn: Reg::R12,
8858                op2: Operand2::Reg(Reg::R0),
8859            })
8860            .unwrap();
8861        // ADD.W ip, ip, r0 = EB0C 0C00 (little-endian halfwords).
8862        assert_eq!(
8863            code,
8864            vec![0x0C, 0xEB, 0x00, 0x0C],
8865            "high-reg Thumb ADD must be 32-bit ADD.W (EB0C 0C00), not corrupt 16-bit; got {code:02X?}"
8866        );
8867        // Must NOT be the buggy 16-bit 0x186C (`adds r4,r5,r1`).
8868        assert_ne!(code, vec![0x6C, 0x18], "regressed to corrupt 16-bit ADDS");
8869
8870        // Low-register add stays 16-bit (no regression for the common case).
8871        let lo = encoder
8872            .encode(&ArmOp::Add {
8873                rd: Reg::R1,
8874                rn: Reg::R2,
8875                op2: Operand2::Reg(Reg::R3),
8876            })
8877            .unwrap();
8878        assert_eq!(
8879            lo.len(),
8880            2,
8881            "low-reg ADD should remain 16-bit, got {lo:02X?}"
8882        );
8883    }
8884
8885    /// #178/#180 sibling: i64 low-word `Adds`/`Subs` can land in R8-R11 pairs;
8886    /// those must fall back to 32-bit ADDS.W/SUBS.W (flag-setting preserved).
8887    #[test]
8888    fn test_encode_thumb_adds_subs_high_reg_use_32bit_178_180() {
8889        let encoder = ArmEncoder::new_thumb2();
8890
8891        // adds r10, r10, r8  → ADDS.W = EB1A 0A08
8892        let adds = encoder
8893            .encode(&ArmOp::Adds {
8894                rd: Reg::R10,
8895                rn: Reg::R10,
8896                op2: Operand2::Reg(Reg::R8),
8897            })
8898            .unwrap();
8899        assert_eq!(
8900            adds,
8901            vec![0x1A, 0xEB, 0x08, 0x0A],
8902            "high-reg ADDS must be 32-bit ADDS.W (EB1A 0A08); got {adds:02X?}"
8903        );
8904
8905        // subs r10, r10, r8  → SUBS.W = EBBA 0A08
8906        let subs = encoder
8907            .encode(&ArmOp::Subs {
8908                rd: Reg::R10,
8909                rn: Reg::R10,
8910                op2: Operand2::Reg(Reg::R8),
8911            })
8912            .unwrap();
8913        assert_eq!(
8914            subs,
8915            vec![0xBA, 0xEB, 0x08, 0x0A],
8916            "high-reg SUBS must be 32-bit SUBS.W (EBBA 0A08); got {subs:02X?}"
8917        );
8918    }
8919
8920    /// #184 (sibling of #180): 16-bit CMN (T1) only encodes R0-R7. High registers
8921    /// must use 32-bit CMN.W, not the corrupt truncated 16-bit form.
8922    #[test]
8923    fn test_encode_thumb_cmn_high_reg_uses_cmn_w_184() {
8924        let encoder = ArmEncoder::new_thumb2();
8925
8926        // cmn r10, r8  → CMN.W = EB1A 0F08 (ADD.W S=1, Rd=PC discarded).
8927        let cmn = encoder
8928            .encode(&ArmOp::Cmn {
8929                rn: Reg::R10,
8930                op2: Operand2::Reg(Reg::R8),
8931            })
8932            .unwrap();
8933        assert_eq!(
8934            cmn,
8935            vec![0x1A, 0xEB, 0x08, 0x0F],
8936            "high-reg CMN must be 32-bit CMN.W (EB1A 0F08); got {cmn:02X?}"
8937        );
8938
8939        // Low registers stay 16-bit: cmn r1, r2 = 0x42D1.
8940        let lo = encoder
8941            .encode(&ArmOp::Cmn {
8942                rn: Reg::R1,
8943                op2: Operand2::Reg(Reg::R2),
8944            })
8945            .unwrap();
8946        assert_eq!(
8947            lo.len(),
8948            2,
8949            "low-reg CMN should remain 16-bit, got {lo:02X?}"
8950        );
8951        assert_eq!(lo, vec![0xD1, 0x42], "low-reg CMN bytes wrong: {lo:02X?}");
8952    }
8953
8954    /// #185 regression: feeding PC (R15) as a data operand to a Thumb-2 op that
8955    /// guards its registers must return Err, not panic under debug-assertions.
8956    /// (Synth never emits PC here; the fuzz harness requires encode() be total.)
8957    #[test]
8958    fn test_encode_pc_operand_returns_err_not_panic_185() {
8959        let encoder = ArmEncoder::new_thumb2();
8960        for op in [
8961            ArmOp::Sdiv {
8962                rd: Reg::PC,
8963                rn: Reg::R0,
8964                rm: Reg::R1,
8965            },
8966            ArmOp::Udiv {
8967                rd: Reg::R0,
8968                rn: Reg::PC,
8969                rm: Reg::R1,
8970            },
8971            ArmOp::Sdiv {
8972                rd: Reg::R0,
8973                rn: Reg::R1,
8974                rm: Reg::PC,
8975            },
8976        ] {
8977            let r = encoder.encode(&op);
8978            assert!(
8979                r.is_err(),
8980                "encode({op:?}) must return Err for a PC operand, got {r:?}"
8981            );
8982        }
8983        // Valid registers still encode fine (no false rejection).
8984        assert!(
8985            encoder
8986                .encode(&ArmOp::Sdiv {
8987                    rd: Reg::R0,
8988                    rn: Reg::R1,
8989                    rm: Reg::R2
8990                })
8991                .is_ok()
8992        );
8993    }
8994
8995    #[test]
8996    fn test_encode_nop_arm32() {
8997        let encoder = ArmEncoder::new_arm32();
8998        let code = encoder.encode(&ArmOp::Nop).unwrap();
8999
9000        assert_eq!(code.len(), 4); // ARM32 instructions are 4 bytes
9001        assert_eq!(code, vec![0x00, 0x00, 0xA0, 0xE1]); // MOV R0, R0
9002    }
9003
9004    #[test]
9005    fn test_encode_nop_thumb() {
9006        let encoder = ArmEncoder::new_thumb2();
9007        let code = encoder.encode(&ArmOp::Nop).unwrap();
9008
9009        assert_eq!(code.len(), 2); // Thumb instructions are 2 bytes
9010        assert_eq!(code, vec![0x00, 0xBF]); // NOP
9011    }
9012
9013    #[test]
9014    fn test_encode_mov_immediate_arm32() {
9015        let encoder = ArmEncoder::new_arm32();
9016        let op = ArmOp::Mov {
9017            rd: Reg::R0,
9018            op2: Operand2::Imm(42),
9019        };
9020
9021        let code = encoder.encode(&op).unwrap();
9022        assert_eq!(code.len(), 4);
9023
9024        // Verify it's a MOV instruction (bits should have immediate flag set)
9025        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9026        assert_eq!(instr & 0x0E000000, 0x02000000); // Check I bit is set
9027    }
9028
9029    #[test]
9030    fn test_encode_add_registers_arm32() {
9031        let encoder = ArmEncoder::new_arm32();
9032        let op = ArmOp::Add {
9033            rd: Reg::R0,
9034            rn: Reg::R1,
9035            op2: Operand2::Reg(Reg::R2),
9036        };
9037
9038        let code = encoder.encode(&op).unwrap();
9039        assert_eq!(code.len(), 4);
9040
9041        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9042        // Verify it's an ADD instruction with correct opcode
9043        assert_eq!(instr & 0x0FE00000, 0x00800000);
9044    }
9045
9046    /// #350 — `encode_thumb32_add_imm` must lower an out-of-range immediate
9047    /// (> 0xFFF) to a legal MOVW(/MOVT) + ADD.W-register sequence instead of
9048    /// erroring. The small-imm fast path (imm <= 0xFFF) stays byte-identical.
9049    #[test]
9050    fn test_encode_add_imm_large_350() {
9051        let enc = ArmEncoder::new_thumb2();
9052
9053        // --- Fast path unchanged: imm <= 0xFFF is a single 4-byte ADD.W ---
9054        let small = enc
9055            .encode_thumb32_add_imm(&Reg::R0, &Reg::R1, 0x123)
9056            .unwrap();
9057        assert_eq!(small.len(), 4, "small imm must stay a single instruction");
9058
9059        // helper: decode a Thumb-2 MOVW/MOVT halfword pair back to its imm16
9060        fn movx_imm16(b: &[u8]) -> u32 {
9061            let hw1 = u16::from_le_bytes([b[0], b[1]]) as u32;
9062            let hw2 = u16::from_le_bytes([b[2], b[3]]) as u32;
9063            let imm4 = hw1 & 0xF;
9064            let i = (hw1 >> 10) & 1;
9065            let imm3 = (hw2 >> 12) & 0x7;
9066            let imm8 = hw2 & 0xFF;
9067            (imm4 << 12) | (i << 11) | (imm3 << 8) | imm8
9068        }
9069        fn movx_rd(b: &[u8]) -> u32 {
9070            (u16::from_le_bytes([b[2], b[3]]) as u32 >> 8) & 0xF
9071        }
9072
9073        // --- rd != rn: scratch is rd. imm = 70000 = 0x11170 needs MOVW+MOVT. ---
9074        // 0x11170: lo16 = 0x1170, hi16 = 0x0001
9075        let seq = enc
9076            .encode_thumb32_add_imm(&Reg::R12, &Reg::R0, 70000)
9077            .unwrap();
9078        assert_eq!(seq.len(), 12, "MOVW + MOVT + ADD = 12 bytes");
9079        // MOVW r12, #0x1170
9080        assert_eq!(u16::from_le_bytes([seq[0], seq[1]]) & 0xFBF0, 0xF240);
9081        assert_eq!(movx_rd(&seq[0..4]), 12);
9082        assert_eq!(movx_imm16(&seq[0..4]), 0x1170);
9083        // MOVT r12, #0x0001
9084        assert_eq!(u16::from_le_bytes([seq[4], seq[5]]) & 0xFBF0, 0xF2C0);
9085        assert_eq!(movx_rd(&seq[4..8]), 12);
9086        assert_eq!(movx_imm16(&seq[4..8]), 0x0001);
9087        // ADD.W r12, r0, r12  (EB00 | rn=0 ; rd=12, rm=12)
9088        let add1 = u16::from_le_bytes([seq[8], seq[9]]) as u32;
9089        let add2 = u16::from_le_bytes([seq[10], seq[11]]) as u32;
9090        assert_eq!(add1 & 0xFFF0, 0xEB00);
9091        assert_eq!(add1 & 0xF, 0); // rn = r0
9092        assert_eq!((add2 >> 8) & 0xF, 12); // rd = r12
9093        assert_eq!(add2 & 0xF, 12); // rm = scratch = r12
9094        // The materialized scratch must reconstruct exactly 70000.
9095        assert_eq!(
9096            (movx_imm16(&seq[4..8]) << 16) | movx_imm16(&seq[0..4]),
9097            70000
9098        );
9099
9100        // --- imm <= 0xFFFF: MOVT is skipped (MOVW + ADD = 8 bytes). ---
9101        let seq16 = enc
9102            .encode_thumb32_add_imm(&Reg::R3, &Reg::R0, 0xABCD)
9103            .unwrap();
9104        assert_eq!(seq16.len(), 8, "imm <= 0xFFFF skips MOVT");
9105        assert_eq!(movx_imm16(&seq16[0..4]), 0xABCD);
9106        assert_eq!(movx_rd(&seq16[0..4]), 3); // scratch = rd = r3
9107
9108        // --- rd == rn (in-place add): scratch must be R12, not rd. ---
9109        // imm = 0x12345: lo16 = 0x2345, hi16 = 0x0001
9110        let inplace = enc
9111            .encode_thumb32_add_imm(&Reg::R5, &Reg::R5, 0x12345)
9112            .unwrap();
9113        assert_eq!(inplace.len(), 12);
9114        assert_eq!(movx_rd(&inplace[0..4]), 12, "rd==rn must use R12 scratch");
9115        assert_eq!(
9116            (movx_imm16(&inplace[4..8]) << 16) | movx_imm16(&inplace[0..4]),
9117            0x12345
9118        );
9119        // ADD.W r5, r5, r12 — rm must be the scratch (12), never rn.
9120        let ip_add2 = u16::from_le_bytes([inplace[10], inplace[11]]) as u32;
9121        assert_eq!(ip_add2 & 0xF, 12);
9122        assert_eq!((ip_add2 >> 8) & 0xF, 5);
9123    }
9124
9125    /// #350 follow-up — the `encoder_no_panic` fuzz harness drives the encoder
9126    /// with ARBITRARY registers, including the one case the in-place lowering
9127    /// cannot serve: rd==rn==R12. There the scratch (R12, the reserved encoder
9128    /// register) would alias Rn and clobber it before the ADD reads it. The
9129    /// encoder contract (#180/#185) is Ok-or-Err, never a panic — so this must
9130    /// return Err, not assert. (Real codegen never emits rd==rn==R12 because R12
9131    /// is non-allocatable; this guards only the fuzz/adversarial path.)
9132    #[test]
9133    fn test_encode_add_imm_large_rd_rn_r12_errs_not_panics_350() {
9134        let enc = ArmEncoder::new_thumb2();
9135        // Out-of-range imm with rd==rn==R12: no free scratch -> Err.
9136        let r = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 70000);
9137        assert!(
9138            r.is_err(),
9139            "rd==rn==R12 with out-of-range imm must Err (no free scratch), got {r:?}"
9140        );
9141        // Small imm with rd==rn==R12 still takes the single-instruction fast path
9142        // (no scratch needed) and must succeed — the guard is scoped to the
9143        // out-of-range lowering only.
9144        let small = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 0x10);
9145        assert!(small.is_ok(), "small imm needs no scratch, must stay Ok");
9146    }
9147
9148    /// #378 — `encode_operand2` (ARM32 data-processing operand) must FAIL
9149    /// HONESTLY on an immediate that is not a valid rotated immediate, rather
9150    /// than silently masking it to `imm & 0xFF` and emitting a WRONG
9151    /// instruction. `0x1FF` has 9 set bits, so it cannot come from rotating an
9152    /// 8-bit imm8 — non-encodable. Real codegen materializes large constants via
9153    /// MOVW/MOVT; this guards the encoder's Ok-or-Err contract (#180/#185)
9154    /// directly. It is an Err (not a panic) so the `encoder_no_panic` fuzz
9155    /// harness — which drives arbitrary operands — still passes.
9156    #[test]
9157    fn test_encode_operand2_non_rotatable_imm_errs_not_masks_378() {
9158        let enc = ArmEncoder::new_arm32();
9159        let bad = enc.encode(&ArmOp::Add {
9160            rd: Reg::R0,
9161            rn: Reg::R1,
9162            op2: Operand2::Imm(0x1FF),
9163        });
9164        assert!(
9165            bad.is_err(),
9166            "non-rotatable ARM32 immediate 0x1FF must Err (was silently masked \
9167             to 0xFF), got {bad:?}"
9168        );
9169        // A representable rotated immediate still encodes fine (regression guard).
9170        let ok = enc.encode(&ArmOp::Add {
9171            rd: Reg::R0,
9172            rn: Reg::R1,
9173            op2: Operand2::Imm(0xFF),
9174        });
9175        assert!(
9176            ok.is_ok(),
9177            "0xFF is a valid rotated immediate, must stay Ok"
9178        );
9179    }
9180
9181    #[test]
9182    fn test_encode_ldr_arm32() {
9183        let encoder = ArmEncoder::new_arm32();
9184        let op = ArmOp::Ldr {
9185            rd: Reg::R0,
9186            addr: MemAddr::imm(Reg::R1, 4),
9187        };
9188
9189        let code = encoder.encode(&op).unwrap();
9190        assert_eq!(code.len(), 4);
9191
9192        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9193        // Verify load bit is set
9194        assert_eq!(instr & 0x00100000, 0x00100000);
9195    }
9196
9197    #[test]
9198    fn test_encode_str_arm32() {
9199        let encoder = ArmEncoder::new_arm32();
9200        let op = ArmOp::Str {
9201            rd: Reg::R0,
9202            addr: MemAddr::imm(Reg::SP, 0),
9203        };
9204
9205        let code = encoder.encode(&op).unwrap();
9206        assert_eq!(code.len(), 4);
9207    }
9208
9209    #[test]
9210    fn test_encode_branch_arm32() {
9211        let encoder = ArmEncoder::new_arm32();
9212        let op = ArmOp::Bl {
9213            label: "main".to_string(),
9214        };
9215
9216        let code = encoder.encode(&op).unwrap();
9217        assert_eq!(code.len(), 4);
9218
9219        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9220        // Verify BL opcode
9221        assert_eq!(instr & 0x0F000000, 0x0B000000);
9222    }
9223
9224    /// Regression test for #167 + #174: the Thumb-2 BL relocatable placeholder
9225    /// must carry a -4 addend so an R_ARM_THM_CALL nets to exactly the symbol S.
9226    /// The correct encoding is what `gas` emits for `bl <extern>`: f7ff fffe
9227    /// (hw1=0xF7FF, hw2=0xFFFE), little-endian bytes FF F7 FE FF.
9228    ///   - 0xD000 (J1=J2=0) → ~+0x600000 garbage addend: `bl c0000c` / truncated
9229    ///     to fit (#167).
9230    ///   - 0xF800 (addend 0) → lands at S+4, one instruction past the callee
9231    ///     entry (#174).
9232    ///   - 0xFFFE (addend -4) → lands at S. Correct.
9233    #[test]
9234    fn test_encode_thumb_bl_placeholder_addend_167_174() {
9235        let encoder = ArmEncoder::new_thumb2();
9236        let op = ArmOp::Bl {
9237            label: "callee".to_string(),
9238        };
9239
9240        let code = encoder.encode(&op).unwrap();
9241        assert_eq!(code.len(), 4, "Thumb-2 BL is 32-bit");
9242
9243        let hw1 = u16::from_le_bytes([code[0], code[1]]);
9244        let hw2 = u16::from_le_bytes([code[2], code[3]]);
9245        assert_eq!(hw1, 0xF7FF, "BL first halfword (matches gas `bl <extern>`)");
9246        assert_eq!(
9247            hw2, 0xFFFE,
9248            "BL second halfword must be 0xFFFE (-4 addend → nets to S), not 0xF800 (→ S+4, #174) or 0xD000 (#167)"
9249        );
9250        assert_ne!(hw2, 0xF800, "0xF800 (addend 0) lands at S+4 (#174)");
9251        assert_ne!(hw2, 0xD000, "0xD000 bakes in a ~+0x600000 addend (#167)");
9252    }
9253
9254    #[test]
9255    fn test_encode_sequence() {
9256        let encoder = ArmEncoder::new_arm32();
9257        let ops = vec![
9258            ArmOp::Mov {
9259                rd: Reg::R0,
9260                op2: Operand2::Imm(42),
9261            },
9262            ArmOp::Mov {
9263                rd: Reg::R1,
9264                op2: Operand2::Imm(10),
9265            },
9266            ArmOp::Add {
9267                rd: Reg::R2,
9268                rn: Reg::R0,
9269                op2: Operand2::Reg(Reg::R1),
9270            },
9271        ];
9272
9273        let code = encoder.encode_sequence(&ops).unwrap();
9274        assert_eq!(code.len(), 12); // 3 instructions * 4 bytes
9275    }
9276
9277    #[test]
9278    fn test_reg_to_bits() {
9279        assert_eq!(reg_to_bits(&Reg::R0), 0);
9280        assert_eq!(reg_to_bits(&Reg::R7), 7);
9281        assert_eq!(reg_to_bits(&Reg::SP), 13);
9282        assert_eq!(reg_to_bits(&Reg::LR), 14);
9283        assert_eq!(reg_to_bits(&Reg::PC), 15);
9284    }
9285
9286    #[test]
9287    fn test_encode_bitwise_operations() {
9288        let encoder = ArmEncoder::new_arm32();
9289
9290        let and_op = ArmOp::And {
9291            rd: Reg::R0,
9292            rn: Reg::R1,
9293            op2: Operand2::Reg(Reg::R2),
9294        };
9295        let and_code = encoder.encode(&and_op).unwrap();
9296        assert_eq!(and_code.len(), 4);
9297
9298        let orr_op = ArmOp::Orr {
9299            rd: Reg::R0,
9300            rn: Reg::R1,
9301            op2: Operand2::Reg(Reg::R2),
9302        };
9303        let orr_code = encoder.encode(&orr_op).unwrap();
9304        assert_eq!(orr_code.len(), 4);
9305
9306        let eor_op = ArmOp::Eor {
9307            rd: Reg::R0,
9308            rn: Reg::R1,
9309            op2: Operand2::Reg(Reg::R2),
9310        };
9311        let eor_code = encoder.encode(&eor_op).unwrap();
9312        assert_eq!(eor_code.len(), 4);
9313    }
9314
9315    // === Thumb-2 32-bit encoding tests ===
9316
9317    #[test]
9318    fn test_encode_sdiv_thumb2() {
9319        let encoder = ArmEncoder::new_thumb2();
9320        let op = ArmOp::Sdiv {
9321            rd: Reg::R0,
9322            rn: Reg::R1,
9323            rm: Reg::R2,
9324        };
9325
9326        let code = encoder.encode(&op).unwrap();
9327        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9328
9329        // SDIV R0, R1, R2: 0xFB91 0xF0F2
9330        // First halfword: 0xFB90 | Rn(1) = 0xFB91
9331        // Second halfword: 0xF0F0 | Rd(0)<<8 | Rm(2) = 0xF0F2
9332        // Little-endian: [0x91, 0xFB, 0xF2, 0xF0]
9333        assert_eq!(code[0], 0x91);
9334        assert_eq!(code[1], 0xFB);
9335        assert_eq!(code[2], 0xF2);
9336        assert_eq!(code[3], 0xF0);
9337    }
9338
9339    #[test]
9340    fn test_encode_udiv_thumb2() {
9341        let encoder = ArmEncoder::new_thumb2();
9342        let op = ArmOp::Udiv {
9343            rd: Reg::R0,
9344            rn: Reg::R1,
9345            rm: Reg::R2,
9346        };
9347
9348        let code = encoder.encode(&op).unwrap();
9349        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9350
9351        // UDIV R0, R1, R2: 0xFBB1 0xF0F2
9352        // Little-endian: [0xB1, 0xFB, 0xF2, 0xF0]
9353        assert_eq!(code[0], 0xB1);
9354        assert_eq!(code[1], 0xFB);
9355        assert_eq!(code[2], 0xF2);
9356        assert_eq!(code[3], 0xF0);
9357    }
9358
9359    #[test]
9360    fn test_encode_mul_thumb2() {
9361        let encoder = ArmEncoder::new_thumb2();
9362        let op = ArmOp::Mul {
9363            rd: Reg::R0,
9364            rn: Reg::R1,
9365            rm: Reg::R2,
9366        };
9367
9368        let code = encoder.encode(&op).unwrap();
9369        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9370    }
9371
9372    #[test]
9373    fn test_encode_and_thumb2() {
9374        let encoder = ArmEncoder::new_thumb2();
9375        let op = ArmOp::And {
9376            rd: Reg::R0,
9377            rn: Reg::R1,
9378            op2: Operand2::Reg(Reg::R2),
9379        };
9380
9381        let code = encoder.encode(&op).unwrap();
9382        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9383    }
9384
9385    #[test]
9386    fn test_encode_lsl_thumb2_low_regs() {
9387        let encoder = ArmEncoder::new_thumb2();
9388        let op = ArmOp::Lsl {
9389            rd: Reg::R0,
9390            rn: Reg::R1,
9391            shift: 5,
9392        };
9393
9394        let code = encoder.encode(&op).unwrap();
9395        assert_eq!(code.len(), 2); // 16-bit for low registers
9396    }
9397
9398    #[test]
9399    fn test_encode_clz_thumb2() {
9400        let encoder = ArmEncoder::new_thumb2();
9401        let op = ArmOp::Clz {
9402            rd: Reg::R0,
9403            rm: Reg::R1,
9404        };
9405
9406        let code = encoder.encode(&op).unwrap();
9407        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
9408    }
9409
9410    #[test]
9411    fn test_encode_bx_thumb2() {
9412        let encoder = ArmEncoder::new_thumb2();
9413        let op = ArmOp::Bx { rm: Reg::LR };
9414
9415        let code = encoder.encode(&op).unwrap();
9416        assert_eq!(code.len(), 2); // 16-bit instruction
9417
9418        // BX LR: 0x4770
9419        assert_eq!(code, vec![0x70, 0x47]);
9420    }
9421
9422    // ========================================================================
9423    // f32 pseudo-op encoding tests
9424    // ========================================================================
9425
9426    #[test]
9427    fn test_encode_f32_abs_arm32() {
9428        let encoder = ArmEncoder::new_arm32();
9429        let op = ArmOp::F32Abs {
9430            sd: VfpReg::S0,
9431            sm: VfpReg::S2,
9432        };
9433        let code = encoder.encode(&op).unwrap();
9434        assert_eq!(code.len(), 4); // Single VFP instruction
9435    }
9436
9437    #[test]
9438    fn test_encode_f32_neg_arm32() {
9439        let encoder = ArmEncoder::new_arm32();
9440        let op = ArmOp::F32Neg {
9441            sd: VfpReg::S0,
9442            sm: VfpReg::S2,
9443        };
9444        let code = encoder.encode(&op).unwrap();
9445        assert_eq!(code.len(), 4);
9446    }
9447
9448    #[test]
9449    fn test_encode_f32_sqrt_arm32() {
9450        let encoder = ArmEncoder::new_arm32();
9451        let op = ArmOp::F32Sqrt {
9452            sd: VfpReg::S0,
9453            sm: VfpReg::S2,
9454        };
9455        let code = encoder.encode(&op).unwrap();
9456        assert_eq!(code.len(), 4);
9457    }
9458
9459    #[test]
9460    fn test_encode_f32_ceil_arm32() {
9461        let encoder = ArmEncoder::new_arm32();
9462        let op = ArmOp::F32Ceil {
9463            sd: VfpReg::S0,
9464            sm: VfpReg::S2,
9465        };
9466        let code = encoder.encode(&op).unwrap();
9467        // VMRS + BIC + ORR + VMSR + VCVT.S32.F32 + VMRS + BIC + VMSR + VCVT.F32.S32
9468        assert_eq!(code.len(), 36);
9469    }
9470
9471    #[test]
9472    fn test_encode_f32_floor_thumb2() {
9473        let encoder = ArmEncoder::new_thumb2();
9474        let op = ArmOp::F32Floor {
9475            sd: VfpReg::S0,
9476            sm: VfpReg::S2,
9477        };
9478        let code = encoder.encode(&op).unwrap();
9479        // VMRS + BIC.W + ORR.W + VMSR + VCVT + VMRS + BIC.W + VMSR + VCVT.F32.S32
9480        assert_eq!(code.len(), 36);
9481    }
9482
9483    #[test]
9484    fn test_encode_f32_min_arm32() {
9485        let encoder = ArmEncoder::new_arm32();
9486        let op = ArmOp::F32Min {
9487            sd: VfpReg::S0,
9488            sn: VfpReg::S2,
9489            sm: VfpReg::S4,
9490        };
9491        let code = encoder.encode(&op).unwrap();
9492        assert_eq!(code.len(), 16); // VMOV + VCMP + VMRS + conditional VMOV
9493    }
9494
9495    #[test]
9496    fn test_encode_f32_max_thumb2() {
9497        let encoder = ArmEncoder::new_thumb2();
9498        let op = ArmOp::F32Max {
9499            sd: VfpReg::S0,
9500            sn: VfpReg::S2,
9501            sm: VfpReg::S4,
9502        };
9503        let code = encoder.encode(&op).unwrap();
9504        // VMOV(4) + VCMP(4) + VMRS(4) + IT(2) + VMOV(4) = 18
9505        assert_eq!(code.len(), 18);
9506    }
9507
9508    #[test]
9509    fn test_encode_f32_copysign_arm32() {
9510        let encoder = ArmEncoder::new_arm32();
9511        let op = ArmOp::F32Copysign {
9512            sd: VfpReg::S0,
9513            sn: VfpReg::S2,
9514            sm: VfpReg::S4,
9515        };
9516        let code = encoder.encode(&op).unwrap();
9517        // VMOV + VMOV + AND + BIC + ORR + VMOV = 6 * 4 = 24
9518        assert_eq!(code.len(), 24);
9519    }
9520
9521    // ========================================================================
9522    // f64 encoding tests
9523    // ========================================================================
9524
9525    #[test]
9526    fn test_encode_f64_add_arm32() {
9527        let encoder = ArmEncoder::new_arm32();
9528        let op = ArmOp::F64Add {
9529            dd: VfpReg::D0,
9530            dn: VfpReg::D1,
9531            dm: VfpReg::D2,
9532        };
9533        let code = encoder.encode(&op).unwrap();
9534        assert_eq!(code.len(), 4);
9535        // VADD.F64 D0, D1, D2: check coprocessor is cp11 (0xB)
9536        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9537        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
9538    }
9539
9540    #[test]
9541    fn test_encode_f64_sub_thumb2() {
9542        let encoder = ArmEncoder::new_thumb2();
9543        let op = ArmOp::F64Sub {
9544            dd: VfpReg::D0,
9545            dn: VfpReg::D1,
9546            dm: VfpReg::D2,
9547        };
9548        let code = encoder.encode(&op).unwrap();
9549        assert_eq!(code.len(), 4); // 32-bit VFP as two Thumb halfwords
9550    }
9551
9552    #[test]
9553    fn test_encode_f64_mul_arm32() {
9554        let encoder = ArmEncoder::new_arm32();
9555        let op = ArmOp::F64Mul {
9556            dd: VfpReg::D0,
9557            dn: VfpReg::D1,
9558            dm: VfpReg::D2,
9559        };
9560        let code = encoder.encode(&op).unwrap();
9561        assert_eq!(code.len(), 4);
9562    }
9563
9564    #[test]
9565    fn test_encode_f64_div_arm32() {
9566        let encoder = ArmEncoder::new_arm32();
9567        let op = ArmOp::F64Div {
9568            dd: VfpReg::D0,
9569            dn: VfpReg::D1,
9570            dm: VfpReg::D2,
9571        };
9572        let code = encoder.encode(&op).unwrap();
9573        assert_eq!(code.len(), 4);
9574    }
9575
9576    #[test]
9577    fn test_encode_f64_abs_arm32() {
9578        let encoder = ArmEncoder::new_arm32();
9579        let op = ArmOp::F64Abs {
9580            dd: VfpReg::D0,
9581            dm: VfpReg::D2,
9582        };
9583        let code = encoder.encode(&op).unwrap();
9584        assert_eq!(code.len(), 4);
9585    }
9586
9587    #[test]
9588    fn test_encode_f64_neg_arm32() {
9589        let encoder = ArmEncoder::new_arm32();
9590        let op = ArmOp::F64Neg {
9591            dd: VfpReg::D0,
9592            dm: VfpReg::D2,
9593        };
9594        let code = encoder.encode(&op).unwrap();
9595        assert_eq!(code.len(), 4);
9596    }
9597
9598    #[test]
9599    fn test_encode_f64_sqrt_arm32() {
9600        let encoder = ArmEncoder::new_arm32();
9601        let op = ArmOp::F64Sqrt {
9602            dd: VfpReg::D0,
9603            dm: VfpReg::D2,
9604        };
9605        let code = encoder.encode(&op).unwrap();
9606        assert_eq!(code.len(), 4);
9607    }
9608
9609    #[test]
9610    fn test_encode_f64_load_arm32() {
9611        let encoder = ArmEncoder::new_arm32();
9612        let op = ArmOp::F64Load {
9613            dd: VfpReg::D0,
9614            addr: MemAddr::imm(Reg::R0, 8),
9615        };
9616        let code = encoder.encode(&op).unwrap();
9617        assert_eq!(code.len(), 4);
9618        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9619        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11 for F64
9620        assert_eq!(instr & 0xFF, 2); // offset 8 / 4 = 2
9621    }
9622
9623    #[test]
9624    fn test_encode_f64_store_thumb2() {
9625        let encoder = ArmEncoder::new_thumb2();
9626        let op = ArmOp::F64Store {
9627            dd: VfpReg::D0,
9628            addr: MemAddr::imm(Reg::SP, 0),
9629        };
9630        let code = encoder.encode(&op).unwrap();
9631        assert_eq!(code.len(), 4);
9632    }
9633
9634    #[test]
9635    fn test_encode_f64_compare_arm32() {
9636        let encoder = ArmEncoder::new_arm32();
9637        let op = ArmOp::F64Eq {
9638            rd: Reg::R0,
9639            dn: VfpReg::D0,
9640            dm: VfpReg::D1,
9641        };
9642        let code = encoder.encode(&op).unwrap();
9643        assert_eq!(code.len(), 16); // VCMP + VMRS + MOV #0 + MOVcond #1
9644    }
9645
9646    #[test]
9647    fn test_encode_f64_compare_thumb2() {
9648        let encoder = ArmEncoder::new_thumb2();
9649        let op = ArmOp::F64Lt {
9650            rd: Reg::R0,
9651            dn: VfpReg::D0,
9652            dm: VfpReg::D1,
9653        };
9654        let code = encoder.encode(&op).unwrap();
9655        // VCMP(4) + VMRS(4) + MOVS(2) + IT(2) + MOV(2) = 14
9656        assert_eq!(code.len(), 14);
9657    }
9658
9659    #[test]
9660    fn test_encode_f64_const_arm32() {
9661        let encoder = ArmEncoder::new_arm32();
9662        let op = ArmOp::F64Const {
9663            dd: VfpReg::D0,
9664            value: 3.125,
9665        };
9666        let code = encoder.encode(&op).unwrap();
9667        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
9668        assert_eq!(code.len(), 20);
9669    }
9670
9671    #[test]
9672    fn test_encode_f64_const_thumb2() {
9673        let encoder = ArmEncoder::new_thumb2();
9674        let op = ArmOp::F64Const {
9675            dd: VfpReg::D0,
9676            value: 2.5,
9677        };
9678        let code = encoder.encode(&op).unwrap();
9679        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
9680        assert_eq!(code.len(), 20);
9681    }
9682
9683    #[test]
9684    fn test_encode_f64_convert_i32s_arm32() {
9685        let encoder = ArmEncoder::new_arm32();
9686        let op = ArmOp::F64ConvertI32S {
9687            dd: VfpReg::D0,
9688            rm: Reg::R0,
9689        };
9690        let code = encoder.encode(&op).unwrap();
9691        // VMOV(4) + VCVT(4) = 8
9692        assert_eq!(code.len(), 8);
9693    }
9694
9695    #[test]
9696    fn test_encode_f64_promote_f32_arm32() {
9697        let encoder = ArmEncoder::new_arm32();
9698        let op = ArmOp::F64PromoteF32 {
9699            dd: VfpReg::D0,
9700            sm: VfpReg::S0,
9701        };
9702        let code = encoder.encode(&op).unwrap();
9703        assert_eq!(code.len(), 4); // Single VCVT.F64.F32 instruction
9704    }
9705
9706    #[test]
9707    fn test_encode_f64_promote_f32_thumb2() {
9708        let encoder = ArmEncoder::new_thumb2();
9709        let op = ArmOp::F64PromoteF32 {
9710            dd: VfpReg::D0,
9711            sm: VfpReg::S0,
9712        };
9713        let code = encoder.encode(&op).unwrap();
9714        assert_eq!(code.len(), 4);
9715    }
9716
9717    #[test]
9718    fn test_encode_i32_trunc_f64s_arm32() {
9719        let encoder = ArmEncoder::new_arm32();
9720        let op = ArmOp::I32TruncF64S {
9721            rd: Reg::R0,
9722            dm: VfpReg::D0,
9723        };
9724        let code = encoder.encode(&op).unwrap();
9725        // VCVT(4) + VMOV(4) = 8
9726        assert_eq!(code.len(), 8);
9727    }
9728
9729    #[test]
9730    fn test_encode_f64_reinterpret_i64_arm32() {
9731        let encoder = ArmEncoder::new_arm32();
9732        let op = ArmOp::F64ReinterpretI64 {
9733            dd: VfpReg::D0,
9734            rmlo: Reg::R0,
9735            rmhi: Reg::R1,
9736        };
9737        let code = encoder.encode(&op).unwrap();
9738        assert_eq!(code.len(), 4); // Single VMOV instruction
9739    }
9740
9741    #[test]
9742    fn test_encode_i64_reinterpret_f64_thumb2() {
9743        let encoder = ArmEncoder::new_thumb2();
9744        let op = ArmOp::I64ReinterpretF64 {
9745            rdlo: Reg::R0,
9746            rdhi: Reg::R1,
9747            dm: VfpReg::D0,
9748        };
9749        let code = encoder.encode(&op).unwrap();
9750        assert_eq!(code.len(), 4);
9751    }
9752
9753    #[test]
9754    fn test_encode_f64_trunc_thumb2() {
9755        let encoder = ArmEncoder::new_thumb2();
9756        let op = ArmOp::F64Trunc {
9757            dd: VfpReg::D0,
9758            dm: VfpReg::D1,
9759        };
9760        let code = encoder.encode(&op).unwrap();
9761        // Two VFP instructions via Thumb encoding
9762        assert_eq!(code.len(), 8);
9763    }
9764
9765    #[test]
9766    fn test_encode_f64_min_arm32() {
9767        let encoder = ArmEncoder::new_arm32();
9768        let op = ArmOp::F64Min {
9769            dd: VfpReg::D0,
9770            dn: VfpReg::D1,
9771            dm: VfpReg::D2,
9772        };
9773        let code = encoder.encode(&op).unwrap();
9774        // VMOV + VCMP + VMRS + conditional VMOV = 16
9775        assert_eq!(code.len(), 16);
9776    }
9777
9778    #[test]
9779    fn test_f64_cp11_encoding() {
9780        // Verify that F64 instructions use coprocessor 11 (0xB), not 10 (0xA)
9781        let encoder = ArmEncoder::new_arm32();
9782
9783        // F64Add
9784        let code = encoder
9785            .encode(&ArmOp::F64Add {
9786                dd: VfpReg::D0,
9787                dn: VfpReg::D0,
9788                dm: VfpReg::D0,
9789            })
9790            .unwrap();
9791        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9792        assert_eq!((instr >> 8) & 0xF, 0xB, "F64 should use cp11");
9793
9794        // F32Add for comparison
9795        let code = encoder
9796            .encode(&ArmOp::F32Add {
9797                sd: VfpReg::S0,
9798                sn: VfpReg::S0,
9799                sm: VfpReg::S0,
9800            })
9801            .unwrap();
9802        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9803        assert_eq!((instr >> 8) & 0xF, 0xA, "F32 should use cp10");
9804    }
9805
9806    #[test]
9807    fn test_dreg_encoding_higher_registers() {
9808        let encoder = ArmEncoder::new_arm32();
9809
9810        // Test with D15 (highest register)
9811        let op = ArmOp::F64Add {
9812            dd: VfpReg::D15,
9813            dn: VfpReg::D14,
9814            dm: VfpReg::D13,
9815        };
9816        let code = encoder.encode(&op).unwrap();
9817        assert_eq!(code.len(), 4);
9818
9819        // Verify the register encoding worked (instruction is valid)
9820        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9821        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
9822    }
9823
9824    // ========================================================================
9825    // Control flow encoding tests
9826    // ========================================================================
9827
9828    #[test]
9829    fn test_encode_label_emits_no_bytes() {
9830        let encoder = ArmEncoder::new_thumb2();
9831        let op = ArmOp::Label {
9832            name: ".Lblock_end_0".to_string(),
9833        };
9834        let code = encoder.encode(&op).unwrap();
9835        assert!(code.is_empty(), "Label should emit zero bytes");
9836
9837        let encoder32 = ArmEncoder::new_arm32();
9838        let code32 = encoder32.encode(&op).unwrap();
9839        assert!(
9840            code32.is_empty(),
9841            "Label should emit zero bytes in ARM32 too"
9842        );
9843    }
9844
9845    #[test]
9846    fn test_encode_bcc_eq_thumb2() {
9847        use synth_synthesis::Condition;
9848        let encoder = ArmEncoder::new_thumb2();
9849        let op = ArmOp::Bcc {
9850            cond: Condition::EQ,
9851            label: "target".to_string(),
9852        };
9853        let code = encoder.encode(&op).unwrap();
9854        assert_eq!(code.len(), 2); // 16-bit conditional branch
9855
9856        // BEQ with offset 0: 0xD000 in little-endian
9857        assert_eq!(code, vec![0x00, 0xD0]);
9858    }
9859
9860    #[test]
9861    fn test_encode_bcc_ne_thumb2() {
9862        use synth_synthesis::Condition;
9863        let encoder = ArmEncoder::new_thumb2();
9864        let op = ArmOp::Bcc {
9865            cond: Condition::NE,
9866            label: "target".to_string(),
9867        };
9868        let code = encoder.encode(&op).unwrap();
9869        assert_eq!(code.len(), 2);
9870
9871        // BNE with offset 0: 0xD100 in little-endian
9872        assert_eq!(code, vec![0x00, 0xD1]);
9873    }
9874
9875    #[test]
9876    fn test_encode_bcc_arm32() {
9877        use synth_synthesis::Condition;
9878        let encoder = ArmEncoder::new_arm32();
9879        let op = ArmOp::Bcc {
9880            cond: Condition::EQ,
9881            label: "target".to_string(),
9882        };
9883        let code = encoder.encode(&op).unwrap();
9884        assert_eq!(code.len(), 4); // 32-bit ARM instruction
9885
9886        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9887        // BEQ: cond=0x0, opcode=0xA, offset=0
9888        assert_eq!(instr & 0xF0000000, 0x00000000); // EQ condition
9889        assert_eq!(instr & 0x0F000000, 0x0A000000); // Branch opcode
9890    }
9891
9892    #[test]
9893    fn test_encode_udf_thumb2() {
9894        let encoder = ArmEncoder::new_thumb2();
9895        let op = ArmOp::Udf { imm: 0 };
9896        let code = encoder.encode(&op).unwrap();
9897        assert_eq!(code.len(), 2); // 16-bit
9898
9899        // UDF #0: 0xDE00 in little-endian
9900        assert_eq!(code, vec![0x00, 0xDE]);
9901    }
9902
9903    /// #610: the i64 rot/div/rem expansions must land the result in the
9904    /// selector-assigned rd pair and leave R0-R3 preserved (restored from the
9905    /// fixed-ABI wrapper's save area) — pre-#610 the rot expansion's own
9906    /// `POP {R4}` restored stale scratch OVER the result (rd_lo == R4) and
9907    /// the div/rem expansions ignored their register fields outright.
9908    #[test]
9909    fn test_610_i64_rot_expansion_ends_with_rd_movs_and_restore() {
9910        let encoder = ArmEncoder::new_thumb2();
9911        for op in [
9912            ArmOp::I64Rotl {
9913                rdlo: Reg::R4,
9914                rdhi: Reg::R5,
9915                rnlo: Reg::R0,
9916                rnhi: Reg::R1,
9917                shift: Reg::R2,
9918            },
9919            ArmOp::I64Rotr {
9920                rdlo: Reg::R4,
9921                rdhi: Reg::R5,
9922                rnlo: Reg::R0,
9923                rnhi: Reg::R1,
9924                shift: Reg::R2,
9925            },
9926        ] {
9927            let code = encoder.encode(&op).unwrap();
9928            assert_eq!(code.len(), 102, "register-independent size (estimator pin)");
9929            // Tail: MOV r5, r1 (0x460D); MOV r4, r0 (0x4604); POP {r0..r3}
9930            // (rd pair r4:r5 does not overlap the save area — all 4 restored).
9931            let tail: Vec<u16> = code[code.len() - 12..]
9932                .chunks(2)
9933                .map(|c| u16::from_le_bytes([c[0], c[1]]))
9934                .collect();
9935            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
9936        }
9937    }
9938
9939    /// #610: div/rem expansions honor rd and carry the divide-by-zero trap
9940    /// guard (`ORRS R12, R2, R3; BNE +0; UDF #0`) after operand marshaling.
9941    #[test]
9942    fn test_610_i64_div_rem_expansion_guard_and_rd() {
9943        let encoder = ArmEncoder::new_thumb2();
9944        let mk = |which: u8| {
9945            let (rdlo, rdhi, rnlo, rnhi, rmlo, rmhi) =
9946                (Reg::R4, Reg::R5, Reg::R0, Reg::R1, Reg::R2, Reg::R3);
9947            match which {
9948                0 => ArmOp::I64DivU {
9949                    rdlo,
9950                    rdhi,
9951                    rnlo,
9952                    rnhi,
9953                    rmlo,
9954                    rmhi,
9955                    elide_zero_guard: false,
9956                },
9957                1 => ArmOp::I64RemU {
9958                    rdlo,
9959                    rdhi,
9960                    rnlo,
9961                    rnhi,
9962                    rmlo,
9963                    rmhi,
9964                    elide_zero_guard: false,
9965                },
9966                2 => ArmOp::I64DivS {
9967                    rdlo,
9968                    rdhi,
9969                    rnlo,
9970                    rnhi,
9971                    rmlo,
9972                    rmhi,
9973                    elide_zero_guard: false,
9974                    elide_overflow_guard: false,
9975                },
9976                _ => ArmOp::I64RemS {
9977                    rdlo,
9978                    rdhi,
9979                    rnlo,
9980                    rnhi,
9981                    rmlo,
9982                    rmhi,
9983                    elide_zero_guard: false,
9984                },
9985            }
9986        };
9987        for which in 0..4u8 {
9988            let code = encoder.encode(&mk(which)).unwrap();
9989            // Zero-divisor trap guard right after the 26-byte marshal prologue.
9990            let guard: Vec<u16> = code[26..34]
9991                .chunks(2)
9992                .map(|c| u16::from_le_bytes([c[0], c[1]]))
9993                .collect();
9994            assert_eq!(
9995                guard,
9996                vec![0xEA52, 0x0C03, 0xD100, 0xDE00],
9997                "ORRS R12,R2,R3; BNE +0; UDF #0"
9998            );
9999            // Tail: result into rd pair (r5:r4), then restore all of R0-R3.
10000            let tail: Vec<u16> = code[code.len() - 12..]
10001                .chunks(2)
10002                .map(|c| u16::from_le_bytes([c[0], c[1]]))
10003                .collect();
10004            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
10005        }
10006    }
10007
10008    /// #610: when rd overlaps R0-R3 the restore must SKIP the result
10009    /// registers (drop the saved caller word) instead of popping over them.
10010    #[test]
10011    fn test_610_i64_divu_rd_in_r0_r1_skips_restore() {
10012        let encoder = ArmEncoder::new_thumb2();
10013        let code = encoder
10014            .encode(&ArmOp::I64DivU {
10015                rdlo: Reg::R0,
10016                rdhi: Reg::R1,
10017                rnlo: Reg::R0,
10018                rnhi: Reg::R1,
10019                rmlo: Reg::R2,
10020                rmhi: Reg::R3,
10021                elide_zero_guard: false,
10022            })
10023            .unwrap();
10024        let tail: Vec<u16> = code[code.len() - 12..]
10025            .chunks(2)
10026            .map(|c| u16::from_le_bytes([c[0], c[1]]))
10027            .collect();
10028        // MOV r1,r1 / MOV r0,r0 (no-ops, size-stable), ADD SP,#4 twice
10029        // (discard saved r0/r1 — the result lives there), POP {r2}, POP {r3}.
10030        assert_eq!(tail, vec![0x4609, 0x4600, 0xB001, 0xB001, 0xBC04, 0xBC08]);
10031    }
10032
10033    /// #610: a fully swapped rd pair (rd_lo=R1, rd_hi=R0) cannot be
10034    /// materialized by two MOVs in either order — must be a loud Err, never
10035    /// silent corruption. (Selector pairs are consecutive, so unreachable.)
10036    #[test]
10037    fn test_610_i64_swapped_rd_pair_rejected() {
10038        let encoder = ArmEncoder::new_thumb2();
10039        let result = encoder.encode(&ArmOp::I64RemU {
10040            rdlo: Reg::R1,
10041            rdhi: Reg::R0,
10042            rnlo: Reg::R2,
10043            rnhi: Reg::R3,
10044            rmlo: Reg::R4,
10045            rmhi: Reg::R5,
10046            elide_zero_guard: false,
10047        });
10048        assert!(result.is_err(), "swapped rd pair must be rejected loudly");
10049    }
10050
10051    /// #632: the I64Popcnt expansion's own scratch restore (`POP {R3,R4,R5}`)
10052    /// must not clobber the result. Pre-fix the total was materialized with
10053    /// `ADDS rd, R4, R5` BEFORE the pop, so any allocator-assigned
10054    /// rd ∈ {R3,R4,R5} received stale stack garbage. Post-fix the count is
10055    /// carried across the restore in R12 (never allocatable, never restored)
10056    /// and moved into rd only after the pop — structurally rd-independent.
10057    #[test]
10058    fn test_632_i64_popcnt_result_survives_scratch_restore() {
10059        let encoder = ArmEncoder::new_thumb2();
10060        // Every allocatable rd, including the restore set {R3,R4,R5} and R8.
10061        for rd in [
10062            Reg::R0,
10063            Reg::R2,
10064            Reg::R3,
10065            Reg::R4,
10066            Reg::R5,
10067            Reg::R6,
10068            Reg::R8,
10069        ] {
10070            let code = encoder
10071                .encode(&ArmOp::I64Popcnt {
10072                    rd,
10073                    rnlo: Reg::R6,
10074                    rnhi: Reg::R7,
10075                })
10076                .unwrap();
10077            assert_eq!(code.len(), 180, "register-independent size (estimator pin)");
10078            let hw: Vec<u16> = code
10079                .chunks(2)
10080                .map(|c| u16::from_le_bytes([c[0], c[1]]))
10081                .collect();
10082            let pop = hw
10083                .iter()
10084                .position(|&h| h == 0xBC38)
10085                .expect("POP {R3,R4,R5} present");
10086            // Immediately before the POP: ADD.W R12, R4, R5 (the total lives
10087            // in R12, which the POP cannot touch).
10088            assert_eq!(
10089                &hw[pop - 2..pop],
10090                &[0xEB04, 0x0C05],
10091                "total must be carried in R12 across the restore"
10092            );
10093            // Immediately after the POP: MOV rd, R12.
10094            let rd_bits = match rd {
10095                Reg::R8 => 8u16,
10096                Reg::R6 => 6,
10097                Reg::R5 => 5,
10098                Reg::R4 => 4,
10099                Reg::R3 => 3,
10100                Reg::R2 => 2,
10101                _ => 0,
10102            };
10103            let expect_mov = 0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7);
10104            assert_eq!(hw[pop + 1], expect_mov, "MOV rd, R12 after the restore");
10105            // No write into rd between the PUSH and the POP (the old
10106            // pre-restore ADDS is gone).
10107            assert!(
10108                !hw[..pop].contains(&(0x1800 | (5 << 6) | (4 << 3) | rd_bits)),
10109                "no ADDS rd, R4, R5 before the restore pop"
10110            );
10111        }
10112    }
10113
10114    /// #632 audit: the entry marshal must be permutation-safe. Pre-fix
10115    /// `MOV R4, rnlo; MOV R5, rnhi` read a clobbered R4 when the operand
10116    /// pair lived at (R3, R4). Post-fix rnlo routes through R12.
10117    #[test]
10118    fn test_632_i64_popcnt_marshal_pair_at_r3_r4() {
10119        let encoder = ArmEncoder::new_thumb2();
10120        let code = encoder
10121            .encode(&ArmOp::I64Popcnt {
10122                rd: Reg::R0,
10123                rnlo: Reg::R3,
10124                rnhi: Reg::R4,
10125            })
10126            .unwrap();
10127        let hw: Vec<u16> = code
10128            .chunks(2)
10129            .map(|c| u16::from_le_bytes([c[0], c[1]]))
10130            .collect();
10131        // PUSH {R3,R4,R5}; MOV R12, R3; MOV R5, R4 (rnhi read BEFORE any
10132        // write to R4); MOV R4, R12.
10133        assert_eq!(hw[0], 0xB438);
10134        assert_eq!(hw[1], 0x4600 | (1 << 7) | (3 << 3) | 4, "MOV R12, rnlo");
10135        assert_eq!(hw[2], 0x4600 | (4 << 3) | 5, "MOV R5, rnhi");
10136        assert_eq!(hw[3], 0x4664, "MOV R4, R12");
10137    }
10138
10139    /// #632: A32 twin — same structural fix on the ARM-mode path
10140    /// (`--target cortex-r5`): total carried in R12 across the restore.
10141    #[test]
10142    fn test_632_a32_i64_popcnt_result_survives_scratch_restore() {
10143        let encoder = ArmEncoder::new_arm32();
10144        for rd in [Reg::R0, Reg::R3, Reg::R4, Reg::R5, Reg::R8] {
10145            let code = encoder
10146                .encode(&ArmOp::I64Popcnt {
10147                    rd,
10148                    rnlo: Reg::R6,
10149                    rnhi: Reg::R7,
10150                })
10151                .unwrap();
10152            let words: Vec<u32> = code
10153                .chunks(4)
10154                .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
10155                .collect();
10156            let pop = words
10157                .iter()
10158                .position(|&w| w == 0xE8BD_0038)
10159                .expect("POP {R3,R4,R5} present");
10160            assert_eq!(words[pop - 1], 0xE084_C005, "ADD R12, R4, R5 before POP");
10161            let rd_bits = match rd {
10162                Reg::R8 => 8u32,
10163                Reg::R5 => 5,
10164                Reg::R4 => 4,
10165                Reg::R3 => 3,
10166                _ => 0,
10167            };
10168            assert_eq!(
10169                words[pop + 1],
10170                0xE1A0_0000 | (rd_bits << 12) | 12,
10171                "MOV rd, R12 after the restore"
10172            );
10173        }
10174    }
10175
10176    /// #633: I64DivS must carry the INT64_MIN/-1 overflow guard (mirroring
10177    /// the i32 path) right after the zero-divisor guard — dividend in R0:R1,
10178    /// divisor in R2:R3 on the #610/#613 fixed-ABI wrapper path.
10179    #[test]
10180    fn test_633_i64_divs_overflow_guard_emitted() {
10181        let encoder = ArmEncoder::new_thumb2();
10182        let code = encoder
10183            .encode(&ArmOp::I64DivS {
10184                rdlo: Reg::R4,
10185                rdhi: Reg::R5,
10186                rnlo: Reg::R0,
10187                rnhi: Reg::R1,
10188                rmlo: Reg::R2,
10189                rmhi: Reg::R3,
10190                elide_zero_guard: false,
10191                elide_overflow_guard: false,
10192            })
10193            .unwrap();
10194        // 26-byte marshal + 8-byte zero-trap, then the 22-byte overflow guard.
10195        let guard: Vec<u16> = code[34..56]
10196            .chunks(2)
10197            .map(|c| u16::from_le_bytes([c[0], c[1]]))
10198            .collect();
10199        assert_eq!(
10200            guard,
10201            vec![
10202                0xEA02, 0x0C03, // AND.W R12, R2, R3
10203                0xF11C, 0x0F01, // CMN.W R12, #1
10204                0xD105, // BNE .no_trap
10205                0x2800, // CMP R0, #0
10206                0xD103, // BNE .no_trap
10207                0xF1B1, 0x4F00, // CMP.W R1, #0x80000000
10208                0xD100, // BNE .no_trap
10209                0xDE00, // UDF #0 — signed-division overflow
10210            ],
10211            "INT64_MIN/-1 overflow guard after the zero-divisor guard"
10212        );
10213    }
10214
10215    /// #633 fix-guard twin: I64RemS must NOT carry the overflow guard —
10216    /// rem_s(INT64_MIN, -1) is defined as 0 and must not trap. Exactly one
10217    /// UDF (the zero-divisor trap) in the whole expansion.
10218    #[test]
10219    fn test_633_i64_rems_has_no_overflow_guard() {
10220        let encoder = ArmEncoder::new_thumb2();
10221        for (is_rem_s, op) in [
10222            (
10223                true,
10224                ArmOp::I64RemS {
10225                    rdlo: Reg::R4,
10226                    rdhi: Reg::R5,
10227                    rnlo: Reg::R0,
10228                    rnhi: Reg::R1,
10229                    rmlo: Reg::R2,
10230                    rmhi: Reg::R3,
10231                    elide_zero_guard: false,
10232                },
10233            ),
10234            (
10235                false,
10236                ArmOp::I64DivS {
10237                    rdlo: Reg::R4,
10238                    rdhi: Reg::R5,
10239                    rnlo: Reg::R0,
10240                    rnhi: Reg::R1,
10241                    rmlo: Reg::R2,
10242                    rmhi: Reg::R3,
10243                    elide_zero_guard: false,
10244                    elide_overflow_guard: false,
10245                },
10246            ),
10247        ] {
10248            let code = encoder.encode(&op).unwrap();
10249            let udfs = code
10250                .chunks(2)
10251                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
10252                .count();
10253            let want = if is_rem_s { 1 } else { 2 };
10254            assert_eq!(
10255                udfs, want,
10256                "rem_s: zero-trap only; div_s: zero-trap + overflow trap"
10257            );
10258        }
10259    }
10260
10261    /// #494 phase 2b: `elide_zero_guard` drops EXACTLY the 8-byte fused
10262    /// zero-trap (`ORRS.W R12,R2,R3; BNE; UDF #0`) and nothing else — the
10263    /// rest of the expansion is byte-identical (splice check).
10264    #[test]
10265    fn test_494_i64_zero_guard_elision_is_exact_splice() {
10266        let encoder = ArmEncoder::new_thumb2();
10267        let mk = |elide_zero_guard: bool| {
10268            encoder
10269                .encode(&ArmOp::I64DivU {
10270                    rdlo: Reg::R4,
10271                    rdhi: Reg::R5,
10272                    rnlo: Reg::R0,
10273                    rnhi: Reg::R1,
10274                    rmlo: Reg::R2,
10275                    rmhi: Reg::R3,
10276                    elide_zero_guard,
10277                })
10278                .unwrap()
10279        };
10280        let full = mk(false);
10281        let elided = mk(true);
10282        assert_eq!(full.len(), elided.len() + 8, "zero guard is 8 bytes");
10283        // Marshal prologue (26 B) unchanged, guard (8 B) gone, tail identical.
10284        assert_eq!(&full[..26], &elided[..26]);
10285        assert_eq!(
10286            &full[26..34],
10287            &[0x52, 0xEA, 0x03, 0x0C, 0x00, 0xD1, 0x00, 0xDE],
10288            "the spliced-out bytes are exactly ORRS.W; BNE; UDF #0"
10289        );
10290        assert_eq!(&full[34..], &elided[26..]);
10291    }
10292
10293    /// #494 phase 2b two-guard distinction (the #633/#634 synergy): a
10294    /// divisor-nonzero fact elides ONLY the zero guard — the INT64_MIN/-1
10295    /// OVERFLOW guard is a separate obligation and must survive
10296    /// `elide_zero_guard: true`. Pinned on div_s in all flag states.
10297    #[test]
10298    fn test_494_i64_divs_overflow_guard_retained_when_only_zero_elided() {
10299        let encoder = ArmEncoder::new_thumb2();
10300        let mk = |zero: bool, ovf: bool| {
10301            encoder
10302                .encode(&ArmOp::I64DivS {
10303                    rdlo: Reg::R4,
10304                    rdhi: Reg::R5,
10305                    rnlo: Reg::R0,
10306                    rnhi: Reg::R1,
10307                    rmlo: Reg::R2,
10308                    rmhi: Reg::R3,
10309                    elide_zero_guard: zero,
10310                    elide_overflow_guard: ovf,
10311                })
10312                .unwrap()
10313        };
10314        let udf_count = |code: &[u8]| {
10315            code.chunks(2)
10316                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
10317                .count()
10318        };
10319        let full = mk(false, false);
10320        let zero_only = mk(true, false);
10321        let both = mk(true, true);
10322        assert_eq!(udf_count(&full), 2, "baseline: zero trap + overflow trap");
10323        assert_eq!(
10324            udf_count(&zero_only),
10325            1,
10326            "divisor-nonzero elides the zero trap ONLY — the #633 overflow \
10327             guard must be retained"
10328        );
10329        // The retained guard is the 22-byte overflow sequence, now right
10330        // after the 26-byte marshal prologue.
10331        let guard: Vec<u16> = zero_only[26..48]
10332            .chunks(2)
10333            .map(|c| u16::from_le_bytes([c[0], c[1]]))
10334            .collect();
10335        assert_eq!(
10336            guard,
10337            vec![
10338                0xEA02, 0x0C03, 0xF11C, 0x0F01, 0xD105, 0x2800, 0xD103, 0xF1B1, 0x4F00, 0xD100,
10339                0xDE00,
10340            ],
10341            "the surviving guard is the INT64_MIN/-1 overflow trap"
10342        );
10343        assert_eq!(full.len(), zero_only.len() + 8);
10344        assert_eq!(zero_only.len(), both.len() + 22);
10345        assert_eq!(udf_count(&both), 0, "both obligations discharged ⇒ no UDF");
10346    }
10347
10348    /// #494 phase 2b A32 twin: zero-guard elision is an exact 12-byte splice
10349    /// and the A32 overflow guard survives a zero-only elision.
10350    #[test]
10351    fn test_494_a32_i64_guard_elision() {
10352        let encoder = ArmEncoder::new_arm32();
10353        let mk = |zero: bool, ovf: bool| {
10354            encoder
10355                .encode(&ArmOp::I64DivS {
10356                    rdlo: Reg::R4,
10357                    rdhi: Reg::R5,
10358                    rnlo: Reg::R0,
10359                    rnhi: Reg::R1,
10360                    rmlo: Reg::R2,
10361                    rmhi: Reg::R3,
10362                    elide_zero_guard: zero,
10363                    elide_overflow_guard: ovf,
10364                })
10365                .unwrap()
10366        };
10367        let full = mk(false, false);
10368        let zero_only = mk(true, false);
10369        let both = mk(true, true);
10370        // A32 zero guard = 3 words (ORRS/BNE/UDF), overflow guard = 6 words.
10371        assert_eq!(full.len(), zero_only.len() + 12);
10372        assert_eq!(zero_only.len(), both.len() + 24);
10373        let udf_count = |code: &[u8]| {
10374            code.chunks(4)
10375                .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
10376                .count()
10377        };
10378        assert_eq!(udf_count(&full), 2);
10379        assert_eq!(
10380            udf_count(&zero_only),
10381            1,
10382            "A32: overflow guard retained under zero-only elision"
10383        );
10384        assert_eq!(udf_count(&both), 0);
10385    }
10386
10387    /// #633: A32 twin — the conditional-execution overflow guard on the
10388    /// ARM-mode I64DivS, and its absence from I64RemS.
10389    #[test]
10390    fn test_633_a32_i64_divs_overflow_guard() {
10391        let encoder = ArmEncoder::new_arm32();
10392        let mk_divs = ArmOp::I64DivS {
10393            rdlo: Reg::R4,
10394            rdhi: Reg::R5,
10395            rnlo: Reg::R0,
10396            rnhi: Reg::R1,
10397            rmlo: Reg::R2,
10398            rmhi: Reg::R3,
10399            elide_zero_guard: false,
10400            elide_overflow_guard: false,
10401        };
10402        let code = encoder.encode(&mk_divs).unwrap();
10403        let words: Vec<u32> = code
10404            .chunks(4)
10405            .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
10406            .collect();
10407        let guard = [
10408            0xE002_C003u32, // AND   R12, R2, R3
10409            0xE37C_0001,    // CMN   R12, #1
10410            0x0350_0000,    // CMPEQ R0, #0
10411            0x0351_0102,    // CMPEQ R1, #0x80000000
10412            0x1A00_0000,    // BNE +1 insn
10413            0xE7F0_00F0,    // UDF #0
10414        ];
10415        assert!(
10416            words.windows(6).any(|w| w == guard),
10417            "A32 I64DivS carries the INT64_MIN/-1 overflow guard"
10418        );
10419        let rems = encoder
10420            .encode(&ArmOp::I64RemS {
10421                rdlo: Reg::R4,
10422                rdhi: Reg::R5,
10423                rnlo: Reg::R0,
10424                rnhi: Reg::R1,
10425                rmlo: Reg::R2,
10426                rmhi: Reg::R3,
10427                elide_zero_guard: false,
10428            })
10429            .unwrap();
10430        let rems_udfs = rems
10431            .chunks(4)
10432            .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
10433            .count();
10434        assert_eq!(rems_udfs, 1, "A32 I64RemS keeps only the zero-divisor trap");
10435    }
10436
10437    #[test]
10438    fn test_encode_nop_thumb2() {
10439        let encoder = ArmEncoder::new_thumb2();
10440        let op = ArmOp::Nop;
10441        let code = encoder.encode(&op).unwrap();
10442        assert_eq!(code.len(), 2); // 16-bit
10443
10444        // NOP: 0xBF00 in little-endian
10445        assert_eq!(code, vec![0x00, 0xBF]);
10446    }
10447
10448    // =========================================================================
10449    // i64 Thumb-2 encoding tests
10450    // =========================================================================
10451
10452    #[test]
10453    fn test_encode_i64_add_thumb2() {
10454        let encoder = ArmEncoder::new_thumb2();
10455        let op = ArmOp::I64Add {
10456            rdlo: Reg::R0,
10457            rdhi: Reg::R1,
10458            rnlo: Reg::R0,
10459            rnhi: Reg::R1,
10460            rmlo: Reg::R2,
10461            rmhi: Reg::R3,
10462        };
10463        let code = encoder.encode(&op).unwrap();
10464        // Should emit ADDS (2 bytes) + ADC.W (4 bytes) = 6 bytes
10465        assert_eq!(code.len(), 6, "I64Add should be 6 bytes (ADDS + ADC.W)");
10466    }
10467
10468    #[test]
10469    fn test_encode_i64_sub_thumb2() {
10470        let encoder = ArmEncoder::new_thumb2();
10471        let op = ArmOp::I64Sub {
10472            rdlo: Reg::R0,
10473            rdhi: Reg::R1,
10474            rnlo: Reg::R0,
10475            rnhi: Reg::R1,
10476            rmlo: Reg::R2,
10477            rmhi: Reg::R3,
10478        };
10479        let code = encoder.encode(&op).unwrap();
10480        // Should emit SUBS (2 bytes) + SBC.W (4 bytes) = 6 bytes
10481        assert_eq!(code.len(), 6, "I64Sub should be 6 bytes (SUBS + SBC.W)");
10482    }
10483
10484    #[test]
10485    fn test_encode_i64_and_thumb2() {
10486        let encoder = ArmEncoder::new_thumb2();
10487        let op = ArmOp::I64And {
10488            rdlo: Reg::R0,
10489            rdhi: Reg::R1,
10490            rnlo: Reg::R0,
10491            rnhi: Reg::R1,
10492            rmlo: Reg::R2,
10493            rmhi: Reg::R3,
10494        };
10495        let code = encoder.encode(&op).unwrap();
10496        // AND.W (4 bytes) + AND.W (4 bytes) = 8 bytes
10497        assert!(code.len() >= 4, "I64And should emit at least 4 bytes");
10498    }
10499
10500    #[test]
10501    fn test_encode_i64_or_thumb2() {
10502        let encoder = ArmEncoder::new_thumb2();
10503        let op = ArmOp::I64Or {
10504            rdlo: Reg::R0,
10505            rdhi: Reg::R1,
10506            rnlo: Reg::R0,
10507            rnhi: Reg::R1,
10508            rmlo: Reg::R2,
10509            rmhi: Reg::R3,
10510        };
10511        let code = encoder.encode(&op).unwrap();
10512        assert!(code.len() >= 4, "I64Or should emit at least 4 bytes");
10513    }
10514
10515    #[test]
10516    fn test_encode_i64_xor_thumb2() {
10517        let encoder = ArmEncoder::new_thumb2();
10518        let op = ArmOp::I64Xor {
10519            rdlo: Reg::R0,
10520            rdhi: Reg::R1,
10521            rnlo: Reg::R0,
10522            rnhi: Reg::R1,
10523            rmlo: Reg::R2,
10524            rmhi: Reg::R3,
10525        };
10526        let code = encoder.encode(&op).unwrap();
10527        assert!(code.len() >= 4, "I64Xor should emit at least 4 bytes");
10528    }
10529
10530    #[test]
10531    fn test_encode_i64_const_small_thumb2() {
10532        let encoder = ArmEncoder::new_thumb2();
10533        // Small constant: only needs MOVW for each half
10534        let op = ArmOp::I64Const {
10535            rdlo: Reg::R0,
10536            rdhi: Reg::R1,
10537            value: 42,
10538        };
10539        let code = encoder.encode(&op).unwrap();
10540        // MOVW R0, #42 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes minimum
10541        assert!(code.len() >= 8, "I64Const should emit at least 8 bytes");
10542    }
10543
10544    #[test]
10545    fn test_encode_i64_const_large_thumb2() {
10546        let encoder = ArmEncoder::new_thumb2();
10547        // Large constant: needs MOVW+MOVT for each half
10548        let op = ArmOp::I64Const {
10549            rdlo: Reg::R0,
10550            rdhi: Reg::R1,
10551            value: 0x1234_5678_9ABC_DEF0_u64 as i64,
10552        };
10553        let code = encoder.encode(&op).unwrap();
10554        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
10555        assert_eq!(
10556            code.len(),
10557            16,
10558            "I64Const with large value should be 16 bytes"
10559        );
10560    }
10561
10562    #[test]
10563    fn test_encode_i64_extend_i32_s_thumb2() {
10564        let encoder = ArmEncoder::new_thumb2();
10565        let op = ArmOp::I64ExtendI32S {
10566            rdlo: Reg::R0,
10567            rdhi: Reg::R1,
10568            rn: Reg::R0,
10569        };
10570        let code = encoder.encode(&op).unwrap();
10571        // When rdlo == rn, only ASR (4 bytes) is emitted
10572        assert_eq!(
10573            code.len(),
10574            4,
10575            "I64ExtendI32S (same reg) should be 4 bytes (ASR only)"
10576        );
10577    }
10578
10579    #[test]
10580    fn test_encode_i64_extend_i32_s_diff_reg_thumb2() {
10581        let encoder = ArmEncoder::new_thumb2();
10582        let op = ArmOp::I64ExtendI32S {
10583            rdlo: Reg::R0,
10584            rdhi: Reg::R1,
10585            rn: Reg::R2,
10586        };
10587        let code = encoder.encode(&op).unwrap();
10588        // MOV rdlo, rn (2 bytes for low regs) + ASR rdhi, rdlo, #31 (4 bytes) = 6 bytes
10589        assert!(
10590            code.len() >= 6,
10591            "I64ExtendI32S (diff reg) should be at least 6 bytes"
10592        );
10593    }
10594
10595    #[test]
10596    fn test_encode_i64_extend_i32_u_thumb2() {
10597        let encoder = ArmEncoder::new_thumb2();
10598        let op = ArmOp::I64ExtendI32U {
10599            rdlo: Reg::R0,
10600            rdhi: Reg::R1,
10601            rn: Reg::R0,
10602        };
10603        let code = encoder.encode(&op).unwrap();
10604        // When rdlo == rn, only MOV rdhi, #0 (2 bytes) is emitted
10605        assert_eq!(
10606            code.len(),
10607            2,
10608            "I64ExtendI32U (same reg) should be 2 bytes (MOV #0 only)"
10609        );
10610    }
10611
10612    #[test]
10613    fn test_encode_i32_wrap_i64_nop_thumb2() {
10614        let encoder = ArmEncoder::new_thumb2();
10615        // When rd == rnlo, should be a NOP
10616        let op = ArmOp::I32WrapI64 {
10617            rd: Reg::R0,
10618            rnlo: Reg::R0,
10619        };
10620        let code = encoder.encode(&op).unwrap();
10621        assert_eq!(code.len(), 2, "I32WrapI64 same reg should be NOP (2 bytes)");
10622        assert_eq!(code, vec![0x00, 0xBF]); // NOP
10623    }
10624
10625    #[test]
10626    fn test_encode_i32_wrap_i64_diff_reg_thumb2() {
10627        let encoder = ArmEncoder::new_thumb2();
10628        let op = ArmOp::I32WrapI64 {
10629            rd: Reg::R2,
10630            rnlo: Reg::R0,
10631        };
10632        let code = encoder.encode(&op).unwrap();
10633        // MOV R2, R0 (2 or 4 bytes)
10634        assert!(
10635            code.len() >= 2,
10636            "I32WrapI64 diff reg should emit at least 2 bytes"
10637        );
10638    }
10639
10640    #[test]
10641    fn test_encode_i64_eqz_thumb2() {
10642        let encoder = ArmEncoder::new_thumb2();
10643        let op = ArmOp::I64Eqz {
10644            rd: Reg::R0,
10645            rnlo: Reg::R0,
10646            rnhi: Reg::R1,
10647        };
10648        let code = encoder.encode(&op).unwrap();
10649        // Delegates to I64SetCondZ which is already encoded
10650        assert!(
10651            code.len() >= 6,
10652            "I64Eqz should emit at least 6 bytes for ORR+ITE+MOV+MOV"
10653        );
10654    }
10655
10656    #[test]
10657    fn test_encode_i64_eq_thumb2() {
10658        let encoder = ArmEncoder::new_thumb2();
10659        let op = ArmOp::I64Eq {
10660            rd: Reg::R0,
10661            rnlo: Reg::R0,
10662            rnhi: Reg::R1,
10663            rmlo: Reg::R2,
10664            rmhi: Reg::R3,
10665        };
10666        let code = encoder.encode(&op).unwrap();
10667        // Delegates to I64SetCond EQ: CMP lo + IT EQ + CMPEQ hi + ITE EQ + MOV 1 + MOV 0
10668        assert!(code.len() >= 10, "I64Eq should emit at least 10 bytes");
10669    }
10670
10671    #[test]
10672    fn test_encode_i64_ldr_thumb2() {
10673        let encoder = ArmEncoder::new_thumb2();
10674        let op = ArmOp::I64Ldr {
10675            rdlo: Reg::R0,
10676            rdhi: Reg::R1,
10677            addr: MemAddr::imm(Reg::SP, 0),
10678        };
10679        let code = encoder.encode(&op).unwrap();
10680        // Two LDR instructions (lo at offset, hi at offset+4)
10681        assert!(code.len() >= 4, "I64Ldr should emit at least 4 bytes");
10682    }
10683
10684    #[test]
10685    fn test_372_i64_ldr_indexed_materializes_address() {
10686        // #372: a memory i64.load carries an index register (R11 + addr + off).
10687        // The encoder must materialize `ip = base + index` (ADD.W) and load via
10688        // `[ip,#off]` — NOT drop the index. A frame (non-indexed) i64.load must
10689        // stay byte-identical (plain `[base,#off]`, no ADD).
10690        let encoder = ArmEncoder::new_thumb2();
10691        let indexed = encoder
10692            .encode(&ArmOp::I64Ldr {
10693                rdlo: Reg::R0,
10694                rdhi: Reg::R1,
10695                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
10696            })
10697            .unwrap();
10698        // ADD.W ip, fp, r0 = eb0b 0c00 (byte-verified vs arm-none-eabi-as).
10699        assert_eq!(
10700            &indexed[0..4],
10701            &[0x0b, 0xeb, 0x00, 0x0c],
10702            "indexed I64Ldr must start with ADD.W ip, base, index"
10703        );
10704        let frame = encoder
10705            .encode(&ArmOp::I64Ldr {
10706                rdlo: Reg::R0,
10707                rdhi: Reg::R1,
10708                addr: MemAddr::imm(Reg::SP, 8),
10709            })
10710            .unwrap();
10711        // No index -> no ADD.W prefix (byte-identical frame access).
10712        assert_ne!(
10713            &frame[0..2],
10714            &[0x0b, 0xeb],
10715            "frame (non-indexed) I64Ldr must NOT emit an ADD.W"
10716        );
10717    }
10718
10719    #[test]
10720    fn test_382_i64_ldst_large_offset_materializes_not_skips() {
10721        // #382: an indexed i64.load/store whose static offset > 0xFFF must
10722        // MATERIALIZE the offset into the base — NOT return Err (skip the fn).
10723        // Sequence for reg_imm(R11, R0, 5000): MOVW ip,#5000 ; ADD ip,r0,ip ;
10724        // ADD ip,ip,fp ; LDR/STR halves at [ip,#0] / [ip,#4]. Byte-verified tail
10725        // vs arm-none-eabi-as.
10726        let encoder = ArmEncoder::new_thumb2();
10727        // 0x1388 > 0xFFF (MemAddr is not Copy, so build it per use).
10728
10729        let ld = encoder
10730            .encode(&ArmOp::I64Ldr {
10731                rdlo: Reg::R0,
10732                rdhi: Reg::R1,
10733                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
10734            })
10735            .expect("large-offset i64.load must lower, not skip");
10736        // MOVW ip,#0x1388 (4) + ADD ip,r0,ip (4) + ADD ip,ip,fp (4) + 2 LDR (8).
10737        assert_eq!(ld.len(), 20, "expected MOVW + 2×ADD + 2×LDR");
10738        // Must NOT be the small-offset `ADD.W ip, fp, r0` (0x0b 0xeb) prefix —
10739        // that path can only reach imm12 offsets.
10740        assert_ne!(
10741            &ld[0..2],
10742            &[0x0b, 0xeb],
10743            "must materialize the large offset"
10744        );
10745        // Effective base built in ip, then halves at [ip,#0] / [ip,#4].
10746        assert_eq!(
10747            &ld[4..20],
10748            &[
10749                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
10750                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
10751                0xdc, 0xf8, 0x00, 0x00, // LDR.W r0, [ip, #0]
10752                0xdc, 0xf8, 0x04, 0x10, // LDR.W r1, [ip, #4]
10753            ],
10754            "large-offset i64.load must fold offset into ip and access [ip,#0]/[ip,#4]"
10755        );
10756
10757        // Store: same base materialization, STR halves.
10758        let st = encoder
10759            .encode(&ArmOp::I64Str {
10760                rdlo: Reg::R2,
10761                rdhi: Reg::R3,
10762                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
10763            })
10764            .expect("large-offset i64.store must lower, not skip");
10765        assert_eq!(st.len(), 20);
10766        assert_eq!(
10767            &st[4..20],
10768            &[
10769                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
10770                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
10771                0xcc, 0xf8, 0x00, 0x20, // STR.W r2, [ip, #0]
10772                0xcc, 0xf8, 0x04, 0x30, // STR.W r3, [ip, #4]
10773            ],
10774            "large-offset i64.store must fold offset into ip and access [ip,#0]/[ip,#4]"
10775        );
10776
10777        // Small-offset (imm12) indexed access stays byte-identical (#372): the
10778        // effective base is a single `ADD.W ip, fp, r0` and the halves keep the
10779        // folded immediates — NO extra MOVW/ADD.
10780        let small = encoder
10781            .encode(&ArmOp::I64Ldr {
10782                rdlo: Reg::R0,
10783                rdhi: Reg::R1,
10784                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 8),
10785            })
10786            .unwrap();
10787        assert_eq!(
10788            &small[0..4],
10789            &[0x0b, 0xeb, 0x00, 0x0c],
10790            "small-offset indexed i64 must keep the single ADD.W ip, fp, r0"
10791        );
10792        assert_eq!(small.len(), 12, "ADD.W + 2×LDR.W (offset folded in imm12)");
10793    }
10794
10795    #[test]
10796    fn test_encode_i64_str_thumb2() {
10797        let encoder = ArmEncoder::new_thumb2();
10798        let op = ArmOp::I64Str {
10799            rdlo: Reg::R0,
10800            rdhi: Reg::R1,
10801            addr: MemAddr::imm(Reg::SP, 0),
10802        };
10803        let code = encoder.encode(&op).unwrap();
10804        // Two STR instructions (lo at offset, hi at offset+4)
10805        assert!(code.len() >= 4, "I64Str should emit at least 4 bytes");
10806    }
10807
10808    #[test]
10809    fn test_encode_i64_all_comparisons_thumb2() {
10810        let encoder = ArmEncoder::new_thumb2();
10811
10812        let ops = vec![
10813            ArmOp::I64Ne {
10814                rd: Reg::R0,
10815                rnlo: Reg::R0,
10816                rnhi: Reg::R1,
10817                rmlo: Reg::R2,
10818                rmhi: Reg::R3,
10819            },
10820            ArmOp::I64LtS {
10821                rd: Reg::R0,
10822                rnlo: Reg::R0,
10823                rnhi: Reg::R1,
10824                rmlo: Reg::R2,
10825                rmhi: Reg::R3,
10826            },
10827            ArmOp::I64LtU {
10828                rd: Reg::R0,
10829                rnlo: Reg::R0,
10830                rnhi: Reg::R1,
10831                rmlo: Reg::R2,
10832                rmhi: Reg::R3,
10833            },
10834            ArmOp::I64LeS {
10835                rd: Reg::R0,
10836                rnlo: Reg::R0,
10837                rnhi: Reg::R1,
10838                rmlo: Reg::R2,
10839                rmhi: Reg::R3,
10840            },
10841            ArmOp::I64LeU {
10842                rd: Reg::R0,
10843                rnlo: Reg::R0,
10844                rnhi: Reg::R1,
10845                rmlo: Reg::R2,
10846                rmhi: Reg::R3,
10847            },
10848            ArmOp::I64GtS {
10849                rd: Reg::R0,
10850                rnlo: Reg::R0,
10851                rnhi: Reg::R1,
10852                rmlo: Reg::R2,
10853                rmhi: Reg::R3,
10854            },
10855            ArmOp::I64GtU {
10856                rd: Reg::R0,
10857                rnlo: Reg::R0,
10858                rnhi: Reg::R1,
10859                rmlo: Reg::R2,
10860                rmhi: Reg::R3,
10861            },
10862            ArmOp::I64GeS {
10863                rd: Reg::R0,
10864                rnlo: Reg::R0,
10865                rnhi: Reg::R1,
10866                rmlo: Reg::R2,
10867                rmhi: Reg::R3,
10868            },
10869            ArmOp::I64GeU {
10870                rd: Reg::R0,
10871                rnlo: Reg::R0,
10872                rnhi: Reg::R1,
10873                rmlo: Reg::R2,
10874                rmhi: Reg::R3,
10875            },
10876        ];
10877
10878        for op in &ops {
10879            let code = encoder.encode(op).unwrap();
10880            assert!(
10881                code.len() >= 8,
10882                "i64 comparison {:?} should emit at least 8 bytes, got {}",
10883                op,
10884                code.len()
10885            );
10886        }
10887    }
10888
10889    #[test]
10890    fn test_encode_i64_const_zero_thumb2() {
10891        let encoder = ArmEncoder::new_thumb2();
10892        let op = ArmOp::I64Const {
10893            rdlo: Reg::R0,
10894            rdhi: Reg::R1,
10895            value: 0,
10896        };
10897        let code = encoder.encode(&op).unwrap();
10898        // MOVW R0, #0 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes
10899        assert_eq!(code.len(), 8, "I64Const(0) should be 8 bytes");
10900    }
10901
10902    #[test]
10903    fn test_encode_i64_const_negative_one_thumb2() {
10904        let encoder = ArmEncoder::new_thumb2();
10905        let op = ArmOp::I64Const {
10906            rdlo: Reg::R0,
10907            rdhi: Reg::R1,
10908            value: -1, // 0xFFFF_FFFF_FFFF_FFFF
10909        };
10910        let code = encoder.encode(&op).unwrap();
10911        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
10912        assert_eq!(code.len(), 16, "I64Const(-1) should be 16 bytes");
10913    }
10914
10915    // =========================================================================
10916    // Sub-word load/store encoding tests
10917    // =========================================================================
10918
10919    #[test]
10920    fn test_encode_ldrb_arm32() {
10921        let encoder = ArmEncoder::new_arm32();
10922        let op = ArmOp::Ldrb {
10923            rd: Reg::R0,
10924            addr: MemAddr::imm(Reg::R1, 4),
10925        };
10926        let code = encoder.encode(&op).unwrap();
10927        assert_eq!(code.len(), 4, "ARM32 LDRB should be 4 bytes");
10928        // LDRB R0, [R1, #4] = 0xE5D10004
10929        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10930        assert_eq!(encoded, 0xE5D10004, "Should encode LDRB R0, [R1, #4]");
10931    }
10932
10933    #[test]
10934    fn test_encode_strb_arm32() {
10935        let encoder = ArmEncoder::new_arm32();
10936        let op = ArmOp::Strb {
10937            rd: Reg::R0,
10938            addr: MemAddr::imm(Reg::R1, 0),
10939        };
10940        let code = encoder.encode(&op).unwrap();
10941        assert_eq!(code.len(), 4, "ARM32 STRB should be 4 bytes");
10942        // STRB R0, [R1, #0] = 0xE5C10000
10943        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10944        assert_eq!(encoded, 0xE5C10000, "Should encode STRB R0, [R1, #0]");
10945    }
10946
10947    #[test]
10948    fn test_encode_ldrh_arm32() {
10949        let encoder = ArmEncoder::new_arm32();
10950        let op = ArmOp::Ldrh {
10951            rd: Reg::R0,
10952            addr: MemAddr::imm(Reg::R1, 2),
10953        };
10954        let code = encoder.encode(&op).unwrap();
10955        assert_eq!(code.len(), 4, "ARM32 LDRH should be 4 bytes");
10956    }
10957
10958    #[test]
10959    fn test_encode_strh_arm32() {
10960        let encoder = ArmEncoder::new_arm32();
10961        let op = ArmOp::Strh {
10962            rd: Reg::R0,
10963            addr: MemAddr::imm(Reg::R1, 0),
10964        };
10965        let code = encoder.encode(&op).unwrap();
10966        assert_eq!(code.len(), 4, "ARM32 STRH should be 4 bytes");
10967    }
10968
10969    #[test]
10970    fn test_encode_ldrsb_arm32() {
10971        let encoder = ArmEncoder::new_arm32();
10972        let op = ArmOp::Ldrsb {
10973            rd: Reg::R0,
10974            addr: MemAddr::imm(Reg::R1, 0),
10975        };
10976        let code = encoder.encode(&op).unwrap();
10977        assert_eq!(code.len(), 4, "ARM32 LDRSB should be 4 bytes");
10978    }
10979
10980    #[test]
10981    fn test_encode_ldrsh_arm32() {
10982        let encoder = ArmEncoder::new_arm32();
10983        let op = ArmOp::Ldrsh {
10984            rd: Reg::R0,
10985            addr: MemAddr::imm(Reg::R1, 0),
10986        };
10987        let code = encoder.encode(&op).unwrap();
10988        assert_eq!(code.len(), 4, "ARM32 LDRSH should be 4 bytes");
10989    }
10990
10991    #[test]
10992    fn test_encode_ldrb_thumb2_16bit() {
10993        let encoder = ArmEncoder::new_thumb2();
10994        let op = ArmOp::Ldrb {
10995            rd: Reg::R0,
10996            addr: MemAddr::imm(Reg::R1, 4),
10997        };
10998        let code = encoder.encode(&op).unwrap();
10999        // Low registers + small offset -> 16-bit encoding
11000        assert_eq!(
11001            code.len(),
11002            2,
11003            "Thumb-2 LDRB with small offset should be 16-bit"
11004        );
11005    }
11006
11007    #[test]
11008    fn test_encode_ldrb_thumb2_32bit() {
11009        let encoder = ArmEncoder::new_thumb2();
11010        let op = ArmOp::Ldrb {
11011            rd: Reg::R0,
11012            addr: MemAddr::imm(Reg::R1, 100), // offset > 31 needs 32-bit
11013        };
11014        let code = encoder.encode(&op).unwrap();
11015        assert_eq!(
11016            code.len(),
11017            4,
11018            "Thumb-2 LDRB with large offset should be 32-bit"
11019        );
11020    }
11021
11022    #[test]
11023    fn test_encode_strb_thumb2_16bit() {
11024        let encoder = ArmEncoder::new_thumb2();
11025        let op = ArmOp::Strb {
11026            rd: Reg::R0,
11027            addr: MemAddr::imm(Reg::R1, 10),
11028        };
11029        let code = encoder.encode(&op).unwrap();
11030        assert_eq!(
11031            code.len(),
11032            2,
11033            "Thumb-2 STRB with small offset should be 16-bit"
11034        );
11035    }
11036
11037    #[test]
11038    fn test_encode_ldrh_thumb2_16bit() {
11039        let encoder = ArmEncoder::new_thumb2();
11040        let op = ArmOp::Ldrh {
11041            rd: Reg::R0,
11042            addr: MemAddr::imm(Reg::R1, 4), // offset aligned to 2, <= 62
11043        };
11044        let code = encoder.encode(&op).unwrap();
11045        assert_eq!(
11046            code.len(),
11047            2,
11048            "Thumb-2 LDRH with small aligned offset should be 16-bit"
11049        );
11050    }
11051
11052    #[test]
11053    fn test_encode_strh_thumb2_16bit() {
11054        let encoder = ArmEncoder::new_thumb2();
11055        let op = ArmOp::Strh {
11056            rd: Reg::R0,
11057            addr: MemAddr::imm(Reg::R1, 4),
11058        };
11059        let code = encoder.encode(&op).unwrap();
11060        assert_eq!(
11061            code.len(),
11062            2,
11063            "Thumb-2 STRH with small aligned offset should be 16-bit"
11064        );
11065    }
11066
11067    #[test]
11068    fn test_encode_ldrsb_thumb2() {
11069        let encoder = ArmEncoder::new_thumb2();
11070        let op = ArmOp::Ldrsb {
11071            rd: Reg::R0,
11072            addr: MemAddr::imm(Reg::R1, 0),
11073        };
11074        let code = encoder.encode(&op).unwrap();
11075        // LDRSB has no 16-bit immediate form, always 32-bit
11076        assert_eq!(code.len(), 4, "Thumb-2 LDRSB should be 32-bit");
11077    }
11078
11079    #[test]
11080    fn test_encode_ldrsh_thumb2() {
11081        let encoder = ArmEncoder::new_thumb2();
11082        let op = ArmOp::Ldrsh {
11083            rd: Reg::R0,
11084            addr: MemAddr::imm(Reg::R1, 0),
11085        };
11086        let code = encoder.encode(&op).unwrap();
11087        assert_eq!(code.len(), 4, "Thumb-2 LDRSH should be 32-bit");
11088    }
11089
11090    #[test]
11091    fn test_encode_memory_size_thumb2() {
11092        let encoder = ArmEncoder::new_thumb2();
11093        let op = ArmOp::MemorySize { rd: Reg::R0 };
11094        let code = encoder.encode(&op).unwrap();
11095        // R0 and R10 are not both low registers, so this needs careful handling
11096        assert!(!code.is_empty(), "MemorySize should produce code");
11097    }
11098
11099    #[test]
11100    fn test_encode_memory_grow_thumb2() {
11101        let encoder = ArmEncoder::new_thumb2();
11102        let op = ArmOp::MemoryGrow {
11103            rd: Reg::R0,
11104            rn: Reg::R0,
11105        };
11106        let code = encoder.encode(&op).unwrap();
11107        assert_eq!(code.len(), 4, "MemoryGrow (MVN) should be 32-bit Thumb-2");
11108    }
11109
11110    #[test]
11111    fn test_encode_subword_reg_offset_thumb2() {
11112        let encoder = ArmEncoder::new_thumb2();
11113
11114        // LDRB with register offset
11115        let op = ArmOp::Ldrb {
11116            rd: Reg::R0,
11117            addr: MemAddr::reg(Reg::R1, Reg::R2),
11118        };
11119        let code = encoder.encode(&op).unwrap();
11120        assert_eq!(
11121            code.len(),
11122            4,
11123            "Thumb-2 LDRB with reg offset should be 32-bit"
11124        );
11125
11126        // STRB with register offset
11127        let op = ArmOp::Strb {
11128            rd: Reg::R0,
11129            addr: MemAddr::reg(Reg::R1, Reg::R2),
11130        };
11131        let code = encoder.encode(&op).unwrap();
11132        assert_eq!(
11133            code.len(),
11134            4,
11135            "Thumb-2 STRB with reg offset should be 32-bit"
11136        );
11137
11138        // LDRH with register offset
11139        let op = ArmOp::Ldrh {
11140            rd: Reg::R0,
11141            addr: MemAddr::reg(Reg::R1, Reg::R2),
11142        };
11143        let code = encoder.encode(&op).unwrap();
11144        assert_eq!(
11145            code.len(),
11146            4,
11147            "Thumb-2 LDRH with reg offset should be 32-bit"
11148        );
11149
11150        // STRH with register offset
11151        let op = ArmOp::Strh {
11152            rd: Reg::R0,
11153            addr: MemAddr::reg(Reg::R1, Reg::R2),
11154        };
11155        let code = encoder.encode(&op).unwrap();
11156        assert_eq!(
11157            code.len(),
11158            4,
11159            "Thumb-2 STRH with reg offset should be 32-bit"
11160        );
11161    }
11162
11163    #[test]
11164    fn test_encode_subword_reg_imm_offset_thumb2() {
11165        let encoder = ArmEncoder::new_thumb2();
11166
11167        // LDRB with both register and immediate offset
11168        let op = ArmOp::Ldrb {
11169            rd: Reg::R0,
11170            addr: MemAddr::reg_imm(Reg::R1, Reg::R2, 4),
11171        };
11172        let code = encoder.encode(&op).unwrap();
11173        // ADD R12, R2, #4 (4 bytes) + LDRB R0, [R1, R12] (4 bytes) = 8 bytes
11174        assert_eq!(
11175            code.len(),
11176            8,
11177            "Thumb-2 LDRB with reg+imm offset should be 8 bytes"
11178        );
11179    }
11180
11181    // ========================================================================
11182    // Helium MVE encoding tests
11183    // ========================================================================
11184
11185    #[test]
11186    fn test_encode_mve_addi32_thumb2() {
11187        let encoder = ArmEncoder::new_thumb2();
11188        let op = ArmOp::MveAddI {
11189            qd: QReg::Q0,
11190            qn: QReg::Q1,
11191            qm: QReg::Q2,
11192            size: MveSize::S32,
11193        };
11194        let code = encoder.encode(&op).unwrap();
11195        assert_eq!(
11196            code.len(),
11197            4,
11198            "MVE VADD.I32 should be 4 bytes (Thumb-2 32-bit)"
11199        );
11200    }
11201
11202    #[test]
11203    fn test_encode_mve_subi16_thumb2() {
11204        let encoder = ArmEncoder::new_thumb2();
11205        let op = ArmOp::MveSubI {
11206            qd: QReg::Q0,
11207            qn: QReg::Q1,
11208            qm: QReg::Q2,
11209            size: MveSize::S16,
11210        };
11211        let code = encoder.encode(&op).unwrap();
11212        assert_eq!(code.len(), 4, "MVE VSUB.I16 should be 4 bytes");
11213    }
11214
11215    #[test]
11216    fn test_encode_mve_muli8_thumb2() {
11217        let encoder = ArmEncoder::new_thumb2();
11218        let op = ArmOp::MveMulI {
11219            qd: QReg::Q0,
11220            qn: QReg::Q1,
11221            qm: QReg::Q2,
11222            size: MveSize::S8,
11223        };
11224        let code = encoder.encode(&op).unwrap();
11225        assert_eq!(code.len(), 4, "MVE VMUL.I8 should be 4 bytes");
11226    }
11227
11228    #[test]
11229    fn test_encode_mve_bitwise_thumb2() {
11230        let encoder = ArmEncoder::new_thumb2();
11231
11232        let ops = vec![
11233            ArmOp::MveAnd {
11234                qd: QReg::Q0,
11235                qn: QReg::Q1,
11236                qm: QReg::Q2,
11237            },
11238            ArmOp::MveOrr {
11239                qd: QReg::Q0,
11240                qn: QReg::Q1,
11241                qm: QReg::Q2,
11242            },
11243            ArmOp::MveEor {
11244                qd: QReg::Q0,
11245                qn: QReg::Q1,
11246                qm: QReg::Q2,
11247            },
11248            ArmOp::MveBic {
11249                qd: QReg::Q0,
11250                qn: QReg::Q1,
11251                qm: QReg::Q2,
11252            },
11253        ];
11254        for op in ops {
11255            let code = encoder.encode(&op).unwrap();
11256            assert_eq!(code.len(), 4, "MVE bitwise op should be 4 bytes");
11257        }
11258    }
11259
11260    #[test]
11261    fn test_encode_mve_mvn_thumb2() {
11262        let encoder = ArmEncoder::new_thumb2();
11263        let op = ArmOp::MveMvn {
11264            qd: QReg::Q0,
11265            qm: QReg::Q1,
11266        };
11267        let code = encoder.encode(&op).unwrap();
11268        assert_eq!(code.len(), 4, "MVE VMVN should be 4 bytes");
11269    }
11270
11271    #[test]
11272    fn test_encode_mve_load_store_thumb2() {
11273        let encoder = ArmEncoder::new_thumb2();
11274
11275        let load = ArmOp::MveLoad {
11276            qd: QReg::Q0,
11277            addr: MemAddr::imm(Reg::R0, 16),
11278        };
11279        let code = encoder.encode(&load).unwrap();
11280        assert_eq!(code.len(), 4, "MVE VLDRW.32 should be 4 bytes");
11281
11282        let store = ArmOp::MveStore {
11283            qd: QReg::Q1,
11284            addr: MemAddr::imm(Reg::R1, 0),
11285        };
11286        let code = encoder.encode(&store).unwrap();
11287        assert_eq!(code.len(), 4, "MVE VSTRW.32 should be 4 bytes");
11288    }
11289
11290    #[test]
11291    fn test_encode_mve_const_thumb2() {
11292        let encoder = ArmEncoder::new_thumb2();
11293        let op = ArmOp::MveConst {
11294            qd: QReg::Q0,
11295            bytes: [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0],
11296        };
11297        let code = encoder.encode(&op).unwrap();
11298        // Should be 4 words of (MOVW R12 + VMOV Sn) = 4 * (4+4) = 32 bytes min
11299        // Some words with hi16=0 skip MOVT, so length varies
11300        assert!(
11301            code.len() >= 24,
11302            "MVE const should produce multiple instructions"
11303        );
11304    }
11305
11306    #[test]
11307    fn test_encode_mve_dup_thumb2() {
11308        let encoder = ArmEncoder::new_thumb2();
11309        let op = ArmOp::MveDup {
11310            qd: QReg::Q0,
11311            rn: Reg::R0,
11312            size: MveSize::S32,
11313        };
11314        let code = encoder.encode(&op).unwrap();
11315        assert_eq!(code.len(), 4, "MVE VDUP.32 should be 4 bytes");
11316    }
11317
11318    #[test]
11319    fn test_encode_mve_extract_lane_thumb2() {
11320        let encoder = ArmEncoder::new_thumb2();
11321        let op = ArmOp::MveExtractLane {
11322            rd: Reg::R0,
11323            qn: QReg::Q1,
11324            lane: 2,
11325            size: MveSize::S32,
11326        };
11327        let code = encoder.encode(&op).unwrap();
11328        assert_eq!(code.len(), 4, "MVE extract lane should be 4 bytes");
11329    }
11330
11331    #[test]
11332    fn test_encode_mve_insert_lane_thumb2() {
11333        let encoder = ArmEncoder::new_thumb2();
11334        let op = ArmOp::MveInsertLane {
11335            qd: QReg::Q0,
11336            rn: Reg::R1,
11337            lane: 3,
11338            size: MveSize::S32,
11339        };
11340        let code = encoder.encode(&op).unwrap();
11341        assert_eq!(code.len(), 4, "MVE insert lane should be 4 bytes");
11342    }
11343
11344    #[test]
11345    fn test_encode_mve_addf32_thumb2() {
11346        let encoder = ArmEncoder::new_thumb2();
11347        let op = ArmOp::MveAddF32 {
11348            qd: QReg::Q0,
11349            qn: QReg::Q1,
11350            qm: QReg::Q2,
11351        };
11352        let code = encoder.encode(&op).unwrap();
11353        assert_eq!(code.len(), 4, "MVE VADD.F32 should be 4 bytes");
11354    }
11355
11356    #[test]
11357    fn test_encode_mve_divf32_thumb2() {
11358        let encoder = ArmEncoder::new_thumb2();
11359        let op = ArmOp::MveDivF32 {
11360            qd: QReg::Q0,
11361            qn: QReg::Q1,
11362            qm: QReg::Q2,
11363        };
11364        let code = encoder.encode(&op).unwrap();
11365        // Lane-wise: 4 x VDIV.F32 = 4 x 4 = 16 bytes
11366        assert_eq!(
11367            code.len(),
11368            16,
11369            "MVE VDIV.F32 (lane-wise) should be 16 bytes"
11370        );
11371    }
11372
11373    #[test]
11374    fn test_encode_mve_sqrtf32_thumb2() {
11375        let encoder = ArmEncoder::new_thumb2();
11376        let op = ArmOp::MveSqrtF32 {
11377            qd: QReg::Q0,
11378            qm: QReg::Q1,
11379        };
11380        let code = encoder.encode(&op).unwrap();
11381        // Lane-wise: 4 x VSQRT.F32 = 4 x 4 = 16 bytes
11382        assert_eq!(
11383            code.len(),
11384            16,
11385            "MVE VSQRT.F32 (lane-wise) should be 16 bytes"
11386        );
11387    }
11388
11389    #[test]
11390    fn test_encode_mve_negf32_thumb2() {
11391        let encoder = ArmEncoder::new_thumb2();
11392        let op = ArmOp::MveNegF32 {
11393            qd: QReg::Q0,
11394            qm: QReg::Q1,
11395        };
11396        let code = encoder.encode(&op).unwrap();
11397        assert_eq!(code.len(), 4, "MVE VNEG.F32 should be 4 bytes");
11398    }
11399
11400    #[test]
11401    fn test_encode_mve_absf32_thumb2() {
11402        let encoder = ArmEncoder::new_thumb2();
11403        let op = ArmOp::MveAbsF32 {
11404            qd: QReg::Q0,
11405            qm: QReg::Q1,
11406        };
11407        let code = encoder.encode(&op).unwrap();
11408        assert_eq!(code.len(), 4, "MVE VABS.F32 should be 4 bytes");
11409    }
11410
11411    /// VCR-RA-001 / immediate-folding precondition: pins the Thumb-2 `AND`
11412    /// immediate encoding for the byte range and documents its bound.
11413    ///
11414    /// The `And { Operand2::Imm }` encoder packs the low 12 bits straight into
11415    /// the `i:imm3:imm8` field WITHOUT applying ThumbExpandImm (the modified-
11416    /// immediate expansion). For `imm <= 0xFF` (e.g. gale's int8 clamps
11417    /// `#0x7e` / `#0x7f`) that is correct — `i:imm3 = 0000` means "imm8
11418    /// zero-extended". So `and r2, r0, #0x7e` encodes to the canonical
11419    /// `00 f0 7e 02`. For `imm >= 0x100` the field would need a true
11420    /// ThumbExpandImm pattern (rotation / replication), which is NOT
11421    /// implemented here — so **immediate folding must gate on `imm <= 0xFF`**
11422    /// until the encoder is hardened to ThumbExpandImm/Ok-or-Err (the
11423    /// "encoder must be Ok-or-Err, never silently wrong" principle, #180/#185).
11424    /// This bound covers the measured `flat_flight` waste (#209).
11425    #[test]
11426    fn and_immediate_encodes_correctly_in_byte_range_documents_fold_bound() {
11427        let encoder = ArmEncoder::new_thumb2();
11428        let op = ArmOp::And {
11429            rd: Reg::R2,
11430            rn: Reg::R0,
11431            op2: Operand2::Imm(0x7e),
11432        };
11433        let code = encoder.encode(&op).unwrap();
11434        assert_eq!(
11435            code,
11436            vec![0x00, 0xf0, 0x7e, 0x02],
11437            "and r2, r0, #0x7e must encode to the canonical AND.W T1 (imm8=0x7e)"
11438        );
11439    }
11440
11441    /// #255: the shared ThumbExpandImm reverse-encoder underpinning the
11442    /// data-processing immediate fix. Encodable modified immediates round-trip to
11443    /// the expected `i:imm3:imm8` field; a genuinely non-modified value is `None`
11444    /// (caller must materialize into a register). Note `1000 = 0xFA ror 30` *is*
11445    /// representable (field 0xF7A) — the old encoder mis-encoded it (raw 0x3E8);
11446    /// this encodes it correctly.
11447    #[test]
11448    fn try_thumb_expand_imm_encodes_modified_immediates() {
11449        assert_eq!(try_thumb_expand_imm(0x7e), Some(0x07e)); // zero-extended byte
11450        assert_eq!(try_thumb_expand_imm(0xff), Some(0x0ff));
11451        assert_eq!(try_thumb_expand_imm(0x0001_0001), Some(0x101)); // 0x00XY00XY
11452        assert_eq!(try_thumb_expand_imm(0xff00_ff00), Some(0x2ff)); // 0xXY00XY00
11453        assert_eq!(try_thumb_expand_imm(0xffff_ffff), Some(0x3ff)); // 0xXYXYXYXY
11454        assert_eq!(try_thumb_expand_imm(0x100), Some(0xf80)); // 0x80 ror 31
11455        assert_eq!(try_thumb_expand_imm(0x8000_0000), Some(0x400)); // 0x80 ror 8
11456        assert_eq!(try_thumb_expand_imm(1000), Some(0xf7a)); // 0xFA ror 30
11457        // Genuinely unrepresentable (bits too far apart for an 8-bit window).
11458        assert_eq!(try_thumb_expand_imm(0x101), None);
11459        assert_eq!(try_thumb_expand_imm(0x12345), None);
11460    }
11461
11462    /// #255: CMP/ADDS/SUBS encode any valid modified immediate correctly, and
11463    /// ERROR (not silently mis-encode) on a genuinely unrepresentable one,
11464    /// forcing the selector to materialize into a register — closing the
11465    /// silent-miscompile class of #251/#253.
11466    #[test]
11467    fn cmp_adds_subs_immediate_error_on_non_modified_imm() {
11468        let encoder = ArmEncoder::new_thumb2();
11469        // cmp r0, #0xff → valid → Ok; cmp r0, #1000 → valid (0xFA ror 30) → Ok.
11470        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 0xff).is_ok());
11471        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 1000).is_ok());
11472        // cmp r0, #0x101 → NOT a modified immediate → Err (materialize-reg).
11473        assert!(
11474            encoder.encode_thumb32_cmp_imm(&Reg::R0, 0x101).is_err(),
11475            "cmp #0x101 must error, not compare the wrong constant"
11476        );
11477        assert!(
11478            encoder
11479                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x101)
11480                .is_err()
11481        );
11482        assert!(
11483            encoder
11484                .encode_thumb32_subs(&Reg::R0, &Reg::R0, 0x101)
11485                .is_err()
11486        );
11487        // ...but a valid modified immediate still encodes.
11488        assert!(
11489            encoder
11490                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x80)
11491                .is_ok()
11492        );
11493    }
11494
11495    /// #257: MLA (multiply-accumulate) encodes as MLS without the bit-4 op flag.
11496    /// `mla r2, r3, r4, r8` (rd=r2, rn=r3, rm=r4, ra=r8) → Thumb-2 `03 fb 04 82`.
11497    #[test]
11498    fn mla_thumb2_encodes_correctly() {
11499        let encoder = ArmEncoder::new_thumb2();
11500        let code = encoder
11501            .encode(&ArmOp::Mla {
11502                rd: Reg::R2,
11503                rn: Reg::R3,
11504                rm: Reg::R4,
11505                ra: Reg::R8,
11506            })
11507            .unwrap();
11508        // hw1 = 0xFB03, hw2 = (8<<12)|(2<<8)|4 = 0x8204
11509        assert_eq!(code, vec![0x03, 0xfb, 0x04, 0x82]);
11510    }
11511
11512    /// #259: LDR/STR (and sub-word) immediate-offset encoders truncated
11513    /// `offset & 0xFFF`, silently targeting the wrong address for offset >= 4096.
11514    /// They now error (the selector must use register-offset addressing) — the
11515    /// load/store sibling of the #253/#255 class. Offsets <= 4095 still encode.
11516    #[test]
11517    fn ldst_imm12_offset_errors_when_out_of_range() {
11518        let encoder = ArmEncoder::new_thumb2();
11519        // offset 0xFFF (4095): valid → Ok; ldr r0, [r1, #4095].
11520        assert!(
11521            encoder
11522                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0xFFF)
11523                .is_ok()
11524        );
11525        // offset 0x1000 (4096): out of imm12 range → Err (not & 0xFFF → #0).
11526        assert!(
11527            encoder
11528                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0x1000)
11529                .is_err(),
11530            "ldr offset 4096 must error, not wrap to 0"
11531        );
11532        assert!(
11533            encoder
11534                .encode_thumb32_str(&Reg::R0, &Reg::R1, 0x1000)
11535                .is_err()
11536        );
11537        assert!(
11538            encoder
11539                .encode_thumb32_ldrb_imm(&Reg::R0, &Reg::R1, 5000)
11540                .is_err()
11541        );
11542        assert!(
11543            encoder
11544                .encode_thumb32_strh_imm(&Reg::R0, &Reg::R1, 5000)
11545                .is_err()
11546        );
11547    }
11548
11549    /// Latent miscompile fix: ADD/SUB with a >0xFF immediate (e.g.
11550    /// `add sp, sp, #frame` for a >=256-byte frame) used ADD.W (T3), whose
11551    /// `i:imm3:imm8` is a ThumbExpandImm modified immediate — so `#256` silently
11552    /// encoded as `#0` (stack corruption). Use ADDW/SUBW (T4), a PLAIN 12-bit
11553    /// immediate, for 0x100..=0xFFF; keep T3 for <=0xFF (bit-identical); error
11554    /// beyond 4095.
11555    #[test]
11556    fn add_sub_large_immediate_use_addw_subw_not_misencoded() {
11557        let encoder = ArmEncoder::new_thumb2();
11558        // add sp, sp, #256  →  ADDW (T4) SP, SP, #256  =  0d f2 00 1d
11559        assert_eq!(
11560            encoder
11561                .encode(&ArmOp::Add {
11562                    rd: Reg::SP,
11563                    rn: Reg::SP,
11564                    op2: Operand2::Imm(256),
11565                })
11566                .unwrap(),
11567            vec![0x0d, 0xf2, 0x00, 0x1d],
11568            "add sp,sp,#256 must be ADDW (plain imm12), not a mis-encoded ADD.W"
11569        );
11570        // sub sp, sp, #256  →  SUBW (T4) SP, SP, #256  =  ad f2 00 1d
11571        assert_eq!(
11572            encoder
11573                .encode(&ArmOp::Sub {
11574                    rd: Reg::SP,
11575                    rn: Reg::SP,
11576                    op2: Operand2::Imm(256),
11577                })
11578                .unwrap(),
11579            vec![0xad, 0xf2, 0x00, 0x1d],
11580        );
11581        // > 4095 has no single-instruction encoding → error, not silent wrong.
11582        assert!(
11583            encoder
11584                .encode(&ArmOp::Add {
11585                    rd: Reg::SP,
11586                    rn: Reg::SP,
11587                    op2: Operand2::Imm(5000),
11588                })
11589                .is_err(),
11590            "add #5000 must error (no single ADDW), not mis-encode"
11591        );
11592    }
11593
11594    /// Closes the data-proc immediate class: AND and CMN now go through
11595    /// `try_thumb_expand_imm` like ORR/EOR/CMP — correct for any modified
11596    /// immediate, `Err` (not raw-pack / NOP) on an un-encodable one. The byte
11597    /// range stays bit-identical (`and r2,r0,#0x7e` is unchanged).
11598    #[test]
11599    fn and_cmn_immediate_thumb_expand_else_error() {
11600        let encoder = ArmEncoder::new_thumb2();
11601        // byte range unchanged (bit-identical with the pre-retrofit encoding)
11602        assert_eq!(
11603            encoder
11604                .encode(&ArmOp::And {
11605                    rd: Reg::R2,
11606                    rn: Reg::R0,
11607                    op2: Operand2::Imm(0x7e),
11608                })
11609                .unwrap(),
11610            vec![0x00, 0xf0, 0x7e, 0x02],
11611        );
11612        // a valid replicated modified immediate now encodes (was silently wrong)
11613        assert!(
11614            encoder
11615                .encode(&ArmOp::And {
11616                    rd: Reg::R2,
11617                    rn: Reg::R0,
11618                    op2: Operand2::Imm(0xff00ff00u32 as i32),
11619                })
11620                .is_ok()
11621        );
11622        // a genuinely un-encodable immediate errors (AND was raw-pack; CMN NOP)
11623        assert!(
11624            encoder
11625                .encode(&ArmOp::And {
11626                    rd: Reg::R2,
11627                    rn: Reg::R0,
11628                    op2: Operand2::Imm(0x101),
11629                })
11630                .is_err()
11631        );
11632        assert!(
11633            encoder
11634                .encode(&ArmOp::Cmn {
11635                    rn: Reg::R0,
11636                    op2: Operand2::Imm(0x101),
11637                })
11638                .is_err(),
11639            "CMN #0x101 must error, not emit a NOP"
11640        );
11641    }
11642
11643    /// VCR-RA-001: ORR/EOR with a small immediate must encode the real
11644    /// instruction (not a silent `0xBF00` NOP). Pins the byte range and the
11645    /// Ok-or-Err bound that makes future Or/Eor immediate folding safe.
11646    #[test]
11647    fn orr_eor_immediate_encode_in_byte_range_else_error() {
11648        let encoder = ArmEncoder::new_thumb2();
11649        // orr r2, r0, #0x7e  →  ORR.W T1, imm8=0x7e
11650        assert_eq!(
11651            encoder
11652                .encode(&ArmOp::Orr {
11653                    rd: Reg::R2,
11654                    rn: Reg::R0,
11655                    op2: Operand2::Imm(0x7e),
11656                })
11657                .unwrap(),
11658            vec![0x40, 0xf0, 0x7e, 0x02],
11659        );
11660        // eor r2, r0, #0x7e  →  EOR.W T1, imm8=0x7e
11661        assert_eq!(
11662            encoder
11663                .encode(&ArmOp::Eor {
11664                    rd: Reg::R2,
11665                    rn: Reg::R0,
11666                    op2: Operand2::Imm(0x7e),
11667                })
11668                .unwrap(),
11669            vec![0x80, 0xf0, 0x7e, 0x02],
11670        );
11671        // Out-of-range immediates error rather than silently mis-encode / NOP.
11672        assert!(
11673            encoder
11674                .encode(&ArmOp::Orr {
11675                    rd: Reg::R2,
11676                    rn: Reg::R0,
11677                    op2: Operand2::Imm(0x140),
11678                })
11679                .is_err(),
11680            "ORR #0x140 must error, not emit a NOP"
11681        );
11682    }
11683
11684    #[test]
11685    fn test_encode_mve_different_qregs() {
11686        let encoder = ArmEncoder::new_thumb2();
11687
11688        // Test that different Q-register numbers produce different encodings
11689        let op1 = ArmOp::MveAddI {
11690            qd: QReg::Q0,
11691            qn: QReg::Q0,
11692            qm: QReg::Q0,
11693            size: MveSize::S32,
11694        };
11695        let op2 = ArmOp::MveAddI {
11696            qd: QReg::Q3,
11697            qn: QReg::Q5,
11698            qm: QReg::Q7,
11699            size: MveSize::S32,
11700        };
11701        let code1 = encoder.encode(&op1).unwrap();
11702        let code2 = encoder.encode(&op2).unwrap();
11703        assert_ne!(
11704            code1, code2,
11705            "Different Q-registers should produce different encodings"
11706        );
11707    }
11708
11709    #[test]
11710    fn test_encode_mve_arm32_loud_err() {
11711        // #615: MVE (Helium) is Thumb-2-only. The ARM32 encoder used to emit
11712        // a silent NOP here (dropping the vector op); it must now be a typed
11713        // Err so a broken "MVE implies Thumb" invariant fails loudly.
11714        let encoder = ArmEncoder::new_arm32();
11715        let op = ArmOp::MveAddI {
11716            qd: QReg::Q0,
11717            qn: QReg::Q1,
11718            qm: QReg::Q2,
11719            size: MveSize::S32,
11720        };
11721        let err = encoder
11722            .encode(&op)
11723            .expect_err("ARM32 MVE must be a loud Err, not a silent NOP (#615)");
11724        assert!(
11725            err.to_string().contains("Thumb-2 only"),
11726            "unexpected error message: {err}"
11727        );
11728    }
11729}