Skip to main content

synth_backend/
arm_encoder.rs

1//! ARM Code Encoder - Converts ARM instructions to binary machine code
2//!
3//! Generates ARM32/Thumb-2 machine code from ARM instruction structures
4
5use synth_core::Result;
6use synth_core::target::FPUPrecision;
7use synth_synthesis::contracts::encoding as encoding_contracts;
8use synth_synthesis::{ArmOp, MemAddr, MveSize, Operand2, QReg, Reg, VfpReg};
9
10/// ARM instruction encoding
11pub struct ArmEncoder {
12    /// Use Thumb mode (vs ARM mode)
13    thumb_mode: bool,
14    /// FPU capability for VFP instruction encoding
15    #[allow(dead_code)]
16    fpu: Option<FPUPrecision>,
17}
18
19impl ArmEncoder {
20    /// Create a new ARM encoder in ARM32 mode
21    pub fn new_arm32() -> Self {
22        Self {
23            thumb_mode: false,
24            fpu: None,
25        }
26    }
27
28    /// Create a new ARM encoder in Thumb-2 mode
29    pub fn new_thumb2() -> Self {
30        Self {
31            thumb_mode: true,
32            fpu: None,
33        }
34    }
35
36    /// Create a new Thumb-2 encoder with FPU capability
37    pub fn new_thumb2_with_fpu(fpu: Option<FPUPrecision>) -> Self {
38        Self {
39            thumb_mode: true,
40            fpu,
41        }
42    }
43
44    /// Encode a single ARM instruction to bytes
45    pub fn encode(&self, op: &ArmOp) -> Result<Vec<u8>> {
46        if self.thumb_mode {
47            self.encode_thumb(op)
48        } else {
49            self.encode_arm(op)
50        }
51    }
52
53    /// Encode an ARM instruction in ARM32 mode (32-bit instructions)
54    /// #206: encode an ARM32 (A32) load/store whose address uses a register
55    /// offset (`[rn, rm{, #off}]`). Returns `None` for ops with no register
56    /// offset (the caller falls through to the immediate-form arms). Computes
57    /// `ip = base + rm` then re-encodes the op against `[ip, #off]`, which works
58    /// uniformly for word/byte/halfword/signed forms. IP (R12) is the scratch
59    /// register the selector already treats as clobberable across memory ops.
60    fn encode_arm_reg_offset_mem(&self, op: &ArmOp) -> Result<Option<Vec<u8>>> {
61        use synth_synthesis::Reg;
62        let addr = match op {
63            ArmOp::Ldr { addr, .. }
64            | ArmOp::Str { addr, .. }
65            | ArmOp::Ldrb { addr, .. }
66            | ArmOp::Strb { addr, .. }
67            | ArmOp::Ldrh { addr, .. }
68            | ArmOp::Strh { addr, .. }
69            | ArmOp::Ldrsb { addr, .. }
70            | ArmOp::Ldrsh { addr, .. } => addr,
71            _ => return Ok(None),
72        };
73        let Some(rm) = addr.offset_reg else {
74            return Ok(None);
75        };
76        let ip = Reg::R12;
77        // ADD ip, base, rm  (cond=AL, opcode=ADD, S=0, register operand2)
78        let add: u32 = 0xE0800000
79            | (reg_to_bits(&addr.base) << 16)
80            | (reg_to_bits(&ip) << 12)
81            | reg_to_bits(&rm);
82        let mut bytes = add.to_le_bytes().to_vec();
83        // Re-encode the op against [ip, #off] (immediate form → no offset_reg,
84        // so this recursion hits the immediate arms, not this helper again).
85        let imm_addr = MemAddr::imm(ip, addr.offset);
86        let imm_op = match op {
87            ArmOp::Ldr { rd, .. } => ArmOp::Ldr {
88                rd: *rd,
89                addr: imm_addr,
90            },
91            ArmOp::Str { rd, .. } => ArmOp::Str {
92                rd: *rd,
93                addr: imm_addr,
94            },
95            ArmOp::Ldrb { rd, .. } => ArmOp::Ldrb {
96                rd: *rd,
97                addr: imm_addr,
98            },
99            ArmOp::Strb { rd, .. } => ArmOp::Strb {
100                rd: *rd,
101                addr: imm_addr,
102            },
103            ArmOp::Ldrh { rd, .. } => ArmOp::Ldrh {
104                rd: *rd,
105                addr: imm_addr,
106            },
107            ArmOp::Strh { rd, .. } => ArmOp::Strh {
108                rd: *rd,
109                addr: imm_addr,
110            },
111            ArmOp::Ldrsb { rd, .. } => ArmOp::Ldrsb {
112                rd: *rd,
113                addr: imm_addr,
114            },
115            ArmOp::Ldrsh { rd, .. } => ArmOp::Ldrsh {
116                rd: *rd,
117                addr: imm_addr,
118            },
119            _ => unreachable!(),
120        };
121        bytes.extend(self.encode_arm(&imm_op)?);
122        Ok(Some(bytes))
123    }
124
125    /// #594: A32 expansion of `ArmOp::CallIndirect` — mirror of the Thumb-2
126    /// arm (same contract: R11 holds the function-pointer table base, entry
127    /// `i` is a 4-byte code address, R12 is the encoder-scratch register):
128    ///
129    /// ```text
130    /// MOVW r12, #size        ; #642: table size (compile-time immediate)
131    /// [MOVT r12, #size>>16]  ; only when size exceeds 16 bits
132    /// CMP  idx, r12          ; bounds guard: index >= size must TRAP
133    /// BLO  +1 insn           ; skip the trap when in bounds
134    /// UDF                    ; WASM Core §4.4.8 out-of-bounds trap
135    /// MOV r12, idx, LSL #2   ; table byte offset
136    /// LDR r12, [r11, r12]    ; load function pointer
137    /// BLX r12                ; indirect call
138    /// ```
139    ///
140    /// #650, `table_byte_offset != 0` (a non-zero table of the contiguous
141    /// R11 region): the pointer load becomes
142    /// `ADD r12, r11, r12; LDR r12, [r12, #offset]` — offset 0 keeps the
143    /// single-load form (single-table modules byte-identical by
144    /// construction).
145    ///
146    /// #664, `null_check` (the table has null slots, linked as ZERO words
147    /// per the layout contract): `CMP r12, #0; BNE +1; UDF` between the
148    /// pointer load and the `BLX` — a call reaching an uninitialized slot
149    /// traps (§4.4.8). `false` keeps the expansion byte-identical.
150    ///
151    /// #676, `type_check` (heterogeneous table): the §4.4.8 type check is
152    /// discharged at RUNTIME against the type-id sidecar — after the bounds
153    /// guard, `MOV r12, idx, LSL #2; ADD r12, r11, r12;
154    /// LDR r12, [r12, #type_off]; CMP r12, #expected_id; BEQ +1; UDF`
155    /// (mirror of the Thumb-2 arm; the dispatch tail recomputes `idx*4`).
156    /// Null slots carry the reserved class id 0, subsuming the #664 null
157    /// trap. `None` (every homogeneous table — the verdict discharged at
158    /// COMPILE time by the closed-world verification, see the #642 selector
159    /// guard) emits nothing and keeps the expansion byte-identical.
160    fn encode_arm_call_indirect(
161        table_index_reg: &Reg,
162        table_size: u32,
163        table_byte_offset: u32,
164        null_check: bool,
165        type_check: Option<(u32, u32)>,
166    ) -> Vec<u8> {
167        let idx = reg_to_bits(table_index_reg);
168        let mut bytes = Vec::with_capacity(32);
169        // MOVW r12, #(size & 0xFFFF) — cond=E 0011 0000 imm4 Rd imm12.
170        let size_lo = table_size & 0xFFFF;
171        let movw: u32 = 0xE300_0000 | ((size_lo >> 12) << 16) | (12 << 12) | (size_lo & 0xFFF);
172        bytes.extend_from_slice(&movw.to_le_bytes());
173        // MOVT r12, #(size >> 16) — only for a table size above 16 bits.
174        let size_hi = table_size >> 16;
175        if size_hi != 0 {
176            let movt: u32 = 0xE340_0000 | ((size_hi >> 12) << 16) | (12 << 12) | (size_hi & 0xFFF);
177            bytes.extend_from_slice(&movt.to_le_bytes());
178        }
179        // CMP idx, r12 — cond=E, opcode=1010, S=1, Rn=idx, Rm=r12.
180        let cmp: u32 = 0xE150_000C | (idx << 16);
181        bytes.extend_from_slice(&cmp.to_le_bytes());
182        // BLO +1 insn (skip the UDF when index < size) — cond=LO(0011),
183        // imm24=0: target = branch + 8.
184        bytes.extend_from_slice(&0x3A00_0000u32.to_le_bytes());
185        // UDF — permanently undefined (same trap idiom as the A32 div-by-zero
186        // guards): call_indirect out-of-bounds trap.
187        bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
188        // #676: runtime type check for a heterogeneous table — load the
189        // indexed slot's structural class id from the type-id sidecar and
190        // trap on mismatch (§4.4.8). Mirror of the Thumb-2 arm; `None`
191        // emits nothing (homogeneous tables byte-identical by construction).
192        if let Some((expected_id, type_off)) = type_check {
193            debug_assert!(expected_id <= 255, "selector enforces the CMP imm8 range");
194            debug_assert!(type_off <= 4095, "selector enforces the LDR imm12 range");
195            // MOV r12, idx, LSL #2 (same as the dispatch tail's scale).
196            bytes.extend_from_slice(&(0xE1A0C000u32 | (2 << 7) | idx).to_le_bytes());
197            // ADD r12, r11, r12 — data-processing ADD (register).
198            bytes.extend_from_slice(&0xE08BC00Cu32.to_le_bytes());
199            // LDR r12, [r12, #type_off] — immediate offset, P=1 U=1 L=1.
200            bytes.extend_from_slice(&(0xE59CC000u32 | (type_off & 0xFFF)).to_le_bytes());
201            // CMP r12, #expected_id — data-processing CMP (immediate).
202            bytes.extend_from_slice(&(0xE35C_0000u32 | (expected_id & 0xFF)).to_le_bytes());
203            // BEQ +1 insn (skip the UDF when the class id matches) —
204            // cond=EQ(0000), imm24=0: target = branch + 8.
205            bytes.extend_from_slice(&0x0A00_0000u32.to_le_bytes());
206            // UDF — the §4.4.8 type-mismatch trap.
207            bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
208        }
209        // MOV r12, idx, LSL #2 — data-processing MOV, register op2 with
210        // imm5=2/LSL: cond=E, opcode=1101, S=0, Rd=r12.
211        let mov: u32 = 0xE1A0C000 | (2 << 7) | idx;
212        bytes.extend_from_slice(&mov.to_le_bytes());
213        if table_byte_offset == 0 {
214            // Table 0 (base = R11 itself): the pre-#650 single-load form.
215            // LDR r12, [r11, r12] — register offset, P=1 U=1 B=0 W=0 L=1.
216            let ldr: u32 = 0xE79BC00C;
217            bytes.extend_from_slice(&ldr.to_le_bytes());
218        } else {
219            // #650: fold the table's compile-time base offset into the
220            // pointer load via the LDR imm12 form.
221            assert!(
222                table_byte_offset <= 4095,
223                "call_indirect table base offset {table_byte_offset} exceeds \
224                 LDR imm12 — the selector must have declined this (#650)"
225            );
226            // ADD r12, r11, r12 — data-processing ADD (register).
227            bytes.extend_from_slice(&0xE08BC00Cu32.to_le_bytes());
228            // LDR r12, [r12, #offset] — immediate offset, P=1 U=1 L=1.
229            let ldr: u32 = 0xE59CC000 | (table_byte_offset & 0xFFF);
230            bytes.extend_from_slice(&ldr.to_le_bytes());
231        }
232        // #664: null-slot trap — only when the table image has null slots
233        // (zero-linked words). A fully-initialized table keeps the pre-#664
234        // bytes identical by construction.
235        if null_check {
236            // CMP r12, #0 — data-processing CMP (immediate), Rn=r12.
237            bytes.extend_from_slice(&0xE35C_0000u32.to_le_bytes());
238            // BNE +1 insn (skip the UDF when the pointer is non-null) —
239            // cond=NE(0001), imm24=0: target = branch + 8.
240            bytes.extend_from_slice(&0x1A00_0000u32.to_le_bytes());
241            // UDF — the §4.4.8 uninitialized-element trap (same idiom as
242            // the bounds guard).
243            bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
244        }
245        // BLX r12 — cond=E, 0001 0010 1111 1111 1111 0011, Rm=r12.
246        let blx: u32 = 0xE12FFF3C;
247        bytes.extend_from_slice(&blx.to_le_bytes());
248        bytes
249    }
250
251    /// #615: A32 (ARM-mode) expansions for the multi-instruction ops that the
252    /// Thumb-2 encoder expands but the A32 arm previously encoded as a single
253    /// literal NOP (`0xE1A00000`) — i64 mul / shifts / rotates / comparisons /
254    /// eqz, plus i64 const/load/store/extend/wrap and the i32 SetCond /
255    /// SelectMove pseudo-ops. Each expansion mirrors its Thumb-2 twin's
256    /// register contract and semantics exactly (A32 conditional execution
257    /// replaces the IT blocks). Returns `Ok(None)` for ops this helper does
258    /// not handle; the caller's match encodes or loudly rejects those.
259    fn encode_arm_expanded(&self, op: &ArmOp) -> Result<Option<Vec<u8>>> {
260        use synth_synthesis::Condition;
261
262        /// A32 condition-field bits (instruction bits [31:28]).
263        fn cond_bits(cond: &Condition) -> u32 {
264            match cond {
265                Condition::EQ => 0x0,
266                Condition::NE => 0x1,
267                Condition::HS => 0x2, // CS: unsigned >=
268                Condition::LO => 0x3, // CC: unsigned <
269                Condition::HI => 0x8, // unsigned >
270                Condition::LS => 0x9, // unsigned <=
271                Condition::GE => 0xA,
272                Condition::LT => 0xB,
273                Condition::GT => 0xC,
274                Condition::LE => 0xD,
275            }
276        }
277        fn w(b: &mut Vec<u8>, word: u32) {
278            b.extend_from_slice(&word.to_le_bytes());
279        }
280        /// MOV<cond> rd, #imm (rotated-immediate form; only 0/1 used here).
281        fn mov_cond_imm(b: &mut Vec<u8>, cond: u32, rd: u32, imm: u32) {
282            w(b, (cond << 28) | 0x03A0_0000 | (rd << 12) | imm);
283        }
284        /// After a flag-setting pair: MOV<cond> rd,#1 ; MOV<!cond> rd,#0.
285        fn set_cond(b: &mut Vec<u8>, cond: &Condition, rd: u32) {
286            mov_cond_imm(b, cond_bits(cond), rd, 1);
287            mov_cond_imm(b, cond_bits(&cond.invert()), rd, 0);
288        }
289        /// CMP rn, rm (register form).
290        fn cmp_reg(b: &mut Vec<u8>, rn: u32, rm: u32) {
291            w(b, 0xE150_0000 | (rn << 16) | rm);
292        }
293        /// SBCS rd, rn, rm — the 64-bit compare idiom's high-word subtract.
294        fn sbcs(b: &mut Vec<u8>, rd: u32, rn: u32, rm: u32) {
295            w(b, 0xE0D0_0000 | (rn << 16) | (rd << 12) | rm);
296        }
297        /// MOVW rd, #imm16.
298        fn movw(b: &mut Vec<u8>, rd: u32, v: u32) {
299            w(
300                b,
301                0xE300_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
302            );
303        }
304        /// MOVT rd, #imm16.
305        fn movt(b: &mut Vec<u8>, rd: u32, v: u32) {
306            w(
307                b,
308                0xE340_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
309            );
310        }
311        /// Register-controlled shift: MOV rd, rn, <LSL|LSR|ASR> rs.
312        /// `ty`: 0=LSL, 1=LSR, 2=ASR. A32 uses the bottom byte of rs;
313        /// amounts of 32 or more yield 0 (LSL/LSR) or all-sign (ASR) — same
314        /// semantics the Thumb-2 expansions rely on.
315        fn shift_reg(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, rs: u32) {
316            w(b, 0xE1A0_0010 | (rd << 12) | (rs << 8) | (ty << 5) | rn);
317        }
318        const LSL: u32 = 0;
319        const LSR: u32 = 1;
320        const ASR: u32 = 2;
321        /// Immediate-shift move: MOV rd, rn, <LSL|LSR|ASR> #imm.
322        fn shift_imm(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, imm: u32) {
323            w(
324                b,
325                0xE1A0_0000 | (rd << 12) | ((imm & 0x1F) << 7) | (ty << 5) | rn,
326            );
327        }
328        /// Data-processing register form: `base | rn<<16 | rd<<12 | rm`.
329        /// `base` carries cond/opcode/S (e.g. 0xE090_0000 = ADDS).
330        fn dp_reg(b: &mut Vec<u8>, base: u32, rd: u32, rn: u32, rm: u32) {
331            w(b, base | (rn << 16) | (rd << 12) | rm);
332        }
333        /// ORR rd, rd, rm, LSR #31 — the carry-propagation idiom of the
334        /// shift-subtract division loop (bring rm's MSB into rd's bit 0).
335        fn orr_lsr31(b: &mut Vec<u8>, rd: u32, rm: u32) {
336            w(
337                b,
338                0xE180_0000 | (rd << 16) | (rd << 12) | (31 << 7) | (1 << 5) | rm,
339            );
340        }
341        /// 64-bit two's-complement negate of the lo:hi pair (MVN/MVN/ADDS/ADC).
342        fn negate64(b: &mut Vec<u8>, lo: u32, hi: u32) {
343            w(b, 0xE1E0_0000 | (lo << 12) | lo); //           MVN  lo, lo
344            w(b, 0xE1E0_0000 | (hi << 12) | hi); //           MVN  hi, hi
345            w(b, 0xE290_0001 | (lo << 16) | (lo << 12)); //   ADDS lo, lo, #1
346            w(b, 0xE2A0_0000 | (hi << 16) | (hi << 12)); //   ADC  hi, hi, #0
347        }
348        /// TST x, x ; BPL +4-instructions — the "skip the negate64 when the
349        /// sign bit is clear" guard of the signed div/rem arms.
350        fn skip_negate_if_positive(b: &mut Vec<u8>, x: u32) {
351            w(b, 0xE110_0000 | (x << 16) | x); // TST x, x
352            w(b, 0x5A00_0003); //                 BPL +4 insns (past negate64)
353        }
354        /// The 64-iteration shift-subtract division loop — A32 transcription
355        /// of the Thumb-2 #610 core: dividend R0:R1, divisor R2:R3, quotient
356        /// R4:R5, remainder R6:R7, loop counter in `counter` (R12 or R8).
357        fn div_loop(b: &mut Vec<u8>, counter: u32) {
358            w(b, 0xE3A0_0040 | (counter << 12)); // MOV counter, #64
359            let loop_start = b.len();
360            // quotient <<= 1
361            shift_imm(b, LSL, 5, 5, 1);
362            orr_lsr31(b, 5, 4);
363            shift_imm(b, LSL, 4, 4, 1);
364            // remainder <<= 1, OR in dividend MSB
365            shift_imm(b, LSL, 7, 7, 1);
366            orr_lsr31(b, 7, 6);
367            shift_imm(b, LSL, 6, 6, 1);
368            orr_lsr31(b, 6, 1);
369            // dividend <<= 1
370            shift_imm(b, LSL, 1, 1, 1);
371            orr_lsr31(b, 1, 0);
372            shift_imm(b, LSL, 0, 0, 1);
373            // if remainder >= divisor (64-bit unsigned): subtract, set q bit
374            w(b, 0xE157_0003); // CMP R7, R3      (high words)
375            w(b, 0x8A00_0002); // BHI .subtract   (+2 insns)
376            w(b, 0x3A00_0004); // BLO .next       (+4 insns)
377            w(b, 0xE156_0002); // CMP R6, R2      (low words, highs equal)
378            w(b, 0x3A00_0002); // BLO .next       (+2 insns)
379            w(b, 0xE056_6002); // .subtract: SUBS R6, R6, R2
380            w(b, 0xE0C7_7003); //            SBC  R7, R7, R3
381            w(b, 0xE384_4001); //            ORR  R4, R4, #1
382            // .next: decrement and loop
383            w(b, 0xE250_0001 | (counter << 16) | (counter << 12)); // SUBS counter, #1
384            let diff = (loop_start as i64) - (b.len() as i64 + 8);
385            w(b, 0x1A00_0000 | (((diff / 4) as u32) & 0x00FF_FFFF)); // BNE loop
386        }
387        /// 32-bit population count on working register `x` — A32 transcription
388        /// of the Thumb-2 I64Popcnt per-word core (mul-based fold): `c` is the
389        /// constant register, R12 the shifted temp. Both are clobbered.
390        fn popcnt_word(b: &mut Vec<u8>, x: u32, c: u32) {
391            // x = x - ((x >> 1) & 0x55555555)
392            shift_imm(b, LSR, 12, x, 1);
393            movw(b, c, 0x5555);
394            movt(b, c, 0x5555);
395            dp_reg(b, 0xE000_0000, 12, 12, c); // AND R12, R12, c
396            dp_reg(b, 0xE040_0000, x, x, 12); //  SUB x, x, R12
397            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
398            movw(b, c, 0x3333);
399            movt(b, c, 0x3333);
400            dp_reg(b, 0xE000_0000, 12, x, c); //  AND R12, x, c
401            shift_imm(b, LSR, x, x, 2);
402            dp_reg(b, 0xE000_0000, x, x, c); //   AND x, x, c
403            dp_reg(b, 0xE080_0000, x, x, 12); //  ADD x, x, R12
404            // x = (x + (x >> 4)) & 0x0F0F0F0F
405            shift_imm(b, LSR, 12, x, 4);
406            dp_reg(b, 0xE080_0000, x, x, 12); //  ADD x, x, R12
407            movw(b, c, 0x0F0F);
408            movt(b, c, 0x0F0F);
409            dp_reg(b, 0xE000_0000, x, x, c); //   AND x, x, c
410            // x = (x * 0x01010101) >> 24
411            movw(b, c, 0x0101);
412            movt(b, c, 0x0101);
413            w(b, 0xE000_0090 | (x << 16) | (c << 8) | x); // MUL x, x, c
414            shift_imm(b, LSR, x, x, 24);
415        }
416
417        let mut b: Vec<u8> = Vec::new();
418        match op {
419            // SetCond: materialize a flags-predicate as 0/1 — the A32 twin of
420            // the Thumb `ITE cond; MOV rd,#1; MOV rd,#0`.
421            ArmOp::SetCond { rd, cond } => {
422                set_cond(&mut b, cond, reg_to_bits(rd));
423            }
424
425            // SelectMove: conditional register move (Thumb: IT cond; MOV).
426            ArmOp::SelectMove { rd, rm, cond } => {
427                w(
428                    &mut b,
429                    (cond_bits(cond) << 28)
430                        | 0x01A0_0000
431                        | (reg_to_bits(rd) << 12)
432                        | reg_to_bits(rm),
433                );
434            }
435
436            // I64SetCond: compare two i64 register pairs, 0/1 into rd.
437            // EQ/NE: CMP lo,lo; CMPEQ hi,hi (only if lows equal); set.
438            // Ordered: CMP lo,lo; SBCS rd,hi,hi; set — with the same
439            // operand-swap + condition mapping as the Thumb-2 arm.
440            ArmOp::I64SetCond {
441                rd,
442                rn_lo,
443                rn_hi,
444                rm_lo,
445                rm_hi,
446                cond,
447            } => {
448                let rd_b = reg_to_bits(rd);
449                let (n_lo, n_hi, m_lo, m_hi) = (
450                    reg_to_bits(rn_lo),
451                    reg_to_bits(rn_hi),
452                    reg_to_bits(rm_lo),
453                    reg_to_bits(rm_hi),
454                );
455                match cond {
456                    Condition::EQ | Condition::NE => {
457                        cmp_reg(&mut b, n_lo, m_lo);
458                        // CMP<EQ> rn_hi, rm_hi — compare highs only if lows equal.
459                        w(&mut b, 0x0150_0000 | (n_hi << 16) | m_hi);
460                        set_cond(&mut b, cond, rd_b);
461                    }
462                    // (swap operands?, condition after SBCS) per the Thumb arm:
463                    // LT/GE/LO/HS compare (rn, rm); GT/LE/HI/LS swap to (rm, rn).
464                    Condition::LT => {
465                        cmp_reg(&mut b, n_lo, m_lo);
466                        sbcs(&mut b, rd_b, n_hi, m_hi);
467                        set_cond(&mut b, &Condition::LT, rd_b);
468                    }
469                    Condition::GE => {
470                        cmp_reg(&mut b, n_lo, m_lo);
471                        sbcs(&mut b, rd_b, n_hi, m_hi);
472                        set_cond(&mut b, &Condition::GE, rd_b);
473                    }
474                    Condition::GT => {
475                        cmp_reg(&mut b, m_lo, n_lo);
476                        sbcs(&mut b, rd_b, m_hi, n_hi);
477                        set_cond(&mut b, &Condition::LT, rd_b);
478                    }
479                    Condition::LE => {
480                        cmp_reg(&mut b, m_lo, n_lo);
481                        sbcs(&mut b, rd_b, m_hi, n_hi);
482                        set_cond(&mut b, &Condition::GE, rd_b);
483                    }
484                    Condition::LO => {
485                        cmp_reg(&mut b, n_lo, m_lo);
486                        sbcs(&mut b, rd_b, n_hi, m_hi);
487                        set_cond(&mut b, &Condition::LO, rd_b);
488                    }
489                    Condition::HS => {
490                        cmp_reg(&mut b, n_lo, m_lo);
491                        sbcs(&mut b, rd_b, n_hi, m_hi);
492                        set_cond(&mut b, &Condition::HS, rd_b);
493                    }
494                    Condition::HI => {
495                        cmp_reg(&mut b, m_lo, n_lo);
496                        sbcs(&mut b, rd_b, m_hi, n_hi);
497                        set_cond(&mut b, &Condition::LO, rd_b);
498                    }
499                    Condition::LS => {
500                        cmp_reg(&mut b, m_lo, n_lo);
501                        sbcs(&mut b, rd_b, m_hi, n_hi);
502                        set_cond(&mut b, &Condition::HS, rd_b);
503                    }
504                }
505            }
506
507            // I64SetCondZ: ORRS rd, lo, hi sets Z iff the pair is zero.
508            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
509                let rd_b = reg_to_bits(rd);
510                w(
511                    &mut b,
512                    0xE190_0000 | (reg_to_bits(rn_lo) << 16) | (rd_b << 12) | reg_to_bits(rn_hi),
513                );
514                set_cond(&mut b, &Condition::EQ, rd_b);
515            }
516
517            // i64 comparison wrappers: delegate to I64SetCond/Z, mirroring the
518            // Thumb-2 delegation arms.
519            ArmOp::I64Eqz { rd, rnlo, rnhi } => {
520                return self
521                    .encode_arm(&ArmOp::I64SetCondZ {
522                        rd: *rd,
523                        rn_lo: *rnlo,
524                        rn_hi: *rnhi,
525                    })
526                    .map(Some);
527            }
528            ArmOp::I64Eq {
529                rd,
530                rnlo,
531                rnhi,
532                rmlo,
533                rmhi,
534            }
535            | ArmOp::I64Ne {
536                rd,
537                rnlo,
538                rnhi,
539                rmlo,
540                rmhi,
541            }
542            | ArmOp::I64LtS {
543                rd,
544                rnlo,
545                rnhi,
546                rmlo,
547                rmhi,
548            }
549            | ArmOp::I64LtU {
550                rd,
551                rnlo,
552                rnhi,
553                rmlo,
554                rmhi,
555            }
556            | ArmOp::I64LeS {
557                rd,
558                rnlo,
559                rnhi,
560                rmlo,
561                rmhi,
562            }
563            | ArmOp::I64LeU {
564                rd,
565                rnlo,
566                rnhi,
567                rmlo,
568                rmhi,
569            }
570            | ArmOp::I64GtS {
571                rd,
572                rnlo,
573                rnhi,
574                rmlo,
575                rmhi,
576            }
577            | ArmOp::I64GtU {
578                rd,
579                rnlo,
580                rnhi,
581                rmlo,
582                rmhi,
583            }
584            | ArmOp::I64GeS {
585                rd,
586                rnlo,
587                rnhi,
588                rmlo,
589                rmhi,
590            }
591            | ArmOp::I64GeU {
592                rd,
593                rnlo,
594                rnhi,
595                rmlo,
596                rmhi,
597            } => {
598                let cond = match op {
599                    ArmOp::I64Eq { .. } => Condition::EQ,
600                    ArmOp::I64Ne { .. } => Condition::NE,
601                    ArmOp::I64LtS { .. } => Condition::LT,
602                    ArmOp::I64LtU { .. } => Condition::LO,
603                    ArmOp::I64LeS { .. } => Condition::LE,
604                    ArmOp::I64LeU { .. } => Condition::LS,
605                    ArmOp::I64GtS { .. } => Condition::GT,
606                    ArmOp::I64GtU { .. } => Condition::HI,
607                    ArmOp::I64GeS { .. } => Condition::GE,
608                    _ => Condition::HS,
609                };
610                return self
611                    .encode_arm(&ArmOp::I64SetCond {
612                        rd: *rd,
613                        rn_lo: *rnlo,
614                        rn_hi: *rnhi,
615                        rm_lo: *rmlo,
616                        rm_hi: *rmhi,
617                        cond,
618                    })
619                    .map(Some);
620            }
621
622            // I64Mul: cross products into R12, then UMULL — same sequence and
623            // ordering as the Thumb-2 arm (R12 is encoder scratch, #212).
624            ArmOp::I64Mul {
625                rd_lo,
626                rd_hi,
627                rn_lo,
628                rn_hi,
629                rm_lo,
630                rm_hi,
631            } => {
632                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
633                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
634                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
635                // MUL R12, rn_lo, rm_hi   (R12 = a_lo * b_hi)
636                w(&mut b, 0xE000_0090 | (12 << 16) | (mh << 8) | nl);
637                // MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
638                w(
639                    &mut b,
640                    0xE020_0090 | (12 << 16) | (12 << 12) | (ml << 8) | nh,
641                );
642                // UMULL rd_lo, rd_hi, rn_lo, rm_lo
643                w(
644                    &mut b,
645                    0xE080_0090 | (dh << 16) | (dl << 12) | (ml << 8) | nl,
646                );
647                // ADD rd_hi, rd_hi, R12
648                w(&mut b, 0xE080_0000 | (dh << 16) | (dh << 12) | 12);
649            }
650
651            // I64Shl / I64ShrU / I64ShrS: same small/large-shift structure as
652            // the Thumb-2 arms (rm_hi is the scratch register; amounts are
653            // masked to 6 bits; register-controlled shifts >= 32 yield 0,
654            // which the small path relies on for n = 0).
655            ArmOp::I64Shl {
656                rd_lo,
657                rd_hi,
658                rn_lo,
659                rn_hi,
660                rm_lo,
661                rm_hi,
662            } => {
663                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
664                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
665                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
666                w(&mut b, 0xE200_003F | (ml << 16) | (ml << 12)); // AND  ml, ml, #63
667                w(&mut b, 0xE250_0020 | (ml << 16) | (mh << 12)); // SUBS mh, ml, #32
668                w(&mut b, 0x5A00_0005); //                            BPL  .large
669                w(&mut b, 0xE260_0020 | (ml << 16) | (mh << 12)); // RSB  mh, ml, #32
670                shift_reg(&mut b, LSR, mh, nl, mh); //               mh = lo >> (32-n)
671                shift_reg(&mut b, LSL, dh, nh, ml); //               dh = hi << n
672                w(&mut b, 0xE180_0000 | (dh << 16) | (dh << 12) | mh); // ORR dh, dh, mh
673                shift_reg(&mut b, LSL, dl, nl, ml); //               dl = lo << n
674                w(&mut b, 0xEA00_0001); //                            B    .done
675                shift_reg(&mut b, LSL, dh, nl, mh); //               .large: dh = lo << (n-32)
676                w(&mut b, 0xE3A0_0000 | (dl << 12)); //              MOV  dl, #0
677            }
678            ArmOp::I64ShrU {
679                rd_lo,
680                rd_hi,
681                rn_lo,
682                rn_hi,
683                rm_lo,
684                rm_hi,
685            } => {
686                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
687                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
688                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
689                w(&mut b, 0xE200_003F | (ml << 16) | (ml << 12)); // AND  ml, ml, #63
690                w(&mut b, 0xE250_0020 | (ml << 16) | (mh << 12)); // SUBS mh, ml, #32
691                w(&mut b, 0x5A00_0005); //                            BPL  .large
692                w(&mut b, 0xE260_0020 | (ml << 16) | (mh << 12)); // RSB  mh, ml, #32
693                shift_reg(&mut b, LSL, mh, nh, mh); //               mh = hi << (32-n)
694                shift_reg(&mut b, LSR, dl, nl, ml); //               dl = lo >> n
695                w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | mh); // ORR dl, dl, mh
696                shift_reg(&mut b, LSR, dh, nh, ml); //               dh = hi >> n
697                w(&mut b, 0xEA00_0001); //                            B    .done
698                shift_reg(&mut b, LSR, dl, nh, mh); //               .large: dl = hi >> (n-32)
699                w(&mut b, 0xE3A0_0000 | (dh << 12)); //              MOV  dh, #0
700            }
701            ArmOp::I64ShrS {
702                rd_lo,
703                rd_hi,
704                rn_lo,
705                rn_hi,
706                rm_lo,
707                rm_hi,
708            } => {
709                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
710                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
711                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
712                w(&mut b, 0xE200_003F | (ml << 16) | (ml << 12)); // AND  ml, ml, #63
713                w(&mut b, 0xE250_0020 | (ml << 16) | (mh << 12)); // SUBS mh, ml, #32
714                w(&mut b, 0x5A00_0005); //                            BPL  .large
715                w(&mut b, 0xE260_0020 | (ml << 16) | (mh << 12)); // RSB  mh, ml, #32
716                shift_reg(&mut b, LSL, mh, nh, mh); //               mh = hi << (32-n)
717                shift_reg(&mut b, LSR, dl, nl, ml); //               dl = lo >> n
718                w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | mh); // ORR dl, dl, mh
719                shift_reg(&mut b, ASR, dh, nh, ml); //               dh = hi >> n (arith)
720                w(&mut b, 0xEA00_0001); //                            B    .done
721                shift_reg(&mut b, ASR, dl, nh, mh); //               .large: dl = hi >> (n-32)
722                w(&mut b, 0xE1A0_0040 | (dh << 12) | (31 << 7) | nh); // ASR dh, nh, #31
723            }
724
725            // I64Rotl / I64Rotr: the #610 fixed-ABI wrapper (A32 form) around
726            // the same fixed-register core as the Thumb-2 arms — value in
727            // R0:R1, amount in R2, scratch R3 + R12.
728            ArmOp::I64Rotl {
729                rdlo,
730                rdhi,
731                rnlo,
732                rnhi,
733                shift,
734            } => {
735                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
736                for word in [
737                    0xE202_203Fu32, // AND  R2, R2, #63   (mask amount mod 64)
738                    0xE252_3020,    // SUBS R3, R2, #32   (R3 = n-32, sets N)
739                    0x5A00_0007,    // BPL  .large        (n >= 32)
740                    // --- small rotation (n < 32) ---
741                    0xE262_3020, // RSB  R3, R2, #32   (R3 = 32-n)
742                    0xE1A0_C330, // LSR  R12, R0, R3   (lo >> (32-n))
743                    0xE1A0_3331, // LSR  R3, R1, R3    (hi >> (32-n))
744                    0xE1A0_1211, // LSL  R1, R1, R2    (hi << n)
745                    0xE181_100C, // ORR  R1, R1, R12   (new_hi)
746                    0xE1A0_0210, // LSL  R0, R0, R2    (lo << n)
747                    0xE180_0003, // ORR  R0, R0, R3    (new_lo)
748                    0xEA00_0007, // B    .done
749                    // --- large rotation (n >= 32), R3 = m = n-32 ---
750                    0xE263_2020, // RSB  R2, R3, #32   (R2 = 32-m = 64-n)
751                    0xE1A0_C231, // LSR  R12, R1, R2   (hi >> (64-n))
752                    0xE1A0_2230, // LSR  R2, R0, R2    (lo >> (64-n))
753                    0xE1A0_0310, // LSL  R0, R0, R3    (lo << m)
754                    0xE1A0_1311, // LSL  R1, R1, R3    (hi << m)
755                    0xE180_C00C, // ORR  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
756                    0xE181_0002, // ORR  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
757                    0xE1A0_100C, // MOV  R1, R12       (new_hi into place)
758                ] {
759                    w(&mut b, word);
760                }
761                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
762            }
763            ArmOp::I64Rotr {
764                rdlo,
765                rdhi,
766                rnlo,
767                rnhi,
768                shift,
769            } => {
770                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
771                for word in [
772                    0xE202_203Fu32, // AND  R2, R2, #63   (mask amount mod 64)
773                    0xE252_3020,    // SUBS R3, R2, #32   (R3 = n-32, sets N)
774                    0x5A00_0007,    // BPL  .large        (n >= 32)
775                    // --- small rotation (n < 32) ---
776                    0xE262_3020, // RSB  R3, R2, #32   (R3 = 32-n)
777                    0xE1A0_C311, // LSL  R12, R1, R3   (hi << (32-n))
778                    0xE1A0_3310, // LSL  R3, R0, R3    (lo << (32-n))
779                    0xE1A0_0230, // LSR  R0, R0, R2    (lo >> n)
780                    0xE180_000C, // ORR  R0, R0, R12   (new_lo)
781                    0xE1A0_1231, // LSR  R1, R1, R2    (hi >> n)
782                    0xE181_1003, // ORR  R1, R1, R3    (new_hi)
783                    0xEA00_0007, // B    .done
784                    // --- large rotation (n >= 32), R3 = m = n-32 ---
785                    0xE263_2020, // RSB  R2, R3, #32   (R2 = 32-m = 64-n)
786                    0xE1A0_C210, // LSL  R12, R0, R2   (lo << (64-n))
787                    0xE1A0_2211, // LSL  R2, R1, R2    (hi << (64-n))
788                    0xE1A0_1331, // LSR  R1, R1, R3    (hi >> m)
789                    0xE181_C00C, // ORR  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
790                    0xE1A0_1330, // LSR  R1, R0, R3    (lo >> m)
791                    0xE181_1002, // ORR  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
792                    0xE1A0_000C, // MOV  R0, R12       (new_lo into place)
793                ] {
794                    w(&mut b, word);
795                }
796                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
797            }
798
799            // I64Clz: CLZ(hi), or 32 + CLZ(lo) when hi == 0. Conditional
800            // execution replaces the Thumb branches; like the Thumb arm, the
801            // high word of the result pair (rnhi) is cleared last.
802            ArmOp::I64Clz { rd, rnlo, rnhi } => {
803                let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
804                w(&mut b, 0xE350_0000 | (hi << 16)); //              CMP   rnhi, #0
805                w(&mut b, 0x116F_0F10 | (rd_b << 12) | hi); //       CLZNE rd, rnhi
806                w(&mut b, 0x016F_0F10 | (rd_b << 12) | lo); //       CLZEQ rd, rnlo
807                w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
808                w(&mut b, 0xE3A0_0000 | (hi << 12)); //              MOV   rnhi, #0
809            }
810
811            // I64Ctz: CLZ(RBIT(lo)), or 32 + CLZ(RBIT(hi)) when lo == 0.
812            // RBIT/CLZ leave the flags intact, so the CMP's Z survives to the
813            // conditional ADD.
814            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
815                let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
816                w(&mut b, 0xE350_0000 | (lo << 16)); //              CMP    rnlo, #0
817                w(&mut b, 0x16FF_0F30 | (rd_b << 12) | lo); //       RBITNE rd, rnlo
818                w(&mut b, 0x06FF_0F30 | (rd_b << 12) | hi); //       RBITEQ rd, rnhi
819                w(&mut b, 0xE16F_0F10 | (rd_b << 12) | rd_b); //     CLZ    rd, rd
820                w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
821                w(&mut b, 0xE3A0_0000 | (hi << 12)); //              MOV    rnhi, #0
822            }
823
824            // I64Const: MOVW/MOVT per half (MOVT elided when the half fits in
825            // 16 bits, mirroring the Thumb-2 arm).
826            ArmOp::I64Const { rdlo, rdhi, value } => {
827                let lo32 = *value as u32;
828                let hi32 = (*value >> 32) as u32;
829                movw(&mut b, reg_to_bits(rdlo), lo32 & 0xFFFF);
830                if lo32 > 0xFFFF {
831                    movt(&mut b, reg_to_bits(rdlo), lo32 >> 16);
832                }
833                movw(&mut b, reg_to_bits(rdhi), hi32 & 0xFFFF);
834                if hi32 > 0xFFFF {
835                    movt(&mut b, reg_to_bits(rdhi), hi32 >> 16);
836                }
837            }
838
839            // I64Ldr / I64Str: two word accesses at [base, #off] / #off+4.
840            // A register offset is materialized into IP once (the #206/#372
841            // hazard: dropping it would read the wrong address).
842            ArmOp::I64Ldr { rdlo, rdhi, addr } | ArmOp::I64Str { rdlo, rdhi, addr } => {
843                let base = if let Some(rm) = addr.offset_reg {
844                    // ADD ip, base, rm
845                    w(
846                        &mut b,
847                        0xE080_0000
848                            | (reg_to_bits(&addr.base) << 16)
849                            | (12 << 12)
850                            | reg_to_bits(&rm),
851                    );
852                    12
853                } else {
854                    reg_to_bits(&addr.base)
855                };
856                if addr.offset < 0 || addr.offset > 0xFFB {
857                    return Err(synth_core::Error::synthesis(format!(
858                        "i64 load/store offset {} out of the A32 imm12 range (0..=4091) — materialize the offset into a register",
859                        addr.offset
860                    )));
861                }
862                let off = addr.offset as u32;
863                let opc: u32 = if matches!(op, ArmOp::I64Ldr { .. }) {
864                    0xE590_0000 // LDR
865                } else {
866                    0xE580_0000 // STR
867                };
868                w(&mut b, opc | (base << 16) | (reg_to_bits(rdlo) << 12) | off);
869                w(
870                    &mut b,
871                    opc | (base << 16) | (reg_to_bits(rdhi) << 12) | (off + 4),
872                );
873            }
874
875            // I64ExtendI32S: rdlo = rn; rdhi = rdlo >> 31 (arithmetic).
876            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
877                if rdlo != rn {
878                    w(
879                        &mut b,
880                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
881                    );
882                }
883                w(
884                    &mut b,
885                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
886                );
887            }
888
889            // I64ExtendI32U: rdlo = rn; rdhi = 0.
890            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
891                if rdlo != rn {
892                    w(
893                        &mut b,
894                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
895                    );
896                }
897                w(&mut b, 0xE3A0_0000 | (reg_to_bits(rdhi) << 12));
898            }
899
900            // I64Extend8S / I64Extend16S: SXTB/SXTH then sign-fill the high word.
901            ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
902                w(
903                    &mut b,
904                    0xE6AF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
905                );
906                w(
907                    &mut b,
908                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
909                );
910            }
911            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
912                w(
913                    &mut b,
914                    0xE6BF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
915                );
916                w(
917                    &mut b,
918                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
919                );
920            }
921            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
922                if rdlo != rnlo {
923                    w(
924                        &mut b,
925                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
926                    );
927                }
928                w(
929                    &mut b,
930                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rnlo),
931                );
932            }
933
934            // I32WrapI64: take the low word. When rd == rnlo this is a genuine
935            // no-op (the one case where a NOP word is the correct encoding).
936            ArmOp::I32WrapI64 { rd, rnlo } => {
937                w(
938                    &mut b,
939                    0xE1A0_0000 | (reg_to_bits(rd) << 12) | reg_to_bits(rnlo),
940                );
941            }
942
943            // I64Add / I64Sub: the classic pair — ADDS lo + ADC hi (SUBS/SBC).
944            // The selector emits these as separate Adds/Adc ops; the fused
945            // variants are verification-constructed, but they encode for real.
946            ArmOp::I64Add {
947                rdlo,
948                rdhi,
949                rnlo,
950                rnhi,
951                rmlo,
952                rmhi,
953            } => {
954                dp_reg(
955                    &mut b,
956                    0xE090_0000, // ADDS
957                    reg_to_bits(rdlo),
958                    reg_to_bits(rnlo),
959                    reg_to_bits(rmlo),
960                );
961                dp_reg(
962                    &mut b,
963                    0xE0A0_0000, // ADC
964                    reg_to_bits(rdhi),
965                    reg_to_bits(rnhi),
966                    reg_to_bits(rmhi),
967                );
968            }
969            ArmOp::I64Sub {
970                rdlo,
971                rdhi,
972                rnlo,
973                rnhi,
974                rmlo,
975                rmhi,
976            } => {
977                dp_reg(
978                    &mut b,
979                    0xE050_0000, // SUBS
980                    reg_to_bits(rdlo),
981                    reg_to_bits(rnlo),
982                    reg_to_bits(rmlo),
983                );
984                dp_reg(
985                    &mut b,
986                    0xE0C0_0000, // SBC
987                    reg_to_bits(rdhi),
988                    reg_to_bits(rnhi),
989                    reg_to_bits(rmhi),
990                );
991            }
992
993            // I64And / I64Or / I64Xor: two independent word ops.
994            ArmOp::I64And {
995                rdlo,
996                rdhi,
997                rnlo,
998                rnhi,
999                rmlo,
1000                rmhi,
1001            }
1002            | ArmOp::I64Or {
1003                rdlo,
1004                rdhi,
1005                rnlo,
1006                rnhi,
1007                rmlo,
1008                rmhi,
1009            }
1010            | ArmOp::I64Xor {
1011                rdlo,
1012                rdhi,
1013                rnlo,
1014                rnhi,
1015                rmlo,
1016                rmhi,
1017            } => {
1018                let base = match op {
1019                    ArmOp::I64And { .. } => 0xE000_0000, // AND
1020                    ArmOp::I64Or { .. } => 0xE180_0000,  // ORR
1021                    _ => 0xE020_0000,                    // EOR
1022                };
1023                dp_reg(
1024                    &mut b,
1025                    base,
1026                    reg_to_bits(rdlo),
1027                    reg_to_bits(rnlo),
1028                    reg_to_bits(rmlo),
1029                );
1030                dp_reg(
1031                    &mut b,
1032                    base,
1033                    reg_to_bits(rdhi),
1034                    reg_to_bits(rnhi),
1035                    reg_to_bits(rmhi),
1036                );
1037            }
1038
1039            // I64DivU: binary long division — A32 transcription of the Thumb-2
1040            // #610/#613 arm (fixed-ABI marshal, zero-divisor trap, 64-round
1041            // shift-subtract core, quotient to R0:R1, result to rd pair).
1042            ArmOp::I64DivU {
1043                rdlo,
1044                rdhi,
1045                rnlo,
1046                rnhi,
1047                rmlo,
1048                rmhi,
1049                elide_zero_guard,
1050            } => {
1051                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1052                // #494 phase 2b: elided only under a certificate-discharged
1053                // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
1054                if !elide_zero_guard {
1055                    emit_a32_i64_divisor_zero_trap(&mut b);
1056                }
1057                w(&mut b, 0xE92D_00F0); // PUSH {R4-R7}
1058                for r in 4..8u32 {
1059                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1060                }
1061                div_loop(&mut b, 12); // counter in R12 (encoder scratch)
1062                w(&mut b, 0xE1A0_0004); // MOV R0, R4 (quotient lo)
1063                w(&mut b, 0xE1A0_1005); // MOV R1, R5 (quotient hi)
1064                w(&mut b, 0xE8BD_00F0); // POP {R4-R7}
1065                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1066            }
1067
1068            // I64DivS: sign-extract, unsigned core, conditional negate —
1069            // A32 transcription of the Thumb-2 arm.
1070            ArmOp::I64DivS {
1071                rdlo,
1072                rdhi,
1073                rnlo,
1074                rnhi,
1075                rmlo,
1076                rmhi,
1077                elide_zero_guard,
1078                elide_overflow_guard,
1079            } => {
1080                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1081                // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
1082                // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
1083                // the #633 overflow guard falls ONLY to
1084                // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
1085                // divisor-nonzero fact alone must keep it.
1086                if !elide_zero_guard {
1087                    emit_a32_i64_divisor_zero_trap(&mut b);
1088                }
1089                if !elide_overflow_guard {
1090                    // #633: INT64_MIN / -1 overflows — trap like the i32 path
1091                    // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
1092                    emit_a32_i64_divs_overflow_trap(&mut b);
1093                }
1094                w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1095                w(&mut b, 0xE021_9003); // EOR R9, R1, R3 (result sign in MSB)
1096                skip_negate_if_positive(&mut b, 1);
1097                negate64(&mut b, 0, 1);
1098                skip_negate_if_positive(&mut b, 3);
1099                negate64(&mut b, 2, 3);
1100                for r in 4..8u32 {
1101                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1102                }
1103                div_loop(&mut b, 8); // counter in R8 (saved above)
1104                w(&mut b, 0xE1A0_0004); // MOV R0, R4
1105                w(&mut b, 0xE1A0_1005); // MOV R1, R5
1106                skip_negate_if_positive(&mut b, 9);
1107                negate64(&mut b, 0, 1);
1108                w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1109                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1110            }
1111
1112            // I64RemU: same core as I64DivU, returns the remainder (R6:R7).
1113            ArmOp::I64RemU {
1114                rdlo,
1115                rdhi,
1116                rnlo,
1117                rnhi,
1118                rmlo,
1119                rmhi,
1120                elide_zero_guard,
1121            } => {
1122                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1123                if !elide_zero_guard {
1124                    emit_a32_i64_divisor_zero_trap(&mut b);
1125                }
1126                w(&mut b, 0xE92D_01F0); // PUSH {R4-R8}
1127                for r in 4..8u32 {
1128                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1129                }
1130                div_loop(&mut b, 8);
1131                w(&mut b, 0xE1A0_0006); // MOV R0, R6 (remainder lo)
1132                w(&mut b, 0xE1A0_1007); // MOV R1, R7 (remainder hi)
1133                w(&mut b, 0xE8BD_01F0); // POP {R4-R8}
1134                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1135            }
1136
1137            // I64RemS: remainder takes the DIVIDEND's sign (WASM semantics).
1138            ArmOp::I64RemS {
1139                rdlo,
1140                rdhi,
1141                rnlo,
1142                rnhi,
1143                rmlo,
1144                rmhi,
1145                elide_zero_guard,
1146            } => {
1147                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1148                if !elide_zero_guard {
1149                    emit_a32_i64_divisor_zero_trap(&mut b);
1150                }
1151                w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1152                w(&mut b, 0xE1A0_9001); // MOV R9, R1 (dividend sign)
1153                skip_negate_if_positive(&mut b, 1);
1154                negate64(&mut b, 0, 1);
1155                skip_negate_if_positive(&mut b, 3);
1156                negate64(&mut b, 2, 3);
1157                for r in 4..8u32 {
1158                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1159                }
1160                div_loop(&mut b, 8);
1161                w(&mut b, 0xE1A0_0006); // MOV R0, R6
1162                w(&mut b, 0xE1A0_1007); // MOV R1, R7
1163                skip_negate_if_positive(&mut b, 9);
1164                negate64(&mut b, 0, 1);
1165                w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1166                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1167            }
1168
1169            // Popcnt (i32): bit-twiddle expansion (no native A32 popcount),
1170            // mirroring the Thumb-2 arm's register contract (R11 + R12 as
1171            // scratch, shift-add fold, final AND #0x3F).
1172            ArmOp::Popcnt { rd, rm } => {
1173                let rd_b = reg_to_bits(rd);
1174                if rd != rm {
1175                    w(&mut b, 0xE1A0_0000 | (rd_b << 12) | reg_to_bits(rm)); // MOV rd, rm
1176                }
1177                // x = x - ((x >> 1) & 0x55555555)
1178                movw(&mut b, 12, 0x5555);
1179                movt(&mut b, 12, 0x5555);
1180                shift_imm(&mut b, LSR, 11, rd_b, 1);
1181                dp_reg(&mut b, 0xE000_0000, 11, 11, 12); // AND R11, R11, R12
1182                dp_reg(&mut b, 0xE040_0000, rd_b, rd_b, 11); // SUB rd, rd, R11
1183                // x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
1184                movw(&mut b, 12, 0x3333);
1185                movt(&mut b, 12, 0x3333);
1186                dp_reg(&mut b, 0xE000_0000, 11, rd_b, 12); // AND R11, rd, R12
1187                shift_imm(&mut b, LSR, rd_b, rd_b, 2);
1188                dp_reg(&mut b, 0xE000_0000, rd_b, rd_b, 12); // AND rd, rd, R12
1189                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11); // ADD rd, rd, R11
1190                // x = (x + (x >> 4)) & 0x0F0F0F0F
1191                shift_imm(&mut b, LSR, 11, rd_b, 4);
1192                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11); // ADD rd, rd, R11
1193                movw(&mut b, 12, 0x0F0F);
1194                movt(&mut b, 12, 0x0F0F);
1195                dp_reg(&mut b, 0xE000_0000, rd_b, rd_b, 12); // AND rd, rd, R12
1196                // x += x >> 8; x += x >> 16; x &= 0x3F
1197                shift_imm(&mut b, LSR, 11, rd_b, 8);
1198                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11);
1199                shift_imm(&mut b, LSR, 11, rd_b, 16);
1200                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 11);
1201                w(&mut b, 0xE200_003F | (rd_b << 16) | (rd_b << 12)); // AND rd, rd, #63
1202            }
1203
1204            // I64Popcnt: POPCNT(lo) + POPCNT(hi) — A32 transcription of the
1205            // Thumb-2 arm (R3/R4/R5 saved, mul-based per-word fold, high
1206            // result word rnhi cleared last, mirroring the Thumb contract).
1207            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
1208                let hi = reg_to_bits(rnhi);
1209                w(&mut b, 0xE92D_0038); // PUSH {R3, R4, R5}
1210                // #632 audit: route rnlo through R12 so a pair living at
1211                // (R3,R4) cannot read a clobbered R4 (sources read before any
1212                // scratch register they could occupy is written).
1213                w(&mut b, 0xE1A0_C000 | reg_to_bits(rnlo)); // MOV R12, rnlo
1214                w(&mut b, 0xE1A0_5000 | hi); //                MOV R5, rnhi
1215                w(&mut b, 0xE1A0_400C); //                     MOV R4, R12
1216                popcnt_word(&mut b, 4, 3);
1217                popcnt_word(&mut b, 5, 3);
1218                // #632: carry the count across the scratch restore in R12 —
1219                // rd is allocator-assigned and can land inside {R3,R4,R5};
1220                // the old `ADD rd, R4, R5` before the POP was destroyed by
1221                // the restore. R12 is never allocatable and never restored.
1222                dp_reg(&mut b, 0xE080_0000, 12, 4, 5); // ADD R12, R4, R5
1223                w(&mut b, 0xE8BD_0038); // POP {R3, R4, R5}
1224                w(&mut b, 0xE1A0_0000 | (reg_to_bits(rd) << 12) | 12); // MOV rd, R12
1225                w(&mut b, 0xE3A0_0000 | (hi << 12)); // MOV rnhi, #0 (i64 hi word)
1226            }
1227
1228            _ => return Ok(None),
1229        }
1230        Ok(Some(b))
1231    }
1232
1233    fn encode_arm(&self, op: &ArmOp) -> Result<Vec<u8>> {
1234        // #615: A32 multi-instruction expansions (i64 arithmetic/shift/rotate/
1235        // compare, SetCond/SelectMove, popcnt, ...). These ops were literal
1236        // NOPs on the A32 path — user-reachable via `--target cortex-r5` —
1237        // so the value silently vanished. Mirror of the #594 CallIndirect
1238        // early-return: if the expansion helper covers the op, its bytes are
1239        // the encoding.
1240        if let Some(bytes) = self.encode_arm_expanded(op)? {
1241            return Ok(bytes);
1242        }
1243        // #206: ARM32 register-offset loads/stores. `encode_mem_addr` only
1244        // returns the 12-bit immediate, so the immediate-form arms below
1245        // silently DROP `addr.offset_reg` — a runtime address index vanished,
1246        // turning `ldr rd,[rn,rm,#off]` into `ldr rd,[rn,#off]` (the access went
1247        // to the wrong address). Compute the effective base into IP and re-encode
1248        // against `[ip, #off]`, which is uniform for word/byte/halfword/signed.
1249        if let Some(bytes) = self.encode_arm_reg_offset_mem(op)? {
1250            return Ok(bytes);
1251        }
1252        // #594: call_indirect was encoded as a literal NOP on the A32 path
1253        // (`--target cortex-r5`) — the call never happened and the function
1254        // silently returned garbage. Emit the same three-instruction expansion
1255        // as the Thumb-2 path (R11 = function-pointer table base, R12 scratch):
1256        //   MOV r12, idx, LSL #2 ; LDR r12, [r11, r12] ; BLX r12
1257        if let ArmOp::CallIndirect {
1258            table_index_reg,
1259            table_size,
1260            table_byte_offset,
1261            null_check,
1262            type_check,
1263            ..
1264        } = op
1265        {
1266            return Ok(Self::encode_arm_call_indirect(
1267                table_index_reg,
1268                *table_size,
1269                *table_byte_offset,
1270                *null_check,
1271                *type_check,
1272            ));
1273        }
1274        let instr: u32 = match op {
1275            // Data processing instructions
1276            ArmOp::Add { rd, rn, op2 } => {
1277                let rd_bits = reg_to_bits(rd);
1278                let rn_bits = reg_to_bits(rn);
1279                let (op2_bits, i_flag) = encode_operand2(op2)?;
1280
1281                // ADD encoding: cond(4) | 00 | I(1) | 0100 | S(1) | Rn(4) | Rd(4) | operand2(12)
1282                0xE0800000 // condition=always(E), opcode=ADD(0100), S=0
1283                    | (i_flag << 25)
1284                    | (rn_bits << 16)
1285                    | (rd_bits << 12)
1286                    | op2_bits
1287            }
1288
1289            ArmOp::Sub { rd, rn, op2 } => {
1290                let rd_bits = reg_to_bits(rd);
1291                let rn_bits = reg_to_bits(rn);
1292                let (op2_bits, i_flag) = encode_operand2(op2)?;
1293
1294                // SUB encoding: opcode=0010
1295                0xE0400000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1296            }
1297
1298            // i64 support: ADDS, ADC, SUBS, SBC for ARM32
1299            ArmOp::Adds { rd, rn, op2 } => {
1300                let rd_bits = reg_to_bits(rd);
1301                let rn_bits = reg_to_bits(rn);
1302                let (op2_bits, i_flag) = encode_operand2(op2)?;
1303
1304                // ADDS encoding: opcode=0100, S=1
1305                0xE0900000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1306            }
1307
1308            ArmOp::Adc { rd, rn, op2 } => {
1309                let rd_bits = reg_to_bits(rd);
1310                let rn_bits = reg_to_bits(rn);
1311                let (op2_bits, i_flag) = encode_operand2(op2)?;
1312
1313                // ADC encoding: opcode=0101
1314                0xE0A00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1315            }
1316
1317            ArmOp::Subs { rd, rn, op2 } => {
1318                let rd_bits = reg_to_bits(rd);
1319                let rn_bits = reg_to_bits(rn);
1320                let (op2_bits, i_flag) = encode_operand2(op2)?;
1321
1322                // SUBS encoding: opcode=0010, S=1
1323                0xE0500000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1324            }
1325
1326            ArmOp::Sbc { rd, rn, op2 } => {
1327                let rd_bits = reg_to_bits(rd);
1328                let rn_bits = reg_to_bits(rn);
1329                let (op2_bits, i_flag) = encode_operand2(op2)?;
1330
1331                // SBC encoding: opcode=0110
1332                0xE0C00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1333            }
1334
1335            ArmOp::Mul { rd, rn, rm } => {
1336                let rd_bits = reg_to_bits(rd);
1337                let rn_bits = reg_to_bits(rn);
1338                let rm_bits = reg_to_bits(rm);
1339
1340                // MUL encoding: cond(4) | 000000 | A(1) | S(1) | Rd(4) | Rn(4) | Rs(4) | 1001 | Rm(4)
1341                0xE0000090 | (rd_bits << 16) | (rn_bits << 8) | rm_bits
1342            }
1343
1344            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
1345                let rdlo_bits = reg_to_bits(rdlo);
1346                let rdhi_bits = reg_to_bits(rdhi);
1347                let rn_bits = reg_to_bits(rn);
1348                let rm_bits = reg_to_bits(rm);
1349
1350                // UMULL encoding: cond(4) | 0000 1000 | RdHi(4) | RdLo(4) | Rm(4) | 1001 | Rn(4)
1351                0xE0800090 | (rdhi_bits << 16) | (rdlo_bits << 12) | (rm_bits << 8) | rn_bits
1352            }
1353
1354            ArmOp::Sdiv { rd, rn, rm } => {
1355                let rd_bits = reg_to_bits(rd);
1356                let rn_bits = reg_to_bits(rn);
1357                let rm_bits = reg_to_bits(rm);
1358
1359                // SDIV encoding: cond(4) | 01110001 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1360                // ARMv7-M and above
1361                0xE710F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1362            }
1363
1364            ArmOp::Udiv { rd, rn, rm } => {
1365                let rd_bits = reg_to_bits(rd);
1366                let rn_bits = reg_to_bits(rn);
1367                let rm_bits = reg_to_bits(rm);
1368
1369                // UDIV encoding: cond(4) | 01110011 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1370                // ARMv7-M and above
1371                0xE730F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1372            }
1373
1374            ArmOp::Mls { rd, rn, rm, ra } => {
1375                let rd_bits = reg_to_bits(rd);
1376                let rn_bits = reg_to_bits(rn);
1377                let rm_bits = reg_to_bits(rm);
1378                let ra_bits = reg_to_bits(ra);
1379
1380                // MLS encoding: cond(4) | 00000110 | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1381                // Rd = Ra - (Rn * Rm)
1382                0xE0600090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1383            }
1384
1385            ArmOp::Mla { rd, rn, rm, ra } => {
1386                let rd_bits = reg_to_bits(rd);
1387                let rn_bits = reg_to_bits(rn);
1388                let rm_bits = reg_to_bits(rm);
1389                let ra_bits = reg_to_bits(ra);
1390
1391                // MLA encoding: cond(4) | 0000001 S | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1392                // Rd = Ra + (Rn * Rm). Base 0xE0200090 (S=0).
1393                0xE0200090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1394            }
1395
1396            ArmOp::And { rd, rn, op2 } => {
1397                let rd_bits = reg_to_bits(rd);
1398                let rn_bits = reg_to_bits(rn);
1399                let (op2_bits, i_flag) = encode_operand2(op2)?;
1400
1401                // AND encoding: opcode=0000
1402                0xE0000000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1403            }
1404
1405            ArmOp::Orr { rd, rn, op2 } => {
1406                let rd_bits = reg_to_bits(rd);
1407                let rn_bits = reg_to_bits(rn);
1408                let (op2_bits, i_flag) = encode_operand2(op2)?;
1409
1410                // ORR encoding: opcode=1100
1411                0xE1800000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1412            }
1413
1414            ArmOp::Eor { rd, rn, op2 } => {
1415                let rd_bits = reg_to_bits(rd);
1416                let rn_bits = reg_to_bits(rn);
1417                let (op2_bits, i_flag) = encode_operand2(op2)?;
1418
1419                // EOR encoding: opcode=0001
1420                0xE0200000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1421            }
1422
1423            // Shift instructions
1424            ArmOp::Lsl { rd, rn, shift } => {
1425                let rd_bits = reg_to_bits(rd);
1426                let rn_bits = reg_to_bits(rn);
1427                let shift_bits = *shift & 0x1F;
1428
1429                // LSL encoding: MOV with shift
1430                0xE1A00000 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1431            }
1432
1433            ArmOp::Lsr { rd, rn, shift } => {
1434                let rd_bits = reg_to_bits(rd);
1435                let rn_bits = reg_to_bits(rn);
1436                let shift_bits = *shift & 0x1F;
1437
1438                // LSR encoding
1439                0xE1A00020 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1440            }
1441
1442            ArmOp::Asr { rd, rn, shift } => {
1443                let rd_bits = reg_to_bits(rd);
1444                let rn_bits = reg_to_bits(rn);
1445                let shift_bits = *shift & 0x1F;
1446
1447                // ASR encoding
1448                0xE1A00040 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1449            }
1450
1451            ArmOp::Ror { rd, rn, shift } => {
1452                let rd_bits = reg_to_bits(rd);
1453                let rn_bits = reg_to_bits(rn);
1454                let shift_bits = *shift & 0x1F;
1455
1456                // ROR encoding: MOV with ROR shift
1457                0xE1A00060 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1458            }
1459
1460            // Register-based shifts (ARM32)
1461            // LSL Rd, Rn, Rm: cond 0001101S 0000 Rd Rs 0001 Rn
1462            ArmOp::LslReg { rd, rn, rm } => {
1463                let rd_bits = reg_to_bits(rd);
1464                let rn_bits = reg_to_bits(rn);
1465                let rm_bits = reg_to_bits(rm);
1466                0xE1A00010 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1467            }
1468            ArmOp::LsrReg { rd, rn, rm } => {
1469                let rd_bits = reg_to_bits(rd);
1470                let rn_bits = reg_to_bits(rn);
1471                let rm_bits = reg_to_bits(rm);
1472                0xE1A00030 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1473            }
1474            ArmOp::AsrReg { rd, rn, rm } => {
1475                let rd_bits = reg_to_bits(rd);
1476                let rn_bits = reg_to_bits(rn);
1477                let rm_bits = reg_to_bits(rm);
1478                0xE1A00050 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1479            }
1480            ArmOp::RorReg { rd, rn, rm } => {
1481                let rd_bits = reg_to_bits(rd);
1482                let rn_bits = reg_to_bits(rn);
1483                let rm_bits = reg_to_bits(rm);
1484                0xE1A00070 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1485            }
1486
1487            // RSB (Reverse Subtract): Rd = imm - Rn
1488            ArmOp::Rsb { rd, rn, imm } => {
1489                let rd_bits = reg_to_bits(rd);
1490                let rn_bits = reg_to_bits(rn);
1491                // RSB encoding: cond(4) | 00 1 0011 S | Rn(4) | Rd(4) | imm12
1492                // Opcode for RSB = 0011, I=1 (immediate), S=0
1493                //
1494                // #681 class audit: the A32 imm12 is a rotate(4):imm8 modified
1495                // immediate; `*imm & 0xFF` silently encoded a WRONG constant
1496                // for imm > 0xFF (#378 masking class). All current emitters use
1497                // imm 32, so erroring here is byte-identical for real codegen.
1498                if *imm > 0xFF {
1499                    return Err(synth_core::Error::synthesis(
1500                        "A32 RSB immediate > 0xFF requires a rotated-immediate encoding \
1501                         (not supported) — materialize into a register",
1502                    ));
1503                }
1504                0xE2600000 | (rn_bits << 16) | (rd_bits << 12) | (*imm & 0xFF)
1505            }
1506
1507            // Bit manipulation instructions
1508            ArmOp::Clz { rd, rm } => {
1509                let rd_bits = reg_to_bits(rd);
1510                let rm_bits = reg_to_bits(rm);
1511
1512                // CLZ encoding: cond(4) | 00010110 | 1111 | Rd(4) | 1111 | 0001 | Rm(4)
1513                // ARMv5T and above
1514                0xE16F0F10 | (rd_bits << 12) | rm_bits
1515            }
1516
1517            ArmOp::Rbit { rd, rm } => {
1518                let rd_bits = reg_to_bits(rd);
1519                let rm_bits = reg_to_bits(rm);
1520
1521                // RBIT encoding: cond(4) | 01101111 | 1111 | Rd(4) | 1111 | 0011 | Rm(4)
1522                // ARMv6T2 and above
1523                0xE6FF0F30 | (rd_bits << 12) | rm_bits
1524            }
1525
1526            ArmOp::Sxtb { rd, rm } => {
1527                let rd_bits = reg_to_bits(rd);
1528                let rm_bits = reg_to_bits(rm);
1529
1530                // SXTB encoding: cond(4) | 01101010 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1531                // ARMv6 and above. rotate=00 for no rotation
1532                0xE6AF0070 | (rd_bits << 12) | rm_bits
1533            }
1534
1535            ArmOp::Sxth { rd, rm } => {
1536                let rd_bits = reg_to_bits(rd);
1537                let rm_bits = reg_to_bits(rm);
1538
1539                // SXTH encoding: cond(4) | 01101011 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1540                // ARMv6 and above. rotate=00 for no rotation
1541                0xE6BF0070 | (rd_bits << 12) | rm_bits
1542            }
1543
1544            ArmOp::Uxtb { rd, rm } => {
1545                let rd_bits = reg_to_bits(rd);
1546                let rm_bits = reg_to_bits(rm);
1547                // UXTB encoding: cond | 01101110 1111 Rd rotate 00 0111 Rm (rotate=00)
1548                0xE6EF0070 | (rd_bits << 12) | rm_bits
1549            }
1550
1551            ArmOp::Uxth { rd, rm } => {
1552                let rd_bits = reg_to_bits(rd);
1553                let rm_bits = reg_to_bits(rm);
1554                // UXTH encoding: cond | 01101111 1111 Rd rotate 00 0111 Rm (rotate=00)
1555                0xE6FF0070 | (rd_bits << 12) | rm_bits
1556            }
1557
1558            // Move instructions
1559            ArmOp::Mov { rd, op2 } => {
1560                let rd_bits = reg_to_bits(rd);
1561                let (op2_bits, i_flag) = encode_operand2(op2)?;
1562
1563                // MOV encoding: opcode=1101
1564                0xE1A00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1565            }
1566
1567            ArmOp::Mvn { rd, op2 } => {
1568                let rd_bits = reg_to_bits(rd);
1569                let (op2_bits, i_flag) = encode_operand2(op2)?;
1570
1571                // MVN encoding: opcode=1111
1572                0xE1E00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1573            }
1574
1575            // MOVW - Move Wide (ARM32)
1576            // Encoding: cond(4) | 0011 0000 | imm4(4) | Rd(4) | imm12(12)
1577            ArmOp::Movw { rd, imm16 } => {
1578                let rd_bits = reg_to_bits(rd);
1579                let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1580                let imm12 = (*imm16 as u32) & 0xFFF;
1581                0xE3000000 | (imm4 << 16) | (rd_bits << 12) | imm12
1582            }
1583
1584            // MOVT - Move Top (ARM32)
1585            // Encoding: cond(4) | 0011 0100 | imm4(4) | Rd(4) | imm12(12)
1586            ArmOp::Movt { rd, imm16 } => {
1587                let rd_bits = reg_to_bits(rd);
1588                let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1589                let imm12 = (*imm16 as u32) & 0xFFF;
1590                0xE3400000 | (imm4 << 16) | (rd_bits << 12) | imm12
1591            }
1592
1593            // #237: symbol-relative MOVW/MOVT (ARM mode) — addend in place, the
1594            // backend records the MOVW_ABS/MOVT_ABS relocation against `symbol`.
1595            ArmOp::MovwSym { rd, addend, .. } => {
1596                let rd_bits = reg_to_bits(rd);
1597                let v = (*addend as u32) & 0xffff;
1598                0xE3000000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1599            }
1600            ArmOp::MovtSym { rd, addend, .. } => {
1601                let rd_bits = reg_to_bits(rd);
1602                let v = ((*addend as u32) >> 16) & 0xffff;
1603                0xE3400000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1604            }
1605
1606            // #345: LdrSym is the Thumb-2 literal-pool address load. A32 mode is
1607            // not used for relocatable native-pointer objects; fail loudly rather
1608            // than miscompile if it is ever reached here.
1609            ArmOp::LdrSym { .. } => {
1610                return Err(synth_core::Error::synthesis(
1611                    "LdrSym (literal-pool address load) is Thumb-2-only",
1612                ));
1613            }
1614
1615            // Compare
1616            ArmOp::Cmp { rn, op2 } => {
1617                let rn_bits = reg_to_bits(rn);
1618                let (op2_bits, i_flag) = encode_operand2(op2)?;
1619
1620                // CMP encoding: opcode=1010, S=1
1621                0xE1500000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1622            }
1623
1624            // Compare Negative (CMN) - computes Rn + op2 and sets flags
1625            ArmOp::Cmn { rn, op2 } => {
1626                let rn_bits = reg_to_bits(rn);
1627                let (op2_bits, i_flag) = encode_operand2(op2)?;
1628
1629                // CMN encoding: opcode=1011, S=1
1630                0xE1700000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1631            }
1632
1633            // Load/Store
1634            ArmOp::Ldr { rd, addr } => {
1635                let rd_bits = reg_to_bits(rd);
1636                let (base_bits, offset_bits) = encode_mem_addr(addr);
1637
1638                // LDR encoding: cond(4) | 01 | I(1) | P(1) | U(1) | B(1) | W(1) | L(1) | Rn(4) | Rd(4) | offset(12)
1639                // P=1 (pre-indexed), U=1 (add offset), L=1 (load)
1640                0xE5900000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1641            }
1642
1643            ArmOp::Str { rd, addr } => {
1644                let rd_bits = reg_to_bits(rd);
1645                let (base_bits, offset_bits) = encode_mem_addr(addr);
1646
1647                // STR encoding: L=0 (store)
1648                0xE5800000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1649            }
1650
1651            // Sub-word loads (ARM32 encoding)
1652            ArmOp::Ldrb { rd, addr } => {
1653                let rd_bits = reg_to_bits(rd);
1654                let (base_bits, offset_bits) = encode_mem_addr(addr);
1655                // LDRB: LDR with B=1 (byte): cond|01|I|P|U|1|W|L|Rn|Rd|offset
1656                0xE5D00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1657            }
1658
1659            ArmOp::Ldrsb { rd, addr } => {
1660                let rd_bits = reg_to_bits(rd);
1661                let (base_bits, offset_bits) = encode_mem_addr(addr);
1662                // LDRSB (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1101|imm4L
1663                // Simplified with immediate offset
1664                let offset_val = offset_bits & 0xFF;
1665                let imm4h = (offset_val >> 4) & 0xF;
1666                let imm4l = offset_val & 0xF;
1667                0xE1D000D0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1668            }
1669
1670            ArmOp::Ldrh { rd, addr } => {
1671                let rd_bits = reg_to_bits(rd);
1672                let (base_bits, offset_bits) = encode_mem_addr(addr);
1673                // LDRH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1011|imm4L
1674                let offset_val = offset_bits & 0xFF;
1675                let imm4h = (offset_val >> 4) & 0xF;
1676                let imm4l = offset_val & 0xF;
1677                0xE1D000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1678            }
1679
1680            ArmOp::Ldrsh { rd, addr } => {
1681                let rd_bits = reg_to_bits(rd);
1682                let (base_bits, offset_bits) = encode_mem_addr(addr);
1683                // LDRSH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1111|imm4L
1684                let offset_val = offset_bits & 0xFF;
1685                let imm4h = (offset_val >> 4) & 0xF;
1686                let imm4l = offset_val & 0xF;
1687                0xE1D000F0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1688            }
1689
1690            // Sub-word stores (ARM32 encoding)
1691            ArmOp::Strb { rd, addr } => {
1692                let rd_bits = reg_to_bits(rd);
1693                let (base_bits, offset_bits) = encode_mem_addr(addr);
1694                // STRB: STR with B=1 (byte): cond|01|I|P|U|1|W|0|Rn|Rd|offset
1695                0xE5C00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1696            }
1697
1698            ArmOp::Strh { rd, addr } => {
1699                let rd_bits = reg_to_bits(rd);
1700                let (base_bits, offset_bits) = encode_mem_addr(addr);
1701                // STRH (misc store): cond|000|P|U|1|W|0|Rn|Rd|imm4H|1011|imm4L
1702                let offset_val = offset_bits & 0xFF;
1703                let imm4h = (offset_val >> 4) & 0xF;
1704                let imm4l = offset_val & 0xF;
1705                0xE1C000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1706            }
1707
1708            // Memory management (ARM32 encoding)
1709            ArmOp::MemorySize { rd } => {
1710                let rd_bits = reg_to_bits(rd);
1711                // MOV rd, R10, LSR #16  (memory size in bytes / 65536 = pages)
1712                // cond|000|1101|S|0000|Rd|shift5|type|0|Rm
1713                // LSR #16: shift5=10000, type=01
1714                0xE1A00820 | (rd_bits << 12) | 0x0A // Rm=R10, shift=16, LSR
1715            }
1716
1717            ArmOp::MemoryGrow { rd, .. } => {
1718                let rd_bits = reg_to_bits(rd);
1719                // On embedded, always fail: MOV rd, #-1
1720                0xE3E00000 | (rd_bits << 12) // MVN rd, #0 = MOV rd, #-1
1721            }
1722
1723            // Label pseudo-instruction: emits no machine code
1724            ArmOp::Label { .. } => {
1725                return Ok(Vec::new());
1726            }
1727
1728            // Branch instructions
1729            ArmOp::B { label: _ } => {
1730                // B encoding: cond(4) | 1010 | offset(24)
1731                // Simplified: branch to offset 0 (will be patched by linker/resolver)
1732                0xEA000000
1733            }
1734
1735            // Conditional branch to label (generic)
1736            ArmOp::Bcc { cond, label: _ } => {
1737                use synth_synthesis::Condition;
1738                let cond_bits: u32 = match cond {
1739                    Condition::EQ => 0x0,
1740                    Condition::NE => 0x1,
1741                    Condition::HS => 0x2,
1742                    Condition::LO => 0x3,
1743                    Condition::HI => 0x8,
1744                    Condition::LS => 0x9,
1745                    Condition::GE => 0xA,
1746                    Condition::LT => 0xB,
1747                    Condition::GT => 0xC,
1748                    Condition::LE => 0xD,
1749                };
1750                // B<cond> with offset 0 (will be patched)
1751                (cond_bits << 28) | 0x0A000000
1752            }
1753
1754            // BHS (Branch if Higher or Same) - used for bounds checking
1755            ArmOp::Bhs { label: _ } => {
1756                // BHS encoding: cond(2=HS) | 1010 | offset(24)
1757                0x2A000000 // BHS with offset 0
1758            }
1759
1760            // BLO (Branch if Lower) - complementary to BHS
1761            ArmOp::Blo { label: _ } => {
1762                // BLO encoding: cond(3=LO) | 1010 | offset(24)
1763                0x3A000000 // BLO with offset 0
1764            }
1765
1766            // Branch with numeric offset (in instructions)
1767            // ARM32 B instruction: offset is in instructions, stored as words
1768            // The offset is relative to PC+8 (due to ARM pipeline)
1769            ArmOp::BOffset { offset } => {
1770                // B encoding: cond(4) | 1010 | offset(24)
1771                // Offset is signed, in words (4-byte units)
1772                // ARM adds PC+8 to the offset, so we need to adjust:
1773                // target = PC + 8 + (offset * 4)
1774                // For backward branch of N instructions: offset = -(N + 2)
1775                // wrapping_sub keeps the encoder total under fuzzing (#186): an
1776                // extreme i32::MIN offset would otherwise overflow-panic; for any
1777                // real branch offset this is identical to `- 2`.
1778                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1779                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1780                0xEA000000 | offset_bits
1781            }
1782
1783            // Conditional branch with numeric offset
1784            ArmOp::BCondOffset { cond, offset } => {
1785                use synth_synthesis::Condition;
1786                let cond_bits: u32 = match cond {
1787                    Condition::EQ => 0x0,
1788                    Condition::NE => 0x1,
1789                    Condition::HS => 0x2,
1790                    Condition::LO => 0x3,
1791                    Condition::HI => 0x8,
1792                    Condition::LS => 0x9,
1793                    Condition::GE => 0xA,
1794                    Condition::LT => 0xB,
1795                    Condition::GT => 0xC,
1796                    Condition::LE => 0xD,
1797                };
1798                // B<cond> encoding: cond(4) | 1010 | offset(24)
1799                // wrapping_sub: total under fuzzing (#186), identical for real offsets.
1800                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1801                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1802                (cond_bits << 28) | 0x0A000000 | offset_bits
1803            }
1804
1805            ArmOp::Bl { label: _ } => {
1806                // BL encoding: cond(4) | 1011 | offset(24)
1807                0xEB000000
1808            }
1809
1810            ArmOp::Bx { rm } => {
1811                let rm_bits = reg_to_bits(rm);
1812
1813                // BX encoding: cond(4) | 000100101111111111110001 | Rm(4)
1814                0xE12FFF10 | rm_bits
1815            }
1816
1817            ArmOp::Blx { rm } => {
1818                let rm_bits = reg_to_bits(rm);
1819
1820                // BLX (register) encoding: cond(4) | 000100101111111111110011 | Rm(4)
1821                0xE12FFF30 | rm_bits
1822            }
1823
1824            ArmOp::Push { regs } => {
1825                // STMDB SP!, {regs} encoding: cond(4) | 100100 | 10 | 1101 | register_list(16)
1826                let mut reg_list: u32 = 0;
1827                for r in regs {
1828                    reg_list |= 1 << reg_to_bits(r);
1829                }
1830                0xE92D0000 | reg_list
1831            }
1832
1833            ArmOp::Pop { regs } => {
1834                // LDMIA SP!, {regs} encoding: cond(4) | 100010 | 11 | 1101 | register_list(16)
1835                let mut reg_list: u32 = 0;
1836                for r in regs {
1837                    reg_list |= 1 << reg_to_bits(r);
1838                }
1839                0xE8BD0000 | reg_list
1840            }
1841
1842            ArmOp::Nop => {
1843                // NOP encoding: MOV R0, R0
1844                0xE1A00000
1845            }
1846
1847            ArmOp::Udf { imm } => {
1848                // UDF (Undefined) encoding in ARM: 0xE7F000F0 | (imm12_hi << 8) | imm4_lo
1849                // We only use imm8, so split into imm4_hi and imm4_lo
1850                let imm8 = *imm as u32;
1851                0xE7F000F0 | ((imm8 & 0xF0) << 4) | (imm8 & 0x0F)
1852            }
1853
1854            // #615: handled by the `encode_arm_expanded` early return at the
1855            // top of this function — a real MOV{cond}/MOV pair now, never a
1856            // silent NOP again.
1857            ArmOp::Popcnt { .. } | ArmOp::SetCond { .. } | ArmOp::SelectMove { .. } => {
1858                unreachable!("handled by encode_arm_expanded (#615)")
1859            }
1860
1861            // Verification-only pseudo-ops: `synth-verify`'s ArmSemantics
1862            // models these, but NO codegen path constructs them (the selector
1863            // lowers select/locals/globals/br_table/call to real instruction
1864            // sequences before the encoder). Encoding one as a NOP silently
1865            // dropped the operation (#615 class); a typed Err keeps the
1866            // encoder total (Ok-or-Err, the `encoder_no_panic` contract)
1867            // while making any future reachability LOUD.
1868            ArmOp::Select { .. }
1869            | ArmOp::LocalGet { .. }
1870            | ArmOp::LocalSet { .. }
1871            | ArmOp::LocalTee { .. }
1872            | ArmOp::GlobalGet { .. }
1873            | ArmOp::GlobalSet { .. }
1874            | ArmOp::BrTable { .. }
1875            | ArmOp::Call { .. } => {
1876                return Err(synth_core::Error::synthesis(format!(
1877                    "verification-only pseudo-op {op:?} reached the A32 encoder — \
1878                     codegen lowers it before encoding; refusing to emit a silent NOP (#615)"
1879                )));
1880            }
1881
1882            // #594: CallIndirect is expanded to a real multi-instruction
1883            // sequence by the early return at the top of this function —
1884            // it must NEVER fall through to a silent NOP again.
1885            ArmOp::CallIndirect { .. } => {
1886                unreachable!("CallIndirect handled by encode_arm_call_indirect (#594)")
1887            }
1888
1889            // #615: every i64 op (and I32WrapI64) is expanded to a real A32
1890            // multi-instruction sequence by `encode_arm_expanded` — the
1891            // "encode as NOP for now" era ended with the value silently
1892            // vanishing on `--target cortex-r5`.
1893            ArmOp::I64Add { .. }
1894            | ArmOp::I64Sub { .. }
1895            | ArmOp::I64DivS { .. }
1896            | ArmOp::I64DivU { .. }
1897            | ArmOp::I64RemS { .. }
1898            | ArmOp::I64RemU { .. }
1899            | ArmOp::I64Clz { .. }
1900            | ArmOp::I64Ctz { .. }
1901            | ArmOp::I64Popcnt { .. }
1902            | ArmOp::I64And { .. }
1903            | ArmOp::I64Or { .. }
1904            | ArmOp::I64Xor { .. }
1905            | ArmOp::I64Eqz { .. }
1906            | ArmOp::I64Eq { .. }
1907            | ArmOp::I64Ne { .. }
1908            | ArmOp::I64LtS { .. }
1909            | ArmOp::I64LtU { .. }
1910            | ArmOp::I64LeS { .. }
1911            | ArmOp::I64LeU { .. }
1912            | ArmOp::I64GtS { .. }
1913            | ArmOp::I64GtU { .. }
1914            | ArmOp::I64GeS { .. }
1915            | ArmOp::I64GeU { .. }
1916            | ArmOp::I64Const { .. }
1917            | ArmOp::I64Ldr { .. }
1918            | ArmOp::I64Str { .. }
1919            | ArmOp::I64ExtendI32S { .. }
1920            | ArmOp::I64ExtendI32U { .. }
1921            | ArmOp::I64Extend8S { .. }
1922            | ArmOp::I64Extend16S { .. }
1923            | ArmOp::I64Extend32S { .. }
1924            | ArmOp::I32WrapI64 { .. } => {
1925                unreachable!("handled by encode_arm_expanded (#615)")
1926            }
1927
1928            // f32 VFP single-precision instructions
1929            ArmOp::F32Add { sd, sn, sm } => encode_vfp_3reg(0xEE300A00, sd, sn, sm)?,
1930            ArmOp::F32Sub { sd, sn, sm } => encode_vfp_3reg(0xEE300A40, sd, sn, sm)?,
1931            ArmOp::F32Mul { sd, sn, sm } => encode_vfp_3reg(0xEE200A00, sd, sn, sm)?,
1932            ArmOp::F32Div { sd, sn, sm } => encode_vfp_3reg(0xEE800A00, sd, sn, sm)?,
1933            ArmOp::F32Abs { sd, sm } => encode_vfp_2reg(0xEEB00AC0, sd, sm)?,
1934            ArmOp::F32Neg { sd, sm } => encode_vfp_2reg(0xEEB10A40, sd, sm)?,
1935            ArmOp::F32Sqrt { sd, sm } => encode_vfp_2reg(0xEEB10AC0, sd, sm)?,
1936
1937            // f32 pseudo-ops — multi-instruction sequences
1938            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
1939            ArmOp::F32Ceil { sd, sm } => {
1940                return self.encode_arm_f32_rounding(sd, sm, 0b01); // Round toward +Inf
1941            }
1942            ArmOp::F32Floor { sd, sm } => {
1943                return self.encode_arm_f32_rounding(sd, sm, 0b10); // Round toward -Inf
1944            }
1945            ArmOp::F32Trunc { sd, sm } => {
1946                return self.encode_arm_f32_rounding(sd, sm, 0b11); // VCVT toward zero
1947            }
1948            ArmOp::F32Nearest { sd, sm } => {
1949                return self.encode_arm_f32_rounding(sd, sm, 0b00); // VCVT to nearest
1950            }
1951            ArmOp::F32Min { sd, sn, sm } => {
1952                return self.encode_arm_f32_minmax(sd, sn, sm, true);
1953            }
1954            ArmOp::F32Max { sd, sn, sm } => {
1955                return self.encode_arm_f32_minmax(sd, sn, sm, false);
1956            }
1957            ArmOp::F32Copysign { sd, sn, sm } => {
1958                return self.encode_arm_f32_copysign(sd, sn, sm);
1959            }
1960
1961            // f32 comparisons — multi-instruction: VCMP + VMRS + conditional MOV
1962            ArmOp::F32Eq { rd, sn, sm } => {
1963                return self.encode_arm_f32_compare(rd, sn, sm, 0x0); // EQ
1964            }
1965            ArmOp::F32Ne { rd, sn, sm } => {
1966                return self.encode_arm_f32_compare(rd, sn, sm, 0x1); // NE
1967            }
1968            ArmOp::F32Lt { rd, sn, sm } => {
1969                return self.encode_arm_f32_compare(rd, sn, sm, 0x4); // MI (less than)
1970            }
1971            ArmOp::F32Le { rd, sn, sm } => {
1972                return self.encode_arm_f32_compare(rd, sn, sm, 0x9); // LS (less or same)
1973            }
1974            ArmOp::F32Gt { rd, sn, sm } => {
1975                return self.encode_arm_f32_compare(rd, sn, sm, 0xC); // GT
1976            }
1977            ArmOp::F32Ge { rd, sn, sm } => {
1978                return self.encode_arm_f32_compare(rd, sn, sm, 0xA); // GE
1979            }
1980
1981            // f32 const — multi-instruction: MOVW + MOVT + VMOV
1982            ArmOp::F32Const { sd, value } => {
1983                return self.encode_arm_f32_const(sd, *value);
1984            }
1985
1986            ArmOp::F32Load { sd, addr } => encode_vfp_ldst(0xED900A00, sd, addr)?,
1987            ArmOp::F32Store { sd, addr } => encode_vfp_ldst(0xED800A00, sd, addr)?,
1988
1989            // f32 conversions — multi-instruction sequences
1990            ArmOp::F32ConvertI32S { sd, rm } => {
1991                return self.encode_arm_f32_convert_i32(sd, rm, true);
1992            }
1993            ArmOp::F32ConvertI32U { sd, rm } => {
1994                return self.encode_arm_f32_convert_i32(sd, rm, false);
1995            }
1996            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
1997                return Err(synth_core::Error::synthesis(
1998                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
1999                ));
2000            }
2001            ArmOp::F32ReinterpretI32 { sd, rm } => encode_vmov_core_sreg(true, sd, rm)?,
2002            ArmOp::I32ReinterpretF32 { rd, sm } => encode_vmov_core_sreg(false, sm, rd)?,
2003            ArmOp::I32TruncF32S { rd, sm } => {
2004                return self.encode_arm_i32_trunc_f32(rd, sm, true);
2005            }
2006            ArmOp::I32TruncF32U { rd, sm } => {
2007                return self.encode_arm_i32_trunc_f32(rd, sm, false);
2008            }
2009
2010            // f64 VFP double-precision instructions (ARM32)
2011            // F64 arithmetic: same as F32 but with sz=1 (bit 8 = 1, cp11 = 0xB)
2012            ArmOp::F64Add { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B00, dd, dn, dm)?,
2013            ArmOp::F64Sub { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B40, dd, dn, dm)?,
2014            ArmOp::F64Mul { dd, dn, dm } => encode_vfp_3reg_f64(0xEE200B00, dd, dn, dm)?,
2015            ArmOp::F64Div { dd, dn, dm } => encode_vfp_3reg_f64(0xEE800B00, dd, dn, dm)?,
2016            ArmOp::F64Abs { dd, dm } => encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?,
2017            ArmOp::F64Neg { dd, dm } => encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?,
2018            ArmOp::F64Sqrt { dd, dm } => encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?,
2019
2020            // f64 pseudo-ops
2021            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
2022            ArmOp::F64Ceil { dd, dm } => {
2023                return self.encode_arm_f64_rounding(dd, dm, 0b01);
2024            }
2025            ArmOp::F64Floor { dd, dm } => {
2026                return self.encode_arm_f64_rounding(dd, dm, 0b10);
2027            }
2028            ArmOp::F64Trunc { dd, dm } => {
2029                return self.encode_arm_f64_rounding(dd, dm, 0b11);
2030            }
2031            ArmOp::F64Nearest { dd, dm } => {
2032                return self.encode_arm_f64_rounding(dd, dm, 0b00);
2033            }
2034            ArmOp::F64Min { dd, dn, dm } => {
2035                return self.encode_arm_f64_minmax(dd, dn, dm, true);
2036            }
2037            ArmOp::F64Max { dd, dn, dm } => {
2038                return self.encode_arm_f64_minmax(dd, dn, dm, false);
2039            }
2040            ArmOp::F64Copysign { dd, dn, dm } => {
2041                return self.encode_arm_f64_copysign(dd, dn, dm);
2042            }
2043
2044            // f64 comparisons
2045            ArmOp::F64Eq { rd, dn, dm } => {
2046                return self.encode_arm_f64_compare(rd, dn, dm, 0x0);
2047            }
2048            ArmOp::F64Ne { rd, dn, dm } => {
2049                return self.encode_arm_f64_compare(rd, dn, dm, 0x1);
2050            }
2051            ArmOp::F64Lt { rd, dn, dm } => {
2052                return self.encode_arm_f64_compare(rd, dn, dm, 0x4);
2053            }
2054            ArmOp::F64Le { rd, dn, dm } => {
2055                return self.encode_arm_f64_compare(rd, dn, dm, 0x9);
2056            }
2057            ArmOp::F64Gt { rd, dn, dm } => {
2058                return self.encode_arm_f64_compare(rd, dn, dm, 0xC);
2059            }
2060            ArmOp::F64Ge { rd, dn, dm } => {
2061                return self.encode_arm_f64_compare(rd, dn, dm, 0xA);
2062            }
2063
2064            ArmOp::F64Const { dd, value } => {
2065                return self.encode_arm_f64_const(dd, *value);
2066            }
2067
2068            ArmOp::F64Load { dd, addr } => encode_vfp_ldst_f64(0xED900B00, dd, addr)?,
2069            ArmOp::F64Store { dd, addr } => encode_vfp_ldst_f64(0xED800B00, dd, addr)?,
2070
2071            ArmOp::F64ConvertI32S { dd, rm } => {
2072                return self.encode_arm_f64_convert_i32(dd, rm, true);
2073            }
2074            ArmOp::F64ConvertI32U { dd, rm } => {
2075                return self.encode_arm_f64_convert_i32(dd, rm, false);
2076            }
2077            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
2078                return Err(synth_core::Error::synthesis(
2079                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2080                ));
2081            }
2082            ArmOp::F64PromoteF32 { dd, sm } => {
2083                return self.encode_arm_f64_promote_f32(dd, sm);
2084            }
2085            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => {
2086                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?
2087            }
2088            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => {
2089                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?
2090            }
2091            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
2092                return Err(synth_core::Error::synthesis(
2093                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
2094                ));
2095            }
2096            ArmOp::I32TruncF64S { rd, dm } => {
2097                return self.encode_arm_i32_trunc_f64(rd, dm, true);
2098            }
2099            ArmOp::I32TruncF64U { rd, dm } => {
2100                return self.encode_arm_i32_trunc_f64(rd, dm, false);
2101            }
2102            // #615: multi-instruction i64 sequences — expanded to real A32 by
2103            // `encode_arm_expanded`, no longer "Thumb-2 only" NOPs.
2104            ArmOp::I64SetCond { .. }
2105            | ArmOp::I64SetCondZ { .. }
2106            | ArmOp::I64Mul { .. }
2107            | ArmOp::I64Shl { .. }
2108            | ArmOp::I64ShrS { .. }
2109            | ArmOp::I64ShrU { .. }
2110            | ArmOp::I64Rotl { .. }
2111            | ArmOp::I64Rotr { .. } => {
2112                unreachable!("handled by encode_arm_expanded (#615)")
2113            }
2114
2115            // MVE instructions — Thumb-2 only (Cortex-M55 is always Thumb-2)
2116            ArmOp::MveLoad { .. }
2117            | ArmOp::MveStore { .. }
2118            | ArmOp::MveConst { .. }
2119            | ArmOp::MveAnd { .. }
2120            | ArmOp::MveOrr { .. }
2121            | ArmOp::MveEor { .. }
2122            | ArmOp::MveMvn { .. }
2123            | ArmOp::MveBic { .. }
2124            | ArmOp::MveAddI { .. }
2125            | ArmOp::MveSubI { .. }
2126            | ArmOp::MveMulI { .. }
2127            | ArmOp::MveNegI { .. }
2128            | ArmOp::MveCmpEqI { .. }
2129            | ArmOp::MveCmpNeI { .. }
2130            | ArmOp::MveCmpLtS { .. }
2131            | ArmOp::MveCmpLtU { .. }
2132            | ArmOp::MveCmpGtS { .. }
2133            | ArmOp::MveCmpGtU { .. }
2134            | ArmOp::MveCmpLeS { .. }
2135            | ArmOp::MveCmpLeU { .. }
2136            | ArmOp::MveCmpGeS { .. }
2137            | ArmOp::MveCmpGeU { .. }
2138            | ArmOp::MveDup { .. }
2139            | ArmOp::MveExtractLane { .. }
2140            | ArmOp::MveInsertLane { .. }
2141            | ArmOp::MveAddF32 { .. }
2142            | ArmOp::MveSubF32 { .. }
2143            | ArmOp::MveMulF32 { .. }
2144            | ArmOp::MveNegF32 { .. }
2145            | ArmOp::MveAbsF32 { .. }
2146            | ArmOp::MveCmpEqF32 { .. }
2147            | ArmOp::MveCmpNeF32 { .. }
2148            | ArmOp::MveCmpLtF32 { .. }
2149            | ArmOp::MveCmpLeF32 { .. }
2150            | ArmOp::MveCmpGtF32 { .. }
2151            | ArmOp::MveCmpGeF32 { .. }
2152            | ArmOp::MveDupF32 { .. }
2153            | ArmOp::MveExtractLaneF32 { .. }
2154            | ArmOp::MveReplaceLaneF32 { .. }
2155            | ArmOp::MveDivF32 { .. }
2156            | ArmOp::MveSqrtF32 { .. } => {
2157                // MVE (Helium) is a Thumb-2-only extension (Cortex-M55); there
2158                // is no A32 encoding. The selector only emits MVE ops for
2159                // Thumb targets — a NOP here silently dropped the vector op
2160                // if that invariant ever broke (#615 class). Err keeps the
2161                // encoder total and the failure loud.
2162                return Err(synth_core::Error::synthesis(format!(
2163                    "MVE op {op:?} has no A32 (ARM-mode) encoding — MVE is Thumb-2 only (#615)"
2164                )));
2165            }
2166        };
2167
2168        // ARM32 instructions are little-endian
2169        Ok(instr.to_le_bytes().to_vec())
2170    }
2171
2172    // === ARM32 VFP multi-instruction helpers ===
2173
2174    /// Encode F32 comparison as ARM32: VCMP.F32 + VMRS + MOV rd,#0 + MOVcond rd,#1
2175    fn encode_arm_f32_compare(
2176        &self,
2177        rd: &Reg,
2178        sn: &VfpReg,
2179        sm: &VfpReg,
2180        cond_code: u32,
2181    ) -> Result<Vec<u8>> {
2182        let mut bytes = Vec::new();
2183
2184        // VCMP.F32 Sn, Sm: 0xEEB40A40 with Sn in Vd position, Sm in Vm position
2185        let sn_num = vfp_sreg_to_num(sn)?;
2186        let sm_num = vfp_sreg_to_num(sm)?;
2187        let (vd, d) = encode_sreg(sn_num);
2188        let (vm, m) = encode_sreg(sm_num);
2189        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2190        bytes.extend_from_slice(&vcmp.to_le_bytes());
2191
2192        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
2193        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2194
2195        // MOV rd, #0: 0xE3A0_0000 | (rd << 12)
2196        let rd_bits = reg_to_bits(rd);
2197        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2198        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2199
2200        // MOVcond rd, #1: cond(4) | 0011 1010 0000 rd(4) 0000 0000 0001
2201        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2202        bytes.extend_from_slice(&mov_one.to_le_bytes());
2203
2204        Ok(bytes)
2205    }
2206
2207    /// Encode F32 constant load as ARM32: MOVW Rt,#lo16 + MOVT Rt,#hi16 + VMOV Sd,Rt
2208    fn encode_arm_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
2209        let mut bytes = Vec::new();
2210        let bits = value.to_bits();
2211
2212        // Use R12 as temp register for constant loading
2213        let rt: u32 = 12; // R12/IP
2214
2215        // MOVW R12, #lo16: 0xE300_C000 | (imm4 << 16) | imm12
2216        let lo16 = bits & 0xFFFF;
2217        let movw = 0xE3000000 | (rt << 12) | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2218        bytes.extend_from_slice(&movw.to_le_bytes());
2219
2220        // MOVT R12, #hi16: 0xE340_C000 | (imm4 << 16) | imm12
2221        let hi16 = (bits >> 16) & 0xFFFF;
2222        let movt = 0xE3400000 | (rt << 12) | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2223        bytes.extend_from_slice(&movt.to_le_bytes());
2224
2225        // VMOV Sd, R12
2226        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
2227        bytes.extend_from_slice(&vmov.to_le_bytes());
2228
2229        Ok(bytes)
2230    }
2231
2232    /// Encode VMOV + VCVT.F32.S32/U32 as ARM32
2233    fn encode_arm_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2234        let mut bytes = Vec::new();
2235
2236        // VMOV Sd, Rm — move integer to VFP register
2237        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
2238        bytes.extend_from_slice(&vmov.to_le_bytes());
2239
2240        // VCVT.F32.S32 Sd, Sd (signed) or VCVT.F32.U32 Sd, Sd (unsigned).
2241        // The "op" bit (bit 7) selects signedness: 1 = signed (S32), 0 =
2242        // unsigned (U32). So signed = 0xEEB80AC0, unsigned = 0xEEB80A40 —
2243        // objdump confirms 0xEEB80A40 decodes to `vcvt.f32.u32` (GI-FPU-002:
2244        // the two were previously swapped, silently making `convert_i32_s`
2245        // an unsigned conversion).
2246        let sd_num = vfp_sreg_to_num(sd)?;
2247        let (vd, d) = encode_sreg(sd_num);
2248        let (vm, m) = encode_sreg(sd_num); // same register as source
2249        let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
2250        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2251        bytes.extend_from_slice(&vcvt.to_le_bytes());
2252
2253        Ok(bytes)
2254    }
2255
2256    /// Encode F32 rounding pseudo-op as ARM32 via VCVT to integer and back.
2257    /// mode: 0b00=nearest, 0b01=floor(-Inf), 0b10=ceil(+Inf), 0b11=trunc(zero)
2258    /// Strategy: VCVT.S32.F32 Sd, Sm (toward zero), then VCVT.F32.S32 Sd, Sd
2259    /// For ceil/floor/nearest, we use VCVTR (round toward mode) + convert back.
2260    /// Simplified: convert to int (toward zero for trunc) then back to float.
2261    /// Encode F32 rounding as ARM32.
2262    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2263    ///
2264    /// For trunc (mode=0b11): uses VCVTR.S32.F32 (always rounds toward zero).
2265    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant
2266    /// which honours FPSCR rmode), then restores FPSCR.
2267    fn encode_arm_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2268        let mut bytes = Vec::new();
2269        let sm_num = vfp_sreg_to_num(sm)?;
2270        let sd_num = vfp_sreg_to_num(sd)?;
2271        let (vd_s, d_s) = encode_sreg(sd_num);
2272        let (vm_s, m_s) = encode_sreg(sm_num);
2273
2274        if mode == 0b11 {
2275            // Trunc (toward zero): VCVTR.S32.F32 — the "R" variant always truncates.
2276            // 0xEEBD0AC0: bit[7]=1 => round toward zero regardless of FPSCR
2277            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2278            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2279        } else {
2280            // ceil/floor/nearest: manipulate FPSCR rounding mode
2281            let rt: u32 = 12; // R12/IP as temp
2282
2283            // VMRS R12, FPSCR
2284            let vmrs = 0xEEF10A10 | (rt << 12);
2285            bytes.extend_from_slice(&vmrs.to_le_bytes());
2286
2287            // BIC R12, R12, #(3 << 22) — clear RMode bits [23:22]
2288            // 3<<22 = 0x00C00000. ARM rotated imm: 0x03 ror 10 (rotation=5, imm8=0x03)
2289            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2290            bytes.extend_from_slice(&bic.to_le_bytes());
2291
2292            // ORR R12, R12, #(mode << 22) — set desired rounding mode
2293            if mode != 0 {
2294                // mode<<22: rotation=5, imm8=mode
2295                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2296                bytes.extend_from_slice(&orr.to_le_bytes());
2297            }
2298
2299            // VMSR FPSCR, R12
2300            let vmsr = 0xEEE10A10 | (rt << 12);
2301            bytes.extend_from_slice(&vmsr.to_le_bytes());
2302
2303            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rounding mode
2304            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2305            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2306
2307            // Restore FPSCR: clear rmode bits back to nearest (default)
2308            bytes.extend_from_slice(&vmrs.to_le_bytes());
2309            bytes.extend_from_slice(&bic.to_le_bytes());
2310            bytes.extend_from_slice(&vmsr.to_le_bytes());
2311        }
2312
2313        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
2314        let (vd2, d2) = encode_sreg(sd_num);
2315        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
2316        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2317
2318        Ok(bytes)
2319    }
2320
2321    /// Encode F32 min/max as ARM32: VCMP + VMRS + conditional VMOV
2322    fn encode_arm_f32_minmax(
2323        &self,
2324        sd: &VfpReg,
2325        sn: &VfpReg,
2326        sm: &VfpReg,
2327        is_min: bool,
2328    ) -> Result<Vec<u8>> {
2329        let mut bytes = Vec::new();
2330        let sn_num = vfp_sreg_to_num(sn)?;
2331        let sm_num = vfp_sreg_to_num(sm)?;
2332        let sd_num = vfp_sreg_to_num(sd)?;
2333
2334        // VMOV Sd, Sn (start with first operand)
2335        let (vd, d) = encode_sreg(sd_num);
2336        let (vn, n) = encode_sreg(sn_num);
2337        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2338        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2339
2340        // VCMP.F32 Sn, Sm
2341        let (vm, m) = encode_sreg(sm_num);
2342        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2343        bytes.extend_from_slice(&vcmp.to_le_bytes());
2344
2345        // VMRS APSR_nzcv, FPSCR
2346        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2347
2348        // For min: if Sn > Sm (GT), use Sm. Condition = GT (0xC)
2349        // For max: if Sn < Sm (MI/LT), use Sm. Condition = MI (0x4)
2350        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2351
2352        // VMOV{cond} Sd, Sm — conditional VMOV
2353        let vmov_cond = (cond << 28) | 0x0EB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2354        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2355
2356        Ok(bytes)
2357    }
2358
2359    /// Encode F32 copysign as ARM32: extract sign from Sm, magnitude from Sn
2360    fn encode_arm_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2361        let mut bytes = Vec::new();
2362
2363        // VMOV R12, Sm (get sign source bits)
2364        let vmov_sm = encode_vmov_core_sreg(false, sm, &Reg::R12)?;
2365        bytes.extend_from_slice(&vmov_sm.to_le_bytes());
2366
2367        // VMOV R0, Sn (get magnitude source bits) — use R0 as temp
2368        let vmov_sn = encode_vmov_core_sreg(false, sn, &Reg::R0)?;
2369        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2370
2371        // AND R12, R12, #0x80000000 (keep only sign bit)
2372        // Thumb-2 constant 0x80000000 needs special encoding; in ARM32 use rotated imm
2373        // 0x80000000 = 0x02 rotated right by 2 (rotation=1, imm8=0x02)
2374        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2375        bytes.extend_from_slice(&and_sign.to_le_bytes());
2376
2377        // BIC R0, R0, #0x80000000 (clear sign bit from magnitude)
2378        // R0 = register 0, so Rn and Rd fields are 0
2379        let bic_sign = 0xE3C00000u32 | (1 << 8) | 0x02;
2380        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2381
2382        // ORR R0, R0, R12 (combine sign + magnitude)
2383        // R0 = register 0, so Rn and Rd fields are 0
2384        let orr = 0xE1800000u32 | 12;
2385        bytes.extend_from_slice(&orr.to_le_bytes());
2386
2387        // VMOV Sd, R0
2388        let vmov_result = encode_vmov_core_sreg(true, sd, &Reg::R0)?;
2389        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2390
2391        Ok(bytes)
2392    }
2393
2394    /// Encode F64 comparison as ARM32: VCMP.F64 + VMRS + MOV rd,#0 + MOVcond rd,#1
2395    fn encode_arm_f64_compare(
2396        &self,
2397        rd: &Reg,
2398        dn: &VfpReg,
2399        dm: &VfpReg,
2400        cond_code: u32,
2401    ) -> Result<Vec<u8>> {
2402        let mut bytes = Vec::new();
2403
2404        // VCMP.F64 Dn, Dm: 0xEEB40B40 with Dn in Vd position, Dm in Vm position
2405        let dn_num = vfp_dreg_to_num(dn)?;
2406        let dm_num = vfp_dreg_to_num(dm)?;
2407        let (vd, d) = encode_dreg(dn_num);
2408        let (vm, m) = encode_dreg(dm_num);
2409        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2410        bytes.extend_from_slice(&vcmp.to_le_bytes());
2411
2412        // VMRS APSR_nzcv, FPSCR
2413        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2414
2415        // MOV rd, #0
2416        let rd_bits = reg_to_bits(rd);
2417        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2418        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2419
2420        // MOVcond rd, #1
2421        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2422        bytes.extend_from_slice(&mov_one.to_le_bytes());
2423
2424        Ok(bytes)
2425    }
2426
2427    /// Encode F64 constant load as ARM32: MOVW + MOVT + MOVW + MOVT + VMOV
2428    fn encode_arm_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
2429        let mut bytes = Vec::new();
2430        let bits = value.to_bits();
2431        let lo32 = bits as u32;
2432        let hi32 = (bits >> 32) as u32;
2433
2434        // Load low 32 bits into R0 (Rd field = 0 for R0)
2435        let lo16 = lo32 & 0xFFFF;
2436        let movw_r0 = 0xE3000000 | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2437        bytes.extend_from_slice(&movw_r0.to_le_bytes());
2438        let hi16 = (lo32 >> 16) & 0xFFFF;
2439        let movt_r0 = 0xE3400000 | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2440        bytes.extend_from_slice(&movt_r0.to_le_bytes());
2441
2442        // Load high 32 bits into R12
2443        let lo16 = hi32 & 0xFFFF;
2444        let movw_r12 = 0xE3000000 | ((lo16 >> 12) << 16) | (12 << 12) | (lo16 & 0xFFF);
2445        bytes.extend_from_slice(&movw_r12.to_le_bytes());
2446        let hi16 = (hi32 >> 16) & 0xFFFF;
2447        let movt_r12 = 0xE3400000 | ((hi16 >> 12) << 16) | (12 << 12) | (hi16 & 0xFFF);
2448        bytes.extend_from_slice(&movt_r12.to_le_bytes());
2449
2450        // VMOV Dd, R0, R12
2451        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
2452        bytes.extend_from_slice(&vmov.to_le_bytes());
2453
2454        Ok(bytes)
2455    }
2456
2457    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as ARM32
2458    fn encode_arm_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2459        let mut bytes = Vec::new();
2460
2461        // Use S0 as intermediate: VMOV S0, Rm
2462        let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
2463        bytes.extend_from_slice(&vmov.to_le_bytes());
2464
2465        // VCVT.F64.S32 Dd, S0 (signed) or VCVT.F64.U32 Dd, S0 (unsigned)
2466        // Base: 0xEEB80B40 (signed) or 0xEEB80BC0 (unsigned)
2467        let dd_num = vfp_dreg_to_num(dd)?;
2468        let (vd, d) = encode_dreg(dd_num);
2469        let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
2470        // S0 is register 0: Vm=0, M=0
2471        let vcvt = base | (d << 22) | (vd << 12);
2472        bytes.extend_from_slice(&vcvt.to_le_bytes());
2473
2474        Ok(bytes)
2475    }
2476
2477    /// Encode VCVT.F64.F32 Dd, Sm as ARM32 (f32 to f64 promotion)
2478    fn encode_arm_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2479        let dd_num = vfp_dreg_to_num(dd)?;
2480        let sm_num = vfp_sreg_to_num(sm)?;
2481        let (vd, d) = encode_dreg(dd_num);
2482        let (vm, m) = encode_sreg(sm_num);
2483
2484        // VCVT.F64.F32 Dd, Sm: 0xEEB70AC0
2485        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
2486        Ok(vcvt.to_le_bytes().to_vec())
2487    }
2488
2489    /// Encode VCVT.S32/U32.F64 Sd, Dm + VMOV Rd, Sd as ARM32
2490    fn encode_arm_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2491        let mut bytes = Vec::new();
2492        let dm_num = vfp_dreg_to_num(dm)?;
2493        let (vm, m) = encode_dreg(dm_num);
2494
2495        // VCVT.S32.F64 S0, Dm (toward zero) or VCVT.U32.F64 S0, Dm
2496        // S0: Vd=0, D=0
2497        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
2498        let vcvt = base | (m << 5) | vm;
2499        bytes.extend_from_slice(&vcvt.to_le_bytes());
2500
2501        // VMOV Rd, S0
2502        let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
2503        bytes.extend_from_slice(&vmov.to_le_bytes());
2504
2505        Ok(bytes)
2506    }
2507
2508    /// Encode F64 rounding pseudo-op as ARM32 via VCVT to integer and back.
2509    /// Encode F64 rounding as ARM32.
2510    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2511    ///
2512    /// For trunc: uses VCVTR.S32.F64 (always truncates).
2513    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F64 (non-R variant),
2514    /// then restores FPSCR.
2515    fn encode_arm_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2516        let mut bytes = Vec::new();
2517        let dm_num = vfp_dreg_to_num(dm)?;
2518        let dd_num = vfp_dreg_to_num(dd)?;
2519        let (vm, m) = encode_dreg(dm_num);
2520        let (vd, d) = encode_dreg(dd_num);
2521
2522        if mode == 0b11 {
2523            // Trunc (toward zero): VCVTR.S32.F64 — bit[7]=1, always truncates
2524            let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
2525            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2526        } else {
2527            // ceil/floor/nearest: manipulate FPSCR rounding mode
2528            let rt: u32 = 12;
2529
2530            // VMRS R12, FPSCR
2531            let vmrs = 0xEEF10A10 | (rt << 12);
2532            bytes.extend_from_slice(&vmrs.to_le_bytes());
2533
2534            // BIC R12, R12, #(3 << 22)
2535            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2536            bytes.extend_from_slice(&bic.to_le_bytes());
2537
2538            // ORR R12, R12, #(mode << 22)
2539            if mode != 0 {
2540                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2541                bytes.extend_from_slice(&orr.to_le_bytes());
2542            }
2543
2544            // VMSR FPSCR, R12
2545            let vmsr = 0xEEE10A10 | (rt << 12);
2546            bytes.extend_from_slice(&vmsr.to_le_bytes());
2547
2548            // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0), uses FPSCR rmode
2549            let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
2550            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2551
2552            // Restore FPSCR
2553            bytes.extend_from_slice(&vmrs.to_le_bytes());
2554            bytes.extend_from_slice(&bic.to_le_bytes());
2555            bytes.extend_from_slice(&vmsr.to_le_bytes());
2556        }
2557
2558        // VCVT.F64.S32 Dd, S0 (convert back to double)
2559        let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
2560        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2561
2562        Ok(bytes)
2563    }
2564
2565    /// Encode F64 min/max as ARM32: VMOV + VCMP + VMRS + conditional VMOV
2566    fn encode_arm_f64_minmax(
2567        &self,
2568        dd: &VfpReg,
2569        dn: &VfpReg,
2570        dm: &VfpReg,
2571        is_min: bool,
2572    ) -> Result<Vec<u8>> {
2573        let mut bytes = Vec::new();
2574        let dn_num = vfp_dreg_to_num(dn)?;
2575        let dm_num = vfp_dreg_to_num(dm)?;
2576        let dd_num = vfp_dreg_to_num(dd)?;
2577
2578        // VMOV.F64 Dd, Dn (start with first operand)
2579        let (vd, d) = encode_dreg(dd_num);
2580        let (vn, n) = encode_dreg(dn_num);
2581        let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2582        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2583
2584        // VCMP.F64 Dn, Dm
2585        let (vm, m) = encode_dreg(dm_num);
2586        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2587        bytes.extend_from_slice(&vcmp.to_le_bytes());
2588
2589        // VMRS APSR_nzcv, FPSCR
2590        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2591
2592        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2593        let vmov_cond = (cond << 28) | 0x0EB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2594        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2595
2596        Ok(bytes)
2597    }
2598
2599    /// Encode F64 copysign as ARM32
2600    fn encode_arm_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
2601        let mut bytes = Vec::new();
2602
2603        // VMOV R0, R12, Dm (get sign source bits)
2604        let vmov_dm = encode_vmov_core_dreg(false, dm, &Reg::R0, &Reg::R12)?;
2605        bytes.extend_from_slice(&vmov_dm.to_le_bytes());
2606
2607        // VMOV R1, R2, Dn (get magnitude source bits)
2608        // We use R1 (lo) and R2 (hi) for the magnitude
2609        let vmov_dn = encode_vmov_core_dreg(false, dn, &Reg::R1, &Reg::R2)?;
2610        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2611
2612        // AND R12, R12, #0x80000000 (keep only sign bit from hi word)
2613        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2614        bytes.extend_from_slice(&and_sign.to_le_bytes());
2615
2616        // BIC R2, R2, #0x80000000 (clear sign bit from magnitude hi word)
2617        let bic_sign = 0xE3C00000u32 | (2 << 16) | (2 << 12) | (1 << 8) | 0x02;
2618        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2619
2620        // ORR R2, R2, R12 (combine sign + magnitude)
2621        let orr = 0xE1800000u32 | (2 << 16) | (2 << 12) | 12;
2622        bytes.extend_from_slice(&orr.to_le_bytes());
2623
2624        // VMOV Dd, R1, R2
2625        let vmov_result = encode_vmov_core_dreg(true, dd, &Reg::R1, &Reg::R2)?;
2626        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2627
2628        Ok(bytes)
2629    }
2630
2631    /// Encode VCVT.S32/U32.F32 + VMOV as ARM32
2632    fn encode_arm_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2633        let mut bytes = Vec::new();
2634
2635        // VCVT.S32.F32 Sd, Sm (toward zero) or VCVT.U32.F32 Sd, Sm
2636        // We use Sm as both source and destination for the intermediate result
2637        let sm_num = vfp_sreg_to_num(sm)?;
2638        let (vd, d) = encode_sreg(sm_num);
2639        let (vm, m) = encode_sreg(sm_num);
2640        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
2641        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2642        bytes.extend_from_slice(&vcvt.to_le_bytes());
2643
2644        // VMOV Rd, Sm — move result back to core register
2645        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
2646        bytes.extend_from_slice(&vmov.to_le_bytes());
2647
2648        Ok(bytes)
2649    }
2650
2651    /// Encode an ARM instruction in Thumb-2 mode (16-bit or 32-bit instructions)
2652    fn encode_thumb(&self, op: &ArmOp) -> Result<Vec<u8>> {
2653        // Thumb-2 supports both 16-bit and 32-bit instructions
2654        // 32-bit instructions are encoded as two 16-bit halfwords (big-endian order)
2655        match op {
2656            // === 16-bit Thumb encodings ===
2657            ArmOp::Add { rd, rn, op2 } => {
2658                let rd_bits = reg_to_bits(rd) as u16;
2659                let rn_bits = reg_to_bits(rn) as u16;
2660
2661                if let Operand2::Reg(rm) = op2 {
2662                    let rm_bits = reg_to_bits(rm) as u16;
2663                    // 16-bit ADDS only has 3-bit register fields (R0-R7). For
2664                    // high registers (e.g. R12, the MemLoad/MemStore base
2665                    // scratch) the bits overflow into adjacent fields, silently
2666                    // corrupting the operands — issue #178/#180: `add ip,ip,r0`
2667                    // was emitted as `adds r4,r5,r1`. Guard on all three regs
2668                    // being low and fall back to 32-bit ADD.W otherwise, exactly
2669                    // as the Sub handler below does.
2670                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2671                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2672                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2673                        Ok(instr.to_le_bytes().to_vec())
2674                    } else {
2675                        // ADD.W Rd, Rn, Rm (32-bit) for high registers
2676                        self.encode_thumb32_add_reg_raw(
2677                            rd_bits as u32,
2678                            rn_bits as u32,
2679                            rm_bits as u32,
2680                        )
2681                    }
2682                } else if let Operand2::Imm(imm) = op2 {
2683                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2684                        // ADDS Rd, Rn, #imm3 (16-bit): 0001 110 imm3 Rn Rd
2685                        let instr: u16 = 0x1C00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2686                        Ok(instr.to_le_bytes().to_vec())
2687                    } else {
2688                        // Use 32-bit ADD for larger immediates
2689                        self.encode_thumb32_add(rd, rn, *imm as u32)
2690                    }
2691                } else {
2692                    // Fallback to 32-bit encoding
2693                    self.encode_thumb32_add(rd, rn, 0)
2694                }
2695            }
2696
2697            ArmOp::Sub { rd, rn, op2 } => {
2698                let rd_bits = reg_to_bits(rd) as u16;
2699                let rn_bits = reg_to_bits(rn) as u16;
2700
2701                if let Operand2::Reg(rm) = op2 {
2702                    let rm_bits = reg_to_bits(rm) as u16;
2703                    // 16-bit SUBS can only use low registers (R0-R7)
2704                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2705                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2706                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2707                        Ok(instr.to_le_bytes().to_vec())
2708                    } else {
2709                        // Use 32-bit SUB.W for high registers
2710                        self.encode_thumb32_sub_reg_raw(
2711                            rd_bits as u32,
2712                            rn_bits as u32,
2713                            rm_bits as u32,
2714                        )
2715                    }
2716                } else if let Operand2::Imm(imm) = op2 {
2717                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2718                        // SUBS Rd, Rn, #imm3 (16-bit): 0001 111 imm3 Rn Rd
2719                        let instr: u16 = 0x1E00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2720                        Ok(instr.to_le_bytes().to_vec())
2721                    } else {
2722                        self.encode_thumb32_sub(rd, rn, *imm as u32)
2723                    }
2724                } else {
2725                    self.encode_thumb32_sub(rd, rn, 0)
2726                }
2727            }
2728
2729            ArmOp::Mov { rd, op2 } => {
2730                let rd_bits = reg_to_bits(rd) as u16;
2731
2732                if let Operand2::Imm(imm) = op2 {
2733                    // #498: the old test here was the SIGNED `*imm <= 255`,
2734                    // so a negative immediate (e.g. -1) fell into the 16-bit
2735                    // MOVS arm and encoded the wrong VALUE (#(imm & 0xFF) =
2736                    // #0xFF). A positive imm above 0xFFFF was equally wrong:
2737                    // MOVW truncates to 16 bits. Split on the UNSIGNED value:
2738                    // imm8 → MOVS, imm16 → MOVW, anything wider (negative or
2739                    // >0xFFFF) → the full-value MOVW+MOVT pair. No emitter
2740                    // produces the wide shape today (both selectors
2741                    // materialize wide constants as explicit Movw/Movt or
2742                    // Movw+Mvn), so this is byte-identical on shipped paths —
2743                    // it retires the latent wrong-value encodings the
2744                    // `estimator_encoder_agreement` oracle had pinned.
2745                    let uimm = *imm as u32;
2746                    if uimm <= 255 && rd_bits < 8 {
2747                        // MOVS Rd, #imm8 (16-bit): 0010 0 Rd imm8
2748                        let imm_bits = (*imm as u16) & 0xFF;
2749                        let instr: u16 = 0x2000 | (rd_bits << 8) | imm_bits;
2750                        Ok(instr.to_le_bytes().to_vec())
2751                    } else if uimm <= 0xFFFF {
2752                        // Use 32-bit MOVW for 16-bit immediates
2753                        self.encode_thumb32_movw(rd, uimm)
2754                    } else {
2755                        // Full 32-bit value: MOVW low16 + MOVT high16
2756                        let mut bytes = self.encode_thumb32_movw(rd, uimm & 0xFFFF)?;
2757                        bytes.extend(self.encode_thumb32_movt_raw(reg_to_bits(rd), uimm >> 16)?);
2758                        Ok(bytes)
2759                    }
2760                } else if let Operand2::Reg(rm) = op2 {
2761                    let rm_bits = reg_to_bits(rm) as u16;
2762                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
2763                    // D = Rd[3], Rd[2:0] in lower bits
2764                    let d_bit = (rd_bits >> 3) & 1;
2765                    let instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
2766                    Ok(instr.to_le_bytes().to_vec())
2767                } else {
2768                    let instr: u16 = 0xBF00; // NOP fallback
2769                    Ok(instr.to_le_bytes().to_vec())
2770                }
2771            }
2772
2773            ArmOp::Push { regs } => {
2774                // Thumb-2 PUSH encoding:
2775                // If all regs in R0-R7 + LR, use 16-bit: 1011 010 M rrrrrrrr
2776                // Otherwise use 32-bit: STMDB SP!, {regs} = 1110 1001 0010 1101 | 0M0 reglist(13)
2777                let mut reg_list: u16 = 0;
2778                let mut need_32bit = false;
2779                for r in regs {
2780                    let bit = reg_to_bits(r);
2781                    if bit >= 8 && *r != Reg::LR {
2782                        need_32bit = true;
2783                    }
2784                    reg_list |= 1 << bit;
2785                }
2786                if !need_32bit {
2787                    // 16-bit PUSH: 1011 010 M rrrrrrrr
2788                    let m_bit = if reg_list & (1 << 14) != 0 {
2789                        1u16
2790                    } else {
2791                        0u16
2792                    };
2793                    let low_regs = reg_list & 0xFF;
2794                    let instr: u16 = 0xB400 | (m_bit << 8) | low_regs;
2795                    Ok(instr.to_le_bytes().to_vec())
2796                } else {
2797                    // 32-bit STMDB SP!, {regs}: E92D | reglist(16)
2798                    let hw1: u16 = 0xE92D;
2799                    let hw2: u16 = reg_list;
2800                    let mut bytes = hw1.to_le_bytes().to_vec();
2801                    bytes.extend_from_slice(&hw2.to_le_bytes());
2802                    Ok(bytes)
2803                }
2804            }
2805
2806            ArmOp::Pop { regs } => {
2807                // Thumb-2 POP encoding:
2808                // If all regs in R0-R7 + PC, use 16-bit: 1011 110 P rrrrrrrr
2809                // Otherwise use 32-bit: LDMIA SP!, {regs} = 1110 1000 1011 1101 | PM0 reglist(13)
2810                let mut reg_list: u16 = 0;
2811                let mut need_32bit = false;
2812                for r in regs {
2813                    let bit = reg_to_bits(r);
2814                    if bit >= 8 && *r != Reg::PC {
2815                        need_32bit = true;
2816                    }
2817                    reg_list |= 1 << bit;
2818                }
2819                if !need_32bit {
2820                    // 16-bit POP: 1011 110 P rrrrrrrr
2821                    let p_bit = if reg_list & (1 << 15) != 0 {
2822                        1u16
2823                    } else {
2824                        0u16
2825                    };
2826                    let low_regs = reg_list & 0xFF;
2827                    let instr: u16 = 0xBC00 | (p_bit << 8) | low_regs;
2828                    Ok(instr.to_le_bytes().to_vec())
2829                } else {
2830                    // 32-bit LDMIA SP!, {regs}: E8BD | reglist(16)
2831                    let hw1: u16 = 0xE8BD;
2832                    let hw2: u16 = reg_list;
2833                    let mut bytes = hw1.to_le_bytes().to_vec();
2834                    bytes.extend_from_slice(&hw2.to_le_bytes());
2835                    Ok(bytes)
2836                }
2837            }
2838
2839            ArmOp::Nop => {
2840                let instr: u16 = 0xBF00; // NOP in Thumb-2
2841                Ok(instr.to_le_bytes().to_vec())
2842            }
2843
2844            ArmOp::Udf { imm } => {
2845                // UDF (Undefined) in Thumb-2: 16-bit encoding is 0xDE00 | imm8
2846                // This triggers UsageFault/HardFault, used for WASM traps
2847                let instr: u16 = 0xDE00 | (*imm as u16);
2848                let bytes = instr.to_le_bytes().to_vec();
2849                encoding_contracts::verify_thumb16(&bytes);
2850                Ok(bytes)
2851            }
2852
2853            // i64 support: ADDS, ADC, SUBS, SBC for register pair arithmetic
2854            // ADDS sets flags (carry), ADC uses carry from previous ADDS
2855            ArmOp::Adds { rd, rn, op2 } => {
2856                let rd_bits = reg_to_bits(rd) as u16;
2857                let rn_bits = reg_to_bits(rn) as u16;
2858
2859                if let Operand2::Reg(rm) = op2 {
2860                    let rm_bits = reg_to_bits(rm) as u16;
2861                    // 16-bit ADDS is R0-R7 only; i64 pair allocation can place
2862                    // operands in R8-R11, which would overflow the 3-bit fields
2863                    // and corrupt the operands (#178/#180 class). Guard and fall
2864                    // back to 32-bit ADDS.W for high registers.
2865                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2866                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2867                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2868                        Ok(instr.to_le_bytes().to_vec())
2869                    } else {
2870                        self.encode_thumb32_adds_reg_raw(
2871                            rd_bits as u32,
2872                            rn_bits as u32,
2873                            rm_bits as u32,
2874                        )
2875                    }
2876                } else {
2877                    // 32-bit Thumb-2 ADDS with immediate
2878                    self.encode_thumb32_adds(rd, rn, 0)
2879                }
2880            }
2881
2882            // ADC: Add with Carry (Thumb-2 32-bit)
2883            // ADC.W Rd, Rn, Rm: EB40 Rn | 00 Rd 00 Rm
2884            ArmOp::Adc { rd, rn, op2 } => {
2885                let rd_bits = reg_to_bits(rd);
2886                let rn_bits = reg_to_bits(rn);
2887
2888                if let Operand2::Reg(rm) = op2 {
2889                    let rm_bits = reg_to_bits(rm);
2890                    // ADC.W Rd, Rn, Rm (T2): 1110 1011 0100 Rn | 0 000 Rd 00 00 Rm
2891                    let hw1: u16 = (0xEB40 | rn_bits) as u16;
2892                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2893
2894                    let mut bytes = hw1.to_le_bytes().to_vec();
2895                    bytes.extend_from_slice(&hw2.to_le_bytes());
2896                    Ok(bytes)
2897                } else {
2898                    // ADC with immediate - use 32-bit encoding
2899                    let hw1: u16 = (0xF140 | rn_bits) as u16;
2900                    let hw2: u16 = (rd_bits << 8) as u16;
2901                    let mut bytes = hw1.to_le_bytes().to_vec();
2902                    bytes.extend_from_slice(&hw2.to_le_bytes());
2903                    Ok(bytes)
2904                }
2905            }
2906
2907            // SUBS sets flags (borrow), SBC uses borrow from previous SUBS
2908            ArmOp::Subs { rd, rn, op2 } => {
2909                let rd_bits = reg_to_bits(rd) as u16;
2910                let rn_bits = reg_to_bits(rn) as u16;
2911
2912                if let Operand2::Reg(rm) = op2 {
2913                    let rm_bits = reg_to_bits(rm) as u16;
2914                    // 16-bit SUBS is R0-R7 only; high-register i64 pair operands
2915                    // would overflow the 3-bit fields (#178/#180 class). Guard
2916                    // and fall back to 32-bit SUBS.W for high registers.
2917                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2918                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2919                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2920                        Ok(instr.to_le_bytes().to_vec())
2921                    } else {
2922                        self.encode_thumb32_subs_reg_raw(
2923                            rd_bits as u32,
2924                            rn_bits as u32,
2925                            rm_bits as u32,
2926                        )
2927                    }
2928                } else {
2929                    // 32-bit Thumb-2 SUBS with immediate
2930                    self.encode_thumb32_subs(rd, rn, 0)
2931                }
2932            }
2933
2934            // SBC: Subtract with Carry (Thumb-2 32-bit)
2935            // SBC.W Rd, Rn, Rm: EB60 Rn | 00 Rd 00 Rm
2936            ArmOp::Sbc { rd, rn, op2 } => {
2937                let rd_bits = reg_to_bits(rd);
2938                let rn_bits = reg_to_bits(rn);
2939
2940                if let Operand2::Reg(rm) = op2 {
2941                    let rm_bits = reg_to_bits(rm);
2942                    // SBC.W Rd, Rn, Rm (T2): 1110 1011 0110 Rn | 0 000 Rd 00 00 Rm
2943                    let hw1: u16 = (0xEB60 | rn_bits) as u16;
2944                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2945
2946                    let mut bytes = hw1.to_le_bytes().to_vec();
2947                    bytes.extend_from_slice(&hw2.to_le_bytes());
2948                    Ok(bytes)
2949                } else {
2950                    // SBC with immediate - use 32-bit encoding
2951                    let hw1: u16 = (0xF160 | rn_bits) as u16;
2952                    let hw2: u16 = (rd_bits << 8) as u16;
2953                    let mut bytes = hw1.to_le_bytes().to_vec();
2954                    bytes.extend_from_slice(&hw2.to_le_bytes());
2955                    Ok(bytes)
2956                }
2957            }
2958
2959            // === 32-bit Thumb-2 encodings ===
2960
2961            // SDIV: 11111011 1001 Rn 1111 Rd 1111 Rm
2962            ArmOp::Sdiv { rd, rn, rm } => {
2963                let rd_bits = reg_to_bits(rd);
2964                let rn_bits = reg_to_bits(rn);
2965                let rm_bits = reg_to_bits(rm);
2966                reg_bits_checked(rd_bits)?;
2967                reg_bits_checked(rn_bits)?;
2968                reg_bits_checked(rm_bits)?;
2969
2970                // Thumb-2 SDIV: FB90 F0F0 | Rn<<16 | Rd<<8 | Rm
2971                // First halfword: 1111 1011 1001 Rn = 0xFB90 | Rn
2972                // Second halfword: 1111 Rd 1111 Rm = 0xF0F0 | Rd<<8 | Rm
2973                let hw1: u16 = (0xFB90 | rn_bits) as u16;
2974                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
2975
2976                // Thumb-2 32-bit instructions: first halfword, then second halfword (little-endian each)
2977                let mut bytes = hw1.to_le_bytes().to_vec();
2978                bytes.extend_from_slice(&hw2.to_le_bytes());
2979                encoding_contracts::verify_thumb32(&bytes);
2980                Ok(bytes)
2981            }
2982
2983            // UDIV: 11111011 1011 Rn 1111 Rd 1111 Rm
2984            ArmOp::Udiv { rd, rn, rm } => {
2985                let rd_bits = reg_to_bits(rd);
2986                let rn_bits = reg_to_bits(rn);
2987                let rm_bits = reg_to_bits(rm);
2988                reg_bits_checked(rd_bits)?;
2989                reg_bits_checked(rn_bits)?;
2990                reg_bits_checked(rm_bits)?;
2991
2992                // Thumb-2 UDIV: FBB0 F0F0 | Rn<<16 | Rd<<8 | Rm
2993                let hw1: u16 = (0xFBB0 | rn_bits) as u16;
2994                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
2995
2996                let mut bytes = hw1.to_le_bytes().to_vec();
2997                bytes.extend_from_slice(&hw2.to_le_bytes());
2998                encoding_contracts::verify_thumb32(&bytes);
2999                Ok(bytes)
3000            }
3001
3002            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
3003                let rdlo_bits = reg_to_bits(rdlo);
3004                let rdhi_bits = reg_to_bits(rdhi);
3005                let rn_bits = reg_to_bits(rn);
3006                let rm_bits = reg_to_bits(rm);
3007                reg_bits_checked(rdlo_bits)?;
3008                reg_bits_checked(rdhi_bits)?;
3009                reg_bits_checked(rn_bits)?;
3010                reg_bits_checked(rm_bits)?;
3011
3012                // Thumb-2 UMULL: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm
3013                let hw1: u16 = (0xFBA0 | rn_bits) as u16;
3014                let hw2: u16 = ((rdlo_bits << 12) | (rdhi_bits << 8) | rm_bits) as u16;
3015
3016                let mut bytes = hw1.to_le_bytes().to_vec();
3017                bytes.extend_from_slice(&hw2.to_le_bytes());
3018                encoding_contracts::verify_thumb32(&bytes);
3019                Ok(bytes)
3020            }
3021
3022            // MUL (Thumb-2 32-bit): MUL Rd, Rn, Rm
3023            ArmOp::Mul { rd, rn, rm } => {
3024                let rd_bits = reg_to_bits(rd);
3025                let rn_bits = reg_to_bits(rn);
3026                let rm_bits = reg_to_bits(rm);
3027
3028                // Thumb-2 MUL: FB00 F000 | Rn | Rd<<8 | Rm
3029                // 11111011 0000 Rn | 1111 Rd 0000 Rm
3030                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3031                let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
3032
3033                let mut bytes = hw1.to_le_bytes().to_vec();
3034                bytes.extend_from_slice(&hw2.to_le_bytes());
3035                Ok(bytes)
3036            }
3037
3038            // MLS: Rd = Ra - Rn * Rm
3039            ArmOp::Mls { rd, rn, rm, ra } => {
3040                let rd_bits = reg_to_bits(rd);
3041                let rn_bits = reg_to_bits(rn);
3042                let rm_bits = reg_to_bits(rm);
3043                let ra_bits = reg_to_bits(ra);
3044
3045                // Thumb-2 MLS: FB00 Rn | Ra Rd 0001 Rm
3046                // 11111011 0000 Rn | Ra Rd 0001 Rm
3047                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3048                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | 0x10 | rm_bits) as u16;
3049
3050                let mut bytes = hw1.to_le_bytes().to_vec();
3051                bytes.extend_from_slice(&hw2.to_le_bytes());
3052                Ok(bytes)
3053            }
3054
3055            ArmOp::Mla { rd, rn, rm, ra } => {
3056                let rd_bits = reg_to_bits(rd);
3057                let rn_bits = reg_to_bits(rn);
3058                let rm_bits = reg_to_bits(rm);
3059                let ra_bits = reg_to_bits(ra);
3060
3061                // Thumb-2 MLA: FB00 Rn | Ra Rd 0000 Rm — same as MLS without the
3062                // bit-4 (0x10) op flag. rd = ra + rn*rm.
3063                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3064                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | rm_bits) as u16;
3065
3066                let mut bytes = hw1.to_le_bytes().to_vec();
3067                bytes.extend_from_slice(&hw2.to_le_bytes());
3068                Ok(bytes)
3069            }
3070
3071            // AND (Thumb-2 32-bit)
3072            ArmOp::And { rd, rn, op2 } => {
3073                if let Operand2::Reg(rm) = op2 {
3074                    let rd_bits = reg_to_bits(rd);
3075                    let rn_bits = reg_to_bits(rn);
3076                    let rm_bits = reg_to_bits(rm);
3077
3078                    // Thumb-2 AND register: EA00 Rn | 0 Rd 00 00 Rm
3079                    let hw1: u16 = (0xEA00 | rn_bits) as u16;
3080                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3081
3082                    let mut bytes = hw1.to_le_bytes().to_vec();
3083                    bytes.extend_from_slice(&hw2.to_le_bytes());
3084                    Ok(bytes)
3085                } else if let Operand2::Imm(imm) = op2 {
3086                    let rd_bits = reg_to_bits(rd);
3087                    let rn_bits = reg_to_bits(rn);
3088
3089                    // Thumb-2 AND.W immediate T1: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8.
3090                    // The i:imm3:imm8 field is a ThumbExpandImm modified immediate —
3091                    // encode it correctly (or error on an un-encodable value)
3092                    // rather than packing raw bits, closing the silent-miscompile
3093                    // class for AND alongside ORR/EOR (#251) / ADD/SUB (#253) /
3094                    // CMP (#255).
3095                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3096                        synth_core::Error::synthesis(
3097                            "AND immediate is not a valid ThumbExpandImm — materialize into a register",
3098                        )
3099                    })?;
3100                    let i_bit = (field >> 11) & 1;
3101                    let imm3 = (field >> 8) & 0x7;
3102                    let imm8 = field & 0xFF;
3103
3104                    let hw1: u16 = (0xF000 | (i_bit << 10) | rn_bits) as u16;
3105                    let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3106
3107                    let mut bytes = hw1.to_le_bytes().to_vec();
3108                    bytes.extend_from_slice(&hw2.to_le_bytes());
3109                    Ok(bytes)
3110                } else {
3111                    // RegShift variant - fallback to NOP
3112                    let instr: u16 = 0xBF00;
3113                    Ok(instr.to_le_bytes().to_vec())
3114                }
3115            }
3116
3117            // ORR (Thumb-2 32-bit)
3118            ArmOp::Orr { rd, rn, op2 } => {
3119                if let Operand2::Reg(rm) = op2 {
3120                    let rd_bits = reg_to_bits(rd);
3121                    let rn_bits = reg_to_bits(rn);
3122                    let rm_bits = reg_to_bits(rm);
3123
3124                    // Thumb-2 ORR: EA40 Rn | 0 Rd 00 00 Rm
3125                    let hw1: u16 = (0xEA40 | rn_bits) as u16;
3126                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3127
3128                    let mut bytes = hw1.to_le_bytes().to_vec();
3129                    bytes.extend_from_slice(&hw2.to_le_bytes());
3130                    Ok(bytes)
3131                } else if let Operand2::Imm(imm) = op2 {
3132                    // ORR.W immediate T1: 11110 i 0 0010 S Rn | 0 imm3 Rd imm8.
3133                    // Only the zero-extended byte form (imm <= 0xFF) is encoded;
3134                    // larger modified immediates need ThumbExpandImm — return an
3135                    // error rather than silently emit a NOP (Ok-or-Err, #180/#185).
3136                    let imm_val = *imm as u32;
3137                    if imm_val > 0xFF {
3138                        return Err(synth_core::Error::synthesis(
3139                            "ORR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3140                        ));
3141                    }
3142                    let rd_bits = reg_to_bits(rd);
3143                    let rn_bits = reg_to_bits(rn);
3144                    let hw1: u16 = (0xF040 | rn_bits) as u16;
3145                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3146                    let mut bytes = hw1.to_le_bytes().to_vec();
3147                    bytes.extend_from_slice(&hw2.to_le_bytes());
3148                    Ok(bytes)
3149                } else {
3150                    let instr: u16 = 0xBF00;
3151                    Ok(instr.to_le_bytes().to_vec())
3152                }
3153            }
3154
3155            // EOR (Thumb-2 32-bit)
3156            ArmOp::Eor { rd, rn, op2 } => {
3157                if let Operand2::Reg(rm) = op2 {
3158                    let rd_bits = reg_to_bits(rd);
3159                    let rn_bits = reg_to_bits(rn);
3160                    let rm_bits = reg_to_bits(rm);
3161
3162                    // Thumb-2 EOR: EA80 Rn | 0 Rd 00 00 Rm
3163                    let hw1: u16 = (0xEA80 | rn_bits) as u16;
3164                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3165
3166                    let mut bytes = hw1.to_le_bytes().to_vec();
3167                    bytes.extend_from_slice(&hw2.to_le_bytes());
3168                    Ok(bytes)
3169                } else if let Operand2::Imm(imm) = op2 {
3170                    // EOR.W immediate T1: 11110 i 0 0100 S Rn | 0 imm3 Rd imm8.
3171                    // Byte form only (imm <= 0xFF); larger needs ThumbExpandImm —
3172                    // error, not a silent NOP (Ok-or-Err, #180/#185).
3173                    let imm_val = *imm as u32;
3174                    if imm_val > 0xFF {
3175                        return Err(synth_core::Error::synthesis(
3176                            "EOR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3177                        ));
3178                    }
3179                    let rd_bits = reg_to_bits(rd);
3180                    let rn_bits = reg_to_bits(rn);
3181                    let hw1: u16 = (0xF080 | rn_bits) as u16;
3182                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3183                    let mut bytes = hw1.to_le_bytes().to_vec();
3184                    bytes.extend_from_slice(&hw2.to_le_bytes());
3185                    Ok(bytes)
3186                } else {
3187                    let instr: u16 = 0xBF00;
3188                    Ok(instr.to_le_bytes().to_vec())
3189                }
3190            }
3191
3192            // Shift operations (16-bit for low registers)
3193            ArmOp::Lsl { rd, rn, shift } => {
3194                let rd_bits = reg_to_bits(rd) as u16;
3195                let rn_bits = reg_to_bits(rn) as u16;
3196                let shift_bits = (*shift as u16) & 0x1F;
3197
3198                if rd_bits < 8 && rn_bits < 8 {
3199                    // LSLS Rd, Rm, #imm5 (16-bit): 0000 0 imm5 Rm Rd
3200                    let instr: u16 = (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3201                    Ok(instr.to_le_bytes().to_vec())
3202                } else {
3203                    // Use 32-bit encoding for high registers
3204                    self.encode_thumb32_shift(rd, rn, *shift, 0b00) // LSL type
3205                }
3206            }
3207
3208            ArmOp::Lsr { rd, rn, shift } => {
3209                let rd_bits = reg_to_bits(rd) as u16;
3210                let rn_bits = reg_to_bits(rn) as u16;
3211                let shift_bits = (*shift as u16) & 0x1F;
3212
3213                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3214                    // LSRS Rd, Rm, #imm5 (16-bit): 0000 1 imm5 Rm Rd
3215                    let instr: u16 = 0x0800 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3216                    Ok(instr.to_le_bytes().to_vec())
3217                } else {
3218                    self.encode_thumb32_shift(rd, rn, *shift, 0b01) // LSR type
3219                }
3220            }
3221
3222            ArmOp::Asr { rd, rn, shift } => {
3223                let rd_bits = reg_to_bits(rd) as u16;
3224                let rn_bits = reg_to_bits(rn) as u16;
3225                let shift_bits = (*shift as u16) & 0x1F;
3226
3227                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3228                    // ASRS Rd, Rm, #imm5 (16-bit): 0001 0 imm5 Rm Rd
3229                    let instr: u16 = 0x1000 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3230                    Ok(instr.to_le_bytes().to_vec())
3231                } else {
3232                    self.encode_thumb32_shift(rd, rn, *shift, 0b10) // ASR type
3233                }
3234            }
3235
3236            ArmOp::Ror { rd, rn, shift } => {
3237                // ROR doesn't have a 16-bit immediate form, use 32-bit
3238                self.encode_thumb32_shift(rd, rn, *shift, 0b11) // ROR type
3239            }
3240
3241            // Register-based shifts (Thumb-2 32-bit)
3242            // Encoding: 11111010 0xxS Rn 1111 Rd 0000 Rm
3243            // xx = shift type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
3244            ArmOp::LslReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b00),
3245            ArmOp::LsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b01),
3246            ArmOp::AsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b10),
3247            ArmOp::RorReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b11),
3248
3249            // RSB (Reverse Subtract): Rd = imm - Rn
3250            // Thumb-2 T2 encoding: 11110 i 0 1110 S Rn | 0 imm3 Rd imm8
3251            ArmOp::Rsb { rd, rn, imm } => {
3252                let rd_bits = reg_to_bits(rd);
3253                let rn_bits = reg_to_bits(rn);
3254
3255                // #681 class audit: the T2 `i:imm3:imm8` field is a
3256                // ThumbExpandImm modified immediate and RSB has NO plain-imm12
3257                // (T4-style) form — packing a raw value > 0xFF silently encodes
3258                // a different constant (#253/#255 class). All current emitters
3259                // use imm 32 (shift complement), which expands to itself, so
3260                // this gate is byte-identical for existing codegen.
3261                let field = try_thumb_expand_imm(*imm).ok_or_else(|| {
3262                    synth_core::Error::synthesis(
3263                        "RSB immediate is not a valid ThumbExpandImm — materialize into a register",
3264                    )
3265                })?;
3266                let i_bit = (field >> 11) & 1;
3267                let imm3 = (field >> 8) & 0x7;
3268                let imm8 = field & 0xFF;
3269
3270                // hw1: 11110 i 01110 0 Rn  (S=0)
3271                let hw1: u16 = (0xF1C0 | (i_bit << 10) | rn_bits) as u16;
3272                // hw2: 0 imm3 Rd imm8
3273                let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3274
3275                let mut bytes = hw1.to_le_bytes().to_vec();
3276                bytes.extend_from_slice(&hw2.to_le_bytes());
3277                Ok(bytes)
3278            }
3279
3280            // CLZ (Thumb-2 32-bit)
3281            ArmOp::Clz { rd, rm } => {
3282                let rd_bits = reg_to_bits(rd);
3283                let rm_bits = reg_to_bits(rm);
3284
3285                // Thumb-2 CLZ: FAB0 Rm | F8 Rd Rm
3286                // 11111010 1011 Rm | 1111 1000 Rd Rm
3287                let hw1: u16 = (0xFAB0 | rm_bits) as u16;
3288                let hw2: u16 = (0xF080 | (rd_bits << 8) | rm_bits) as u16;
3289
3290                let mut bytes = hw1.to_le_bytes().to_vec();
3291                bytes.extend_from_slice(&hw2.to_le_bytes());
3292                Ok(bytes)
3293            }
3294
3295            // RBIT (Thumb-2 32-bit)
3296            ArmOp::Rbit { rd, rm } => {
3297                let rd_bits = reg_to_bits(rd);
3298                let rm_bits = reg_to_bits(rm);
3299
3300                // Thumb-2 RBIT: FA90 Rm | F0 Rd A0 Rm
3301                // 11111010 1001 Rm | 1111 Rd 1010 Rm
3302                let hw1: u16 = (0xFA90 | rm_bits) as u16;
3303                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rm_bits) as u16;
3304
3305                let mut bytes = hw1.to_le_bytes().to_vec();
3306                bytes.extend_from_slice(&hw2.to_le_bytes());
3307                Ok(bytes)
3308            }
3309
3310            // SXTB (16-bit for low registers)
3311            ArmOp::Sxtb { rd, rm } => {
3312                let rd_bits = reg_to_bits(rd) as u16;
3313                let rm_bits = reg_to_bits(rm) as u16;
3314
3315                if rd_bits < 8 && rm_bits < 8 {
3316                    // SXTB Rd, Rm (16-bit): 1011 0010 01 Rm Rd
3317                    let instr: u16 = 0xB240 | (rm_bits << 3) | rd_bits;
3318                    Ok(instr.to_le_bytes().to_vec())
3319                } else {
3320                    // Thumb-2 SXTB.W: FA4F F(rd)80 (rm)
3321                    // 11111010 0100 1111 | 1111 Rd 10 rotate Rm
3322                    let rd_bits32 = rd_bits as u32;
3323                    let rm_bits32 = rm_bits as u32;
3324                    let hw1: u16 = 0xFA4F;
3325                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3326                    let mut bytes = hw1.to_le_bytes().to_vec();
3327                    bytes.extend_from_slice(&hw2.to_le_bytes());
3328                    Ok(bytes)
3329                }
3330            }
3331
3332            // SXTH (16-bit for low registers)
3333            ArmOp::Sxth { rd, rm } => {
3334                let rd_bits = reg_to_bits(rd) as u16;
3335                let rm_bits = reg_to_bits(rm) as u16;
3336
3337                if rd_bits < 8 && rm_bits < 8 {
3338                    // SXTH Rd, Rm (16-bit): 1011 0010 00 Rm Rd
3339                    let instr: u16 = 0xB200 | (rm_bits << 3) | rd_bits;
3340                    Ok(instr.to_le_bytes().to_vec())
3341                } else {
3342                    // Thumb-2 SXTH.W: FA0F F(rd)80 (rm)
3343                    // 11111010 0000 1111 | 1111 Rd 10 rotate Rm
3344                    let rd_bits32 = rd_bits as u32;
3345                    let rm_bits32 = rm_bits as u32;
3346                    let hw1: u16 = 0xFA0F;
3347                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3348                    let mut bytes = hw1.to_le_bytes().to_vec();
3349                    bytes.extend_from_slice(&hw2.to_le_bytes());
3350                    Ok(bytes)
3351                }
3352            }
3353
3354            // UXTB Rd,Rm — zero-extend byte (rd = rm & 0xff)
3355            ArmOp::Uxtb { rd, rm } => {
3356                let rd_bits = reg_to_bits(rd) as u16;
3357                let rm_bits = reg_to_bits(rm) as u16;
3358                if rd_bits < 8 && rm_bits < 8 {
3359                    // UXTB Rd, Rm (16-bit): 1011 0010 11 Rm Rd
3360                    let instr: u16 = 0xB2C0 | (rm_bits << 3) | rd_bits;
3361                    Ok(instr.to_le_bytes().to_vec())
3362                } else {
3363                    // Thumb-2 UXTB.W: FA5F F(rd)80 (rm)
3364                    let hw1: u16 = 0xFA5F;
3365                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3366                    let mut bytes = hw1.to_le_bytes().to_vec();
3367                    bytes.extend_from_slice(&hw2.to_le_bytes());
3368                    Ok(bytes)
3369                }
3370            }
3371
3372            // UXTH Rd,Rm — zero-extend halfword (rd = rm & 0xffff)
3373            ArmOp::Uxth { rd, rm } => {
3374                let rd_bits = reg_to_bits(rd) as u16;
3375                let rm_bits = reg_to_bits(rm) as u16;
3376                if rd_bits < 8 && rm_bits < 8 {
3377                    // UXTH Rd, Rm (16-bit): 1011 0010 10 Rm Rd
3378                    let instr: u16 = 0xB280 | (rm_bits << 3) | rd_bits;
3379                    Ok(instr.to_le_bytes().to_vec())
3380                } else {
3381                    // Thumb-2 UXTH.W: FA1F F(rd)80 (rm)
3382                    let hw1: u16 = 0xFA1F;
3383                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3384                    let mut bytes = hw1.to_le_bytes().to_vec();
3385                    bytes.extend_from_slice(&hw2.to_le_bytes());
3386                    Ok(bytes)
3387                }
3388            }
3389
3390            // CMP (can be 16-bit for low registers)
3391            ArmOp::Cmp { rn, op2 } => {
3392                let rn_bits = reg_to_bits(rn) as u16;
3393
3394                if let Operand2::Imm(imm) = op2 {
3395                    // Only use 16-bit encoding for non-negative immediates 0-255
3396                    // Negative immediates must use 32-bit encoding
3397                    if *imm >= 0 && *imm <= 255 && rn_bits < 8 {
3398                        // CMP Rn, #imm8 (16-bit): 0010 1 Rn imm8
3399                        let instr: u16 = 0x2800 | (rn_bits << 8) | (*imm as u16 & 0xFF);
3400                        Ok(instr.to_le_bytes().to_vec())
3401                    } else {
3402                        self.encode_thumb32_cmp_imm(rn, *imm as u32)
3403                    }
3404                } else if let Operand2::Reg(rm) = op2 {
3405                    let rm_bits = reg_to_bits(rm) as u16;
3406                    if rn_bits < 8 && rm_bits < 8 {
3407                        // CMP Rn, Rm (16-bit low): 0100 0010 10 Rm Rn
3408                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
3409                        Ok(instr.to_le_bytes().to_vec())
3410                    } else {
3411                        // CMP Rn, Rm (16-bit high): 0100 0101 N Rm Rn[2:0]
3412                        let n_bit = (rn_bits >> 3) & 1;
3413                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
3414                        Ok(instr.to_le_bytes().to_vec())
3415                    }
3416                } else {
3417                    let instr: u16 = 0xBF00;
3418                    Ok(instr.to_le_bytes().to_vec())
3419                }
3420            }
3421
3422            // CMN (Compare Negative) - computes Rn + op2 and sets flags
3423            // CMN Rn, #1 sets Z flag if Rn == -1 (since -1 + 1 = 0)
3424            ArmOp::Cmn { rn, op2 } => {
3425                let rn_bits = reg_to_bits(rn) as u16;
3426
3427                if let Operand2::Imm(imm) = op2 {
3428                    // CMN.W Rn, #imm (32-bit): i:imm3:imm8 is a ThumbExpandImm
3429                    // modified immediate (the field sits in imm3=hw2[14:12],
3430                    // imm8=hw2[7:0], i=hw1[10]). Encode it correctly, or error on
3431                    // an un-encodable value — replacing the old silent `0xBF00`
3432                    // NOP (the last of the silent-miscompile data-proc encoders).
3433                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3434                        synth_core::Error::synthesis(
3435                            "CMN immediate is not a valid ThumbExpandImm — materialize into a register",
3436                        )
3437                    })?;
3438                    let i_bit = (field >> 11) & 1;
3439                    let imm3 = (field >> 8) & 0x7;
3440                    let imm8 = field & 0xFF;
3441                    let hw1: u16 = (0xF110 | (i_bit << 10) as u16) | rn_bits;
3442                    let hw2: u16 = (imm3 << 12) as u16 | 0x0F00 | imm8 as u16;
3443                    let mut bytes = hw1.to_le_bytes().to_vec();
3444                    bytes.extend_from_slice(&hw2.to_le_bytes());
3445                    Ok(bytes)
3446                } else if let Operand2::Reg(rm) = op2 {
3447                    let rm_bits = reg_to_bits(rm) as u16;
3448                    // 16-bit CMN (T1) only encodes R0-R7; high registers overflow
3449                    // the 3-bit fields and corrupt the operands (#184, the #180
3450                    // class). CMN has no high-register 16-bit form, so fall back
3451                    // to 32-bit CMN.W (T2): EB10 Rn | 0F00 Rm (ADD.W with S=1 and
3452                    // Rd discarded as PC/1111).
3453                    if rn_bits < 8 && rm_bits < 8 {
3454                        // CMN Rn, Rm (16-bit): 0100 0010 11 Rm Rn
3455                        let instr: u16 = 0x42C0 | (rm_bits << 3) | rn_bits;
3456                        Ok(instr.to_le_bytes().to_vec())
3457                    } else {
3458                        let hw1: u16 = 0xEB10 | rn_bits;
3459                        let hw2: u16 = 0x0F00 | rm_bits;
3460                        let mut bytes = hw1.to_le_bytes().to_vec();
3461                        bytes.extend_from_slice(&hw2.to_le_bytes());
3462                        Ok(bytes)
3463                    }
3464                } else {
3465                    Ok(vec![0xBF, 0x00])
3466                }
3467            }
3468
3469            // LDR (can be 16-bit for simple cases)
3470            ArmOp::Ldr { rd, addr } => {
3471                let rd_bits = reg_to_bits(rd);
3472                let base_bits = reg_to_bits(&addr.base);
3473
3474                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3475                if let Some(offset_reg) = &addr.offset_reg {
3476                    let rm_bits = reg_to_bits(offset_reg);
3477
3478                    // If there's also an immediate offset, we need to ADD it first
3479                    if addr.offset != 0 {
3480                        // Use R12 (IP) as scratch to avoid clobbering the address register
3481                        // ADD R12, Rm, #offset; LDR Rd, [base, R12]
3482                        let scratch = Reg::R12;
3483                        let mut bytes =
3484                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3485                        bytes.extend(self.encode_thumb32_ldr_reg(rd, &addr.base, &scratch)?);
3486                        return Ok(bytes);
3487                    }
3488
3489                    // Simple register offset: LDR Rd, [Rn, Rm]
3490                    // 16-bit: only if Rd, Rn, Rm < R8
3491                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3492                        // LDR Rd, [Rn, Rm] (16-bit): 0101 100 Rm Rn Rd
3493                        let instr: u16 = 0x5800
3494                            | ((rm_bits as u16) << 6)
3495                            | ((base_bits as u16) << 3)
3496                            | (rd_bits as u16);
3497                        return Ok(instr.to_le_bytes().to_vec());
3498                    }
3499
3500                    // 32-bit register offset
3501                    return self.encode_thumb32_ldr_reg(rd, &addr.base, offset_reg);
3502                }
3503
3504                // Immediate offset mode [base, #imm]
3505                let offset = addr.offset as u32;
3506
3507                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3508                    // LDR Rd, [Rn, #imm5*4] (16-bit): 0110 1 imm5 Rn Rd
3509                    let imm5 = (offset >> 2) as u16;
3510                    let instr: u16 =
3511                        0x6800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3512                    Ok(instr.to_le_bytes().to_vec())
3513                } else {
3514                    self.encode_thumb32_ldr(rd, &addr.base, offset)
3515                }
3516            }
3517
3518            // STR (can be 16-bit for simple cases)
3519            ArmOp::Str { rd, addr } => {
3520                let rd_bits = reg_to_bits(rd);
3521                let base_bits = reg_to_bits(&addr.base);
3522
3523                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3524                if let Some(offset_reg) = &addr.offset_reg {
3525                    let rm_bits = reg_to_bits(offset_reg);
3526
3527                    // If there's also an immediate offset, we need to ADD it first
3528                    if addr.offset != 0 {
3529                        // Use R12 (IP) as scratch to avoid clobbering the address register
3530                        // ADD R12, Rm, #offset; STR Rd, [base, R12]
3531                        let scratch = Reg::R12;
3532                        let mut bytes =
3533                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3534                        bytes.extend(self.encode_thumb32_str_reg(rd, &addr.base, &scratch)?);
3535                        return Ok(bytes);
3536                    }
3537
3538                    // Simple register offset: STR Rd, [Rn, Rm]
3539                    // 16-bit: only if Rd, Rn, Rm < R8
3540                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3541                        // STR Rd, [Rn, Rm] (16-bit): 0101 000 Rm Rn Rd
3542                        let instr: u16 = 0x5000
3543                            | ((rm_bits as u16) << 6)
3544                            | ((base_bits as u16) << 3)
3545                            | (rd_bits as u16);
3546                        return Ok(instr.to_le_bytes().to_vec());
3547                    }
3548
3549                    // 32-bit register offset
3550                    return self.encode_thumb32_str_reg(rd, &addr.base, offset_reg);
3551                }
3552
3553                // Immediate offset mode [base, #imm]
3554                let offset = addr.offset as u32;
3555
3556                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3557                    // STR Rd, [Rn, #imm5*4] (16-bit): 0110 0 imm5 Rn Rd
3558                    let imm5 = (offset >> 2) as u16;
3559                    let instr: u16 =
3560                        0x6000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3561                    Ok(instr.to_le_bytes().to_vec())
3562                } else {
3563                    self.encode_thumb32_str(rd, &addr.base, offset)
3564                }
3565            }
3566
3567            // LDRB (Thumb-2)
3568            ArmOp::Ldrb { rd, addr } => {
3569                let rd_bits = reg_to_bits(rd);
3570                let base_bits = reg_to_bits(&addr.base);
3571
3572                if let Some(offset_reg) = &addr.offset_reg {
3573                    if addr.offset != 0 {
3574                        let scratch = Reg::R12;
3575                        let mut bytes =
3576                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3577                        bytes.extend(self.encode_thumb32_ldrb_reg(rd, &addr.base, &scratch)?);
3578                        return Ok(bytes);
3579                    }
3580                    return self.encode_thumb32_ldrb_reg(rd, &addr.base, offset_reg);
3581                }
3582
3583                let offset = addr.offset as u32;
3584                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3585                    // LDRB Rd, [Rn, #imm5] (16-bit): 0111 1 imm5 Rn Rd
3586                    let instr: u16 = 0x7800
3587                        | ((offset as u16) << 6)
3588                        | ((base_bits as u16) << 3)
3589                        | (rd_bits as u16);
3590                    Ok(instr.to_le_bytes().to_vec())
3591                } else {
3592                    self.encode_thumb32_ldrb_imm(rd, &addr.base, offset)
3593                }
3594            }
3595
3596            // LDRSB (Thumb-2)
3597            ArmOp::Ldrsb { rd, addr } => {
3598                let rd_bits = reg_to_bits(rd);
3599                let base_bits = reg_to_bits(&addr.base);
3600
3601                if let Some(offset_reg) = &addr.offset_reg {
3602                    if addr.offset != 0 {
3603                        let scratch = Reg::R12;
3604                        let mut bytes =
3605                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3606                        bytes.extend(self.encode_thumb32_ldrsb_reg(rd, &addr.base, &scratch)?);
3607                        return Ok(bytes);
3608                    }
3609                    return self.encode_thumb32_ldrsb_reg(rd, &addr.base, offset_reg);
3610                }
3611
3612                let offset = addr.offset as u32;
3613                // LDRSB has no 16-bit immediate form (only register)
3614                // For 16-bit reg form: only if Rd, Rn, Rm < R8
3615                if rd_bits < 8 && base_bits < 8 && offset == 0 {
3616                    // No immediate 16-bit encoding for LDRSB; use 32-bit
3617                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3618                } else {
3619                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3620                }
3621            }
3622
3623            // LDRH (Thumb-2)
3624            ArmOp::Ldrh { rd, addr } => {
3625                let rd_bits = reg_to_bits(rd);
3626                let base_bits = reg_to_bits(&addr.base);
3627
3628                if let Some(offset_reg) = &addr.offset_reg {
3629                    if addr.offset != 0 {
3630                        let scratch = Reg::R12;
3631                        let mut bytes =
3632                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3633                        bytes.extend(self.encode_thumb32_ldrh_reg(rd, &addr.base, &scratch)?);
3634                        return Ok(bytes);
3635                    }
3636                    return self.encode_thumb32_ldrh_reg(rd, &addr.base, offset_reg);
3637                }
3638
3639                let offset = addr.offset as u32;
3640                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3641                    // LDRH Rd, [Rn, #imm5*2] (16-bit): 1000 1 imm5 Rn Rd
3642                    let imm5 = (offset >> 1) as u16;
3643                    let instr: u16 =
3644                        0x8800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3645                    Ok(instr.to_le_bytes().to_vec())
3646                } else {
3647                    self.encode_thumb32_ldrh_imm(rd, &addr.base, offset)
3648                }
3649            }
3650
3651            // LDRSH (Thumb-2)
3652            ArmOp::Ldrsh { rd, addr } => {
3653                if let Some(offset_reg) = &addr.offset_reg {
3654                    if addr.offset != 0 {
3655                        let scratch = Reg::R12;
3656                        let mut bytes =
3657                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3658                        bytes.extend(self.encode_thumb32_ldrsh_reg(rd, &addr.base, &scratch)?);
3659                        return Ok(bytes);
3660                    }
3661                    return self.encode_thumb32_ldrsh_reg(rd, &addr.base, offset_reg);
3662                }
3663
3664                let offset = addr.offset as u32;
3665                self.encode_thumb32_ldrsh_imm(rd, &addr.base, offset)
3666            }
3667
3668            // STRB (Thumb-2)
3669            ArmOp::Strb { rd, addr } => {
3670                let rd_bits = reg_to_bits(rd);
3671                let base_bits = reg_to_bits(&addr.base);
3672
3673                if let Some(offset_reg) = &addr.offset_reg {
3674                    if addr.offset != 0 {
3675                        let scratch = Reg::R12;
3676                        let mut bytes =
3677                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3678                        bytes.extend(self.encode_thumb32_strb_reg(rd, &addr.base, &scratch)?);
3679                        return Ok(bytes);
3680                    }
3681                    return self.encode_thumb32_strb_reg(rd, &addr.base, offset_reg);
3682                }
3683
3684                let offset = addr.offset as u32;
3685                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3686                    // STRB Rd, [Rn, #imm5] (16-bit): 0111 0 imm5 Rn Rd
3687                    let instr: u16 = 0x7000
3688                        | ((offset as u16) << 6)
3689                        | ((base_bits as u16) << 3)
3690                        | (rd_bits as u16);
3691                    Ok(instr.to_le_bytes().to_vec())
3692                } else {
3693                    self.encode_thumb32_strb_imm(rd, &addr.base, offset)
3694                }
3695            }
3696
3697            // STRH (Thumb-2)
3698            ArmOp::Strh { rd, addr } => {
3699                let rd_bits = reg_to_bits(rd);
3700                let base_bits = reg_to_bits(&addr.base);
3701
3702                if let Some(offset_reg) = &addr.offset_reg {
3703                    if addr.offset != 0 {
3704                        let scratch = Reg::R12;
3705                        let mut bytes =
3706                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3707                        bytes.extend(self.encode_thumb32_strh_reg(rd, &addr.base, &scratch)?);
3708                        return Ok(bytes);
3709                    }
3710                    return self.encode_thumb32_strh_reg(rd, &addr.base, offset_reg);
3711                }
3712
3713                let offset = addr.offset as u32;
3714                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3715                    // STRH Rd, [Rn, #imm5*2] (16-bit): 1000 0 imm5 Rn Rd
3716                    let imm5 = (offset >> 1) as u16;
3717                    let instr: u16 =
3718                        0x8000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3719                    Ok(instr.to_le_bytes().to_vec())
3720                } else {
3721                    self.encode_thumb32_strh_imm(rd, &addr.base, offset)
3722                }
3723            }
3724
3725            // MemorySize (Thumb-2)
3726            ArmOp::MemorySize { rd } => {
3727                // LSR rd, R10, #16 — memory size in bytes / 65536 = pages
3728                // Thumb-2 16-bit: LSRS Rd, Rm, #imm5 — 0000 1 imm5 Rm Rd
3729                let rd_bits = reg_to_bits(rd);
3730                let r10_bits = reg_to_bits(&Reg::R10);
3731                if rd_bits < 8 && r10_bits < 8 {
3732                    let instr: u16 =
3733                        0x0800 | (16u16 << 6) | ((r10_bits as u16) << 3) | (rd_bits as u16);
3734                    Ok(instr.to_le_bytes().to_vec())
3735                } else {
3736                    // Thumb-2 32-bit LSR: 1110 1010 010 0 1111 | 0 imm3 Rd imm2 01 Rm
3737                    let imm5: u32 = 16;
3738                    let imm3 = (imm5 >> 2) & 0x7;
3739                    let imm2 = imm5 & 0x3;
3740                    let hw1: u16 = 0xEA4F;
3741                    let hw2: u16 =
3742                        ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | 0x10 | r10_bits) as u16;
3743                    let mut bytes = hw1.to_le_bytes().to_vec();
3744                    bytes.extend_from_slice(&hw2.to_le_bytes());
3745                    Ok(bytes)
3746                }
3747            }
3748
3749            // MemoryGrow (Thumb-2)
3750            ArmOp::MemoryGrow { rd, .. } => {
3751                // On embedded with fixed memory, always return -1 (failure)
3752                // MVN rd, #0 → MOV rd, #-1
3753                // Thumb-2 32-bit: MVN: 1111 0 i 0 0 0 1 1 0 1111 | 0 imm3 Rd imm8
3754                let rd_bits = reg_to_bits(rd);
3755                let hw1: u16 = 0xF06F; // MVN with i=0
3756                let hw2: u16 = (rd_bits << 8) as u16; // imm8=0 → ~0 = 0xFFFFFFFF = -1
3757                let mut bytes = hw1.to_le_bytes().to_vec();
3758                bytes.extend_from_slice(&hw2.to_le_bytes());
3759                Ok(bytes)
3760            }
3761
3762            // BX (16-bit)
3763            ArmOp::Bx { rm } => {
3764                let rm_bits = reg_to_bits(rm) as u16;
3765                // BX Rm (16-bit): 0100 0111 0 Rm 000
3766                let instr: u16 = 0x4700 | (rm_bits << 3);
3767                Ok(instr.to_le_bytes().to_vec())
3768            }
3769
3770            // BLX (16-bit) - Branch with Link and Exchange
3771            // BLX Rm: 0100 0111 1 Rm 000
3772            ArmOp::Blx { rm } => {
3773                let rm_bits = reg_to_bits(rm) as u16;
3774                let instr: u16 = 0x4780 | (rm_bits << 3);
3775                Ok(instr.to_le_bytes().to_vec())
3776            }
3777
3778            // CallIndirect - indirect function call via table lookup
3779            // table_index_reg contains the table index
3780            // Generates (#642): MOVW ip,#size [; MOVT]; CMP idx,ip; BLO +1;
3781            //                   UDF #0; LSL R12,idx,#2; LDR R12,[R11,R12]; BLX R12
3782            // #650, table_byte_offset != 0 (a non-zero table in the contiguous
3783            // R11 region): the pointer load becomes
3784            //                   ADD R12,R11,R12; LDR R12,[R12,#offset]
3785            // #664, null_check (the table has null slots, linked as ZERO
3786            // words): the loaded pointer is null-checked before the BLX —
3787            //                   CMP.W R12,#0; BNE +1; UDF #0
3788            // #676, type_check (heterogeneous table — runtime §4.4.8 type
3789            // check against the type-id sidecar at R11+off): after the
3790            // bounds guard —
3791            //                   LSL R12,idx,#2; ADD R12,R11,R12;
3792            //                   LDR R12,[R12,#type_off]; CMP.W R12,#id;
3793            //                   BEQ +1; UDF #0
3794            // (the dispatch tail then recomputes idx*4 — idx stays live).
3795            ArmOp::CallIndirect {
3796                rd: _,
3797                type_idx: _,
3798                table_index_reg,
3799                table_size,
3800                table_byte_offset,
3801                null_check,
3802                type_check,
3803            } => {
3804                let idx_reg = reg_to_bits(table_index_reg);
3805                let mut bytes = Vec::new();
3806
3807                // The expansion:
3808                // 1. Bounds guard (#642): trap (UDF #0, WASM Core §4.4.8) when
3809                //    index >= table size. Without it an out-of-bounds index
3810                //    reads past the table and BLXes whatever word lies there —
3811                //    an uncontrolled indirect branch instead of a trap.
3812                // 2. Multiplies index by 4 (function pointer size)
3813                // 3. Loads function pointer from table (table base in R11)
3814                // 4. Calls the function via BLX
3815                //
3816                // Table base setup must be done by caller/runtime. The type
3817                // check §4.4.8 also requires is discharged at COMPILE time:
3818                // the selector only emits this op after verifying the closed-
3819                // world property that every table entry's signature equals the
3820                // expected type (the raw code-pointer table carries no runtime
3821                // type ids to compare) — see the #642 selector guard.
3822
3823                // MOVW R12, #(size & 0xFFFF) — Thumb-2 T3:
3824                // 11110 i 100100 imm4 | 0 imm3 Rd imm8 (Rd=R12).
3825                let size_lo = *table_size & 0xFFFF;
3826                let hw1: u16 =
3827                    (0xF240 | (((size_lo >> 11) & 1) << 10) | ((size_lo >> 12) & 0xF)) as u16;
3828                let hw2: u16 =
3829                    ((((size_lo >> 8) & 0x7) << 12) | (12 << 8) | (size_lo & 0xFF)) as u16;
3830                bytes.extend_from_slice(&hw1.to_le_bytes());
3831                bytes.extend_from_slice(&hw2.to_le_bytes());
3832                // MOVT R12, #(size >> 16) — only when the table size exceeds
3833                // 16 bits (never in practice, but the guard must not compare
3834                // against a truncated size).
3835                let size_hi = *table_size >> 16;
3836                if size_hi != 0 {
3837                    let hw1: u16 =
3838                        (0xF2C0 | (((size_hi >> 11) & 1) << 10) | ((size_hi >> 12) & 0xF)) as u16;
3839                    let hw2: u16 =
3840                        ((((size_hi >> 8) & 0x7) << 12) | (12 << 8) | (size_hi & 0xFF)) as u16;
3841                    bytes.extend_from_slice(&hw1.to_le_bytes());
3842                    bytes.extend_from_slice(&hw2.to_le_bytes());
3843                }
3844                // CMP idx, R12 — 16-bit T2 (high-register capable):
3845                // 010001 01 N Rm(4) Rn(3), Rn full = N:Rn3.
3846                let cmp: u16 = (0x4500 | ((idx_reg & 8) << 4) | (12 << 3) | (idx_reg & 7)) as u16;
3847                bytes.extend_from_slice(&cmp.to_le_bytes());
3848                // BLO +1 insn (skip the UDF when index < size) — B<cond>.N
3849                // imm8=0: target = branch + 4. LO = unsigned lower.
3850                bytes.extend_from_slice(&0xD300u16.to_le_bytes());
3851                // UDF #0 — call_indirect out-of-bounds trap (same trap idiom as
3852                // the div-by-zero guards).
3853                bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3854
3855                // #676: runtime type check — ONLY for a heterogeneous table
3856                // (mixed signatures, closed-world verdict impossible). Load
3857                // the indexed slot's structural class id from the type-id
3858                // sidecar (`R11 + type_off + idx*4`; `type_off` = sidecar
3859                // base + this table's base offset, a compile-time constant)
3860                // and compare it against the expected type's class id — a
3861                // mismatch is the WASM Core §4.4.8 type trap. Null slots
3862                // carry the reserved id 0, so this compare subsumes the
3863                // #664 null trap (the selector passes `null_check: false`).
3864                // `None` emits NOTHING: every homogeneous table keeps the
3865                // pre-#676 bytes identical BY CONSTRUCTION. R12 stays the
3866                // only scratch (#212); the dispatch tail below recomputes
3867                // idx*4 — the index register is never clobbered here.
3868                if let Some((expected_id, type_off)) = type_check {
3869                    debug_assert!(*expected_id <= 255, "selector enforces the CMP imm8 range");
3870                    debug_assert!(*type_off <= 4095, "selector enforces the LDR imm12 range");
3871                    // MOV.W R12, idx, LSL #2 (same encoding as the dispatch
3872                    // tail's index scale below).
3873                    bytes.extend_from_slice(&0xEA4Fu16.to_le_bytes());
3874                    bytes.extend_from_slice(
3875                        &(((0x0C00 | (0b10 << 6)) | idx_reg) as u16).to_le_bytes(),
3876                    );
3877                    // ADD.W R12, R11, R12 (the #650 base-add form).
3878                    bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
3879                    bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
3880                    // LDR.W R12, [R12, #type_off] — T3 LDR (immediate):
3881                    // 1111 1000 1101 Rn=1100 | Rt=1100 imm12.
3882                    bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
3883                    bytes.extend_from_slice(
3884                        &(0xC000u16 | (*type_off as u16 & 0x0FFF)).to_le_bytes(),
3885                    );
3886                    // CMP.W R12, #expected_id — T2 CMP (immediate), imm8
3887                    // (same form as the #664 null check's CMP.W R12, #0).
3888                    bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
3889                    bytes.extend_from_slice(
3890                        &(0x0F00u16 | (*expected_id as u16 & 0xFF)).to_le_bytes(),
3891                    );
3892                    // BEQ +1 insn (skip the UDF when the class id matches) —
3893                    // B<cond>.N imm8=0: target = branch + 4. EQ.
3894                    bytes.extend_from_slice(&0xD000u16.to_le_bytes());
3895                    // UDF #0 — the §4.4.8 type-mismatch trap (same idiom as
3896                    // the bounds guard above).
3897                    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3898                }
3899
3900                // LSL R12, idx_reg, #2 (multiply index by 4)
3901                // Thumb-2 MOV with shift: 11101010 010 S 1111 | 0 imm3 Rd imm2 type Rm
3902                // LSL: type=00 (bits 5:4), imm5=2 -> imm3=000, imm2=10 (bits 7:6)
3903                // #597: the shift amount was previously shifted into bits 5:4 —
3904                // the TYPE field — encoding `mov.w ip, rm, ASR #32`, which
3905                // destroyed the index and dispatched table entry 0 for every
3906                // call. imm2 lives at bits 7:6.
3907                let hw1: u16 = 0xEA4F_u16; // MOV.W R12, Rm, LSL #2
3908                let hw2: u16 = ((0x0C00 | (0b10 << 6)) | idx_reg) as u16;
3909                bytes.extend_from_slice(&hw1.to_le_bytes());
3910                bytes.extend_from_slice(&hw2.to_le_bytes());
3911
3912                if *table_byte_offset == 0 {
3913                    // Table 0 (base = R11 itself): the pre-#650 single-load
3914                    // form — a single-table module's bytes stay identical BY
3915                    // CONSTRUCTION.
3916                    // LDR R12, [R11, R12] - load function pointer
3917                    // Thumb-2 LDR (register): 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
3918                    // Rn=R11, Rt=R12, Rm=R12, imm2=00 (no shift)
3919                    let ldr_hw1: u16 = 0xF85B; // LDR.W Rt, [R11, Rm]
3920                    let ldr_hw2: u16 = 0xC00C; // Rt=R12, imm2=00, Rm=R12
3921                    bytes.extend_from_slice(&ldr_hw1.to_le_bytes());
3922                    bytes.extend_from_slice(&ldr_hw2.to_le_bytes());
3923                } else {
3924                    // #650: table N of the contiguous R11 region — fold the
3925                    // compile-time base offset into the pointer load via the
3926                    // LDR imm12 form (R12 stays the only scratch, per the
3927                    // #212 convention).
3928                    assert!(
3929                        *table_byte_offset <= 4095,
3930                        "call_indirect table base offset {table_byte_offset} exceeds \
3931                         LDR imm12 — the selector must have declined this (#650)"
3932                    );
3933                    // ADD.W R12, R11, R12 — T3 ADD (register):
3934                    // 11101011000 S=0 Rn=1011 | 0 imm3=000 Rd=1100 imm2=00 type=00 Rm=1100
3935                    bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
3936                    bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
3937                    // LDR.W R12, [R12, #offset] — T3 LDR (immediate):
3938                    // 1111 1000 1101 Rn=1100 | Rt=1100 imm12
3939                    bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
3940                    bytes.extend_from_slice(
3941                        &((0xC000u16) | (*table_byte_offset as u16 & 0x0FFF)).to_le_bytes(),
3942                    );
3943                }
3944
3945                // #664: null-slot trap — ONLY when the table image carries
3946                // null (uninitialized) slots, which the layout contract
3947                // requires to be linked as ZERO words. A fully-initialized
3948                // table skips this branch entirely, keeping the pre-#664
3949                // expansion byte-identical BY CONSTRUCTION (the #650
3950                // offset-0 trick).
3951                if *null_check {
3952                    // CMP.W R12, #0 — T2 CMP (immediate): 11110 i 0 1101 1
3953                    // Rn(4) | 0 imm3 1111 imm8, Rn=R12, imm=0.
3954                    bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
3955                    bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
3956                    // BNE +1 insn (skip the UDF when the pointer is non-null)
3957                    // — B<cond>.N imm8=0: target = branch + 4. NE.
3958                    bytes.extend_from_slice(&0xD100u16.to_le_bytes());
3959                    // UDF #0 — call_indirect null-funcref trap (WASM Core
3960                    // §4.4.8: calling an uninitialized element traps; same
3961                    // trap idiom as the bounds guard above).
3962                    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3963                }
3964
3965                // BLX R12 (call function indirectly)
3966                // BLX Rm (16-bit): 0100 0111 1 Rm 000
3967                let blx: u16 = 0x47E0; // BLX R12
3968                bytes.extend_from_slice(&blx.to_le_bytes());
3969
3970                Ok(bytes)
3971            }
3972
3973            // Label pseudo-instruction: emits no machine code
3974            ArmOp::Label { .. } => Ok(Vec::new()),
3975
3976            // Conditional branch to label (generic) - offset 0, will be patched
3977            ArmOp::Bcc { cond, label: _ } => {
3978                use synth_synthesis::Condition;
3979                let cond_bits: u16 = match cond {
3980                    Condition::EQ => 0x0,
3981                    Condition::NE => 0x1,
3982                    Condition::HS => 0x2,
3983                    Condition::LO => 0x3,
3984                    Condition::HI => 0x8,
3985                    Condition::LS => 0x9,
3986                    Condition::GE => 0xA,
3987                    Condition::LT => 0xB,
3988                    Condition::GT => 0xC,
3989                    Condition::LE => 0xD,
3990                };
3991                // 16-bit B<cond> with offset 0: 1101 cond imm8
3992                let instr: u16 = 0xD000 | (cond_bits << 8);
3993                Ok(instr.to_le_bytes().to_vec())
3994            }
3995
3996            // Branch instructions
3997            ArmOp::B { label: _ } => {
3998                // Simplified: B.N with offset 0
3999                // For real usage, would need label resolution
4000                let instr: u16 = 0xE000; // B.N #0
4001                Ok(instr.to_le_bytes().to_vec())
4002            }
4003
4004            // BHS (Branch if Higher or Same) - used for bounds checking
4005            // Condition code: 0x2 (C set)
4006            ArmOp::Bhs { label: _ } => {
4007                // 16-bit B<cond> with offset 0: 1101 cond imm8
4008                // cond = 0x2 (HS)
4009                let instr: u16 = 0xD200; // BHS.N #0
4010                Ok(instr.to_le_bytes().to_vec())
4011            }
4012
4013            // BLO (Branch if Lower) - complementary to BHS
4014            // Condition code: 0x3 (C clear)
4015            ArmOp::Blo { label: _ } => {
4016                // 16-bit B<cond> with offset 0: 1101 cond imm8
4017                // cond = 0x3 (LO)
4018                let instr: u16 = 0xD300; // BLO.N #0
4019                Ok(instr.to_le_bytes().to_vec())
4020            }
4021
4022            // Branch with numeric offset (Thumb-2)
4023            // Thumb-2 B.W instruction: 32-bit with +-16MB range
4024            ArmOp::BOffset { offset } => {
4025                // offset is already the halfword displacement: (target - branch - 4) / 2
4026                // This is the raw encoded value, accounting for variable-length instructions
4027                let halfword_offset = *offset;
4028
4029                // 16-bit B.N encoding: 1110 0 imm11 (11-bit signed halfword offset)
4030                // Range: -1024 to +1022 halfwords
4031                if (-1024..=1022).contains(&halfword_offset) {
4032                    // 16-bit B.N encoding: 1110 0 imm11
4033                    let imm11 = (halfword_offset as u16) & 0x7FF;
4034                    let instr: u16 = 0xE000 | imm11;
4035                    Ok(instr.to_le_bytes().to_vec())
4036                } else {
4037                    // 32-bit B.W encoding for larger offsets
4038                    // First halfword: 1111 0 S imm10
4039                    // Second halfword: 10 J1 0 J2 imm11
4040                    // Total offset = SignExtend(S:I1:I2:imm10:imm11:0)
4041                    // where I1 = NOT(J1 XOR S), I2 = NOT(J2 XOR S)
4042
4043                    // The B.W (T4) encoding packs the signed offset as:
4044                    //   S:I1:I2:imm10:imm11:0  (25-bit signed, halfword-aligned)
4045                    // where J1 = NOT(I1 XOR S), J2 = NOT(I2 XOR S)
4046                    // Input halfword_offset already equals (target - PC - 4) / 2,
4047                    // so the full byte offset = halfword_offset << 1.
4048                    // The encoding fields split that 25-bit signed value (including the
4049                    // implicit trailing zero) as: S | imm10 | imm11
4050                    // with I1 = bit 23 and I2 = bit 22 of the signed offset.
4051                    let signed_offset = halfword_offset << 1; // byte offset
4052                    let s = if signed_offset < 0 { 1u32 } else { 0u32 };
4053                    let uoffset = signed_offset as u32;
4054                    let imm10 = (uoffset >> 12) & 0x3FF; // bits [21:12]
4055                    let imm11 = (uoffset >> 1) & 0x7FF; // bits [11:1]
4056                    let i1 = (uoffset >> 23) & 1; // bit 23
4057                    let i2 = (uoffset >> 22) & 1; // bit 22
4058                    let j1 = (!(i1 ^ s)) & 1; // J1 = NOT(I1 XOR S)
4059                    let j2 = (!(i2 ^ s)) & 1; // J2 = NOT(I2 XOR S)
4060
4061                    let hw1: u16 = (0xF000 | (s << 10) | imm10) as u16;
4062                    let hw2: u16 = (0x9000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4063
4064                    let mut bytes = hw1.to_le_bytes().to_vec();
4065                    bytes.extend_from_slice(&hw2.to_le_bytes());
4066                    Ok(bytes)
4067                }
4068            }
4069
4070            // Conditional branch with numeric offset (Thumb-2)
4071            ArmOp::BCondOffset { cond, offset } => {
4072                use synth_synthesis::Condition;
4073                let cond_bits: u16 = match cond {
4074                    Condition::EQ => 0x0,
4075                    Condition::NE => 0x1,
4076                    Condition::HS => 0x2,
4077                    Condition::LO => 0x3,
4078                    Condition::HI => 0x8,
4079                    Condition::LS => 0x9,
4080                    Condition::GE => 0xA,
4081                    Condition::LT => 0xB,
4082                    Condition::GT => 0xC,
4083                    Condition::LE => 0xD,
4084                };
4085
4086                // offset is already the halfword displacement: (target - branch - 4) / 2
4087                // This is the raw imm8 value for 16-bit B<cond> encoding
4088                let halfword_offset = *offset;
4089
4090                // 16-bit B<cond> encoding: 1101 cond imm8
4091                // Range: -256 to +254 halfwords (imm8 is sign-extended and shifted left 1)
4092                if (-128..=127).contains(&halfword_offset) {
4093                    let imm8 = (halfword_offset as u16) & 0xFF;
4094                    let instr: u16 = 0xD000 | (cond_bits << 8) | imm8;
4095                    Ok(instr.to_le_bytes().to_vec())
4096                } else {
4097                    // 32-bit B<cond>.W for larger offsets
4098                    // First halfword: 1111 0 S cond imm6
4099                    // Second halfword: 10 J1 0 J2 imm11
4100                    let offset = halfword_offset >> 1;
4101                    let s = if offset < 0 { 1u32 } else { 0u32 };
4102                    let imm6 = ((offset >> 11) as u32) & 0x3F;
4103                    let imm11 = (offset as u32) & 0x7FF;
4104                    let j1 = if s == 1 { 1 } else { 0 };
4105                    let j2 = if s == 1 { 1 } else { 0 };
4106
4107                    let hw1: u16 = (0xF000 | (s << 10) | ((cond_bits as u32) << 6) | imm6) as u16;
4108                    let hw2: u16 = (0x8000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4109
4110                    let mut bytes = hw1.to_le_bytes().to_vec();
4111                    bytes.extend_from_slice(&hw2.to_le_bytes());
4112                    Ok(bytes)
4113                }
4114            }
4115
4116            ArmOp::Bl { label: _ } => {
4117                // BL is always 32-bit in Thumb-2, encoded here as a relocatable
4118                // placeholder; an R_ARM_THM_CALL relocation patches the target
4119                // (see arm_backend.rs). The placeholder must carry an embedded
4120                // addend of -4 so the relocation nets to exactly the symbol S.
4121                //
4122                // Thumb BL computes `target = (P + 4) + signed_offset`. Under
4123                // R_ARM_THM_CALL the linker resolves using the in-place addend;
4124                // a 0xF800 placeholder (addend 0) lands at S+4 — every call one
4125                // instruction past the callee entry (#174). The correct
4126                // placeholder is what `gas` emits for `bl <extern>`:
4127                //   f7ff fffe  ->  `bl <self>`  (S=1, J1=J2=1, imm = -4 addend),
4128                // i.e. hw1=0xF7FF, hw2=0xFFFE. This nets to S, not S+4.
4129                // (The earlier 0xD000 was worse still — a ~+0x600000 addend,
4130                // the garbage `bl c0000c` and "truncated to fit" of #167.)
4131                let hw1: u16 = 0xF7FF;
4132                let hw2: u16 = 0xFFFE;
4133                let mut bytes = hw1.to_le_bytes().to_vec();
4134                bytes.extend_from_slice(&hw2.to_le_bytes());
4135                Ok(bytes)
4136            }
4137
4138            // MVN
4139            ArmOp::Mvn { rd, op2 } => {
4140                if let Operand2::Reg(rm) = op2 {
4141                    let rd_bits = reg_to_bits(rd) as u16;
4142                    let rm_bits = reg_to_bits(rm) as u16;
4143
4144                    if rd_bits < 8 && rm_bits < 8 {
4145                        // MVNS Rd, Rm (16-bit): 0100 0011 11 Rm Rd
4146                        let instr: u16 = 0x43C0 | (rm_bits << 3) | rd_bits;
4147                        Ok(instr.to_le_bytes().to_vec())
4148                    } else {
4149                        // 32-bit MVN
4150                        let hw1: u16 = 0xEA6F_u16;
4151                        let hw2: u16 = ((reg_to_bits(rd) << 8) | reg_to_bits(rm)) as u16;
4152                        let mut bytes = hw1.to_le_bytes().to_vec();
4153                        bytes.extend_from_slice(&hw2.to_le_bytes());
4154                        Ok(bytes)
4155                    }
4156                } else {
4157                    let instr: u16 = 0xBF00;
4158                    Ok(instr.to_le_bytes().to_vec())
4159                }
4160            }
4161
4162            // MOVW - Move Wide (Thumb-2 32-bit)
4163            ArmOp::Movw { rd, imm16 } => {
4164                self.encode_thumb32_movw_raw(reg_to_bits(rd), *imm16 as u32)
4165            }
4166
4167            // MOVT - Move Top (Thumb-2 32-bit)
4168            ArmOp::Movt { rd, imm16 } => {
4169                self.encode_thumb32_movt_raw(reg_to_bits(rd), *imm16 as u32)
4170            }
4171
4172            // #237: symbol-relative MOVW/MOVT. Encode the addend's low/high 16
4173            // bits in place; the backend records an R_ARM_MOVW_ABS_NC /
4174            // R_ARM_MOVT_ABS relocation against `symbol`, so the linker adds the
4175            // symbol's final address to the in-place addend (REL semantics).
4176            ArmOp::MovwSym { rd, addend, .. } => {
4177                self.encode_thumb32_movw_raw(reg_to_bits(rd), (*addend as u32) & 0xffff)
4178            }
4179            ArmOp::MovtSym { rd, addend, .. } => {
4180                self.encode_thumb32_movt_raw(reg_to_bits(rd), ((*addend as u32) >> 16) & 0xffff)
4181            }
4182
4183            // #345: literal-pool address load — emit a PLACEHOLDER `LDR.W rd,
4184            // [pc, #0]` (U=1, imm12=0). The backend (arm_backend.rs) places the
4185            // 4-byte pool word at the end of the function, records the R_ARM_ABS32
4186            // relocation against `symbol+addend`, and patches the imm12 with the
4187            // real PC-relative distance once the pool offset is known.
4188            // Encoding T2: 1111 1000 1101 1111 | Rt(4) imm12(12), with the literal
4189            // base = Align(PC,4) and PC = address of this instruction + 4.
4190            ArmOp::LdrSym { rd, .. } => {
4191                let rt = reg_to_bits(rd) as u16;
4192                let hw1: u16 = 0xF8DF; // LDR.W (literal), U=1
4193                let hw2: u16 = rt << 12; // imm12 = 0 placeholder
4194                let mut bytes = Vec::with_capacity(4);
4195                bytes.extend_from_slice(&hw1.to_le_bytes());
4196                bytes.extend_from_slice(&hw2.to_le_bytes());
4197                Ok(bytes)
4198            }
4199
4200            // SetCond: Materialize condition flag into register (0 or 1)
4201            // Strategy: ITE <cond>; MOV Rd, #1; MOV Rd, #0
4202            // IMPORTANT: Must use ITE (If-Then-Else) because 16-bit Thumb MOV
4203            // always sets flags (MOVS). We need to evaluate the condition BEFORE
4204            // any MOV instruction clobbers the flags from CMP.
4205            ArmOp::SetCond { rd, cond } => {
4206                let rd_bits = reg_to_bits(rd) as u16;
4207
4208                // Condition code encoding for IT block
4209                use synth_synthesis::Condition;
4210                let cond_bits: u16 = match cond {
4211                    Condition::EQ => 0x0,
4212                    Condition::NE => 0x1,
4213                    Condition::LT => 0xB,
4214                    Condition::LE => 0xD,
4215                    Condition::GT => 0xC,
4216                    Condition::GE => 0xA,
4217                    Condition::LO => 0x3, // CC/LO (unsigned <)
4218                    Condition::LS => 0x9, // LS (unsigned <=)
4219                    Condition::HI => 0x8, // HI (unsigned >)
4220                    Condition::HS => 0x2, // CS/HS (unsigned >=)
4221                };
4222
4223                // ITE <cond>: encodes If-Then-Else block
4224                // The mask field depends on firstcond[0]:
4225                // - If firstcond[0] = 0: mask = 0xC for TE pattern (ITE EQ = BF0C)
4226                // - If firstcond[0] = 1: mask = 0x4 for TE pattern (ITE NE = BF14)
4227                let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4228                let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4229
4230                // Materialize 0/1 into Rd. The 16-bit MOVS (T1) encodes Rd in a
4231                // 3-bit field (bits[10:8]) — only R0–R7. For a high register
4232                // (R8–R12) `rd_bits << 8` overflows into bit 11 and silently
4233                // turns MOVS into CMP (00100 → 00101), corrupting the result
4234                // (this mis-materialized gale's `has_waiter`, so its `local.set`
4235                // stored a stale register → the binary-sem WAKE dispatch read
4236                // garbage). Use the 32-bit MOV.W (T2) for high registers, which
4237                // has a 4-bit Rd field. MOV.W with S=0 doesn't set flags, which
4238                // is fine inside the ITE (the materialized value is the result;
4239                // the flags are not consumed afterwards).
4240                let mut bytes = ite_instr.to_le_bytes().to_vec();
4241                let push_mov = |bytes: &mut Vec<u8>, imm: u16| {
4242                    if rd_bits <= 7 {
4243                        let m: u16 = 0x2000 | (rd_bits << 8) | imm; // 16-bit MOVS Rd,#imm
4244                        bytes.extend_from_slice(&m.to_le_bytes());
4245                    } else {
4246                        // 32-bit MOV.W Rd, #imm (T2): F04F | (Rd<<8) | imm8
4247                        let hw1: u16 = 0xF04F;
4248                        let hw2: u16 = (rd_bits << 8) | imm;
4249                        bytes.extend_from_slice(&hw1.to_le_bytes());
4250                        bytes.extend_from_slice(&hw2.to_le_bytes());
4251                    }
4252                };
4253                push_mov(&mut bytes, 1); // Then branch (condition true)  → 1
4254                push_mov(&mut bytes, 0); // Else branch (condition false) → 0
4255                Ok(bytes)
4256            }
4257
4258            // I64SetCond: Compare two i64 register pairs, result 0/1 in rd
4259            // EQ/NE: CMP lo,lo; IT EQ; CMPEQ hi,hi; ITE <cond>; MOV 1; MOV 0
4260            // LT: CMP lo,lo; SBCS rd,hi,hi; ITE LT; MOV 1; MOV 0
4261            // GT: CMP lo,lo (swapped); SBCS rd,hi,hi (swapped); ITE LT; MOV 1; MOV 0
4262            ArmOp::I64SetCond {
4263                rd,
4264                rn_lo,
4265                rn_hi,
4266                rm_lo,
4267                rm_hi,
4268                cond,
4269            } => {
4270                use synth_synthesis::Condition;
4271                let rd_bits = reg_to_bits(rd) as u16;
4272                let mut bytes = Vec::new();
4273
4274                // Helper: encode CMP Rn, Rm (16-bit)
4275                let encode_cmp_reg = |rn: &synth_synthesis::Reg,
4276                                      rm: &synth_synthesis::Reg|
4277                 -> Vec<u8> {
4278                    let rn_bits = reg_to_bits(rn) as u16;
4279                    let rm_bits = reg_to_bits(rm) as u16;
4280                    if rn_bits < 8 && rm_bits < 8 {
4281                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
4282                        instr.to_le_bytes().to_vec()
4283                    } else {
4284                        let n_bit = (rn_bits >> 3) & 1;
4285                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
4286                        instr.to_le_bytes().to_vec()
4287                    }
4288                };
4289
4290                // Helper: encode ITE <cond> (2 bytes)
4291                let encode_ite = |cond_bits: u16| -> Vec<u8> {
4292                    let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4293                    let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4294                    ite_instr.to_le_bytes().to_vec()
4295                };
4296
4297                // Helper: encode SetCond (ITE + MOV #1 + MOV #0) for given condition
4298                let encode_setcond = |cond_bits: u16, rd_bits: u16| -> Vec<u8> {
4299                    let mut b = encode_ite(cond_bits);
4300                    if rd_bits < 8 {
4301                        let mov_one: u16 = 0x2001 | (rd_bits << 8);
4302                        let mov_zero: u16 = 0x2000 | (rd_bits << 8);
4303                        b.extend_from_slice(&mov_one.to_le_bytes());
4304                        b.extend_from_slice(&mov_zero.to_le_bytes());
4305                    } else {
4306                        // #311: rd >= R8 — the 16-bit MOV imm8 form has a 3-bit
4307                        // rd field; rd_bits<<8 overflows into bit 11 and
4308                        // TRANSMUTES the MOV into CMP (0x2001|0x0800 = 0x2801 =
4309                        // CMP r0,#1): the boolean dies in the flags and the
4310                        // consumer reads a stale register. Use the 32-bit
4311                        // MOV.W (T2: F04F 0000|rd<<8|imm8) — IT-legal,
4312                        // flag-preserving. Same class as H-CODE-9 / #180.
4313                        for imm in [1u16, 0u16] {
4314                            let hw1: u16 = 0xF04F;
4315                            let hw2: u16 = (rd_bits << 8) | imm;
4316                            b.extend_from_slice(&hw1.to_le_bytes());
4317                            b.extend_from_slice(&hw2.to_le_bytes());
4318                        }
4319                    }
4320                    b
4321                };
4322
4323                match cond {
4324                    Condition::EQ | Condition::NE => {
4325                        // CMP rn_lo, rm_lo (compare low words)
4326                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4327
4328                        // IT EQ (execute next instruction only if Z=1)
4329                        let it_eq: u16 = 0xBF08; // IT EQ: cond=0000, mask=1000
4330                        bytes.extend_from_slice(&it_eq.to_le_bytes());
4331
4332                        // CMPEQ rn_hi, rm_hi (compare high words, only if low equal)
4333                        bytes.extend_from_slice(&encode_cmp_reg(rn_hi, rm_hi));
4334
4335                        // ITE <cond>; MOV rd, #1; MOV rd, #0
4336                        let cond_bits: u16 = match cond {
4337                            Condition::EQ => 0x0,
4338                            Condition::NE => 0x1,
4339                            _ => unreachable!(),
4340                        };
4341                        bytes.extend_from_slice(&encode_setcond(cond_bits, rd_bits));
4342                    }
4343
4344                    Condition::LT => {
4345                        // CMP rn_lo, rm_lo (sets C flag for borrow)
4346                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4347
4348                        // SBCS rd, rn_hi, rm_hi (subtract with carry, sets N,V flags)
4349                        // SBCS.W Rd, Rn, Rm: EB70 Rn | 0000 Rd 0000 Rm
4350                        let rn_hi_bits = reg_to_bits(rn_hi);
4351                        let rm_hi_bits = reg_to_bits(rm_hi);
4352                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4353                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4354                        bytes.extend_from_slice(&hw1.to_le_bytes());
4355                        bytes.extend_from_slice(&hw2.to_le_bytes());
4356
4357                        // ITE LT; MOV rd, #1; MOV rd, #0
4358                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4359                    }
4360
4361                    Condition::GT => {
4362                        // GT(a,b) = LT(b,a): swap operands
4363                        // CMP rm_lo, rn_lo (swapped)
4364                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4365
4366                        // SBCS rd, rm_hi, rn_hi (swapped)
4367                        let rm_hi_bits = reg_to_bits(rm_hi);
4368                        let rn_hi_bits = reg_to_bits(rn_hi);
4369                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4370                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4371                        bytes.extend_from_slice(&hw1.to_le_bytes());
4372                        bytes.extend_from_slice(&hw2.to_le_bytes());
4373
4374                        // ITE LT; MOV rd, #1; MOV rd, #0
4375                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4376                    }
4377
4378                    Condition::LE => {
4379                        // LE(a,b) = !GT(a,b): use GT logic but invert result
4380                        // GT(a,b) = LT(b,a): so we do CMP(b,a) and check LT, then invert
4381                        // CMP rm_lo, rn_lo (swapped, same as GT)
4382                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4383
4384                        // SBCS rd, rm_hi, rn_hi (swapped)
4385                        let rm_hi_bits = reg_to_bits(rm_hi);
4386                        let rn_hi_bits = reg_to_bits(rn_hi);
4387                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4388                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4389                        bytes.extend_from_slice(&hw1.to_le_bytes());
4390                        bytes.extend_from_slice(&hw2.to_le_bytes());
4391
4392                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT, so inverting GT result)
4393                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4394                    }
4395
4396                    Condition::GE => {
4397                        // GE(a,b) = !LT(a,b): use LT logic but invert result
4398                        // CMP rn_lo, rm_lo (same as LT)
4399                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4400
4401                        // SBCS rd, rn_hi, rm_hi (same as LT)
4402                        let rn_hi_bits = reg_to_bits(rn_hi);
4403                        let rm_hi_bits = reg_to_bits(rm_hi);
4404                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4405                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4406                        bytes.extend_from_slice(&hw1.to_le_bytes());
4407                        bytes.extend_from_slice(&hw2.to_le_bytes());
4408
4409                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT)
4410                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4411                    }
4412
4413                    // Unsigned comparisons - same instruction sequence, different conditions
4414                    Condition::LO => {
4415                        // LO (unsigned LT): CMP lo, SBCS hi, check C=0
4416                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4417                        let rn_hi_bits = reg_to_bits(rn_hi);
4418                        let rm_hi_bits = reg_to_bits(rm_hi);
4419                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4420                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4421                        bytes.extend_from_slice(&hw1.to_le_bytes());
4422                        bytes.extend_from_slice(&hw2.to_le_bytes());
4423                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4424                    }
4425
4426                    Condition::HI => {
4427                        // HI (unsigned GT): swap operands and check LO
4428                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4429                        let rm_hi_bits = reg_to_bits(rm_hi);
4430                        let rn_hi_bits = reg_to_bits(rn_hi);
4431                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4432                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4433                        bytes.extend_from_slice(&hw1.to_le_bytes());
4434                        bytes.extend_from_slice(&hw2.to_le_bytes());
4435                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4436                    }
4437
4438                    Condition::LS => {
4439                        // LS (unsigned LE): !(a > b) = !(HI), so do HI and invert
4440                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4441                        let rm_hi_bits = reg_to_bits(rm_hi);
4442                        let rn_hi_bits = reg_to_bits(rn_hi);
4443                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4444                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4445                        bytes.extend_from_slice(&hw1.to_le_bytes());
4446                        bytes.extend_from_slice(&hw2.to_le_bytes());
4447                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4448                    }
4449
4450                    Condition::HS => {
4451                        // HS (unsigned GE): !(a < b) = !(LO)
4452                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4453                        let rn_hi_bits = reg_to_bits(rn_hi);
4454                        let rm_hi_bits = reg_to_bits(rm_hi);
4455                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4456                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4457                        bytes.extend_from_slice(&hw1.to_le_bytes());
4458                        bytes.extend_from_slice(&hw2.to_le_bytes());
4459                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4460                    }
4461                }
4462
4463                Ok(bytes)
4464            }
4465
4466            // I64SetCondZ: Test if i64 register pair is zero, result 0/1 in rd
4467            // ORR.W rd, rn_lo, rn_hi; CMP rd, #0; ITE EQ; MOV 1; MOV 0
4468            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
4469                let rd_bits = reg_to_bits(rd);
4470                let rn_lo_bits = reg_to_bits(rn_lo);
4471                let rn_hi_bits = reg_to_bits(rn_hi);
4472                let mut bytes = Vec::new();
4473
4474                // ORR.W rd, rn_lo, rn_hi: EA40 rn_lo | 0000 rd 0000 rn_hi
4475                let hw1: u16 = (0xEA40 | rn_lo_bits) as u16;
4476                let hw2: u16 = ((rd_bits << 8) | rn_hi_bits) as u16;
4477                bytes.extend_from_slice(&hw1.to_le_bytes());
4478                bytes.extend_from_slice(&hw2.to_le_bytes());
4479
4480                // CMP rd, #0 — 16-bit form only for r0-r7 (3-bit rd field);
4481                // high registers take CMP.W (T2: F1B0|rn 0F00|imm8). This was
4482                // H-CODE-9: rd_bits<<8 overflowing the field compared the
4483                // WRONG register. Same hardening as the #311 SetCond fix.
4484                if rd_bits < 8 {
4485                    let cmp_instr: u16 = 0x2800 | ((rd_bits as u16) << 8);
4486                    bytes.extend_from_slice(&cmp_instr.to_le_bytes());
4487                } else {
4488                    let hw1: u16 = 0xF1B0 | (rd_bits as u16);
4489                    let hw2: u16 = 0x0F00;
4490                    bytes.extend_from_slice(&hw1.to_le_bytes());
4491                    bytes.extend_from_slice(&hw2.to_le_bytes());
4492                }
4493
4494                // ITE EQ; MOV rd, #1; MOV rd, #0 (32-bit MOV.W for rd >= R8,
4495                // #311 — see I64SetCond)
4496                let mask = 0xC_u16; // ITE EQ mask: firstcond[0]=0, mask=0xC
4497                let ite_instr: u16 = 0xBF00 | mask;
4498                bytes.extend_from_slice(&ite_instr.to_le_bytes());
4499                if rd_bits < 8 {
4500                    let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
4501                    let mov_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
4502                    bytes.extend_from_slice(&mov_one.to_le_bytes());
4503                    bytes.extend_from_slice(&mov_zero.to_le_bytes());
4504                } else {
4505                    for imm in [1u16, 0u16] {
4506                        let hw1: u16 = 0xF04F;
4507                        let hw2: u16 = ((rd_bits as u16) << 8) | imm;
4508                        bytes.extend_from_slice(&hw1.to_le_bytes());
4509                        bytes.extend_from_slice(&hw2.to_le_bytes());
4510                    }
4511                }
4512
4513                Ok(bytes)
4514            }
4515
4516            // I64Mul: 64-bit multiply using UMULL + MLA cross products
4517            // Formula: result = (a_lo * b_lo) + ((a_lo * b_hi + a_hi * b_lo) << 32)
4518            // Uses R12 as scratch register
4519            ArmOp::I64Mul {
4520                rd_lo,
4521                rd_hi,
4522                rn_lo,
4523                rn_hi,
4524                rm_lo,
4525                rm_hi,
4526            } => {
4527                let rd_lo_bits = reg_to_bits(rd_lo);
4528                let rd_hi_bits = reg_to_bits(rd_hi);
4529                let rn_lo_bits = reg_to_bits(rn_lo);
4530                let rn_hi_bits = reg_to_bits(rn_hi);
4531                let rm_lo_bits = reg_to_bits(rm_lo);
4532                let rm_hi_bits = reg_to_bits(rm_hi);
4533                let r12: u32 = 12; // IP scratch register
4534                let mut bytes = Vec::new();
4535
4536                // 1. MUL R12, rn_lo, rm_hi  (R12 = a_lo * b_hi)
4537                // Thumb-2 MUL: hw1=0xFB00|Rn, hw2=0xF000|(Rd<<8)|Rm
4538                let hw1: u16 = (0xFB00 | rn_lo_bits) as u16;
4539                let hw2: u16 = (0xF000 | (r12 << 8) | rm_hi_bits) as u16;
4540                bytes.extend_from_slice(&hw1.to_le_bytes());
4541                bytes.extend_from_slice(&hw2.to_le_bytes());
4542
4543                // 2. MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
4544                // Thumb-2 MLA: hw1=0xFB00|Rn, hw2=(Ra<<12)|(Rd<<8)|Rm
4545                let hw1: u16 = (0xFB00 | rn_hi_bits) as u16;
4546                let hw2: u16 = ((r12 << 12) | (r12 << 8) | rm_lo_bits) as u16;
4547                bytes.extend_from_slice(&hw1.to_le_bytes());
4548                bytes.extend_from_slice(&hw2.to_le_bytes());
4549
4550                // 3. UMULL rd_lo, rd_hi, rn_lo, rm_lo  (rd_lo:rd_hi = a_lo * b_lo)
4551                // Thumb-2 UMULL: hw1=0xFBA0|Rn, hw2=(RdLo<<12)|(RdHi<<8)|Rm
4552                let hw1: u16 = (0xFBA0 | rn_lo_bits) as u16;
4553                let hw2: u16 = ((rd_lo_bits << 12) | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4554                bytes.extend_from_slice(&hw1.to_le_bytes());
4555                bytes.extend_from_slice(&hw2.to_le_bytes());
4556
4557                // 4. ADD rd_hi, R12  (rd_hi += cross products)
4558                // 16-bit high reg ADD: 01000100 D Rm Rdn[2:0]
4559                let d_bit = (rd_hi_bits >> 3) & 1;
4560                let add_instr: u16 =
4561                    (0x4400 | (d_bit << 7) | (r12 << 3) | (rd_hi_bits & 0x7)) as u16;
4562                bytes.extend_from_slice(&add_instr.to_le_bytes());
4563
4564                Ok(bytes)
4565            }
4566
4567            // I64Shl: 64-bit shift left with branch for n<32 vs n>=32
4568            // rm_hi (R3) is used as temp register
4569            ArmOp::I64Shl {
4570                rd_lo,
4571                rd_hi,
4572                rn_lo,
4573                rn_hi,
4574                rm_lo,
4575                rm_hi,
4576            } => {
4577                let rd_lo_bits = reg_to_bits(rd_lo);
4578                let rd_hi_bits = reg_to_bits(rd_hi);
4579                let rn_lo_bits = reg_to_bits(rn_lo);
4580                let rn_hi_bits = reg_to_bits(rn_hi);
4581                let rm_lo_bits = reg_to_bits(rm_lo);
4582                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4583                let mut bytes = Vec::new();
4584
4585                // AND.W rm_lo, rm_lo, #63  (mask shift amount to 6 bits)
4586                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4587                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4588                bytes.extend_from_slice(&hw1.to_le_bytes());
4589                bytes.extend_from_slice(&hw2.to_le_bytes());
4590
4591                // SUBS.W rm_hi, rm_lo, #32  (rm_hi = n-32, sets flags)
4592                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4593                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4594                bytes.extend_from_slice(&hw1.to_le_bytes());
4595                bytes.extend_from_slice(&hw2.to_le_bytes());
4596
4597                // BPL .large (branch if n >= 32, offset = +10 halfwords)
4598                let bpl: u16 = 0xD50A;
4599                bytes.extend_from_slice(&bpl.to_le_bytes());
4600
4601                // --- Small shift (n < 32) ---
4602                // RSB.W rm_hi, rm_lo, #32  (rm_hi = 32-n)
4603                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4604                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4605                bytes.extend_from_slice(&hw1.to_le_bytes());
4606                bytes.extend_from_slice(&hw2.to_le_bytes());
4607
4608                // LSR.W rm_hi, rn_lo, rm_hi  (rm_hi = lo >> (32-n), overflow bits)
4609                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4610                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4611                bytes.extend_from_slice(&hw1.to_le_bytes());
4612                bytes.extend_from_slice(&hw2.to_le_bytes());
4613
4614                // LSL.W rd_hi, rn_hi, rm_lo  (hi <<= n)
4615                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4616                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4617                bytes.extend_from_slice(&hw1.to_le_bytes());
4618                bytes.extend_from_slice(&hw2.to_le_bytes());
4619
4620                // ORR.W rd_hi, rd_hi, rm_hi  (hi |= overflow bits from lo)
4621                let hw1: u16 = (0xEA40 | rd_hi_bits) as u16;
4622                let hw2: u16 = ((rd_hi_bits << 8) | rm_hi_bits) as u16;
4623                bytes.extend_from_slice(&hw1.to_le_bytes());
4624                bytes.extend_from_slice(&hw2.to_le_bytes());
4625
4626                // LSL.W rd_lo, rn_lo, rm_lo  (lo <<= n)
4627                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4628                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4629                bytes.extend_from_slice(&hw1.to_le_bytes());
4630                bytes.extend_from_slice(&hw2.to_le_bytes());
4631
4632                // B .done (skip large shift: +2 halfwords)
4633                let b_done: u16 = 0xE002;
4634                bytes.extend_from_slice(&b_done.to_le_bytes());
4635
4636                // --- Large shift (n >= 32) ---
4637                // LSL.W rd_hi, rn_lo, rm_hi  (hi = lo << (n-32))
4638                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4639                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_hi_bits) as u16;
4640                bytes.extend_from_slice(&hw1.to_le_bytes());
4641                bytes.extend_from_slice(&hw2.to_le_bytes());
4642
4643                // MOV rd_lo, #0
4644                let mov_zero: u16 = 0x2000 | ((rd_lo_bits as u16) << 8);
4645                bytes.extend_from_slice(&mov_zero.to_le_bytes());
4646
4647                Ok(bytes) // Total: 38 bytes
4648            }
4649
4650            // I64ShrU: 64-bit logical shift right with branch for n<32 vs n>=32
4651            ArmOp::I64ShrU {
4652                rd_lo,
4653                rd_hi,
4654                rn_lo,
4655                rn_hi,
4656                rm_lo,
4657                rm_hi,
4658            } => {
4659                let rd_lo_bits = reg_to_bits(rd_lo);
4660                let rd_hi_bits = reg_to_bits(rd_hi);
4661                let rn_lo_bits = reg_to_bits(rn_lo);
4662                let rn_hi_bits = reg_to_bits(rn_hi);
4663                let rm_lo_bits = reg_to_bits(rm_lo);
4664                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4665                let mut bytes = Vec::new();
4666
4667                // AND.W rm_lo, rm_lo, #63
4668                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4669                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4670                bytes.extend_from_slice(&hw1.to_le_bytes());
4671                bytes.extend_from_slice(&hw2.to_le_bytes());
4672
4673                // SUBS.W rm_hi, rm_lo, #32
4674                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4675                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4676                bytes.extend_from_slice(&hw1.to_le_bytes());
4677                bytes.extend_from_slice(&hw2.to_le_bytes());
4678
4679                // BPL .large (+10 halfwords)
4680                let bpl: u16 = 0xD50A;
4681                bytes.extend_from_slice(&bpl.to_le_bytes());
4682
4683                // --- Small shift (n < 32) ---
4684                // RSB.W rm_hi, rm_lo, #32  (rm_hi = 32-n)
4685                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4686                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4687                bytes.extend_from_slice(&hw1.to_le_bytes());
4688                bytes.extend_from_slice(&hw2.to_le_bytes());
4689
4690                // LSL.W rm_hi, rn_hi, rm_hi  (rm_hi = hi << (32-n), bits flowing to lo)
4691                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4692                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4693                bytes.extend_from_slice(&hw1.to_le_bytes());
4694                bytes.extend_from_slice(&hw2.to_le_bytes());
4695
4696                // LSR.W rd_lo, rn_lo, rm_lo  (lo >>= n)
4697                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4698                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4699                bytes.extend_from_slice(&hw1.to_le_bytes());
4700                bytes.extend_from_slice(&hw2.to_le_bytes());
4701
4702                // ORR.W rd_lo, rd_lo, rm_hi  (lo |= overflow from hi)
4703                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4704                let hw2: u16 = ((rd_lo_bits << 8) | rm_hi_bits) as u16;
4705                bytes.extend_from_slice(&hw1.to_le_bytes());
4706                bytes.extend_from_slice(&hw2.to_le_bytes());
4707
4708                // LSR.W rd_hi, rn_hi, rm_lo  (hi >>= n, logical)
4709                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4710                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4711                bytes.extend_from_slice(&hw1.to_le_bytes());
4712                bytes.extend_from_slice(&hw2.to_le_bytes());
4713
4714                // B .done (+2 halfwords)
4715                let b_done: u16 = 0xE002;
4716                bytes.extend_from_slice(&b_done.to_le_bytes());
4717
4718                // --- Large shift (n >= 32) ---
4719                // LSR.W rd_lo, rn_hi, rm_hi  (lo = hi >> (n-32))
4720                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4721                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_hi_bits) as u16;
4722                bytes.extend_from_slice(&hw1.to_le_bytes());
4723                bytes.extend_from_slice(&hw2.to_le_bytes());
4724
4725                // MOV rd_hi, #0
4726                let mov_zero: u16 = 0x2000 | ((rd_hi_bits as u16) << 8);
4727                bytes.extend_from_slice(&mov_zero.to_le_bytes());
4728
4729                Ok(bytes) // Total: 38 bytes
4730            }
4731
4732            // I64ShrS: 64-bit arithmetic shift right with branch for n<32 vs n>=32
4733            ArmOp::I64ShrS {
4734                rd_lo,
4735                rd_hi,
4736                rn_lo,
4737                rn_hi,
4738                rm_lo,
4739                rm_hi,
4740            } => {
4741                let rd_lo_bits = reg_to_bits(rd_lo);
4742                let rd_hi_bits = reg_to_bits(rd_hi);
4743                let rn_lo_bits = reg_to_bits(rn_lo);
4744                let rn_hi_bits = reg_to_bits(rn_hi);
4745                let rm_lo_bits = reg_to_bits(rm_lo);
4746                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4747                let mut bytes = Vec::new();
4748
4749                // AND.W rm_lo, rm_lo, #63
4750                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4751                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4752                bytes.extend_from_slice(&hw1.to_le_bytes());
4753                bytes.extend_from_slice(&hw2.to_le_bytes());
4754
4755                // SUBS.W rm_hi, rm_lo, #32
4756                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4757                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4758                bytes.extend_from_slice(&hw1.to_le_bytes());
4759                bytes.extend_from_slice(&hw2.to_le_bytes());
4760
4761                // BPL .large (+10 halfwords)
4762                let bpl: u16 = 0xD50A;
4763                bytes.extend_from_slice(&bpl.to_le_bytes());
4764
4765                // --- Small shift (n < 32) ---
4766                // RSB.W rm_hi, rm_lo, #32
4767                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4768                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4769                bytes.extend_from_slice(&hw1.to_le_bytes());
4770                bytes.extend_from_slice(&hw2.to_le_bytes());
4771
4772                // LSL.W rm_hi, rn_hi, rm_hi  (rm_hi = hi << (32-n), bits flowing to lo)
4773                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4774                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4775                bytes.extend_from_slice(&hw1.to_le_bytes());
4776                bytes.extend_from_slice(&hw2.to_le_bytes());
4777
4778                // LSR.W rd_lo, rn_lo, rm_lo  (lo >>= n, logical for lo word)
4779                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4780                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4781                bytes.extend_from_slice(&hw1.to_le_bytes());
4782                bytes.extend_from_slice(&hw2.to_le_bytes());
4783
4784                // ORR.W rd_lo, rd_lo, rm_hi  (lo |= overflow from hi)
4785                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4786                let hw2: u16 = ((rd_lo_bits << 8) | rm_hi_bits) as u16;
4787                bytes.extend_from_slice(&hw1.to_le_bytes());
4788                bytes.extend_from_slice(&hw2.to_le_bytes());
4789
4790                // ASR.W rd_hi, rn_hi, rm_lo  (hi >>= n, arithmetic/sign-extending)
4791                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
4792                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4793                bytes.extend_from_slice(&hw1.to_le_bytes());
4794                bytes.extend_from_slice(&hw2.to_le_bytes());
4795
4796                // B .done (+3 halfwords, large shift is 8 bytes)
4797                let b_done: u16 = 0xE003;
4798                bytes.extend_from_slice(&b_done.to_le_bytes());
4799
4800                // --- Large shift (n >= 32) ---
4801                // ASR.W rd_lo, rn_hi, rm_hi  (lo = hi >>> (n-32))
4802                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
4803                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_hi_bits) as u16;
4804                bytes.extend_from_slice(&hw1.to_le_bytes());
4805                bytes.extend_from_slice(&hw2.to_le_bytes());
4806
4807                // ASR.W rd_hi, rn_hi, #31  (hi = sign extension, all 0s or all 1s)
4808                // Thumb-2 ASR immediate: hw1=0xEA4F, hw2=imm3:Rd:imm2:10:Rm
4809                // imm5=31=11111 → imm3=111, imm2=11
4810                let hw1: u16 = 0xEA4F;
4811                let hw2: u16 = (0x7000 | (rd_hi_bits << 8) | 0x00E0 | rn_hi_bits) as u16;
4812                bytes.extend_from_slice(&hw1.to_le_bytes());
4813                bytes.extend_from_slice(&hw2.to_le_bytes());
4814
4815                Ok(bytes) // Total: 40 bytes
4816            }
4817
4818            // I64Rotl: 64-bit rotate left (#610 rewrite).
4819            // For n < 32: new_hi = (hi << n) | (lo >> (32-n)), new_lo = (lo << n) | (hi >> (32-n))
4820            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
4821            //
4822            // Fixed-reg core: value in R0:R1, amount in R2, scratch R3 + R12
4823            // (all four saved/marshaled by the #610 fixed-ABI wrapper; the
4824            // pre-#610 expansion wrote through the selector's registers with
4825            // colliding R3/R4 scratch and restored the saved R4 OVER the
4826            // result). Relies on ARM register-shift semantics: amounts >= 32
4827            // yield 0 for LSL/LSR, which makes n = 0 and n = 32 exact.
4828            ArmOp::I64Rotl {
4829                rdlo,
4830                rdhi,
4831                rnlo,
4832                rnhi,
4833                shift,
4834            } => {
4835                let mut bytes = Vec::new();
4836                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
4837
4838                let core: [u16; 35] = [
4839                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
4840                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
4841                    0xD50E, //         BPL    .large        (n >= 32)
4842                    // --- small rotation (n < 32) ---
4843                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
4844                    0xFA20, 0xFC03, // LSR.W  R12, R0, R3   (lo >> (32-n))
4845                    0xFA21, 0xF303, // LSR.W  R3, R1, R3    (hi >> (32-n))
4846                    0xFA01, 0xF102, // LSL.W  R1, R1, R2    (hi << n)
4847                    0xEA41, 0x010C, // ORR.W  R1, R1, R12   (new_hi)
4848                    0xFA00, 0xF002, // LSL.W  R0, R0, R2    (lo << n)
4849                    0xEA40, 0x0003, // ORR.W  R0, R0, R3    (new_lo)
4850                    0xE00E, //         B      .done
4851                    // --- large rotation (n >= 32), R3 = m = n-32 ---
4852                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
4853                    0xFA21, 0xFC02, // LSR.W  R12, R1, R2   (hi >> (64-n))
4854                    0xFA20, 0xF202, // LSR.W  R2, R0, R2    (lo >> (64-n))
4855                    0xFA00, 0xF003, // LSL.W  R0, R0, R3    (lo << m)
4856                    0xFA01, 0xF103, // LSL.W  R1, R1, R3    (hi << m)
4857                    0xEA40, 0x0C0C, // ORR.W  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
4858                    0xEA41, 0x0002, // ORR.W  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
4859                    0x4661, //         MOV    R1, R12       (new_hi into place)
4860                            // .done: result in R0:R1
4861                ];
4862                for hw in core {
4863                    bytes.extend_from_slice(&hw.to_le_bytes());
4864                }
4865
4866                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
4867                Ok(bytes) // Total: 102 bytes
4868            }
4869
4870            // I64Rotr: 64-bit rotate right (#610 rewrite).
4871            // For n < 32: new_lo = (lo >> n) | (hi << (32-n)), new_hi = (hi >> n) | (lo << (32-n))
4872            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
4873            //
4874            // Same fixed-reg core contract as I64Rotl: value in R0:R1, amount
4875            // in R2, scratch R3 + R12, all covered by the fixed-ABI wrapper.
4876            ArmOp::I64Rotr {
4877                rdlo,
4878                rdhi,
4879                rnlo,
4880                rnhi,
4881                shift,
4882            } => {
4883                let mut bytes = Vec::new();
4884                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
4885
4886                let core: [u16; 35] = [
4887                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
4888                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
4889                    0xD50E, //         BPL    .large        (n >= 32)
4890                    // --- small rotation (n < 32) ---
4891                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
4892                    0xFA01, 0xFC03, // LSL.W  R12, R1, R3   (hi << (32-n))
4893                    0xFA00, 0xF303, // LSL.W  R3, R0, R3    (lo << (32-n))
4894                    0xFA20, 0xF002, // LSR.W  R0, R0, R2    (lo >> n)
4895                    0xEA40, 0x000C, // ORR.W  R0, R0, R12   (new_lo)
4896                    0xFA21, 0xF102, // LSR.W  R1, R1, R2    (hi >> n)
4897                    0xEA41, 0x0103, // ORR.W  R1, R1, R3    (new_hi)
4898                    0xE00E, //         B      .done
4899                    // --- large rotation (n >= 32), R3 = m = n-32 ---
4900                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
4901                    0xFA00, 0xFC02, // LSL.W  R12, R0, R2   (lo << (64-n))
4902                    0xFA01, 0xF202, // LSL.W  R2, R1, R2    (hi << (64-n))
4903                    0xFA21, 0xF103, // LSR.W  R1, R1, R3    (hi >> m)
4904                    0xEA41, 0x0C0C, // ORR.W  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
4905                    0xFA20, 0xF103, // LSR.W  R1, R0, R3    (lo >> m)
4906                    0xEA41, 0x0102, // ORR.W  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
4907                    0x4660, //         MOV    R0, R12       (new_lo into place)
4908                            // .done: result in R0:R1
4909                ];
4910                for hw in core {
4911                    bytes.extend_from_slice(&hw.to_le_bytes());
4912                }
4913
4914                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
4915                Ok(bytes) // Total: 102 bytes
4916            }
4917
4918            // I64Clz: Count leading zeros in 64-bit value
4919            // If hi != 0: result = CLZ(hi)
4920            // If hi == 0: result = 32 + CLZ(lo)
4921            //
4922            // Layout (using CMP+BNE approach for consistency):
4923            // 0: CMP.W rnhi, #0 (4 bytes)
4924            // 4: BEQ .hi_zero (2 bytes) - branch forward to offset 14
4925            // 6: CLZ.W rd, rnhi (4 bytes)
4926            // 10: B .done (2 bytes) - branch forward to offset 22
4927            // 12: NOP (2 bytes) - padding for alignment
4928            // 14: .hi_zero: CLZ.W rd, rnlo (4 bytes)
4929            // 18: ADD.W rd, rd, #32 (4 bytes)
4930            // 22: .done
4931            ArmOp::I64Clz { rd, rnlo, rnhi } => {
4932                let rd_bits = reg_to_bits(rd);
4933                let rn_lo_bits = reg_to_bits(rnlo);
4934                let rn_hi_bits = reg_to_bits(rnhi);
4935                let mut bytes = Vec::new();
4936
4937                // CMP.W rnhi, #0 (4 bytes at offset 0)
4938                let hw1: u16 = (0xF1B0 | rn_hi_bits) as u16;
4939                let hw2: u16 = 0x0F00;
4940                bytes.extend_from_slice(&hw1.to_le_bytes());
4941                bytes.extend_from_slice(&hw2.to_le_bytes());
4942
4943                // BEQ .hi_zero (2 bytes at offset 4)
4944                // PC = 4 + 4 = 8, target = 14, offset = 6, imm8 = 3
4945                let beq: u16 = 0xD003;
4946                bytes.extend_from_slice(&beq.to_le_bytes());
4947
4948                // CLZ.W rd, rnhi (4 bytes at offset 6)
4949                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4950                let hw1: u16 = (0xFAB0 | rn_hi_bits) as u16;
4951                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_hi_bits) as u16;
4952                bytes.extend_from_slice(&hw1.to_le_bytes());
4953                bytes.extend_from_slice(&hw2.to_le_bytes());
4954
4955                // B .done (2 bytes at offset 10)
4956                // PC = 10 + 4 = 14, target = 22, offset = 8, imm11 = 4
4957                let b_done: u16 = 0xE004;
4958                bytes.extend_from_slice(&b_done.to_le_bytes());
4959
4960                // NOP (2 bytes at offset 12) - padding
4961                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
4962
4963                // .hi_zero: (offset 14)
4964                // CLZ.W rd, rnlo (4 bytes)
4965                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4966                let hw1: u16 = (0xFAB0 | rn_lo_bits) as u16;
4967                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_lo_bits) as u16;
4968                bytes.extend_from_slice(&hw1.to_le_bytes());
4969                bytes.extend_from_slice(&hw2.to_le_bytes());
4970
4971                // ADD.W rd, rd, #32 (4 bytes at offset 18)
4972                let hw1: u16 = (0xF100 | rd_bits) as u16;
4973                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
4974                bytes.extend_from_slice(&hw1.to_le_bytes());
4975                bytes.extend_from_slice(&hw2.to_le_bytes());
4976
4977                // .done: (offset 22)
4978                // i64.clz returns i64, so clear high word: MOV rnhi, #0 (2 bytes)
4979                // MOVS Rn, #0: 0010 0 Rn 00000000
4980                let mov0: u16 = (0x2000 | (rn_hi_bits << 8)) as u16;
4981                bytes.extend_from_slice(&mov0.to_le_bytes());
4982
4983                Ok(bytes)
4984            }
4985
4986            // I64Ctz: Count trailing zeros in 64-bit value
4987            // If lo != 0: result = CTZ(lo) = CLZ(RBIT(lo))
4988            // If lo == 0: result = 32 + CTZ(hi) = 32 + CLZ(RBIT(hi))
4989            //
4990            // Layout:
4991            // 0: CMP.W rnlo, #0 (4 bytes)
4992            // 4: BEQ .lo_zero (2 bytes) - branch to offset 18
4993            // 6: RBIT.W rd, rnlo (4 bytes)
4994            // 10: CLZ.W rd, rd (4 bytes)
4995            // 14: B .done (2 bytes) - branch to offset 30
4996            // 16: NOP (2 bytes) - padding
4997            // 18: .lo_zero: RBIT.W rd, rnhi (4 bytes)
4998            // 22: CLZ.W rd, rd (4 bytes)
4999            // 26: ADD.W rd, rd, #32 (4 bytes)
5000            // 30: .done
5001            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
5002                let rd_bits = reg_to_bits(rd);
5003                let rn_lo_bits = reg_to_bits(rnlo);
5004                let rn_hi_bits = reg_to_bits(rnhi);
5005                let mut bytes = Vec::new();
5006
5007                // CMP.W rnlo, #0 (4 bytes at offset 0)
5008                let hw1: u16 = (0xF1B0 | rn_lo_bits) as u16;
5009                let hw2: u16 = 0x0F00;
5010                bytes.extend_from_slice(&hw1.to_le_bytes());
5011                bytes.extend_from_slice(&hw2.to_le_bytes());
5012
5013                // BEQ .lo_zero (2 bytes at offset 4)
5014                // PC = 4 + 4 = 8, target = 18, offset = 10, imm8 = 5
5015                let beq: u16 = 0xD005;
5016                bytes.extend_from_slice(&beq.to_le_bytes());
5017
5018                // RBIT.W rd, rnlo (4 bytes at offset 6)
5019                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5020                let hw1: u16 = (0xFA90 | rn_lo_bits) as u16;
5021                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_lo_bits) as u16;
5022                bytes.extend_from_slice(&hw1.to_le_bytes());
5023                bytes.extend_from_slice(&hw2.to_le_bytes());
5024
5025                // CLZ.W rd, rd (4 bytes at offset 10)
5026                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5027                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5028                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5029                bytes.extend_from_slice(&hw1.to_le_bytes());
5030                bytes.extend_from_slice(&hw2.to_le_bytes());
5031
5032                // B .done (2 bytes at offset 14)
5033                // PC = 14 + 4 = 18, target = 30, offset = 12, imm11 = 6
5034                let b_done: u16 = 0xE006;
5035                bytes.extend_from_slice(&b_done.to_le_bytes());
5036
5037                // NOP (2 bytes at offset 16) - padding
5038                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5039
5040                // .lo_zero: (offset 18)
5041                // RBIT.W rd, rnhi (4 bytes)
5042                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5043                let hw1: u16 = (0xFA90 | rn_hi_bits) as u16;
5044                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_hi_bits) as u16;
5045                bytes.extend_from_slice(&hw1.to_le_bytes());
5046                bytes.extend_from_slice(&hw2.to_le_bytes());
5047
5048                // CLZ.W rd, rd (4 bytes at offset 22)
5049                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5050                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5051                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5052                bytes.extend_from_slice(&hw1.to_le_bytes());
5053                bytes.extend_from_slice(&hw2.to_le_bytes());
5054
5055                // ADD.W rd, rd, #32 (4 bytes at offset 26)
5056                let hw1: u16 = (0xF100 | rd_bits) as u16;
5057                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5058                bytes.extend_from_slice(&hw1.to_le_bytes());
5059                bytes.extend_from_slice(&hw2.to_le_bytes());
5060
5061                // .done: (offset 30)
5062                // i64.ctz returns i64, so clear high word: MOV rnhi, #0 (2 bytes)
5063                let mov0: u16 = (0x2000 | (rn_hi_bits << 8)) as u16;
5064                bytes.extend_from_slice(&mov0.to_le_bytes());
5065
5066                Ok(bytes)
5067            }
5068
5069            // I64Popcnt: Population count of 64-bit value
5070            // result = POPCNT(lo) + POPCNT(hi)
5071            // Using SIMD-style parallel bit counting algorithm
5072            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
5073                let rd_bits = reg_to_bits(rd);
5074                let rn_lo_bits = reg_to_bits(rnlo);
5075                let rn_hi_bits = reg_to_bits(rnhi);
5076                let r12: u32 = 12; // IP scratch
5077                let r3: u32 = 3; // Scratch for hi popcnt result
5078                let mut bytes = Vec::new();
5079
5080                // PUSH {R3, R4, R5} - save scratch registers
5081                bytes.extend_from_slice(&0xB438u16.to_le_bytes());
5082
5083                // Strategy: compute popcnt(lo) -> R4, popcnt(hi) -> R5, add them -> rd
5084                // Using lookup table approach for each byte would be too large
5085                // Using shift-and-add approach instead
5086
5087                // For simplicity and correctness, use the efficient parallel algorithm
5088                // but implement it as a series of inline operations
5089
5090                // Marshal the operand pair into the fixed scratch regs, routing
5091                // rnlo through R12 (#632 audit): writing R4 first corrupted the
5092                // rnhi read for a pair living at (R3,R4) — every source is read
5093                // before any scratch register it could occupy is written.
5094                // MOV R12, rnlo
5095                let mov: u16 = (0x4600 | (1 << 7) | (rn_lo_bits << 3) | 4) as u16;
5096                bytes.extend_from_slice(&mov.to_le_bytes());
5097                // MOV R5, rnhi (R4 untouched so far; rnhi == R5 is a no-op)
5098                let mov: u16 = (0x4600 | (rn_hi_bits << 3) | 5) as u16;
5099                bytes.extend_from_slice(&mov.to_le_bytes());
5100                // MOV R4, R12
5101                bytes.extend_from_slice(&0x4664u16.to_le_bytes());
5102
5103                // --- POPCNT for R4 (lo word) ---
5104                // Step 1: x = x - ((x >> 1) & 0x55555555)
5105                // LSR.W R12, R4, #1
5106                let hw1: u16 = 0xEA4F;
5107                let hw2: u16 = ((r12 << 8) | 0x50 | 4) as u16;
5108                bytes.extend_from_slice(&hw1.to_le_bytes());
5109                bytes.extend_from_slice(&hw2.to_le_bytes());
5110
5111                // Load 0x55555555 into R3 using MOVW/MOVT
5112                // MOVW R3, #0x5555
5113                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5114                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5115                // MOVT R3, #0x5555
5116                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5117                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5118
5119                // AND.W R12, R12, R3
5120                let hw1: u16 = (0xEA00 | r12) as u16;
5121                let hw2: u16 = ((r12 << 8) | r3) as u16;
5122                bytes.extend_from_slice(&hw1.to_le_bytes());
5123                bytes.extend_from_slice(&hw2.to_le_bytes());
5124
5125                // SUB.W R4, R4, R12
5126                let hw1: u16 = (0xEBA0 | 4) as u16;
5127                let hw2: u16 = ((4 << 8) | r12) as u16;
5128                bytes.extend_from_slice(&hw1.to_le_bytes());
5129                bytes.extend_from_slice(&hw2.to_le_bytes());
5130
5131                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5132                // Load 0x33333333 into R3
5133                // MOVW R3, #0x3333
5134                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5135                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5136                // MOVT R3, #0x3333
5137                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5138                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5139
5140                // AND.W R12, R4, R3
5141                let hw1: u16 = (0xEA00 | 4) as u16;
5142                let hw2: u16 = ((r12 << 8) | r3) as u16;
5143                bytes.extend_from_slice(&hw1.to_le_bytes());
5144                bytes.extend_from_slice(&hw2.to_le_bytes());
5145
5146                // LSR.W R4, R4, #2
5147                let hw1: u16 = 0xEA4F;
5148                let hw2: u16 = ((4 << 8) | 0x90 | 4) as u16;
5149                bytes.extend_from_slice(&hw1.to_le_bytes());
5150                bytes.extend_from_slice(&hw2.to_le_bytes());
5151
5152                // AND.W R4, R4, R3
5153                let hw1: u16 = (0xEA00 | 4) as u16;
5154                let hw2: u16 = ((4 << 8) | r3) as u16;
5155                bytes.extend_from_slice(&hw1.to_le_bytes());
5156                bytes.extend_from_slice(&hw2.to_le_bytes());
5157
5158                // ADD.W R4, R4, R12
5159                let hw1: u16 = (0xEB00 | 4) as u16;
5160                let hw2: u16 = ((4 << 8) | r12) as u16;
5161                bytes.extend_from_slice(&hw1.to_le_bytes());
5162                bytes.extend_from_slice(&hw2.to_le_bytes());
5163
5164                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5165                // LSR.W R12, R4, #4
5166                // hw2 = (imm3 << 12) | (Rd << 8) | (imm2 << 6) | (type << 4) | Rm
5167                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5168                let hw1: u16 = 0xEA4F;
5169                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 4) as u16;
5170                bytes.extend_from_slice(&hw1.to_le_bytes());
5171                bytes.extend_from_slice(&hw2.to_le_bytes());
5172
5173                // ADD.W R4, R4, R12
5174                let hw1: u16 = (0xEB00 | 4) as u16;
5175                let hw2: u16 = ((4 << 8) | r12) as u16;
5176                bytes.extend_from_slice(&hw1.to_le_bytes());
5177                bytes.extend_from_slice(&hw2.to_le_bytes());
5178
5179                // Load 0x0F0F0F0F into R3
5180                // MOVW R3, #0x0F0F (imm4=0, i=1, imm3=7, imm8=0x0F)
5181                // hw1 = 11110 1 10 0100 0000 = 0xF640
5182                // hw2 = 0 111 0011 00001111 = 0x730F
5183                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5184                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5185                // MOVT R3, #0x0F0F
5186                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5187                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5188
5189                // AND.W R4, R4, R3
5190                let hw1: u16 = (0xEA00 | 4) as u16;
5191                let hw2: u16 = ((4 << 8) | r3) as u16;
5192                bytes.extend_from_slice(&hw1.to_le_bytes());
5193                bytes.extend_from_slice(&hw2.to_le_bytes());
5194
5195                // Step 4: x = x * 0x01010101 >> 24
5196                // Load 0x01010101 into R3
5197                // MOVW R3, #0x0101
5198                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5199                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5200                // MOVT R3, #0x0101
5201                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5202                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5203
5204                // MUL R4, R4, R3
5205                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5206                let hw1: u16 = (0xFB00 | 4) as u16;
5207                let hw2: u16 = (0xF000 | (4 << 8) | r3) as u16;
5208                bytes.extend_from_slice(&hw1.to_le_bytes());
5209                bytes.extend_from_slice(&hw2.to_le_bytes());
5210
5211                // LSR.W R4, R4, #24
5212                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5213                let hw1: u16 = 0xEA4F;
5214                let hw2: u16 = (0x6000 | (4 << 8) | 0x10 | 4) as u16;
5215                bytes.extend_from_slice(&hw1.to_le_bytes());
5216                bytes.extend_from_slice(&hw2.to_le_bytes());
5217
5218                // --- POPCNT for R5 (hi word) - same algorithm ---
5219                // Step 1
5220                let hw1: u16 = 0xEA4F;
5221                let hw2: u16 = ((r12 << 8) | 0x50 | 5) as u16;
5222                bytes.extend_from_slice(&hw1.to_le_bytes());
5223                bytes.extend_from_slice(&hw2.to_le_bytes());
5224
5225                // Load 0x55555555 into R3
5226                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5227                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5228                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5229                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5230
5231                let hw1: u16 = (0xEA00 | r12) as u16;
5232                let hw2: u16 = ((r12 << 8) | r3) as u16;
5233                bytes.extend_from_slice(&hw1.to_le_bytes());
5234                bytes.extend_from_slice(&hw2.to_le_bytes());
5235
5236                let hw1: u16 = (0xEBA0 | 5) as u16;
5237                let hw2: u16 = ((5 << 8) | r12) as u16;
5238                bytes.extend_from_slice(&hw1.to_le_bytes());
5239                bytes.extend_from_slice(&hw2.to_le_bytes());
5240
5241                // Step 2
5242                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5243                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5244                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5245                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5246
5247                let hw1: u16 = (0xEA00 | 5) as u16;
5248                let hw2: u16 = ((r12 << 8) | r3) as u16;
5249                bytes.extend_from_slice(&hw1.to_le_bytes());
5250                bytes.extend_from_slice(&hw2.to_le_bytes());
5251
5252                let hw1: u16 = 0xEA4F;
5253                let hw2: u16 = ((5 << 8) | 0x90 | 5) as u16;
5254                bytes.extend_from_slice(&hw1.to_le_bytes());
5255                bytes.extend_from_slice(&hw2.to_le_bytes());
5256
5257                let hw1: u16 = (0xEA00 | 5) as u16;
5258                let hw2: u16 = ((5 << 8) | r3) as u16;
5259                bytes.extend_from_slice(&hw1.to_le_bytes());
5260                bytes.extend_from_slice(&hw2.to_le_bytes());
5261
5262                let hw1: u16 = (0xEB00 | 5) as u16;
5263                let hw2: u16 = ((5 << 8) | r12) as u16;
5264                bytes.extend_from_slice(&hw1.to_le_bytes());
5265                bytes.extend_from_slice(&hw2.to_le_bytes());
5266
5267                // Step 3: LSR.W R12, R5, #4
5268                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5269                let hw1: u16 = 0xEA4F;
5270                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 5) as u16;
5271                bytes.extend_from_slice(&hw1.to_le_bytes());
5272                bytes.extend_from_slice(&hw2.to_le_bytes());
5273
5274                let hw1: u16 = (0xEB00 | 5) as u16;
5275                let hw2: u16 = ((5 << 8) | r12) as u16;
5276                bytes.extend_from_slice(&hw1.to_le_bytes());
5277                bytes.extend_from_slice(&hw2.to_le_bytes());
5278
5279                // Load 0x0F0F0F0F into R3 (for hi-word)
5280                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5281                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5282                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5283                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5284
5285                let hw1: u16 = (0xEA00 | 5) as u16;
5286                let hw2: u16 = ((5 << 8) | r3) as u16;
5287                bytes.extend_from_slice(&hw1.to_le_bytes());
5288                bytes.extend_from_slice(&hw2.to_le_bytes());
5289
5290                // Step 4
5291                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5292                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5293                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5294                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5295
5296                // MUL R5, R5, R3
5297                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5298                let hw1: u16 = (0xFB00 | 5) as u16;
5299                let hw2: u16 = (0xF000 | (5 << 8) | r3) as u16;
5300                bytes.extend_from_slice(&hw1.to_le_bytes());
5301                bytes.extend_from_slice(&hw2.to_le_bytes());
5302
5303                // LSR.W R5, R5, #24
5304                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5305                let hw1: u16 = 0xEA4F;
5306                let hw2: u16 = (0x6000 | (5 << 8) | 0x10 | 5) as u16;
5307                bytes.extend_from_slice(&hw1.to_le_bytes());
5308                bytes.extend_from_slice(&hw2.to_le_bytes());
5309
5310                // #632: the count must be carried ACROSS the scratch restore
5311                // in a register the POP cannot touch. rd is allocator-assigned
5312                // (any of R0-R8) and can land inside the {R3,R4,R5} restore set
5313                // — the old `ADDS rd, R4, R5; POP {R3,R4,R5}` destroyed the
5314                // result one instruction after computing it (0 for every input
5315                // under qemu). R12 is encoder scratch: never allocatable (#212)
5316                // and never in a restore set, so no choice of rd can collide.
5317                // ADD.W R12, R4, R5
5318                bytes.extend_from_slice(&0xEB04u16.to_le_bytes());
5319                bytes.extend_from_slice(&0x0C05u16.to_le_bytes());
5320
5321                // POP {R3, R4, R5}
5322                bytes.extend_from_slice(&0xBC38u16.to_le_bytes());
5323
5324                // MOV rd, R12 — after the restore. The 4-bit Rd (D:rd) form is
5325                // also total over rd = R8, where the old ADDS T1 3-bit field
5326                // silently corrupted the encoding (#178/#180 class).
5327                let mov: u16 =
5328                    (0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7)) as u16;
5329                bytes.extend_from_slice(&mov.to_le_bytes());
5330
5331                // i64.popcnt returns i64, so clear high word: MOV.W rnhi, #0
5332                // (T2, 4 bytes — total over rnhi = R8, where the old 16-bit
5333                // MOVS encoding overflowed its 3-bit field into CMP R0, #0).
5334                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5335                bytes.extend_from_slice(&(((rn_hi_bits & 0xF) << 8) as u16).to_le_bytes());
5336
5337                Ok(bytes)
5338            }
5339
5340            // I64Extend8S: Sign-extend low 8 bits to 64 bits
5341            // Result: rdlo = sign_extend_8(rnlo), rdhi = rdlo >> 31
5342            ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
5343                let rdlo_bits = reg_to_bits(rdlo);
5344                let rdhi_bits = reg_to_bits(rdhi);
5345                let rnlo_bits = reg_to_bits(rnlo);
5346                let mut bytes = Vec::new();
5347
5348                // SXTB.W rdlo, rnlo (sign-extend byte to 32-bit)
5349                // SXTB T2: hw1 = 0xFA4F, hw2 = 0xF0<Rd><Rm>
5350                let hw1: u16 = 0xFA4F_u16;
5351                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5352                bytes.extend_from_slice(&hw1.to_le_bytes());
5353                bytes.extend_from_slice(&hw2.to_le_bytes());
5354
5355                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5356                // ASR (immediate): hw1 = 0xEA4F, hw2 = imm3:Rd:imm2:type:Rm
5357                // For imm5=31: imm3=111, imm2=11, type=10 (ASR)
5358                // hw2 = (7 << 12) | (rdhi << 8) | (3 << 6) | (2 << 4) | rdlo
5359                let hw1: u16 = 0xEA4F;
5360                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5361                bytes.extend_from_slice(&hw1.to_le_bytes());
5362                bytes.extend_from_slice(&hw2.to_le_bytes());
5363
5364                Ok(bytes)
5365            }
5366
5367            // I64Extend16S: Sign-extend low 16 bits to 64 bits
5368            // Result: rdlo = sign_extend_16(rnlo), rdhi = rdlo >> 31
5369            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
5370                let rdlo_bits = reg_to_bits(rdlo);
5371                let rdhi_bits = reg_to_bits(rdhi);
5372                let rnlo_bits = reg_to_bits(rnlo);
5373                let mut bytes = Vec::new();
5374
5375                // SXTH.W rdlo, rnlo (sign-extend halfword to 32-bit)
5376                // SXTH T2: hw1 = 0xFA0F, hw2 = 0xF0<Rd><Rm>
5377                let hw1: u16 = 0xFA0F_u16;
5378                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5379                bytes.extend_from_slice(&hw1.to_le_bytes());
5380                bytes.extend_from_slice(&hw2.to_le_bytes());
5381
5382                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5383                let hw1: u16 = 0xEA4F;
5384                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5385                bytes.extend_from_slice(&hw1.to_le_bytes());
5386                bytes.extend_from_slice(&hw2.to_le_bytes());
5387
5388                Ok(bytes)
5389            }
5390
5391            // I64Extend32S: Sign-extend low 32 bits to 64 bits
5392            // Result: rdlo = rnlo, rdhi = rnlo >> 31
5393            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
5394                let rdlo_bits = reg_to_bits(rdlo);
5395                let rdhi_bits = reg_to_bits(rdhi);
5396                let rnlo_bits = reg_to_bits(rnlo);
5397                let mut bytes = Vec::new();
5398
5399                // MOV rdlo, rnlo (if different)
5400                if rdlo_bits != rnlo_bits {
5401                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5402                    let d_bit = ((rdlo_bits >> 3) & 1) as u16;
5403                    let mov: u16 = 0x4600
5404                        | (d_bit << 7)
5405                        | ((rnlo_bits as u16) << 3)
5406                        | ((rdlo_bits & 0x7) as u16);
5407                    bytes.extend_from_slice(&mov.to_le_bytes());
5408                }
5409
5410                // ASR.W rdhi, rnlo, #31 (sign-extend to high word)
5411                let hw1: u16 = 0xEA4F;
5412                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rnlo_bits) as u16;
5413                bytes.extend_from_slice(&hw1.to_le_bytes());
5414                bytes.extend_from_slice(&hw2.to_le_bytes());
5415
5416                Ok(bytes)
5417            }
5418
5419            // SelectMove: IT <cond>; MOV{cond} rd, rm
5420            // Conditional move: only execute MOV if condition is true
5421            ArmOp::SelectMove { rd, rm, cond } => {
5422                let rd_bits = reg_to_bits(rd) as u16;
5423                let rm_bits = reg_to_bits(rm) as u16;
5424
5425                // Condition code encoding for IT block
5426                use synth_synthesis::Condition;
5427                let cond_bits: u16 = match cond {
5428                    Condition::EQ => 0x0, // Equal
5429                    Condition::NE => 0x1, // Not equal
5430                    Condition::HS => 0x2, // Higher or same (unsigned >=)
5431                    Condition::LO => 0x3, // Lower (unsigned <)
5432                    Condition::HI => 0x8, // Higher (unsigned >)
5433                    Condition::LS => 0x9, // Lower or same (unsigned <=)
5434                    Condition::GE => 0xA, // Greater or equal (signed)
5435                    Condition::LT => 0xB, // Less than (signed)
5436                    Condition::GT => 0xC, // Greater than (signed)
5437                    Condition::LE => 0xD, // Less or equal (signed)
5438                };
5439
5440                // IT <cond>: single Then block (mask = 0x8 for T only)
5441                // IT instruction: 1011 1111 firstcond mask
5442                let it_instr: u16 = 0xBF00 | (cond_bits << 4) | 0x8;
5443
5444                // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5445                // This MOV will only execute if condition is true due to IT block
5446                let d_bit = (rd_bits >> 3) & 1;
5447                let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5448
5449                // Emit: IT <cond>, MOV rd, rm
5450                let mut bytes = it_instr.to_le_bytes().to_vec();
5451                bytes.extend_from_slice(&mov_instr.to_le_bytes());
5452                Ok(bytes)
5453            }
5454
5455            // Popcnt: Population count (count set bits)
5456            // ARM Cortex-M has no native POPCNT, so we implement the bit manipulation algorithm:
5457            // x = x - ((x >> 1) & 0x55555555);
5458            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
5459            // x = (x + (x >> 4)) & 0x0F0F0F0F;
5460            // x = x + (x >> 8);
5461            // x = x + (x >> 16);
5462            // return x & 0x3F;
5463            //
5464            // Uses rd as working register and R12 as scratch for constants
5465            ArmOp::Popcnt { rd, rm } => {
5466                let mut bytes = Vec::new();
5467
5468                // First, move rm to rd if they're different
5469                if rd != rm {
5470                    let rd_bits = reg_to_bits(rd) as u16;
5471                    let rm_bits = reg_to_bits(rm) as u16;
5472                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5473                    let d_bit = (rd_bits >> 3) & 1;
5474                    let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5475                    bytes.extend_from_slice(&mov_instr.to_le_bytes());
5476                }
5477
5478                // Step 1: x = x - ((x >> 1) & 0x55555555)
5479                // Load 0x55555555 into R12
5480                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x5555)?);
5481                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x5555)?);
5482
5483                // R12_temp = rd >> 1
5484                // We need a second scratch register. Use R11.
5485                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 1)?);
5486
5487                // R11 = R11 & R12 (R11 = (x >> 1) & 0x55555555)
5488                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(11, 11, 12)?);
5489
5490                // rd = rd - R11
5491                bytes.extend_from_slice(&self.encode_thumb32_sub_reg_raw(
5492                    reg_to_bits(rd),
5493                    reg_to_bits(rd),
5494                    11,
5495                )?);
5496
5497                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5498                // Load 0x33333333 into R12
5499                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x3333)?);
5500                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x3333)?);
5501
5502                // R11 = rd & R12
5503                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5504                    11,
5505                    reg_to_bits(rd),
5506                    12,
5507                )?);
5508
5509                // rd = rd >> 2
5510                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(
5511                    reg_to_bits(rd),
5512                    reg_to_bits(rd),
5513                    2,
5514                )?);
5515
5516                // rd = rd & R12
5517                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5518                    reg_to_bits(rd),
5519                    reg_to_bits(rd),
5520                    12,
5521                )?);
5522
5523                // rd = rd + R11
5524                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5525                    reg_to_bits(rd),
5526                    reg_to_bits(rd),
5527                    11,
5528                )?);
5529
5530                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5531                // R11 = rd >> 4
5532                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 4)?);
5533
5534                // rd = rd + R11
5535                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5536                    reg_to_bits(rd),
5537                    reg_to_bits(rd),
5538                    11,
5539                )?);
5540
5541                // Load 0x0F0F0F0F into R12
5542                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x0F0F)?);
5543                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x0F0F)?);
5544
5545                // rd = rd & R12
5546                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5547                    reg_to_bits(rd),
5548                    reg_to_bits(rd),
5549                    12,
5550                )?);
5551
5552                // Step 4: x = x + (x >> 8)
5553                // R11 = rd >> 8
5554                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 8)?);
5555
5556                // rd = rd + R11
5557                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5558                    reg_to_bits(rd),
5559                    reg_to_bits(rd),
5560                    11,
5561                )?);
5562
5563                // Step 5: x = x + (x >> 16)
5564                // R11 = rd >> 16
5565                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 16)?);
5566
5567                // rd = rd + R11
5568                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5569                    reg_to_bits(rd),
5570                    reg_to_bits(rd),
5571                    11,
5572                )?);
5573
5574                // Step 6: return x & 0x3F
5575                // AND with 0x3F (small immediate, can use BIC or AND with immediate)
5576                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5577                    reg_to_bits(rd),
5578                    reg_to_bits(rd),
5579                    0x3F,
5580                )?);
5581
5582                Ok(bytes)
5583            }
5584
5585            // I64DivU: 64-bit unsigned division using binary long division
5586            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = quotient
5587            // Uses: R4-R7, R12 as loop counter (avoid R8 for Renode compatibility)
5588            //
5589            // #610: the fixed-ABI wrapper marshals the selector-assigned
5590            // operand registers into the core's fixed regs and lands the
5591            // result in rd — pre-#610 this arm IGNORED its register fields,
5592            // so the selector read its rd pair (e.g. R4:R5) after the core's
5593            // own POP restored the stale caller values over it: 0 for every
5594            // input. A zero divisor now traps (UDF #0), per WASM semantics.
5595            ArmOp::I64DivU {
5596                rdlo,
5597                rdhi,
5598                rnlo,
5599                rnhi,
5600                rmlo,
5601                rmhi,
5602                elide_zero_guard,
5603            } => {
5604                let mut bytes = Vec::new();
5605                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5606                // #494 phase 2b: elided only under a certificate-discharged
5607                // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
5608                if !elide_zero_guard {
5609                    emit_i64_divisor_zero_trap(&mut bytes);
5610                }
5611
5612                // PUSH {R4-R7} - save scratch registers (NO LR — this is inline code)
5613                // 16-bit PUSH: 1011 010 M rrrrrrrr where M=0 (no LR), r=R4-R7 = 0xF0
5614                // Encoding: 1011 0100 1111 0000 = 0xB4F0
5615                bytes.extend_from_slice(&0xB4F0u16.to_le_bytes());
5616
5617                // Initialize quotient (R4:R5) = 0
5618                bytes.extend_from_slice(&0x2400u16.to_le_bytes()); // MOV R4, #0
5619                bytes.extend_from_slice(&0x2500u16.to_le_bytes()); // MOV R5, #0
5620
5621                // Initialize remainder (R6:R7) = 0
5622                bytes.extend_from_slice(&0x2600u16.to_le_bytes()); // MOV R6, #0
5623                bytes.extend_from_slice(&0x2700u16.to_le_bytes()); // MOV R7, #0
5624
5625                // Initialize loop counter R12 = 64 (use R12 scratch instead of R8)
5626                // MOV.W R12, #64: F04F 0C40
5627                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5628                bytes.extend_from_slice(&0x0C40u16.to_le_bytes());
5629
5630                // Loop start
5631                let loop_start = bytes.len();
5632
5633                // === Loop body: process one bit ===
5634
5635                // 1. Shift quotient R4:R5 left by 1
5636                // LSLS R5, R5, #1 (16-bit: 0000 0010 1010 1101 = 0x006D -> actually 0x002D for LSL R5,R5,#1)
5637                // LSL Rd, Rm, #imm5: 000 00 imm5 Rm Rd = 000 00 00001 101 101 = 0x006D
5638                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5639                // Get carry from R4 into R5: ORR R5, R5, R4 LSR #31
5640                // Thumb-2 ORR with shifted register: EA45 75D4 = ORR.W R5, R5, R4, LSR #31
5641                // 11101010 010 S Rn | 0 imm3 Rd imm2 type Rm
5642                // type=01 (LSR), imm5=31 (imm3=111, imm2=11)
5643                bytes.extend_from_slice(&0xEA45u16.to_le_bytes());
5644                bytes.extend_from_slice(&0x75D4u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5645                // LSLS R4, R4, #1: 000 00 00001 100 100 = 0x0064
5646                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5647
5648                // 2. Shift remainder R6:R7 left by 1, OR in MSB of dividend R1
5649                // LSLS R7, R7, #1
5650                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5651                // ORR.W R7, R7, R6, LSR #31
5652                bytes.extend_from_slice(&0xEA47u16.to_le_bytes());
5653                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5654                // LSLS R6, R6, #1
5655                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5656                // ORR.W R6, R6, R1, LSR #31 (bring in MSB of dividend high)
5657                bytes.extend_from_slice(&0xEA46u16.to_le_bytes());
5658                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5659
5660                // 3. Shift dividend R0:R1 left by 1
5661                // LSLS R1, R1, #1
5662                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5663                // ORR.W R1, R1, R0, LSR #31
5664                bytes.extend_from_slice(&0xEA41u16.to_le_bytes());
5665                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5666                // LSLS R0, R0, #1
5667                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5668
5669                // 4. Compare remainder >= divisor (64-bit unsigned comparison)
5670                // Compare high words first: CMP R7, R3
5671                // CMP Rn, Rm encoding: 0x4280 | (Rm << 3) | Rn
5672                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3 (16-bit)
5673                // BHI means R7 > R3 (unsigned) - definitely subtract
5674                // BLO means R7 < R3 - definitely don't subtract
5675                // BEQ means need to check low words
5676
5677                // If high > divisor high: branch to subtract (forward +offset)
5678                // BHI.N +6 (skip CMP, skip BLO, do subtract)
5679                // BHI: 1101 1000 offset8 where cond=1000 (HI)
5680                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4 (to subtract block)
5681
5682                // If high < divisor high: branch past subtract
5683                // BLO.N +10 (skip to decrement)
5684                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BLO/BCC +12 (past subtract)
5685
5686                // High words equal, compare low: CMP R6, R2
5687                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2 (16-bit)
5688                // BLO/BCC past subtract (skip SUBS+SBC.W+ORR.W = 10 bytes = 4 halfwords from PC+4)
5689                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords (past subtract)
5690
5691                // === Subtract block: remainder -= divisor, quotient |= 1 ===
5692                // SUBS R6, R6, R2
5693                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2 (16-bit)
5694                // SBC R7, R7, R3 (with borrow)
5695                // Thumb-2 SBC.W: EB67 0703 = SBC.W R7, R7, R3
5696                bytes.extend_from_slice(&0xEB67u16.to_le_bytes());
5697                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5698                // ORR R4, R4, #1 (set bit 0 of quotient low)
5699                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5700                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5701
5702                // === Decrement counter and loop ===
5703                // SUBS.W R12, R12, #1 (decrement loop counter)
5704                // SUBS.W R12, R12, #1: F1BC 0C01
5705                bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
5706                bytes.extend_from_slice(&0x0C01u16.to_le_bytes());
5707
5708                // BNE back to loop_start
5709                let branch_offset_bytes = bytes.len() - loop_start + 4; // +4 for pipeline
5710                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5711                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5712                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5713
5714                // === Loop done, move quotient to R0:R1 ===
5715                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5716                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5717
5718                // POP {R4-R7} - restore scratch registers (NO PC — inline code continues)
5719                // 16-bit POP: 1011 110 P rrrrrrrr where P=0 (no PC), r=R4-R7 = 0xF0
5720                // Encoding: 1011 1100 1111 0000 = 0xBCF0
5721                bytes.extend_from_slice(&0xBCF0u16.to_le_bytes());
5722
5723                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5724                Ok(bytes)
5725            }
5726
5727            // I64DivS: 64-bit signed division
5728            // Converts to unsigned, divides, then applies sign
5729            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5730            //   ->  R0:R1 = quotient (signed)
5731            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5732            ArmOp::I64DivS {
5733                rdlo,
5734                rdhi,
5735                rnlo,
5736                rnhi,
5737                rmlo,
5738                rmhi,
5739                elide_zero_guard,
5740                elide_overflow_guard,
5741            } => {
5742                let mut bytes = Vec::new();
5743                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5744                // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
5745                // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
5746                // the #633 overflow guard falls ONLY to
5747                // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
5748                // divisor-nonzero fact alone must keep it.
5749                if !elide_zero_guard {
5750                    emit_i64_divisor_zero_trap(&mut bytes);
5751                }
5752                if !elide_overflow_guard {
5753                    // #633: INT64_MIN / -1 overflows — trap like the i32 path
5754                    // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
5755                    emit_i64_divs_overflow_trap(&mut bytes);
5756                }
5757
5758                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5759                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5760                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5761
5762                // Save result sign in R9: R9 = R1 XOR R3 (sign bit = MSB)
5763                // EOR.W R9, R1, R3
5764                bytes.extend_from_slice(&0xEA81u16.to_le_bytes());
5765                bytes.extend_from_slice(&0x0903u16.to_le_bytes());
5766
5767                // If dividend negative (R1 MSB set), negate it
5768                // TST R1, R1 (check sign)
5769                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5770                // BPL skip_neg_dividend (+10 bytes = 5 halfwords)
5771                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5772
5773                // Negate R0:R1 (64-bit): RSBS R0, R0, #0; SBC R1, R1, R1 LSL #1
5774                // Actually: MVN R0, R0; MVN R1, R1; ADDS R0, R0, #1; ADC R1, R1, #0
5775                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5776                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5777                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5778                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5779                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5780
5781                // If divisor negative (R3 MSB set), negate it
5782                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5783                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5784
5785                // Negate R2:R3
5786                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
5787                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
5788                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
5789                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
5790                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
5791
5792                // === Now do unsigned division (same as I64DivU) ===
5793                // Initialize quotient (R4:R5) = 0
5794                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5795                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5796                // Initialize remainder (R6:R7) = 0
5797                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5798                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5799                // Initialize loop counter R8 = 64
5800                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5801                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5802
5803                let loop_start = bytes.len();
5804
5805                // Shift quotient left
5806                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5807                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5808                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5809                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5810
5811                // Shift remainder left, OR in MSB of dividend
5812                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5813                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5814                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5815                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5816                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5817                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5818
5819                // Shift dividend left
5820                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5821                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5822                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5823                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5824
5825                // Compare and conditionally subtract
5826                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5827                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5828                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5829                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5830                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5831
5832                // Subtract and set quotient bit
5833                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5834                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5835                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5836                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5837                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5838
5839                // Decrement and loop
5840                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
5841                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
5842
5843                let branch_offset_bytes = bytes.len() - loop_start + 4;
5844                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5845                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5846                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5847
5848                // Move quotient to R0:R1
5849                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5850                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5851
5852                // If result should be negative (R9 MSB set), negate R0:R1
5853                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9 (check MSB)
5854                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
5855                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8 (skip negation)
5856
5857                // Negate result R0:R1
5858                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5859                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5860                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5861                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5862                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5863
5864                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
5865                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
5866                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5867
5868                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5869                Ok(bytes)
5870            }
5871
5872            // I64RemU: 64-bit unsigned remainder using binary long division
5873            // Same algorithm as I64DivU but returns remainder instead of quotient
5874            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = remainder
5875            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5876            ArmOp::I64RemU {
5877                rdlo,
5878                rdhi,
5879                rnlo,
5880                rnhi,
5881                rmlo,
5882                rmhi,
5883                elide_zero_guard,
5884            } => {
5885                let mut bytes = Vec::new();
5886                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5887                if !elide_zero_guard {
5888                    emit_i64_divisor_zero_trap(&mut bytes);
5889                }
5890
5891                // PUSH {R4-R8} - save scratch registers (NO LR — inline code)
5892                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5893                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
5894
5895                // Initialize quotient (R4:R5) = 0 (computed but not returned)
5896                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5897                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5898                // Initialize remainder (R6:R7) = 0
5899                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5900                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5901                // Initialize loop counter R8 = 64
5902                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5903                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5904
5905                let loop_start = bytes.len();
5906
5907                // Shift quotient left (not needed for result, but keeps algorithm same)
5908                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5909                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5910                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5911                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5912
5913                // Shift remainder left, OR in MSB of dividend
5914                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5915                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5916                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5917                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5918                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5919                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5920
5921                // Shift dividend left
5922                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5923                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5924                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5925                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5926
5927                // Compare and conditionally subtract
5928                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5929                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5930                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5931                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5932                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5933
5934                // Subtract and set quotient bit
5935                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5936                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5937                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5938                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5939                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5940
5941                // Decrement and loop
5942                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
5943                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
5944
5945                let branch_offset_bytes = bytes.len() - loop_start + 4;
5946                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5947                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5948                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5949
5950                // Move REMAINDER to R0:R1 (difference from I64DivU)
5951                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
5952                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
5953
5954                // POP {R4-R8} - restore scratch registers (NO PC — inline code continues)
5955                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
5956                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
5957
5958                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5959                Ok(bytes)
5960            }
5961
5962            // I64RemS: 64-bit signed remainder
5963            // Remainder sign follows dividend sign (not quotient rule)
5964            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5965            //   ->  R0:R1 = remainder (signed, same sign as dividend)
5966            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5967            ArmOp::I64RemS {
5968                rdlo,
5969                rdhi,
5970                rnlo,
5971                rnhi,
5972                rmlo,
5973                rmhi,
5974                elide_zero_guard,
5975            } => {
5976                let mut bytes = Vec::new();
5977                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5978                if !elide_zero_guard {
5979                    emit_i64_divisor_zero_trap(&mut bytes);
5980                }
5981
5982                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5983                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5984                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5985
5986                // Save dividend sign in R9 (remainder sign = dividend sign)
5987                // MOV R9, R1 (just need the sign bit)
5988                bytes.extend_from_slice(&0x4689u16.to_le_bytes()); // MOV R9, R1
5989
5990                // If dividend negative (R1 MSB set), negate it
5991                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5992                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5993
5994                // Negate R0:R1
5995                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5996                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5997                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5998                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5999                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6000
6001                // If divisor negative (R3 MSB set), negate it
6002                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
6003                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6004
6005                // Negate R2:R3
6006                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
6007                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
6008                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
6009                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
6010                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
6011
6012                // === Unsigned division algorithm ===
6013                // Initialize quotient (R4:R5) = 0
6014                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6015                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6016                // Initialize remainder (R6:R7) = 0
6017                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6018                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6019                // Initialize loop counter R8 = 64
6020                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6021                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6022
6023                let loop_start = bytes.len();
6024
6025                // Shift quotient left
6026                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6027                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6028                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6029                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6030
6031                // Shift remainder left, OR in MSB of dividend
6032                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6033                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6034                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6035                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6036                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6037                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6038
6039                // Shift dividend left
6040                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6041                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6042                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6043                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6044
6045                // Compare and conditionally subtract
6046                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6047                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6048                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6049                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6050                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6051
6052                // Subtract and set quotient bit
6053                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6054                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6055                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6056                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6057                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6058
6059                // Decrement and loop
6060                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6061                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6062
6063                let branch_offset_bytes = bytes.len() - loop_start + 4;
6064                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6065                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6066                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6067
6068                // Move remainder to R0:R1
6069                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6070                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6071
6072                // If original dividend was negative (R9 MSB set), negate remainder
6073                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9
6074                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6075                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6076
6077                // Negate result R0:R1
6078                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6079                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6080                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6081                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6082                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6083
6084                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6085                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6086                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6087
6088                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6089                Ok(bytes)
6090            }
6091
6092            // === F32 VFP single-precision Thumb-2 encodings ===
6093            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6094            ArmOp::F32Add { sd, sn, sm } => {
6095                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A00, sd, sn, sm)?))
6096            }
6097            ArmOp::F32Sub { sd, sn, sm } => {
6098                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A40, sd, sn, sm)?))
6099            }
6100            ArmOp::F32Mul { sd, sn, sm } => {
6101                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE200A00, sd, sn, sm)?))
6102            }
6103            ArmOp::F32Div { sd, sn, sm } => {
6104                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE800A00, sd, sn, sm)?))
6105            }
6106            ArmOp::F32Abs { sd, sm } => {
6107                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB00AC0, sd, sm)?))
6108            }
6109            ArmOp::F32Neg { sd, sm } => {
6110                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10A40, sd, sm)?))
6111            }
6112            ArmOp::F32Sqrt { sd, sm } => {
6113                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10AC0, sd, sm)?))
6114            }
6115
6116            // f32 pseudo-ops — multi-instruction sequences
6117            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6118            ArmOp::F32Ceil { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b01),
6119            ArmOp::F32Floor { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b10),
6120            ArmOp::F32Trunc { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b11),
6121            ArmOp::F32Nearest { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b00),
6122            ArmOp::F32Min { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, true),
6123            ArmOp::F32Max { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, false),
6124            ArmOp::F32Copysign { sd, sn, sm } => self.encode_thumb_f32_copysign(sd, sn, sm),
6125
6126            // f32 comparisons — VCMP + VMRS + MOV #0 + IT + MOV #1
6127            ArmOp::F32Eq { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x0),
6128            ArmOp::F32Ne { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x1),
6129            ArmOp::F32Lt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x4),
6130            ArmOp::F32Le { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x9),
6131            ArmOp::F32Gt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xC),
6132            ArmOp::F32Ge { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xA),
6133
6134            ArmOp::F32Const { sd, value } => self.encode_thumb_f32_const(sd, *value),
6135
6136            ArmOp::F32Load { sd, addr } => {
6137                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED900A00, sd, addr)?))
6138            }
6139            ArmOp::F32Store { sd, addr } => {
6140                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED800A00, sd, addr)?))
6141            }
6142
6143            ArmOp::F32ConvertI32S { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, true),
6144            ArmOp::F32ConvertI32U { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, false),
6145            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
6146                Err(synth_core::Error::synthesis(
6147                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6148                ))
6149            }
6150            ArmOp::F32ReinterpretI32 { sd, rm } => {
6151                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(true, sd, rm)?))
6152            }
6153            ArmOp::I32ReinterpretF32 { rd, sm } => {
6154                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(false, sm, rd)?))
6155            }
6156            ArmOp::I32TruncF32S { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, true),
6157            ArmOp::I32TruncF32U { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, false),
6158
6159            // === F64 VFP double-precision Thumb-2 encodings ===
6160            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6161            ArmOp::F64Add { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6162                0xEE300B00, dd, dn, dm,
6163            )?)),
6164            ArmOp::F64Sub { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6165                0xEE300B40, dd, dn, dm,
6166            )?)),
6167            ArmOp::F64Mul { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6168                0xEE200B00, dd, dn, dm,
6169            )?)),
6170            ArmOp::F64Div { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6171                0xEE800B00, dd, dn, dm,
6172            )?)),
6173            ArmOp::F64Abs { dd, dm } => {
6174                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?))
6175            }
6176            ArmOp::F64Neg { dd, dm } => {
6177                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?))
6178            }
6179            ArmOp::F64Sqrt { dd, dm } => {
6180                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?))
6181            }
6182
6183            // f64 pseudo-ops
6184            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6185            ArmOp::F64Ceil { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b01),
6186            ArmOp::F64Floor { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b10),
6187            ArmOp::F64Trunc { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b11),
6188            ArmOp::F64Nearest { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b00),
6189            ArmOp::F64Min { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, true),
6190            ArmOp::F64Max { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, false),
6191            ArmOp::F64Copysign { dd, dn, dm } => self.encode_thumb_f64_copysign(dd, dn, dm),
6192
6193            // f64 comparisons
6194            ArmOp::F64Eq { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x0),
6195            ArmOp::F64Ne { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x1),
6196            ArmOp::F64Lt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x4),
6197            ArmOp::F64Le { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x9),
6198            ArmOp::F64Gt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xC),
6199            ArmOp::F64Ge { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xA),
6200
6201            ArmOp::F64Const { dd, value } => self.encode_thumb_f64_const(dd, *value),
6202
6203            ArmOp::F64Load { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6204                0xED900B00, dd, addr,
6205            )?)),
6206            ArmOp::F64Store { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6207                0xED800B00, dd, addr,
6208            )?)),
6209
6210            ArmOp::F64ConvertI32S { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, true),
6211            ArmOp::F64ConvertI32U { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, false),
6212            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
6213                Err(synth_core::Error::synthesis(
6214                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6215                ))
6216            }
6217            ArmOp::F64PromoteF32 { dd, sm } => self.encode_thumb_f64_promote_f32(dd, sm),
6218            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => Ok(vfp_to_thumb_bytes(
6219                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?,
6220            )),
6221            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => Ok(vfp_to_thumb_bytes(
6222                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?,
6223            )),
6224            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
6225                Err(synth_core::Error::synthesis(
6226                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
6227                ))
6228            }
6229            ArmOp::I32TruncF64S { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, true),
6230            ArmOp::I32TruncF64U { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, false),
6231
6232            // ===== i64 operations: encode as multi-instruction Thumb-2 sequences =====
6233
6234            // I64Add: ADDS rdlo, rnlo, rmlo; ADC.W rdhi, rnhi, rmhi
6235            ArmOp::I64Add {
6236                rdlo,
6237                rdhi,
6238                rnlo,
6239                rnhi,
6240                rmlo,
6241                rmhi,
6242            } => {
6243                let mut bytes = Vec::new();
6244                // ADDS rdlo, rnlo, rmlo (16-bit)
6245                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adds {
6246                    rd: *rdlo,
6247                    rn: *rnlo,
6248                    op2: Operand2::Reg(*rmlo),
6249                })?);
6250                // ADC.W rdhi, rnhi, rmhi (32-bit)
6251                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adc {
6252                    rd: *rdhi,
6253                    rn: *rnhi,
6254                    op2: Operand2::Reg(*rmhi),
6255                })?);
6256                Ok(bytes)
6257            }
6258
6259            // I64Sub: SUBS rdlo, rnlo, rmlo; SBC.W rdhi, rnhi, rmhi
6260            ArmOp::I64Sub {
6261                rdlo,
6262                rdhi,
6263                rnlo,
6264                rnhi,
6265                rmlo,
6266                rmhi,
6267            } => {
6268                let mut bytes = Vec::new();
6269                // SUBS rdlo, rnlo, rmlo (16-bit)
6270                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Subs {
6271                    rd: *rdlo,
6272                    rn: *rnlo,
6273                    op2: Operand2::Reg(*rmlo),
6274                })?);
6275                // SBC.W rdhi, rnhi, rmhi (32-bit)
6276                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Sbc {
6277                    rd: *rdhi,
6278                    rn: *rnhi,
6279                    op2: Operand2::Reg(*rmhi),
6280                })?);
6281                Ok(bytes)
6282            }
6283
6284            // I64And: AND rdlo, rnlo, rmlo; AND rdhi, rnhi, rmhi
6285            ArmOp::I64And {
6286                rdlo,
6287                rdhi,
6288                rnlo,
6289                rnhi,
6290                rmlo,
6291                rmhi,
6292            } => {
6293                let mut bytes = Vec::new();
6294                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6295                    rd: *rdlo,
6296                    rn: *rnlo,
6297                    op2: Operand2::Reg(*rmlo),
6298                })?);
6299                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6300                    rd: *rdhi,
6301                    rn: *rnhi,
6302                    op2: Operand2::Reg(*rmhi),
6303                })?);
6304                Ok(bytes)
6305            }
6306
6307            // I64Or: ORR rdlo, rnlo, rmlo; ORR rdhi, rnhi, rmhi
6308            ArmOp::I64Or {
6309                rdlo,
6310                rdhi,
6311                rnlo,
6312                rnhi,
6313                rmlo,
6314                rmhi,
6315            } => {
6316                let mut bytes = Vec::new();
6317                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6318                    rd: *rdlo,
6319                    rn: *rnlo,
6320                    op2: Operand2::Reg(*rmlo),
6321                })?);
6322                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6323                    rd: *rdhi,
6324                    rn: *rnhi,
6325                    op2: Operand2::Reg(*rmhi),
6326                })?);
6327                Ok(bytes)
6328            }
6329
6330            // I64Xor: EOR rdlo, rnlo, rmlo; EOR rdhi, rnhi, rmhi
6331            ArmOp::I64Xor {
6332                rdlo,
6333                rdhi,
6334                rnlo,
6335                rnhi,
6336                rmlo,
6337                rmhi,
6338            } => {
6339                let mut bytes = Vec::new();
6340                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6341                    rd: *rdlo,
6342                    rn: *rnlo,
6343                    op2: Operand2::Reg(*rmlo),
6344                })?);
6345                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6346                    rd: *rdhi,
6347                    rn: *rnhi,
6348                    op2: Operand2::Reg(*rmhi),
6349                })?);
6350                Ok(bytes)
6351            }
6352
6353            // I64Eqz: ORR scratch, lo, hi; ITE EQ; MOV rd, #1; MOV rd, #0
6354            ArmOp::I64Eqz { rd, rnlo, rnhi } => self.encode_thumb(&ArmOp::I64SetCondZ {
6355                rd: *rd,
6356                rn_lo: *rnlo,
6357                rn_hi: *rnhi,
6358            }),
6359
6360            // I64 comparisons: delegate to I64SetCond
6361            ArmOp::I64Eq {
6362                rd,
6363                rnlo,
6364                rnhi,
6365                rmlo,
6366                rmhi,
6367            } => self.encode_thumb(&ArmOp::I64SetCond {
6368                rd: *rd,
6369                rn_lo: *rnlo,
6370                rn_hi: *rnhi,
6371                rm_lo: *rmlo,
6372                rm_hi: *rmhi,
6373                cond: synth_synthesis::Condition::EQ,
6374            }),
6375
6376            ArmOp::I64Ne {
6377                rd,
6378                rnlo,
6379                rnhi,
6380                rmlo,
6381                rmhi,
6382            } => self.encode_thumb(&ArmOp::I64SetCond {
6383                rd: *rd,
6384                rn_lo: *rnlo,
6385                rn_hi: *rnhi,
6386                rm_lo: *rmlo,
6387                rm_hi: *rmhi,
6388                cond: synth_synthesis::Condition::NE,
6389            }),
6390
6391            ArmOp::I64LtS {
6392                rd,
6393                rnlo,
6394                rnhi,
6395                rmlo,
6396                rmhi,
6397            } => self.encode_thumb(&ArmOp::I64SetCond {
6398                rd: *rd,
6399                rn_lo: *rnlo,
6400                rn_hi: *rnhi,
6401                rm_lo: *rmlo,
6402                rm_hi: *rmhi,
6403                cond: synth_synthesis::Condition::LT,
6404            }),
6405
6406            ArmOp::I64LtU {
6407                rd,
6408                rnlo,
6409                rnhi,
6410                rmlo,
6411                rmhi,
6412            } => self.encode_thumb(&ArmOp::I64SetCond {
6413                rd: *rd,
6414                rn_lo: *rnlo,
6415                rn_hi: *rnhi,
6416                rm_lo: *rmlo,
6417                rm_hi: *rmhi,
6418                cond: synth_synthesis::Condition::LO,
6419            }),
6420
6421            ArmOp::I64LeS {
6422                rd,
6423                rnlo,
6424                rnhi,
6425                rmlo,
6426                rmhi,
6427            } => self.encode_thumb(&ArmOp::I64SetCond {
6428                rd: *rd,
6429                rn_lo: *rnlo,
6430                rn_hi: *rnhi,
6431                rm_lo: *rmlo,
6432                rm_hi: *rmhi,
6433                cond: synth_synthesis::Condition::LE,
6434            }),
6435
6436            ArmOp::I64LeU {
6437                rd,
6438                rnlo,
6439                rnhi,
6440                rmlo,
6441                rmhi,
6442            } => self.encode_thumb(&ArmOp::I64SetCond {
6443                rd: *rd,
6444                rn_lo: *rnlo,
6445                rn_hi: *rnhi,
6446                rm_lo: *rmlo,
6447                rm_hi: *rmhi,
6448                cond: synth_synthesis::Condition::LS,
6449            }),
6450
6451            ArmOp::I64GtS {
6452                rd,
6453                rnlo,
6454                rnhi,
6455                rmlo,
6456                rmhi,
6457            } => self.encode_thumb(&ArmOp::I64SetCond {
6458                rd: *rd,
6459                rn_lo: *rnlo,
6460                rn_hi: *rnhi,
6461                rm_lo: *rmlo,
6462                rm_hi: *rmhi,
6463                cond: synth_synthesis::Condition::GT,
6464            }),
6465
6466            ArmOp::I64GtU {
6467                rd,
6468                rnlo,
6469                rnhi,
6470                rmlo,
6471                rmhi,
6472            } => self.encode_thumb(&ArmOp::I64SetCond {
6473                rd: *rd,
6474                rn_lo: *rnlo,
6475                rn_hi: *rnhi,
6476                rm_lo: *rmlo,
6477                rm_hi: *rmhi,
6478                cond: synth_synthesis::Condition::HI,
6479            }),
6480
6481            ArmOp::I64GeS {
6482                rd,
6483                rnlo,
6484                rnhi,
6485                rmlo,
6486                rmhi,
6487            } => self.encode_thumb(&ArmOp::I64SetCond {
6488                rd: *rd,
6489                rn_lo: *rnlo,
6490                rn_hi: *rnhi,
6491                rm_lo: *rmlo,
6492                rm_hi: *rmhi,
6493                cond: synth_synthesis::Condition::GE,
6494            }),
6495
6496            ArmOp::I64GeU {
6497                rd,
6498                rnlo,
6499                rnhi,
6500                rmlo,
6501                rmhi,
6502            } => self.encode_thumb(&ArmOp::I64SetCond {
6503                rd: *rd,
6504                rn_lo: *rnlo,
6505                rn_hi: *rnhi,
6506                rm_lo: *rmlo,
6507                rm_hi: *rmhi,
6508                cond: synth_synthesis::Condition::HS,
6509            }),
6510
6511            // I64Const: MOVW rdlo, lo16; MOVT rdlo, hi16; MOVW rdhi, lo16_hi; MOVT rdhi, hi16_hi
6512            ArmOp::I64Const { rdlo, rdhi, value } => {
6513                let lo32 = *value as u32;
6514                let hi32 = (*value >> 32) as u32;
6515                let mut bytes = Vec::new();
6516                // Load low 32 bits into rdlo
6517                bytes.extend_from_slice(
6518                    &self.encode_thumb32_movw_raw(reg_to_bits(rdlo), lo32 & 0xFFFF)?,
6519                );
6520                if lo32 > 0xFFFF {
6521                    bytes.extend_from_slice(
6522                        &self.encode_thumb32_movt_raw(reg_to_bits(rdlo), lo32 >> 16)?,
6523                    );
6524                }
6525                // Load high 32 bits into rdhi
6526                bytes.extend_from_slice(
6527                    &self.encode_thumb32_movw_raw(reg_to_bits(rdhi), hi32 & 0xFFFF)?,
6528                );
6529                if hi32 > 0xFFFF {
6530                    bytes.extend_from_slice(
6531                        &self.encode_thumb32_movt_raw(reg_to_bits(rdhi), hi32 >> 16)?,
6532                    );
6533                }
6534                Ok(bytes)
6535            }
6536
6537            // I64Ldr: LDR rdlo, [base, offset]; LDR rdhi, [base, offset+4]
6538            ArmOp::I64Ldr { rdlo, rdhi, addr } => {
6539                let mut bytes = Vec::new();
6540                // #372/#382: a memory `i64.load` carries an index register
6541                // (`reg_imm(R11, addr_reg, offset)` = R11 + addr + offset). The
6542                // immediate `encode_thumb32_ldr` below uses only base+offset and
6543                // would SILENTLY DROP `offset_reg` — the #206 defect, here for
6544                // i64. `i64_effective_base` materializes the effective base into
6545                // `ip` (and, when `offset+4 > 0xFFF`, folds the offset in too so
6546                // the function is NOT skipped — #382), returning the residual
6547                // imm12 for the two halves. Frame i64 loads (no `offset_reg`, e.g.
6548                // a spilled local at `[SP, #off]`) keep the plain `[base,#off]`
6549                // form unchanged — so existing output is byte-identical.
6550                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6551                bytes.extend_from_slice(&self.encode_thumb32_ldr(rdlo, &base, offset)?);
6552                bytes.extend_from_slice(&self.encode_thumb32_ldr(
6553                    rdhi,
6554                    &base,
6555                    offset.wrapping_add(4),
6556                )?);
6557                Ok(bytes)
6558            }
6559
6560            // I64Str: STR rdlo, [base, offset]; STR rdhi, [base, offset+4]
6561            ArmOp::I64Str { rdlo, rdhi, addr } => {
6562                let mut bytes = Vec::new();
6563                // #372/#382: same index-materialization + large-offset fold as
6564                // I64Ldr (see above).
6565                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6566                bytes.extend_from_slice(&self.encode_thumb32_str(rdlo, &base, offset)?);
6567                bytes.extend_from_slice(&self.encode_thumb32_str(
6568                    rdhi,
6569                    &base,
6570                    offset.wrapping_add(4),
6571                )?);
6572                Ok(bytes)
6573            }
6574
6575            // I64ExtendI32S: MOV rdlo, rn; ASR rdhi, rdlo, #31 (sign-extend)
6576            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
6577                let mut bytes = Vec::new();
6578                if rdlo != rn {
6579                    // MOV rdlo, rn (16-bit)
6580                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6581                        rd: *rdlo,
6582                        op2: Operand2::Reg(*rn),
6583                    })?);
6584                }
6585                // ASR rdhi, rdlo, #31 (sign-extend: fill high word with sign bit)
6586                bytes.extend_from_slice(
6587                    &self.encode_thumb32_shift(rdhi, rdlo, 31, 0b10)?, // ASR type
6588                );
6589                Ok(bytes)
6590            }
6591
6592            // I64ExtendI32U: MOV rdlo, rn; MOV rdhi, #0
6593            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
6594                let mut bytes = Vec::new();
6595                if rdlo != rn {
6596                    // MOV rdlo, rn
6597                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6598                        rd: *rdlo,
6599                        op2: Operand2::Reg(*rn),
6600                    })?);
6601                }
6602                // MOV rdhi, #0 (16-bit: MOVS Rd, #0)
6603                let rdhi_bits = reg_to_bits(rdhi) as u16;
6604                let instr: u16 = 0x2000 | (rdhi_bits << 8);
6605                bytes.extend_from_slice(&instr.to_le_bytes());
6606                Ok(bytes)
6607            }
6608
6609            // I32WrapI64: MOV rd, rnlo (just take low 32 bits)
6610            ArmOp::I32WrapI64 { rd, rnlo } => {
6611                if rd == rnlo {
6612                    // No-op: already in the right register
6613                    let instr: u16 = 0xBF00; // NOP
6614                    Ok(instr.to_le_bytes().to_vec())
6615                } else {
6616                    // MOV rd, rnlo
6617                    self.encode_thumb(&ArmOp::Mov {
6618                        rd: *rd,
6619                        op2: Operand2::Reg(*rnlo),
6620                    })
6621                }
6622            }
6623
6624            // ===== Helium MVE operations (Thumb-2 encoding) =====
6625            ArmOp::MveLoad { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vldrw(qd, addr))),
6626            ArmOp::MveStore { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vstrw(qd, addr))),
6627            ArmOp::MveConst { qd, bytes } => self.encode_thumb_mve_const(qd, bytes),
6628            ArmOp::MveAnd { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6629                0xEF000150, qd, qn, qm,
6630            ))),
6631            ArmOp::MveOrr { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6632                0xEF200150, qd, qn, qm,
6633            ))),
6634            ArmOp::MveEor { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6635                0xFF000150, qd, qn, qm,
6636            ))),
6637            ArmOp::MveMvn { qd, qm } => {
6638                // VMVN Qd, Qm: 0xFFB005C0 | Qd<<12 | Qm
6639                let qd_enc = qreg_to_num(qd);
6640                let qm_enc = qreg_to_num(qm);
6641                let instr: u32 = 0xFFB005C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6642                Ok(vfp_to_thumb_bytes(instr))
6643            }
6644            ArmOp::MveBic { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6645                0xEF100150, qd, qn, qm,
6646            ))),
6647            ArmOp::MveAddI { qd, qn, qm, size } => {
6648                let sz = mve_size_bits(size);
6649                let base: u32 = 0xEF000840 | (sz << 20);
6650                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6651            }
6652            ArmOp::MveSubI { qd, qn, qm, size } => {
6653                let sz = mve_size_bits(size);
6654                let base: u32 = 0xFF000840 | (sz << 20);
6655                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6656            }
6657            ArmOp::MveMulI { qd, qn, qm, size } => {
6658                let sz = mve_size_bits(size);
6659                let base: u32 = 0xEF000950 | (sz << 20);
6660                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6661            }
6662            ArmOp::MveNegI { qd, qm, size } => {
6663                let sz = mve_size_bits(size);
6664                // VNEG.Sx Qd, Qm
6665                let qd_enc = qreg_to_num(qd);
6666                let qm_enc = qreg_to_num(qm);
6667                let base: u32 = 0xFFB103C0 | (sz << 18);
6668                let instr = base | ((qd_enc * 2) << 12) | (qm_enc * 2);
6669                Ok(vfp_to_thumb_bytes(instr))
6670            }
6671            ArmOp::MveDup { qd, rn, size } => {
6672                let sz = mve_size_bits(size);
6673                let qd_enc = qreg_to_num(qd);
6674                let rn_bits = reg_to_bits(rn);
6675                // VDUP.sz Qd, Rn: EEA0 0B10 variant
6676                // size encoding: 00=32, 01=16, 10=8
6677                let be = match sz {
6678                    0 => 0b00u32, // 8-bit
6679                    1 => 0b01,    // 16-bit
6680                    _ => 0b00,    // 32-bit (default)
6681                };
6682                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12) | (be << 5);
6683                Ok(vfp_to_thumb_bytes(instr))
6684            }
6685            ArmOp::MveExtractLane { rd, qn, lane, size } => {
6686                let qn_enc = qreg_to_num(qn);
6687                let rd_bits = reg_to_bits(rd);
6688                // VMOV.sz Rd, Dn[x] — extract from Q-register lane
6689                // For 32-bit: VMOV Rd, Dn — where Dn is the appropriate D-register
6690                let d_reg = qn_enc * 2 + ((*lane as u32) >> 1);
6691                let lane_in_d = (*lane as u32) & 1;
6692                let _sz = mve_size_bits(size);
6693                // VMOV Rd, Dn[x]: EE10 0B10 for 32-bit
6694                let instr: u32 = 0xEE100B10 | (d_reg << 16) | (rd_bits << 12) | (lane_in_d << 21);
6695                Ok(vfp_to_thumb_bytes(instr))
6696            }
6697            ArmOp::MveInsertLane { qd, rn, lane, size } => {
6698                let qd_enc = qreg_to_num(qd);
6699                let rn_bits = reg_to_bits(rn);
6700                let d_reg = qd_enc * 2 + ((*lane as u32) >> 1);
6701                let lane_in_d = (*lane as u32) & 1;
6702                let _sz = mve_size_bits(size);
6703                // VMOV Dn[x], Rn: EE00 0B10 for 32-bit
6704                let instr: u32 = 0xEE000B10 | (d_reg << 16) | (rn_bits << 12) | (lane_in_d << 21);
6705                Ok(vfp_to_thumb_bytes(instr))
6706            }
6707
6708            // MVE float comparisons — emit VCMP + VPSEL sequence (simplified: just VCMP)
6709            ArmOp::MveCmpEqI { qd, qn, qm, size }
6710            | ArmOp::MveCmpNeI { qd, qn, qm, size }
6711            | ArmOp::MveCmpLtS { qd, qn, qm, size }
6712            | ArmOp::MveCmpLtU { qd, qn, qm, size }
6713            | ArmOp::MveCmpGtS { qd, qn, qm, size }
6714            | ArmOp::MveCmpGtU { qd, qn, qm, size }
6715            | ArmOp::MveCmpLeS { qd, qn, qm, size }
6716            | ArmOp::MveCmpLeU { qd, qn, qm, size }
6717            | ArmOp::MveCmpGeS { qd, qn, qm, size }
6718            | ArmOp::MveCmpGeU { qd, qn, qm, size } => {
6719                // Encode as VADD (placeholder encoding — real implementation
6720                // would use VCMP + VPSEL pair)
6721                let sz = mve_size_bits(size);
6722                let base: u32 = 0xEF000840 | (sz << 20);
6723                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6724            }
6725
6726            // f32x4 MVE arithmetic
6727            ArmOp::MveAddF32 { qd, qn, qm } => {
6728                // VADD.F32 Qd, Qn, Qm (MVE): 0xEF000D40
6729                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6730            }
6731            ArmOp::MveSubF32 { qd, qn, qm } => {
6732                // VSUB.F32 Qd, Qn, Qm (MVE): 0xEF200D40
6733                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF200D40, qd, qn, qm)))
6734            }
6735            ArmOp::MveMulF32 { qd, qn, qm } => {
6736                // VMUL.F32 Qd, Qn, Qm (MVE): 0xFF000D50
6737                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xFF000D50, qd, qn, qm)))
6738            }
6739            ArmOp::MveNegF32 { qd, qm } => {
6740                let qd_enc = qreg_to_num(qd);
6741                let qm_enc = qreg_to_num(qm);
6742                // VNEG.F32 Qd, Qm: FFB907C0
6743                let instr: u32 = 0xFFB907C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6744                Ok(vfp_to_thumb_bytes(instr))
6745            }
6746            ArmOp::MveAbsF32 { qd, qm } => {
6747                let qd_enc = qreg_to_num(qd);
6748                let qm_enc = qreg_to_num(qm);
6749                // VABS.F32 Qd, Qm: FFB90740
6750                let instr: u32 = 0xFFB90740 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6751                Ok(vfp_to_thumb_bytes(instr))
6752            }
6753            ArmOp::MveCmpEqF32 { qd, qn, qm }
6754            | ArmOp::MveCmpNeF32 { qd, qn, qm }
6755            | ArmOp::MveCmpLtF32 { qd, qn, qm }
6756            | ArmOp::MveCmpLeF32 { qd, qn, qm }
6757            | ArmOp::MveCmpGtF32 { qd, qn, qm }
6758            | ArmOp::MveCmpGeF32 { qd, qn, qm } => {
6759                // Placeholder: encode as VADD.F32 (real impl needs VCMP.F32 + VPSEL)
6760                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6761            }
6762            ArmOp::MveDupF32 { qd, rn } => {
6763                let qd_enc = qreg_to_num(qd);
6764                let rn_bits = reg_to_bits(rn);
6765                // VDUP.32 Qd, Rn (same encoding as integer VDUP.32)
6766                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12);
6767                Ok(vfp_to_thumb_bytes(instr))
6768            }
6769            ArmOp::MveExtractLaneF32 { rd, qn, lane } => {
6770                let qn_enc = qreg_to_num(qn);
6771                let rd_bits = reg_to_bits(rd);
6772                // VMOV Rd, Sn where Sn = Q*4 + lane
6773                let s_num = qn_enc * 4 + (*lane as u32);
6774                let (vn, n) = encode_sreg(s_num);
6775                let instr: u32 = 0xEE100A10 | (vn << 16) | (rd_bits << 12) | (n << 7);
6776                Ok(vfp_to_thumb_bytes(instr))
6777            }
6778            ArmOp::MveReplaceLaneF32 { qd, rn, lane } => {
6779                let qd_enc = qreg_to_num(qd);
6780                let rn_bits = reg_to_bits(rn);
6781                // VMOV Sn, Rn where Sn = Q*4 + lane
6782                let s_num = qd_enc * 4 + (*lane as u32);
6783                let (vn, n) = encode_sreg(s_num);
6784                let instr: u32 = 0xEE000A10 | (vn << 16) | (rn_bits << 12) | (n << 7);
6785                Ok(vfp_to_thumb_bytes(instr))
6786            }
6787            ArmOp::MveDivF32 { qd, qn, qm } => {
6788                // Lane-wise: extract 4 S-regs, VDIV, insert back
6789                self.encode_thumb_mve_lane_wise_f32_binop(qd, qn, qm, 0xEE800A00)
6790            }
6791            ArmOp::MveSqrtF32 { qd, qm } => {
6792                // Lane-wise: extract 4 S-regs, VSQRT, insert back
6793                self.encode_thumb_mve_lane_wise_f32_sqrt(qd, qm)
6794            }
6795
6796            // Catch-all for any remaining ops
6797            _ => {
6798                let instr: u16 = 0xBF00; // NOP
6799                Ok(instr.to_le_bytes().to_vec())
6800            }
6801        }
6802    }
6803
6804    // === Thumb-2 VFP multi-instruction helpers ===
6805
6806    /// Encode F32 comparison as Thumb-2: VCMP.F32 + VMRS + MOVS rd,#0 + IT + MOV rd,#1
6807    fn encode_thumb_f32_compare(
6808        &self,
6809        rd: &Reg,
6810        sn: &VfpReg,
6811        sm: &VfpReg,
6812        cond_code: u32,
6813    ) -> Result<Vec<u8>> {
6814        let mut bytes = Vec::new();
6815        let rd_bits = reg_to_bits(rd);
6816
6817        // #709 (bug found under #708/#709): the `MOVS Rd,#0` below is a
6818        // FLAG-SETTING 16-bit move. Emitting it AFTER `VMRS APSR_nzcv, FPSCR`
6819        // (as the original code did) clobbered the N/Z/C/V flags the VMRS just
6820        // transferred from the VFP compare, so the following `IT<cond>` read
6821        // stale flags and every f32 comparison silently returned 0 (verified:
6822        // `flt(1.0,2.0)` → 0 on Cortex-M4F). The 619 harness never caught it
6823        // because it deliberately skipped compare EXECUTION on a false premise
6824        // (unicorn DOES model VMRS→APSR). Fix: materialize the `#0` FIRST, then
6825        // VCMP+VMRS set the flags the `IT` consumes. Instruction sizes are
6826        // unchanged (pure reorder), so the estimator↔encoder oracle (#511) is
6827        // untouched — only the byte ORDER differs.
6828
6829        // MOVS Rd, #0 (16-bit): 0010 0 Rd(3) 0000 0000 — its flag side effect
6830        // is immediately overwritten by the VMRS below.
6831        if rd_bits < 8 {
6832            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
6833            bytes.extend_from_slice(&movs_zero.to_le_bytes());
6834        } else {
6835            // MOV.W Rd, #0 (32-bit Thumb-2)
6836            let hw1: u16 = 0xF04F;
6837            let hw2: u16 = (rd_bits as u16) << 8;
6838            bytes.extend_from_slice(&hw1.to_le_bytes());
6839            bytes.extend_from_slice(&hw2.to_le_bytes());
6840        }
6841
6842        // VCMP.F32 Sn, Sm
6843        let sn_num = vfp_sreg_to_num(sn)?;
6844        let sm_num = vfp_sreg_to_num(sm)?;
6845        let (vd, d) = encode_sreg(sn_num);
6846        let (vm, m) = encode_sreg(sm_num);
6847        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
6848        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
6849
6850        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10 (sets the flags IT consumes)
6851        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
6852
6853        // IT<cond> — If-Then for conditional MOV
6854        // IT encoding: 1011 1111 cond(4) mask(4)
6855        // mask = 0x8 for single "then" (IT)
6856        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
6857        bytes.extend_from_slice(&it.to_le_bytes());
6858
6859        // MOV Rd, #1 (16-bit, conditional due to IT): 0010 0 Rd(3) 0000 0001
6860        if rd_bits < 8 {
6861            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
6862            bytes.extend_from_slice(&mov_one.to_le_bytes());
6863        } else {
6864            // MOV.W Rd, #1 (32-bit)
6865            let hw1: u16 = 0xF04F;
6866            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
6867            bytes.extend_from_slice(&hw1.to_le_bytes());
6868            bytes.extend_from_slice(&hw2.to_le_bytes());
6869        }
6870
6871        Ok(bytes)
6872    }
6873
6874    /// Encode F32 constant load as Thumb-2: MOVW + MOVT + VMOV
6875    fn encode_thumb_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
6876        let mut bytes = Vec::new();
6877        let bits = value.to_bits();
6878        let rt: u32 = 12; // R12/IP as temp
6879
6880        // MOVW R12, #lo16
6881        // Thumb-2 MOVW: 11110 i 10 0100 imm4 | 0 imm3 Rd imm8
6882        let lo16 = bits & 0xFFFF;
6883        let imm4 = (lo16 >> 12) & 0xF;
6884        let i_bit = (lo16 >> 11) & 1;
6885        let imm3 = (lo16 >> 8) & 0x7;
6886        let imm8 = lo16 & 0xFF;
6887        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
6888        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
6889        bytes.extend_from_slice(&hw1.to_le_bytes());
6890        bytes.extend_from_slice(&hw2.to_le_bytes());
6891
6892        // MOVT R12, #hi16
6893        let hi16 = (bits >> 16) & 0xFFFF;
6894        let imm4 = (hi16 >> 12) & 0xF;
6895        let i_bit = (hi16 >> 11) & 1;
6896        let imm3 = (hi16 >> 8) & 0x7;
6897        let imm8 = hi16 & 0xFF;
6898        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
6899        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
6900        bytes.extend_from_slice(&hw1.to_le_bytes());
6901        bytes.extend_from_slice(&hw2.to_le_bytes());
6902
6903        // VMOV Sd, R12
6904        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
6905        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6906
6907        Ok(bytes)
6908    }
6909
6910    /// Encode VMOV + VCVT.F32.xS32 as Thumb-2
6911    fn encode_thumb_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
6912        let mut bytes = Vec::new();
6913
6914        // VMOV Sd, Rm
6915        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
6916        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6917
6918        // VCVT.F32.S32/U32 Sd, Sd. Bit 7 (op) = 1 for signed (S32), 0 for
6919        // unsigned (U32): signed = 0xEEB80AC0, unsigned = 0xEEB80A40
6920        // (GI-FPU-002: previously swapped — see the ARM32 twin).
6921        let sd_num = vfp_sreg_to_num(sd)?;
6922        let (vd, d) = encode_sreg(sd_num);
6923        let (vm, m) = encode_sreg(sd_num);
6924        let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
6925        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
6926        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
6927
6928        Ok(bytes)
6929    }
6930
6931    /// Encode F32 rounding pseudo-op as Thumb-2 via VCVT to integer and back
6932    /// Encode F32 rounding as Thumb-2.
6933    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
6934    ///
6935    /// For trunc: uses VCVTR.S32.F32 (always truncates).
6936    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant),
6937    /// then restores FPSCR.
6938    fn encode_thumb_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
6939        let mut bytes = Vec::new();
6940        let sm_num = vfp_sreg_to_num(sm)?;
6941        let sd_num = vfp_sreg_to_num(sd)?;
6942        let (vd_s, d_s) = encode_sreg(sd_num);
6943        let (vm_s, m_s) = encode_sreg(sm_num);
6944
6945        if mode == 0b11 {
6946            // Trunc (toward zero): VCVTR.S32.F32 — bit[7]=1, always truncates
6947            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
6948            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
6949        } else {
6950            // ceil/floor/nearest: manipulate FPSCR rounding mode
6951            let rt: u32 = 12; // R12/IP as temp
6952
6953            // VMRS R12, FPSCR
6954            let vmrs = 0xEEF10A10 | (rt << 12);
6955            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
6956
6957            // BIC.W R12, R12, #(3 << 22) — clear RMode bits [23:22]
6958            // Thumb-2 modified immediate for 3<<22 = 0x00C00000:
6959            // BIC.W encoding: 11110 i 0 0001 S Rn | 0 imm3 Rd imm8
6960            // 0x00C00000 = 0x03 shifted left by 22 => Thumb mod-imm: i=0, imm3=0b101, imm8=0x03
6961            let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF); // BIC, Rn=R12
6962            let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
6963            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
6964            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
6965
6966            // ORR.W R12, R12, #(mode << 22)
6967            if mode != 0 {
6968                let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF); // ORR, Rn=R12
6969                let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
6970                bytes.extend_from_slice(&orr_hw1.to_le_bytes());
6971                bytes.extend_from_slice(&orr_hw2.to_le_bytes());
6972            }
6973
6974            // VMSR FPSCR, R12
6975            let vmsr = 0xEEE10A10 | (rt << 12);
6976            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
6977
6978            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rmode
6979            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
6980            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
6981
6982            // Restore FPSCR: clear rmode bits back to nearest (default)
6983            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
6984            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
6985            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
6986            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
6987        }
6988
6989        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
6990        let (vd2, d2) = encode_sreg(sd_num);
6991        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
6992        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
6993
6994        Ok(bytes)
6995    }
6996
6997    /// Encode F32 min/max as Thumb-2: VMOV + VCMP + VMRS + IT + VMOV
6998    fn encode_thumb_f32_minmax(
6999        &self,
7000        sd: &VfpReg,
7001        sn: &VfpReg,
7002        sm: &VfpReg,
7003        is_min: bool,
7004    ) -> Result<Vec<u8>> {
7005        let mut bytes = Vec::new();
7006        let sn_num = vfp_sreg_to_num(sn)?;
7007        let sm_num = vfp_sreg_to_num(sm)?;
7008        let sd_num = vfp_sreg_to_num(sd)?;
7009
7010        // VMOV.F32 Sd, Sn
7011        let (vd, d) = encode_sreg(sd_num);
7012        let (vn, n) = encode_sreg(sn_num);
7013        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
7014        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sn));
7015
7016        // VCMP.F32 Sn, Sm
7017        let (vm, m) = encode_sreg(sm_num);
7018        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7019        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7020
7021        // VMRS APSR_nzcv, FPSCR
7022        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7023
7024        // IT GT (for min) or IT MI (for max)
7025        let cond: u16 = if is_min { 0xC } else { 0x4 };
7026        let it: u16 = 0xBF00 | (cond << 4) | 0x8;
7027        bytes.extend_from_slice(&it.to_le_bytes());
7028
7029        // VMOV{cond}.F32 Sd, Sm — conditional VMOV in IT block
7030        let vmov_sm = 0xEEB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7031        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sm));
7032
7033        Ok(bytes)
7034    }
7035
7036    /// Encode F32 copysign as Thumb-2
7037    fn encode_thumb_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7038        let mut bytes = Vec::new();
7039
7040        // VMOV R12, Sm (get sign source bits)
7041        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
7042            false,
7043            sm,
7044            &Reg::R12,
7045        )?));
7046
7047        // VMOV R0, Sn (get magnitude source bits)
7048        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
7049            false,
7050            sn,
7051            &Reg::R0,
7052        )?));
7053
7054        // AND.W R12, R12, #0x80000000
7055        // Thumb-2 modified immediate: 0x80000000 = constant 0x80 with rotation
7056        // Using T1 encoding: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8
7057        // 0x80000000: i=0, imm3=0b001, imm8=0x00 (rotation=4, value=0x80)
7058        // Actually encoding #0x80000000 as modified constant:
7059        // bit pattern 1 followed by 31 zeros: enc = 0b0100_00000000 = 0x0100? No.
7060        // ARM modified immediate: abcdefgh rotated. 0x80000000 = 0x80 ROR 2 = enc 0x0102
7061        // Actually: value = abcdefgh ROR (2*rot). 0x80 = 10000000, ROR 2 gives 0x20000000.
7062        // For 0x80000000: 0x02 ROR 2 = 0x80000000. So imm12 = (1<<8) | 0x02 = 0x102
7063        let hw1: u16 = 0xF000 | 12; // AND.W R12, R12, #modified_const (i=0, Rn=R12)
7064        let hw2: u16 = (0x1 << 12) | (12 << 8) | 0x02; // imm3=1, Rd=R12, imm8=0x02
7065        bytes.extend_from_slice(&hw1.to_le_bytes());
7066        bytes.extend_from_slice(&hw2.to_le_bytes());
7067
7068        // BIC.W R0, R0, #0x80000000 (R0 = register 0, fields are zero)
7069        let hw1: u16 = 0xF020; // BIC.W R0, R0, #modified_const (i=0, Rn=R0)
7070        let hw2: u16 = (0x1 << 12) | 0x02; // imm3=1, Rd=R0, imm8=0x02
7071        bytes.extend_from_slice(&hw1.to_le_bytes());
7072        bytes.extend_from_slice(&hw2.to_le_bytes());
7073
7074        // ORR.W R0, R0, R12 (R0 = register 0)
7075        let hw1: u16 = 0xEA40; // ORR.W R0, R0, R12 (Rn=R0)
7076        let hw2: u16 = 12; // Rd=R0, Rm=R12
7077        bytes.extend_from_slice(&hw1.to_le_bytes());
7078        bytes.extend_from_slice(&hw2.to_le_bytes());
7079
7080        // VMOV Sd, R0
7081        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
7082            true,
7083            sd,
7084            &Reg::R0,
7085        )?));
7086
7087        Ok(bytes)
7088    }
7089
7090    /// Encode F64 comparison as Thumb-2: VCMP.F64 + VMRS + MOV #0 + IT + MOV #1
7091    fn encode_thumb_f64_compare(
7092        &self,
7093        rd: &Reg,
7094        dn: &VfpReg,
7095        dm: &VfpReg,
7096        cond_code: u32,
7097    ) -> Result<Vec<u8>> {
7098        let mut bytes = Vec::new();
7099        let rd_bits = reg_to_bits(rd);
7100
7101        // VCMP.F64 Dn, Dm
7102        let dn_num = vfp_dreg_to_num(dn)?;
7103        let dm_num = vfp_dreg_to_num(dm)?;
7104        let (vd, d) = encode_dreg(dn_num);
7105        let (vm, m) = encode_dreg(dm_num);
7106        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7107        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7108
7109        // VMRS APSR_nzcv, FPSCR
7110        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7111
7112        // MOVS Rd, #0
7113        if rd_bits < 8 {
7114            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
7115            bytes.extend_from_slice(&movs_zero.to_le_bytes());
7116        } else {
7117            let hw1: u16 = 0xF04F;
7118            let hw2: u16 = (rd_bits as u16) << 8;
7119            bytes.extend_from_slice(&hw1.to_le_bytes());
7120            bytes.extend_from_slice(&hw2.to_le_bytes());
7121        }
7122
7123        // IT<cond>
7124        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7125        bytes.extend_from_slice(&it.to_le_bytes());
7126
7127        // MOV Rd, #1
7128        if rd_bits < 8 {
7129            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7130            bytes.extend_from_slice(&mov_one.to_le_bytes());
7131        } else {
7132            let hw1: u16 = 0xF04F;
7133            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7134            bytes.extend_from_slice(&hw1.to_le_bytes());
7135            bytes.extend_from_slice(&hw2.to_le_bytes());
7136        }
7137
7138        Ok(bytes)
7139    }
7140
7141    /// Encode F64 constant load as Thumb-2: MOVW+MOVT (lo32 into R0) + MOVW+MOVT (hi32 into R12) + VMOV Dd, R0, R12
7142    fn encode_thumb_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
7143        let mut bytes = Vec::new();
7144        let bits = value.to_bits();
7145        let lo32 = bits as u32;
7146        let hi32 = (bits >> 32) as u32;
7147
7148        // MOVW R0, #lo16(lo32)
7149        let lo16 = lo32 & 0xFFFF;
7150        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(0, lo16)?);
7151
7152        // MOVT R0, #hi16(lo32)
7153        let hi16 = (lo32 >> 16) & 0xFFFF;
7154        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(0, hi16)?);
7155
7156        // MOVW R12, #lo16(hi32)
7157        let lo16 = hi32 & 0xFFFF;
7158        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
7159
7160        // MOVT R12, #hi16(hi32)
7161        let hi16 = (hi32 >> 16) & 0xFFFF;
7162        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
7163
7164        // VMOV Dd, R0, R12
7165        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
7166        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7167
7168        Ok(bytes)
7169    }
7170
7171    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as Thumb-2
7172    fn encode_thumb_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7173        let mut bytes = Vec::new();
7174
7175        // VMOV S0, Rm
7176        let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
7177        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7178
7179        // VCVT.F64.S32 Dd, S0 or VCVT.F64.U32 Dd, S0
7180        let dd_num = vfp_dreg_to_num(dd)?;
7181        let (vd, d) = encode_dreg(dd_num);
7182        let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
7183        let vcvt = base | (d << 22) | (vd << 12);
7184        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7185
7186        Ok(bytes)
7187    }
7188
7189    /// Encode VCVT.F64.F32 Dd, Sm as Thumb-2
7190    fn encode_thumb_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7191        let dd_num = vfp_dreg_to_num(dd)?;
7192        let sm_num = vfp_sreg_to_num(sm)?;
7193        let (vd, d) = encode_dreg(dd_num);
7194        let (vm, m) = encode_sreg(sm_num);
7195
7196        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7197        Ok(vfp_to_thumb_bytes(vcvt))
7198    }
7199
7200    /// Encode VCVT.S32/U32.F64 S0, Dm + VMOV Rd, S0 as Thumb-2
7201    fn encode_thumb_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7202        let mut bytes = Vec::new();
7203        let dm_num = vfp_dreg_to_num(dm)?;
7204        let (vm, m) = encode_dreg(dm_num);
7205
7206        // VCVT.S32.F64 S0, Dm or VCVT.U32.F64 S0, Dm
7207        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
7208        let vcvt = base | (m << 5) | vm;
7209        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7210
7211        // VMOV Rd, S0
7212        let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
7213        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7214
7215        Ok(bytes)
7216    }
7217
7218    /// Encode F64 rounding pseudo-op as Thumb-2 via VCVT to integer and back
7219    /// Encode F64 rounding as Thumb-2.
7220    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
7221    fn encode_thumb_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7222        let mut bytes = Vec::new();
7223        let dm_num = vfp_dreg_to_num(dm)?;
7224        let dd_num = vfp_dreg_to_num(dd)?;
7225        let (vm, m) = encode_dreg(dm_num);
7226        let (vd, d) = encode_dreg(dd_num);
7227
7228        if mode == 0b11 {
7229            // Trunc: VCVTR.S32.F64 — bit[7]=1, always truncates
7230            let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
7231            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7232        } else {
7233            let rt: u32 = 12;
7234
7235            // VMRS R12, FPSCR
7236            let vmrs = 0xEEF10A10 | (rt << 12);
7237            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7238
7239            // BIC.W R12, R12, #(3 << 22)
7240            let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF);
7241            let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
7242            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7243            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7244
7245            // ORR.W R12, R12, #(mode << 22)
7246            if mode != 0 {
7247                let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF);
7248                let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
7249                bytes.extend_from_slice(&orr_hw1.to_le_bytes());
7250                bytes.extend_from_slice(&orr_hw2.to_le_bytes());
7251            }
7252
7253            // VMSR FPSCR, R12
7254            let vmsr = 0xEEE10A10 | (rt << 12);
7255            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7256
7257            // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0)
7258            let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
7259            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7260
7261            // Restore FPSCR
7262            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7263            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7264            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7265            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7266        }
7267
7268        // VCVT.F64.S32 Dd, S0
7269        let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
7270        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
7271
7272        Ok(bytes)
7273    }
7274
7275    /// Encode F64 min/max as Thumb-2
7276    fn encode_thumb_f64_minmax(
7277        &self,
7278        dd: &VfpReg,
7279        dn: &VfpReg,
7280        dm: &VfpReg,
7281        is_min: bool,
7282    ) -> Result<Vec<u8>> {
7283        let mut bytes = Vec::new();
7284        let dn_num = vfp_dreg_to_num(dn)?;
7285        let dm_num = vfp_dreg_to_num(dm)?;
7286        let dd_num = vfp_dreg_to_num(dd)?;
7287
7288        // VMOV.F64 Dd, Dn
7289        let (vd, d) = encode_dreg(dd_num);
7290        let (vn, n) = encode_dreg(dn_num);
7291        let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
7292        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_dn));
7293
7294        // VCMP.F64 Dn, Dm
7295        let (vm, m) = encode_dreg(dm_num);
7296        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7297        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7298
7299        // VMRS APSR_nzcv, FPSCR
7300        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7301
7302        // IT GT (for min) or IT MI (for max)
7303        let cond: u16 = if is_min { 0xC } else { 0x4 };
7304        let it: u16 = 0xBF00 | (cond << 4) | 0x8;
7305        bytes.extend_from_slice(&it.to_le_bytes());
7306
7307        // VMOV{cond}.F64 Dd, Dm
7308        let vmov_dm = 0xEEB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7309        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_dm));
7310
7311        Ok(bytes)
7312    }
7313
7314    /// Encode F64 copysign as Thumb-2
7315    fn encode_thumb_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7316        let mut bytes = Vec::new();
7317
7318        // VMOV R0, R12, Dm (get sign source)
7319        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_dreg(
7320            false,
7321            dm,
7322            &Reg::R0,
7323            &Reg::R12,
7324        )?));
7325
7326        // VMOV R1, R2, Dn (get magnitude source)
7327        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_dreg(
7328            false,
7329            dn,
7330            &Reg::R1,
7331            &Reg::R2,
7332        )?));
7333
7334        // AND.W R12, R12, #0x80000000 (i=0, Rn=R12)
7335        let hw1: u16 = 0xF000 | 12;
7336        let hw2: u16 = (0x1 << 12) | (12 << 8) | 0x02;
7337        bytes.extend_from_slice(&hw1.to_le_bytes());
7338        bytes.extend_from_slice(&hw2.to_le_bytes());
7339
7340        // BIC.W R2, R2, #0x80000000 (i=0, Rn=R2)
7341        let hw1: u16 = 0xF020 | 2;
7342        let hw2: u16 = (0x1 << 12) | (2 << 8) | 0x02;
7343        bytes.extend_from_slice(&hw1.to_le_bytes());
7344        bytes.extend_from_slice(&hw2.to_le_bytes());
7345
7346        // ORR.W R2, R2, R12
7347        let hw1: u16 = 0xEA40 | 2;
7348        let hw2: u16 = (2 << 8) | 12;
7349        bytes.extend_from_slice(&hw1.to_le_bytes());
7350        bytes.extend_from_slice(&hw2.to_le_bytes());
7351
7352        // VMOV Dd, R1, R2
7353        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_dreg(
7354            true,
7355            dd,
7356            &Reg::R1,
7357            &Reg::R2,
7358        )?));
7359
7360        Ok(bytes)
7361    }
7362
7363    /// Encode VCVT.S32/U32.F32 + VMOV as Thumb-2
7364    fn encode_thumb_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7365        let mut bytes = Vec::new();
7366
7367        let sm_num = vfp_sreg_to_num(sm)?;
7368        let (vd, d) = encode_sreg(sm_num);
7369        let (vm, m) = encode_sreg(sm_num);
7370        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
7371        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7372        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7373
7374        // VMOV Rd, Sm
7375        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
7376        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7377
7378        Ok(bytes)
7379    }
7380
7381    // === Thumb-2 32-bit encoding helpers ===
7382
7383    /// Encode Thumb-2 32-bit ADD with immediate
7384    fn encode_thumb32_add(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7385        let rd_bits = reg_to_bits(rd);
7386        let rn_bits = reg_to_bits(rn);
7387
7388        // The `i:imm3:imm8` field is split the same way for both forms.
7389        let i_bit = (imm >> 11) & 1;
7390        let imm3 = (imm >> 8) & 0x7;
7391        let imm8 = imm & 0xFF;
7392
7393        let hw1_base = if imm <= 0xFF {
7394            // ADD.W (T3): the field is a ThumbExpandImm modified immediate. For
7395            // imm <= 0xFF (i:imm3 = 0000) it is the zero-extended byte, which is
7396            // correct — keep this form so existing encodings stay bit-identical.
7397            0xF100
7398        } else if imm <= 0xFFF {
7399            // ADDW (T4): a PLAIN 12-bit immediate (0..4095) — no ThumbExpandImm.
7400            // This is what makes `add sp, sp, #frame` correct for frame sizes
7401            // >= 256, which ADD.W (T3) would silently mis-encode (e.g. #256 -> #0).
7402            0xF200
7403        } else {
7404            return Err(synth_core::Error::synthesis(
7405                "ADD immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7406            ));
7407        };
7408
7409        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7410        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7411
7412        let mut bytes = hw1.to_le_bytes().to_vec();
7413        bytes.extend_from_slice(&hw2.to_le_bytes());
7414        Ok(bytes)
7415    }
7416
7417    /// Encode Thumb-2 32-bit SUB with immediate
7418    fn encode_thumb32_sub(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7419        let rd_bits = reg_to_bits(rd);
7420        let rn_bits = reg_to_bits(rn);
7421
7422        let i_bit = (imm >> 11) & 1;
7423        let imm3 = (imm >> 8) & 0x7;
7424        let imm8 = imm & 0xFF;
7425
7426        let hw1_base = if imm <= 0xFF {
7427            // SUB.W (T3) modified immediate — correct for the zero-extended byte
7428            // (imm <= 0xFF). Kept bit-identical for existing encodings.
7429            0xF1A0
7430        } else if imm <= 0xFFF {
7431            // SUBW (T4): plain 12-bit immediate (0..4095). Makes
7432            // `sub sp, sp, #frame` correct for frame sizes >= 256.
7433            0xF2A0
7434        } else {
7435            return Err(synth_core::Error::synthesis(
7436                "SUB immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7437            ));
7438        };
7439
7440        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7441        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7442
7443        let mut bytes = hw1.to_le_bytes().to_vec();
7444        bytes.extend_from_slice(&hw2.to_le_bytes());
7445        Ok(bytes)
7446    }
7447
7448    /// Encode Thumb-2 32-bit ADDS with immediate (sets flags)
7449    fn encode_thumb32_adds(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7450        let rd_bits = reg_to_bits(rd);
7451        let rn_bits = reg_to_bits(rn);
7452
7453        // ADDS.W (flag-setting) has only the modified-immediate form — error on
7454        // an un-encodable value rather than silently add the wrong constant.
7455        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7456            synth_core::Error::synthesis(
7457                "ADDS immediate is not a valid ThumbExpandImm — materialize into a register",
7458            )
7459        })?;
7460        let i_bit = (field >> 11) & 1;
7461        let imm3 = (field >> 8) & 0x7;
7462        let imm8 = field & 0xFF;
7463
7464        // ADDS.W Rd, Rn, #imm (with S=1)
7465        // First halfword: 1111 0 i 0 1000 1 Rn = F110 | i<<10 | Rn
7466        let hw1: u16 = (0xF110 | (i_bit << 10) | rn_bits) as u16;
7467        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7468
7469        let mut bytes = hw1.to_le_bytes().to_vec();
7470        bytes.extend_from_slice(&hw2.to_le_bytes());
7471        Ok(bytes)
7472    }
7473
7474    /// Encode Thumb-2 32-bit SUBS with immediate (sets flags)
7475    fn encode_thumb32_subs(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7476        let rd_bits = reg_to_bits(rd);
7477        let rn_bits = reg_to_bits(rn);
7478
7479        // SUBS.W (flag-setting) has only the modified-immediate form — error on
7480        // an un-encodable value rather than silently subtract the wrong constant.
7481        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7482            synth_core::Error::synthesis(
7483                "SUBS immediate is not a valid ThumbExpandImm — materialize into a register",
7484            )
7485        })?;
7486        let i_bit = (field >> 11) & 1;
7487        let imm3 = (field >> 8) & 0x7;
7488        let imm8 = field & 0xFF;
7489
7490        // SUBS.W Rd, Rn, #imm (with S=1)
7491        // First halfword: 1111 0 i 0 1101 1 Rn = F1B0 | i<<10 | Rn
7492        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7493        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7494
7495        let mut bytes = hw1.to_le_bytes().to_vec();
7496        bytes.extend_from_slice(&hw2.to_le_bytes());
7497        Ok(bytes)
7498    }
7499
7500    /// Encode Thumb-2 32-bit MOVW (16-bit immediate)
7501    ///
7502    /// # Contract (Verus-style)
7503    /// ```text
7504    /// requires rd <= R14
7505    /// ensures result.len() == 4
7506    /// ensures (imm & 0xFFFF) can be reconstructed from the encoding
7507    /// ```
7508    fn encode_thumb32_movw(&self, rd: &Reg, imm: u32) -> Result<Vec<u8>> {
7509        let rd_bits = reg_to_bits(rd);
7510        reg_bits_checked(rd_bits)?;
7511        let imm16 = imm & 0xFFFF;
7512
7513        // MOVW Rd, #imm16
7514        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7515        let imm4 = (imm16 >> 12) & 0xF;
7516        let i_bit = (imm16 >> 11) & 1;
7517        let imm3 = (imm16 >> 8) & 0x7;
7518        let imm8 = imm16 & 0xFF;
7519
7520        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7521        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7522
7523        let mut bytes = hw1.to_le_bytes().to_vec();
7524        bytes.extend_from_slice(&hw2.to_le_bytes());
7525        encoding_contracts::verify_thumb32(&bytes);
7526        Ok(bytes)
7527    }
7528
7529    /// Encode Thumb-2 32-bit shift with immediate
7530    ///
7531    /// # Contract (Verus-style)
7532    /// ```text
7533    /// requires rd <= R14, rm <= R14
7534    /// ensures result.len() == 4
7535    /// ```
7536    fn encode_thumb32_shift(
7537        &self,
7538        rd: &Reg,
7539        rm: &Reg,
7540        shift: u32,
7541        shift_type: u8,
7542    ) -> Result<Vec<u8>> {
7543        let rd_bits = reg_to_bits(rd);
7544        let rm_bits = reg_to_bits(rm);
7545        reg_bits_checked(rd_bits)?;
7546        reg_bits_checked(rm_bits)?;
7547        let imm5 = shift & 0x1F;
7548        let imm2 = imm5 & 0x3;
7549        let imm3 = (imm5 >> 2) & 0x7;
7550
7551        // MOV.W Rd, Rm, <shift> #imm
7552        // EA4F 0 imm3 Rd imm2 type Rm
7553        let hw1: u16 = 0xEA4F;
7554        let hw2: u16 =
7555            ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | ((shift_type as u32) << 4) | rm_bits)
7556                as u16;
7557
7558        let mut bytes = hw1.to_le_bytes().to_vec();
7559        bytes.extend_from_slice(&hw2.to_le_bytes());
7560        Ok(bytes)
7561    }
7562
7563    /// Encode Thumb-2 32-bit shift by register
7564    /// Encoding: 11111010 0xx0 Rn | 1111 Rd 0000 Rm
7565    /// shift_type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
7566    fn encode_thumb32_shift_reg(
7567        &self,
7568        rd: &Reg,
7569        rn: &Reg,
7570        rm: &Reg,
7571        shift_type: u8,
7572    ) -> Result<Vec<u8>> {
7573        let rd_bits = reg_to_bits(rd);
7574        let rn_bits = reg_to_bits(rn);
7575        let rm_bits = reg_to_bits(rm);
7576
7577        // hw1: 1111 1010 0xx0 Rn
7578        let hw1: u16 = (0xFA00 | ((shift_type as u32) << 5) | rn_bits) as u16;
7579        // hw2: 1111 Rd 0000 Rm
7580        let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
7581
7582        let mut bytes = hw1.to_le_bytes().to_vec();
7583        bytes.extend_from_slice(&hw2.to_le_bytes());
7584        Ok(bytes)
7585    }
7586
7587    /// Encode Thumb-2 32-bit CMP with immediate
7588    fn encode_thumb32_cmp_imm(&self, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7589        let rn_bits = reg_to_bits(rn);
7590
7591        // CMP.W has only the modified-immediate form (no plain-imm12 like ADDW),
7592        // so an un-encodable immediate MUST be materialized into a register by
7593        // the selector. Error rather than silently compare the wrong constant.
7594        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7595            synth_core::Error::synthesis(
7596                "CMP immediate is not a valid ThumbExpandImm — materialize into a register",
7597            )
7598        })?;
7599        let i_bit = (field >> 11) & 1;
7600        let imm3 = (field >> 8) & 0x7;
7601        let imm8 = field & 0xFF;
7602
7603        // CMP.W Rn, #imm
7604        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7605        let hw2: u16 = ((imm3 << 12) | 0x0F00 | imm8) as u16;
7606
7607        let mut bytes = hw1.to_le_bytes().to_vec();
7608        bytes.extend_from_slice(&hw2.to_le_bytes());
7609        Ok(bytes)
7610    }
7611
7612    /// #372/#382: resolve the base register AND residual immediate offset for an
7613    /// `I64Ldr`/`I64Str` whose address may carry an index register. Returns
7614    /// `(base, low_offset)`; the caller accesses the halves at `[base,
7615    /// #low_offset]` and `[base, #low_offset + 4]`.
7616    ///
7617    /// - Frame access (no `offset_reg`, e.g. a spilled local at `[SP, #off]`):
7618    ///   returns `(addr.base, off)` and emits NOTHING — byte-identical.
7619    /// - Memory access (`reg_imm(R11, addr, offset)` = `R11 + addr + offset`)
7620    ///   with `offset + 4 <= 0xFFF`: emits `ADD.W ip, base, index` and returns
7621    ///   `(ip, offset)`, folding `offset`/`offset+4` into the halves' imm12.
7622    ///   Byte-identical to the pre-#382 (#372) behavior.
7623    /// - Memory access with `offset + 4 > 0xFFF`: the imm12 form cannot hold the
7624    ///   high half's offset, so `encode_thumb32_ldr`'s `check_ldst_imm12` (#259)
7625    ///   rightly refused it and the WHOLE function was skipped (#382). Instead
7626    ///   MATERIALIZE the offset into the base: `ADD ip, index, #offset` (against
7627    ///   the read-only INDEX register, so `encode_thumb32_add_imm` never trips its
7628    ///   `rd==rn==R12` alias trap), then `ADD.W ip, ip, base` (+ R11), and return
7629    ///   `(ip, 0)` so the halves use `[ip, #0]` / `[ip, #4]`.
7630    ///
7631    /// The effective address is fully materialized into `ip` BEFORE the halves
7632    /// are accessed, so an `rdlo` aliasing the index register is safe.
7633    fn i64_effective_base(&self, bytes: &mut Vec<u8>, addr: &MemAddr) -> Result<(Reg, u32)> {
7634        let offset = if addr.offset < 0 {
7635            0u32
7636        } else {
7637            addr.offset as u32
7638        };
7639        match addr.offset_reg {
7640            Some(idx) => {
7641                let ip = Reg::R12;
7642                if offset.wrapping_add(4) > 0xFFF {
7643                    // Large static offset (#382): fold it (and R11) into ip so the
7644                    // imm12 halves stay in range instead of skipping the function.
7645                    // ADD ip, index, #offset  (index != ip → no add_imm alias trap)
7646                    bytes.extend_from_slice(&self.encode_thumb32_add_imm(&ip, &idx, offset)?);
7647                    // ADD.W ip, ip, base  (+ R11)
7648                    bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
7649                        reg_to_bits(&ip),
7650                        reg_to_bits(&ip),
7651                        reg_to_bits(&addr.base),
7652                    )?);
7653                    Ok((ip, 0))
7654                } else {
7655                    // ADD.W ip, addr.base, idx  (Thumb-2, byte-verified vs as)
7656                    let hw1: u16 = 0xEB00 | reg_to_bits(&addr.base) as u16;
7657                    let hw2: u16 = 0x0C00 | reg_to_bits(&idx) as u16;
7658                    bytes.extend_from_slice(&hw1.to_le_bytes());
7659                    bytes.extend_from_slice(&hw2.to_le_bytes());
7660                    Ok((ip, offset))
7661                }
7662            }
7663            None => Ok((addr.base, offset)),
7664        }
7665    }
7666
7667    /// Encode Thumb-2 32-bit LDR
7668    fn encode_thumb32_ldr(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7669        let rd_bits = reg_to_bits(rd);
7670        let base_bits = reg_to_bits(base);
7671
7672        // LDR.W Rd, [Rn, #imm12]
7673        check_ldst_imm12(offset)?;
7674        let hw1: u16 = (0xF8D0 | base_bits) as u16;
7675        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7676
7677        let mut bytes = hw1.to_le_bytes().to_vec();
7678        bytes.extend_from_slice(&hw2.to_le_bytes());
7679        Ok(bytes)
7680    }
7681
7682    /// Encode Thumb-2 32-bit STR
7683    fn encode_thumb32_str(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7684        let rd_bits = reg_to_bits(rd);
7685        let base_bits = reg_to_bits(base);
7686
7687        // STR.W Rd, [Rn, #imm12]
7688        check_ldst_imm12(offset)?;
7689        let hw1: u16 = (0xF8C0 | base_bits) as u16;
7690        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7691
7692        let mut bytes = hw1.to_le_bytes().to_vec();
7693        bytes.extend_from_slice(&hw2.to_le_bytes());
7694        Ok(bytes)
7695    }
7696
7697    /// Encode Thumb-2 32-bit LDR with register offset: LDR.W Rd, [Rn, Rm]
7698    fn encode_thumb32_ldr_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7699        let rd_bits = reg_to_bits(rd);
7700        let base_bits = reg_to_bits(base);
7701        let rm_bits = reg_to_bits(offset_reg);
7702
7703        // LDR.W Rd, [Rn, Rm, LSL #0]
7704        // Encoding: 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
7705        // imm2 = 00 for no shift (LSL #0)
7706        let hw1: u16 = (0xF850 | base_bits) as u16;
7707        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7708
7709        let mut bytes = hw1.to_le_bytes().to_vec();
7710        bytes.extend_from_slice(&hw2.to_le_bytes());
7711        Ok(bytes)
7712    }
7713
7714    /// Encode Thumb-2 32-bit STR with register offset: STR.W Rd, [Rn, Rm]
7715    fn encode_thumb32_str_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7716        let rd_bits = reg_to_bits(rd);
7717        let base_bits = reg_to_bits(base);
7718        let rm_bits = reg_to_bits(offset_reg);
7719
7720        // STR.W Rd, [Rn, Rm, LSL #0]
7721        // Encoding: 1111 1000 0100 Rn | Rt 0000 00 imm2 Rm
7722        // imm2 = 00 for no shift (LSL #0)
7723        let hw1: u16 = (0xF840 | base_bits) as u16;
7724        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7725
7726        let mut bytes = hw1.to_le_bytes().to_vec();
7727        bytes.extend_from_slice(&hw2.to_le_bytes());
7728        Ok(bytes)
7729    }
7730
7731    // === Sub-word load/store Thumb-2 encoding helpers ===
7732
7733    /// Encode Thumb-2 32-bit LDRB with immediate: LDRB.W Rd, [Rn, #imm12]
7734    fn encode_thumb32_ldrb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7735        let rd_bits = reg_to_bits(rd);
7736        let base_bits = reg_to_bits(base);
7737        // LDRB.W Rd, [Rn, #imm12]: 1111 1000 1001 Rn | Rt imm12
7738        check_ldst_imm12(offset)?;
7739        let hw1: u16 = (0xF890 | base_bits) as u16;
7740        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7741        let mut bytes = hw1.to_le_bytes().to_vec();
7742        bytes.extend_from_slice(&hw2.to_le_bytes());
7743        Ok(bytes)
7744    }
7745
7746    /// Encode Thumb-2 32-bit LDRB with register: LDRB.W Rd, [Rn, Rm]
7747    fn encode_thumb32_ldrb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7748        let rd_bits = reg_to_bits(rd);
7749        let base_bits = reg_to_bits(base);
7750        let rm_bits = reg_to_bits(offset_reg);
7751        // LDRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0001 Rn | Rt 0000 00 imm2 Rm
7752        let hw1: u16 = (0xF810 | base_bits) as u16;
7753        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7754        let mut bytes = hw1.to_le_bytes().to_vec();
7755        bytes.extend_from_slice(&hw2.to_le_bytes());
7756        Ok(bytes)
7757    }
7758
7759    /// Encode Thumb-2 32-bit LDRSB with immediate: LDRSB.W Rd, [Rn, #imm12]
7760    fn encode_thumb32_ldrsb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7761        let rd_bits = reg_to_bits(rd);
7762        let base_bits = reg_to_bits(base);
7763        // LDRSB.W Rd, [Rn, #imm12]: 1111 1001 1001 Rn | Rt imm12
7764        check_ldst_imm12(offset)?;
7765        let hw1: u16 = (0xF990 | base_bits) as u16;
7766        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7767        let mut bytes = hw1.to_le_bytes().to_vec();
7768        bytes.extend_from_slice(&hw2.to_le_bytes());
7769        Ok(bytes)
7770    }
7771
7772    /// Encode Thumb-2 32-bit LDRSB with register: LDRSB.W Rd, [Rn, Rm]
7773    fn encode_thumb32_ldrsb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7774        let rd_bits = reg_to_bits(rd);
7775        let base_bits = reg_to_bits(base);
7776        let rm_bits = reg_to_bits(offset_reg);
7777        // LDRSB.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0001 Rn | Rt 0000 00 imm2 Rm
7778        let hw1: u16 = (0xF910 | base_bits) as u16;
7779        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7780        let mut bytes = hw1.to_le_bytes().to_vec();
7781        bytes.extend_from_slice(&hw2.to_le_bytes());
7782        Ok(bytes)
7783    }
7784
7785    /// Encode Thumb-2 32-bit LDRH with immediate: LDRH.W Rd, [Rn, #imm12]
7786    fn encode_thumb32_ldrh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7787        let rd_bits = reg_to_bits(rd);
7788        let base_bits = reg_to_bits(base);
7789        // LDRH.W Rd, [Rn, #imm12]: 1111 1000 1011 Rn | Rt imm12
7790        check_ldst_imm12(offset)?;
7791        let hw1: u16 = (0xF8B0 | base_bits) as u16;
7792        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) 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 LDRH with register: LDRH.W Rd, [Rn, Rm]
7799    fn encode_thumb32_ldrh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7800        let rd_bits = reg_to_bits(rd);
7801        let base_bits = reg_to_bits(base);
7802        let rm_bits = reg_to_bits(offset_reg);
7803        // LDRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0011 Rn | Rt 0000 00 imm2 Rm
7804        let hw1: u16 = (0xF830 | base_bits) as u16;
7805        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7806        let mut bytes = hw1.to_le_bytes().to_vec();
7807        bytes.extend_from_slice(&hw2.to_le_bytes());
7808        Ok(bytes)
7809    }
7810
7811    /// Encode Thumb-2 32-bit LDRSH with immediate: LDRSH.W Rd, [Rn, #imm12]
7812    fn encode_thumb32_ldrsh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7813        let rd_bits = reg_to_bits(rd);
7814        let base_bits = reg_to_bits(base);
7815        // LDRSH.W Rd, [Rn, #imm12]: 1111 1001 1011 Rn | Rt imm12
7816        check_ldst_imm12(offset)?;
7817        let hw1: u16 = (0xF9B0 | base_bits) as u16;
7818        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7819        let mut bytes = hw1.to_le_bytes().to_vec();
7820        bytes.extend_from_slice(&hw2.to_le_bytes());
7821        Ok(bytes)
7822    }
7823
7824    /// Encode Thumb-2 32-bit LDRSH with register: LDRSH.W Rd, [Rn, Rm]
7825    fn encode_thumb32_ldrsh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7826        let rd_bits = reg_to_bits(rd);
7827        let base_bits = reg_to_bits(base);
7828        let rm_bits = reg_to_bits(offset_reg);
7829        // LDRSH.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0011 Rn | Rt 0000 00 imm2 Rm
7830        let hw1: u16 = (0xF930 | base_bits) as u16;
7831        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7832        let mut bytes = hw1.to_le_bytes().to_vec();
7833        bytes.extend_from_slice(&hw2.to_le_bytes());
7834        Ok(bytes)
7835    }
7836
7837    /// Encode Thumb-2 32-bit STRB with immediate: STRB.W Rd, [Rn, #imm12]
7838    fn encode_thumb32_strb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7839        let rd_bits = reg_to_bits(rd);
7840        let base_bits = reg_to_bits(base);
7841        // STRB.W Rd, [Rn, #imm12]: 1111 1000 1000 Rn | Rt imm12
7842        check_ldst_imm12(offset)?;
7843        let hw1: u16 = (0xF880 | base_bits) as u16;
7844        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7845        let mut bytes = hw1.to_le_bytes().to_vec();
7846        bytes.extend_from_slice(&hw2.to_le_bytes());
7847        Ok(bytes)
7848    }
7849
7850    /// Encode Thumb-2 32-bit STRB with register: STRB.W Rd, [Rn, Rm]
7851    fn encode_thumb32_strb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7852        let rd_bits = reg_to_bits(rd);
7853        let base_bits = reg_to_bits(base);
7854        let rm_bits = reg_to_bits(offset_reg);
7855        // STRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0000 Rn | Rt 0000 00 imm2 Rm
7856        let hw1: u16 = (0xF800 | base_bits) as u16;
7857        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7858        let mut bytes = hw1.to_le_bytes().to_vec();
7859        bytes.extend_from_slice(&hw2.to_le_bytes());
7860        Ok(bytes)
7861    }
7862
7863    /// Encode Thumb-2 32-bit STRH with immediate: STRH.W Rd, [Rn, #imm12]
7864    fn encode_thumb32_strh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7865        let rd_bits = reg_to_bits(rd);
7866        let base_bits = reg_to_bits(base);
7867        // STRH.W Rd, [Rn, #imm12]: 1111 1000 1010 Rn | Rt imm12
7868        check_ldst_imm12(offset)?;
7869        let hw1: u16 = (0xF8A0 | base_bits) as u16;
7870        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7871        let mut bytes = hw1.to_le_bytes().to_vec();
7872        bytes.extend_from_slice(&hw2.to_le_bytes());
7873        Ok(bytes)
7874    }
7875
7876    /// Encode Thumb-2 32-bit STRH with register: STRH.W Rd, [Rn, Rm]
7877    fn encode_thumb32_strh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7878        let rd_bits = reg_to_bits(rd);
7879        let base_bits = reg_to_bits(base);
7880        let rm_bits = reg_to_bits(offset_reg);
7881        // STRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0010 Rn | Rt 0000 00 imm2 Rm
7882        let hw1: u16 = (0xF820 | base_bits) as u16;
7883        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7884        let mut bytes = hw1.to_le_bytes().to_vec();
7885        bytes.extend_from_slice(&hw2.to_le_bytes());
7886        Ok(bytes)
7887    }
7888
7889    /// Encode Thumb-2 32-bit ADD with immediate: ADD.W Rd, Rn, #imm
7890    fn encode_thumb32_add_imm(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7891        let rd_bits = reg_to_bits(rd);
7892        let rn_bits = reg_to_bits(rn);
7893
7894        // In-range immediates (<= 0xFFF) delegate to `encode_thumb32_add`,
7895        // which picks the correct form per value:
7896        //   - imm <= 0xFF  -> ADD.W (T3). Its `i:imm3:imm8` field is a
7897        //     ThumbExpandImm MODIFIED immediate — raw == expanded only here.
7898        //   - 0x100..=0xFFF -> ADDW (T4, 0xF200): a PLAIN 12-bit immediate.
7899        //
7900        // #681: this function used to pack the raw value into the T3 field for
7901        // ALL imm <= 0xFFF. ThumbExpandImm(0x200) = 0 and ThumbExpandImm(0x400)
7902        // = 0x8000_0000, so every dynamic-address load/store with a static
7903        // offset in 0x100..=0xFFF silently computed a WRONG address — and in
7904        // --safety-bounds software the guard checked the intended address while
7905        // the access used the mis-encoded one (bounds bypass). Same
7906        // ThumbExpandImm raw-packing class as #253/#255, reached via #382.
7907        if imm <= 0xFFF {
7908            self.encode_thumb32_add(rd, rn, imm)
7909        } else {
7910            // Out-of-range immediate (> 0xFFF): materialize it into a scratch
7911            // register, then ADD.W Rd, Rn, scratch. This is the #180/#185
7912            // "encoder must produce a legal sequence, not assert" class — see #350.
7913            //
7914            // Scratch choice (must NEVER equal Rn, or Rn would be clobbered before
7915            // the ADD reads it):
7916            //   - rd != rn  => use rd itself (rn is untouched, since rd != rn).
7917            //   - rd == rn  => use R12/IP (the reserved encoder scratch). rd/rn are
7918            //                  never R12 (R12 is non-allocatable), so it can't alias.
7919            //
7920            // The materialized value is the same whether or not MOVT is emitted, so
7921            // the byte length depends only on `imm` (and rd==rn) — the size probe and
7922            // the final emit therefore agree (mandatory: the function is encoded twice).
7923            let scratch: u32 = if rd_bits == rn_bits {
7924                12 // R12/IP — in-place add, can't use rd because rd == rn
7925            } else {
7926                rd_bits // rn is preserved because rd != rn
7927            };
7928            // Invariant: the scratch must never alias Rn (would clobber it before
7929            // the ADD reads it). Unreachable in real codegen (rd/rn are never R12,
7930            // which is reserved encoder scratch), but the encoder is also driven by
7931            // the `encoder_no_panic` fuzz harness with ARBITRARY registers — incl.
7932            // rd==rn==R12, which makes scratch (R12) alias Rn. The encoder contract
7933            // (#180/#185) is Ok-or-Err, never a panic, so return a typed error
7934            // instead of asserting. #350 follow-up.
7935            if scratch == rn_bits {
7936                return Err(synth_core::Error::synthesis(format!(
7937                    "ADD #imm: cannot lower #{imm:#x} for Rd==Rn==R12 — no free scratch \
7938                     register (R12 is the reserved encoder scratch and aliases Rn here)"
7939                )));
7940            }
7941
7942            let lo16 = imm & 0xFFFF;
7943            let hi16 = (imm >> 16) & 0xFFFF;
7944
7945            let mut bytes = self.encode_thumb32_movw_raw(scratch, lo16)?;
7946            if hi16 != 0 {
7947                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(scratch, hi16)?);
7948            }
7949            bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rn_bits, scratch)?);
7950            Ok(bytes)
7951        }
7952    }
7953
7954    // === Raw encoding helpers for POPCNT (take register numbers directly) ===
7955
7956    /// Encode Thumb-2 32-bit MOVW (16-bit immediate) - raw version
7957    ///
7958    /// # Contract (Verus-style)
7959    /// ```text
7960    /// requires rd <= 14, imm16 <= 0xFFFF
7961    /// ensures result.len() == 4
7962    /// ```
7963    fn encode_thumb32_movw_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
7964        reg_bits_checked(rd)?;
7965        encoding_contracts::verify_imm16(imm16);
7966        // MOVW Rd, #imm16
7967        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7968        let imm16 = imm16 & 0xFFFF;
7969        let imm4 = (imm16 >> 12) & 0xF;
7970        let i_bit = (imm16 >> 11) & 1;
7971        let imm3 = (imm16 >> 8) & 0x7;
7972        let imm8 = imm16 & 0xFF;
7973
7974        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7975        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
7976
7977        let mut bytes = hw1.to_le_bytes().to_vec();
7978        bytes.extend_from_slice(&hw2.to_le_bytes());
7979        encoding_contracts::verify_thumb32(&bytes);
7980        Ok(bytes)
7981    }
7982
7983    /// Encode Thumb-2 32-bit MOVT (move top 16 bits) - raw version
7984    ///
7985    /// # Contract (Verus-style)
7986    /// ```text
7987    /// requires rd <= 14, imm16 <= 0xFFFF
7988    /// ensures result.len() == 4
7989    /// ```
7990    fn encode_thumb32_movt_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
7991        reg_bits_checked(rd)?;
7992        encoding_contracts::verify_imm16(imm16);
7993        // MOVT Rd, #imm16
7994        // 1111 0 i 10 1 1 0 0 imm4 | 0 imm3 Rd imm8
7995        let imm16 = imm16 & 0xFFFF;
7996        let imm4 = (imm16 >> 12) & 0xF;
7997        let i_bit = (imm16 >> 11) & 1;
7998        let imm3 = (imm16 >> 8) & 0x7;
7999        let imm8 = imm16 & 0xFF;
8000
8001        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
8002        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8003
8004        let mut bytes = hw1.to_le_bytes().to_vec();
8005        bytes.extend_from_slice(&hw2.to_le_bytes());
8006        encoding_contracts::verify_thumb32(&bytes);
8007        Ok(bytes)
8008    }
8009
8010    /// Encode Thumb-2 32-bit LSR (logical shift right) with immediate - raw version
8011    fn encode_thumb32_lsr_raw(&self, rd: u32, rm: u32, shift: u32) -> Result<Vec<u8>> {
8012        // MOV.W Rd, Rm, LSR #imm
8013        // EA4F 0 imm3 Rd imm2 01 Rm
8014        let imm5 = shift & 0x1F;
8015        let imm2 = imm5 & 0x3;
8016        let imm3 = (imm5 >> 2) & 0x7;
8017
8018        let hw1: u16 = 0xEA4F;
8019        let hw2: u16 = ((imm3 << 12) | (rd << 8) | (imm2 << 6) | (0b01 << 4) | rm) as u16;
8020
8021        let mut bytes = hw1.to_le_bytes().to_vec();
8022        bytes.extend_from_slice(&hw2.to_le_bytes());
8023        Ok(bytes)
8024    }
8025
8026    /// Encode Thumb-2 32-bit AND (register) - raw version
8027    fn encode_thumb32_and_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8028        // AND.W Rd, Rn, Rm
8029        // EA00 Rn | 0 Rd 00 00 Rm
8030        let hw1: u16 = (0xEA00 | rn) as u16;
8031        let hw2: u16 = ((rd << 8) | rm) as u16;
8032
8033        let mut bytes = hw1.to_le_bytes().to_vec();
8034        bytes.extend_from_slice(&hw2.to_le_bytes());
8035        Ok(bytes)
8036    }
8037
8038    /// Encode Thumb-2 32-bit AND with immediate - raw version
8039    fn encode_thumb32_and_imm_raw(&self, rd: u32, rn: u32, imm: u32) -> Result<Vec<u8>> {
8040        // AND.W Rd, Rn, #<modified_immediate>
8041        // F0 00 Rn | 0 imm3 Rd imm8
8042        //
8043        // #681 class audit: the field is a ThumbExpandImm modified immediate,
8044        // not a raw value. The only current caller (POPCNT final mask) passes
8045        // 0x3F, which expands to itself — the gate is byte-identical today and
8046        // closes the raw-packing landmine for any future caller.
8047        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
8048            synth_core::Error::synthesis(
8049                "AND immediate is not a valid ThumbExpandImm — materialize into a register",
8050            )
8051        })?;
8052        let i_bit = (field >> 11) & 1;
8053        let imm3 = (field >> 8) & 0x7;
8054        let imm8 = field & 0xFF;
8055
8056        let hw1: u16 = (0xF000 | (i_bit << 10) | rn) as u16;
8057        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8058
8059        let mut bytes = hw1.to_le_bytes().to_vec();
8060        bytes.extend_from_slice(&hw2.to_le_bytes());
8061        Ok(bytes)
8062    }
8063
8064    /// Encode Thumb-2 32-bit SUB (register) - raw version
8065    fn encode_thumb32_sub_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8066        // SUB.W Rd, Rn, Rm
8067        // EBA0 Rn | 0 Rd 00 00 Rm
8068        let hw1: u16 = (0xEBA0 | rn) as u16;
8069        let hw2: u16 = ((rd << 8) | rm) as u16;
8070
8071        let mut bytes = hw1.to_le_bytes().to_vec();
8072        bytes.extend_from_slice(&hw2.to_le_bytes());
8073        Ok(bytes)
8074    }
8075
8076    /// Encode Thumb-2 32-bit ADD (register) - raw version
8077    fn encode_thumb32_add_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8078        // ADD.W Rd, Rn, Rm
8079        // EB00 Rn | 0 Rd 00 00 Rm
8080        let hw1: u16 = (0xEB00 | rn) as u16;
8081        let hw2: u16 = ((rd << 8) | rm) as u16;
8082
8083        let mut bytes = hw1.to_le_bytes().to_vec();
8084        bytes.extend_from_slice(&hw2.to_le_bytes());
8085        Ok(bytes)
8086    }
8087
8088    /// Encode Thumb-2 32-bit ADDS (register, flag-setting) - raw version.
8089    /// Used as the high-register fallback for `ArmOp::Adds` (i64 low-word add)
8090    /// so R8-R11 pair operands don't overflow the 16-bit field — #178/#180.
8091    fn encode_thumb32_adds_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8092        // ADDS.W Rd, Rn, Rm (T3, S=1): EB10 Rn | 0 Rd 00 00 Rm
8093        let hw1: u16 = (0xEB10 | rn) as u16;
8094        let hw2: u16 = ((rd << 8) | rm) as u16;
8095        let mut bytes = hw1.to_le_bytes().to_vec();
8096        bytes.extend_from_slice(&hw2.to_le_bytes());
8097        Ok(bytes)
8098    }
8099
8100    /// Encode Thumb-2 32-bit SUBS (register, flag-setting) - raw version.
8101    /// High-register fallback for `ArmOp::Subs` (i64 low-word subtract) — #178/#180.
8102    fn encode_thumb32_subs_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8103        // SUBS.W Rd, Rn, Rm (T3, S=1): EBB0 Rn | 0 Rd 00 00 Rm
8104        let hw1: u16 = (0xEBB0 | rn) as u16;
8105        let hw2: u16 = ((rd << 8) | rm) as u16;
8106        let mut bytes = hw1.to_le_bytes().to_vec();
8107        bytes.extend_from_slice(&hw2.to_le_bytes());
8108        Ok(bytes)
8109    }
8110
8111    /// Encode a sequence of ARM instructions
8112    pub fn encode_sequence(&self, ops: &[ArmOp]) -> Result<Vec<u8>> {
8113        let mut code = Vec::new();
8114
8115        for op in ops {
8116            let encoded = self.encode(op)?;
8117            code.extend_from_slice(&encoded);
8118        }
8119
8120        Ok(code)
8121    }
8122}
8123
8124/// Convert register to bit encoding (0-15)
8125/// Reverse of the ARMv7-M `ThumbExpandImm`: given a 32-bit immediate, return the
8126/// 12-bit `i:imm3:imm8` field if it is a representable modified immediate, else
8127/// `None` (the caller must materialize the value into a register). This is the
8128/// shared correct path for the data-processing immediate encoders — without it
8129/// they pack raw bits and silently mis-encode any value `> 0xFF` that isn't a
8130/// modified immediate (the silent-miscompile class behind #251/#253/#255).
8131fn try_thumb_expand_imm(value: u32) -> Option<u32> {
8132    // i:imm3 = 0000 → 8-bit value, zero-extended (00000000 00000000 00000000 XY).
8133    if value <= 0xFF {
8134        return Some(value);
8135    }
8136    let b0 = value & 0xFF; // byte 0
8137    let b1 = (value >> 8) & 0xFF; // byte 1
8138    // 0x00XY00XY (i:imm3 = 0001) — XY in bytes 0 and 2
8139    if value == (b0 << 16) | b0 {
8140        return Some(0x100 | b0);
8141    }
8142    // 0xXY00XY00 (i:imm3 = 0010) — XY in bytes 1 and 3
8143    if value == (b1 << 24) | (b1 << 8) {
8144        return Some(0x200 | b1);
8145    }
8146    // 0xXYXYXYXY (i:imm3 = 0011) — XY in all four bytes
8147    if value == (b0 << 24) | (b0 << 16) | (b0 << 8) | b0 {
8148        return Some(0x300 | b0);
8149    }
8150    // An 8-bit value with bit 7 set, rotated right by 8..=31. `rotate_left(rot)`
8151    // undoes the encoded right rotation; if the result is `1bbbbbbb` (0x80..=0xFF)
8152    // the value is representable. imm12[11:7] = rot, imm12[6:0] = low 7 bits.
8153    for rot in 8..=31u32 {
8154        let unrot = value.rotate_left(rot);
8155        if (0x80..=0xFF).contains(&unrot) {
8156            return Some((rot << 7) | (unrot & 0x7F));
8157        }
8158    }
8159    None
8160}
8161
8162/// Guard a Thumb-2 `LDR/STR Rd, [Rn, #imm12]` offset. The imm12 form supports
8163/// `0..=4095`; a larger offset must be materialized into a register by the
8164/// selector (register-offset addressing). Returning `Err` rather than silently
8165/// masking `offset & 0xFFF` closes the wrong-address miscompile class (#259,
8166/// the load/store sibling of #253/#255).
8167fn check_ldst_imm12(offset: u32) -> Result<()> {
8168    if offset > 0xFFF {
8169        Err(synth_core::Error::synthesis(
8170            "load/store immediate offset > 0xFFF (4095) — materialize the offset into a register",
8171        ))
8172    } else {
8173        Ok(())
8174    }
8175}
8176
8177fn reg_to_bits(reg: &Reg) -> u32 {
8178    match reg {
8179        Reg::R0 => 0,
8180        Reg::R1 => 1,
8181        Reg::R2 => 2,
8182        Reg::R3 => 3,
8183        Reg::R4 => 4,
8184        Reg::R5 => 5,
8185        Reg::R6 => 6,
8186        Reg::R7 => 7,
8187        Reg::R8 => 8,
8188        Reg::R9 => 9,
8189        Reg::R10 => 10,
8190        Reg::R11 => 11,
8191        Reg::R12 => 12,
8192        Reg::SP => 13,
8193        Reg::LR => 14,
8194        Reg::PC => 15,
8195    }
8196}
8197
8198// ======================================================================
8199// #610 — i64 fixed-ABI expansion wrappers.
8200//
8201// The hand-written multi-instruction i64 cores (rotl/rotr and the div/rem
8202// shift-subtract loops) compute in FIXED low registers. Before #610 the
8203// div/rem arms ignored their operand fields outright (hardcoded R0:R1 /
8204// R2:R3 in, result to R0:R1) and the rot arms used R3/R4 scratch that
8205// collided with selector-assigned registers — then restored the saved
8206// scratch OVER the result (`POP {R4}` with rd_lo == R4), so the op
8207// returned the caller's stale register: 0 for every input under qemu.
8208//
8209// These wrappers make each core honor its register parameters:
8210//   1. save R0-R3,
8211//   2. marshal the operand registers into the core's fixed input regs via
8212//      the stack (permutation-safe: every source is read before any fixed
8213//      register is written),
8214//   3. run the fixed-reg core (self-preserving for R4+; R12 is encoder
8215//      scratch and never allocatable, #212),
8216//   4. MOV the result pair from R0:R1 into the selector's rd pair,
8217//   5. restore R0-R3, skipping any register the result now occupies.
8218//
8219// All emitted lengths are register-independent so the optimized path's
8220// byte-size estimator (`estimate_arm_byte_size`, pinned by the
8221// estimator↔encoder agreement oracle #498/#511) stays a constant per op.
8222// ======================================================================
8223
8224/// Steps 1+2: `PUSH {R0-R3}`, then marshal `srcs` (operand registers, any of
8225/// R0-R12) into `R0..R<n>` via individual stack pushes. Sources are all read
8226/// before any destination register is written, so arbitrary source/target
8227/// permutations (including operands living in R0-R3) are safe.
8228fn emit_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8229    debug_assert!(srcs.len() <= 4);
8230    // PUSH {R0-R3} — save the caller-visible low registers.
8231    bytes.extend_from_slice(&0xB40Fu16.to_le_bytes());
8232    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8233    for src in srcs.iter().rev() {
8234        let rt = reg_to_bits(src) as u16;
8235        bytes.extend_from_slice(&0xF84Du16.to_le_bytes());
8236        bytes.extend_from_slice(&((rt << 12) | 0x0D04).to_le_bytes());
8237    }
8238    // POP {Ri} — Ri := srcs[i].
8239    for i in 0..srcs.len() as u16 {
8240        bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes());
8241    }
8242}
8243
8244/// Steps 4+5: move the core's R0:R1 result into the selector's rd pair, then
8245/// restore the R0-R3 saved by [`emit_i64_fixed_abi_entry`], skipping any
8246/// register the result now lives in (its saved caller word is discarded).
8247fn emit_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8248    let lo = reg_to_bits(rdlo);
8249    let hi = reg_to_bits(rdhi);
8250    if lo == 1 && hi == 0 {
8251        // A fully swapped pair would clobber one half in either MOV order.
8252        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8253        return Err(synth_core::Error::synthesis(
8254            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8255        ));
8256    }
8257    let mov16 = |bytes: &mut Vec<u8>, rd: u32, rm: u32| {
8258        let d = ((rd >> 3) & 1) as u16;
8259        bytes.extend_from_slice(
8260            &(0x4600u16 | (d << 7) | ((rm as u16) << 3) | ((rd & 7) as u16)).to_le_bytes(),
8261        );
8262    };
8263    if hi == 0 {
8264        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8265        mov16(bytes, lo, 0);
8266        mov16(bytes, hi, 1);
8267    } else {
8268        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8269        mov16(bytes, hi, 1);
8270        mov16(bytes, lo, 0);
8271    }
8272    for i in 0..4u32 {
8273        if i == lo || i == hi {
8274            // The result lives here — drop the saved caller word.
8275            bytes.extend_from_slice(&0xB001u16.to_le_bytes()); // ADD SP, #4
8276        } else {
8277            bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes()); // POP {Ri}
8278        }
8279    }
8280    Ok(())
8281}
8282
8283/// WASM `i64.div_*` / `i64.rem_*` by zero must trap, matching the i32 path's
8284/// cmp/bne/udf guard. Emitted after marshaling, when the divisor pair is in
8285/// R2:R3: `ORRS R12, R2, R3` — `BNE` over a `UDF #0` when nonzero.
8286fn emit_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8287    bytes.extend_from_slice(&0xEA52u16.to_le_bytes()); // ORRS.W R12, R2, R3
8288    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8289    bytes.extend_from_slice(&0xD100u16.to_le_bytes()); // BNE.N +0 (skip the UDF)
8290    bytes.extend_from_slice(&0xDE00u16.to_le_bytes()); // UDF #0 — divide by zero
8291}
8292
8293/// WASM `i64.div_s(INT64_MIN, -1)` must trap (Core §4.3.2 `idiv_s`: the
8294/// quotient +2^63 is unrepresentable), matching the i32 path's overflow
8295/// guard — #633: without it the core negated INT64_MIN onto itself and
8296/// silently returned INT64_MIN. Emitted after marshaling, when the dividend
8297/// pair is in R0:R1 and the divisor pair in R2:R3; R12 is encoder scratch.
8298///
8299/// div_s ONLY — `i64.rem_s(INT64_MIN, -1)` is defined as 0 and must NOT
8300/// trap (`irem_s`), so the I64RemS arm never calls this. 22 bytes,
8301/// register-independent (estimator contract, #498/#511).
8302fn emit_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8303    // AND.W R12, R2, R3 — R12 == 0xFFFFFFFF iff divisor == -1
8304    bytes.extend_from_slice(&0xEA02u16.to_le_bytes());
8305    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8306    // CMN.W R12, #1 — EQ iff both divisor words are all-ones
8307    bytes.extend_from_slice(&0xF11Cu16.to_le_bytes());
8308    bytes.extend_from_slice(&0x0F01u16.to_le_bytes());
8309    // BNE .no_trap
8310    bytes.extend_from_slice(&0xD105u16.to_le_bytes());
8311    // CMP R0, #0 — dividend lo word of INT64_MIN
8312    bytes.extend_from_slice(&0x2800u16.to_le_bytes());
8313    // BNE .no_trap
8314    bytes.extend_from_slice(&0xD103u16.to_le_bytes());
8315    // CMP.W R1, #0x80000000 — dividend hi word of INT64_MIN
8316    bytes.extend_from_slice(&0xF1B1u16.to_le_bytes());
8317    bytes.extend_from_slice(&0x4F00u16.to_le_bytes());
8318    // BNE .no_trap
8319    bytes.extend_from_slice(&0xD100u16.to_le_bytes());
8320    // UDF #0 — signed-division overflow
8321    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
8322    // .no_trap:
8323}
8324
8325// ======================================================================
8326// #615 — A32 (ARM-mode) twins of the #610 i64 fixed-ABI wrappers above.
8327// Identical register contract, A32 encodings: the multi-instruction i64
8328// cores (rotl/rotr, div/rem) compute in fixed low registers (value/dividend
8329// R0:R1, amount R2 / divisor R2:R3, result to R0:R1); the wrappers marshal
8330// the selector-assigned operand registers in and the result out, saving and
8331// restoring the caller-visible R0-R3 around the core.
8332// ======================================================================
8333
8334/// A32 steps 1+2: `STMDB SP!, {R0-R3}`, then marshal `srcs` into `R0..R<n>`
8335/// via individual stack pushes (`STR src, [SP, #-4]!` in reverse order, then
8336/// `LDR Ri, [SP], #4`). Every source is read before any fixed register is
8337/// written, so arbitrary source/target permutations are safe.
8338fn emit_a32_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8339    debug_assert!(srcs.len() <= 4);
8340    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8341    // PUSH {R0-R3} — save the caller-visible low registers.
8342    w(bytes, 0xE92D_000F);
8343    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8344    for src in srcs.iter().rev() {
8345        w(bytes, 0xE52D_0004 | (reg_to_bits(src) << 12));
8346    }
8347    // LDR Ri, [SP], #4 — Ri := srcs[i].
8348    for i in 0..srcs.len() as u32 {
8349        w(bytes, 0xE49D_0004 | (i << 12));
8350    }
8351}
8352
8353/// A32 steps 4+5: move the core's R0:R1 result into the selector's rd pair,
8354/// then restore the R0-R3 saved by [`emit_a32_i64_fixed_abi_entry`], skipping
8355/// any register the result now lives in (its saved caller word is discarded).
8356fn emit_a32_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8357    let lo = reg_to_bits(rdlo);
8358    let hi = reg_to_bits(rdhi);
8359    if lo == 1 && hi == 0 {
8360        // A fully swapped pair would clobber one half in either MOV order.
8361        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8362        return Err(synth_core::Error::synthesis(
8363            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8364        ));
8365    }
8366    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8367    let mov = |bytes: &mut Vec<u8>, rd: u32, rm: u32| w(bytes, 0xE1A0_0000 | (rd << 12) | rm);
8368    if hi == 0 {
8369        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8370        mov(bytes, lo, 0);
8371        mov(bytes, hi, 1);
8372    } else {
8373        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8374        mov(bytes, hi, 1);
8375        mov(bytes, lo, 0);
8376    }
8377    for i in 0..4u32 {
8378        if i == lo || i == hi {
8379            // The result lives here — drop the saved caller word.
8380            w(bytes, 0xE28D_D004); // ADD SP, SP, #4
8381        } else {
8382            w(bytes, 0xE49D_0004 | (i << 12)); // LDR Ri, [SP], #4
8383        }
8384    }
8385    Ok(())
8386}
8387
8388/// A32 zero-divisor trap, emitted after marshaling when the divisor pair is
8389/// in R2:R3: `ORRS R12, R2, R3` sets Z iff the divisor is zero; `BNE` skips a
8390/// `UDF #0` (WASM div/rem-by-zero must trap, matching the Thumb-2 twin).
8391fn emit_a32_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8392    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8393    w(bytes, 0xE192_C003); // ORRS R12, R2, R3
8394    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8395    w(bytes, 0xE7F0_00F0); // UDF #0 — divide by zero
8396}
8397
8398/// A32 twin of [`emit_i64_divs_overflow_trap`] (#633): trap on
8399/// `i64.div_s(INT64_MIN, -1)`. Conditional execution replaces the Thumb
8400/// branches — the CMPEQ chain leaves EQ set only when divisor == -1 AND
8401/// dividend == INT64_MIN. div_s only; rem_s must keep returning 0.
8402fn emit_a32_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8403    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8404    w(bytes, 0xE002_C003); // AND   R12, R2, R3 (== 0xFFFFFFFF iff divisor == -1)
8405    w(bytes, 0xE37C_0001); // CMN   R12, #1     (EQ iff divisor == -1)
8406    w(bytes, 0x0350_0000); // CMPEQ R0, #0      (EQ iff also dividend lo == 0)
8407    w(bytes, 0x0351_0102); // CMPEQ R1, #0x80000000 (EQ iff dividend == INT64_MIN)
8408    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8409    w(bytes, 0xE7F0_00F0); // UDF #0 — signed-division overflow
8410}
8411
8412/// Fallible form of the `verify_reg_bits` contract. PC (R15) is not a valid
8413/// data operand for the Thumb-2 encodings that use this guard (SDIV/UDIV/MLS/…
8414/// are UNPREDICTABLE with PC). Synth's own codegen never emits PC there, but
8415/// the encoder must stay *total* over arbitrary `ArmOp` inputs — the fuzz
8416/// harness (`encoder_no_panic`) requires Ok-or-Err, never a panic. Pre-fix, the
8417/// `debug_assert` in `verify_reg_bits` aborted under `-Cdebug-assertions`.
8418/// Returns a typed Err instead. See #185.
8419fn reg_bits_checked(bits: u32) -> Result<()> {
8420    if bits > 14 {
8421        return Err(synth_core::Error::synthesis(format!(
8422            "register bits {bits} (PC/R15) is not a valid operand for this Thumb-2 encoding"
8423        )));
8424    }
8425    Ok(())
8426}
8427
8428/// Try to encode a 32-bit value as an ARM rotated immediate (imm8 ROR 2*rot4).
8429/// Returns Some((encoded_bits, 1)) if representable, None otherwise.
8430fn try_encode_rotated_imm(val: u32) -> Option<(u32, u32)> {
8431    if val == 0 {
8432        return Some((0, 1));
8433    }
8434    for rot in 0..16u32 {
8435        let shift = rot * 2;
8436        // Rotate left by shift (undo the ROR) to see if result fits in 8 bits
8437        let unrotated = val.rotate_left(shift);
8438        if unrotated <= 0xFF {
8439            // Encoded as: rot4(4 bits) | imm8(8 bits) = rotate_imm << 8 | imm8
8440            return Some(((rot << 8) | unrotated, 1));
8441        }
8442    }
8443    None
8444}
8445
8446/// Encode operand2 field and return (bits, immediate_flag).
8447/// For ARM32 mode, immediates use the rotated-immediate encoding (imm8 ROR 2*rot4).
8448/// Panics if an immediate value cannot be represented. Callers that need large
8449/// immediates should use MOVW/MOVT instead of Operand2::Imm.
8450fn encode_operand2(op2: &Operand2) -> Result<(u32, u32)> {
8451    match op2 {
8452        Operand2::Imm(val) => {
8453            let uval = *val as u32;
8454            // Attempt rotated-immediate encoding (ARM32 Operand2)
8455            if let Some(encoded) = try_encode_rotated_imm(uval) {
8456                Ok(encoded)
8457            } else {
8458                // #378-class honesty: an immediate that can't be expressed as an
8459                // ARM32 rotated immediate is an INTERNAL selector bug — large
8460                // constants must be materialized via MOVW/MOVT, not passed here.
8461                // FAIL HONESTLY with an Err rather than silently masking to
8462                // `uval & 0xFF` and emitting a WRONG immediate. The encoder is
8463                // Ok-or-Err, never corrupt (#180/#185); a loud Err is also why
8464                // this is an Err and not a panic (the `encoder_no_panic` fuzz
8465                // contract — malformed/oversized input must degrade, not crash).
8466                Err(synth_core::Error::synthesis(format!(
8467                    "encode_operand2: immediate {uval:#x} ({val}) is not an ARM32 \
8468                     rotated immediate — the selector must materialize large \
8469                     constants via MOVW/MOVT"
8470                )))
8471            }
8472        }
8473
8474        Operand2::Reg(reg) => {
8475            let reg_bits = reg_to_bits(reg);
8476            Ok((reg_bits, 0)) // I=0 for register
8477        }
8478
8479        Operand2::RegShift {
8480            rm,
8481            shift: _,
8482            amount,
8483        } => {
8484            // Simplified encoding with shift
8485            let rm_bits = reg_to_bits(rm);
8486            let shift_bits = (*amount & 0x1F) << 7;
8487            Ok((shift_bits | rm_bits, 0))
8488        }
8489    }
8490}
8491
8492/// Encode memory address to (base_reg, offset)
8493fn encode_mem_addr(addr: &MemAddr) -> (u32, u32) {
8494    let base_bits = reg_to_bits(&addr.base);
8495    let offset_bits = (addr.offset as u32) & 0xFFF; // 12-bit offset
8496    (base_bits, offset_bits)
8497}
8498
8499/// S-register number: S0=0, S1=1, ..., S31=31
8500fn vfp_sreg_to_num(reg: &VfpReg) -> Result<u32> {
8501    match reg {
8502        VfpReg::S0 => Ok(0),
8503        VfpReg::S1 => Ok(1),
8504        VfpReg::S2 => Ok(2),
8505        VfpReg::S3 => Ok(3),
8506        VfpReg::S4 => Ok(4),
8507        VfpReg::S5 => Ok(5),
8508        VfpReg::S6 => Ok(6),
8509        VfpReg::S7 => Ok(7),
8510        VfpReg::S8 => Ok(8),
8511        VfpReg::S9 => Ok(9),
8512        VfpReg::S10 => Ok(10),
8513        VfpReg::S11 => Ok(11),
8514        VfpReg::S12 => Ok(12),
8515        VfpReg::S13 => Ok(13),
8516        VfpReg::S14 => Ok(14),
8517        VfpReg::S15 => Ok(15),
8518        VfpReg::S16 => Ok(16),
8519        VfpReg::S17 => Ok(17),
8520        VfpReg::S18 => Ok(18),
8521        VfpReg::S19 => Ok(19),
8522        VfpReg::S20 => Ok(20),
8523        VfpReg::S21 => Ok(21),
8524        VfpReg::S22 => Ok(22),
8525        VfpReg::S23 => Ok(23),
8526        VfpReg::S24 => Ok(24),
8527        VfpReg::S25 => Ok(25),
8528        VfpReg::S26 => Ok(26),
8529        VfpReg::S27 => Ok(27),
8530        VfpReg::S28 => Ok(28),
8531        VfpReg::S29 => Ok(29),
8532        VfpReg::S30 => Ok(30),
8533        VfpReg::S31 => Ok(31),
8534        // D-registers are not used in F32 single-precision encodings
8535        _ => Err(synth_core::Error::SynthesisError(
8536            "D-register not supported in single-precision VFP encoding".to_string(),
8537        )),
8538    }
8539}
8540
8541/// D-register number: D0=0, D1=1, ..., D15=15
8542fn vfp_dreg_to_num(reg: &VfpReg) -> Result<u32> {
8543    match reg {
8544        VfpReg::D0 => Ok(0),
8545        VfpReg::D1 => Ok(1),
8546        VfpReg::D2 => Ok(2),
8547        VfpReg::D3 => Ok(3),
8548        VfpReg::D4 => Ok(4),
8549        VfpReg::D5 => Ok(5),
8550        VfpReg::D6 => Ok(6),
8551        VfpReg::D7 => Ok(7),
8552        VfpReg::D8 => Ok(8),
8553        VfpReg::D9 => Ok(9),
8554        VfpReg::D10 => Ok(10),
8555        VfpReg::D11 => Ok(11),
8556        VfpReg::D12 => Ok(12),
8557        VfpReg::D13 => Ok(13),
8558        VfpReg::D14 => Ok(14),
8559        VfpReg::D15 => Ok(15),
8560        // S-registers are not used in F64 double-precision encodings
8561        _ => Err(synth_core::Error::SynthesisError(
8562            "S-register not supported in double-precision VFP encoding".to_string(),
8563        )),
8564    }
8565}
8566
8567/// Split S-register into (Vx[3:0], qualifier_bit) for VFP encoding.
8568/// For an S-register number s: Vx = s >> 1, qualifier = s & 1.
8569/// The qualifier bit goes to D (bit 22), N (bit 7), or M (bit 5) depending on role.
8570fn encode_sreg(s: u32) -> (u32, u32) {
8571    (s >> 1, s & 1)
8572}
8573
8574/// Split D-register into (Vx[3:0], qualifier_bit) for VFP double-precision encoding.
8575/// For a D-register number d: Vx = d & 0xF, qualifier = (d >> 4) & 1.
8576/// For D0-D15, qualifier is always 0.
8577fn encode_dreg(d: u32) -> (u32, u32) {
8578    (d & 0xF, (d >> 4) & 1)
8579}
8580
8581/// Encode a VFP 3-register arithmetic instruction (VADD.F32, VSUB.F32, VMUL.F32, VDIV.F32).
8582/// Returns the full 32-bit instruction word.
8583///
8584/// VFP encoding: [cond 1110] [D opc1 Vn] [Vd 101 sz] [N opc2 M 0 Vm]
8585/// For single-precision (sz=0), coprocessor = 0xA (bits[11:8]).
8586fn encode_vfp_3reg(base: u32, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<u32> {
8587    let sd_num = vfp_sreg_to_num(sd)?;
8588    let sn_num = vfp_sreg_to_num(sn)?;
8589    let sm_num = vfp_sreg_to_num(sm)?;
8590    let (vd, d) = encode_sreg(sd_num);
8591    let (vn, n) = encode_sreg(sn_num);
8592    let (vm, m) = encode_sreg(sm_num);
8593
8594    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8595}
8596
8597/// Encode a VFP 2-register instruction (VNEG.F32, VABS.F32, VSQRT.F32).
8598/// Returns the full 32-bit instruction word.
8599fn encode_vfp_2reg(base: u32, sd: &VfpReg, sm: &VfpReg) -> Result<u32> {
8600    let sd_num = vfp_sreg_to_num(sd)?;
8601    let sm_num = vfp_sreg_to_num(sm)?;
8602    let (vd, d) = encode_sreg(sd_num);
8603    let (vm, m) = encode_sreg(sm_num);
8604
8605    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8606}
8607
8608/// Encode a VFP load/store (VLDR.F32 / VSTR.F32).
8609/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8610/// U bit (bit 23) controls add/subtract offset.
8611fn encode_vfp_ldst(base: u32, sd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8612    let sd_num = vfp_sreg_to_num(sd)?;
8613    let (vd, d) = encode_sreg(sd_num);
8614    let rn = reg_to_bits(&addr.base);
8615
8616    let offset = addr.offset;
8617    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8618    let abs_offset = offset.unsigned_abs();
8619    let imm8 = (abs_offset / 4) & 0xFF;
8620
8621    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8622}
8623
8624/// Encode VMOV between core register and S-register.
8625/// VMOV Sn, Rt: 0xEE00_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8626/// VMOV Rt, Sn: 0xEE10_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8627fn encode_vmov_core_sreg(to_sreg: bool, sreg: &VfpReg, core: &Reg) -> Result<u32> {
8628    let s_num = vfp_sreg_to_num(sreg)?;
8629    let (vn, n) = encode_sreg(s_num);
8630    let rt = reg_to_bits(core);
8631
8632    let base = if to_sreg { 0xEE000A10 } else { 0xEE100A10 };
8633    Ok(base | (vn << 16) | (rt << 12) | (n << 7))
8634}
8635
8636/// Encode a VFP 3-register double-precision instruction (VADD.F64, VSUB.F64, etc.).
8637/// For double-precision (sz=1), coprocessor = 0xB (bits[11:8]).
8638/// The base should have bit 8 = 1 for F64 (0xB suffix instead of 0xA).
8639fn encode_vfp_3reg_f64(base: u32, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<u32> {
8640    let dd_num = vfp_dreg_to_num(dd)?;
8641    let dn_num = vfp_dreg_to_num(dn)?;
8642    let dm_num = vfp_dreg_to_num(dm)?;
8643    let (vd, d) = encode_dreg(dd_num);
8644    let (vn, n) = encode_dreg(dn_num);
8645    let (vm, m) = encode_dreg(dm_num);
8646
8647    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8648}
8649
8650/// Encode a VFP 2-register double-precision instruction (VNEG.F64, VABS.F64, VSQRT.F64).
8651fn encode_vfp_2reg_f64(base: u32, dd: &VfpReg, dm: &VfpReg) -> Result<u32> {
8652    let dd_num = vfp_dreg_to_num(dd)?;
8653    let dm_num = vfp_dreg_to_num(dm)?;
8654    let (vd, d) = encode_dreg(dd_num);
8655    let (vm, m) = encode_dreg(dm_num);
8656
8657    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8658}
8659
8660/// Encode a VFP load/store for double-precision (VLDR.64 / VSTR.64).
8661/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8662fn encode_vfp_ldst_f64(base: u32, dd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8663    let dd_num = vfp_dreg_to_num(dd)?;
8664    let (vd, d) = encode_dreg(dd_num);
8665    let rn = reg_to_bits(&addr.base);
8666
8667    let offset = addr.offset;
8668    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8669    let abs_offset = offset.unsigned_abs();
8670    let imm8 = (abs_offset / 4) & 0xFF;
8671
8672    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8673}
8674
8675/// Encode VMOV between two core registers and a D-register.
8676/// VMOV Dm, Rt, Rt2: 0xEC40_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8677/// VMOV Rt, Rt2, Dm: 0xEC50_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8678fn encode_vmov_core_dreg(
8679    to_dreg: bool,
8680    dreg: &VfpReg,
8681    core_lo: &Reg,
8682    core_hi: &Reg,
8683) -> Result<u32> {
8684    let d_num = vfp_dreg_to_num(dreg)?;
8685    let (vm, m) = encode_dreg(d_num);
8686    let rt = reg_to_bits(core_lo);
8687    let rt2 = reg_to_bits(core_hi);
8688
8689    let base = if to_dreg { 0xEC400B10 } else { 0xEC500B10 };
8690    Ok(base | (rt2 << 16) | (rt << 12) | (m << 5) | vm)
8691}
8692
8693/// Emit a VFP 32-bit instruction as Thumb-2 bytes (two LE halfwords).
8694fn vfp_to_thumb_bytes(instr: u32) -> Vec<u8> {
8695    let hw1 = ((instr >> 16) & 0xFFFF) as u16;
8696    let hw2 = (instr & 0xFFFF) as u16;
8697    let mut bytes = hw1.to_le_bytes().to_vec();
8698    bytes.extend_from_slice(&hw2.to_le_bytes());
8699    bytes
8700}
8701
8702// ============================================================================
8703// Helium MVE encoding helpers
8704// ============================================================================
8705
8706/// Q-register number: Q0=0, Q1=1, ..., Q7=7
8707fn qreg_to_num(reg: &QReg) -> u32 {
8708    match reg {
8709        QReg::Q0 => 0,
8710        QReg::Q1 => 1,
8711        QReg::Q2 => 2,
8712        QReg::Q3 => 3,
8713        QReg::Q4 => 4,
8714        QReg::Q5 => 5,
8715        QReg::Q6 => 6,
8716        QReg::Q7 => 7,
8717    }
8718}
8719
8720/// MVE element size to encoding bits: S8=0b00, S16=0b01, S32=0b10
8721fn mve_size_bits(size: &MveSize) -> u32 {
8722    match size {
8723        MveSize::S8 => 0b00,
8724        MveSize::S16 => 0b01,
8725        MveSize::S32 => 0b10,
8726    }
8727}
8728
8729/// Encode MVE 3-register instruction.
8730/// Q-registers are encoded as D-register pairs: Q0=D0:D1, Q1=D2:D3, etc.
8731/// In NEON/MVE encoding, the Q-register uses D-register number = Qn * 2.
8732fn encode_mve_3reg(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
8733    let d = qreg_to_num(qd) * 2;
8734    let n = qreg_to_num(qn) * 2;
8735    let m = qreg_to_num(qm) * 2;
8736
8737    // Standard NEON/MVE 3-register encoding:
8738    // D bit (bit 22) = Vd[4], Vd[3:0] = bits [15:12]
8739    // N bit (bit 7)  = Vn[4], Vn[3:0] = bits [19:16]
8740    // M bit (bit 5)  = Vm[4], Vm[3:0] = bits [3:0]
8741    let vd = d & 0xF;
8742    let d_bit = (d >> 4) & 1;
8743    let vn = n & 0xF;
8744    let n_bit = (n >> 4) & 1;
8745    let vm = m & 0xF;
8746    let m_bit = (m >> 4) & 1;
8747
8748    base | (d_bit << 22) | (vn << 16) | (vd << 12) | (n_bit << 7) | (m_bit << 5) | vm
8749}
8750
8751/// Encode MVE 3-register bitwise instruction (VAND, VORR, VEOR, VBIC).
8752fn encode_mve_3reg_bitwise(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
8753    encode_mve_3reg(base, qd, qn, qm)
8754}
8755
8756/// Encode MVE VLDRW.32 Qd, [Rn, #offset]
8757/// Format: EC9x xxxx - contiguous load, word-sized elements
8758fn encode_mve_vldrw(qd: &QReg, addr: &MemAddr) -> u32 {
8759    let qd_enc = qreg_to_num(qd) * 2;
8760    let rn = reg_to_bits(&addr.base);
8761    let offset = addr.offset;
8762    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8763    let abs_offset = offset.unsigned_abs();
8764    let imm7 = (abs_offset / 4) & 0x7F; // 7-bit word-aligned offset
8765
8766    // VLDRW.32 Qd, [Rn, #imm]: ED10 xx80 variant
8767    0xED100E80
8768        | (u_bit << 23)
8769        | ((qd_enc >> 4) << 22)
8770        | (rn << 16)
8771        | ((qd_enc & 0xF) << 12)
8772        | (imm7 & 0x7F)
8773}
8774
8775/// Encode MVE VSTRW.32 Qd, [Rn, #offset]
8776fn encode_mve_vstrw(qd: &QReg, addr: &MemAddr) -> u32 {
8777    let qd_enc = qreg_to_num(qd) * 2;
8778    let rn = reg_to_bits(&addr.base);
8779    let offset = addr.offset;
8780    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8781    let abs_offset = offset.unsigned_abs();
8782    let imm7 = (abs_offset / 4) & 0x7F;
8783
8784    0xED000E80
8785        | (u_bit << 23)
8786        | ((qd_enc >> 4) << 22)
8787        | (rn << 16)
8788        | ((qd_enc & 0xF) << 12)
8789        | (imm7 & 0x7F)
8790}
8791
8792impl ArmEncoder {
8793    /// Encode MVE constant load: MOVW+MOVT+VMOV for each 32-bit word, then assemble Q-register
8794    fn encode_thumb_mve_const(&self, qd: &QReg, bytes: &[u8; 16]) -> Result<Vec<u8>> {
8795        let mut result = Vec::new();
8796        let qd_num = qreg_to_num(qd);
8797
8798        // Load each 32-bit word into R12 (temp) then VMOV into S-register
8799        for i in 0..4 {
8800            let word = u32::from_le_bytes([
8801                bytes[i * 4],
8802                bytes[i * 4 + 1],
8803                bytes[i * 4 + 2],
8804                bytes[i * 4 + 3],
8805            ]);
8806            let lo16 = word & 0xFFFF;
8807            let hi16 = (word >> 16) & 0xFFFF;
8808
8809            // MOVW R12, #lo16
8810            result.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
8811            // MOVT R12, #hi16
8812            if hi16 != 0 {
8813                result.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
8814            }
8815
8816            // VMOV Sn, R12 where Sn = Qd*4 + i
8817            let s_num = qd_num * 4 + i as u32;
8818            let (vn, n) = encode_sreg(s_num);
8819            let vmov: u32 = 0xEE000A10 | (vn << 16) | (12 << 12) | (n << 7);
8820            result.extend_from_slice(&vfp_to_thumb_bytes(vmov));
8821        }
8822
8823        Ok(result)
8824    }
8825
8826    /// Encode lane-wise f32 binary operation (VDIV, etc.) via S-register extraction
8827    fn encode_thumb_mve_lane_wise_f32_binop(
8828        &self,
8829        qd: &QReg,
8830        qn: &QReg,
8831        qm: &QReg,
8832        vfp_base: u32,
8833    ) -> Result<Vec<u8>> {
8834        let mut result = Vec::new();
8835        let qd_num = qreg_to_num(qd);
8836        let qn_num = qreg_to_num(qn);
8837        let qm_num = qreg_to_num(qm);
8838
8839        // For each lane 0..3: use S-registers directly (Q aliasing)
8840        for i in 0..4u32 {
8841            let sd = qd_num * 4 + i;
8842            let sn = qn_num * 4 + i;
8843            let sm = qm_num * 4 + i;
8844
8845            let (vd, d) = encode_sreg(sd);
8846            let (vn, n) = encode_sreg(sn);
8847            let (vm, m) = encode_sreg(sm);
8848
8849            let instr = vfp_base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
8850            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
8851        }
8852
8853        Ok(result)
8854    }
8855
8856    /// Encode lane-wise f32 VSQRT via S-register extraction
8857    fn encode_thumb_mve_lane_wise_f32_sqrt(&self, qd: &QReg, qm: &QReg) -> Result<Vec<u8>> {
8858        let mut result = Vec::new();
8859        let qd_num = qreg_to_num(qd);
8860        let qm_num = qreg_to_num(qm);
8861
8862        // VSQRT.F32 base: 0xEEB10AC0
8863        for i in 0..4u32 {
8864            let sd = qd_num * 4 + i;
8865            let sm = qm_num * 4 + i;
8866
8867            let (vd, d) = encode_sreg(sd);
8868            let (vm, m) = encode_sreg(sm);
8869
8870            let instr: u32 = 0xEEB10AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
8871            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
8872        }
8873
8874        Ok(result)
8875    }
8876}
8877
8878#[cfg(test)]
8879mod tests {
8880    use super::*;
8881
8882    #[test]
8883    fn test_encoder_creation() {
8884        let encoder_arm = ArmEncoder::new_arm32();
8885        assert!(!encoder_arm.thumb_mode);
8886
8887        let encoder_thumb = ArmEncoder::new_thumb2();
8888        assert!(encoder_thumb.thumb_mode);
8889    }
8890
8891    /// #204 WAKE-path regression: `SetCond` materialized 0/1 with the 16-bit
8892    /// `MOVS Rd,#imm` (T1), whose Rd field is 3 bits (R0–R7). For a high Rd
8893    /// (R8–R12) `rd_bits << 8` overflows bit 11, flipping the opcode MOVS→CMP
8894    /// (`0x2c00`), so the boolean was never written — gale's `has_waiter` kept a
8895    /// stale value and the binary-sem WAKE dispatch read garbage. High Rd must
8896    /// use the 32-bit `MOV.W` (T2). Verify the bytes, not the IR.
8897    /// #311: the SAME high-Rd MOVS→CMP transmutation as #204, but in the
8898    /// i64 comparison expansions (I64SetCond / I64SetCondZ) — missed by the
8899    /// #204 hardening. With rd=R8 the boolean died in the flags
8900    /// (`ite eq; cmpeq r0,#1; cmpne r0,#0`), so gale's packed-u64 select
8901    /// read a stale register on silicon. High Rd must take MOV.W / CMP.W.
8902    #[test]
8903    fn test_encode_i64setcond_high_reg_uses_mov_w_311() {
8904        use synth_synthesis::{ArmOp, Condition, Reg};
8905        let enc = ArmEncoder::new_thumb2();
8906        let bytes = enc
8907            .encode(&ArmOp::I64SetCond {
8908                rd: Reg::R8,
8909                rn_lo: Reg::R2,
8910                rn_hi: Reg::R3,
8911                rm_lo: Reg::R6,
8912                rm_hi: Reg::R7,
8913                cond: Condition::EQ,
8914            })
8915            .unwrap();
8916        // The 32-bit MOV.W immediate (T2) first halfword is 0xF04F; the
8917        // 16-bit transmuted forms would contain 0x2801/0x2800 (CMP r0,#1/#0).
8918        let halfwords: Vec<u16> = bytes
8919            .chunks(2)
8920            .map(|c| u16::from_le_bytes([c[0], c[1]]))
8921            .collect();
8922        assert!(
8923            halfwords.iter().filter(|&&h| h == 0xF04F).count() == 2,
8924            "high rd must use two MOV.W (T2) encodings, got {halfwords:04x?}"
8925        );
8926        assert!(
8927            !halfwords.contains(&0x2801) && !halfwords.contains(&0x2800),
8928            "no transmuted 16-bit CMP imm: {halfwords:04x?}"
8929        );
8930
8931        let bytes_z = enc
8932            .encode(&ArmOp::I64SetCondZ {
8933                rd: Reg::R8,
8934                rn_lo: Reg::R2,
8935                rn_hi: Reg::R3,
8936            })
8937            .unwrap();
8938        let hw_z: Vec<u16> = bytes_z
8939            .chunks(2)
8940            .map(|c| u16::from_le_bytes([c[0], c[1]]))
8941            .collect();
8942        assert!(
8943            hw_z.iter().filter(|&&h| h == 0xF04F).count() == 2,
8944            "SetCondZ high rd MOV.W: {hw_z:04x?}"
8945        );
8946        // CMP.W rd,#0 (T2) first halfword: 0xF1B0 | rd
8947        assert!(
8948            hw_z.contains(&(0xF1B0 | 8)),
8949            "SetCondZ high rd must use CMP.W: {hw_z:04x?}"
8950        );
8951    }
8952
8953    #[test]
8954    fn test_encode_setcond_high_reg_uses_mov_w_204() {
8955        use synth_synthesis::{ArmOp, Condition, Reg};
8956        let enc = ArmEncoder::new_thumb2();
8957        // R12 (high): must be ITE + MOV.W #1 + MOV.W #0, never a 16-bit MOVS/CMP.
8958        let hi = enc
8959            .encode(&ArmOp::SetCond {
8960                rd: Reg::R12,
8961                cond: Condition::NE,
8962            })
8963            .unwrap();
8964        assert_eq!(hi.len(), 10, "ITE(2) + MOV.W(4) + MOV.W(4): {hi:02x?}");
8965        // both value halfwords are MOV.W (0xF04F) — NOT the corrupt CMP (0x2c..).
8966        assert_eq!(&hi[2..4], &[0x4F, 0xF0], "then = MOV.W: {hi:02x?}");
8967        assert_eq!(&hi[6..8], &[0x4F, 0xF0], "else = MOV.W: {hi:02x?}");
8968        assert_eq!(hi[4] & 0x0F, 0x01, "then imm = #1");
8969        assert_eq!(hi[8] & 0x0F, 0x00, "else imm = #0");
8970        // Low Rd keeps the compact 16-bit MOVS form.
8971        let lo = enc
8972            .encode(&ArmOp::SetCond {
8973                rd: Reg::R0,
8974                cond: Condition::NE,
8975            })
8976            .unwrap();
8977        assert_eq!(lo.len(), 6, "ITE(2) + MOVS(2) + MOVS(2): {lo:02x?}");
8978        assert_eq!(lo[2..4], [0x01, 0x20], "then = MOVS R0,#1");
8979        assert_eq!(lo[4..6], [0x00, 0x20], "else = MOVS R0,#0");
8980    }
8981
8982    /// #209 Opt 1b: UMULL RdLo, RdHi, Rn, Rm encodes correctly on both ISAs.
8983    /// Thumb-2 T1: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm.
8984    /// A32:        cond 0000 1000 RdHi RdLo Rm 1001 Rn.
8985    #[test]
8986    fn test_encode_umull_209b() {
8987        use synth_synthesis::{ArmOp, Reg};
8988        let op = ArmOp::Umull {
8989            rdlo: Reg::R4,
8990            rdhi: Reg::R5,
8991            rn: Reg::R0,
8992            rm: Reg::R3,
8993        };
8994        // Thumb-2: hw1 = 0xFBA0 | 0 = 0xFBA0; hw2 = (4<<12)|(5<<8)|3 = 0x4503.
8995        let t = ArmEncoder::new_thumb2().encode(&op).unwrap();
8996        assert_eq!(
8997            t,
8998            vec![0xA0, 0xFB, 0x03, 0x45],
8999            "umull r4,r5,r0,r3 (T2): {t:02x?}"
9000        );
9001        // A32: 0xE0800090 | (5<<16) | (4<<12) | (3<<8) | 0 = 0xE0854390.
9002        let a = ArmEncoder::new_arm32().encode(&op).unwrap();
9003        assert_eq!(
9004            a,
9005            0xE085_4390u32.to_le_bytes().to_vec(),
9006            "umull (A32): {a:02x?}"
9007        );
9008    }
9009
9010    /// #206 regression: the ARM32 (A32) `Ldr`/`Str` encoders fed `addr` through
9011    /// `encode_mem_addr`, which returns only the 12-bit immediate — so a register
9012    /// offset (`[rn, rm, #off]`) was silently dropped to `[rn, #off]`, sending
9013    /// the access to the wrong runtime address (silent miscompile on the default
9014    /// `--target arm`). A register offset must materialize `ip = rn + rm` and
9015    /// load from `[ip, #off]`. Verify the bytes.
9016    #[test]
9017    fn test_encode_arm32_indexed_load_keeps_index_206() {
9018        use synth_synthesis::{ArmOp, MemAddr, Reg};
9019        let enc = ArmEncoder::new_arm32();
9020        // ldr r0, [r11, r1, #8]  must NOT collapse to a single immediate ldr.
9021        let bytes = enc
9022            .encode(&ArmOp::Ldr {
9023                rd: Reg::R0,
9024                addr: MemAddr::reg_imm(Reg::R11, Reg::R1, 8),
9025            })
9026            .unwrap();
9027        assert_eq!(
9028            bytes.len(),
9029            8,
9030            "expected ADD ip + LDR (2 words): {bytes:02x?}"
9031        );
9032        let add = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9033        let ldr = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9034        // ADD ip, r11, r1  = 0xE08BC001
9035        assert_eq!(add, 0xE08B_C001, "ADD ip,r11,r1: {add:#010x}");
9036        // LDR r0, [ip, #8] = 0xE59C0008
9037        assert_eq!(ldr, 0xE59C_0008, "LDR r0,[ip,#8]: {ldr:#010x}");
9038        // A bare immediate ldr (the bug) would be 0xE59B0008 (base=r11) — reject.
9039        assert_ne!(ldr, 0xE59B_0008, "index must not be dropped");
9040    }
9041
9042    /// #594 regression: `call_indirect` on the A32 path (`--target cortex-r5`)
9043    /// was encoded as a literal NOP (0xE1A00000) — the call never happened and
9044    /// the function silently returned the leftover table-index value. The A32
9045    /// encoder must emit a real dispatch expansion, since #642 guarded by an
9046    /// inline bounds check:
9047    /// `MOVW r12, #size; CMP idx, r12; BLO +1; UDF;
9048    ///  MOV r12, idx, LSL #2; LDR r12, [r11, r12]; BLX r12`.
9049    #[test]
9050    fn test_encode_arm32_call_indirect_is_real_call_594() {
9051        use synth_synthesis::{ArmOp, Reg};
9052        let enc = ArmEncoder::new_arm32();
9053        let bytes = enc
9054            .encode(&ArmOp::CallIndirect {
9055                rd: Reg::R0,
9056                type_idx: 0,
9057                table_index_reg: Reg::R0,
9058                table_size: 4,
9059                table_byte_offset: 0,
9060                null_check: false,
9061                type_check: None,
9062            })
9063            .unwrap();
9064        assert_eq!(
9065            bytes.len(),
9066            28,
9067            "expected MOVW + CMP + BLO + UDF + MOV + LDR + BLX (7 words): {bytes:02x?}"
9068        );
9069        let words: Vec<u32> = bytes
9070            .chunks_exact(4)
9071            .map(|w| u32::from_le_bytes(w.try_into().unwrap()))
9072            .collect();
9073        // #642 bounds guard: MOVW r12, #4; CMP r0, r12; BLO +1; UDF
9074        assert_eq!(words[0], 0xE300_C004, "MOVW r12,#4: {:#010x}", words[0]);
9075        assert_eq!(words[1], 0xE150_000C, "CMP r0,r12: {:#010x}", words[1]);
9076        assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9077        assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9078        // MOV r12, r0, LSL #2 = 0xE1A0C100
9079        assert_eq!(
9080            words[4], 0xE1A0_C100,
9081            "MOV r12,r0,LSL#2: {:#010x}",
9082            words[4]
9083        );
9084        // LDR r12, [r11, r12] = 0xE79BC00C
9085        assert_eq!(
9086            words[5], 0xE79B_C00C,
9087            "LDR r12,[r11,r12]: {:#010x}",
9088            words[5]
9089        );
9090        // BLX r12 = 0xE12FFF3C
9091        assert_eq!(words[6], 0xE12F_FF3C, "BLX r12: {:#010x}", words[6]);
9092        // The bug: a single NOP word. Must never come back.
9093        assert!(
9094            !bytes
9095                .chunks_exact(4)
9096                .any(|w| w == 0xE1A0_0000u32.to_le_bytes()),
9097            "call_indirect must not contain a NOP (#594): {bytes:02x?}"
9098        );
9099
9100        // A non-R0 index register lands in the MOV's Rm and CMP's Rn fields.
9101        let bytes = enc
9102            .encode(&ArmOp::CallIndirect {
9103                rd: Reg::R0,
9104                type_idx: 0,
9105                table_index_reg: Reg::R4,
9106                table_size: 4,
9107                table_byte_offset: 0,
9108                null_check: false,
9109                type_check: None,
9110            })
9111            .unwrap();
9112        let cmp = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9113        assert_eq!(cmp, 0xE154_000C, "CMP r4,r12: {cmp:#010x}");
9114        let mov = u32::from_le_bytes(bytes[16..20].try_into().unwrap());
9115        assert_eq!(mov, 0xE1A0_C104, "MOV r12,r4,LSL#2: {mov:#010x}");
9116    }
9117
9118    /// #642: a table size above 16 bits must not be silently truncated by the
9119    /// MOVW — the A32 guard adds a MOVT for the high half.
9120    #[test]
9121    fn test_encode_arm32_call_indirect_wide_table_size_642() {
9122        use synth_synthesis::{ArmOp, Reg};
9123        let enc = ArmEncoder::new_arm32();
9124        let bytes = enc
9125            .encode(&ArmOp::CallIndirect {
9126                rd: Reg::R0,
9127                type_idx: 0,
9128                table_index_reg: Reg::R0,
9129                table_size: 0x0002_0003,
9130                table_byte_offset: 0,
9131                null_check: false,
9132                type_check: None,
9133            })
9134            .unwrap();
9135        assert_eq!(bytes.len(), 32, "MOVT arm adds one word: {bytes:02x?}");
9136        let movw = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9137        let movt = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9138        assert_eq!(movw, 0xE300_C003, "MOVW r12,#3: {movw:#010x}");
9139        assert_eq!(movt, 0xE340_C002, "MOVT r12,#2: {movt:#010x}");
9140    }
9141
9142    /// #597 anchor (justified correctness RE-PIN of the #594-era freeze): the
9143    /// Thumb-2 `CallIndirect` expansion is `mov.w ip, rm, LSL #2; ldr.w ip,
9144    /// [r11, ip]; blx ip`.
9145    ///
9146    /// The #594 PR froze the then-current bytes `4F EA 20 0C ...` whose first
9147    /// word decodes as `mov.w ip, rm, ASR #32` — the intended `LSL #2` had
9148    /// its shift amount in the TYPE field (bits 5:4) instead of imm2 (bits
9149    /// 7:6), so the index was destroyed and every call_indirect dispatched
9150    /// table entry 0 (shipped miscompile, masked by index-0 probes). #597
9151    /// corrects the encoding; new bytes `4F EA 80 0C ...` were
9152    /// execution-validated under unicorn against the wasmtime oracle on a
9153    /// multi-entry table (indexes 0, 1, 3 —
9154    /// scripts/repro/call_indirect_597_differential.py) before this pin was
9155    /// replaced. Old pin: [4F EA 20 0C, 5B F8 0C C0, E0 47] (ASR #32 — must
9156    /// never come back).
9157    #[test]
9158    fn test_encode_thumb_call_indirect_lsl2_597() {
9159        use synth_synthesis::{ArmOp, Reg};
9160        let enc = ArmEncoder::new_thumb2();
9161        let bytes = enc
9162            .encode(&ArmOp::CallIndirect {
9163                rd: Reg::R0,
9164                type_idx: 0,
9165                table_index_reg: Reg::R0,
9166                table_size: 4,
9167                table_byte_offset: 0,
9168                null_check: false,
9169                type_check: None,
9170            })
9171            .unwrap();
9172        assert_eq!(
9173            bytes,
9174            vec![
9175                // #642 bounds guard: movw ip,#4; cmp r0,ip; blo +1; udf #0
9176                0x40, 0xF2, 0x04, 0x0C, // movw ip, #4
9177                0x60, 0x45, // cmp r0, ip
9178                0x00, 0xD3, // blo .+4 (skip the udf)
9179                0x00, 0xDE, // udf #0 — OOB index trap (WASM §4.4.8)
9180                // #597-pinned dispatch
9181                0x4F, 0xEA, 0x80, 0x0C, // mov.w ip, r0, lsl #2
9182                0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9183                0xE0, 0x47, // blx ip
9184            ],
9185            "Thumb-2 CallIndirect: bounds guard + mov.w/ldr.w/blx dispatch: {bytes:02x?}"
9186        );
9187        // The #597 bug bytes (ASR #32 dispatch first word) must never come back.
9188        assert!(
9189            !bytes.windows(4).any(|w| w == [0x4F, 0xEA, 0x20, 0x0C]),
9190            "mov.w ip, rm, ASR #32 — the #597 type-field bug"
9191        );
9192
9193        // A non-R0 index register lands in the mov.w's Rm field (hw2 bits 3:0)
9194        // and the cmp's Rn field.
9195        let bytes = enc
9196            .encode(&ArmOp::CallIndirect {
9197                rd: Reg::R0,
9198                type_idx: 0,
9199                table_index_reg: Reg::R4,
9200                table_size: 4,
9201                table_byte_offset: 0,
9202                null_check: false,
9203                type_check: None,
9204            })
9205            .unwrap();
9206        assert_eq!(&bytes[4..6], &[0x64, 0x45], "cmp r4, ip: {bytes:02x?}");
9207        assert_eq!(
9208            &bytes[10..14],
9209            &[0x4F, 0xEA, 0x84, 0x0C],
9210            "mov.w ip, r4, LSL #2: {bytes:02x?}"
9211        );
9212    }
9213
9214    /// #642: the Thumb-2 bounds guard for a high-register index (R8 — the top
9215    /// of the allocatable pool) uses the high-reg-capable 16-bit CMP (T2) with
9216    /// the N bit set; a table size above 16 bits adds a MOVT.
9217    #[test]
9218    fn test_encode_thumb_call_indirect_guard_shapes_642() {
9219        use synth_synthesis::{ArmOp, Reg};
9220        let enc = ArmEncoder::new_thumb2();
9221        let bytes = enc
9222            .encode(&ArmOp::CallIndirect {
9223                rd: Reg::R0,
9224                type_idx: 0,
9225                table_index_reg: Reg::R8,
9226                table_size: 3,
9227                table_byte_offset: 0,
9228                null_check: false,
9229                type_check: None,
9230            })
9231            .unwrap();
9232        // cmp r8, ip — T2: 0x4500 | N(1)<<7 | Rm(12)<<3 | Rn(0) = 0x45E0
9233        assert_eq!(&bytes[4..6], &[0xE0, 0x45], "cmp r8, ip: {bytes:02x?}");
9234
9235        let bytes = enc
9236            .encode(&ArmOp::CallIndirect {
9237                rd: Reg::R0,
9238                type_idx: 0,
9239                table_index_reg: Reg::R0,
9240                table_size: 0x0002_0003,
9241                table_byte_offset: 0,
9242                null_check: false,
9243                type_check: None,
9244            })
9245            .unwrap();
9246        // movw ip,#3 then movt ip,#2 — the size must not be truncated.
9247        assert_eq!(
9248            &bytes[0..8],
9249            &[0x40, 0xF2, 0x03, 0x0C, 0xC0, 0xF2, 0x02, 0x0C],
9250            "movw ip,#3; movt ip,#2: {bytes:02x?}"
9251        );
9252    }
9253
9254    /// #650: a non-zero table base offset (table N of the contiguous R11
9255    /// region) routes the Thumb-2 pointer load through
9256    /// `add.w ip, r11, ip; ldr.w ip, [ip, #offset]` — and offset 0 keeps the
9257    /// pre-#650 single-load bytes IDENTICAL (the by-construction pin).
9258    #[test]
9259    fn test_encode_thumb_call_indirect_table_offset_650() {
9260        use synth_synthesis::{ArmOp, Reg};
9261        let enc = ArmEncoder::new_thumb2();
9262        // falcon's fused-component shape: table 0 has 7 entries, so table 1
9263        // sits at byte offset 28.
9264        let bytes = enc
9265            .encode(&ArmOp::CallIndirect {
9266                rd: Reg::R0,
9267                type_idx: 0,
9268                table_index_reg: Reg::R1,
9269                table_size: 41,
9270                table_byte_offset: 28,
9271                null_check: false,
9272                type_check: None,
9273            })
9274            .unwrap();
9275        assert_eq!(
9276            bytes,
9277            vec![
9278                // #642 bounds guard against TABLE 1's OWN size (41)
9279                0x40, 0xF2, 0x29, 0x0C, // movw ip, #41
9280                0x61, 0x45, // cmp r1, ip
9281                0x00, 0xD3, // blo .+4 (skip the udf)
9282                0x00, 0xDE, // udf #0 — OOB trap (WASM §4.4.8)
9283                // dispatch through table 1's base (R11 + 28)
9284                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9285                0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9286                0xDC, 0xF8, 0x1C, 0xC0, // ldr.w ip, [ip, #28]
9287                0xE0, 0x47, // blx ip
9288            ],
9289            "Thumb-2 table-1 dispatch (#650): {bytes:02x?}"
9290        );
9291
9292        // Offset 0 must stay the #597-pinned single-load form (no add.w, no
9293        // imm-form ldr) — single-table byte identity by construction.
9294        let zero = enc
9295            .encode(&ArmOp::CallIndirect {
9296                rd: Reg::R0,
9297                type_idx: 0,
9298                table_index_reg: Reg::R1,
9299                table_size: 41,
9300                table_byte_offset: 0,
9301                null_check: false,
9302                type_check: None,
9303            })
9304            .unwrap();
9305        assert_eq!(
9306            &zero[10..],
9307            &[
9308                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9309                0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9310                0xE0, 0x47, // blx ip
9311            ],
9312            "offset 0 keeps the pre-#650 dispatch bytes: {zero:02x?}"
9313        );
9314    }
9315
9316    /// #650: the A32 twin — `add r12, r11, r12; ldr r12, [r12, #offset]` for
9317    /// a non-zero table base offset; offset 0 keeps the #594/#642 form.
9318    #[test]
9319    fn test_encode_arm32_call_indirect_table_offset_650() {
9320        use synth_synthesis::{ArmOp, Reg};
9321        let enc = ArmEncoder::new_arm32();
9322        let bytes = enc
9323            .encode(&ArmOp::CallIndirect {
9324                rd: Reg::R0,
9325                type_idx: 0,
9326                table_index_reg: Reg::R1,
9327                table_size: 41,
9328                table_byte_offset: 28,
9329                null_check: false,
9330                type_check: None,
9331            })
9332            .unwrap();
9333        let words: Vec<u32> = bytes
9334            .chunks_exact(4)
9335            .map(|w| u32::from_le_bytes(w.try_into().unwrap()))
9336            .collect();
9337        assert_eq!(words[0], 0xE300_C029, "MOVW r12,#41: {:#010x}", words[0]);
9338        assert_eq!(words[1], 0xE151_000C, "CMP r1,r12: {:#010x}", words[1]);
9339        assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9340        assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9341        assert_eq!(
9342            words[4], 0xE1A0_C101,
9343            "MOV r12,r1,LSL#2: {:#010x}",
9344            words[4]
9345        );
9346        assert_eq!(
9347            words[5], 0xE08B_C00C,
9348            "ADD r12,r11,r12 (#650): {:#010x}",
9349            words[5]
9350        );
9351        assert_eq!(
9352            words[6], 0xE59C_C01C,
9353            "LDR r12,[r12,#28] (#650): {:#010x}",
9354            words[6]
9355        );
9356        assert_eq!(words[7], 0xE12F_FF3C, "BLX r12: {:#010x}", words[7]);
9357    }
9358
9359    /// #664: `null_check` inserts a null-funcref trap between the Thumb-2
9360    /// pointer load and the `BLX` (`cmp.w ip, #0; bne .+4; udf #0`) — a
9361    /// zero-linked (uninitialized) slot must TRAP (WASM §4.4.8), never
9362    /// branch to address 0. `null_check: false` keeps the expansion
9363    /// byte-identical to the pre-#664 form (by-construction pin).
9364    #[test]
9365    fn test_encode_thumb_call_indirect_null_check_664() {
9366        use synth_synthesis::{ArmOp, Reg};
9367        let enc = ArmEncoder::new_thumb2();
9368        let op = |null_check| ArmOp::CallIndirect {
9369            rd: Reg::R0,
9370            type_idx: 0,
9371            table_index_reg: Reg::R1,
9372            table_size: 4,
9373            table_byte_offset: 0,
9374            null_check,
9375            type_check: None,
9376        };
9377        let with = enc.encode(&op(true)).unwrap();
9378        let without = enc.encode(&op(false)).unwrap();
9379        // The checked form = the unchecked form with EXACTLY the three-insn
9380        // null check spliced in before the final BLX (byte identity of the
9381        // shared prefix/suffix — nothing else may move).
9382        assert_eq!(
9383            with.len(),
9384            without.len() + 8,
9385            "cmp.w (4) + bne (2) + udf (2): {with:02x?}"
9386        );
9387        let blx_at = without.len() - 2;
9388        assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9389        assert_eq!(
9390            &with[blx_at..],
9391            &[
9392                0xBC, 0xF1, 0x00, 0x0F, // cmp.w ip, #0
9393                0x00, 0xD1, // bne .+4 (skip the udf)
9394                0x00, 0xDE, // udf #0 — null-funcref trap (#664)
9395                0xE0, 0x47, // blx ip
9396            ],
9397            "null check precedes the BLX: {with:02x?}"
9398        );
9399        assert_eq!(&with[with.len() - 2..], &without[blx_at..], "same BLX");
9400    }
9401
9402    /// #664: the A32 twin — `cmp r12, #0; bne .+8; udf` before the `BLX`;
9403    /// `null_check: false` keeps the #594/#642/#650 bytes identical.
9404    #[test]
9405    fn test_encode_arm32_call_indirect_null_check_664() {
9406        use synth_synthesis::{ArmOp, Reg};
9407        let enc = ArmEncoder::new_arm32();
9408        let op = |null_check| ArmOp::CallIndirect {
9409            rd: Reg::R0,
9410            type_idx: 0,
9411            table_index_reg: Reg::R1,
9412            table_size: 4,
9413            table_byte_offset: 0,
9414            null_check,
9415            type_check: None,
9416        };
9417        let with = enc.encode(&op(true)).unwrap();
9418        let without = enc.encode(&op(false)).unwrap();
9419        assert_eq!(with.len(), without.len() + 12, "3 A32 words: {with:02x?}");
9420        let blx_at = without.len() - 4;
9421        assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9422        let words: Vec<u32> = with[blx_at..]
9423            .chunks_exact(4)
9424            .map(|w| u32::from_le_bytes(w.try_into().unwrap()))
9425            .collect();
9426        assert_eq!(words[0], 0xE35C_0000, "CMP r12,#0: {:#010x}", words[0]);
9427        assert_eq!(words[1], 0x1A00_0000, "BNE +1 insn: {:#010x}", words[1]);
9428        assert_eq!(words[2], 0xE7F0_00F0, "UDF (null trap): {:#010x}", words[2]);
9429        assert_eq!(words[3], 0xE12F_FF3C, "BLX r12: {:#010x}", words[3]);
9430    }
9431
9432    /// #676: `type_check` splices the runtime type check — scale the index,
9433    /// load the slot's structural class id from the type-id sidecar
9434    /// (`ldr.w ip, [ip, #type_off]`), compare against the expected class id
9435    /// and trap on mismatch (WASM §4.4.8) — between the bounds guard and
9436    /// the dispatch tail. `type_check: None` keeps the expansion
9437    /// byte-identical to the pre-#676 form (by-construction pin, the same
9438    /// trick as #650 offset-0 / #664 `null_check: false`).
9439    #[test]
9440    fn test_encode_thumb_call_indirect_type_check_676() {
9441        use synth_synthesis::{ArmOp, Reg};
9442        let enc = ArmEncoder::new_thumb2();
9443        let op = |type_check| ArmOp::CallIndirect {
9444            rd: Reg::R0,
9445            type_idx: 1,
9446            table_index_reg: Reg::R1,
9447            table_size: 5,
9448            table_byte_offset: 0,
9449            null_check: false,
9450            type_check,
9451        };
9452        let with = enc.encode(&op(Some((2, 20)))).unwrap();
9453        let without = enc.encode(&op(None)).unwrap();
9454        // The checked form = the unchecked form with EXACTLY the six-insn
9455        // type check spliced in after the bounds guard (byte identity of
9456        // the shared prefix/suffix — nothing else may move).
9457        assert_eq!(
9458            with.len(),
9459            without.len() + 20,
9460            "lsl.w(4)+add.w(4)+ldr.w(4)+cmp.w(4)+beq(2)+udf(2): {with:02x?}"
9461        );
9462        // Bounds guard: movw(4) + cmp(2) + blo(2) + udf(2) = 10 bytes.
9463        let guard_end = 10;
9464        assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
9465        assert_eq!(
9466            &with[guard_end..guard_end + 20],
9467            &[
9468                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9469                0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9470                0xDC, 0xF8, 0x14, 0xC0, // ldr.w ip, [ip, #20] — sidecar slot id
9471                0xBC, 0xF1, 0x02, 0x0F, // cmp.w ip, #2 — expected class id
9472                0x00, 0xD0, // beq .+4 (skip the udf on a match)
9473                0x00, 0xDE, // udf #0 — §4.4.8 type-mismatch trap (#676)
9474            ],
9475            "type check follows the bounds guard: {with:02x?}"
9476        );
9477        assert_eq!(
9478            &with[guard_end + 20..],
9479            &without[guard_end..],
9480            "dispatch tail unchanged (idx*4 recomputed)"
9481        );
9482    }
9483
9484    /// #676: the A32 twin — `mov r12, idx, lsl #2; add r12, r11, r12;
9485    /// ldr r12, [r12, #type_off]; cmp r12, #id; beq .+8; udf` after the
9486    /// bounds guard; `type_check: None` keeps the #594/#642/#650/#664
9487    /// bytes identical.
9488    #[test]
9489    fn test_encode_arm32_call_indirect_type_check_676() {
9490        use synth_synthesis::{ArmOp, Reg};
9491        let enc = ArmEncoder::new_arm32();
9492        let op = |type_check| ArmOp::CallIndirect {
9493            rd: Reg::R0,
9494            type_idx: 1,
9495            table_index_reg: Reg::R1,
9496            table_size: 5,
9497            table_byte_offset: 0,
9498            null_check: false,
9499            type_check,
9500        };
9501        let with = enc.encode(&op(Some((2, 20)))).unwrap();
9502        let without = enc.encode(&op(None)).unwrap();
9503        assert_eq!(with.len(), without.len() + 24, "6 A32 words: {with:02x?}");
9504        // Bounds guard: movw + cmp + blo + udf = 4 words = 16 bytes.
9505        let guard_end = 16;
9506        assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
9507        let words: Vec<u32> = with[guard_end..guard_end + 24]
9508            .chunks_exact(4)
9509            .map(|w| u32::from_le_bytes(w.try_into().unwrap()))
9510            .collect();
9511        assert_eq!(
9512            words[0], 0xE1A0_C101,
9513            "MOV r12,r1,LSL#2: {:#010x}",
9514            words[0]
9515        );
9516        assert_eq!(words[1], 0xE08B_C00C, "ADD r12,r11,r12: {:#010x}", words[1]);
9517        assert_eq!(
9518            words[2], 0xE59C_C014,
9519            "LDR r12,[r12,#20] (sidecar): {:#010x}",
9520            words[2]
9521        );
9522        assert_eq!(
9523            words[3], 0xE35C_0002,
9524            "CMP r12,#2 (expected class id): {:#010x}",
9525            words[3]
9526        );
9527        assert_eq!(words[4], 0x0A00_0000, "BEQ +1 insn: {:#010x}", words[4]);
9528        assert_eq!(
9529            words[5], 0xE7F0_00F0,
9530            "UDF (type-mismatch trap): {:#010x}",
9531            words[5]
9532        );
9533        assert_eq!(
9534            &with[guard_end + 24..],
9535            &without[guard_end..],
9536            "dispatch tail unchanged"
9537        );
9538    }
9539
9540    /// #178/#180 regression: the Thumb `Add`/`Adds`/`Subs` reg-forms used the
9541    /// 16-bit encoding unconditionally. For high registers (R12 base scratch,
9542    /// R8-R11 i64 pairs) the 3-bit register fields overflow and corrupt the
9543    /// operands — `add ip,ip,r0` came out as `adds r4,r5,r1` (0x186C), silently
9544    /// dropping the address operand and miscompiling every optimized memory
9545    /// access. High registers must use the 32-bit `.W` forms.
9546    #[test]
9547    fn test_encode_thumb_add_high_reg_uses_add_w_178_180() {
9548        let encoder = ArmEncoder::new_thumb2();
9549
9550        // add ip, ip, r0  — the exact MemLoad/MemStore base+addr op.
9551        let code = encoder
9552            .encode(&ArmOp::Add {
9553                rd: Reg::R12,
9554                rn: Reg::R12,
9555                op2: Operand2::Reg(Reg::R0),
9556            })
9557            .unwrap();
9558        // ADD.W ip, ip, r0 = EB0C 0C00 (little-endian halfwords).
9559        assert_eq!(
9560            code,
9561            vec![0x0C, 0xEB, 0x00, 0x0C],
9562            "high-reg Thumb ADD must be 32-bit ADD.W (EB0C 0C00), not corrupt 16-bit; got {code:02X?}"
9563        );
9564        // Must NOT be the buggy 16-bit 0x186C (`adds r4,r5,r1`).
9565        assert_ne!(code, vec![0x6C, 0x18], "regressed to corrupt 16-bit ADDS");
9566
9567        // Low-register add stays 16-bit (no regression for the common case).
9568        let lo = encoder
9569            .encode(&ArmOp::Add {
9570                rd: Reg::R1,
9571                rn: Reg::R2,
9572                op2: Operand2::Reg(Reg::R3),
9573            })
9574            .unwrap();
9575        assert_eq!(
9576            lo.len(),
9577            2,
9578            "low-reg ADD should remain 16-bit, got {lo:02X?}"
9579        );
9580    }
9581
9582    /// #178/#180 sibling: i64 low-word `Adds`/`Subs` can land in R8-R11 pairs;
9583    /// those must fall back to 32-bit ADDS.W/SUBS.W (flag-setting preserved).
9584    #[test]
9585    fn test_encode_thumb_adds_subs_high_reg_use_32bit_178_180() {
9586        let encoder = ArmEncoder::new_thumb2();
9587
9588        // adds r10, r10, r8  → ADDS.W = EB1A 0A08
9589        let adds = encoder
9590            .encode(&ArmOp::Adds {
9591                rd: Reg::R10,
9592                rn: Reg::R10,
9593                op2: Operand2::Reg(Reg::R8),
9594            })
9595            .unwrap();
9596        assert_eq!(
9597            adds,
9598            vec![0x1A, 0xEB, 0x08, 0x0A],
9599            "high-reg ADDS must be 32-bit ADDS.W (EB1A 0A08); got {adds:02X?}"
9600        );
9601
9602        // subs r10, r10, r8  → SUBS.W = EBBA 0A08
9603        let subs = encoder
9604            .encode(&ArmOp::Subs {
9605                rd: Reg::R10,
9606                rn: Reg::R10,
9607                op2: Operand2::Reg(Reg::R8),
9608            })
9609            .unwrap();
9610        assert_eq!(
9611            subs,
9612            vec![0xBA, 0xEB, 0x08, 0x0A],
9613            "high-reg SUBS must be 32-bit SUBS.W (EBBA 0A08); got {subs:02X?}"
9614        );
9615    }
9616
9617    /// #184 (sibling of #180): 16-bit CMN (T1) only encodes R0-R7. High registers
9618    /// must use 32-bit CMN.W, not the corrupt truncated 16-bit form.
9619    #[test]
9620    fn test_encode_thumb_cmn_high_reg_uses_cmn_w_184() {
9621        let encoder = ArmEncoder::new_thumb2();
9622
9623        // cmn r10, r8  → CMN.W = EB1A 0F08 (ADD.W S=1, Rd=PC discarded).
9624        let cmn = encoder
9625            .encode(&ArmOp::Cmn {
9626                rn: Reg::R10,
9627                op2: Operand2::Reg(Reg::R8),
9628            })
9629            .unwrap();
9630        assert_eq!(
9631            cmn,
9632            vec![0x1A, 0xEB, 0x08, 0x0F],
9633            "high-reg CMN must be 32-bit CMN.W (EB1A 0F08); got {cmn:02X?}"
9634        );
9635
9636        // Low registers stay 16-bit: cmn r1, r2 = 0x42D1.
9637        let lo = encoder
9638            .encode(&ArmOp::Cmn {
9639                rn: Reg::R1,
9640                op2: Operand2::Reg(Reg::R2),
9641            })
9642            .unwrap();
9643        assert_eq!(
9644            lo.len(),
9645            2,
9646            "low-reg CMN should remain 16-bit, got {lo:02X?}"
9647        );
9648        assert_eq!(lo, vec![0xD1, 0x42], "low-reg CMN bytes wrong: {lo:02X?}");
9649    }
9650
9651    /// #185 regression: feeding PC (R15) as a data operand to a Thumb-2 op that
9652    /// guards its registers must return Err, not panic under debug-assertions.
9653    /// (Synth never emits PC here; the fuzz harness requires encode() be total.)
9654    #[test]
9655    fn test_encode_pc_operand_returns_err_not_panic_185() {
9656        let encoder = ArmEncoder::new_thumb2();
9657        for op in [
9658            ArmOp::Sdiv {
9659                rd: Reg::PC,
9660                rn: Reg::R0,
9661                rm: Reg::R1,
9662            },
9663            ArmOp::Udiv {
9664                rd: Reg::R0,
9665                rn: Reg::PC,
9666                rm: Reg::R1,
9667            },
9668            ArmOp::Sdiv {
9669                rd: Reg::R0,
9670                rn: Reg::R1,
9671                rm: Reg::PC,
9672            },
9673        ] {
9674            let r = encoder.encode(&op);
9675            assert!(
9676                r.is_err(),
9677                "encode({op:?}) must return Err for a PC operand, got {r:?}"
9678            );
9679        }
9680        // Valid registers still encode fine (no false rejection).
9681        assert!(
9682            encoder
9683                .encode(&ArmOp::Sdiv {
9684                    rd: Reg::R0,
9685                    rn: Reg::R1,
9686                    rm: Reg::R2
9687                })
9688                .is_ok()
9689        );
9690    }
9691
9692    #[test]
9693    fn test_encode_nop_arm32() {
9694        let encoder = ArmEncoder::new_arm32();
9695        let code = encoder.encode(&ArmOp::Nop).unwrap();
9696
9697        assert_eq!(code.len(), 4); // ARM32 instructions are 4 bytes
9698        assert_eq!(code, vec![0x00, 0x00, 0xA0, 0xE1]); // MOV R0, R0
9699    }
9700
9701    #[test]
9702    fn test_encode_nop_thumb() {
9703        let encoder = ArmEncoder::new_thumb2();
9704        let code = encoder.encode(&ArmOp::Nop).unwrap();
9705
9706        assert_eq!(code.len(), 2); // Thumb instructions are 2 bytes
9707        assert_eq!(code, vec![0x00, 0xBF]); // NOP
9708    }
9709
9710    #[test]
9711    fn test_encode_mov_immediate_arm32() {
9712        let encoder = ArmEncoder::new_arm32();
9713        let op = ArmOp::Mov {
9714            rd: Reg::R0,
9715            op2: Operand2::Imm(42),
9716        };
9717
9718        let code = encoder.encode(&op).unwrap();
9719        assert_eq!(code.len(), 4);
9720
9721        // Verify it's a MOV instruction (bits should have immediate flag set)
9722        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9723        assert_eq!(instr & 0x0E000000, 0x02000000); // Check I bit is set
9724    }
9725
9726    #[test]
9727    fn test_encode_add_registers_arm32() {
9728        let encoder = ArmEncoder::new_arm32();
9729        let op = ArmOp::Add {
9730            rd: Reg::R0,
9731            rn: Reg::R1,
9732            op2: Operand2::Reg(Reg::R2),
9733        };
9734
9735        let code = encoder.encode(&op).unwrap();
9736        assert_eq!(code.len(), 4);
9737
9738        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9739        // Verify it's an ADD instruction with correct opcode
9740        assert_eq!(instr & 0x0FE00000, 0x00800000);
9741    }
9742
9743    /// #350 — `encode_thumb32_add_imm` must lower an out-of-range immediate
9744    /// (> 0xFFF) to a legal MOVW(/MOVT) + ADD.W-register sequence instead of
9745    /// erroring. The small-imm fast path (imm <= 0xFFF) stays byte-identical.
9746    #[test]
9747    fn test_encode_add_imm_large_350() {
9748        let enc = ArmEncoder::new_thumb2();
9749
9750        // --- Fast path: imm <= 0xFFF is a single 4-byte instruction, and the
9751        // VALUE must be right (#681: this test used to assert only the length,
9752        // letting the raw-packed T3 mis-encoding of 0x123 pass CI). 0x123 is
9753        // not ThumbExpandImm-representable, so it must be ADDW (T4, plain
9754        // imm12): clang `addw r0, r1, #0x123` = f201 0023.
9755        let small = enc
9756            .encode_thumb32_add_imm(&Reg::R0, &Reg::R1, 0x123)
9757            .unwrap();
9758        assert_eq!(small, vec![0x01, 0xF2, 0x23, 0x10], "ADDW r0, r1, #0x123");
9759
9760        // helper: decode a Thumb-2 MOVW/MOVT halfword pair back to its imm16
9761        fn movx_imm16(b: &[u8]) -> u32 {
9762            let hw1 = u16::from_le_bytes([b[0], b[1]]) as u32;
9763            let hw2 = u16::from_le_bytes([b[2], b[3]]) as u32;
9764            let imm4 = hw1 & 0xF;
9765            let i = (hw1 >> 10) & 1;
9766            let imm3 = (hw2 >> 12) & 0x7;
9767            let imm8 = hw2 & 0xFF;
9768            (imm4 << 12) | (i << 11) | (imm3 << 8) | imm8
9769        }
9770        fn movx_rd(b: &[u8]) -> u32 {
9771            (u16::from_le_bytes([b[2], b[3]]) as u32 >> 8) & 0xF
9772        }
9773
9774        // --- rd != rn: scratch is rd. imm = 70000 = 0x11170 needs MOVW+MOVT. ---
9775        // 0x11170: lo16 = 0x1170, hi16 = 0x0001
9776        let seq = enc
9777            .encode_thumb32_add_imm(&Reg::R12, &Reg::R0, 70000)
9778            .unwrap();
9779        assert_eq!(seq.len(), 12, "MOVW + MOVT + ADD = 12 bytes");
9780        // MOVW r12, #0x1170
9781        assert_eq!(u16::from_le_bytes([seq[0], seq[1]]) & 0xFBF0, 0xF240);
9782        assert_eq!(movx_rd(&seq[0..4]), 12);
9783        assert_eq!(movx_imm16(&seq[0..4]), 0x1170);
9784        // MOVT r12, #0x0001
9785        assert_eq!(u16::from_le_bytes([seq[4], seq[5]]) & 0xFBF0, 0xF2C0);
9786        assert_eq!(movx_rd(&seq[4..8]), 12);
9787        assert_eq!(movx_imm16(&seq[4..8]), 0x0001);
9788        // ADD.W r12, r0, r12  (EB00 | rn=0 ; rd=12, rm=12)
9789        let add1 = u16::from_le_bytes([seq[8], seq[9]]) as u32;
9790        let add2 = u16::from_le_bytes([seq[10], seq[11]]) as u32;
9791        assert_eq!(add1 & 0xFFF0, 0xEB00);
9792        assert_eq!(add1 & 0xF, 0); // rn = r0
9793        assert_eq!((add2 >> 8) & 0xF, 12); // rd = r12
9794        assert_eq!(add2 & 0xF, 12); // rm = scratch = r12
9795        // The materialized scratch must reconstruct exactly 70000.
9796        assert_eq!(
9797            (movx_imm16(&seq[4..8]) << 16) | movx_imm16(&seq[0..4]),
9798            70000
9799        );
9800
9801        // --- imm <= 0xFFFF: MOVT is skipped (MOVW + ADD = 8 bytes). ---
9802        let seq16 = enc
9803            .encode_thumb32_add_imm(&Reg::R3, &Reg::R0, 0xABCD)
9804            .unwrap();
9805        assert_eq!(seq16.len(), 8, "imm <= 0xFFFF skips MOVT");
9806        assert_eq!(movx_imm16(&seq16[0..4]), 0xABCD);
9807        assert_eq!(movx_rd(&seq16[0..4]), 3); // scratch = rd = r3
9808
9809        // --- rd == rn (in-place add): scratch must be R12, not rd. ---
9810        // imm = 0x12345: lo16 = 0x2345, hi16 = 0x0001
9811        let inplace = enc
9812            .encode_thumb32_add_imm(&Reg::R5, &Reg::R5, 0x12345)
9813            .unwrap();
9814        assert_eq!(inplace.len(), 12);
9815        assert_eq!(movx_rd(&inplace[0..4]), 12, "rd==rn must use R12 scratch");
9816        assert_eq!(
9817            (movx_imm16(&inplace[4..8]) << 16) | movx_imm16(&inplace[0..4]),
9818            0x12345
9819        );
9820        // ADD.W r5, r5, r12 — rm must be the scratch (12), never rn.
9821        let ip_add2 = u16::from_le_bytes([inplace[10], inplace[11]]) as u32;
9822        assert_eq!(ip_add2 & 0xF, 12);
9823        assert_eq!((ip_add2 >> 8) & 0xF, 5);
9824    }
9825
9826    /// #681 — `encode_thumb32_add_imm` packed a RAW immediate into the T3
9827    /// ADD.W `i:imm3:imm8` field, which is a ThumbExpandImm MODIFIED immediate:
9828    /// ThumbExpandImm(0x200) = 0, ThumbExpandImm(0x400) = 0x8000_0000. Every
9829    /// dynamic-address load/store with a static offset in 0x100..=0xFFF
9830    /// computed a wrong address (and bypassed --safety-bounds software: the
9831    /// guard checked the intended address, the access used the mis-encoded
9832    /// one). Fix: imm <= 0xFF keeps T3 (raw == expanded there, bit-identical);
9833    /// 0x100..=0xFFF uses ADDW (T4, plain imm12) — same lowering
9834    /// `encode_thumb32_add` already uses per #253.
9835    ///
9836    /// Every expected byte sequence below is pinned against clang
9837    /// (`-target thumbv7m-none-eabi`) output, bit-for-bit (#544 pattern).
9838    #[test]
9839    fn test_encode_add_imm_thumb_expand_681() {
9840        let enc = ArmEncoder::new_thumb2();
9841        let add = |rd: &Reg, rn: &Reg, imm: u32| enc.encode_thumb32_add_imm(rd, rn, imm).unwrap();
9842
9843        // imm <= 0xFF stays T3 ADD.W (raw == ThumbExpandImm-expanded):
9844        // clang: add.w r12, r0, #0xff  = f100 0cff
9845        assert_eq!(add(&Reg::R12, &Reg::R0, 0xFF), vec![0x00, 0xF1, 0xFF, 0x0C]);
9846
9847        // 0x100..=0xFFF must be ADDW (T4, plain imm12). The old T3 raw packing
9848        // decoded as +0 (0x100/0x200), +0x80000000 (0x400), etc.
9849        // clang: addw r12, r0, #0x100 = f200 1c00
9850        assert_eq!(
9851            add(&Reg::R12, &Reg::R0, 0x100),
9852            vec![0x00, 0xF2, 0x00, 0x1C]
9853        );
9854        // clang: addw r12, r0, #0x104 = f200 1c04
9855        assert_eq!(
9856            add(&Reg::R12, &Reg::R0, 0x104),
9857            vec![0x00, 0xF2, 0x04, 0x1C]
9858        );
9859        // clang: addw r12, r0, #0x200 = f200 2c00
9860        assert_eq!(
9861            add(&Reg::R12, &Reg::R0, 0x200),
9862            vec![0x00, 0xF2, 0x00, 0x2C]
9863        );
9864        // clang: addw r12, r0, #0x3fc = f200 3cfc
9865        assert_eq!(
9866            add(&Reg::R12, &Reg::R0, 0x3FC),
9867            vec![0x00, 0xF2, 0xFC, 0x3C]
9868        );
9869        // clang: addw r12, r0, #0x400 = f200 4c00
9870        assert_eq!(
9871            add(&Reg::R12, &Reg::R0, 0x400),
9872            vec![0x00, 0xF2, 0x00, 0x4C]
9873        );
9874        // clang: addw r12, r0, #0xfff = f600 7cff
9875        assert_eq!(
9876            add(&Reg::R12, &Reg::R0, 0xFFF),
9877            vec![0x00, 0xF6, 0xFF, 0x7C]
9878        );
9879        // Non-scratch rd/rn — clang: addw r1, r2, #0x104 = f202 1104
9880        assert_eq!(add(&Reg::R1, &Reg::R2, 0x104), vec![0x02, 0xF2, 0x04, 0x11]);
9881    }
9882
9883    /// #681 class audit — the T2 RSB and AND.W immediate fields are also
9884    /// ThumbExpandImm-coded and were raw-packed. Neither has a plain-imm12
9885    /// (T4-style) form, so a non-representable immediate must Err loudly
9886    /// (#253/#255/#378 class: never silently encode a different constant).
9887    /// Existing emitters only use representable values (RSB #32, AND #0x3F),
9888    /// pinned here bit-for-bit against clang.
9889    #[test]
9890    fn test_rsb_and_imm_thumb_expand_gate_681() {
9891        let enc = ArmEncoder::new_thumb2();
9892
9893        // clang: rsb.w r3, r2, #0x20 = f1c2 0320 — byte-identical to before.
9894        let rsb = enc
9895            .encode(&ArmOp::Rsb {
9896                rd: Reg::R3,
9897                rn: Reg::R2,
9898                imm: 32,
9899            })
9900            .unwrap();
9901        assert_eq!(rsb, vec![0xC2, 0xF1, 0x20, 0x03]);
9902
9903        // 0x101 is not ThumbExpandImm-representable -> must Err, not mis-encode.
9904        assert!(
9905            enc.encode(&ArmOp::Rsb {
9906                rd: Reg::R3,
9907                rn: Reg::R2,
9908                imm: 0x101,
9909            })
9910            .is_err(),
9911            "non-ThumbExpandImm RSB immediate must Err"
9912        );
9913
9914        // clang: and r4, r4, #0x3f = f004 043f — byte-identical to before.
9915        let and = enc.encode_thumb32_and_imm_raw(4, 4, 0x3F).unwrap();
9916        assert_eq!(and, vec![0x04, 0xF0, 0x3F, 0x04]);
9917        assert!(
9918            enc.encode_thumb32_and_imm_raw(4, 4, 0x101).is_err(),
9919            "non-ThumbExpandImm AND immediate must Err"
9920        );
9921
9922        // A32 RSB: imm12 is a rotate:imm8 modified immediate; > 0xFF used to be
9923        // silently masked to `imm & 0xFF` (#378 masking class) -> must Err.
9924        let a32 = ArmEncoder::new_arm32();
9925        assert!(
9926            a32.encode(&ArmOp::Rsb {
9927                rd: Reg::R3,
9928                rn: Reg::R2,
9929                imm: 0x120,
9930            })
9931            .is_err(),
9932            "A32 RSB immediate > 0xFF must Err, not mask"
9933        );
9934        // imm 32 (the only value real codegen emits) still encodes.
9935        assert!(
9936            a32.encode(&ArmOp::Rsb {
9937                rd: Reg::R3,
9938                rn: Reg::R2,
9939                imm: 32,
9940            })
9941            .is_ok()
9942        );
9943    }
9944
9945    /// #350 follow-up — the `encoder_no_panic` fuzz harness drives the encoder
9946    /// with ARBITRARY registers, including the one case the in-place lowering
9947    /// cannot serve: rd==rn==R12. There the scratch (R12, the reserved encoder
9948    /// register) would alias Rn and clobber it before the ADD reads it. The
9949    /// encoder contract (#180/#185) is Ok-or-Err, never a panic — so this must
9950    /// return Err, not assert. (Real codegen never emits rd==rn==R12 because R12
9951    /// is non-allocatable; this guards only the fuzz/adversarial path.)
9952    #[test]
9953    fn test_encode_add_imm_large_rd_rn_r12_errs_not_panics_350() {
9954        let enc = ArmEncoder::new_thumb2();
9955        // Out-of-range imm with rd==rn==R12: no free scratch -> Err.
9956        let r = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 70000);
9957        assert!(
9958            r.is_err(),
9959            "rd==rn==R12 with out-of-range imm must Err (no free scratch), got {r:?}"
9960        );
9961        // Small imm with rd==rn==R12 still takes the single-instruction fast path
9962        // (no scratch needed) and must succeed — the guard is scoped to the
9963        // out-of-range lowering only.
9964        let small = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 0x10);
9965        assert!(small.is_ok(), "small imm needs no scratch, must stay Ok");
9966    }
9967
9968    /// #378 — `encode_operand2` (ARM32 data-processing operand) must FAIL
9969    /// HONESTLY on an immediate that is not a valid rotated immediate, rather
9970    /// than silently masking it to `imm & 0xFF` and emitting a WRONG
9971    /// instruction. `0x1FF` has 9 set bits, so it cannot come from rotating an
9972    /// 8-bit imm8 — non-encodable. Real codegen materializes large constants via
9973    /// MOVW/MOVT; this guards the encoder's Ok-or-Err contract (#180/#185)
9974    /// directly. It is an Err (not a panic) so the `encoder_no_panic` fuzz
9975    /// harness — which drives arbitrary operands — still passes.
9976    #[test]
9977    fn test_encode_operand2_non_rotatable_imm_errs_not_masks_378() {
9978        let enc = ArmEncoder::new_arm32();
9979        let bad = enc.encode(&ArmOp::Add {
9980            rd: Reg::R0,
9981            rn: Reg::R1,
9982            op2: Operand2::Imm(0x1FF),
9983        });
9984        assert!(
9985            bad.is_err(),
9986            "non-rotatable ARM32 immediate 0x1FF must Err (was silently masked \
9987             to 0xFF), got {bad:?}"
9988        );
9989        // A representable rotated immediate still encodes fine (regression guard).
9990        let ok = enc.encode(&ArmOp::Add {
9991            rd: Reg::R0,
9992            rn: Reg::R1,
9993            op2: Operand2::Imm(0xFF),
9994        });
9995        assert!(
9996            ok.is_ok(),
9997            "0xFF is a valid rotated immediate, must stay Ok"
9998        );
9999    }
10000
10001    #[test]
10002    fn test_encode_ldr_arm32() {
10003        let encoder = ArmEncoder::new_arm32();
10004        let op = ArmOp::Ldr {
10005            rd: Reg::R0,
10006            addr: MemAddr::imm(Reg::R1, 4),
10007        };
10008
10009        let code = encoder.encode(&op).unwrap();
10010        assert_eq!(code.len(), 4);
10011
10012        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10013        // Verify load bit is set
10014        assert_eq!(instr & 0x00100000, 0x00100000);
10015    }
10016
10017    #[test]
10018    fn test_encode_str_arm32() {
10019        let encoder = ArmEncoder::new_arm32();
10020        let op = ArmOp::Str {
10021            rd: Reg::R0,
10022            addr: MemAddr::imm(Reg::SP, 0),
10023        };
10024
10025        let code = encoder.encode(&op).unwrap();
10026        assert_eq!(code.len(), 4);
10027    }
10028
10029    #[test]
10030    fn test_encode_branch_arm32() {
10031        let encoder = ArmEncoder::new_arm32();
10032        let op = ArmOp::Bl {
10033            label: "main".to_string(),
10034        };
10035
10036        let code = encoder.encode(&op).unwrap();
10037        assert_eq!(code.len(), 4);
10038
10039        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10040        // Verify BL opcode
10041        assert_eq!(instr & 0x0F000000, 0x0B000000);
10042    }
10043
10044    /// Regression test for #167 + #174: the Thumb-2 BL relocatable placeholder
10045    /// must carry a -4 addend so an R_ARM_THM_CALL nets to exactly the symbol S.
10046    /// The correct encoding is what `gas` emits for `bl <extern>`: f7ff fffe
10047    /// (hw1=0xF7FF, hw2=0xFFFE), little-endian bytes FF F7 FE FF.
10048    ///   - 0xD000 (J1=J2=0) → ~+0x600000 garbage addend: `bl c0000c` / truncated
10049    ///     to fit (#167).
10050    ///   - 0xF800 (addend 0) → lands at S+4, one instruction past the callee
10051    ///     entry (#174).
10052    ///   - 0xFFFE (addend -4) → lands at S. Correct.
10053    #[test]
10054    fn test_encode_thumb_bl_placeholder_addend_167_174() {
10055        let encoder = ArmEncoder::new_thumb2();
10056        let op = ArmOp::Bl {
10057            label: "callee".to_string(),
10058        };
10059
10060        let code = encoder.encode(&op).unwrap();
10061        assert_eq!(code.len(), 4, "Thumb-2 BL is 32-bit");
10062
10063        let hw1 = u16::from_le_bytes([code[0], code[1]]);
10064        let hw2 = u16::from_le_bytes([code[2], code[3]]);
10065        assert_eq!(hw1, 0xF7FF, "BL first halfword (matches gas `bl <extern>`)");
10066        assert_eq!(
10067            hw2, 0xFFFE,
10068            "BL second halfword must be 0xFFFE (-4 addend → nets to S), not 0xF800 (→ S+4, #174) or 0xD000 (#167)"
10069        );
10070        assert_ne!(hw2, 0xF800, "0xF800 (addend 0) lands at S+4 (#174)");
10071        assert_ne!(hw2, 0xD000, "0xD000 bakes in a ~+0x600000 addend (#167)");
10072    }
10073
10074    #[test]
10075    fn test_encode_sequence() {
10076        let encoder = ArmEncoder::new_arm32();
10077        let ops = vec![
10078            ArmOp::Mov {
10079                rd: Reg::R0,
10080                op2: Operand2::Imm(42),
10081            },
10082            ArmOp::Mov {
10083                rd: Reg::R1,
10084                op2: Operand2::Imm(10),
10085            },
10086            ArmOp::Add {
10087                rd: Reg::R2,
10088                rn: Reg::R0,
10089                op2: Operand2::Reg(Reg::R1),
10090            },
10091        ];
10092
10093        let code = encoder.encode_sequence(&ops).unwrap();
10094        assert_eq!(code.len(), 12); // 3 instructions * 4 bytes
10095    }
10096
10097    #[test]
10098    fn test_reg_to_bits() {
10099        assert_eq!(reg_to_bits(&Reg::R0), 0);
10100        assert_eq!(reg_to_bits(&Reg::R7), 7);
10101        assert_eq!(reg_to_bits(&Reg::SP), 13);
10102        assert_eq!(reg_to_bits(&Reg::LR), 14);
10103        assert_eq!(reg_to_bits(&Reg::PC), 15);
10104    }
10105
10106    #[test]
10107    fn test_encode_bitwise_operations() {
10108        let encoder = ArmEncoder::new_arm32();
10109
10110        let and_op = ArmOp::And {
10111            rd: Reg::R0,
10112            rn: Reg::R1,
10113            op2: Operand2::Reg(Reg::R2),
10114        };
10115        let and_code = encoder.encode(&and_op).unwrap();
10116        assert_eq!(and_code.len(), 4);
10117
10118        let orr_op = ArmOp::Orr {
10119            rd: Reg::R0,
10120            rn: Reg::R1,
10121            op2: Operand2::Reg(Reg::R2),
10122        };
10123        let orr_code = encoder.encode(&orr_op).unwrap();
10124        assert_eq!(orr_code.len(), 4);
10125
10126        let eor_op = ArmOp::Eor {
10127            rd: Reg::R0,
10128            rn: Reg::R1,
10129            op2: Operand2::Reg(Reg::R2),
10130        };
10131        let eor_code = encoder.encode(&eor_op).unwrap();
10132        assert_eq!(eor_code.len(), 4);
10133    }
10134
10135    // === Thumb-2 32-bit encoding tests ===
10136
10137    #[test]
10138    fn test_encode_sdiv_thumb2() {
10139        let encoder = ArmEncoder::new_thumb2();
10140        let op = ArmOp::Sdiv {
10141            rd: Reg::R0,
10142            rn: Reg::R1,
10143            rm: Reg::R2,
10144        };
10145
10146        let code = encoder.encode(&op).unwrap();
10147        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10148
10149        // SDIV R0, R1, R2: 0xFB91 0xF0F2
10150        // First halfword: 0xFB90 | Rn(1) = 0xFB91
10151        // Second halfword: 0xF0F0 | Rd(0)<<8 | Rm(2) = 0xF0F2
10152        // Little-endian: [0x91, 0xFB, 0xF2, 0xF0]
10153        assert_eq!(code[0], 0x91);
10154        assert_eq!(code[1], 0xFB);
10155        assert_eq!(code[2], 0xF2);
10156        assert_eq!(code[3], 0xF0);
10157    }
10158
10159    #[test]
10160    fn test_encode_udiv_thumb2() {
10161        let encoder = ArmEncoder::new_thumb2();
10162        let op = ArmOp::Udiv {
10163            rd: Reg::R0,
10164            rn: Reg::R1,
10165            rm: Reg::R2,
10166        };
10167
10168        let code = encoder.encode(&op).unwrap();
10169        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10170
10171        // UDIV R0, R1, R2: 0xFBB1 0xF0F2
10172        // Little-endian: [0xB1, 0xFB, 0xF2, 0xF0]
10173        assert_eq!(code[0], 0xB1);
10174        assert_eq!(code[1], 0xFB);
10175        assert_eq!(code[2], 0xF2);
10176        assert_eq!(code[3], 0xF0);
10177    }
10178
10179    #[test]
10180    fn test_encode_mul_thumb2() {
10181        let encoder = ArmEncoder::new_thumb2();
10182        let op = ArmOp::Mul {
10183            rd: Reg::R0,
10184            rn: Reg::R1,
10185            rm: Reg::R2,
10186        };
10187
10188        let code = encoder.encode(&op).unwrap();
10189        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10190    }
10191
10192    #[test]
10193    fn test_encode_and_thumb2() {
10194        let encoder = ArmEncoder::new_thumb2();
10195        let op = ArmOp::And {
10196            rd: Reg::R0,
10197            rn: Reg::R1,
10198            op2: Operand2::Reg(Reg::R2),
10199        };
10200
10201        let code = encoder.encode(&op).unwrap();
10202        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10203    }
10204
10205    #[test]
10206    fn test_encode_lsl_thumb2_low_regs() {
10207        let encoder = ArmEncoder::new_thumb2();
10208        let op = ArmOp::Lsl {
10209            rd: Reg::R0,
10210            rn: Reg::R1,
10211            shift: 5,
10212        };
10213
10214        let code = encoder.encode(&op).unwrap();
10215        assert_eq!(code.len(), 2); // 16-bit for low registers
10216    }
10217
10218    #[test]
10219    fn test_encode_clz_thumb2() {
10220        let encoder = ArmEncoder::new_thumb2();
10221        let op = ArmOp::Clz {
10222            rd: Reg::R0,
10223            rm: Reg::R1,
10224        };
10225
10226        let code = encoder.encode(&op).unwrap();
10227        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10228    }
10229
10230    #[test]
10231    fn test_encode_bx_thumb2() {
10232        let encoder = ArmEncoder::new_thumb2();
10233        let op = ArmOp::Bx { rm: Reg::LR };
10234
10235        let code = encoder.encode(&op).unwrap();
10236        assert_eq!(code.len(), 2); // 16-bit instruction
10237
10238        // BX LR: 0x4770
10239        assert_eq!(code, vec![0x70, 0x47]);
10240    }
10241
10242    // ========================================================================
10243    // f32 pseudo-op encoding tests
10244    // ========================================================================
10245
10246    #[test]
10247    fn test_encode_f32_abs_arm32() {
10248        let encoder = ArmEncoder::new_arm32();
10249        let op = ArmOp::F32Abs {
10250            sd: VfpReg::S0,
10251            sm: VfpReg::S2,
10252        };
10253        let code = encoder.encode(&op).unwrap();
10254        assert_eq!(code.len(), 4); // Single VFP instruction
10255    }
10256
10257    #[test]
10258    fn test_encode_f32_neg_arm32() {
10259        let encoder = ArmEncoder::new_arm32();
10260        let op = ArmOp::F32Neg {
10261            sd: VfpReg::S0,
10262            sm: VfpReg::S2,
10263        };
10264        let code = encoder.encode(&op).unwrap();
10265        assert_eq!(code.len(), 4);
10266    }
10267
10268    #[test]
10269    fn test_encode_f32_sqrt_arm32() {
10270        let encoder = ArmEncoder::new_arm32();
10271        let op = ArmOp::F32Sqrt {
10272            sd: VfpReg::S0,
10273            sm: VfpReg::S2,
10274        };
10275        let code = encoder.encode(&op).unwrap();
10276        assert_eq!(code.len(), 4);
10277    }
10278
10279    #[test]
10280    fn test_encode_f32_ceil_arm32() {
10281        let encoder = ArmEncoder::new_arm32();
10282        let op = ArmOp::F32Ceil {
10283            sd: VfpReg::S0,
10284            sm: VfpReg::S2,
10285        };
10286        let code = encoder.encode(&op).unwrap();
10287        // VMRS + BIC + ORR + VMSR + VCVT.S32.F32 + VMRS + BIC + VMSR + VCVT.F32.S32
10288        assert_eq!(code.len(), 36);
10289    }
10290
10291    #[test]
10292    fn test_encode_f32_floor_thumb2() {
10293        let encoder = ArmEncoder::new_thumb2();
10294        let op = ArmOp::F32Floor {
10295            sd: VfpReg::S0,
10296            sm: VfpReg::S2,
10297        };
10298        let code = encoder.encode(&op).unwrap();
10299        // VMRS + BIC.W + ORR.W + VMSR + VCVT + VMRS + BIC.W + VMSR + VCVT.F32.S32
10300        assert_eq!(code.len(), 36);
10301    }
10302
10303    #[test]
10304    fn test_encode_f32_min_arm32() {
10305        let encoder = ArmEncoder::new_arm32();
10306        let op = ArmOp::F32Min {
10307            sd: VfpReg::S0,
10308            sn: VfpReg::S2,
10309            sm: VfpReg::S4,
10310        };
10311        let code = encoder.encode(&op).unwrap();
10312        assert_eq!(code.len(), 16); // VMOV + VCMP + VMRS + conditional VMOV
10313    }
10314
10315    #[test]
10316    fn test_encode_f32_max_thumb2() {
10317        let encoder = ArmEncoder::new_thumb2();
10318        let op = ArmOp::F32Max {
10319            sd: VfpReg::S0,
10320            sn: VfpReg::S2,
10321            sm: VfpReg::S4,
10322        };
10323        let code = encoder.encode(&op).unwrap();
10324        // VMOV(4) + VCMP(4) + VMRS(4) + IT(2) + VMOV(4) = 18
10325        assert_eq!(code.len(), 18);
10326    }
10327
10328    #[test]
10329    fn test_encode_f32_copysign_arm32() {
10330        let encoder = ArmEncoder::new_arm32();
10331        let op = ArmOp::F32Copysign {
10332            sd: VfpReg::S0,
10333            sn: VfpReg::S2,
10334            sm: VfpReg::S4,
10335        };
10336        let code = encoder.encode(&op).unwrap();
10337        // VMOV + VMOV + AND + BIC + ORR + VMOV = 6 * 4 = 24
10338        assert_eq!(code.len(), 24);
10339    }
10340
10341    // ========================================================================
10342    // f64 encoding tests
10343    // ========================================================================
10344
10345    #[test]
10346    fn test_encode_f64_add_arm32() {
10347        let encoder = ArmEncoder::new_arm32();
10348        let op = ArmOp::F64Add {
10349            dd: VfpReg::D0,
10350            dn: VfpReg::D1,
10351            dm: VfpReg::D2,
10352        };
10353        let code = encoder.encode(&op).unwrap();
10354        assert_eq!(code.len(), 4);
10355        // VADD.F64 D0, D1, D2: check coprocessor is cp11 (0xB)
10356        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10357        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
10358    }
10359
10360    #[test]
10361    fn test_encode_f64_sub_thumb2() {
10362        let encoder = ArmEncoder::new_thumb2();
10363        let op = ArmOp::F64Sub {
10364            dd: VfpReg::D0,
10365            dn: VfpReg::D1,
10366            dm: VfpReg::D2,
10367        };
10368        let code = encoder.encode(&op).unwrap();
10369        assert_eq!(code.len(), 4); // 32-bit VFP as two Thumb halfwords
10370    }
10371
10372    #[test]
10373    fn test_encode_f64_mul_arm32() {
10374        let encoder = ArmEncoder::new_arm32();
10375        let op = ArmOp::F64Mul {
10376            dd: VfpReg::D0,
10377            dn: VfpReg::D1,
10378            dm: VfpReg::D2,
10379        };
10380        let code = encoder.encode(&op).unwrap();
10381        assert_eq!(code.len(), 4);
10382    }
10383
10384    #[test]
10385    fn test_encode_f64_div_arm32() {
10386        let encoder = ArmEncoder::new_arm32();
10387        let op = ArmOp::F64Div {
10388            dd: VfpReg::D0,
10389            dn: VfpReg::D1,
10390            dm: VfpReg::D2,
10391        };
10392        let code = encoder.encode(&op).unwrap();
10393        assert_eq!(code.len(), 4);
10394    }
10395
10396    #[test]
10397    fn test_encode_f64_abs_arm32() {
10398        let encoder = ArmEncoder::new_arm32();
10399        let op = ArmOp::F64Abs {
10400            dd: VfpReg::D0,
10401            dm: VfpReg::D2,
10402        };
10403        let code = encoder.encode(&op).unwrap();
10404        assert_eq!(code.len(), 4);
10405    }
10406
10407    #[test]
10408    fn test_encode_f64_neg_arm32() {
10409        let encoder = ArmEncoder::new_arm32();
10410        let op = ArmOp::F64Neg {
10411            dd: VfpReg::D0,
10412            dm: VfpReg::D2,
10413        };
10414        let code = encoder.encode(&op).unwrap();
10415        assert_eq!(code.len(), 4);
10416    }
10417
10418    #[test]
10419    fn test_encode_f64_sqrt_arm32() {
10420        let encoder = ArmEncoder::new_arm32();
10421        let op = ArmOp::F64Sqrt {
10422            dd: VfpReg::D0,
10423            dm: VfpReg::D2,
10424        };
10425        let code = encoder.encode(&op).unwrap();
10426        assert_eq!(code.len(), 4);
10427    }
10428
10429    #[test]
10430    fn test_encode_f64_load_arm32() {
10431        let encoder = ArmEncoder::new_arm32();
10432        let op = ArmOp::F64Load {
10433            dd: VfpReg::D0,
10434            addr: MemAddr::imm(Reg::R0, 8),
10435        };
10436        let code = encoder.encode(&op).unwrap();
10437        assert_eq!(code.len(), 4);
10438        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10439        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11 for F64
10440        assert_eq!(instr & 0xFF, 2); // offset 8 / 4 = 2
10441    }
10442
10443    #[test]
10444    fn test_encode_f64_store_thumb2() {
10445        let encoder = ArmEncoder::new_thumb2();
10446        let op = ArmOp::F64Store {
10447            dd: VfpReg::D0,
10448            addr: MemAddr::imm(Reg::SP, 0),
10449        };
10450        let code = encoder.encode(&op).unwrap();
10451        assert_eq!(code.len(), 4);
10452    }
10453
10454    #[test]
10455    fn test_encode_f64_compare_arm32() {
10456        let encoder = ArmEncoder::new_arm32();
10457        let op = ArmOp::F64Eq {
10458            rd: Reg::R0,
10459            dn: VfpReg::D0,
10460            dm: VfpReg::D1,
10461        };
10462        let code = encoder.encode(&op).unwrap();
10463        assert_eq!(code.len(), 16); // VCMP + VMRS + MOV #0 + MOVcond #1
10464    }
10465
10466    #[test]
10467    fn test_encode_f64_compare_thumb2() {
10468        let encoder = ArmEncoder::new_thumb2();
10469        let op = ArmOp::F64Lt {
10470            rd: Reg::R0,
10471            dn: VfpReg::D0,
10472            dm: VfpReg::D1,
10473        };
10474        let code = encoder.encode(&op).unwrap();
10475        // VCMP(4) + VMRS(4) + MOVS(2) + IT(2) + MOV(2) = 14
10476        assert_eq!(code.len(), 14);
10477    }
10478
10479    #[test]
10480    fn test_encode_f64_const_arm32() {
10481        let encoder = ArmEncoder::new_arm32();
10482        let op = ArmOp::F64Const {
10483            dd: VfpReg::D0,
10484            value: 3.125,
10485        };
10486        let code = encoder.encode(&op).unwrap();
10487        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
10488        assert_eq!(code.len(), 20);
10489    }
10490
10491    #[test]
10492    fn test_encode_f64_const_thumb2() {
10493        let encoder = ArmEncoder::new_thumb2();
10494        let op = ArmOp::F64Const {
10495            dd: VfpReg::D0,
10496            value: 2.5,
10497        };
10498        let code = encoder.encode(&op).unwrap();
10499        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
10500        assert_eq!(code.len(), 20);
10501    }
10502
10503    #[test]
10504    fn test_encode_f64_convert_i32s_arm32() {
10505        let encoder = ArmEncoder::new_arm32();
10506        let op = ArmOp::F64ConvertI32S {
10507            dd: VfpReg::D0,
10508            rm: Reg::R0,
10509        };
10510        let code = encoder.encode(&op).unwrap();
10511        // VMOV(4) + VCVT(4) = 8
10512        assert_eq!(code.len(), 8);
10513    }
10514
10515    #[test]
10516    fn test_encode_f64_promote_f32_arm32() {
10517        let encoder = ArmEncoder::new_arm32();
10518        let op = ArmOp::F64PromoteF32 {
10519            dd: VfpReg::D0,
10520            sm: VfpReg::S0,
10521        };
10522        let code = encoder.encode(&op).unwrap();
10523        assert_eq!(code.len(), 4); // Single VCVT.F64.F32 instruction
10524    }
10525
10526    #[test]
10527    fn test_encode_f64_promote_f32_thumb2() {
10528        let encoder = ArmEncoder::new_thumb2();
10529        let op = ArmOp::F64PromoteF32 {
10530            dd: VfpReg::D0,
10531            sm: VfpReg::S0,
10532        };
10533        let code = encoder.encode(&op).unwrap();
10534        assert_eq!(code.len(), 4);
10535    }
10536
10537    #[test]
10538    fn test_encode_i32_trunc_f64s_arm32() {
10539        let encoder = ArmEncoder::new_arm32();
10540        let op = ArmOp::I32TruncF64S {
10541            rd: Reg::R0,
10542            dm: VfpReg::D0,
10543        };
10544        let code = encoder.encode(&op).unwrap();
10545        // VCVT(4) + VMOV(4) = 8
10546        assert_eq!(code.len(), 8);
10547    }
10548
10549    #[test]
10550    fn test_encode_f64_reinterpret_i64_arm32() {
10551        let encoder = ArmEncoder::new_arm32();
10552        let op = ArmOp::F64ReinterpretI64 {
10553            dd: VfpReg::D0,
10554            rmlo: Reg::R0,
10555            rmhi: Reg::R1,
10556        };
10557        let code = encoder.encode(&op).unwrap();
10558        assert_eq!(code.len(), 4); // Single VMOV instruction
10559    }
10560
10561    #[test]
10562    fn test_encode_i64_reinterpret_f64_thumb2() {
10563        let encoder = ArmEncoder::new_thumb2();
10564        let op = ArmOp::I64ReinterpretF64 {
10565            rdlo: Reg::R0,
10566            rdhi: Reg::R1,
10567            dm: VfpReg::D0,
10568        };
10569        let code = encoder.encode(&op).unwrap();
10570        assert_eq!(code.len(), 4);
10571    }
10572
10573    #[test]
10574    fn test_encode_f64_trunc_thumb2() {
10575        let encoder = ArmEncoder::new_thumb2();
10576        let op = ArmOp::F64Trunc {
10577            dd: VfpReg::D0,
10578            dm: VfpReg::D1,
10579        };
10580        let code = encoder.encode(&op).unwrap();
10581        // Two VFP instructions via Thumb encoding
10582        assert_eq!(code.len(), 8);
10583    }
10584
10585    #[test]
10586    fn test_encode_f64_min_arm32() {
10587        let encoder = ArmEncoder::new_arm32();
10588        let op = ArmOp::F64Min {
10589            dd: VfpReg::D0,
10590            dn: VfpReg::D1,
10591            dm: VfpReg::D2,
10592        };
10593        let code = encoder.encode(&op).unwrap();
10594        // VMOV + VCMP + VMRS + conditional VMOV = 16
10595        assert_eq!(code.len(), 16);
10596    }
10597
10598    #[test]
10599    fn test_f64_cp11_encoding() {
10600        // Verify that F64 instructions use coprocessor 11 (0xB), not 10 (0xA)
10601        let encoder = ArmEncoder::new_arm32();
10602
10603        // F64Add
10604        let code = encoder
10605            .encode(&ArmOp::F64Add {
10606                dd: VfpReg::D0,
10607                dn: VfpReg::D0,
10608                dm: VfpReg::D0,
10609            })
10610            .unwrap();
10611        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10612        assert_eq!((instr >> 8) & 0xF, 0xB, "F64 should use cp11");
10613
10614        // F32Add for comparison
10615        let code = encoder
10616            .encode(&ArmOp::F32Add {
10617                sd: VfpReg::S0,
10618                sn: VfpReg::S0,
10619                sm: VfpReg::S0,
10620            })
10621            .unwrap();
10622        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10623        assert_eq!((instr >> 8) & 0xF, 0xA, "F32 should use cp10");
10624    }
10625
10626    #[test]
10627    fn test_dreg_encoding_higher_registers() {
10628        let encoder = ArmEncoder::new_arm32();
10629
10630        // Test with D15 (highest register)
10631        let op = ArmOp::F64Add {
10632            dd: VfpReg::D15,
10633            dn: VfpReg::D14,
10634            dm: VfpReg::D13,
10635        };
10636        let code = encoder.encode(&op).unwrap();
10637        assert_eq!(code.len(), 4);
10638
10639        // Verify the register encoding worked (instruction is valid)
10640        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10641        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
10642    }
10643
10644    // ========================================================================
10645    // Control flow encoding tests
10646    // ========================================================================
10647
10648    #[test]
10649    fn test_encode_label_emits_no_bytes() {
10650        let encoder = ArmEncoder::new_thumb2();
10651        let op = ArmOp::Label {
10652            name: ".Lblock_end_0".to_string(),
10653        };
10654        let code = encoder.encode(&op).unwrap();
10655        assert!(code.is_empty(), "Label should emit zero bytes");
10656
10657        let encoder32 = ArmEncoder::new_arm32();
10658        let code32 = encoder32.encode(&op).unwrap();
10659        assert!(
10660            code32.is_empty(),
10661            "Label should emit zero bytes in ARM32 too"
10662        );
10663    }
10664
10665    #[test]
10666    fn test_encode_bcc_eq_thumb2() {
10667        use synth_synthesis::Condition;
10668        let encoder = ArmEncoder::new_thumb2();
10669        let op = ArmOp::Bcc {
10670            cond: Condition::EQ,
10671            label: "target".to_string(),
10672        };
10673        let code = encoder.encode(&op).unwrap();
10674        assert_eq!(code.len(), 2); // 16-bit conditional branch
10675
10676        // BEQ with offset 0: 0xD000 in little-endian
10677        assert_eq!(code, vec![0x00, 0xD0]);
10678    }
10679
10680    #[test]
10681    fn test_encode_bcc_ne_thumb2() {
10682        use synth_synthesis::Condition;
10683        let encoder = ArmEncoder::new_thumb2();
10684        let op = ArmOp::Bcc {
10685            cond: Condition::NE,
10686            label: "target".to_string(),
10687        };
10688        let code = encoder.encode(&op).unwrap();
10689        assert_eq!(code.len(), 2);
10690
10691        // BNE with offset 0: 0xD100 in little-endian
10692        assert_eq!(code, vec![0x00, 0xD1]);
10693    }
10694
10695    #[test]
10696    fn test_encode_bcc_arm32() {
10697        use synth_synthesis::Condition;
10698        let encoder = ArmEncoder::new_arm32();
10699        let op = ArmOp::Bcc {
10700            cond: Condition::EQ,
10701            label: "target".to_string(),
10702        };
10703        let code = encoder.encode(&op).unwrap();
10704        assert_eq!(code.len(), 4); // 32-bit ARM instruction
10705
10706        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10707        // BEQ: cond=0x0, opcode=0xA, offset=0
10708        assert_eq!(instr & 0xF0000000, 0x00000000); // EQ condition
10709        assert_eq!(instr & 0x0F000000, 0x0A000000); // Branch opcode
10710    }
10711
10712    #[test]
10713    fn test_encode_udf_thumb2() {
10714        let encoder = ArmEncoder::new_thumb2();
10715        let op = ArmOp::Udf { imm: 0 };
10716        let code = encoder.encode(&op).unwrap();
10717        assert_eq!(code.len(), 2); // 16-bit
10718
10719        // UDF #0: 0xDE00 in little-endian
10720        assert_eq!(code, vec![0x00, 0xDE]);
10721    }
10722
10723    /// #610: the i64 rot/div/rem expansions must land the result in the
10724    /// selector-assigned rd pair and leave R0-R3 preserved (restored from the
10725    /// fixed-ABI wrapper's save area) — pre-#610 the rot expansion's own
10726    /// `POP {R4}` restored stale scratch OVER the result (rd_lo == R4) and
10727    /// the div/rem expansions ignored their register fields outright.
10728    #[test]
10729    fn test_610_i64_rot_expansion_ends_with_rd_movs_and_restore() {
10730        let encoder = ArmEncoder::new_thumb2();
10731        for op in [
10732            ArmOp::I64Rotl {
10733                rdlo: Reg::R4,
10734                rdhi: Reg::R5,
10735                rnlo: Reg::R0,
10736                rnhi: Reg::R1,
10737                shift: Reg::R2,
10738            },
10739            ArmOp::I64Rotr {
10740                rdlo: Reg::R4,
10741                rdhi: Reg::R5,
10742                rnlo: Reg::R0,
10743                rnhi: Reg::R1,
10744                shift: Reg::R2,
10745            },
10746        ] {
10747            let code = encoder.encode(&op).unwrap();
10748            assert_eq!(code.len(), 102, "register-independent size (estimator pin)");
10749            // Tail: MOV r5, r1 (0x460D); MOV r4, r0 (0x4604); POP {r0..r3}
10750            // (rd pair r4:r5 does not overlap the save area — all 4 restored).
10751            let tail: Vec<u16> = code[code.len() - 12..]
10752                .chunks(2)
10753                .map(|c| u16::from_le_bytes([c[0], c[1]]))
10754                .collect();
10755            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
10756        }
10757    }
10758
10759    /// #610: div/rem expansions honor rd and carry the divide-by-zero trap
10760    /// guard (`ORRS R12, R2, R3; BNE +0; UDF #0`) after operand marshaling.
10761    #[test]
10762    fn test_610_i64_div_rem_expansion_guard_and_rd() {
10763        let encoder = ArmEncoder::new_thumb2();
10764        let mk = |which: u8| {
10765            let (rdlo, rdhi, rnlo, rnhi, rmlo, rmhi) =
10766                (Reg::R4, Reg::R5, Reg::R0, Reg::R1, Reg::R2, Reg::R3);
10767            match which {
10768                0 => ArmOp::I64DivU {
10769                    rdlo,
10770                    rdhi,
10771                    rnlo,
10772                    rnhi,
10773                    rmlo,
10774                    rmhi,
10775                    elide_zero_guard: false,
10776                },
10777                1 => ArmOp::I64RemU {
10778                    rdlo,
10779                    rdhi,
10780                    rnlo,
10781                    rnhi,
10782                    rmlo,
10783                    rmhi,
10784                    elide_zero_guard: false,
10785                },
10786                2 => ArmOp::I64DivS {
10787                    rdlo,
10788                    rdhi,
10789                    rnlo,
10790                    rnhi,
10791                    rmlo,
10792                    rmhi,
10793                    elide_zero_guard: false,
10794                    elide_overflow_guard: false,
10795                },
10796                _ => ArmOp::I64RemS {
10797                    rdlo,
10798                    rdhi,
10799                    rnlo,
10800                    rnhi,
10801                    rmlo,
10802                    rmhi,
10803                    elide_zero_guard: false,
10804                },
10805            }
10806        };
10807        for which in 0..4u8 {
10808            let code = encoder.encode(&mk(which)).unwrap();
10809            // Zero-divisor trap guard right after the 26-byte marshal prologue.
10810            let guard: Vec<u16> = code[26..34]
10811                .chunks(2)
10812                .map(|c| u16::from_le_bytes([c[0], c[1]]))
10813                .collect();
10814            assert_eq!(
10815                guard,
10816                vec![0xEA52, 0x0C03, 0xD100, 0xDE00],
10817                "ORRS R12,R2,R3; BNE +0; UDF #0"
10818            );
10819            // Tail: result into rd pair (r5:r4), then restore all of R0-R3.
10820            let tail: Vec<u16> = code[code.len() - 12..]
10821                .chunks(2)
10822                .map(|c| u16::from_le_bytes([c[0], c[1]]))
10823                .collect();
10824            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
10825        }
10826    }
10827
10828    /// #610: when rd overlaps R0-R3 the restore must SKIP the result
10829    /// registers (drop the saved caller word) instead of popping over them.
10830    #[test]
10831    fn test_610_i64_divu_rd_in_r0_r1_skips_restore() {
10832        let encoder = ArmEncoder::new_thumb2();
10833        let code = encoder
10834            .encode(&ArmOp::I64DivU {
10835                rdlo: Reg::R0,
10836                rdhi: Reg::R1,
10837                rnlo: Reg::R0,
10838                rnhi: Reg::R1,
10839                rmlo: Reg::R2,
10840                rmhi: Reg::R3,
10841                elide_zero_guard: false,
10842            })
10843            .unwrap();
10844        let tail: Vec<u16> = code[code.len() - 12..]
10845            .chunks(2)
10846            .map(|c| u16::from_le_bytes([c[0], c[1]]))
10847            .collect();
10848        // MOV r1,r1 / MOV r0,r0 (no-ops, size-stable), ADD SP,#4 twice
10849        // (discard saved r0/r1 — the result lives there), POP {r2}, POP {r3}.
10850        assert_eq!(tail, vec![0x4609, 0x4600, 0xB001, 0xB001, 0xBC04, 0xBC08]);
10851    }
10852
10853    /// #610: a fully swapped rd pair (rd_lo=R1, rd_hi=R0) cannot be
10854    /// materialized by two MOVs in either order — must be a loud Err, never
10855    /// silent corruption. (Selector pairs are consecutive, so unreachable.)
10856    #[test]
10857    fn test_610_i64_swapped_rd_pair_rejected() {
10858        let encoder = ArmEncoder::new_thumb2();
10859        let result = encoder.encode(&ArmOp::I64RemU {
10860            rdlo: Reg::R1,
10861            rdhi: Reg::R0,
10862            rnlo: Reg::R2,
10863            rnhi: Reg::R3,
10864            rmlo: Reg::R4,
10865            rmhi: Reg::R5,
10866            elide_zero_guard: false,
10867        });
10868        assert!(result.is_err(), "swapped rd pair must be rejected loudly");
10869    }
10870
10871    /// #632: the I64Popcnt expansion's own scratch restore (`POP {R3,R4,R5}`)
10872    /// must not clobber the result. Pre-fix the total was materialized with
10873    /// `ADDS rd, R4, R5` BEFORE the pop, so any allocator-assigned
10874    /// rd ∈ {R3,R4,R5} received stale stack garbage. Post-fix the count is
10875    /// carried across the restore in R12 (never allocatable, never restored)
10876    /// and moved into rd only after the pop — structurally rd-independent.
10877    #[test]
10878    fn test_632_i64_popcnt_result_survives_scratch_restore() {
10879        let encoder = ArmEncoder::new_thumb2();
10880        // Every allocatable rd, including the restore set {R3,R4,R5} and R8.
10881        for rd in [
10882            Reg::R0,
10883            Reg::R2,
10884            Reg::R3,
10885            Reg::R4,
10886            Reg::R5,
10887            Reg::R6,
10888            Reg::R8,
10889        ] {
10890            let code = encoder
10891                .encode(&ArmOp::I64Popcnt {
10892                    rd,
10893                    rnlo: Reg::R6,
10894                    rnhi: Reg::R7,
10895                })
10896                .unwrap();
10897            assert_eq!(code.len(), 180, "register-independent size (estimator pin)");
10898            let hw: Vec<u16> = code
10899                .chunks(2)
10900                .map(|c| u16::from_le_bytes([c[0], c[1]]))
10901                .collect();
10902            let pop = hw
10903                .iter()
10904                .position(|&h| h == 0xBC38)
10905                .expect("POP {R3,R4,R5} present");
10906            // Immediately before the POP: ADD.W R12, R4, R5 (the total lives
10907            // in R12, which the POP cannot touch).
10908            assert_eq!(
10909                &hw[pop - 2..pop],
10910                &[0xEB04, 0x0C05],
10911                "total must be carried in R12 across the restore"
10912            );
10913            // Immediately after the POP: MOV rd, R12.
10914            let rd_bits = match rd {
10915                Reg::R8 => 8u16,
10916                Reg::R6 => 6,
10917                Reg::R5 => 5,
10918                Reg::R4 => 4,
10919                Reg::R3 => 3,
10920                Reg::R2 => 2,
10921                _ => 0,
10922            };
10923            let expect_mov = 0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7);
10924            assert_eq!(hw[pop + 1], expect_mov, "MOV rd, R12 after the restore");
10925            // No write into rd between the PUSH and the POP (the old
10926            // pre-restore ADDS is gone).
10927            assert!(
10928                !hw[..pop].contains(&(0x1800 | (5 << 6) | (4 << 3) | rd_bits)),
10929                "no ADDS rd, R4, R5 before the restore pop"
10930            );
10931        }
10932    }
10933
10934    /// #632 audit: the entry marshal must be permutation-safe. Pre-fix
10935    /// `MOV R4, rnlo; MOV R5, rnhi` read a clobbered R4 when the operand
10936    /// pair lived at (R3, R4). Post-fix rnlo routes through R12.
10937    #[test]
10938    fn test_632_i64_popcnt_marshal_pair_at_r3_r4() {
10939        let encoder = ArmEncoder::new_thumb2();
10940        let code = encoder
10941            .encode(&ArmOp::I64Popcnt {
10942                rd: Reg::R0,
10943                rnlo: Reg::R3,
10944                rnhi: Reg::R4,
10945            })
10946            .unwrap();
10947        let hw: Vec<u16> = code
10948            .chunks(2)
10949            .map(|c| u16::from_le_bytes([c[0], c[1]]))
10950            .collect();
10951        // PUSH {R3,R4,R5}; MOV R12, R3; MOV R5, R4 (rnhi read BEFORE any
10952        // write to R4); MOV R4, R12.
10953        assert_eq!(hw[0], 0xB438);
10954        assert_eq!(hw[1], 0x4600 | (1 << 7) | (3 << 3) | 4, "MOV R12, rnlo");
10955        assert_eq!(hw[2], 0x4600 | (4 << 3) | 5, "MOV R5, rnhi");
10956        assert_eq!(hw[3], 0x4664, "MOV R4, R12");
10957    }
10958
10959    /// #632: A32 twin — same structural fix on the ARM-mode path
10960    /// (`--target cortex-r5`): total carried in R12 across the restore.
10961    #[test]
10962    fn test_632_a32_i64_popcnt_result_survives_scratch_restore() {
10963        let encoder = ArmEncoder::new_arm32();
10964        for rd in [Reg::R0, Reg::R3, Reg::R4, Reg::R5, Reg::R8] {
10965            let code = encoder
10966                .encode(&ArmOp::I64Popcnt {
10967                    rd,
10968                    rnlo: Reg::R6,
10969                    rnhi: Reg::R7,
10970                })
10971                .unwrap();
10972            let words: Vec<u32> = code
10973                .chunks(4)
10974                .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
10975                .collect();
10976            let pop = words
10977                .iter()
10978                .position(|&w| w == 0xE8BD_0038)
10979                .expect("POP {R3,R4,R5} present");
10980            assert_eq!(words[pop - 1], 0xE084_C005, "ADD R12, R4, R5 before POP");
10981            let rd_bits = match rd {
10982                Reg::R8 => 8u32,
10983                Reg::R5 => 5,
10984                Reg::R4 => 4,
10985                Reg::R3 => 3,
10986                _ => 0,
10987            };
10988            assert_eq!(
10989                words[pop + 1],
10990                0xE1A0_0000 | (rd_bits << 12) | 12,
10991                "MOV rd, R12 after the restore"
10992            );
10993        }
10994    }
10995
10996    /// #633: I64DivS must carry the INT64_MIN/-1 overflow guard (mirroring
10997    /// the i32 path) right after the zero-divisor guard — dividend in R0:R1,
10998    /// divisor in R2:R3 on the #610/#613 fixed-ABI wrapper path.
10999    #[test]
11000    fn test_633_i64_divs_overflow_guard_emitted() {
11001        let encoder = ArmEncoder::new_thumb2();
11002        let code = encoder
11003            .encode(&ArmOp::I64DivS {
11004                rdlo: Reg::R4,
11005                rdhi: Reg::R5,
11006                rnlo: Reg::R0,
11007                rnhi: Reg::R1,
11008                rmlo: Reg::R2,
11009                rmhi: Reg::R3,
11010                elide_zero_guard: false,
11011                elide_overflow_guard: false,
11012            })
11013            .unwrap();
11014        // 26-byte marshal + 8-byte zero-trap, then the 22-byte overflow guard.
11015        let guard: Vec<u16> = code[34..56]
11016            .chunks(2)
11017            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11018            .collect();
11019        assert_eq!(
11020            guard,
11021            vec![
11022                0xEA02, 0x0C03, // AND.W R12, R2, R3
11023                0xF11C, 0x0F01, // CMN.W R12, #1
11024                0xD105, // BNE .no_trap
11025                0x2800, // CMP R0, #0
11026                0xD103, // BNE .no_trap
11027                0xF1B1, 0x4F00, // CMP.W R1, #0x80000000
11028                0xD100, // BNE .no_trap
11029                0xDE00, // UDF #0 — signed-division overflow
11030            ],
11031            "INT64_MIN/-1 overflow guard after the zero-divisor guard"
11032        );
11033    }
11034
11035    /// #633 fix-guard twin: I64RemS must NOT carry the overflow guard —
11036    /// rem_s(INT64_MIN, -1) is defined as 0 and must not trap. Exactly one
11037    /// UDF (the zero-divisor trap) in the whole expansion.
11038    #[test]
11039    fn test_633_i64_rems_has_no_overflow_guard() {
11040        let encoder = ArmEncoder::new_thumb2();
11041        for (is_rem_s, op) in [
11042            (
11043                true,
11044                ArmOp::I64RemS {
11045                    rdlo: Reg::R4,
11046                    rdhi: Reg::R5,
11047                    rnlo: Reg::R0,
11048                    rnhi: Reg::R1,
11049                    rmlo: Reg::R2,
11050                    rmhi: Reg::R3,
11051                    elide_zero_guard: false,
11052                },
11053            ),
11054            (
11055                false,
11056                ArmOp::I64DivS {
11057                    rdlo: Reg::R4,
11058                    rdhi: Reg::R5,
11059                    rnlo: Reg::R0,
11060                    rnhi: Reg::R1,
11061                    rmlo: Reg::R2,
11062                    rmhi: Reg::R3,
11063                    elide_zero_guard: false,
11064                    elide_overflow_guard: false,
11065                },
11066            ),
11067        ] {
11068            let code = encoder.encode(&op).unwrap();
11069            let udfs = code
11070                .chunks(2)
11071                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11072                .count();
11073            let want = if is_rem_s { 1 } else { 2 };
11074            assert_eq!(
11075                udfs, want,
11076                "rem_s: zero-trap only; div_s: zero-trap + overflow trap"
11077            );
11078        }
11079    }
11080
11081    /// #494 phase 2b: `elide_zero_guard` drops EXACTLY the 8-byte fused
11082    /// zero-trap (`ORRS.W R12,R2,R3; BNE; UDF #0`) and nothing else — the
11083    /// rest of the expansion is byte-identical (splice check).
11084    #[test]
11085    fn test_494_i64_zero_guard_elision_is_exact_splice() {
11086        let encoder = ArmEncoder::new_thumb2();
11087        let mk = |elide_zero_guard: bool| {
11088            encoder
11089                .encode(&ArmOp::I64DivU {
11090                    rdlo: Reg::R4,
11091                    rdhi: Reg::R5,
11092                    rnlo: Reg::R0,
11093                    rnhi: Reg::R1,
11094                    rmlo: Reg::R2,
11095                    rmhi: Reg::R3,
11096                    elide_zero_guard,
11097                })
11098                .unwrap()
11099        };
11100        let full = mk(false);
11101        let elided = mk(true);
11102        assert_eq!(full.len(), elided.len() + 8, "zero guard is 8 bytes");
11103        // Marshal prologue (26 B) unchanged, guard (8 B) gone, tail identical.
11104        assert_eq!(&full[..26], &elided[..26]);
11105        assert_eq!(
11106            &full[26..34],
11107            &[0x52, 0xEA, 0x03, 0x0C, 0x00, 0xD1, 0x00, 0xDE],
11108            "the spliced-out bytes are exactly ORRS.W; BNE; UDF #0"
11109        );
11110        assert_eq!(&full[34..], &elided[26..]);
11111    }
11112
11113    /// #494 phase 2b two-guard distinction (the #633/#634 synergy): a
11114    /// divisor-nonzero fact elides ONLY the zero guard — the INT64_MIN/-1
11115    /// OVERFLOW guard is a separate obligation and must survive
11116    /// `elide_zero_guard: true`. Pinned on div_s in all flag states.
11117    #[test]
11118    fn test_494_i64_divs_overflow_guard_retained_when_only_zero_elided() {
11119        let encoder = ArmEncoder::new_thumb2();
11120        let mk = |zero: bool, ovf: bool| {
11121            encoder
11122                .encode(&ArmOp::I64DivS {
11123                    rdlo: Reg::R4,
11124                    rdhi: Reg::R5,
11125                    rnlo: Reg::R0,
11126                    rnhi: Reg::R1,
11127                    rmlo: Reg::R2,
11128                    rmhi: Reg::R3,
11129                    elide_zero_guard: zero,
11130                    elide_overflow_guard: ovf,
11131                })
11132                .unwrap()
11133        };
11134        let udf_count = |code: &[u8]| {
11135            code.chunks(2)
11136                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11137                .count()
11138        };
11139        let full = mk(false, false);
11140        let zero_only = mk(true, false);
11141        let both = mk(true, true);
11142        assert_eq!(udf_count(&full), 2, "baseline: zero trap + overflow trap");
11143        assert_eq!(
11144            udf_count(&zero_only),
11145            1,
11146            "divisor-nonzero elides the zero trap ONLY — the #633 overflow \
11147             guard must be retained"
11148        );
11149        // The retained guard is the 22-byte overflow sequence, now right
11150        // after the 26-byte marshal prologue.
11151        let guard: Vec<u16> = zero_only[26..48]
11152            .chunks(2)
11153            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11154            .collect();
11155        assert_eq!(
11156            guard,
11157            vec![
11158                0xEA02, 0x0C03, 0xF11C, 0x0F01, 0xD105, 0x2800, 0xD103, 0xF1B1, 0x4F00, 0xD100,
11159                0xDE00,
11160            ],
11161            "the surviving guard is the INT64_MIN/-1 overflow trap"
11162        );
11163        assert_eq!(full.len(), zero_only.len() + 8);
11164        assert_eq!(zero_only.len(), both.len() + 22);
11165        assert_eq!(udf_count(&both), 0, "both obligations discharged ⇒ no UDF");
11166    }
11167
11168    /// #494 phase 2b A32 twin: zero-guard elision is an exact 12-byte splice
11169    /// and the A32 overflow guard survives a zero-only elision.
11170    #[test]
11171    fn test_494_a32_i64_guard_elision() {
11172        let encoder = ArmEncoder::new_arm32();
11173        let mk = |zero: bool, ovf: bool| {
11174            encoder
11175                .encode(&ArmOp::I64DivS {
11176                    rdlo: Reg::R4,
11177                    rdhi: Reg::R5,
11178                    rnlo: Reg::R0,
11179                    rnhi: Reg::R1,
11180                    rmlo: Reg::R2,
11181                    rmhi: Reg::R3,
11182                    elide_zero_guard: zero,
11183                    elide_overflow_guard: ovf,
11184                })
11185                .unwrap()
11186        };
11187        let full = mk(false, false);
11188        let zero_only = mk(true, false);
11189        let both = mk(true, true);
11190        // A32 zero guard = 3 words (ORRS/BNE/UDF), overflow guard = 6 words.
11191        assert_eq!(full.len(), zero_only.len() + 12);
11192        assert_eq!(zero_only.len(), both.len() + 24);
11193        let udf_count = |code: &[u8]| {
11194            code.chunks(4)
11195                .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
11196                .count()
11197        };
11198        assert_eq!(udf_count(&full), 2);
11199        assert_eq!(
11200            udf_count(&zero_only),
11201            1,
11202            "A32: overflow guard retained under zero-only elision"
11203        );
11204        assert_eq!(udf_count(&both), 0);
11205    }
11206
11207    /// #633: A32 twin — the conditional-execution overflow guard on the
11208    /// ARM-mode I64DivS, and its absence from I64RemS.
11209    #[test]
11210    fn test_633_a32_i64_divs_overflow_guard() {
11211        let encoder = ArmEncoder::new_arm32();
11212        let mk_divs = ArmOp::I64DivS {
11213            rdlo: Reg::R4,
11214            rdhi: Reg::R5,
11215            rnlo: Reg::R0,
11216            rnhi: Reg::R1,
11217            rmlo: Reg::R2,
11218            rmhi: Reg::R3,
11219            elide_zero_guard: false,
11220            elide_overflow_guard: false,
11221        };
11222        let code = encoder.encode(&mk_divs).unwrap();
11223        let words: Vec<u32> = code
11224            .chunks(4)
11225            .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11226            .collect();
11227        let guard = [
11228            0xE002_C003u32, // AND   R12, R2, R3
11229            0xE37C_0001,    // CMN   R12, #1
11230            0x0350_0000,    // CMPEQ R0, #0
11231            0x0351_0102,    // CMPEQ R1, #0x80000000
11232            0x1A00_0000,    // BNE +1 insn
11233            0xE7F0_00F0,    // UDF #0
11234        ];
11235        assert!(
11236            words.windows(6).any(|w| w == guard),
11237            "A32 I64DivS carries the INT64_MIN/-1 overflow guard"
11238        );
11239        let rems = encoder
11240            .encode(&ArmOp::I64RemS {
11241                rdlo: Reg::R4,
11242                rdhi: Reg::R5,
11243                rnlo: Reg::R0,
11244                rnhi: Reg::R1,
11245                rmlo: Reg::R2,
11246                rmhi: Reg::R3,
11247                elide_zero_guard: false,
11248            })
11249            .unwrap();
11250        let rems_udfs = rems
11251            .chunks(4)
11252            .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
11253            .count();
11254        assert_eq!(rems_udfs, 1, "A32 I64RemS keeps only the zero-divisor trap");
11255    }
11256
11257    #[test]
11258    fn test_encode_nop_thumb2() {
11259        let encoder = ArmEncoder::new_thumb2();
11260        let op = ArmOp::Nop;
11261        let code = encoder.encode(&op).unwrap();
11262        assert_eq!(code.len(), 2); // 16-bit
11263
11264        // NOP: 0xBF00 in little-endian
11265        assert_eq!(code, vec![0x00, 0xBF]);
11266    }
11267
11268    // =========================================================================
11269    // i64 Thumb-2 encoding tests
11270    // =========================================================================
11271
11272    #[test]
11273    fn test_encode_i64_add_thumb2() {
11274        let encoder = ArmEncoder::new_thumb2();
11275        let op = ArmOp::I64Add {
11276            rdlo: Reg::R0,
11277            rdhi: Reg::R1,
11278            rnlo: Reg::R0,
11279            rnhi: Reg::R1,
11280            rmlo: Reg::R2,
11281            rmhi: Reg::R3,
11282        };
11283        let code = encoder.encode(&op).unwrap();
11284        // Should emit ADDS (2 bytes) + ADC.W (4 bytes) = 6 bytes
11285        assert_eq!(code.len(), 6, "I64Add should be 6 bytes (ADDS + ADC.W)");
11286    }
11287
11288    #[test]
11289    fn test_encode_i64_sub_thumb2() {
11290        let encoder = ArmEncoder::new_thumb2();
11291        let op = ArmOp::I64Sub {
11292            rdlo: Reg::R0,
11293            rdhi: Reg::R1,
11294            rnlo: Reg::R0,
11295            rnhi: Reg::R1,
11296            rmlo: Reg::R2,
11297            rmhi: Reg::R3,
11298        };
11299        let code = encoder.encode(&op).unwrap();
11300        // Should emit SUBS (2 bytes) + SBC.W (4 bytes) = 6 bytes
11301        assert_eq!(code.len(), 6, "I64Sub should be 6 bytes (SUBS + SBC.W)");
11302    }
11303
11304    #[test]
11305    fn test_encode_i64_and_thumb2() {
11306        let encoder = ArmEncoder::new_thumb2();
11307        let op = ArmOp::I64And {
11308            rdlo: Reg::R0,
11309            rdhi: Reg::R1,
11310            rnlo: Reg::R0,
11311            rnhi: Reg::R1,
11312            rmlo: Reg::R2,
11313            rmhi: Reg::R3,
11314        };
11315        let code = encoder.encode(&op).unwrap();
11316        // AND.W (4 bytes) + AND.W (4 bytes) = 8 bytes
11317        assert!(code.len() >= 4, "I64And should emit at least 4 bytes");
11318    }
11319
11320    #[test]
11321    fn test_encode_i64_or_thumb2() {
11322        let encoder = ArmEncoder::new_thumb2();
11323        let op = ArmOp::I64Or {
11324            rdlo: Reg::R0,
11325            rdhi: Reg::R1,
11326            rnlo: Reg::R0,
11327            rnhi: Reg::R1,
11328            rmlo: Reg::R2,
11329            rmhi: Reg::R3,
11330        };
11331        let code = encoder.encode(&op).unwrap();
11332        assert!(code.len() >= 4, "I64Or should emit at least 4 bytes");
11333    }
11334
11335    #[test]
11336    fn test_encode_i64_xor_thumb2() {
11337        let encoder = ArmEncoder::new_thumb2();
11338        let op = ArmOp::I64Xor {
11339            rdlo: Reg::R0,
11340            rdhi: Reg::R1,
11341            rnlo: Reg::R0,
11342            rnhi: Reg::R1,
11343            rmlo: Reg::R2,
11344            rmhi: Reg::R3,
11345        };
11346        let code = encoder.encode(&op).unwrap();
11347        assert!(code.len() >= 4, "I64Xor should emit at least 4 bytes");
11348    }
11349
11350    #[test]
11351    fn test_encode_i64_const_small_thumb2() {
11352        let encoder = ArmEncoder::new_thumb2();
11353        // Small constant: only needs MOVW for each half
11354        let op = ArmOp::I64Const {
11355            rdlo: Reg::R0,
11356            rdhi: Reg::R1,
11357            value: 42,
11358        };
11359        let code = encoder.encode(&op).unwrap();
11360        // MOVW R0, #42 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes minimum
11361        assert!(code.len() >= 8, "I64Const should emit at least 8 bytes");
11362    }
11363
11364    #[test]
11365    fn test_encode_i64_const_large_thumb2() {
11366        let encoder = ArmEncoder::new_thumb2();
11367        // Large constant: needs MOVW+MOVT for each half
11368        let op = ArmOp::I64Const {
11369            rdlo: Reg::R0,
11370            rdhi: Reg::R1,
11371            value: 0x1234_5678_9ABC_DEF0_u64 as i64,
11372        };
11373        let code = encoder.encode(&op).unwrap();
11374        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
11375        assert_eq!(
11376            code.len(),
11377            16,
11378            "I64Const with large value should be 16 bytes"
11379        );
11380    }
11381
11382    #[test]
11383    fn test_encode_i64_extend_i32_s_thumb2() {
11384        let encoder = ArmEncoder::new_thumb2();
11385        let op = ArmOp::I64ExtendI32S {
11386            rdlo: Reg::R0,
11387            rdhi: Reg::R1,
11388            rn: Reg::R0,
11389        };
11390        let code = encoder.encode(&op).unwrap();
11391        // When rdlo == rn, only ASR (4 bytes) is emitted
11392        assert_eq!(
11393            code.len(),
11394            4,
11395            "I64ExtendI32S (same reg) should be 4 bytes (ASR only)"
11396        );
11397    }
11398
11399    #[test]
11400    fn test_encode_i64_extend_i32_s_diff_reg_thumb2() {
11401        let encoder = ArmEncoder::new_thumb2();
11402        let op = ArmOp::I64ExtendI32S {
11403            rdlo: Reg::R0,
11404            rdhi: Reg::R1,
11405            rn: Reg::R2,
11406        };
11407        let code = encoder.encode(&op).unwrap();
11408        // MOV rdlo, rn (2 bytes for low regs) + ASR rdhi, rdlo, #31 (4 bytes) = 6 bytes
11409        assert!(
11410            code.len() >= 6,
11411            "I64ExtendI32S (diff reg) should be at least 6 bytes"
11412        );
11413    }
11414
11415    #[test]
11416    fn test_encode_i64_extend_i32_u_thumb2() {
11417        let encoder = ArmEncoder::new_thumb2();
11418        let op = ArmOp::I64ExtendI32U {
11419            rdlo: Reg::R0,
11420            rdhi: Reg::R1,
11421            rn: Reg::R0,
11422        };
11423        let code = encoder.encode(&op).unwrap();
11424        // When rdlo == rn, only MOV rdhi, #0 (2 bytes) is emitted
11425        assert_eq!(
11426            code.len(),
11427            2,
11428            "I64ExtendI32U (same reg) should be 2 bytes (MOV #0 only)"
11429        );
11430    }
11431
11432    #[test]
11433    fn test_encode_i32_wrap_i64_nop_thumb2() {
11434        let encoder = ArmEncoder::new_thumb2();
11435        // When rd == rnlo, should be a NOP
11436        let op = ArmOp::I32WrapI64 {
11437            rd: Reg::R0,
11438            rnlo: Reg::R0,
11439        };
11440        let code = encoder.encode(&op).unwrap();
11441        assert_eq!(code.len(), 2, "I32WrapI64 same reg should be NOP (2 bytes)");
11442        assert_eq!(code, vec![0x00, 0xBF]); // NOP
11443    }
11444
11445    #[test]
11446    fn test_encode_i32_wrap_i64_diff_reg_thumb2() {
11447        let encoder = ArmEncoder::new_thumb2();
11448        let op = ArmOp::I32WrapI64 {
11449            rd: Reg::R2,
11450            rnlo: Reg::R0,
11451        };
11452        let code = encoder.encode(&op).unwrap();
11453        // MOV R2, R0 (2 or 4 bytes)
11454        assert!(
11455            code.len() >= 2,
11456            "I32WrapI64 diff reg should emit at least 2 bytes"
11457        );
11458    }
11459
11460    #[test]
11461    fn test_encode_i64_eqz_thumb2() {
11462        let encoder = ArmEncoder::new_thumb2();
11463        let op = ArmOp::I64Eqz {
11464            rd: Reg::R0,
11465            rnlo: Reg::R0,
11466            rnhi: Reg::R1,
11467        };
11468        let code = encoder.encode(&op).unwrap();
11469        // Delegates to I64SetCondZ which is already encoded
11470        assert!(
11471            code.len() >= 6,
11472            "I64Eqz should emit at least 6 bytes for ORR+ITE+MOV+MOV"
11473        );
11474    }
11475
11476    #[test]
11477    fn test_encode_i64_eq_thumb2() {
11478        let encoder = ArmEncoder::new_thumb2();
11479        let op = ArmOp::I64Eq {
11480            rd: Reg::R0,
11481            rnlo: Reg::R0,
11482            rnhi: Reg::R1,
11483            rmlo: Reg::R2,
11484            rmhi: Reg::R3,
11485        };
11486        let code = encoder.encode(&op).unwrap();
11487        // Delegates to I64SetCond EQ: CMP lo + IT EQ + CMPEQ hi + ITE EQ + MOV 1 + MOV 0
11488        assert!(code.len() >= 10, "I64Eq should emit at least 10 bytes");
11489    }
11490
11491    #[test]
11492    fn test_encode_i64_ldr_thumb2() {
11493        let encoder = ArmEncoder::new_thumb2();
11494        let op = ArmOp::I64Ldr {
11495            rdlo: Reg::R0,
11496            rdhi: Reg::R1,
11497            addr: MemAddr::imm(Reg::SP, 0),
11498        };
11499        let code = encoder.encode(&op).unwrap();
11500        // Two LDR instructions (lo at offset, hi at offset+4)
11501        assert!(code.len() >= 4, "I64Ldr should emit at least 4 bytes");
11502    }
11503
11504    #[test]
11505    fn test_372_i64_ldr_indexed_materializes_address() {
11506        // #372: a memory i64.load carries an index register (R11 + addr + off).
11507        // The encoder must materialize `ip = base + index` (ADD.W) and load via
11508        // `[ip,#off]` — NOT drop the index. A frame (non-indexed) i64.load must
11509        // stay byte-identical (plain `[base,#off]`, no ADD).
11510        let encoder = ArmEncoder::new_thumb2();
11511        let indexed = encoder
11512            .encode(&ArmOp::I64Ldr {
11513                rdlo: Reg::R0,
11514                rdhi: Reg::R1,
11515                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
11516            })
11517            .unwrap();
11518        // ADD.W ip, fp, r0 = eb0b 0c00 (byte-verified vs arm-none-eabi-as).
11519        assert_eq!(
11520            &indexed[0..4],
11521            &[0x0b, 0xeb, 0x00, 0x0c],
11522            "indexed I64Ldr must start with ADD.W ip, base, index"
11523        );
11524        let frame = encoder
11525            .encode(&ArmOp::I64Ldr {
11526                rdlo: Reg::R0,
11527                rdhi: Reg::R1,
11528                addr: MemAddr::imm(Reg::SP, 8),
11529            })
11530            .unwrap();
11531        // No index -> no ADD.W prefix (byte-identical frame access).
11532        assert_ne!(
11533            &frame[0..2],
11534            &[0x0b, 0xeb],
11535            "frame (non-indexed) I64Ldr must NOT emit an ADD.W"
11536        );
11537    }
11538
11539    #[test]
11540    fn test_382_i64_ldst_large_offset_materializes_not_skips() {
11541        // #382: an indexed i64.load/store whose static offset > 0xFFF must
11542        // MATERIALIZE the offset into the base — NOT return Err (skip the fn).
11543        // Sequence for reg_imm(R11, R0, 5000): MOVW ip,#5000 ; ADD ip,r0,ip ;
11544        // ADD ip,ip,fp ; LDR/STR halves at [ip,#0] / [ip,#4]. Byte-verified tail
11545        // vs arm-none-eabi-as.
11546        let encoder = ArmEncoder::new_thumb2();
11547        // 0x1388 > 0xFFF (MemAddr is not Copy, so build it per use).
11548
11549        let ld = encoder
11550            .encode(&ArmOp::I64Ldr {
11551                rdlo: Reg::R0,
11552                rdhi: Reg::R1,
11553                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
11554            })
11555            .expect("large-offset i64.load must lower, not skip");
11556        // MOVW ip,#0x1388 (4) + ADD ip,r0,ip (4) + ADD ip,ip,fp (4) + 2 LDR (8).
11557        assert_eq!(ld.len(), 20, "expected MOVW + 2×ADD + 2×LDR");
11558        // Must NOT be the small-offset `ADD.W ip, fp, r0` (0x0b 0xeb) prefix —
11559        // that path can only reach imm12 offsets.
11560        assert_ne!(
11561            &ld[0..2],
11562            &[0x0b, 0xeb],
11563            "must materialize the large offset"
11564        );
11565        // Effective base built in ip, then halves at [ip,#0] / [ip,#4].
11566        assert_eq!(
11567            &ld[4..20],
11568            &[
11569                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
11570                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
11571                0xdc, 0xf8, 0x00, 0x00, // LDR.W r0, [ip, #0]
11572                0xdc, 0xf8, 0x04, 0x10, // LDR.W r1, [ip, #4]
11573            ],
11574            "large-offset i64.load must fold offset into ip and access [ip,#0]/[ip,#4]"
11575        );
11576
11577        // Store: same base materialization, STR halves.
11578        let st = encoder
11579            .encode(&ArmOp::I64Str {
11580                rdlo: Reg::R2,
11581                rdhi: Reg::R3,
11582                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
11583            })
11584            .expect("large-offset i64.store must lower, not skip");
11585        assert_eq!(st.len(), 20);
11586        assert_eq!(
11587            &st[4..20],
11588            &[
11589                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
11590                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
11591                0xcc, 0xf8, 0x00, 0x20, // STR.W r2, [ip, #0]
11592                0xcc, 0xf8, 0x04, 0x30, // STR.W r3, [ip, #4]
11593            ],
11594            "large-offset i64.store must fold offset into ip and access [ip,#0]/[ip,#4]"
11595        );
11596
11597        // Small-offset (imm12) indexed access stays byte-identical (#372): the
11598        // effective base is a single `ADD.W ip, fp, r0` and the halves keep the
11599        // folded immediates — NO extra MOVW/ADD.
11600        let small = encoder
11601            .encode(&ArmOp::I64Ldr {
11602                rdlo: Reg::R0,
11603                rdhi: Reg::R1,
11604                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 8),
11605            })
11606            .unwrap();
11607        assert_eq!(
11608            &small[0..4],
11609            &[0x0b, 0xeb, 0x00, 0x0c],
11610            "small-offset indexed i64 must keep the single ADD.W ip, fp, r0"
11611        );
11612        assert_eq!(small.len(), 12, "ADD.W + 2×LDR.W (offset folded in imm12)");
11613    }
11614
11615    #[test]
11616    fn test_encode_i64_str_thumb2() {
11617        let encoder = ArmEncoder::new_thumb2();
11618        let op = ArmOp::I64Str {
11619            rdlo: Reg::R0,
11620            rdhi: Reg::R1,
11621            addr: MemAddr::imm(Reg::SP, 0),
11622        };
11623        let code = encoder.encode(&op).unwrap();
11624        // Two STR instructions (lo at offset, hi at offset+4)
11625        assert!(code.len() >= 4, "I64Str should emit at least 4 bytes");
11626    }
11627
11628    #[test]
11629    fn test_encode_i64_all_comparisons_thumb2() {
11630        let encoder = ArmEncoder::new_thumb2();
11631
11632        let ops = vec![
11633            ArmOp::I64Ne {
11634                rd: Reg::R0,
11635                rnlo: Reg::R0,
11636                rnhi: Reg::R1,
11637                rmlo: Reg::R2,
11638                rmhi: Reg::R3,
11639            },
11640            ArmOp::I64LtS {
11641                rd: Reg::R0,
11642                rnlo: Reg::R0,
11643                rnhi: Reg::R1,
11644                rmlo: Reg::R2,
11645                rmhi: Reg::R3,
11646            },
11647            ArmOp::I64LtU {
11648                rd: Reg::R0,
11649                rnlo: Reg::R0,
11650                rnhi: Reg::R1,
11651                rmlo: Reg::R2,
11652                rmhi: Reg::R3,
11653            },
11654            ArmOp::I64LeS {
11655                rd: Reg::R0,
11656                rnlo: Reg::R0,
11657                rnhi: Reg::R1,
11658                rmlo: Reg::R2,
11659                rmhi: Reg::R3,
11660            },
11661            ArmOp::I64LeU {
11662                rd: Reg::R0,
11663                rnlo: Reg::R0,
11664                rnhi: Reg::R1,
11665                rmlo: Reg::R2,
11666                rmhi: Reg::R3,
11667            },
11668            ArmOp::I64GtS {
11669                rd: Reg::R0,
11670                rnlo: Reg::R0,
11671                rnhi: Reg::R1,
11672                rmlo: Reg::R2,
11673                rmhi: Reg::R3,
11674            },
11675            ArmOp::I64GtU {
11676                rd: Reg::R0,
11677                rnlo: Reg::R0,
11678                rnhi: Reg::R1,
11679                rmlo: Reg::R2,
11680                rmhi: Reg::R3,
11681            },
11682            ArmOp::I64GeS {
11683                rd: Reg::R0,
11684                rnlo: Reg::R0,
11685                rnhi: Reg::R1,
11686                rmlo: Reg::R2,
11687                rmhi: Reg::R3,
11688            },
11689            ArmOp::I64GeU {
11690                rd: Reg::R0,
11691                rnlo: Reg::R0,
11692                rnhi: Reg::R1,
11693                rmlo: Reg::R2,
11694                rmhi: Reg::R3,
11695            },
11696        ];
11697
11698        for op in &ops {
11699            let code = encoder.encode(op).unwrap();
11700            assert!(
11701                code.len() >= 8,
11702                "i64 comparison {:?} should emit at least 8 bytes, got {}",
11703                op,
11704                code.len()
11705            );
11706        }
11707    }
11708
11709    #[test]
11710    fn test_encode_i64_const_zero_thumb2() {
11711        let encoder = ArmEncoder::new_thumb2();
11712        let op = ArmOp::I64Const {
11713            rdlo: Reg::R0,
11714            rdhi: Reg::R1,
11715            value: 0,
11716        };
11717        let code = encoder.encode(&op).unwrap();
11718        // MOVW R0, #0 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes
11719        assert_eq!(code.len(), 8, "I64Const(0) should be 8 bytes");
11720    }
11721
11722    #[test]
11723    fn test_encode_i64_const_negative_one_thumb2() {
11724        let encoder = ArmEncoder::new_thumb2();
11725        let op = ArmOp::I64Const {
11726            rdlo: Reg::R0,
11727            rdhi: Reg::R1,
11728            value: -1, // 0xFFFF_FFFF_FFFF_FFFF
11729        };
11730        let code = encoder.encode(&op).unwrap();
11731        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
11732        assert_eq!(code.len(), 16, "I64Const(-1) should be 16 bytes");
11733    }
11734
11735    // =========================================================================
11736    // Sub-word load/store encoding tests
11737    // =========================================================================
11738
11739    #[test]
11740    fn test_encode_ldrb_arm32() {
11741        let encoder = ArmEncoder::new_arm32();
11742        let op = ArmOp::Ldrb {
11743            rd: Reg::R0,
11744            addr: MemAddr::imm(Reg::R1, 4),
11745        };
11746        let code = encoder.encode(&op).unwrap();
11747        assert_eq!(code.len(), 4, "ARM32 LDRB should be 4 bytes");
11748        // LDRB R0, [R1, #4] = 0xE5D10004
11749        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11750        assert_eq!(encoded, 0xE5D10004, "Should encode LDRB R0, [R1, #4]");
11751    }
11752
11753    #[test]
11754    fn test_encode_strb_arm32() {
11755        let encoder = ArmEncoder::new_arm32();
11756        let op = ArmOp::Strb {
11757            rd: Reg::R0,
11758            addr: MemAddr::imm(Reg::R1, 0),
11759        };
11760        let code = encoder.encode(&op).unwrap();
11761        assert_eq!(code.len(), 4, "ARM32 STRB should be 4 bytes");
11762        // STRB R0, [R1, #0] = 0xE5C10000
11763        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11764        assert_eq!(encoded, 0xE5C10000, "Should encode STRB R0, [R1, #0]");
11765    }
11766
11767    #[test]
11768    fn test_encode_ldrh_arm32() {
11769        let encoder = ArmEncoder::new_arm32();
11770        let op = ArmOp::Ldrh {
11771            rd: Reg::R0,
11772            addr: MemAddr::imm(Reg::R1, 2),
11773        };
11774        let code = encoder.encode(&op).unwrap();
11775        assert_eq!(code.len(), 4, "ARM32 LDRH should be 4 bytes");
11776    }
11777
11778    #[test]
11779    fn test_encode_strh_arm32() {
11780        let encoder = ArmEncoder::new_arm32();
11781        let op = ArmOp::Strh {
11782            rd: Reg::R0,
11783            addr: MemAddr::imm(Reg::R1, 0),
11784        };
11785        let code = encoder.encode(&op).unwrap();
11786        assert_eq!(code.len(), 4, "ARM32 STRH should be 4 bytes");
11787    }
11788
11789    #[test]
11790    fn test_encode_ldrsb_arm32() {
11791        let encoder = ArmEncoder::new_arm32();
11792        let op = ArmOp::Ldrsb {
11793            rd: Reg::R0,
11794            addr: MemAddr::imm(Reg::R1, 0),
11795        };
11796        let code = encoder.encode(&op).unwrap();
11797        assert_eq!(code.len(), 4, "ARM32 LDRSB should be 4 bytes");
11798    }
11799
11800    #[test]
11801    fn test_encode_ldrsh_arm32() {
11802        let encoder = ArmEncoder::new_arm32();
11803        let op = ArmOp::Ldrsh {
11804            rd: Reg::R0,
11805            addr: MemAddr::imm(Reg::R1, 0),
11806        };
11807        let code = encoder.encode(&op).unwrap();
11808        assert_eq!(code.len(), 4, "ARM32 LDRSH should be 4 bytes");
11809    }
11810
11811    #[test]
11812    fn test_encode_ldrb_thumb2_16bit() {
11813        let encoder = ArmEncoder::new_thumb2();
11814        let op = ArmOp::Ldrb {
11815            rd: Reg::R0,
11816            addr: MemAddr::imm(Reg::R1, 4),
11817        };
11818        let code = encoder.encode(&op).unwrap();
11819        // Low registers + small offset -> 16-bit encoding
11820        assert_eq!(
11821            code.len(),
11822            2,
11823            "Thumb-2 LDRB with small offset should be 16-bit"
11824        );
11825    }
11826
11827    #[test]
11828    fn test_encode_ldrb_thumb2_32bit() {
11829        let encoder = ArmEncoder::new_thumb2();
11830        let op = ArmOp::Ldrb {
11831            rd: Reg::R0,
11832            addr: MemAddr::imm(Reg::R1, 100), // offset > 31 needs 32-bit
11833        };
11834        let code = encoder.encode(&op).unwrap();
11835        assert_eq!(
11836            code.len(),
11837            4,
11838            "Thumb-2 LDRB with large offset should be 32-bit"
11839        );
11840    }
11841
11842    #[test]
11843    fn test_encode_strb_thumb2_16bit() {
11844        let encoder = ArmEncoder::new_thumb2();
11845        let op = ArmOp::Strb {
11846            rd: Reg::R0,
11847            addr: MemAddr::imm(Reg::R1, 10),
11848        };
11849        let code = encoder.encode(&op).unwrap();
11850        assert_eq!(
11851            code.len(),
11852            2,
11853            "Thumb-2 STRB with small offset should be 16-bit"
11854        );
11855    }
11856
11857    #[test]
11858    fn test_encode_ldrh_thumb2_16bit() {
11859        let encoder = ArmEncoder::new_thumb2();
11860        let op = ArmOp::Ldrh {
11861            rd: Reg::R0,
11862            addr: MemAddr::imm(Reg::R1, 4), // offset aligned to 2, <= 62
11863        };
11864        let code = encoder.encode(&op).unwrap();
11865        assert_eq!(
11866            code.len(),
11867            2,
11868            "Thumb-2 LDRH with small aligned offset should be 16-bit"
11869        );
11870    }
11871
11872    #[test]
11873    fn test_encode_strh_thumb2_16bit() {
11874        let encoder = ArmEncoder::new_thumb2();
11875        let op = ArmOp::Strh {
11876            rd: Reg::R0,
11877            addr: MemAddr::imm(Reg::R1, 4),
11878        };
11879        let code = encoder.encode(&op).unwrap();
11880        assert_eq!(
11881            code.len(),
11882            2,
11883            "Thumb-2 STRH with small aligned offset should be 16-bit"
11884        );
11885    }
11886
11887    #[test]
11888    fn test_encode_ldrsb_thumb2() {
11889        let encoder = ArmEncoder::new_thumb2();
11890        let op = ArmOp::Ldrsb {
11891            rd: Reg::R0,
11892            addr: MemAddr::imm(Reg::R1, 0),
11893        };
11894        let code = encoder.encode(&op).unwrap();
11895        // LDRSB has no 16-bit immediate form, always 32-bit
11896        assert_eq!(code.len(), 4, "Thumb-2 LDRSB should be 32-bit");
11897    }
11898
11899    #[test]
11900    fn test_encode_ldrsh_thumb2() {
11901        let encoder = ArmEncoder::new_thumb2();
11902        let op = ArmOp::Ldrsh {
11903            rd: Reg::R0,
11904            addr: MemAddr::imm(Reg::R1, 0),
11905        };
11906        let code = encoder.encode(&op).unwrap();
11907        assert_eq!(code.len(), 4, "Thumb-2 LDRSH should be 32-bit");
11908    }
11909
11910    #[test]
11911    fn test_encode_memory_size_thumb2() {
11912        let encoder = ArmEncoder::new_thumb2();
11913        let op = ArmOp::MemorySize { rd: Reg::R0 };
11914        let code = encoder.encode(&op).unwrap();
11915        // R0 and R10 are not both low registers, so this needs careful handling
11916        assert!(!code.is_empty(), "MemorySize should produce code");
11917    }
11918
11919    #[test]
11920    fn test_encode_memory_grow_thumb2() {
11921        let encoder = ArmEncoder::new_thumb2();
11922        let op = ArmOp::MemoryGrow {
11923            rd: Reg::R0,
11924            rn: Reg::R0,
11925        };
11926        let code = encoder.encode(&op).unwrap();
11927        assert_eq!(code.len(), 4, "MemoryGrow (MVN) should be 32-bit Thumb-2");
11928    }
11929
11930    #[test]
11931    fn test_encode_subword_reg_offset_thumb2() {
11932        let encoder = ArmEncoder::new_thumb2();
11933
11934        // LDRB with register offset
11935        let op = ArmOp::Ldrb {
11936            rd: Reg::R0,
11937            addr: MemAddr::reg(Reg::R1, Reg::R2),
11938        };
11939        let code = encoder.encode(&op).unwrap();
11940        assert_eq!(
11941            code.len(),
11942            4,
11943            "Thumb-2 LDRB with reg offset should be 32-bit"
11944        );
11945
11946        // STRB with register offset
11947        let op = ArmOp::Strb {
11948            rd: Reg::R0,
11949            addr: MemAddr::reg(Reg::R1, Reg::R2),
11950        };
11951        let code = encoder.encode(&op).unwrap();
11952        assert_eq!(
11953            code.len(),
11954            4,
11955            "Thumb-2 STRB with reg offset should be 32-bit"
11956        );
11957
11958        // LDRH with register offset
11959        let op = ArmOp::Ldrh {
11960            rd: Reg::R0,
11961            addr: MemAddr::reg(Reg::R1, Reg::R2),
11962        };
11963        let code = encoder.encode(&op).unwrap();
11964        assert_eq!(
11965            code.len(),
11966            4,
11967            "Thumb-2 LDRH with reg offset should be 32-bit"
11968        );
11969
11970        // STRH with register offset
11971        let op = ArmOp::Strh {
11972            rd: Reg::R0,
11973            addr: MemAddr::reg(Reg::R1, Reg::R2),
11974        };
11975        let code = encoder.encode(&op).unwrap();
11976        assert_eq!(
11977            code.len(),
11978            4,
11979            "Thumb-2 STRH with reg offset should be 32-bit"
11980        );
11981    }
11982
11983    #[test]
11984    fn test_encode_subword_reg_imm_offset_thumb2() {
11985        let encoder = ArmEncoder::new_thumb2();
11986
11987        // LDRB with both register and immediate offset
11988        let op = ArmOp::Ldrb {
11989            rd: Reg::R0,
11990            addr: MemAddr::reg_imm(Reg::R1, Reg::R2, 4),
11991        };
11992        let code = encoder.encode(&op).unwrap();
11993        // ADD R12, R2, #4 (4 bytes) + LDRB R0, [R1, R12] (4 bytes) = 8 bytes
11994        assert_eq!(
11995            code.len(),
11996            8,
11997            "Thumb-2 LDRB with reg+imm offset should be 8 bytes"
11998        );
11999    }
12000
12001    // ========================================================================
12002    // Helium MVE encoding tests
12003    // ========================================================================
12004
12005    #[test]
12006    fn test_encode_mve_addi32_thumb2() {
12007        let encoder = ArmEncoder::new_thumb2();
12008        let op = ArmOp::MveAddI {
12009            qd: QReg::Q0,
12010            qn: QReg::Q1,
12011            qm: QReg::Q2,
12012            size: MveSize::S32,
12013        };
12014        let code = encoder.encode(&op).unwrap();
12015        assert_eq!(
12016            code.len(),
12017            4,
12018            "MVE VADD.I32 should be 4 bytes (Thumb-2 32-bit)"
12019        );
12020    }
12021
12022    #[test]
12023    fn test_encode_mve_subi16_thumb2() {
12024        let encoder = ArmEncoder::new_thumb2();
12025        let op = ArmOp::MveSubI {
12026            qd: QReg::Q0,
12027            qn: QReg::Q1,
12028            qm: QReg::Q2,
12029            size: MveSize::S16,
12030        };
12031        let code = encoder.encode(&op).unwrap();
12032        assert_eq!(code.len(), 4, "MVE VSUB.I16 should be 4 bytes");
12033    }
12034
12035    #[test]
12036    fn test_encode_mve_muli8_thumb2() {
12037        let encoder = ArmEncoder::new_thumb2();
12038        let op = ArmOp::MveMulI {
12039            qd: QReg::Q0,
12040            qn: QReg::Q1,
12041            qm: QReg::Q2,
12042            size: MveSize::S8,
12043        };
12044        let code = encoder.encode(&op).unwrap();
12045        assert_eq!(code.len(), 4, "MVE VMUL.I8 should be 4 bytes");
12046    }
12047
12048    #[test]
12049    fn test_encode_mve_bitwise_thumb2() {
12050        let encoder = ArmEncoder::new_thumb2();
12051
12052        let ops = vec![
12053            ArmOp::MveAnd {
12054                qd: QReg::Q0,
12055                qn: QReg::Q1,
12056                qm: QReg::Q2,
12057            },
12058            ArmOp::MveOrr {
12059                qd: QReg::Q0,
12060                qn: QReg::Q1,
12061                qm: QReg::Q2,
12062            },
12063            ArmOp::MveEor {
12064                qd: QReg::Q0,
12065                qn: QReg::Q1,
12066                qm: QReg::Q2,
12067            },
12068            ArmOp::MveBic {
12069                qd: QReg::Q0,
12070                qn: QReg::Q1,
12071                qm: QReg::Q2,
12072            },
12073        ];
12074        for op in ops {
12075            let code = encoder.encode(&op).unwrap();
12076            assert_eq!(code.len(), 4, "MVE bitwise op should be 4 bytes");
12077        }
12078    }
12079
12080    #[test]
12081    fn test_encode_mve_mvn_thumb2() {
12082        let encoder = ArmEncoder::new_thumb2();
12083        let op = ArmOp::MveMvn {
12084            qd: QReg::Q0,
12085            qm: QReg::Q1,
12086        };
12087        let code = encoder.encode(&op).unwrap();
12088        assert_eq!(code.len(), 4, "MVE VMVN should be 4 bytes");
12089    }
12090
12091    #[test]
12092    fn test_encode_mve_load_store_thumb2() {
12093        let encoder = ArmEncoder::new_thumb2();
12094
12095        let load = ArmOp::MveLoad {
12096            qd: QReg::Q0,
12097            addr: MemAddr::imm(Reg::R0, 16),
12098        };
12099        let code = encoder.encode(&load).unwrap();
12100        assert_eq!(code.len(), 4, "MVE VLDRW.32 should be 4 bytes");
12101
12102        let store = ArmOp::MveStore {
12103            qd: QReg::Q1,
12104            addr: MemAddr::imm(Reg::R1, 0),
12105        };
12106        let code = encoder.encode(&store).unwrap();
12107        assert_eq!(code.len(), 4, "MVE VSTRW.32 should be 4 bytes");
12108    }
12109
12110    #[test]
12111    fn test_encode_mve_const_thumb2() {
12112        let encoder = ArmEncoder::new_thumb2();
12113        let op = ArmOp::MveConst {
12114            qd: QReg::Q0,
12115            bytes: [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0],
12116        };
12117        let code = encoder.encode(&op).unwrap();
12118        // Should be 4 words of (MOVW R12 + VMOV Sn) = 4 * (4+4) = 32 bytes min
12119        // Some words with hi16=0 skip MOVT, so length varies
12120        assert!(
12121            code.len() >= 24,
12122            "MVE const should produce multiple instructions"
12123        );
12124    }
12125
12126    #[test]
12127    fn test_encode_mve_dup_thumb2() {
12128        let encoder = ArmEncoder::new_thumb2();
12129        let op = ArmOp::MveDup {
12130            qd: QReg::Q0,
12131            rn: Reg::R0,
12132            size: MveSize::S32,
12133        };
12134        let code = encoder.encode(&op).unwrap();
12135        assert_eq!(code.len(), 4, "MVE VDUP.32 should be 4 bytes");
12136    }
12137
12138    #[test]
12139    fn test_encode_mve_extract_lane_thumb2() {
12140        let encoder = ArmEncoder::new_thumb2();
12141        let op = ArmOp::MveExtractLane {
12142            rd: Reg::R0,
12143            qn: QReg::Q1,
12144            lane: 2,
12145            size: MveSize::S32,
12146        };
12147        let code = encoder.encode(&op).unwrap();
12148        assert_eq!(code.len(), 4, "MVE extract lane should be 4 bytes");
12149    }
12150
12151    #[test]
12152    fn test_encode_mve_insert_lane_thumb2() {
12153        let encoder = ArmEncoder::new_thumb2();
12154        let op = ArmOp::MveInsertLane {
12155            qd: QReg::Q0,
12156            rn: Reg::R1,
12157            lane: 3,
12158            size: MveSize::S32,
12159        };
12160        let code = encoder.encode(&op).unwrap();
12161        assert_eq!(code.len(), 4, "MVE insert lane should be 4 bytes");
12162    }
12163
12164    #[test]
12165    fn test_encode_mve_addf32_thumb2() {
12166        let encoder = ArmEncoder::new_thumb2();
12167        let op = ArmOp::MveAddF32 {
12168            qd: QReg::Q0,
12169            qn: QReg::Q1,
12170            qm: QReg::Q2,
12171        };
12172        let code = encoder.encode(&op).unwrap();
12173        assert_eq!(code.len(), 4, "MVE VADD.F32 should be 4 bytes");
12174    }
12175
12176    #[test]
12177    fn test_encode_mve_divf32_thumb2() {
12178        let encoder = ArmEncoder::new_thumb2();
12179        let op = ArmOp::MveDivF32 {
12180            qd: QReg::Q0,
12181            qn: QReg::Q1,
12182            qm: QReg::Q2,
12183        };
12184        let code = encoder.encode(&op).unwrap();
12185        // Lane-wise: 4 x VDIV.F32 = 4 x 4 = 16 bytes
12186        assert_eq!(
12187            code.len(),
12188            16,
12189            "MVE VDIV.F32 (lane-wise) should be 16 bytes"
12190        );
12191    }
12192
12193    #[test]
12194    fn test_encode_mve_sqrtf32_thumb2() {
12195        let encoder = ArmEncoder::new_thumb2();
12196        let op = ArmOp::MveSqrtF32 {
12197            qd: QReg::Q0,
12198            qm: QReg::Q1,
12199        };
12200        let code = encoder.encode(&op).unwrap();
12201        // Lane-wise: 4 x VSQRT.F32 = 4 x 4 = 16 bytes
12202        assert_eq!(
12203            code.len(),
12204            16,
12205            "MVE VSQRT.F32 (lane-wise) should be 16 bytes"
12206        );
12207    }
12208
12209    #[test]
12210    fn test_encode_mve_negf32_thumb2() {
12211        let encoder = ArmEncoder::new_thumb2();
12212        let op = ArmOp::MveNegF32 {
12213            qd: QReg::Q0,
12214            qm: QReg::Q1,
12215        };
12216        let code = encoder.encode(&op).unwrap();
12217        assert_eq!(code.len(), 4, "MVE VNEG.F32 should be 4 bytes");
12218    }
12219
12220    #[test]
12221    fn test_encode_mve_absf32_thumb2() {
12222        let encoder = ArmEncoder::new_thumb2();
12223        let op = ArmOp::MveAbsF32 {
12224            qd: QReg::Q0,
12225            qm: QReg::Q1,
12226        };
12227        let code = encoder.encode(&op).unwrap();
12228        assert_eq!(code.len(), 4, "MVE VABS.F32 should be 4 bytes");
12229    }
12230
12231    /// VCR-RA-001 / immediate-folding precondition: pins the Thumb-2 `AND`
12232    /// immediate encoding for the byte range and documents its bound.
12233    ///
12234    /// The `And { Operand2::Imm }` encoder packs the low 12 bits straight into
12235    /// the `i:imm3:imm8` field WITHOUT applying ThumbExpandImm (the modified-
12236    /// immediate expansion). For `imm <= 0xFF` (e.g. gale's int8 clamps
12237    /// `#0x7e` / `#0x7f`) that is correct — `i:imm3 = 0000` means "imm8
12238    /// zero-extended". So `and r2, r0, #0x7e` encodes to the canonical
12239    /// `00 f0 7e 02`. For `imm >= 0x100` the field would need a true
12240    /// ThumbExpandImm pattern (rotation / replication), which is NOT
12241    /// implemented here — so **immediate folding must gate on `imm <= 0xFF`**
12242    /// until the encoder is hardened to ThumbExpandImm/Ok-or-Err (the
12243    /// "encoder must be Ok-or-Err, never silently wrong" principle, #180/#185).
12244    /// This bound covers the measured `flat_flight` waste (#209).
12245    #[test]
12246    fn and_immediate_encodes_correctly_in_byte_range_documents_fold_bound() {
12247        let encoder = ArmEncoder::new_thumb2();
12248        let op = ArmOp::And {
12249            rd: Reg::R2,
12250            rn: Reg::R0,
12251            op2: Operand2::Imm(0x7e),
12252        };
12253        let code = encoder.encode(&op).unwrap();
12254        assert_eq!(
12255            code,
12256            vec![0x00, 0xf0, 0x7e, 0x02],
12257            "and r2, r0, #0x7e must encode to the canonical AND.W T1 (imm8=0x7e)"
12258        );
12259    }
12260
12261    /// #255: the shared ThumbExpandImm reverse-encoder underpinning the
12262    /// data-processing immediate fix. Encodable modified immediates round-trip to
12263    /// the expected `i:imm3:imm8` field; a genuinely non-modified value is `None`
12264    /// (caller must materialize into a register). Note `1000 = 0xFA ror 30` *is*
12265    /// representable (field 0xF7A) — the old encoder mis-encoded it (raw 0x3E8);
12266    /// this encodes it correctly.
12267    #[test]
12268    fn try_thumb_expand_imm_encodes_modified_immediates() {
12269        assert_eq!(try_thumb_expand_imm(0x7e), Some(0x07e)); // zero-extended byte
12270        assert_eq!(try_thumb_expand_imm(0xff), Some(0x0ff));
12271        assert_eq!(try_thumb_expand_imm(0x0001_0001), Some(0x101)); // 0x00XY00XY
12272        assert_eq!(try_thumb_expand_imm(0xff00_ff00), Some(0x2ff)); // 0xXY00XY00
12273        assert_eq!(try_thumb_expand_imm(0xffff_ffff), Some(0x3ff)); // 0xXYXYXYXY
12274        assert_eq!(try_thumb_expand_imm(0x100), Some(0xf80)); // 0x80 ror 31
12275        assert_eq!(try_thumb_expand_imm(0x8000_0000), Some(0x400)); // 0x80 ror 8
12276        assert_eq!(try_thumb_expand_imm(1000), Some(0xf7a)); // 0xFA ror 30
12277        // Genuinely unrepresentable (bits too far apart for an 8-bit window).
12278        assert_eq!(try_thumb_expand_imm(0x101), None);
12279        assert_eq!(try_thumb_expand_imm(0x12345), None);
12280    }
12281
12282    /// #255: CMP/ADDS/SUBS encode any valid modified immediate correctly, and
12283    /// ERROR (not silently mis-encode) on a genuinely unrepresentable one,
12284    /// forcing the selector to materialize into a register — closing the
12285    /// silent-miscompile class of #251/#253.
12286    #[test]
12287    fn cmp_adds_subs_immediate_error_on_non_modified_imm() {
12288        let encoder = ArmEncoder::new_thumb2();
12289        // cmp r0, #0xff → valid → Ok; cmp r0, #1000 → valid (0xFA ror 30) → Ok.
12290        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 0xff).is_ok());
12291        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 1000).is_ok());
12292        // cmp r0, #0x101 → NOT a modified immediate → Err (materialize-reg).
12293        assert!(
12294            encoder.encode_thumb32_cmp_imm(&Reg::R0, 0x101).is_err(),
12295            "cmp #0x101 must error, not compare the wrong constant"
12296        );
12297        assert!(
12298            encoder
12299                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x101)
12300                .is_err()
12301        );
12302        assert!(
12303            encoder
12304                .encode_thumb32_subs(&Reg::R0, &Reg::R0, 0x101)
12305                .is_err()
12306        );
12307        // ...but a valid modified immediate still encodes.
12308        assert!(
12309            encoder
12310                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x80)
12311                .is_ok()
12312        );
12313    }
12314
12315    /// #257: MLA (multiply-accumulate) encodes as MLS without the bit-4 op flag.
12316    /// `mla r2, r3, r4, r8` (rd=r2, rn=r3, rm=r4, ra=r8) → Thumb-2 `03 fb 04 82`.
12317    #[test]
12318    fn mla_thumb2_encodes_correctly() {
12319        let encoder = ArmEncoder::new_thumb2();
12320        let code = encoder
12321            .encode(&ArmOp::Mla {
12322                rd: Reg::R2,
12323                rn: Reg::R3,
12324                rm: Reg::R4,
12325                ra: Reg::R8,
12326            })
12327            .unwrap();
12328        // hw1 = 0xFB03, hw2 = (8<<12)|(2<<8)|4 = 0x8204
12329        assert_eq!(code, vec![0x03, 0xfb, 0x04, 0x82]);
12330    }
12331
12332    /// #259: LDR/STR (and sub-word) immediate-offset encoders truncated
12333    /// `offset & 0xFFF`, silently targeting the wrong address for offset >= 4096.
12334    /// They now error (the selector must use register-offset addressing) — the
12335    /// load/store sibling of the #253/#255 class. Offsets <= 4095 still encode.
12336    #[test]
12337    fn ldst_imm12_offset_errors_when_out_of_range() {
12338        let encoder = ArmEncoder::new_thumb2();
12339        // offset 0xFFF (4095): valid → Ok; ldr r0, [r1, #4095].
12340        assert!(
12341            encoder
12342                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0xFFF)
12343                .is_ok()
12344        );
12345        // offset 0x1000 (4096): out of imm12 range → Err (not & 0xFFF → #0).
12346        assert!(
12347            encoder
12348                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0x1000)
12349                .is_err(),
12350            "ldr offset 4096 must error, not wrap to 0"
12351        );
12352        assert!(
12353            encoder
12354                .encode_thumb32_str(&Reg::R0, &Reg::R1, 0x1000)
12355                .is_err()
12356        );
12357        assert!(
12358            encoder
12359                .encode_thumb32_ldrb_imm(&Reg::R0, &Reg::R1, 5000)
12360                .is_err()
12361        );
12362        assert!(
12363            encoder
12364                .encode_thumb32_strh_imm(&Reg::R0, &Reg::R1, 5000)
12365                .is_err()
12366        );
12367    }
12368
12369    /// Latent miscompile fix: ADD/SUB with a >0xFF immediate (e.g.
12370    /// `add sp, sp, #frame` for a >=256-byte frame) used ADD.W (T3), whose
12371    /// `i:imm3:imm8` is a ThumbExpandImm modified immediate — so `#256` silently
12372    /// encoded as `#0` (stack corruption). Use ADDW/SUBW (T4), a PLAIN 12-bit
12373    /// immediate, for 0x100..=0xFFF; keep T3 for <=0xFF (bit-identical); error
12374    /// beyond 4095.
12375    #[test]
12376    fn add_sub_large_immediate_use_addw_subw_not_misencoded() {
12377        let encoder = ArmEncoder::new_thumb2();
12378        // add sp, sp, #256  →  ADDW (T4) SP, SP, #256  =  0d f2 00 1d
12379        assert_eq!(
12380            encoder
12381                .encode(&ArmOp::Add {
12382                    rd: Reg::SP,
12383                    rn: Reg::SP,
12384                    op2: Operand2::Imm(256),
12385                })
12386                .unwrap(),
12387            vec![0x0d, 0xf2, 0x00, 0x1d],
12388            "add sp,sp,#256 must be ADDW (plain imm12), not a mis-encoded ADD.W"
12389        );
12390        // sub sp, sp, #256  →  SUBW (T4) SP, SP, #256  =  ad f2 00 1d
12391        assert_eq!(
12392            encoder
12393                .encode(&ArmOp::Sub {
12394                    rd: Reg::SP,
12395                    rn: Reg::SP,
12396                    op2: Operand2::Imm(256),
12397                })
12398                .unwrap(),
12399            vec![0xad, 0xf2, 0x00, 0x1d],
12400        );
12401        // > 4095 has no single-instruction encoding → error, not silent wrong.
12402        assert!(
12403            encoder
12404                .encode(&ArmOp::Add {
12405                    rd: Reg::SP,
12406                    rn: Reg::SP,
12407                    op2: Operand2::Imm(5000),
12408                })
12409                .is_err(),
12410            "add #5000 must error (no single ADDW), not mis-encode"
12411        );
12412    }
12413
12414    /// Closes the data-proc immediate class: AND and CMN now go through
12415    /// `try_thumb_expand_imm` like ORR/EOR/CMP — correct for any modified
12416    /// immediate, `Err` (not raw-pack / NOP) on an un-encodable one. The byte
12417    /// range stays bit-identical (`and r2,r0,#0x7e` is unchanged).
12418    #[test]
12419    fn and_cmn_immediate_thumb_expand_else_error() {
12420        let encoder = ArmEncoder::new_thumb2();
12421        // byte range unchanged (bit-identical with the pre-retrofit encoding)
12422        assert_eq!(
12423            encoder
12424                .encode(&ArmOp::And {
12425                    rd: Reg::R2,
12426                    rn: Reg::R0,
12427                    op2: Operand2::Imm(0x7e),
12428                })
12429                .unwrap(),
12430            vec![0x00, 0xf0, 0x7e, 0x02],
12431        );
12432        // a valid replicated modified immediate now encodes (was silently wrong)
12433        assert!(
12434            encoder
12435                .encode(&ArmOp::And {
12436                    rd: Reg::R2,
12437                    rn: Reg::R0,
12438                    op2: Operand2::Imm(0xff00ff00u32 as i32),
12439                })
12440                .is_ok()
12441        );
12442        // a genuinely un-encodable immediate errors (AND was raw-pack; CMN NOP)
12443        assert!(
12444            encoder
12445                .encode(&ArmOp::And {
12446                    rd: Reg::R2,
12447                    rn: Reg::R0,
12448                    op2: Operand2::Imm(0x101),
12449                })
12450                .is_err()
12451        );
12452        assert!(
12453            encoder
12454                .encode(&ArmOp::Cmn {
12455                    rn: Reg::R0,
12456                    op2: Operand2::Imm(0x101),
12457                })
12458                .is_err(),
12459            "CMN #0x101 must error, not emit a NOP"
12460        );
12461    }
12462
12463    /// VCR-RA-001: ORR/EOR with a small immediate must encode the real
12464    /// instruction (not a silent `0xBF00` NOP). Pins the byte range and the
12465    /// Ok-or-Err bound that makes future Or/Eor immediate folding safe.
12466    #[test]
12467    fn orr_eor_immediate_encode_in_byte_range_else_error() {
12468        let encoder = ArmEncoder::new_thumb2();
12469        // orr r2, r0, #0x7e  →  ORR.W T1, imm8=0x7e
12470        assert_eq!(
12471            encoder
12472                .encode(&ArmOp::Orr {
12473                    rd: Reg::R2,
12474                    rn: Reg::R0,
12475                    op2: Operand2::Imm(0x7e),
12476                })
12477                .unwrap(),
12478            vec![0x40, 0xf0, 0x7e, 0x02],
12479        );
12480        // eor r2, r0, #0x7e  →  EOR.W T1, imm8=0x7e
12481        assert_eq!(
12482            encoder
12483                .encode(&ArmOp::Eor {
12484                    rd: Reg::R2,
12485                    rn: Reg::R0,
12486                    op2: Operand2::Imm(0x7e),
12487                })
12488                .unwrap(),
12489            vec![0x80, 0xf0, 0x7e, 0x02],
12490        );
12491        // Out-of-range immediates error rather than silently mis-encode / NOP.
12492        assert!(
12493            encoder
12494                .encode(&ArmOp::Orr {
12495                    rd: Reg::R2,
12496                    rn: Reg::R0,
12497                    op2: Operand2::Imm(0x140),
12498                })
12499                .is_err(),
12500            "ORR #0x140 must error, not emit a NOP"
12501        );
12502    }
12503
12504    #[test]
12505    fn test_encode_mve_different_qregs() {
12506        let encoder = ArmEncoder::new_thumb2();
12507
12508        // Test that different Q-register numbers produce different encodings
12509        let op1 = ArmOp::MveAddI {
12510            qd: QReg::Q0,
12511            qn: QReg::Q0,
12512            qm: QReg::Q0,
12513            size: MveSize::S32,
12514        };
12515        let op2 = ArmOp::MveAddI {
12516            qd: QReg::Q3,
12517            qn: QReg::Q5,
12518            qm: QReg::Q7,
12519            size: MveSize::S32,
12520        };
12521        let code1 = encoder.encode(&op1).unwrap();
12522        let code2 = encoder.encode(&op2).unwrap();
12523        assert_ne!(
12524            code1, code2,
12525            "Different Q-registers should produce different encodings"
12526        );
12527    }
12528
12529    #[test]
12530    fn test_encode_mve_arm32_loud_err() {
12531        // #615: MVE (Helium) is Thumb-2-only. The ARM32 encoder used to emit
12532        // a silent NOP here (dropping the vector op); it must now be a typed
12533        // Err so a broken "MVE implies Thumb" invariant fails loudly.
12534        let encoder = ArmEncoder::new_arm32();
12535        let op = ArmOp::MveAddI {
12536            qd: QReg::Q0,
12537            qn: QReg::Q1,
12538            qm: QReg::Q2,
12539            size: MveSize::S32,
12540        };
12541        let err = encoder
12542            .encode(&op)
12543            .expect_err("ARM32 MVE must be a loud Err, not a silent NOP (#615)");
12544        assert!(
12545            err.to_string().contains("Thumb-2 only"),
12546            "unexpected error message: {err}"
12547        );
12548    }
12549}