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            // GI-FPU-002 (#369): no synth A32 target carries an FPU (cortex-r5
2086            // has none — the selector declines every float op there), so the
2087            // A32 encoder refuses loudly instead of shipping an untested
2088            // encoding (#615: never a silent wrong byte).
2089            ArmOp::F32DemoteF64 { .. } => {
2090                return Err(synth_core::Error::synthesis(
2091                    "F32DemoteF64 has no A32 encoding (no A32 target has an FPU)",
2092                ));
2093            }
2094            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => {
2095                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?
2096            }
2097            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => {
2098                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?
2099            }
2100            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
2101                return Err(synth_core::Error::synthesis(
2102                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
2103                ));
2104            }
2105            ArmOp::I32TruncF64S { rd, dm } => {
2106                return self.encode_arm_i32_trunc_f64(rd, dm, true);
2107            }
2108            ArmOp::I32TruncF64U { rd, dm } => {
2109                return self.encode_arm_i32_trunc_f64(rd, dm, false);
2110            }
2111            // #615: multi-instruction i64 sequences — expanded to real A32 by
2112            // `encode_arm_expanded`, no longer "Thumb-2 only" NOPs.
2113            ArmOp::I64SetCond { .. }
2114            | ArmOp::I64SetCondZ { .. }
2115            | ArmOp::I64Mul { .. }
2116            | ArmOp::I64Shl { .. }
2117            | ArmOp::I64ShrS { .. }
2118            | ArmOp::I64ShrU { .. }
2119            | ArmOp::I64Rotl { .. }
2120            | ArmOp::I64Rotr { .. } => {
2121                unreachable!("handled by encode_arm_expanded (#615)")
2122            }
2123
2124            // MVE instructions — Thumb-2 only (Cortex-M55 is always Thumb-2)
2125            ArmOp::MveLoad { .. }
2126            | ArmOp::MveStore { .. }
2127            | ArmOp::MveConst { .. }
2128            | ArmOp::MveAnd { .. }
2129            | ArmOp::MveOrr { .. }
2130            | ArmOp::MveEor { .. }
2131            | ArmOp::MveMvn { .. }
2132            | ArmOp::MveBic { .. }
2133            | ArmOp::MveAddI { .. }
2134            | ArmOp::MveSubI { .. }
2135            | ArmOp::MveMulI { .. }
2136            | ArmOp::MveNegI { .. }
2137            | ArmOp::MveCmpEqI { .. }
2138            | ArmOp::MveCmpNeI { .. }
2139            | ArmOp::MveCmpLtS { .. }
2140            | ArmOp::MveCmpLtU { .. }
2141            | ArmOp::MveCmpGtS { .. }
2142            | ArmOp::MveCmpGtU { .. }
2143            | ArmOp::MveCmpLeS { .. }
2144            | ArmOp::MveCmpLeU { .. }
2145            | ArmOp::MveCmpGeS { .. }
2146            | ArmOp::MveCmpGeU { .. }
2147            | ArmOp::MveDup { .. }
2148            | ArmOp::MveExtractLane { .. }
2149            | ArmOp::MveInsertLane { .. }
2150            | ArmOp::MveAddF32 { .. }
2151            | ArmOp::MveSubF32 { .. }
2152            | ArmOp::MveMulF32 { .. }
2153            | ArmOp::MveNegF32 { .. }
2154            | ArmOp::MveAbsF32 { .. }
2155            | ArmOp::MveCmpEqF32 { .. }
2156            | ArmOp::MveCmpNeF32 { .. }
2157            | ArmOp::MveCmpLtF32 { .. }
2158            | ArmOp::MveCmpLeF32 { .. }
2159            | ArmOp::MveCmpGtF32 { .. }
2160            | ArmOp::MveCmpGeF32 { .. }
2161            | ArmOp::MveDupF32 { .. }
2162            | ArmOp::MveExtractLaneF32 { .. }
2163            | ArmOp::MveReplaceLaneF32 { .. }
2164            | ArmOp::MveDivF32 { .. }
2165            | ArmOp::MveSqrtF32 { .. } => {
2166                // MVE (Helium) is a Thumb-2-only extension (Cortex-M55); there
2167                // is no A32 encoding. The selector only emits MVE ops for
2168                // Thumb targets — a NOP here silently dropped the vector op
2169                // if that invariant ever broke (#615 class). Err keeps the
2170                // encoder total and the failure loud.
2171                return Err(synth_core::Error::synthesis(format!(
2172                    "MVE op {op:?} has no A32 (ARM-mode) encoding — MVE is Thumb-2 only (#615)"
2173                )));
2174            }
2175        };
2176
2177        // ARM32 instructions are little-endian
2178        Ok(instr.to_le_bytes().to_vec())
2179    }
2180
2181    // === ARM32 VFP multi-instruction helpers ===
2182
2183    /// Encode F32 comparison as ARM32: VCMP.F32 + VMRS + MOV rd,#0 + MOVcond rd,#1
2184    fn encode_arm_f32_compare(
2185        &self,
2186        rd: &Reg,
2187        sn: &VfpReg,
2188        sm: &VfpReg,
2189        cond_code: u32,
2190    ) -> Result<Vec<u8>> {
2191        let mut bytes = Vec::new();
2192
2193        // VCMP.F32 Sn, Sm: 0xEEB40A40 with Sn in Vd position, Sm in Vm position
2194        let sn_num = vfp_sreg_to_num(sn)?;
2195        let sm_num = vfp_sreg_to_num(sm)?;
2196        let (vd, d) = encode_sreg(sn_num);
2197        let (vm, m) = encode_sreg(sm_num);
2198        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2199        bytes.extend_from_slice(&vcmp.to_le_bytes());
2200
2201        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
2202        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2203
2204        // MOV rd, #0: 0xE3A0_0000 | (rd << 12)
2205        let rd_bits = reg_to_bits(rd);
2206        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2207        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2208
2209        // MOVcond rd, #1: cond(4) | 0011 1010 0000 rd(4) 0000 0000 0001
2210        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2211        bytes.extend_from_slice(&mov_one.to_le_bytes());
2212
2213        Ok(bytes)
2214    }
2215
2216    /// Encode F32 constant load as ARM32: MOVW Rt,#lo16 + MOVT Rt,#hi16 + VMOV Sd,Rt
2217    fn encode_arm_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
2218        let mut bytes = Vec::new();
2219        let bits = value.to_bits();
2220
2221        // Use R12 as temp register for constant loading
2222        let rt: u32 = 12; // R12/IP
2223
2224        // MOVW R12, #lo16: 0xE300_C000 | (imm4 << 16) | imm12
2225        let lo16 = bits & 0xFFFF;
2226        let movw = 0xE3000000 | (rt << 12) | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2227        bytes.extend_from_slice(&movw.to_le_bytes());
2228
2229        // MOVT R12, #hi16: 0xE340_C000 | (imm4 << 16) | imm12
2230        let hi16 = (bits >> 16) & 0xFFFF;
2231        let movt = 0xE3400000 | (rt << 12) | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2232        bytes.extend_from_slice(&movt.to_le_bytes());
2233
2234        // VMOV Sd, R12
2235        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
2236        bytes.extend_from_slice(&vmov.to_le_bytes());
2237
2238        Ok(bytes)
2239    }
2240
2241    /// Encode VMOV + VCVT.F32.S32/U32 as ARM32
2242    fn encode_arm_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2243        let mut bytes = Vec::new();
2244
2245        // VMOV Sd, Rm — move integer to VFP register
2246        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
2247        bytes.extend_from_slice(&vmov.to_le_bytes());
2248
2249        // VCVT.F32.S32 Sd, Sd (signed) or VCVT.F32.U32 Sd, Sd (unsigned).
2250        // The "op" bit (bit 7) selects signedness: 1 = signed (S32), 0 =
2251        // unsigned (U32). So signed = 0xEEB80AC0, unsigned = 0xEEB80A40 —
2252        // objdump confirms 0xEEB80A40 decodes to `vcvt.f32.u32` (GI-FPU-002:
2253        // the two were previously swapped, silently making `convert_i32_s`
2254        // an unsigned conversion).
2255        let sd_num = vfp_sreg_to_num(sd)?;
2256        let (vd, d) = encode_sreg(sd_num);
2257        let (vm, m) = encode_sreg(sd_num); // same register as source
2258        let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
2259        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2260        bytes.extend_from_slice(&vcvt.to_le_bytes());
2261
2262        Ok(bytes)
2263    }
2264
2265    /// Encode F32 rounding pseudo-op as ARM32 via VCVT to integer and back.
2266    /// mode: 0b00=nearest, 0b01=floor(-Inf), 0b10=ceil(+Inf), 0b11=trunc(zero)
2267    /// Strategy: VCVT.S32.F32 Sd, Sm (toward zero), then VCVT.F32.S32 Sd, Sd
2268    /// For ceil/floor/nearest, we use VCVTR (round toward mode) + convert back.
2269    /// Simplified: convert to int (toward zero for trunc) then back to float.
2270    /// Encode F32 rounding as ARM32.
2271    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2272    ///
2273    /// For trunc (mode=0b11): uses VCVTR.S32.F32 (always rounds toward zero).
2274    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant
2275    /// which honours FPSCR rmode), then restores FPSCR.
2276    fn encode_arm_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2277        let mut bytes = Vec::new();
2278        let sm_num = vfp_sreg_to_num(sm)?;
2279        let sd_num = vfp_sreg_to_num(sd)?;
2280        let (vd_s, d_s) = encode_sreg(sd_num);
2281        let (vm_s, m_s) = encode_sreg(sm_num);
2282
2283        if mode == 0b11 {
2284            // Trunc (toward zero): VCVTR.S32.F32 — the "R" variant always truncates.
2285            // 0xEEBD0AC0: bit[7]=1 => round toward zero regardless of FPSCR
2286            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2287            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2288        } else {
2289            // ceil/floor/nearest: manipulate FPSCR rounding mode
2290            let rt: u32 = 12; // R12/IP as temp
2291
2292            // VMRS R12, FPSCR
2293            let vmrs = 0xEEF10A10 | (rt << 12);
2294            bytes.extend_from_slice(&vmrs.to_le_bytes());
2295
2296            // BIC R12, R12, #(3 << 22) — clear RMode bits [23:22]
2297            // 3<<22 = 0x00C00000. ARM rotated imm: 0x03 ror 10 (rotation=5, imm8=0x03)
2298            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2299            bytes.extend_from_slice(&bic.to_le_bytes());
2300
2301            // ORR R12, R12, #(mode << 22) — set desired rounding mode
2302            if mode != 0 {
2303                // mode<<22: rotation=5, imm8=mode
2304                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2305                bytes.extend_from_slice(&orr.to_le_bytes());
2306            }
2307
2308            // VMSR FPSCR, R12
2309            let vmsr = 0xEEE10A10 | (rt << 12);
2310            bytes.extend_from_slice(&vmsr.to_le_bytes());
2311
2312            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rounding mode
2313            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2314            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2315
2316            // Restore FPSCR: clear rmode bits back to nearest (default)
2317            bytes.extend_from_slice(&vmrs.to_le_bytes());
2318            bytes.extend_from_slice(&bic.to_le_bytes());
2319            bytes.extend_from_slice(&vmsr.to_le_bytes());
2320        }
2321
2322        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
2323        let (vd2, d2) = encode_sreg(sd_num);
2324        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
2325        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2326
2327        Ok(bytes)
2328    }
2329
2330    /// Encode F32 min/max as ARM32: VCMP + VMRS + conditional VMOV
2331    fn encode_arm_f32_minmax(
2332        &self,
2333        sd: &VfpReg,
2334        sn: &VfpReg,
2335        sm: &VfpReg,
2336        is_min: bool,
2337    ) -> Result<Vec<u8>> {
2338        let mut bytes = Vec::new();
2339        let sn_num = vfp_sreg_to_num(sn)?;
2340        let sm_num = vfp_sreg_to_num(sm)?;
2341        let sd_num = vfp_sreg_to_num(sd)?;
2342
2343        // VMOV Sd, Sn (start with first operand)
2344        let (vd, d) = encode_sreg(sd_num);
2345        let (vn, n) = encode_sreg(sn_num);
2346        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2347        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2348
2349        // VCMP.F32 Sn, Sm
2350        let (vm, m) = encode_sreg(sm_num);
2351        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2352        bytes.extend_from_slice(&vcmp.to_le_bytes());
2353
2354        // VMRS APSR_nzcv, FPSCR
2355        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2356
2357        // For min: if Sn > Sm (GT), use Sm. Condition = GT (0xC)
2358        // For max: if Sn < Sm (MI/LT), use Sm. Condition = MI (0x4)
2359        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2360
2361        // VMOV{cond} Sd, Sm — conditional VMOV
2362        let vmov_cond = (cond << 28) | 0x0EB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2363        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2364
2365        Ok(bytes)
2366    }
2367
2368    /// Encode F32 copysign as ARM32: extract sign from Sm, magnitude from Sn
2369    fn encode_arm_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2370        let mut bytes = Vec::new();
2371
2372        // VMOV R12, Sm (get sign source bits)
2373        let vmov_sm = encode_vmov_core_sreg(false, sm, &Reg::R12)?;
2374        bytes.extend_from_slice(&vmov_sm.to_le_bytes());
2375
2376        // VMOV R0, Sn (get magnitude source bits) — use R0 as temp
2377        let vmov_sn = encode_vmov_core_sreg(false, sn, &Reg::R0)?;
2378        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2379
2380        // AND R12, R12, #0x80000000 (keep only sign bit)
2381        // Thumb-2 constant 0x80000000 needs special encoding; in ARM32 use rotated imm
2382        // 0x80000000 = 0x02 rotated right by 2 (rotation=1, imm8=0x02)
2383        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2384        bytes.extend_from_slice(&and_sign.to_le_bytes());
2385
2386        // BIC R0, R0, #0x80000000 (clear sign bit from magnitude)
2387        // R0 = register 0, so Rn and Rd fields are 0
2388        let bic_sign = 0xE3C00000u32 | (1 << 8) | 0x02;
2389        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2390
2391        // ORR R0, R0, R12 (combine sign + magnitude)
2392        // R0 = register 0, so Rn and Rd fields are 0
2393        let orr = 0xE1800000u32 | 12;
2394        bytes.extend_from_slice(&orr.to_le_bytes());
2395
2396        // VMOV Sd, R0
2397        let vmov_result = encode_vmov_core_sreg(true, sd, &Reg::R0)?;
2398        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2399
2400        Ok(bytes)
2401    }
2402
2403    /// Encode F64 comparison as ARM32: VCMP.F64 + VMRS + MOV rd,#0 + MOVcond rd,#1
2404    fn encode_arm_f64_compare(
2405        &self,
2406        rd: &Reg,
2407        dn: &VfpReg,
2408        dm: &VfpReg,
2409        cond_code: u32,
2410    ) -> Result<Vec<u8>> {
2411        let mut bytes = Vec::new();
2412
2413        // VCMP.F64 Dn, Dm: 0xEEB40B40 with Dn in Vd position, Dm in Vm position
2414        let dn_num = vfp_dreg_to_num(dn)?;
2415        let dm_num = vfp_dreg_to_num(dm)?;
2416        let (vd, d) = encode_dreg(dn_num);
2417        let (vm, m) = encode_dreg(dm_num);
2418        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2419        bytes.extend_from_slice(&vcmp.to_le_bytes());
2420
2421        // VMRS APSR_nzcv, FPSCR
2422        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2423
2424        // MOV rd, #0
2425        let rd_bits = reg_to_bits(rd);
2426        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2427        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2428
2429        // MOVcond rd, #1
2430        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2431        bytes.extend_from_slice(&mov_one.to_le_bytes());
2432
2433        Ok(bytes)
2434    }
2435
2436    /// Encode F64 constant load as ARM32: MOVW + MOVT + MOVW + MOVT + VMOV
2437    fn encode_arm_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
2438        let mut bytes = Vec::new();
2439        let bits = value.to_bits();
2440        let lo32 = bits as u32;
2441        let hi32 = (bits >> 32) as u32;
2442
2443        // Load low 32 bits into R0 (Rd field = 0 for R0)
2444        let lo16 = lo32 & 0xFFFF;
2445        let movw_r0 = 0xE3000000 | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2446        bytes.extend_from_slice(&movw_r0.to_le_bytes());
2447        let hi16 = (lo32 >> 16) & 0xFFFF;
2448        let movt_r0 = 0xE3400000 | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2449        bytes.extend_from_slice(&movt_r0.to_le_bytes());
2450
2451        // Load high 32 bits into R12
2452        let lo16 = hi32 & 0xFFFF;
2453        let movw_r12 = 0xE3000000 | ((lo16 >> 12) << 16) | (12 << 12) | (lo16 & 0xFFF);
2454        bytes.extend_from_slice(&movw_r12.to_le_bytes());
2455        let hi16 = (hi32 >> 16) & 0xFFFF;
2456        let movt_r12 = 0xE3400000 | ((hi16 >> 12) << 16) | (12 << 12) | (hi16 & 0xFFF);
2457        bytes.extend_from_slice(&movt_r12.to_le_bytes());
2458
2459        // VMOV Dd, R0, R12
2460        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
2461        bytes.extend_from_slice(&vmov.to_le_bytes());
2462
2463        Ok(bytes)
2464    }
2465
2466    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as ARM32
2467    fn encode_arm_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2468        let mut bytes = Vec::new();
2469
2470        // Use S0 as intermediate: VMOV S0, Rm
2471        let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
2472        bytes.extend_from_slice(&vmov.to_le_bytes());
2473
2474        // VCVT.F64.S32 Dd, S0 (signed) or VCVT.F64.U32 Dd, S0 (unsigned)
2475        // Base: 0xEEB80B40 (signed) or 0xEEB80BC0 (unsigned)
2476        let dd_num = vfp_dreg_to_num(dd)?;
2477        let (vd, d) = encode_dreg(dd_num);
2478        let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
2479        // S0 is register 0: Vm=0, M=0
2480        let vcvt = base | (d << 22) | (vd << 12);
2481        bytes.extend_from_slice(&vcvt.to_le_bytes());
2482
2483        Ok(bytes)
2484    }
2485
2486    /// Encode VCVT.F64.F32 Dd, Sm as ARM32 (f32 to f64 promotion)
2487    fn encode_arm_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2488        let dd_num = vfp_dreg_to_num(dd)?;
2489        let sm_num = vfp_sreg_to_num(sm)?;
2490        let (vd, d) = encode_dreg(dd_num);
2491        let (vm, m) = encode_sreg(sm_num);
2492
2493        // VCVT.F64.F32 Dd, Sm: 0xEEB70AC0
2494        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
2495        Ok(vcvt.to_le_bytes().to_vec())
2496    }
2497
2498    /// Encode VCVT.S32/U32.F64 Sd, Dm + VMOV Rd, Sd as ARM32
2499    fn encode_arm_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2500        let mut bytes = Vec::new();
2501        let dm_num = vfp_dreg_to_num(dm)?;
2502        let (vm, m) = encode_dreg(dm_num);
2503
2504        // VCVT.S32.F64 S0, Dm (toward zero) or VCVT.U32.F64 S0, Dm
2505        // S0: Vd=0, D=0
2506        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
2507        let vcvt = base | (m << 5) | vm;
2508        bytes.extend_from_slice(&vcvt.to_le_bytes());
2509
2510        // VMOV Rd, S0
2511        let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
2512        bytes.extend_from_slice(&vmov.to_le_bytes());
2513
2514        Ok(bytes)
2515    }
2516
2517    /// Encode F64 rounding pseudo-op as ARM32 via VCVT to integer and back.
2518    /// Encode F64 rounding as ARM32.
2519    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2520    ///
2521    /// For trunc: uses VCVTR.S32.F64 (always truncates).
2522    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F64 (non-R variant),
2523    /// then restores FPSCR.
2524    fn encode_arm_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2525        let mut bytes = Vec::new();
2526        let dm_num = vfp_dreg_to_num(dm)?;
2527        let dd_num = vfp_dreg_to_num(dd)?;
2528        let (vm, m) = encode_dreg(dm_num);
2529        let (vd, d) = encode_dreg(dd_num);
2530
2531        if mode == 0b11 {
2532            // Trunc (toward zero): VCVTR.S32.F64 — bit[7]=1, always truncates
2533            let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
2534            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2535        } else {
2536            // ceil/floor/nearest: manipulate FPSCR rounding mode
2537            let rt: u32 = 12;
2538
2539            // VMRS R12, FPSCR
2540            let vmrs = 0xEEF10A10 | (rt << 12);
2541            bytes.extend_from_slice(&vmrs.to_le_bytes());
2542
2543            // BIC R12, R12, #(3 << 22)
2544            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2545            bytes.extend_from_slice(&bic.to_le_bytes());
2546
2547            // ORR R12, R12, #(mode << 22)
2548            if mode != 0 {
2549                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2550                bytes.extend_from_slice(&orr.to_le_bytes());
2551            }
2552
2553            // VMSR FPSCR, R12
2554            let vmsr = 0xEEE10A10 | (rt << 12);
2555            bytes.extend_from_slice(&vmsr.to_le_bytes());
2556
2557            // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0), uses FPSCR rmode
2558            let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
2559            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2560
2561            // Restore FPSCR
2562            bytes.extend_from_slice(&vmrs.to_le_bytes());
2563            bytes.extend_from_slice(&bic.to_le_bytes());
2564            bytes.extend_from_slice(&vmsr.to_le_bytes());
2565        }
2566
2567        // VCVT.F64.S32 Dd, S0 (convert back to double)
2568        let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
2569        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2570
2571        Ok(bytes)
2572    }
2573
2574    /// Encode F64 min/max as ARM32: VMOV + VCMP + VMRS + conditional VMOV
2575    fn encode_arm_f64_minmax(
2576        &self,
2577        dd: &VfpReg,
2578        dn: &VfpReg,
2579        dm: &VfpReg,
2580        is_min: bool,
2581    ) -> Result<Vec<u8>> {
2582        let mut bytes = Vec::new();
2583        let dn_num = vfp_dreg_to_num(dn)?;
2584        let dm_num = vfp_dreg_to_num(dm)?;
2585        let dd_num = vfp_dreg_to_num(dd)?;
2586
2587        // VMOV.F64 Dd, Dn (start with first operand)
2588        let (vd, d) = encode_dreg(dd_num);
2589        let (vn, n) = encode_dreg(dn_num);
2590        let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2591        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2592
2593        // VCMP.F64 Dn, Dm
2594        let (vm, m) = encode_dreg(dm_num);
2595        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2596        bytes.extend_from_slice(&vcmp.to_le_bytes());
2597
2598        // VMRS APSR_nzcv, FPSCR
2599        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2600
2601        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2602        let vmov_cond = (cond << 28) | 0x0EB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2603        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2604
2605        Ok(bytes)
2606    }
2607
2608    /// Encode F64 copysign as ARM32
2609    fn encode_arm_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
2610        let mut bytes = Vec::new();
2611
2612        // VMOV R0, R12, Dm (get sign source bits)
2613        let vmov_dm = encode_vmov_core_dreg(false, dm, &Reg::R0, &Reg::R12)?;
2614        bytes.extend_from_slice(&vmov_dm.to_le_bytes());
2615
2616        // VMOV R1, R2, Dn (get magnitude source bits)
2617        // We use R1 (lo) and R2 (hi) for the magnitude
2618        let vmov_dn = encode_vmov_core_dreg(false, dn, &Reg::R1, &Reg::R2)?;
2619        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2620
2621        // AND R12, R12, #0x80000000 (keep only sign bit from hi word)
2622        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2623        bytes.extend_from_slice(&and_sign.to_le_bytes());
2624
2625        // BIC R2, R2, #0x80000000 (clear sign bit from magnitude hi word)
2626        let bic_sign = 0xE3C00000u32 | (2 << 16) | (2 << 12) | (1 << 8) | 0x02;
2627        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2628
2629        // ORR R2, R2, R12 (combine sign + magnitude)
2630        let orr = 0xE1800000u32 | (2 << 16) | (2 << 12) | 12;
2631        bytes.extend_from_slice(&orr.to_le_bytes());
2632
2633        // VMOV Dd, R1, R2
2634        let vmov_result = encode_vmov_core_dreg(true, dd, &Reg::R1, &Reg::R2)?;
2635        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2636
2637        Ok(bytes)
2638    }
2639
2640    /// Encode VCVT.S32/U32.F32 + VMOV as ARM32
2641    fn encode_arm_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2642        let mut bytes = Vec::new();
2643
2644        // VCVT.S32.F32 Sd, Sm (toward zero) or VCVT.U32.F32 Sd, Sm
2645        // We use Sm as both source and destination for the intermediate result
2646        let sm_num = vfp_sreg_to_num(sm)?;
2647        let (vd, d) = encode_sreg(sm_num);
2648        let (vm, m) = encode_sreg(sm_num);
2649        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
2650        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2651        bytes.extend_from_slice(&vcvt.to_le_bytes());
2652
2653        // VMOV Rd, Sm — move result back to core register
2654        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
2655        bytes.extend_from_slice(&vmov.to_le_bytes());
2656
2657        Ok(bytes)
2658    }
2659
2660    /// Encode an ARM instruction in Thumb-2 mode (16-bit or 32-bit instructions)
2661    fn encode_thumb(&self, op: &ArmOp) -> Result<Vec<u8>> {
2662        // Thumb-2 supports both 16-bit and 32-bit instructions
2663        // 32-bit instructions are encoded as two 16-bit halfwords (big-endian order)
2664        match op {
2665            // === 16-bit Thumb encodings ===
2666            ArmOp::Add { rd, rn, op2 } => {
2667                let rd_bits = reg_to_bits(rd) as u16;
2668                let rn_bits = reg_to_bits(rn) as u16;
2669
2670                if let Operand2::Reg(rm) = op2 {
2671                    let rm_bits = reg_to_bits(rm) as u16;
2672                    // 16-bit ADDS only has 3-bit register fields (R0-R7). For
2673                    // high registers (e.g. R12, the MemLoad/MemStore base
2674                    // scratch) the bits overflow into adjacent fields, silently
2675                    // corrupting the operands — issue #178/#180: `add ip,ip,r0`
2676                    // was emitted as `adds r4,r5,r1`. Guard on all three regs
2677                    // being low and fall back to 32-bit ADD.W otherwise, exactly
2678                    // as the Sub handler below does.
2679                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2680                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2681                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2682                        Ok(instr.to_le_bytes().to_vec())
2683                    } else {
2684                        // ADD.W Rd, Rn, Rm (32-bit) for high registers
2685                        self.encode_thumb32_add_reg_raw(
2686                            rd_bits as u32,
2687                            rn_bits as u32,
2688                            rm_bits as u32,
2689                        )
2690                    }
2691                } else if let Operand2::Imm(imm) = op2 {
2692                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2693                        // ADDS Rd, Rn, #imm3 (16-bit): 0001 110 imm3 Rn Rd
2694                        let instr: u16 = 0x1C00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2695                        Ok(instr.to_le_bytes().to_vec())
2696                    } else {
2697                        // Use 32-bit ADD for larger immediates
2698                        self.encode_thumb32_add(rd, rn, *imm as u32)
2699                    }
2700                } else {
2701                    // Fallback to 32-bit encoding
2702                    self.encode_thumb32_add(rd, rn, 0)
2703                }
2704            }
2705
2706            ArmOp::Sub { rd, rn, op2 } => {
2707                let rd_bits = reg_to_bits(rd) as u16;
2708                let rn_bits = reg_to_bits(rn) as u16;
2709
2710                if let Operand2::Reg(rm) = op2 {
2711                    let rm_bits = reg_to_bits(rm) as u16;
2712                    // 16-bit SUBS can only use low registers (R0-R7)
2713                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2714                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2715                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2716                        Ok(instr.to_le_bytes().to_vec())
2717                    } else {
2718                        // Use 32-bit SUB.W for high registers
2719                        self.encode_thumb32_sub_reg_raw(
2720                            rd_bits as u32,
2721                            rn_bits as u32,
2722                            rm_bits as u32,
2723                        )
2724                    }
2725                } else if let Operand2::Imm(imm) = op2 {
2726                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2727                        // SUBS Rd, Rn, #imm3 (16-bit): 0001 111 imm3 Rn Rd
2728                        let instr: u16 = 0x1E00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2729                        Ok(instr.to_le_bytes().to_vec())
2730                    } else {
2731                        self.encode_thumb32_sub(rd, rn, *imm as u32)
2732                    }
2733                } else {
2734                    self.encode_thumb32_sub(rd, rn, 0)
2735                }
2736            }
2737
2738            ArmOp::Mov { rd, op2 } => {
2739                let rd_bits = reg_to_bits(rd) as u16;
2740
2741                if let Operand2::Imm(imm) = op2 {
2742                    // #498: the old test here was the SIGNED `*imm <= 255`,
2743                    // so a negative immediate (e.g. -1) fell into the 16-bit
2744                    // MOVS arm and encoded the wrong VALUE (#(imm & 0xFF) =
2745                    // #0xFF). A positive imm above 0xFFFF was equally wrong:
2746                    // MOVW truncates to 16 bits. Split on the UNSIGNED value:
2747                    // imm8 → MOVS, imm16 → MOVW, anything wider (negative or
2748                    // >0xFFFF) → the full-value MOVW+MOVT pair. No emitter
2749                    // produces the wide shape today (both selectors
2750                    // materialize wide constants as explicit Movw/Movt or
2751                    // Movw+Mvn), so this is byte-identical on shipped paths —
2752                    // it retires the latent wrong-value encodings the
2753                    // `estimator_encoder_agreement` oracle had pinned.
2754                    let uimm = *imm as u32;
2755                    if uimm <= 255 && rd_bits < 8 {
2756                        // MOVS Rd, #imm8 (16-bit): 0010 0 Rd imm8
2757                        let imm_bits = (*imm as u16) & 0xFF;
2758                        let instr: u16 = 0x2000 | (rd_bits << 8) | imm_bits;
2759                        Ok(instr.to_le_bytes().to_vec())
2760                    } else if uimm <= 0xFFFF {
2761                        // Use 32-bit MOVW for 16-bit immediates
2762                        self.encode_thumb32_movw(rd, uimm)
2763                    } else {
2764                        // Full 32-bit value: MOVW low16 + MOVT high16
2765                        let mut bytes = self.encode_thumb32_movw(rd, uimm & 0xFFFF)?;
2766                        bytes.extend(self.encode_thumb32_movt_raw(reg_to_bits(rd), uimm >> 16)?);
2767                        Ok(bytes)
2768                    }
2769                } else if let Operand2::Reg(rm) = op2 {
2770                    let rm_bits = reg_to_bits(rm) as u16;
2771                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
2772                    // D = Rd[3], Rd[2:0] in lower bits
2773                    let d_bit = (rd_bits >> 3) & 1;
2774                    let instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
2775                    Ok(instr.to_le_bytes().to_vec())
2776                } else {
2777                    let instr: u16 = 0xBF00; // NOP fallback
2778                    Ok(instr.to_le_bytes().to_vec())
2779                }
2780            }
2781
2782            ArmOp::Push { regs } => {
2783                // Thumb-2 PUSH encoding:
2784                // If all regs in R0-R7 + LR, use 16-bit: 1011 010 M rrrrrrrr
2785                // Otherwise use 32-bit: STMDB SP!, {regs} = 1110 1001 0010 1101 | 0M0 reglist(13)
2786                let mut reg_list: u16 = 0;
2787                let mut need_32bit = false;
2788                for r in regs {
2789                    let bit = reg_to_bits(r);
2790                    if bit >= 8 && *r != Reg::LR {
2791                        need_32bit = true;
2792                    }
2793                    reg_list |= 1 << bit;
2794                }
2795                if !need_32bit {
2796                    // 16-bit PUSH: 1011 010 M rrrrrrrr
2797                    let m_bit = if reg_list & (1 << 14) != 0 {
2798                        1u16
2799                    } else {
2800                        0u16
2801                    };
2802                    let low_regs = reg_list & 0xFF;
2803                    let instr: u16 = 0xB400 | (m_bit << 8) | low_regs;
2804                    Ok(instr.to_le_bytes().to_vec())
2805                } else {
2806                    // 32-bit STMDB SP!, {regs}: E92D | reglist(16)
2807                    let hw1: u16 = 0xE92D;
2808                    let hw2: u16 = reg_list;
2809                    let mut bytes = hw1.to_le_bytes().to_vec();
2810                    bytes.extend_from_slice(&hw2.to_le_bytes());
2811                    Ok(bytes)
2812                }
2813            }
2814
2815            ArmOp::Pop { regs } => {
2816                // Thumb-2 POP encoding:
2817                // If all regs in R0-R7 + PC, use 16-bit: 1011 110 P rrrrrrrr
2818                // Otherwise use 32-bit: LDMIA SP!, {regs} = 1110 1000 1011 1101 | PM0 reglist(13)
2819                let mut reg_list: u16 = 0;
2820                let mut need_32bit = false;
2821                for r in regs {
2822                    let bit = reg_to_bits(r);
2823                    if bit >= 8 && *r != Reg::PC {
2824                        need_32bit = true;
2825                    }
2826                    reg_list |= 1 << bit;
2827                }
2828                if !need_32bit {
2829                    // 16-bit POP: 1011 110 P rrrrrrrr
2830                    let p_bit = if reg_list & (1 << 15) != 0 {
2831                        1u16
2832                    } else {
2833                        0u16
2834                    };
2835                    let low_regs = reg_list & 0xFF;
2836                    let instr: u16 = 0xBC00 | (p_bit << 8) | low_regs;
2837                    Ok(instr.to_le_bytes().to_vec())
2838                } else {
2839                    // 32-bit LDMIA SP!, {regs}: E8BD | reglist(16)
2840                    let hw1: u16 = 0xE8BD;
2841                    let hw2: u16 = reg_list;
2842                    let mut bytes = hw1.to_le_bytes().to_vec();
2843                    bytes.extend_from_slice(&hw2.to_le_bytes());
2844                    Ok(bytes)
2845                }
2846            }
2847
2848            ArmOp::Nop => {
2849                let instr: u16 = 0xBF00; // NOP in Thumb-2
2850                Ok(instr.to_le_bytes().to_vec())
2851            }
2852
2853            ArmOp::Udf { imm } => {
2854                // UDF (Undefined) in Thumb-2: 16-bit encoding is 0xDE00 | imm8
2855                // This triggers UsageFault/HardFault, used for WASM traps
2856                let instr: u16 = 0xDE00 | (*imm as u16);
2857                let bytes = instr.to_le_bytes().to_vec();
2858                encoding_contracts::verify_thumb16(&bytes);
2859                Ok(bytes)
2860            }
2861
2862            // i64 support: ADDS, ADC, SUBS, SBC for register pair arithmetic
2863            // ADDS sets flags (carry), ADC uses carry from previous ADDS
2864            ArmOp::Adds { rd, rn, op2 } => {
2865                let rd_bits = reg_to_bits(rd) as u16;
2866                let rn_bits = reg_to_bits(rn) as u16;
2867
2868                if let Operand2::Reg(rm) = op2 {
2869                    let rm_bits = reg_to_bits(rm) as u16;
2870                    // 16-bit ADDS is R0-R7 only; i64 pair allocation can place
2871                    // operands in R8-R11, which would overflow the 3-bit fields
2872                    // and corrupt the operands (#178/#180 class). Guard and fall
2873                    // back to 32-bit ADDS.W for high registers.
2874                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2875                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2876                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2877                        Ok(instr.to_le_bytes().to_vec())
2878                    } else {
2879                        self.encode_thumb32_adds_reg_raw(
2880                            rd_bits as u32,
2881                            rn_bits as u32,
2882                            rm_bits as u32,
2883                        )
2884                    }
2885                } else {
2886                    // 32-bit Thumb-2 ADDS with immediate
2887                    self.encode_thumb32_adds(rd, rn, 0)
2888                }
2889            }
2890
2891            // ADC: Add with Carry (Thumb-2 32-bit)
2892            // ADC.W Rd, Rn, Rm: EB40 Rn | 00 Rd 00 Rm
2893            ArmOp::Adc { rd, rn, op2 } => {
2894                let rd_bits = reg_to_bits(rd);
2895                let rn_bits = reg_to_bits(rn);
2896
2897                if let Operand2::Reg(rm) = op2 {
2898                    let rm_bits = reg_to_bits(rm);
2899                    // ADC.W Rd, Rn, Rm (T2): 1110 1011 0100 Rn | 0 000 Rd 00 00 Rm
2900                    let hw1: u16 = (0xEB40 | rn_bits) as u16;
2901                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2902
2903                    let mut bytes = hw1.to_le_bytes().to_vec();
2904                    bytes.extend_from_slice(&hw2.to_le_bytes());
2905                    Ok(bytes)
2906                } else {
2907                    // ADC with immediate - use 32-bit encoding
2908                    let hw1: u16 = (0xF140 | rn_bits) as u16;
2909                    let hw2: u16 = (rd_bits << 8) as u16;
2910                    let mut bytes = hw1.to_le_bytes().to_vec();
2911                    bytes.extend_from_slice(&hw2.to_le_bytes());
2912                    Ok(bytes)
2913                }
2914            }
2915
2916            // SUBS sets flags (borrow), SBC uses borrow from previous SUBS
2917            ArmOp::Subs { rd, rn, op2 } => {
2918                let rd_bits = reg_to_bits(rd) as u16;
2919                let rn_bits = reg_to_bits(rn) as u16;
2920
2921                if let Operand2::Reg(rm) = op2 {
2922                    let rm_bits = reg_to_bits(rm) as u16;
2923                    // 16-bit SUBS is R0-R7 only; high-register i64 pair operands
2924                    // would overflow the 3-bit fields (#178/#180 class). Guard
2925                    // and fall back to 32-bit SUBS.W for high registers.
2926                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2927                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2928                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2929                        Ok(instr.to_le_bytes().to_vec())
2930                    } else {
2931                        self.encode_thumb32_subs_reg_raw(
2932                            rd_bits as u32,
2933                            rn_bits as u32,
2934                            rm_bits as u32,
2935                        )
2936                    }
2937                } else {
2938                    // 32-bit Thumb-2 SUBS with immediate
2939                    self.encode_thumb32_subs(rd, rn, 0)
2940                }
2941            }
2942
2943            // SBC: Subtract with Carry (Thumb-2 32-bit)
2944            // SBC.W Rd, Rn, Rm: EB60 Rn | 00 Rd 00 Rm
2945            ArmOp::Sbc { rd, rn, op2 } => {
2946                let rd_bits = reg_to_bits(rd);
2947                let rn_bits = reg_to_bits(rn);
2948
2949                if let Operand2::Reg(rm) = op2 {
2950                    let rm_bits = reg_to_bits(rm);
2951                    // SBC.W Rd, Rn, Rm (T2): 1110 1011 0110 Rn | 0 000 Rd 00 00 Rm
2952                    let hw1: u16 = (0xEB60 | rn_bits) as u16;
2953                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2954
2955                    let mut bytes = hw1.to_le_bytes().to_vec();
2956                    bytes.extend_from_slice(&hw2.to_le_bytes());
2957                    Ok(bytes)
2958                } else {
2959                    // SBC with immediate - use 32-bit encoding
2960                    let hw1: u16 = (0xF160 | rn_bits) as u16;
2961                    let hw2: u16 = (rd_bits << 8) as u16;
2962                    let mut bytes = hw1.to_le_bytes().to_vec();
2963                    bytes.extend_from_slice(&hw2.to_le_bytes());
2964                    Ok(bytes)
2965                }
2966            }
2967
2968            // === 32-bit Thumb-2 encodings ===
2969
2970            // SDIV: 11111011 1001 Rn 1111 Rd 1111 Rm
2971            ArmOp::Sdiv { rd, rn, rm } => {
2972                let rd_bits = reg_to_bits(rd);
2973                let rn_bits = reg_to_bits(rn);
2974                let rm_bits = reg_to_bits(rm);
2975                reg_bits_checked(rd_bits)?;
2976                reg_bits_checked(rn_bits)?;
2977                reg_bits_checked(rm_bits)?;
2978
2979                // Thumb-2 SDIV: FB90 F0F0 | Rn<<16 | Rd<<8 | Rm
2980                // First halfword: 1111 1011 1001 Rn = 0xFB90 | Rn
2981                // Second halfword: 1111 Rd 1111 Rm = 0xF0F0 | Rd<<8 | Rm
2982                let hw1: u16 = (0xFB90 | rn_bits) as u16;
2983                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
2984
2985                // Thumb-2 32-bit instructions: first halfword, then second halfword (little-endian each)
2986                let mut bytes = hw1.to_le_bytes().to_vec();
2987                bytes.extend_from_slice(&hw2.to_le_bytes());
2988                encoding_contracts::verify_thumb32(&bytes);
2989                Ok(bytes)
2990            }
2991
2992            // UDIV: 11111011 1011 Rn 1111 Rd 1111 Rm
2993            ArmOp::Udiv { rd, rn, rm } => {
2994                let rd_bits = reg_to_bits(rd);
2995                let rn_bits = reg_to_bits(rn);
2996                let rm_bits = reg_to_bits(rm);
2997                reg_bits_checked(rd_bits)?;
2998                reg_bits_checked(rn_bits)?;
2999                reg_bits_checked(rm_bits)?;
3000
3001                // Thumb-2 UDIV: FBB0 F0F0 | Rn<<16 | Rd<<8 | Rm
3002                let hw1: u16 = (0xFBB0 | rn_bits) as u16;
3003                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
3004
3005                let mut bytes = hw1.to_le_bytes().to_vec();
3006                bytes.extend_from_slice(&hw2.to_le_bytes());
3007                encoding_contracts::verify_thumb32(&bytes);
3008                Ok(bytes)
3009            }
3010
3011            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
3012                let rdlo_bits = reg_to_bits(rdlo);
3013                let rdhi_bits = reg_to_bits(rdhi);
3014                let rn_bits = reg_to_bits(rn);
3015                let rm_bits = reg_to_bits(rm);
3016                reg_bits_checked(rdlo_bits)?;
3017                reg_bits_checked(rdhi_bits)?;
3018                reg_bits_checked(rn_bits)?;
3019                reg_bits_checked(rm_bits)?;
3020
3021                // Thumb-2 UMULL: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm
3022                let hw1: u16 = (0xFBA0 | rn_bits) as u16;
3023                let hw2: u16 = ((rdlo_bits << 12) | (rdhi_bits << 8) | rm_bits) as u16;
3024
3025                let mut bytes = hw1.to_le_bytes().to_vec();
3026                bytes.extend_from_slice(&hw2.to_le_bytes());
3027                encoding_contracts::verify_thumb32(&bytes);
3028                Ok(bytes)
3029            }
3030
3031            // MUL (Thumb-2 32-bit): MUL Rd, Rn, Rm
3032            ArmOp::Mul { rd, rn, rm } => {
3033                let rd_bits = reg_to_bits(rd);
3034                let rn_bits = reg_to_bits(rn);
3035                let rm_bits = reg_to_bits(rm);
3036
3037                // Thumb-2 MUL: FB00 F000 | Rn | Rd<<8 | Rm
3038                // 11111011 0000 Rn | 1111 Rd 0000 Rm
3039                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3040                let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
3041
3042                let mut bytes = hw1.to_le_bytes().to_vec();
3043                bytes.extend_from_slice(&hw2.to_le_bytes());
3044                Ok(bytes)
3045            }
3046
3047            // MLS: Rd = Ra - Rn * Rm
3048            ArmOp::Mls { rd, rn, rm, ra } => {
3049                let rd_bits = reg_to_bits(rd);
3050                let rn_bits = reg_to_bits(rn);
3051                let rm_bits = reg_to_bits(rm);
3052                let ra_bits = reg_to_bits(ra);
3053
3054                // Thumb-2 MLS: FB00 Rn | Ra Rd 0001 Rm
3055                // 11111011 0000 Rn | Ra Rd 0001 Rm
3056                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3057                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | 0x10 | rm_bits) as u16;
3058
3059                let mut bytes = hw1.to_le_bytes().to_vec();
3060                bytes.extend_from_slice(&hw2.to_le_bytes());
3061                Ok(bytes)
3062            }
3063
3064            ArmOp::Mla { rd, rn, rm, ra } => {
3065                let rd_bits = reg_to_bits(rd);
3066                let rn_bits = reg_to_bits(rn);
3067                let rm_bits = reg_to_bits(rm);
3068                let ra_bits = reg_to_bits(ra);
3069
3070                // Thumb-2 MLA: FB00 Rn | Ra Rd 0000 Rm — same as MLS without the
3071                // bit-4 (0x10) op flag. rd = ra + rn*rm.
3072                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3073                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | rm_bits) as u16;
3074
3075                let mut bytes = hw1.to_le_bytes().to_vec();
3076                bytes.extend_from_slice(&hw2.to_le_bytes());
3077                Ok(bytes)
3078            }
3079
3080            // AND (Thumb-2 32-bit)
3081            ArmOp::And { rd, rn, op2 } => {
3082                if let Operand2::Reg(rm) = op2 {
3083                    let rd_bits = reg_to_bits(rd);
3084                    let rn_bits = reg_to_bits(rn);
3085                    let rm_bits = reg_to_bits(rm);
3086
3087                    // Thumb-2 AND register: EA00 Rn | 0 Rd 00 00 Rm
3088                    let hw1: u16 = (0xEA00 | rn_bits) as u16;
3089                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3090
3091                    let mut bytes = hw1.to_le_bytes().to_vec();
3092                    bytes.extend_from_slice(&hw2.to_le_bytes());
3093                    Ok(bytes)
3094                } else if let Operand2::Imm(imm) = op2 {
3095                    let rd_bits = reg_to_bits(rd);
3096                    let rn_bits = reg_to_bits(rn);
3097
3098                    // Thumb-2 AND.W immediate T1: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8.
3099                    // The i:imm3:imm8 field is a ThumbExpandImm modified immediate —
3100                    // encode it correctly (or error on an un-encodable value)
3101                    // rather than packing raw bits, closing the silent-miscompile
3102                    // class for AND alongside ORR/EOR (#251) / ADD/SUB (#253) /
3103                    // CMP (#255).
3104                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3105                        synth_core::Error::synthesis(
3106                            "AND immediate is not a valid ThumbExpandImm — materialize into a register",
3107                        )
3108                    })?;
3109                    let i_bit = (field >> 11) & 1;
3110                    let imm3 = (field >> 8) & 0x7;
3111                    let imm8 = field & 0xFF;
3112
3113                    let hw1: u16 = (0xF000 | (i_bit << 10) | rn_bits) as u16;
3114                    let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3115
3116                    let mut bytes = hw1.to_le_bytes().to_vec();
3117                    bytes.extend_from_slice(&hw2.to_le_bytes());
3118                    Ok(bytes)
3119                } else {
3120                    // RegShift variant - fallback to NOP
3121                    let instr: u16 = 0xBF00;
3122                    Ok(instr.to_le_bytes().to_vec())
3123                }
3124            }
3125
3126            // ORR (Thumb-2 32-bit)
3127            ArmOp::Orr { rd, rn, op2 } => {
3128                if let Operand2::Reg(rm) = op2 {
3129                    let rd_bits = reg_to_bits(rd);
3130                    let rn_bits = reg_to_bits(rn);
3131                    let rm_bits = reg_to_bits(rm);
3132
3133                    // Thumb-2 ORR: EA40 Rn | 0 Rd 00 00 Rm
3134                    let hw1: u16 = (0xEA40 | rn_bits) as u16;
3135                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3136
3137                    let mut bytes = hw1.to_le_bytes().to_vec();
3138                    bytes.extend_from_slice(&hw2.to_le_bytes());
3139                    Ok(bytes)
3140                } else if let Operand2::Imm(imm) = op2 {
3141                    // ORR.W immediate T1: 11110 i 0 0010 S Rn | 0 imm3 Rd imm8.
3142                    // Only the zero-extended byte form (imm <= 0xFF) is encoded;
3143                    // larger modified immediates need ThumbExpandImm — return an
3144                    // error rather than silently emit a NOP (Ok-or-Err, #180/#185).
3145                    let imm_val = *imm as u32;
3146                    if imm_val > 0xFF {
3147                        return Err(synth_core::Error::synthesis(
3148                            "ORR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3149                        ));
3150                    }
3151                    let rd_bits = reg_to_bits(rd);
3152                    let rn_bits = reg_to_bits(rn);
3153                    let hw1: u16 = (0xF040 | rn_bits) as u16;
3154                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3155                    let mut bytes = hw1.to_le_bytes().to_vec();
3156                    bytes.extend_from_slice(&hw2.to_le_bytes());
3157                    Ok(bytes)
3158                } else {
3159                    let instr: u16 = 0xBF00;
3160                    Ok(instr.to_le_bytes().to_vec())
3161                }
3162            }
3163
3164            // EOR (Thumb-2 32-bit)
3165            ArmOp::Eor { rd, rn, op2 } => {
3166                if let Operand2::Reg(rm) = op2 {
3167                    let rd_bits = reg_to_bits(rd);
3168                    let rn_bits = reg_to_bits(rn);
3169                    let rm_bits = reg_to_bits(rm);
3170
3171                    // Thumb-2 EOR: EA80 Rn | 0 Rd 00 00 Rm
3172                    let hw1: u16 = (0xEA80 | rn_bits) as u16;
3173                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3174
3175                    let mut bytes = hw1.to_le_bytes().to_vec();
3176                    bytes.extend_from_slice(&hw2.to_le_bytes());
3177                    Ok(bytes)
3178                } else if let Operand2::Imm(imm) = op2 {
3179                    // EOR.W immediate T1: 11110 i 0 0100 S Rn | 0 imm3 Rd imm8.
3180                    // Byte form only (imm <= 0xFF); larger needs ThumbExpandImm —
3181                    // error, not a silent NOP (Ok-or-Err, #180/#185).
3182                    let imm_val = *imm as u32;
3183                    if imm_val > 0xFF {
3184                        return Err(synth_core::Error::synthesis(
3185                            "EOR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3186                        ));
3187                    }
3188                    let rd_bits = reg_to_bits(rd);
3189                    let rn_bits = reg_to_bits(rn);
3190                    let hw1: u16 = (0xF080 | rn_bits) as u16;
3191                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3192                    let mut bytes = hw1.to_le_bytes().to_vec();
3193                    bytes.extend_from_slice(&hw2.to_le_bytes());
3194                    Ok(bytes)
3195                } else {
3196                    let instr: u16 = 0xBF00;
3197                    Ok(instr.to_le_bytes().to_vec())
3198                }
3199            }
3200
3201            // Shift operations (16-bit for low registers)
3202            ArmOp::Lsl { rd, rn, shift } => {
3203                let rd_bits = reg_to_bits(rd) as u16;
3204                let rn_bits = reg_to_bits(rn) as u16;
3205                let shift_bits = (*shift as u16) & 0x1F;
3206
3207                if rd_bits < 8 && rn_bits < 8 {
3208                    // LSLS Rd, Rm, #imm5 (16-bit): 0000 0 imm5 Rm Rd
3209                    let instr: u16 = (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3210                    Ok(instr.to_le_bytes().to_vec())
3211                } else {
3212                    // Use 32-bit encoding for high registers
3213                    self.encode_thumb32_shift(rd, rn, *shift, 0b00) // LSL type
3214                }
3215            }
3216
3217            ArmOp::Lsr { rd, rn, shift } => {
3218                let rd_bits = reg_to_bits(rd) as u16;
3219                let rn_bits = reg_to_bits(rn) as u16;
3220                let shift_bits = (*shift as u16) & 0x1F;
3221
3222                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3223                    // LSRS Rd, Rm, #imm5 (16-bit): 0000 1 imm5 Rm Rd
3224                    let instr: u16 = 0x0800 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3225                    Ok(instr.to_le_bytes().to_vec())
3226                } else {
3227                    self.encode_thumb32_shift(rd, rn, *shift, 0b01) // LSR type
3228                }
3229            }
3230
3231            ArmOp::Asr { rd, rn, shift } => {
3232                let rd_bits = reg_to_bits(rd) as u16;
3233                let rn_bits = reg_to_bits(rn) as u16;
3234                let shift_bits = (*shift as u16) & 0x1F;
3235
3236                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3237                    // ASRS Rd, Rm, #imm5 (16-bit): 0001 0 imm5 Rm Rd
3238                    let instr: u16 = 0x1000 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3239                    Ok(instr.to_le_bytes().to_vec())
3240                } else {
3241                    self.encode_thumb32_shift(rd, rn, *shift, 0b10) // ASR type
3242                }
3243            }
3244
3245            ArmOp::Ror { rd, rn, shift } => {
3246                // ROR doesn't have a 16-bit immediate form, use 32-bit
3247                self.encode_thumb32_shift(rd, rn, *shift, 0b11) // ROR type
3248            }
3249
3250            // Register-based shifts (Thumb-2 32-bit)
3251            // Encoding: 11111010 0xxS Rn 1111 Rd 0000 Rm
3252            // xx = shift type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
3253            ArmOp::LslReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b00),
3254            ArmOp::LsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b01),
3255            ArmOp::AsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b10),
3256            ArmOp::RorReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b11),
3257
3258            // RSB (Reverse Subtract): Rd = imm - Rn
3259            // Thumb-2 T2 encoding: 11110 i 0 1110 S Rn | 0 imm3 Rd imm8
3260            ArmOp::Rsb { rd, rn, imm } => {
3261                let rd_bits = reg_to_bits(rd);
3262                let rn_bits = reg_to_bits(rn);
3263
3264                // #681 class audit: the T2 `i:imm3:imm8` field is a
3265                // ThumbExpandImm modified immediate and RSB has NO plain-imm12
3266                // (T4-style) form — packing a raw value > 0xFF silently encodes
3267                // a different constant (#253/#255 class). All current emitters
3268                // use imm 32 (shift complement), which expands to itself, so
3269                // this gate is byte-identical for existing codegen.
3270                let field = try_thumb_expand_imm(*imm).ok_or_else(|| {
3271                    synth_core::Error::synthesis(
3272                        "RSB immediate is not a valid ThumbExpandImm — materialize into a register",
3273                    )
3274                })?;
3275                let i_bit = (field >> 11) & 1;
3276                let imm3 = (field >> 8) & 0x7;
3277                let imm8 = field & 0xFF;
3278
3279                // hw1: 11110 i 01110 0 Rn  (S=0)
3280                let hw1: u16 = (0xF1C0 | (i_bit << 10) | rn_bits) as u16;
3281                // hw2: 0 imm3 Rd imm8
3282                let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3283
3284                let mut bytes = hw1.to_le_bytes().to_vec();
3285                bytes.extend_from_slice(&hw2.to_le_bytes());
3286                Ok(bytes)
3287            }
3288
3289            // CLZ (Thumb-2 32-bit)
3290            ArmOp::Clz { rd, rm } => {
3291                let rd_bits = reg_to_bits(rd);
3292                let rm_bits = reg_to_bits(rm);
3293
3294                // Thumb-2 CLZ: FAB0 Rm | F8 Rd Rm
3295                // 11111010 1011 Rm | 1111 1000 Rd Rm
3296                let hw1: u16 = (0xFAB0 | rm_bits) as u16;
3297                let hw2: u16 = (0xF080 | (rd_bits << 8) | rm_bits) as u16;
3298
3299                let mut bytes = hw1.to_le_bytes().to_vec();
3300                bytes.extend_from_slice(&hw2.to_le_bytes());
3301                Ok(bytes)
3302            }
3303
3304            // RBIT (Thumb-2 32-bit)
3305            ArmOp::Rbit { rd, rm } => {
3306                let rd_bits = reg_to_bits(rd);
3307                let rm_bits = reg_to_bits(rm);
3308
3309                // Thumb-2 RBIT: FA90 Rm | F0 Rd A0 Rm
3310                // 11111010 1001 Rm | 1111 Rd 1010 Rm
3311                let hw1: u16 = (0xFA90 | rm_bits) as u16;
3312                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rm_bits) as u16;
3313
3314                let mut bytes = hw1.to_le_bytes().to_vec();
3315                bytes.extend_from_slice(&hw2.to_le_bytes());
3316                Ok(bytes)
3317            }
3318
3319            // SXTB (16-bit for low registers)
3320            ArmOp::Sxtb { rd, rm } => {
3321                let rd_bits = reg_to_bits(rd) as u16;
3322                let rm_bits = reg_to_bits(rm) as u16;
3323
3324                if rd_bits < 8 && rm_bits < 8 {
3325                    // SXTB Rd, Rm (16-bit): 1011 0010 01 Rm Rd
3326                    let instr: u16 = 0xB240 | (rm_bits << 3) | rd_bits;
3327                    Ok(instr.to_le_bytes().to_vec())
3328                } else {
3329                    // Thumb-2 SXTB.W: FA4F F(rd)80 (rm)
3330                    // 11111010 0100 1111 | 1111 Rd 10 rotate Rm
3331                    let rd_bits32 = rd_bits as u32;
3332                    let rm_bits32 = rm_bits as u32;
3333                    let hw1: u16 = 0xFA4F;
3334                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3335                    let mut bytes = hw1.to_le_bytes().to_vec();
3336                    bytes.extend_from_slice(&hw2.to_le_bytes());
3337                    Ok(bytes)
3338                }
3339            }
3340
3341            // SXTH (16-bit for low registers)
3342            ArmOp::Sxth { rd, rm } => {
3343                let rd_bits = reg_to_bits(rd) as u16;
3344                let rm_bits = reg_to_bits(rm) as u16;
3345
3346                if rd_bits < 8 && rm_bits < 8 {
3347                    // SXTH Rd, Rm (16-bit): 1011 0010 00 Rm Rd
3348                    let instr: u16 = 0xB200 | (rm_bits << 3) | rd_bits;
3349                    Ok(instr.to_le_bytes().to_vec())
3350                } else {
3351                    // Thumb-2 SXTH.W: FA0F F(rd)80 (rm)
3352                    // 11111010 0000 1111 | 1111 Rd 10 rotate Rm
3353                    let rd_bits32 = rd_bits as u32;
3354                    let rm_bits32 = rm_bits as u32;
3355                    let hw1: u16 = 0xFA0F;
3356                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3357                    let mut bytes = hw1.to_le_bytes().to_vec();
3358                    bytes.extend_from_slice(&hw2.to_le_bytes());
3359                    Ok(bytes)
3360                }
3361            }
3362
3363            // UXTB Rd,Rm — zero-extend byte (rd = rm & 0xff)
3364            ArmOp::Uxtb { rd, rm } => {
3365                let rd_bits = reg_to_bits(rd) as u16;
3366                let rm_bits = reg_to_bits(rm) as u16;
3367                if rd_bits < 8 && rm_bits < 8 {
3368                    // UXTB Rd, Rm (16-bit): 1011 0010 11 Rm Rd
3369                    let instr: u16 = 0xB2C0 | (rm_bits << 3) | rd_bits;
3370                    Ok(instr.to_le_bytes().to_vec())
3371                } else {
3372                    // Thumb-2 UXTB.W: FA5F F(rd)80 (rm)
3373                    let hw1: u16 = 0xFA5F;
3374                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3375                    let mut bytes = hw1.to_le_bytes().to_vec();
3376                    bytes.extend_from_slice(&hw2.to_le_bytes());
3377                    Ok(bytes)
3378                }
3379            }
3380
3381            // UXTH Rd,Rm — zero-extend halfword (rd = rm & 0xffff)
3382            ArmOp::Uxth { rd, rm } => {
3383                let rd_bits = reg_to_bits(rd) as u16;
3384                let rm_bits = reg_to_bits(rm) as u16;
3385                if rd_bits < 8 && rm_bits < 8 {
3386                    // UXTH Rd, Rm (16-bit): 1011 0010 10 Rm Rd
3387                    let instr: u16 = 0xB280 | (rm_bits << 3) | rd_bits;
3388                    Ok(instr.to_le_bytes().to_vec())
3389                } else {
3390                    // Thumb-2 UXTH.W: FA1F F(rd)80 (rm)
3391                    let hw1: u16 = 0xFA1F;
3392                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3393                    let mut bytes = hw1.to_le_bytes().to_vec();
3394                    bytes.extend_from_slice(&hw2.to_le_bytes());
3395                    Ok(bytes)
3396                }
3397            }
3398
3399            // CMP (can be 16-bit for low registers)
3400            ArmOp::Cmp { rn, op2 } => {
3401                let rn_bits = reg_to_bits(rn) as u16;
3402
3403                if let Operand2::Imm(imm) = op2 {
3404                    // Only use 16-bit encoding for non-negative immediates 0-255
3405                    // Negative immediates must use 32-bit encoding
3406                    if *imm >= 0 && *imm <= 255 && rn_bits < 8 {
3407                        // CMP Rn, #imm8 (16-bit): 0010 1 Rn imm8
3408                        let instr: u16 = 0x2800 | (rn_bits << 8) | (*imm as u16 & 0xFF);
3409                        Ok(instr.to_le_bytes().to_vec())
3410                    } else {
3411                        self.encode_thumb32_cmp_imm(rn, *imm as u32)
3412                    }
3413                } else if let Operand2::Reg(rm) = op2 {
3414                    let rm_bits = reg_to_bits(rm) as u16;
3415                    if rn_bits < 8 && rm_bits < 8 {
3416                        // CMP Rn, Rm (16-bit low): 0100 0010 10 Rm Rn
3417                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
3418                        Ok(instr.to_le_bytes().to_vec())
3419                    } else {
3420                        // CMP Rn, Rm (16-bit high): 0100 0101 N Rm Rn[2:0]
3421                        let n_bit = (rn_bits >> 3) & 1;
3422                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
3423                        Ok(instr.to_le_bytes().to_vec())
3424                    }
3425                } else {
3426                    let instr: u16 = 0xBF00;
3427                    Ok(instr.to_le_bytes().to_vec())
3428                }
3429            }
3430
3431            // CMN (Compare Negative) - computes Rn + op2 and sets flags
3432            // CMN Rn, #1 sets Z flag if Rn == -1 (since -1 + 1 = 0)
3433            ArmOp::Cmn { rn, op2 } => {
3434                let rn_bits = reg_to_bits(rn) as u16;
3435
3436                if let Operand2::Imm(imm) = op2 {
3437                    // CMN.W Rn, #imm (32-bit): i:imm3:imm8 is a ThumbExpandImm
3438                    // modified immediate (the field sits in imm3=hw2[14:12],
3439                    // imm8=hw2[7:0], i=hw1[10]). Encode it correctly, or error on
3440                    // an un-encodable value — replacing the old silent `0xBF00`
3441                    // NOP (the last of the silent-miscompile data-proc encoders).
3442                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3443                        synth_core::Error::synthesis(
3444                            "CMN immediate is not a valid ThumbExpandImm — materialize into a register",
3445                        )
3446                    })?;
3447                    let i_bit = (field >> 11) & 1;
3448                    let imm3 = (field >> 8) & 0x7;
3449                    let imm8 = field & 0xFF;
3450                    let hw1: u16 = (0xF110 | (i_bit << 10) as u16) | rn_bits;
3451                    let hw2: u16 = (imm3 << 12) as u16 | 0x0F00 | imm8 as u16;
3452                    let mut bytes = hw1.to_le_bytes().to_vec();
3453                    bytes.extend_from_slice(&hw2.to_le_bytes());
3454                    Ok(bytes)
3455                } else if let Operand2::Reg(rm) = op2 {
3456                    let rm_bits = reg_to_bits(rm) as u16;
3457                    // 16-bit CMN (T1) only encodes R0-R7; high registers overflow
3458                    // the 3-bit fields and corrupt the operands (#184, the #180
3459                    // class). CMN has no high-register 16-bit form, so fall back
3460                    // to 32-bit CMN.W (T2): EB10 Rn | 0F00 Rm (ADD.W with S=1 and
3461                    // Rd discarded as PC/1111).
3462                    if rn_bits < 8 && rm_bits < 8 {
3463                        // CMN Rn, Rm (16-bit): 0100 0010 11 Rm Rn
3464                        let instr: u16 = 0x42C0 | (rm_bits << 3) | rn_bits;
3465                        Ok(instr.to_le_bytes().to_vec())
3466                    } else {
3467                        let hw1: u16 = 0xEB10 | rn_bits;
3468                        let hw2: u16 = 0x0F00 | rm_bits;
3469                        let mut bytes = hw1.to_le_bytes().to_vec();
3470                        bytes.extend_from_slice(&hw2.to_le_bytes());
3471                        Ok(bytes)
3472                    }
3473                } else {
3474                    Ok(vec![0xBF, 0x00])
3475                }
3476            }
3477
3478            // LDR (can be 16-bit for simple cases)
3479            ArmOp::Ldr { rd, addr } => {
3480                let rd_bits = reg_to_bits(rd);
3481                let base_bits = reg_to_bits(&addr.base);
3482
3483                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3484                if let Some(offset_reg) = &addr.offset_reg {
3485                    let rm_bits = reg_to_bits(offset_reg);
3486
3487                    // If there's also an immediate offset, we need to ADD it first
3488                    if addr.offset != 0 {
3489                        // Use R12 (IP) as scratch to avoid clobbering the address register
3490                        // ADD R12, Rm, #offset; LDR Rd, [base, R12]
3491                        let scratch = Reg::R12;
3492                        let mut bytes =
3493                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3494                        bytes.extend(self.encode_thumb32_ldr_reg(rd, &addr.base, &scratch)?);
3495                        return Ok(bytes);
3496                    }
3497
3498                    // Simple register offset: LDR Rd, [Rn, Rm]
3499                    // 16-bit: only if Rd, Rn, Rm < R8
3500                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3501                        // LDR Rd, [Rn, Rm] (16-bit): 0101 100 Rm Rn Rd
3502                        let instr: u16 = 0x5800
3503                            | ((rm_bits as u16) << 6)
3504                            | ((base_bits as u16) << 3)
3505                            | (rd_bits as u16);
3506                        return Ok(instr.to_le_bytes().to_vec());
3507                    }
3508
3509                    // 32-bit register offset
3510                    return self.encode_thumb32_ldr_reg(rd, &addr.base, offset_reg);
3511                }
3512
3513                // Immediate offset mode [base, #imm]
3514                let offset = addr.offset as u32;
3515
3516                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3517                    // LDR Rd, [Rn, #imm5*4] (16-bit): 0110 1 imm5 Rn Rd
3518                    let imm5 = (offset >> 2) as u16;
3519                    let instr: u16 =
3520                        0x6800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3521                    Ok(instr.to_le_bytes().to_vec())
3522                } else {
3523                    self.encode_thumb32_ldr(rd, &addr.base, offset)
3524                }
3525            }
3526
3527            // STR (can be 16-bit for simple cases)
3528            ArmOp::Str { rd, addr } => {
3529                let rd_bits = reg_to_bits(rd);
3530                let base_bits = reg_to_bits(&addr.base);
3531
3532                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3533                if let Some(offset_reg) = &addr.offset_reg {
3534                    let rm_bits = reg_to_bits(offset_reg);
3535
3536                    // If there's also an immediate offset, we need to ADD it first
3537                    if addr.offset != 0 {
3538                        // Use R12 (IP) as scratch to avoid clobbering the address register
3539                        // ADD R12, Rm, #offset; STR Rd, [base, R12]
3540                        let scratch = Reg::R12;
3541                        let mut bytes =
3542                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3543                        bytes.extend(self.encode_thumb32_str_reg(rd, &addr.base, &scratch)?);
3544                        return Ok(bytes);
3545                    }
3546
3547                    // Simple register offset: STR Rd, [Rn, Rm]
3548                    // 16-bit: only if Rd, Rn, Rm < R8
3549                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3550                        // STR Rd, [Rn, Rm] (16-bit): 0101 000 Rm Rn Rd
3551                        let instr: u16 = 0x5000
3552                            | ((rm_bits as u16) << 6)
3553                            | ((base_bits as u16) << 3)
3554                            | (rd_bits as u16);
3555                        return Ok(instr.to_le_bytes().to_vec());
3556                    }
3557
3558                    // 32-bit register offset
3559                    return self.encode_thumb32_str_reg(rd, &addr.base, offset_reg);
3560                }
3561
3562                // Immediate offset mode [base, #imm]
3563                let offset = addr.offset as u32;
3564
3565                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3566                    // STR Rd, [Rn, #imm5*4] (16-bit): 0110 0 imm5 Rn Rd
3567                    let imm5 = (offset >> 2) as u16;
3568                    let instr: u16 =
3569                        0x6000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3570                    Ok(instr.to_le_bytes().to_vec())
3571                } else {
3572                    self.encode_thumb32_str(rd, &addr.base, offset)
3573                }
3574            }
3575
3576            // LDRB (Thumb-2)
3577            ArmOp::Ldrb { rd, addr } => {
3578                let rd_bits = reg_to_bits(rd);
3579                let base_bits = reg_to_bits(&addr.base);
3580
3581                if let Some(offset_reg) = &addr.offset_reg {
3582                    if addr.offset != 0 {
3583                        let scratch = Reg::R12;
3584                        let mut bytes =
3585                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3586                        bytes.extend(self.encode_thumb32_ldrb_reg(rd, &addr.base, &scratch)?);
3587                        return Ok(bytes);
3588                    }
3589                    return self.encode_thumb32_ldrb_reg(rd, &addr.base, offset_reg);
3590                }
3591
3592                let offset = addr.offset as u32;
3593                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3594                    // LDRB Rd, [Rn, #imm5] (16-bit): 0111 1 imm5 Rn Rd
3595                    let instr: u16 = 0x7800
3596                        | ((offset as u16) << 6)
3597                        | ((base_bits as u16) << 3)
3598                        | (rd_bits as u16);
3599                    Ok(instr.to_le_bytes().to_vec())
3600                } else {
3601                    self.encode_thumb32_ldrb_imm(rd, &addr.base, offset)
3602                }
3603            }
3604
3605            // LDRSB (Thumb-2)
3606            ArmOp::Ldrsb { rd, addr } => {
3607                let rd_bits = reg_to_bits(rd);
3608                let base_bits = reg_to_bits(&addr.base);
3609
3610                if let Some(offset_reg) = &addr.offset_reg {
3611                    if addr.offset != 0 {
3612                        let scratch = Reg::R12;
3613                        let mut bytes =
3614                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3615                        bytes.extend(self.encode_thumb32_ldrsb_reg(rd, &addr.base, &scratch)?);
3616                        return Ok(bytes);
3617                    }
3618                    return self.encode_thumb32_ldrsb_reg(rd, &addr.base, offset_reg);
3619                }
3620
3621                let offset = addr.offset as u32;
3622                // LDRSB has no 16-bit immediate form (only register)
3623                // For 16-bit reg form: only if Rd, Rn, Rm < R8
3624                if rd_bits < 8 && base_bits < 8 && offset == 0 {
3625                    // No immediate 16-bit encoding for LDRSB; use 32-bit
3626                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3627                } else {
3628                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3629                }
3630            }
3631
3632            // LDRH (Thumb-2)
3633            ArmOp::Ldrh { rd, addr } => {
3634                let rd_bits = reg_to_bits(rd);
3635                let base_bits = reg_to_bits(&addr.base);
3636
3637                if let Some(offset_reg) = &addr.offset_reg {
3638                    if addr.offset != 0 {
3639                        let scratch = Reg::R12;
3640                        let mut bytes =
3641                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3642                        bytes.extend(self.encode_thumb32_ldrh_reg(rd, &addr.base, &scratch)?);
3643                        return Ok(bytes);
3644                    }
3645                    return self.encode_thumb32_ldrh_reg(rd, &addr.base, offset_reg);
3646                }
3647
3648                let offset = addr.offset as u32;
3649                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3650                    // LDRH Rd, [Rn, #imm5*2] (16-bit): 1000 1 imm5 Rn Rd
3651                    let imm5 = (offset >> 1) as u16;
3652                    let instr: u16 =
3653                        0x8800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3654                    Ok(instr.to_le_bytes().to_vec())
3655                } else {
3656                    self.encode_thumb32_ldrh_imm(rd, &addr.base, offset)
3657                }
3658            }
3659
3660            // LDRSH (Thumb-2)
3661            ArmOp::Ldrsh { rd, addr } => {
3662                if let Some(offset_reg) = &addr.offset_reg {
3663                    if addr.offset != 0 {
3664                        let scratch = Reg::R12;
3665                        let mut bytes =
3666                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3667                        bytes.extend(self.encode_thumb32_ldrsh_reg(rd, &addr.base, &scratch)?);
3668                        return Ok(bytes);
3669                    }
3670                    return self.encode_thumb32_ldrsh_reg(rd, &addr.base, offset_reg);
3671                }
3672
3673                let offset = addr.offset as u32;
3674                self.encode_thumb32_ldrsh_imm(rd, &addr.base, offset)
3675            }
3676
3677            // STRB (Thumb-2)
3678            ArmOp::Strb { rd, addr } => {
3679                let rd_bits = reg_to_bits(rd);
3680                let base_bits = reg_to_bits(&addr.base);
3681
3682                if let Some(offset_reg) = &addr.offset_reg {
3683                    if addr.offset != 0 {
3684                        let scratch = Reg::R12;
3685                        let mut bytes =
3686                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3687                        bytes.extend(self.encode_thumb32_strb_reg(rd, &addr.base, &scratch)?);
3688                        return Ok(bytes);
3689                    }
3690                    return self.encode_thumb32_strb_reg(rd, &addr.base, offset_reg);
3691                }
3692
3693                let offset = addr.offset as u32;
3694                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3695                    // STRB Rd, [Rn, #imm5] (16-bit): 0111 0 imm5 Rn Rd
3696                    let instr: u16 = 0x7000
3697                        | ((offset as u16) << 6)
3698                        | ((base_bits as u16) << 3)
3699                        | (rd_bits as u16);
3700                    Ok(instr.to_le_bytes().to_vec())
3701                } else {
3702                    self.encode_thumb32_strb_imm(rd, &addr.base, offset)
3703                }
3704            }
3705
3706            // STRH (Thumb-2)
3707            ArmOp::Strh { rd, addr } => {
3708                let rd_bits = reg_to_bits(rd);
3709                let base_bits = reg_to_bits(&addr.base);
3710
3711                if let Some(offset_reg) = &addr.offset_reg {
3712                    if addr.offset != 0 {
3713                        let scratch = Reg::R12;
3714                        let mut bytes =
3715                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3716                        bytes.extend(self.encode_thumb32_strh_reg(rd, &addr.base, &scratch)?);
3717                        return Ok(bytes);
3718                    }
3719                    return self.encode_thumb32_strh_reg(rd, &addr.base, offset_reg);
3720                }
3721
3722                let offset = addr.offset as u32;
3723                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3724                    // STRH Rd, [Rn, #imm5*2] (16-bit): 1000 0 imm5 Rn Rd
3725                    let imm5 = (offset >> 1) as u16;
3726                    let instr: u16 =
3727                        0x8000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3728                    Ok(instr.to_le_bytes().to_vec())
3729                } else {
3730                    self.encode_thumb32_strh_imm(rd, &addr.base, offset)
3731                }
3732            }
3733
3734            // MemorySize (Thumb-2)
3735            ArmOp::MemorySize { rd } => {
3736                // LSR rd, R10, #16 — memory size in bytes / 65536 = pages
3737                // Thumb-2 16-bit: LSRS Rd, Rm, #imm5 — 0000 1 imm5 Rm Rd
3738                let rd_bits = reg_to_bits(rd);
3739                let r10_bits = reg_to_bits(&Reg::R10);
3740                if rd_bits < 8 && r10_bits < 8 {
3741                    let instr: u16 =
3742                        0x0800 | (16u16 << 6) | ((r10_bits as u16) << 3) | (rd_bits as u16);
3743                    Ok(instr.to_le_bytes().to_vec())
3744                } else {
3745                    // Thumb-2 32-bit LSR: 1110 1010 010 0 1111 | 0 imm3 Rd imm2 01 Rm
3746                    let imm5: u32 = 16;
3747                    let imm3 = (imm5 >> 2) & 0x7;
3748                    let imm2 = imm5 & 0x3;
3749                    let hw1: u16 = 0xEA4F;
3750                    let hw2: u16 =
3751                        ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | 0x10 | r10_bits) as u16;
3752                    let mut bytes = hw1.to_le_bytes().to_vec();
3753                    bytes.extend_from_slice(&hw2.to_le_bytes());
3754                    Ok(bytes)
3755                }
3756            }
3757
3758            // MemoryGrow (Thumb-2)
3759            ArmOp::MemoryGrow { rd, .. } => {
3760                // On embedded with fixed memory, always return -1 (failure)
3761                // MVN rd, #0 → MOV rd, #-1
3762                // Thumb-2 32-bit: MVN: 1111 0 i 0 0 0 1 1 0 1111 | 0 imm3 Rd imm8
3763                let rd_bits = reg_to_bits(rd);
3764                let hw1: u16 = 0xF06F; // MVN with i=0
3765                let hw2: u16 = (rd_bits << 8) as u16; // imm8=0 → ~0 = 0xFFFFFFFF = -1
3766                let mut bytes = hw1.to_le_bytes().to_vec();
3767                bytes.extend_from_slice(&hw2.to_le_bytes());
3768                Ok(bytes)
3769            }
3770
3771            // BX (16-bit)
3772            ArmOp::Bx { rm } => {
3773                let rm_bits = reg_to_bits(rm) as u16;
3774                // BX Rm (16-bit): 0100 0111 0 Rm 000
3775                let instr: u16 = 0x4700 | (rm_bits << 3);
3776                Ok(instr.to_le_bytes().to_vec())
3777            }
3778
3779            // BLX (16-bit) - Branch with Link and Exchange
3780            // BLX Rm: 0100 0111 1 Rm 000
3781            ArmOp::Blx { rm } => {
3782                let rm_bits = reg_to_bits(rm) as u16;
3783                let instr: u16 = 0x4780 | (rm_bits << 3);
3784                Ok(instr.to_le_bytes().to_vec())
3785            }
3786
3787            // CallIndirect - indirect function call via table lookup
3788            // table_index_reg contains the table index
3789            // Generates (#642): MOVW ip,#size [; MOVT]; CMP idx,ip; BLO +1;
3790            //                   UDF #0; LSL R12,idx,#2; LDR R12,[R11,R12]; BLX R12
3791            // #650, table_byte_offset != 0 (a non-zero table in the contiguous
3792            // R11 region): the pointer load becomes
3793            //                   ADD R12,R11,R12; LDR R12,[R12,#offset]
3794            // #664, null_check (the table has null slots, linked as ZERO
3795            // words): the loaded pointer is null-checked before the BLX —
3796            //                   CMP.W R12,#0; BNE +1; UDF #0
3797            // #676, type_check (heterogeneous table — runtime §4.4.8 type
3798            // check against the type-id sidecar at R11+off): after the
3799            // bounds guard —
3800            //                   LSL R12,idx,#2; ADD R12,R11,R12;
3801            //                   LDR R12,[R12,#type_off]; CMP.W R12,#id;
3802            //                   BEQ +1; UDF #0
3803            // (the dispatch tail then recomputes idx*4 — idx stays live).
3804            ArmOp::CallIndirect {
3805                rd: _,
3806                type_idx: _,
3807                table_index_reg,
3808                table_size,
3809                table_byte_offset,
3810                null_check,
3811                type_check,
3812            } => {
3813                let idx_reg = reg_to_bits(table_index_reg);
3814                let mut bytes = Vec::new();
3815
3816                // The expansion:
3817                // 1. Bounds guard (#642): trap (UDF #0, WASM Core §4.4.8) when
3818                //    index >= table size. Without it an out-of-bounds index
3819                //    reads past the table and BLXes whatever word lies there —
3820                //    an uncontrolled indirect branch instead of a trap.
3821                // 2. Multiplies index by 4 (function pointer size)
3822                // 3. Loads function pointer from table (table base in R11)
3823                // 4. Calls the function via BLX
3824                //
3825                // Table base setup must be done by caller/runtime. The type
3826                // check §4.4.8 also requires is discharged at COMPILE time:
3827                // the selector only emits this op after verifying the closed-
3828                // world property that every table entry's signature equals the
3829                // expected type (the raw code-pointer table carries no runtime
3830                // type ids to compare) — see the #642 selector guard.
3831
3832                // MOVW R12, #(size & 0xFFFF) — Thumb-2 T3:
3833                // 11110 i 100100 imm4 | 0 imm3 Rd imm8 (Rd=R12).
3834                let size_lo = *table_size & 0xFFFF;
3835                let hw1: u16 =
3836                    (0xF240 | (((size_lo >> 11) & 1) << 10) | ((size_lo >> 12) & 0xF)) as u16;
3837                let hw2: u16 =
3838                    ((((size_lo >> 8) & 0x7) << 12) | (12 << 8) | (size_lo & 0xFF)) as u16;
3839                bytes.extend_from_slice(&hw1.to_le_bytes());
3840                bytes.extend_from_slice(&hw2.to_le_bytes());
3841                // MOVT R12, #(size >> 16) — only when the table size exceeds
3842                // 16 bits (never in practice, but the guard must not compare
3843                // against a truncated size).
3844                let size_hi = *table_size >> 16;
3845                if size_hi != 0 {
3846                    let hw1: u16 =
3847                        (0xF2C0 | (((size_hi >> 11) & 1) << 10) | ((size_hi >> 12) & 0xF)) as u16;
3848                    let hw2: u16 =
3849                        ((((size_hi >> 8) & 0x7) << 12) | (12 << 8) | (size_hi & 0xFF)) as u16;
3850                    bytes.extend_from_slice(&hw1.to_le_bytes());
3851                    bytes.extend_from_slice(&hw2.to_le_bytes());
3852                }
3853                // CMP idx, R12 — 16-bit T2 (high-register capable):
3854                // 010001 01 N Rm(4) Rn(3), Rn full = N:Rn3.
3855                let cmp: u16 = (0x4500 | ((idx_reg & 8) << 4) | (12 << 3) | (idx_reg & 7)) as u16;
3856                bytes.extend_from_slice(&cmp.to_le_bytes());
3857                // BLO +1 insn (skip the UDF when index < size) — B<cond>.N
3858                // imm8=0: target = branch + 4. LO = unsigned lower.
3859                bytes.extend_from_slice(&0xD300u16.to_le_bytes());
3860                // UDF #0 — call_indirect out-of-bounds trap (same trap idiom as
3861                // the div-by-zero guards).
3862                bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3863
3864                // #676: runtime type check — ONLY for a heterogeneous table
3865                // (mixed signatures, closed-world verdict impossible). Load
3866                // the indexed slot's structural class id from the type-id
3867                // sidecar (`R11 + type_off + idx*4`; `type_off` = sidecar
3868                // base + this table's base offset, a compile-time constant)
3869                // and compare it against the expected type's class id — a
3870                // mismatch is the WASM Core §4.4.8 type trap. Null slots
3871                // carry the reserved id 0, so this compare subsumes the
3872                // #664 null trap (the selector passes `null_check: false`).
3873                // `None` emits NOTHING: every homogeneous table keeps the
3874                // pre-#676 bytes identical BY CONSTRUCTION. R12 stays the
3875                // only scratch (#212); the dispatch tail below recomputes
3876                // idx*4 — the index register is never clobbered here.
3877                if let Some((expected_id, type_off)) = type_check {
3878                    debug_assert!(*expected_id <= 255, "selector enforces the CMP imm8 range");
3879                    debug_assert!(*type_off <= 4095, "selector enforces the LDR imm12 range");
3880                    // MOV.W R12, idx, LSL #2 (same encoding as the dispatch
3881                    // tail's index scale below).
3882                    bytes.extend_from_slice(&0xEA4Fu16.to_le_bytes());
3883                    bytes.extend_from_slice(
3884                        &(((0x0C00 | (0b10 << 6)) | idx_reg) as u16).to_le_bytes(),
3885                    );
3886                    // ADD.W R12, R11, R12 (the #650 base-add form).
3887                    bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
3888                    bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
3889                    // LDR.W R12, [R12, #type_off] — T3 LDR (immediate):
3890                    // 1111 1000 1101 Rn=1100 | Rt=1100 imm12.
3891                    bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
3892                    bytes.extend_from_slice(
3893                        &(0xC000u16 | (*type_off as u16 & 0x0FFF)).to_le_bytes(),
3894                    );
3895                    // CMP.W R12, #expected_id — T2 CMP (immediate), imm8
3896                    // (same form as the #664 null check's CMP.W R12, #0).
3897                    bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
3898                    bytes.extend_from_slice(
3899                        &(0x0F00u16 | (*expected_id as u16 & 0xFF)).to_le_bytes(),
3900                    );
3901                    // BEQ +1 insn (skip the UDF when the class id matches) —
3902                    // B<cond>.N imm8=0: target = branch + 4. EQ.
3903                    bytes.extend_from_slice(&0xD000u16.to_le_bytes());
3904                    // UDF #0 — the §4.4.8 type-mismatch trap (same idiom as
3905                    // the bounds guard above).
3906                    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3907                }
3908
3909                // LSL R12, idx_reg, #2 (multiply index by 4)
3910                // Thumb-2 MOV with shift: 11101010 010 S 1111 | 0 imm3 Rd imm2 type Rm
3911                // LSL: type=00 (bits 5:4), imm5=2 -> imm3=000, imm2=10 (bits 7:6)
3912                // #597: the shift amount was previously shifted into bits 5:4 —
3913                // the TYPE field — encoding `mov.w ip, rm, ASR #32`, which
3914                // destroyed the index and dispatched table entry 0 for every
3915                // call. imm2 lives at bits 7:6.
3916                let hw1: u16 = 0xEA4F_u16; // MOV.W R12, Rm, LSL #2
3917                let hw2: u16 = ((0x0C00 | (0b10 << 6)) | idx_reg) as u16;
3918                bytes.extend_from_slice(&hw1.to_le_bytes());
3919                bytes.extend_from_slice(&hw2.to_le_bytes());
3920
3921                if *table_byte_offset == 0 {
3922                    // Table 0 (base = R11 itself): the pre-#650 single-load
3923                    // form — a single-table module's bytes stay identical BY
3924                    // CONSTRUCTION.
3925                    // LDR R12, [R11, R12] - load function pointer
3926                    // Thumb-2 LDR (register): 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
3927                    // Rn=R11, Rt=R12, Rm=R12, imm2=00 (no shift)
3928                    let ldr_hw1: u16 = 0xF85B; // LDR.W Rt, [R11, Rm]
3929                    let ldr_hw2: u16 = 0xC00C; // Rt=R12, imm2=00, Rm=R12
3930                    bytes.extend_from_slice(&ldr_hw1.to_le_bytes());
3931                    bytes.extend_from_slice(&ldr_hw2.to_le_bytes());
3932                } else {
3933                    // #650: table N of the contiguous R11 region — fold the
3934                    // compile-time base offset into the pointer load via the
3935                    // LDR imm12 form (R12 stays the only scratch, per the
3936                    // #212 convention).
3937                    assert!(
3938                        *table_byte_offset <= 4095,
3939                        "call_indirect table base offset {table_byte_offset} exceeds \
3940                         LDR imm12 — the selector must have declined this (#650)"
3941                    );
3942                    // ADD.W R12, R11, R12 — T3 ADD (register):
3943                    // 11101011000 S=0 Rn=1011 | 0 imm3=000 Rd=1100 imm2=00 type=00 Rm=1100
3944                    bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
3945                    bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
3946                    // LDR.W R12, [R12, #offset] — T3 LDR (immediate):
3947                    // 1111 1000 1101 Rn=1100 | Rt=1100 imm12
3948                    bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
3949                    bytes.extend_from_slice(
3950                        &((0xC000u16) | (*table_byte_offset as u16 & 0x0FFF)).to_le_bytes(),
3951                    );
3952                }
3953
3954                // #664: null-slot trap — ONLY when the table image carries
3955                // null (uninitialized) slots, which the layout contract
3956                // requires to be linked as ZERO words. A fully-initialized
3957                // table skips this branch entirely, keeping the pre-#664
3958                // expansion byte-identical BY CONSTRUCTION (the #650
3959                // offset-0 trick).
3960                if *null_check {
3961                    // CMP.W R12, #0 — T2 CMP (immediate): 11110 i 0 1101 1
3962                    // Rn(4) | 0 imm3 1111 imm8, Rn=R12, imm=0.
3963                    bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
3964                    bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
3965                    // BNE +1 insn (skip the UDF when the pointer is non-null)
3966                    // — B<cond>.N imm8=0: target = branch + 4. NE.
3967                    bytes.extend_from_slice(&0xD100u16.to_le_bytes());
3968                    // UDF #0 — call_indirect null-funcref trap (WASM Core
3969                    // §4.4.8: calling an uninitialized element traps; same
3970                    // trap idiom as the bounds guard above).
3971                    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3972                }
3973
3974                // BLX R12 (call function indirectly)
3975                // BLX Rm (16-bit): 0100 0111 1 Rm 000
3976                let blx: u16 = 0x47E0; // BLX R12
3977                bytes.extend_from_slice(&blx.to_le_bytes());
3978
3979                Ok(bytes)
3980            }
3981
3982            // Label pseudo-instruction: emits no machine code
3983            ArmOp::Label { .. } => Ok(Vec::new()),
3984
3985            // Conditional branch to label (generic) - offset 0, will be patched
3986            ArmOp::Bcc { cond, label: _ } => {
3987                use synth_synthesis::Condition;
3988                let cond_bits: u16 = match cond {
3989                    Condition::EQ => 0x0,
3990                    Condition::NE => 0x1,
3991                    Condition::HS => 0x2,
3992                    Condition::LO => 0x3,
3993                    Condition::HI => 0x8,
3994                    Condition::LS => 0x9,
3995                    Condition::GE => 0xA,
3996                    Condition::LT => 0xB,
3997                    Condition::GT => 0xC,
3998                    Condition::LE => 0xD,
3999                };
4000                // 16-bit B<cond> with offset 0: 1101 cond imm8
4001                let instr: u16 = 0xD000 | (cond_bits << 8);
4002                Ok(instr.to_le_bytes().to_vec())
4003            }
4004
4005            // Branch instructions
4006            ArmOp::B { label: _ } => {
4007                // Simplified: B.N with offset 0
4008                // For real usage, would need label resolution
4009                let instr: u16 = 0xE000; // B.N #0
4010                Ok(instr.to_le_bytes().to_vec())
4011            }
4012
4013            // BHS (Branch if Higher or Same) - used for bounds checking
4014            // Condition code: 0x2 (C set)
4015            ArmOp::Bhs { label: _ } => {
4016                // 16-bit B<cond> with offset 0: 1101 cond imm8
4017                // cond = 0x2 (HS)
4018                let instr: u16 = 0xD200; // BHS.N #0
4019                Ok(instr.to_le_bytes().to_vec())
4020            }
4021
4022            // BLO (Branch if Lower) - complementary to BHS
4023            // Condition code: 0x3 (C clear)
4024            ArmOp::Blo { label: _ } => {
4025                // 16-bit B<cond> with offset 0: 1101 cond imm8
4026                // cond = 0x3 (LO)
4027                let instr: u16 = 0xD300; // BLO.N #0
4028                Ok(instr.to_le_bytes().to_vec())
4029            }
4030
4031            // Branch with numeric offset (Thumb-2)
4032            // Thumb-2 B.W instruction: 32-bit with +-16MB range
4033            ArmOp::BOffset { offset } => {
4034                // offset is already the halfword displacement: (target - branch - 4) / 2
4035                // This is the raw encoded value, accounting for variable-length instructions
4036                let halfword_offset = *offset;
4037
4038                // 16-bit B.N encoding: 1110 0 imm11 (11-bit signed halfword offset)
4039                // Range: -1024 to +1022 halfwords
4040                if (-1024..=1022).contains(&halfword_offset) {
4041                    // 16-bit B.N encoding: 1110 0 imm11
4042                    let imm11 = (halfword_offset as u16) & 0x7FF;
4043                    let instr: u16 = 0xE000 | imm11;
4044                    Ok(instr.to_le_bytes().to_vec())
4045                } else {
4046                    // 32-bit B.W encoding for larger offsets
4047                    // First halfword: 1111 0 S imm10
4048                    // Second halfword: 10 J1 0 J2 imm11
4049                    // Total offset = SignExtend(S:I1:I2:imm10:imm11:0)
4050                    // where I1 = NOT(J1 XOR S), I2 = NOT(J2 XOR S)
4051
4052                    // The B.W (T4) encoding packs the signed offset as:
4053                    //   S:I1:I2:imm10:imm11:0  (25-bit signed, halfword-aligned)
4054                    // where J1 = NOT(I1 XOR S), J2 = NOT(I2 XOR S)
4055                    // Input halfword_offset already equals (target - PC - 4) / 2,
4056                    // so the full byte offset = halfword_offset << 1.
4057                    // The encoding fields split that 25-bit signed value (including the
4058                    // implicit trailing zero) as: S | imm10 | imm11
4059                    // with I1 = bit 23 and I2 = bit 22 of the signed offset.
4060                    let signed_offset = halfword_offset << 1; // byte offset
4061                    let s = if signed_offset < 0 { 1u32 } else { 0u32 };
4062                    let uoffset = signed_offset as u32;
4063                    let imm10 = (uoffset >> 12) & 0x3FF; // bits [21:12]
4064                    let imm11 = (uoffset >> 1) & 0x7FF; // bits [11:1]
4065                    let i1 = (uoffset >> 23) & 1; // bit 23
4066                    let i2 = (uoffset >> 22) & 1; // bit 22
4067                    let j1 = (!(i1 ^ s)) & 1; // J1 = NOT(I1 XOR S)
4068                    let j2 = (!(i2 ^ s)) & 1; // J2 = NOT(I2 XOR S)
4069
4070                    let hw1: u16 = (0xF000 | (s << 10) | imm10) as u16;
4071                    let hw2: u16 = (0x9000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4072
4073                    let mut bytes = hw1.to_le_bytes().to_vec();
4074                    bytes.extend_from_slice(&hw2.to_le_bytes());
4075                    Ok(bytes)
4076                }
4077            }
4078
4079            // Conditional branch with numeric offset (Thumb-2)
4080            ArmOp::BCondOffset { cond, offset } => {
4081                use synth_synthesis::Condition;
4082                let cond_bits: u16 = match cond {
4083                    Condition::EQ => 0x0,
4084                    Condition::NE => 0x1,
4085                    Condition::HS => 0x2,
4086                    Condition::LO => 0x3,
4087                    Condition::HI => 0x8,
4088                    Condition::LS => 0x9,
4089                    Condition::GE => 0xA,
4090                    Condition::LT => 0xB,
4091                    Condition::GT => 0xC,
4092                    Condition::LE => 0xD,
4093                };
4094
4095                // offset is already the halfword displacement: (target - branch - 4) / 2
4096                // This is the raw imm8 value for 16-bit B<cond> encoding
4097                let halfword_offset = *offset;
4098
4099                // 16-bit B<cond> encoding: 1101 cond imm8
4100                // Range: -256 to +254 halfwords (imm8 is sign-extended and shifted left 1)
4101                if (-128..=127).contains(&halfword_offset) {
4102                    let imm8 = (halfword_offset as u16) & 0xFF;
4103                    let instr: u16 = 0xD000 | (cond_bits << 8) | imm8;
4104                    Ok(instr.to_le_bytes().to_vec())
4105                } else {
4106                    // 32-bit B<cond>.W (encoding T3) for larger offsets
4107                    // First halfword: 1111 0 S cond(4) imm6
4108                    // Second halfword: 10 J1 0 J2 imm11
4109                    //
4110                    // Per ARMv7-M, the branch BYTE offset is
4111                    // SignExtend(S:J2:J1:imm6:imm11:'0'), i.e. the field value
4112                    // S:J2:J1:imm6:imm11 IS the signed 20-bit HALFWORD offset —
4113                    // imm11/imm6/J1/J2/S take `halfword_offset` bits [10:0],
4114                    // [16:11], 17, 18 and 19 directly (mirroring the T4
4115                    // unconditional arm above).
4116                    //
4117                    // #740: this arm previously packed `halfword_offset >> 1`
4118                    // into imm6:imm11 — HALVING the displacement — so every
4119                    // wide conditional branch (span > 254 bytes) landed at half
4120                    // its intended offset: gust_poll's loop-head `br_if` to an
4121                    // outer block end jumped mid-shape. Narrow (16-bit) B<cond>
4122                    // encodings were unaffected, which is why short-range CF
4123                    // fixtures never caught it.
4124                    if !(-(1 << 19)..(1 << 19)).contains(&halfword_offset) {
4125                        return Err(synth_core::Error::synthesis(format!(
4126                            "B<cond>.W (T3) halfword offset {halfword_offset} exceeds \
4127                             the signed 20-bit encoding range (±1 MB) — refusing to \
4128                             emit a truncated branch"
4129                        )));
4130                    }
4131                    let u = halfword_offset as u32;
4132                    let imm11 = u & 0x7FF; // halfword offset bits [10:0]
4133                    let imm6 = (u >> 11) & 0x3F; // bits [16:11]
4134                    let j1 = (u >> 17) & 1; // bit 17
4135                    let j2 = (u >> 18) & 1; // bit 18
4136                    let s = (u >> 19) & 1; // sign (range-checked above)
4137
4138                    let hw1: u16 = (0xF000 | (s << 10) | ((cond_bits as u32) << 6) | imm6) as u16;
4139                    let hw2: u16 = (0x8000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4140
4141                    let mut bytes = hw1.to_le_bytes().to_vec();
4142                    bytes.extend_from_slice(&hw2.to_le_bytes());
4143                    Ok(bytes)
4144                }
4145            }
4146
4147            ArmOp::Bl { label: _ } => {
4148                // BL is always 32-bit in Thumb-2, encoded here as a relocatable
4149                // placeholder; an R_ARM_THM_CALL relocation patches the target
4150                // (see arm_backend.rs). The placeholder must carry an embedded
4151                // addend of -4 so the relocation nets to exactly the symbol S.
4152                //
4153                // Thumb BL computes `target = (P + 4) + signed_offset`. Under
4154                // R_ARM_THM_CALL the linker resolves using the in-place addend;
4155                // a 0xF800 placeholder (addend 0) lands at S+4 — every call one
4156                // instruction past the callee entry (#174). The correct
4157                // placeholder is what `gas` emits for `bl <extern>`:
4158                //   f7ff fffe  ->  `bl <self>`  (S=1, J1=J2=1, imm = -4 addend),
4159                // i.e. hw1=0xF7FF, hw2=0xFFFE. This nets to S, not S+4.
4160                // (The earlier 0xD000 was worse still — a ~+0x600000 addend,
4161                // the garbage `bl c0000c` and "truncated to fit" of #167.)
4162                let hw1: u16 = 0xF7FF;
4163                let hw2: u16 = 0xFFFE;
4164                let mut bytes = hw1.to_le_bytes().to_vec();
4165                bytes.extend_from_slice(&hw2.to_le_bytes());
4166                Ok(bytes)
4167            }
4168
4169            // MVN
4170            ArmOp::Mvn { rd, op2 } => {
4171                if let Operand2::Reg(rm) = op2 {
4172                    let rd_bits = reg_to_bits(rd) as u16;
4173                    let rm_bits = reg_to_bits(rm) as u16;
4174
4175                    if rd_bits < 8 && rm_bits < 8 {
4176                        // MVNS Rd, Rm (16-bit): 0100 0011 11 Rm Rd
4177                        let instr: u16 = 0x43C0 | (rm_bits << 3) | rd_bits;
4178                        Ok(instr.to_le_bytes().to_vec())
4179                    } else {
4180                        // 32-bit MVN
4181                        let hw1: u16 = 0xEA6F_u16;
4182                        let hw2: u16 = ((reg_to_bits(rd) << 8) | reg_to_bits(rm)) as u16;
4183                        let mut bytes = hw1.to_le_bytes().to_vec();
4184                        bytes.extend_from_slice(&hw2.to_le_bytes());
4185                        Ok(bytes)
4186                    }
4187                } else {
4188                    let instr: u16 = 0xBF00;
4189                    Ok(instr.to_le_bytes().to_vec())
4190                }
4191            }
4192
4193            // MOVW - Move Wide (Thumb-2 32-bit)
4194            ArmOp::Movw { rd, imm16 } => {
4195                self.encode_thumb32_movw_raw(reg_to_bits(rd), *imm16 as u32)
4196            }
4197
4198            // MOVT - Move Top (Thumb-2 32-bit)
4199            ArmOp::Movt { rd, imm16 } => {
4200                self.encode_thumb32_movt_raw(reg_to_bits(rd), *imm16 as u32)
4201            }
4202
4203            // #237: symbol-relative MOVW/MOVT. Encode the addend's low/high 16
4204            // bits in place; the backend records an R_ARM_MOVW_ABS_NC /
4205            // R_ARM_MOVT_ABS relocation against `symbol`, so the linker adds the
4206            // symbol's final address to the in-place addend (REL semantics).
4207            ArmOp::MovwSym { rd, addend, .. } => {
4208                self.encode_thumb32_movw_raw(reg_to_bits(rd), (*addend as u32) & 0xffff)
4209            }
4210            ArmOp::MovtSym { rd, addend, .. } => {
4211                self.encode_thumb32_movt_raw(reg_to_bits(rd), ((*addend as u32) >> 16) & 0xffff)
4212            }
4213
4214            // #345: literal-pool address load — emit a PLACEHOLDER `LDR.W rd,
4215            // [pc, #0]` (U=1, imm12=0). The backend (arm_backend.rs) places the
4216            // 4-byte pool word at the end of the function, records the R_ARM_ABS32
4217            // relocation against `symbol+addend`, and patches the imm12 with the
4218            // real PC-relative distance once the pool offset is known.
4219            // Encoding T2: 1111 1000 1101 1111 | Rt(4) imm12(12), with the literal
4220            // base = Align(PC,4) and PC = address of this instruction + 4.
4221            ArmOp::LdrSym { rd, .. } => {
4222                let rt = reg_to_bits(rd) as u16;
4223                let hw1: u16 = 0xF8DF; // LDR.W (literal), U=1
4224                let hw2: u16 = rt << 12; // imm12 = 0 placeholder
4225                let mut bytes = Vec::with_capacity(4);
4226                bytes.extend_from_slice(&hw1.to_le_bytes());
4227                bytes.extend_from_slice(&hw2.to_le_bytes());
4228                Ok(bytes)
4229            }
4230
4231            // SetCond: Materialize condition flag into register (0 or 1)
4232            // Strategy: ITE <cond>; MOV Rd, #1; MOV Rd, #0
4233            // IMPORTANT: Must use ITE (If-Then-Else) because 16-bit Thumb MOV
4234            // always sets flags (MOVS). We need to evaluate the condition BEFORE
4235            // any MOV instruction clobbers the flags from CMP.
4236            ArmOp::SetCond { rd, cond } => {
4237                let rd_bits = reg_to_bits(rd) as u16;
4238
4239                // Condition code encoding for IT block
4240                use synth_synthesis::Condition;
4241                let cond_bits: u16 = match cond {
4242                    Condition::EQ => 0x0,
4243                    Condition::NE => 0x1,
4244                    Condition::LT => 0xB,
4245                    Condition::LE => 0xD,
4246                    Condition::GT => 0xC,
4247                    Condition::GE => 0xA,
4248                    Condition::LO => 0x3, // CC/LO (unsigned <)
4249                    Condition::LS => 0x9, // LS (unsigned <=)
4250                    Condition::HI => 0x8, // HI (unsigned >)
4251                    Condition::HS => 0x2, // CS/HS (unsigned >=)
4252                };
4253
4254                // ITE <cond>: encodes If-Then-Else block
4255                // The mask field depends on firstcond[0]:
4256                // - If firstcond[0] = 0: mask = 0xC for TE pattern (ITE EQ = BF0C)
4257                // - If firstcond[0] = 1: mask = 0x4 for TE pattern (ITE NE = BF14)
4258                let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4259                let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4260
4261                // Materialize 0/1 into Rd. The 16-bit MOVS (T1) encodes Rd in a
4262                // 3-bit field (bits[10:8]) — only R0–R7. For a high register
4263                // (R8–R12) `rd_bits << 8` overflows into bit 11 and silently
4264                // turns MOVS into CMP (00100 → 00101), corrupting the result
4265                // (this mis-materialized gale's `has_waiter`, so its `local.set`
4266                // stored a stale register → the binary-sem WAKE dispatch read
4267                // garbage). Use the 32-bit MOV.W (T2) for high registers, which
4268                // has a 4-bit Rd field. MOV.W with S=0 doesn't set flags, which
4269                // is fine inside the ITE (the materialized value is the result;
4270                // the flags are not consumed afterwards).
4271                let mut bytes = ite_instr.to_le_bytes().to_vec();
4272                let push_mov = |bytes: &mut Vec<u8>, imm: u16| {
4273                    if rd_bits <= 7 {
4274                        let m: u16 = 0x2000 | (rd_bits << 8) | imm; // 16-bit MOVS Rd,#imm
4275                        bytes.extend_from_slice(&m.to_le_bytes());
4276                    } else {
4277                        // 32-bit MOV.W Rd, #imm (T2): F04F | (Rd<<8) | imm8
4278                        let hw1: u16 = 0xF04F;
4279                        let hw2: u16 = (rd_bits << 8) | imm;
4280                        bytes.extend_from_slice(&hw1.to_le_bytes());
4281                        bytes.extend_from_slice(&hw2.to_le_bytes());
4282                    }
4283                };
4284                push_mov(&mut bytes, 1); // Then branch (condition true)  → 1
4285                push_mov(&mut bytes, 0); // Else branch (condition false) → 0
4286                Ok(bytes)
4287            }
4288
4289            // I64SetCond: Compare two i64 register pairs, result 0/1 in rd
4290            // EQ/NE: CMP lo,lo; IT EQ; CMPEQ hi,hi; ITE <cond>; MOV 1; MOV 0
4291            // LT: CMP lo,lo; SBCS rd,hi,hi; ITE LT; MOV 1; MOV 0
4292            // GT: CMP lo,lo (swapped); SBCS rd,hi,hi (swapped); ITE LT; MOV 1; MOV 0
4293            ArmOp::I64SetCond {
4294                rd,
4295                rn_lo,
4296                rn_hi,
4297                rm_lo,
4298                rm_hi,
4299                cond,
4300            } => {
4301                use synth_synthesis::Condition;
4302                let rd_bits = reg_to_bits(rd) as u16;
4303                let mut bytes = Vec::new();
4304
4305                // Helper: encode CMP Rn, Rm (16-bit)
4306                let encode_cmp_reg = |rn: &synth_synthesis::Reg,
4307                                      rm: &synth_synthesis::Reg|
4308                 -> Vec<u8> {
4309                    let rn_bits = reg_to_bits(rn) as u16;
4310                    let rm_bits = reg_to_bits(rm) as u16;
4311                    if rn_bits < 8 && rm_bits < 8 {
4312                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
4313                        instr.to_le_bytes().to_vec()
4314                    } else {
4315                        let n_bit = (rn_bits >> 3) & 1;
4316                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
4317                        instr.to_le_bytes().to_vec()
4318                    }
4319                };
4320
4321                // Helper: encode ITE <cond> (2 bytes)
4322                let encode_ite = |cond_bits: u16| -> Vec<u8> {
4323                    let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4324                    let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4325                    ite_instr.to_le_bytes().to_vec()
4326                };
4327
4328                // Helper: encode SetCond (ITE + MOV #1 + MOV #0) for given condition
4329                let encode_setcond = |cond_bits: u16, rd_bits: u16| -> Vec<u8> {
4330                    let mut b = encode_ite(cond_bits);
4331                    if rd_bits < 8 {
4332                        let mov_one: u16 = 0x2001 | (rd_bits << 8);
4333                        let mov_zero: u16 = 0x2000 | (rd_bits << 8);
4334                        b.extend_from_slice(&mov_one.to_le_bytes());
4335                        b.extend_from_slice(&mov_zero.to_le_bytes());
4336                    } else {
4337                        // #311: rd >= R8 — the 16-bit MOV imm8 form has a 3-bit
4338                        // rd field; rd_bits<<8 overflows into bit 11 and
4339                        // TRANSMUTES the MOV into CMP (0x2001|0x0800 = 0x2801 =
4340                        // CMP r0,#1): the boolean dies in the flags and the
4341                        // consumer reads a stale register. Use the 32-bit
4342                        // MOV.W (T2: F04F 0000|rd<<8|imm8) — IT-legal,
4343                        // flag-preserving. Same class as H-CODE-9 / #180.
4344                        for imm in [1u16, 0u16] {
4345                            let hw1: u16 = 0xF04F;
4346                            let hw2: u16 = (rd_bits << 8) | imm;
4347                            b.extend_from_slice(&hw1.to_le_bytes());
4348                            b.extend_from_slice(&hw2.to_le_bytes());
4349                        }
4350                    }
4351                    b
4352                };
4353
4354                match cond {
4355                    Condition::EQ | Condition::NE => {
4356                        // CMP rn_lo, rm_lo (compare low words)
4357                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4358
4359                        // IT EQ (execute next instruction only if Z=1)
4360                        let it_eq: u16 = 0xBF08; // IT EQ: cond=0000, mask=1000
4361                        bytes.extend_from_slice(&it_eq.to_le_bytes());
4362
4363                        // CMPEQ rn_hi, rm_hi (compare high words, only if low equal)
4364                        bytes.extend_from_slice(&encode_cmp_reg(rn_hi, rm_hi));
4365
4366                        // ITE <cond>; MOV rd, #1; MOV rd, #0
4367                        let cond_bits: u16 = match cond {
4368                            Condition::EQ => 0x0,
4369                            Condition::NE => 0x1,
4370                            _ => unreachable!(),
4371                        };
4372                        bytes.extend_from_slice(&encode_setcond(cond_bits, rd_bits));
4373                    }
4374
4375                    Condition::LT => {
4376                        // CMP rn_lo, rm_lo (sets C flag for borrow)
4377                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4378
4379                        // SBCS rd, rn_hi, rm_hi (subtract with carry, sets N,V flags)
4380                        // SBCS.W Rd, Rn, Rm: EB70 Rn | 0000 Rd 0000 Rm
4381                        let rn_hi_bits = reg_to_bits(rn_hi);
4382                        let rm_hi_bits = reg_to_bits(rm_hi);
4383                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4384                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4385                        bytes.extend_from_slice(&hw1.to_le_bytes());
4386                        bytes.extend_from_slice(&hw2.to_le_bytes());
4387
4388                        // ITE LT; MOV rd, #1; MOV rd, #0
4389                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4390                    }
4391
4392                    Condition::GT => {
4393                        // GT(a,b) = LT(b,a): swap operands
4394                        // CMP rm_lo, rn_lo (swapped)
4395                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4396
4397                        // SBCS rd, rm_hi, rn_hi (swapped)
4398                        let rm_hi_bits = reg_to_bits(rm_hi);
4399                        let rn_hi_bits = reg_to_bits(rn_hi);
4400                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4401                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4402                        bytes.extend_from_slice(&hw1.to_le_bytes());
4403                        bytes.extend_from_slice(&hw2.to_le_bytes());
4404
4405                        // ITE LT; MOV rd, #1; MOV rd, #0
4406                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4407                    }
4408
4409                    Condition::LE => {
4410                        // LE(a,b) = !GT(a,b): use GT logic but invert result
4411                        // GT(a,b) = LT(b,a): so we do CMP(b,a) and check LT, then invert
4412                        // CMP rm_lo, rn_lo (swapped, same as GT)
4413                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4414
4415                        // SBCS rd, rm_hi, rn_hi (swapped)
4416                        let rm_hi_bits = reg_to_bits(rm_hi);
4417                        let rn_hi_bits = reg_to_bits(rn_hi);
4418                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4419                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4420                        bytes.extend_from_slice(&hw1.to_le_bytes());
4421                        bytes.extend_from_slice(&hw2.to_le_bytes());
4422
4423                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT, so inverting GT result)
4424                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4425                    }
4426
4427                    Condition::GE => {
4428                        // GE(a,b) = !LT(a,b): use LT logic but invert result
4429                        // CMP rn_lo, rm_lo (same as LT)
4430                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4431
4432                        // SBCS rd, rn_hi, rm_hi (same as LT)
4433                        let rn_hi_bits = reg_to_bits(rn_hi);
4434                        let rm_hi_bits = reg_to_bits(rm_hi);
4435                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4436                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4437                        bytes.extend_from_slice(&hw1.to_le_bytes());
4438                        bytes.extend_from_slice(&hw2.to_le_bytes());
4439
4440                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT)
4441                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4442                    }
4443
4444                    // Unsigned comparisons - same instruction sequence, different conditions
4445                    Condition::LO => {
4446                        // LO (unsigned LT): CMP lo, SBCS hi, check C=0
4447                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4448                        let rn_hi_bits = reg_to_bits(rn_hi);
4449                        let rm_hi_bits = reg_to_bits(rm_hi);
4450                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4451                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4452                        bytes.extend_from_slice(&hw1.to_le_bytes());
4453                        bytes.extend_from_slice(&hw2.to_le_bytes());
4454                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4455                    }
4456
4457                    Condition::HI => {
4458                        // HI (unsigned GT): swap operands and check LO
4459                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4460                        let rm_hi_bits = reg_to_bits(rm_hi);
4461                        let rn_hi_bits = reg_to_bits(rn_hi);
4462                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4463                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4464                        bytes.extend_from_slice(&hw1.to_le_bytes());
4465                        bytes.extend_from_slice(&hw2.to_le_bytes());
4466                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4467                    }
4468
4469                    Condition::LS => {
4470                        // LS (unsigned LE): !(a > b) = !(HI), so do HI and invert
4471                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4472                        let rm_hi_bits = reg_to_bits(rm_hi);
4473                        let rn_hi_bits = reg_to_bits(rn_hi);
4474                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4475                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4476                        bytes.extend_from_slice(&hw1.to_le_bytes());
4477                        bytes.extend_from_slice(&hw2.to_le_bytes());
4478                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4479                    }
4480
4481                    Condition::HS => {
4482                        // HS (unsigned GE): !(a < b) = !(LO)
4483                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4484                        let rn_hi_bits = reg_to_bits(rn_hi);
4485                        let rm_hi_bits = reg_to_bits(rm_hi);
4486                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4487                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4488                        bytes.extend_from_slice(&hw1.to_le_bytes());
4489                        bytes.extend_from_slice(&hw2.to_le_bytes());
4490                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4491                    }
4492                }
4493
4494                Ok(bytes)
4495            }
4496
4497            // I64SetCondZ: Test if i64 register pair is zero, result 0/1 in rd
4498            // ORR.W rd, rn_lo, rn_hi; CMP rd, #0; ITE EQ; MOV 1; MOV 0
4499            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
4500                let rd_bits = reg_to_bits(rd);
4501                let rn_lo_bits = reg_to_bits(rn_lo);
4502                let rn_hi_bits = reg_to_bits(rn_hi);
4503                let mut bytes = Vec::new();
4504
4505                // ORR.W rd, rn_lo, rn_hi: EA40 rn_lo | 0000 rd 0000 rn_hi
4506                let hw1: u16 = (0xEA40 | rn_lo_bits) as u16;
4507                let hw2: u16 = ((rd_bits << 8) | rn_hi_bits) as u16;
4508                bytes.extend_from_slice(&hw1.to_le_bytes());
4509                bytes.extend_from_slice(&hw2.to_le_bytes());
4510
4511                // CMP rd, #0 — 16-bit form only for r0-r7 (3-bit rd field);
4512                // high registers take CMP.W (T2: F1B0|rn 0F00|imm8). This was
4513                // H-CODE-9: rd_bits<<8 overflowing the field compared the
4514                // WRONG register. Same hardening as the #311 SetCond fix.
4515                if rd_bits < 8 {
4516                    let cmp_instr: u16 = 0x2800 | ((rd_bits as u16) << 8);
4517                    bytes.extend_from_slice(&cmp_instr.to_le_bytes());
4518                } else {
4519                    let hw1: u16 = 0xF1B0 | (rd_bits as u16);
4520                    let hw2: u16 = 0x0F00;
4521                    bytes.extend_from_slice(&hw1.to_le_bytes());
4522                    bytes.extend_from_slice(&hw2.to_le_bytes());
4523                }
4524
4525                // ITE EQ; MOV rd, #1; MOV rd, #0 (32-bit MOV.W for rd >= R8,
4526                // #311 — see I64SetCond)
4527                let mask = 0xC_u16; // ITE EQ mask: firstcond[0]=0, mask=0xC
4528                let ite_instr: u16 = 0xBF00 | mask;
4529                bytes.extend_from_slice(&ite_instr.to_le_bytes());
4530                if rd_bits < 8 {
4531                    let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
4532                    let mov_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
4533                    bytes.extend_from_slice(&mov_one.to_le_bytes());
4534                    bytes.extend_from_slice(&mov_zero.to_le_bytes());
4535                } else {
4536                    for imm in [1u16, 0u16] {
4537                        let hw1: u16 = 0xF04F;
4538                        let hw2: u16 = ((rd_bits as u16) << 8) | imm;
4539                        bytes.extend_from_slice(&hw1.to_le_bytes());
4540                        bytes.extend_from_slice(&hw2.to_le_bytes());
4541                    }
4542                }
4543
4544                Ok(bytes)
4545            }
4546
4547            // I64Mul: 64-bit multiply using UMULL + MLA cross products
4548            // Formula: result = (a_lo * b_lo) + ((a_lo * b_hi + a_hi * b_lo) << 32)
4549            // Uses R12 as scratch register
4550            ArmOp::I64Mul {
4551                rd_lo,
4552                rd_hi,
4553                rn_lo,
4554                rn_hi,
4555                rm_lo,
4556                rm_hi,
4557            } => {
4558                let rd_lo_bits = reg_to_bits(rd_lo);
4559                let rd_hi_bits = reg_to_bits(rd_hi);
4560                let rn_lo_bits = reg_to_bits(rn_lo);
4561                let rn_hi_bits = reg_to_bits(rn_hi);
4562                let rm_lo_bits = reg_to_bits(rm_lo);
4563                let rm_hi_bits = reg_to_bits(rm_hi);
4564                let r12: u32 = 12; // IP scratch register
4565                let mut bytes = Vec::new();
4566
4567                // 1. MUL R12, rn_lo, rm_hi  (R12 = a_lo * b_hi)
4568                // Thumb-2 MUL: hw1=0xFB00|Rn, hw2=0xF000|(Rd<<8)|Rm
4569                let hw1: u16 = (0xFB00 | rn_lo_bits) as u16;
4570                let hw2: u16 = (0xF000 | (r12 << 8) | rm_hi_bits) as u16;
4571                bytes.extend_from_slice(&hw1.to_le_bytes());
4572                bytes.extend_from_slice(&hw2.to_le_bytes());
4573
4574                // 2. MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
4575                // Thumb-2 MLA: hw1=0xFB00|Rn, hw2=(Ra<<12)|(Rd<<8)|Rm
4576                let hw1: u16 = (0xFB00 | rn_hi_bits) as u16;
4577                let hw2: u16 = ((r12 << 12) | (r12 << 8) | rm_lo_bits) as u16;
4578                bytes.extend_from_slice(&hw1.to_le_bytes());
4579                bytes.extend_from_slice(&hw2.to_le_bytes());
4580
4581                // 3. UMULL rd_lo, rd_hi, rn_lo, rm_lo  (rd_lo:rd_hi = a_lo * b_lo)
4582                // Thumb-2 UMULL: hw1=0xFBA0|Rn, hw2=(RdLo<<12)|(RdHi<<8)|Rm
4583                let hw1: u16 = (0xFBA0 | rn_lo_bits) as u16;
4584                let hw2: u16 = ((rd_lo_bits << 12) | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4585                bytes.extend_from_slice(&hw1.to_le_bytes());
4586                bytes.extend_from_slice(&hw2.to_le_bytes());
4587
4588                // 4. ADD rd_hi, R12  (rd_hi += cross products)
4589                // 16-bit high reg ADD: 01000100 D Rm Rdn[2:0]
4590                let d_bit = (rd_hi_bits >> 3) & 1;
4591                let add_instr: u16 =
4592                    (0x4400 | (d_bit << 7) | (r12 << 3) | (rd_hi_bits & 0x7)) as u16;
4593                bytes.extend_from_slice(&add_instr.to_le_bytes());
4594
4595                Ok(bytes)
4596            }
4597
4598            // I64Shl: 64-bit shift left with branch for n<32 vs n>=32
4599            // rm_hi (R3) is used as temp register
4600            ArmOp::I64Shl {
4601                rd_lo,
4602                rd_hi,
4603                rn_lo,
4604                rn_hi,
4605                rm_lo,
4606                rm_hi,
4607            } => {
4608                let rd_lo_bits = reg_to_bits(rd_lo);
4609                let rd_hi_bits = reg_to_bits(rd_hi);
4610                let rn_lo_bits = reg_to_bits(rn_lo);
4611                let rn_hi_bits = reg_to_bits(rn_hi);
4612                let rm_lo_bits = reg_to_bits(rm_lo);
4613                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4614                let mut bytes = Vec::new();
4615
4616                // AND.W rm_lo, rm_lo, #63  (mask shift amount to 6 bits)
4617                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4618                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4619                bytes.extend_from_slice(&hw1.to_le_bytes());
4620                bytes.extend_from_slice(&hw2.to_le_bytes());
4621
4622                // SUBS.W rm_hi, rm_lo, #32  (rm_hi = n-32, sets flags)
4623                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4624                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4625                bytes.extend_from_slice(&hw1.to_le_bytes());
4626                bytes.extend_from_slice(&hw2.to_le_bytes());
4627
4628                // BPL .large (branch if n >= 32, offset = +10 halfwords)
4629                let bpl: u16 = 0xD50A;
4630                bytes.extend_from_slice(&bpl.to_le_bytes());
4631
4632                // --- Small shift (n < 32) ---
4633                // RSB.W rm_hi, rm_lo, #32  (rm_hi = 32-n)
4634                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4635                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4636                bytes.extend_from_slice(&hw1.to_le_bytes());
4637                bytes.extend_from_slice(&hw2.to_le_bytes());
4638
4639                // LSR.W rm_hi, rn_lo, rm_hi  (rm_hi = lo >> (32-n), overflow bits)
4640                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4641                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4642                bytes.extend_from_slice(&hw1.to_le_bytes());
4643                bytes.extend_from_slice(&hw2.to_le_bytes());
4644
4645                // LSL.W rd_hi, rn_hi, rm_lo  (hi <<= n)
4646                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4647                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4648                bytes.extend_from_slice(&hw1.to_le_bytes());
4649                bytes.extend_from_slice(&hw2.to_le_bytes());
4650
4651                // ORR.W rd_hi, rd_hi, rm_hi  (hi |= overflow bits from lo)
4652                let hw1: u16 = (0xEA40 | rd_hi_bits) as u16;
4653                let hw2: u16 = ((rd_hi_bits << 8) | rm_hi_bits) as u16;
4654                bytes.extend_from_slice(&hw1.to_le_bytes());
4655                bytes.extend_from_slice(&hw2.to_le_bytes());
4656
4657                // LSL.W rd_lo, rn_lo, rm_lo  (lo <<= n)
4658                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4659                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4660                bytes.extend_from_slice(&hw1.to_le_bytes());
4661                bytes.extend_from_slice(&hw2.to_le_bytes());
4662
4663                // B .done — `.done` is the END of the expansion, i.e. PAST the
4664                // large-shift arm's trailing zero-fill. #916: that zero-fill is
4665                // 1 halfword for a low rd_lo but 2 for R8-R12 (MOV.W), so the
4666                // displacement is DERIVED from its real width instead of the
4667                // hard-coded 0xE002 — widening the MOV without this would
4668                // overshoot `.done` and turn a data miscompile into a
4669                // control-flow one. Thumb `B` reads PC as its own address + 4
4670                // (= +2 halfwords), so imm11 = (large-arm halfwords) - 1.
4671                let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_lo_bits);
4672                let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4673                bytes.extend_from_slice(&b_done.to_le_bytes());
4674
4675                // --- Large shift (n >= 32) ---
4676                // LSL.W rd_hi, rn_lo, rm_hi  (hi = lo << (n-32))
4677                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4678                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_hi_bits) as u16;
4679                bytes.extend_from_slice(&hw1.to_le_bytes());
4680                bytes.extend_from_slice(&hw2.to_le_bytes());
4681
4682                // MOV rd_lo, #0 (#916: MOV.W for rd_lo >= R8). NOTE the order
4683                // is load-bearing — zeroing rd_lo BEFORE the LSL.W would
4684                // destroy rn_lo in the in-place case rd_lo == rn_lo, so this
4685                // cannot be reordered to dodge the displacement change.
4686                emit_thumb_zero_fill(&mut bytes, rd_lo_bits);
4687
4688                Ok(bytes) // 38 bytes (40 when rd_lo >= R8 takes MOV.W)
4689            }
4690
4691            // I64ShrU: 64-bit logical shift right with branch for n<32 vs n>=32
4692            ArmOp::I64ShrU {
4693                rd_lo,
4694                rd_hi,
4695                rn_lo,
4696                rn_hi,
4697                rm_lo,
4698                rm_hi,
4699            } => {
4700                let rd_lo_bits = reg_to_bits(rd_lo);
4701                let rd_hi_bits = reg_to_bits(rd_hi);
4702                let rn_lo_bits = reg_to_bits(rn_lo);
4703                let rn_hi_bits = reg_to_bits(rn_hi);
4704                let rm_lo_bits = reg_to_bits(rm_lo);
4705                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4706                let mut bytes = Vec::new();
4707
4708                // AND.W rm_lo, rm_lo, #63
4709                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4710                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4711                bytes.extend_from_slice(&hw1.to_le_bytes());
4712                bytes.extend_from_slice(&hw2.to_le_bytes());
4713
4714                // SUBS.W rm_hi, rm_lo, #32
4715                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4716                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4717                bytes.extend_from_slice(&hw1.to_le_bytes());
4718                bytes.extend_from_slice(&hw2.to_le_bytes());
4719
4720                // BPL .large (+10 halfwords)
4721                let bpl: u16 = 0xD50A;
4722                bytes.extend_from_slice(&bpl.to_le_bytes());
4723
4724                // --- Small shift (n < 32) ---
4725                // RSB.W rm_hi, rm_lo, #32  (rm_hi = 32-n)
4726                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4727                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4728                bytes.extend_from_slice(&hw1.to_le_bytes());
4729                bytes.extend_from_slice(&hw2.to_le_bytes());
4730
4731                // LSL.W rm_hi, rn_hi, rm_hi  (rm_hi = hi << (32-n), bits flowing to lo)
4732                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4733                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4734                bytes.extend_from_slice(&hw1.to_le_bytes());
4735                bytes.extend_from_slice(&hw2.to_le_bytes());
4736
4737                // LSR.W rd_lo, rn_lo, rm_lo  (lo >>= n)
4738                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4739                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4740                bytes.extend_from_slice(&hw1.to_le_bytes());
4741                bytes.extend_from_slice(&hw2.to_le_bytes());
4742
4743                // ORR.W rd_lo, rd_lo, rm_hi  (lo |= overflow from hi)
4744                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4745                let hw2: u16 = ((rd_lo_bits << 8) | rm_hi_bits) as u16;
4746                bytes.extend_from_slice(&hw1.to_le_bytes());
4747                bytes.extend_from_slice(&hw2.to_le_bytes());
4748
4749                // LSR.W rd_hi, rn_hi, rm_lo  (hi >>= n, logical)
4750                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4751                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4752                bytes.extend_from_slice(&hw1.to_le_bytes());
4753                bytes.extend_from_slice(&hw2.to_le_bytes());
4754
4755                // B .done — see I64Shl: `.done` is the END of the expansion,
4756                // past the trailing zero-fill, so the displacement is derived
4757                // from that zero-fill's real width (#916).
4758                let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_hi_bits);
4759                let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4760                bytes.extend_from_slice(&b_done.to_le_bytes());
4761
4762                // --- Large shift (n >= 32) ---
4763                // LSR.W rd_lo, rn_hi, rm_hi  (lo = hi >> (n-32))
4764                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4765                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_hi_bits) as u16;
4766                bytes.extend_from_slice(&hw1.to_le_bytes());
4767                bytes.extend_from_slice(&hw2.to_le_bytes());
4768
4769                // MOV rd_hi, #0 (#916: MOV.W for rd_hi >= R8). Order is
4770                // load-bearing: the LSR.W above reads rn_hi, which may BE
4771                // rd_hi in the in-place case.
4772                emit_thumb_zero_fill(&mut bytes, rd_hi_bits);
4773
4774                Ok(bytes) // 38 bytes (40 when rd_hi >= R8 takes MOV.W)
4775            }
4776
4777            // I64ShrS: 64-bit arithmetic shift right with branch for n<32 vs n>=32
4778            ArmOp::I64ShrS {
4779                rd_lo,
4780                rd_hi,
4781                rn_lo,
4782                rn_hi,
4783                rm_lo,
4784                rm_hi,
4785            } => {
4786                let rd_lo_bits = reg_to_bits(rd_lo);
4787                let rd_hi_bits = reg_to_bits(rd_hi);
4788                let rn_lo_bits = reg_to_bits(rn_lo);
4789                let rn_hi_bits = reg_to_bits(rn_hi);
4790                let rm_lo_bits = reg_to_bits(rm_lo);
4791                let rm_hi_bits = reg_to_bits(rm_hi); // temp
4792                let mut bytes = Vec::new();
4793
4794                // AND.W rm_lo, rm_lo, #63
4795                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4796                let hw2: u16 = ((rm_lo_bits << 8) | 0x3F) as u16;
4797                bytes.extend_from_slice(&hw1.to_le_bytes());
4798                bytes.extend_from_slice(&hw2.to_le_bytes());
4799
4800                // SUBS.W rm_hi, rm_lo, #32
4801                let hw1: u16 = (0xF1B0 | rm_lo_bits) as u16;
4802                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4803                bytes.extend_from_slice(&hw1.to_le_bytes());
4804                bytes.extend_from_slice(&hw2.to_le_bytes());
4805
4806                // BPL .large (+10 halfwords)
4807                let bpl: u16 = 0xD50A;
4808                bytes.extend_from_slice(&bpl.to_le_bytes());
4809
4810                // --- Small shift (n < 32) ---
4811                // RSB.W rm_hi, rm_lo, #32
4812                let hw1: u16 = (0xF1C0 | rm_lo_bits) as u16;
4813                let hw2: u16 = ((rm_hi_bits << 8) | 0x20) as u16;
4814                bytes.extend_from_slice(&hw1.to_le_bytes());
4815                bytes.extend_from_slice(&hw2.to_le_bytes());
4816
4817                // LSL.W rm_hi, rn_hi, rm_hi  (rm_hi = hi << (32-n), bits flowing to lo)
4818                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4819                let hw2: u16 = (0xF000 | (rm_hi_bits << 8) | rm_hi_bits) as u16;
4820                bytes.extend_from_slice(&hw1.to_le_bytes());
4821                bytes.extend_from_slice(&hw2.to_le_bytes());
4822
4823                // LSR.W rd_lo, rn_lo, rm_lo  (lo >>= n, logical for lo word)
4824                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4825                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_lo_bits) as u16;
4826                bytes.extend_from_slice(&hw1.to_le_bytes());
4827                bytes.extend_from_slice(&hw2.to_le_bytes());
4828
4829                // ORR.W rd_lo, rd_lo, rm_hi  (lo |= overflow from hi)
4830                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4831                let hw2: u16 = ((rd_lo_bits << 8) | rm_hi_bits) as u16;
4832                bytes.extend_from_slice(&hw1.to_le_bytes());
4833                bytes.extend_from_slice(&hw2.to_le_bytes());
4834
4835                // ASR.W rd_hi, rn_hi, rm_lo  (hi >>= n, arithmetic/sign-extending)
4836                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
4837                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4838                bytes.extend_from_slice(&hw1.to_le_bytes());
4839                bytes.extend_from_slice(&hw2.to_le_bytes());
4840
4841                // B .done (+3 halfwords, large shift is 8 bytes)
4842                let b_done: u16 = 0xE003;
4843                bytes.extend_from_slice(&b_done.to_le_bytes());
4844
4845                // --- Large shift (n >= 32) ---
4846                // ASR.W rd_lo, rn_hi, rm_hi  (lo = hi >>> (n-32))
4847                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
4848                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | rm_hi_bits) as u16;
4849                bytes.extend_from_slice(&hw1.to_le_bytes());
4850                bytes.extend_from_slice(&hw2.to_le_bytes());
4851
4852                // ASR.W rd_hi, rn_hi, #31  (hi = sign extension, all 0s or all 1s)
4853                // Thumb-2 ASR immediate: hw1=0xEA4F, hw2=imm3:Rd:imm2:10:Rm
4854                // imm5=31=11111 → imm3=111, imm2=11
4855                let hw1: u16 = 0xEA4F;
4856                let hw2: u16 = (0x7000 | (rd_hi_bits << 8) | 0x00E0 | rn_hi_bits) as u16;
4857                bytes.extend_from_slice(&hw1.to_le_bytes());
4858                bytes.extend_from_slice(&hw2.to_le_bytes());
4859
4860                Ok(bytes) // Total: 40 bytes
4861            }
4862
4863            // I64Rotl: 64-bit rotate left (#610 rewrite).
4864            // For n < 32: new_hi = (hi << n) | (lo >> (32-n)), new_lo = (lo << n) | (hi >> (32-n))
4865            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
4866            //
4867            // Fixed-reg core: value in R0:R1, amount in R2, scratch R3 + R12
4868            // (all four saved/marshaled by the #610 fixed-ABI wrapper; the
4869            // pre-#610 expansion wrote through the selector's registers with
4870            // colliding R3/R4 scratch and restored the saved R4 OVER the
4871            // result). Relies on ARM register-shift semantics: amounts >= 32
4872            // yield 0 for LSL/LSR, which makes n = 0 and n = 32 exact.
4873            ArmOp::I64Rotl {
4874                rdlo,
4875                rdhi,
4876                rnlo,
4877                rnhi,
4878                shift,
4879            } => {
4880                let mut bytes = Vec::new();
4881                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
4882
4883                let core: [u16; 35] = [
4884                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
4885                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
4886                    0xD50E, //         BPL    .large        (n >= 32)
4887                    // --- small rotation (n < 32) ---
4888                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
4889                    0xFA20, 0xFC03, // LSR.W  R12, R0, R3   (lo >> (32-n))
4890                    0xFA21, 0xF303, // LSR.W  R3, R1, R3    (hi >> (32-n))
4891                    0xFA01, 0xF102, // LSL.W  R1, R1, R2    (hi << n)
4892                    0xEA41, 0x010C, // ORR.W  R1, R1, R12   (new_hi)
4893                    0xFA00, 0xF002, // LSL.W  R0, R0, R2    (lo << n)
4894                    0xEA40, 0x0003, // ORR.W  R0, R0, R3    (new_lo)
4895                    0xE00E, //         B      .done
4896                    // --- large rotation (n >= 32), R3 = m = n-32 ---
4897                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
4898                    0xFA21, 0xFC02, // LSR.W  R12, R1, R2   (hi >> (64-n))
4899                    0xFA20, 0xF202, // LSR.W  R2, R0, R2    (lo >> (64-n))
4900                    0xFA00, 0xF003, // LSL.W  R0, R0, R3    (lo << m)
4901                    0xFA01, 0xF103, // LSL.W  R1, R1, R3    (hi << m)
4902                    0xEA40, 0x0C0C, // ORR.W  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
4903                    0xEA41, 0x0002, // ORR.W  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
4904                    0x4661, //         MOV    R1, R12       (new_hi into place)
4905                            // .done: result in R0:R1
4906                ];
4907                for hw in core {
4908                    bytes.extend_from_slice(&hw.to_le_bytes());
4909                }
4910
4911                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
4912                Ok(bytes) // Total: 102 bytes
4913            }
4914
4915            // I64Rotr: 64-bit rotate right (#610 rewrite).
4916            // For n < 32: new_lo = (lo >> n) | (hi << (32-n)), new_hi = (hi >> n) | (lo << (32-n))
4917            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
4918            //
4919            // Same fixed-reg core contract as I64Rotl: value in R0:R1, amount
4920            // in R2, scratch R3 + R12, all covered by the fixed-ABI wrapper.
4921            ArmOp::I64Rotr {
4922                rdlo,
4923                rdhi,
4924                rnlo,
4925                rnhi,
4926                shift,
4927            } => {
4928                let mut bytes = Vec::new();
4929                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
4930
4931                let core: [u16; 35] = [
4932                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
4933                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
4934                    0xD50E, //         BPL    .large        (n >= 32)
4935                    // --- small rotation (n < 32) ---
4936                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
4937                    0xFA01, 0xFC03, // LSL.W  R12, R1, R3   (hi << (32-n))
4938                    0xFA00, 0xF303, // LSL.W  R3, R0, R3    (lo << (32-n))
4939                    0xFA20, 0xF002, // LSR.W  R0, R0, R2    (lo >> n)
4940                    0xEA40, 0x000C, // ORR.W  R0, R0, R12   (new_lo)
4941                    0xFA21, 0xF102, // LSR.W  R1, R1, R2    (hi >> n)
4942                    0xEA41, 0x0103, // ORR.W  R1, R1, R3    (new_hi)
4943                    0xE00E, //         B      .done
4944                    // --- large rotation (n >= 32), R3 = m = n-32 ---
4945                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
4946                    0xFA00, 0xFC02, // LSL.W  R12, R0, R2   (lo << (64-n))
4947                    0xFA01, 0xF202, // LSL.W  R2, R1, R2    (hi << (64-n))
4948                    0xFA21, 0xF103, // LSR.W  R1, R1, R3    (hi >> m)
4949                    0xEA41, 0x0C0C, // ORR.W  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
4950                    0xFA20, 0xF103, // LSR.W  R1, R0, R3    (lo >> m)
4951                    0xEA41, 0x0102, // ORR.W  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
4952                    0x4660, //         MOV    R0, R12       (new_lo into place)
4953                            // .done: result in R0:R1
4954                ];
4955                for hw in core {
4956                    bytes.extend_from_slice(&hw.to_le_bytes());
4957                }
4958
4959                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
4960                Ok(bytes) // Total: 102 bytes
4961            }
4962
4963            // I64Clz: Count leading zeros in 64-bit value
4964            // If hi != 0: result = CLZ(hi)
4965            // If hi == 0: result = 32 + CLZ(lo)
4966            //
4967            // Layout (using CMP+BNE approach for consistency):
4968            // 0: CMP.W rnhi, #0 (4 bytes)
4969            // 4: BEQ .hi_zero (2 bytes) - branch forward to offset 14
4970            // 6: CLZ.W rd, rnhi (4 bytes)
4971            // 10: B .done (2 bytes) - branch forward to offset 22
4972            // 12: NOP (2 bytes) - padding for alignment
4973            // 14: .hi_zero: CLZ.W rd, rnlo (4 bytes)
4974            // 18: ADD.W rd, rd, #32 (4 bytes)
4975            // 22: .done
4976            ArmOp::I64Clz { rd, rnlo, rnhi } => {
4977                let rd_bits = reg_to_bits(rd);
4978                let rn_lo_bits = reg_to_bits(rnlo);
4979                let rn_hi_bits = reg_to_bits(rnhi);
4980                let mut bytes = Vec::new();
4981
4982                // CMP.W rnhi, #0 (4 bytes at offset 0)
4983                let hw1: u16 = (0xF1B0 | rn_hi_bits) as u16;
4984                let hw2: u16 = 0x0F00;
4985                bytes.extend_from_slice(&hw1.to_le_bytes());
4986                bytes.extend_from_slice(&hw2.to_le_bytes());
4987
4988                // BEQ .hi_zero (2 bytes at offset 4)
4989                // PC = 4 + 4 = 8, target = 14, offset = 6, imm8 = 3
4990                let beq: u16 = 0xD003;
4991                bytes.extend_from_slice(&beq.to_le_bytes());
4992
4993                // CLZ.W rd, rnhi (4 bytes at offset 6)
4994                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
4995                let hw1: u16 = (0xFAB0 | rn_hi_bits) as u16;
4996                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_hi_bits) as u16;
4997                bytes.extend_from_slice(&hw1.to_le_bytes());
4998                bytes.extend_from_slice(&hw2.to_le_bytes());
4999
5000                // B .done (2 bytes at offset 10)
5001                // PC = 10 + 4 = 14, target = 22, offset = 8, imm11 = 4
5002                let b_done: u16 = 0xE004;
5003                bytes.extend_from_slice(&b_done.to_le_bytes());
5004
5005                // NOP (2 bytes at offset 12) - padding
5006                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5007
5008                // .hi_zero: (offset 14)
5009                // CLZ.W rd, rnlo (4 bytes)
5010                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5011                let hw1: u16 = (0xFAB0 | rn_lo_bits) as u16;
5012                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_lo_bits) as u16;
5013                bytes.extend_from_slice(&hw1.to_le_bytes());
5014                bytes.extend_from_slice(&hw2.to_le_bytes());
5015
5016                // ADD.W rd, rd, #32 (4 bytes at offset 18)
5017                let hw1: u16 = (0xF100 | rd_bits) as u16;
5018                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5019                bytes.extend_from_slice(&hw1.to_le_bytes());
5020                bytes.extend_from_slice(&hw2.to_le_bytes());
5021
5022                // .done: (offset 22)
5023                // i64.clz returns i64, so clear high word (#916: MOV.W for
5024                // rnhi >= R8 — this site is UNCONDITIONALLY reached, so every
5025                // i64.clz with a high `rnhi` returned garbage in its upper 32
5026                // bits, not just the shift ops the issue named). No branch
5027                // displacement changes: `B .done` above targets offset 22,
5028                // which IS this instruction's own address, and `BEQ` targets
5029                // offset 14, before it.
5030                emit_thumb_zero_fill(&mut bytes, rn_hi_bits);
5031
5032                Ok(bytes) // 24 bytes (26 when rnhi >= R8 takes MOV.W)
5033            }
5034
5035            // I64Ctz: Count trailing zeros in 64-bit value
5036            // If lo != 0: result = CTZ(lo) = CLZ(RBIT(lo))
5037            // If lo == 0: result = 32 + CTZ(hi) = 32 + CLZ(RBIT(hi))
5038            //
5039            // Layout:
5040            // 0: CMP.W rnlo, #0 (4 bytes)
5041            // 4: BEQ .lo_zero (2 bytes) - branch to offset 18
5042            // 6: RBIT.W rd, rnlo (4 bytes)
5043            // 10: CLZ.W rd, rd (4 bytes)
5044            // 14: B .done (2 bytes) - branch to offset 30
5045            // 16: NOP (2 bytes) - padding
5046            // 18: .lo_zero: RBIT.W rd, rnhi (4 bytes)
5047            // 22: CLZ.W rd, rd (4 bytes)
5048            // 26: ADD.W rd, rd, #32 (4 bytes)
5049            // 30: .done
5050            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
5051                let rd_bits = reg_to_bits(rd);
5052                let rn_lo_bits = reg_to_bits(rnlo);
5053                let rn_hi_bits = reg_to_bits(rnhi);
5054                let mut bytes = Vec::new();
5055
5056                // CMP.W rnlo, #0 (4 bytes at offset 0)
5057                let hw1: u16 = (0xF1B0 | rn_lo_bits) as u16;
5058                let hw2: u16 = 0x0F00;
5059                bytes.extend_from_slice(&hw1.to_le_bytes());
5060                bytes.extend_from_slice(&hw2.to_le_bytes());
5061
5062                // BEQ .lo_zero (2 bytes at offset 4)
5063                // PC = 4 + 4 = 8, target = 18, offset = 10, imm8 = 5
5064                let beq: u16 = 0xD005;
5065                bytes.extend_from_slice(&beq.to_le_bytes());
5066
5067                // RBIT.W rd, rnlo (4 bytes at offset 6)
5068                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5069                let hw1: u16 = (0xFA90 | rn_lo_bits) as u16;
5070                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_lo_bits) as u16;
5071                bytes.extend_from_slice(&hw1.to_le_bytes());
5072                bytes.extend_from_slice(&hw2.to_le_bytes());
5073
5074                // CLZ.W rd, rd (4 bytes at offset 10)
5075                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5076                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5077                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5078                bytes.extend_from_slice(&hw1.to_le_bytes());
5079                bytes.extend_from_slice(&hw2.to_le_bytes());
5080
5081                // B .done (2 bytes at offset 14)
5082                // PC = 14 + 4 = 18, target = 30, offset = 12, imm11 = 6
5083                let b_done: u16 = 0xE006;
5084                bytes.extend_from_slice(&b_done.to_le_bytes());
5085
5086                // NOP (2 bytes at offset 16) - padding
5087                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5088
5089                // .lo_zero: (offset 18)
5090                // RBIT.W rd, rnhi (4 bytes)
5091                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5092                let hw1: u16 = (0xFA90 | rn_hi_bits) as u16;
5093                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_hi_bits) as u16;
5094                bytes.extend_from_slice(&hw1.to_le_bytes());
5095                bytes.extend_from_slice(&hw2.to_le_bytes());
5096
5097                // CLZ.W rd, rd (4 bytes at offset 22)
5098                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5099                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5100                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5101                bytes.extend_from_slice(&hw1.to_le_bytes());
5102                bytes.extend_from_slice(&hw2.to_le_bytes());
5103
5104                // ADD.W rd, rd, #32 (4 bytes at offset 26)
5105                let hw1: u16 = (0xF100 | rd_bits) as u16;
5106                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5107                bytes.extend_from_slice(&hw1.to_le_bytes());
5108                bytes.extend_from_slice(&hw2.to_le_bytes());
5109
5110                // .done: (offset 30)
5111                // i64.ctz returns i64, so clear high word (#916: MOV.W for
5112                // rnhi >= R8). As with I64Clz this site is unconditional, and
5113                // no displacement moves: `B .done` targets offset 30 = this
5114                // instruction's own address, `BEQ` targets offset 18.
5115                emit_thumb_zero_fill(&mut bytes, rn_hi_bits);
5116
5117                Ok(bytes) // 32 bytes (34 when rnhi >= R8 takes MOV.W)
5118            }
5119
5120            // I64Popcnt: Population count of 64-bit value
5121            // result = POPCNT(lo) + POPCNT(hi)
5122            // Using SIMD-style parallel bit counting algorithm
5123            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
5124                let rd_bits = reg_to_bits(rd);
5125                let rn_lo_bits = reg_to_bits(rnlo);
5126                let rn_hi_bits = reg_to_bits(rnhi);
5127                let r12: u32 = 12; // IP scratch
5128                let r3: u32 = 3; // Scratch for hi popcnt result
5129                let mut bytes = Vec::new();
5130
5131                // PUSH {R3, R4, R5} - save scratch registers
5132                bytes.extend_from_slice(&0xB438u16.to_le_bytes());
5133
5134                // Strategy: compute popcnt(lo) -> R4, popcnt(hi) -> R5, add them -> rd
5135                // Using lookup table approach for each byte would be too large
5136                // Using shift-and-add approach instead
5137
5138                // For simplicity and correctness, use the efficient parallel algorithm
5139                // but implement it as a series of inline operations
5140
5141                // Marshal the operand pair into the fixed scratch regs, routing
5142                // rnlo through R12 (#632 audit): writing R4 first corrupted the
5143                // rnhi read for a pair living at (R3,R4) — every source is read
5144                // before any scratch register it could occupy is written.
5145                // MOV R12, rnlo
5146                let mov: u16 = (0x4600 | (1 << 7) | (rn_lo_bits << 3) | 4) as u16;
5147                bytes.extend_from_slice(&mov.to_le_bytes());
5148                // MOV R5, rnhi (R4 untouched so far; rnhi == R5 is a no-op)
5149                let mov: u16 = (0x4600 | (rn_hi_bits << 3) | 5) as u16;
5150                bytes.extend_from_slice(&mov.to_le_bytes());
5151                // MOV R4, R12
5152                bytes.extend_from_slice(&0x4664u16.to_le_bytes());
5153
5154                // --- POPCNT for R4 (lo word) ---
5155                // Step 1: x = x - ((x >> 1) & 0x55555555)
5156                // LSR.W R12, R4, #1
5157                let hw1: u16 = 0xEA4F;
5158                let hw2: u16 = ((r12 << 8) | 0x50 | 4) as u16;
5159                bytes.extend_from_slice(&hw1.to_le_bytes());
5160                bytes.extend_from_slice(&hw2.to_le_bytes());
5161
5162                // Load 0x55555555 into R3 using MOVW/MOVT
5163                // MOVW R3, #0x5555
5164                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5165                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5166                // MOVT R3, #0x5555
5167                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5168                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5169
5170                // AND.W R12, R12, R3
5171                let hw1: u16 = (0xEA00 | r12) as u16;
5172                let hw2: u16 = ((r12 << 8) | r3) as u16;
5173                bytes.extend_from_slice(&hw1.to_le_bytes());
5174                bytes.extend_from_slice(&hw2.to_le_bytes());
5175
5176                // SUB.W R4, R4, R12
5177                let hw1: u16 = (0xEBA0 | 4) as u16;
5178                let hw2: u16 = ((4 << 8) | r12) as u16;
5179                bytes.extend_from_slice(&hw1.to_le_bytes());
5180                bytes.extend_from_slice(&hw2.to_le_bytes());
5181
5182                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5183                // Load 0x33333333 into R3
5184                // MOVW R3, #0x3333
5185                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5186                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5187                // MOVT R3, #0x3333
5188                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5189                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5190
5191                // AND.W R12, R4, R3
5192                let hw1: u16 = (0xEA00 | 4) as u16;
5193                let hw2: u16 = ((r12 << 8) | r3) as u16;
5194                bytes.extend_from_slice(&hw1.to_le_bytes());
5195                bytes.extend_from_slice(&hw2.to_le_bytes());
5196
5197                // LSR.W R4, R4, #2
5198                let hw1: u16 = 0xEA4F;
5199                let hw2: u16 = ((4 << 8) | 0x90 | 4) as u16;
5200                bytes.extend_from_slice(&hw1.to_le_bytes());
5201                bytes.extend_from_slice(&hw2.to_le_bytes());
5202
5203                // AND.W R4, R4, R3
5204                let hw1: u16 = (0xEA00 | 4) as u16;
5205                let hw2: u16 = ((4 << 8) | r3) as u16;
5206                bytes.extend_from_slice(&hw1.to_le_bytes());
5207                bytes.extend_from_slice(&hw2.to_le_bytes());
5208
5209                // ADD.W R4, R4, R12
5210                let hw1: u16 = (0xEB00 | 4) as u16;
5211                let hw2: u16 = ((4 << 8) | r12) as u16;
5212                bytes.extend_from_slice(&hw1.to_le_bytes());
5213                bytes.extend_from_slice(&hw2.to_le_bytes());
5214
5215                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5216                // LSR.W R12, R4, #4
5217                // hw2 = (imm3 << 12) | (Rd << 8) | (imm2 << 6) | (type << 4) | Rm
5218                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5219                let hw1: u16 = 0xEA4F;
5220                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 4) as u16;
5221                bytes.extend_from_slice(&hw1.to_le_bytes());
5222                bytes.extend_from_slice(&hw2.to_le_bytes());
5223
5224                // ADD.W R4, R4, R12
5225                let hw1: u16 = (0xEB00 | 4) as u16;
5226                let hw2: u16 = ((4 << 8) | r12) as u16;
5227                bytes.extend_from_slice(&hw1.to_le_bytes());
5228                bytes.extend_from_slice(&hw2.to_le_bytes());
5229
5230                // Load 0x0F0F0F0F into R3
5231                // MOVW R3, #0x0F0F (imm4=0, i=1, imm3=7, imm8=0x0F)
5232                // hw1 = 11110 1 10 0100 0000 = 0xF640
5233                // hw2 = 0 111 0011 00001111 = 0x730F
5234                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5235                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5236                // MOVT R3, #0x0F0F
5237                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5238                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5239
5240                // AND.W R4, R4, R3
5241                let hw1: u16 = (0xEA00 | 4) as u16;
5242                let hw2: u16 = ((4 << 8) | r3) as u16;
5243                bytes.extend_from_slice(&hw1.to_le_bytes());
5244                bytes.extend_from_slice(&hw2.to_le_bytes());
5245
5246                // Step 4: x = x * 0x01010101 >> 24
5247                // Load 0x01010101 into R3
5248                // MOVW R3, #0x0101
5249                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5250                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5251                // MOVT R3, #0x0101
5252                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5253                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5254
5255                // MUL R4, R4, R3
5256                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5257                let hw1: u16 = (0xFB00 | 4) as u16;
5258                let hw2: u16 = (0xF000 | (4 << 8) | r3) as u16;
5259                bytes.extend_from_slice(&hw1.to_le_bytes());
5260                bytes.extend_from_slice(&hw2.to_le_bytes());
5261
5262                // LSR.W R4, R4, #24
5263                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5264                let hw1: u16 = 0xEA4F;
5265                let hw2: u16 = (0x6000 | (4 << 8) | 0x10 | 4) as u16;
5266                bytes.extend_from_slice(&hw1.to_le_bytes());
5267                bytes.extend_from_slice(&hw2.to_le_bytes());
5268
5269                // --- POPCNT for R5 (hi word) - same algorithm ---
5270                // Step 1
5271                let hw1: u16 = 0xEA4F;
5272                let hw2: u16 = ((r12 << 8) | 0x50 | 5) as u16;
5273                bytes.extend_from_slice(&hw1.to_le_bytes());
5274                bytes.extend_from_slice(&hw2.to_le_bytes());
5275
5276                // Load 0x55555555 into R3
5277                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5278                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5279                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5280                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5281
5282                let hw1: u16 = (0xEA00 | r12) as u16;
5283                let hw2: u16 = ((r12 << 8) | r3) as u16;
5284                bytes.extend_from_slice(&hw1.to_le_bytes());
5285                bytes.extend_from_slice(&hw2.to_le_bytes());
5286
5287                let hw1: u16 = (0xEBA0 | 5) as u16;
5288                let hw2: u16 = ((5 << 8) | r12) as u16;
5289                bytes.extend_from_slice(&hw1.to_le_bytes());
5290                bytes.extend_from_slice(&hw2.to_le_bytes());
5291
5292                // Step 2
5293                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5294                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5295                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5296                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5297
5298                let hw1: u16 = (0xEA00 | 5) as u16;
5299                let hw2: u16 = ((r12 << 8) | r3) as u16;
5300                bytes.extend_from_slice(&hw1.to_le_bytes());
5301                bytes.extend_from_slice(&hw2.to_le_bytes());
5302
5303                let hw1: u16 = 0xEA4F;
5304                let hw2: u16 = ((5 << 8) | 0x90 | 5) as u16;
5305                bytes.extend_from_slice(&hw1.to_le_bytes());
5306                bytes.extend_from_slice(&hw2.to_le_bytes());
5307
5308                let hw1: u16 = (0xEA00 | 5) as u16;
5309                let hw2: u16 = ((5 << 8) | r3) as u16;
5310                bytes.extend_from_slice(&hw1.to_le_bytes());
5311                bytes.extend_from_slice(&hw2.to_le_bytes());
5312
5313                let hw1: u16 = (0xEB00 | 5) as u16;
5314                let hw2: u16 = ((5 << 8) | r12) as u16;
5315                bytes.extend_from_slice(&hw1.to_le_bytes());
5316                bytes.extend_from_slice(&hw2.to_le_bytes());
5317
5318                // Step 3: LSR.W R12, R5, #4
5319                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5320                let hw1: u16 = 0xEA4F;
5321                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 5) as u16;
5322                bytes.extend_from_slice(&hw1.to_le_bytes());
5323                bytes.extend_from_slice(&hw2.to_le_bytes());
5324
5325                let hw1: u16 = (0xEB00 | 5) as u16;
5326                let hw2: u16 = ((5 << 8) | r12) as u16;
5327                bytes.extend_from_slice(&hw1.to_le_bytes());
5328                bytes.extend_from_slice(&hw2.to_le_bytes());
5329
5330                // Load 0x0F0F0F0F into R3 (for hi-word)
5331                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5332                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5333                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5334                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5335
5336                let hw1: u16 = (0xEA00 | 5) as u16;
5337                let hw2: u16 = ((5 << 8) | r3) as u16;
5338                bytes.extend_from_slice(&hw1.to_le_bytes());
5339                bytes.extend_from_slice(&hw2.to_le_bytes());
5340
5341                // Step 4
5342                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5343                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5344                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5345                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5346
5347                // MUL R5, R5, R3
5348                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5349                let hw1: u16 = (0xFB00 | 5) as u16;
5350                let hw2: u16 = (0xF000 | (5 << 8) | r3) as u16;
5351                bytes.extend_from_slice(&hw1.to_le_bytes());
5352                bytes.extend_from_slice(&hw2.to_le_bytes());
5353
5354                // LSR.W R5, R5, #24
5355                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5356                let hw1: u16 = 0xEA4F;
5357                let hw2: u16 = (0x6000 | (5 << 8) | 0x10 | 5) as u16;
5358                bytes.extend_from_slice(&hw1.to_le_bytes());
5359                bytes.extend_from_slice(&hw2.to_le_bytes());
5360
5361                // #632: the count must be carried ACROSS the scratch restore
5362                // in a register the POP cannot touch. rd is allocator-assigned
5363                // (any of R0-R8) and can land inside the {R3,R4,R5} restore set
5364                // — the old `ADDS rd, R4, R5; POP {R3,R4,R5}` destroyed the
5365                // result one instruction after computing it (0 for every input
5366                // under qemu). R12 is encoder scratch: never allocatable (#212)
5367                // and never in a restore set, so no choice of rd can collide.
5368                // ADD.W R12, R4, R5
5369                bytes.extend_from_slice(&0xEB04u16.to_le_bytes());
5370                bytes.extend_from_slice(&0x0C05u16.to_le_bytes());
5371
5372                // POP {R3, R4, R5}
5373                bytes.extend_from_slice(&0xBC38u16.to_le_bytes());
5374
5375                // MOV rd, R12 — after the restore. The 4-bit Rd (D:rd) form is
5376                // also total over rd = R8, where the old ADDS T1 3-bit field
5377                // silently corrupted the encoding (#178/#180 class).
5378                let mov: u16 =
5379                    (0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7)) as u16;
5380                bytes.extend_from_slice(&mov.to_le_bytes());
5381
5382                // i64.popcnt returns i64, so clear high word: MOV.W rnhi, #0
5383                // (T2, 4 bytes — total over rnhi = R8, where the old 16-bit
5384                // MOVS encoding overflowed its 3-bit field into CMP R0, #0).
5385                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5386                bytes.extend_from_slice(&(((rn_hi_bits & 0xF) << 8) as u16).to_le_bytes());
5387
5388                Ok(bytes)
5389            }
5390
5391            // I64Extend8S: Sign-extend low 8 bits to 64 bits
5392            // Result: rdlo = sign_extend_8(rnlo), rdhi = rdlo >> 31
5393            ArmOp::I64Extend8S { 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                // SXTB.W rdlo, rnlo (sign-extend byte to 32-bit)
5400                // SXTB T2: hw1 = 0xFA4F, hw2 = 0xF0<Rd><Rm>
5401                let hw1: u16 = 0xFA4F_u16;
5402                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5403                bytes.extend_from_slice(&hw1.to_le_bytes());
5404                bytes.extend_from_slice(&hw2.to_le_bytes());
5405
5406                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5407                // ASR (immediate): hw1 = 0xEA4F, hw2 = imm3:Rd:imm2:type:Rm
5408                // For imm5=31: imm3=111, imm2=11, type=10 (ASR)
5409                // hw2 = (7 << 12) | (rdhi << 8) | (3 << 6) | (2 << 4) | rdlo
5410                let hw1: u16 = 0xEA4F;
5411                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5412                bytes.extend_from_slice(&hw1.to_le_bytes());
5413                bytes.extend_from_slice(&hw2.to_le_bytes());
5414
5415                Ok(bytes)
5416            }
5417
5418            // I64Extend16S: Sign-extend low 16 bits to 64 bits
5419            // Result: rdlo = sign_extend_16(rnlo), rdhi = rdlo >> 31
5420            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
5421                let rdlo_bits = reg_to_bits(rdlo);
5422                let rdhi_bits = reg_to_bits(rdhi);
5423                let rnlo_bits = reg_to_bits(rnlo);
5424                let mut bytes = Vec::new();
5425
5426                // SXTH.W rdlo, rnlo (sign-extend halfword to 32-bit)
5427                // SXTH T2: hw1 = 0xFA0F, hw2 = 0xF0<Rd><Rm>
5428                let hw1: u16 = 0xFA0F_u16;
5429                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5430                bytes.extend_from_slice(&hw1.to_le_bytes());
5431                bytes.extend_from_slice(&hw2.to_le_bytes());
5432
5433                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5434                let hw1: u16 = 0xEA4F;
5435                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5436                bytes.extend_from_slice(&hw1.to_le_bytes());
5437                bytes.extend_from_slice(&hw2.to_le_bytes());
5438
5439                Ok(bytes)
5440            }
5441
5442            // I64Extend32S: Sign-extend low 32 bits to 64 bits
5443            // Result: rdlo = rnlo, rdhi = rnlo >> 31
5444            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
5445                let rdlo_bits = reg_to_bits(rdlo);
5446                let rdhi_bits = reg_to_bits(rdhi);
5447                let rnlo_bits = reg_to_bits(rnlo);
5448                let mut bytes = Vec::new();
5449
5450                // MOV rdlo, rnlo (if different)
5451                if rdlo_bits != rnlo_bits {
5452                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5453                    let d_bit = ((rdlo_bits >> 3) & 1) as u16;
5454                    let mov: u16 = 0x4600
5455                        | (d_bit << 7)
5456                        | ((rnlo_bits as u16) << 3)
5457                        | ((rdlo_bits & 0x7) as u16);
5458                    bytes.extend_from_slice(&mov.to_le_bytes());
5459                }
5460
5461                // ASR.W rdhi, rnlo, #31 (sign-extend to high word)
5462                let hw1: u16 = 0xEA4F;
5463                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rnlo_bits) as u16;
5464                bytes.extend_from_slice(&hw1.to_le_bytes());
5465                bytes.extend_from_slice(&hw2.to_le_bytes());
5466
5467                Ok(bytes)
5468            }
5469
5470            // SelectMove: IT <cond>; MOV{cond} rd, rm
5471            // Conditional move: only execute MOV if condition is true
5472            ArmOp::SelectMove { rd, rm, cond } => {
5473                let rd_bits = reg_to_bits(rd) as u16;
5474                let rm_bits = reg_to_bits(rm) as u16;
5475
5476                // Condition code encoding for IT block
5477                use synth_synthesis::Condition;
5478                let cond_bits: u16 = match cond {
5479                    Condition::EQ => 0x0, // Equal
5480                    Condition::NE => 0x1, // Not equal
5481                    Condition::HS => 0x2, // Higher or same (unsigned >=)
5482                    Condition::LO => 0x3, // Lower (unsigned <)
5483                    Condition::HI => 0x8, // Higher (unsigned >)
5484                    Condition::LS => 0x9, // Lower or same (unsigned <=)
5485                    Condition::GE => 0xA, // Greater or equal (signed)
5486                    Condition::LT => 0xB, // Less than (signed)
5487                    Condition::GT => 0xC, // Greater than (signed)
5488                    Condition::LE => 0xD, // Less or equal (signed)
5489                };
5490
5491                // IT <cond>: single Then block (mask = 0x8 for T only)
5492                // IT instruction: 1011 1111 firstcond mask
5493                let it_instr: u16 = 0xBF00 | (cond_bits << 4) | 0x8;
5494
5495                // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5496                // This MOV will only execute if condition is true due to IT block
5497                let d_bit = (rd_bits >> 3) & 1;
5498                let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5499
5500                // Emit: IT <cond>, MOV rd, rm
5501                let mut bytes = it_instr.to_le_bytes().to_vec();
5502                bytes.extend_from_slice(&mov_instr.to_le_bytes());
5503                Ok(bytes)
5504            }
5505
5506            // Popcnt: Population count (count set bits)
5507            // ARM Cortex-M has no native POPCNT, so we implement the bit manipulation algorithm:
5508            // x = x - ((x >> 1) & 0x55555555);
5509            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
5510            // x = (x + (x >> 4)) & 0x0F0F0F0F;
5511            // x = x + (x >> 8);
5512            // x = x + (x >> 16);
5513            // return x & 0x3F;
5514            //
5515            // Uses rd as working register and R12 as scratch for constants
5516            ArmOp::Popcnt { rd, rm } => {
5517                let mut bytes = Vec::new();
5518
5519                // First, move rm to rd if they're different
5520                if rd != rm {
5521                    let rd_bits = reg_to_bits(rd) as u16;
5522                    let rm_bits = reg_to_bits(rm) as u16;
5523                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5524                    let d_bit = (rd_bits >> 3) & 1;
5525                    let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5526                    bytes.extend_from_slice(&mov_instr.to_le_bytes());
5527                }
5528
5529                // Step 1: x = x - ((x >> 1) & 0x55555555)
5530                // Load 0x55555555 into R12
5531                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x5555)?);
5532                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x5555)?);
5533
5534                // R12_temp = rd >> 1
5535                // We need a second scratch register. Use R11.
5536                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 1)?);
5537
5538                // R11 = R11 & R12 (R11 = (x >> 1) & 0x55555555)
5539                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(11, 11, 12)?);
5540
5541                // rd = rd - R11
5542                bytes.extend_from_slice(&self.encode_thumb32_sub_reg_raw(
5543                    reg_to_bits(rd),
5544                    reg_to_bits(rd),
5545                    11,
5546                )?);
5547
5548                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5549                // Load 0x33333333 into R12
5550                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x3333)?);
5551                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x3333)?);
5552
5553                // R11 = rd & R12
5554                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5555                    11,
5556                    reg_to_bits(rd),
5557                    12,
5558                )?);
5559
5560                // rd = rd >> 2
5561                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(
5562                    reg_to_bits(rd),
5563                    reg_to_bits(rd),
5564                    2,
5565                )?);
5566
5567                // rd = rd & R12
5568                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5569                    reg_to_bits(rd),
5570                    reg_to_bits(rd),
5571                    12,
5572                )?);
5573
5574                // rd = rd + R11
5575                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5576                    reg_to_bits(rd),
5577                    reg_to_bits(rd),
5578                    11,
5579                )?);
5580
5581                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5582                // R11 = rd >> 4
5583                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 4)?);
5584
5585                // rd = rd + R11
5586                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5587                    reg_to_bits(rd),
5588                    reg_to_bits(rd),
5589                    11,
5590                )?);
5591
5592                // Load 0x0F0F0F0F into R12
5593                bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, 0x0F0F)?);
5594                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, 0x0F0F)?);
5595
5596                // rd = rd & R12
5597                bytes.extend_from_slice(&self.encode_thumb32_and_reg_raw(
5598                    reg_to_bits(rd),
5599                    reg_to_bits(rd),
5600                    12,
5601                )?);
5602
5603                // Step 4: x = x + (x >> 8)
5604                // R11 = rd >> 8
5605                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 8)?);
5606
5607                // rd = rd + R11
5608                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5609                    reg_to_bits(rd),
5610                    reg_to_bits(rd),
5611                    11,
5612                )?);
5613
5614                // Step 5: x = x + (x >> 16)
5615                // R11 = rd >> 16
5616                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(11, reg_to_bits(rd), 16)?);
5617
5618                // rd = rd + R11
5619                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
5620                    reg_to_bits(rd),
5621                    reg_to_bits(rd),
5622                    11,
5623                )?);
5624
5625                // Step 6: return x & 0x3F
5626                // AND with 0x3F (small immediate, can use BIC or AND with immediate)
5627                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5628                    reg_to_bits(rd),
5629                    reg_to_bits(rd),
5630                    0x3F,
5631                )?);
5632
5633                Ok(bytes)
5634            }
5635
5636            // I64DivU: 64-bit unsigned division using binary long division
5637            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = quotient
5638            // Uses: R4-R7, R12 as loop counter (avoid R8 for Renode compatibility)
5639            //
5640            // #610: the fixed-ABI wrapper marshals the selector-assigned
5641            // operand registers into the core's fixed regs and lands the
5642            // result in rd — pre-#610 this arm IGNORED its register fields,
5643            // so the selector read its rd pair (e.g. R4:R5) after the core's
5644            // own POP restored the stale caller values over it: 0 for every
5645            // input. A zero divisor now traps (UDF #0), per WASM semantics.
5646            ArmOp::I64DivU {
5647                rdlo,
5648                rdhi,
5649                rnlo,
5650                rnhi,
5651                rmlo,
5652                rmhi,
5653                elide_zero_guard,
5654            } => {
5655                let mut bytes = Vec::new();
5656                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5657                // #494 phase 2b: elided only under a certificate-discharged
5658                // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
5659                if !elide_zero_guard {
5660                    emit_i64_divisor_zero_trap(&mut bytes);
5661                }
5662
5663                // PUSH {R4-R7} - save scratch registers (NO LR — this is inline code)
5664                // 16-bit PUSH: 1011 010 M rrrrrrrr where M=0 (no LR), r=R4-R7 = 0xF0
5665                // Encoding: 1011 0100 1111 0000 = 0xB4F0
5666                bytes.extend_from_slice(&0xB4F0u16.to_le_bytes());
5667
5668                // Initialize quotient (R4:R5) = 0
5669                bytes.extend_from_slice(&0x2400u16.to_le_bytes()); // MOV R4, #0
5670                bytes.extend_from_slice(&0x2500u16.to_le_bytes()); // MOV R5, #0
5671
5672                // Initialize remainder (R6:R7) = 0
5673                bytes.extend_from_slice(&0x2600u16.to_le_bytes()); // MOV R6, #0
5674                bytes.extend_from_slice(&0x2700u16.to_le_bytes()); // MOV R7, #0
5675
5676                // Initialize loop counter R12 = 64 (use R12 scratch instead of R8)
5677                // MOV.W R12, #64: F04F 0C40
5678                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5679                bytes.extend_from_slice(&0x0C40u16.to_le_bytes());
5680
5681                // Loop start
5682                let loop_start = bytes.len();
5683
5684                // === Loop body: process one bit ===
5685
5686                // 1. Shift quotient R4:R5 left by 1
5687                // LSLS R5, R5, #1 (16-bit: 0000 0010 1010 1101 = 0x006D -> actually 0x002D for LSL R5,R5,#1)
5688                // LSL Rd, Rm, #imm5: 000 00 imm5 Rm Rd = 000 00 00001 101 101 = 0x006D
5689                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5690                // Get carry from R4 into R5: ORR R5, R5, R4 LSR #31
5691                // Thumb-2 ORR with shifted register: EA45 75D4 = ORR.W R5, R5, R4, LSR #31
5692                // 11101010 010 S Rn | 0 imm3 Rd imm2 type Rm
5693                // type=01 (LSR), imm5=31 (imm3=111, imm2=11)
5694                bytes.extend_from_slice(&0xEA45u16.to_le_bytes());
5695                bytes.extend_from_slice(&0x75D4u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5696                // LSLS R4, R4, #1: 000 00 00001 100 100 = 0x0064
5697                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5698
5699                // 2. Shift remainder R6:R7 left by 1, OR in MSB of dividend R1
5700                // LSLS R7, R7, #1
5701                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5702                // ORR.W R7, R7, R6, LSR #31
5703                bytes.extend_from_slice(&0xEA47u16.to_le_bytes());
5704                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5705                // LSLS R6, R6, #1
5706                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5707                // ORR.W R6, R6, R1, LSR #31 (bring in MSB of dividend high)
5708                bytes.extend_from_slice(&0xEA46u16.to_le_bytes());
5709                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5710
5711                // 3. Shift dividend R0:R1 left by 1
5712                // LSLS R1, R1, #1
5713                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5714                // ORR.W R1, R1, R0, LSR #31
5715                bytes.extend_from_slice(&0xEA41u16.to_le_bytes());
5716                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5717                // LSLS R0, R0, #1
5718                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5719
5720                // 4. Compare remainder >= divisor (64-bit unsigned comparison)
5721                // Compare high words first: CMP R7, R3
5722                // CMP Rn, Rm encoding: 0x4280 | (Rm << 3) | Rn
5723                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3 (16-bit)
5724                // BHI means R7 > R3 (unsigned) - definitely subtract
5725                // BLO means R7 < R3 - definitely don't subtract
5726                // BEQ means need to check low words
5727
5728                // If high > divisor high: branch to subtract (forward +offset)
5729                // BHI.N +6 (skip CMP, skip BLO, do subtract)
5730                // BHI: 1101 1000 offset8 where cond=1000 (HI)
5731                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4 (to subtract block)
5732
5733                // If high < divisor high: branch past subtract
5734                // BLO.N +10 (skip to decrement)
5735                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BLO/BCC +12 (past subtract)
5736
5737                // High words equal, compare low: CMP R6, R2
5738                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2 (16-bit)
5739                // BLO/BCC past subtract (skip SUBS+SBC.W+ORR.W = 10 bytes = 4 halfwords from PC+4)
5740                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords (past subtract)
5741
5742                // === Subtract block: remainder -= divisor, quotient |= 1 ===
5743                // SUBS R6, R6, R2
5744                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2 (16-bit)
5745                // SBC R7, R7, R3 (with borrow)
5746                // Thumb-2 SBC.W: EB67 0703 = SBC.W R7, R7, R3
5747                bytes.extend_from_slice(&0xEB67u16.to_le_bytes());
5748                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5749                // ORR R4, R4, #1 (set bit 0 of quotient low)
5750                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5751                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5752
5753                // === Decrement counter and loop ===
5754                // SUBS.W R12, R12, #1 (decrement loop counter)
5755                // SUBS.W R12, R12, #1: F1BC 0C01
5756                bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
5757                bytes.extend_from_slice(&0x0C01u16.to_le_bytes());
5758
5759                // BNE back to loop_start
5760                let branch_offset_bytes = bytes.len() - loop_start + 4; // +4 for pipeline
5761                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5762                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5763                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5764
5765                // === Loop done, move quotient to R0:R1 ===
5766                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5767                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5768
5769                // POP {R4-R7} - restore scratch registers (NO PC — inline code continues)
5770                // 16-bit POP: 1011 110 P rrrrrrrr where P=0 (no PC), r=R4-R7 = 0xF0
5771                // Encoding: 1011 1100 1111 0000 = 0xBCF0
5772                bytes.extend_from_slice(&0xBCF0u16.to_le_bytes());
5773
5774                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5775                Ok(bytes)
5776            }
5777
5778            // I64DivS: 64-bit signed division
5779            // Converts to unsigned, divides, then applies sign
5780            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5781            //   ->  R0:R1 = quotient (signed)
5782            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5783            ArmOp::I64DivS {
5784                rdlo,
5785                rdhi,
5786                rnlo,
5787                rnhi,
5788                rmlo,
5789                rmhi,
5790                elide_zero_guard,
5791                elide_overflow_guard,
5792            } => {
5793                let mut bytes = Vec::new();
5794                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5795                // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
5796                // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
5797                // the #633 overflow guard falls ONLY to
5798                // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
5799                // divisor-nonzero fact alone must keep it.
5800                if !elide_zero_guard {
5801                    emit_i64_divisor_zero_trap(&mut bytes);
5802                }
5803                if !elide_overflow_guard {
5804                    // #633: INT64_MIN / -1 overflows — trap like the i32 path
5805                    // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
5806                    emit_i64_divs_overflow_trap(&mut bytes);
5807                }
5808
5809                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5810                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5811                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5812
5813                // Save result sign in R9: R9 = R1 XOR R3 (sign bit = MSB)
5814                // EOR.W R9, R1, R3
5815                bytes.extend_from_slice(&0xEA81u16.to_le_bytes());
5816                bytes.extend_from_slice(&0x0903u16.to_le_bytes());
5817
5818                // If dividend negative (R1 MSB set), negate it
5819                // TST R1, R1 (check sign)
5820                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5821                // BPL skip_neg_dividend (+10 bytes = 5 halfwords)
5822                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5823
5824                // Negate R0:R1 (64-bit): RSBS R0, R0, #0; SBC R1, R1, R1 LSL #1
5825                // Actually: MVN R0, R0; MVN R1, R1; ADDS R0, R0, #1; ADC R1, R1, #0
5826                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5827                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5828                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5829                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5830                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5831
5832                // If divisor negative (R3 MSB set), negate it
5833                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5834                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5835
5836                // Negate R2:R3
5837                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
5838                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
5839                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
5840                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
5841                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
5842
5843                // === Now do unsigned division (same as I64DivU) ===
5844                // Initialize quotient (R4:R5) = 0
5845                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5846                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5847                // Initialize remainder (R6:R7) = 0
5848                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5849                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5850                // Initialize loop counter R8 = 64
5851                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5852                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5853
5854                let loop_start = bytes.len();
5855
5856                // Shift quotient left
5857                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5858                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5859                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5860                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5861
5862                // Shift remainder left, OR in MSB of dividend
5863                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5864                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5865                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5866                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5867                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5868                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5869
5870                // Shift dividend left
5871                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5872                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5873                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5874                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5875
5876                // Compare and conditionally subtract
5877                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5878                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5879                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5880                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5881                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5882
5883                // Subtract and set quotient bit
5884                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5885                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5886                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5887                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5888                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5889
5890                // Decrement and loop
5891                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
5892                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
5893
5894                let branch_offset_bytes = bytes.len() - loop_start + 4;
5895                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5896                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5897                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5898
5899                // Move quotient to R0:R1
5900                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5901                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5902
5903                // If result should be negative (R9 MSB set), negate R0:R1
5904                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9 (check MSB)
5905                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
5906                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8 (skip negation)
5907
5908                // Negate result R0:R1
5909                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5910                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5911                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5912                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5913                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5914
5915                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
5916                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
5917                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5918
5919                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5920                Ok(bytes)
5921            }
5922
5923            // I64RemU: 64-bit unsigned remainder using binary long division
5924            // Same algorithm as I64DivU but returns remainder instead of quotient
5925            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = remainder
5926            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5927            ArmOp::I64RemU {
5928                rdlo,
5929                rdhi,
5930                rnlo,
5931                rnhi,
5932                rmlo,
5933                rmhi,
5934                elide_zero_guard,
5935            } => {
5936                let mut bytes = Vec::new();
5937                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5938                if !elide_zero_guard {
5939                    emit_i64_divisor_zero_trap(&mut bytes);
5940                }
5941
5942                // PUSH {R4-R8} - save scratch registers (NO LR — inline code)
5943                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5944                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
5945
5946                // Initialize quotient (R4:R5) = 0 (computed but not returned)
5947                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5948                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5949                // Initialize remainder (R6:R7) = 0
5950                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5951                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5952                // Initialize loop counter R8 = 64
5953                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5954                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5955
5956                let loop_start = bytes.len();
5957
5958                // Shift quotient left (not needed for result, but keeps algorithm same)
5959                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5960                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5961                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5962                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5963
5964                // Shift remainder left, OR in MSB of dividend
5965                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5966                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5967                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5968                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5969                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5970                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5971
5972                // Shift dividend left
5973                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5974                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5975                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5976                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5977
5978                // Compare and conditionally subtract
5979                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5980                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5981                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5982                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5983                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5984
5985                // Subtract and set quotient bit
5986                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5987                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5988                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5989                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5990                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5991
5992                // Decrement and loop
5993                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
5994                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
5995
5996                let branch_offset_bytes = bytes.len() - loop_start + 4;
5997                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5998                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5999                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6000
6001                // Move REMAINDER to R0:R1 (difference from I64DivU)
6002                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6003                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6004
6005                // POP {R4-R8} - restore scratch registers (NO PC — inline code continues)
6006                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6007                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6008
6009                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6010                Ok(bytes)
6011            }
6012
6013            // I64RemS: 64-bit signed remainder
6014            // Remainder sign follows dividend sign (not quotient rule)
6015            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
6016            //   ->  R0:R1 = remainder (signed, same sign as dividend)
6017            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6018            ArmOp::I64RemS {
6019                rdlo,
6020                rdhi,
6021                rnlo,
6022                rnhi,
6023                rmlo,
6024                rmhi,
6025                elide_zero_guard,
6026            } => {
6027                let mut bytes = Vec::new();
6028                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6029                if !elide_zero_guard {
6030                    emit_i64_divisor_zero_trap(&mut bytes);
6031                }
6032
6033                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
6034                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6035                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6036
6037                // Save dividend sign in R9 (remainder sign = dividend sign)
6038                // MOV R9, R1 (just need the sign bit)
6039                bytes.extend_from_slice(&0x4689u16.to_le_bytes()); // MOV R9, R1
6040
6041                // If dividend negative (R1 MSB set), negate it
6042                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
6043                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6044
6045                // Negate R0:R1
6046                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6047                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6048                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6049                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6050                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6051
6052                // If divisor negative (R3 MSB set), negate it
6053                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
6054                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6055
6056                // Negate R2:R3
6057                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
6058                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
6059                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
6060                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
6061                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
6062
6063                // === Unsigned division algorithm ===
6064                // Initialize quotient (R4:R5) = 0
6065                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6066                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6067                // Initialize remainder (R6:R7) = 0
6068                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6069                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6070                // Initialize loop counter R8 = 64
6071                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6072                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6073
6074                let loop_start = bytes.len();
6075
6076                // Shift quotient left
6077                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6078                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6079                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6080                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6081
6082                // Shift remainder left, OR in MSB of dividend
6083                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6084                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6085                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6086                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6087                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6088                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6089
6090                // Shift dividend left
6091                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6092                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6093                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6094                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6095
6096                // Compare and conditionally subtract
6097                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6098                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6099                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6100                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6101                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6102
6103                // Subtract and set quotient bit
6104                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6105                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6106                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6107                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6108                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6109
6110                // Decrement and loop
6111                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6112                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6113
6114                let branch_offset_bytes = bytes.len() - loop_start + 4;
6115                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6116                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6117                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6118
6119                // Move remainder to R0:R1
6120                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6121                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6122
6123                // If original dividend was negative (R9 MSB set), negate remainder
6124                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9
6125                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6126                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6127
6128                // Negate result R0:R1
6129                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6130                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6131                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6132                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6133                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6134
6135                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6136                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6137                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6138
6139                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6140                Ok(bytes)
6141            }
6142
6143            // === F32 VFP single-precision Thumb-2 encodings ===
6144            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6145            ArmOp::F32Add { sd, sn, sm } => {
6146                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A00, sd, sn, sm)?))
6147            }
6148            ArmOp::F32Sub { sd, sn, sm } => {
6149                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A40, sd, sn, sm)?))
6150            }
6151            ArmOp::F32Mul { sd, sn, sm } => {
6152                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE200A00, sd, sn, sm)?))
6153            }
6154            ArmOp::F32Div { sd, sn, sm } => {
6155                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE800A00, sd, sn, sm)?))
6156            }
6157            ArmOp::F32Abs { sd, sm } => {
6158                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB00AC0, sd, sm)?))
6159            }
6160            ArmOp::F32Neg { sd, sm } => {
6161                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10A40, sd, sm)?))
6162            }
6163            ArmOp::F32Sqrt { sd, sm } => {
6164                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10AC0, sd, sm)?))
6165            }
6166
6167            // f32 pseudo-ops — multi-instruction sequences
6168            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6169            ArmOp::F32Ceil { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b01),
6170            ArmOp::F32Floor { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b10),
6171            ArmOp::F32Trunc { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b11),
6172            ArmOp::F32Nearest { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b00),
6173            ArmOp::F32Min { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, true),
6174            ArmOp::F32Max { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, false),
6175            ArmOp::F32Copysign { sd, sn, sm } => self.encode_thumb_f32_copysign(sd, sn, sm),
6176
6177            // f32 comparisons — VCMP + VMRS + MOV #0 + IT + MOV #1
6178            ArmOp::F32Eq { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x0),
6179            ArmOp::F32Ne { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x1),
6180            ArmOp::F32Lt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x4),
6181            ArmOp::F32Le { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x9),
6182            ArmOp::F32Gt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xC),
6183            ArmOp::F32Ge { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xA),
6184
6185            ArmOp::F32Const { sd, value } => self.encode_thumb_f32_const(sd, *value),
6186
6187            ArmOp::F32Load { sd, addr } => {
6188                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED900A00, sd, addr)?))
6189            }
6190            ArmOp::F32Store { sd, addr } => {
6191                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED800A00, sd, addr)?))
6192            }
6193
6194            ArmOp::F32ConvertI32S { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, true),
6195            ArmOp::F32ConvertI32U { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, false),
6196            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
6197                Err(synth_core::Error::synthesis(
6198                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6199                ))
6200            }
6201            ArmOp::F32ReinterpretI32 { sd, rm } => {
6202                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(true, sd, rm)?))
6203            }
6204            ArmOp::I32ReinterpretF32 { rd, sm } => {
6205                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(false, sm, rd)?))
6206            }
6207            ArmOp::I32TruncF32S { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, true),
6208            ArmOp::I32TruncF32U { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, false),
6209
6210            // === F64 VFP double-precision Thumb-2 encodings ===
6211            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6212            ArmOp::F64Add { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6213                0xEE300B00, dd, dn, dm,
6214            )?)),
6215            ArmOp::F64Sub { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6216                0xEE300B40, dd, dn, dm,
6217            )?)),
6218            ArmOp::F64Mul { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6219                0xEE200B00, dd, dn, dm,
6220            )?)),
6221            ArmOp::F64Div { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6222                0xEE800B00, dd, dn, dm,
6223            )?)),
6224            ArmOp::F64Abs { dd, dm } => {
6225                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?))
6226            }
6227            ArmOp::F64Neg { dd, dm } => {
6228                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?))
6229            }
6230            ArmOp::F64Sqrt { dd, dm } => {
6231                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?))
6232            }
6233
6234            // f64 pseudo-ops
6235            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6236            ArmOp::F64Ceil { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b01),
6237            ArmOp::F64Floor { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b10),
6238            ArmOp::F64Trunc { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b11),
6239            ArmOp::F64Nearest { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b00),
6240            ArmOp::F64Min { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, true),
6241            ArmOp::F64Max { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, false),
6242            ArmOp::F64Copysign { dd, dn, dm } => self.encode_thumb_f64_copysign(dd, dn, dm),
6243
6244            // f64 comparisons
6245            ArmOp::F64Eq { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x0),
6246            ArmOp::F64Ne { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x1),
6247            ArmOp::F64Lt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x4),
6248            ArmOp::F64Le { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x9),
6249            ArmOp::F64Gt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xC),
6250            ArmOp::F64Ge { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xA),
6251
6252            ArmOp::F64Const { dd, value } => self.encode_thumb_f64_const(dd, *value),
6253
6254            ArmOp::F64Load { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6255                0xED900B00, dd, addr,
6256            )?)),
6257            ArmOp::F64Store { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6258                0xED800B00, dd, addr,
6259            )?)),
6260
6261            ArmOp::F64ConvertI32S { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, true),
6262            ArmOp::F64ConvertI32U { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, false),
6263            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
6264                Err(synth_core::Error::synthesis(
6265                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6266                ))
6267            }
6268            ArmOp::F64PromoteF32 { dd, sm } => self.encode_thumb_f64_promote_f32(dd, sm),
6269            ArmOp::F32DemoteF64 { sd, dm } => self.encode_thumb_f32_demote_f64(sd, dm),
6270            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => Ok(vfp_to_thumb_bytes(
6271                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?,
6272            )),
6273            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => Ok(vfp_to_thumb_bytes(
6274                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?,
6275            )),
6276            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
6277                Err(synth_core::Error::synthesis(
6278                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
6279                ))
6280            }
6281            ArmOp::I32TruncF64S { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, true),
6282            ArmOp::I32TruncF64U { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, false),
6283
6284            // ===== i64 operations: encode as multi-instruction Thumb-2 sequences =====
6285
6286            // I64Add: ADDS rdlo, rnlo, rmlo; ADC.W rdhi, rnhi, rmhi
6287            ArmOp::I64Add {
6288                rdlo,
6289                rdhi,
6290                rnlo,
6291                rnhi,
6292                rmlo,
6293                rmhi,
6294            } => {
6295                let mut bytes = Vec::new();
6296                // ADDS rdlo, rnlo, rmlo (16-bit)
6297                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adds {
6298                    rd: *rdlo,
6299                    rn: *rnlo,
6300                    op2: Operand2::Reg(*rmlo),
6301                })?);
6302                // ADC.W rdhi, rnhi, rmhi (32-bit)
6303                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adc {
6304                    rd: *rdhi,
6305                    rn: *rnhi,
6306                    op2: Operand2::Reg(*rmhi),
6307                })?);
6308                Ok(bytes)
6309            }
6310
6311            // I64Sub: SUBS rdlo, rnlo, rmlo; SBC.W rdhi, rnhi, rmhi
6312            ArmOp::I64Sub {
6313                rdlo,
6314                rdhi,
6315                rnlo,
6316                rnhi,
6317                rmlo,
6318                rmhi,
6319            } => {
6320                let mut bytes = Vec::new();
6321                // SUBS rdlo, rnlo, rmlo (16-bit)
6322                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Subs {
6323                    rd: *rdlo,
6324                    rn: *rnlo,
6325                    op2: Operand2::Reg(*rmlo),
6326                })?);
6327                // SBC.W rdhi, rnhi, rmhi (32-bit)
6328                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Sbc {
6329                    rd: *rdhi,
6330                    rn: *rnhi,
6331                    op2: Operand2::Reg(*rmhi),
6332                })?);
6333                Ok(bytes)
6334            }
6335
6336            // I64And: AND rdlo, rnlo, rmlo; AND rdhi, rnhi, rmhi
6337            ArmOp::I64And {
6338                rdlo,
6339                rdhi,
6340                rnlo,
6341                rnhi,
6342                rmlo,
6343                rmhi,
6344            } => {
6345                let mut bytes = Vec::new();
6346                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6347                    rd: *rdlo,
6348                    rn: *rnlo,
6349                    op2: Operand2::Reg(*rmlo),
6350                })?);
6351                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6352                    rd: *rdhi,
6353                    rn: *rnhi,
6354                    op2: Operand2::Reg(*rmhi),
6355                })?);
6356                Ok(bytes)
6357            }
6358
6359            // I64Or: ORR rdlo, rnlo, rmlo; ORR rdhi, rnhi, rmhi
6360            ArmOp::I64Or {
6361                rdlo,
6362                rdhi,
6363                rnlo,
6364                rnhi,
6365                rmlo,
6366                rmhi,
6367            } => {
6368                let mut bytes = Vec::new();
6369                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6370                    rd: *rdlo,
6371                    rn: *rnlo,
6372                    op2: Operand2::Reg(*rmlo),
6373                })?);
6374                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6375                    rd: *rdhi,
6376                    rn: *rnhi,
6377                    op2: Operand2::Reg(*rmhi),
6378                })?);
6379                Ok(bytes)
6380            }
6381
6382            // I64Xor: EOR rdlo, rnlo, rmlo; EOR rdhi, rnhi, rmhi
6383            ArmOp::I64Xor {
6384                rdlo,
6385                rdhi,
6386                rnlo,
6387                rnhi,
6388                rmlo,
6389                rmhi,
6390            } => {
6391                let mut bytes = Vec::new();
6392                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6393                    rd: *rdlo,
6394                    rn: *rnlo,
6395                    op2: Operand2::Reg(*rmlo),
6396                })?);
6397                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6398                    rd: *rdhi,
6399                    rn: *rnhi,
6400                    op2: Operand2::Reg(*rmhi),
6401                })?);
6402                Ok(bytes)
6403            }
6404
6405            // I64Eqz: ORR scratch, lo, hi; ITE EQ; MOV rd, #1; MOV rd, #0
6406            ArmOp::I64Eqz { rd, rnlo, rnhi } => self.encode_thumb(&ArmOp::I64SetCondZ {
6407                rd: *rd,
6408                rn_lo: *rnlo,
6409                rn_hi: *rnhi,
6410            }),
6411
6412            // I64 comparisons: delegate to I64SetCond
6413            ArmOp::I64Eq {
6414                rd,
6415                rnlo,
6416                rnhi,
6417                rmlo,
6418                rmhi,
6419            } => self.encode_thumb(&ArmOp::I64SetCond {
6420                rd: *rd,
6421                rn_lo: *rnlo,
6422                rn_hi: *rnhi,
6423                rm_lo: *rmlo,
6424                rm_hi: *rmhi,
6425                cond: synth_synthesis::Condition::EQ,
6426            }),
6427
6428            ArmOp::I64Ne {
6429                rd,
6430                rnlo,
6431                rnhi,
6432                rmlo,
6433                rmhi,
6434            } => self.encode_thumb(&ArmOp::I64SetCond {
6435                rd: *rd,
6436                rn_lo: *rnlo,
6437                rn_hi: *rnhi,
6438                rm_lo: *rmlo,
6439                rm_hi: *rmhi,
6440                cond: synth_synthesis::Condition::NE,
6441            }),
6442
6443            ArmOp::I64LtS {
6444                rd,
6445                rnlo,
6446                rnhi,
6447                rmlo,
6448                rmhi,
6449            } => self.encode_thumb(&ArmOp::I64SetCond {
6450                rd: *rd,
6451                rn_lo: *rnlo,
6452                rn_hi: *rnhi,
6453                rm_lo: *rmlo,
6454                rm_hi: *rmhi,
6455                cond: synth_synthesis::Condition::LT,
6456            }),
6457
6458            ArmOp::I64LtU {
6459                rd,
6460                rnlo,
6461                rnhi,
6462                rmlo,
6463                rmhi,
6464            } => self.encode_thumb(&ArmOp::I64SetCond {
6465                rd: *rd,
6466                rn_lo: *rnlo,
6467                rn_hi: *rnhi,
6468                rm_lo: *rmlo,
6469                rm_hi: *rmhi,
6470                cond: synth_synthesis::Condition::LO,
6471            }),
6472
6473            ArmOp::I64LeS {
6474                rd,
6475                rnlo,
6476                rnhi,
6477                rmlo,
6478                rmhi,
6479            } => self.encode_thumb(&ArmOp::I64SetCond {
6480                rd: *rd,
6481                rn_lo: *rnlo,
6482                rn_hi: *rnhi,
6483                rm_lo: *rmlo,
6484                rm_hi: *rmhi,
6485                cond: synth_synthesis::Condition::LE,
6486            }),
6487
6488            ArmOp::I64LeU {
6489                rd,
6490                rnlo,
6491                rnhi,
6492                rmlo,
6493                rmhi,
6494            } => self.encode_thumb(&ArmOp::I64SetCond {
6495                rd: *rd,
6496                rn_lo: *rnlo,
6497                rn_hi: *rnhi,
6498                rm_lo: *rmlo,
6499                rm_hi: *rmhi,
6500                cond: synth_synthesis::Condition::LS,
6501            }),
6502
6503            ArmOp::I64GtS {
6504                rd,
6505                rnlo,
6506                rnhi,
6507                rmlo,
6508                rmhi,
6509            } => self.encode_thumb(&ArmOp::I64SetCond {
6510                rd: *rd,
6511                rn_lo: *rnlo,
6512                rn_hi: *rnhi,
6513                rm_lo: *rmlo,
6514                rm_hi: *rmhi,
6515                cond: synth_synthesis::Condition::GT,
6516            }),
6517
6518            ArmOp::I64GtU {
6519                rd,
6520                rnlo,
6521                rnhi,
6522                rmlo,
6523                rmhi,
6524            } => self.encode_thumb(&ArmOp::I64SetCond {
6525                rd: *rd,
6526                rn_lo: *rnlo,
6527                rn_hi: *rnhi,
6528                rm_lo: *rmlo,
6529                rm_hi: *rmhi,
6530                cond: synth_synthesis::Condition::HI,
6531            }),
6532
6533            ArmOp::I64GeS {
6534                rd,
6535                rnlo,
6536                rnhi,
6537                rmlo,
6538                rmhi,
6539            } => self.encode_thumb(&ArmOp::I64SetCond {
6540                rd: *rd,
6541                rn_lo: *rnlo,
6542                rn_hi: *rnhi,
6543                rm_lo: *rmlo,
6544                rm_hi: *rmhi,
6545                cond: synth_synthesis::Condition::GE,
6546            }),
6547
6548            ArmOp::I64GeU {
6549                rd,
6550                rnlo,
6551                rnhi,
6552                rmlo,
6553                rmhi,
6554            } => self.encode_thumb(&ArmOp::I64SetCond {
6555                rd: *rd,
6556                rn_lo: *rnlo,
6557                rn_hi: *rnhi,
6558                rm_lo: *rmlo,
6559                rm_hi: *rmhi,
6560                cond: synth_synthesis::Condition::HS,
6561            }),
6562
6563            // I64Const: MOVW rdlo, lo16; MOVT rdlo, hi16; MOVW rdhi, lo16_hi; MOVT rdhi, hi16_hi
6564            ArmOp::I64Const { rdlo, rdhi, value } => {
6565                let lo32 = *value as u32;
6566                let hi32 = (*value >> 32) as u32;
6567                let mut bytes = Vec::new();
6568                // Load low 32 bits into rdlo
6569                bytes.extend_from_slice(
6570                    &self.encode_thumb32_movw_raw(reg_to_bits(rdlo), lo32 & 0xFFFF)?,
6571                );
6572                if lo32 > 0xFFFF {
6573                    bytes.extend_from_slice(
6574                        &self.encode_thumb32_movt_raw(reg_to_bits(rdlo), lo32 >> 16)?,
6575                    );
6576                }
6577                // Load high 32 bits into rdhi
6578                bytes.extend_from_slice(
6579                    &self.encode_thumb32_movw_raw(reg_to_bits(rdhi), hi32 & 0xFFFF)?,
6580                );
6581                if hi32 > 0xFFFF {
6582                    bytes.extend_from_slice(
6583                        &self.encode_thumb32_movt_raw(reg_to_bits(rdhi), hi32 >> 16)?,
6584                    );
6585                }
6586                Ok(bytes)
6587            }
6588
6589            // I64Ldr: LDR rdlo, [base, offset]; LDR rdhi, [base, offset+4]
6590            ArmOp::I64Ldr { rdlo, rdhi, addr } => {
6591                let mut bytes = Vec::new();
6592                // #372/#382: a memory `i64.load` carries an index register
6593                // (`reg_imm(R11, addr_reg, offset)` = R11 + addr + offset). The
6594                // immediate `encode_thumb32_ldr` below uses only base+offset and
6595                // would SILENTLY DROP `offset_reg` — the #206 defect, here for
6596                // i64. `i64_effective_base` materializes the effective base into
6597                // `ip` (and, when `offset+4 > 0xFFF`, folds the offset in too so
6598                // the function is NOT skipped — #382), returning the residual
6599                // imm12 for the two halves. Frame i64 loads (no `offset_reg`, e.g.
6600                // a spilled local at `[SP, #off]`) keep the plain `[base,#off]`
6601                // form unchanged — so existing output is byte-identical.
6602                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6603                bytes.extend_from_slice(&self.encode_thumb32_ldr(rdlo, &base, offset)?);
6604                bytes.extend_from_slice(&self.encode_thumb32_ldr(
6605                    rdhi,
6606                    &base,
6607                    offset.wrapping_add(4),
6608                )?);
6609                Ok(bytes)
6610            }
6611
6612            // I64Str: STR rdlo, [base, offset]; STR rdhi, [base, offset+4]
6613            ArmOp::I64Str { rdlo, rdhi, addr } => {
6614                let mut bytes = Vec::new();
6615                // #372/#382: same index-materialization + large-offset fold as
6616                // I64Ldr (see above).
6617                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6618                bytes.extend_from_slice(&self.encode_thumb32_str(rdlo, &base, offset)?);
6619                bytes.extend_from_slice(&self.encode_thumb32_str(
6620                    rdhi,
6621                    &base,
6622                    offset.wrapping_add(4),
6623                )?);
6624                Ok(bytes)
6625            }
6626
6627            // I64ExtendI32S: MOV rdlo, rn; ASR rdhi, rdlo, #31 (sign-extend)
6628            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
6629                let mut bytes = Vec::new();
6630                if rdlo != rn {
6631                    // MOV rdlo, rn (16-bit)
6632                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6633                        rd: *rdlo,
6634                        op2: Operand2::Reg(*rn),
6635                    })?);
6636                }
6637                // ASR rdhi, rdlo, #31 (sign-extend: fill high word with sign bit)
6638                bytes.extend_from_slice(
6639                    &self.encode_thumb32_shift(rdhi, rdlo, 31, 0b10)?, // ASR type
6640                );
6641                Ok(bytes)
6642            }
6643
6644            // I64ExtendI32U: MOV rdlo, rn; MOV rdhi, #0
6645            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
6646                let mut bytes = Vec::new();
6647                if rdlo != rn {
6648                    // MOV rdlo, rn
6649                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6650                        rd: *rdlo,
6651                        op2: Operand2::Reg(*rn),
6652                    })?);
6653                }
6654                // MOV rdhi, #0 (#916: MOV.W for rdhi >= R8). Unconditional
6655                // site with no branches in the expansion — before the fix this
6656                // emitted the literal two-instruction stream [4608, 2800], half
6657                // of which was `CMP r0,#0` rather than the high-word clear, so
6658                // every i64.extend_i32_u into a high pair leaked stale bits.
6659                emit_thumb_zero_fill(&mut bytes, reg_to_bits(rdhi));
6660                Ok(bytes)
6661            }
6662
6663            // I32WrapI64: MOV rd, rnlo (just take low 32 bits)
6664            ArmOp::I32WrapI64 { rd, rnlo } => {
6665                if rd == rnlo {
6666                    // No-op: already in the right register
6667                    let instr: u16 = 0xBF00; // NOP
6668                    Ok(instr.to_le_bytes().to_vec())
6669                } else {
6670                    // MOV rd, rnlo
6671                    self.encode_thumb(&ArmOp::Mov {
6672                        rd: *rd,
6673                        op2: Operand2::Reg(*rnlo),
6674                    })
6675                }
6676            }
6677
6678            // ===== Helium MVE operations (Thumb-2 encoding) =====
6679            ArmOp::MveLoad { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vldrw(qd, addr))),
6680            ArmOp::MveStore { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vstrw(qd, addr))),
6681            ArmOp::MveConst { qd, bytes } => self.encode_thumb_mve_const(qd, bytes),
6682            ArmOp::MveAnd { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6683                0xEF000150, qd, qn, qm,
6684            ))),
6685            ArmOp::MveOrr { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6686                0xEF200150, qd, qn, qm,
6687            ))),
6688            ArmOp::MveEor { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6689                0xFF000150, qd, qn, qm,
6690            ))),
6691            ArmOp::MveMvn { qd, qm } => {
6692                // VMVN Qd, Qm: 0xFFB005C0 | Qd<<12 | Qm
6693                let qd_enc = qreg_to_num(qd);
6694                let qm_enc = qreg_to_num(qm);
6695                let instr: u32 = 0xFFB005C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6696                Ok(vfp_to_thumb_bytes(instr))
6697            }
6698            ArmOp::MveBic { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6699                0xEF100150, qd, qn, qm,
6700            ))),
6701            ArmOp::MveAddI { qd, qn, qm, size } => {
6702                let sz = mve_size_bits(size);
6703                let base: u32 = 0xEF000840 | (sz << 20);
6704                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6705            }
6706            ArmOp::MveSubI { qd, qn, qm, size } => {
6707                let sz = mve_size_bits(size);
6708                let base: u32 = 0xFF000840 | (sz << 20);
6709                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6710            }
6711            ArmOp::MveMulI { qd, qn, qm, size } => {
6712                let sz = mve_size_bits(size);
6713                let base: u32 = 0xEF000950 | (sz << 20);
6714                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6715            }
6716            ArmOp::MveNegI { qd, qm, size } => {
6717                let sz = mve_size_bits(size);
6718                // VNEG.Sx Qd, Qm
6719                let qd_enc = qreg_to_num(qd);
6720                let qm_enc = qreg_to_num(qm);
6721                let base: u32 = 0xFFB103C0 | (sz << 18);
6722                let instr = base | ((qd_enc * 2) << 12) | (qm_enc * 2);
6723                Ok(vfp_to_thumb_bytes(instr))
6724            }
6725            ArmOp::MveDup { qd, rn, size } => {
6726                let sz = mve_size_bits(size);
6727                let qd_enc = qreg_to_num(qd);
6728                let rn_bits = reg_to_bits(rn);
6729                // VDUP.sz Qd, Rn: EEA0 0B10 variant
6730                // size encoding: 00=32, 01=16, 10=8
6731                let be = match sz {
6732                    0 => 0b00u32, // 8-bit
6733                    1 => 0b01,    // 16-bit
6734                    _ => 0b00,    // 32-bit (default)
6735                };
6736                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12) | (be << 5);
6737                Ok(vfp_to_thumb_bytes(instr))
6738            }
6739            ArmOp::MveExtractLane { rd, qn, lane, size } => {
6740                let qn_enc = qreg_to_num(qn);
6741                let rd_bits = reg_to_bits(rd);
6742                // VMOV.sz Rd, Dn[x] — extract from Q-register lane
6743                // For 32-bit: VMOV Rd, Dn — where Dn is the appropriate D-register
6744                let d_reg = qn_enc * 2 + ((*lane as u32) >> 1);
6745                let lane_in_d = (*lane as u32) & 1;
6746                let _sz = mve_size_bits(size);
6747                // VMOV Rd, Dn[x]: EE10 0B10 for 32-bit
6748                let instr: u32 = 0xEE100B10 | (d_reg << 16) | (rd_bits << 12) | (lane_in_d << 21);
6749                Ok(vfp_to_thumb_bytes(instr))
6750            }
6751            ArmOp::MveInsertLane { qd, rn, lane, size } => {
6752                let qd_enc = qreg_to_num(qd);
6753                let rn_bits = reg_to_bits(rn);
6754                let d_reg = qd_enc * 2 + ((*lane as u32) >> 1);
6755                let lane_in_d = (*lane as u32) & 1;
6756                let _sz = mve_size_bits(size);
6757                // VMOV Dn[x], Rn: EE00 0B10 for 32-bit
6758                let instr: u32 = 0xEE000B10 | (d_reg << 16) | (rn_bits << 12) | (lane_in_d << 21);
6759                Ok(vfp_to_thumb_bytes(instr))
6760            }
6761
6762            // MVE float comparisons — emit VCMP + VPSEL sequence (simplified: just VCMP)
6763            ArmOp::MveCmpEqI { qd, qn, qm, size }
6764            | ArmOp::MveCmpNeI { qd, qn, qm, size }
6765            | ArmOp::MveCmpLtS { qd, qn, qm, size }
6766            | ArmOp::MveCmpLtU { qd, qn, qm, size }
6767            | ArmOp::MveCmpGtS { qd, qn, qm, size }
6768            | ArmOp::MveCmpGtU { qd, qn, qm, size }
6769            | ArmOp::MveCmpLeS { qd, qn, qm, size }
6770            | ArmOp::MveCmpLeU { qd, qn, qm, size }
6771            | ArmOp::MveCmpGeS { qd, qn, qm, size }
6772            | ArmOp::MveCmpGeU { qd, qn, qm, size } => {
6773                // Encode as VADD (placeholder encoding — real implementation
6774                // would use VCMP + VPSEL pair)
6775                let sz = mve_size_bits(size);
6776                let base: u32 = 0xEF000840 | (sz << 20);
6777                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6778            }
6779
6780            // f32x4 MVE arithmetic
6781            ArmOp::MveAddF32 { qd, qn, qm } => {
6782                // VADD.F32 Qd, Qn, Qm (MVE): 0xEF000D40
6783                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6784            }
6785            ArmOp::MveSubF32 { qd, qn, qm } => {
6786                // VSUB.F32 Qd, Qn, Qm (MVE): 0xEF200D40
6787                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF200D40, qd, qn, qm)))
6788            }
6789            ArmOp::MveMulF32 { qd, qn, qm } => {
6790                // VMUL.F32 Qd, Qn, Qm (MVE): 0xFF000D50
6791                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xFF000D50, qd, qn, qm)))
6792            }
6793            ArmOp::MveNegF32 { qd, qm } => {
6794                let qd_enc = qreg_to_num(qd);
6795                let qm_enc = qreg_to_num(qm);
6796                // VNEG.F32 Qd, Qm: FFB907C0
6797                let instr: u32 = 0xFFB907C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6798                Ok(vfp_to_thumb_bytes(instr))
6799            }
6800            ArmOp::MveAbsF32 { qd, qm } => {
6801                let qd_enc = qreg_to_num(qd);
6802                let qm_enc = qreg_to_num(qm);
6803                // VABS.F32 Qd, Qm: FFB90740
6804                let instr: u32 = 0xFFB90740 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6805                Ok(vfp_to_thumb_bytes(instr))
6806            }
6807            ArmOp::MveCmpEqF32 { qd, qn, qm }
6808            | ArmOp::MveCmpNeF32 { qd, qn, qm }
6809            | ArmOp::MveCmpLtF32 { qd, qn, qm }
6810            | ArmOp::MveCmpLeF32 { qd, qn, qm }
6811            | ArmOp::MveCmpGtF32 { qd, qn, qm }
6812            | ArmOp::MveCmpGeF32 { qd, qn, qm } => {
6813                // Placeholder: encode as VADD.F32 (real impl needs VCMP.F32 + VPSEL)
6814                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6815            }
6816            ArmOp::MveDupF32 { qd, rn } => {
6817                let qd_enc = qreg_to_num(qd);
6818                let rn_bits = reg_to_bits(rn);
6819                // VDUP.32 Qd, Rn (same encoding as integer VDUP.32)
6820                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12);
6821                Ok(vfp_to_thumb_bytes(instr))
6822            }
6823            ArmOp::MveExtractLaneF32 { rd, qn, lane } => {
6824                let qn_enc = qreg_to_num(qn);
6825                let rd_bits = reg_to_bits(rd);
6826                // VMOV Rd, Sn where Sn = Q*4 + lane
6827                let s_num = qn_enc * 4 + (*lane as u32);
6828                let (vn, n) = encode_sreg(s_num);
6829                let instr: u32 = 0xEE100A10 | (vn << 16) | (rd_bits << 12) | (n << 7);
6830                Ok(vfp_to_thumb_bytes(instr))
6831            }
6832            ArmOp::MveReplaceLaneF32 { qd, rn, lane } => {
6833                let qd_enc = qreg_to_num(qd);
6834                let rn_bits = reg_to_bits(rn);
6835                // VMOV Sn, Rn where Sn = Q*4 + lane
6836                let s_num = qd_enc * 4 + (*lane as u32);
6837                let (vn, n) = encode_sreg(s_num);
6838                let instr: u32 = 0xEE000A10 | (vn << 16) | (rn_bits << 12) | (n << 7);
6839                Ok(vfp_to_thumb_bytes(instr))
6840            }
6841            ArmOp::MveDivF32 { qd, qn, qm } => {
6842                // Lane-wise: extract 4 S-regs, VDIV, insert back
6843                self.encode_thumb_mve_lane_wise_f32_binop(qd, qn, qm, 0xEE800A00)
6844            }
6845            ArmOp::MveSqrtF32 { qd, qm } => {
6846                // Lane-wise: extract 4 S-regs, VSQRT, insert back
6847                self.encode_thumb_mve_lane_wise_f32_sqrt(qd, qm)
6848            }
6849
6850            // Catch-all for any remaining ops
6851            _ => {
6852                let instr: u16 = 0xBF00; // NOP
6853                Ok(instr.to_le_bytes().to_vec())
6854            }
6855        }
6856    }
6857
6858    // === Thumb-2 VFP multi-instruction helpers ===
6859
6860    /// Encode F32 comparison as Thumb-2: VCMP.F32 + VMRS + MOVS rd,#0 + IT + MOV rd,#1
6861    fn encode_thumb_f32_compare(
6862        &self,
6863        rd: &Reg,
6864        sn: &VfpReg,
6865        sm: &VfpReg,
6866        cond_code: u32,
6867    ) -> Result<Vec<u8>> {
6868        let mut bytes = Vec::new();
6869        let rd_bits = reg_to_bits(rd);
6870
6871        // #709 (bug found under #708/#709): the `MOVS Rd,#0` below is a
6872        // FLAG-SETTING 16-bit move. Emitting it AFTER `VMRS APSR_nzcv, FPSCR`
6873        // (as the original code did) clobbered the N/Z/C/V flags the VMRS just
6874        // transferred from the VFP compare, so the following `IT<cond>` read
6875        // stale flags and every f32 comparison silently returned 0 (verified:
6876        // `flt(1.0,2.0)` → 0 on Cortex-M4F). The 619 harness never caught it
6877        // because it deliberately skipped compare EXECUTION on a false premise
6878        // (unicorn DOES model VMRS→APSR). Fix: materialize the `#0` FIRST, then
6879        // VCMP+VMRS set the flags the `IT` consumes. Instruction sizes are
6880        // unchanged (pure reorder), so the estimator↔encoder oracle (#511) is
6881        // untouched — only the byte ORDER differs.
6882
6883        // MOVS Rd, #0 (16-bit): 0010 0 Rd(3) 0000 0000 — its flag side effect
6884        // is immediately overwritten by the VMRS below.
6885        if rd_bits < 8 {
6886            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
6887            bytes.extend_from_slice(&movs_zero.to_le_bytes());
6888        } else {
6889            // MOV.W Rd, #0 (32-bit Thumb-2)
6890            let hw1: u16 = 0xF04F;
6891            let hw2: u16 = (rd_bits as u16) << 8;
6892            bytes.extend_from_slice(&hw1.to_le_bytes());
6893            bytes.extend_from_slice(&hw2.to_le_bytes());
6894        }
6895
6896        // VCMP.F32 Sn, Sm
6897        let sn_num = vfp_sreg_to_num(sn)?;
6898        let sm_num = vfp_sreg_to_num(sm)?;
6899        let (vd, d) = encode_sreg(sn_num);
6900        let (vm, m) = encode_sreg(sm_num);
6901        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
6902        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
6903
6904        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10 (sets the flags IT consumes)
6905        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
6906
6907        // IT<cond> — If-Then for conditional MOV
6908        // IT encoding: 1011 1111 cond(4) mask(4)
6909        // mask = 0x8 for single "then" (IT)
6910        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
6911        bytes.extend_from_slice(&it.to_le_bytes());
6912
6913        // MOV Rd, #1 (16-bit, conditional due to IT): 0010 0 Rd(3) 0000 0001
6914        if rd_bits < 8 {
6915            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
6916            bytes.extend_from_slice(&mov_one.to_le_bytes());
6917        } else {
6918            // MOV.W Rd, #1 (32-bit)
6919            let hw1: u16 = 0xF04F;
6920            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
6921            bytes.extend_from_slice(&hw1.to_le_bytes());
6922            bytes.extend_from_slice(&hw2.to_le_bytes());
6923        }
6924
6925        Ok(bytes)
6926    }
6927
6928    /// Encode F32 constant load as Thumb-2: MOVW + MOVT + VMOV
6929    fn encode_thumb_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
6930        let mut bytes = Vec::new();
6931        let bits = value.to_bits();
6932        let rt: u32 = 12; // R12/IP as temp
6933
6934        // MOVW R12, #lo16
6935        // Thumb-2 MOVW: 11110 i 10 0100 imm4 | 0 imm3 Rd imm8
6936        let lo16 = bits & 0xFFFF;
6937        let imm4 = (lo16 >> 12) & 0xF;
6938        let i_bit = (lo16 >> 11) & 1;
6939        let imm3 = (lo16 >> 8) & 0x7;
6940        let imm8 = lo16 & 0xFF;
6941        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
6942        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
6943        bytes.extend_from_slice(&hw1.to_le_bytes());
6944        bytes.extend_from_slice(&hw2.to_le_bytes());
6945
6946        // MOVT R12, #hi16
6947        let hi16 = (bits >> 16) & 0xFFFF;
6948        let imm4 = (hi16 >> 12) & 0xF;
6949        let i_bit = (hi16 >> 11) & 1;
6950        let imm3 = (hi16 >> 8) & 0x7;
6951        let imm8 = hi16 & 0xFF;
6952        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
6953        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
6954        bytes.extend_from_slice(&hw1.to_le_bytes());
6955        bytes.extend_from_slice(&hw2.to_le_bytes());
6956
6957        // VMOV Sd, R12
6958        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
6959        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6960
6961        Ok(bytes)
6962    }
6963
6964    /// Encode VMOV + VCVT.F32.xS32 as Thumb-2
6965    fn encode_thumb_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
6966        let mut bytes = Vec::new();
6967
6968        // VMOV Sd, Rm
6969        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
6970        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
6971
6972        // VCVT.F32.S32/U32 Sd, Sd. Bit 7 (op) = 1 for signed (S32), 0 for
6973        // unsigned (U32): signed = 0xEEB80AC0, unsigned = 0xEEB80A40
6974        // (GI-FPU-002: previously swapped — see the ARM32 twin).
6975        let sd_num = vfp_sreg_to_num(sd)?;
6976        let (vd, d) = encode_sreg(sd_num);
6977        let (vm, m) = encode_sreg(sd_num);
6978        let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
6979        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
6980        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
6981
6982        Ok(bytes)
6983    }
6984
6985    /// Encode F32 rounding pseudo-op as Thumb-2 via VCVT to integer and back
6986    /// Encode F32 rounding as Thumb-2.
6987    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
6988    ///
6989    /// For trunc: uses VCVTR.S32.F32 (always truncates).
6990    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant),
6991    /// then restores FPSCR.
6992    fn encode_thumb_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
6993        let mut bytes = Vec::new();
6994        let sm_num = vfp_sreg_to_num(sm)?;
6995        let sd_num = vfp_sreg_to_num(sd)?;
6996        let (vd_s, d_s) = encode_sreg(sd_num);
6997        let (vm_s, m_s) = encode_sreg(sm_num);
6998
6999        if mode == 0b11 {
7000            // Trunc (toward zero): VCVTR.S32.F32 — bit[7]=1, always truncates
7001            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7002            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7003        } else {
7004            // ceil/floor/nearest: manipulate FPSCR rounding mode
7005            let rt: u32 = 12; // R12/IP as temp
7006
7007            // VMRS R12, FPSCR
7008            let vmrs = 0xEEF10A10 | (rt << 12);
7009            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7010
7011            // BIC.W R12, R12, #(3 << 22) — clear RMode bits [23:22]
7012            // Thumb-2 modified immediate for 3<<22 = 0x00C00000:
7013            // BIC.W encoding: 11110 i 0 0001 S Rn | 0 imm3 Rd imm8
7014            // 0x00C00000 = 0x03 shifted left by 22 => Thumb mod-imm: i=0, imm3=0b101, imm8=0x03
7015            let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF); // BIC, Rn=R12
7016            let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
7017            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7018            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7019
7020            // ORR.W R12, R12, #(mode << 22)
7021            if mode != 0 {
7022                let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF); // ORR, Rn=R12
7023                let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
7024                bytes.extend_from_slice(&orr_hw1.to_le_bytes());
7025                bytes.extend_from_slice(&orr_hw2.to_le_bytes());
7026            }
7027
7028            // VMSR FPSCR, R12
7029            let vmsr = 0xEEE10A10 | (rt << 12);
7030            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7031
7032            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rmode
7033            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7034            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7035
7036            // Restore FPSCR: clear rmode bits back to nearest (default)
7037            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7038            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7039            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7040            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7041        }
7042
7043        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
7044        let (vd2, d2) = encode_sreg(sd_num);
7045        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
7046        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
7047
7048        Ok(bytes)
7049    }
7050
7051    /// Encode F32 min/max as Thumb-2: VMOV + VCMP + VMRS + IT + VMOV
7052    fn encode_thumb_f32_minmax(
7053        &self,
7054        sd: &VfpReg,
7055        sn: &VfpReg,
7056        sm: &VfpReg,
7057        is_min: bool,
7058    ) -> Result<Vec<u8>> {
7059        let mut bytes = Vec::new();
7060        let sn_num = vfp_sreg_to_num(sn)?;
7061        let sm_num = vfp_sreg_to_num(sm)?;
7062        let sd_num = vfp_sreg_to_num(sd)?;
7063
7064        // VMOV.F32 Sd, Sn
7065        let (vd, d) = encode_sreg(sd_num);
7066        let (vn, n) = encode_sreg(sn_num);
7067        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
7068        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sn));
7069
7070        // VCMP.F32 Sn, Sm
7071        let (vm, m) = encode_sreg(sm_num);
7072        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7073        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7074
7075        // VMRS APSR_nzcv, FPSCR
7076        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7077
7078        // IT GT (for min) or IT MI (for max)
7079        let cond: u16 = if is_min { 0xC } else { 0x4 };
7080        let it: u16 = 0xBF00 | (cond << 4) | 0x8;
7081        bytes.extend_from_slice(&it.to_le_bytes());
7082
7083        // VMOV{cond}.F32 Sd, Sm — conditional VMOV in IT block
7084        let vmov_sm = 0xEEB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7085        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sm));
7086
7087        Ok(bytes)
7088    }
7089
7090    /// Encode F32 copysign as Thumb-2
7091    /// Encode F32 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7092    /// encoder scratch, #212), the flags, and Sd:
7093    ///
7094    ///   VMOV R12, Sm ; CMP R12, #0    (N flag = the sign bit)
7095    ///   VABS.F32 Sd, Sn               (magnitude, sign cleared)
7096    ///   IT MI ; VNEG.F32(MI) Sd, Sd
7097    ///
7098    /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7099    /// The R12 capture happens BEFORE Sd is written, so Sd aliasing Sn or Sm
7100    /// is safe. (The previous sequence staged the magnitude through R0 —
7101    /// clobbering a live allocator-owned value, the #615 class; caught while
7102    /// composing the F64 twin for #369.)
7103    fn encode_thumb_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7104        let mut bytes = Vec::new();
7105
7106        // VMOV R12, Sm (sign source bits)
7107        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
7108            false,
7109            sm,
7110            &Reg::R12,
7111        )?));
7112        // CMP.W R12, #0 — N = bit31 (the sign, incl. -0.0 / -NaN).
7113        bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7114        bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7115        // VABS.F32 Sd, Sn
7116        let sd_num = vfp_sreg_to_num(sd)?;
7117        let sn_num = vfp_sreg_to_num(sn)?;
7118        let (vd, d) = encode_sreg(sd_num);
7119        let (vn, n) = encode_sreg(sn_num);
7120        let vabs = 0xEEB00AC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7121        bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7122        // IT MI ; VNEG.F32(MI) Sd, Sd
7123        bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7124        let vneg = 0xEEB10A40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7125        bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7126
7127        Ok(bytes)
7128    }
7129
7130    /// Encode F64 comparison as Thumb-2: VCMP.F64 + VMRS + MOV #0 + IT + MOV #1
7131    fn encode_thumb_f64_compare(
7132        &self,
7133        rd: &Reg,
7134        dn: &VfpReg,
7135        dm: &VfpReg,
7136        cond_code: u32,
7137    ) -> Result<Vec<u8>> {
7138        let mut bytes = Vec::new();
7139        let rd_bits = reg_to_bits(rd);
7140
7141        // #712-class fix (found at f64-phase-2 wiring, #369): the 16-bit
7142        // `MOVS Rd,#0` is FLAG-SETTING. The original order emitted it AFTER
7143        // `VMRS APSR_nzcv, FPSCR`, clobbering the N/Z/C/V flags the VMRS just
7144        // transferred, so the following `IT<cond>` read stale flags and every
7145        // f64 comparison silently returned 0 — the exact bug the f32 compare
7146        // encoder shipped with and #712 fixed. Same fix: materialize the `#0`
7147        // FIRST (its flag side effect is overwritten by the VMRS), then
7148        // VCMP+VMRS set the flags the IT consumes. Pure reorder — sizes
7149        // unchanged.
7150
7151        // MOVS Rd, #0
7152        if rd_bits < 8 {
7153            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
7154            bytes.extend_from_slice(&movs_zero.to_le_bytes());
7155        } else {
7156            let hw1: u16 = 0xF04F;
7157            let hw2: u16 = (rd_bits as u16) << 8;
7158            bytes.extend_from_slice(&hw1.to_le_bytes());
7159            bytes.extend_from_slice(&hw2.to_le_bytes());
7160        }
7161
7162        // VCMP.F64 Dn, Dm
7163        let dn_num = vfp_dreg_to_num(dn)?;
7164        let dm_num = vfp_dreg_to_num(dm)?;
7165        let (vd, d) = encode_dreg(dn_num);
7166        let (vm, m) = encode_dreg(dm_num);
7167        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7168        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7169
7170        // VMRS APSR_nzcv, FPSCR (sets the flags the IT consumes)
7171        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7172
7173        // IT<cond>
7174        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7175        bytes.extend_from_slice(&it.to_le_bytes());
7176
7177        // MOV Rd, #1
7178        if rd_bits < 8 {
7179            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7180            bytes.extend_from_slice(&mov_one.to_le_bytes());
7181        } else {
7182            let hw1: u16 = 0xF04F;
7183            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7184            bytes.extend_from_slice(&hw1.to_le_bytes());
7185            bytes.extend_from_slice(&hw2.to_le_bytes());
7186        }
7187
7188        Ok(bytes)
7189    }
7190
7191    /// Encode F64 constant load as Thumb-2: MOVW+MOVT (lo32 into R0) + MOVW+MOVT (hi32 into R12) + VMOV Dd, R0, R12
7192    fn encode_thumb_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
7193        let mut bytes = Vec::new();
7194        let bits = value.to_bits();
7195        let lo32 = bits as u32;
7196        let hi32 = (bits >> 32) as u32;
7197
7198        // MOVW R0, #lo16(lo32)
7199        let lo16 = lo32 & 0xFFFF;
7200        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(0, lo16)?);
7201
7202        // MOVT R0, #hi16(lo32)
7203        let hi16 = (lo32 >> 16) & 0xFFFF;
7204        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(0, hi16)?);
7205
7206        // MOVW R12, #lo16(hi32)
7207        let lo16 = hi32 & 0xFFFF;
7208        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
7209
7210        // MOVT R12, #hi16(hi32)
7211        let hi16 = (hi32 >> 16) & 0xFFFF;
7212        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
7213
7214        // VMOV Dd, R0, R12
7215        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
7216        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7217
7218        Ok(bytes)
7219    }
7220
7221    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as Thumb-2
7222    /// Encode i32 → f64 conversion as Thumb-2. The integer stages through the
7223    /// DESTINATION's own low S-alias (`S(2d)`) — allocator-owned by
7224    /// definition — never S0 (which may hold a live value; the previous
7225    /// pseudo-op's S0 staging was the #615 class). Also fixes the SWAPPED
7226    /// signed/unsigned VCVT bases (bit7 = 1 is SIGNED — the same swap the f32
7227    /// twin had; latent here because f64.convert_i32_* was decode-dropped
7228    /// until #369): clang-verified vcvt.f64.s32 d1,s2 = eeb8 1bc1,
7229    /// vcvt.f64.u32 d1,s2 = eeb8 1b41.
7230    fn encode_thumb_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7231        let dd_num = vfp_dreg_to_num(dd)?;
7232        if dd_num > 7 {
7233            return Err(synth_core::Error::synthesis(format!(
7234                "F64ConvertI32: destination {dd:?} has no S-register alias \
7235                 (D8..D15) — the selector allocates only D0..D7"
7236            )));
7237        }
7238        let mut bytes = Vec::new();
7239
7240        // VMOV S(2d), Rm — stage the integer in the destination's low word.
7241        let (vn_s, n_s) = encode_sreg(2 * dd_num);
7242        let rt = reg_to_bits(rm);
7243        let vmov = 0xEE000A10 | (vn_s << 16) | (rt << 12) | (n_s << 7);
7244        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7245
7246        // VCVT.F64.S32/U32 Dd, S(2d)
7247        let (vd, d) = encode_dreg(dd_num);
7248        let (vm, m) = encode_sreg(2 * dd_num);
7249        let base = if signed { 0xEEB80BC0 } else { 0xEEB80B40 };
7250        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7251        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7252
7253        Ok(bytes)
7254    }
7255
7256    /// Encode VCVT.F64.F32 Dd, Sm as Thumb-2
7257    fn encode_thumb_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7258        let dd_num = vfp_dreg_to_num(dd)?;
7259        let sm_num = vfp_sreg_to_num(sm)?;
7260        let (vd, d) = encode_dreg(dd_num);
7261        let (vm, m) = encode_sreg(sm_num);
7262
7263        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7264        Ok(vfp_to_thumb_bytes(vcvt))
7265    }
7266
7267    /// Encode VCVT.F32.F64 Sd, Dm (f32.demote_f64) as Thumb-2 — single
7268    /// instruction, round-to-nearest-even per FPSCR default, exactly WASM
7269    /// §4.3.3 demote (clang-verified: vcvt.f32.f64 s1,d2 = eef7 0bc2).
7270    fn encode_thumb_f32_demote_f64(&self, sd: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7271        let sd_num = vfp_sreg_to_num(sd)?;
7272        let dm_num = vfp_dreg_to_num(dm)?;
7273        let (vd, d) = encode_sreg(sd_num);
7274        let (vm, m) = encode_dreg(dm_num);
7275
7276        let vcvt = 0xEEB70BC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7277        Ok(vfp_to_thumb_bytes(vcvt))
7278    }
7279
7280    /// Encode f64 → i32 truncation as Thumb-2 (round-toward-zero VCVT).
7281    ///
7282    /// The 32-bit result stages through the SOURCE's own low S-alias, `S(2m)`,
7283    /// clobbering half of an operand the selector has already popped. The
7284    /// overlapping write is well-defined: VCVT reads its source operand before
7285    /// writing (compilers emit `vcvt.f32.f64 s0, d0` routinely).
7286    ///
7287    /// # The one precondition, and who actually provides it
7288    ///
7289    /// `dm` must be a DEAD TEMP — never a pinned param/local home. That is the
7290    /// whole safety argument, and it is worth naming the guarantor precisely
7291    /// (#946): **`select_with_stack`** provides it, by copying a home into a
7292    /// fresh D-temp first. Visible in the shipped output for
7293    /// `(func (param f64) (result i32) (i32.trunc_f64_s (local.get 0)))`:
7294    ///
7295    /// ```text
7296    ///   vmov r1, r2, d0     ; read the param out of its AAPCS-VFP home D0
7297    ///   vmov d1, r1, r2     ; ...into a fresh D-temp
7298    ///   vcvt.s32.f64 s2, d1 ; convert from the TEMP, staging into its own S2
7299    /// ```
7300    ///
7301    /// `InstructionSelector::select` / `select_default` do NOT provide it —
7302    /// `alloc_vfp_dreg` is a bare round-robin `(n + 1) % 16` with no liveness
7303    /// or home test. That path is not reachable from `synth compile`
7304    /// (`arm_backend.rs` calls `select_with_stack` exclusively; the only
7305    /// non-test caller of `select` is `examples/compile_add.rs`), so this is
7306    /// not a live miscompile — but a caller reaching that `pub` API directly
7307    /// gets no such guarantee.
7308    ///
7309    /// # What this deliberately does NOT claim
7310    ///
7311    /// An earlier version of this comment said the staging register is "never
7312    /// S0, which may hold an unrelated live value (the #615 class)". **That is
7313    /// false**, and measurably so: for
7314    /// `(func (result i32) (i32.trunc_f64_s (f64.const 3.7)))` the shipped
7315    /// compiler emits `vcvt.s32.f64 s0, d0`.
7316    ///
7317    /// It is also unnecessary. S0 is only dangerous as an *unrelated* scratch;
7318    /// here it is always the low half of `dm` itself, which the precondition
7319    /// above already makes dead. Naming a guard the code does not have — and
7320    /// does not need — invites a future reader to lean on it. The dead-temp
7321    /// precondition is the only thing holding this up.
7322    fn encode_thumb_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7323        let dm_num = vfp_dreg_to_num(dm)?;
7324        if dm_num > 7 {
7325            return Err(synth_core::Error::synthesis(format!(
7326                "I32TruncF64: source {dm:?} has no S-register alias \
7327                 (D8..D15) — the selector allocates only D0..D7"
7328            )));
7329        }
7330        let mut bytes = Vec::new();
7331
7332        // VCVT.S32/U32.F64 S(2m), Dm (clang-verified:
7333        // vcvt.s32.f64 s1,d2 = eefd 0bc2 ; vcvt.u32.f64 s1,d2 = eefc 0bc2)
7334        let (vm, m) = encode_dreg(dm_num);
7335        let (vd_s, d_s) = encode_sreg(2 * dm_num);
7336        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
7337        let vcvt = base | (d_s << 22) | (vd_s << 12) | (m << 5) | vm;
7338        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7339
7340        // VMOV Rd, S(2m)
7341        let rt = reg_to_bits(rd);
7342        let vmov = 0xEE100A10 | (vd_s << 16) | (rt << 12) | (d_s << 7);
7343        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7344
7345        Ok(bytes)
7346    }
7347
7348    /// Encode F64 rounding as a SINGLE Thumb-2 VRINT (FPv5 / cortex-m7dp).
7349    /// `mode` keeps the legacy FPSCR-RMode numbering of the callers —
7350    /// 0b00=nearest(ties-to-even)→VRINTN, 0b01=+inf(ceil)→VRINTP,
7351    /// 0b10=-inf(floor)→VRINTM, 0b11=zero(trunc)→VRINTZ — but the rounding
7352    /// mode is now ENCODED in the instruction, not smuggled through FPSCR.
7353    /// (The previous pseudo-op round-tripped through a 32-bit integer in S0:
7354    /// wrong for |x| >= 2^31, NaN/±inf collapsed to 0, -0.0 lost, and it
7355    /// CLOBBERED S0/R12 behind the allocator's back — the #615 class.)
7356    /// VRINT quietens an sNaN and preserves the sign of ±0.0/NaN per IEEE 754
7357    /// roundToIntegral, which is exactly WASM Core §4.3.3 f64.ceil/floor/
7358    /// trunc/nearest. VRINTN/P/M live in the FE "always-execute" space (never
7359    /// IT-conditional; none of these sequences emits them inside an IT block).
7360    fn encode_thumb_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7361        let dd_num = vfp_dreg_to_num(dd)?;
7362        let dm_num = vfp_dreg_to_num(dm)?;
7363        let (vd, d) = encode_dreg(dd_num);
7364        let (vm, m) = encode_dreg(dm_num);
7365        // clang-verified bases (thumbv7em, fpv5-d16):
7366        //   vrintn.f64 d1,d2 = feb9 1b42 ; vrintp = feba 1b42
7367        //   vrintm.f64 d1,d2 = febb 1b42 ; vrintz = eeb6 1bc2
7368        let base: u32 = match mode {
7369            0b00 => 0xFEB90B40, // VRINTN.F64 (round to nearest, ties to even)
7370            0b01 => 0xFEBA0B40, // VRINTP.F64 (round toward +inf)
7371            0b10 => 0xFEBB0B40, // VRINTM.F64 (round toward -inf)
7372            _ => 0xEEB60BC0,    // VRINTZ.F64 (round toward zero)
7373        };
7374        Ok(vfp_to_thumb_bytes(
7375            base | (d << 22) | (vd << 12) | (m << 5) | vm,
7376        ))
7377    }
7378
7379    /// Encode F64 min/max as Thumb-2 with WASM Core §4.3.3 semantics:
7380    ///
7381    ///   VCMP.F64 Dn, Dm ; VMRS APSR_nzcv, FPSCR
7382    ///   VMINNM.F64/VMAXNM.F64 Dd, Dn, Dm      (FPv5; -0.0 < +0.0 ordered)
7383    ///   IT VS ; VADD.F64(VS) Dd, Dn, Dm       (unordered ⇒ NaN-propagating)
7384    ///
7385    /// VMINNM/VMAXNM alone are IEEE minNum/maxNum, which return the NUMBER
7386    /// when exactly one operand is NaN — WASM requires NaN. The VS-guarded
7387    /// VADD overwrites the result with a quiet NaN whenever the compare was
7388    /// unordered (either operand NaN); on the ordered path VMINNM/VMAXNM
7389    /// order -0.0 below +0.0, matching WASM's min(+0,-0) = -0 / max = +0.
7390    /// Clobbers ONLY Dd and the flags (the previous pseudo-op's ordered IT
7391    /// GT/MI select returned the WRONG operand for NaN and ±0 mixes).
7392    ///
7393    /// Ok-or-Err: `dd` must not alias `dn`/`dm` — the VS fix-up reads them
7394    /// AFTER VMINNM wrote `dd` (the selector always allocates a fresh
7395    /// destination while both sources are still marked live).
7396    fn encode_thumb_f64_minmax(
7397        &self,
7398        dd: &VfpReg,
7399        dn: &VfpReg,
7400        dm: &VfpReg,
7401        is_min: bool,
7402    ) -> Result<Vec<u8>> {
7403        if dd == dn || dd == dm {
7404            return Err(synth_core::Error::synthesis(format!(
7405                "F64{}: destination {dd:?} aliases a source ({dn:?},{dm:?}) — \
7406                 the unordered NaN fix-up would read a clobbered operand \
7407                 (compiler bug: the selector must allocate a fresh D-temp)",
7408                if is_min { "Min" } else { "Max" },
7409            )));
7410        }
7411        let mut bytes = Vec::new();
7412        let dd_num = vfp_dreg_to_num(dd)?;
7413        let dn_num = vfp_dreg_to_num(dn)?;
7414        let dm_num = vfp_dreg_to_num(dm)?;
7415        let (vd, d) = encode_dreg(dd_num);
7416        let (vn, n) = encode_dreg(dn_num);
7417        let (vm, m) = encode_dreg(dm_num);
7418
7419        // VCMP.F64 Dn, Dm (clang-verified: vcmp.f64 d2,d3 = eeb4 2b43)
7420        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7421        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7422        // VMRS APSR_nzcv, FPSCR
7423        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7424        // VMINNM.F64 / VMAXNM.F64 Dd, Dn, Dm (clang-verified:
7425        // vminnm.f64 d1,d2,d3 = fe82 1b43 ; vmaxnm = fe82 1b03)
7426        let base: u32 = if is_min { 0xFE800B40 } else { 0xFE800B00 };
7427        let vnm = base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7428        bytes.extend_from_slice(&vfp_to_thumb_bytes(vnm));
7429        // IT VS (unordered ⇒ at least one NaN operand)
7430        bytes.extend_from_slice(&0xBF68_u16.to_le_bytes());
7431        // VADD.F64(VS) Dd, Dn, Dm — NaN + x propagates a quiet NaN
7432        let vadd = 0xEE300B00 | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7433        bytes.extend_from_slice(&vfp_to_thumb_bytes(vadd));
7434
7435        Ok(bytes)
7436    }
7437
7438    /// Encode F64 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7439    /// encoder scratch, #212), the flags, and Dd:
7440    ///
7441    ///   VMOV R12, S(2m+1)   (high word of the SIGN source Dm)
7442    ///   CMP  R12, #0        (N flag = the sign bit)
7443    ///   VABS.F64 Dd, Dn     (magnitude, sign cleared)
7444    ///   IT MI ; VNEG.F64(MI) Dd, Dd
7445    ///
7446    /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7447    /// The R12 capture happens BEFORE Dd is written, so Dd aliasing Dn or Dm
7448    /// is safe. (The previous pseudo-op clobbered R0/R1/R2 behind the
7449    /// allocator's back — the #615 class.)
7450    fn encode_thumb_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7451        let dm_num = vfp_dreg_to_num(dm)?;
7452        if dm_num > 7 {
7453            return Err(synth_core::Error::synthesis(format!(
7454                "F64Copysign: sign source {dm:?} has no S-register alias \
7455                 (D8..D15) — the selector allocates only D0..D7"
7456            )));
7457        }
7458        let mut bytes = Vec::new();
7459        // VMOV R12, S(2m+1) — the sign source's high word.
7460        let (vn_s, n_s) = encode_sreg(2 * dm_num + 1);
7461        let vmov = 0xEE100A10 | (vn_s << 16) | (12 << 12) | (n_s << 7);
7462        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7463        // CMP R12, #0 (T2: CMP.W R12, #0) — N = bit31 of the sign word.
7464        bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7465        bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7466        // VABS.F64 Dd, Dn
7467        let dd_num = vfp_dreg_to_num(dd)?;
7468        let dn_num = vfp_dreg_to_num(dn)?;
7469        let (vd, d) = encode_dreg(dd_num);
7470        let (vn, n) = encode_dreg(dn_num);
7471        let vabs = 0xEEB00BC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7472        bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7473        // IT MI ; VNEG.F64(MI) Dd, Dd
7474        bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7475        let vneg = 0xEEB10B40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7476        bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7477
7478        Ok(bytes)
7479    }
7480
7481    /// Encode VCVT.S32/U32.F32 + VMOV as Thumb-2
7482    fn encode_thumb_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7483        let mut bytes = Vec::new();
7484
7485        let sm_num = vfp_sreg_to_num(sm)?;
7486        let (vd, d) = encode_sreg(sm_num);
7487        let (vm, m) = encode_sreg(sm_num);
7488        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
7489        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7490        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7491
7492        // VMOV Rd, Sm
7493        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
7494        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7495
7496        Ok(bytes)
7497    }
7498
7499    // === Thumb-2 32-bit encoding helpers ===
7500
7501    /// Encode Thumb-2 32-bit ADD with immediate
7502    fn encode_thumb32_add(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7503        let rd_bits = reg_to_bits(rd);
7504        let rn_bits = reg_to_bits(rn);
7505
7506        // The `i:imm3:imm8` field is split the same way for both forms.
7507        let i_bit = (imm >> 11) & 1;
7508        let imm3 = (imm >> 8) & 0x7;
7509        let imm8 = imm & 0xFF;
7510
7511        let hw1_base = if imm <= 0xFF {
7512            // ADD.W (T3): the field is a ThumbExpandImm modified immediate. For
7513            // imm <= 0xFF (i:imm3 = 0000) it is the zero-extended byte, which is
7514            // correct — keep this form so existing encodings stay bit-identical.
7515            0xF100
7516        } else if imm <= 0xFFF {
7517            // ADDW (T4): a PLAIN 12-bit immediate (0..4095) — no ThumbExpandImm.
7518            // This is what makes `add sp, sp, #frame` correct for frame sizes
7519            // >= 256, which ADD.W (T3) would silently mis-encode (e.g. #256 -> #0).
7520            0xF200
7521        } else {
7522            return Err(synth_core::Error::synthesis(
7523                "ADD immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7524            ));
7525        };
7526
7527        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7528        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7529
7530        let mut bytes = hw1.to_le_bytes().to_vec();
7531        bytes.extend_from_slice(&hw2.to_le_bytes());
7532        Ok(bytes)
7533    }
7534
7535    /// Encode Thumb-2 32-bit SUB with immediate
7536    fn encode_thumb32_sub(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7537        let rd_bits = reg_to_bits(rd);
7538        let rn_bits = reg_to_bits(rn);
7539
7540        let i_bit = (imm >> 11) & 1;
7541        let imm3 = (imm >> 8) & 0x7;
7542        let imm8 = imm & 0xFF;
7543
7544        let hw1_base = if imm <= 0xFF {
7545            // SUB.W (T3) modified immediate — correct for the zero-extended byte
7546            // (imm <= 0xFF). Kept bit-identical for existing encodings.
7547            0xF1A0
7548        } else if imm <= 0xFFF {
7549            // SUBW (T4): plain 12-bit immediate (0..4095). Makes
7550            // `sub sp, sp, #frame` correct for frame sizes >= 256.
7551            0xF2A0
7552        } else {
7553            return Err(synth_core::Error::synthesis(
7554                "SUB immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7555            ));
7556        };
7557
7558        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7559        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7560
7561        let mut bytes = hw1.to_le_bytes().to_vec();
7562        bytes.extend_from_slice(&hw2.to_le_bytes());
7563        Ok(bytes)
7564    }
7565
7566    /// Encode Thumb-2 32-bit ADDS with immediate (sets flags)
7567    fn encode_thumb32_adds(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7568        let rd_bits = reg_to_bits(rd);
7569        let rn_bits = reg_to_bits(rn);
7570
7571        // ADDS.W (flag-setting) has only the modified-immediate form — error on
7572        // an un-encodable value rather than silently add the wrong constant.
7573        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7574            synth_core::Error::synthesis(
7575                "ADDS immediate is not a valid ThumbExpandImm — materialize into a register",
7576            )
7577        })?;
7578        let i_bit = (field >> 11) & 1;
7579        let imm3 = (field >> 8) & 0x7;
7580        let imm8 = field & 0xFF;
7581
7582        // ADDS.W Rd, Rn, #imm (with S=1)
7583        // First halfword: 1111 0 i 0 1000 1 Rn = F110 | i<<10 | Rn
7584        let hw1: u16 = (0xF110 | (i_bit << 10) | rn_bits) as u16;
7585        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7586
7587        let mut bytes = hw1.to_le_bytes().to_vec();
7588        bytes.extend_from_slice(&hw2.to_le_bytes());
7589        Ok(bytes)
7590    }
7591
7592    /// Encode Thumb-2 32-bit SUBS with immediate (sets flags)
7593    fn encode_thumb32_subs(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7594        let rd_bits = reg_to_bits(rd);
7595        let rn_bits = reg_to_bits(rn);
7596
7597        // SUBS.W (flag-setting) has only the modified-immediate form — error on
7598        // an un-encodable value rather than silently subtract the wrong constant.
7599        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7600            synth_core::Error::synthesis(
7601                "SUBS immediate is not a valid ThumbExpandImm — materialize into a register",
7602            )
7603        })?;
7604        let i_bit = (field >> 11) & 1;
7605        let imm3 = (field >> 8) & 0x7;
7606        let imm8 = field & 0xFF;
7607
7608        // SUBS.W Rd, Rn, #imm (with S=1)
7609        // First halfword: 1111 0 i 0 1101 1 Rn = F1B0 | i<<10 | Rn
7610        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7611        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7612
7613        let mut bytes = hw1.to_le_bytes().to_vec();
7614        bytes.extend_from_slice(&hw2.to_le_bytes());
7615        Ok(bytes)
7616    }
7617
7618    /// Encode Thumb-2 32-bit MOVW (16-bit immediate)
7619    ///
7620    /// # Contract (Verus-style)
7621    /// ```text
7622    /// requires rd <= R14
7623    /// ensures result.len() == 4
7624    /// ensures (imm & 0xFFFF) can be reconstructed from the encoding
7625    /// ```
7626    fn encode_thumb32_movw(&self, rd: &Reg, imm: u32) -> Result<Vec<u8>> {
7627        let rd_bits = reg_to_bits(rd);
7628        reg_bits_checked(rd_bits)?;
7629        let imm16 = imm & 0xFFFF;
7630
7631        // MOVW Rd, #imm16
7632        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7633        let imm4 = (imm16 >> 12) & 0xF;
7634        let i_bit = (imm16 >> 11) & 1;
7635        let imm3 = (imm16 >> 8) & 0x7;
7636        let imm8 = imm16 & 0xFF;
7637
7638        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7639        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7640
7641        let mut bytes = hw1.to_le_bytes().to_vec();
7642        bytes.extend_from_slice(&hw2.to_le_bytes());
7643        encoding_contracts::verify_thumb32(&bytes);
7644        Ok(bytes)
7645    }
7646
7647    /// Encode Thumb-2 32-bit shift with immediate
7648    ///
7649    /// # Contract (Verus-style)
7650    /// ```text
7651    /// requires rd <= R14, rm <= R14
7652    /// ensures result.len() == 4
7653    /// ```
7654    fn encode_thumb32_shift(
7655        &self,
7656        rd: &Reg,
7657        rm: &Reg,
7658        shift: u32,
7659        shift_type: u8,
7660    ) -> Result<Vec<u8>> {
7661        let rd_bits = reg_to_bits(rd);
7662        let rm_bits = reg_to_bits(rm);
7663        reg_bits_checked(rd_bits)?;
7664        reg_bits_checked(rm_bits)?;
7665        let imm5 = shift & 0x1F;
7666        let imm2 = imm5 & 0x3;
7667        let imm3 = (imm5 >> 2) & 0x7;
7668
7669        // MOV.W Rd, Rm, <shift> #imm
7670        // EA4F 0 imm3 Rd imm2 type Rm
7671        let hw1: u16 = 0xEA4F;
7672        let hw2: u16 =
7673            ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | ((shift_type as u32) << 4) | rm_bits)
7674                as u16;
7675
7676        let mut bytes = hw1.to_le_bytes().to_vec();
7677        bytes.extend_from_slice(&hw2.to_le_bytes());
7678        Ok(bytes)
7679    }
7680
7681    /// Encode Thumb-2 32-bit shift by register
7682    /// Encoding: 11111010 0xx0 Rn | 1111 Rd 0000 Rm
7683    /// shift_type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
7684    fn encode_thumb32_shift_reg(
7685        &self,
7686        rd: &Reg,
7687        rn: &Reg,
7688        rm: &Reg,
7689        shift_type: u8,
7690    ) -> Result<Vec<u8>> {
7691        let rd_bits = reg_to_bits(rd);
7692        let rn_bits = reg_to_bits(rn);
7693        let rm_bits = reg_to_bits(rm);
7694
7695        // hw1: 1111 1010 0xx0 Rn
7696        let hw1: u16 = (0xFA00 | ((shift_type as u32) << 5) | rn_bits) as u16;
7697        // hw2: 1111 Rd 0000 Rm
7698        let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
7699
7700        let mut bytes = hw1.to_le_bytes().to_vec();
7701        bytes.extend_from_slice(&hw2.to_le_bytes());
7702        Ok(bytes)
7703    }
7704
7705    /// Encode Thumb-2 32-bit CMP with immediate
7706    fn encode_thumb32_cmp_imm(&self, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7707        let rn_bits = reg_to_bits(rn);
7708
7709        // CMP.W has only the modified-immediate form (no plain-imm12 like ADDW),
7710        // so an un-encodable immediate MUST be materialized into a register by
7711        // the selector. Error rather than silently compare the wrong constant.
7712        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7713            synth_core::Error::synthesis(
7714                "CMP immediate is not a valid ThumbExpandImm — materialize into a register",
7715            )
7716        })?;
7717        let i_bit = (field >> 11) & 1;
7718        let imm3 = (field >> 8) & 0x7;
7719        let imm8 = field & 0xFF;
7720
7721        // CMP.W Rn, #imm
7722        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7723        let hw2: u16 = ((imm3 << 12) | 0x0F00 | imm8) as u16;
7724
7725        let mut bytes = hw1.to_le_bytes().to_vec();
7726        bytes.extend_from_slice(&hw2.to_le_bytes());
7727        Ok(bytes)
7728    }
7729
7730    /// #372/#382: resolve the base register AND residual immediate offset for an
7731    /// `I64Ldr`/`I64Str` whose address may carry an index register. Returns
7732    /// `(base, low_offset)`; the caller accesses the halves at `[base,
7733    /// #low_offset]` and `[base, #low_offset + 4]`.
7734    ///
7735    /// - Frame access (no `offset_reg`, e.g. a spilled local at `[SP, #off]`):
7736    ///   returns `(addr.base, off)` and emits NOTHING — byte-identical.
7737    /// - Memory access (`reg_imm(R11, addr, offset)` = `R11 + addr + offset`)
7738    ///   with `offset + 4 <= 0xFFF`: emits `ADD.W ip, base, index` and returns
7739    ///   `(ip, offset)`, folding `offset`/`offset+4` into the halves' imm12.
7740    ///   Byte-identical to the pre-#382 (#372) behavior.
7741    /// - Memory access with `offset + 4 > 0xFFF`: the imm12 form cannot hold the
7742    ///   high half's offset, so `encode_thumb32_ldr`'s `check_ldst_imm12` (#259)
7743    ///   rightly refused it and the WHOLE function was skipped (#382). Instead
7744    ///   MATERIALIZE the offset into the base: `ADD ip, index, #offset` (against
7745    ///   the read-only INDEX register, so `encode_thumb32_add_imm` never trips its
7746    ///   `rd==rn==R12` alias trap), then `ADD.W ip, ip, base` (+ R11), and return
7747    ///   `(ip, 0)` so the halves use `[ip, #0]` / `[ip, #4]`.
7748    ///
7749    /// The effective address is fully materialized into `ip` BEFORE the halves
7750    /// are accessed, so an `rdlo` aliasing the index register is safe.
7751    fn i64_effective_base(&self, bytes: &mut Vec<u8>, addr: &MemAddr) -> Result<(Reg, u32)> {
7752        let offset = if addr.offset < 0 {
7753            0u32
7754        } else {
7755            addr.offset as u32
7756        };
7757        match addr.offset_reg {
7758            Some(idx) => {
7759                let ip = Reg::R12;
7760                if offset.wrapping_add(4) > 0xFFF {
7761                    // Large static offset (#382): fold it (and R11) into ip so the
7762                    // imm12 halves stay in range instead of skipping the function.
7763                    // ADD ip, index, #offset  (index != ip → no add_imm alias trap)
7764                    bytes.extend_from_slice(&self.encode_thumb32_add_imm(&ip, &idx, offset)?);
7765                    // ADD.W ip, ip, base  (+ R11)
7766                    bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
7767                        reg_to_bits(&ip),
7768                        reg_to_bits(&ip),
7769                        reg_to_bits(&addr.base),
7770                    )?);
7771                    Ok((ip, 0))
7772                } else {
7773                    // ADD.W ip, addr.base, idx  (Thumb-2, byte-verified vs as)
7774                    let hw1: u16 = 0xEB00 | reg_to_bits(&addr.base) as u16;
7775                    let hw2: u16 = 0x0C00 | reg_to_bits(&idx) as u16;
7776                    bytes.extend_from_slice(&hw1.to_le_bytes());
7777                    bytes.extend_from_slice(&hw2.to_le_bytes());
7778                    Ok((ip, offset))
7779                }
7780            }
7781            None => Ok((addr.base, offset)),
7782        }
7783    }
7784
7785    /// Encode Thumb-2 32-bit LDR
7786    fn encode_thumb32_ldr(&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
7790        // LDR.W Rd, [Rn, #imm12]
7791        check_ldst_imm12(offset)?;
7792        let hw1: u16 = (0xF8D0 | base_bits) as u16;
7793        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7794
7795        let mut bytes = hw1.to_le_bytes().to_vec();
7796        bytes.extend_from_slice(&hw2.to_le_bytes());
7797        Ok(bytes)
7798    }
7799
7800    /// Encode Thumb-2 32-bit STR
7801    fn encode_thumb32_str(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7802        let rd_bits = reg_to_bits(rd);
7803        let base_bits = reg_to_bits(base);
7804
7805        // STR.W Rd, [Rn, #imm12]
7806        check_ldst_imm12(offset)?;
7807        let hw1: u16 = (0xF8C0 | base_bits) as u16;
7808        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7809
7810        let mut bytes = hw1.to_le_bytes().to_vec();
7811        bytes.extend_from_slice(&hw2.to_le_bytes());
7812        Ok(bytes)
7813    }
7814
7815    /// Encode Thumb-2 32-bit LDR with register offset: LDR.W Rd, [Rn, Rm]
7816    fn encode_thumb32_ldr_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7817        let rd_bits = reg_to_bits(rd);
7818        let base_bits = reg_to_bits(base);
7819        let rm_bits = reg_to_bits(offset_reg);
7820
7821        // LDR.W Rd, [Rn, Rm, LSL #0]
7822        // Encoding: 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
7823        // imm2 = 00 for no shift (LSL #0)
7824        let hw1: u16 = (0xF850 | base_bits) as u16;
7825        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7826
7827        let mut bytes = hw1.to_le_bytes().to_vec();
7828        bytes.extend_from_slice(&hw2.to_le_bytes());
7829        Ok(bytes)
7830    }
7831
7832    /// Encode Thumb-2 32-bit STR with register offset: STR.W Rd, [Rn, Rm]
7833    fn encode_thumb32_str_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7834        let rd_bits = reg_to_bits(rd);
7835        let base_bits = reg_to_bits(base);
7836        let rm_bits = reg_to_bits(offset_reg);
7837
7838        // STR.W Rd, [Rn, Rm, LSL #0]
7839        // Encoding: 1111 1000 0100 Rn | Rt 0000 00 imm2 Rm
7840        // imm2 = 00 for no shift (LSL #0)
7841        let hw1: u16 = (0xF840 | base_bits) as u16;
7842        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7843
7844        let mut bytes = hw1.to_le_bytes().to_vec();
7845        bytes.extend_from_slice(&hw2.to_le_bytes());
7846        Ok(bytes)
7847    }
7848
7849    // === Sub-word load/store Thumb-2 encoding helpers ===
7850
7851    /// Encode Thumb-2 32-bit LDRB with immediate: LDRB.W Rd, [Rn, #imm12]
7852    fn encode_thumb32_ldrb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7853        let rd_bits = reg_to_bits(rd);
7854        let base_bits = reg_to_bits(base);
7855        // LDRB.W Rd, [Rn, #imm12]: 1111 1000 1001 Rn | Rt imm12
7856        check_ldst_imm12(offset)?;
7857        let hw1: u16 = (0xF890 | base_bits) as u16;
7858        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7859        let mut bytes = hw1.to_le_bytes().to_vec();
7860        bytes.extend_from_slice(&hw2.to_le_bytes());
7861        Ok(bytes)
7862    }
7863
7864    /// Encode Thumb-2 32-bit LDRB with register: LDRB.W Rd, [Rn, Rm]
7865    fn encode_thumb32_ldrb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7866        let rd_bits = reg_to_bits(rd);
7867        let base_bits = reg_to_bits(base);
7868        let rm_bits = reg_to_bits(offset_reg);
7869        // LDRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0001 Rn | Rt 0000 00 imm2 Rm
7870        let hw1: u16 = (0xF810 | base_bits) as u16;
7871        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7872        let mut bytes = hw1.to_le_bytes().to_vec();
7873        bytes.extend_from_slice(&hw2.to_le_bytes());
7874        Ok(bytes)
7875    }
7876
7877    /// Encode Thumb-2 32-bit LDRSB with immediate: LDRSB.W Rd, [Rn, #imm12]
7878    fn encode_thumb32_ldrsb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7879        let rd_bits = reg_to_bits(rd);
7880        let base_bits = reg_to_bits(base);
7881        // LDRSB.W Rd, [Rn, #imm12]: 1111 1001 1001 Rn | Rt imm12
7882        check_ldst_imm12(offset)?;
7883        let hw1: u16 = (0xF990 | base_bits) as u16;
7884        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7885        let mut bytes = hw1.to_le_bytes().to_vec();
7886        bytes.extend_from_slice(&hw2.to_le_bytes());
7887        Ok(bytes)
7888    }
7889
7890    /// Encode Thumb-2 32-bit LDRSB with register: LDRSB.W Rd, [Rn, Rm]
7891    fn encode_thumb32_ldrsb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7892        let rd_bits = reg_to_bits(rd);
7893        let base_bits = reg_to_bits(base);
7894        let rm_bits = reg_to_bits(offset_reg);
7895        // LDRSB.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0001 Rn | Rt 0000 00 imm2 Rm
7896        let hw1: u16 = (0xF910 | base_bits) as u16;
7897        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7898        let mut bytes = hw1.to_le_bytes().to_vec();
7899        bytes.extend_from_slice(&hw2.to_le_bytes());
7900        Ok(bytes)
7901    }
7902
7903    /// Encode Thumb-2 32-bit LDRH with immediate: LDRH.W Rd, [Rn, #imm12]
7904    fn encode_thumb32_ldrh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7905        let rd_bits = reg_to_bits(rd);
7906        let base_bits = reg_to_bits(base);
7907        // LDRH.W Rd, [Rn, #imm12]: 1111 1000 1011 Rn | Rt imm12
7908        check_ldst_imm12(offset)?;
7909        let hw1: u16 = (0xF8B0 | base_bits) as u16;
7910        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7911        let mut bytes = hw1.to_le_bytes().to_vec();
7912        bytes.extend_from_slice(&hw2.to_le_bytes());
7913        Ok(bytes)
7914    }
7915
7916    /// Encode Thumb-2 32-bit LDRH with register: LDRH.W Rd, [Rn, Rm]
7917    fn encode_thumb32_ldrh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7918        let rd_bits = reg_to_bits(rd);
7919        let base_bits = reg_to_bits(base);
7920        let rm_bits = reg_to_bits(offset_reg);
7921        // LDRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0011 Rn | Rt 0000 00 imm2 Rm
7922        let hw1: u16 = (0xF830 | base_bits) as u16;
7923        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7924        let mut bytes = hw1.to_le_bytes().to_vec();
7925        bytes.extend_from_slice(&hw2.to_le_bytes());
7926        Ok(bytes)
7927    }
7928
7929    /// Encode Thumb-2 32-bit LDRSH with immediate: LDRSH.W Rd, [Rn, #imm12]
7930    fn encode_thumb32_ldrsh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7931        let rd_bits = reg_to_bits(rd);
7932        let base_bits = reg_to_bits(base);
7933        // LDRSH.W Rd, [Rn, #imm12]: 1111 1001 1011 Rn | Rt imm12
7934        check_ldst_imm12(offset)?;
7935        let hw1: u16 = (0xF9B0 | base_bits) as u16;
7936        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7937        let mut bytes = hw1.to_le_bytes().to_vec();
7938        bytes.extend_from_slice(&hw2.to_le_bytes());
7939        Ok(bytes)
7940    }
7941
7942    /// Encode Thumb-2 32-bit LDRSH with register: LDRSH.W Rd, [Rn, Rm]
7943    fn encode_thumb32_ldrsh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7944        let rd_bits = reg_to_bits(rd);
7945        let base_bits = reg_to_bits(base);
7946        let rm_bits = reg_to_bits(offset_reg);
7947        // LDRSH.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0011 Rn | Rt 0000 00 imm2 Rm
7948        let hw1: u16 = (0xF930 | base_bits) as u16;
7949        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7950        let mut bytes = hw1.to_le_bytes().to_vec();
7951        bytes.extend_from_slice(&hw2.to_le_bytes());
7952        Ok(bytes)
7953    }
7954
7955    /// Encode Thumb-2 32-bit STRB with immediate: STRB.W Rd, [Rn, #imm12]
7956    fn encode_thumb32_strb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7957        let rd_bits = reg_to_bits(rd);
7958        let base_bits = reg_to_bits(base);
7959        // STRB.W Rd, [Rn, #imm12]: 1111 1000 1000 Rn | Rt imm12
7960        check_ldst_imm12(offset)?;
7961        let hw1: u16 = (0xF880 | base_bits) as u16;
7962        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7963        let mut bytes = hw1.to_le_bytes().to_vec();
7964        bytes.extend_from_slice(&hw2.to_le_bytes());
7965        Ok(bytes)
7966    }
7967
7968    /// Encode Thumb-2 32-bit STRB with register: STRB.W Rd, [Rn, Rm]
7969    fn encode_thumb32_strb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7970        let rd_bits = reg_to_bits(rd);
7971        let base_bits = reg_to_bits(base);
7972        let rm_bits = reg_to_bits(offset_reg);
7973        // STRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0000 Rn | Rt 0000 00 imm2 Rm
7974        let hw1: u16 = (0xF800 | base_bits) as u16;
7975        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7976        let mut bytes = hw1.to_le_bytes().to_vec();
7977        bytes.extend_from_slice(&hw2.to_le_bytes());
7978        Ok(bytes)
7979    }
7980
7981    /// Encode Thumb-2 32-bit STRH with immediate: STRH.W Rd, [Rn, #imm12]
7982    fn encode_thumb32_strh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7983        let rd_bits = reg_to_bits(rd);
7984        let base_bits = reg_to_bits(base);
7985        // STRH.W Rd, [Rn, #imm12]: 1111 1000 1010 Rn | Rt imm12
7986        check_ldst_imm12(offset)?;
7987        let hw1: u16 = (0xF8A0 | base_bits) as u16;
7988        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7989        let mut bytes = hw1.to_le_bytes().to_vec();
7990        bytes.extend_from_slice(&hw2.to_le_bytes());
7991        Ok(bytes)
7992    }
7993
7994    /// Encode Thumb-2 32-bit STRH with register: STRH.W Rd, [Rn, Rm]
7995    fn encode_thumb32_strh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7996        let rd_bits = reg_to_bits(rd);
7997        let base_bits = reg_to_bits(base);
7998        let rm_bits = reg_to_bits(offset_reg);
7999        // STRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0010 Rn | Rt 0000 00 imm2 Rm
8000        let hw1: u16 = (0xF820 | base_bits) as u16;
8001        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8002        let mut bytes = hw1.to_le_bytes().to_vec();
8003        bytes.extend_from_slice(&hw2.to_le_bytes());
8004        Ok(bytes)
8005    }
8006
8007    /// Encode Thumb-2 32-bit ADD with immediate: ADD.W Rd, Rn, #imm
8008    fn encode_thumb32_add_imm(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
8009        let rd_bits = reg_to_bits(rd);
8010        let rn_bits = reg_to_bits(rn);
8011
8012        // In-range immediates (<= 0xFFF) delegate to `encode_thumb32_add`,
8013        // which picks the correct form per value:
8014        //   - imm <= 0xFF  -> ADD.W (T3). Its `i:imm3:imm8` field is a
8015        //     ThumbExpandImm MODIFIED immediate — raw == expanded only here.
8016        //   - 0x100..=0xFFF -> ADDW (T4, 0xF200): a PLAIN 12-bit immediate.
8017        //
8018        // #681: this function used to pack the raw value into the T3 field for
8019        // ALL imm <= 0xFFF. ThumbExpandImm(0x200) = 0 and ThumbExpandImm(0x400)
8020        // = 0x8000_0000, so every dynamic-address load/store with a static
8021        // offset in 0x100..=0xFFF silently computed a WRONG address — and in
8022        // --safety-bounds software the guard checked the intended address while
8023        // the access used the mis-encoded one (bounds bypass). Same
8024        // ThumbExpandImm raw-packing class as #253/#255, reached via #382.
8025        if imm <= 0xFFF {
8026            self.encode_thumb32_add(rd, rn, imm)
8027        } else {
8028            // Out-of-range immediate (> 0xFFF): materialize it into a scratch
8029            // register, then ADD.W Rd, Rn, scratch. This is the #180/#185
8030            // "encoder must produce a legal sequence, not assert" class — see #350.
8031            //
8032            // Scratch choice (must NEVER equal Rn, or Rn would be clobbered before
8033            // the ADD reads it):
8034            //   - rd != rn  => use rd itself (rn is untouched, since rd != rn).
8035            //   - rd == rn  => use R12/IP (the reserved encoder scratch). rd/rn are
8036            //                  never R12 (R12 is non-allocatable), so it can't alias.
8037            //
8038            // The materialized value is the same whether or not MOVT is emitted, so
8039            // the byte length depends only on `imm` (and rd==rn) — the size probe and
8040            // the final emit therefore agree (mandatory: the function is encoded twice).
8041            let scratch: u32 = if rd_bits == rn_bits {
8042                12 // R12/IP — in-place add, can't use rd because rd == rn
8043            } else {
8044                rd_bits // rn is preserved because rd != rn
8045            };
8046            // Invariant: the scratch must never alias Rn (would clobber it before
8047            // the ADD reads it). Unreachable in real codegen (rd/rn are never R12,
8048            // which is reserved encoder scratch), but the encoder is also driven by
8049            // the `encoder_no_panic` fuzz harness with ARBITRARY registers — incl.
8050            // rd==rn==R12, which makes scratch (R12) alias Rn. The encoder contract
8051            // (#180/#185) is Ok-or-Err, never a panic, so return a typed error
8052            // instead of asserting. #350 follow-up.
8053            if scratch == rn_bits {
8054                return Err(synth_core::Error::synthesis(format!(
8055                    "ADD #imm: cannot lower #{imm:#x} for Rd==Rn==R12 — no free scratch \
8056                     register (R12 is the reserved encoder scratch and aliases Rn here)"
8057                )));
8058            }
8059
8060            let lo16 = imm & 0xFFFF;
8061            let hi16 = (imm >> 16) & 0xFFFF;
8062
8063            let mut bytes = self.encode_thumb32_movw_raw(scratch, lo16)?;
8064            if hi16 != 0 {
8065                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(scratch, hi16)?);
8066            }
8067            bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rn_bits, scratch)?);
8068            Ok(bytes)
8069        }
8070    }
8071
8072    // === Raw encoding helpers for POPCNT (take register numbers directly) ===
8073
8074    /// Encode Thumb-2 32-bit MOVW (16-bit immediate) - raw version
8075    ///
8076    /// # Contract (Verus-style)
8077    /// ```text
8078    /// requires rd <= 14, imm16 <= 0xFFFF
8079    /// ensures result.len() == 4
8080    /// ```
8081    fn encode_thumb32_movw_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8082        reg_bits_checked(rd)?;
8083        encoding_contracts::verify_imm16(imm16);
8084        // MOVW Rd, #imm16
8085        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
8086        let imm16 = imm16 & 0xFFFF;
8087        let imm4 = (imm16 >> 12) & 0xF;
8088        let i_bit = (imm16 >> 11) & 1;
8089        let imm3 = (imm16 >> 8) & 0x7;
8090        let imm8 = imm16 & 0xFF;
8091
8092        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
8093        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8094
8095        let mut bytes = hw1.to_le_bytes().to_vec();
8096        bytes.extend_from_slice(&hw2.to_le_bytes());
8097        encoding_contracts::verify_thumb32(&bytes);
8098        Ok(bytes)
8099    }
8100
8101    /// Encode Thumb-2 32-bit MOVT (move top 16 bits) - raw version
8102    ///
8103    /// # Contract (Verus-style)
8104    /// ```text
8105    /// requires rd <= 14, imm16 <= 0xFFFF
8106    /// ensures result.len() == 4
8107    /// ```
8108    fn encode_thumb32_movt_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8109        reg_bits_checked(rd)?;
8110        encoding_contracts::verify_imm16(imm16);
8111        // MOVT Rd, #imm16
8112        // 1111 0 i 10 1 1 0 0 imm4 | 0 imm3 Rd imm8
8113        let imm16 = imm16 & 0xFFFF;
8114        let imm4 = (imm16 >> 12) & 0xF;
8115        let i_bit = (imm16 >> 11) & 1;
8116        let imm3 = (imm16 >> 8) & 0x7;
8117        let imm8 = imm16 & 0xFF;
8118
8119        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
8120        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8121
8122        let mut bytes = hw1.to_le_bytes().to_vec();
8123        bytes.extend_from_slice(&hw2.to_le_bytes());
8124        encoding_contracts::verify_thumb32(&bytes);
8125        Ok(bytes)
8126    }
8127
8128    /// Encode Thumb-2 32-bit LSR (logical shift right) with immediate - raw version
8129    fn encode_thumb32_lsr_raw(&self, rd: u32, rm: u32, shift: u32) -> Result<Vec<u8>> {
8130        // MOV.W Rd, Rm, LSR #imm
8131        // EA4F 0 imm3 Rd imm2 01 Rm
8132        let imm5 = shift & 0x1F;
8133        let imm2 = imm5 & 0x3;
8134        let imm3 = (imm5 >> 2) & 0x7;
8135
8136        let hw1: u16 = 0xEA4F;
8137        let hw2: u16 = ((imm3 << 12) | (rd << 8) | (imm2 << 6) | (0b01 << 4) | rm) as u16;
8138
8139        let mut bytes = hw1.to_le_bytes().to_vec();
8140        bytes.extend_from_slice(&hw2.to_le_bytes());
8141        Ok(bytes)
8142    }
8143
8144    /// Encode Thumb-2 32-bit AND (register) - raw version
8145    fn encode_thumb32_and_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8146        // AND.W Rd, Rn, Rm
8147        // EA00 Rn | 0 Rd 00 00 Rm
8148        let hw1: u16 = (0xEA00 | rn) as u16;
8149        let hw2: u16 = ((rd << 8) | rm) as u16;
8150
8151        let mut bytes = hw1.to_le_bytes().to_vec();
8152        bytes.extend_from_slice(&hw2.to_le_bytes());
8153        Ok(bytes)
8154    }
8155
8156    /// Encode Thumb-2 32-bit AND with immediate - raw version
8157    fn encode_thumb32_and_imm_raw(&self, rd: u32, rn: u32, imm: u32) -> Result<Vec<u8>> {
8158        // AND.W Rd, Rn, #<modified_immediate>
8159        // F0 00 Rn | 0 imm3 Rd imm8
8160        //
8161        // #681 class audit: the field is a ThumbExpandImm modified immediate,
8162        // not a raw value. The only current caller (POPCNT final mask) passes
8163        // 0x3F, which expands to itself — the gate is byte-identical today and
8164        // closes the raw-packing landmine for any future caller.
8165        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
8166            synth_core::Error::synthesis(
8167                "AND immediate is not a valid ThumbExpandImm — materialize into a register",
8168            )
8169        })?;
8170        let i_bit = (field >> 11) & 1;
8171        let imm3 = (field >> 8) & 0x7;
8172        let imm8 = field & 0xFF;
8173
8174        let hw1: u16 = (0xF000 | (i_bit << 10) | rn) as u16;
8175        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8176
8177        let mut bytes = hw1.to_le_bytes().to_vec();
8178        bytes.extend_from_slice(&hw2.to_le_bytes());
8179        Ok(bytes)
8180    }
8181
8182    /// Encode Thumb-2 32-bit SUB (register) - raw version
8183    fn encode_thumb32_sub_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8184        // SUB.W Rd, Rn, Rm
8185        // EBA0 Rn | 0 Rd 00 00 Rm
8186        let hw1: u16 = (0xEBA0 | rn) as u16;
8187        let hw2: u16 = ((rd << 8) | rm) as u16;
8188
8189        let mut bytes = hw1.to_le_bytes().to_vec();
8190        bytes.extend_from_slice(&hw2.to_le_bytes());
8191        Ok(bytes)
8192    }
8193
8194    /// Encode Thumb-2 32-bit ADD (register) - raw version
8195    fn encode_thumb32_add_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8196        // ADD.W Rd, Rn, Rm
8197        // EB00 Rn | 0 Rd 00 00 Rm
8198        let hw1: u16 = (0xEB00 | rn) as u16;
8199        let hw2: u16 = ((rd << 8) | rm) as u16;
8200
8201        let mut bytes = hw1.to_le_bytes().to_vec();
8202        bytes.extend_from_slice(&hw2.to_le_bytes());
8203        Ok(bytes)
8204    }
8205
8206    /// Encode Thumb-2 32-bit ADDS (register, flag-setting) - raw version.
8207    /// Used as the high-register fallback for `ArmOp::Adds` (i64 low-word add)
8208    /// so R8-R11 pair operands don't overflow the 16-bit field — #178/#180.
8209    fn encode_thumb32_adds_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8210        // ADDS.W Rd, Rn, Rm (T3, S=1): EB10 Rn | 0 Rd 00 00 Rm
8211        let hw1: u16 = (0xEB10 | rn) as u16;
8212        let hw2: u16 = ((rd << 8) | rm) as u16;
8213        let mut bytes = hw1.to_le_bytes().to_vec();
8214        bytes.extend_from_slice(&hw2.to_le_bytes());
8215        Ok(bytes)
8216    }
8217
8218    /// Encode Thumb-2 32-bit SUBS (register, flag-setting) - raw version.
8219    /// High-register fallback for `ArmOp::Subs` (i64 low-word subtract) — #178/#180.
8220    fn encode_thumb32_subs_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8221        // SUBS.W Rd, Rn, Rm (T3, S=1): EBB0 Rn | 0 Rd 00 00 Rm
8222        let hw1: u16 = (0xEBB0 | rn) as u16;
8223        let hw2: u16 = ((rd << 8) | rm) as u16;
8224        let mut bytes = hw1.to_le_bytes().to_vec();
8225        bytes.extend_from_slice(&hw2.to_le_bytes());
8226        Ok(bytes)
8227    }
8228
8229    /// Encode a sequence of ARM instructions
8230    pub fn encode_sequence(&self, ops: &[ArmOp]) -> Result<Vec<u8>> {
8231        let mut code = Vec::new();
8232
8233        for op in ops {
8234            let encoded = self.encode(op)?;
8235            code.extend_from_slice(&encoded);
8236        }
8237
8238        Ok(code)
8239    }
8240}
8241
8242/// Convert register to bit encoding (0-15)
8243/// Reverse of the ARMv7-M `ThumbExpandImm`: given a 32-bit immediate, return the
8244/// 12-bit `i:imm3:imm8` field if it is a representable modified immediate, else
8245/// `None` (the caller must materialize the value into a register). This is the
8246/// shared correct path for the data-processing immediate encoders — without it
8247/// they pack raw bits and silently mis-encode any value `> 0xFF` that isn't a
8248/// modified immediate (the silent-miscompile class behind #251/#253/#255).
8249fn try_thumb_expand_imm(value: u32) -> Option<u32> {
8250    // i:imm3 = 0000 → 8-bit value, zero-extended (00000000 00000000 00000000 XY).
8251    if value <= 0xFF {
8252        return Some(value);
8253    }
8254    let b0 = value & 0xFF; // byte 0
8255    let b1 = (value >> 8) & 0xFF; // byte 1
8256    // 0x00XY00XY (i:imm3 = 0001) — XY in bytes 0 and 2
8257    if value == (b0 << 16) | b0 {
8258        return Some(0x100 | b0);
8259    }
8260    // 0xXY00XY00 (i:imm3 = 0010) — XY in bytes 1 and 3
8261    if value == (b1 << 24) | (b1 << 8) {
8262        return Some(0x200 | b1);
8263    }
8264    // 0xXYXYXYXY (i:imm3 = 0011) — XY in all four bytes
8265    if value == (b0 << 24) | (b0 << 16) | (b0 << 8) | b0 {
8266        return Some(0x300 | b0);
8267    }
8268    // An 8-bit value with bit 7 set, rotated right by 8..=31. `rotate_left(rot)`
8269    // undoes the encoded right rotation; if the result is `1bbbbbbb` (0x80..=0xFF)
8270    // the value is representable. imm12[11:7] = rot, imm12[6:0] = low 7 bits.
8271    for rot in 8..=31u32 {
8272        let unrot = value.rotate_left(rot);
8273        if (0x80..=0xFF).contains(&unrot) {
8274            return Some((rot << 7) | (unrot & 0x7F));
8275        }
8276    }
8277    None
8278}
8279
8280/// Guard a Thumb-2 `LDR/STR Rd, [Rn, #imm12]` offset. The imm12 form supports
8281/// `0..=4095`; a larger offset must be materialized into a register by the
8282/// selector (register-offset addressing). Returning `Err` rather than silently
8283/// masking `offset & 0xFFF` closes the wrong-address miscompile class (#259,
8284/// the load/store sibling of #253/#255).
8285fn check_ldst_imm12(offset: u32) -> Result<()> {
8286    if offset > 0xFFF {
8287        Err(synth_core::Error::synthesis(
8288            "load/store immediate offset > 0xFFF (4095) — materialize the offset into a register",
8289        ))
8290    } else {
8291        Ok(())
8292    }
8293}
8294
8295/// #916 — emit `Rd = 0` in Thumb-2, correctly for EVERY destination register.
8296///
8297/// The 16-bit `MOVS Rd, #imm8` (T1) is `0010 0 Rd(3) imm8` — the Rd field is
8298/// **three bits**. For R8-R12 `reg_to_bits` yields 8..12, so `rd_bits << 8`
8299/// overflows into bit 11 and `0x2000 | 0x0800` is `0x2800` = `CMP r0, #0`:
8300/// not a move at all. The destination is never written (it keeps stale data)
8301/// and the flags are clobbered. Same class as #180 / H-CODE-9, and the same
8302/// defect #311 fixed for `I64SetCond`.
8303///
8304/// High registers therefore take the 32-bit `MOV.W Rd, #imm8` (T2,
8305/// `F04F 0000 | Rd<<8 | imm8`), whose Rd field is four bits. `MOV.W` with S=0
8306/// does not set flags, which is what these zero-fill sites want anyway.
8307///
8308/// **Callers with branches must consult [`thumb_zero_fill_halfwords`].** This
8309/// emits 1 halfword for R0-R7 and 2 for R8-R12; any branch whose target lies
8310/// PAST this instruction moves when it widens and its displacement has to be
8311/// derived rather than hard-coded. (A branch targeting this instruction's own
8312/// address is unaffected — an instruction cannot move itself.)
8313fn emit_thumb_zero_fill(bytes: &mut Vec<u8>, rd_bits: u32) {
8314    if rd_bits < 8 {
8315        let movs: u16 = 0x2000 | ((rd_bits as u16) << 8);
8316        bytes.extend_from_slice(&movs.to_le_bytes());
8317    } else {
8318        bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
8319        bytes.extend_from_slice(&((rd_bits as u16) << 8).to_le_bytes());
8320    }
8321}
8322
8323/// Halfword length of the encoding [`emit_thumb_zero_fill`] picks for
8324/// `rd_bits`. Branch displacements spanning the zero-fill derive from this so
8325/// the encoder cannot drift from itself (#916; the byte-size estimator mirrors
8326/// it in `synth_synthesis::estimate_arm_byte_size`, pinned by the #498
8327/// `estimator_encoder_agreement` oracle).
8328fn thumb_zero_fill_halfwords(rd_bits: u32) -> u16 {
8329    if rd_bits < 8 { 1 } else { 2 }
8330}
8331
8332fn reg_to_bits(reg: &Reg) -> u32 {
8333    match reg {
8334        Reg::R0 => 0,
8335        Reg::R1 => 1,
8336        Reg::R2 => 2,
8337        Reg::R3 => 3,
8338        Reg::R4 => 4,
8339        Reg::R5 => 5,
8340        Reg::R6 => 6,
8341        Reg::R7 => 7,
8342        Reg::R8 => 8,
8343        Reg::R9 => 9,
8344        Reg::R10 => 10,
8345        Reg::R11 => 11,
8346        Reg::R12 => 12,
8347        Reg::SP => 13,
8348        Reg::LR => 14,
8349        Reg::PC => 15,
8350    }
8351}
8352
8353// ======================================================================
8354// #610 — i64 fixed-ABI expansion wrappers.
8355//
8356// The hand-written multi-instruction i64 cores (rotl/rotr and the div/rem
8357// shift-subtract loops) compute in FIXED low registers. Before #610 the
8358// div/rem arms ignored their operand fields outright (hardcoded R0:R1 /
8359// R2:R3 in, result to R0:R1) and the rot arms used R3/R4 scratch that
8360// collided with selector-assigned registers — then restored the saved
8361// scratch OVER the result (`POP {R4}` with rd_lo == R4), so the op
8362// returned the caller's stale register: 0 for every input under qemu.
8363//
8364// These wrappers make each core honor its register parameters:
8365//   1. save R0-R3,
8366//   2. marshal the operand registers into the core's fixed input regs via
8367//      the stack (permutation-safe: every source is read before any fixed
8368//      register is written),
8369//   3. run the fixed-reg core (self-preserving for R4+; R12 is encoder
8370//      scratch and never allocatable, #212),
8371//   4. MOV the result pair from R0:R1 into the selector's rd pair,
8372//   5. restore R0-R3, skipping any register the result now occupies.
8373//
8374// All emitted lengths are register-independent so the optimized path's
8375// byte-size estimator (`estimate_arm_byte_size`, pinned by the
8376// estimator↔encoder agreement oracle #498/#511) stays a constant per op.
8377// ======================================================================
8378
8379/// Steps 1+2: `PUSH {R0-R3}`, then marshal `srcs` (operand registers, any of
8380/// R0-R12) into `R0..R<n>` via individual stack pushes. Sources are all read
8381/// before any destination register is written, so arbitrary source/target
8382/// permutations (including operands living in R0-R3) are safe.
8383fn emit_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8384    debug_assert!(srcs.len() <= 4);
8385    // PUSH {R0-R3} — save the caller-visible low registers.
8386    bytes.extend_from_slice(&0xB40Fu16.to_le_bytes());
8387    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8388    for src in srcs.iter().rev() {
8389        let rt = reg_to_bits(src) as u16;
8390        bytes.extend_from_slice(&0xF84Du16.to_le_bytes());
8391        bytes.extend_from_slice(&((rt << 12) | 0x0D04).to_le_bytes());
8392    }
8393    // POP {Ri} — Ri := srcs[i].
8394    for i in 0..srcs.len() as u16 {
8395        bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes());
8396    }
8397}
8398
8399/// Steps 4+5: move the core's R0:R1 result into the selector's rd pair, then
8400/// restore the R0-R3 saved by [`emit_i64_fixed_abi_entry`], skipping any
8401/// register the result now lives in (its saved caller word is discarded).
8402fn emit_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8403    let lo = reg_to_bits(rdlo);
8404    let hi = reg_to_bits(rdhi);
8405    if lo == 1 && hi == 0 {
8406        // A fully swapped pair would clobber one half in either MOV order.
8407        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8408        return Err(synth_core::Error::synthesis(
8409            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8410        ));
8411    }
8412    let mov16 = |bytes: &mut Vec<u8>, rd: u32, rm: u32| {
8413        let d = ((rd >> 3) & 1) as u16;
8414        bytes.extend_from_slice(
8415            &(0x4600u16 | (d << 7) | ((rm as u16) << 3) | ((rd & 7) as u16)).to_le_bytes(),
8416        );
8417    };
8418    if hi == 0 {
8419        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8420        mov16(bytes, lo, 0);
8421        mov16(bytes, hi, 1);
8422    } else {
8423        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8424        mov16(bytes, hi, 1);
8425        mov16(bytes, lo, 0);
8426    }
8427    for i in 0..4u32 {
8428        if i == lo || i == hi {
8429            // The result lives here — drop the saved caller word.
8430            bytes.extend_from_slice(&0xB001u16.to_le_bytes()); // ADD SP, #4
8431        } else {
8432            bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes()); // POP {Ri}
8433        }
8434    }
8435    Ok(())
8436}
8437
8438/// WASM `i64.div_*` / `i64.rem_*` by zero must trap, matching the i32 path's
8439/// cmp/bne/udf guard. Emitted after marshaling, when the divisor pair is in
8440/// R2:R3: `ORRS R12, R2, R3` — `BNE` over a `UDF #0` when nonzero.
8441fn emit_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8442    bytes.extend_from_slice(&0xEA52u16.to_le_bytes()); // ORRS.W R12, R2, R3
8443    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8444    bytes.extend_from_slice(&0xD100u16.to_le_bytes()); // BNE.N +0 (skip the UDF)
8445    bytes.extend_from_slice(&0xDE00u16.to_le_bytes()); // UDF #0 — divide by zero
8446}
8447
8448/// WASM `i64.div_s(INT64_MIN, -1)` must trap (Core §4.3.2 `idiv_s`: the
8449/// quotient +2^63 is unrepresentable), matching the i32 path's overflow
8450/// guard — #633: without it the core negated INT64_MIN onto itself and
8451/// silently returned INT64_MIN. Emitted after marshaling, when the dividend
8452/// pair is in R0:R1 and the divisor pair in R2:R3; R12 is encoder scratch.
8453///
8454/// div_s ONLY — `i64.rem_s(INT64_MIN, -1)` is defined as 0 and must NOT
8455/// trap (`irem_s`), so the I64RemS arm never calls this. 22 bytes,
8456/// register-independent (estimator contract, #498/#511).
8457fn emit_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8458    // AND.W R12, R2, R3 — R12 == 0xFFFFFFFF iff divisor == -1
8459    bytes.extend_from_slice(&0xEA02u16.to_le_bytes());
8460    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8461    // CMN.W R12, #1 — EQ iff both divisor words are all-ones
8462    bytes.extend_from_slice(&0xF11Cu16.to_le_bytes());
8463    bytes.extend_from_slice(&0x0F01u16.to_le_bytes());
8464    // BNE .no_trap
8465    bytes.extend_from_slice(&0xD105u16.to_le_bytes());
8466    // CMP R0, #0 — dividend lo word of INT64_MIN
8467    bytes.extend_from_slice(&0x2800u16.to_le_bytes());
8468    // BNE .no_trap
8469    bytes.extend_from_slice(&0xD103u16.to_le_bytes());
8470    // CMP.W R1, #0x80000000 — dividend hi word of INT64_MIN
8471    bytes.extend_from_slice(&0xF1B1u16.to_le_bytes());
8472    bytes.extend_from_slice(&0x4F00u16.to_le_bytes());
8473    // BNE .no_trap
8474    bytes.extend_from_slice(&0xD100u16.to_le_bytes());
8475    // UDF #0 — signed-division overflow
8476    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
8477    // .no_trap:
8478}
8479
8480// ======================================================================
8481// #615 — A32 (ARM-mode) twins of the #610 i64 fixed-ABI wrappers above.
8482// Identical register contract, A32 encodings: the multi-instruction i64
8483// cores (rotl/rotr, div/rem) compute in fixed low registers (value/dividend
8484// R0:R1, amount R2 / divisor R2:R3, result to R0:R1); the wrappers marshal
8485// the selector-assigned operand registers in and the result out, saving and
8486// restoring the caller-visible R0-R3 around the core.
8487// ======================================================================
8488
8489/// A32 steps 1+2: `STMDB SP!, {R0-R3}`, then marshal `srcs` into `R0..R<n>`
8490/// via individual stack pushes (`STR src, [SP, #-4]!` in reverse order, then
8491/// `LDR Ri, [SP], #4`). Every source is read before any fixed register is
8492/// written, so arbitrary source/target permutations are safe.
8493fn emit_a32_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8494    debug_assert!(srcs.len() <= 4);
8495    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8496    // PUSH {R0-R3} — save the caller-visible low registers.
8497    w(bytes, 0xE92D_000F);
8498    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8499    for src in srcs.iter().rev() {
8500        w(bytes, 0xE52D_0004 | (reg_to_bits(src) << 12));
8501    }
8502    // LDR Ri, [SP], #4 — Ri := srcs[i].
8503    for i in 0..srcs.len() as u32 {
8504        w(bytes, 0xE49D_0004 | (i << 12));
8505    }
8506}
8507
8508/// A32 steps 4+5: move the core's R0:R1 result into the selector's rd pair,
8509/// then restore the R0-R3 saved by [`emit_a32_i64_fixed_abi_entry`], skipping
8510/// any register the result now lives in (its saved caller word is discarded).
8511fn emit_a32_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8512    let lo = reg_to_bits(rdlo);
8513    let hi = reg_to_bits(rdhi);
8514    if lo == 1 && hi == 0 {
8515        // A fully swapped pair would clobber one half in either MOV order.
8516        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8517        return Err(synth_core::Error::synthesis(
8518            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8519        ));
8520    }
8521    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8522    let mov = |bytes: &mut Vec<u8>, rd: u32, rm: u32| w(bytes, 0xE1A0_0000 | (rd << 12) | rm);
8523    if hi == 0 {
8524        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8525        mov(bytes, lo, 0);
8526        mov(bytes, hi, 1);
8527    } else {
8528        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8529        mov(bytes, hi, 1);
8530        mov(bytes, lo, 0);
8531    }
8532    for i in 0..4u32 {
8533        if i == lo || i == hi {
8534            // The result lives here — drop the saved caller word.
8535            w(bytes, 0xE28D_D004); // ADD SP, SP, #4
8536        } else {
8537            w(bytes, 0xE49D_0004 | (i << 12)); // LDR Ri, [SP], #4
8538        }
8539    }
8540    Ok(())
8541}
8542
8543/// A32 zero-divisor trap, emitted after marshaling when the divisor pair is
8544/// in R2:R3: `ORRS R12, R2, R3` sets Z iff the divisor is zero; `BNE` skips a
8545/// `UDF #0` (WASM div/rem-by-zero must trap, matching the Thumb-2 twin).
8546fn emit_a32_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8547    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8548    w(bytes, 0xE192_C003); // ORRS R12, R2, R3
8549    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8550    w(bytes, 0xE7F0_00F0); // UDF #0 — divide by zero
8551}
8552
8553/// A32 twin of [`emit_i64_divs_overflow_trap`] (#633): trap on
8554/// `i64.div_s(INT64_MIN, -1)`. Conditional execution replaces the Thumb
8555/// branches — the CMPEQ chain leaves EQ set only when divisor == -1 AND
8556/// dividend == INT64_MIN. div_s only; rem_s must keep returning 0.
8557fn emit_a32_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8558    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8559    w(bytes, 0xE002_C003); // AND   R12, R2, R3 (== 0xFFFFFFFF iff divisor == -1)
8560    w(bytes, 0xE37C_0001); // CMN   R12, #1     (EQ iff divisor == -1)
8561    w(bytes, 0x0350_0000); // CMPEQ R0, #0      (EQ iff also dividend lo == 0)
8562    w(bytes, 0x0351_0102); // CMPEQ R1, #0x80000000 (EQ iff dividend == INT64_MIN)
8563    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8564    w(bytes, 0xE7F0_00F0); // UDF #0 — signed-division overflow
8565}
8566
8567/// Fallible form of the `verify_reg_bits` contract. PC (R15) is not a valid
8568/// data operand for the Thumb-2 encodings that use this guard (SDIV/UDIV/MLS/…
8569/// are UNPREDICTABLE with PC). Synth's own codegen never emits PC there, but
8570/// the encoder must stay *total* over arbitrary `ArmOp` inputs — the fuzz
8571/// harness (`encoder_no_panic`) requires Ok-or-Err, never a panic. Pre-fix, the
8572/// `debug_assert` in `verify_reg_bits` aborted under `-Cdebug-assertions`.
8573/// Returns a typed Err instead. See #185.
8574fn reg_bits_checked(bits: u32) -> Result<()> {
8575    if bits > 14 {
8576        return Err(synth_core::Error::synthesis(format!(
8577            "register bits {bits} (PC/R15) is not a valid operand for this Thumb-2 encoding"
8578        )));
8579    }
8580    Ok(())
8581}
8582
8583/// Try to encode a 32-bit value as an ARM rotated immediate (imm8 ROR 2*rot4).
8584/// Returns Some((encoded_bits, 1)) if representable, None otherwise.
8585fn try_encode_rotated_imm(val: u32) -> Option<(u32, u32)> {
8586    if val == 0 {
8587        return Some((0, 1));
8588    }
8589    for rot in 0..16u32 {
8590        let shift = rot * 2;
8591        // Rotate left by shift (undo the ROR) to see if result fits in 8 bits
8592        let unrotated = val.rotate_left(shift);
8593        if unrotated <= 0xFF {
8594            // Encoded as: rot4(4 bits) | imm8(8 bits) = rotate_imm << 8 | imm8
8595            return Some(((rot << 8) | unrotated, 1));
8596        }
8597    }
8598    None
8599}
8600
8601/// Encode operand2 field and return (bits, immediate_flag).
8602/// For ARM32 mode, immediates use the rotated-immediate encoding (imm8 ROR 2*rot4).
8603/// Panics if an immediate value cannot be represented. Callers that need large
8604/// immediates should use MOVW/MOVT instead of Operand2::Imm.
8605fn encode_operand2(op2: &Operand2) -> Result<(u32, u32)> {
8606    match op2 {
8607        Operand2::Imm(val) => {
8608            let uval = *val as u32;
8609            // Attempt rotated-immediate encoding (ARM32 Operand2)
8610            if let Some(encoded) = try_encode_rotated_imm(uval) {
8611                Ok(encoded)
8612            } else {
8613                // #378-class honesty: an immediate that can't be expressed as an
8614                // ARM32 rotated immediate is an INTERNAL selector bug — large
8615                // constants must be materialized via MOVW/MOVT, not passed here.
8616                // FAIL HONESTLY with an Err rather than silently masking to
8617                // `uval & 0xFF` and emitting a WRONG immediate. The encoder is
8618                // Ok-or-Err, never corrupt (#180/#185); a loud Err is also why
8619                // this is an Err and not a panic (the `encoder_no_panic` fuzz
8620                // contract — malformed/oversized input must degrade, not crash).
8621                Err(synth_core::Error::synthesis(format!(
8622                    "encode_operand2: immediate {uval:#x} ({val}) is not an ARM32 \
8623                     rotated immediate — the selector must materialize large \
8624                     constants via MOVW/MOVT"
8625                )))
8626            }
8627        }
8628
8629        Operand2::Reg(reg) => {
8630            let reg_bits = reg_to_bits(reg);
8631            Ok((reg_bits, 0)) // I=0 for register
8632        }
8633
8634        Operand2::RegShift {
8635            rm,
8636            shift: _,
8637            amount,
8638        } => {
8639            // Simplified encoding with shift
8640            let rm_bits = reg_to_bits(rm);
8641            let shift_bits = (*amount & 0x1F) << 7;
8642            Ok((shift_bits | rm_bits, 0))
8643        }
8644    }
8645}
8646
8647/// Encode memory address to (base_reg, offset)
8648fn encode_mem_addr(addr: &MemAddr) -> (u32, u32) {
8649    let base_bits = reg_to_bits(&addr.base);
8650    let offset_bits = (addr.offset as u32) & 0xFFF; // 12-bit offset
8651    (base_bits, offset_bits)
8652}
8653
8654/// S-register number: S0=0, S1=1, ..., S31=31
8655fn vfp_sreg_to_num(reg: &VfpReg) -> Result<u32> {
8656    match reg {
8657        VfpReg::S0 => Ok(0),
8658        VfpReg::S1 => Ok(1),
8659        VfpReg::S2 => Ok(2),
8660        VfpReg::S3 => Ok(3),
8661        VfpReg::S4 => Ok(4),
8662        VfpReg::S5 => Ok(5),
8663        VfpReg::S6 => Ok(6),
8664        VfpReg::S7 => Ok(7),
8665        VfpReg::S8 => Ok(8),
8666        VfpReg::S9 => Ok(9),
8667        VfpReg::S10 => Ok(10),
8668        VfpReg::S11 => Ok(11),
8669        VfpReg::S12 => Ok(12),
8670        VfpReg::S13 => Ok(13),
8671        VfpReg::S14 => Ok(14),
8672        VfpReg::S15 => Ok(15),
8673        VfpReg::S16 => Ok(16),
8674        VfpReg::S17 => Ok(17),
8675        VfpReg::S18 => Ok(18),
8676        VfpReg::S19 => Ok(19),
8677        VfpReg::S20 => Ok(20),
8678        VfpReg::S21 => Ok(21),
8679        VfpReg::S22 => Ok(22),
8680        VfpReg::S23 => Ok(23),
8681        VfpReg::S24 => Ok(24),
8682        VfpReg::S25 => Ok(25),
8683        VfpReg::S26 => Ok(26),
8684        VfpReg::S27 => Ok(27),
8685        VfpReg::S28 => Ok(28),
8686        VfpReg::S29 => Ok(29),
8687        VfpReg::S30 => Ok(30),
8688        VfpReg::S31 => Ok(31),
8689        // D-registers are not used in F32 single-precision encodings
8690        _ => Err(synth_core::Error::SynthesisError(
8691            "D-register not supported in single-precision VFP encoding".to_string(),
8692        )),
8693    }
8694}
8695
8696/// D-register number: D0=0, D1=1, ..., D15=15
8697fn vfp_dreg_to_num(reg: &VfpReg) -> Result<u32> {
8698    match reg {
8699        VfpReg::D0 => Ok(0),
8700        VfpReg::D1 => Ok(1),
8701        VfpReg::D2 => Ok(2),
8702        VfpReg::D3 => Ok(3),
8703        VfpReg::D4 => Ok(4),
8704        VfpReg::D5 => Ok(5),
8705        VfpReg::D6 => Ok(6),
8706        VfpReg::D7 => Ok(7),
8707        VfpReg::D8 => Ok(8),
8708        VfpReg::D9 => Ok(9),
8709        VfpReg::D10 => Ok(10),
8710        VfpReg::D11 => Ok(11),
8711        VfpReg::D12 => Ok(12),
8712        VfpReg::D13 => Ok(13),
8713        VfpReg::D14 => Ok(14),
8714        VfpReg::D15 => Ok(15),
8715        // S-registers are not used in F64 double-precision encodings
8716        _ => Err(synth_core::Error::SynthesisError(
8717            "S-register not supported in double-precision VFP encoding".to_string(),
8718        )),
8719    }
8720}
8721
8722/// Split S-register into (Vx[3:0], qualifier_bit) for VFP encoding.
8723/// For an S-register number s: Vx = s >> 1, qualifier = s & 1.
8724/// The qualifier bit goes to D (bit 22), N (bit 7), or M (bit 5) depending on role.
8725fn encode_sreg(s: u32) -> (u32, u32) {
8726    (s >> 1, s & 1)
8727}
8728
8729/// Split D-register into (Vx[3:0], qualifier_bit) for VFP double-precision encoding.
8730/// For a D-register number d: Vx = d & 0xF, qualifier = (d >> 4) & 1.
8731/// For D0-D15, qualifier is always 0.
8732fn encode_dreg(d: u32) -> (u32, u32) {
8733    (d & 0xF, (d >> 4) & 1)
8734}
8735
8736/// Encode a VFP 3-register arithmetic instruction (VADD.F32, VSUB.F32, VMUL.F32, VDIV.F32).
8737/// Returns the full 32-bit instruction word.
8738///
8739/// VFP encoding: [cond 1110] [D opc1 Vn] [Vd 101 sz] [N opc2 M 0 Vm]
8740/// For single-precision (sz=0), coprocessor = 0xA (bits[11:8]).
8741fn encode_vfp_3reg(base: u32, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<u32> {
8742    let sd_num = vfp_sreg_to_num(sd)?;
8743    let sn_num = vfp_sreg_to_num(sn)?;
8744    let sm_num = vfp_sreg_to_num(sm)?;
8745    let (vd, d) = encode_sreg(sd_num);
8746    let (vn, n) = encode_sreg(sn_num);
8747    let (vm, m) = encode_sreg(sm_num);
8748
8749    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8750}
8751
8752/// Encode a VFP 2-register instruction (VNEG.F32, VABS.F32, VSQRT.F32).
8753/// Returns the full 32-bit instruction word.
8754fn encode_vfp_2reg(base: u32, sd: &VfpReg, sm: &VfpReg) -> Result<u32> {
8755    let sd_num = vfp_sreg_to_num(sd)?;
8756    let sm_num = vfp_sreg_to_num(sm)?;
8757    let (vd, d) = encode_sreg(sd_num);
8758    let (vm, m) = encode_sreg(sm_num);
8759
8760    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8761}
8762
8763/// Encode a VFP load/store (VLDR.F32 / VSTR.F32).
8764/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8765/// U bit (bit 23) controls add/subtract offset.
8766fn encode_vfp_ldst(base: u32, sd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8767    let sd_num = vfp_sreg_to_num(sd)?;
8768    let (vd, d) = encode_sreg(sd_num);
8769    let rn = reg_to_bits(&addr.base);
8770
8771    let offset = addr.offset;
8772    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8773    let abs_offset = offset.unsigned_abs();
8774    let imm8 = (abs_offset / 4) & 0xFF;
8775
8776    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8777}
8778
8779/// Encode VMOV between core register and S-register.
8780/// VMOV Sn, Rt: 0xEE00_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8781/// VMOV Rt, Sn: 0xEE10_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8782fn encode_vmov_core_sreg(to_sreg: bool, sreg: &VfpReg, core: &Reg) -> Result<u32> {
8783    let s_num = vfp_sreg_to_num(sreg)?;
8784    let (vn, n) = encode_sreg(s_num);
8785    let rt = reg_to_bits(core);
8786
8787    let base = if to_sreg { 0xEE000A10 } else { 0xEE100A10 };
8788    Ok(base | (vn << 16) | (rt << 12) | (n << 7))
8789}
8790
8791/// Encode a VFP 3-register double-precision instruction (VADD.F64, VSUB.F64, etc.).
8792/// For double-precision (sz=1), coprocessor = 0xB (bits[11:8]).
8793/// The base should have bit 8 = 1 for F64 (0xB suffix instead of 0xA).
8794fn encode_vfp_3reg_f64(base: u32, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<u32> {
8795    let dd_num = vfp_dreg_to_num(dd)?;
8796    let dn_num = vfp_dreg_to_num(dn)?;
8797    let dm_num = vfp_dreg_to_num(dm)?;
8798    let (vd, d) = encode_dreg(dd_num);
8799    let (vn, n) = encode_dreg(dn_num);
8800    let (vm, m) = encode_dreg(dm_num);
8801
8802    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8803}
8804
8805/// Encode a VFP 2-register double-precision instruction (VNEG.F64, VABS.F64, VSQRT.F64).
8806fn encode_vfp_2reg_f64(base: u32, dd: &VfpReg, dm: &VfpReg) -> Result<u32> {
8807    let dd_num = vfp_dreg_to_num(dd)?;
8808    let dm_num = vfp_dreg_to_num(dm)?;
8809    let (vd, d) = encode_dreg(dd_num);
8810    let (vm, m) = encode_dreg(dm_num);
8811
8812    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8813}
8814
8815/// Encode a VFP load/store for double-precision (VLDR.64 / VSTR.64).
8816/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8817fn encode_vfp_ldst_f64(base: u32, dd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8818    let dd_num = vfp_dreg_to_num(dd)?;
8819    let (vd, d) = encode_dreg(dd_num);
8820    let rn = reg_to_bits(&addr.base);
8821
8822    let offset = addr.offset;
8823    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8824    let abs_offset = offset.unsigned_abs();
8825    let imm8 = (abs_offset / 4) & 0xFF;
8826
8827    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8828}
8829
8830/// Encode VMOV between two core registers and a D-register.
8831/// VMOV Dm, Rt, Rt2: 0xEC40_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8832/// VMOV Rt, Rt2, Dm: 0xEC50_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8833fn encode_vmov_core_dreg(
8834    to_dreg: bool,
8835    dreg: &VfpReg,
8836    core_lo: &Reg,
8837    core_hi: &Reg,
8838) -> Result<u32> {
8839    let d_num = vfp_dreg_to_num(dreg)?;
8840    let (vm, m) = encode_dreg(d_num);
8841    let rt = reg_to_bits(core_lo);
8842    let rt2 = reg_to_bits(core_hi);
8843
8844    let base = if to_dreg { 0xEC400B10 } else { 0xEC500B10 };
8845    Ok(base | (rt2 << 16) | (rt << 12) | (m << 5) | vm)
8846}
8847
8848/// Emit a VFP 32-bit instruction as Thumb-2 bytes (two LE halfwords).
8849fn vfp_to_thumb_bytes(instr: u32) -> Vec<u8> {
8850    let hw1 = ((instr >> 16) & 0xFFFF) as u16;
8851    let hw2 = (instr & 0xFFFF) as u16;
8852    let mut bytes = hw1.to_le_bytes().to_vec();
8853    bytes.extend_from_slice(&hw2.to_le_bytes());
8854    bytes
8855}
8856
8857// ============================================================================
8858// Helium MVE encoding helpers
8859// ============================================================================
8860
8861/// Q-register number: Q0=0, Q1=1, ..., Q7=7
8862fn qreg_to_num(reg: &QReg) -> u32 {
8863    match reg {
8864        QReg::Q0 => 0,
8865        QReg::Q1 => 1,
8866        QReg::Q2 => 2,
8867        QReg::Q3 => 3,
8868        QReg::Q4 => 4,
8869        QReg::Q5 => 5,
8870        QReg::Q6 => 6,
8871        QReg::Q7 => 7,
8872    }
8873}
8874
8875/// MVE element size to encoding bits: S8=0b00, S16=0b01, S32=0b10
8876fn mve_size_bits(size: &MveSize) -> u32 {
8877    match size {
8878        MveSize::S8 => 0b00,
8879        MveSize::S16 => 0b01,
8880        MveSize::S32 => 0b10,
8881    }
8882}
8883
8884/// Encode MVE 3-register instruction.
8885/// Q-registers are encoded as D-register pairs: Q0=D0:D1, Q1=D2:D3, etc.
8886/// In NEON/MVE encoding, the Q-register uses D-register number = Qn * 2.
8887fn encode_mve_3reg(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
8888    let d = qreg_to_num(qd) * 2;
8889    let n = qreg_to_num(qn) * 2;
8890    let m = qreg_to_num(qm) * 2;
8891
8892    // Standard NEON/MVE 3-register encoding:
8893    // D bit (bit 22) = Vd[4], Vd[3:0] = bits [15:12]
8894    // N bit (bit 7)  = Vn[4], Vn[3:0] = bits [19:16]
8895    // M bit (bit 5)  = Vm[4], Vm[3:0] = bits [3:0]
8896    let vd = d & 0xF;
8897    let d_bit = (d >> 4) & 1;
8898    let vn = n & 0xF;
8899    let n_bit = (n >> 4) & 1;
8900    let vm = m & 0xF;
8901    let m_bit = (m >> 4) & 1;
8902
8903    base | (d_bit << 22) | (vn << 16) | (vd << 12) | (n_bit << 7) | (m_bit << 5) | vm
8904}
8905
8906/// Encode MVE 3-register bitwise instruction (VAND, VORR, VEOR, VBIC).
8907fn encode_mve_3reg_bitwise(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
8908    encode_mve_3reg(base, qd, qn, qm)
8909}
8910
8911/// Encode MVE VLDRW.32 Qd, [Rn, #offset]
8912/// Format: EC9x xxxx - contiguous load, word-sized elements
8913fn encode_mve_vldrw(qd: &QReg, addr: &MemAddr) -> u32 {
8914    let qd_enc = qreg_to_num(qd) * 2;
8915    let rn = reg_to_bits(&addr.base);
8916    let offset = addr.offset;
8917    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8918    let abs_offset = offset.unsigned_abs();
8919    let imm7 = (abs_offset / 4) & 0x7F; // 7-bit word-aligned offset
8920
8921    // VLDRW.32 Qd, [Rn, #imm]: ED10 xx80 variant
8922    0xED100E80
8923        | (u_bit << 23)
8924        | ((qd_enc >> 4) << 22)
8925        | (rn << 16)
8926        | ((qd_enc & 0xF) << 12)
8927        | (imm7 & 0x7F)
8928}
8929
8930/// Encode MVE VSTRW.32 Qd, [Rn, #offset]
8931fn encode_mve_vstrw(qd: &QReg, addr: &MemAddr) -> u32 {
8932    let qd_enc = qreg_to_num(qd) * 2;
8933    let rn = reg_to_bits(&addr.base);
8934    let offset = addr.offset;
8935    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8936    let abs_offset = offset.unsigned_abs();
8937    let imm7 = (abs_offset / 4) & 0x7F;
8938
8939    0xED000E80
8940        | (u_bit << 23)
8941        | ((qd_enc >> 4) << 22)
8942        | (rn << 16)
8943        | ((qd_enc & 0xF) << 12)
8944        | (imm7 & 0x7F)
8945}
8946
8947impl ArmEncoder {
8948    /// Encode MVE constant load: MOVW+MOVT+VMOV for each 32-bit word, then assemble Q-register
8949    fn encode_thumb_mve_const(&self, qd: &QReg, bytes: &[u8; 16]) -> Result<Vec<u8>> {
8950        let mut result = Vec::new();
8951        let qd_num = qreg_to_num(qd);
8952
8953        // Load each 32-bit word into R12 (temp) then VMOV into S-register
8954        for i in 0..4 {
8955            let word = u32::from_le_bytes([
8956                bytes[i * 4],
8957                bytes[i * 4 + 1],
8958                bytes[i * 4 + 2],
8959                bytes[i * 4 + 3],
8960            ]);
8961            let lo16 = word & 0xFFFF;
8962            let hi16 = (word >> 16) & 0xFFFF;
8963
8964            // MOVW R12, #lo16
8965            result.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
8966            // MOVT R12, #hi16
8967            if hi16 != 0 {
8968                result.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
8969            }
8970
8971            // VMOV Sn, R12 where Sn = Qd*4 + i
8972            let s_num = qd_num * 4 + i as u32;
8973            let (vn, n) = encode_sreg(s_num);
8974            let vmov: u32 = 0xEE000A10 | (vn << 16) | (12 << 12) | (n << 7);
8975            result.extend_from_slice(&vfp_to_thumb_bytes(vmov));
8976        }
8977
8978        Ok(result)
8979    }
8980
8981    /// Encode lane-wise f32 binary operation (VDIV, etc.) via S-register extraction
8982    fn encode_thumb_mve_lane_wise_f32_binop(
8983        &self,
8984        qd: &QReg,
8985        qn: &QReg,
8986        qm: &QReg,
8987        vfp_base: u32,
8988    ) -> Result<Vec<u8>> {
8989        let mut result = Vec::new();
8990        let qd_num = qreg_to_num(qd);
8991        let qn_num = qreg_to_num(qn);
8992        let qm_num = qreg_to_num(qm);
8993
8994        // For each lane 0..3: use S-registers directly (Q aliasing)
8995        for i in 0..4u32 {
8996            let sd = qd_num * 4 + i;
8997            let sn = qn_num * 4 + i;
8998            let sm = qm_num * 4 + i;
8999
9000            let (vd, d) = encode_sreg(sd);
9001            let (vn, n) = encode_sreg(sn);
9002            let (vm, m) = encode_sreg(sm);
9003
9004            let instr = vfp_base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
9005            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9006        }
9007
9008        Ok(result)
9009    }
9010
9011    /// Encode lane-wise f32 VSQRT via S-register extraction
9012    fn encode_thumb_mve_lane_wise_f32_sqrt(&self, qd: &QReg, qm: &QReg) -> Result<Vec<u8>> {
9013        let mut result = Vec::new();
9014        let qd_num = qreg_to_num(qd);
9015        let qm_num = qreg_to_num(qm);
9016
9017        // VSQRT.F32 base: 0xEEB10AC0
9018        for i in 0..4u32 {
9019            let sd = qd_num * 4 + i;
9020            let sm = qm_num * 4 + i;
9021
9022            let (vd, d) = encode_sreg(sd);
9023            let (vm, m) = encode_sreg(sm);
9024
9025            let instr: u32 = 0xEEB10AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
9026            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9027        }
9028
9029        Ok(result)
9030    }
9031}
9032
9033#[cfg(test)]
9034mod tests {
9035    use super::*;
9036
9037    #[test]
9038    fn test_encoder_creation() {
9039        let encoder_arm = ArmEncoder::new_arm32();
9040        assert!(!encoder_arm.thumb_mode);
9041
9042        let encoder_thumb = ArmEncoder::new_thumb2();
9043        assert!(encoder_thumb.thumb_mode);
9044    }
9045
9046    /// #204 WAKE-path regression: `SetCond` materialized 0/1 with the 16-bit
9047    /// `MOVS Rd,#imm` (T1), whose Rd field is 3 bits (R0–R7). For a high Rd
9048    /// (R8–R12) `rd_bits << 8` overflows bit 11, flipping the opcode MOVS→CMP
9049    /// (`0x2c00`), so the boolean was never written — gale's `has_waiter` kept a
9050    /// stale value and the binary-sem WAKE dispatch read garbage. High Rd must
9051    /// use the 32-bit `MOV.W` (T2). Verify the bytes, not the IR.
9052    /// #311: the SAME high-Rd MOVS→CMP transmutation as #204, but in the
9053    /// i64 comparison expansions (I64SetCond / I64SetCondZ) — missed by the
9054    /// #204 hardening. With rd=R8 the boolean died in the flags
9055    /// (`ite eq; cmpeq r0,#1; cmpne r0,#0`), so gale's packed-u64 select
9056    /// read a stale register on silicon. High Rd must take MOV.W / CMP.W.
9057    #[test]
9058    fn test_encode_i64setcond_high_reg_uses_mov_w_311() {
9059        use synth_synthesis::{ArmOp, Condition, Reg};
9060        let enc = ArmEncoder::new_thumb2();
9061        let bytes = enc
9062            .encode(&ArmOp::I64SetCond {
9063                rd: Reg::R8,
9064                rn_lo: Reg::R2,
9065                rn_hi: Reg::R3,
9066                rm_lo: Reg::R6,
9067                rm_hi: Reg::R7,
9068                cond: Condition::EQ,
9069            })
9070            .unwrap();
9071        // The 32-bit MOV.W immediate (T2) first halfword is 0xF04F; the
9072        // 16-bit transmuted forms would contain 0x2801/0x2800 (CMP r0,#1/#0).
9073        let halfwords: Vec<u16> = bytes
9074            .chunks(2)
9075            .map(|c| u16::from_le_bytes([c[0], c[1]]))
9076            .collect();
9077        assert!(
9078            halfwords.iter().filter(|&&h| h == 0xF04F).count() == 2,
9079            "high rd must use two MOV.W (T2) encodings, got {halfwords:04x?}"
9080        );
9081        assert!(
9082            !halfwords.contains(&0x2801) && !halfwords.contains(&0x2800),
9083            "no transmuted 16-bit CMP imm: {halfwords:04x?}"
9084        );
9085
9086        let bytes_z = enc
9087            .encode(&ArmOp::I64SetCondZ {
9088                rd: Reg::R8,
9089                rn_lo: Reg::R2,
9090                rn_hi: Reg::R3,
9091            })
9092            .unwrap();
9093        let hw_z: Vec<u16> = bytes_z
9094            .chunks(2)
9095            .map(|c| u16::from_le_bytes([c[0], c[1]]))
9096            .collect();
9097        assert!(
9098            hw_z.iter().filter(|&&h| h == 0xF04F).count() == 2,
9099            "SetCondZ high rd MOV.W: {hw_z:04x?}"
9100        );
9101        // CMP.W rd,#0 (T2) first halfword: 0xF1B0 | rd
9102        assert!(
9103            hw_z.contains(&(0xF1B0 | 8)),
9104            "SetCondZ high rd must use CMP.W: {hw_z:04x?}"
9105        );
9106    }
9107
9108    #[test]
9109    fn test_encode_setcond_high_reg_uses_mov_w_204() {
9110        use synth_synthesis::{ArmOp, Condition, Reg};
9111        let enc = ArmEncoder::new_thumb2();
9112        // R12 (high): must be ITE + MOV.W #1 + MOV.W #0, never a 16-bit MOVS/CMP.
9113        let hi = enc
9114            .encode(&ArmOp::SetCond {
9115                rd: Reg::R12,
9116                cond: Condition::NE,
9117            })
9118            .unwrap();
9119        assert_eq!(hi.len(), 10, "ITE(2) + MOV.W(4) + MOV.W(4): {hi:02x?}");
9120        // both value halfwords are MOV.W (0xF04F) — NOT the corrupt CMP (0x2c..).
9121        assert_eq!(&hi[2..4], &[0x4F, 0xF0], "then = MOV.W: {hi:02x?}");
9122        assert_eq!(&hi[6..8], &[0x4F, 0xF0], "else = MOV.W: {hi:02x?}");
9123        assert_eq!(hi[4] & 0x0F, 0x01, "then imm = #1");
9124        assert_eq!(hi[8] & 0x0F, 0x00, "else imm = #0");
9125        // Low Rd keeps the compact 16-bit MOVS form.
9126        let lo = enc
9127            .encode(&ArmOp::SetCond {
9128                rd: Reg::R0,
9129                cond: Condition::NE,
9130            })
9131            .unwrap();
9132        assert_eq!(lo.len(), 6, "ITE(2) + MOVS(2) + MOVS(2): {lo:02x?}");
9133        assert_eq!(lo[2..4], [0x01, 0x20], "then = MOVS R0,#1");
9134        assert_eq!(lo[4..6], [0x00, 0x20], "else = MOVS R0,#0");
9135    }
9136
9137    /// #209 Opt 1b: UMULL RdLo, RdHi, Rn, Rm encodes correctly on both ISAs.
9138    /// Thumb-2 T1: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm.
9139    /// A32:        cond 0000 1000 RdHi RdLo Rm 1001 Rn.
9140    #[test]
9141    fn test_encode_umull_209b() {
9142        use synth_synthesis::{ArmOp, Reg};
9143        let op = ArmOp::Umull {
9144            rdlo: Reg::R4,
9145            rdhi: Reg::R5,
9146            rn: Reg::R0,
9147            rm: Reg::R3,
9148        };
9149        // Thumb-2: hw1 = 0xFBA0 | 0 = 0xFBA0; hw2 = (4<<12)|(5<<8)|3 = 0x4503.
9150        let t = ArmEncoder::new_thumb2().encode(&op).unwrap();
9151        assert_eq!(
9152            t,
9153            vec![0xA0, 0xFB, 0x03, 0x45],
9154            "umull r4,r5,r0,r3 (T2): {t:02x?}"
9155        );
9156        // A32: 0xE0800090 | (5<<16) | (4<<12) | (3<<8) | 0 = 0xE0854390.
9157        let a = ArmEncoder::new_arm32().encode(&op).unwrap();
9158        assert_eq!(
9159            a,
9160            0xE085_4390u32.to_le_bytes().to_vec(),
9161            "umull (A32): {a:02x?}"
9162        );
9163    }
9164
9165    /// #206 regression: the ARM32 (A32) `Ldr`/`Str` encoders fed `addr` through
9166    /// `encode_mem_addr`, which returns only the 12-bit immediate — so a register
9167    /// offset (`[rn, rm, #off]`) was silently dropped to `[rn, #off]`, sending
9168    /// the access to the wrong runtime address (silent miscompile on the default
9169    /// `--target arm`). A register offset must materialize `ip = rn + rm` and
9170    /// load from `[ip, #off]`. Verify the bytes.
9171    #[test]
9172    fn test_encode_arm32_indexed_load_keeps_index_206() {
9173        use synth_synthesis::{ArmOp, MemAddr, Reg};
9174        let enc = ArmEncoder::new_arm32();
9175        // ldr r0, [r11, r1, #8]  must NOT collapse to a single immediate ldr.
9176        let bytes = enc
9177            .encode(&ArmOp::Ldr {
9178                rd: Reg::R0,
9179                addr: MemAddr::reg_imm(Reg::R11, Reg::R1, 8),
9180            })
9181            .unwrap();
9182        assert_eq!(
9183            bytes.len(),
9184            8,
9185            "expected ADD ip + LDR (2 words): {bytes:02x?}"
9186        );
9187        let add = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9188        let ldr = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9189        // ADD ip, r11, r1  = 0xE08BC001
9190        assert_eq!(add, 0xE08B_C001, "ADD ip,r11,r1: {add:#010x}");
9191        // LDR r0, [ip, #8] = 0xE59C0008
9192        assert_eq!(ldr, 0xE59C_0008, "LDR r0,[ip,#8]: {ldr:#010x}");
9193        // A bare immediate ldr (the bug) would be 0xE59B0008 (base=r11) — reject.
9194        assert_ne!(ldr, 0xE59B_0008, "index must not be dropped");
9195    }
9196
9197    /// #594 regression: `call_indirect` on the A32 path (`--target cortex-r5`)
9198    /// was encoded as a literal NOP (0xE1A00000) — the call never happened and
9199    /// the function silently returned the leftover table-index value. The A32
9200    /// encoder must emit a real dispatch expansion, since #642 guarded by an
9201    /// inline bounds check:
9202    /// `MOVW r12, #size; CMP idx, r12; BLO +1; UDF;
9203    ///  MOV r12, idx, LSL #2; LDR r12, [r11, r12]; BLX r12`.
9204    #[test]
9205    fn test_encode_arm32_call_indirect_is_real_call_594() {
9206        use synth_synthesis::{ArmOp, Reg};
9207        let enc = ArmEncoder::new_arm32();
9208        let bytes = enc
9209            .encode(&ArmOp::CallIndirect {
9210                rd: Reg::R0,
9211                type_idx: 0,
9212                table_index_reg: Reg::R0,
9213                table_size: 4,
9214                table_byte_offset: 0,
9215                null_check: false,
9216                type_check: None,
9217            })
9218            .unwrap();
9219        assert_eq!(
9220            bytes.len(),
9221            28,
9222            "expected MOVW + CMP + BLO + UDF + MOV + LDR + BLX (7 words): {bytes:02x?}"
9223        );
9224        let words: Vec<u32> = bytes
9225            .chunks_exact(4)
9226            .map(|w| u32::from_le_bytes(w.try_into().unwrap()))
9227            .collect();
9228        // #642 bounds guard: MOVW r12, #4; CMP r0, r12; BLO +1; UDF
9229        assert_eq!(words[0], 0xE300_C004, "MOVW r12,#4: {:#010x}", words[0]);
9230        assert_eq!(words[1], 0xE150_000C, "CMP r0,r12: {:#010x}", words[1]);
9231        assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9232        assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9233        // MOV r12, r0, LSL #2 = 0xE1A0C100
9234        assert_eq!(
9235            words[4], 0xE1A0_C100,
9236            "MOV r12,r0,LSL#2: {:#010x}",
9237            words[4]
9238        );
9239        // LDR r12, [r11, r12] = 0xE79BC00C
9240        assert_eq!(
9241            words[5], 0xE79B_C00C,
9242            "LDR r12,[r11,r12]: {:#010x}",
9243            words[5]
9244        );
9245        // BLX r12 = 0xE12FFF3C
9246        assert_eq!(words[6], 0xE12F_FF3C, "BLX r12: {:#010x}", words[6]);
9247        // The bug: a single NOP word. Must never come back.
9248        assert!(
9249            !bytes
9250                .chunks_exact(4)
9251                .any(|w| w == 0xE1A0_0000u32.to_le_bytes()),
9252            "call_indirect must not contain a NOP (#594): {bytes:02x?}"
9253        );
9254
9255        // A non-R0 index register lands in the MOV's Rm and CMP's Rn fields.
9256        let bytes = enc
9257            .encode(&ArmOp::CallIndirect {
9258                rd: Reg::R0,
9259                type_idx: 0,
9260                table_index_reg: Reg::R4,
9261                table_size: 4,
9262                table_byte_offset: 0,
9263                null_check: false,
9264                type_check: None,
9265            })
9266            .unwrap();
9267        let cmp = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9268        assert_eq!(cmp, 0xE154_000C, "CMP r4,r12: {cmp:#010x}");
9269        let mov = u32::from_le_bytes(bytes[16..20].try_into().unwrap());
9270        assert_eq!(mov, 0xE1A0_C104, "MOV r12,r4,LSL#2: {mov:#010x}");
9271    }
9272
9273    /// #642: a table size above 16 bits must not be silently truncated by the
9274    /// MOVW — the A32 guard adds a MOVT for the high half.
9275    #[test]
9276    fn test_encode_arm32_call_indirect_wide_table_size_642() {
9277        use synth_synthesis::{ArmOp, Reg};
9278        let enc = ArmEncoder::new_arm32();
9279        let bytes = enc
9280            .encode(&ArmOp::CallIndirect {
9281                rd: Reg::R0,
9282                type_idx: 0,
9283                table_index_reg: Reg::R0,
9284                table_size: 0x0002_0003,
9285                table_byte_offset: 0,
9286                null_check: false,
9287                type_check: None,
9288            })
9289            .unwrap();
9290        assert_eq!(bytes.len(), 32, "MOVT arm adds one word: {bytes:02x?}");
9291        let movw = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9292        let movt = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9293        assert_eq!(movw, 0xE300_C003, "MOVW r12,#3: {movw:#010x}");
9294        assert_eq!(movt, 0xE340_C002, "MOVT r12,#2: {movt:#010x}");
9295    }
9296
9297    /// #597 anchor (justified correctness RE-PIN of the #594-era freeze): the
9298    /// Thumb-2 `CallIndirect` expansion is `mov.w ip, rm, LSL #2; ldr.w ip,
9299    /// [r11, ip]; blx ip`.
9300    ///
9301    /// The #594 PR froze the then-current bytes `4F EA 20 0C ...` whose first
9302    /// word decodes as `mov.w ip, rm, ASR #32` — the intended `LSL #2` had
9303    /// its shift amount in the TYPE field (bits 5:4) instead of imm2 (bits
9304    /// 7:6), so the index was destroyed and every call_indirect dispatched
9305    /// table entry 0 (shipped miscompile, masked by index-0 probes). #597
9306    /// corrects the encoding; new bytes `4F EA 80 0C ...` were
9307    /// execution-validated under unicorn against the wasmtime oracle on a
9308    /// multi-entry table (indexes 0, 1, 3 —
9309    /// scripts/repro/call_indirect_597_differential.py) before this pin was
9310    /// replaced. Old pin: [4F EA 20 0C, 5B F8 0C C0, E0 47] (ASR #32 — must
9311    /// never come back).
9312    #[test]
9313    fn test_encode_thumb_call_indirect_lsl2_597() {
9314        use synth_synthesis::{ArmOp, Reg};
9315        let enc = ArmEncoder::new_thumb2();
9316        let bytes = enc
9317            .encode(&ArmOp::CallIndirect {
9318                rd: Reg::R0,
9319                type_idx: 0,
9320                table_index_reg: Reg::R0,
9321                table_size: 4,
9322                table_byte_offset: 0,
9323                null_check: false,
9324                type_check: None,
9325            })
9326            .unwrap();
9327        assert_eq!(
9328            bytes,
9329            vec![
9330                // #642 bounds guard: movw ip,#4; cmp r0,ip; blo +1; udf #0
9331                0x40, 0xF2, 0x04, 0x0C, // movw ip, #4
9332                0x60, 0x45, // cmp r0, ip
9333                0x00, 0xD3, // blo .+4 (skip the udf)
9334                0x00, 0xDE, // udf #0 — OOB index trap (WASM §4.4.8)
9335                // #597-pinned dispatch
9336                0x4F, 0xEA, 0x80, 0x0C, // mov.w ip, r0, lsl #2
9337                0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9338                0xE0, 0x47, // blx ip
9339            ],
9340            "Thumb-2 CallIndirect: bounds guard + mov.w/ldr.w/blx dispatch: {bytes:02x?}"
9341        );
9342        // The #597 bug bytes (ASR #32 dispatch first word) must never come back.
9343        assert!(
9344            !bytes.windows(4).any(|w| w == [0x4F, 0xEA, 0x20, 0x0C]),
9345            "mov.w ip, rm, ASR #32 — the #597 type-field bug"
9346        );
9347
9348        // A non-R0 index register lands in the mov.w's Rm field (hw2 bits 3:0)
9349        // and the cmp's Rn field.
9350        let bytes = enc
9351            .encode(&ArmOp::CallIndirect {
9352                rd: Reg::R0,
9353                type_idx: 0,
9354                table_index_reg: Reg::R4,
9355                table_size: 4,
9356                table_byte_offset: 0,
9357                null_check: false,
9358                type_check: None,
9359            })
9360            .unwrap();
9361        assert_eq!(&bytes[4..6], &[0x64, 0x45], "cmp r4, ip: {bytes:02x?}");
9362        assert_eq!(
9363            &bytes[10..14],
9364            &[0x4F, 0xEA, 0x84, 0x0C],
9365            "mov.w ip, r4, LSL #2: {bytes:02x?}"
9366        );
9367    }
9368
9369    /// #642: the Thumb-2 bounds guard for a high-register index (R8 — the top
9370    /// of the allocatable pool) uses the high-reg-capable 16-bit CMP (T2) with
9371    /// the N bit set; a table size above 16 bits adds a MOVT.
9372    #[test]
9373    fn test_encode_thumb_call_indirect_guard_shapes_642() {
9374        use synth_synthesis::{ArmOp, Reg};
9375        let enc = ArmEncoder::new_thumb2();
9376        let bytes = enc
9377            .encode(&ArmOp::CallIndirect {
9378                rd: Reg::R0,
9379                type_idx: 0,
9380                table_index_reg: Reg::R8,
9381                table_size: 3,
9382                table_byte_offset: 0,
9383                null_check: false,
9384                type_check: None,
9385            })
9386            .unwrap();
9387        // cmp r8, ip — T2: 0x4500 | N(1)<<7 | Rm(12)<<3 | Rn(0) = 0x45E0
9388        assert_eq!(&bytes[4..6], &[0xE0, 0x45], "cmp r8, ip: {bytes:02x?}");
9389
9390        let bytes = enc
9391            .encode(&ArmOp::CallIndirect {
9392                rd: Reg::R0,
9393                type_idx: 0,
9394                table_index_reg: Reg::R0,
9395                table_size: 0x0002_0003,
9396                table_byte_offset: 0,
9397                null_check: false,
9398                type_check: None,
9399            })
9400            .unwrap();
9401        // movw ip,#3 then movt ip,#2 — the size must not be truncated.
9402        assert_eq!(
9403            &bytes[0..8],
9404            &[0x40, 0xF2, 0x03, 0x0C, 0xC0, 0xF2, 0x02, 0x0C],
9405            "movw ip,#3; movt ip,#2: {bytes:02x?}"
9406        );
9407    }
9408
9409    /// #650: a non-zero table base offset (table N of the contiguous R11
9410    /// region) routes the Thumb-2 pointer load through
9411    /// `add.w ip, r11, ip; ldr.w ip, [ip, #offset]` — and offset 0 keeps the
9412    /// pre-#650 single-load bytes IDENTICAL (the by-construction pin).
9413    #[test]
9414    fn test_encode_thumb_call_indirect_table_offset_650() {
9415        use synth_synthesis::{ArmOp, Reg};
9416        let enc = ArmEncoder::new_thumb2();
9417        // falcon's fused-component shape: table 0 has 7 entries, so table 1
9418        // sits at byte offset 28.
9419        let bytes = enc
9420            .encode(&ArmOp::CallIndirect {
9421                rd: Reg::R0,
9422                type_idx: 0,
9423                table_index_reg: Reg::R1,
9424                table_size: 41,
9425                table_byte_offset: 28,
9426                null_check: false,
9427                type_check: None,
9428            })
9429            .unwrap();
9430        assert_eq!(
9431            bytes,
9432            vec![
9433                // #642 bounds guard against TABLE 1's OWN size (41)
9434                0x40, 0xF2, 0x29, 0x0C, // movw ip, #41
9435                0x61, 0x45, // cmp r1, ip
9436                0x00, 0xD3, // blo .+4 (skip the udf)
9437                0x00, 0xDE, // udf #0 — OOB trap (WASM §4.4.8)
9438                // dispatch through table 1's base (R11 + 28)
9439                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9440                0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9441                0xDC, 0xF8, 0x1C, 0xC0, // ldr.w ip, [ip, #28]
9442                0xE0, 0x47, // blx ip
9443            ],
9444            "Thumb-2 table-1 dispatch (#650): {bytes:02x?}"
9445        );
9446
9447        // Offset 0 must stay the #597-pinned single-load form (no add.w, no
9448        // imm-form ldr) — single-table byte identity by construction.
9449        let zero = enc
9450            .encode(&ArmOp::CallIndirect {
9451                rd: Reg::R0,
9452                type_idx: 0,
9453                table_index_reg: Reg::R1,
9454                table_size: 41,
9455                table_byte_offset: 0,
9456                null_check: false,
9457                type_check: None,
9458            })
9459            .unwrap();
9460        assert_eq!(
9461            &zero[10..],
9462            &[
9463                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9464                0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9465                0xE0, 0x47, // blx ip
9466            ],
9467            "offset 0 keeps the pre-#650 dispatch bytes: {zero:02x?}"
9468        );
9469    }
9470
9471    /// #650: the A32 twin — `add r12, r11, r12; ldr r12, [r12, #offset]` for
9472    /// a non-zero table base offset; offset 0 keeps the #594/#642 form.
9473    #[test]
9474    fn test_encode_arm32_call_indirect_table_offset_650() {
9475        use synth_synthesis::{ArmOp, Reg};
9476        let enc = ArmEncoder::new_arm32();
9477        let bytes = enc
9478            .encode(&ArmOp::CallIndirect {
9479                rd: Reg::R0,
9480                type_idx: 0,
9481                table_index_reg: Reg::R1,
9482                table_size: 41,
9483                table_byte_offset: 28,
9484                null_check: false,
9485                type_check: None,
9486            })
9487            .unwrap();
9488        let words: Vec<u32> = bytes
9489            .chunks_exact(4)
9490            .map(|w| u32::from_le_bytes(w.try_into().unwrap()))
9491            .collect();
9492        assert_eq!(words[0], 0xE300_C029, "MOVW r12,#41: {:#010x}", words[0]);
9493        assert_eq!(words[1], 0xE151_000C, "CMP r1,r12: {:#010x}", words[1]);
9494        assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9495        assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9496        assert_eq!(
9497            words[4], 0xE1A0_C101,
9498            "MOV r12,r1,LSL#2: {:#010x}",
9499            words[4]
9500        );
9501        assert_eq!(
9502            words[5], 0xE08B_C00C,
9503            "ADD r12,r11,r12 (#650): {:#010x}",
9504            words[5]
9505        );
9506        assert_eq!(
9507            words[6], 0xE59C_C01C,
9508            "LDR r12,[r12,#28] (#650): {:#010x}",
9509            words[6]
9510        );
9511        assert_eq!(words[7], 0xE12F_FF3C, "BLX r12: {:#010x}", words[7]);
9512    }
9513
9514    /// #664: `null_check` inserts a null-funcref trap between the Thumb-2
9515    /// pointer load and the `BLX` (`cmp.w ip, #0; bne .+4; udf #0`) — a
9516    /// zero-linked (uninitialized) slot must TRAP (WASM §4.4.8), never
9517    /// branch to address 0. `null_check: false` keeps the expansion
9518    /// byte-identical to the pre-#664 form (by-construction pin).
9519    #[test]
9520    fn test_encode_thumb_call_indirect_null_check_664() {
9521        use synth_synthesis::{ArmOp, Reg};
9522        let enc = ArmEncoder::new_thumb2();
9523        let op = |null_check| ArmOp::CallIndirect {
9524            rd: Reg::R0,
9525            type_idx: 0,
9526            table_index_reg: Reg::R1,
9527            table_size: 4,
9528            table_byte_offset: 0,
9529            null_check,
9530            type_check: None,
9531        };
9532        let with = enc.encode(&op(true)).unwrap();
9533        let without = enc.encode(&op(false)).unwrap();
9534        // The checked form = the unchecked form with EXACTLY the three-insn
9535        // null check spliced in before the final BLX (byte identity of the
9536        // shared prefix/suffix — nothing else may move).
9537        assert_eq!(
9538            with.len(),
9539            without.len() + 8,
9540            "cmp.w (4) + bne (2) + udf (2): {with:02x?}"
9541        );
9542        let blx_at = without.len() - 2;
9543        assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9544        assert_eq!(
9545            &with[blx_at..],
9546            &[
9547                0xBC, 0xF1, 0x00, 0x0F, // cmp.w ip, #0
9548                0x00, 0xD1, // bne .+4 (skip the udf)
9549                0x00, 0xDE, // udf #0 — null-funcref trap (#664)
9550                0xE0, 0x47, // blx ip
9551            ],
9552            "null check precedes the BLX: {with:02x?}"
9553        );
9554        assert_eq!(&with[with.len() - 2..], &without[blx_at..], "same BLX");
9555    }
9556
9557    /// #664: the A32 twin — `cmp r12, #0; bne .+8; udf` before the `BLX`;
9558    /// `null_check: false` keeps the #594/#642/#650 bytes identical.
9559    #[test]
9560    fn test_encode_arm32_call_indirect_null_check_664() {
9561        use synth_synthesis::{ArmOp, Reg};
9562        let enc = ArmEncoder::new_arm32();
9563        let op = |null_check| ArmOp::CallIndirect {
9564            rd: Reg::R0,
9565            type_idx: 0,
9566            table_index_reg: Reg::R1,
9567            table_size: 4,
9568            table_byte_offset: 0,
9569            null_check,
9570            type_check: None,
9571        };
9572        let with = enc.encode(&op(true)).unwrap();
9573        let without = enc.encode(&op(false)).unwrap();
9574        assert_eq!(with.len(), without.len() + 12, "3 A32 words: {with:02x?}");
9575        let blx_at = without.len() - 4;
9576        assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9577        let words: Vec<u32> = with[blx_at..]
9578            .chunks_exact(4)
9579            .map(|w| u32::from_le_bytes(w.try_into().unwrap()))
9580            .collect();
9581        assert_eq!(words[0], 0xE35C_0000, "CMP r12,#0: {:#010x}", words[0]);
9582        assert_eq!(words[1], 0x1A00_0000, "BNE +1 insn: {:#010x}", words[1]);
9583        assert_eq!(words[2], 0xE7F0_00F0, "UDF (null trap): {:#010x}", words[2]);
9584        assert_eq!(words[3], 0xE12F_FF3C, "BLX r12: {:#010x}", words[3]);
9585    }
9586
9587    /// #676: `type_check` splices the runtime type check — scale the index,
9588    /// load the slot's structural class id from the type-id sidecar
9589    /// (`ldr.w ip, [ip, #type_off]`), compare against the expected class id
9590    /// and trap on mismatch (WASM §4.4.8) — between the bounds guard and
9591    /// the dispatch tail. `type_check: None` keeps the expansion
9592    /// byte-identical to the pre-#676 form (by-construction pin, the same
9593    /// trick as #650 offset-0 / #664 `null_check: false`).
9594    #[test]
9595    fn test_encode_thumb_call_indirect_type_check_676() {
9596        use synth_synthesis::{ArmOp, Reg};
9597        let enc = ArmEncoder::new_thumb2();
9598        let op = |type_check| ArmOp::CallIndirect {
9599            rd: Reg::R0,
9600            type_idx: 1,
9601            table_index_reg: Reg::R1,
9602            table_size: 5,
9603            table_byte_offset: 0,
9604            null_check: false,
9605            type_check,
9606        };
9607        let with = enc.encode(&op(Some((2, 20)))).unwrap();
9608        let without = enc.encode(&op(None)).unwrap();
9609        // The checked form = the unchecked form with EXACTLY the six-insn
9610        // type check spliced in after the bounds guard (byte identity of
9611        // the shared prefix/suffix — nothing else may move).
9612        assert_eq!(
9613            with.len(),
9614            without.len() + 20,
9615            "lsl.w(4)+add.w(4)+ldr.w(4)+cmp.w(4)+beq(2)+udf(2): {with:02x?}"
9616        );
9617        // Bounds guard: movw(4) + cmp(2) + blo(2) + udf(2) = 10 bytes.
9618        let guard_end = 10;
9619        assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
9620        assert_eq!(
9621            &with[guard_end..guard_end + 20],
9622            &[
9623                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9624                0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9625                0xDC, 0xF8, 0x14, 0xC0, // ldr.w ip, [ip, #20] — sidecar slot id
9626                0xBC, 0xF1, 0x02, 0x0F, // cmp.w ip, #2 — expected class id
9627                0x00, 0xD0, // beq .+4 (skip the udf on a match)
9628                0x00, 0xDE, // udf #0 — §4.4.8 type-mismatch trap (#676)
9629            ],
9630            "type check follows the bounds guard: {with:02x?}"
9631        );
9632        assert_eq!(
9633            &with[guard_end + 20..],
9634            &without[guard_end..],
9635            "dispatch tail unchanged (idx*4 recomputed)"
9636        );
9637    }
9638
9639    /// #676: the A32 twin — `mov r12, idx, lsl #2; add r12, r11, r12;
9640    /// ldr r12, [r12, #type_off]; cmp r12, #id; beq .+8; udf` after the
9641    /// bounds guard; `type_check: None` keeps the #594/#642/#650/#664
9642    /// bytes identical.
9643    #[test]
9644    fn test_encode_arm32_call_indirect_type_check_676() {
9645        use synth_synthesis::{ArmOp, Reg};
9646        let enc = ArmEncoder::new_arm32();
9647        let op = |type_check| ArmOp::CallIndirect {
9648            rd: Reg::R0,
9649            type_idx: 1,
9650            table_index_reg: Reg::R1,
9651            table_size: 5,
9652            table_byte_offset: 0,
9653            null_check: false,
9654            type_check,
9655        };
9656        let with = enc.encode(&op(Some((2, 20)))).unwrap();
9657        let without = enc.encode(&op(None)).unwrap();
9658        assert_eq!(with.len(), without.len() + 24, "6 A32 words: {with:02x?}");
9659        // Bounds guard: movw + cmp + blo + udf = 4 words = 16 bytes.
9660        let guard_end = 16;
9661        assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
9662        let words: Vec<u32> = with[guard_end..guard_end + 24]
9663            .chunks_exact(4)
9664            .map(|w| u32::from_le_bytes(w.try_into().unwrap()))
9665            .collect();
9666        assert_eq!(
9667            words[0], 0xE1A0_C101,
9668            "MOV r12,r1,LSL#2: {:#010x}",
9669            words[0]
9670        );
9671        assert_eq!(words[1], 0xE08B_C00C, "ADD r12,r11,r12: {:#010x}", words[1]);
9672        assert_eq!(
9673            words[2], 0xE59C_C014,
9674            "LDR r12,[r12,#20] (sidecar): {:#010x}",
9675            words[2]
9676        );
9677        assert_eq!(
9678            words[3], 0xE35C_0002,
9679            "CMP r12,#2 (expected class id): {:#010x}",
9680            words[3]
9681        );
9682        assert_eq!(words[4], 0x0A00_0000, "BEQ +1 insn: {:#010x}", words[4]);
9683        assert_eq!(
9684            words[5], 0xE7F0_00F0,
9685            "UDF (type-mismatch trap): {:#010x}",
9686            words[5]
9687        );
9688        assert_eq!(
9689            &with[guard_end + 24..],
9690            &without[guard_end..],
9691            "dispatch tail unchanged"
9692        );
9693    }
9694
9695    /// #178/#180 regression: the Thumb `Add`/`Adds`/`Subs` reg-forms used the
9696    /// 16-bit encoding unconditionally. For high registers (R12 base scratch,
9697    /// R8-R11 i64 pairs) the 3-bit register fields overflow and corrupt the
9698    /// operands — `add ip,ip,r0` came out as `adds r4,r5,r1` (0x186C), silently
9699    /// dropping the address operand and miscompiling every optimized memory
9700    /// access. High registers must use the 32-bit `.W` forms.
9701    #[test]
9702    fn test_encode_thumb_add_high_reg_uses_add_w_178_180() {
9703        let encoder = ArmEncoder::new_thumb2();
9704
9705        // add ip, ip, r0  — the exact MemLoad/MemStore base+addr op.
9706        let code = encoder
9707            .encode(&ArmOp::Add {
9708                rd: Reg::R12,
9709                rn: Reg::R12,
9710                op2: Operand2::Reg(Reg::R0),
9711            })
9712            .unwrap();
9713        // ADD.W ip, ip, r0 = EB0C 0C00 (little-endian halfwords).
9714        assert_eq!(
9715            code,
9716            vec![0x0C, 0xEB, 0x00, 0x0C],
9717            "high-reg Thumb ADD must be 32-bit ADD.W (EB0C 0C00), not corrupt 16-bit; got {code:02X?}"
9718        );
9719        // Must NOT be the buggy 16-bit 0x186C (`adds r4,r5,r1`).
9720        assert_ne!(code, vec![0x6C, 0x18], "regressed to corrupt 16-bit ADDS");
9721
9722        // Low-register add stays 16-bit (no regression for the common case).
9723        let lo = encoder
9724            .encode(&ArmOp::Add {
9725                rd: Reg::R1,
9726                rn: Reg::R2,
9727                op2: Operand2::Reg(Reg::R3),
9728            })
9729            .unwrap();
9730        assert_eq!(
9731            lo.len(),
9732            2,
9733            "low-reg ADD should remain 16-bit, got {lo:02X?}"
9734        );
9735    }
9736
9737    /// #178/#180 sibling: i64 low-word `Adds`/`Subs` can land in R8-R11 pairs;
9738    /// those must fall back to 32-bit ADDS.W/SUBS.W (flag-setting preserved).
9739    #[test]
9740    fn test_encode_thumb_adds_subs_high_reg_use_32bit_178_180() {
9741        let encoder = ArmEncoder::new_thumb2();
9742
9743        // adds r10, r10, r8  → ADDS.W = EB1A 0A08
9744        let adds = encoder
9745            .encode(&ArmOp::Adds {
9746                rd: Reg::R10,
9747                rn: Reg::R10,
9748                op2: Operand2::Reg(Reg::R8),
9749            })
9750            .unwrap();
9751        assert_eq!(
9752            adds,
9753            vec![0x1A, 0xEB, 0x08, 0x0A],
9754            "high-reg ADDS must be 32-bit ADDS.W (EB1A 0A08); got {adds:02X?}"
9755        );
9756
9757        // subs r10, r10, r8  → SUBS.W = EBBA 0A08
9758        let subs = encoder
9759            .encode(&ArmOp::Subs {
9760                rd: Reg::R10,
9761                rn: Reg::R10,
9762                op2: Operand2::Reg(Reg::R8),
9763            })
9764            .unwrap();
9765        assert_eq!(
9766            subs,
9767            vec![0xBA, 0xEB, 0x08, 0x0A],
9768            "high-reg SUBS must be 32-bit SUBS.W (EBBA 0A08); got {subs:02X?}"
9769        );
9770    }
9771
9772    /// #184 (sibling of #180): 16-bit CMN (T1) only encodes R0-R7. High registers
9773    /// must use 32-bit CMN.W, not the corrupt truncated 16-bit form.
9774    #[test]
9775    fn test_encode_thumb_cmn_high_reg_uses_cmn_w_184() {
9776        let encoder = ArmEncoder::new_thumb2();
9777
9778        // cmn r10, r8  → CMN.W = EB1A 0F08 (ADD.W S=1, Rd=PC discarded).
9779        let cmn = encoder
9780            .encode(&ArmOp::Cmn {
9781                rn: Reg::R10,
9782                op2: Operand2::Reg(Reg::R8),
9783            })
9784            .unwrap();
9785        assert_eq!(
9786            cmn,
9787            vec![0x1A, 0xEB, 0x08, 0x0F],
9788            "high-reg CMN must be 32-bit CMN.W (EB1A 0F08); got {cmn:02X?}"
9789        );
9790
9791        // Low registers stay 16-bit: cmn r1, r2 = 0x42D1.
9792        let lo = encoder
9793            .encode(&ArmOp::Cmn {
9794                rn: Reg::R1,
9795                op2: Operand2::Reg(Reg::R2),
9796            })
9797            .unwrap();
9798        assert_eq!(
9799            lo.len(),
9800            2,
9801            "low-reg CMN should remain 16-bit, got {lo:02X?}"
9802        );
9803        assert_eq!(lo, vec![0xD1, 0x42], "low-reg CMN bytes wrong: {lo:02X?}");
9804    }
9805
9806    /// #185 regression: feeding PC (R15) as a data operand to a Thumb-2 op that
9807    /// guards its registers must return Err, not panic under debug-assertions.
9808    /// (Synth never emits PC here; the fuzz harness requires encode() be total.)
9809    #[test]
9810    fn test_encode_pc_operand_returns_err_not_panic_185() {
9811        let encoder = ArmEncoder::new_thumb2();
9812        for op in [
9813            ArmOp::Sdiv {
9814                rd: Reg::PC,
9815                rn: Reg::R0,
9816                rm: Reg::R1,
9817            },
9818            ArmOp::Udiv {
9819                rd: Reg::R0,
9820                rn: Reg::PC,
9821                rm: Reg::R1,
9822            },
9823            ArmOp::Sdiv {
9824                rd: Reg::R0,
9825                rn: Reg::R1,
9826                rm: Reg::PC,
9827            },
9828        ] {
9829            let r = encoder.encode(&op);
9830            assert!(
9831                r.is_err(),
9832                "encode({op:?}) must return Err for a PC operand, got {r:?}"
9833            );
9834        }
9835        // Valid registers still encode fine (no false rejection).
9836        assert!(
9837            encoder
9838                .encode(&ArmOp::Sdiv {
9839                    rd: Reg::R0,
9840                    rn: Reg::R1,
9841                    rm: Reg::R2
9842                })
9843                .is_ok()
9844        );
9845    }
9846
9847    #[test]
9848    fn test_encode_nop_arm32() {
9849        let encoder = ArmEncoder::new_arm32();
9850        let code = encoder.encode(&ArmOp::Nop).unwrap();
9851
9852        assert_eq!(code.len(), 4); // ARM32 instructions are 4 bytes
9853        assert_eq!(code, vec![0x00, 0x00, 0xA0, 0xE1]); // MOV R0, R0
9854    }
9855
9856    #[test]
9857    fn test_encode_nop_thumb() {
9858        let encoder = ArmEncoder::new_thumb2();
9859        let code = encoder.encode(&ArmOp::Nop).unwrap();
9860
9861        assert_eq!(code.len(), 2); // Thumb instructions are 2 bytes
9862        assert_eq!(code, vec![0x00, 0xBF]); // NOP
9863    }
9864
9865    #[test]
9866    fn test_encode_mov_immediate_arm32() {
9867        let encoder = ArmEncoder::new_arm32();
9868        let op = ArmOp::Mov {
9869            rd: Reg::R0,
9870            op2: Operand2::Imm(42),
9871        };
9872
9873        let code = encoder.encode(&op).unwrap();
9874        assert_eq!(code.len(), 4);
9875
9876        // Verify it's a MOV instruction (bits should have immediate flag set)
9877        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9878        assert_eq!(instr & 0x0E000000, 0x02000000); // Check I bit is set
9879    }
9880
9881    #[test]
9882    fn test_encode_add_registers_arm32() {
9883        let encoder = ArmEncoder::new_arm32();
9884        let op = ArmOp::Add {
9885            rd: Reg::R0,
9886            rn: Reg::R1,
9887            op2: Operand2::Reg(Reg::R2),
9888        };
9889
9890        let code = encoder.encode(&op).unwrap();
9891        assert_eq!(code.len(), 4);
9892
9893        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
9894        // Verify it's an ADD instruction with correct opcode
9895        assert_eq!(instr & 0x0FE00000, 0x00800000);
9896    }
9897
9898    /// #350 — `encode_thumb32_add_imm` must lower an out-of-range immediate
9899    /// (> 0xFFF) to a legal MOVW(/MOVT) + ADD.W-register sequence instead of
9900    /// erroring. The small-imm fast path (imm <= 0xFFF) stays byte-identical.
9901    #[test]
9902    fn test_encode_add_imm_large_350() {
9903        let enc = ArmEncoder::new_thumb2();
9904
9905        // --- Fast path: imm <= 0xFFF is a single 4-byte instruction, and the
9906        // VALUE must be right (#681: this test used to assert only the length,
9907        // letting the raw-packed T3 mis-encoding of 0x123 pass CI). 0x123 is
9908        // not ThumbExpandImm-representable, so it must be ADDW (T4, plain
9909        // imm12): clang `addw r0, r1, #0x123` = f201 0023.
9910        let small = enc
9911            .encode_thumb32_add_imm(&Reg::R0, &Reg::R1, 0x123)
9912            .unwrap();
9913        assert_eq!(small, vec![0x01, 0xF2, 0x23, 0x10], "ADDW r0, r1, #0x123");
9914
9915        // helper: decode a Thumb-2 MOVW/MOVT halfword pair back to its imm16
9916        fn movx_imm16(b: &[u8]) -> u32 {
9917            let hw1 = u16::from_le_bytes([b[0], b[1]]) as u32;
9918            let hw2 = u16::from_le_bytes([b[2], b[3]]) as u32;
9919            let imm4 = hw1 & 0xF;
9920            let i = (hw1 >> 10) & 1;
9921            let imm3 = (hw2 >> 12) & 0x7;
9922            let imm8 = hw2 & 0xFF;
9923            (imm4 << 12) | (i << 11) | (imm3 << 8) | imm8
9924        }
9925        fn movx_rd(b: &[u8]) -> u32 {
9926            (u16::from_le_bytes([b[2], b[3]]) as u32 >> 8) & 0xF
9927        }
9928
9929        // --- rd != rn: scratch is rd. imm = 70000 = 0x11170 needs MOVW+MOVT. ---
9930        // 0x11170: lo16 = 0x1170, hi16 = 0x0001
9931        let seq = enc
9932            .encode_thumb32_add_imm(&Reg::R12, &Reg::R0, 70000)
9933            .unwrap();
9934        assert_eq!(seq.len(), 12, "MOVW + MOVT + ADD = 12 bytes");
9935        // MOVW r12, #0x1170
9936        assert_eq!(u16::from_le_bytes([seq[0], seq[1]]) & 0xFBF0, 0xF240);
9937        assert_eq!(movx_rd(&seq[0..4]), 12);
9938        assert_eq!(movx_imm16(&seq[0..4]), 0x1170);
9939        // MOVT r12, #0x0001
9940        assert_eq!(u16::from_le_bytes([seq[4], seq[5]]) & 0xFBF0, 0xF2C0);
9941        assert_eq!(movx_rd(&seq[4..8]), 12);
9942        assert_eq!(movx_imm16(&seq[4..8]), 0x0001);
9943        // ADD.W r12, r0, r12  (EB00 | rn=0 ; rd=12, rm=12)
9944        let add1 = u16::from_le_bytes([seq[8], seq[9]]) as u32;
9945        let add2 = u16::from_le_bytes([seq[10], seq[11]]) as u32;
9946        assert_eq!(add1 & 0xFFF0, 0xEB00);
9947        assert_eq!(add1 & 0xF, 0); // rn = r0
9948        assert_eq!((add2 >> 8) & 0xF, 12); // rd = r12
9949        assert_eq!(add2 & 0xF, 12); // rm = scratch = r12
9950        // The materialized scratch must reconstruct exactly 70000.
9951        assert_eq!(
9952            (movx_imm16(&seq[4..8]) << 16) | movx_imm16(&seq[0..4]),
9953            70000
9954        );
9955
9956        // --- imm <= 0xFFFF: MOVT is skipped (MOVW + ADD = 8 bytes). ---
9957        let seq16 = enc
9958            .encode_thumb32_add_imm(&Reg::R3, &Reg::R0, 0xABCD)
9959            .unwrap();
9960        assert_eq!(seq16.len(), 8, "imm <= 0xFFFF skips MOVT");
9961        assert_eq!(movx_imm16(&seq16[0..4]), 0xABCD);
9962        assert_eq!(movx_rd(&seq16[0..4]), 3); // scratch = rd = r3
9963
9964        // --- rd == rn (in-place add): scratch must be R12, not rd. ---
9965        // imm = 0x12345: lo16 = 0x2345, hi16 = 0x0001
9966        let inplace = enc
9967            .encode_thumb32_add_imm(&Reg::R5, &Reg::R5, 0x12345)
9968            .unwrap();
9969        assert_eq!(inplace.len(), 12);
9970        assert_eq!(movx_rd(&inplace[0..4]), 12, "rd==rn must use R12 scratch");
9971        assert_eq!(
9972            (movx_imm16(&inplace[4..8]) << 16) | movx_imm16(&inplace[0..4]),
9973            0x12345
9974        );
9975        // ADD.W r5, r5, r12 — rm must be the scratch (12), never rn.
9976        let ip_add2 = u16::from_le_bytes([inplace[10], inplace[11]]) as u32;
9977        assert_eq!(ip_add2 & 0xF, 12);
9978        assert_eq!((ip_add2 >> 8) & 0xF, 5);
9979    }
9980
9981    /// #681 — `encode_thumb32_add_imm` packed a RAW immediate into the T3
9982    /// ADD.W `i:imm3:imm8` field, which is a ThumbExpandImm MODIFIED immediate:
9983    /// ThumbExpandImm(0x200) = 0, ThumbExpandImm(0x400) = 0x8000_0000. Every
9984    /// dynamic-address load/store with a static offset in 0x100..=0xFFF
9985    /// computed a wrong address (and bypassed --safety-bounds software: the
9986    /// guard checked the intended address, the access used the mis-encoded
9987    /// one). Fix: imm <= 0xFF keeps T3 (raw == expanded there, bit-identical);
9988    /// 0x100..=0xFFF uses ADDW (T4, plain imm12) — same lowering
9989    /// `encode_thumb32_add` already uses per #253.
9990    ///
9991    /// Every expected byte sequence below is pinned against clang
9992    /// (`-target thumbv7m-none-eabi`) output, bit-for-bit (#544 pattern).
9993    #[test]
9994    fn test_encode_add_imm_thumb_expand_681() {
9995        let enc = ArmEncoder::new_thumb2();
9996        let add = |rd: &Reg, rn: &Reg, imm: u32| enc.encode_thumb32_add_imm(rd, rn, imm).unwrap();
9997
9998        // imm <= 0xFF stays T3 ADD.W (raw == ThumbExpandImm-expanded):
9999        // clang: add.w r12, r0, #0xff  = f100 0cff
10000        assert_eq!(add(&Reg::R12, &Reg::R0, 0xFF), vec![0x00, 0xF1, 0xFF, 0x0C]);
10001
10002        // 0x100..=0xFFF must be ADDW (T4, plain imm12). The old T3 raw packing
10003        // decoded as +0 (0x100/0x200), +0x80000000 (0x400), etc.
10004        // clang: addw r12, r0, #0x100 = f200 1c00
10005        assert_eq!(
10006            add(&Reg::R12, &Reg::R0, 0x100),
10007            vec![0x00, 0xF2, 0x00, 0x1C]
10008        );
10009        // clang: addw r12, r0, #0x104 = f200 1c04
10010        assert_eq!(
10011            add(&Reg::R12, &Reg::R0, 0x104),
10012            vec![0x00, 0xF2, 0x04, 0x1C]
10013        );
10014        // clang: addw r12, r0, #0x200 = f200 2c00
10015        assert_eq!(
10016            add(&Reg::R12, &Reg::R0, 0x200),
10017            vec![0x00, 0xF2, 0x00, 0x2C]
10018        );
10019        // clang: addw r12, r0, #0x3fc = f200 3cfc
10020        assert_eq!(
10021            add(&Reg::R12, &Reg::R0, 0x3FC),
10022            vec![0x00, 0xF2, 0xFC, 0x3C]
10023        );
10024        // clang: addw r12, r0, #0x400 = f200 4c00
10025        assert_eq!(
10026            add(&Reg::R12, &Reg::R0, 0x400),
10027            vec![0x00, 0xF2, 0x00, 0x4C]
10028        );
10029        // clang: addw r12, r0, #0xfff = f600 7cff
10030        assert_eq!(
10031            add(&Reg::R12, &Reg::R0, 0xFFF),
10032            vec![0x00, 0xF6, 0xFF, 0x7C]
10033        );
10034        // Non-scratch rd/rn — clang: addw r1, r2, #0x104 = f202 1104
10035        assert_eq!(add(&Reg::R1, &Reg::R2, 0x104), vec![0x02, 0xF2, 0x04, 0x11]);
10036    }
10037
10038    /// #681 class audit — the T2 RSB and AND.W immediate fields are also
10039    /// ThumbExpandImm-coded and were raw-packed. Neither has a plain-imm12
10040    /// (T4-style) form, so a non-representable immediate must Err loudly
10041    /// (#253/#255/#378 class: never silently encode a different constant).
10042    /// Existing emitters only use representable values (RSB #32, AND #0x3F),
10043    /// pinned here bit-for-bit against clang.
10044    #[test]
10045    fn test_rsb_and_imm_thumb_expand_gate_681() {
10046        let enc = ArmEncoder::new_thumb2();
10047
10048        // clang: rsb.w r3, r2, #0x20 = f1c2 0320 — byte-identical to before.
10049        let rsb = enc
10050            .encode(&ArmOp::Rsb {
10051                rd: Reg::R3,
10052                rn: Reg::R2,
10053                imm: 32,
10054            })
10055            .unwrap();
10056        assert_eq!(rsb, vec![0xC2, 0xF1, 0x20, 0x03]);
10057
10058        // 0x101 is not ThumbExpandImm-representable -> must Err, not mis-encode.
10059        assert!(
10060            enc.encode(&ArmOp::Rsb {
10061                rd: Reg::R3,
10062                rn: Reg::R2,
10063                imm: 0x101,
10064            })
10065            .is_err(),
10066            "non-ThumbExpandImm RSB immediate must Err"
10067        );
10068
10069        // clang: and r4, r4, #0x3f = f004 043f — byte-identical to before.
10070        let and = enc.encode_thumb32_and_imm_raw(4, 4, 0x3F).unwrap();
10071        assert_eq!(and, vec![0x04, 0xF0, 0x3F, 0x04]);
10072        assert!(
10073            enc.encode_thumb32_and_imm_raw(4, 4, 0x101).is_err(),
10074            "non-ThumbExpandImm AND immediate must Err"
10075        );
10076
10077        // A32 RSB: imm12 is a rotate:imm8 modified immediate; > 0xFF used to be
10078        // silently masked to `imm & 0xFF` (#378 masking class) -> must Err.
10079        let a32 = ArmEncoder::new_arm32();
10080        assert!(
10081            a32.encode(&ArmOp::Rsb {
10082                rd: Reg::R3,
10083                rn: Reg::R2,
10084                imm: 0x120,
10085            })
10086            .is_err(),
10087            "A32 RSB immediate > 0xFF must Err, not mask"
10088        );
10089        // imm 32 (the only value real codegen emits) still encodes.
10090        assert!(
10091            a32.encode(&ArmOp::Rsb {
10092                rd: Reg::R3,
10093                rn: Reg::R2,
10094                imm: 32,
10095            })
10096            .is_ok()
10097        );
10098    }
10099
10100    /// #350 follow-up — the `encoder_no_panic` fuzz harness drives the encoder
10101    /// with ARBITRARY registers, including the one case the in-place lowering
10102    /// cannot serve: rd==rn==R12. There the scratch (R12, the reserved encoder
10103    /// register) would alias Rn and clobber it before the ADD reads it. The
10104    /// encoder contract (#180/#185) is Ok-or-Err, never a panic — so this must
10105    /// return Err, not assert. (Real codegen never emits rd==rn==R12 because R12
10106    /// is non-allocatable; this guards only the fuzz/adversarial path.)
10107    #[test]
10108    fn test_encode_add_imm_large_rd_rn_r12_errs_not_panics_350() {
10109        let enc = ArmEncoder::new_thumb2();
10110        // Out-of-range imm with rd==rn==R12: no free scratch -> Err.
10111        let r = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 70000);
10112        assert!(
10113            r.is_err(),
10114            "rd==rn==R12 with out-of-range imm must Err (no free scratch), got {r:?}"
10115        );
10116        // Small imm with rd==rn==R12 still takes the single-instruction fast path
10117        // (no scratch needed) and must succeed — the guard is scoped to the
10118        // out-of-range lowering only.
10119        let small = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 0x10);
10120        assert!(small.is_ok(), "small imm needs no scratch, must stay Ok");
10121    }
10122
10123    /// #378 — `encode_operand2` (ARM32 data-processing operand) must FAIL
10124    /// HONESTLY on an immediate that is not a valid rotated immediate, rather
10125    /// than silently masking it to `imm & 0xFF` and emitting a WRONG
10126    /// instruction. `0x1FF` has 9 set bits, so it cannot come from rotating an
10127    /// 8-bit imm8 — non-encodable. Real codegen materializes large constants via
10128    /// MOVW/MOVT; this guards the encoder's Ok-or-Err contract (#180/#185)
10129    /// directly. It is an Err (not a panic) so the `encoder_no_panic` fuzz
10130    /// harness — which drives arbitrary operands — still passes.
10131    #[test]
10132    fn test_encode_operand2_non_rotatable_imm_errs_not_masks_378() {
10133        let enc = ArmEncoder::new_arm32();
10134        let bad = enc.encode(&ArmOp::Add {
10135            rd: Reg::R0,
10136            rn: Reg::R1,
10137            op2: Operand2::Imm(0x1FF),
10138        });
10139        assert!(
10140            bad.is_err(),
10141            "non-rotatable ARM32 immediate 0x1FF must Err (was silently masked \
10142             to 0xFF), got {bad:?}"
10143        );
10144        // A representable rotated immediate still encodes fine (regression guard).
10145        let ok = enc.encode(&ArmOp::Add {
10146            rd: Reg::R0,
10147            rn: Reg::R1,
10148            op2: Operand2::Imm(0xFF),
10149        });
10150        assert!(
10151            ok.is_ok(),
10152            "0xFF is a valid rotated immediate, must stay Ok"
10153        );
10154    }
10155
10156    #[test]
10157    fn test_encode_ldr_arm32() {
10158        let encoder = ArmEncoder::new_arm32();
10159        let op = ArmOp::Ldr {
10160            rd: Reg::R0,
10161            addr: MemAddr::imm(Reg::R1, 4),
10162        };
10163
10164        let code = encoder.encode(&op).unwrap();
10165        assert_eq!(code.len(), 4);
10166
10167        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10168        // Verify load bit is set
10169        assert_eq!(instr & 0x00100000, 0x00100000);
10170    }
10171
10172    #[test]
10173    fn test_encode_str_arm32() {
10174        let encoder = ArmEncoder::new_arm32();
10175        let op = ArmOp::Str {
10176            rd: Reg::R0,
10177            addr: MemAddr::imm(Reg::SP, 0),
10178        };
10179
10180        let code = encoder.encode(&op).unwrap();
10181        assert_eq!(code.len(), 4);
10182    }
10183
10184    #[test]
10185    fn test_encode_branch_arm32() {
10186        let encoder = ArmEncoder::new_arm32();
10187        let op = ArmOp::Bl {
10188            label: "main".to_string(),
10189        };
10190
10191        let code = encoder.encode(&op).unwrap();
10192        assert_eq!(code.len(), 4);
10193
10194        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10195        // Verify BL opcode
10196        assert_eq!(instr & 0x0F000000, 0x0B000000);
10197    }
10198
10199    /// Regression test for #167 + #174: the Thumb-2 BL relocatable placeholder
10200    /// must carry a -4 addend so an R_ARM_THM_CALL nets to exactly the symbol S.
10201    /// The correct encoding is what `gas` emits for `bl <extern>`: f7ff fffe
10202    /// (hw1=0xF7FF, hw2=0xFFFE), little-endian bytes FF F7 FE FF.
10203    ///   - 0xD000 (J1=J2=0) → ~+0x600000 garbage addend: `bl c0000c` / truncated
10204    ///     to fit (#167).
10205    ///   - 0xF800 (addend 0) → lands at S+4, one instruction past the callee
10206    ///     entry (#174).
10207    ///   - 0xFFFE (addend -4) → lands at S. Correct.
10208    #[test]
10209    fn test_encode_thumb_bl_placeholder_addend_167_174() {
10210        let encoder = ArmEncoder::new_thumb2();
10211        let op = ArmOp::Bl {
10212            label: "callee".to_string(),
10213        };
10214
10215        let code = encoder.encode(&op).unwrap();
10216        assert_eq!(code.len(), 4, "Thumb-2 BL is 32-bit");
10217
10218        let hw1 = u16::from_le_bytes([code[0], code[1]]);
10219        let hw2 = u16::from_le_bytes([code[2], code[3]]);
10220        assert_eq!(hw1, 0xF7FF, "BL first halfword (matches gas `bl <extern>`)");
10221        assert_eq!(
10222            hw2, 0xFFFE,
10223            "BL second halfword must be 0xFFFE (-4 addend → nets to S), not 0xF800 (→ S+4, #174) or 0xD000 (#167)"
10224        );
10225        assert_ne!(hw2, 0xF800, "0xF800 (addend 0) lands at S+4 (#174)");
10226        assert_ne!(hw2, 0xD000, "0xD000 bakes in a ~+0x600000 addend (#167)");
10227    }
10228
10229    /// #740: the Thumb-2 32-bit B<cond>.W (encoding T3) must pack the
10230    /// HALFWORD offset directly into S:J2:J1:imm6:imm11 — the byte offset is
10231    /// SignExtend(S:J2:J1:imm6:imm11:'0'). The old arm packed
10232    /// `halfword_offset >> 1`, HALVING every wide conditional branch's
10233    /// displacement: gust_poll's loop-head `br_if` to an outer block end
10234    /// landed mid-shape (a spurious state write + spurious calls on the
10235    /// empty-budget path). Narrow (16-bit) B<cond> was unaffected — only
10236    /// spans > 254 bytes hit the bug. Bytes cross-checked against the llvm
10237    /// disassembler (`bne.w #0x224` = f040 8112).
10238    #[test]
10239    fn test_encode_thumb_bcond_wide_t3_halfword_offset_740() {
10240        use synth_synthesis::Condition;
10241        let encoder = ArmEncoder::new_thumb2();
10242
10243        // gust_poll's loop-head edge: NE, +0x112 halfwords (+0x224 bytes).
10244        let code = encoder
10245            .encode(&ArmOp::BCondOffset {
10246                cond: Condition::NE,
10247                offset: 0x112,
10248            })
10249            .unwrap();
10250        assert_eq!(code.len(), 4, "offset beyond ±127 halfwords must be wide");
10251        let hw1 = u16::from_le_bytes([code[0], code[1]]);
10252        let hw2 = u16::from_le_bytes([code[2], code[3]]);
10253        assert_eq!(hw1, 0xF040, "T3 hw1: 1111 0 S=0 cond=NE imm6=0");
10254        assert_eq!(
10255            hw2, 0x8112,
10256            "T3 hw2 imm11 must carry halfword offset bits [10:0] directly — \
10257             0x8089 (offset>>1) is the halved #740 miscompile"
10258        );
10259
10260        // Backward wide branch: EQ, -0x100 halfwords. S=1, J2=J1=1,
10261        // imm6=0b111111, imm11=0x700 → f43f af00.
10262        let code = encoder
10263            .encode(&ArmOp::BCondOffset {
10264                cond: Condition::EQ,
10265                offset: -0x100,
10266            })
10267            .unwrap();
10268        assert_eq!(code.len(), 4);
10269        let hw1 = u16::from_le_bytes([code[0], code[1]]);
10270        let hw2 = u16::from_le_bytes([code[2], code[3]]);
10271        assert_eq!(hw1, 0xF43F, "T3 hw1: S=1, cond=EQ, imm6=0x3F");
10272        assert_eq!(hw2, 0xAF00, "T3 hw2: J1=1 J2=1 imm11=0x700");
10273
10274        // Narrow encoding stays byte-identical (in-range offsets untouched).
10275        let code = encoder
10276            .encode(&ArmOp::BCondOffset {
10277                cond: Condition::EQ,
10278                offset: 5,
10279            })
10280            .unwrap();
10281        assert_eq!(code, vec![0x05, 0xD0], "narrow B<cond> unchanged");
10282
10283        // Out of the signed 20-bit T3 range: loud Err, never a truncated jump.
10284        assert!(
10285            encoder
10286                .encode(&ArmOp::BCondOffset {
10287                    cond: Condition::NE,
10288                    offset: 1 << 19,
10289                })
10290                .is_err(),
10291            "out-of-range T3 offset must be a loud decline"
10292        );
10293    }
10294
10295    #[test]
10296    fn test_encode_sequence() {
10297        let encoder = ArmEncoder::new_arm32();
10298        let ops = vec![
10299            ArmOp::Mov {
10300                rd: Reg::R0,
10301                op2: Operand2::Imm(42),
10302            },
10303            ArmOp::Mov {
10304                rd: Reg::R1,
10305                op2: Operand2::Imm(10),
10306            },
10307            ArmOp::Add {
10308                rd: Reg::R2,
10309                rn: Reg::R0,
10310                op2: Operand2::Reg(Reg::R1),
10311            },
10312        ];
10313
10314        let code = encoder.encode_sequence(&ops).unwrap();
10315        assert_eq!(code.len(), 12); // 3 instructions * 4 bytes
10316    }
10317
10318    #[test]
10319    fn test_reg_to_bits() {
10320        assert_eq!(reg_to_bits(&Reg::R0), 0);
10321        assert_eq!(reg_to_bits(&Reg::R7), 7);
10322        assert_eq!(reg_to_bits(&Reg::SP), 13);
10323        assert_eq!(reg_to_bits(&Reg::LR), 14);
10324        assert_eq!(reg_to_bits(&Reg::PC), 15);
10325    }
10326
10327    #[test]
10328    fn test_encode_bitwise_operations() {
10329        let encoder = ArmEncoder::new_arm32();
10330
10331        let and_op = ArmOp::And {
10332            rd: Reg::R0,
10333            rn: Reg::R1,
10334            op2: Operand2::Reg(Reg::R2),
10335        };
10336        let and_code = encoder.encode(&and_op).unwrap();
10337        assert_eq!(and_code.len(), 4);
10338
10339        let orr_op = ArmOp::Orr {
10340            rd: Reg::R0,
10341            rn: Reg::R1,
10342            op2: Operand2::Reg(Reg::R2),
10343        };
10344        let orr_code = encoder.encode(&orr_op).unwrap();
10345        assert_eq!(orr_code.len(), 4);
10346
10347        let eor_op = ArmOp::Eor {
10348            rd: Reg::R0,
10349            rn: Reg::R1,
10350            op2: Operand2::Reg(Reg::R2),
10351        };
10352        let eor_code = encoder.encode(&eor_op).unwrap();
10353        assert_eq!(eor_code.len(), 4);
10354    }
10355
10356    // === Thumb-2 32-bit encoding tests ===
10357
10358    #[test]
10359    fn test_encode_sdiv_thumb2() {
10360        let encoder = ArmEncoder::new_thumb2();
10361        let op = ArmOp::Sdiv {
10362            rd: Reg::R0,
10363            rn: Reg::R1,
10364            rm: Reg::R2,
10365        };
10366
10367        let code = encoder.encode(&op).unwrap();
10368        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10369
10370        // SDIV R0, R1, R2: 0xFB91 0xF0F2
10371        // First halfword: 0xFB90 | Rn(1) = 0xFB91
10372        // Second halfword: 0xF0F0 | Rd(0)<<8 | Rm(2) = 0xF0F2
10373        // Little-endian: [0x91, 0xFB, 0xF2, 0xF0]
10374        assert_eq!(code[0], 0x91);
10375        assert_eq!(code[1], 0xFB);
10376        assert_eq!(code[2], 0xF2);
10377        assert_eq!(code[3], 0xF0);
10378    }
10379
10380    #[test]
10381    fn test_encode_udiv_thumb2() {
10382        let encoder = ArmEncoder::new_thumb2();
10383        let op = ArmOp::Udiv {
10384            rd: Reg::R0,
10385            rn: Reg::R1,
10386            rm: Reg::R2,
10387        };
10388
10389        let code = encoder.encode(&op).unwrap();
10390        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10391
10392        // UDIV R0, R1, R2: 0xFBB1 0xF0F2
10393        // Little-endian: [0xB1, 0xFB, 0xF2, 0xF0]
10394        assert_eq!(code[0], 0xB1);
10395        assert_eq!(code[1], 0xFB);
10396        assert_eq!(code[2], 0xF2);
10397        assert_eq!(code[3], 0xF0);
10398    }
10399
10400    #[test]
10401    fn test_encode_mul_thumb2() {
10402        let encoder = ArmEncoder::new_thumb2();
10403        let op = ArmOp::Mul {
10404            rd: Reg::R0,
10405            rn: Reg::R1,
10406            rm: Reg::R2,
10407        };
10408
10409        let code = encoder.encode(&op).unwrap();
10410        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10411    }
10412
10413    #[test]
10414    fn test_encode_and_thumb2() {
10415        let encoder = ArmEncoder::new_thumb2();
10416        let op = ArmOp::And {
10417            rd: Reg::R0,
10418            rn: Reg::R1,
10419            op2: Operand2::Reg(Reg::R2),
10420        };
10421
10422        let code = encoder.encode(&op).unwrap();
10423        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10424    }
10425
10426    #[test]
10427    fn test_encode_lsl_thumb2_low_regs() {
10428        let encoder = ArmEncoder::new_thumb2();
10429        let op = ArmOp::Lsl {
10430            rd: Reg::R0,
10431            rn: Reg::R1,
10432            shift: 5,
10433        };
10434
10435        let code = encoder.encode(&op).unwrap();
10436        assert_eq!(code.len(), 2); // 16-bit for low registers
10437    }
10438
10439    #[test]
10440    fn test_encode_clz_thumb2() {
10441        let encoder = ArmEncoder::new_thumb2();
10442        let op = ArmOp::Clz {
10443            rd: Reg::R0,
10444            rm: Reg::R1,
10445        };
10446
10447        let code = encoder.encode(&op).unwrap();
10448        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10449    }
10450
10451    #[test]
10452    fn test_encode_bx_thumb2() {
10453        let encoder = ArmEncoder::new_thumb2();
10454        let op = ArmOp::Bx { rm: Reg::LR };
10455
10456        let code = encoder.encode(&op).unwrap();
10457        assert_eq!(code.len(), 2); // 16-bit instruction
10458
10459        // BX LR: 0x4770
10460        assert_eq!(code, vec![0x70, 0x47]);
10461    }
10462
10463    // ========================================================================
10464    // f32 pseudo-op encoding tests
10465    // ========================================================================
10466
10467    #[test]
10468    fn test_encode_f32_abs_arm32() {
10469        let encoder = ArmEncoder::new_arm32();
10470        let op = ArmOp::F32Abs {
10471            sd: VfpReg::S0,
10472            sm: VfpReg::S2,
10473        };
10474        let code = encoder.encode(&op).unwrap();
10475        assert_eq!(code.len(), 4); // Single VFP instruction
10476    }
10477
10478    #[test]
10479    fn test_encode_f32_neg_arm32() {
10480        let encoder = ArmEncoder::new_arm32();
10481        let op = ArmOp::F32Neg {
10482            sd: VfpReg::S0,
10483            sm: VfpReg::S2,
10484        };
10485        let code = encoder.encode(&op).unwrap();
10486        assert_eq!(code.len(), 4);
10487    }
10488
10489    #[test]
10490    fn test_encode_f32_sqrt_arm32() {
10491        let encoder = ArmEncoder::new_arm32();
10492        let op = ArmOp::F32Sqrt {
10493            sd: VfpReg::S0,
10494            sm: VfpReg::S2,
10495        };
10496        let code = encoder.encode(&op).unwrap();
10497        assert_eq!(code.len(), 4);
10498    }
10499
10500    #[test]
10501    fn test_encode_f32_ceil_arm32() {
10502        let encoder = ArmEncoder::new_arm32();
10503        let op = ArmOp::F32Ceil {
10504            sd: VfpReg::S0,
10505            sm: VfpReg::S2,
10506        };
10507        let code = encoder.encode(&op).unwrap();
10508        // VMRS + BIC + ORR + VMSR + VCVT.S32.F32 + VMRS + BIC + VMSR + VCVT.F32.S32
10509        assert_eq!(code.len(), 36);
10510    }
10511
10512    #[test]
10513    fn test_encode_f32_floor_thumb2() {
10514        let encoder = ArmEncoder::new_thumb2();
10515        let op = ArmOp::F32Floor {
10516            sd: VfpReg::S0,
10517            sm: VfpReg::S2,
10518        };
10519        let code = encoder.encode(&op).unwrap();
10520        // VMRS + BIC.W + ORR.W + VMSR + VCVT + VMRS + BIC.W + VMSR + VCVT.F32.S32
10521        assert_eq!(code.len(), 36);
10522    }
10523
10524    #[test]
10525    fn test_encode_f32_min_arm32() {
10526        let encoder = ArmEncoder::new_arm32();
10527        let op = ArmOp::F32Min {
10528            sd: VfpReg::S0,
10529            sn: VfpReg::S2,
10530            sm: VfpReg::S4,
10531        };
10532        let code = encoder.encode(&op).unwrap();
10533        assert_eq!(code.len(), 16); // VMOV + VCMP + VMRS + conditional VMOV
10534    }
10535
10536    #[test]
10537    fn test_encode_f32_max_thumb2() {
10538        let encoder = ArmEncoder::new_thumb2();
10539        let op = ArmOp::F32Max {
10540            sd: VfpReg::S0,
10541            sn: VfpReg::S2,
10542            sm: VfpReg::S4,
10543        };
10544        let code = encoder.encode(&op).unwrap();
10545        // VMOV(4) + VCMP(4) + VMRS(4) + IT(2) + VMOV(4) = 18
10546        assert_eq!(code.len(), 18);
10547    }
10548
10549    #[test]
10550    fn test_encode_f32_copysign_arm32() {
10551        let encoder = ArmEncoder::new_arm32();
10552        let op = ArmOp::F32Copysign {
10553            sd: VfpReg::S0,
10554            sn: VfpReg::S2,
10555            sm: VfpReg::S4,
10556        };
10557        let code = encoder.encode(&op).unwrap();
10558        // VMOV + VMOV + AND + BIC + ORR + VMOV = 6 * 4 = 24
10559        assert_eq!(code.len(), 24);
10560    }
10561
10562    // ========================================================================
10563    // f64 encoding tests
10564    // ========================================================================
10565
10566    #[test]
10567    fn test_encode_f64_add_arm32() {
10568        let encoder = ArmEncoder::new_arm32();
10569        let op = ArmOp::F64Add {
10570            dd: VfpReg::D0,
10571            dn: VfpReg::D1,
10572            dm: VfpReg::D2,
10573        };
10574        let code = encoder.encode(&op).unwrap();
10575        assert_eq!(code.len(), 4);
10576        // VADD.F64 D0, D1, D2: check coprocessor is cp11 (0xB)
10577        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10578        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
10579    }
10580
10581    #[test]
10582    fn test_encode_f64_sub_thumb2() {
10583        let encoder = ArmEncoder::new_thumb2();
10584        let op = ArmOp::F64Sub {
10585            dd: VfpReg::D0,
10586            dn: VfpReg::D1,
10587            dm: VfpReg::D2,
10588        };
10589        let code = encoder.encode(&op).unwrap();
10590        assert_eq!(code.len(), 4); // 32-bit VFP as two Thumb halfwords
10591    }
10592
10593    #[test]
10594    fn test_encode_f64_mul_arm32() {
10595        let encoder = ArmEncoder::new_arm32();
10596        let op = ArmOp::F64Mul {
10597            dd: VfpReg::D0,
10598            dn: VfpReg::D1,
10599            dm: VfpReg::D2,
10600        };
10601        let code = encoder.encode(&op).unwrap();
10602        assert_eq!(code.len(), 4);
10603    }
10604
10605    #[test]
10606    fn test_encode_f64_div_arm32() {
10607        let encoder = ArmEncoder::new_arm32();
10608        let op = ArmOp::F64Div {
10609            dd: VfpReg::D0,
10610            dn: VfpReg::D1,
10611            dm: VfpReg::D2,
10612        };
10613        let code = encoder.encode(&op).unwrap();
10614        assert_eq!(code.len(), 4);
10615    }
10616
10617    #[test]
10618    fn test_encode_f64_abs_arm32() {
10619        let encoder = ArmEncoder::new_arm32();
10620        let op = ArmOp::F64Abs {
10621            dd: VfpReg::D0,
10622            dm: VfpReg::D2,
10623        };
10624        let code = encoder.encode(&op).unwrap();
10625        assert_eq!(code.len(), 4);
10626    }
10627
10628    #[test]
10629    fn test_encode_f64_neg_arm32() {
10630        let encoder = ArmEncoder::new_arm32();
10631        let op = ArmOp::F64Neg {
10632            dd: VfpReg::D0,
10633            dm: VfpReg::D2,
10634        };
10635        let code = encoder.encode(&op).unwrap();
10636        assert_eq!(code.len(), 4);
10637    }
10638
10639    #[test]
10640    fn test_encode_f64_sqrt_arm32() {
10641        let encoder = ArmEncoder::new_arm32();
10642        let op = ArmOp::F64Sqrt {
10643            dd: VfpReg::D0,
10644            dm: VfpReg::D2,
10645        };
10646        let code = encoder.encode(&op).unwrap();
10647        assert_eq!(code.len(), 4);
10648    }
10649
10650    #[test]
10651    fn test_encode_f64_load_arm32() {
10652        let encoder = ArmEncoder::new_arm32();
10653        let op = ArmOp::F64Load {
10654            dd: VfpReg::D0,
10655            addr: MemAddr::imm(Reg::R0, 8),
10656        };
10657        let code = encoder.encode(&op).unwrap();
10658        assert_eq!(code.len(), 4);
10659        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10660        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11 for F64
10661        assert_eq!(instr & 0xFF, 2); // offset 8 / 4 = 2
10662    }
10663
10664    #[test]
10665    fn test_encode_f64_store_thumb2() {
10666        let encoder = ArmEncoder::new_thumb2();
10667        let op = ArmOp::F64Store {
10668            dd: VfpReg::D0,
10669            addr: MemAddr::imm(Reg::SP, 0),
10670        };
10671        let code = encoder.encode(&op).unwrap();
10672        assert_eq!(code.len(), 4);
10673    }
10674
10675    #[test]
10676    fn test_encode_f64_compare_arm32() {
10677        let encoder = ArmEncoder::new_arm32();
10678        let op = ArmOp::F64Eq {
10679            rd: Reg::R0,
10680            dn: VfpReg::D0,
10681            dm: VfpReg::D1,
10682        };
10683        let code = encoder.encode(&op).unwrap();
10684        assert_eq!(code.len(), 16); // VCMP + VMRS + MOV #0 + MOVcond #1
10685    }
10686
10687    #[test]
10688    fn test_encode_f64_compare_thumb2() {
10689        let encoder = ArmEncoder::new_thumb2();
10690        let op = ArmOp::F64Lt {
10691            rd: Reg::R0,
10692            dn: VfpReg::D0,
10693            dm: VfpReg::D1,
10694        };
10695        let code = encoder.encode(&op).unwrap();
10696        // VCMP(4) + VMRS(4) + MOVS(2) + IT(2) + MOV(2) = 14
10697        assert_eq!(code.len(), 14);
10698    }
10699
10700    #[test]
10701    fn test_encode_f64_const_arm32() {
10702        let encoder = ArmEncoder::new_arm32();
10703        let op = ArmOp::F64Const {
10704            dd: VfpReg::D0,
10705            value: 3.125,
10706        };
10707        let code = encoder.encode(&op).unwrap();
10708        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
10709        assert_eq!(code.len(), 20);
10710    }
10711
10712    #[test]
10713    fn test_encode_f64_const_thumb2() {
10714        let encoder = ArmEncoder::new_thumb2();
10715        let op = ArmOp::F64Const {
10716            dd: VfpReg::D0,
10717            value: 2.5,
10718        };
10719        let code = encoder.encode(&op).unwrap();
10720        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
10721        assert_eq!(code.len(), 20);
10722    }
10723
10724    #[test]
10725    fn test_encode_f64_convert_i32s_arm32() {
10726        let encoder = ArmEncoder::new_arm32();
10727        let op = ArmOp::F64ConvertI32S {
10728            dd: VfpReg::D0,
10729            rm: Reg::R0,
10730        };
10731        let code = encoder.encode(&op).unwrap();
10732        // VMOV(4) + VCVT(4) = 8
10733        assert_eq!(code.len(), 8);
10734    }
10735
10736    #[test]
10737    fn test_encode_f64_promote_f32_arm32() {
10738        let encoder = ArmEncoder::new_arm32();
10739        let op = ArmOp::F64PromoteF32 {
10740            dd: VfpReg::D0,
10741            sm: VfpReg::S0,
10742        };
10743        let code = encoder.encode(&op).unwrap();
10744        assert_eq!(code.len(), 4); // Single VCVT.F64.F32 instruction
10745    }
10746
10747    #[test]
10748    fn test_encode_f64_promote_f32_thumb2() {
10749        let encoder = ArmEncoder::new_thumb2();
10750        let op = ArmOp::F64PromoteF32 {
10751            dd: VfpReg::D0,
10752            sm: VfpReg::S0,
10753        };
10754        let code = encoder.encode(&op).unwrap();
10755        assert_eq!(code.len(), 4);
10756    }
10757
10758    #[test]
10759    fn test_encode_i32_trunc_f64s_arm32() {
10760        let encoder = ArmEncoder::new_arm32();
10761        let op = ArmOp::I32TruncF64S {
10762            rd: Reg::R0,
10763            dm: VfpReg::D0,
10764        };
10765        let code = encoder.encode(&op).unwrap();
10766        // VCVT(4) + VMOV(4) = 8
10767        assert_eq!(code.len(), 8);
10768    }
10769
10770    #[test]
10771    fn test_encode_f64_reinterpret_i64_arm32() {
10772        let encoder = ArmEncoder::new_arm32();
10773        let op = ArmOp::F64ReinterpretI64 {
10774            dd: VfpReg::D0,
10775            rmlo: Reg::R0,
10776            rmhi: Reg::R1,
10777        };
10778        let code = encoder.encode(&op).unwrap();
10779        assert_eq!(code.len(), 4); // Single VMOV instruction
10780    }
10781
10782    #[test]
10783    fn test_encode_i64_reinterpret_f64_thumb2() {
10784        let encoder = ArmEncoder::new_thumb2();
10785        let op = ArmOp::I64ReinterpretF64 {
10786            rdlo: Reg::R0,
10787            rdhi: Reg::R1,
10788            dm: VfpReg::D0,
10789        };
10790        let code = encoder.encode(&op).unwrap();
10791        assert_eq!(code.len(), 4);
10792    }
10793
10794    #[test]
10795    fn test_encode_f64_trunc_thumb2() {
10796        let encoder = ArmEncoder::new_thumb2();
10797        let op = ArmOp::F64Trunc {
10798            dd: VfpReg::D0,
10799            dm: VfpReg::D1,
10800        };
10801        let code = encoder.encode(&op).unwrap();
10802        // GI-FPU-002 phase 3 (#369): a single VRINTZ.F64 (clang-verified
10803        // vrintz.f64 d0,d1 base) — no more FPSCR dance / S0 clobber.
10804        assert_eq!(code.len(), 4);
10805        assert_eq!(code, vec![0xb6, 0xee, 0xc1, 0x0b]);
10806    }
10807
10808    /// GI-FPU-002 phase 3 (#369): the rewritten f64 tail sequences, byte-exact
10809    /// against clang (`-target thumbv7em-none-eabi -mfpu=fpv5-d16`). Each
10810    /// clobbers ONLY its destination (+R12/flags where noted) — the previous
10811    /// pseudo-ops staged through live S0/R0-R2 (the #615 class) and the
10812    /// min/max/rounding semantics were wrong (ordered IT select returned the
10813    /// wrong operand on NaN/±0; rounding round-tripped through a 32-bit int).
10814    #[test]
10815    fn test_369_f64_tail_thumb2_encodings_match_clang() {
10816        let enc = ArmEncoder::new_thumb2();
10817        // vrintn/vrintp/vrintm.f64 d1, d2 (FE space, never IT'd).
10818        for (op, want) in [
10819            (
10820                ArmOp::F64Nearest {
10821                    dd: VfpReg::D1,
10822                    dm: VfpReg::D2,
10823                },
10824                vec![0xb9, 0xfe, 0x42, 0x1b],
10825            ),
10826            (
10827                ArmOp::F64Ceil {
10828                    dd: VfpReg::D1,
10829                    dm: VfpReg::D2,
10830                },
10831                vec![0xba, 0xfe, 0x42, 0x1b],
10832            ),
10833            (
10834                ArmOp::F64Floor {
10835                    dd: VfpReg::D1,
10836                    dm: VfpReg::D2,
10837                },
10838                vec![0xbb, 0xfe, 0x42, 0x1b],
10839            ),
10840        ] {
10841            assert_eq!(enc.encode(&op).unwrap(), want, "{op:?}");
10842        }
10843        // vcmp.f64 d1,d2 ; vmrs ; vminnm.f64 d0,d1,d2 ; it vs ; vaddvs.f64
10844        let min = enc
10845            .encode(&ArmOp::F64Min {
10846                dd: VfpReg::D0,
10847                dn: VfpReg::D1,
10848                dm: VfpReg::D2,
10849            })
10850            .unwrap();
10851        assert_eq!(
10852            min,
10853            vec![
10854                0xb4, 0xee, 0x42, 0x1b, // vcmp.f64 d1, d2
10855                0xf1, 0xee, 0x10, 0xfa, // vmrs APSR_nzcv, fpscr
10856                0x81, 0xfe, 0x42, 0x0b, // vminnm.f64 d0, d1, d2
10857                0x68, 0xbf, // it vs
10858                0x31, 0xee, 0x02, 0x0b, // vaddvs.f64 d0, d1, d2
10859            ]
10860        );
10861        // vmaxnm variant flips only bit6 of the VMINNM word.
10862        let max = enc
10863            .encode(&ArmOp::F64Max {
10864                dd: VfpReg::D0,
10865                dn: VfpReg::D1,
10866                dm: VfpReg::D2,
10867            })
10868            .unwrap();
10869        assert_eq!(&max[8..12], &[0x81, 0xfe, 0x02, 0x0b]);
10870        // Destination aliasing a source must ERR (the NaN fix-up would read
10871        // a clobbered operand), never encode.
10872        assert!(
10873            enc.encode(&ArmOp::F64Min {
10874                dd: VfpReg::D1,
10875                dn: VfpReg::D1,
10876                dm: VfpReg::D2,
10877            })
10878            .is_err()
10879        );
10880        // copysign d0,(mag)d1,(sign)d2:
10881        // vmov r12,s5 ; cmp.w r12,#0 ; vabs.f64 d0,d1 ; it mi ; vnegmi.f64 d0,d0
10882        let cs = enc
10883            .encode(&ArmOp::F64Copysign {
10884                dd: VfpReg::D0,
10885                dn: VfpReg::D1,
10886                dm: VfpReg::D2,
10887            })
10888            .unwrap();
10889        assert_eq!(
10890            cs,
10891            vec![
10892                0x12, 0xee, 0x90, 0xca, // vmov r12, s5
10893                0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
10894                0xb0, 0xee, 0xc1, 0x0b, // vabs.f64 d0, d1
10895                0x48, 0xbf, // it mi
10896                0xb1, 0xee, 0x40, 0x0b, // vnegmi.f64 d0, d0
10897            ]
10898        );
10899        // f32 copysign s0,(mag)s1,(sign)s2 — the R0-clobber-free rewrite:
10900        // vmov r12,s2 ; cmp.w r12,#0 ; vabs.f32 s0,s1 ; it mi ; vnegmi.f32
10901        let cs32 = enc
10902            .encode(&ArmOp::F32Copysign {
10903                sd: VfpReg::S0,
10904                sn: VfpReg::S1,
10905                sm: VfpReg::S2,
10906            })
10907            .unwrap();
10908        assert_eq!(
10909            cs32,
10910            vec![
10911                0x11, 0xee, 0x10, 0xca, // vmov r12, s2
10912                0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
10913                0xb0, 0xee, 0xe0, 0x0a, // vabs.f32 s0, s1
10914                0x48, 0xbf, // it mi
10915                0xb1, 0xee, 0x40, 0x0a, // vnegmi.f32 s0, s0
10916            ]
10917        );
10918        // i32 -> f64 stages through the DESTINATION's S-alias (never S0) and
10919        // uses the CORRECT signed/unsigned VCVT bases (previously swapped):
10920        // vmov s0,r3 ; vcvt.f64.s32 d0,s0
10921        let conv_s = enc
10922            .encode(&ArmOp::F64ConvertI32S {
10923                dd: VfpReg::D0,
10924                rm: Reg::R3,
10925            })
10926            .unwrap();
10927        assert_eq!(
10928            conv_s,
10929            vec![
10930                0x00, 0xee, 0x10, 0x3a, // vmov s0, r3
10931                0xb8, 0xee, 0xc0, 0x0b, // vcvt.f64.s32 d0, s0
10932            ]
10933        );
10934        let conv_u = enc
10935            .encode(&ArmOp::F64ConvertI32U {
10936                dd: VfpReg::D0,
10937                rm: Reg::R3,
10938            })
10939            .unwrap();
10940        assert_eq!(&conv_u[4..8], &[0xb8, 0xee, 0x40, 0x0b]); // vcvt.f64.u32
10941        // f64 -> i32 stages through the SOURCE's S-alias (never S0):
10942        // vcvt.s32.f64 s2,d1 ; vmov r3,s2
10943        let trunc_s = enc
10944            .encode(&ArmOp::I32TruncF64S {
10945                rd: Reg::R3,
10946                dm: VfpReg::D1,
10947            })
10948            .unwrap();
10949        assert_eq!(
10950            trunc_s,
10951            vec![
10952                0xbd, 0xee, 0xc1, 0x1b, // vcvt.s32.f64 s2, d1
10953                0x11, 0xee, 0x10, 0x3a, // vmov r3, s2
10954            ]
10955        );
10956        let trunc_u = enc
10957            .encode(&ArmOp::I32TruncF64U {
10958                rd: Reg::R3,
10959                dm: VfpReg::D1,
10960            })
10961            .unwrap();
10962        assert_eq!(&trunc_u[0..4], &[0xbc, 0xee, 0xc1, 0x1b]); // vcvt.u32.f64
10963        // f32.demote_f64: vcvt.f32.f64 s1, d2
10964        let demote = enc
10965            .encode(&ArmOp::F32DemoteF64 {
10966                sd: VfpReg::S1,
10967                dm: VfpReg::D2,
10968            })
10969            .unwrap();
10970        assert_eq!(demote, vec![0xf7, 0xee, 0xc2, 0x0b]);
10971    }
10972
10973    #[test]
10974    fn test_encode_f64_min_arm32() {
10975        let encoder = ArmEncoder::new_arm32();
10976        let op = ArmOp::F64Min {
10977            dd: VfpReg::D0,
10978            dn: VfpReg::D1,
10979            dm: VfpReg::D2,
10980        };
10981        let code = encoder.encode(&op).unwrap();
10982        // VMOV + VCMP + VMRS + conditional VMOV = 16
10983        assert_eq!(code.len(), 16);
10984    }
10985
10986    #[test]
10987    fn test_f64_cp11_encoding() {
10988        // Verify that F64 instructions use coprocessor 11 (0xB), not 10 (0xA)
10989        let encoder = ArmEncoder::new_arm32();
10990
10991        // F64Add
10992        let code = encoder
10993            .encode(&ArmOp::F64Add {
10994                dd: VfpReg::D0,
10995                dn: VfpReg::D0,
10996                dm: VfpReg::D0,
10997            })
10998            .unwrap();
10999        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11000        assert_eq!((instr >> 8) & 0xF, 0xB, "F64 should use cp11");
11001
11002        // F32Add for comparison
11003        let code = encoder
11004            .encode(&ArmOp::F32Add {
11005                sd: VfpReg::S0,
11006                sn: VfpReg::S0,
11007                sm: VfpReg::S0,
11008            })
11009            .unwrap();
11010        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11011        assert_eq!((instr >> 8) & 0xF, 0xA, "F32 should use cp10");
11012    }
11013
11014    #[test]
11015    fn test_dreg_encoding_higher_registers() {
11016        let encoder = ArmEncoder::new_arm32();
11017
11018        // Test with D15 (highest register)
11019        let op = ArmOp::F64Add {
11020            dd: VfpReg::D15,
11021            dn: VfpReg::D14,
11022            dm: VfpReg::D13,
11023        };
11024        let code = encoder.encode(&op).unwrap();
11025        assert_eq!(code.len(), 4);
11026
11027        // Verify the register encoding worked (instruction is valid)
11028        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11029        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
11030    }
11031
11032    // ========================================================================
11033    // Control flow encoding tests
11034    // ========================================================================
11035
11036    #[test]
11037    fn test_encode_label_emits_no_bytes() {
11038        let encoder = ArmEncoder::new_thumb2();
11039        let op = ArmOp::Label {
11040            name: ".Lblock_end_0".to_string(),
11041        };
11042        let code = encoder.encode(&op).unwrap();
11043        assert!(code.is_empty(), "Label should emit zero bytes");
11044
11045        let encoder32 = ArmEncoder::new_arm32();
11046        let code32 = encoder32.encode(&op).unwrap();
11047        assert!(
11048            code32.is_empty(),
11049            "Label should emit zero bytes in ARM32 too"
11050        );
11051    }
11052
11053    #[test]
11054    fn test_encode_bcc_eq_thumb2() {
11055        use synth_synthesis::Condition;
11056        let encoder = ArmEncoder::new_thumb2();
11057        let op = ArmOp::Bcc {
11058            cond: Condition::EQ,
11059            label: "target".to_string(),
11060        };
11061        let code = encoder.encode(&op).unwrap();
11062        assert_eq!(code.len(), 2); // 16-bit conditional branch
11063
11064        // BEQ with offset 0: 0xD000 in little-endian
11065        assert_eq!(code, vec![0x00, 0xD0]);
11066    }
11067
11068    #[test]
11069    fn test_encode_bcc_ne_thumb2() {
11070        use synth_synthesis::Condition;
11071        let encoder = ArmEncoder::new_thumb2();
11072        let op = ArmOp::Bcc {
11073            cond: Condition::NE,
11074            label: "target".to_string(),
11075        };
11076        let code = encoder.encode(&op).unwrap();
11077        assert_eq!(code.len(), 2);
11078
11079        // BNE with offset 0: 0xD100 in little-endian
11080        assert_eq!(code, vec![0x00, 0xD1]);
11081    }
11082
11083    #[test]
11084    fn test_encode_bcc_arm32() {
11085        use synth_synthesis::Condition;
11086        let encoder = ArmEncoder::new_arm32();
11087        let op = ArmOp::Bcc {
11088            cond: Condition::EQ,
11089            label: "target".to_string(),
11090        };
11091        let code = encoder.encode(&op).unwrap();
11092        assert_eq!(code.len(), 4); // 32-bit ARM instruction
11093
11094        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11095        // BEQ: cond=0x0, opcode=0xA, offset=0
11096        assert_eq!(instr & 0xF0000000, 0x00000000); // EQ condition
11097        assert_eq!(instr & 0x0F000000, 0x0A000000); // Branch opcode
11098    }
11099
11100    #[test]
11101    fn test_encode_udf_thumb2() {
11102        let encoder = ArmEncoder::new_thumb2();
11103        let op = ArmOp::Udf { imm: 0 };
11104        let code = encoder.encode(&op).unwrap();
11105        assert_eq!(code.len(), 2); // 16-bit
11106
11107        // UDF #0: 0xDE00 in little-endian
11108        assert_eq!(code, vec![0x00, 0xDE]);
11109    }
11110
11111    /// #610: the i64 rot/div/rem expansions must land the result in the
11112    /// selector-assigned rd pair and leave R0-R3 preserved (restored from the
11113    /// fixed-ABI wrapper's save area) — pre-#610 the rot expansion's own
11114    /// `POP {R4}` restored stale scratch OVER the result (rd_lo == R4) and
11115    /// the div/rem expansions ignored their register fields outright.
11116    #[test]
11117    fn test_610_i64_rot_expansion_ends_with_rd_movs_and_restore() {
11118        let encoder = ArmEncoder::new_thumb2();
11119        for op in [
11120            ArmOp::I64Rotl {
11121                rdlo: Reg::R4,
11122                rdhi: Reg::R5,
11123                rnlo: Reg::R0,
11124                rnhi: Reg::R1,
11125                shift: Reg::R2,
11126            },
11127            ArmOp::I64Rotr {
11128                rdlo: Reg::R4,
11129                rdhi: Reg::R5,
11130                rnlo: Reg::R0,
11131                rnhi: Reg::R1,
11132                shift: Reg::R2,
11133            },
11134        ] {
11135            let code = encoder.encode(&op).unwrap();
11136            assert_eq!(code.len(), 102, "register-independent size (estimator pin)");
11137            // Tail: MOV r5, r1 (0x460D); MOV r4, r0 (0x4604); POP {r0..r3}
11138            // (rd pair r4:r5 does not overlap the save area — all 4 restored).
11139            let tail: Vec<u16> = code[code.len() - 12..]
11140                .chunks(2)
11141                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11142                .collect();
11143            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11144        }
11145    }
11146
11147    /// #610: div/rem expansions honor rd and carry the divide-by-zero trap
11148    /// guard (`ORRS R12, R2, R3; BNE +0; UDF #0`) after operand marshaling.
11149    #[test]
11150    fn test_610_i64_div_rem_expansion_guard_and_rd() {
11151        let encoder = ArmEncoder::new_thumb2();
11152        let mk = |which: u8| {
11153            let (rdlo, rdhi, rnlo, rnhi, rmlo, rmhi) =
11154                (Reg::R4, Reg::R5, Reg::R0, Reg::R1, Reg::R2, Reg::R3);
11155            match which {
11156                0 => ArmOp::I64DivU {
11157                    rdlo,
11158                    rdhi,
11159                    rnlo,
11160                    rnhi,
11161                    rmlo,
11162                    rmhi,
11163                    elide_zero_guard: false,
11164                },
11165                1 => ArmOp::I64RemU {
11166                    rdlo,
11167                    rdhi,
11168                    rnlo,
11169                    rnhi,
11170                    rmlo,
11171                    rmhi,
11172                    elide_zero_guard: false,
11173                },
11174                2 => ArmOp::I64DivS {
11175                    rdlo,
11176                    rdhi,
11177                    rnlo,
11178                    rnhi,
11179                    rmlo,
11180                    rmhi,
11181                    elide_zero_guard: false,
11182                    elide_overflow_guard: false,
11183                },
11184                _ => ArmOp::I64RemS {
11185                    rdlo,
11186                    rdhi,
11187                    rnlo,
11188                    rnhi,
11189                    rmlo,
11190                    rmhi,
11191                    elide_zero_guard: false,
11192                },
11193            }
11194        };
11195        for which in 0..4u8 {
11196            let code = encoder.encode(&mk(which)).unwrap();
11197            // Zero-divisor trap guard right after the 26-byte marshal prologue.
11198            let guard: Vec<u16> = code[26..34]
11199                .chunks(2)
11200                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11201                .collect();
11202            assert_eq!(
11203                guard,
11204                vec![0xEA52, 0x0C03, 0xD100, 0xDE00],
11205                "ORRS R12,R2,R3; BNE +0; UDF #0"
11206            );
11207            // Tail: result into rd pair (r5:r4), then restore all of R0-R3.
11208            let tail: Vec<u16> = code[code.len() - 12..]
11209                .chunks(2)
11210                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11211                .collect();
11212            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11213        }
11214    }
11215
11216    /// #610: when rd overlaps R0-R3 the restore must SKIP the result
11217    /// registers (drop the saved caller word) instead of popping over them.
11218    #[test]
11219    fn test_610_i64_divu_rd_in_r0_r1_skips_restore() {
11220        let encoder = ArmEncoder::new_thumb2();
11221        let code = encoder
11222            .encode(&ArmOp::I64DivU {
11223                rdlo: Reg::R0,
11224                rdhi: Reg::R1,
11225                rnlo: Reg::R0,
11226                rnhi: Reg::R1,
11227                rmlo: Reg::R2,
11228                rmhi: Reg::R3,
11229                elide_zero_guard: false,
11230            })
11231            .unwrap();
11232        let tail: Vec<u16> = code[code.len() - 12..]
11233            .chunks(2)
11234            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11235            .collect();
11236        // MOV r1,r1 / MOV r0,r0 (no-ops, size-stable), ADD SP,#4 twice
11237        // (discard saved r0/r1 — the result lives there), POP {r2}, POP {r3}.
11238        assert_eq!(tail, vec![0x4609, 0x4600, 0xB001, 0xB001, 0xBC04, 0xBC08]);
11239    }
11240
11241    /// #610: a fully swapped rd pair (rd_lo=R1, rd_hi=R0) cannot be
11242    /// materialized by two MOVs in either order — must be a loud Err, never
11243    /// silent corruption. (Selector pairs are consecutive, so unreachable.)
11244    #[test]
11245    fn test_610_i64_swapped_rd_pair_rejected() {
11246        let encoder = ArmEncoder::new_thumb2();
11247        let result = encoder.encode(&ArmOp::I64RemU {
11248            rdlo: Reg::R1,
11249            rdhi: Reg::R0,
11250            rnlo: Reg::R2,
11251            rnhi: Reg::R3,
11252            rmlo: Reg::R4,
11253            rmhi: Reg::R5,
11254            elide_zero_guard: false,
11255        });
11256        assert!(result.is_err(), "swapped rd pair must be rejected loudly");
11257    }
11258
11259    /// #632: the I64Popcnt expansion's own scratch restore (`POP {R3,R4,R5}`)
11260    /// must not clobber the result. Pre-fix the total was materialized with
11261    /// `ADDS rd, R4, R5` BEFORE the pop, so any allocator-assigned
11262    /// rd ∈ {R3,R4,R5} received stale stack garbage. Post-fix the count is
11263    /// carried across the restore in R12 (never allocatable, never restored)
11264    /// and moved into rd only after the pop — structurally rd-independent.
11265    #[test]
11266    fn test_632_i64_popcnt_result_survives_scratch_restore() {
11267        let encoder = ArmEncoder::new_thumb2();
11268        // Every allocatable rd, including the restore set {R3,R4,R5} and R8.
11269        for rd in [
11270            Reg::R0,
11271            Reg::R2,
11272            Reg::R3,
11273            Reg::R4,
11274            Reg::R5,
11275            Reg::R6,
11276            Reg::R8,
11277        ] {
11278            let code = encoder
11279                .encode(&ArmOp::I64Popcnt {
11280                    rd,
11281                    rnlo: Reg::R6,
11282                    rnhi: Reg::R7,
11283                })
11284                .unwrap();
11285            assert_eq!(code.len(), 180, "register-independent size (estimator pin)");
11286            let hw: Vec<u16> = code
11287                .chunks(2)
11288                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11289                .collect();
11290            let pop = hw
11291                .iter()
11292                .position(|&h| h == 0xBC38)
11293                .expect("POP {R3,R4,R5} present");
11294            // Immediately before the POP: ADD.W R12, R4, R5 (the total lives
11295            // in R12, which the POP cannot touch).
11296            assert_eq!(
11297                &hw[pop - 2..pop],
11298                &[0xEB04, 0x0C05],
11299                "total must be carried in R12 across the restore"
11300            );
11301            // Immediately after the POP: MOV rd, R12.
11302            let rd_bits = match rd {
11303                Reg::R8 => 8u16,
11304                Reg::R6 => 6,
11305                Reg::R5 => 5,
11306                Reg::R4 => 4,
11307                Reg::R3 => 3,
11308                Reg::R2 => 2,
11309                _ => 0,
11310            };
11311            let expect_mov = 0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7);
11312            assert_eq!(hw[pop + 1], expect_mov, "MOV rd, R12 after the restore");
11313            // No write into rd between the PUSH and the POP (the old
11314            // pre-restore ADDS is gone).
11315            assert!(
11316                !hw[..pop].contains(&(0x1800 | (5 << 6) | (4 << 3) | rd_bits)),
11317                "no ADDS rd, R4, R5 before the restore pop"
11318            );
11319        }
11320    }
11321
11322    /// #632 audit: the entry marshal must be permutation-safe. Pre-fix
11323    /// `MOV R4, rnlo; MOV R5, rnhi` read a clobbered R4 when the operand
11324    /// pair lived at (R3, R4). Post-fix rnlo routes through R12.
11325    #[test]
11326    fn test_632_i64_popcnt_marshal_pair_at_r3_r4() {
11327        let encoder = ArmEncoder::new_thumb2();
11328        let code = encoder
11329            .encode(&ArmOp::I64Popcnt {
11330                rd: Reg::R0,
11331                rnlo: Reg::R3,
11332                rnhi: Reg::R4,
11333            })
11334            .unwrap();
11335        let hw: Vec<u16> = code
11336            .chunks(2)
11337            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11338            .collect();
11339        // PUSH {R3,R4,R5}; MOV R12, R3; MOV R5, R4 (rnhi read BEFORE any
11340        // write to R4); MOV R4, R12.
11341        assert_eq!(hw[0], 0xB438);
11342        assert_eq!(hw[1], 0x4600 | (1 << 7) | (3 << 3) | 4, "MOV R12, rnlo");
11343        assert_eq!(hw[2], 0x4600 | (4 << 3) | 5, "MOV R5, rnhi");
11344        assert_eq!(hw[3], 0x4664, "MOV R4, R12");
11345    }
11346
11347    /// #632: A32 twin — same structural fix on the ARM-mode path
11348    /// (`--target cortex-r5`): total carried in R12 across the restore.
11349    #[test]
11350    fn test_632_a32_i64_popcnt_result_survives_scratch_restore() {
11351        let encoder = ArmEncoder::new_arm32();
11352        for rd in [Reg::R0, Reg::R3, Reg::R4, Reg::R5, Reg::R8] {
11353            let code = encoder
11354                .encode(&ArmOp::I64Popcnt {
11355                    rd,
11356                    rnlo: Reg::R6,
11357                    rnhi: Reg::R7,
11358                })
11359                .unwrap();
11360            let words: Vec<u32> = code
11361                .chunks(4)
11362                .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11363                .collect();
11364            let pop = words
11365                .iter()
11366                .position(|&w| w == 0xE8BD_0038)
11367                .expect("POP {R3,R4,R5} present");
11368            assert_eq!(words[pop - 1], 0xE084_C005, "ADD R12, R4, R5 before POP");
11369            let rd_bits = match rd {
11370                Reg::R8 => 8u32,
11371                Reg::R5 => 5,
11372                Reg::R4 => 4,
11373                Reg::R3 => 3,
11374                _ => 0,
11375            };
11376            assert_eq!(
11377                words[pop + 1],
11378                0xE1A0_0000 | (rd_bits << 12) | 12,
11379                "MOV rd, R12 after the restore"
11380            );
11381        }
11382    }
11383
11384    /// #633: I64DivS must carry the INT64_MIN/-1 overflow guard (mirroring
11385    /// the i32 path) right after the zero-divisor guard — dividend in R0:R1,
11386    /// divisor in R2:R3 on the #610/#613 fixed-ABI wrapper path.
11387    #[test]
11388    fn test_633_i64_divs_overflow_guard_emitted() {
11389        let encoder = ArmEncoder::new_thumb2();
11390        let code = encoder
11391            .encode(&ArmOp::I64DivS {
11392                rdlo: Reg::R4,
11393                rdhi: Reg::R5,
11394                rnlo: Reg::R0,
11395                rnhi: Reg::R1,
11396                rmlo: Reg::R2,
11397                rmhi: Reg::R3,
11398                elide_zero_guard: false,
11399                elide_overflow_guard: false,
11400            })
11401            .unwrap();
11402        // 26-byte marshal + 8-byte zero-trap, then the 22-byte overflow guard.
11403        let guard: Vec<u16> = code[34..56]
11404            .chunks(2)
11405            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11406            .collect();
11407        assert_eq!(
11408            guard,
11409            vec![
11410                0xEA02, 0x0C03, // AND.W R12, R2, R3
11411                0xF11C, 0x0F01, // CMN.W R12, #1
11412                0xD105, // BNE .no_trap
11413                0x2800, // CMP R0, #0
11414                0xD103, // BNE .no_trap
11415                0xF1B1, 0x4F00, // CMP.W R1, #0x80000000
11416                0xD100, // BNE .no_trap
11417                0xDE00, // UDF #0 — signed-division overflow
11418            ],
11419            "INT64_MIN/-1 overflow guard after the zero-divisor guard"
11420        );
11421    }
11422
11423    /// #633 fix-guard twin: I64RemS must NOT carry the overflow guard —
11424    /// rem_s(INT64_MIN, -1) is defined as 0 and must not trap. Exactly one
11425    /// UDF (the zero-divisor trap) in the whole expansion.
11426    #[test]
11427    fn test_633_i64_rems_has_no_overflow_guard() {
11428        let encoder = ArmEncoder::new_thumb2();
11429        for (is_rem_s, op) in [
11430            (
11431                true,
11432                ArmOp::I64RemS {
11433                    rdlo: Reg::R4,
11434                    rdhi: Reg::R5,
11435                    rnlo: Reg::R0,
11436                    rnhi: Reg::R1,
11437                    rmlo: Reg::R2,
11438                    rmhi: Reg::R3,
11439                    elide_zero_guard: false,
11440                },
11441            ),
11442            (
11443                false,
11444                ArmOp::I64DivS {
11445                    rdlo: Reg::R4,
11446                    rdhi: Reg::R5,
11447                    rnlo: Reg::R0,
11448                    rnhi: Reg::R1,
11449                    rmlo: Reg::R2,
11450                    rmhi: Reg::R3,
11451                    elide_zero_guard: false,
11452                    elide_overflow_guard: false,
11453                },
11454            ),
11455        ] {
11456            let code = encoder.encode(&op).unwrap();
11457            let udfs = code
11458                .chunks(2)
11459                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11460                .count();
11461            let want = if is_rem_s { 1 } else { 2 };
11462            assert_eq!(
11463                udfs, want,
11464                "rem_s: zero-trap only; div_s: zero-trap + overflow trap"
11465            );
11466        }
11467    }
11468
11469    /// #494 phase 2b: `elide_zero_guard` drops EXACTLY the 8-byte fused
11470    /// zero-trap (`ORRS.W R12,R2,R3; BNE; UDF #0`) and nothing else — the
11471    /// rest of the expansion is byte-identical (splice check).
11472    #[test]
11473    fn test_494_i64_zero_guard_elision_is_exact_splice() {
11474        let encoder = ArmEncoder::new_thumb2();
11475        let mk = |elide_zero_guard: bool| {
11476            encoder
11477                .encode(&ArmOp::I64DivU {
11478                    rdlo: Reg::R4,
11479                    rdhi: Reg::R5,
11480                    rnlo: Reg::R0,
11481                    rnhi: Reg::R1,
11482                    rmlo: Reg::R2,
11483                    rmhi: Reg::R3,
11484                    elide_zero_guard,
11485                })
11486                .unwrap()
11487        };
11488        let full = mk(false);
11489        let elided = mk(true);
11490        assert_eq!(full.len(), elided.len() + 8, "zero guard is 8 bytes");
11491        // Marshal prologue (26 B) unchanged, guard (8 B) gone, tail identical.
11492        assert_eq!(&full[..26], &elided[..26]);
11493        assert_eq!(
11494            &full[26..34],
11495            &[0x52, 0xEA, 0x03, 0x0C, 0x00, 0xD1, 0x00, 0xDE],
11496            "the spliced-out bytes are exactly ORRS.W; BNE; UDF #0"
11497        );
11498        assert_eq!(&full[34..], &elided[26..]);
11499    }
11500
11501    /// #494 phase 2b two-guard distinction (the #633/#634 synergy): a
11502    /// divisor-nonzero fact elides ONLY the zero guard — the INT64_MIN/-1
11503    /// OVERFLOW guard is a separate obligation and must survive
11504    /// `elide_zero_guard: true`. Pinned on div_s in all flag states.
11505    #[test]
11506    fn test_494_i64_divs_overflow_guard_retained_when_only_zero_elided() {
11507        let encoder = ArmEncoder::new_thumb2();
11508        let mk = |zero: bool, ovf: bool| {
11509            encoder
11510                .encode(&ArmOp::I64DivS {
11511                    rdlo: Reg::R4,
11512                    rdhi: Reg::R5,
11513                    rnlo: Reg::R0,
11514                    rnhi: Reg::R1,
11515                    rmlo: Reg::R2,
11516                    rmhi: Reg::R3,
11517                    elide_zero_guard: zero,
11518                    elide_overflow_guard: ovf,
11519                })
11520                .unwrap()
11521        };
11522        let udf_count = |code: &[u8]| {
11523            code.chunks(2)
11524                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11525                .count()
11526        };
11527        let full = mk(false, false);
11528        let zero_only = mk(true, false);
11529        let both = mk(true, true);
11530        assert_eq!(udf_count(&full), 2, "baseline: zero trap + overflow trap");
11531        assert_eq!(
11532            udf_count(&zero_only),
11533            1,
11534            "divisor-nonzero elides the zero trap ONLY — the #633 overflow \
11535             guard must be retained"
11536        );
11537        // The retained guard is the 22-byte overflow sequence, now right
11538        // after the 26-byte marshal prologue.
11539        let guard: Vec<u16> = zero_only[26..48]
11540            .chunks(2)
11541            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11542            .collect();
11543        assert_eq!(
11544            guard,
11545            vec![
11546                0xEA02, 0x0C03, 0xF11C, 0x0F01, 0xD105, 0x2800, 0xD103, 0xF1B1, 0x4F00, 0xD100,
11547                0xDE00,
11548            ],
11549            "the surviving guard is the INT64_MIN/-1 overflow trap"
11550        );
11551        assert_eq!(full.len(), zero_only.len() + 8);
11552        assert_eq!(zero_only.len(), both.len() + 22);
11553        assert_eq!(udf_count(&both), 0, "both obligations discharged ⇒ no UDF");
11554    }
11555
11556    /// #494 phase 2b A32 twin: zero-guard elision is an exact 12-byte splice
11557    /// and the A32 overflow guard survives a zero-only elision.
11558    #[test]
11559    fn test_494_a32_i64_guard_elision() {
11560        let encoder = ArmEncoder::new_arm32();
11561        let mk = |zero: bool, ovf: bool| {
11562            encoder
11563                .encode(&ArmOp::I64DivS {
11564                    rdlo: Reg::R4,
11565                    rdhi: Reg::R5,
11566                    rnlo: Reg::R0,
11567                    rnhi: Reg::R1,
11568                    rmlo: Reg::R2,
11569                    rmhi: Reg::R3,
11570                    elide_zero_guard: zero,
11571                    elide_overflow_guard: ovf,
11572                })
11573                .unwrap()
11574        };
11575        let full = mk(false, false);
11576        let zero_only = mk(true, false);
11577        let both = mk(true, true);
11578        // A32 zero guard = 3 words (ORRS/BNE/UDF), overflow guard = 6 words.
11579        assert_eq!(full.len(), zero_only.len() + 12);
11580        assert_eq!(zero_only.len(), both.len() + 24);
11581        let udf_count = |code: &[u8]| {
11582            code.chunks(4)
11583                .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
11584                .count()
11585        };
11586        assert_eq!(udf_count(&full), 2);
11587        assert_eq!(
11588            udf_count(&zero_only),
11589            1,
11590            "A32: overflow guard retained under zero-only elision"
11591        );
11592        assert_eq!(udf_count(&both), 0);
11593    }
11594
11595    /// #633: A32 twin — the conditional-execution overflow guard on the
11596    /// ARM-mode I64DivS, and its absence from I64RemS.
11597    #[test]
11598    fn test_633_a32_i64_divs_overflow_guard() {
11599        let encoder = ArmEncoder::new_arm32();
11600        let mk_divs = ArmOp::I64DivS {
11601            rdlo: Reg::R4,
11602            rdhi: Reg::R5,
11603            rnlo: Reg::R0,
11604            rnhi: Reg::R1,
11605            rmlo: Reg::R2,
11606            rmhi: Reg::R3,
11607            elide_zero_guard: false,
11608            elide_overflow_guard: false,
11609        };
11610        let code = encoder.encode(&mk_divs).unwrap();
11611        let words: Vec<u32> = code
11612            .chunks(4)
11613            .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11614            .collect();
11615        let guard = [
11616            0xE002_C003u32, // AND   R12, R2, R3
11617            0xE37C_0001,    // CMN   R12, #1
11618            0x0350_0000,    // CMPEQ R0, #0
11619            0x0351_0102,    // CMPEQ R1, #0x80000000
11620            0x1A00_0000,    // BNE +1 insn
11621            0xE7F0_00F0,    // UDF #0
11622        ];
11623        assert!(
11624            words.windows(6).any(|w| w == guard),
11625            "A32 I64DivS carries the INT64_MIN/-1 overflow guard"
11626        );
11627        let rems = encoder
11628            .encode(&ArmOp::I64RemS {
11629                rdlo: Reg::R4,
11630                rdhi: Reg::R5,
11631                rnlo: Reg::R0,
11632                rnhi: Reg::R1,
11633                rmlo: Reg::R2,
11634                rmhi: Reg::R3,
11635                elide_zero_guard: false,
11636            })
11637            .unwrap();
11638        let rems_udfs = rems
11639            .chunks(4)
11640            .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
11641            .count();
11642        assert_eq!(rems_udfs, 1, "A32 I64RemS keeps only the zero-divisor trap");
11643    }
11644
11645    #[test]
11646    fn test_encode_nop_thumb2() {
11647        let encoder = ArmEncoder::new_thumb2();
11648        let op = ArmOp::Nop;
11649        let code = encoder.encode(&op).unwrap();
11650        assert_eq!(code.len(), 2); // 16-bit
11651
11652        // NOP: 0xBF00 in little-endian
11653        assert_eq!(code, vec![0x00, 0xBF]);
11654    }
11655
11656    // =========================================================================
11657    // i64 Thumb-2 encoding tests
11658    // =========================================================================
11659
11660    #[test]
11661    fn test_encode_i64_add_thumb2() {
11662        let encoder = ArmEncoder::new_thumb2();
11663        let op = ArmOp::I64Add {
11664            rdlo: Reg::R0,
11665            rdhi: Reg::R1,
11666            rnlo: Reg::R0,
11667            rnhi: Reg::R1,
11668            rmlo: Reg::R2,
11669            rmhi: Reg::R3,
11670        };
11671        let code = encoder.encode(&op).unwrap();
11672        // Should emit ADDS (2 bytes) + ADC.W (4 bytes) = 6 bytes
11673        assert_eq!(code.len(), 6, "I64Add should be 6 bytes (ADDS + ADC.W)");
11674    }
11675
11676    #[test]
11677    fn test_encode_i64_sub_thumb2() {
11678        let encoder = ArmEncoder::new_thumb2();
11679        let op = ArmOp::I64Sub {
11680            rdlo: Reg::R0,
11681            rdhi: Reg::R1,
11682            rnlo: Reg::R0,
11683            rnhi: Reg::R1,
11684            rmlo: Reg::R2,
11685            rmhi: Reg::R3,
11686        };
11687        let code = encoder.encode(&op).unwrap();
11688        // Should emit SUBS (2 bytes) + SBC.W (4 bytes) = 6 bytes
11689        assert_eq!(code.len(), 6, "I64Sub should be 6 bytes (SUBS + SBC.W)");
11690    }
11691
11692    #[test]
11693    fn test_encode_i64_and_thumb2() {
11694        let encoder = ArmEncoder::new_thumb2();
11695        let op = ArmOp::I64And {
11696            rdlo: Reg::R0,
11697            rdhi: Reg::R1,
11698            rnlo: Reg::R0,
11699            rnhi: Reg::R1,
11700            rmlo: Reg::R2,
11701            rmhi: Reg::R3,
11702        };
11703        let code = encoder.encode(&op).unwrap();
11704        // AND.W (4 bytes) + AND.W (4 bytes) = 8 bytes
11705        assert!(code.len() >= 4, "I64And should emit at least 4 bytes");
11706    }
11707
11708    #[test]
11709    fn test_encode_i64_or_thumb2() {
11710        let encoder = ArmEncoder::new_thumb2();
11711        let op = ArmOp::I64Or {
11712            rdlo: Reg::R0,
11713            rdhi: Reg::R1,
11714            rnlo: Reg::R0,
11715            rnhi: Reg::R1,
11716            rmlo: Reg::R2,
11717            rmhi: Reg::R3,
11718        };
11719        let code = encoder.encode(&op).unwrap();
11720        assert!(code.len() >= 4, "I64Or should emit at least 4 bytes");
11721    }
11722
11723    #[test]
11724    fn test_encode_i64_xor_thumb2() {
11725        let encoder = ArmEncoder::new_thumb2();
11726        let op = ArmOp::I64Xor {
11727            rdlo: Reg::R0,
11728            rdhi: Reg::R1,
11729            rnlo: Reg::R0,
11730            rnhi: Reg::R1,
11731            rmlo: Reg::R2,
11732            rmhi: Reg::R3,
11733        };
11734        let code = encoder.encode(&op).unwrap();
11735        assert!(code.len() >= 4, "I64Xor should emit at least 4 bytes");
11736    }
11737
11738    #[test]
11739    fn test_encode_i64_const_small_thumb2() {
11740        let encoder = ArmEncoder::new_thumb2();
11741        // Small constant: only needs MOVW for each half
11742        let op = ArmOp::I64Const {
11743            rdlo: Reg::R0,
11744            rdhi: Reg::R1,
11745            value: 42,
11746        };
11747        let code = encoder.encode(&op).unwrap();
11748        // MOVW R0, #42 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes minimum
11749        assert!(code.len() >= 8, "I64Const should emit at least 8 bytes");
11750    }
11751
11752    #[test]
11753    fn test_encode_i64_const_large_thumb2() {
11754        let encoder = ArmEncoder::new_thumb2();
11755        // Large constant: needs MOVW+MOVT for each half
11756        let op = ArmOp::I64Const {
11757            rdlo: Reg::R0,
11758            rdhi: Reg::R1,
11759            value: 0x1234_5678_9ABC_DEF0_u64 as i64,
11760        };
11761        let code = encoder.encode(&op).unwrap();
11762        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
11763        assert_eq!(
11764            code.len(),
11765            16,
11766            "I64Const with large value should be 16 bytes"
11767        );
11768    }
11769
11770    #[test]
11771    fn test_encode_i64_extend_i32_s_thumb2() {
11772        let encoder = ArmEncoder::new_thumb2();
11773        let op = ArmOp::I64ExtendI32S {
11774            rdlo: Reg::R0,
11775            rdhi: Reg::R1,
11776            rn: Reg::R0,
11777        };
11778        let code = encoder.encode(&op).unwrap();
11779        // When rdlo == rn, only ASR (4 bytes) is emitted
11780        assert_eq!(
11781            code.len(),
11782            4,
11783            "I64ExtendI32S (same reg) should be 4 bytes (ASR only)"
11784        );
11785    }
11786
11787    #[test]
11788    fn test_encode_i64_extend_i32_s_diff_reg_thumb2() {
11789        let encoder = ArmEncoder::new_thumb2();
11790        let op = ArmOp::I64ExtendI32S {
11791            rdlo: Reg::R0,
11792            rdhi: Reg::R1,
11793            rn: Reg::R2,
11794        };
11795        let code = encoder.encode(&op).unwrap();
11796        // MOV rdlo, rn (2 bytes for low regs) + ASR rdhi, rdlo, #31 (4 bytes) = 6 bytes
11797        assert!(
11798            code.len() >= 6,
11799            "I64ExtendI32S (diff reg) should be at least 6 bytes"
11800        );
11801    }
11802
11803    #[test]
11804    fn test_encode_i64_extend_i32_u_thumb2() {
11805        let encoder = ArmEncoder::new_thumb2();
11806        let op = ArmOp::I64ExtendI32U {
11807            rdlo: Reg::R0,
11808            rdhi: Reg::R1,
11809            rn: Reg::R0,
11810        };
11811        let code = encoder.encode(&op).unwrap();
11812        // When rdlo == rn, only MOV rdhi, #0 (2 bytes) is emitted
11813        assert_eq!(
11814            code.len(),
11815            2,
11816            "I64ExtendI32U (same reg) should be 2 bytes (MOV #0 only)"
11817        );
11818    }
11819
11820    #[test]
11821    fn test_encode_i32_wrap_i64_nop_thumb2() {
11822        let encoder = ArmEncoder::new_thumb2();
11823        // When rd == rnlo, should be a NOP
11824        let op = ArmOp::I32WrapI64 {
11825            rd: Reg::R0,
11826            rnlo: Reg::R0,
11827        };
11828        let code = encoder.encode(&op).unwrap();
11829        assert_eq!(code.len(), 2, "I32WrapI64 same reg should be NOP (2 bytes)");
11830        assert_eq!(code, vec![0x00, 0xBF]); // NOP
11831    }
11832
11833    #[test]
11834    fn test_encode_i32_wrap_i64_diff_reg_thumb2() {
11835        let encoder = ArmEncoder::new_thumb2();
11836        let op = ArmOp::I32WrapI64 {
11837            rd: Reg::R2,
11838            rnlo: Reg::R0,
11839        };
11840        let code = encoder.encode(&op).unwrap();
11841        // MOV R2, R0 (2 or 4 bytes)
11842        assert!(
11843            code.len() >= 2,
11844            "I32WrapI64 diff reg should emit at least 2 bytes"
11845        );
11846    }
11847
11848    #[test]
11849    fn test_encode_i64_eqz_thumb2() {
11850        let encoder = ArmEncoder::new_thumb2();
11851        let op = ArmOp::I64Eqz {
11852            rd: Reg::R0,
11853            rnlo: Reg::R0,
11854            rnhi: Reg::R1,
11855        };
11856        let code = encoder.encode(&op).unwrap();
11857        // Delegates to I64SetCondZ which is already encoded
11858        assert!(
11859            code.len() >= 6,
11860            "I64Eqz should emit at least 6 bytes for ORR+ITE+MOV+MOV"
11861        );
11862    }
11863
11864    #[test]
11865    fn test_encode_i64_eq_thumb2() {
11866        let encoder = ArmEncoder::new_thumb2();
11867        let op = ArmOp::I64Eq {
11868            rd: Reg::R0,
11869            rnlo: Reg::R0,
11870            rnhi: Reg::R1,
11871            rmlo: Reg::R2,
11872            rmhi: Reg::R3,
11873        };
11874        let code = encoder.encode(&op).unwrap();
11875        // Delegates to I64SetCond EQ: CMP lo + IT EQ + CMPEQ hi + ITE EQ + MOV 1 + MOV 0
11876        assert!(code.len() >= 10, "I64Eq should emit at least 10 bytes");
11877    }
11878
11879    #[test]
11880    fn test_encode_i64_ldr_thumb2() {
11881        let encoder = ArmEncoder::new_thumb2();
11882        let op = ArmOp::I64Ldr {
11883            rdlo: Reg::R0,
11884            rdhi: Reg::R1,
11885            addr: MemAddr::imm(Reg::SP, 0),
11886        };
11887        let code = encoder.encode(&op).unwrap();
11888        // Two LDR instructions (lo at offset, hi at offset+4)
11889        assert!(code.len() >= 4, "I64Ldr should emit at least 4 bytes");
11890    }
11891
11892    #[test]
11893    fn test_372_i64_ldr_indexed_materializes_address() {
11894        // #372: a memory i64.load carries an index register (R11 + addr + off).
11895        // The encoder must materialize `ip = base + index` (ADD.W) and load via
11896        // `[ip,#off]` — NOT drop the index. A frame (non-indexed) i64.load must
11897        // stay byte-identical (plain `[base,#off]`, no ADD).
11898        let encoder = ArmEncoder::new_thumb2();
11899        let indexed = encoder
11900            .encode(&ArmOp::I64Ldr {
11901                rdlo: Reg::R0,
11902                rdhi: Reg::R1,
11903                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
11904            })
11905            .unwrap();
11906        // ADD.W ip, fp, r0 = eb0b 0c00 (byte-verified vs arm-none-eabi-as).
11907        assert_eq!(
11908            &indexed[0..4],
11909            &[0x0b, 0xeb, 0x00, 0x0c],
11910            "indexed I64Ldr must start with ADD.W ip, base, index"
11911        );
11912        let frame = encoder
11913            .encode(&ArmOp::I64Ldr {
11914                rdlo: Reg::R0,
11915                rdhi: Reg::R1,
11916                addr: MemAddr::imm(Reg::SP, 8),
11917            })
11918            .unwrap();
11919        // No index -> no ADD.W prefix (byte-identical frame access).
11920        assert_ne!(
11921            &frame[0..2],
11922            &[0x0b, 0xeb],
11923            "frame (non-indexed) I64Ldr must NOT emit an ADD.W"
11924        );
11925    }
11926
11927    #[test]
11928    fn test_382_i64_ldst_large_offset_materializes_not_skips() {
11929        // #382: an indexed i64.load/store whose static offset > 0xFFF must
11930        // MATERIALIZE the offset into the base — NOT return Err (skip the fn).
11931        // Sequence for reg_imm(R11, R0, 5000): MOVW ip,#5000 ; ADD ip,r0,ip ;
11932        // ADD ip,ip,fp ; LDR/STR halves at [ip,#0] / [ip,#4]. Byte-verified tail
11933        // vs arm-none-eabi-as.
11934        let encoder = ArmEncoder::new_thumb2();
11935        // 0x1388 > 0xFFF (MemAddr is not Copy, so build it per use).
11936
11937        let ld = encoder
11938            .encode(&ArmOp::I64Ldr {
11939                rdlo: Reg::R0,
11940                rdhi: Reg::R1,
11941                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
11942            })
11943            .expect("large-offset i64.load must lower, not skip");
11944        // MOVW ip,#0x1388 (4) + ADD ip,r0,ip (4) + ADD ip,ip,fp (4) + 2 LDR (8).
11945        assert_eq!(ld.len(), 20, "expected MOVW + 2×ADD + 2×LDR");
11946        // Must NOT be the small-offset `ADD.W ip, fp, r0` (0x0b 0xeb) prefix —
11947        // that path can only reach imm12 offsets.
11948        assert_ne!(
11949            &ld[0..2],
11950            &[0x0b, 0xeb],
11951            "must materialize the large offset"
11952        );
11953        // Effective base built in ip, then halves at [ip,#0] / [ip,#4].
11954        assert_eq!(
11955            &ld[4..20],
11956            &[
11957                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
11958                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
11959                0xdc, 0xf8, 0x00, 0x00, // LDR.W r0, [ip, #0]
11960                0xdc, 0xf8, 0x04, 0x10, // LDR.W r1, [ip, #4]
11961            ],
11962            "large-offset i64.load must fold offset into ip and access [ip,#0]/[ip,#4]"
11963        );
11964
11965        // Store: same base materialization, STR halves.
11966        let st = encoder
11967            .encode(&ArmOp::I64Str {
11968                rdlo: Reg::R2,
11969                rdhi: Reg::R3,
11970                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
11971            })
11972            .expect("large-offset i64.store must lower, not skip");
11973        assert_eq!(st.len(), 20);
11974        assert_eq!(
11975            &st[4..20],
11976            &[
11977                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
11978                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
11979                0xcc, 0xf8, 0x00, 0x20, // STR.W r2, [ip, #0]
11980                0xcc, 0xf8, 0x04, 0x30, // STR.W r3, [ip, #4]
11981            ],
11982            "large-offset i64.store must fold offset into ip and access [ip,#0]/[ip,#4]"
11983        );
11984
11985        // Small-offset (imm12) indexed access stays byte-identical (#372): the
11986        // effective base is a single `ADD.W ip, fp, r0` and the halves keep the
11987        // folded immediates — NO extra MOVW/ADD.
11988        let small = encoder
11989            .encode(&ArmOp::I64Ldr {
11990                rdlo: Reg::R0,
11991                rdhi: Reg::R1,
11992                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 8),
11993            })
11994            .unwrap();
11995        assert_eq!(
11996            &small[0..4],
11997            &[0x0b, 0xeb, 0x00, 0x0c],
11998            "small-offset indexed i64 must keep the single ADD.W ip, fp, r0"
11999        );
12000        assert_eq!(small.len(), 12, "ADD.W + 2×LDR.W (offset folded in imm12)");
12001    }
12002
12003    #[test]
12004    fn test_encode_i64_str_thumb2() {
12005        let encoder = ArmEncoder::new_thumb2();
12006        let op = ArmOp::I64Str {
12007            rdlo: Reg::R0,
12008            rdhi: Reg::R1,
12009            addr: MemAddr::imm(Reg::SP, 0),
12010        };
12011        let code = encoder.encode(&op).unwrap();
12012        // Two STR instructions (lo at offset, hi at offset+4)
12013        assert!(code.len() >= 4, "I64Str should emit at least 4 bytes");
12014    }
12015
12016    #[test]
12017    fn test_encode_i64_all_comparisons_thumb2() {
12018        let encoder = ArmEncoder::new_thumb2();
12019
12020        let ops = vec![
12021            ArmOp::I64Ne {
12022                rd: Reg::R0,
12023                rnlo: Reg::R0,
12024                rnhi: Reg::R1,
12025                rmlo: Reg::R2,
12026                rmhi: Reg::R3,
12027            },
12028            ArmOp::I64LtS {
12029                rd: Reg::R0,
12030                rnlo: Reg::R0,
12031                rnhi: Reg::R1,
12032                rmlo: Reg::R2,
12033                rmhi: Reg::R3,
12034            },
12035            ArmOp::I64LtU {
12036                rd: Reg::R0,
12037                rnlo: Reg::R0,
12038                rnhi: Reg::R1,
12039                rmlo: Reg::R2,
12040                rmhi: Reg::R3,
12041            },
12042            ArmOp::I64LeS {
12043                rd: Reg::R0,
12044                rnlo: Reg::R0,
12045                rnhi: Reg::R1,
12046                rmlo: Reg::R2,
12047                rmhi: Reg::R3,
12048            },
12049            ArmOp::I64LeU {
12050                rd: Reg::R0,
12051                rnlo: Reg::R0,
12052                rnhi: Reg::R1,
12053                rmlo: Reg::R2,
12054                rmhi: Reg::R3,
12055            },
12056            ArmOp::I64GtS {
12057                rd: Reg::R0,
12058                rnlo: Reg::R0,
12059                rnhi: Reg::R1,
12060                rmlo: Reg::R2,
12061                rmhi: Reg::R3,
12062            },
12063            ArmOp::I64GtU {
12064                rd: Reg::R0,
12065                rnlo: Reg::R0,
12066                rnhi: Reg::R1,
12067                rmlo: Reg::R2,
12068                rmhi: Reg::R3,
12069            },
12070            ArmOp::I64GeS {
12071                rd: Reg::R0,
12072                rnlo: Reg::R0,
12073                rnhi: Reg::R1,
12074                rmlo: Reg::R2,
12075                rmhi: Reg::R3,
12076            },
12077            ArmOp::I64GeU {
12078                rd: Reg::R0,
12079                rnlo: Reg::R0,
12080                rnhi: Reg::R1,
12081                rmlo: Reg::R2,
12082                rmhi: Reg::R3,
12083            },
12084        ];
12085
12086        for op in &ops {
12087            let code = encoder.encode(op).unwrap();
12088            assert!(
12089                code.len() >= 8,
12090                "i64 comparison {:?} should emit at least 8 bytes, got {}",
12091                op,
12092                code.len()
12093            );
12094        }
12095    }
12096
12097    #[test]
12098    fn test_encode_i64_const_zero_thumb2() {
12099        let encoder = ArmEncoder::new_thumb2();
12100        let op = ArmOp::I64Const {
12101            rdlo: Reg::R0,
12102            rdhi: Reg::R1,
12103            value: 0,
12104        };
12105        let code = encoder.encode(&op).unwrap();
12106        // MOVW R0, #0 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes
12107        assert_eq!(code.len(), 8, "I64Const(0) should be 8 bytes");
12108    }
12109
12110    #[test]
12111    fn test_encode_i64_const_negative_one_thumb2() {
12112        let encoder = ArmEncoder::new_thumb2();
12113        let op = ArmOp::I64Const {
12114            rdlo: Reg::R0,
12115            rdhi: Reg::R1,
12116            value: -1, // 0xFFFF_FFFF_FFFF_FFFF
12117        };
12118        let code = encoder.encode(&op).unwrap();
12119        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
12120        assert_eq!(code.len(), 16, "I64Const(-1) should be 16 bytes");
12121    }
12122
12123    // =========================================================================
12124    // Sub-word load/store encoding tests
12125    // =========================================================================
12126
12127    #[test]
12128    fn test_encode_ldrb_arm32() {
12129        let encoder = ArmEncoder::new_arm32();
12130        let op = ArmOp::Ldrb {
12131            rd: Reg::R0,
12132            addr: MemAddr::imm(Reg::R1, 4),
12133        };
12134        let code = encoder.encode(&op).unwrap();
12135        assert_eq!(code.len(), 4, "ARM32 LDRB should be 4 bytes");
12136        // LDRB R0, [R1, #4] = 0xE5D10004
12137        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12138        assert_eq!(encoded, 0xE5D10004, "Should encode LDRB R0, [R1, #4]");
12139    }
12140
12141    #[test]
12142    fn test_encode_strb_arm32() {
12143        let encoder = ArmEncoder::new_arm32();
12144        let op = ArmOp::Strb {
12145            rd: Reg::R0,
12146            addr: MemAddr::imm(Reg::R1, 0),
12147        };
12148        let code = encoder.encode(&op).unwrap();
12149        assert_eq!(code.len(), 4, "ARM32 STRB should be 4 bytes");
12150        // STRB R0, [R1, #0] = 0xE5C10000
12151        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12152        assert_eq!(encoded, 0xE5C10000, "Should encode STRB R0, [R1, #0]");
12153    }
12154
12155    #[test]
12156    fn test_encode_ldrh_arm32() {
12157        let encoder = ArmEncoder::new_arm32();
12158        let op = ArmOp::Ldrh {
12159            rd: Reg::R0,
12160            addr: MemAddr::imm(Reg::R1, 2),
12161        };
12162        let code = encoder.encode(&op).unwrap();
12163        assert_eq!(code.len(), 4, "ARM32 LDRH should be 4 bytes");
12164    }
12165
12166    #[test]
12167    fn test_encode_strh_arm32() {
12168        let encoder = ArmEncoder::new_arm32();
12169        let op = ArmOp::Strh {
12170            rd: Reg::R0,
12171            addr: MemAddr::imm(Reg::R1, 0),
12172        };
12173        let code = encoder.encode(&op).unwrap();
12174        assert_eq!(code.len(), 4, "ARM32 STRH should be 4 bytes");
12175    }
12176
12177    #[test]
12178    fn test_encode_ldrsb_arm32() {
12179        let encoder = ArmEncoder::new_arm32();
12180        let op = ArmOp::Ldrsb {
12181            rd: Reg::R0,
12182            addr: MemAddr::imm(Reg::R1, 0),
12183        };
12184        let code = encoder.encode(&op).unwrap();
12185        assert_eq!(code.len(), 4, "ARM32 LDRSB should be 4 bytes");
12186    }
12187
12188    #[test]
12189    fn test_encode_ldrsh_arm32() {
12190        let encoder = ArmEncoder::new_arm32();
12191        let op = ArmOp::Ldrsh {
12192            rd: Reg::R0,
12193            addr: MemAddr::imm(Reg::R1, 0),
12194        };
12195        let code = encoder.encode(&op).unwrap();
12196        assert_eq!(code.len(), 4, "ARM32 LDRSH should be 4 bytes");
12197    }
12198
12199    #[test]
12200    fn test_encode_ldrb_thumb2_16bit() {
12201        let encoder = ArmEncoder::new_thumb2();
12202        let op = ArmOp::Ldrb {
12203            rd: Reg::R0,
12204            addr: MemAddr::imm(Reg::R1, 4),
12205        };
12206        let code = encoder.encode(&op).unwrap();
12207        // Low registers + small offset -> 16-bit encoding
12208        assert_eq!(
12209            code.len(),
12210            2,
12211            "Thumb-2 LDRB with small offset should be 16-bit"
12212        );
12213    }
12214
12215    #[test]
12216    fn test_encode_ldrb_thumb2_32bit() {
12217        let encoder = ArmEncoder::new_thumb2();
12218        let op = ArmOp::Ldrb {
12219            rd: Reg::R0,
12220            addr: MemAddr::imm(Reg::R1, 100), // offset > 31 needs 32-bit
12221        };
12222        let code = encoder.encode(&op).unwrap();
12223        assert_eq!(
12224            code.len(),
12225            4,
12226            "Thumb-2 LDRB with large offset should be 32-bit"
12227        );
12228    }
12229
12230    #[test]
12231    fn test_encode_strb_thumb2_16bit() {
12232        let encoder = ArmEncoder::new_thumb2();
12233        let op = ArmOp::Strb {
12234            rd: Reg::R0,
12235            addr: MemAddr::imm(Reg::R1, 10),
12236        };
12237        let code = encoder.encode(&op).unwrap();
12238        assert_eq!(
12239            code.len(),
12240            2,
12241            "Thumb-2 STRB with small offset should be 16-bit"
12242        );
12243    }
12244
12245    #[test]
12246    fn test_encode_ldrh_thumb2_16bit() {
12247        let encoder = ArmEncoder::new_thumb2();
12248        let op = ArmOp::Ldrh {
12249            rd: Reg::R0,
12250            addr: MemAddr::imm(Reg::R1, 4), // offset aligned to 2, <= 62
12251        };
12252        let code = encoder.encode(&op).unwrap();
12253        assert_eq!(
12254            code.len(),
12255            2,
12256            "Thumb-2 LDRH with small aligned offset should be 16-bit"
12257        );
12258    }
12259
12260    #[test]
12261    fn test_encode_strh_thumb2_16bit() {
12262        let encoder = ArmEncoder::new_thumb2();
12263        let op = ArmOp::Strh {
12264            rd: Reg::R0,
12265            addr: MemAddr::imm(Reg::R1, 4),
12266        };
12267        let code = encoder.encode(&op).unwrap();
12268        assert_eq!(
12269            code.len(),
12270            2,
12271            "Thumb-2 STRH with small aligned offset should be 16-bit"
12272        );
12273    }
12274
12275    #[test]
12276    fn test_encode_ldrsb_thumb2() {
12277        let encoder = ArmEncoder::new_thumb2();
12278        let op = ArmOp::Ldrsb {
12279            rd: Reg::R0,
12280            addr: MemAddr::imm(Reg::R1, 0),
12281        };
12282        let code = encoder.encode(&op).unwrap();
12283        // LDRSB has no 16-bit immediate form, always 32-bit
12284        assert_eq!(code.len(), 4, "Thumb-2 LDRSB should be 32-bit");
12285    }
12286
12287    #[test]
12288    fn test_encode_ldrsh_thumb2() {
12289        let encoder = ArmEncoder::new_thumb2();
12290        let op = ArmOp::Ldrsh {
12291            rd: Reg::R0,
12292            addr: MemAddr::imm(Reg::R1, 0),
12293        };
12294        let code = encoder.encode(&op).unwrap();
12295        assert_eq!(code.len(), 4, "Thumb-2 LDRSH should be 32-bit");
12296    }
12297
12298    #[test]
12299    fn test_encode_memory_size_thumb2() {
12300        let encoder = ArmEncoder::new_thumb2();
12301        let op = ArmOp::MemorySize { rd: Reg::R0 };
12302        let code = encoder.encode(&op).unwrap();
12303        // R0 and R10 are not both low registers, so this needs careful handling
12304        assert!(!code.is_empty(), "MemorySize should produce code");
12305    }
12306
12307    #[test]
12308    fn test_encode_memory_grow_thumb2() {
12309        let encoder = ArmEncoder::new_thumb2();
12310        let op = ArmOp::MemoryGrow {
12311            rd: Reg::R0,
12312            rn: Reg::R0,
12313        };
12314        let code = encoder.encode(&op).unwrap();
12315        assert_eq!(code.len(), 4, "MemoryGrow (MVN) should be 32-bit Thumb-2");
12316    }
12317
12318    #[test]
12319    fn test_encode_subword_reg_offset_thumb2() {
12320        let encoder = ArmEncoder::new_thumb2();
12321
12322        // LDRB with register offset
12323        let op = ArmOp::Ldrb {
12324            rd: Reg::R0,
12325            addr: MemAddr::reg(Reg::R1, Reg::R2),
12326        };
12327        let code = encoder.encode(&op).unwrap();
12328        assert_eq!(
12329            code.len(),
12330            4,
12331            "Thumb-2 LDRB with reg offset should be 32-bit"
12332        );
12333
12334        // STRB with register offset
12335        let op = ArmOp::Strb {
12336            rd: Reg::R0,
12337            addr: MemAddr::reg(Reg::R1, Reg::R2),
12338        };
12339        let code = encoder.encode(&op).unwrap();
12340        assert_eq!(
12341            code.len(),
12342            4,
12343            "Thumb-2 STRB with reg offset should be 32-bit"
12344        );
12345
12346        // LDRH with register offset
12347        let op = ArmOp::Ldrh {
12348            rd: Reg::R0,
12349            addr: MemAddr::reg(Reg::R1, Reg::R2),
12350        };
12351        let code = encoder.encode(&op).unwrap();
12352        assert_eq!(
12353            code.len(),
12354            4,
12355            "Thumb-2 LDRH with reg offset should be 32-bit"
12356        );
12357
12358        // STRH with register offset
12359        let op = ArmOp::Strh {
12360            rd: Reg::R0,
12361            addr: MemAddr::reg(Reg::R1, Reg::R2),
12362        };
12363        let code = encoder.encode(&op).unwrap();
12364        assert_eq!(
12365            code.len(),
12366            4,
12367            "Thumb-2 STRH with reg offset should be 32-bit"
12368        );
12369    }
12370
12371    #[test]
12372    fn test_encode_subword_reg_imm_offset_thumb2() {
12373        let encoder = ArmEncoder::new_thumb2();
12374
12375        // LDRB with both register and immediate offset
12376        let op = ArmOp::Ldrb {
12377            rd: Reg::R0,
12378            addr: MemAddr::reg_imm(Reg::R1, Reg::R2, 4),
12379        };
12380        let code = encoder.encode(&op).unwrap();
12381        // ADD R12, R2, #4 (4 bytes) + LDRB R0, [R1, R12] (4 bytes) = 8 bytes
12382        assert_eq!(
12383            code.len(),
12384            8,
12385            "Thumb-2 LDRB with reg+imm offset should be 8 bytes"
12386        );
12387    }
12388
12389    // ========================================================================
12390    // Helium MVE encoding tests
12391    // ========================================================================
12392
12393    #[test]
12394    fn test_encode_mve_addi32_thumb2() {
12395        let encoder = ArmEncoder::new_thumb2();
12396        let op = ArmOp::MveAddI {
12397            qd: QReg::Q0,
12398            qn: QReg::Q1,
12399            qm: QReg::Q2,
12400            size: MveSize::S32,
12401        };
12402        let code = encoder.encode(&op).unwrap();
12403        assert_eq!(
12404            code.len(),
12405            4,
12406            "MVE VADD.I32 should be 4 bytes (Thumb-2 32-bit)"
12407        );
12408    }
12409
12410    #[test]
12411    fn test_encode_mve_subi16_thumb2() {
12412        let encoder = ArmEncoder::new_thumb2();
12413        let op = ArmOp::MveSubI {
12414            qd: QReg::Q0,
12415            qn: QReg::Q1,
12416            qm: QReg::Q2,
12417            size: MveSize::S16,
12418        };
12419        let code = encoder.encode(&op).unwrap();
12420        assert_eq!(code.len(), 4, "MVE VSUB.I16 should be 4 bytes");
12421    }
12422
12423    #[test]
12424    fn test_encode_mve_muli8_thumb2() {
12425        let encoder = ArmEncoder::new_thumb2();
12426        let op = ArmOp::MveMulI {
12427            qd: QReg::Q0,
12428            qn: QReg::Q1,
12429            qm: QReg::Q2,
12430            size: MveSize::S8,
12431        };
12432        let code = encoder.encode(&op).unwrap();
12433        assert_eq!(code.len(), 4, "MVE VMUL.I8 should be 4 bytes");
12434    }
12435
12436    #[test]
12437    fn test_encode_mve_bitwise_thumb2() {
12438        let encoder = ArmEncoder::new_thumb2();
12439
12440        let ops = vec![
12441            ArmOp::MveAnd {
12442                qd: QReg::Q0,
12443                qn: QReg::Q1,
12444                qm: QReg::Q2,
12445            },
12446            ArmOp::MveOrr {
12447                qd: QReg::Q0,
12448                qn: QReg::Q1,
12449                qm: QReg::Q2,
12450            },
12451            ArmOp::MveEor {
12452                qd: QReg::Q0,
12453                qn: QReg::Q1,
12454                qm: QReg::Q2,
12455            },
12456            ArmOp::MveBic {
12457                qd: QReg::Q0,
12458                qn: QReg::Q1,
12459                qm: QReg::Q2,
12460            },
12461        ];
12462        for op in ops {
12463            let code = encoder.encode(&op).unwrap();
12464            assert_eq!(code.len(), 4, "MVE bitwise op should be 4 bytes");
12465        }
12466    }
12467
12468    #[test]
12469    fn test_encode_mve_mvn_thumb2() {
12470        let encoder = ArmEncoder::new_thumb2();
12471        let op = ArmOp::MveMvn {
12472            qd: QReg::Q0,
12473            qm: QReg::Q1,
12474        };
12475        let code = encoder.encode(&op).unwrap();
12476        assert_eq!(code.len(), 4, "MVE VMVN should be 4 bytes");
12477    }
12478
12479    #[test]
12480    fn test_encode_mve_load_store_thumb2() {
12481        let encoder = ArmEncoder::new_thumb2();
12482
12483        let load = ArmOp::MveLoad {
12484            qd: QReg::Q0,
12485            addr: MemAddr::imm(Reg::R0, 16),
12486        };
12487        let code = encoder.encode(&load).unwrap();
12488        assert_eq!(code.len(), 4, "MVE VLDRW.32 should be 4 bytes");
12489
12490        let store = ArmOp::MveStore {
12491            qd: QReg::Q1,
12492            addr: MemAddr::imm(Reg::R1, 0),
12493        };
12494        let code = encoder.encode(&store).unwrap();
12495        assert_eq!(code.len(), 4, "MVE VSTRW.32 should be 4 bytes");
12496    }
12497
12498    #[test]
12499    fn test_encode_mve_const_thumb2() {
12500        let encoder = ArmEncoder::new_thumb2();
12501        let op = ArmOp::MveConst {
12502            qd: QReg::Q0,
12503            bytes: [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0],
12504        };
12505        let code = encoder.encode(&op).unwrap();
12506        // Should be 4 words of (MOVW R12 + VMOV Sn) = 4 * (4+4) = 32 bytes min
12507        // Some words with hi16=0 skip MOVT, so length varies
12508        assert!(
12509            code.len() >= 24,
12510            "MVE const should produce multiple instructions"
12511        );
12512    }
12513
12514    #[test]
12515    fn test_encode_mve_dup_thumb2() {
12516        let encoder = ArmEncoder::new_thumb2();
12517        let op = ArmOp::MveDup {
12518            qd: QReg::Q0,
12519            rn: Reg::R0,
12520            size: MveSize::S32,
12521        };
12522        let code = encoder.encode(&op).unwrap();
12523        assert_eq!(code.len(), 4, "MVE VDUP.32 should be 4 bytes");
12524    }
12525
12526    #[test]
12527    fn test_encode_mve_extract_lane_thumb2() {
12528        let encoder = ArmEncoder::new_thumb2();
12529        let op = ArmOp::MveExtractLane {
12530            rd: Reg::R0,
12531            qn: QReg::Q1,
12532            lane: 2,
12533            size: MveSize::S32,
12534        };
12535        let code = encoder.encode(&op).unwrap();
12536        assert_eq!(code.len(), 4, "MVE extract lane should be 4 bytes");
12537    }
12538
12539    #[test]
12540    fn test_encode_mve_insert_lane_thumb2() {
12541        let encoder = ArmEncoder::new_thumb2();
12542        let op = ArmOp::MveInsertLane {
12543            qd: QReg::Q0,
12544            rn: Reg::R1,
12545            lane: 3,
12546            size: MveSize::S32,
12547        };
12548        let code = encoder.encode(&op).unwrap();
12549        assert_eq!(code.len(), 4, "MVE insert lane should be 4 bytes");
12550    }
12551
12552    #[test]
12553    fn test_encode_mve_addf32_thumb2() {
12554        let encoder = ArmEncoder::new_thumb2();
12555        let op = ArmOp::MveAddF32 {
12556            qd: QReg::Q0,
12557            qn: QReg::Q1,
12558            qm: QReg::Q2,
12559        };
12560        let code = encoder.encode(&op).unwrap();
12561        assert_eq!(code.len(), 4, "MVE VADD.F32 should be 4 bytes");
12562    }
12563
12564    #[test]
12565    fn test_encode_mve_divf32_thumb2() {
12566        let encoder = ArmEncoder::new_thumb2();
12567        let op = ArmOp::MveDivF32 {
12568            qd: QReg::Q0,
12569            qn: QReg::Q1,
12570            qm: QReg::Q2,
12571        };
12572        let code = encoder.encode(&op).unwrap();
12573        // Lane-wise: 4 x VDIV.F32 = 4 x 4 = 16 bytes
12574        assert_eq!(
12575            code.len(),
12576            16,
12577            "MVE VDIV.F32 (lane-wise) should be 16 bytes"
12578        );
12579    }
12580
12581    #[test]
12582    fn test_encode_mve_sqrtf32_thumb2() {
12583        let encoder = ArmEncoder::new_thumb2();
12584        let op = ArmOp::MveSqrtF32 {
12585            qd: QReg::Q0,
12586            qm: QReg::Q1,
12587        };
12588        let code = encoder.encode(&op).unwrap();
12589        // Lane-wise: 4 x VSQRT.F32 = 4 x 4 = 16 bytes
12590        assert_eq!(
12591            code.len(),
12592            16,
12593            "MVE VSQRT.F32 (lane-wise) should be 16 bytes"
12594        );
12595    }
12596
12597    #[test]
12598    fn test_encode_mve_negf32_thumb2() {
12599        let encoder = ArmEncoder::new_thumb2();
12600        let op = ArmOp::MveNegF32 {
12601            qd: QReg::Q0,
12602            qm: QReg::Q1,
12603        };
12604        let code = encoder.encode(&op).unwrap();
12605        assert_eq!(code.len(), 4, "MVE VNEG.F32 should be 4 bytes");
12606    }
12607
12608    #[test]
12609    fn test_encode_mve_absf32_thumb2() {
12610        let encoder = ArmEncoder::new_thumb2();
12611        let op = ArmOp::MveAbsF32 {
12612            qd: QReg::Q0,
12613            qm: QReg::Q1,
12614        };
12615        let code = encoder.encode(&op).unwrap();
12616        assert_eq!(code.len(), 4, "MVE VABS.F32 should be 4 bytes");
12617    }
12618
12619    /// VCR-RA-001 / immediate-folding precondition: pins the Thumb-2 `AND`
12620    /// immediate encoding for the byte range and documents its bound.
12621    ///
12622    /// The `And { Operand2::Imm }` encoder packs the low 12 bits straight into
12623    /// the `i:imm3:imm8` field WITHOUT applying ThumbExpandImm (the modified-
12624    /// immediate expansion). For `imm <= 0xFF` (e.g. gale's int8 clamps
12625    /// `#0x7e` / `#0x7f`) that is correct — `i:imm3 = 0000` means "imm8
12626    /// zero-extended". So `and r2, r0, #0x7e` encodes to the canonical
12627    /// `00 f0 7e 02`. For `imm >= 0x100` the field would need a true
12628    /// ThumbExpandImm pattern (rotation / replication), which is NOT
12629    /// implemented here — so **immediate folding must gate on `imm <= 0xFF`**
12630    /// until the encoder is hardened to ThumbExpandImm/Ok-or-Err (the
12631    /// "encoder must be Ok-or-Err, never silently wrong" principle, #180/#185).
12632    /// This bound covers the measured `flat_flight` waste (#209).
12633    #[test]
12634    fn and_immediate_encodes_correctly_in_byte_range_documents_fold_bound() {
12635        let encoder = ArmEncoder::new_thumb2();
12636        let op = ArmOp::And {
12637            rd: Reg::R2,
12638            rn: Reg::R0,
12639            op2: Operand2::Imm(0x7e),
12640        };
12641        let code = encoder.encode(&op).unwrap();
12642        assert_eq!(
12643            code,
12644            vec![0x00, 0xf0, 0x7e, 0x02],
12645            "and r2, r0, #0x7e must encode to the canonical AND.W T1 (imm8=0x7e)"
12646        );
12647    }
12648
12649    /// #255: the shared ThumbExpandImm reverse-encoder underpinning the
12650    /// data-processing immediate fix. Encodable modified immediates round-trip to
12651    /// the expected `i:imm3:imm8` field; a genuinely non-modified value is `None`
12652    /// (caller must materialize into a register). Note `1000 = 0xFA ror 30` *is*
12653    /// representable (field 0xF7A) — the old encoder mis-encoded it (raw 0x3E8);
12654    /// this encodes it correctly.
12655    #[test]
12656    fn try_thumb_expand_imm_encodes_modified_immediates() {
12657        assert_eq!(try_thumb_expand_imm(0x7e), Some(0x07e)); // zero-extended byte
12658        assert_eq!(try_thumb_expand_imm(0xff), Some(0x0ff));
12659        assert_eq!(try_thumb_expand_imm(0x0001_0001), Some(0x101)); // 0x00XY00XY
12660        assert_eq!(try_thumb_expand_imm(0xff00_ff00), Some(0x2ff)); // 0xXY00XY00
12661        assert_eq!(try_thumb_expand_imm(0xffff_ffff), Some(0x3ff)); // 0xXYXYXYXY
12662        assert_eq!(try_thumb_expand_imm(0x100), Some(0xf80)); // 0x80 ror 31
12663        assert_eq!(try_thumb_expand_imm(0x8000_0000), Some(0x400)); // 0x80 ror 8
12664        assert_eq!(try_thumb_expand_imm(1000), Some(0xf7a)); // 0xFA ror 30
12665        // Genuinely unrepresentable (bits too far apart for an 8-bit window).
12666        assert_eq!(try_thumb_expand_imm(0x101), None);
12667        assert_eq!(try_thumb_expand_imm(0x12345), None);
12668    }
12669
12670    /// #255: CMP/ADDS/SUBS encode any valid modified immediate correctly, and
12671    /// ERROR (not silently mis-encode) on a genuinely unrepresentable one,
12672    /// forcing the selector to materialize into a register — closing the
12673    /// silent-miscompile class of #251/#253.
12674    #[test]
12675    fn cmp_adds_subs_immediate_error_on_non_modified_imm() {
12676        let encoder = ArmEncoder::new_thumb2();
12677        // cmp r0, #0xff → valid → Ok; cmp r0, #1000 → valid (0xFA ror 30) → Ok.
12678        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 0xff).is_ok());
12679        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 1000).is_ok());
12680        // cmp r0, #0x101 → NOT a modified immediate → Err (materialize-reg).
12681        assert!(
12682            encoder.encode_thumb32_cmp_imm(&Reg::R0, 0x101).is_err(),
12683            "cmp #0x101 must error, not compare the wrong constant"
12684        );
12685        assert!(
12686            encoder
12687                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x101)
12688                .is_err()
12689        );
12690        assert!(
12691            encoder
12692                .encode_thumb32_subs(&Reg::R0, &Reg::R0, 0x101)
12693                .is_err()
12694        );
12695        // ...but a valid modified immediate still encodes.
12696        assert!(
12697            encoder
12698                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x80)
12699                .is_ok()
12700        );
12701    }
12702
12703    /// #257: MLA (multiply-accumulate) encodes as MLS without the bit-4 op flag.
12704    /// `mla r2, r3, r4, r8` (rd=r2, rn=r3, rm=r4, ra=r8) → Thumb-2 `03 fb 04 82`.
12705    #[test]
12706    fn mla_thumb2_encodes_correctly() {
12707        let encoder = ArmEncoder::new_thumb2();
12708        let code = encoder
12709            .encode(&ArmOp::Mla {
12710                rd: Reg::R2,
12711                rn: Reg::R3,
12712                rm: Reg::R4,
12713                ra: Reg::R8,
12714            })
12715            .unwrap();
12716        // hw1 = 0xFB03, hw2 = (8<<12)|(2<<8)|4 = 0x8204
12717        assert_eq!(code, vec![0x03, 0xfb, 0x04, 0x82]);
12718    }
12719
12720    /// #259: LDR/STR (and sub-word) immediate-offset encoders truncated
12721    /// `offset & 0xFFF`, silently targeting the wrong address for offset >= 4096.
12722    /// They now error (the selector must use register-offset addressing) — the
12723    /// load/store sibling of the #253/#255 class. Offsets <= 4095 still encode.
12724    #[test]
12725    fn ldst_imm12_offset_errors_when_out_of_range() {
12726        let encoder = ArmEncoder::new_thumb2();
12727        // offset 0xFFF (4095): valid → Ok; ldr r0, [r1, #4095].
12728        assert!(
12729            encoder
12730                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0xFFF)
12731                .is_ok()
12732        );
12733        // offset 0x1000 (4096): out of imm12 range → Err (not & 0xFFF → #0).
12734        assert!(
12735            encoder
12736                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0x1000)
12737                .is_err(),
12738            "ldr offset 4096 must error, not wrap to 0"
12739        );
12740        assert!(
12741            encoder
12742                .encode_thumb32_str(&Reg::R0, &Reg::R1, 0x1000)
12743                .is_err()
12744        );
12745        assert!(
12746            encoder
12747                .encode_thumb32_ldrb_imm(&Reg::R0, &Reg::R1, 5000)
12748                .is_err()
12749        );
12750        assert!(
12751            encoder
12752                .encode_thumb32_strh_imm(&Reg::R0, &Reg::R1, 5000)
12753                .is_err()
12754        );
12755    }
12756
12757    /// Latent miscompile fix: ADD/SUB with a >0xFF immediate (e.g.
12758    /// `add sp, sp, #frame` for a >=256-byte frame) used ADD.W (T3), whose
12759    /// `i:imm3:imm8` is a ThumbExpandImm modified immediate — so `#256` silently
12760    /// encoded as `#0` (stack corruption). Use ADDW/SUBW (T4), a PLAIN 12-bit
12761    /// immediate, for 0x100..=0xFFF; keep T3 for <=0xFF (bit-identical); error
12762    /// beyond 4095.
12763    #[test]
12764    fn add_sub_large_immediate_use_addw_subw_not_misencoded() {
12765        let encoder = ArmEncoder::new_thumb2();
12766        // add sp, sp, #256  →  ADDW (T4) SP, SP, #256  =  0d f2 00 1d
12767        assert_eq!(
12768            encoder
12769                .encode(&ArmOp::Add {
12770                    rd: Reg::SP,
12771                    rn: Reg::SP,
12772                    op2: Operand2::Imm(256),
12773                })
12774                .unwrap(),
12775            vec![0x0d, 0xf2, 0x00, 0x1d],
12776            "add sp,sp,#256 must be ADDW (plain imm12), not a mis-encoded ADD.W"
12777        );
12778        // sub sp, sp, #256  →  SUBW (T4) SP, SP, #256  =  ad f2 00 1d
12779        assert_eq!(
12780            encoder
12781                .encode(&ArmOp::Sub {
12782                    rd: Reg::SP,
12783                    rn: Reg::SP,
12784                    op2: Operand2::Imm(256),
12785                })
12786                .unwrap(),
12787            vec![0xad, 0xf2, 0x00, 0x1d],
12788        );
12789        // > 4095 has no single-instruction encoding → error, not silent wrong.
12790        assert!(
12791            encoder
12792                .encode(&ArmOp::Add {
12793                    rd: Reg::SP,
12794                    rn: Reg::SP,
12795                    op2: Operand2::Imm(5000),
12796                })
12797                .is_err(),
12798            "add #5000 must error (no single ADDW), not mis-encode"
12799        );
12800    }
12801
12802    /// Closes the data-proc immediate class: AND and CMN now go through
12803    /// `try_thumb_expand_imm` like ORR/EOR/CMP — correct for any modified
12804    /// immediate, `Err` (not raw-pack / NOP) on an un-encodable one. The byte
12805    /// range stays bit-identical (`and r2,r0,#0x7e` is unchanged).
12806    #[test]
12807    fn and_cmn_immediate_thumb_expand_else_error() {
12808        let encoder = ArmEncoder::new_thumb2();
12809        // byte range unchanged (bit-identical with the pre-retrofit encoding)
12810        assert_eq!(
12811            encoder
12812                .encode(&ArmOp::And {
12813                    rd: Reg::R2,
12814                    rn: Reg::R0,
12815                    op2: Operand2::Imm(0x7e),
12816                })
12817                .unwrap(),
12818            vec![0x00, 0xf0, 0x7e, 0x02],
12819        );
12820        // a valid replicated modified immediate now encodes (was silently wrong)
12821        assert!(
12822            encoder
12823                .encode(&ArmOp::And {
12824                    rd: Reg::R2,
12825                    rn: Reg::R0,
12826                    op2: Operand2::Imm(0xff00ff00u32 as i32),
12827                })
12828                .is_ok()
12829        );
12830        // a genuinely un-encodable immediate errors (AND was raw-pack; CMN NOP)
12831        assert!(
12832            encoder
12833                .encode(&ArmOp::And {
12834                    rd: Reg::R2,
12835                    rn: Reg::R0,
12836                    op2: Operand2::Imm(0x101),
12837                })
12838                .is_err()
12839        );
12840        assert!(
12841            encoder
12842                .encode(&ArmOp::Cmn {
12843                    rn: Reg::R0,
12844                    op2: Operand2::Imm(0x101),
12845                })
12846                .is_err(),
12847            "CMN #0x101 must error, not emit a NOP"
12848        );
12849    }
12850
12851    /// VCR-RA-001: ORR/EOR with a small immediate must encode the real
12852    /// instruction (not a silent `0xBF00` NOP). Pins the byte range and the
12853    /// Ok-or-Err bound that makes future Or/Eor immediate folding safe.
12854    #[test]
12855    fn orr_eor_immediate_encode_in_byte_range_else_error() {
12856        let encoder = ArmEncoder::new_thumb2();
12857        // orr r2, r0, #0x7e  →  ORR.W T1, imm8=0x7e
12858        assert_eq!(
12859            encoder
12860                .encode(&ArmOp::Orr {
12861                    rd: Reg::R2,
12862                    rn: Reg::R0,
12863                    op2: Operand2::Imm(0x7e),
12864                })
12865                .unwrap(),
12866            vec![0x40, 0xf0, 0x7e, 0x02],
12867        );
12868        // eor r2, r0, #0x7e  →  EOR.W T1, imm8=0x7e
12869        assert_eq!(
12870            encoder
12871                .encode(&ArmOp::Eor {
12872                    rd: Reg::R2,
12873                    rn: Reg::R0,
12874                    op2: Operand2::Imm(0x7e),
12875                })
12876                .unwrap(),
12877            vec![0x80, 0xf0, 0x7e, 0x02],
12878        );
12879        // Out-of-range immediates error rather than silently mis-encode / NOP.
12880        assert!(
12881            encoder
12882                .encode(&ArmOp::Orr {
12883                    rd: Reg::R2,
12884                    rn: Reg::R0,
12885                    op2: Operand2::Imm(0x140),
12886                })
12887                .is_err(),
12888            "ORR #0x140 must error, not emit a NOP"
12889        );
12890    }
12891
12892    #[test]
12893    fn test_encode_mve_different_qregs() {
12894        let encoder = ArmEncoder::new_thumb2();
12895
12896        // Test that different Q-register numbers produce different encodings
12897        let op1 = ArmOp::MveAddI {
12898            qd: QReg::Q0,
12899            qn: QReg::Q0,
12900            qm: QReg::Q0,
12901            size: MveSize::S32,
12902        };
12903        let op2 = ArmOp::MveAddI {
12904            qd: QReg::Q3,
12905            qn: QReg::Q5,
12906            qm: QReg::Q7,
12907            size: MveSize::S32,
12908        };
12909        let code1 = encoder.encode(&op1).unwrap();
12910        let code2 = encoder.encode(&op2).unwrap();
12911        assert_ne!(
12912            code1, code2,
12913            "Different Q-registers should produce different encodings"
12914        );
12915    }
12916
12917    #[test]
12918    fn test_encode_mve_arm32_loud_err() {
12919        // #615: MVE (Helium) is Thumb-2-only. The ARM32 encoder used to emit
12920        // a silent NOP here (dropping the vector op); it must now be a typed
12921        // Err so a broken "MVE implies Thumb" invariant fails loudly.
12922        let encoder = ArmEncoder::new_arm32();
12923        let op = ArmOp::MveAddI {
12924            qd: QReg::Q0,
12925            qn: QReg::Q1,
12926            qm: QReg::Q2,
12927            size: MveSize::S32,
12928        };
12929        let err = encoder
12930            .encode(&op)
12931            .expect_err("ARM32 MVE must be a loud Err, not a silent NOP (#615)");
12932        assert!(
12933            err.to_string().contains("Thumb-2 only"),
12934            "unexpected error message: {err}"
12935        );
12936    }
12937}