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 / RQ-63-ARMI64OFF (#1165): encode an ARM32 (A32) word/sub-word
55    /// load/store whose effective base must be computed into IP — because the
56    /// address carries a register offset (`[rn, rm{, #off}]`, #206) and/or
57    /// because its static offset exceeds the form's immediate field (imm12 for
58    /// LDR/STR/LDRB/STRB, imm8 for LDRH/STRH/LDRSB/LDRSH; #1165 — the
59    /// immediate arms used to MASK such an offset to a wrong address). Returns
60    /// `None` for a plain in-range immediate access (the caller falls through
61    /// to the immediate-form arms, byte-identical). Otherwise
62    /// `a32_effective_base` materializes `ip` and the op is re-encoded against
63    /// `[ip, #residual]`, which is uniform for word/byte/halfword/signed forms.
64    /// IP (R12) is the scratch register the selector already treats as
65    /// clobberable across memory ops.
66    fn encode_arm_reg_offset_mem(&self, op: &ArmOp) -> Result<Option<Vec<u8>>> {
67        let (addr, imm_max) = match op {
68            ArmOp::Ldr { addr, .. }
69            | ArmOp::Str { addr, .. }
70            | ArmOp::Ldrb { addr, .. }
71            | ArmOp::Strb { addr, .. } => (addr, A32_LDST_IMM12_MAX),
72            ArmOp::Ldrh { addr, .. }
73            | ArmOp::Strh { addr, .. }
74            | ArmOp::Ldrsb { addr, .. }
75            | ArmOp::Ldrsh { addr, .. } => (addr, A32_LDST_IMM8_MAX),
76            _ => return Ok(None),
77        };
78        if addr.offset_reg.is_none() && (addr.offset as u32) <= imm_max {
79            return Ok(None);
80        }
81        let mut bytes = Vec::new();
82        let (base, residual) = a32_effective_base(&mut bytes, addr, imm_max)?;
83        // Re-encode the op against [base, #residual] (immediate form, in range
84        // by construction → this recursion hits the immediate arms, not this
85        // helper again).
86        let imm_addr = MemAddr::imm(base, residual as i32);
87        let imm_op = match op {
88            ArmOp::Ldr { rd, .. } => ArmOp::Ldr {
89                rd: *rd,
90                addr: imm_addr,
91            },
92            ArmOp::Str { rd, .. } => ArmOp::Str {
93                rd: *rd,
94                addr: imm_addr,
95            },
96            ArmOp::Ldrb { rd, .. } => ArmOp::Ldrb {
97                rd: *rd,
98                addr: imm_addr,
99            },
100            ArmOp::Strb { rd, .. } => ArmOp::Strb {
101                rd: *rd,
102                addr: imm_addr,
103            },
104            ArmOp::Ldrh { rd, .. } => ArmOp::Ldrh {
105                rd: *rd,
106                addr: imm_addr,
107            },
108            ArmOp::Strh { rd, .. } => ArmOp::Strh {
109                rd: *rd,
110                addr: imm_addr,
111            },
112            ArmOp::Ldrsb { rd, .. } => ArmOp::Ldrsb {
113                rd: *rd,
114                addr: imm_addr,
115            },
116            ArmOp::Ldrsh { rd, .. } => ArmOp::Ldrsh {
117                rd: *rd,
118                addr: imm_addr,
119            },
120            _ => unreachable!(),
121        };
122        bytes.extend(self.encode_arm(&imm_op)?);
123        Ok(Some(bytes))
124    }
125
126    /// #594: A32 expansion of `ArmOp::CallIndirect` — mirror of the Thumb-2
127    /// arm (same contract: R11 holds the function-pointer table base, entry
128    /// `i` is a 4-byte code address, R12 is the encoder-scratch register):
129    ///
130    /// ```text
131    /// MOVW r12, #size        ; #642: table size (compile-time immediate)
132    /// [MOVT r12, #size>>16]  ; only when size exceeds 16 bits
133    /// CMP  idx, r12          ; bounds guard: index >= size must TRAP
134    /// BLO  +1 insn           ; skip the trap when in bounds
135    /// UDF                    ; WASM Core §4.4.8 out-of-bounds trap
136    /// MOV r12, idx, LSL #2   ; table byte offset
137    /// LDR r12, [r11, r12]    ; load function pointer
138    /// BLX r12                ; indirect call
139    /// ```
140    ///
141    /// #650, `table_byte_offset != 0` (a non-zero table of the contiguous
142    /// R11 region): the pointer load becomes
143    /// `ADD r12, r11, r12; LDR r12, [r12, #offset]` — offset 0 keeps the
144    /// single-load form (single-table modules byte-identical by
145    /// construction).
146    ///
147    /// #664, `null_check` (the table has null slots, linked as ZERO words
148    /// per the layout contract): `CMP r12, #0; BNE +1; UDF` between the
149    /// pointer load and the `BLX` — a call reaching an uninitialized slot
150    /// traps (§4.4.8). `false` keeps the expansion byte-identical.
151    ///
152    /// #676, `type_check` (heterogeneous table): the §4.4.8 type check is
153    /// discharged at RUNTIME against the type-id sidecar — after the bounds
154    /// guard, `MOV r12, idx, LSL #2; ADD r12, r11, r12;
155    /// LDR r12, [r12, #type_off]; CMP r12, #expected_id; BEQ +1; UDF`
156    /// (mirror of the Thumb-2 arm; the dispatch tail recomputes `idx*4`).
157    /// Null slots carry the reserved class id 0, subsuming the #664 null
158    /// trap. `None` (every homogeneous table — the verdict discharged at
159    /// COMPILE time by the closed-world verification, see the #642 selector
160    /// guard) emits nothing and keeps the expansion byte-identical.
161    fn encode_arm_call_indirect(
162        table_index_reg: &Reg,
163        table_size: u32,
164        table_byte_offset: u32,
165        null_check: bool,
166        type_check: Option<(u32, u32)>,
167    ) -> Vec<u8> {
168        let idx = reg_to_bits(table_index_reg);
169        let mut bytes = Vec::with_capacity(32);
170        // MOVW r12, #(size & 0xFFFF) — cond=E 0011 0000 imm4 Rd imm12.
171        let size_lo = table_size & 0xFFFF;
172        let movw: u32 = 0xE300_0000 | ((size_lo >> 12) << 16) | (12 << 12) | (size_lo & 0xFFF);
173        bytes.extend_from_slice(&movw.to_le_bytes());
174        // MOVT r12, #(size >> 16) — only for a table size above 16 bits.
175        let size_hi = table_size >> 16;
176        if size_hi != 0 {
177            let movt: u32 = 0xE340_0000 | ((size_hi >> 12) << 16) | (12 << 12) | (size_hi & 0xFFF);
178            bytes.extend_from_slice(&movt.to_le_bytes());
179        }
180        // CMP idx, r12 — cond=E, opcode=1010, S=1, Rn=idx, Rm=r12.
181        let cmp: u32 = 0xE150_000C | (idx << 16);
182        bytes.extend_from_slice(&cmp.to_le_bytes());
183        // BLO +1 insn (skip the UDF when index < size) — cond=LO(0011),
184        // imm24=0: target = branch + 8.
185        bytes.extend_from_slice(&0x3A00_0000u32.to_le_bytes());
186        // UDF — permanently undefined (same trap idiom as the A32 div-by-zero
187        // guards): call_indirect out-of-bounds trap.
188        bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
189        // #676: runtime type check for a heterogeneous table — load the
190        // indexed slot's structural class id from the type-id sidecar and
191        // trap on mismatch (§4.4.8). Mirror of the Thumb-2 arm; `None`
192        // emits nothing (homogeneous tables byte-identical by construction).
193        if let Some((expected_id, type_off)) = type_check {
194            // RQ-61-IMMRANGE (#1072): these `debug_assert`s are compiled out
195            // in release, where the `& 0xFF` / `& 0xFFF` masks below would
196            // silently TRUNCATE an out-of-range value (id 256 compares as 0,
197            // letting a NULL slot pass the §4.4.8 check). The enforcement
198            // claim is DEMONSTRATED, not assumed: the sole `Some` producer is
199            // `resolve_runtime_type_check` (instruction_selector.rs), which
200            // loud-declines id > 255 and offset > 4095, and
201            // `test_676_call_indirect_runtime_check_range_declines` trips
202            // both declines (mutation-checked: disabling either turns it red).
203            debug_assert!(expected_id <= 255, "selector enforces the CMP imm8 range");
204            debug_assert!(type_off <= 4095, "selector enforces the LDR imm12 range");
205            // MOV r12, idx, LSL #2 (same as the dispatch tail's scale).
206            bytes.extend_from_slice(&(0xE1A0C000u32 | (2 << 7) | idx).to_le_bytes());
207            // ADD r12, r11, r12 — data-processing ADD (register).
208            bytes.extend_from_slice(&0xE08BC00Cu32.to_le_bytes());
209            // LDR r12, [r12, #type_off] — immediate offset, P=1 U=1 L=1.
210            bytes.extend_from_slice(&(0xE59CC000u32 | (type_off & 0xFFF)).to_le_bytes());
211            // CMP r12, #expected_id — data-processing CMP (immediate).
212            bytes.extend_from_slice(&(0xE35C_0000u32 | (expected_id & 0xFF)).to_le_bytes());
213            // BEQ +1 insn (skip the UDF when the class id matches) —
214            // cond=EQ(0000), imm24=0: target = branch + 8.
215            bytes.extend_from_slice(&0x0A00_0000u32.to_le_bytes());
216            // UDF — the §4.4.8 type-mismatch trap.
217            bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
218        }
219        // MOV r12, idx, LSL #2 — data-processing MOV, register op2 with
220        // imm5=2/LSL: cond=E, opcode=1101, S=0, Rd=r12.
221        let mov: u32 = 0xE1A0C000 | (2 << 7) | idx;
222        bytes.extend_from_slice(&mov.to_le_bytes());
223        if table_byte_offset == 0 {
224            // Table 0 (base = R11 itself): the pre-#650 single-load form.
225            // LDR r12, [r11, r12] — register offset, P=1 U=1 B=0 W=0 L=1.
226            let ldr: u32 = 0xE79BC00C;
227            bytes.extend_from_slice(&ldr.to_le_bytes());
228        } else {
229            // #650: fold the table's compile-time base offset into the
230            // pointer load via the LDR imm12 form.
231            assert!(
232                table_byte_offset <= 4095,
233                "call_indirect table base offset {table_byte_offset} exceeds \
234                 LDR imm12 — the selector must have declined this (#650)"
235            );
236            // ADD r12, r11, r12 — data-processing ADD (register).
237            bytes.extend_from_slice(&0xE08BC00Cu32.to_le_bytes());
238            // LDR r12, [r12, #offset] — immediate offset, P=1 U=1 L=1.
239            let ldr: u32 = 0xE59CC000 | (table_byte_offset & 0xFFF);
240            bytes.extend_from_slice(&ldr.to_le_bytes());
241        }
242        // #664: null-slot trap — only when the table image has null slots
243        // (zero-linked words). A fully-initialized table keeps the pre-#664
244        // bytes identical by construction.
245        if null_check {
246            // CMP r12, #0 — data-processing CMP (immediate), Rn=r12.
247            bytes.extend_from_slice(&0xE35C_0000u32.to_le_bytes());
248            // BNE +1 insn (skip the UDF when the pointer is non-null) —
249            // cond=NE(0001), imm24=0: target = branch + 8.
250            bytes.extend_from_slice(&0x1A00_0000u32.to_le_bytes());
251            // UDF — the §4.4.8 uninitialized-element trap (same idiom as
252            // the bounds guard).
253            bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
254        }
255        // BLX r12 — cond=E, 0001 0010 1111 1111 1111 0011, Rm=r12.
256        let blx: u32 = 0xE12FFF3C;
257        bytes.extend_from_slice(&blx.to_le_bytes());
258        bytes
259    }
260
261    /// #615: A32 (ARM-mode) expansions for the multi-instruction ops that the
262    /// Thumb-2 encoder expands but the A32 arm previously encoded as a single
263    /// literal NOP (`0xE1A00000`) — i64 mul / shifts / rotates / comparisons /
264    /// eqz, plus i64 const/load/store/extend/wrap and the i32 SetCond /
265    /// SelectMove pseudo-ops. Each expansion mirrors its Thumb-2 twin's
266    /// register contract and semantics exactly (A32 conditional execution
267    /// replaces the IT blocks). Returns `Ok(None)` for ops this helper does
268    /// not handle; the caller's match encodes or loudly rejects those.
269    fn encode_arm_expanded(&self, op: &ArmOp) -> Result<Option<Vec<u8>>> {
270        use synth_synthesis::Condition;
271
272        /// A32 condition-field bits (instruction bits [31:28]).
273        fn cond_bits(cond: &Condition) -> u32 {
274            match cond {
275                Condition::EQ => 0x0,
276                Condition::NE => 0x1,
277                Condition::HS => 0x2, // CS: unsigned >=
278                Condition::LO => 0x3, // CC: unsigned <
279                Condition::HI => 0x8, // unsigned >
280                Condition::LS => 0x9, // unsigned <=
281                Condition::GE => 0xA,
282                Condition::LT => 0xB,
283                Condition::GT => 0xC,
284                Condition::LE => 0xD,
285            }
286        }
287        fn w(b: &mut Vec<u8>, word: u32) {
288            b.extend_from_slice(&word.to_le_bytes());
289        }
290        /// MOV<cond> rd, #imm (rotated-immediate form; only 0/1 used here).
291        fn mov_cond_imm(b: &mut Vec<u8>, cond: u32, rd: u32, imm: u32) {
292            w(b, (cond << 28) | 0x03A0_0000 | (rd << 12) | imm);
293        }
294        /// After a flag-setting pair: MOV<cond> rd,#1 ; MOV<!cond> rd,#0.
295        fn set_cond(b: &mut Vec<u8>, cond: &Condition, rd: u32) {
296            mov_cond_imm(b, cond_bits(cond), rd, 1);
297            mov_cond_imm(b, cond_bits(&cond.invert()), rd, 0);
298        }
299        /// CMP rn, rm (register form).
300        fn cmp_reg(b: &mut Vec<u8>, rn: u32, rm: u32) {
301            w(b, 0xE150_0000 | (rn << 16) | rm);
302        }
303        /// SBCS rd, rn, rm — the 64-bit compare idiom's high-word subtract.
304        fn sbcs(b: &mut Vec<u8>, rd: u32, rn: u32, rm: u32) {
305            w(b, 0xE0D0_0000 | (rn << 16) | (rd << 12) | rm);
306        }
307        /// MOVW rd, #imm16.
308        fn movw(b: &mut Vec<u8>, rd: u32, v: u32) {
309            w(
310                b,
311                0xE300_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
312            );
313        }
314        /// MOVT rd, #imm16.
315        fn movt(b: &mut Vec<u8>, rd: u32, v: u32) {
316            w(
317                b,
318                0xE340_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
319            );
320        }
321        /// Register-controlled shift: MOV rd, rn, <LSL|LSR|ASR> rs.
322        /// `ty`: 0=LSL, 1=LSR, 2=ASR. A32 uses the bottom byte of rs;
323        /// amounts of 32 or more yield 0 (LSL/LSR) or all-sign (ASR) — same
324        /// semantics the Thumb-2 expansions rely on.
325        fn shift_reg(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, rs: u32) {
326            w(b, 0xE1A0_0010 | (rd << 12) | (rs << 8) | (ty << 5) | rn);
327        }
328        const LSL: u32 = 0;
329        const LSR: u32 = 1;
330        const ASR: u32 = 2;
331        /// Immediate-shift move: MOV rd, rn, <LSL|LSR|ASR> #imm.
332        fn shift_imm(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, imm: u32) {
333            w(
334                b,
335                0xE1A0_0000 | (rd << 12) | ((imm & 0x1F) << 7) | (ty << 5) | rn,
336            );
337        }
338        /// Data-processing register form: `base | rn<<16 | rd<<12 | rm`.
339        /// `base` carries cond/opcode/S (e.g. 0xE090_0000 = ADDS).
340        fn dp_reg(b: &mut Vec<u8>, base: u32, rd: u32, rn: u32, rm: u32) {
341            w(b, base | (rn << 16) | (rd << 12) | rm);
342        }
343        /// Data-processing with an immediate-shifted register operand:
344        /// `<op> rd, rn, rm, <LSL|LSR|ASR> #imm` — the A32 barrel shifter
345        /// folds a shift into the second operand for free. #1021 uses this to
346        /// run the popcnt SWAR fold on R12 alone (no second scratch, so R11 —
347        /// the linear-memory base — is never touched).
348        fn dp_reg_shift(b: &mut Vec<u8>, base: u32, rd: u32, rn: u32, rm: u32, ty: u32, imm: u32) {
349            w(
350                b,
351                base | (rn << 16) | (rd << 12) | ((imm & 0x1F) << 7) | (ty << 5) | rm,
352            );
353        }
354        /// ORR rd, rd, rm, LSR #31 — the carry-propagation idiom of the
355        /// shift-subtract division loop (bring rm's MSB into rd's bit 0).
356        fn orr_lsr31(b: &mut Vec<u8>, rd: u32, rm: u32) {
357            w(
358                b,
359                0xE180_0000 | (rd << 16) | (rd << 12) | (31 << 7) | (1 << 5) | rm,
360            );
361        }
362        /// 64-bit two's-complement negate of the lo:hi pair (MVN/MVN/ADDS/ADC).
363        fn negate64(b: &mut Vec<u8>, lo: u32, hi: u32) {
364            w(b, 0xE1E0_0000 | (lo << 12) | lo); //           MVN  lo, lo
365            w(b, 0xE1E0_0000 | (hi << 12) | hi); //           MVN  hi, hi
366            w(b, 0xE290_0001 | (lo << 16) | (lo << 12)); //   ADDS lo, lo, #1
367            w(b, 0xE2A0_0000 | (hi << 16) | (hi << 12)); //   ADC  hi, hi, #0
368        }
369        /// TST x, x ; BPL +4-instructions — the "skip the negate64 when the
370        /// sign bit is clear" guard of the signed div/rem arms.
371        fn skip_negate_if_positive(b: &mut Vec<u8>, x: u32) {
372            w(b, 0xE110_0000 | (x << 16) | x); // TST x, x
373            w(b, 0x5A00_0003); //                 BPL +4 insns (past negate64)
374        }
375        /// The 64-iteration shift-subtract division loop — A32 transcription
376        /// of the Thumb-2 #610 core: dividend R0:R1, divisor R2:R3, quotient
377        /// R4:R5, remainder R6:R7, loop counter in `counter` (R12 or R8).
378        fn div_loop(b: &mut Vec<u8>, counter: u32) {
379            w(b, 0xE3A0_0040 | (counter << 12)); // MOV counter, #64
380            let loop_start = b.len();
381            // quotient <<= 1
382            shift_imm(b, LSL, 5, 5, 1);
383            orr_lsr31(b, 5, 4);
384            shift_imm(b, LSL, 4, 4, 1);
385            // remainder <<= 1, OR in dividend MSB
386            shift_imm(b, LSL, 7, 7, 1);
387            orr_lsr31(b, 7, 6);
388            shift_imm(b, LSL, 6, 6, 1);
389            orr_lsr31(b, 6, 1);
390            // dividend <<= 1
391            shift_imm(b, LSL, 1, 1, 1);
392            orr_lsr31(b, 1, 0);
393            shift_imm(b, LSL, 0, 0, 1);
394            // if remainder >= divisor (64-bit unsigned): subtract, set q bit
395            w(b, 0xE157_0003); // CMP R7, R3      (high words)
396            w(b, 0x8A00_0002); // BHI .subtract   (+2 insns)
397            w(b, 0x3A00_0004); // BLO .next       (+4 insns)
398            w(b, 0xE156_0002); // CMP R6, R2      (low words, highs equal)
399            w(b, 0x3A00_0002); // BLO .next       (+2 insns)
400            w(b, 0xE056_6002); // .subtract: SUBS R6, R6, R2
401            w(b, 0xE0C7_7003); //            SBC  R7, R7, R3
402            w(b, 0xE384_4001); //            ORR  R4, R4, #1
403            // .next: decrement and loop
404            w(b, 0xE250_0001 | (counter << 16) | (counter << 12)); // SUBS counter, #1
405            let diff = (loop_start as i64) - (b.len() as i64 + 8);
406            w(b, 0x1A00_0000 | (((diff / 4) as u32) & 0x00FF_FFFF)); // BNE loop
407        }
408        /// 32-bit population count on working register `x` — A32 transcription
409        /// of the Thumb-2 I64Popcnt per-word core (mul-based fold): `c` is the
410        /// constant register, R12 the shifted temp. Both are clobbered.
411        fn popcnt_word(b: &mut Vec<u8>, x: u32, c: u32) {
412            // x = x - ((x >> 1) & 0x55555555)
413            shift_imm(b, LSR, 12, x, 1);
414            movw(b, c, 0x5555);
415            movt(b, c, 0x5555);
416            dp_reg(b, 0xE000_0000, 12, 12, c); // AND R12, R12, c
417            dp_reg(b, 0xE040_0000, x, x, 12); //  SUB x, x, R12
418            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
419            movw(b, c, 0x3333);
420            movt(b, c, 0x3333);
421            dp_reg(b, 0xE000_0000, 12, x, c); //  AND R12, x, c
422            shift_imm(b, LSR, x, x, 2);
423            dp_reg(b, 0xE000_0000, x, x, c); //   AND x, x, c
424            dp_reg(b, 0xE080_0000, x, x, 12); //  ADD x, x, R12
425            // x = (x + (x >> 4)) & 0x0F0F0F0F
426            shift_imm(b, LSR, 12, x, 4);
427            dp_reg(b, 0xE080_0000, x, x, 12); //  ADD x, x, R12
428            movw(b, c, 0x0F0F);
429            movt(b, c, 0x0F0F);
430            dp_reg(b, 0xE000_0000, x, x, c); //   AND x, x, c
431            // x = (x * 0x01010101) >> 24
432            movw(b, c, 0x0101);
433            movt(b, c, 0x0101);
434            w(b, 0xE000_0090 | (x << 16) | (c << 8) | x); // MUL x, x, c
435            shift_imm(b, LSR, x, x, 24);
436        }
437
438        let mut b: Vec<u8> = Vec::new();
439        match op {
440            // SetCond: materialize a flags-predicate as 0/1 — the A32 twin of
441            // the Thumb `ITE cond; MOV rd,#1; MOV rd,#0`.
442            ArmOp::SetCond { rd, cond } => {
443                set_cond(&mut b, cond, reg_to_bits(rd));
444            }
445
446            // SelectMove: conditional register move (Thumb: IT cond; MOV).
447            ArmOp::SelectMove { rd, rm, cond } => {
448                w(
449                    &mut b,
450                    (cond_bits(cond) << 28)
451                        | 0x01A0_0000
452                        | (reg_to_bits(rd) << 12)
453                        | reg_to_bits(rm),
454                );
455            }
456
457            // I64SetCond: compare two i64 register pairs, 0/1 into rd.
458            // EQ/NE: CMP lo,lo; CMPEQ hi,hi (only if lows equal); set.
459            // Ordered: CMP lo,lo; SBCS rd,hi,hi; set — with the same
460            // operand-swap + condition mapping as the Thumb-2 arm.
461            ArmOp::I64SetCond {
462                rd,
463                rn_lo,
464                rn_hi,
465                rm_lo,
466                rm_hi,
467                cond,
468            } => {
469                let rd_b = reg_to_bits(rd);
470                let (n_lo, n_hi, m_lo, m_hi) = (
471                    reg_to_bits(rn_lo),
472                    reg_to_bits(rn_hi),
473                    reg_to_bits(rm_lo),
474                    reg_to_bits(rm_hi),
475                );
476                match cond {
477                    Condition::EQ | Condition::NE => {
478                        cmp_reg(&mut b, n_lo, m_lo);
479                        // CMP<EQ> rn_hi, rm_hi — compare highs only if lows equal.
480                        w(&mut b, 0x0150_0000 | (n_hi << 16) | m_hi);
481                        set_cond(&mut b, cond, rd_b);
482                    }
483                    // (swap operands?, condition after SBCS) per the Thumb arm:
484                    // LT/GE/LO/HS compare (rn, rm); GT/LE/HI/LS swap to (rm, rn).
485                    Condition::LT => {
486                        cmp_reg(&mut b, n_lo, m_lo);
487                        sbcs(&mut b, rd_b, n_hi, m_hi);
488                        set_cond(&mut b, &Condition::LT, rd_b);
489                    }
490                    Condition::GE => {
491                        cmp_reg(&mut b, n_lo, m_lo);
492                        sbcs(&mut b, rd_b, n_hi, m_hi);
493                        set_cond(&mut b, &Condition::GE, rd_b);
494                    }
495                    Condition::GT => {
496                        cmp_reg(&mut b, m_lo, n_lo);
497                        sbcs(&mut b, rd_b, m_hi, n_hi);
498                        set_cond(&mut b, &Condition::LT, rd_b);
499                    }
500                    Condition::LE => {
501                        cmp_reg(&mut b, m_lo, n_lo);
502                        sbcs(&mut b, rd_b, m_hi, n_hi);
503                        set_cond(&mut b, &Condition::GE, rd_b);
504                    }
505                    Condition::LO => {
506                        cmp_reg(&mut b, n_lo, m_lo);
507                        sbcs(&mut b, rd_b, n_hi, m_hi);
508                        set_cond(&mut b, &Condition::LO, rd_b);
509                    }
510                    Condition::HS => {
511                        cmp_reg(&mut b, n_lo, m_lo);
512                        sbcs(&mut b, rd_b, n_hi, m_hi);
513                        set_cond(&mut b, &Condition::HS, rd_b);
514                    }
515                    Condition::HI => {
516                        cmp_reg(&mut b, m_lo, n_lo);
517                        sbcs(&mut b, rd_b, m_hi, n_hi);
518                        set_cond(&mut b, &Condition::LO, rd_b);
519                    }
520                    Condition::LS => {
521                        cmp_reg(&mut b, m_lo, n_lo);
522                        sbcs(&mut b, rd_b, m_hi, n_hi);
523                        set_cond(&mut b, &Condition::HS, rd_b);
524                    }
525                }
526            }
527
528            // I64SetCondZ: ORRS rd, lo, hi sets Z iff the pair is zero.
529            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
530                let rd_b = reg_to_bits(rd);
531                w(
532                    &mut b,
533                    0xE190_0000 | (reg_to_bits(rn_lo) << 16) | (rd_b << 12) | reg_to_bits(rn_hi),
534                );
535                set_cond(&mut b, &Condition::EQ, rd_b);
536            }
537
538            // i64 comparison wrappers: delegate to I64SetCond/Z, mirroring the
539            // Thumb-2 delegation arms.
540            ArmOp::I64Eqz { rd, rnlo, rnhi } => {
541                return self
542                    .encode_arm(&ArmOp::I64SetCondZ {
543                        rd: *rd,
544                        rn_lo: *rnlo,
545                        rn_hi: *rnhi,
546                    })
547                    .map(Some);
548            }
549            ArmOp::I64Eq {
550                rd,
551                rnlo,
552                rnhi,
553                rmlo,
554                rmhi,
555            }
556            | ArmOp::I64Ne {
557                rd,
558                rnlo,
559                rnhi,
560                rmlo,
561                rmhi,
562            }
563            | ArmOp::I64LtS {
564                rd,
565                rnlo,
566                rnhi,
567                rmlo,
568                rmhi,
569            }
570            | ArmOp::I64LtU {
571                rd,
572                rnlo,
573                rnhi,
574                rmlo,
575                rmhi,
576            }
577            | ArmOp::I64LeS {
578                rd,
579                rnlo,
580                rnhi,
581                rmlo,
582                rmhi,
583            }
584            | ArmOp::I64LeU {
585                rd,
586                rnlo,
587                rnhi,
588                rmlo,
589                rmhi,
590            }
591            | ArmOp::I64GtS {
592                rd,
593                rnlo,
594                rnhi,
595                rmlo,
596                rmhi,
597            }
598            | ArmOp::I64GtU {
599                rd,
600                rnlo,
601                rnhi,
602                rmlo,
603                rmhi,
604            }
605            | ArmOp::I64GeS {
606                rd,
607                rnlo,
608                rnhi,
609                rmlo,
610                rmhi,
611            }
612            | ArmOp::I64GeU {
613                rd,
614                rnlo,
615                rnhi,
616                rmlo,
617                rmhi,
618            } => {
619                let cond = match op {
620                    ArmOp::I64Eq { .. } => Condition::EQ,
621                    ArmOp::I64Ne { .. } => Condition::NE,
622                    ArmOp::I64LtS { .. } => Condition::LT,
623                    ArmOp::I64LtU { .. } => Condition::LO,
624                    ArmOp::I64LeS { .. } => Condition::LE,
625                    ArmOp::I64LeU { .. } => Condition::LS,
626                    ArmOp::I64GtS { .. } => Condition::GT,
627                    ArmOp::I64GtU { .. } => Condition::HI,
628                    ArmOp::I64GeS { .. } => Condition::GE,
629                    _ => Condition::HS,
630                };
631                return self
632                    .encode_arm(&ArmOp::I64SetCond {
633                        rd: *rd,
634                        rn_lo: *rnlo,
635                        rn_hi: *rnhi,
636                        rm_lo: *rmlo,
637                        rm_hi: *rmhi,
638                        cond,
639                    })
640                    .map(Some);
641            }
642
643            // I64Mul: cross products into R12, then UMULL — same sequence and
644            // ordering as the Thumb-2 arm (R12 is encoder scratch, #212).
645            ArmOp::I64Mul {
646                rd_lo,
647                rd_hi,
648                rn_lo,
649                rn_hi,
650                rm_lo,
651                rm_hi,
652            } => {
653                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
654                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
655                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
656                // MUL R12, rn_lo, rm_hi   (R12 = a_lo * b_hi)
657                w(&mut b, 0xE000_0090 | (12 << 16) | (mh << 8) | nl);
658                // MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
659                w(
660                    &mut b,
661                    0xE020_0090 | (12 << 16) | (12 << 12) | (ml << 8) | nh,
662                );
663                // UMULL rd_lo, rd_hi, rn_lo, rm_lo
664                w(
665                    &mut b,
666                    0xE080_0090 | (dh << 16) | (dl << 12) | (ml << 8) | nl,
667                );
668                // ADD rd_hi, rd_hi, R12
669                w(&mut b, 0xE080_0000 | (dh << 16) | (dh << 12) | 12);
670            }
671
672            // I64Shl / I64ShrU / I64ShrS: same small/large-shift structure as
673            // the Thumb-2 arms. #1048: the expansion must never write its own
674            // input operands — the pre-#1048 A32 arms masked the amount in
675            // place (`AND ml, ml, #63`) and used the amount's home high
676            // register as scratch, identically to the Thumb-2 defect. R12
677            // (encoder scratch, never allocatable, #212) is the ONLY
678            // temporary; the masked amount is re-derived from the untouched
679            // rm_lo where a second live temp would otherwise be needed.
680            // Register-controlled shifts >= 32 yield 0, which the small path
681            // relies on for n = 0. Same #1039-style loud alias guards as the
682            // Thumb-2 arms.
683            ArmOp::I64Shl {
684                rd_lo,
685                rd_hi,
686                rn_lo,
687                rn_hi,
688                rm_lo,
689                rm_hi: _,
690            } => {
691                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
692                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
693                let ml = reg_to_bits(rm_lo);
694                if dh == nl || dh == ml {
695                    return Err(synth_core::Error::synthesis(format!(
696                        "I64Shl (A32): rd_hi {rd_hi:?} aliases an input ({rn_lo:?}/{rm_lo:?}) still live inside the expansion (#1048)"
697                    )));
698                }
699                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
700                w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
701                w(&mut b, 0x5A00_0007); //                            BPL  .large
702                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
703                shift_reg(&mut b, LSL, dh, nh, 12); //               dh = hi << n
704                w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB  r12, r12, #32
705                shift_reg(&mut b, LSR, 12, nl, 12); //               r12 = lo >> (32-n)
706                w(&mut b, 0xE180_0000 | (dh << 16) | (dh << 12) | 12); // ORR dh, dh, r12
707                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
708                shift_reg(&mut b, LSL, dl, nl, 12); //               dl = lo << n
709                w(&mut b, 0xEA00_0001); //                            B    .done
710                shift_reg(&mut b, LSL, dh, nl, 12); //               .large: dh = lo << (n-32)
711                w(&mut b, 0xE3A0_0000 | (dl << 12)); //              MOV  dl, #0
712            }
713            ArmOp::I64ShrU {
714                rd_lo,
715                rd_hi,
716                rn_lo,
717                rn_hi,
718                rm_lo,
719                rm_hi: _,
720            } => {
721                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
722                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
723                let ml = reg_to_bits(rm_lo);
724                if dl == nh || dl == ml {
725                    return Err(synth_core::Error::synthesis(format!(
726                        "I64ShrU (A32): rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
727                    )));
728                }
729                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
730                w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
731                w(&mut b, 0x5A00_0007); //                            BPL  .large
732                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
733                shift_reg(&mut b, LSR, dl, nl, 12); //               dl = lo >> n
734                w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB  r12, r12, #32
735                shift_reg(&mut b, LSL, 12, nh, 12); //               r12 = hi << (32-n)
736                w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | 12); // ORR dl, dl, r12
737                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
738                shift_reg(&mut b, LSR, dh, nh, 12); //               dh = hi >> n
739                w(&mut b, 0xEA00_0001); //                            B    .done
740                shift_reg(&mut b, LSR, dl, nh, 12); //               .large: dl = hi >> (n-32)
741                w(&mut b, 0xE3A0_0000 | (dh << 12)); //              MOV  dh, #0
742            }
743            ArmOp::I64ShrS {
744                rd_lo,
745                rd_hi,
746                rn_lo,
747                rn_hi,
748                rm_lo,
749                rm_hi: _,
750            } => {
751                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
752                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
753                let ml = reg_to_bits(rm_lo);
754                if dl == nh || dl == ml {
755                    return Err(synth_core::Error::synthesis(format!(
756                        "I64ShrS (A32): rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
757                    )));
758                }
759                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
760                w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
761                w(&mut b, 0x5A00_0007); //                            BPL  .large
762                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
763                shift_reg(&mut b, LSR, dl, nl, 12); //               dl = lo >> n
764                w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB  r12, r12, #32
765                shift_reg(&mut b, LSL, 12, nh, 12); //               r12 = hi << (32-n)
766                w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | 12); // ORR dl, dl, r12
767                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
768                shift_reg(&mut b, ASR, dh, nh, 12); //               dh = hi >> n (arith)
769                w(&mut b, 0xEA00_0001); //                            B    .done
770                shift_reg(&mut b, ASR, dl, nh, 12); //               .large: dl = hi >> (n-32)
771                w(&mut b, 0xE1A0_0040 | (dh << 12) | (31 << 7) | nh); // ASR dh, nh, #31
772            }
773
774            // I64Rotl / I64Rotr: the #610 fixed-ABI wrapper (A32 form) around
775            // the same fixed-register core as the Thumb-2 arms — value in
776            // R0:R1, amount in R2, scratch R3 + R12.
777            ArmOp::I64Rotl {
778                rdlo,
779                rdhi,
780                rnlo,
781                rnhi,
782                shift,
783            } => {
784                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
785                for word in [
786                    0xE202_203Fu32, // AND  R2, R2, #63   (mask amount mod 64)
787                    0xE252_3020,    // SUBS R3, R2, #32   (R3 = n-32, sets N)
788                    0x5A00_0007,    // BPL  .large        (n >= 32)
789                    // --- small rotation (n < 32) ---
790                    0xE262_3020, // RSB  R3, R2, #32   (R3 = 32-n)
791                    0xE1A0_C330, // LSR  R12, R0, R3   (lo >> (32-n))
792                    0xE1A0_3331, // LSR  R3, R1, R3    (hi >> (32-n))
793                    0xE1A0_1211, // LSL  R1, R1, R2    (hi << n)
794                    0xE181_100C, // ORR  R1, R1, R12   (new_hi)
795                    0xE1A0_0210, // LSL  R0, R0, R2    (lo << n)
796                    0xE180_0003, // ORR  R0, R0, R3    (new_lo)
797                    0xEA00_0007, // B    .done
798                    // --- large rotation (n >= 32), R3 = m = n-32 ---
799                    0xE263_2020, // RSB  R2, R3, #32   (R2 = 32-m = 64-n)
800                    0xE1A0_C231, // LSR  R12, R1, R2   (hi >> (64-n))
801                    0xE1A0_2230, // LSR  R2, R0, R2    (lo >> (64-n))
802                    0xE1A0_0310, // LSL  R0, R0, R3    (lo << m)
803                    0xE1A0_1311, // LSL  R1, R1, R3    (hi << m)
804                    0xE180_C00C, // ORR  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
805                    0xE181_0002, // ORR  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
806                    0xE1A0_100C, // MOV  R1, R12       (new_hi into place)
807                ] {
808                    w(&mut b, word);
809                }
810                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
811            }
812            ArmOp::I64Rotr {
813                rdlo,
814                rdhi,
815                rnlo,
816                rnhi,
817                shift,
818            } => {
819                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
820                for word in [
821                    0xE202_203Fu32, // AND  R2, R2, #63   (mask amount mod 64)
822                    0xE252_3020,    // SUBS R3, R2, #32   (R3 = n-32, sets N)
823                    0x5A00_0007,    // BPL  .large        (n >= 32)
824                    // --- small rotation (n < 32) ---
825                    0xE262_3020, // RSB  R3, R2, #32   (R3 = 32-n)
826                    0xE1A0_C311, // LSL  R12, R1, R3   (hi << (32-n))
827                    0xE1A0_3310, // LSL  R3, R0, R3    (lo << (32-n))
828                    0xE1A0_0230, // LSR  R0, R0, R2    (lo >> n)
829                    0xE180_000C, // ORR  R0, R0, R12   (new_lo)
830                    0xE1A0_1231, // LSR  R1, R1, R2    (hi >> n)
831                    0xE181_1003, // ORR  R1, R1, R3    (new_hi)
832                    0xEA00_0007, // B    .done
833                    // --- large rotation (n >= 32), R3 = m = n-32 ---
834                    0xE263_2020, // RSB  R2, R3, #32   (R2 = 32-m = 64-n)
835                    0xE1A0_C210, // LSL  R12, R0, R2   (lo << (64-n))
836                    0xE1A0_2211, // LSL  R2, R1, R2    (hi << (64-n))
837                    0xE1A0_1331, // LSR  R1, R1, R3    (hi >> m)
838                    0xE181_C00C, // ORR  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
839                    0xE1A0_1330, // LSR  R1, R0, R3    (lo >> m)
840                    0xE181_1002, // ORR  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
841                    0xE1A0_000C, // MOV  R0, R12       (new_lo into place)
842                ] {
843                    w(&mut b, word);
844                }
845                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
846            }
847
848            // I64Clz: CLZ(hi), or 32 + CLZ(lo) when hi == 0. Conditional
849            // execution replaces the Thumb branches; like the Thumb arm, the
850            // high word of the result pair (rnhi) is cleared last.
851            ArmOp::I64Clz { rd, rnlo, rnhi } => {
852                let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
853                w(&mut b, 0xE350_0000 | (hi << 16)); //              CMP   rnhi, #0
854                w(&mut b, 0x116F_0F10 | (rd_b << 12) | hi); //       CLZNE rd, rnhi
855                w(&mut b, 0x016F_0F10 | (rd_b << 12) | lo); //       CLZEQ rd, rnlo
856                w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
857                // #1048: the former trailing `MOV rnhi, #0` is GONE — it
858                // wrote the OPERAND's home high register (see the Thumb-2
859                // I64Clz comment). Callers that relied on the implicit clear
860                // emit their own explicit hi-zero op.
861            }
862
863            // I64Ctz: CLZ(RBIT(lo)), or 32 + CLZ(RBIT(hi)) when lo == 0.
864            // RBIT/CLZ leave the flags intact, so the CMP's Z survives to the
865            // conditional ADD.
866            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
867                let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
868                w(&mut b, 0xE350_0000 | (lo << 16)); //              CMP    rnlo, #0
869                w(&mut b, 0x16FF_0F30 | (rd_b << 12) | lo); //       RBITNE rd, rnlo
870                w(&mut b, 0x06FF_0F30 | (rd_b << 12) | hi); //       RBITEQ rd, rnhi
871                w(&mut b, 0xE16F_0F10 | (rd_b << 12) | rd_b); //     CLZ    rd, rd
872                w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
873                // #1048: no trailing `MOV rnhi, #0` — see I64Clz above.
874            }
875
876            // I64Const: MOVW/MOVT per half (MOVT elided when the half fits in
877            // 16 bits, mirroring the Thumb-2 arm).
878            ArmOp::I64Const { rdlo, rdhi, value } => {
879                let lo32 = *value as u32;
880                let hi32 = (*value >> 32) as u32;
881                movw(&mut b, reg_to_bits(rdlo), lo32 & 0xFFFF);
882                if lo32 > 0xFFFF {
883                    movt(&mut b, reg_to_bits(rdlo), lo32 >> 16);
884                }
885                movw(&mut b, reg_to_bits(rdhi), hi32 & 0xFFFF);
886                if hi32 > 0xFFFF {
887                    movt(&mut b, reg_to_bits(rdhi), hi32 >> 16);
888                }
889            }
890
891            // I64Ldr / I64Str: two word accesses at [base, #off] / #off+4.
892            // A register offset is materialized into IP once (the #206/#372
893            // hazard: dropping it would read the wrong address).
894            // RQ-63-ARMI64OFF (#1165): an offset the pair form cannot hold
895            // (> 0xFFB, so the high half's +4 leaves imm12) used to be a loud
896            // decline here — 46 of the 110 core-module declines in v0.62's ARM
897            // census, all on the `-b arm` (Arm32) default target. It is now
898            // MATERIALIZED (MOVW/MOVT + ADD, the A32 mirror of #382's Thumb-2
899            // `i64_effective_base`); in-range offsets stay byte-identical.
900            ArmOp::I64Ldr { rdlo, rdhi, addr } | ArmOp::I64Str { rdlo, rdhi, addr } => {
901                let (base, off) = a32_effective_base(&mut b, addr, A32_I64_PAIR_IMM12_MAX)?;
902                let base = reg_to_bits(&base);
903                let opc: u32 = if matches!(op, ArmOp::I64Ldr { .. }) {
904                    0xE590_0000 // LDR
905                } else {
906                    0xE580_0000 // STR
907                };
908                w(&mut b, opc | (base << 16) | (reg_to_bits(rdlo) << 12) | off);
909                w(
910                    &mut b,
911                    opc | (base << 16) | (reg_to_bits(rdhi) << 12) | (off + 4),
912                );
913            }
914
915            // I64ExtendI32S: rdlo = rn; rdhi = rdlo >> 31 (arithmetic).
916            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
917                if rdlo != rn {
918                    w(
919                        &mut b,
920                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
921                    );
922                }
923                w(
924                    &mut b,
925                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
926                );
927            }
928
929            // I64ExtendI32U: rdlo = rn; rdhi = 0.
930            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
931                if rdlo != rn {
932                    w(
933                        &mut b,
934                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
935                    );
936                }
937                w(&mut b, 0xE3A0_0000 | (reg_to_bits(rdhi) << 12));
938            }
939
940            // I64Extend8S / I64Extend16S: SXTB/SXTH then sign-fill the high word.
941            ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
942                w(
943                    &mut b,
944                    0xE6AF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
945                );
946                w(
947                    &mut b,
948                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
949                );
950            }
951            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
952                w(
953                    &mut b,
954                    0xE6BF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
955                );
956                w(
957                    &mut b,
958                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
959                );
960            }
961            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
962                if rdlo != rnlo {
963                    w(
964                        &mut b,
965                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
966                    );
967                }
968                w(
969                    &mut b,
970                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rnlo),
971                );
972            }
973
974            // I32WrapI64: take the low word. When rd == rnlo this is a genuine
975            // no-op (the one case where a NOP word is the correct encoding).
976            ArmOp::I32WrapI64 { rd, rnlo } => {
977                w(
978                    &mut b,
979                    0xE1A0_0000 | (reg_to_bits(rd) << 12) | reg_to_bits(rnlo),
980                );
981            }
982
983            // I64Add / I64Sub: the classic pair — ADDS lo + ADC hi (SUBS/SBC).
984            // The selector emits these as separate Adds/Adc ops; the fused
985            // variants are verification-constructed, but they encode for real.
986            ArmOp::I64Add {
987                rdlo,
988                rdhi,
989                rnlo,
990                rnhi,
991                rmlo,
992                rmhi,
993            } => {
994                dp_reg(
995                    &mut b,
996                    0xE090_0000, // ADDS
997                    reg_to_bits(rdlo),
998                    reg_to_bits(rnlo),
999                    reg_to_bits(rmlo),
1000                );
1001                dp_reg(
1002                    &mut b,
1003                    0xE0A0_0000, // ADC
1004                    reg_to_bits(rdhi),
1005                    reg_to_bits(rnhi),
1006                    reg_to_bits(rmhi),
1007                );
1008            }
1009            ArmOp::I64Sub {
1010                rdlo,
1011                rdhi,
1012                rnlo,
1013                rnhi,
1014                rmlo,
1015                rmhi,
1016            } => {
1017                dp_reg(
1018                    &mut b,
1019                    0xE050_0000, // SUBS
1020                    reg_to_bits(rdlo),
1021                    reg_to_bits(rnlo),
1022                    reg_to_bits(rmlo),
1023                );
1024                dp_reg(
1025                    &mut b,
1026                    0xE0C0_0000, // SBC
1027                    reg_to_bits(rdhi),
1028                    reg_to_bits(rnhi),
1029                    reg_to_bits(rmhi),
1030                );
1031            }
1032
1033            // I64And / I64Or / I64Xor: two independent word ops.
1034            ArmOp::I64And {
1035                rdlo,
1036                rdhi,
1037                rnlo,
1038                rnhi,
1039                rmlo,
1040                rmhi,
1041            }
1042            | ArmOp::I64Or {
1043                rdlo,
1044                rdhi,
1045                rnlo,
1046                rnhi,
1047                rmlo,
1048                rmhi,
1049            }
1050            | ArmOp::I64Xor {
1051                rdlo,
1052                rdhi,
1053                rnlo,
1054                rnhi,
1055                rmlo,
1056                rmhi,
1057            } => {
1058                let base = match op {
1059                    ArmOp::I64And { .. } => 0xE000_0000, // AND
1060                    ArmOp::I64Or { .. } => 0xE180_0000,  // ORR
1061                    _ => 0xE020_0000,                    // EOR
1062                };
1063                dp_reg(
1064                    &mut b,
1065                    base,
1066                    reg_to_bits(rdlo),
1067                    reg_to_bits(rnlo),
1068                    reg_to_bits(rmlo),
1069                );
1070                dp_reg(
1071                    &mut b,
1072                    base,
1073                    reg_to_bits(rdhi),
1074                    reg_to_bits(rnhi),
1075                    reg_to_bits(rmhi),
1076                );
1077            }
1078
1079            // I64DivU: binary long division — A32 transcription of the Thumb-2
1080            // #610/#613 arm (fixed-ABI marshal, zero-divisor trap, 64-round
1081            // shift-subtract core, quotient to R0:R1, result to rd pair).
1082            ArmOp::I64DivU {
1083                rdlo,
1084                rdhi,
1085                rnlo,
1086                rnhi,
1087                rmlo,
1088                rmhi,
1089                elide_zero_guard,
1090            } => {
1091                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1092                // #494 phase 2b: elided only under a certificate-discharged
1093                // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
1094                if !elide_zero_guard {
1095                    emit_a32_i64_divisor_zero_trap(&mut b);
1096                }
1097                w(&mut b, 0xE92D_00F0); // PUSH {R4-R7}
1098                for r in 4..8u32 {
1099                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1100                }
1101                div_loop(&mut b, 12); // counter in R12 (encoder scratch)
1102                w(&mut b, 0xE1A0_0004); // MOV R0, R4 (quotient lo)
1103                w(&mut b, 0xE1A0_1005); // MOV R1, R5 (quotient hi)
1104                w(&mut b, 0xE8BD_00F0); // POP {R4-R7}
1105                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1106            }
1107
1108            // I64DivS: sign-extract, unsigned core, conditional negate —
1109            // A32 transcription of the Thumb-2 arm.
1110            ArmOp::I64DivS {
1111                rdlo,
1112                rdhi,
1113                rnlo,
1114                rnhi,
1115                rmlo,
1116                rmhi,
1117                elide_zero_guard,
1118                elide_overflow_guard,
1119            } => {
1120                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1121                // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
1122                // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
1123                // the #633 overflow guard falls ONLY to
1124                // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
1125                // divisor-nonzero fact alone must keep it.
1126                if !elide_zero_guard {
1127                    emit_a32_i64_divisor_zero_trap(&mut b);
1128                }
1129                if !elide_overflow_guard {
1130                    // #633: INT64_MIN / -1 overflows — trap like the i32 path
1131                    // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
1132                    emit_a32_i64_divs_overflow_trap(&mut b);
1133                }
1134                w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1135                w(&mut b, 0xE021_9003); // EOR R9, R1, R3 (result sign in MSB)
1136                skip_negate_if_positive(&mut b, 1);
1137                negate64(&mut b, 0, 1);
1138                skip_negate_if_positive(&mut b, 3);
1139                negate64(&mut b, 2, 3);
1140                for r in 4..8u32 {
1141                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1142                }
1143                div_loop(&mut b, 8); // counter in R8 (saved above)
1144                w(&mut b, 0xE1A0_0004); // MOV R0, R4
1145                w(&mut b, 0xE1A0_1005); // MOV R1, R5
1146                skip_negate_if_positive(&mut b, 9);
1147                negate64(&mut b, 0, 1);
1148                w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1149                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1150            }
1151
1152            // I64RemU: same core as I64DivU, returns the remainder (R6:R7).
1153            ArmOp::I64RemU {
1154                rdlo,
1155                rdhi,
1156                rnlo,
1157                rnhi,
1158                rmlo,
1159                rmhi,
1160                elide_zero_guard,
1161            } => {
1162                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1163                if !elide_zero_guard {
1164                    emit_a32_i64_divisor_zero_trap(&mut b);
1165                }
1166                w(&mut b, 0xE92D_01F0); // PUSH {R4-R8}
1167                for r in 4..8u32 {
1168                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1169                }
1170                div_loop(&mut b, 8);
1171                w(&mut b, 0xE1A0_0006); // MOV R0, R6 (remainder lo)
1172                w(&mut b, 0xE1A0_1007); // MOV R1, R7 (remainder hi)
1173                w(&mut b, 0xE8BD_01F0); // POP {R4-R8}
1174                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1175            }
1176
1177            // I64RemS: remainder takes the DIVIDEND's sign (WASM semantics).
1178            ArmOp::I64RemS {
1179                rdlo,
1180                rdhi,
1181                rnlo,
1182                rnhi,
1183                rmlo,
1184                rmhi,
1185                elide_zero_guard,
1186            } => {
1187                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1188                if !elide_zero_guard {
1189                    emit_a32_i64_divisor_zero_trap(&mut b);
1190                }
1191                w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1192                w(&mut b, 0xE1A0_9001); // MOV R9, R1 (dividend sign)
1193                skip_negate_if_positive(&mut b, 1);
1194                negate64(&mut b, 0, 1);
1195                skip_negate_if_positive(&mut b, 3);
1196                negate64(&mut b, 2, 3);
1197                for r in 4..8u32 {
1198                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1199                }
1200                div_loop(&mut b, 8);
1201                w(&mut b, 0xE1A0_0006); // MOV R0, R6
1202                w(&mut b, 0xE1A0_1007); // MOV R1, R7
1203                skip_negate_if_positive(&mut b, 9);
1204                negate64(&mut b, 0, 1);
1205                w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1206                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1207            }
1208
1209            // Popcnt (i32): bit-twiddle expansion (no native A32 popcount),
1210            // mirroring the Thumb-2 arm's #1021 register contract: R12 is the
1211            // ONLY scratch. The previous transcription copied the old Thumb
1212            // contract's R11 borrow — but R11 is the linear-memory base on
1213            // this path too, so it inherited the same live miscompile. A32
1214            // has no ThumbExpandImm for 0xXYXYXYXY masks, so instead the
1215            // barrel shifter folds each shift into the mask AND itself
1216            // (`AND R12, R12, rd, LSR #n`), and step 2 recovers `x & C` from
1217            // one term via `x - (((x >> 2) & C) << 2) = x & C` — the second
1218            // temp disappears algebraically. Straight-line, no PUSH/POP,
1219            // nothing to skip on a trap edge.
1220            ArmOp::Popcnt { rd, rm } => {
1221                let rd_b = reg_to_bits(rd);
1222                // Defensive (#1021), same contract as the Thumb-2 arm.
1223                if rd_b >= 11 {
1224                    return Err(synth_core::Error::synthesis(
1225                        "Popcnt destination must be R0-R10: R11 is the linear-memory \
1226                         base and R12 is the expansion's scratch (#1021)",
1227                    ));
1228                }
1229                if rd != rm {
1230                    w(&mut b, 0xE1A0_0000 | (rd_b << 12) | reg_to_bits(rm)); // MOV rd, rm
1231                }
1232                // x = x - ((x >> 1) & 0x55555555)
1233                movw(&mut b, 12, 0x5555);
1234                movt(&mut b, 12, 0x5555);
1235                dp_reg_shift(&mut b, 0xE000_0000, 12, 12, rd_b, LSR, 1); // AND R12, R12, rd, LSR #1
1236                dp_reg(&mut b, 0xE040_0000, rd_b, rd_b, 12); //             SUB rd, rd, R12
1237                // x = (x & 0x33333333) + ((x >> 2) & 0x33333333), one temp:
1238                //   R12 = (x >> 2) & C; x - (R12 << 2) = x & C; then + R12.
1239                movw(&mut b, 12, 0x3333);
1240                movt(&mut b, 12, 0x3333);
1241                dp_reg_shift(&mut b, 0xE000_0000, 12, 12, rd_b, LSR, 2); // AND R12, R12, rd, LSR #2
1242                dp_reg_shift(&mut b, 0xE040_0000, rd_b, rd_b, 12, LSL, 2); // SUB rd, rd, R12, LSL #2
1243                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 12); //              ADD rd, rd, R12
1244                // x = (x + (x >> 4)) & 0x0F0F0F0F
1245                dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 4); // ADD rd, rd, rd, LSR #4
1246                movw(&mut b, 12, 0x0F0F);
1247                movt(&mut b, 12, 0x0F0F);
1248                dp_reg(&mut b, 0xE000_0000, rd_b, rd_b, 12); // AND rd, rd, R12
1249                // x += x >> 8; x += x >> 16; x &= 0x3F
1250                dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 8);
1251                dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 16);
1252                w(&mut b, 0xE200_003F | (rd_b << 16) | (rd_b << 12)); // AND rd, rd, #63
1253            }
1254
1255            // I64Popcnt: POPCNT(lo) + POPCNT(hi) — A32 transcription of the
1256            // Thumb-2 arm (R3/R4/R5 saved, mul-based per-word fold, high
1257            // result word rnhi cleared last, mirroring the Thumb contract).
1258            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
1259                let hi = reg_to_bits(rnhi);
1260                w(&mut b, 0xE92D_0038); // PUSH {R3, R4, R5}
1261                // #632 audit: route rnlo through R12 so a pair living at
1262                // (R3,R4) cannot read a clobbered R4 (sources read before any
1263                // scratch register they could occupy is written).
1264                w(&mut b, 0xE1A0_C000 | reg_to_bits(rnlo)); // MOV R12, rnlo
1265                w(&mut b, 0xE1A0_5000 | hi); //                MOV R5, rnhi
1266                w(&mut b, 0xE1A0_400C); //                     MOV R4, R12
1267                popcnt_word(&mut b, 4, 3);
1268                popcnt_word(&mut b, 5, 3);
1269                // #632: carry the count across the scratch restore in R12 —
1270                // rd is allocator-assigned and can land inside {R3,R4,R5};
1271                // the old `ADD rd, R4, R5` before the POP was destroyed by
1272                // the restore. R12 is never allocatable and never restored.
1273                dp_reg(&mut b, 0xE080_0000, 12, 4, 5); // ADD R12, R4, R5
1274                w(&mut b, 0xE8BD_0038); // POP {R3, R4, R5}
1275                w(&mut b, 0xE1A0_0000 | (reg_to_bits(rd) << 12) | 12); // MOV rd, R12
1276                // #1048: no trailing `MOV rnhi, #0` — the hi-word clear wrote
1277                // the OPERAND's home high register; callers emit it explicitly.
1278            }
1279
1280            _ => return Ok(None),
1281        }
1282        Ok(Some(b))
1283    }
1284
1285    fn encode_arm(&self, op: &ArmOp) -> Result<Vec<u8>> {
1286        // #615: A32 multi-instruction expansions (i64 arithmetic/shift/rotate/
1287        // compare, SetCond/SelectMove, popcnt, ...). These ops were literal
1288        // NOPs on the A32 path — user-reachable via `--target cortex-r5` —
1289        // so the value silently vanished. Mirror of the #594 CallIndirect
1290        // early-return: if the expansion helper covers the op, its bytes are
1291        // the encoding.
1292        if let Some(bytes) = self.encode_arm_expanded(op)? {
1293            return Ok(bytes);
1294        }
1295        // #206: ARM32 register-offset loads/stores. `encode_mem_addr` only
1296        // returns the 12-bit immediate, so the immediate-form arms below
1297        // silently DROP `addr.offset_reg` — a runtime address index vanished,
1298        // turning `ldr rd,[rn,rm,#off]` into `ldr rd,[rn,#off]` (the access went
1299        // to the wrong address). Compute the effective base into IP and re-encode
1300        // against `[ip, #off]`, which is uniform for word/byte/halfword/signed.
1301        if let Some(bytes) = self.encode_arm_reg_offset_mem(op)? {
1302            return Ok(bytes);
1303        }
1304        // #594: call_indirect was encoded as a literal NOP on the A32 path
1305        // (`--target cortex-r5`) — the call never happened and the function
1306        // silently returned garbage. Emit the same three-instruction expansion
1307        // as the Thumb-2 path (R11 = function-pointer table base, R12 scratch):
1308        //   MOV r12, idx, LSL #2 ; LDR r12, [r11, r12] ; BLX r12
1309        if let ArmOp::CallIndirect {
1310            table_index_reg,
1311            table_size,
1312            table_byte_offset,
1313            null_check,
1314            type_check,
1315            ..
1316        } = op
1317        {
1318            return Ok(Self::encode_arm_call_indirect(
1319                table_index_reg,
1320                *table_size,
1321                *table_byte_offset,
1322                *null_check,
1323                *type_check,
1324            ));
1325        }
1326        let instr: u32 = match op {
1327            // Data processing instructions
1328            ArmOp::Add { rd, rn, op2 } => {
1329                let rd_bits = reg_to_bits(rd);
1330                let rn_bits = reg_to_bits(rn);
1331                let (op2_bits, i_flag) = encode_operand2(op2)?;
1332
1333                // ADD encoding: cond(4) | 00 | I(1) | 0100 | S(1) | Rn(4) | Rd(4) | operand2(12)
1334                0xE0800000 // condition=always(E), opcode=ADD(0100), S=0
1335                    | (i_flag << 25)
1336                    | (rn_bits << 16)
1337                    | (rd_bits << 12)
1338                    | op2_bits
1339            }
1340
1341            ArmOp::Sub { rd, rn, op2 } => {
1342                let rd_bits = reg_to_bits(rd);
1343                let rn_bits = reg_to_bits(rn);
1344                let (op2_bits, i_flag) = encode_operand2(op2)?;
1345
1346                // SUB encoding: opcode=0010
1347                0xE0400000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1348            }
1349
1350            // i64 support: ADDS, ADC, SUBS, SBC for ARM32
1351            ArmOp::Adds { rd, rn, op2 } => {
1352                let rd_bits = reg_to_bits(rd);
1353                let rn_bits = reg_to_bits(rn);
1354                let (op2_bits, i_flag) = encode_operand2(op2)?;
1355
1356                // ADDS encoding: opcode=0100, S=1
1357                0xE0900000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1358            }
1359
1360            ArmOp::Adc { rd, rn, op2 } => {
1361                let rd_bits = reg_to_bits(rd);
1362                let rn_bits = reg_to_bits(rn);
1363                let (op2_bits, i_flag) = encode_operand2(op2)?;
1364
1365                // ADC encoding: opcode=0101
1366                0xE0A00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1367            }
1368
1369            ArmOp::Subs { rd, rn, op2 } => {
1370                let rd_bits = reg_to_bits(rd);
1371                let rn_bits = reg_to_bits(rn);
1372                let (op2_bits, i_flag) = encode_operand2(op2)?;
1373
1374                // SUBS encoding: opcode=0010, S=1
1375                0xE0500000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1376            }
1377
1378            ArmOp::Sbc { rd, rn, op2 } => {
1379                let rd_bits = reg_to_bits(rd);
1380                let rn_bits = reg_to_bits(rn);
1381                let (op2_bits, i_flag) = encode_operand2(op2)?;
1382
1383                // SBC encoding: opcode=0110
1384                0xE0C00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1385            }
1386
1387            ArmOp::Mul { rd, rn, rm } => {
1388                let rd_bits = reg_to_bits(rd);
1389                let rn_bits = reg_to_bits(rn);
1390                let rm_bits = reg_to_bits(rm);
1391
1392                // MUL encoding: cond(4) | 000000 | A(1) | S(1) | Rd(4) | Rn(4) | Rs(4) | 1001 | Rm(4)
1393                0xE0000090 | (rd_bits << 16) | (rn_bits << 8) | rm_bits
1394            }
1395
1396            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
1397                let rdlo_bits = reg_to_bits(rdlo);
1398                let rdhi_bits = reg_to_bits(rdhi);
1399                let rn_bits = reg_to_bits(rn);
1400                let rm_bits = reg_to_bits(rm);
1401
1402                // UMULL encoding: cond(4) | 0000 1000 | RdHi(4) | RdLo(4) | Rm(4) | 1001 | Rn(4)
1403                0xE0800090 | (rdhi_bits << 16) | (rdlo_bits << 12) | (rm_bits << 8) | rn_bits
1404            }
1405
1406            ArmOp::Sdiv { rd, rn, rm } => {
1407                let rd_bits = reg_to_bits(rd);
1408                let rn_bits = reg_to_bits(rn);
1409                let rm_bits = reg_to_bits(rm);
1410
1411                // SDIV encoding: cond(4) | 01110001 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1412                // ARMv7-M and above
1413                0xE710F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1414            }
1415
1416            ArmOp::Udiv { rd, rn, rm } => {
1417                let rd_bits = reg_to_bits(rd);
1418                let rn_bits = reg_to_bits(rn);
1419                let rm_bits = reg_to_bits(rm);
1420
1421                // UDIV encoding: cond(4) | 01110011 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1422                // ARMv7-M and above
1423                0xE730F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1424            }
1425
1426            ArmOp::Mls { rd, rn, rm, ra } => {
1427                let rd_bits = reg_to_bits(rd);
1428                let rn_bits = reg_to_bits(rn);
1429                let rm_bits = reg_to_bits(rm);
1430                let ra_bits = reg_to_bits(ra);
1431
1432                // MLS encoding: cond(4) | 00000110 | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1433                // Rd = Ra - (Rn * Rm)
1434                0xE0600090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1435            }
1436
1437            ArmOp::Mla { rd, rn, rm, ra } => {
1438                let rd_bits = reg_to_bits(rd);
1439                let rn_bits = reg_to_bits(rn);
1440                let rm_bits = reg_to_bits(rm);
1441                let ra_bits = reg_to_bits(ra);
1442
1443                // MLA encoding: cond(4) | 0000001 S | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1444                // Rd = Ra + (Rn * Rm). Base 0xE0200090 (S=0).
1445                0xE0200090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1446            }
1447
1448            ArmOp::And { rd, rn, op2 } => {
1449                let rd_bits = reg_to_bits(rd);
1450                let rn_bits = reg_to_bits(rn);
1451                let (op2_bits, i_flag) = encode_operand2(op2)?;
1452
1453                // AND encoding: opcode=0000
1454                0xE0000000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1455            }
1456
1457            ArmOp::Orr { rd, rn, op2 } => {
1458                let rd_bits = reg_to_bits(rd);
1459                let rn_bits = reg_to_bits(rn);
1460                let (op2_bits, i_flag) = encode_operand2(op2)?;
1461
1462                // ORR encoding: opcode=1100
1463                0xE1800000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1464            }
1465
1466            ArmOp::Eor { rd, rn, op2 } => {
1467                let rd_bits = reg_to_bits(rd);
1468                let rn_bits = reg_to_bits(rn);
1469                let (op2_bits, i_flag) = encode_operand2(op2)?;
1470
1471                // EOR encoding: opcode=0001
1472                0xE0200000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1473            }
1474
1475            // Shift instructions
1476            ArmOp::Lsl { rd, rn, shift } => {
1477                let rd_bits = reg_to_bits(rd);
1478                let rn_bits = reg_to_bits(rn);
1479                let shift_bits = *shift & 0x1F;
1480
1481                // LSL encoding: MOV with shift
1482                0xE1A00000 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1483            }
1484
1485            ArmOp::Lsr { rd, rn, shift } => {
1486                let rd_bits = reg_to_bits(rd);
1487                let rn_bits = reg_to_bits(rn);
1488                let shift_bits = *shift & 0x1F;
1489
1490                // LSR encoding
1491                0xE1A00020 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1492            }
1493
1494            ArmOp::Asr { rd, rn, shift } => {
1495                let rd_bits = reg_to_bits(rd);
1496                let rn_bits = reg_to_bits(rn);
1497                let shift_bits = *shift & 0x1F;
1498
1499                // ASR encoding
1500                0xE1A00040 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1501            }
1502
1503            ArmOp::Ror { rd, rn, shift } => {
1504                let rd_bits = reg_to_bits(rd);
1505                let rn_bits = reg_to_bits(rn);
1506                let shift_bits = *shift & 0x1F;
1507
1508                // ROR encoding: MOV with ROR shift
1509                0xE1A00060 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1510            }
1511
1512            // Register-based shifts (ARM32)
1513            // LSL Rd, Rn, Rm: cond 0001101S 0000 Rd Rs 0001 Rn
1514            ArmOp::LslReg { rd, rn, rm } => {
1515                let rd_bits = reg_to_bits(rd);
1516                let rn_bits = reg_to_bits(rn);
1517                let rm_bits = reg_to_bits(rm);
1518                0xE1A00010 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1519            }
1520            ArmOp::LsrReg { rd, rn, rm } => {
1521                let rd_bits = reg_to_bits(rd);
1522                let rn_bits = reg_to_bits(rn);
1523                let rm_bits = reg_to_bits(rm);
1524                0xE1A00030 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1525            }
1526            ArmOp::AsrReg { rd, rn, rm } => {
1527                let rd_bits = reg_to_bits(rd);
1528                let rn_bits = reg_to_bits(rn);
1529                let rm_bits = reg_to_bits(rm);
1530                0xE1A00050 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1531            }
1532            ArmOp::RorReg { rd, rn, rm } => {
1533                let rd_bits = reg_to_bits(rd);
1534                let rn_bits = reg_to_bits(rn);
1535                let rm_bits = reg_to_bits(rm);
1536                0xE1A00070 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1537            }
1538
1539            // RSB (Reverse Subtract): Rd = imm - Rn
1540            ArmOp::Rsb { rd, rn, imm } => {
1541                let rd_bits = reg_to_bits(rd);
1542                let rn_bits = reg_to_bits(rn);
1543                // RSB encoding: cond(4) | 00 1 0011 S | Rn(4) | Rd(4) | imm12
1544                // Opcode for RSB = 0011, I=1 (immediate), S=0
1545                //
1546                // #681 class audit: the A32 imm12 is a rotate(4):imm8 modified
1547                // immediate; `*imm & 0xFF` silently encoded a WRONG constant
1548                // for imm > 0xFF (#378 masking class). All current emitters use
1549                // imm 32, so erroring here is byte-identical for real codegen.
1550                if *imm > 0xFF {
1551                    return Err(synth_core::Error::synthesis(
1552                        "A32 RSB immediate > 0xFF requires a rotated-immediate encoding \
1553                         (not supported) — materialize into a register",
1554                    ));
1555                }
1556                0xE2600000 | (rn_bits << 16) | (rd_bits << 12) | (*imm & 0xFF)
1557            }
1558
1559            // Bit manipulation instructions
1560            ArmOp::Clz { rd, rm } => {
1561                let rd_bits = reg_to_bits(rd);
1562                let rm_bits = reg_to_bits(rm);
1563
1564                // CLZ encoding: cond(4) | 00010110 | 1111 | Rd(4) | 1111 | 0001 | Rm(4)
1565                // ARMv5T and above
1566                0xE16F0F10 | (rd_bits << 12) | rm_bits
1567            }
1568
1569            ArmOp::Rbit { rd, rm } => {
1570                let rd_bits = reg_to_bits(rd);
1571                let rm_bits = reg_to_bits(rm);
1572
1573                // RBIT encoding: cond(4) | 01101111 | 1111 | Rd(4) | 1111 | 0011 | Rm(4)
1574                // ARMv6T2 and above
1575                0xE6FF0F30 | (rd_bits << 12) | rm_bits
1576            }
1577
1578            ArmOp::Sxtb { rd, rm } => {
1579                let rd_bits = reg_to_bits(rd);
1580                let rm_bits = reg_to_bits(rm);
1581
1582                // SXTB encoding: cond(4) | 01101010 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1583                // ARMv6 and above. rotate=00 for no rotation
1584                0xE6AF0070 | (rd_bits << 12) | rm_bits
1585            }
1586
1587            ArmOp::Sxth { rd, rm } => {
1588                let rd_bits = reg_to_bits(rd);
1589                let rm_bits = reg_to_bits(rm);
1590
1591                // SXTH encoding: cond(4) | 01101011 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1592                // ARMv6 and above. rotate=00 for no rotation
1593                0xE6BF0070 | (rd_bits << 12) | rm_bits
1594            }
1595
1596            ArmOp::Uxtb { rd, rm } => {
1597                let rd_bits = reg_to_bits(rd);
1598                let rm_bits = reg_to_bits(rm);
1599                // UXTB encoding: cond | 01101110 1111 Rd rotate 00 0111 Rm (rotate=00)
1600                0xE6EF0070 | (rd_bits << 12) | rm_bits
1601            }
1602
1603            ArmOp::Uxth { rd, rm } => {
1604                let rd_bits = reg_to_bits(rd);
1605                let rm_bits = reg_to_bits(rm);
1606                // UXTH encoding: cond | 01101111 1111 Rd rotate 00 0111 Rm (rotate=00)
1607                0xE6FF0070 | (rd_bits << 12) | rm_bits
1608            }
1609
1610            // Move instructions
1611            ArmOp::Mov { rd, op2 } => {
1612                let rd_bits = reg_to_bits(rd);
1613                let (op2_bits, i_flag) = encode_operand2(op2)?;
1614
1615                // MOV encoding: opcode=1101
1616                0xE1A00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1617            }
1618
1619            ArmOp::Mvn { rd, op2 } => {
1620                let rd_bits = reg_to_bits(rd);
1621                let (op2_bits, i_flag) = encode_operand2(op2)?;
1622
1623                // MVN encoding: opcode=1111
1624                0xE1E00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1625            }
1626
1627            // MOVW - Move Wide (ARM32)
1628            // Encoding: cond(4) | 0011 0000 | imm4(4) | Rd(4) | imm12(12)
1629            ArmOp::Movw { rd, imm16 } => {
1630                let rd_bits = reg_to_bits(rd);
1631                let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1632                let imm12 = (*imm16 as u32) & 0xFFF;
1633                0xE3000000 | (imm4 << 16) | (rd_bits << 12) | imm12
1634            }
1635
1636            // MOVT - Move Top (ARM32)
1637            // Encoding: cond(4) | 0011 0100 | imm4(4) | Rd(4) | imm12(12)
1638            ArmOp::Movt { rd, imm16 } => {
1639                let rd_bits = reg_to_bits(rd);
1640                let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1641                let imm12 = (*imm16 as u32) & 0xFFF;
1642                0xE3400000 | (imm4 << 16) | (rd_bits << 12) | imm12
1643            }
1644
1645            // #237: symbol-relative MOVW/MOVT (ARM mode) — addend in place, the
1646            // backend records the MOVW_ABS/MOVT_ABS relocation against `symbol`.
1647            ArmOp::MovwSym { rd, addend, .. } => {
1648                let rd_bits = reg_to_bits(rd);
1649                let v = (*addend as u32) & 0xffff;
1650                0xE3000000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1651            }
1652            ArmOp::MovtSym { rd, addend, .. } => {
1653                let rd_bits = reg_to_bits(rd);
1654                let v = ((*addend as u32) >> 16) & 0xffff;
1655                0xE3400000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1656            }
1657
1658            // #345: LdrSym is the Thumb-2 literal-pool address load. A32 mode is
1659            // not used for relocatable native-pointer objects; fail loudly rather
1660            // than miscompile if it is ever reached here.
1661            ArmOp::LdrSym { .. } => {
1662                return Err(synth_core::Error::synthesis(
1663                    "LdrSym (literal-pool address load) is Thumb-2-only",
1664                ));
1665            }
1666
1667            // Compare
1668            ArmOp::Cmp { rn, op2 } => {
1669                let rn_bits = reg_to_bits(rn);
1670                let (op2_bits, i_flag) = encode_operand2(op2)?;
1671
1672                // CMP encoding: opcode=1010, S=1
1673                0xE1500000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1674            }
1675
1676            // Compare Negative (CMN) - computes Rn + op2 and sets flags
1677            ArmOp::Cmn { rn, op2 } => {
1678                let rn_bits = reg_to_bits(rn);
1679                let (op2_bits, i_flag) = encode_operand2(op2)?;
1680
1681                // CMN encoding: opcode=1011, S=1
1682                0xE1700000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1683            }
1684
1685            // Load/Store
1686            ArmOp::Ldr { rd, addr } => {
1687                let rd_bits = reg_to_bits(rd);
1688                let (base_bits, offset_bits) = encode_mem_addr(addr)?;
1689
1690                // LDR encoding: cond(4) | 01 | I(1) | P(1) | U(1) | B(1) | W(1) | L(1) | Rn(4) | Rd(4) | offset(12)
1691                // P=1 (pre-indexed), U=1 (add offset), L=1 (load)
1692                0xE5900000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1693            }
1694
1695            ArmOp::Str { rd, addr } => {
1696                let rd_bits = reg_to_bits(rd);
1697                let (base_bits, offset_bits) = encode_mem_addr(addr)?;
1698
1699                // STR encoding: L=0 (store)
1700                0xE5800000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1701            }
1702
1703            // Sub-word loads (ARM32 encoding)
1704            ArmOp::Ldrb { rd, addr } => {
1705                let rd_bits = reg_to_bits(rd);
1706                let (base_bits, offset_bits) = encode_mem_addr(addr)?;
1707                // LDRB: LDR with B=1 (byte): cond|01|I|P|U|1|W|L|Rn|Rd|offset
1708                0xE5D00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1709            }
1710
1711            ArmOp::Ldrsb { rd, addr } => {
1712                let rd_bits = reg_to_bits(rd);
1713                let (base_bits, offset_val) = encode_mem_addr_imm8(addr)?;
1714                // LDRSB (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1101|imm4L
1715                let imm4h = (offset_val >> 4) & 0xF;
1716                let imm4l = offset_val & 0xF;
1717                0xE1D000D0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1718            }
1719
1720            ArmOp::Ldrh { rd, addr } => {
1721                let rd_bits = reg_to_bits(rd);
1722                let (base_bits, offset_val) = encode_mem_addr_imm8(addr)?;
1723                // LDRH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1011|imm4L
1724                let imm4h = (offset_val >> 4) & 0xF;
1725                let imm4l = offset_val & 0xF;
1726                0xE1D000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1727            }
1728
1729            ArmOp::Ldrsh { rd, addr } => {
1730                let rd_bits = reg_to_bits(rd);
1731                let (base_bits, offset_val) = encode_mem_addr_imm8(addr)?;
1732                // LDRSH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1111|imm4L
1733                let imm4h = (offset_val >> 4) & 0xF;
1734                let imm4l = offset_val & 0xF;
1735                0xE1D000F0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1736            }
1737
1738            // Sub-word stores (ARM32 encoding)
1739            ArmOp::Strb { rd, addr } => {
1740                let rd_bits = reg_to_bits(rd);
1741                let (base_bits, offset_bits) = encode_mem_addr(addr)?;
1742                // STRB: STR with B=1 (byte): cond|01|I|P|U|1|W|0|Rn|Rd|offset
1743                0xE5C00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1744            }
1745
1746            ArmOp::Strh { rd, addr } => {
1747                let rd_bits = reg_to_bits(rd);
1748                let (base_bits, offset_val) = encode_mem_addr_imm8(addr)?;
1749                // STRH (misc store): cond|000|P|U|1|W|0|Rn|Rd|imm4H|1011|imm4L
1750                let imm4h = (offset_val >> 4) & 0xF;
1751                let imm4l = offset_val & 0xF;
1752                0xE1C000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1753            }
1754
1755            // Memory management (ARM32 encoding)
1756            ArmOp::MemorySize { rd } => {
1757                let rd_bits = reg_to_bits(rd);
1758                // MOV rd, R10, LSR #16  (memory size in bytes / 65536 = pages)
1759                // cond|000|1101|S|0000|Rd|shift5|type|0|Rm
1760                // LSR #16: shift5=10000, type=01
1761                0xE1A00820 | (rd_bits << 12) | 0x0A // Rm=R10, shift=16, LSR
1762            }
1763
1764            ArmOp::MemoryGrow { rd, .. } => {
1765                let rd_bits = reg_to_bits(rd);
1766                // On embedded, always fail: MOV rd, #-1
1767                0xE3E00000 | (rd_bits << 12) // MVN rd, #0 = MOV rd, #-1
1768            }
1769
1770            // Label pseudo-instruction: emits no machine code
1771            ArmOp::Label { .. } => {
1772                return Ok(Vec::new());
1773            }
1774
1775            // Branch instructions
1776            ArmOp::B { label: _ } => {
1777                // B encoding: cond(4) | 1010 | offset(24)
1778                // Simplified: branch to offset 0 (will be patched by linker/resolver)
1779                0xEA000000
1780            }
1781
1782            // Conditional branch to label (generic)
1783            ArmOp::Bcc { cond, label: _ } => {
1784                use synth_synthesis::Condition;
1785                let cond_bits: u32 = match cond {
1786                    Condition::EQ => 0x0,
1787                    Condition::NE => 0x1,
1788                    Condition::HS => 0x2,
1789                    Condition::LO => 0x3,
1790                    Condition::HI => 0x8,
1791                    Condition::LS => 0x9,
1792                    Condition::GE => 0xA,
1793                    Condition::LT => 0xB,
1794                    Condition::GT => 0xC,
1795                    Condition::LE => 0xD,
1796                };
1797                // B<cond> with offset 0 (will be patched)
1798                (cond_bits << 28) | 0x0A000000
1799            }
1800
1801            // BHS (Branch if Higher or Same) - used for bounds checking
1802            ArmOp::Bhs { label: _ } => {
1803                // BHS encoding: cond(2=HS) | 1010 | offset(24)
1804                0x2A000000 // BHS with offset 0
1805            }
1806
1807            // BLO (Branch if Lower) - complementary to BHS
1808            ArmOp::Blo { label: _ } => {
1809                // BLO encoding: cond(3=LO) | 1010 | offset(24)
1810                0x3A000000 // BLO with offset 0
1811            }
1812
1813            // Branch with numeric offset (in instructions)
1814            // ARM32 B instruction: offset is in instructions, stored as words
1815            // The offset is relative to PC+8 (due to ARM pipeline)
1816            ArmOp::BOffset { offset } => {
1817                // B encoding: cond(4) | 1010 | offset(24)
1818                // Offset is signed, in words (4-byte units)
1819                // ARM adds PC+8 to the offset, so we need to adjust:
1820                // target = PC + 8 + (offset * 4)
1821                // For backward branch of N instructions: offset = -(N + 2)
1822                // wrapping_sub keeps the encoder total under fuzzing (#186): an
1823                // extreme i32::MIN offset would otherwise overflow-panic; for any
1824                // real branch offset this is identical to `- 2`.
1825                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1826                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1827                0xEA000000 | offset_bits
1828            }
1829
1830            // Conditional branch with numeric offset
1831            ArmOp::BCondOffset { cond, offset } => {
1832                use synth_synthesis::Condition;
1833                let cond_bits: u32 = match cond {
1834                    Condition::EQ => 0x0,
1835                    Condition::NE => 0x1,
1836                    Condition::HS => 0x2,
1837                    Condition::LO => 0x3,
1838                    Condition::HI => 0x8,
1839                    Condition::LS => 0x9,
1840                    Condition::GE => 0xA,
1841                    Condition::LT => 0xB,
1842                    Condition::GT => 0xC,
1843                    Condition::LE => 0xD,
1844                };
1845                // B<cond> encoding: cond(4) | 1010 | offset(24)
1846                // wrapping_sub: total under fuzzing (#186), identical for real offsets.
1847                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1848                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1849                (cond_bits << 28) | 0x0A000000 | offset_bits
1850            }
1851
1852            ArmOp::Bl { label: _ } => {
1853                // BL encoding: cond(4) | 1011 | offset(24). Relocatable
1854                // placeholder; an R_ARM_CALL relocation patches the target.
1855                //
1856                // #1040: the placeholder must carry an embedded addend of -8,
1857                // not 0. A32 `BL` computes `target = P + 8 + (imm24 << 2)`, so
1858                // under REL semantics a 0 addend (`eb000000`) resolves two
1859                // instructions PAST the callee entry — the A32 twin of the
1860                // Thumb #174 bug. The correct word is what `gas` emits for
1861                // `bl <extern>` in ARM mode:
1862                //   ebfffffe  ->  `bl <self>`  (imm24 = -2, offset = -8),
1863                // which nets to exactly S. Verified against
1864                // `arm-none-eabi-as -march=armv7-r`, which emits `ebfffffe`
1865                // with an R_ARM_CALL relocation.
1866                0xEBFFFFFE
1867            }
1868
1869            ArmOp::Bx { rm } => {
1870                let rm_bits = reg_to_bits(rm);
1871
1872                // BX encoding: cond(4) | 000100101111111111110001 | Rm(4)
1873                0xE12FFF10 | rm_bits
1874            }
1875
1876            ArmOp::Blx { rm } => {
1877                let rm_bits = reg_to_bits(rm);
1878
1879                // BLX (register) encoding: cond(4) | 000100101111111111110011 | Rm(4)
1880                0xE12FFF30 | rm_bits
1881            }
1882
1883            ArmOp::Push { regs } => {
1884                // STMDB SP!, {regs} encoding: cond(4) | 100100 | 10 | 1101 | register_list(16)
1885                let mut reg_list: u32 = 0;
1886                for r in regs {
1887                    reg_list |= 1 << reg_to_bits(r);
1888                }
1889                0xE92D0000 | reg_list
1890            }
1891
1892            ArmOp::Pop { regs } => {
1893                // LDMIA SP!, {regs} encoding: cond(4) | 100010 | 11 | 1101 | register_list(16)
1894                let mut reg_list: u32 = 0;
1895                for r in regs {
1896                    reg_list |= 1 << reg_to_bits(r);
1897                }
1898                0xE8BD0000 | reg_list
1899            }
1900
1901            ArmOp::Nop => {
1902                // NOP encoding: MOV R0, R0
1903                0xE1A00000
1904            }
1905
1906            ArmOp::Udf { imm } => {
1907                // UDF (Undefined) encoding in ARM: 0xE7F000F0 | (imm12_hi << 8) | imm4_lo
1908                // We only use imm8, so split into imm4_hi and imm4_lo
1909                let imm8 = *imm as u32;
1910                0xE7F000F0 | ((imm8 & 0xF0) << 4) | (imm8 & 0x0F)
1911            }
1912
1913            // #615: handled by the `encode_arm_expanded` early return at the
1914            // top of this function — a real MOV{cond}/MOV pair now, never a
1915            // silent NOP again.
1916            ArmOp::Popcnt { .. } | ArmOp::SetCond { .. } | ArmOp::SelectMove { .. } => {
1917                unreachable!("handled by encode_arm_expanded (#615)")
1918            }
1919
1920            // Verification-only pseudo-ops: `synth-verify`'s ArmSemantics
1921            // models these, but NO codegen path constructs them (the selector
1922            // lowers select/locals/globals/br_table/call to real instruction
1923            // sequences before the encoder). Encoding one as a NOP silently
1924            // dropped the operation (#615 class); a typed Err keeps the
1925            // encoder total (Ok-or-Err, the `encoder_no_panic` contract)
1926            // while making any future reachability LOUD.
1927            ArmOp::Select { .. }
1928            | ArmOp::LocalGet { .. }
1929            | ArmOp::LocalSet { .. }
1930            | ArmOp::LocalTee { .. }
1931            | ArmOp::GlobalGet { .. }
1932            | ArmOp::GlobalSet { .. }
1933            | ArmOp::BrTable { .. }
1934            | ArmOp::Call { .. } => {
1935                return Err(synth_core::Error::synthesis(format!(
1936                    "verification-only pseudo-op {op:?} reached the A32 encoder — \
1937                     codegen lowers it before encoding; refusing to emit a silent NOP (#615)"
1938                )));
1939            }
1940
1941            // #594: CallIndirect is expanded to a real multi-instruction
1942            // sequence by the early return at the top of this function —
1943            // it must NEVER fall through to a silent NOP again.
1944            ArmOp::CallIndirect { .. } => {
1945                unreachable!("CallIndirect handled by encode_arm_call_indirect (#594)")
1946            }
1947
1948            // #615: every i64 op (and I32WrapI64) is expanded to a real A32
1949            // multi-instruction sequence by `encode_arm_expanded` — the
1950            // "encode as NOP for now" era ended with the value silently
1951            // vanishing on `--target cortex-r5`.
1952            ArmOp::I64Add { .. }
1953            | ArmOp::I64Sub { .. }
1954            | ArmOp::I64DivS { .. }
1955            | ArmOp::I64DivU { .. }
1956            | ArmOp::I64RemS { .. }
1957            | ArmOp::I64RemU { .. }
1958            | ArmOp::I64Clz { .. }
1959            | ArmOp::I64Ctz { .. }
1960            | ArmOp::I64Popcnt { .. }
1961            | ArmOp::I64And { .. }
1962            | ArmOp::I64Or { .. }
1963            | ArmOp::I64Xor { .. }
1964            | ArmOp::I64Eqz { .. }
1965            | ArmOp::I64Eq { .. }
1966            | ArmOp::I64Ne { .. }
1967            | ArmOp::I64LtS { .. }
1968            | ArmOp::I64LtU { .. }
1969            | ArmOp::I64LeS { .. }
1970            | ArmOp::I64LeU { .. }
1971            | ArmOp::I64GtS { .. }
1972            | ArmOp::I64GtU { .. }
1973            | ArmOp::I64GeS { .. }
1974            | ArmOp::I64GeU { .. }
1975            | ArmOp::I64Const { .. }
1976            | ArmOp::I64Ldr { .. }
1977            | ArmOp::I64Str { .. }
1978            | ArmOp::I64ExtendI32S { .. }
1979            | ArmOp::I64ExtendI32U { .. }
1980            | ArmOp::I64Extend8S { .. }
1981            | ArmOp::I64Extend16S { .. }
1982            | ArmOp::I64Extend32S { .. }
1983            | ArmOp::I32WrapI64 { .. } => {
1984                unreachable!("handled by encode_arm_expanded (#615)")
1985            }
1986
1987            // f32 VFP single-precision instructions
1988            ArmOp::F32Add { sd, sn, sm } => encode_vfp_3reg(0xEE300A00, sd, sn, sm)?,
1989            ArmOp::F32Sub { sd, sn, sm } => encode_vfp_3reg(0xEE300A40, sd, sn, sm)?,
1990            ArmOp::F32Mul { sd, sn, sm } => encode_vfp_3reg(0xEE200A00, sd, sn, sm)?,
1991            ArmOp::F32Div { sd, sn, sm } => encode_vfp_3reg(0xEE800A00, sd, sn, sm)?,
1992            ArmOp::F32Abs { sd, sm } => encode_vfp_2reg(0xEEB00AC0, sd, sm)?,
1993            ArmOp::F32Neg { sd, sm } => encode_vfp_2reg(0xEEB10A40, sd, sm)?,
1994            ArmOp::F32Sqrt { sd, sm } => encode_vfp_2reg(0xEEB10AC0, sd, sm)?,
1995
1996            // f32 pseudo-ops — multi-instruction sequences
1997            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
1998            ArmOp::F32Ceil { sd, sm } => {
1999                return self.encode_arm_f32_rounding(sd, sm, 0b01); // Round toward +Inf
2000            }
2001            ArmOp::F32Floor { sd, sm } => {
2002                return self.encode_arm_f32_rounding(sd, sm, 0b10); // Round toward -Inf
2003            }
2004            ArmOp::F32Trunc { sd, sm } => {
2005                return self.encode_arm_f32_rounding(sd, sm, 0b11); // VCVT toward zero
2006            }
2007            ArmOp::F32Nearest { sd, sm } => {
2008                return self.encode_arm_f32_rounding(sd, sm, 0b00); // VCVT to nearest
2009            }
2010            ArmOp::F32Min { sd, sn, sm } => {
2011                return self.encode_arm_f32_minmax(sd, sn, sm, true);
2012            }
2013            ArmOp::F32Max { sd, sn, sm } => {
2014                return self.encode_arm_f32_minmax(sd, sn, sm, false);
2015            }
2016            ArmOp::F32Copysign { sd, sn, sm } => {
2017                return self.encode_arm_f32_copysign(sd, sn, sm);
2018            }
2019
2020            // f32 comparisons — multi-instruction: VCMP + VMRS + conditional MOV
2021            ArmOp::F32Eq { rd, sn, sm } => {
2022                return self.encode_arm_f32_compare(rd, sn, sm, 0x0); // EQ
2023            }
2024            ArmOp::F32Ne { rd, sn, sm } => {
2025                return self.encode_arm_f32_compare(rd, sn, sm, 0x1); // NE
2026            }
2027            ArmOp::F32Lt { rd, sn, sm } => {
2028                return self.encode_arm_f32_compare(rd, sn, sm, 0x4); // MI (less than)
2029            }
2030            ArmOp::F32Le { rd, sn, sm } => {
2031                return self.encode_arm_f32_compare(rd, sn, sm, 0x9); // LS (less or same)
2032            }
2033            ArmOp::F32Gt { rd, sn, sm } => {
2034                return self.encode_arm_f32_compare(rd, sn, sm, 0xC); // GT
2035            }
2036            ArmOp::F32Ge { rd, sn, sm } => {
2037                return self.encode_arm_f32_compare(rd, sn, sm, 0xA); // GE
2038            }
2039
2040            // f32 const — multi-instruction: MOVW + MOVT + VMOV
2041            ArmOp::F32Const { sd, value } => {
2042                return self.encode_arm_f32_const(sd, *value);
2043            }
2044
2045            ArmOp::F32Load { sd, addr } => encode_vfp_ldst(0xED900A00, sd, addr)?,
2046            ArmOp::F32Store { sd, addr } => encode_vfp_ldst(0xED800A00, sd, addr)?,
2047
2048            // f32 conversions — multi-instruction sequences
2049            ArmOp::F32ConvertI32S { sd, rm } => {
2050                return self.encode_arm_f32_convert_i32(sd, rm, true);
2051            }
2052            ArmOp::F32ConvertI32U { sd, rm } => {
2053                return self.encode_arm_f32_convert_i32(sd, rm, false);
2054            }
2055            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
2056                return Err(synth_core::Error::synthesis(
2057                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2058                ));
2059            }
2060            ArmOp::F32ReinterpretI32 { sd, rm } => encode_vmov_core_sreg(true, sd, rm)?,
2061            ArmOp::I32ReinterpretF32 { rd, sm } => encode_vmov_core_sreg(false, sm, rd)?,
2062            ArmOp::I32TruncF32S { rd, sm } => {
2063                return self.encode_arm_i32_trunc_f32(rd, sm, true);
2064            }
2065            ArmOp::I32TruncF32U { rd, sm } => {
2066                return self.encode_arm_i32_trunc_f32(rd, sm, false);
2067            }
2068
2069            // f64 VFP double-precision instructions (ARM32)
2070            // F64 arithmetic: same as F32 but with sz=1 (bit 8 = 1, cp11 = 0xB)
2071            ArmOp::F64Add { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B00, dd, dn, dm)?,
2072            ArmOp::F64Sub { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B40, dd, dn, dm)?,
2073            ArmOp::F64Mul { dd, dn, dm } => encode_vfp_3reg_f64(0xEE200B00, dd, dn, dm)?,
2074            ArmOp::F64Div { dd, dn, dm } => encode_vfp_3reg_f64(0xEE800B00, dd, dn, dm)?,
2075            ArmOp::F64Abs { dd, dm } => encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?,
2076            ArmOp::F64Neg { dd, dm } => encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?,
2077            ArmOp::F64Sqrt { dd, dm } => encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?,
2078
2079            // f64 pseudo-ops
2080            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
2081            ArmOp::F64Ceil { dd, dm } => {
2082                return self.encode_arm_f64_rounding(dd, dm, 0b01);
2083            }
2084            ArmOp::F64Floor { dd, dm } => {
2085                return self.encode_arm_f64_rounding(dd, dm, 0b10);
2086            }
2087            ArmOp::F64Trunc { dd, dm } => {
2088                return self.encode_arm_f64_rounding(dd, dm, 0b11);
2089            }
2090            ArmOp::F64Nearest { dd, dm } => {
2091                return self.encode_arm_f64_rounding(dd, dm, 0b00);
2092            }
2093            ArmOp::F64Min { dd, dn, dm } => {
2094                return self.encode_arm_f64_minmax(dd, dn, dm, true);
2095            }
2096            ArmOp::F64Max { dd, dn, dm } => {
2097                return self.encode_arm_f64_minmax(dd, dn, dm, false);
2098            }
2099            ArmOp::F64Copysign { dd, dn, dm } => {
2100                return self.encode_arm_f64_copysign(dd, dn, dm);
2101            }
2102
2103            // f64 comparisons
2104            ArmOp::F64Eq { rd, dn, dm } => {
2105                return self.encode_arm_f64_compare(rd, dn, dm, 0x0);
2106            }
2107            ArmOp::F64Ne { rd, dn, dm } => {
2108                return self.encode_arm_f64_compare(rd, dn, dm, 0x1);
2109            }
2110            ArmOp::F64Lt { rd, dn, dm } => {
2111                return self.encode_arm_f64_compare(rd, dn, dm, 0x4);
2112            }
2113            ArmOp::F64Le { rd, dn, dm } => {
2114                return self.encode_arm_f64_compare(rd, dn, dm, 0x9);
2115            }
2116            ArmOp::F64Gt { rd, dn, dm } => {
2117                return self.encode_arm_f64_compare(rd, dn, dm, 0xC);
2118            }
2119            ArmOp::F64Ge { rd, dn, dm } => {
2120                return self.encode_arm_f64_compare(rd, dn, dm, 0xA);
2121            }
2122
2123            ArmOp::F64Const { dd, value } => {
2124                return self.encode_arm_f64_const(dd, *value);
2125            }
2126
2127            ArmOp::F64Load { dd, addr } => encode_vfp_ldst_f64(0xED900B00, dd, addr)?,
2128            ArmOp::F64Store { dd, addr } => encode_vfp_ldst_f64(0xED800B00, dd, addr)?,
2129
2130            ArmOp::F64ConvertI32S { dd, rm } => {
2131                return self.encode_arm_f64_convert_i32(dd, rm, true);
2132            }
2133            ArmOp::F64ConvertI32U { dd, rm } => {
2134                return self.encode_arm_f64_convert_i32(dd, rm, false);
2135            }
2136            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
2137                return Err(synth_core::Error::synthesis(
2138                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2139                ));
2140            }
2141            ArmOp::F64PromoteF32 { dd, sm } => {
2142                return self.encode_arm_f64_promote_f32(dd, sm);
2143            }
2144            // GI-FPU-002 (#369): no synth A32 target carries an FPU (cortex-r5
2145            // has none — the selector declines every float op there), so the
2146            // A32 encoder refuses loudly instead of shipping an untested
2147            // encoding (#615: never a silent wrong byte).
2148            ArmOp::F32DemoteF64 { .. } => {
2149                return Err(synth_core::Error::synthesis(
2150                    "F32DemoteF64 has no A32 encoding (no A32 target has an FPU)",
2151                ));
2152            }
2153            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => {
2154                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?
2155            }
2156            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => {
2157                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?
2158            }
2159            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
2160                return Err(synth_core::Error::synthesis(
2161                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
2162                ));
2163            }
2164            ArmOp::I32TruncF64S { rd, dm } => {
2165                return self.encode_arm_i32_trunc_f64(rd, dm, true);
2166            }
2167            ArmOp::I32TruncF64U { rd, dm } => {
2168                return self.encode_arm_i32_trunc_f64(rd, dm, false);
2169            }
2170            // #615: multi-instruction i64 sequences — expanded to real A32 by
2171            // `encode_arm_expanded`, no longer "Thumb-2 only" NOPs.
2172            ArmOp::I64SetCond { .. }
2173            | ArmOp::I64SetCondZ { .. }
2174            | ArmOp::I64Mul { .. }
2175            | ArmOp::I64Shl { .. }
2176            | ArmOp::I64ShrS { .. }
2177            | ArmOp::I64ShrU { .. }
2178            | ArmOp::I64Rotl { .. }
2179            | ArmOp::I64Rotr { .. } => {
2180                unreachable!("handled by encode_arm_expanded (#615)")
2181            }
2182
2183            // MVE instructions — Thumb-2 only (Cortex-M55 is always Thumb-2)
2184            ArmOp::MveLoad { .. }
2185            | ArmOp::MveStore { .. }
2186            | ArmOp::MveConst { .. }
2187            | ArmOp::MveAnd { .. }
2188            | ArmOp::MveOrr { .. }
2189            | ArmOp::MveEor { .. }
2190            | ArmOp::MveMvn { .. }
2191            | ArmOp::MveBic { .. }
2192            | ArmOp::MveAddI { .. }
2193            | ArmOp::MveSubI { .. }
2194            | ArmOp::MveMulI { .. }
2195            | ArmOp::MveNegI { .. }
2196            | ArmOp::MveCmpEqI { .. }
2197            | ArmOp::MveCmpNeI { .. }
2198            | ArmOp::MveCmpLtS { .. }
2199            | ArmOp::MveCmpLtU { .. }
2200            | ArmOp::MveCmpGtS { .. }
2201            | ArmOp::MveCmpGtU { .. }
2202            | ArmOp::MveCmpLeS { .. }
2203            | ArmOp::MveCmpLeU { .. }
2204            | ArmOp::MveCmpGeS { .. }
2205            | ArmOp::MveCmpGeU { .. }
2206            | ArmOp::MveDup { .. }
2207            | ArmOp::MveExtractLane { .. }
2208            | ArmOp::MveInsertLane { .. }
2209            | ArmOp::MveAddF32 { .. }
2210            | ArmOp::MveSubF32 { .. }
2211            | ArmOp::MveMulF32 { .. }
2212            | ArmOp::MveNegF32 { .. }
2213            | ArmOp::MveAbsF32 { .. }
2214            | ArmOp::MveCmpEqF32 { .. }
2215            | ArmOp::MveCmpNeF32 { .. }
2216            | ArmOp::MveCmpLtF32 { .. }
2217            | ArmOp::MveCmpLeF32 { .. }
2218            | ArmOp::MveCmpGtF32 { .. }
2219            | ArmOp::MveCmpGeF32 { .. }
2220            | ArmOp::MveDupF32 { .. }
2221            | ArmOp::MveExtractLaneF32 { .. }
2222            | ArmOp::MveReplaceLaneF32 { .. }
2223            | ArmOp::MveDivF32 { .. }
2224            | ArmOp::MveSqrtF32 { .. } => {
2225                // MVE (Helium) is a Thumb-2-only extension (Cortex-M55); there
2226                // is no A32 encoding. The selector only emits MVE ops for
2227                // Thumb targets — a NOP here silently dropped the vector op
2228                // if that invariant ever broke (#615 class). Err keeps the
2229                // encoder total and the failure loud.
2230                return Err(synth_core::Error::synthesis(format!(
2231                    "MVE op {op:?} has no A32 (ARM-mode) encoding — MVE is Thumb-2 only (#615)"
2232                )));
2233            }
2234        };
2235
2236        // ARM32 instructions are little-endian
2237        Ok(instr.to_le_bytes().to_vec())
2238    }
2239
2240    // === ARM32 VFP multi-instruction helpers ===
2241
2242    /// Encode F32 comparison as ARM32: VCMP.F32 + VMRS + MOV rd,#0 + MOVcond rd,#1
2243    fn encode_arm_f32_compare(
2244        &self,
2245        rd: &Reg,
2246        sn: &VfpReg,
2247        sm: &VfpReg,
2248        cond_code: u32,
2249    ) -> Result<Vec<u8>> {
2250        let mut bytes = Vec::new();
2251
2252        // VCMP.F32 Sn, Sm: 0xEEB40A40 with Sn in Vd position, Sm in Vm position
2253        let sn_num = vfp_sreg_to_num(sn)?;
2254        let sm_num = vfp_sreg_to_num(sm)?;
2255        let (vd, d) = encode_sreg(sn_num);
2256        let (vm, m) = encode_sreg(sm_num);
2257        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2258        bytes.extend_from_slice(&vcmp.to_le_bytes());
2259
2260        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
2261        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2262
2263        // MOV rd, #0: 0xE3A0_0000 | (rd << 12)
2264        let rd_bits = reg_to_bits(rd);
2265        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2266        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2267
2268        // MOVcond rd, #1: cond(4) | 0011 1010 0000 rd(4) 0000 0000 0001
2269        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2270        bytes.extend_from_slice(&mov_one.to_le_bytes());
2271
2272        Ok(bytes)
2273    }
2274
2275    /// Encode F32 constant load as ARM32: MOVW Rt,#lo16 + MOVT Rt,#hi16 + VMOV Sd,Rt
2276    fn encode_arm_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
2277        let mut bytes = Vec::new();
2278        let bits = value.to_bits();
2279
2280        // Use R12 as temp register for constant loading
2281        let rt: u32 = 12; // R12/IP
2282
2283        // MOVW R12, #lo16: 0xE300_C000 | (imm4 << 16) | imm12
2284        let lo16 = bits & 0xFFFF;
2285        let movw = 0xE3000000 | (rt << 12) | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2286        bytes.extend_from_slice(&movw.to_le_bytes());
2287
2288        // MOVT R12, #hi16: 0xE340_C000 | (imm4 << 16) | imm12
2289        let hi16 = (bits >> 16) & 0xFFFF;
2290        let movt = 0xE3400000 | (rt << 12) | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2291        bytes.extend_from_slice(&movt.to_le_bytes());
2292
2293        // VMOV Sd, R12
2294        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
2295        bytes.extend_from_slice(&vmov.to_le_bytes());
2296
2297        Ok(bytes)
2298    }
2299
2300    /// Encode VMOV + VCVT.F32.S32/U32 as ARM32
2301    fn encode_arm_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2302        let mut bytes = Vec::new();
2303
2304        // VMOV Sd, Rm — move integer to VFP register
2305        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
2306        bytes.extend_from_slice(&vmov.to_le_bytes());
2307
2308        // VCVT.F32.S32 Sd, Sd (signed) or VCVT.F32.U32 Sd, Sd (unsigned).
2309        // The "op" bit (bit 7) selects signedness: 1 = signed (S32), 0 =
2310        // unsigned (U32). So signed = 0xEEB80AC0, unsigned = 0xEEB80A40 —
2311        // objdump confirms 0xEEB80A40 decodes to `vcvt.f32.u32` (GI-FPU-002:
2312        // the two were previously swapped, silently making `convert_i32_s`
2313        // an unsigned conversion).
2314        let sd_num = vfp_sreg_to_num(sd)?;
2315        let (vd, d) = encode_sreg(sd_num);
2316        let (vm, m) = encode_sreg(sd_num); // same register as source
2317        let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
2318        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2319        bytes.extend_from_slice(&vcvt.to_le_bytes());
2320
2321        Ok(bytes)
2322    }
2323
2324    /// Encode F32 rounding pseudo-op as ARM32 via VCVT to integer and back.
2325    /// mode: 0b00=nearest, 0b01=floor(-Inf), 0b10=ceil(+Inf), 0b11=trunc(zero)
2326    /// Strategy: VCVT.S32.F32 Sd, Sm (toward zero), then VCVT.F32.S32 Sd, Sd
2327    /// For ceil/floor/nearest, we use VCVTR (round toward mode) + convert back.
2328    /// Simplified: convert to int (toward zero for trunc) then back to float.
2329    /// Encode F32 rounding as ARM32.
2330    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2331    ///
2332    /// For trunc (mode=0b11): uses VCVTR.S32.F32 (always rounds toward zero).
2333    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant
2334    /// which honours FPSCR rmode), then restores FPSCR.
2335    fn encode_arm_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2336        let mut bytes = Vec::new();
2337        let sm_num = vfp_sreg_to_num(sm)?;
2338        let sd_num = vfp_sreg_to_num(sd)?;
2339        let (vd_s, d_s) = encode_sreg(sd_num);
2340        let (vm_s, m_s) = encode_sreg(sm_num);
2341
2342        if mode == 0b11 {
2343            // Trunc (toward zero): VCVTR.S32.F32 — the "R" variant always truncates.
2344            // 0xEEBD0AC0: bit[7]=1 => round toward zero regardless of FPSCR
2345            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2346            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2347        } else {
2348            // ceil/floor/nearest: manipulate FPSCR rounding mode
2349            let rt: u32 = 12; // R12/IP as temp
2350
2351            // VMRS R12, FPSCR
2352            let vmrs = 0xEEF10A10 | (rt << 12);
2353            bytes.extend_from_slice(&vmrs.to_le_bytes());
2354
2355            // BIC R12, R12, #(3 << 22) — clear RMode bits [23:22]
2356            // 3<<22 = 0x00C00000. ARM rotated imm: 0x03 ror 10 (rotation=5, imm8=0x03)
2357            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2358            bytes.extend_from_slice(&bic.to_le_bytes());
2359
2360            // ORR R12, R12, #(mode << 22) — set desired rounding mode
2361            if mode != 0 {
2362                // mode<<22: rotation=5, imm8=mode
2363                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2364                bytes.extend_from_slice(&orr.to_le_bytes());
2365            }
2366
2367            // VMSR FPSCR, R12
2368            let vmsr = 0xEEE10A10 | (rt << 12);
2369            bytes.extend_from_slice(&vmsr.to_le_bytes());
2370
2371            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rounding mode
2372            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2373            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2374
2375            // Restore FPSCR: clear rmode bits back to nearest (default)
2376            bytes.extend_from_slice(&vmrs.to_le_bytes());
2377            bytes.extend_from_slice(&bic.to_le_bytes());
2378            bytes.extend_from_slice(&vmsr.to_le_bytes());
2379        }
2380
2381        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
2382        let (vd2, d2) = encode_sreg(sd_num);
2383        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
2384        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2385
2386        Ok(bytes)
2387    }
2388
2389    /// Encode F32 min/max as ARM32: VCMP + VMRS + conditional VMOV
2390    fn encode_arm_f32_minmax(
2391        &self,
2392        sd: &VfpReg,
2393        sn: &VfpReg,
2394        sm: &VfpReg,
2395        is_min: bool,
2396    ) -> Result<Vec<u8>> {
2397        let mut bytes = Vec::new();
2398        let sn_num = vfp_sreg_to_num(sn)?;
2399        let sm_num = vfp_sreg_to_num(sm)?;
2400        let sd_num = vfp_sreg_to_num(sd)?;
2401
2402        // VMOV Sd, Sn (start with first operand)
2403        let (vd, d) = encode_sreg(sd_num);
2404        let (vn, n) = encode_sreg(sn_num);
2405        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2406        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2407
2408        // VCMP.F32 Sn, Sm
2409        let (vm, m) = encode_sreg(sm_num);
2410        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2411        bytes.extend_from_slice(&vcmp.to_le_bytes());
2412
2413        // VMRS APSR_nzcv, FPSCR
2414        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2415
2416        // For min: if Sn > Sm (GT), use Sm. Condition = GT (0xC)
2417        // For max: if Sn < Sm (MI/LT), use Sm. Condition = MI (0x4)
2418        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2419
2420        // VMOV{cond} Sd, Sm — conditional VMOV
2421        let vmov_cond = (cond << 28) | 0x0EB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2422        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2423
2424        Ok(bytes)
2425    }
2426
2427    /// Encode F32 copysign as ARM32: extract sign from Sm, magnitude from Sn
2428    fn encode_arm_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2429        let mut bytes = Vec::new();
2430
2431        // VMOV R12, Sm (get sign source bits)
2432        let vmov_sm = encode_vmov_core_sreg(false, sm, &Reg::R12)?;
2433        bytes.extend_from_slice(&vmov_sm.to_le_bytes());
2434
2435        // VMOV R0, Sn (get magnitude source bits) — use R0 as temp
2436        let vmov_sn = encode_vmov_core_sreg(false, sn, &Reg::R0)?;
2437        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2438
2439        // AND R12, R12, #0x80000000 (keep only sign bit)
2440        // Thumb-2 constant 0x80000000 needs special encoding; in ARM32 use rotated imm
2441        // 0x80000000 = 0x02 rotated right by 2 (rotation=1, imm8=0x02)
2442        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2443        bytes.extend_from_slice(&and_sign.to_le_bytes());
2444
2445        // BIC R0, R0, #0x80000000 (clear sign bit from magnitude)
2446        // R0 = register 0, so Rn and Rd fields are 0
2447        let bic_sign = 0xE3C00000u32 | (1 << 8) | 0x02;
2448        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2449
2450        // ORR R0, R0, R12 (combine sign + magnitude)
2451        // R0 = register 0, so Rn and Rd fields are 0
2452        let orr = 0xE1800000u32 | 12;
2453        bytes.extend_from_slice(&orr.to_le_bytes());
2454
2455        // VMOV Sd, R0
2456        let vmov_result = encode_vmov_core_sreg(true, sd, &Reg::R0)?;
2457        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2458
2459        Ok(bytes)
2460    }
2461
2462    /// Encode F64 comparison as ARM32: VCMP.F64 + VMRS + MOV rd,#0 + MOVcond rd,#1
2463    fn encode_arm_f64_compare(
2464        &self,
2465        rd: &Reg,
2466        dn: &VfpReg,
2467        dm: &VfpReg,
2468        cond_code: u32,
2469    ) -> Result<Vec<u8>> {
2470        let mut bytes = Vec::new();
2471
2472        // VCMP.F64 Dn, Dm: 0xEEB40B40 with Dn in Vd position, Dm in Vm position
2473        let dn_num = vfp_dreg_to_num(dn)?;
2474        let dm_num = vfp_dreg_to_num(dm)?;
2475        let (vd, d) = encode_dreg(dn_num);
2476        let (vm, m) = encode_dreg(dm_num);
2477        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2478        bytes.extend_from_slice(&vcmp.to_le_bytes());
2479
2480        // VMRS APSR_nzcv, FPSCR
2481        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2482
2483        // MOV rd, #0
2484        let rd_bits = reg_to_bits(rd);
2485        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2486        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2487
2488        // MOVcond rd, #1
2489        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2490        bytes.extend_from_slice(&mov_one.to_le_bytes());
2491
2492        Ok(bytes)
2493    }
2494
2495    /// Encode F64 constant load as ARM32: MOVW + MOVT + MOVW + MOVT + VMOV
2496    fn encode_arm_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
2497        let mut bytes = Vec::new();
2498        let bits = value.to_bits();
2499        let lo32 = bits as u32;
2500        let hi32 = (bits >> 32) as u32;
2501
2502        // Load low 32 bits into R0 (Rd field = 0 for R0)
2503        let lo16 = lo32 & 0xFFFF;
2504        let movw_r0 = 0xE3000000 | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2505        bytes.extend_from_slice(&movw_r0.to_le_bytes());
2506        let hi16 = (lo32 >> 16) & 0xFFFF;
2507        let movt_r0 = 0xE3400000 | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2508        bytes.extend_from_slice(&movt_r0.to_le_bytes());
2509
2510        // Load high 32 bits into R12
2511        let lo16 = hi32 & 0xFFFF;
2512        let movw_r12 = 0xE3000000 | ((lo16 >> 12) << 16) | (12 << 12) | (lo16 & 0xFFF);
2513        bytes.extend_from_slice(&movw_r12.to_le_bytes());
2514        let hi16 = (hi32 >> 16) & 0xFFFF;
2515        let movt_r12 = 0xE3400000 | ((hi16 >> 12) << 16) | (12 << 12) | (hi16 & 0xFFF);
2516        bytes.extend_from_slice(&movt_r12.to_le_bytes());
2517
2518        // VMOV Dd, R0, R12
2519        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
2520        bytes.extend_from_slice(&vmov.to_le_bytes());
2521
2522        Ok(bytes)
2523    }
2524
2525    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as ARM32
2526    fn encode_arm_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2527        let mut bytes = Vec::new();
2528
2529        // Use S0 as intermediate: VMOV S0, Rm
2530        let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
2531        bytes.extend_from_slice(&vmov.to_le_bytes());
2532
2533        // VCVT.F64.S32 Dd, S0 (signed) or VCVT.F64.U32 Dd, S0 (unsigned)
2534        // Base: 0xEEB80B40 (signed) or 0xEEB80BC0 (unsigned)
2535        let dd_num = vfp_dreg_to_num(dd)?;
2536        let (vd, d) = encode_dreg(dd_num);
2537        let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
2538        // S0 is register 0: Vm=0, M=0
2539        let vcvt = base | (d << 22) | (vd << 12);
2540        bytes.extend_from_slice(&vcvt.to_le_bytes());
2541
2542        Ok(bytes)
2543    }
2544
2545    /// Encode VCVT.F64.F32 Dd, Sm as ARM32 (f32 to f64 promotion)
2546    fn encode_arm_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2547        let dd_num = vfp_dreg_to_num(dd)?;
2548        let sm_num = vfp_sreg_to_num(sm)?;
2549        let (vd, d) = encode_dreg(dd_num);
2550        let (vm, m) = encode_sreg(sm_num);
2551
2552        // VCVT.F64.F32 Dd, Sm: 0xEEB70AC0
2553        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
2554        Ok(vcvt.to_le_bytes().to_vec())
2555    }
2556
2557    /// Encode VCVT.S32/U32.F64 Sd, Dm + VMOV Rd, Sd as ARM32
2558    fn encode_arm_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2559        let mut bytes = Vec::new();
2560        let dm_num = vfp_dreg_to_num(dm)?;
2561        let (vm, m) = encode_dreg(dm_num);
2562
2563        // VCVT.S32.F64 S0, Dm (toward zero) or VCVT.U32.F64 S0, Dm
2564        // S0: Vd=0, D=0
2565        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
2566        let vcvt = base | (m << 5) | vm;
2567        bytes.extend_from_slice(&vcvt.to_le_bytes());
2568
2569        // VMOV Rd, S0
2570        let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
2571        bytes.extend_from_slice(&vmov.to_le_bytes());
2572
2573        Ok(bytes)
2574    }
2575
2576    /// Encode F64 rounding pseudo-op as ARM32 via VCVT to integer and back.
2577    /// Encode F64 rounding as ARM32.
2578    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2579    ///
2580    /// For trunc: uses VCVTR.S32.F64 (always truncates).
2581    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F64 (non-R variant),
2582    /// then restores FPSCR.
2583    fn encode_arm_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2584        let mut bytes = Vec::new();
2585        let dm_num = vfp_dreg_to_num(dm)?;
2586        let dd_num = vfp_dreg_to_num(dd)?;
2587        let (vm, m) = encode_dreg(dm_num);
2588        let (vd, d) = encode_dreg(dd_num);
2589
2590        if mode == 0b11 {
2591            // Trunc (toward zero): VCVTR.S32.F64 — bit[7]=1, always truncates
2592            let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
2593            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2594        } else {
2595            // ceil/floor/nearest: manipulate FPSCR rounding mode
2596            let rt: u32 = 12;
2597
2598            // VMRS R12, FPSCR
2599            let vmrs = 0xEEF10A10 | (rt << 12);
2600            bytes.extend_from_slice(&vmrs.to_le_bytes());
2601
2602            // BIC R12, R12, #(3 << 22)
2603            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2604            bytes.extend_from_slice(&bic.to_le_bytes());
2605
2606            // ORR R12, R12, #(mode << 22)
2607            if mode != 0 {
2608                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2609                bytes.extend_from_slice(&orr.to_le_bytes());
2610            }
2611
2612            // VMSR FPSCR, R12
2613            let vmsr = 0xEEE10A10 | (rt << 12);
2614            bytes.extend_from_slice(&vmsr.to_le_bytes());
2615
2616            // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0), uses FPSCR rmode
2617            let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
2618            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2619
2620            // Restore FPSCR
2621            bytes.extend_from_slice(&vmrs.to_le_bytes());
2622            bytes.extend_from_slice(&bic.to_le_bytes());
2623            bytes.extend_from_slice(&vmsr.to_le_bytes());
2624        }
2625
2626        // VCVT.F64.S32 Dd, S0 (convert back to double)
2627        let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
2628        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2629
2630        Ok(bytes)
2631    }
2632
2633    /// Encode F64 min/max as ARM32: VMOV + VCMP + VMRS + conditional VMOV
2634    fn encode_arm_f64_minmax(
2635        &self,
2636        dd: &VfpReg,
2637        dn: &VfpReg,
2638        dm: &VfpReg,
2639        is_min: bool,
2640    ) -> Result<Vec<u8>> {
2641        let mut bytes = Vec::new();
2642        let dn_num = vfp_dreg_to_num(dn)?;
2643        let dm_num = vfp_dreg_to_num(dm)?;
2644        let dd_num = vfp_dreg_to_num(dd)?;
2645
2646        // VMOV.F64 Dd, Dn (start with first operand)
2647        let (vd, d) = encode_dreg(dd_num);
2648        let (vn, n) = encode_dreg(dn_num);
2649        let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2650        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2651
2652        // VCMP.F64 Dn, Dm
2653        let (vm, m) = encode_dreg(dm_num);
2654        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2655        bytes.extend_from_slice(&vcmp.to_le_bytes());
2656
2657        // VMRS APSR_nzcv, FPSCR
2658        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2659
2660        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2661        let vmov_cond = (cond << 28) | 0x0EB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2662        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2663
2664        Ok(bytes)
2665    }
2666
2667    /// Encode F64 copysign as ARM32
2668    fn encode_arm_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
2669        let mut bytes = Vec::new();
2670
2671        // VMOV R0, R12, Dm (get sign source bits)
2672        let vmov_dm = encode_vmov_core_dreg(false, dm, &Reg::R0, &Reg::R12)?;
2673        bytes.extend_from_slice(&vmov_dm.to_le_bytes());
2674
2675        // VMOV R1, R2, Dn (get magnitude source bits)
2676        // We use R1 (lo) and R2 (hi) for the magnitude
2677        let vmov_dn = encode_vmov_core_dreg(false, dn, &Reg::R1, &Reg::R2)?;
2678        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2679
2680        // AND R12, R12, #0x80000000 (keep only sign bit from hi word)
2681        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2682        bytes.extend_from_slice(&and_sign.to_le_bytes());
2683
2684        // BIC R2, R2, #0x80000000 (clear sign bit from magnitude hi word)
2685        let bic_sign = 0xE3C00000u32 | (2 << 16) | (2 << 12) | (1 << 8) | 0x02;
2686        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2687
2688        // ORR R2, R2, R12 (combine sign + magnitude)
2689        let orr = 0xE1800000u32 | (2 << 16) | (2 << 12) | 12;
2690        bytes.extend_from_slice(&orr.to_le_bytes());
2691
2692        // VMOV Dd, R1, R2
2693        let vmov_result = encode_vmov_core_dreg(true, dd, &Reg::R1, &Reg::R2)?;
2694        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2695
2696        Ok(bytes)
2697    }
2698
2699    /// Encode VCVT.S32/U32.F32 + VMOV as ARM32
2700    fn encode_arm_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2701        let mut bytes = Vec::new();
2702
2703        // VCVT.S32.F32 Sd, Sm (toward zero) or VCVT.U32.F32 Sd, Sm
2704        // We use Sm as both source and destination for the intermediate result
2705        let sm_num = vfp_sreg_to_num(sm)?;
2706        let (vd, d) = encode_sreg(sm_num);
2707        let (vm, m) = encode_sreg(sm_num);
2708        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
2709        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2710        bytes.extend_from_slice(&vcvt.to_le_bytes());
2711
2712        // VMOV Rd, Sm — move result back to core register
2713        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
2714        bytes.extend_from_slice(&vmov.to_le_bytes());
2715
2716        Ok(bytes)
2717    }
2718
2719    /// Encode an ARM instruction in Thumb-2 mode (16-bit or 32-bit instructions)
2720    fn encode_thumb(&self, op: &ArmOp) -> Result<Vec<u8>> {
2721        // Thumb-2 supports both 16-bit and 32-bit instructions
2722        // 32-bit instructions are encoded as two 16-bit halfwords (big-endian order)
2723        match op {
2724            // === 16-bit Thumb encodings ===
2725            ArmOp::Add { rd, rn, op2 } => {
2726                let rd_bits = reg_to_bits(rd) as u16;
2727                let rn_bits = reg_to_bits(rn) as u16;
2728
2729                if let Operand2::Reg(rm) = op2 {
2730                    let rm_bits = reg_to_bits(rm) as u16;
2731                    // 16-bit ADDS only has 3-bit register fields (R0-R7). For
2732                    // high registers (e.g. R12, the MemLoad/MemStore base
2733                    // scratch) the bits overflow into adjacent fields, silently
2734                    // corrupting the operands — issue #178/#180: `add ip,ip,r0`
2735                    // was emitted as `adds r4,r5,r1`. Guard on all three regs
2736                    // being low and fall back to 32-bit ADD.W otherwise, exactly
2737                    // as the Sub handler below does.
2738                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2739                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2740                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2741                        Ok(instr.to_le_bytes().to_vec())
2742                    } else {
2743                        // ADD.W Rd, Rn, Rm (32-bit) for high registers
2744                        self.encode_thumb32_add_reg_raw(
2745                            rd_bits as u32,
2746                            rn_bits as u32,
2747                            rm_bits as u32,
2748                        )
2749                    }
2750                } else if let Operand2::Imm(imm) = op2 {
2751                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2752                        // ADDS Rd, Rn, #imm3 (16-bit): 0001 110 imm3 Rn Rd
2753                        let instr: u16 = 0x1C00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2754                        Ok(instr.to_le_bytes().to_vec())
2755                    } else {
2756                        // Use 32-bit ADD for larger immediates
2757                        self.encode_thumb32_add(rd, rn, *imm as u32)
2758                    }
2759                } else {
2760                    // Fallback to 32-bit encoding
2761                    self.encode_thumb32_add(rd, rn, 0)
2762                }
2763            }
2764
2765            ArmOp::Sub { rd, rn, op2 } => {
2766                let rd_bits = reg_to_bits(rd) as u16;
2767                let rn_bits = reg_to_bits(rn) as u16;
2768
2769                if let Operand2::Reg(rm) = op2 {
2770                    let rm_bits = reg_to_bits(rm) as u16;
2771                    // 16-bit SUBS can only use low registers (R0-R7)
2772                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2773                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2774                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2775                        Ok(instr.to_le_bytes().to_vec())
2776                    } else {
2777                        // Use 32-bit SUB.W for high registers
2778                        self.encode_thumb32_sub_reg_raw(
2779                            rd_bits as u32,
2780                            rn_bits as u32,
2781                            rm_bits as u32,
2782                        )
2783                    }
2784                } else if let Operand2::Imm(imm) = op2 {
2785                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2786                        // SUBS Rd, Rn, #imm3 (16-bit): 0001 111 imm3 Rn Rd
2787                        let instr: u16 = 0x1E00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2788                        Ok(instr.to_le_bytes().to_vec())
2789                    } else {
2790                        self.encode_thumb32_sub(rd, rn, *imm as u32)
2791                    }
2792                } else {
2793                    self.encode_thumb32_sub(rd, rn, 0)
2794                }
2795            }
2796
2797            ArmOp::Mov { rd, op2 } => {
2798                let rd_bits = reg_to_bits(rd) as u16;
2799
2800                if let Operand2::Imm(imm) = op2 {
2801                    // #498: the old test here was the SIGNED `*imm <= 255`,
2802                    // so a negative immediate (e.g. -1) fell into the 16-bit
2803                    // MOVS arm and encoded the wrong VALUE (#(imm & 0xFF) =
2804                    // #0xFF). A positive imm above 0xFFFF was equally wrong:
2805                    // MOVW truncates to 16 bits. Split on the UNSIGNED value:
2806                    // imm8 → MOVS, imm16 → MOVW, anything wider (negative or
2807                    // >0xFFFF) → the full-value MOVW+MOVT pair. No emitter
2808                    // produces the wide shape today (both selectors
2809                    // materialize wide constants as explicit Movw/Movt or
2810                    // Movw+Mvn), so this is byte-identical on shipped paths —
2811                    // it retires the latent wrong-value encodings the
2812                    // `estimator_encoder_agreement` oracle had pinned.
2813                    let uimm = *imm as u32;
2814                    if uimm <= 255 && rd_bits < 8 {
2815                        // MOVS Rd, #imm8 (16-bit): 0010 0 Rd imm8
2816                        let imm_bits = (*imm as u16) & 0xFF;
2817                        let instr: u16 = 0x2000 | (rd_bits << 8) | imm_bits;
2818                        Ok(instr.to_le_bytes().to_vec())
2819                    } else if uimm <= 0xFFFF {
2820                        // Use 32-bit MOVW for 16-bit immediates
2821                        self.encode_thumb32_movw(rd, uimm)
2822                    } else {
2823                        // Full 32-bit value: MOVW low16 + MOVT high16
2824                        let mut bytes = self.encode_thumb32_movw(rd, uimm & 0xFFFF)?;
2825                        bytes.extend(self.encode_thumb32_movt_raw(reg_to_bits(rd), uimm >> 16)?);
2826                        Ok(bytes)
2827                    }
2828                } else if let Operand2::Reg(rm) = op2 {
2829                    let rm_bits = reg_to_bits(rm) as u16;
2830                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
2831                    // D = Rd[3], Rd[2:0] in lower bits
2832                    let d_bit = (rd_bits >> 3) & 1;
2833                    let instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
2834                    Ok(instr.to_le_bytes().to_vec())
2835                } else {
2836                    let instr: u16 = 0xBF00; // NOP fallback
2837                    Ok(instr.to_le_bytes().to_vec())
2838                }
2839            }
2840
2841            ArmOp::Push { regs } => {
2842                // Thumb-2 PUSH encoding:
2843                // If all regs in R0-R7 + LR, use 16-bit: 1011 010 M rrrrrrrr
2844                // Otherwise use 32-bit: STMDB SP!, {regs} = 1110 1001 0010 1101 | 0M0 reglist(13)
2845                let mut reg_list: u16 = 0;
2846                let mut need_32bit = false;
2847                for r in regs {
2848                    let bit = reg_to_bits(r);
2849                    if bit >= 8 && *r != Reg::LR {
2850                        need_32bit = true;
2851                    }
2852                    reg_list |= 1 << bit;
2853                }
2854                if !need_32bit {
2855                    // 16-bit PUSH: 1011 010 M rrrrrrrr
2856                    let m_bit = if reg_list & (1 << 14) != 0 {
2857                        1u16
2858                    } else {
2859                        0u16
2860                    };
2861                    let low_regs = reg_list & 0xFF;
2862                    let instr: u16 = 0xB400 | (m_bit << 8) | low_regs;
2863                    Ok(instr.to_le_bytes().to_vec())
2864                } else {
2865                    // 32-bit STMDB SP!, {regs}: E92D | reglist(16)
2866                    let hw1: u16 = 0xE92D;
2867                    let hw2: u16 = reg_list;
2868                    let mut bytes = hw1.to_le_bytes().to_vec();
2869                    bytes.extend_from_slice(&hw2.to_le_bytes());
2870                    Ok(bytes)
2871                }
2872            }
2873
2874            ArmOp::Pop { regs } => {
2875                // Thumb-2 POP encoding:
2876                // If all regs in R0-R7 + PC, use 16-bit: 1011 110 P rrrrrrrr
2877                // Otherwise use 32-bit: LDMIA SP!, {regs} = 1110 1000 1011 1101 | PM0 reglist(13)
2878                let mut reg_list: u16 = 0;
2879                let mut need_32bit = false;
2880                for r in regs {
2881                    let bit = reg_to_bits(r);
2882                    if bit >= 8 && *r != Reg::PC {
2883                        need_32bit = true;
2884                    }
2885                    reg_list |= 1 << bit;
2886                }
2887                if !need_32bit {
2888                    // 16-bit POP: 1011 110 P rrrrrrrr
2889                    let p_bit = if reg_list & (1 << 15) != 0 {
2890                        1u16
2891                    } else {
2892                        0u16
2893                    };
2894                    let low_regs = reg_list & 0xFF;
2895                    let instr: u16 = 0xBC00 | (p_bit << 8) | low_regs;
2896                    Ok(instr.to_le_bytes().to_vec())
2897                } else {
2898                    // 32-bit LDMIA SP!, {regs}: E8BD | reglist(16)
2899                    let hw1: u16 = 0xE8BD;
2900                    let hw2: u16 = reg_list;
2901                    let mut bytes = hw1.to_le_bytes().to_vec();
2902                    bytes.extend_from_slice(&hw2.to_le_bytes());
2903                    Ok(bytes)
2904                }
2905            }
2906
2907            ArmOp::Nop => {
2908                let instr: u16 = 0xBF00; // NOP in Thumb-2
2909                Ok(instr.to_le_bytes().to_vec())
2910            }
2911
2912            ArmOp::Udf { imm } => {
2913                // UDF (Undefined) in Thumb-2: 16-bit encoding is 0xDE00 | imm8
2914                // This triggers UsageFault/HardFault, used for WASM traps
2915                let instr: u16 = 0xDE00 | (*imm as u16);
2916                let bytes = instr.to_le_bytes().to_vec();
2917                encoding_contracts::verify_thumb16(&bytes);
2918                Ok(bytes)
2919            }
2920
2921            // i64 support: ADDS, ADC, SUBS, SBC for register pair arithmetic
2922            // ADDS sets flags (carry), ADC uses carry from previous ADDS
2923            ArmOp::Adds { rd, rn, op2 } => {
2924                let rd_bits = reg_to_bits(rd) as u16;
2925                let rn_bits = reg_to_bits(rn) as u16;
2926
2927                if let Operand2::Reg(rm) = op2 {
2928                    let rm_bits = reg_to_bits(rm) as u16;
2929                    // 16-bit ADDS is R0-R7 only; i64 pair allocation can place
2930                    // operands in R8-R11, which would overflow the 3-bit fields
2931                    // and corrupt the operands (#178/#180 class). Guard and fall
2932                    // back to 32-bit ADDS.W for high registers.
2933                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2934                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2935                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2936                        Ok(instr.to_le_bytes().to_vec())
2937                    } else {
2938                        self.encode_thumb32_adds_reg_raw(
2939                            rd_bits as u32,
2940                            rn_bits as u32,
2941                            rm_bits as u32,
2942                        )
2943                    }
2944                } else {
2945                    // 32-bit Thumb-2 ADDS with immediate
2946                    self.encode_thumb32_adds(rd, rn, 0)
2947                }
2948            }
2949
2950            // ADC: Add with Carry (Thumb-2 32-bit)
2951            // ADC.W Rd, Rn, Rm: EB40 Rn | 00 Rd 00 Rm
2952            ArmOp::Adc { rd, rn, op2 } => {
2953                let rd_bits = reg_to_bits(rd);
2954                let rn_bits = reg_to_bits(rn);
2955
2956                if let Operand2::Reg(rm) = op2 {
2957                    let rm_bits = reg_to_bits(rm);
2958                    // ADC.W Rd, Rn, Rm (T2): 1110 1011 0100 Rn | 0 000 Rd 00 00 Rm
2959                    let hw1: u16 = (0xEB40 | rn_bits) as u16;
2960                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2961
2962                    let mut bytes = hw1.to_le_bytes().to_vec();
2963                    bytes.extend_from_slice(&hw2.to_le_bytes());
2964                    Ok(bytes)
2965                } else {
2966                    // ADC with immediate - use 32-bit encoding
2967                    let hw1: u16 = (0xF140 | rn_bits) as u16;
2968                    let hw2: u16 = (rd_bits << 8) as u16;
2969                    let mut bytes = hw1.to_le_bytes().to_vec();
2970                    bytes.extend_from_slice(&hw2.to_le_bytes());
2971                    Ok(bytes)
2972                }
2973            }
2974
2975            // SUBS sets flags (borrow), SBC uses borrow from previous SUBS
2976            ArmOp::Subs { rd, rn, op2 } => {
2977                let rd_bits = reg_to_bits(rd) as u16;
2978                let rn_bits = reg_to_bits(rn) as u16;
2979
2980                if let Operand2::Reg(rm) = op2 {
2981                    let rm_bits = reg_to_bits(rm) as u16;
2982                    // 16-bit SUBS is R0-R7 only; high-register i64 pair operands
2983                    // would overflow the 3-bit fields (#178/#180 class). Guard
2984                    // and fall back to 32-bit SUBS.W for high registers.
2985                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2986                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2987                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2988                        Ok(instr.to_le_bytes().to_vec())
2989                    } else {
2990                        self.encode_thumb32_subs_reg_raw(
2991                            rd_bits as u32,
2992                            rn_bits as u32,
2993                            rm_bits as u32,
2994                        )
2995                    }
2996                } else {
2997                    // 32-bit Thumb-2 SUBS with immediate
2998                    self.encode_thumb32_subs(rd, rn, 0)
2999                }
3000            }
3001
3002            // SBC: Subtract with Carry (Thumb-2 32-bit)
3003            // SBC.W Rd, Rn, Rm: EB60 Rn | 00 Rd 00 Rm
3004            ArmOp::Sbc { rd, rn, op2 } => {
3005                let rd_bits = reg_to_bits(rd);
3006                let rn_bits = reg_to_bits(rn);
3007
3008                if let Operand2::Reg(rm) = op2 {
3009                    let rm_bits = reg_to_bits(rm);
3010                    // SBC.W Rd, Rn, Rm (T2): 1110 1011 0110 Rn | 0 000 Rd 00 00 Rm
3011                    let hw1: u16 = (0xEB60 | rn_bits) as u16;
3012                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3013
3014                    let mut bytes = hw1.to_le_bytes().to_vec();
3015                    bytes.extend_from_slice(&hw2.to_le_bytes());
3016                    Ok(bytes)
3017                } else {
3018                    // SBC with immediate - use 32-bit encoding
3019                    let hw1: u16 = (0xF160 | rn_bits) as u16;
3020                    let hw2: u16 = (rd_bits << 8) as u16;
3021                    let mut bytes = hw1.to_le_bytes().to_vec();
3022                    bytes.extend_from_slice(&hw2.to_le_bytes());
3023                    Ok(bytes)
3024                }
3025            }
3026
3027            // === 32-bit Thumb-2 encodings ===
3028
3029            // SDIV: 11111011 1001 Rn 1111 Rd 1111 Rm
3030            ArmOp::Sdiv { rd, rn, rm } => {
3031                let rd_bits = reg_to_bits(rd);
3032                let rn_bits = reg_to_bits(rn);
3033                let rm_bits = reg_to_bits(rm);
3034                reg_bits_checked(rd_bits)?;
3035                reg_bits_checked(rn_bits)?;
3036                reg_bits_checked(rm_bits)?;
3037
3038                // Thumb-2 SDIV: FB90 F0F0 | Rn<<16 | Rd<<8 | Rm
3039                // First halfword: 1111 1011 1001 Rn = 0xFB90 | Rn
3040                // Second halfword: 1111 Rd 1111 Rm = 0xF0F0 | Rd<<8 | Rm
3041                let hw1: u16 = (0xFB90 | rn_bits) as u16;
3042                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
3043
3044                // Thumb-2 32-bit instructions: first halfword, then second halfword (little-endian each)
3045                let mut bytes = hw1.to_le_bytes().to_vec();
3046                bytes.extend_from_slice(&hw2.to_le_bytes());
3047                encoding_contracts::verify_thumb32(&bytes);
3048                Ok(bytes)
3049            }
3050
3051            // UDIV: 11111011 1011 Rn 1111 Rd 1111 Rm
3052            ArmOp::Udiv { rd, rn, rm } => {
3053                let rd_bits = reg_to_bits(rd);
3054                let rn_bits = reg_to_bits(rn);
3055                let rm_bits = reg_to_bits(rm);
3056                reg_bits_checked(rd_bits)?;
3057                reg_bits_checked(rn_bits)?;
3058                reg_bits_checked(rm_bits)?;
3059
3060                // Thumb-2 UDIV: FBB0 F0F0 | Rn<<16 | Rd<<8 | Rm
3061                let hw1: u16 = (0xFBB0 | rn_bits) as u16;
3062                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
3063
3064                let mut bytes = hw1.to_le_bytes().to_vec();
3065                bytes.extend_from_slice(&hw2.to_le_bytes());
3066                encoding_contracts::verify_thumb32(&bytes);
3067                Ok(bytes)
3068            }
3069
3070            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
3071                let rdlo_bits = reg_to_bits(rdlo);
3072                let rdhi_bits = reg_to_bits(rdhi);
3073                let rn_bits = reg_to_bits(rn);
3074                let rm_bits = reg_to_bits(rm);
3075                reg_bits_checked(rdlo_bits)?;
3076                reg_bits_checked(rdhi_bits)?;
3077                reg_bits_checked(rn_bits)?;
3078                reg_bits_checked(rm_bits)?;
3079
3080                // Thumb-2 UMULL: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm
3081                let hw1: u16 = (0xFBA0 | rn_bits) as u16;
3082                let hw2: u16 = ((rdlo_bits << 12) | (rdhi_bits << 8) | rm_bits) as u16;
3083
3084                let mut bytes = hw1.to_le_bytes().to_vec();
3085                bytes.extend_from_slice(&hw2.to_le_bytes());
3086                encoding_contracts::verify_thumb32(&bytes);
3087                Ok(bytes)
3088            }
3089
3090            // MUL (Thumb-2 32-bit): MUL Rd, Rn, Rm
3091            ArmOp::Mul { rd, rn, rm } => {
3092                let rd_bits = reg_to_bits(rd);
3093                let rn_bits = reg_to_bits(rn);
3094                let rm_bits = reg_to_bits(rm);
3095
3096                // Thumb-2 MUL: FB00 F000 | Rn | Rd<<8 | Rm
3097                // 11111011 0000 Rn | 1111 Rd 0000 Rm
3098                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3099                let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
3100
3101                let mut bytes = hw1.to_le_bytes().to_vec();
3102                bytes.extend_from_slice(&hw2.to_le_bytes());
3103                Ok(bytes)
3104            }
3105
3106            // MLS: Rd = Ra - Rn * Rm
3107            ArmOp::Mls { rd, rn, rm, ra } => {
3108                let rd_bits = reg_to_bits(rd);
3109                let rn_bits = reg_to_bits(rn);
3110                let rm_bits = reg_to_bits(rm);
3111                let ra_bits = reg_to_bits(ra);
3112
3113                // Thumb-2 MLS: FB00 Rn | Ra Rd 0001 Rm
3114                // 11111011 0000 Rn | Ra Rd 0001 Rm
3115                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3116                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | 0x10 | rm_bits) as u16;
3117
3118                let mut bytes = hw1.to_le_bytes().to_vec();
3119                bytes.extend_from_slice(&hw2.to_le_bytes());
3120                Ok(bytes)
3121            }
3122
3123            ArmOp::Mla { rd, rn, rm, ra } => {
3124                let rd_bits = reg_to_bits(rd);
3125                let rn_bits = reg_to_bits(rn);
3126                let rm_bits = reg_to_bits(rm);
3127                let ra_bits = reg_to_bits(ra);
3128
3129                // Thumb-2 MLA: FB00 Rn | Ra Rd 0000 Rm — same as MLS without the
3130                // bit-4 (0x10) op flag. rd = ra + rn*rm.
3131                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3132                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | rm_bits) as u16;
3133
3134                let mut bytes = hw1.to_le_bytes().to_vec();
3135                bytes.extend_from_slice(&hw2.to_le_bytes());
3136                Ok(bytes)
3137            }
3138
3139            // AND (Thumb-2 32-bit)
3140            ArmOp::And { rd, rn, op2 } => {
3141                if let Operand2::Reg(rm) = op2 {
3142                    let rd_bits = reg_to_bits(rd);
3143                    let rn_bits = reg_to_bits(rn);
3144                    let rm_bits = reg_to_bits(rm);
3145
3146                    // Thumb-2 AND register: EA00 Rn | 0 Rd 00 00 Rm
3147                    let hw1: u16 = (0xEA00 | rn_bits) as u16;
3148                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3149
3150                    let mut bytes = hw1.to_le_bytes().to_vec();
3151                    bytes.extend_from_slice(&hw2.to_le_bytes());
3152                    Ok(bytes)
3153                } else if let Operand2::Imm(imm) = op2 {
3154                    let rd_bits = reg_to_bits(rd);
3155                    let rn_bits = reg_to_bits(rn);
3156
3157                    // Thumb-2 AND.W immediate T1: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8.
3158                    // The i:imm3:imm8 field is a ThumbExpandImm modified immediate —
3159                    // encode it correctly (or error on an un-encodable value)
3160                    // rather than packing raw bits, closing the silent-miscompile
3161                    // class for AND alongside ORR/EOR (#251) / ADD/SUB (#253) /
3162                    // CMP (#255).
3163                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3164                        synth_core::Error::synthesis(
3165                            "AND immediate is not a valid ThumbExpandImm — materialize into a register",
3166                        )
3167                    })?;
3168                    let i_bit = (field >> 11) & 1;
3169                    let imm3 = (field >> 8) & 0x7;
3170                    let imm8 = field & 0xFF;
3171
3172                    let hw1: u16 = (0xF000 | (i_bit << 10) | rn_bits) as u16;
3173                    let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) 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 {
3179                    // RegShift variant - fallback to NOP
3180                    let instr: u16 = 0xBF00;
3181                    Ok(instr.to_le_bytes().to_vec())
3182                }
3183            }
3184
3185            // ORR (Thumb-2 32-bit)
3186            ArmOp::Orr { rd, rn, op2 } => {
3187                if let Operand2::Reg(rm) = op2 {
3188                    let rd_bits = reg_to_bits(rd);
3189                    let rn_bits = reg_to_bits(rn);
3190                    let rm_bits = reg_to_bits(rm);
3191
3192                    // Thumb-2 ORR: EA40 Rn | 0 Rd 00 00 Rm
3193                    let hw1: u16 = (0xEA40 | rn_bits) as u16;
3194                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3195
3196                    let mut bytes = hw1.to_le_bytes().to_vec();
3197                    bytes.extend_from_slice(&hw2.to_le_bytes());
3198                    Ok(bytes)
3199                } else if let Operand2::Imm(imm) = op2 {
3200                    // ORR.W immediate T1: 11110 i 0 0010 S Rn | 0 imm3 Rd imm8.
3201                    // Only the zero-extended byte form (imm <= 0xFF) is encoded;
3202                    // larger modified immediates need ThumbExpandImm — return an
3203                    // error rather than silently emit a NOP (Ok-or-Err, #180/#185).
3204                    let imm_val = *imm as u32;
3205                    if imm_val > 0xFF {
3206                        return Err(synth_core::Error::synthesis(
3207                            "ORR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3208                        ));
3209                    }
3210                    let rd_bits = reg_to_bits(rd);
3211                    let rn_bits = reg_to_bits(rn);
3212                    let hw1: u16 = (0xF040 | rn_bits) as u16;
3213                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3214                    let mut bytes = hw1.to_le_bytes().to_vec();
3215                    bytes.extend_from_slice(&hw2.to_le_bytes());
3216                    Ok(bytes)
3217                } else {
3218                    let instr: u16 = 0xBF00;
3219                    Ok(instr.to_le_bytes().to_vec())
3220                }
3221            }
3222
3223            // EOR (Thumb-2 32-bit)
3224            ArmOp::Eor { rd, rn, op2 } => {
3225                if let Operand2::Reg(rm) = op2 {
3226                    let rd_bits = reg_to_bits(rd);
3227                    let rn_bits = reg_to_bits(rn);
3228                    let rm_bits = reg_to_bits(rm);
3229
3230                    // Thumb-2 EOR: EA80 Rn | 0 Rd 00 00 Rm
3231                    let hw1: u16 = (0xEA80 | rn_bits) as u16;
3232                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3233
3234                    let mut bytes = hw1.to_le_bytes().to_vec();
3235                    bytes.extend_from_slice(&hw2.to_le_bytes());
3236                    Ok(bytes)
3237                } else if let Operand2::Imm(imm) = op2 {
3238                    // EOR.W immediate T1: 11110 i 0 0100 S Rn | 0 imm3 Rd imm8.
3239                    // Byte form only (imm <= 0xFF); larger needs ThumbExpandImm —
3240                    // error, not a silent NOP (Ok-or-Err, #180/#185).
3241                    let imm_val = *imm as u32;
3242                    if imm_val > 0xFF {
3243                        return Err(synth_core::Error::synthesis(
3244                            "EOR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3245                        ));
3246                    }
3247                    let rd_bits = reg_to_bits(rd);
3248                    let rn_bits = reg_to_bits(rn);
3249                    let hw1: u16 = (0xF080 | rn_bits) as u16;
3250                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3251                    let mut bytes = hw1.to_le_bytes().to_vec();
3252                    bytes.extend_from_slice(&hw2.to_le_bytes());
3253                    Ok(bytes)
3254                } else {
3255                    let instr: u16 = 0xBF00;
3256                    Ok(instr.to_le_bytes().to_vec())
3257                }
3258            }
3259
3260            // Shift operations (16-bit for low registers)
3261            ArmOp::Lsl { rd, rn, shift } => {
3262                let rd_bits = reg_to_bits(rd) as u16;
3263                let rn_bits = reg_to_bits(rn) as u16;
3264                let shift_bits = (*shift as u16) & 0x1F;
3265
3266                if rd_bits < 8 && rn_bits < 8 {
3267                    // LSLS Rd, Rm, #imm5 (16-bit): 0000 0 imm5 Rm Rd
3268                    let instr: u16 = (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3269                    Ok(instr.to_le_bytes().to_vec())
3270                } else {
3271                    // Use 32-bit encoding for high registers
3272                    self.encode_thumb32_shift(rd, rn, *shift, 0b00) // LSL type
3273                }
3274            }
3275
3276            ArmOp::Lsr { rd, rn, shift } => {
3277                let rd_bits = reg_to_bits(rd) as u16;
3278                let rn_bits = reg_to_bits(rn) as u16;
3279                let shift_bits = (*shift as u16) & 0x1F;
3280
3281                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3282                    // LSRS Rd, Rm, #imm5 (16-bit): 0000 1 imm5 Rm Rd
3283                    let instr: u16 = 0x0800 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3284                    Ok(instr.to_le_bytes().to_vec())
3285                } else {
3286                    self.encode_thumb32_shift(rd, rn, *shift, 0b01) // LSR type
3287                }
3288            }
3289
3290            ArmOp::Asr { rd, rn, shift } => {
3291                let rd_bits = reg_to_bits(rd) as u16;
3292                let rn_bits = reg_to_bits(rn) as u16;
3293                let shift_bits = (*shift as u16) & 0x1F;
3294
3295                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3296                    // ASRS Rd, Rm, #imm5 (16-bit): 0001 0 imm5 Rm Rd
3297                    let instr: u16 = 0x1000 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3298                    Ok(instr.to_le_bytes().to_vec())
3299                } else {
3300                    self.encode_thumb32_shift(rd, rn, *shift, 0b10) // ASR type
3301                }
3302            }
3303
3304            ArmOp::Ror { rd, rn, shift } => {
3305                // ROR doesn't have a 16-bit immediate form, use 32-bit
3306                self.encode_thumb32_shift(rd, rn, *shift, 0b11) // ROR type
3307            }
3308
3309            // Register-based shifts (Thumb-2 32-bit)
3310            // Encoding: 11111010 0xxS Rn 1111 Rd 0000 Rm
3311            // xx = shift type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
3312            ArmOp::LslReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b00),
3313            ArmOp::LsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b01),
3314            ArmOp::AsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b10),
3315            ArmOp::RorReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b11),
3316
3317            // RSB (Reverse Subtract): Rd = imm - Rn
3318            // Thumb-2 T2 encoding: 11110 i 0 1110 S Rn | 0 imm3 Rd imm8
3319            ArmOp::Rsb { rd, rn, imm } => {
3320                let rd_bits = reg_to_bits(rd);
3321                let rn_bits = reg_to_bits(rn);
3322
3323                // #681 class audit: the T2 `i:imm3:imm8` field is a
3324                // ThumbExpandImm modified immediate and RSB has NO plain-imm12
3325                // (T4-style) form — packing a raw value > 0xFF silently encodes
3326                // a different constant (#253/#255 class). All current emitters
3327                // use imm 32 (shift complement), which expands to itself, so
3328                // this gate is byte-identical for existing codegen.
3329                let field = try_thumb_expand_imm(*imm).ok_or_else(|| {
3330                    synth_core::Error::synthesis(
3331                        "RSB immediate is not a valid ThumbExpandImm — materialize into a register",
3332                    )
3333                })?;
3334                let i_bit = (field >> 11) & 1;
3335                let imm3 = (field >> 8) & 0x7;
3336                let imm8 = field & 0xFF;
3337
3338                // hw1: 11110 i 01110 0 Rn  (S=0)
3339                let hw1: u16 = (0xF1C0 | (i_bit << 10) | rn_bits) as u16;
3340                // hw2: 0 imm3 Rd imm8
3341                let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3342
3343                let mut bytes = hw1.to_le_bytes().to_vec();
3344                bytes.extend_from_slice(&hw2.to_le_bytes());
3345                Ok(bytes)
3346            }
3347
3348            // CLZ (Thumb-2 32-bit)
3349            ArmOp::Clz { rd, rm } => {
3350                let rd_bits = reg_to_bits(rd);
3351                let rm_bits = reg_to_bits(rm);
3352
3353                // Thumb-2 CLZ: FAB0 Rm | F8 Rd Rm
3354                // 11111010 1011 Rm | 1111 1000 Rd Rm
3355                let hw1: u16 = (0xFAB0 | rm_bits) as u16;
3356                let hw2: u16 = (0xF080 | (rd_bits << 8) | rm_bits) as u16;
3357
3358                let mut bytes = hw1.to_le_bytes().to_vec();
3359                bytes.extend_from_slice(&hw2.to_le_bytes());
3360                Ok(bytes)
3361            }
3362
3363            // RBIT (Thumb-2 32-bit)
3364            ArmOp::Rbit { rd, rm } => {
3365                let rd_bits = reg_to_bits(rd);
3366                let rm_bits = reg_to_bits(rm);
3367
3368                // Thumb-2 RBIT: FA90 Rm | F0 Rd A0 Rm
3369                // 11111010 1001 Rm | 1111 Rd 1010 Rm
3370                let hw1: u16 = (0xFA90 | rm_bits) as u16;
3371                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rm_bits) as u16;
3372
3373                let mut bytes = hw1.to_le_bytes().to_vec();
3374                bytes.extend_from_slice(&hw2.to_le_bytes());
3375                Ok(bytes)
3376            }
3377
3378            // SXTB (16-bit for low registers)
3379            ArmOp::Sxtb { rd, rm } => {
3380                let rd_bits = reg_to_bits(rd) as u16;
3381                let rm_bits = reg_to_bits(rm) as u16;
3382
3383                if rd_bits < 8 && rm_bits < 8 {
3384                    // SXTB Rd, Rm (16-bit): 1011 0010 01 Rm Rd
3385                    let instr: u16 = 0xB240 | (rm_bits << 3) | rd_bits;
3386                    Ok(instr.to_le_bytes().to_vec())
3387                } else {
3388                    // Thumb-2 SXTB.W: FA4F F(rd)80 (rm)
3389                    // 11111010 0100 1111 | 1111 Rd 10 rotate Rm
3390                    let rd_bits32 = rd_bits as u32;
3391                    let rm_bits32 = rm_bits as u32;
3392                    let hw1: u16 = 0xFA4F;
3393                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3394                    let mut bytes = hw1.to_le_bytes().to_vec();
3395                    bytes.extend_from_slice(&hw2.to_le_bytes());
3396                    Ok(bytes)
3397                }
3398            }
3399
3400            // SXTH (16-bit for low registers)
3401            ArmOp::Sxth { rd, rm } => {
3402                let rd_bits = reg_to_bits(rd) as u16;
3403                let rm_bits = reg_to_bits(rm) as u16;
3404
3405                if rd_bits < 8 && rm_bits < 8 {
3406                    // SXTH Rd, Rm (16-bit): 1011 0010 00 Rm Rd
3407                    let instr: u16 = 0xB200 | (rm_bits << 3) | rd_bits;
3408                    Ok(instr.to_le_bytes().to_vec())
3409                } else {
3410                    // Thumb-2 SXTH.W: FA0F F(rd)80 (rm)
3411                    // 11111010 0000 1111 | 1111 Rd 10 rotate Rm
3412                    let rd_bits32 = rd_bits as u32;
3413                    let rm_bits32 = rm_bits as u32;
3414                    let hw1: u16 = 0xFA0F;
3415                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3416                    let mut bytes = hw1.to_le_bytes().to_vec();
3417                    bytes.extend_from_slice(&hw2.to_le_bytes());
3418                    Ok(bytes)
3419                }
3420            }
3421
3422            // UXTB Rd,Rm — zero-extend byte (rd = rm & 0xff)
3423            ArmOp::Uxtb { rd, rm } => {
3424                let rd_bits = reg_to_bits(rd) as u16;
3425                let rm_bits = reg_to_bits(rm) as u16;
3426                if rd_bits < 8 && rm_bits < 8 {
3427                    // UXTB Rd, Rm (16-bit): 1011 0010 11 Rm Rd
3428                    let instr: u16 = 0xB2C0 | (rm_bits << 3) | rd_bits;
3429                    Ok(instr.to_le_bytes().to_vec())
3430                } else {
3431                    // Thumb-2 UXTB.W: FA5F F(rd)80 (rm)
3432                    let hw1: u16 = 0xFA5F;
3433                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3434                    let mut bytes = hw1.to_le_bytes().to_vec();
3435                    bytes.extend_from_slice(&hw2.to_le_bytes());
3436                    Ok(bytes)
3437                }
3438            }
3439
3440            // UXTH Rd,Rm — zero-extend halfword (rd = rm & 0xffff)
3441            ArmOp::Uxth { rd, rm } => {
3442                let rd_bits = reg_to_bits(rd) as u16;
3443                let rm_bits = reg_to_bits(rm) as u16;
3444                if rd_bits < 8 && rm_bits < 8 {
3445                    // UXTH Rd, Rm (16-bit): 1011 0010 10 Rm Rd
3446                    let instr: u16 = 0xB280 | (rm_bits << 3) | rd_bits;
3447                    Ok(instr.to_le_bytes().to_vec())
3448                } else {
3449                    // Thumb-2 UXTH.W: FA1F F(rd)80 (rm)
3450                    let hw1: u16 = 0xFA1F;
3451                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) 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                }
3456            }
3457
3458            // CMP (can be 16-bit for low registers)
3459            ArmOp::Cmp { rn, op2 } => {
3460                let rn_bits = reg_to_bits(rn) as u16;
3461
3462                if let Operand2::Imm(imm) = op2 {
3463                    // Only use 16-bit encoding for non-negative immediates 0-255
3464                    // Negative immediates must use 32-bit encoding
3465                    if *imm >= 0 && *imm <= 255 && rn_bits < 8 {
3466                        // CMP Rn, #imm8 (16-bit): 0010 1 Rn imm8
3467                        let instr: u16 = 0x2800 | (rn_bits << 8) | (*imm as u16 & 0xFF);
3468                        Ok(instr.to_le_bytes().to_vec())
3469                    } else {
3470                        self.encode_thumb32_cmp_imm(rn, *imm as u32)
3471                    }
3472                } else if let Operand2::Reg(rm) = op2 {
3473                    let rm_bits = reg_to_bits(rm) as u16;
3474                    if rn_bits < 8 && rm_bits < 8 {
3475                        // CMP Rn, Rm (16-bit low): 0100 0010 10 Rm Rn
3476                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
3477                        Ok(instr.to_le_bytes().to_vec())
3478                    } else {
3479                        // CMP Rn, Rm (16-bit high): 0100 0101 N Rm Rn[2:0]
3480                        let n_bit = (rn_bits >> 3) & 1;
3481                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
3482                        Ok(instr.to_le_bytes().to_vec())
3483                    }
3484                } else {
3485                    let instr: u16 = 0xBF00;
3486                    Ok(instr.to_le_bytes().to_vec())
3487                }
3488            }
3489
3490            // CMN (Compare Negative) - computes Rn + op2 and sets flags
3491            // CMN Rn, #1 sets Z flag if Rn == -1 (since -1 + 1 = 0)
3492            ArmOp::Cmn { rn, op2 } => {
3493                let rn_bits = reg_to_bits(rn) as u16;
3494
3495                if let Operand2::Imm(imm) = op2 {
3496                    // CMN.W Rn, #imm (32-bit): i:imm3:imm8 is a ThumbExpandImm
3497                    // modified immediate (the field sits in imm3=hw2[14:12],
3498                    // imm8=hw2[7:0], i=hw1[10]). Encode it correctly, or error on
3499                    // an un-encodable value — replacing the old silent `0xBF00`
3500                    // NOP (the last of the silent-miscompile data-proc encoders).
3501                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3502                        synth_core::Error::synthesis(
3503                            "CMN immediate is not a valid ThumbExpandImm — materialize into a register",
3504                        )
3505                    })?;
3506                    let i_bit = (field >> 11) & 1;
3507                    let imm3 = (field >> 8) & 0x7;
3508                    let imm8 = field & 0xFF;
3509                    let hw1: u16 = (0xF110 | (i_bit << 10) as u16) | rn_bits;
3510                    let hw2: u16 = (imm3 << 12) as u16 | 0x0F00 | imm8 as u16;
3511                    let mut bytes = hw1.to_le_bytes().to_vec();
3512                    bytes.extend_from_slice(&hw2.to_le_bytes());
3513                    Ok(bytes)
3514                } else if let Operand2::Reg(rm) = op2 {
3515                    let rm_bits = reg_to_bits(rm) as u16;
3516                    // 16-bit CMN (T1) only encodes R0-R7; high registers overflow
3517                    // the 3-bit fields and corrupt the operands (#184, the #180
3518                    // class). CMN has no high-register 16-bit form, so fall back
3519                    // to 32-bit CMN.W (T2): EB10 Rn | 0F00 Rm (ADD.W with S=1 and
3520                    // Rd discarded as PC/1111).
3521                    if rn_bits < 8 && rm_bits < 8 {
3522                        // CMN Rn, Rm (16-bit): 0100 0010 11 Rm Rn
3523                        let instr: u16 = 0x42C0 | (rm_bits << 3) | rn_bits;
3524                        Ok(instr.to_le_bytes().to_vec())
3525                    } else {
3526                        let hw1: u16 = 0xEB10 | rn_bits;
3527                        let hw2: u16 = 0x0F00 | rm_bits;
3528                        let mut bytes = hw1.to_le_bytes().to_vec();
3529                        bytes.extend_from_slice(&hw2.to_le_bytes());
3530                        Ok(bytes)
3531                    }
3532                } else {
3533                    Ok(vec![0xBF, 0x00])
3534                }
3535            }
3536
3537            // LDR (can be 16-bit for simple cases)
3538            ArmOp::Ldr { rd, addr } => {
3539                let rd_bits = reg_to_bits(rd);
3540                let base_bits = reg_to_bits(&addr.base);
3541
3542                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3543                if let Some(offset_reg) = &addr.offset_reg {
3544                    let rm_bits = reg_to_bits(offset_reg);
3545
3546                    // If there's also an immediate offset, we need to ADD it first
3547                    if addr.offset != 0 {
3548                        // Use R12 (IP) as scratch to avoid clobbering the address register
3549                        // ADD R12, Rm, #offset; LDR Rd, [base, R12]
3550                        let scratch = Reg::R12;
3551                        let mut bytes =
3552                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3553                        bytes.extend(self.encode_thumb32_ldr_reg(rd, &addr.base, &scratch)?);
3554                        return Ok(bytes);
3555                    }
3556
3557                    // Simple register offset: LDR Rd, [Rn, Rm]
3558                    // 16-bit: only if Rd, Rn, Rm < R8
3559                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3560                        // LDR Rd, [Rn, Rm] (16-bit): 0101 100 Rm Rn Rd
3561                        let instr: u16 = 0x5800
3562                            | ((rm_bits as u16) << 6)
3563                            | ((base_bits as u16) << 3)
3564                            | (rd_bits as u16);
3565                        return Ok(instr.to_le_bytes().to_vec());
3566                    }
3567
3568                    // 32-bit register offset
3569                    return self.encode_thumb32_ldr_reg(rd, &addr.base, offset_reg);
3570                }
3571
3572                // Immediate offset mode [base, #imm]
3573                let offset = addr.offset as u32;
3574
3575                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3576                    // LDR Rd, [Rn, #imm5*4] (16-bit): 0110 1 imm5 Rn Rd
3577                    let imm5 = (offset >> 2) as u16;
3578                    let instr: u16 =
3579                        0x6800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3580                    Ok(instr.to_le_bytes().to_vec())
3581                } else {
3582                    self.encode_thumb32_ldr(rd, &addr.base, offset)
3583                }
3584            }
3585
3586            // STR (can be 16-bit for simple cases)
3587            ArmOp::Str { rd, addr } => {
3588                let rd_bits = reg_to_bits(rd);
3589                let base_bits = reg_to_bits(&addr.base);
3590
3591                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3592                if let Some(offset_reg) = &addr.offset_reg {
3593                    let rm_bits = reg_to_bits(offset_reg);
3594
3595                    // If there's also an immediate offset, we need to ADD it first
3596                    if addr.offset != 0 {
3597                        // Use R12 (IP) as scratch to avoid clobbering the address register
3598                        // ADD R12, Rm, #offset; STR Rd, [base, R12]
3599                        let scratch = Reg::R12;
3600                        let mut bytes =
3601                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3602                        bytes.extend(self.encode_thumb32_str_reg(rd, &addr.base, &scratch)?);
3603                        return Ok(bytes);
3604                    }
3605
3606                    // Simple register offset: STR Rd, [Rn, Rm]
3607                    // 16-bit: only if Rd, Rn, Rm < R8
3608                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3609                        // STR Rd, [Rn, Rm] (16-bit): 0101 000 Rm Rn Rd
3610                        let instr: u16 = 0x5000
3611                            | ((rm_bits as u16) << 6)
3612                            | ((base_bits as u16) << 3)
3613                            | (rd_bits as u16);
3614                        return Ok(instr.to_le_bytes().to_vec());
3615                    }
3616
3617                    // 32-bit register offset
3618                    return self.encode_thumb32_str_reg(rd, &addr.base, offset_reg);
3619                }
3620
3621                // Immediate offset mode [base, #imm]
3622                let offset = addr.offset as u32;
3623
3624                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3625                    // STR Rd, [Rn, #imm5*4] (16-bit): 0110 0 imm5 Rn Rd
3626                    let imm5 = (offset >> 2) as u16;
3627                    let instr: u16 =
3628                        0x6000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3629                    Ok(instr.to_le_bytes().to_vec())
3630                } else {
3631                    self.encode_thumb32_str(rd, &addr.base, offset)
3632                }
3633            }
3634
3635            // LDRB (Thumb-2)
3636            ArmOp::Ldrb { rd, addr } => {
3637                let rd_bits = reg_to_bits(rd);
3638                let base_bits = reg_to_bits(&addr.base);
3639
3640                if let Some(offset_reg) = &addr.offset_reg {
3641                    if addr.offset != 0 {
3642                        let scratch = Reg::R12;
3643                        let mut bytes =
3644                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3645                        bytes.extend(self.encode_thumb32_ldrb_reg(rd, &addr.base, &scratch)?);
3646                        return Ok(bytes);
3647                    }
3648                    return self.encode_thumb32_ldrb_reg(rd, &addr.base, offset_reg);
3649                }
3650
3651                let offset = addr.offset as u32;
3652                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3653                    // LDRB Rd, [Rn, #imm5] (16-bit): 0111 1 imm5 Rn Rd
3654                    let instr: u16 = 0x7800
3655                        | ((offset as u16) << 6)
3656                        | ((base_bits as u16) << 3)
3657                        | (rd_bits as u16);
3658                    Ok(instr.to_le_bytes().to_vec())
3659                } else {
3660                    self.encode_thumb32_ldrb_imm(rd, &addr.base, offset)
3661                }
3662            }
3663
3664            // LDRSB (Thumb-2)
3665            ArmOp::Ldrsb { rd, addr } => {
3666                let rd_bits = reg_to_bits(rd);
3667                let base_bits = reg_to_bits(&addr.base);
3668
3669                if let Some(offset_reg) = &addr.offset_reg {
3670                    if addr.offset != 0 {
3671                        let scratch = Reg::R12;
3672                        let mut bytes =
3673                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3674                        bytes.extend(self.encode_thumb32_ldrsb_reg(rd, &addr.base, &scratch)?);
3675                        return Ok(bytes);
3676                    }
3677                    return self.encode_thumb32_ldrsb_reg(rd, &addr.base, offset_reg);
3678                }
3679
3680                let offset = addr.offset as u32;
3681                // LDRSB has no 16-bit immediate form (only register)
3682                // For 16-bit reg form: only if Rd, Rn, Rm < R8
3683                if rd_bits < 8 && base_bits < 8 && offset == 0 {
3684                    // No immediate 16-bit encoding for LDRSB; use 32-bit
3685                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3686                } else {
3687                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3688                }
3689            }
3690
3691            // LDRH (Thumb-2)
3692            ArmOp::Ldrh { rd, addr } => {
3693                let rd_bits = reg_to_bits(rd);
3694                let base_bits = reg_to_bits(&addr.base);
3695
3696                if let Some(offset_reg) = &addr.offset_reg {
3697                    if addr.offset != 0 {
3698                        let scratch = Reg::R12;
3699                        let mut bytes =
3700                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3701                        bytes.extend(self.encode_thumb32_ldrh_reg(rd, &addr.base, &scratch)?);
3702                        return Ok(bytes);
3703                    }
3704                    return self.encode_thumb32_ldrh_reg(rd, &addr.base, offset_reg);
3705                }
3706
3707                let offset = addr.offset as u32;
3708                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3709                    // LDRH Rd, [Rn, #imm5*2] (16-bit): 1000 1 imm5 Rn Rd
3710                    let imm5 = (offset >> 1) as u16;
3711                    let instr: u16 =
3712                        0x8800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3713                    Ok(instr.to_le_bytes().to_vec())
3714                } else {
3715                    self.encode_thumb32_ldrh_imm(rd, &addr.base, offset)
3716                }
3717            }
3718
3719            // LDRSH (Thumb-2)
3720            ArmOp::Ldrsh { rd, addr } => {
3721                if let Some(offset_reg) = &addr.offset_reg {
3722                    if addr.offset != 0 {
3723                        let scratch = Reg::R12;
3724                        let mut bytes =
3725                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3726                        bytes.extend(self.encode_thumb32_ldrsh_reg(rd, &addr.base, &scratch)?);
3727                        return Ok(bytes);
3728                    }
3729                    return self.encode_thumb32_ldrsh_reg(rd, &addr.base, offset_reg);
3730                }
3731
3732                let offset = addr.offset as u32;
3733                self.encode_thumb32_ldrsh_imm(rd, &addr.base, offset)
3734            }
3735
3736            // STRB (Thumb-2)
3737            ArmOp::Strb { rd, addr } => {
3738                let rd_bits = reg_to_bits(rd);
3739                let base_bits = reg_to_bits(&addr.base);
3740
3741                if let Some(offset_reg) = &addr.offset_reg {
3742                    if addr.offset != 0 {
3743                        let scratch = Reg::R12;
3744                        let mut bytes =
3745                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3746                        bytes.extend(self.encode_thumb32_strb_reg(rd, &addr.base, &scratch)?);
3747                        return Ok(bytes);
3748                    }
3749                    return self.encode_thumb32_strb_reg(rd, &addr.base, offset_reg);
3750                }
3751
3752                let offset = addr.offset as u32;
3753                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3754                    // STRB Rd, [Rn, #imm5] (16-bit): 0111 0 imm5 Rn Rd
3755                    let instr: u16 = 0x7000
3756                        | ((offset as u16) << 6)
3757                        | ((base_bits as u16) << 3)
3758                        | (rd_bits as u16);
3759                    Ok(instr.to_le_bytes().to_vec())
3760                } else {
3761                    self.encode_thumb32_strb_imm(rd, &addr.base, offset)
3762                }
3763            }
3764
3765            // STRH (Thumb-2)
3766            ArmOp::Strh { rd, addr } => {
3767                let rd_bits = reg_to_bits(rd);
3768                let base_bits = reg_to_bits(&addr.base);
3769
3770                if let Some(offset_reg) = &addr.offset_reg {
3771                    if addr.offset != 0 {
3772                        let scratch = Reg::R12;
3773                        let mut bytes =
3774                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3775                        bytes.extend(self.encode_thumb32_strh_reg(rd, &addr.base, &scratch)?);
3776                        return Ok(bytes);
3777                    }
3778                    return self.encode_thumb32_strh_reg(rd, &addr.base, offset_reg);
3779                }
3780
3781                let offset = addr.offset as u32;
3782                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3783                    // STRH Rd, [Rn, #imm5*2] (16-bit): 1000 0 imm5 Rn Rd
3784                    let imm5 = (offset >> 1) as u16;
3785                    let instr: u16 =
3786                        0x8000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3787                    Ok(instr.to_le_bytes().to_vec())
3788                } else {
3789                    self.encode_thumb32_strh_imm(rd, &addr.base, offset)
3790                }
3791            }
3792
3793            // MemorySize (Thumb-2)
3794            ArmOp::MemorySize { rd } => {
3795                // LSR rd, R10, #16 — memory size in bytes / 65536 = pages
3796                // Thumb-2 16-bit: LSRS Rd, Rm, #imm5 — 0000 1 imm5 Rm Rd
3797                let rd_bits = reg_to_bits(rd);
3798                let r10_bits = reg_to_bits(&Reg::R10);
3799                if rd_bits < 8 && r10_bits < 8 {
3800                    let instr: u16 =
3801                        0x0800 | (16u16 << 6) | ((r10_bits as u16) << 3) | (rd_bits as u16);
3802                    Ok(instr.to_le_bytes().to_vec())
3803                } else {
3804                    // Thumb-2 32-bit LSR: 1110 1010 010 0 1111 | 0 imm3 Rd imm2 01 Rm
3805                    let imm5: u32 = 16;
3806                    let imm3 = (imm5 >> 2) & 0x7;
3807                    let imm2 = imm5 & 0x3;
3808                    let hw1: u16 = 0xEA4F;
3809                    let hw2: u16 =
3810                        ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | 0x10 | r10_bits) as u16;
3811                    let mut bytes = hw1.to_le_bytes().to_vec();
3812                    bytes.extend_from_slice(&hw2.to_le_bytes());
3813                    Ok(bytes)
3814                }
3815            }
3816
3817            // MemoryGrow (Thumb-2)
3818            ArmOp::MemoryGrow { rd, .. } => {
3819                // On embedded with fixed memory, always return -1 (failure)
3820                // MVN rd, #0 → MOV rd, #-1
3821                // Thumb-2 32-bit: MVN: 1111 0 i 0 0 0 1 1 0 1111 | 0 imm3 Rd imm8
3822                let rd_bits = reg_to_bits(rd);
3823                let hw1: u16 = 0xF06F; // MVN with i=0
3824                let hw2: u16 = (rd_bits << 8) as u16; // imm8=0 → ~0 = 0xFFFFFFFF = -1
3825                let mut bytes = hw1.to_le_bytes().to_vec();
3826                bytes.extend_from_slice(&hw2.to_le_bytes());
3827                Ok(bytes)
3828            }
3829
3830            // BX (16-bit)
3831            ArmOp::Bx { rm } => {
3832                let rm_bits = reg_to_bits(rm) as u16;
3833                // BX Rm (16-bit): 0100 0111 0 Rm 000
3834                let instr: u16 = 0x4700 | (rm_bits << 3);
3835                Ok(instr.to_le_bytes().to_vec())
3836            }
3837
3838            // BLX (16-bit) - Branch with Link and Exchange
3839            // BLX Rm: 0100 0111 1 Rm 000
3840            ArmOp::Blx { rm } => {
3841                let rm_bits = reg_to_bits(rm) as u16;
3842                let instr: u16 = 0x4780 | (rm_bits << 3);
3843                Ok(instr.to_le_bytes().to_vec())
3844            }
3845
3846            // CallIndirect - indirect function call via table lookup
3847            // table_index_reg contains the table index
3848            // Generates (#642): MOVW ip,#size [; MOVT]; CMP idx,ip; BLO +1;
3849            //                   UDF #0; LSL R12,idx,#2; LDR R12,[R11,R12]; BLX R12
3850            // #650, table_byte_offset != 0 (a non-zero table in the contiguous
3851            // R11 region): the pointer load becomes
3852            //                   ADD R12,R11,R12; LDR R12,[R12,#offset]
3853            // #664, null_check (the table has null slots, linked as ZERO
3854            // words): the loaded pointer is null-checked before the BLX —
3855            //                   CMP.W R12,#0; BNE +1; UDF #0
3856            // #676, type_check (heterogeneous table — runtime §4.4.8 type
3857            // check against the type-id sidecar at R11+off): after the
3858            // bounds guard —
3859            //                   LSL R12,idx,#2; ADD R12,R11,R12;
3860            //                   LDR R12,[R12,#type_off]; CMP.W R12,#id;
3861            //                   BEQ +1; UDF #0
3862            // (the dispatch tail then recomputes idx*4 — idx stays live).
3863            ArmOp::CallIndirect {
3864                rd: _,
3865                type_idx: _,
3866                table_index_reg,
3867                table_size,
3868                table_byte_offset,
3869                null_check,
3870                type_check,
3871            } => {
3872                let idx_reg = reg_to_bits(table_index_reg);
3873                let mut bytes = Vec::new();
3874
3875                // The expansion:
3876                // 1. Bounds guard (#642): trap (UDF #0, WASM Core §4.4.8) when
3877                //    index >= table size. Without it an out-of-bounds index
3878                //    reads past the table and BLXes whatever word lies there —
3879                //    an uncontrolled indirect branch instead of a trap.
3880                // 2. Multiplies index by 4 (function pointer size)
3881                // 3. Loads function pointer from table (table base in R11)
3882                // 4. Calls the function via BLX
3883                //
3884                // Table base setup must be done by caller/runtime. The type
3885                // check §4.4.8 also requires is discharged at COMPILE time:
3886                // the selector only emits this op after verifying the closed-
3887                // world property that every table entry's signature equals the
3888                // expected type (the raw code-pointer table carries no runtime
3889                // type ids to compare) — see the #642 selector guard.
3890
3891                // MOVW R12, #(size & 0xFFFF) — Thumb-2 T3:
3892                // 11110 i 100100 imm4 | 0 imm3 Rd imm8 (Rd=R12).
3893                let size_lo = *table_size & 0xFFFF;
3894                let hw1: u16 =
3895                    (0xF240 | (((size_lo >> 11) & 1) << 10) | ((size_lo >> 12) & 0xF)) as u16;
3896                let hw2: u16 =
3897                    ((((size_lo >> 8) & 0x7) << 12) | (12 << 8) | (size_lo & 0xFF)) as u16;
3898                bytes.extend_from_slice(&hw1.to_le_bytes());
3899                bytes.extend_from_slice(&hw2.to_le_bytes());
3900                // MOVT R12, #(size >> 16) — only when the table size exceeds
3901                // 16 bits (never in practice, but the guard must not compare
3902                // against a truncated size).
3903                let size_hi = *table_size >> 16;
3904                if size_hi != 0 {
3905                    let hw1: u16 =
3906                        (0xF2C0 | (((size_hi >> 11) & 1) << 10) | ((size_hi >> 12) & 0xF)) as u16;
3907                    let hw2: u16 =
3908                        ((((size_hi >> 8) & 0x7) << 12) | (12 << 8) | (size_hi & 0xFF)) as u16;
3909                    bytes.extend_from_slice(&hw1.to_le_bytes());
3910                    bytes.extend_from_slice(&hw2.to_le_bytes());
3911                }
3912                // CMP idx, R12 — 16-bit T2 (high-register capable):
3913                // 010001 01 N Rm(4) Rn(3), Rn full = N:Rn3.
3914                let cmp: u16 = (0x4500 | ((idx_reg & 8) << 4) | (12 << 3) | (idx_reg & 7)) as u16;
3915                bytes.extend_from_slice(&cmp.to_le_bytes());
3916                // BLO +1 insn (skip the UDF when index < size) — B<cond>.N
3917                // imm8=0: target = branch + 4. LO = unsigned lower.
3918                bytes.extend_from_slice(&0xD300u16.to_le_bytes());
3919                // UDF #0 — call_indirect out-of-bounds trap (same trap idiom as
3920                // the div-by-zero guards).
3921                bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3922
3923                // #676: runtime type check — ONLY for a heterogeneous table
3924                // (mixed signatures, closed-world verdict impossible). Load
3925                // the indexed slot's structural class id from the type-id
3926                // sidecar (`R11 + type_off + idx*4`; `type_off` = sidecar
3927                // base + this table's base offset, a compile-time constant)
3928                // and compare it against the expected type's class id — a
3929                // mismatch is the WASM Core §4.4.8 type trap. Null slots
3930                // carry the reserved id 0, so this compare subsumes the
3931                // #664 null trap (the selector passes `null_check: false`).
3932                // `None` emits NOTHING: every homogeneous table keeps the
3933                // pre-#676 bytes identical BY CONSTRUCTION. R12 stays the
3934                // only scratch (#212); the dispatch tail below recomputes
3935                // idx*4 — the index register is never clobbered here.
3936                if let Some((expected_id, type_off)) = type_check {
3937                    // RQ-61-IMMRANGE (#1072): compiled out in release, where
3938                    // the masks below silently TRUNCATE (id 256 compares as
3939                    // 0 — a NULL slot would pass the §4.4.8 check). The
3940                    // enforcement claim is DEMONSTRATED: the sole `Some`
3941                    // producer, `resolve_runtime_type_check`, loud-declines
3942                    // id > 255 / offset > 4095, tripped by
3943                    // `test_676_call_indirect_runtime_check_range_declines`
3944                    // (mutation-checked — see the A32 twin above).
3945                    debug_assert!(*expected_id <= 255, "selector enforces the CMP imm8 range");
3946                    debug_assert!(*type_off <= 4095, "selector enforces the LDR imm12 range");
3947                    // MOV.W R12, idx, LSL #2 (same encoding as the dispatch
3948                    // tail's index scale below).
3949                    bytes.extend_from_slice(&0xEA4Fu16.to_le_bytes());
3950                    bytes.extend_from_slice(
3951                        &(((0x0C00 | (0b10 << 6)) | idx_reg) as u16).to_le_bytes(),
3952                    );
3953                    // ADD.W R12, R11, R12 (the #650 base-add form).
3954                    bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
3955                    bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
3956                    // LDR.W R12, [R12, #type_off] — T3 LDR (immediate):
3957                    // 1111 1000 1101 Rn=1100 | Rt=1100 imm12.
3958                    bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
3959                    bytes.extend_from_slice(
3960                        &(0xC000u16 | (*type_off as u16 & 0x0FFF)).to_le_bytes(),
3961                    );
3962                    // CMP.W R12, #expected_id — T2 CMP (immediate), imm8
3963                    // (same form as the #664 null check's CMP.W R12, #0).
3964                    bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
3965                    bytes.extend_from_slice(
3966                        &(0x0F00u16 | (*expected_id as u16 & 0xFF)).to_le_bytes(),
3967                    );
3968                    // BEQ +1 insn (skip the UDF when the class id matches) —
3969                    // B<cond>.N imm8=0: target = branch + 4. EQ.
3970                    bytes.extend_from_slice(&0xD000u16.to_le_bytes());
3971                    // UDF #0 — the §4.4.8 type-mismatch trap (same idiom as
3972                    // the bounds guard above).
3973                    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3974                }
3975
3976                // LSL R12, idx_reg, #2 (multiply index by 4)
3977                // Thumb-2 MOV with shift: 11101010 010 S 1111 | 0 imm3 Rd imm2 type Rm
3978                // LSL: type=00 (bits 5:4), imm5=2 -> imm3=000, imm2=10 (bits 7:6)
3979                // #597: the shift amount was previously shifted into bits 5:4 —
3980                // the TYPE field — encoding `mov.w ip, rm, ASR #32`, which
3981                // destroyed the index and dispatched table entry 0 for every
3982                // call. imm2 lives at bits 7:6.
3983                let hw1: u16 = 0xEA4F_u16; // MOV.W R12, Rm, LSL #2
3984                let hw2: u16 = ((0x0C00 | (0b10 << 6)) | idx_reg) as u16;
3985                bytes.extend_from_slice(&hw1.to_le_bytes());
3986                bytes.extend_from_slice(&hw2.to_le_bytes());
3987
3988                if *table_byte_offset == 0 {
3989                    // Table 0 (base = R11 itself): the pre-#650 single-load
3990                    // form — a single-table module's bytes stay identical BY
3991                    // CONSTRUCTION.
3992                    // LDR R12, [R11, R12] - load function pointer
3993                    // Thumb-2 LDR (register): 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
3994                    // Rn=R11, Rt=R12, Rm=R12, imm2=00 (no shift)
3995                    let ldr_hw1: u16 = 0xF85B; // LDR.W Rt, [R11, Rm]
3996                    let ldr_hw2: u16 = 0xC00C; // Rt=R12, imm2=00, Rm=R12
3997                    bytes.extend_from_slice(&ldr_hw1.to_le_bytes());
3998                    bytes.extend_from_slice(&ldr_hw2.to_le_bytes());
3999                } else {
4000                    // #650: table N of the contiguous R11 region — fold the
4001                    // compile-time base offset into the pointer load via the
4002                    // LDR imm12 form (R12 stays the only scratch, per the
4003                    // #212 convention).
4004                    assert!(
4005                        *table_byte_offset <= 4095,
4006                        "call_indirect table base offset {table_byte_offset} exceeds \
4007                         LDR imm12 — the selector must have declined this (#650)"
4008                    );
4009                    // ADD.W R12, R11, R12 — T3 ADD (register):
4010                    // 11101011000 S=0 Rn=1011 | 0 imm3=000 Rd=1100 imm2=00 type=00 Rm=1100
4011                    bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
4012                    bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
4013                    // LDR.W R12, [R12, #offset] — T3 LDR (immediate):
4014                    // 1111 1000 1101 Rn=1100 | Rt=1100 imm12
4015                    bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
4016                    bytes.extend_from_slice(
4017                        &((0xC000u16) | (*table_byte_offset as u16 & 0x0FFF)).to_le_bytes(),
4018                    );
4019                }
4020
4021                // #664: null-slot trap — ONLY when the table image carries
4022                // null (uninitialized) slots, which the layout contract
4023                // requires to be linked as ZERO words. A fully-initialized
4024                // table skips this branch entirely, keeping the pre-#664
4025                // expansion byte-identical BY CONSTRUCTION (the #650
4026                // offset-0 trick).
4027                if *null_check {
4028                    // CMP.W R12, #0 — T2 CMP (immediate): 11110 i 0 1101 1
4029                    // Rn(4) | 0 imm3 1111 imm8, Rn=R12, imm=0.
4030                    bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
4031                    bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
4032                    // BNE +1 insn (skip the UDF when the pointer is non-null)
4033                    // — B<cond>.N imm8=0: target = branch + 4. NE.
4034                    bytes.extend_from_slice(&0xD100u16.to_le_bytes());
4035                    // UDF #0 — call_indirect null-funcref trap (WASM Core
4036                    // §4.4.8: calling an uninitialized element traps; same
4037                    // trap idiom as the bounds guard above).
4038                    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
4039                }
4040
4041                // BLX R12 (call function indirectly)
4042                // BLX Rm (16-bit): 0100 0111 1 Rm 000
4043                let blx: u16 = 0x47E0; // BLX R12
4044                bytes.extend_from_slice(&blx.to_le_bytes());
4045
4046                Ok(bytes)
4047            }
4048
4049            // Label pseudo-instruction: emits no machine code
4050            ArmOp::Label { .. } => Ok(Vec::new()),
4051
4052            // Conditional branch to label (generic) - offset 0, will be patched
4053            ArmOp::Bcc { cond, label: _ } => {
4054                use synth_synthesis::Condition;
4055                let cond_bits: u16 = match cond {
4056                    Condition::EQ => 0x0,
4057                    Condition::NE => 0x1,
4058                    Condition::HS => 0x2,
4059                    Condition::LO => 0x3,
4060                    Condition::HI => 0x8,
4061                    Condition::LS => 0x9,
4062                    Condition::GE => 0xA,
4063                    Condition::LT => 0xB,
4064                    Condition::GT => 0xC,
4065                    Condition::LE => 0xD,
4066                };
4067                // 16-bit B<cond> with offset 0: 1101 cond imm8
4068                let instr: u16 = 0xD000 | (cond_bits << 8);
4069                Ok(instr.to_le_bytes().to_vec())
4070            }
4071
4072            // Branch instructions
4073            ArmOp::B { label: _ } => {
4074                // Simplified: B.N with offset 0
4075                // For real usage, would need label resolution
4076                let instr: u16 = 0xE000; // B.N #0
4077                Ok(instr.to_le_bytes().to_vec())
4078            }
4079
4080            // BHS (Branch if Higher or Same) - used for bounds checking
4081            // Condition code: 0x2 (C set)
4082            ArmOp::Bhs { label: _ } => {
4083                // 16-bit B<cond> with offset 0: 1101 cond imm8
4084                // cond = 0x2 (HS)
4085                let instr: u16 = 0xD200; // BHS.N #0
4086                Ok(instr.to_le_bytes().to_vec())
4087            }
4088
4089            // BLO (Branch if Lower) - complementary to BHS
4090            // Condition code: 0x3 (C clear)
4091            ArmOp::Blo { label: _ } => {
4092                // 16-bit B<cond> with offset 0: 1101 cond imm8
4093                // cond = 0x3 (LO)
4094                let instr: u16 = 0xD300; // BLO.N #0
4095                Ok(instr.to_le_bytes().to_vec())
4096            }
4097
4098            // Branch with numeric offset (Thumb-2)
4099            // Thumb-2 B.W instruction: 32-bit with +-16MB range
4100            ArmOp::BOffset { offset } => {
4101                // offset is already the halfword displacement: (target - branch - 4) / 2
4102                // This is the raw encoded value, accounting for variable-length instructions
4103                let halfword_offset = *offset;
4104
4105                // 16-bit B.N encoding: 1110 0 imm11 (11-bit signed halfword offset)
4106                // Range: -1024 to +1022 halfwords
4107                if (-1024..=1022).contains(&halfword_offset) {
4108                    // 16-bit B.N encoding: 1110 0 imm11
4109                    let imm11 = (halfword_offset as u16) & 0x7FF;
4110                    let instr: u16 = 0xE000 | imm11;
4111                    Ok(instr.to_le_bytes().to_vec())
4112                } else {
4113                    // 32-bit B.W encoding for larger offsets
4114                    // First halfword: 1111 0 S imm10
4115                    // Second halfword: 10 J1 0 J2 imm11
4116                    // Total offset = SignExtend(S:I1:I2:imm10:imm11:0)
4117                    // where I1 = NOT(J1 XOR S), I2 = NOT(J2 XOR S)
4118
4119                    // The B.W (T4) encoding packs the signed offset as:
4120                    //   S:I1:I2:imm10:imm11:0  (25-bit signed, halfword-aligned)
4121                    // where J1 = NOT(I1 XOR S), J2 = NOT(I2 XOR S)
4122                    // Input halfword_offset already equals (target - PC - 4) / 2,
4123                    // so the full byte offset = halfword_offset << 1.
4124                    // The encoding fields split that 25-bit signed value (including the
4125                    // implicit trailing zero) as: S | imm10 | imm11
4126                    // with I1 = bit 23 and I2 = bit 22 of the signed offset.
4127                    let signed_offset = halfword_offset << 1; // byte offset
4128                    let s = if signed_offset < 0 { 1u32 } else { 0u32 };
4129                    let uoffset = signed_offset as u32;
4130                    let imm10 = (uoffset >> 12) & 0x3FF; // bits [21:12]
4131                    let imm11 = (uoffset >> 1) & 0x7FF; // bits [11:1]
4132                    let i1 = (uoffset >> 23) & 1; // bit 23
4133                    let i2 = (uoffset >> 22) & 1; // bit 22
4134                    let j1 = (!(i1 ^ s)) & 1; // J1 = NOT(I1 XOR S)
4135                    let j2 = (!(i2 ^ s)) & 1; // J2 = NOT(I2 XOR S)
4136
4137                    let hw1: u16 = (0xF000 | (s << 10) | imm10) as u16;
4138                    let hw2: u16 = (0x9000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4139
4140                    let mut bytes = hw1.to_le_bytes().to_vec();
4141                    bytes.extend_from_slice(&hw2.to_le_bytes());
4142                    Ok(bytes)
4143                }
4144            }
4145
4146            // Conditional branch with numeric offset (Thumb-2)
4147            ArmOp::BCondOffset { cond, offset } => {
4148                use synth_synthesis::Condition;
4149                let cond_bits: u16 = match cond {
4150                    Condition::EQ => 0x0,
4151                    Condition::NE => 0x1,
4152                    Condition::HS => 0x2,
4153                    Condition::LO => 0x3,
4154                    Condition::HI => 0x8,
4155                    Condition::LS => 0x9,
4156                    Condition::GE => 0xA,
4157                    Condition::LT => 0xB,
4158                    Condition::GT => 0xC,
4159                    Condition::LE => 0xD,
4160                };
4161
4162                // offset is already the halfword displacement: (target - branch - 4) / 2
4163                // This is the raw imm8 value for 16-bit B<cond> encoding
4164                let halfword_offset = *offset;
4165
4166                // 16-bit B<cond> encoding: 1101 cond imm8
4167                // Range: -256 to +254 halfwords (imm8 is sign-extended and shifted left 1)
4168                if (-128..=127).contains(&halfword_offset) {
4169                    let imm8 = (halfword_offset as u16) & 0xFF;
4170                    let instr: u16 = 0xD000 | (cond_bits << 8) | imm8;
4171                    Ok(instr.to_le_bytes().to_vec())
4172                } else {
4173                    // 32-bit B<cond>.W (encoding T3) for larger offsets
4174                    // First halfword: 1111 0 S cond(4) imm6
4175                    // Second halfword: 10 J1 0 J2 imm11
4176                    //
4177                    // Per ARMv7-M, the branch BYTE offset is
4178                    // SignExtend(S:J2:J1:imm6:imm11:'0'), i.e. the field value
4179                    // S:J2:J1:imm6:imm11 IS the signed 20-bit HALFWORD offset —
4180                    // imm11/imm6/J1/J2/S take `halfword_offset` bits [10:0],
4181                    // [16:11], 17, 18 and 19 directly (mirroring the T4
4182                    // unconditional arm above).
4183                    //
4184                    // #740: this arm previously packed `halfword_offset >> 1`
4185                    // into imm6:imm11 — HALVING the displacement — so every
4186                    // wide conditional branch (span > 254 bytes) landed at half
4187                    // its intended offset: gust_poll's loop-head `br_if` to an
4188                    // outer block end jumped mid-shape. Narrow (16-bit) B<cond>
4189                    // encodings were unaffected, which is why short-range CF
4190                    // fixtures never caught it.
4191                    if !(-(1 << 19)..(1 << 19)).contains(&halfword_offset) {
4192                        return Err(synth_core::Error::synthesis(format!(
4193                            "B<cond>.W (T3) halfword offset {halfword_offset} exceeds \
4194                             the signed 20-bit encoding range (±1 MB) — refusing to \
4195                             emit a truncated branch"
4196                        )));
4197                    }
4198                    let u = halfword_offset as u32;
4199                    let imm11 = u & 0x7FF; // halfword offset bits [10:0]
4200                    let imm6 = (u >> 11) & 0x3F; // bits [16:11]
4201                    let j1 = (u >> 17) & 1; // bit 17
4202                    let j2 = (u >> 18) & 1; // bit 18
4203                    let s = (u >> 19) & 1; // sign (range-checked above)
4204
4205                    let hw1: u16 = (0xF000 | (s << 10) | ((cond_bits as u32) << 6) | imm6) as u16;
4206                    let hw2: u16 = (0x8000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4207
4208                    let mut bytes = hw1.to_le_bytes().to_vec();
4209                    bytes.extend_from_slice(&hw2.to_le_bytes());
4210                    Ok(bytes)
4211                }
4212            }
4213
4214            ArmOp::Bl { label: _ } => {
4215                // BL is always 32-bit in Thumb-2, encoded here as a relocatable
4216                // placeholder; an R_ARM_THM_CALL relocation patches the target
4217                // (see arm_backend.rs). The placeholder must carry an embedded
4218                // addend of -4 so the relocation nets to exactly the symbol S.
4219                //
4220                // Thumb BL computes `target = (P + 4) + signed_offset`. Under
4221                // R_ARM_THM_CALL the linker resolves using the in-place addend;
4222                // a 0xF800 placeholder (addend 0) lands at S+4 — every call one
4223                // instruction past the callee entry (#174). The correct
4224                // placeholder is what `gas` emits for `bl <extern>`:
4225                //   f7ff fffe  ->  `bl <self>`  (S=1, J1=J2=1, imm = -4 addend),
4226                // i.e. hw1=0xF7FF, hw2=0xFFFE. This nets to S, not S+4.
4227                // (The earlier 0xD000 was worse still — a ~+0x600000 addend,
4228                // the garbage `bl c0000c` and "truncated to fit" of #167.)
4229                let hw1: u16 = 0xF7FF;
4230                let hw2: u16 = 0xFFFE;
4231                let mut bytes = hw1.to_le_bytes().to_vec();
4232                bytes.extend_from_slice(&hw2.to_le_bytes());
4233                Ok(bytes)
4234            }
4235
4236            // MVN
4237            ArmOp::Mvn { rd, op2 } => {
4238                if let Operand2::Reg(rm) = op2 {
4239                    let rd_bits = reg_to_bits(rd) as u16;
4240                    let rm_bits = reg_to_bits(rm) as u16;
4241
4242                    if rd_bits < 8 && rm_bits < 8 {
4243                        // MVNS Rd, Rm (16-bit): 0100 0011 11 Rm Rd
4244                        let instr: u16 = 0x43C0 | (rm_bits << 3) | rd_bits;
4245                        Ok(instr.to_le_bytes().to_vec())
4246                    } else {
4247                        // 32-bit MVN
4248                        let hw1: u16 = 0xEA6F_u16;
4249                        let hw2: u16 = ((reg_to_bits(rd) << 8) | reg_to_bits(rm)) as u16;
4250                        let mut bytes = hw1.to_le_bytes().to_vec();
4251                        bytes.extend_from_slice(&hw2.to_le_bytes());
4252                        Ok(bytes)
4253                    }
4254                } else {
4255                    let instr: u16 = 0xBF00;
4256                    Ok(instr.to_le_bytes().to_vec())
4257                }
4258            }
4259
4260            // MOVW - Move Wide (Thumb-2 32-bit)
4261            ArmOp::Movw { rd, imm16 } => {
4262                self.encode_thumb32_movw_raw(reg_to_bits(rd), *imm16 as u32)
4263            }
4264
4265            // MOVT - Move Top (Thumb-2 32-bit)
4266            ArmOp::Movt { rd, imm16 } => {
4267                self.encode_thumb32_movt_raw(reg_to_bits(rd), *imm16 as u32)
4268            }
4269
4270            // #237: symbol-relative MOVW/MOVT. Encode the addend's low/high 16
4271            // bits in place; the backend records an R_ARM_MOVW_ABS_NC /
4272            // R_ARM_MOVT_ABS relocation against `symbol`, so the linker adds the
4273            // symbol's final address to the in-place addend (REL semantics).
4274            ArmOp::MovwSym { rd, addend, .. } => {
4275                self.encode_thumb32_movw_raw(reg_to_bits(rd), (*addend as u32) & 0xffff)
4276            }
4277            ArmOp::MovtSym { rd, addend, .. } => {
4278                self.encode_thumb32_movt_raw(reg_to_bits(rd), ((*addend as u32) >> 16) & 0xffff)
4279            }
4280
4281            // #345: literal-pool address load — emit a PLACEHOLDER `LDR.W rd,
4282            // [pc, #0]` (U=1, imm12=0). The backend (arm_backend.rs) places the
4283            // 4-byte pool word at the end of the function, records the R_ARM_ABS32
4284            // relocation against `symbol+addend`, and patches the imm12 with the
4285            // real PC-relative distance once the pool offset is known.
4286            // Encoding T2: 1111 1000 1101 1111 | Rt(4) imm12(12), with the literal
4287            // base = Align(PC,4) and PC = address of this instruction + 4.
4288            ArmOp::LdrSym { rd, .. } => {
4289                let rt = reg_to_bits(rd) as u16;
4290                let hw1: u16 = 0xF8DF; // LDR.W (literal), U=1
4291                let hw2: u16 = rt << 12; // imm12 = 0 placeholder
4292                let mut bytes = Vec::with_capacity(4);
4293                bytes.extend_from_slice(&hw1.to_le_bytes());
4294                bytes.extend_from_slice(&hw2.to_le_bytes());
4295                Ok(bytes)
4296            }
4297
4298            // SetCond: Materialize condition flag into register (0 or 1)
4299            // Strategy: ITE <cond>; MOV Rd, #1; MOV Rd, #0
4300            // IMPORTANT: Must use ITE (If-Then-Else) because 16-bit Thumb MOV
4301            // always sets flags (MOVS). We need to evaluate the condition BEFORE
4302            // any MOV instruction clobbers the flags from CMP.
4303            ArmOp::SetCond { rd, cond } => {
4304                let rd_bits = reg_to_bits(rd) as u16;
4305
4306                // Condition code encoding for IT block
4307                use synth_synthesis::Condition;
4308                let cond_bits: u16 = match cond {
4309                    Condition::EQ => 0x0,
4310                    Condition::NE => 0x1,
4311                    Condition::LT => 0xB,
4312                    Condition::LE => 0xD,
4313                    Condition::GT => 0xC,
4314                    Condition::GE => 0xA,
4315                    Condition::LO => 0x3, // CC/LO (unsigned <)
4316                    Condition::LS => 0x9, // LS (unsigned <=)
4317                    Condition::HI => 0x8, // HI (unsigned >)
4318                    Condition::HS => 0x2, // CS/HS (unsigned >=)
4319                };
4320
4321                // ITE <cond>: encodes If-Then-Else block
4322                // The mask field depends on firstcond[0]:
4323                // - If firstcond[0] = 0: mask = 0xC for TE pattern (ITE EQ = BF0C)
4324                // - If firstcond[0] = 1: mask = 0x4 for TE pattern (ITE NE = BF14)
4325                let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4326                let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4327
4328                // Materialize 0/1 into Rd. The 16-bit MOVS (T1) encodes Rd in a
4329                // 3-bit field (bits[10:8]) — only R0–R7. For a high register
4330                // (R8–R12) `rd_bits << 8` overflows into bit 11 and silently
4331                // turns MOVS into CMP (00100 → 00101), corrupting the result
4332                // (this mis-materialized gale's `has_waiter`, so its `local.set`
4333                // stored a stale register → the binary-sem WAKE dispatch read
4334                // garbage). Use the 32-bit MOV.W (T2) for high registers, which
4335                // has a 4-bit Rd field. MOV.W with S=0 doesn't set flags, which
4336                // is fine inside the ITE (the materialized value is the result;
4337                // the flags are not consumed afterwards).
4338                let mut bytes = ite_instr.to_le_bytes().to_vec();
4339                let push_mov = |bytes: &mut Vec<u8>, imm: u16| {
4340                    if rd_bits <= 7 {
4341                        let m: u16 = 0x2000 | (rd_bits << 8) | imm; // 16-bit MOVS Rd,#imm
4342                        bytes.extend_from_slice(&m.to_le_bytes());
4343                    } else {
4344                        // 32-bit MOV.W Rd, #imm (T2): F04F | (Rd<<8) | imm8
4345                        let hw1: u16 = 0xF04F;
4346                        let hw2: u16 = (rd_bits << 8) | imm;
4347                        bytes.extend_from_slice(&hw1.to_le_bytes());
4348                        bytes.extend_from_slice(&hw2.to_le_bytes());
4349                    }
4350                };
4351                push_mov(&mut bytes, 1); // Then branch (condition true)  → 1
4352                push_mov(&mut bytes, 0); // Else branch (condition false) → 0
4353                Ok(bytes)
4354            }
4355
4356            // I64SetCond: Compare two i64 register pairs, result 0/1 in rd
4357            // EQ/NE: CMP lo,lo; IT EQ; CMPEQ hi,hi; ITE <cond>; MOV 1; MOV 0
4358            // LT: CMP lo,lo; SBCS rd,hi,hi; ITE LT; MOV 1; MOV 0
4359            // GT: CMP lo,lo (swapped); SBCS rd,hi,hi (swapped); ITE LT; MOV 1; MOV 0
4360            ArmOp::I64SetCond {
4361                rd,
4362                rn_lo,
4363                rn_hi,
4364                rm_lo,
4365                rm_hi,
4366                cond,
4367            } => {
4368                use synth_synthesis::Condition;
4369                let rd_bits = reg_to_bits(rd) as u16;
4370                let mut bytes = Vec::new();
4371
4372                // Helper: encode CMP Rn, Rm (16-bit)
4373                let encode_cmp_reg = |rn: &synth_synthesis::Reg,
4374                                      rm: &synth_synthesis::Reg|
4375                 -> Vec<u8> {
4376                    let rn_bits = reg_to_bits(rn) as u16;
4377                    let rm_bits = reg_to_bits(rm) as u16;
4378                    if rn_bits < 8 && rm_bits < 8 {
4379                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
4380                        instr.to_le_bytes().to_vec()
4381                    } else {
4382                        let n_bit = (rn_bits >> 3) & 1;
4383                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
4384                        instr.to_le_bytes().to_vec()
4385                    }
4386                };
4387
4388                // Helper: encode ITE <cond> (2 bytes)
4389                let encode_ite = |cond_bits: u16| -> Vec<u8> {
4390                    let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4391                    let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4392                    ite_instr.to_le_bytes().to_vec()
4393                };
4394
4395                // Helper: encode SetCond (ITE + MOV #1 + MOV #0) for given condition
4396                let encode_setcond = |cond_bits: u16, rd_bits: u16| -> Vec<u8> {
4397                    let mut b = encode_ite(cond_bits);
4398                    if rd_bits < 8 {
4399                        let mov_one: u16 = 0x2001 | (rd_bits << 8);
4400                        let mov_zero: u16 = 0x2000 | (rd_bits << 8);
4401                        b.extend_from_slice(&mov_one.to_le_bytes());
4402                        b.extend_from_slice(&mov_zero.to_le_bytes());
4403                    } else {
4404                        // #311: rd >= R8 — the 16-bit MOV imm8 form has a 3-bit
4405                        // rd field; rd_bits<<8 overflows into bit 11 and
4406                        // TRANSMUTES the MOV into CMP (0x2001|0x0800 = 0x2801 =
4407                        // CMP r0,#1): the boolean dies in the flags and the
4408                        // consumer reads a stale register. Use the 32-bit
4409                        // MOV.W (T2: F04F 0000|rd<<8|imm8) — IT-legal,
4410                        // flag-preserving. Same class as H-CODE-9 / #180.
4411                        for imm in [1u16, 0u16] {
4412                            let hw1: u16 = 0xF04F;
4413                            let hw2: u16 = (rd_bits << 8) | imm;
4414                            b.extend_from_slice(&hw1.to_le_bytes());
4415                            b.extend_from_slice(&hw2.to_le_bytes());
4416                        }
4417                    }
4418                    b
4419                };
4420
4421                match cond {
4422                    Condition::EQ | Condition::NE => {
4423                        // CMP rn_lo, rm_lo (compare low words)
4424                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4425
4426                        // IT EQ (execute next instruction only if Z=1)
4427                        let it_eq: u16 = 0xBF08; // IT EQ: cond=0000, mask=1000
4428                        bytes.extend_from_slice(&it_eq.to_le_bytes());
4429
4430                        // CMPEQ rn_hi, rm_hi (compare high words, only if low equal)
4431                        bytes.extend_from_slice(&encode_cmp_reg(rn_hi, rm_hi));
4432
4433                        // ITE <cond>; MOV rd, #1; MOV rd, #0
4434                        let cond_bits: u16 = match cond {
4435                            Condition::EQ => 0x0,
4436                            Condition::NE => 0x1,
4437                            _ => unreachable!(),
4438                        };
4439                        bytes.extend_from_slice(&encode_setcond(cond_bits, rd_bits));
4440                    }
4441
4442                    Condition::LT => {
4443                        // CMP rn_lo, rm_lo (sets C flag for borrow)
4444                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4445
4446                        // SBCS rd, rn_hi, rm_hi (subtract with carry, sets N,V flags)
4447                        // SBCS.W Rd, Rn, Rm: EB70 Rn | 0000 Rd 0000 Rm
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
4455                        // ITE LT; MOV rd, #1; MOV rd, #0
4456                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4457                    }
4458
4459                    Condition::GT => {
4460                        // GT(a,b) = LT(b,a): swap operands
4461                        // CMP rm_lo, rn_lo (swapped)
4462                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4463
4464                        // SBCS rd, rm_hi, rn_hi (swapped)
4465                        let rm_hi_bits = reg_to_bits(rm_hi);
4466                        let rn_hi_bits = reg_to_bits(rn_hi);
4467                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4468                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4469                        bytes.extend_from_slice(&hw1.to_le_bytes());
4470                        bytes.extend_from_slice(&hw2.to_le_bytes());
4471
4472                        // ITE LT; MOV rd, #1; MOV rd, #0
4473                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4474                    }
4475
4476                    Condition::LE => {
4477                        // LE(a,b) = !GT(a,b): use GT logic but invert result
4478                        // GT(a,b) = LT(b,a): so we do CMP(b,a) and check LT, then invert
4479                        // CMP rm_lo, rn_lo (swapped, same as GT)
4480                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4481
4482                        // SBCS rd, rm_hi, rn_hi (swapped)
4483                        let rm_hi_bits = reg_to_bits(rm_hi);
4484                        let rn_hi_bits = reg_to_bits(rn_hi);
4485                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4486                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4487                        bytes.extend_from_slice(&hw1.to_le_bytes());
4488                        bytes.extend_from_slice(&hw2.to_le_bytes());
4489
4490                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT, so inverting GT result)
4491                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4492                    }
4493
4494                    Condition::GE => {
4495                        // GE(a,b) = !LT(a,b): use LT logic but invert result
4496                        // CMP rn_lo, rm_lo (same as LT)
4497                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4498
4499                        // SBCS rd, rn_hi, rm_hi (same as LT)
4500                        let rn_hi_bits = reg_to_bits(rn_hi);
4501                        let rm_hi_bits = reg_to_bits(rm_hi);
4502                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4503                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4504                        bytes.extend_from_slice(&hw1.to_le_bytes());
4505                        bytes.extend_from_slice(&hw2.to_le_bytes());
4506
4507                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT)
4508                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4509                    }
4510
4511                    // Unsigned comparisons - same instruction sequence, different conditions
4512                    Condition::LO => {
4513                        // LO (unsigned LT): CMP lo, SBCS hi, check C=0
4514                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4515                        let rn_hi_bits = reg_to_bits(rn_hi);
4516                        let rm_hi_bits = reg_to_bits(rm_hi);
4517                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4518                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4519                        bytes.extend_from_slice(&hw1.to_le_bytes());
4520                        bytes.extend_from_slice(&hw2.to_le_bytes());
4521                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4522                    }
4523
4524                    Condition::HI => {
4525                        // HI (unsigned GT): swap operands and check LO
4526                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4527                        let rm_hi_bits = reg_to_bits(rm_hi);
4528                        let rn_hi_bits = reg_to_bits(rn_hi);
4529                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4530                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4531                        bytes.extend_from_slice(&hw1.to_le_bytes());
4532                        bytes.extend_from_slice(&hw2.to_le_bytes());
4533                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4534                    }
4535
4536                    Condition::LS => {
4537                        // LS (unsigned LE): !(a > b) = !(HI), so do HI and invert
4538                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4539                        let rm_hi_bits = reg_to_bits(rm_hi);
4540                        let rn_hi_bits = reg_to_bits(rn_hi);
4541                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4542                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4543                        bytes.extend_from_slice(&hw1.to_le_bytes());
4544                        bytes.extend_from_slice(&hw2.to_le_bytes());
4545                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4546                    }
4547
4548                    Condition::HS => {
4549                        // HS (unsigned GE): !(a < b) = !(LO)
4550                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4551                        let rn_hi_bits = reg_to_bits(rn_hi);
4552                        let rm_hi_bits = reg_to_bits(rm_hi);
4553                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4554                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4555                        bytes.extend_from_slice(&hw1.to_le_bytes());
4556                        bytes.extend_from_slice(&hw2.to_le_bytes());
4557                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4558                    }
4559                }
4560
4561                Ok(bytes)
4562            }
4563
4564            // I64SetCondZ: Test if i64 register pair is zero, result 0/1 in rd
4565            // ORR.W rd, rn_lo, rn_hi; CMP rd, #0; ITE EQ; MOV 1; MOV 0
4566            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
4567                let rd_bits = reg_to_bits(rd);
4568                let rn_lo_bits = reg_to_bits(rn_lo);
4569                let rn_hi_bits = reg_to_bits(rn_hi);
4570                let mut bytes = Vec::new();
4571
4572                // ORR.W rd, rn_lo, rn_hi: EA40 rn_lo | 0000 rd 0000 rn_hi
4573                let hw1: u16 = (0xEA40 | rn_lo_bits) as u16;
4574                let hw2: u16 = ((rd_bits << 8) | rn_hi_bits) as u16;
4575                bytes.extend_from_slice(&hw1.to_le_bytes());
4576                bytes.extend_from_slice(&hw2.to_le_bytes());
4577
4578                // CMP rd, #0 — 16-bit form only for r0-r7 (3-bit rd field);
4579                // high registers take CMP.W (T2: F1B0|rn 0F00|imm8). This was
4580                // H-CODE-9: rd_bits<<8 overflowing the field compared the
4581                // WRONG register. Same hardening as the #311 SetCond fix.
4582                if rd_bits < 8 {
4583                    let cmp_instr: u16 = 0x2800 | ((rd_bits as u16) << 8);
4584                    bytes.extend_from_slice(&cmp_instr.to_le_bytes());
4585                } else {
4586                    let hw1: u16 = 0xF1B0 | (rd_bits as u16);
4587                    let hw2: u16 = 0x0F00;
4588                    bytes.extend_from_slice(&hw1.to_le_bytes());
4589                    bytes.extend_from_slice(&hw2.to_le_bytes());
4590                }
4591
4592                // ITE EQ; MOV rd, #1; MOV rd, #0 (32-bit MOV.W for rd >= R8,
4593                // #311 — see I64SetCond)
4594                let mask = 0xC_u16; // ITE EQ mask: firstcond[0]=0, mask=0xC
4595                let ite_instr: u16 = 0xBF00 | mask;
4596                bytes.extend_from_slice(&ite_instr.to_le_bytes());
4597                if rd_bits < 8 {
4598                    let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
4599                    let mov_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
4600                    bytes.extend_from_slice(&mov_one.to_le_bytes());
4601                    bytes.extend_from_slice(&mov_zero.to_le_bytes());
4602                } else {
4603                    for imm in [1u16, 0u16] {
4604                        let hw1: u16 = 0xF04F;
4605                        let hw2: u16 = ((rd_bits as u16) << 8) | imm;
4606                        bytes.extend_from_slice(&hw1.to_le_bytes());
4607                        bytes.extend_from_slice(&hw2.to_le_bytes());
4608                    }
4609                }
4610
4611                Ok(bytes)
4612            }
4613
4614            // I64Mul: 64-bit multiply using UMULL + MLA cross products
4615            // Formula: result = (a_lo * b_lo) + ((a_lo * b_hi + a_hi * b_lo) << 32)
4616            // Uses R12 as scratch register
4617            ArmOp::I64Mul {
4618                rd_lo,
4619                rd_hi,
4620                rn_lo,
4621                rn_hi,
4622                rm_lo,
4623                rm_hi,
4624            } => {
4625                let rd_lo_bits = reg_to_bits(rd_lo);
4626                let rd_hi_bits = reg_to_bits(rd_hi);
4627                let rn_lo_bits = reg_to_bits(rn_lo);
4628                let rn_hi_bits = reg_to_bits(rn_hi);
4629                let rm_lo_bits = reg_to_bits(rm_lo);
4630                let rm_hi_bits = reg_to_bits(rm_hi);
4631                let r12: u32 = 12; // IP scratch register
4632                let mut bytes = Vec::new();
4633
4634                // 1. MUL R12, rn_lo, rm_hi  (R12 = a_lo * b_hi)
4635                // Thumb-2 MUL: hw1=0xFB00|Rn, hw2=0xF000|(Rd<<8)|Rm
4636                let hw1: u16 = (0xFB00 | rn_lo_bits) as u16;
4637                let hw2: u16 = (0xF000 | (r12 << 8) | rm_hi_bits) as u16;
4638                bytes.extend_from_slice(&hw1.to_le_bytes());
4639                bytes.extend_from_slice(&hw2.to_le_bytes());
4640
4641                // 2. MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
4642                // Thumb-2 MLA: hw1=0xFB00|Rn, hw2=(Ra<<12)|(Rd<<8)|Rm
4643                let hw1: u16 = (0xFB00 | rn_hi_bits) as u16;
4644                let hw2: u16 = ((r12 << 12) | (r12 << 8) | rm_lo_bits) as u16;
4645                bytes.extend_from_slice(&hw1.to_le_bytes());
4646                bytes.extend_from_slice(&hw2.to_le_bytes());
4647
4648                // 3. UMULL rd_lo, rd_hi, rn_lo, rm_lo  (rd_lo:rd_hi = a_lo * b_lo)
4649                // Thumb-2 UMULL: hw1=0xFBA0|Rn, hw2=(RdLo<<12)|(RdHi<<8)|Rm
4650                let hw1: u16 = (0xFBA0 | rn_lo_bits) as u16;
4651                let hw2: u16 = ((rd_lo_bits << 12) | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4652                bytes.extend_from_slice(&hw1.to_le_bytes());
4653                bytes.extend_from_slice(&hw2.to_le_bytes());
4654
4655                // 4. ADD rd_hi, R12  (rd_hi += cross products)
4656                // 16-bit high reg ADD: 01000100 D Rm Rdn[2:0]
4657                let d_bit = (rd_hi_bits >> 3) & 1;
4658                let add_instr: u16 =
4659                    (0x4400 | (d_bit << 7) | (r12 << 3) | (rd_hi_bits & 0x7)) as u16;
4660                bytes.extend_from_slice(&add_instr.to_le_bytes());
4661
4662                Ok(bytes)
4663            }
4664
4665            // I64Shl: 64-bit shift left with branch for n<32 vs n>=32.
4666            //
4667            // #1048: the expansion must NEVER write its own input operands.
4668            // The pre-#1048 sequence masked the amount IN PLACE
4669            // (`AND.W rm_lo, rm_lo, #63`) and used the amount's home high
4670            // register `rm_hi` as scratch (`SUBS.W rm_hi, rm_lo, #32`, RSB,
4671            // LSR) — so re-reading the amount after the shift returned a
4672            // mangled value (amt=64 read back 0, amt=67 read back 3). The
4673            // rewrite uses R12 — encoder scratch, never allocatable (#212) —
4674            // as the ONLY temporary, re-deriving the masked amount from the
4675            // untouched rm_lo whenever a second live temp would otherwise be
4676            // needed. This matches the Rocq/SMT pseudo-op models
4677            // (I64ShlPseudo writes rd_lo/rd_hi ONLY), which were proven over
4678            // exactly this non-clobbering contract all along.
4679            ArmOp::I64Shl {
4680                rd_lo,
4681                rd_hi,
4682                rn_lo,
4683                rn_hi,
4684                rm_lo,
4685                rm_hi: _,
4686            } => {
4687                let rd_lo_bits = reg_to_bits(rd_lo);
4688                let rd_hi_bits = reg_to_bits(rd_hi);
4689                let rn_lo_bits = reg_to_bits(rn_lo);
4690                let rn_hi_bits = reg_to_bits(rn_hi);
4691                let rm_lo_bits = reg_to_bits(rm_lo);
4692                let r12: u32 = 12; // the only scratch — never allocatable
4693                let mut bytes = Vec::new();
4694
4695                // #1039 house style: refuse a destination that would collide
4696                // with an input still needed after the destination is first
4697                // written, loudly — never misassemble. rd_hi is written before
4698                // rn_lo and rm_lo are last read; the in-place form
4699                // rd == rn (select_default) has rd_hi == rn_hi and stays legal.
4700                if rd_hi_bits == rn_lo_bits || rd_hi_bits == rm_lo_bits {
4701                    return Err(synth_core::Error::synthesis(format!(
4702                        "I64Shl: rd_hi {rd_hi:?} aliases an input ({rn_lo:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4703                    )));
4704                }
4705
4706                // AND.W R12, rm_lo, #63  (n — the amount register is only READ)
4707                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4708                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4709                bytes.extend_from_slice(&hw1.to_le_bytes());
4710                bytes.extend_from_slice(&hw2.to_le_bytes());
4711
4712                // SUBS.W R12, R12, #32  (R12 = n-32, sets flags)
4713                let hw1: u16 = (0xF1B0 | r12) as u16;
4714                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4715                bytes.extend_from_slice(&hw1.to_le_bytes());
4716                bytes.extend_from_slice(&hw2.to_le_bytes());
4717
4718                // BPL .large (branch if n >= 32, offset = +14 halfwords)
4719                let bpl: u16 = 0xD50E;
4720                bytes.extend_from_slice(&bpl.to_le_bytes());
4721
4722                // --- Small shift (n < 32) ---
4723                // AND.W R12, rm_lo, #63  (n again — R12 held n-32)
4724                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4725                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4726                bytes.extend_from_slice(&hw1.to_le_bytes());
4727                bytes.extend_from_slice(&hw2.to_le_bytes());
4728
4729                // LSL.W rd_hi, rn_hi, R12  (hi << n; rn_hi's last read)
4730                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4731                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4732                bytes.extend_from_slice(&hw1.to_le_bytes());
4733                bytes.extend_from_slice(&hw2.to_le_bytes());
4734
4735                // RSB.W R12, R12, #32  (R12 = 32-n)
4736                let hw1: u16 = (0xF1C0 | r12) as u16;
4737                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4738                bytes.extend_from_slice(&hw1.to_le_bytes());
4739                bytes.extend_from_slice(&hw2.to_le_bytes());
4740
4741                // LSR.W R12, rn_lo, R12  (overflow = lo >> (32-n); n=0 gives
4742                // a register shift by 32 which yields 0 — exact)
4743                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4744                let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4745                bytes.extend_from_slice(&hw1.to_le_bytes());
4746                bytes.extend_from_slice(&hw2.to_le_bytes());
4747
4748                // ORR.W rd_hi, rd_hi, R12  (hi |= overflow bits from lo)
4749                let hw1: u16 = (0xEA40 | rd_hi_bits) as u16;
4750                let hw2: u16 = ((rd_hi_bits << 8) | r12) as u16;
4751                bytes.extend_from_slice(&hw1.to_le_bytes());
4752                bytes.extend_from_slice(&hw2.to_le_bytes());
4753
4754                // AND.W R12, rm_lo, #63  (n once more for the low half)
4755                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4756                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4757                bytes.extend_from_slice(&hw1.to_le_bytes());
4758                bytes.extend_from_slice(&hw2.to_le_bytes());
4759
4760                // LSL.W rd_lo, rn_lo, R12  (lo << n)
4761                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4762                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4763                bytes.extend_from_slice(&hw1.to_le_bytes());
4764                bytes.extend_from_slice(&hw2.to_le_bytes());
4765
4766                // B .done — `.done` is the END of the expansion, i.e. PAST the
4767                // large-shift arm's trailing zero-fill. #916: that zero-fill is
4768                // 1 halfword for a low rd_lo but 2 for R8-R12 (MOV.W), so the
4769                // displacement is DERIVED from its real width instead of the
4770                // hard-coded 0xE002 — widening the MOV without this would
4771                // overshoot `.done` and turn a data miscompile into a
4772                // control-flow one. Thumb `B` reads PC as its own address + 4
4773                // (= +2 halfwords), so imm11 = (large-arm halfwords) - 1.
4774                let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_lo_bits);
4775                let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4776                bytes.extend_from_slice(&b_done.to_le_bytes());
4777
4778                // --- Large shift (n >= 32) --- (R12 still holds n-32)
4779                // LSL.W rd_hi, rn_lo, R12  (hi = lo << (n-32))
4780                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4781                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4782                bytes.extend_from_slice(&hw1.to_le_bytes());
4783                bytes.extend_from_slice(&hw2.to_le_bytes());
4784
4785                // MOV rd_lo, #0 (#916: MOV.W for rd_lo >= R8). NOTE the order
4786                // is load-bearing — zeroing rd_lo BEFORE the LSL.W would
4787                // destroy rn_lo in the in-place case rd_lo == rn_lo, so this
4788                // cannot be reordered to dodge the displacement change.
4789                emit_thumb_zero_fill(&mut bytes, rd_lo_bits);
4790
4791                Ok(bytes) // 46 bytes (48 when rd_lo >= R8 takes MOV.W)
4792            }
4793
4794            // I64ShrU: 64-bit logical shift right with branch for n<32 vs
4795            // n>=32. #1048: R12-only scratch, operands never written — see
4796            // the I64Shl comment for the full rationale.
4797            ArmOp::I64ShrU {
4798                rd_lo,
4799                rd_hi,
4800                rn_lo,
4801                rn_hi,
4802                rm_lo,
4803                rm_hi: _,
4804            } => {
4805                let rd_lo_bits = reg_to_bits(rd_lo);
4806                let rd_hi_bits = reg_to_bits(rd_hi);
4807                let rn_lo_bits = reg_to_bits(rn_lo);
4808                let rn_hi_bits = reg_to_bits(rn_hi);
4809                let rm_lo_bits = reg_to_bits(rm_lo);
4810                let r12: u32 = 12; // the only scratch — never allocatable
4811                let mut bytes = Vec::new();
4812
4813                // #1039 house style: rd_lo is written before rn_hi and rm_lo
4814                // are last read — refuse the collision loudly. The in-place
4815                // form rd == rn (select_default) has rd_lo == rn_lo and stays
4816                // legal.
4817                if rd_lo_bits == rn_hi_bits || rd_lo_bits == rm_lo_bits {
4818                    return Err(synth_core::Error::synthesis(format!(
4819                        "I64ShrU: rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4820                    )));
4821                }
4822
4823                // AND.W R12, rm_lo, #63  (n — the amount register is only READ)
4824                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4825                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4826                bytes.extend_from_slice(&hw1.to_le_bytes());
4827                bytes.extend_from_slice(&hw2.to_le_bytes());
4828
4829                // SUBS.W R12, R12, #32  (R12 = n-32, sets flags)
4830                let hw1: u16 = (0xF1B0 | r12) as u16;
4831                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4832                bytes.extend_from_slice(&hw1.to_le_bytes());
4833                bytes.extend_from_slice(&hw2.to_le_bytes());
4834
4835                // BPL .large (+14 halfwords)
4836                let bpl: u16 = 0xD50E;
4837                bytes.extend_from_slice(&bpl.to_le_bytes());
4838
4839                // --- Small shift (n < 32) ---
4840                // AND.W R12, rm_lo, #63  (n again — R12 held n-32)
4841                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4842                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4843                bytes.extend_from_slice(&hw1.to_le_bytes());
4844                bytes.extend_from_slice(&hw2.to_le_bytes());
4845
4846                // LSR.W rd_lo, rn_lo, R12  (lo >> n; rn_lo's last read)
4847                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4848                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4849                bytes.extend_from_slice(&hw1.to_le_bytes());
4850                bytes.extend_from_slice(&hw2.to_le_bytes());
4851
4852                // RSB.W R12, R12, #32  (R12 = 32-n)
4853                let hw1: u16 = (0xF1C0 | r12) as u16;
4854                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4855                bytes.extend_from_slice(&hw1.to_le_bytes());
4856                bytes.extend_from_slice(&hw2.to_le_bytes());
4857
4858                // LSL.W R12, rn_hi, R12  (overflow = hi << (32-n); n=0 gives
4859                // a register shift by 32 which yields 0 — exact)
4860                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4861                let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4862                bytes.extend_from_slice(&hw1.to_le_bytes());
4863                bytes.extend_from_slice(&hw2.to_le_bytes());
4864
4865                // ORR.W rd_lo, rd_lo, R12  (lo |= overflow from hi)
4866                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4867                let hw2: u16 = ((rd_lo_bits << 8) | r12) as u16;
4868                bytes.extend_from_slice(&hw1.to_le_bytes());
4869                bytes.extend_from_slice(&hw2.to_le_bytes());
4870
4871                // AND.W R12, rm_lo, #63  (n once more for the high half)
4872                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4873                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4874                bytes.extend_from_slice(&hw1.to_le_bytes());
4875                bytes.extend_from_slice(&hw2.to_le_bytes());
4876
4877                // LSR.W rd_hi, rn_hi, R12  (hi >> n, logical)
4878                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4879                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4880                bytes.extend_from_slice(&hw1.to_le_bytes());
4881                bytes.extend_from_slice(&hw2.to_le_bytes());
4882
4883                // B .done — see I64Shl: `.done` is the END of the expansion,
4884                // past the trailing zero-fill, so the displacement is derived
4885                // from that zero-fill's real width (#916).
4886                let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_hi_bits);
4887                let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4888                bytes.extend_from_slice(&b_done.to_le_bytes());
4889
4890                // --- Large shift (n >= 32) --- (R12 still holds n-32)
4891                // LSR.W rd_lo, rn_hi, R12  (lo = hi >> (n-32))
4892                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4893                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4894                bytes.extend_from_slice(&hw1.to_le_bytes());
4895                bytes.extend_from_slice(&hw2.to_le_bytes());
4896
4897                // MOV rd_hi, #0 (#916: MOV.W for rd_hi >= R8). Order is
4898                // load-bearing: the LSR.W above reads rn_hi, which may BE
4899                // rd_hi in the in-place case.
4900                emit_thumb_zero_fill(&mut bytes, rd_hi_bits);
4901
4902                Ok(bytes) // 46 bytes (48 when rd_hi >= R8 takes MOV.W)
4903            }
4904
4905            // I64ShrS: 64-bit arithmetic shift right with branch for n<32 vs
4906            // n>=32. #1048: R12-only scratch, operands never written — see
4907            // the I64Shl comment for the full rationale.
4908            ArmOp::I64ShrS {
4909                rd_lo,
4910                rd_hi,
4911                rn_lo,
4912                rn_hi,
4913                rm_lo,
4914                rm_hi: _,
4915            } => {
4916                let rd_lo_bits = reg_to_bits(rd_lo);
4917                let rd_hi_bits = reg_to_bits(rd_hi);
4918                let rn_lo_bits = reg_to_bits(rn_lo);
4919                let rn_hi_bits = reg_to_bits(rn_hi);
4920                let rm_lo_bits = reg_to_bits(rm_lo);
4921                let r12: u32 = 12; // the only scratch — never allocatable
4922                let mut bytes = Vec::new();
4923
4924                // #1039 house style: rd_lo is written before rn_hi and rm_lo
4925                // are last read (on BOTH arms of the diamond — the large arm's
4926                // trailing `ASR rd_hi, rn_hi, #31` also reads rn_hi after
4927                // rd_lo is written). The in-place form rd == rn stays legal.
4928                if rd_lo_bits == rn_hi_bits || rd_lo_bits == rm_lo_bits {
4929                    return Err(synth_core::Error::synthesis(format!(
4930                        "I64ShrS: rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4931                    )));
4932                }
4933
4934                // AND.W R12, rm_lo, #63  (n — the amount register is only READ)
4935                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4936                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4937                bytes.extend_from_slice(&hw1.to_le_bytes());
4938                bytes.extend_from_slice(&hw2.to_le_bytes());
4939
4940                // SUBS.W R12, R12, #32  (R12 = n-32, sets flags)
4941                let hw1: u16 = (0xF1B0 | r12) as u16;
4942                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4943                bytes.extend_from_slice(&hw1.to_le_bytes());
4944                bytes.extend_from_slice(&hw2.to_le_bytes());
4945
4946                // BPL .large (+14 halfwords)
4947                let bpl: u16 = 0xD50E;
4948                bytes.extend_from_slice(&bpl.to_le_bytes());
4949
4950                // --- Small shift (n < 32) ---
4951                // AND.W R12, rm_lo, #63  (n again — R12 held n-32)
4952                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4953                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4954                bytes.extend_from_slice(&hw1.to_le_bytes());
4955                bytes.extend_from_slice(&hw2.to_le_bytes());
4956
4957                // LSR.W rd_lo, rn_lo, R12  (lo >> n, logical for lo word)
4958                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4959                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4960                bytes.extend_from_slice(&hw1.to_le_bytes());
4961                bytes.extend_from_slice(&hw2.to_le_bytes());
4962
4963                // RSB.W R12, R12, #32  (R12 = 32-n)
4964                let hw1: u16 = (0xF1C0 | r12) as u16;
4965                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4966                bytes.extend_from_slice(&hw1.to_le_bytes());
4967                bytes.extend_from_slice(&hw2.to_le_bytes());
4968
4969                // LSL.W R12, rn_hi, R12  (overflow = hi << (32-n); n=0 gives
4970                // a register shift by 32 which yields 0 — exact)
4971                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4972                let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4973                bytes.extend_from_slice(&hw1.to_le_bytes());
4974                bytes.extend_from_slice(&hw2.to_le_bytes());
4975
4976                // ORR.W rd_lo, rd_lo, R12  (lo |= overflow from hi)
4977                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4978                let hw2: u16 = ((rd_lo_bits << 8) | r12) as u16;
4979                bytes.extend_from_slice(&hw1.to_le_bytes());
4980                bytes.extend_from_slice(&hw2.to_le_bytes());
4981
4982                // AND.W R12, rm_lo, #63  (n once more for the high half)
4983                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4984                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4985                bytes.extend_from_slice(&hw1.to_le_bytes());
4986                bytes.extend_from_slice(&hw2.to_le_bytes());
4987
4988                // ASR.W rd_hi, rn_hi, R12  (hi >> n, arithmetic/sign-extending)
4989                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
4990                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4991                bytes.extend_from_slice(&hw1.to_le_bytes());
4992                bytes.extend_from_slice(&hw2.to_le_bytes());
4993
4994                // B .done (+3 halfwords, large shift is 8 bytes)
4995                let b_done: u16 = 0xE003;
4996                bytes.extend_from_slice(&b_done.to_le_bytes());
4997
4998                // --- Large shift (n >= 32) --- (R12 still holds n-32)
4999                // ASR.W rd_lo, rn_hi, R12  (lo = hi >>> (n-32))
5000                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
5001                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
5002                bytes.extend_from_slice(&hw1.to_le_bytes());
5003                bytes.extend_from_slice(&hw2.to_le_bytes());
5004
5005                // ASR.W rd_hi, rn_hi, #31  (hi = sign extension, all 0s or all 1s)
5006                // Thumb-2 ASR immediate: hw1=0xEA4F, hw2=imm3:Rd:imm2:10:Rm
5007                // imm5=31=11111 → imm3=111, imm2=11
5008                let hw1: u16 = 0xEA4F;
5009                let hw2: u16 = (0x7000 | (rd_hi_bits << 8) | 0x00E0 | rn_hi_bits) as u16;
5010                bytes.extend_from_slice(&hw1.to_le_bytes());
5011                bytes.extend_from_slice(&hw2.to_le_bytes());
5012
5013                Ok(bytes) // Total: 48 bytes
5014            }
5015
5016            // I64Rotl: 64-bit rotate left (#610 rewrite).
5017            // For n < 32: new_hi = (hi << n) | (lo >> (32-n)), new_lo = (lo << n) | (hi >> (32-n))
5018            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
5019            //
5020            // Fixed-reg core: value in R0:R1, amount in R2, scratch R3 + R12
5021            // (all four saved/marshaled by the #610 fixed-ABI wrapper; the
5022            // pre-#610 expansion wrote through the selector's registers with
5023            // colliding R3/R4 scratch and restored the saved R4 OVER the
5024            // result). Relies on ARM register-shift semantics: amounts >= 32
5025            // yield 0 for LSL/LSR, which makes n = 0 and n = 32 exact.
5026            ArmOp::I64Rotl {
5027                rdlo,
5028                rdhi,
5029                rnlo,
5030                rnhi,
5031                shift,
5032            } => {
5033                let mut bytes = Vec::new();
5034                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
5035
5036                let core: [u16; 35] = [
5037                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
5038                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
5039                    0xD50E, //         BPL    .large        (n >= 32)
5040                    // --- small rotation (n < 32) ---
5041                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
5042                    0xFA20, 0xFC03, // LSR.W  R12, R0, R3   (lo >> (32-n))
5043                    0xFA21, 0xF303, // LSR.W  R3, R1, R3    (hi >> (32-n))
5044                    0xFA01, 0xF102, // LSL.W  R1, R1, R2    (hi << n)
5045                    0xEA41, 0x010C, // ORR.W  R1, R1, R12   (new_hi)
5046                    0xFA00, 0xF002, // LSL.W  R0, R0, R2    (lo << n)
5047                    0xEA40, 0x0003, // ORR.W  R0, R0, R3    (new_lo)
5048                    0xE00E, //         B      .done
5049                    // --- large rotation (n >= 32), R3 = m = n-32 ---
5050                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
5051                    0xFA21, 0xFC02, // LSR.W  R12, R1, R2   (hi >> (64-n))
5052                    0xFA20, 0xF202, // LSR.W  R2, R0, R2    (lo >> (64-n))
5053                    0xFA00, 0xF003, // LSL.W  R0, R0, R3    (lo << m)
5054                    0xFA01, 0xF103, // LSL.W  R1, R1, R3    (hi << m)
5055                    0xEA40, 0x0C0C, // ORR.W  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
5056                    0xEA41, 0x0002, // ORR.W  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
5057                    0x4661, //         MOV    R1, R12       (new_hi into place)
5058                            // .done: result in R0:R1
5059                ];
5060                for hw in core {
5061                    bytes.extend_from_slice(&hw.to_le_bytes());
5062                }
5063
5064                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5065                Ok(bytes) // Total: 102 bytes
5066            }
5067
5068            // I64Rotr: 64-bit rotate right (#610 rewrite).
5069            // For n < 32: new_lo = (lo >> n) | (hi << (32-n)), new_hi = (hi >> n) | (lo << (32-n))
5070            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
5071            //
5072            // Same fixed-reg core contract as I64Rotl: value in R0:R1, amount
5073            // in R2, scratch R3 + R12, all covered by the fixed-ABI wrapper.
5074            ArmOp::I64Rotr {
5075                rdlo,
5076                rdhi,
5077                rnlo,
5078                rnhi,
5079                shift,
5080            } => {
5081                let mut bytes = Vec::new();
5082                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
5083
5084                let core: [u16; 35] = [
5085                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
5086                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
5087                    0xD50E, //         BPL    .large        (n >= 32)
5088                    // --- small rotation (n < 32) ---
5089                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
5090                    0xFA01, 0xFC03, // LSL.W  R12, R1, R3   (hi << (32-n))
5091                    0xFA00, 0xF303, // LSL.W  R3, R0, R3    (lo << (32-n))
5092                    0xFA20, 0xF002, // LSR.W  R0, R0, R2    (lo >> n)
5093                    0xEA40, 0x000C, // ORR.W  R0, R0, R12   (new_lo)
5094                    0xFA21, 0xF102, // LSR.W  R1, R1, R2    (hi >> n)
5095                    0xEA41, 0x0103, // ORR.W  R1, R1, R3    (new_hi)
5096                    0xE00E, //         B      .done
5097                    // --- large rotation (n >= 32), R3 = m = n-32 ---
5098                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
5099                    0xFA00, 0xFC02, // LSL.W  R12, R0, R2   (lo << (64-n))
5100                    0xFA01, 0xF202, // LSL.W  R2, R1, R2    (hi << (64-n))
5101                    0xFA21, 0xF103, // LSR.W  R1, R1, R3    (hi >> m)
5102                    0xEA41, 0x0C0C, // ORR.W  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
5103                    0xFA20, 0xF103, // LSR.W  R1, R0, R3    (lo >> m)
5104                    0xEA41, 0x0102, // ORR.W  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
5105                    0x4660, //         MOV    R0, R12       (new_lo into place)
5106                            // .done: result in R0:R1
5107                ];
5108                for hw in core {
5109                    bytes.extend_from_slice(&hw.to_le_bytes());
5110                }
5111
5112                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5113                Ok(bytes) // Total: 102 bytes
5114            }
5115
5116            // I64Clz: Count leading zeros in 64-bit value
5117            // If hi != 0: result = CLZ(hi)
5118            // If hi == 0: result = 32 + CLZ(lo)
5119            //
5120            // Layout (using CMP+BNE approach for consistency):
5121            // 0: CMP.W rnhi, #0 (4 bytes)
5122            // 4: BEQ .hi_zero (2 bytes) - branch forward to offset 14
5123            // 6: CLZ.W rd, rnhi (4 bytes)
5124            // 10: B .done (2 bytes) - branch forward to offset 22
5125            // 12: NOP (2 bytes) - padding for alignment
5126            // 14: .hi_zero: CLZ.W rd, rnlo (4 bytes)
5127            // 18: ADD.W rd, rd, #32 (4 bytes)
5128            // 22: .done
5129            ArmOp::I64Clz { rd, rnlo, rnhi } => {
5130                let rd_bits = reg_to_bits(rd);
5131                let rn_lo_bits = reg_to_bits(rnlo);
5132                let rn_hi_bits = reg_to_bits(rnhi);
5133                let mut bytes = Vec::new();
5134
5135                // CMP.W rnhi, #0 (4 bytes at offset 0)
5136                let hw1: u16 = (0xF1B0 | rn_hi_bits) as u16;
5137                let hw2: u16 = 0x0F00;
5138                bytes.extend_from_slice(&hw1.to_le_bytes());
5139                bytes.extend_from_slice(&hw2.to_le_bytes());
5140
5141                // BEQ .hi_zero (2 bytes at offset 4)
5142                // PC = 4 + 4 = 8, target = 14, offset = 6, imm8 = 3
5143                let beq: u16 = 0xD003;
5144                bytes.extend_from_slice(&beq.to_le_bytes());
5145
5146                // CLZ.W rd, rnhi (4 bytes at offset 6)
5147                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5148                let hw1: u16 = (0xFAB0 | rn_hi_bits) as u16;
5149                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_hi_bits) as u16;
5150                bytes.extend_from_slice(&hw1.to_le_bytes());
5151                bytes.extend_from_slice(&hw2.to_le_bytes());
5152
5153                // B .done (2 bytes at offset 10)
5154                // PC = 10 + 4 = 14, target = 22, offset = 8, imm11 = 4
5155                let b_done: u16 = 0xE004;
5156                bytes.extend_from_slice(&b_done.to_le_bytes());
5157
5158                // NOP (2 bytes at offset 12) - padding
5159                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5160
5161                // .hi_zero: (offset 14)
5162                // CLZ.W rd, rnlo (4 bytes)
5163                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5164                let hw1: u16 = (0xFAB0 | rn_lo_bits) as u16;
5165                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_lo_bits) as u16;
5166                bytes.extend_from_slice(&hw1.to_le_bytes());
5167                bytes.extend_from_slice(&hw2.to_le_bytes());
5168
5169                // ADD.W rd, rd, #32 (4 bytes at offset 18)
5170                let hw1: u16 = (0xF100 | rd_bits) as u16;
5171                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5172                bytes.extend_from_slice(&hw1.to_le_bytes());
5173                bytes.extend_from_slice(&hw2.to_le_bytes());
5174
5175                // .done: (offset 22 — the end of the expansion)
5176                //
5177                // #1048: the former trailing hi-word clear (`MOV rnhi, #0`)
5178                // is GONE. It was aimed at the RESULT's high half but wrote
5179                // the OPERAND's home high register — a real executed
5180                // miscompile on the direct selector, which allocates a fresh
5181                // destination pair and zeroes its own dst_hi, leaving the
5182                // operand's hi limb destroyed for any later re-read. The
5183                // Rocq/SMT models of I64ClzPseudo always said "writes rd
5184                // ONLY"; the callers that relied on the implicit clear
5185                // (select_default, optimizer_bridge) now emit their own
5186                // explicit hi-zero op. `B .done` above targets offset 22 =
5187                // past-the-end, `BEQ` targets offset 14 — no displacement
5188                // moves.
5189
5190                Ok(bytes) // 22 bytes, register-independent
5191            }
5192
5193            // I64Ctz: Count trailing zeros in 64-bit value
5194            // If lo != 0: result = CTZ(lo) = CLZ(RBIT(lo))
5195            // If lo == 0: result = 32 + CTZ(hi) = 32 + CLZ(RBIT(hi))
5196            //
5197            // Layout:
5198            // 0: CMP.W rnlo, #0 (4 bytes)
5199            // 4: BEQ .lo_zero (2 bytes) - branch to offset 18
5200            // 6: RBIT.W rd, rnlo (4 bytes)
5201            // 10: CLZ.W rd, rd (4 bytes)
5202            // 14: B .done (2 bytes) - branch to offset 30
5203            // 16: NOP (2 bytes) - padding
5204            // 18: .lo_zero: RBIT.W rd, rnhi (4 bytes)
5205            // 22: CLZ.W rd, rd (4 bytes)
5206            // 26: ADD.W rd, rd, #32 (4 bytes)
5207            // 30: .done
5208            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
5209                let rd_bits = reg_to_bits(rd);
5210                let rn_lo_bits = reg_to_bits(rnlo);
5211                let rn_hi_bits = reg_to_bits(rnhi);
5212                let mut bytes = Vec::new();
5213
5214                // CMP.W rnlo, #0 (4 bytes at offset 0)
5215                let hw1: u16 = (0xF1B0 | rn_lo_bits) as u16;
5216                let hw2: u16 = 0x0F00;
5217                bytes.extend_from_slice(&hw1.to_le_bytes());
5218                bytes.extend_from_slice(&hw2.to_le_bytes());
5219
5220                // BEQ .lo_zero (2 bytes at offset 4)
5221                // PC = 4 + 4 = 8, target = 18, offset = 10, imm8 = 5
5222                let beq: u16 = 0xD005;
5223                bytes.extend_from_slice(&beq.to_le_bytes());
5224
5225                // RBIT.W rd, rnlo (4 bytes at offset 6)
5226                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5227                let hw1: u16 = (0xFA90 | rn_lo_bits) as u16;
5228                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_lo_bits) as u16;
5229                bytes.extend_from_slice(&hw1.to_le_bytes());
5230                bytes.extend_from_slice(&hw2.to_le_bytes());
5231
5232                // CLZ.W rd, rd (4 bytes at offset 10)
5233                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5234                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5235                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5236                bytes.extend_from_slice(&hw1.to_le_bytes());
5237                bytes.extend_from_slice(&hw2.to_le_bytes());
5238
5239                // B .done (2 bytes at offset 14)
5240                // PC = 14 + 4 = 18, target = 30, offset = 12, imm11 = 6
5241                let b_done: u16 = 0xE006;
5242                bytes.extend_from_slice(&b_done.to_le_bytes());
5243
5244                // NOP (2 bytes at offset 16) - padding
5245                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5246
5247                // .lo_zero: (offset 18)
5248                // RBIT.W rd, rnhi (4 bytes)
5249                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5250                let hw1: u16 = (0xFA90 | rn_hi_bits) as u16;
5251                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_hi_bits) as u16;
5252                bytes.extend_from_slice(&hw1.to_le_bytes());
5253                bytes.extend_from_slice(&hw2.to_le_bytes());
5254
5255                // CLZ.W rd, rd (4 bytes at offset 22)
5256                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5257                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5258                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5259                bytes.extend_from_slice(&hw1.to_le_bytes());
5260                bytes.extend_from_slice(&hw2.to_le_bytes());
5261
5262                // ADD.W rd, rd, #32 (4 bytes at offset 26)
5263                let hw1: u16 = (0xF100 | rd_bits) as u16;
5264                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5265                bytes.extend_from_slice(&hw1.to_le_bytes());
5266                bytes.extend_from_slice(&hw2.to_le_bytes());
5267
5268                // .done: (offset 30 — the end of the expansion)
5269                // #1048: the former trailing `MOV rnhi, #0` is GONE — it
5270                // wrote the OPERAND's home high register (see the I64Clz
5271                // comment above). `B .done` targets offset 30 = past-the-end,
5272                // `BEQ` targets offset 18 — no displacement moves.
5273
5274                Ok(bytes) // 30 bytes, register-independent
5275            }
5276
5277            // I64Popcnt: Population count of 64-bit value
5278            // result = POPCNT(lo) + POPCNT(hi)
5279            // Using SIMD-style parallel bit counting algorithm
5280            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
5281                let rd_bits = reg_to_bits(rd);
5282                let rn_lo_bits = reg_to_bits(rnlo);
5283                let rn_hi_bits = reg_to_bits(rnhi);
5284                let r12: u32 = 12; // IP scratch
5285                let r3: u32 = 3; // Scratch for hi popcnt result
5286                let mut bytes = Vec::new();
5287
5288                // PUSH {R3, R4, R5} - save scratch registers
5289                bytes.extend_from_slice(&0xB438u16.to_le_bytes());
5290
5291                // Strategy: compute popcnt(lo) -> R4, popcnt(hi) -> R5, add them -> rd
5292                // Using lookup table approach for each byte would be too large
5293                // Using shift-and-add approach instead
5294
5295                // For simplicity and correctness, use the efficient parallel algorithm
5296                // but implement it as a series of inline operations
5297
5298                // Marshal the operand pair into the fixed scratch regs, routing
5299                // rnlo through R12 (#632 audit): writing R4 first corrupted the
5300                // rnhi read for a pair living at (R3,R4) — every source is read
5301                // before any scratch register it could occupy is written.
5302                // MOV R12, rnlo
5303                let mov: u16 = (0x4600 | (1 << 7) | (rn_lo_bits << 3) | 4) as u16;
5304                bytes.extend_from_slice(&mov.to_le_bytes());
5305                // MOV R5, rnhi (R4 untouched so far; rnhi == R5 is a no-op)
5306                let mov: u16 = (0x4600 | (rn_hi_bits << 3) | 5) as u16;
5307                bytes.extend_from_slice(&mov.to_le_bytes());
5308                // MOV R4, R12
5309                bytes.extend_from_slice(&0x4664u16.to_le_bytes());
5310
5311                // --- POPCNT for R4 (lo word) ---
5312                // Step 1: x = x - ((x >> 1) & 0x55555555)
5313                // LSR.W R12, R4, #1
5314                let hw1: u16 = 0xEA4F;
5315                let hw2: u16 = ((r12 << 8) | 0x50 | 4) as u16;
5316                bytes.extend_from_slice(&hw1.to_le_bytes());
5317                bytes.extend_from_slice(&hw2.to_le_bytes());
5318
5319                // Load 0x55555555 into R3 using MOVW/MOVT
5320                // MOVW R3, #0x5555
5321                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5322                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5323                // MOVT R3, #0x5555
5324                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5325                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5326
5327                // AND.W R12, R12, R3
5328                let hw1: u16 = (0xEA00 | r12) as u16;
5329                let hw2: u16 = ((r12 << 8) | r3) as u16;
5330                bytes.extend_from_slice(&hw1.to_le_bytes());
5331                bytes.extend_from_slice(&hw2.to_le_bytes());
5332
5333                // SUB.W R4, R4, R12
5334                let hw1: u16 = (0xEBA0 | 4) as u16;
5335                let hw2: u16 = ((4 << 8) | r12) as u16;
5336                bytes.extend_from_slice(&hw1.to_le_bytes());
5337                bytes.extend_from_slice(&hw2.to_le_bytes());
5338
5339                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5340                // Load 0x33333333 into R3
5341                // MOVW R3, #0x3333
5342                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5343                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5344                // MOVT R3, #0x3333
5345                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5346                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5347
5348                // AND.W R12, R4, R3
5349                let hw1: u16 = (0xEA00 | 4) as u16;
5350                let hw2: u16 = ((r12 << 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 R4, R4, #2
5355                let hw1: u16 = 0xEA4F;
5356                let hw2: u16 = ((4 << 8) | 0x90 | 4) as u16;
5357                bytes.extend_from_slice(&hw1.to_le_bytes());
5358                bytes.extend_from_slice(&hw2.to_le_bytes());
5359
5360                // AND.W R4, R4, R3
5361                let hw1: u16 = (0xEA00 | 4) as u16;
5362                let hw2: u16 = ((4 << 8) | r3) as u16;
5363                bytes.extend_from_slice(&hw1.to_le_bytes());
5364                bytes.extend_from_slice(&hw2.to_le_bytes());
5365
5366                // ADD.W R4, R4, R12
5367                let hw1: u16 = (0xEB00 | 4) as u16;
5368                let hw2: u16 = ((4 << 8) | r12) as u16;
5369                bytes.extend_from_slice(&hw1.to_le_bytes());
5370                bytes.extend_from_slice(&hw2.to_le_bytes());
5371
5372                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5373                // LSR.W R12, R4, #4
5374                // hw2 = (imm3 << 12) | (Rd << 8) | (imm2 << 6) | (type << 4) | Rm
5375                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5376                let hw1: u16 = 0xEA4F;
5377                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 4) as u16;
5378                bytes.extend_from_slice(&hw1.to_le_bytes());
5379                bytes.extend_from_slice(&hw2.to_le_bytes());
5380
5381                // ADD.W R4, R4, R12
5382                let hw1: u16 = (0xEB00 | 4) as u16;
5383                let hw2: u16 = ((4 << 8) | r12) as u16;
5384                bytes.extend_from_slice(&hw1.to_le_bytes());
5385                bytes.extend_from_slice(&hw2.to_le_bytes());
5386
5387                // Load 0x0F0F0F0F into R3
5388                // MOVW R3, #0x0F0F (imm4=0, i=1, imm3=7, imm8=0x0F)
5389                // hw1 = 11110 1 10 0100 0000 = 0xF640
5390                // hw2 = 0 111 0011 00001111 = 0x730F
5391                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5392                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5393                // MOVT R3, #0x0F0F
5394                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5395                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5396
5397                // AND.W R4, R4, R3
5398                let hw1: u16 = (0xEA00 | 4) as u16;
5399                let hw2: u16 = ((4 << 8) | r3) as u16;
5400                bytes.extend_from_slice(&hw1.to_le_bytes());
5401                bytes.extend_from_slice(&hw2.to_le_bytes());
5402
5403                // Step 4: x = x * 0x01010101 >> 24
5404                // Load 0x01010101 into R3
5405                // MOVW R3, #0x0101
5406                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5407                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5408                // MOVT R3, #0x0101
5409                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5410                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5411
5412                // MUL R4, R4, R3
5413                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5414                let hw1: u16 = (0xFB00 | 4) as u16;
5415                let hw2: u16 = (0xF000 | (4 << 8) | r3) as u16;
5416                bytes.extend_from_slice(&hw1.to_le_bytes());
5417                bytes.extend_from_slice(&hw2.to_le_bytes());
5418
5419                // LSR.W R4, R4, #24
5420                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5421                let hw1: u16 = 0xEA4F;
5422                let hw2: u16 = (0x6000 | (4 << 8) | 0x10 | 4) as u16;
5423                bytes.extend_from_slice(&hw1.to_le_bytes());
5424                bytes.extend_from_slice(&hw2.to_le_bytes());
5425
5426                // --- POPCNT for R5 (hi word) - same algorithm ---
5427                // Step 1
5428                let hw1: u16 = 0xEA4F;
5429                let hw2: u16 = ((r12 << 8) | 0x50 | 5) as u16;
5430                bytes.extend_from_slice(&hw1.to_le_bytes());
5431                bytes.extend_from_slice(&hw2.to_le_bytes());
5432
5433                // Load 0x55555555 into R3
5434                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5435                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5436                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5437                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5438
5439                let hw1: u16 = (0xEA00 | r12) as u16;
5440                let hw2: u16 = ((r12 << 8) | r3) as u16;
5441                bytes.extend_from_slice(&hw1.to_le_bytes());
5442                bytes.extend_from_slice(&hw2.to_le_bytes());
5443
5444                let hw1: u16 = (0xEBA0 | 5) as u16;
5445                let hw2: u16 = ((5 << 8) | r12) as u16;
5446                bytes.extend_from_slice(&hw1.to_le_bytes());
5447                bytes.extend_from_slice(&hw2.to_le_bytes());
5448
5449                // Step 2
5450                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5451                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5452                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5453                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5454
5455                let hw1: u16 = (0xEA00 | 5) as u16;
5456                let hw2: u16 = ((r12 << 8) | r3) as u16;
5457                bytes.extend_from_slice(&hw1.to_le_bytes());
5458                bytes.extend_from_slice(&hw2.to_le_bytes());
5459
5460                let hw1: u16 = 0xEA4F;
5461                let hw2: u16 = ((5 << 8) | 0x90 | 5) as u16;
5462                bytes.extend_from_slice(&hw1.to_le_bytes());
5463                bytes.extend_from_slice(&hw2.to_le_bytes());
5464
5465                let hw1: u16 = (0xEA00 | 5) as u16;
5466                let hw2: u16 = ((5 << 8) | r3) as u16;
5467                bytes.extend_from_slice(&hw1.to_le_bytes());
5468                bytes.extend_from_slice(&hw2.to_le_bytes());
5469
5470                let hw1: u16 = (0xEB00 | 5) as u16;
5471                let hw2: u16 = ((5 << 8) | r12) as u16;
5472                bytes.extend_from_slice(&hw1.to_le_bytes());
5473                bytes.extend_from_slice(&hw2.to_le_bytes());
5474
5475                // Step 3: LSR.W R12, R5, #4
5476                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5477                let hw1: u16 = 0xEA4F;
5478                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 5) as u16;
5479                bytes.extend_from_slice(&hw1.to_le_bytes());
5480                bytes.extend_from_slice(&hw2.to_le_bytes());
5481
5482                let hw1: u16 = (0xEB00 | 5) as u16;
5483                let hw2: u16 = ((5 << 8) | r12) as u16;
5484                bytes.extend_from_slice(&hw1.to_le_bytes());
5485                bytes.extend_from_slice(&hw2.to_le_bytes());
5486
5487                // Load 0x0F0F0F0F into R3 (for hi-word)
5488                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5489                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5490                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5491                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5492
5493                let hw1: u16 = (0xEA00 | 5) as u16;
5494                let hw2: u16 = ((5 << 8) | r3) as u16;
5495                bytes.extend_from_slice(&hw1.to_le_bytes());
5496                bytes.extend_from_slice(&hw2.to_le_bytes());
5497
5498                // Step 4
5499                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5500                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5501                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5502                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5503
5504                // MUL R5, R5, R3
5505                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5506                let hw1: u16 = (0xFB00 | 5) as u16;
5507                let hw2: u16 = (0xF000 | (5 << 8) | r3) as u16;
5508                bytes.extend_from_slice(&hw1.to_le_bytes());
5509                bytes.extend_from_slice(&hw2.to_le_bytes());
5510
5511                // LSR.W R5, R5, #24
5512                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5513                let hw1: u16 = 0xEA4F;
5514                let hw2: u16 = (0x6000 | (5 << 8) | 0x10 | 5) as u16;
5515                bytes.extend_from_slice(&hw1.to_le_bytes());
5516                bytes.extend_from_slice(&hw2.to_le_bytes());
5517
5518                // #632: the count must be carried ACROSS the scratch restore
5519                // in a register the POP cannot touch. rd is allocator-assigned
5520                // (any of R0-R8) and can land inside the {R3,R4,R5} restore set
5521                // — the old `ADDS rd, R4, R5; POP {R3,R4,R5}` destroyed the
5522                // result one instruction after computing it (0 for every input
5523                // under qemu). R12 is encoder scratch: never allocatable (#212)
5524                // and never in a restore set, so no choice of rd can collide.
5525                // ADD.W R12, R4, R5
5526                bytes.extend_from_slice(&0xEB04u16.to_le_bytes());
5527                bytes.extend_from_slice(&0x0C05u16.to_le_bytes());
5528
5529                // POP {R3, R4, R5}
5530                bytes.extend_from_slice(&0xBC38u16.to_le_bytes());
5531
5532                // MOV rd, R12 — after the restore. The 4-bit Rd (D:rd) form is
5533                // also total over rd = R8, where the old ADDS T1 3-bit field
5534                // silently corrupted the encoding (#178/#180 class).
5535                let mov: u16 =
5536                    (0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7)) as u16;
5537                bytes.extend_from_slice(&mov.to_le_bytes());
5538
5539                // #1048: the former trailing `MOV.W rnhi, #0` hi-word clear
5540                // is GONE — it wrote the OPERAND's home high register (see
5541                // the I64Clz comment). Callers that relied on the implicit
5542                // clear emit their own explicit hi-zero op.
5543
5544                Ok(bytes)
5545            }
5546
5547            // I64Extend8S: Sign-extend low 8 bits to 64 bits
5548            // Result: rdlo = sign_extend_8(rnlo), rdhi = rdlo >> 31
5549            ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
5550                let rdlo_bits = reg_to_bits(rdlo);
5551                let rdhi_bits = reg_to_bits(rdhi);
5552                let rnlo_bits = reg_to_bits(rnlo);
5553                let mut bytes = Vec::new();
5554
5555                // SXTB.W rdlo, rnlo (sign-extend byte to 32-bit)
5556                // SXTB T2: hw1 = 0xFA4F, hw2 = 0xF0<Rd><Rm>
5557                let hw1: u16 = 0xFA4F_u16;
5558                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5559                bytes.extend_from_slice(&hw1.to_le_bytes());
5560                bytes.extend_from_slice(&hw2.to_le_bytes());
5561
5562                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5563                // ASR (immediate): hw1 = 0xEA4F, hw2 = imm3:Rd:imm2:type:Rm
5564                // For imm5=31: imm3=111, imm2=11, type=10 (ASR)
5565                // hw2 = (7 << 12) | (rdhi << 8) | (3 << 6) | (2 << 4) | rdlo
5566                let hw1: u16 = 0xEA4F;
5567                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5568                bytes.extend_from_slice(&hw1.to_le_bytes());
5569                bytes.extend_from_slice(&hw2.to_le_bytes());
5570
5571                Ok(bytes)
5572            }
5573
5574            // I64Extend16S: Sign-extend low 16 bits to 64 bits
5575            // Result: rdlo = sign_extend_16(rnlo), rdhi = rdlo >> 31
5576            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
5577                let rdlo_bits = reg_to_bits(rdlo);
5578                let rdhi_bits = reg_to_bits(rdhi);
5579                let rnlo_bits = reg_to_bits(rnlo);
5580                let mut bytes = Vec::new();
5581
5582                // SXTH.W rdlo, rnlo (sign-extend halfword to 32-bit)
5583                // SXTH T2: hw1 = 0xFA0F, hw2 = 0xF0<Rd><Rm>
5584                let hw1: u16 = 0xFA0F_u16;
5585                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5586                bytes.extend_from_slice(&hw1.to_le_bytes());
5587                bytes.extend_from_slice(&hw2.to_le_bytes());
5588
5589                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5590                let hw1: u16 = 0xEA4F;
5591                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5592                bytes.extend_from_slice(&hw1.to_le_bytes());
5593                bytes.extend_from_slice(&hw2.to_le_bytes());
5594
5595                Ok(bytes)
5596            }
5597
5598            // I64Extend32S: Sign-extend low 32 bits to 64 bits
5599            // Result: rdlo = rnlo, rdhi = rnlo >> 31
5600            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
5601                let rdlo_bits = reg_to_bits(rdlo);
5602                let rdhi_bits = reg_to_bits(rdhi);
5603                let rnlo_bits = reg_to_bits(rnlo);
5604                let mut bytes = Vec::new();
5605
5606                // MOV rdlo, rnlo (if different)
5607                if rdlo_bits != rnlo_bits {
5608                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5609                    let d_bit = ((rdlo_bits >> 3) & 1) as u16;
5610                    let mov: u16 = 0x4600
5611                        | (d_bit << 7)
5612                        | ((rnlo_bits as u16) << 3)
5613                        | ((rdlo_bits & 0x7) as u16);
5614                    bytes.extend_from_slice(&mov.to_le_bytes());
5615                }
5616
5617                // ASR.W rdhi, rnlo, #31 (sign-extend to high word)
5618                let hw1: u16 = 0xEA4F;
5619                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rnlo_bits) as u16;
5620                bytes.extend_from_slice(&hw1.to_le_bytes());
5621                bytes.extend_from_slice(&hw2.to_le_bytes());
5622
5623                Ok(bytes)
5624            }
5625
5626            // SelectMove: IT <cond>; MOV{cond} rd, rm
5627            // Conditional move: only execute MOV if condition is true
5628            ArmOp::SelectMove { rd, rm, cond } => {
5629                let rd_bits = reg_to_bits(rd) as u16;
5630                let rm_bits = reg_to_bits(rm) as u16;
5631
5632                // Condition code encoding for IT block
5633                use synth_synthesis::Condition;
5634                let cond_bits: u16 = match cond {
5635                    Condition::EQ => 0x0, // Equal
5636                    Condition::NE => 0x1, // Not equal
5637                    Condition::HS => 0x2, // Higher or same (unsigned >=)
5638                    Condition::LO => 0x3, // Lower (unsigned <)
5639                    Condition::HI => 0x8, // Higher (unsigned >)
5640                    Condition::LS => 0x9, // Lower or same (unsigned <=)
5641                    Condition::GE => 0xA, // Greater or equal (signed)
5642                    Condition::LT => 0xB, // Less than (signed)
5643                    Condition::GT => 0xC, // Greater than (signed)
5644                    Condition::LE => 0xD, // Less or equal (signed)
5645                };
5646
5647                // IT <cond>: single Then block (mask = 0x8 for T only)
5648                // IT instruction: 1011 1111 firstcond mask
5649                let it_instr: u16 = 0xBF00 | (cond_bits << 4) | 0x8;
5650
5651                // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5652                // This MOV will only execute if condition is true due to IT block
5653                let d_bit = (rd_bits >> 3) & 1;
5654                let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5655
5656                // Emit: IT <cond>, MOV rd, rm
5657                let mut bytes = it_instr.to_le_bytes().to_vec();
5658                bytes.extend_from_slice(&mov_instr.to_le_bytes());
5659                Ok(bytes)
5660            }
5661
5662            // Popcnt: Population count (count set bits)
5663            // ARM Cortex-M has no native POPCNT, so we implement the bit manipulation algorithm:
5664            // x = x - ((x >> 1) & 0x55555555);
5665            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
5666            // x = (x + (x >> 4)) & 0x0F0F0F0F;
5667            // x = x + (x >> 8);
5668            // x = x + (x >> 16);
5669            // return x & 0x3F;
5670            //
5671            // #1021: R12 (IP, never allocatable) is the ONLY scratch. The
5672            // previous expansion borrowed R11 as a second temp — but R11 is
5673            // the WASM linear-memory base, materialized at entry and read by
5674            // every later LDR/STR, and it is NOT in the pushed set, so the
5675            // clobber leaked to the CALLER too (a live memory-safety
5676            // miscompile: loads through `base = x >> 16`). The second temp is
5677            // eliminated the way the healthy i64.popcnt discipline implies —
5678            // never touch an unsaved register — but without its PUSH/POP
5679            // wrapper: the SWAR masks 0x55555555 / 0x33333333 / 0x0F0F0F0F
5680            // are all `0xXYXYXYXY` ThumbExpandImm modified immediates, so
5681            // each AND takes its mask from the instruction itself and R12
5682            // alone carries every intermediate. Straight-line, no branches,
5683            // no stack traffic — nothing to skip on a trap edge.
5684            ArmOp::Popcnt { rd, rm } => {
5685                let rd_bits = reg_to_bits(rd);
5686                // Defensive (#1021): rd = R11/R12/SP/PC would silently
5687                // corrupt the linear-memory base, the expansion's own
5688                // scratch, or the stack. The selector never assigns them
5689                // (pool R0-R8); refuse loudly if that ever changes.
5690                if rd_bits >= 11 {
5691                    return Err(synth_core::Error::synthesis(
5692                        "Popcnt destination must be R0-R10: R11 is the linear-memory \
5693                         base and R12 is the expansion's scratch (#1021)",
5694                    ));
5695                }
5696                let mut bytes = Vec::new();
5697
5698                // First, move rm to rd if they're different
5699                if rd != rm {
5700                    let rm_bits = reg_to_bits(rm) as u16;
5701                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5702                    let d_bit = ((rd_bits as u16) >> 3) & 1;
5703                    let mov_instr: u16 =
5704                        0x4600 | (d_bit << 7) | (rm_bits << 3) | ((rd_bits as u16) & 0x7);
5705                    bytes.extend_from_slice(&mov_instr.to_le_bytes());
5706                }
5707
5708                // Step 1: x = x - ((x >> 1) & 0x55555555)
5709                // R12 = rd >> 1
5710                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 1)?);
5711                // R12 = R12 & 0x55555555 (modified immediate, no constant reg)
5712                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(12, 12, 0x5555_5555)?);
5713                // rd = rd - R12
5714                bytes.extend_from_slice(&self.encode_thumb32_sub_reg_raw(rd_bits, rd_bits, 12)?);
5715
5716                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5717                // R12 = rd & 0x33333333
5718                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5719                    12,
5720                    rd_bits,
5721                    0x3333_3333,
5722                )?);
5723                // rd = rd >> 2
5724                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(rd_bits, rd_bits, 2)?);
5725                // rd = rd & 0x33333333
5726                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5727                    rd_bits,
5728                    rd_bits,
5729                    0x3333_3333,
5730                )?);
5731                // rd = rd + R12
5732                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5733
5734                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5735                // R12 = rd >> 4
5736                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 4)?);
5737                // rd = rd + R12
5738                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5739                // rd = rd & 0x0F0F0F0F
5740                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5741                    rd_bits,
5742                    rd_bits,
5743                    0x0F0F_0F0F,
5744                )?);
5745
5746                // Step 4: x = x + (x >> 8)
5747                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 8)?);
5748                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5749
5750                // Step 5: x = x + (x >> 16)
5751                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 16)?);
5752                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5753
5754                // Step 6: return x & 0x3F
5755                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(rd_bits, rd_bits, 0x3F)?);
5756
5757                Ok(bytes)
5758            }
5759
5760            // I64DivU: 64-bit unsigned division using binary long division
5761            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = quotient
5762            // Uses: R4-R7, R12 as loop counter (avoid R8 for Renode compatibility)
5763            //
5764            // #610: the fixed-ABI wrapper marshals the selector-assigned
5765            // operand registers into the core's fixed regs and lands the
5766            // result in rd — pre-#610 this arm IGNORED its register fields,
5767            // so the selector read its rd pair (e.g. R4:R5) after the core's
5768            // own POP restored the stale caller values over it: 0 for every
5769            // input. A zero divisor now traps (UDF #0), per WASM semantics.
5770            ArmOp::I64DivU {
5771                rdlo,
5772                rdhi,
5773                rnlo,
5774                rnhi,
5775                rmlo,
5776                rmhi,
5777                elide_zero_guard,
5778            } => {
5779                let mut bytes = Vec::new();
5780                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5781                // #494 phase 2b: elided only under a certificate-discharged
5782                // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
5783                if !elide_zero_guard {
5784                    emit_i64_divisor_zero_trap(&mut bytes);
5785                }
5786
5787                // PUSH {R4-R7} - save scratch registers (NO LR — this is inline code)
5788                // 16-bit PUSH: 1011 010 M rrrrrrrr where M=0 (no LR), r=R4-R7 = 0xF0
5789                // Encoding: 1011 0100 1111 0000 = 0xB4F0
5790                bytes.extend_from_slice(&0xB4F0u16.to_le_bytes());
5791
5792                // Initialize quotient (R4:R5) = 0
5793                bytes.extend_from_slice(&0x2400u16.to_le_bytes()); // MOV R4, #0
5794                bytes.extend_from_slice(&0x2500u16.to_le_bytes()); // MOV R5, #0
5795
5796                // Initialize remainder (R6:R7) = 0
5797                bytes.extend_from_slice(&0x2600u16.to_le_bytes()); // MOV R6, #0
5798                bytes.extend_from_slice(&0x2700u16.to_le_bytes()); // MOV R7, #0
5799
5800                // Initialize loop counter R12 = 64 (use R12 scratch instead of R8)
5801                // MOV.W R12, #64: F04F 0C40
5802                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5803                bytes.extend_from_slice(&0x0C40u16.to_le_bytes());
5804
5805                // Loop start
5806                let loop_start = bytes.len();
5807
5808                // === Loop body: process one bit ===
5809
5810                // 1. Shift quotient R4:R5 left by 1
5811                // LSLS R5, R5, #1 (16-bit: 0000 0010 1010 1101 = 0x006D -> actually 0x002D for LSL R5,R5,#1)
5812                // LSL Rd, Rm, #imm5: 000 00 imm5 Rm Rd = 000 00 00001 101 101 = 0x006D
5813                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5814                // Get carry from R4 into R5: ORR R5, R5, R4 LSR #31
5815                // Thumb-2 ORR with shifted register: EA45 75D4 = ORR.W R5, R5, R4, LSR #31
5816                // 11101010 010 S Rn | 0 imm3 Rd imm2 type Rm
5817                // type=01 (LSR), imm5=31 (imm3=111, imm2=11)
5818                bytes.extend_from_slice(&0xEA45u16.to_le_bytes());
5819                bytes.extend_from_slice(&0x75D4u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5820                // LSLS R4, R4, #1: 000 00 00001 100 100 = 0x0064
5821                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5822
5823                // 2. Shift remainder R6:R7 left by 1, OR in MSB of dividend R1
5824                // LSLS R7, R7, #1
5825                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5826                // ORR.W R7, R7, R6, LSR #31
5827                bytes.extend_from_slice(&0xEA47u16.to_le_bytes());
5828                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5829                // LSLS R6, R6, #1
5830                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5831                // ORR.W R6, R6, R1, LSR #31 (bring in MSB of dividend high)
5832                bytes.extend_from_slice(&0xEA46u16.to_le_bytes());
5833                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5834
5835                // 3. Shift dividend R0:R1 left by 1
5836                // LSLS R1, R1, #1
5837                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5838                // ORR.W R1, R1, R0, LSR #31
5839                bytes.extend_from_slice(&0xEA41u16.to_le_bytes());
5840                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5841                // LSLS R0, R0, #1
5842                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5843
5844                // 4. Compare remainder >= divisor (64-bit unsigned comparison)
5845                // Compare high words first: CMP R7, R3
5846                // CMP Rn, Rm encoding: 0x4280 | (Rm << 3) | Rn
5847                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3 (16-bit)
5848                // BHI means R7 > R3 (unsigned) - definitely subtract
5849                // BLO means R7 < R3 - definitely don't subtract
5850                // BEQ means need to check low words
5851
5852                // If high > divisor high: branch to subtract (forward +offset)
5853                // BHI.N +6 (skip CMP, skip BLO, do subtract)
5854                // BHI: 1101 1000 offset8 where cond=1000 (HI)
5855                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4 (to subtract block)
5856
5857                // If high < divisor high: branch past subtract
5858                // BLO.N +10 (skip to decrement)
5859                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BLO/BCC +12 (past subtract)
5860
5861                // High words equal, compare low: CMP R6, R2
5862                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2 (16-bit)
5863                // BLO/BCC past subtract (skip SUBS+SBC.W+ORR.W = 10 bytes = 4 halfwords from PC+4)
5864                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords (past subtract)
5865
5866                // === Subtract block: remainder -= divisor, quotient |= 1 ===
5867                // SUBS R6, R6, R2
5868                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2 (16-bit)
5869                // SBC R7, R7, R3 (with borrow)
5870                // Thumb-2 SBC.W: EB67 0703 = SBC.W R7, R7, R3
5871                bytes.extend_from_slice(&0xEB67u16.to_le_bytes());
5872                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5873                // ORR R4, R4, #1 (set bit 0 of quotient low)
5874                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5875                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5876
5877                // === Decrement counter and loop ===
5878                // SUBS.W R12, R12, #1 (decrement loop counter)
5879                // SUBS.W R12, R12, #1: F1BC 0C01
5880                bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
5881                bytes.extend_from_slice(&0x0C01u16.to_le_bytes());
5882
5883                // BNE back to loop_start
5884                let branch_offset_bytes = bytes.len() - loop_start + 4; // +4 for pipeline
5885                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5886                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5887                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5888
5889                // === Loop done, move quotient to R0:R1 ===
5890                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5891                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5892
5893                // POP {R4-R7} - restore scratch registers (NO PC — inline code continues)
5894                // 16-bit POP: 1011 110 P rrrrrrrr where P=0 (no PC), r=R4-R7 = 0xF0
5895                // Encoding: 1011 1100 1111 0000 = 0xBCF0
5896                bytes.extend_from_slice(&0xBCF0u16.to_le_bytes());
5897
5898                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5899                Ok(bytes)
5900            }
5901
5902            // I64DivS: 64-bit signed division
5903            // Converts to unsigned, divides, then applies sign
5904            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5905            //   ->  R0:R1 = quotient (signed)
5906            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5907            ArmOp::I64DivS {
5908                rdlo,
5909                rdhi,
5910                rnlo,
5911                rnhi,
5912                rmlo,
5913                rmhi,
5914                elide_zero_guard,
5915                elide_overflow_guard,
5916            } => {
5917                let mut bytes = Vec::new();
5918                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5919                // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
5920                // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
5921                // the #633 overflow guard falls ONLY to
5922                // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
5923                // divisor-nonzero fact alone must keep it.
5924                if !elide_zero_guard {
5925                    emit_i64_divisor_zero_trap(&mut bytes);
5926                }
5927                if !elide_overflow_guard {
5928                    // #633: INT64_MIN / -1 overflows — trap like the i32 path
5929                    // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
5930                    emit_i64_divs_overflow_trap(&mut bytes);
5931                }
5932
5933                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5934                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5935                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5936
5937                // Save result sign in R9: R9 = R1 XOR R3 (sign bit = MSB)
5938                // EOR.W R9, R1, R3
5939                bytes.extend_from_slice(&0xEA81u16.to_le_bytes());
5940                bytes.extend_from_slice(&0x0903u16.to_le_bytes());
5941
5942                // If dividend negative (R1 MSB set), negate it
5943                // TST R1, R1 (check sign)
5944                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5945                // BPL skip_neg_dividend (+10 bytes = 5 halfwords)
5946                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5947
5948                // Negate R0:R1 (64-bit): RSBS R0, R0, #0; SBC R1, R1, R1 LSL #1
5949                // Actually: MVN R0, R0; MVN R1, R1; ADDS R0, R0, #1; ADC R1, R1, #0
5950                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5951                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5952                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5953                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5954                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5955
5956                // If divisor negative (R3 MSB set), negate it
5957                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5958                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5959
5960                // Negate R2:R3
5961                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
5962                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
5963                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
5964                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
5965                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
5966
5967                // === Now do unsigned division (same as I64DivU) ===
5968                // Initialize quotient (R4:R5) = 0
5969                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5970                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5971                // Initialize remainder (R6:R7) = 0
5972                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5973                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5974                // Initialize loop counter R8 = 64
5975                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5976                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5977
5978                let loop_start = bytes.len();
5979
5980                // Shift quotient left
5981                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5982                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5983                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5984                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5985
5986                // Shift remainder left, OR in MSB of dividend
5987                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5988                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5989                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5990                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5991                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5992                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5993
5994                // Shift dividend left
5995                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5996                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5997                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5998                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5999
6000                // Compare and conditionally subtract
6001                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6002                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6003                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6004                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6005                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6006
6007                // Subtract and set quotient bit
6008                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6009                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6010                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6011                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6012                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6013
6014                // Decrement and loop
6015                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6016                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6017
6018                let branch_offset_bytes = bytes.len() - loop_start + 4;
6019                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6020                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6021                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6022
6023                // Move quotient to R0:R1
6024                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
6025                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
6026
6027                // If result should be negative (R9 MSB set), negate R0:R1
6028                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9 (check MSB)
6029                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6030                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8 (skip negation)
6031
6032                // Negate result R0:R1
6033                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6034                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6035                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6036                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6037                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6038
6039                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6040                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6041                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6042
6043                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6044                Ok(bytes)
6045            }
6046
6047            // I64RemU: 64-bit unsigned remainder using binary long division
6048            // Same algorithm as I64DivU but returns remainder instead of quotient
6049            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = remainder
6050            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6051            ArmOp::I64RemU {
6052                rdlo,
6053                rdhi,
6054                rnlo,
6055                rnhi,
6056                rmlo,
6057                rmhi,
6058                elide_zero_guard,
6059            } => {
6060                let mut bytes = Vec::new();
6061                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6062                if !elide_zero_guard {
6063                    emit_i64_divisor_zero_trap(&mut bytes);
6064                }
6065
6066                // PUSH {R4-R8} - save scratch registers (NO LR — inline code)
6067                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6068                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6069
6070                // Initialize quotient (R4:R5) = 0 (computed but not returned)
6071                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6072                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6073                // Initialize remainder (R6:R7) = 0
6074                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6075                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6076                // Initialize loop counter R8 = 64
6077                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6078                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6079
6080                let loop_start = bytes.len();
6081
6082                // Shift quotient left (not needed for result, but keeps algorithm same)
6083                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6084                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6085                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6086                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6087
6088                // Shift remainder left, OR in MSB of dividend
6089                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6090                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6091                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6092                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6093                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6094                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6095
6096                // Shift dividend left
6097                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6098                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6099                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6100                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6101
6102                // Compare and conditionally subtract
6103                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6104                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6105                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6106                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6107                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6108
6109                // Subtract and set quotient bit
6110                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6111                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6112                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6113                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6114                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6115
6116                // Decrement and loop
6117                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6118                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6119
6120                let branch_offset_bytes = bytes.len() - loop_start + 4;
6121                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6122                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6123                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6124
6125                // Move REMAINDER to R0:R1 (difference from I64DivU)
6126                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6127                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6128
6129                // POP {R4-R8} - restore scratch registers (NO PC — inline code continues)
6130                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6131                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6132
6133                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6134                Ok(bytes)
6135            }
6136
6137            // I64RemS: 64-bit signed remainder
6138            // Remainder sign follows dividend sign (not quotient rule)
6139            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
6140            //   ->  R0:R1 = remainder (signed, same sign as dividend)
6141            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6142            ArmOp::I64RemS {
6143                rdlo,
6144                rdhi,
6145                rnlo,
6146                rnhi,
6147                rmlo,
6148                rmhi,
6149                elide_zero_guard,
6150            } => {
6151                let mut bytes = Vec::new();
6152                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6153                if !elide_zero_guard {
6154                    emit_i64_divisor_zero_trap(&mut bytes);
6155                }
6156
6157                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
6158                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6159                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6160
6161                // Save dividend sign in R9 (remainder sign = dividend sign)
6162                // MOV R9, R1 (just need the sign bit)
6163                bytes.extend_from_slice(&0x4689u16.to_le_bytes()); // MOV R9, R1
6164
6165                // If dividend negative (R1 MSB set), negate it
6166                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
6167                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6168
6169                // Negate R0:R1
6170                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6171                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6172                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6173                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6174                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6175
6176                // If divisor negative (R3 MSB set), negate it
6177                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
6178                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6179
6180                // Negate R2:R3
6181                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
6182                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
6183                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
6184                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
6185                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
6186
6187                // === Unsigned division algorithm ===
6188                // Initialize quotient (R4:R5) = 0
6189                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6190                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6191                // Initialize remainder (R6:R7) = 0
6192                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6193                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6194                // Initialize loop counter R8 = 64
6195                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6196                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6197
6198                let loop_start = bytes.len();
6199
6200                // Shift quotient left
6201                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6202                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6203                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6204                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6205
6206                // Shift remainder left, OR in MSB of dividend
6207                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6208                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6209                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6210                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6211                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6212                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6213
6214                // Shift dividend left
6215                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6216                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6217                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6218                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6219
6220                // Compare and conditionally subtract
6221                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6222                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6223                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6224                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6225                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6226
6227                // Subtract and set quotient bit
6228                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6229                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6230                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6231                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6232                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6233
6234                // Decrement and loop
6235                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6236                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6237
6238                let branch_offset_bytes = bytes.len() - loop_start + 4;
6239                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6240                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6241                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6242
6243                // Move remainder to R0:R1
6244                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6245                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6246
6247                // If original dividend was negative (R9 MSB set), negate remainder
6248                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9
6249                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6250                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6251
6252                // Negate result R0:R1
6253                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6254                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6255                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6256                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6257                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6258
6259                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6260                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6261                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6262
6263                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6264                Ok(bytes)
6265            }
6266
6267            // === F32 VFP single-precision Thumb-2 encodings ===
6268            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6269            ArmOp::F32Add { sd, sn, sm } => {
6270                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A00, sd, sn, sm)?))
6271            }
6272            ArmOp::F32Sub { sd, sn, sm } => {
6273                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A40, sd, sn, sm)?))
6274            }
6275            ArmOp::F32Mul { sd, sn, sm } => {
6276                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE200A00, sd, sn, sm)?))
6277            }
6278            ArmOp::F32Div { sd, sn, sm } => {
6279                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE800A00, sd, sn, sm)?))
6280            }
6281            ArmOp::F32Abs { sd, sm } => {
6282                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB00AC0, sd, sm)?))
6283            }
6284            ArmOp::F32Neg { sd, sm } => {
6285                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10A40, sd, sm)?))
6286            }
6287            ArmOp::F32Sqrt { sd, sm } => {
6288                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10AC0, sd, sm)?))
6289            }
6290
6291            // f32 pseudo-ops — multi-instruction sequences
6292            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6293            ArmOp::F32Ceil { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b01),
6294            ArmOp::F32Floor { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b10),
6295            ArmOp::F32Trunc { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b11),
6296            ArmOp::F32Nearest { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b00),
6297            ArmOp::F32Min { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, true),
6298            ArmOp::F32Max { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, false),
6299            ArmOp::F32Copysign { sd, sn, sm } => self.encode_thumb_f32_copysign(sd, sn, sm),
6300
6301            // f32 comparisons — VCMP + VMRS + MOV #0 + IT + MOV #1
6302            ArmOp::F32Eq { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x0),
6303            ArmOp::F32Ne { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x1),
6304            ArmOp::F32Lt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x4),
6305            ArmOp::F32Le { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x9),
6306            ArmOp::F32Gt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xC),
6307            ArmOp::F32Ge { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xA),
6308
6309            ArmOp::F32Const { sd, value } => self.encode_thumb_f32_const(sd, *value),
6310
6311            ArmOp::F32Load { sd, addr } => {
6312                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED900A00, sd, addr)?))
6313            }
6314            ArmOp::F32Store { sd, addr } => {
6315                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED800A00, sd, addr)?))
6316            }
6317
6318            ArmOp::F32ConvertI32S { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, true),
6319            ArmOp::F32ConvertI32U { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, false),
6320            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
6321                Err(synth_core::Error::synthesis(
6322                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6323                ))
6324            }
6325            ArmOp::F32ReinterpretI32 { sd, rm } => {
6326                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(true, sd, rm)?))
6327            }
6328            ArmOp::I32ReinterpretF32 { rd, sm } => {
6329                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(false, sm, rd)?))
6330            }
6331            ArmOp::I32TruncF32S { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, true),
6332            ArmOp::I32TruncF32U { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, false),
6333
6334            // === F64 VFP double-precision Thumb-2 encodings ===
6335            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6336            ArmOp::F64Add { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6337                0xEE300B00, dd, dn, dm,
6338            )?)),
6339            ArmOp::F64Sub { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6340                0xEE300B40, dd, dn, dm,
6341            )?)),
6342            ArmOp::F64Mul { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6343                0xEE200B00, dd, dn, dm,
6344            )?)),
6345            ArmOp::F64Div { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6346                0xEE800B00, dd, dn, dm,
6347            )?)),
6348            ArmOp::F64Abs { dd, dm } => {
6349                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?))
6350            }
6351            ArmOp::F64Neg { dd, dm } => {
6352                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?))
6353            }
6354            ArmOp::F64Sqrt { dd, dm } => {
6355                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?))
6356            }
6357
6358            // f64 pseudo-ops
6359            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6360            ArmOp::F64Ceil { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b01),
6361            ArmOp::F64Floor { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b10),
6362            ArmOp::F64Trunc { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b11),
6363            ArmOp::F64Nearest { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b00),
6364            ArmOp::F64Min { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, true),
6365            ArmOp::F64Max { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, false),
6366            ArmOp::F64Copysign { dd, dn, dm } => self.encode_thumb_f64_copysign(dd, dn, dm),
6367
6368            // f64 comparisons
6369            ArmOp::F64Eq { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x0),
6370            ArmOp::F64Ne { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x1),
6371            ArmOp::F64Lt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x4),
6372            ArmOp::F64Le { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x9),
6373            ArmOp::F64Gt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xC),
6374            ArmOp::F64Ge { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xA),
6375
6376            ArmOp::F64Const { dd, value } => self.encode_thumb_f64_const(dd, *value),
6377
6378            ArmOp::F64Load { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6379                0xED900B00, dd, addr,
6380            )?)),
6381            ArmOp::F64Store { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6382                0xED800B00, dd, addr,
6383            )?)),
6384
6385            ArmOp::F64ConvertI32S { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, true),
6386            ArmOp::F64ConvertI32U { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, false),
6387            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
6388                Err(synth_core::Error::synthesis(
6389                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6390                ))
6391            }
6392            ArmOp::F64PromoteF32 { dd, sm } => self.encode_thumb_f64_promote_f32(dd, sm),
6393            ArmOp::F32DemoteF64 { sd, dm } => self.encode_thumb_f32_demote_f64(sd, dm),
6394            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => Ok(vfp_to_thumb_bytes(
6395                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?,
6396            )),
6397            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => Ok(vfp_to_thumb_bytes(
6398                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?,
6399            )),
6400            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
6401                Err(synth_core::Error::synthesis(
6402                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
6403                ))
6404            }
6405            ArmOp::I32TruncF64S { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, true),
6406            ArmOp::I32TruncF64U { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, false),
6407
6408            // ===== i64 operations: encode as multi-instruction Thumb-2 sequences =====
6409
6410            // I64Add: ADDS rdlo, rnlo, rmlo; ADC.W rdhi, rnhi, rmhi
6411            ArmOp::I64Add {
6412                rdlo,
6413                rdhi,
6414                rnlo,
6415                rnhi,
6416                rmlo,
6417                rmhi,
6418            } => {
6419                let mut bytes = Vec::new();
6420                // ADDS rdlo, rnlo, rmlo (16-bit)
6421                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adds {
6422                    rd: *rdlo,
6423                    rn: *rnlo,
6424                    op2: Operand2::Reg(*rmlo),
6425                })?);
6426                // ADC.W rdhi, rnhi, rmhi (32-bit)
6427                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adc {
6428                    rd: *rdhi,
6429                    rn: *rnhi,
6430                    op2: Operand2::Reg(*rmhi),
6431                })?);
6432                Ok(bytes)
6433            }
6434
6435            // I64Sub: SUBS rdlo, rnlo, rmlo; SBC.W rdhi, rnhi, rmhi
6436            ArmOp::I64Sub {
6437                rdlo,
6438                rdhi,
6439                rnlo,
6440                rnhi,
6441                rmlo,
6442                rmhi,
6443            } => {
6444                let mut bytes = Vec::new();
6445                // SUBS rdlo, rnlo, rmlo (16-bit)
6446                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Subs {
6447                    rd: *rdlo,
6448                    rn: *rnlo,
6449                    op2: Operand2::Reg(*rmlo),
6450                })?);
6451                // SBC.W rdhi, rnhi, rmhi (32-bit)
6452                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Sbc {
6453                    rd: *rdhi,
6454                    rn: *rnhi,
6455                    op2: Operand2::Reg(*rmhi),
6456                })?);
6457                Ok(bytes)
6458            }
6459
6460            // I64And: AND rdlo, rnlo, rmlo; AND rdhi, rnhi, rmhi
6461            ArmOp::I64And {
6462                rdlo,
6463                rdhi,
6464                rnlo,
6465                rnhi,
6466                rmlo,
6467                rmhi,
6468            } => {
6469                let mut bytes = Vec::new();
6470                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6471                    rd: *rdlo,
6472                    rn: *rnlo,
6473                    op2: Operand2::Reg(*rmlo),
6474                })?);
6475                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6476                    rd: *rdhi,
6477                    rn: *rnhi,
6478                    op2: Operand2::Reg(*rmhi),
6479                })?);
6480                Ok(bytes)
6481            }
6482
6483            // I64Or: ORR rdlo, rnlo, rmlo; ORR rdhi, rnhi, rmhi
6484            ArmOp::I64Or {
6485                rdlo,
6486                rdhi,
6487                rnlo,
6488                rnhi,
6489                rmlo,
6490                rmhi,
6491            } => {
6492                let mut bytes = Vec::new();
6493                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6494                    rd: *rdlo,
6495                    rn: *rnlo,
6496                    op2: Operand2::Reg(*rmlo),
6497                })?);
6498                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6499                    rd: *rdhi,
6500                    rn: *rnhi,
6501                    op2: Operand2::Reg(*rmhi),
6502                })?);
6503                Ok(bytes)
6504            }
6505
6506            // I64Xor: EOR rdlo, rnlo, rmlo; EOR rdhi, rnhi, rmhi
6507            ArmOp::I64Xor {
6508                rdlo,
6509                rdhi,
6510                rnlo,
6511                rnhi,
6512                rmlo,
6513                rmhi,
6514            } => {
6515                let mut bytes = Vec::new();
6516                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6517                    rd: *rdlo,
6518                    rn: *rnlo,
6519                    op2: Operand2::Reg(*rmlo),
6520                })?);
6521                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6522                    rd: *rdhi,
6523                    rn: *rnhi,
6524                    op2: Operand2::Reg(*rmhi),
6525                })?);
6526                Ok(bytes)
6527            }
6528
6529            // I64Eqz: ORR scratch, lo, hi; ITE EQ; MOV rd, #1; MOV rd, #0
6530            ArmOp::I64Eqz { rd, rnlo, rnhi } => self.encode_thumb(&ArmOp::I64SetCondZ {
6531                rd: *rd,
6532                rn_lo: *rnlo,
6533                rn_hi: *rnhi,
6534            }),
6535
6536            // I64 comparisons: delegate to I64SetCond
6537            ArmOp::I64Eq {
6538                rd,
6539                rnlo,
6540                rnhi,
6541                rmlo,
6542                rmhi,
6543            } => self.encode_thumb(&ArmOp::I64SetCond {
6544                rd: *rd,
6545                rn_lo: *rnlo,
6546                rn_hi: *rnhi,
6547                rm_lo: *rmlo,
6548                rm_hi: *rmhi,
6549                cond: synth_synthesis::Condition::EQ,
6550            }),
6551
6552            ArmOp::I64Ne {
6553                rd,
6554                rnlo,
6555                rnhi,
6556                rmlo,
6557                rmhi,
6558            } => self.encode_thumb(&ArmOp::I64SetCond {
6559                rd: *rd,
6560                rn_lo: *rnlo,
6561                rn_hi: *rnhi,
6562                rm_lo: *rmlo,
6563                rm_hi: *rmhi,
6564                cond: synth_synthesis::Condition::NE,
6565            }),
6566
6567            ArmOp::I64LtS {
6568                rd,
6569                rnlo,
6570                rnhi,
6571                rmlo,
6572                rmhi,
6573            } => self.encode_thumb(&ArmOp::I64SetCond {
6574                rd: *rd,
6575                rn_lo: *rnlo,
6576                rn_hi: *rnhi,
6577                rm_lo: *rmlo,
6578                rm_hi: *rmhi,
6579                cond: synth_synthesis::Condition::LT,
6580            }),
6581
6582            ArmOp::I64LtU {
6583                rd,
6584                rnlo,
6585                rnhi,
6586                rmlo,
6587                rmhi,
6588            } => self.encode_thumb(&ArmOp::I64SetCond {
6589                rd: *rd,
6590                rn_lo: *rnlo,
6591                rn_hi: *rnhi,
6592                rm_lo: *rmlo,
6593                rm_hi: *rmhi,
6594                cond: synth_synthesis::Condition::LO,
6595            }),
6596
6597            ArmOp::I64LeS {
6598                rd,
6599                rnlo,
6600                rnhi,
6601                rmlo,
6602                rmhi,
6603            } => self.encode_thumb(&ArmOp::I64SetCond {
6604                rd: *rd,
6605                rn_lo: *rnlo,
6606                rn_hi: *rnhi,
6607                rm_lo: *rmlo,
6608                rm_hi: *rmhi,
6609                cond: synth_synthesis::Condition::LE,
6610            }),
6611
6612            ArmOp::I64LeU {
6613                rd,
6614                rnlo,
6615                rnhi,
6616                rmlo,
6617                rmhi,
6618            } => self.encode_thumb(&ArmOp::I64SetCond {
6619                rd: *rd,
6620                rn_lo: *rnlo,
6621                rn_hi: *rnhi,
6622                rm_lo: *rmlo,
6623                rm_hi: *rmhi,
6624                cond: synth_synthesis::Condition::LS,
6625            }),
6626
6627            ArmOp::I64GtS {
6628                rd,
6629                rnlo,
6630                rnhi,
6631                rmlo,
6632                rmhi,
6633            } => self.encode_thumb(&ArmOp::I64SetCond {
6634                rd: *rd,
6635                rn_lo: *rnlo,
6636                rn_hi: *rnhi,
6637                rm_lo: *rmlo,
6638                rm_hi: *rmhi,
6639                cond: synth_synthesis::Condition::GT,
6640            }),
6641
6642            ArmOp::I64GtU {
6643                rd,
6644                rnlo,
6645                rnhi,
6646                rmlo,
6647                rmhi,
6648            } => self.encode_thumb(&ArmOp::I64SetCond {
6649                rd: *rd,
6650                rn_lo: *rnlo,
6651                rn_hi: *rnhi,
6652                rm_lo: *rmlo,
6653                rm_hi: *rmhi,
6654                cond: synth_synthesis::Condition::HI,
6655            }),
6656
6657            ArmOp::I64GeS {
6658                rd,
6659                rnlo,
6660                rnhi,
6661                rmlo,
6662                rmhi,
6663            } => self.encode_thumb(&ArmOp::I64SetCond {
6664                rd: *rd,
6665                rn_lo: *rnlo,
6666                rn_hi: *rnhi,
6667                rm_lo: *rmlo,
6668                rm_hi: *rmhi,
6669                cond: synth_synthesis::Condition::GE,
6670            }),
6671
6672            ArmOp::I64GeU {
6673                rd,
6674                rnlo,
6675                rnhi,
6676                rmlo,
6677                rmhi,
6678            } => self.encode_thumb(&ArmOp::I64SetCond {
6679                rd: *rd,
6680                rn_lo: *rnlo,
6681                rn_hi: *rnhi,
6682                rm_lo: *rmlo,
6683                rm_hi: *rmhi,
6684                cond: synth_synthesis::Condition::HS,
6685            }),
6686
6687            // I64Const: MOVW rdlo, lo16; MOVT rdlo, hi16; MOVW rdhi, lo16_hi; MOVT rdhi, hi16_hi
6688            ArmOp::I64Const { rdlo, rdhi, value } => {
6689                let lo32 = *value as u32;
6690                let hi32 = (*value >> 32) as u32;
6691                let mut bytes = Vec::new();
6692                // Load low 32 bits into rdlo
6693                bytes.extend_from_slice(
6694                    &self.encode_thumb32_movw_raw(reg_to_bits(rdlo), lo32 & 0xFFFF)?,
6695                );
6696                if lo32 > 0xFFFF {
6697                    bytes.extend_from_slice(
6698                        &self.encode_thumb32_movt_raw(reg_to_bits(rdlo), lo32 >> 16)?,
6699                    );
6700                }
6701                // Load high 32 bits into rdhi
6702                bytes.extend_from_slice(
6703                    &self.encode_thumb32_movw_raw(reg_to_bits(rdhi), hi32 & 0xFFFF)?,
6704                );
6705                if hi32 > 0xFFFF {
6706                    bytes.extend_from_slice(
6707                        &self.encode_thumb32_movt_raw(reg_to_bits(rdhi), hi32 >> 16)?,
6708                    );
6709                }
6710                Ok(bytes)
6711            }
6712
6713            // I64Ldr: LDR rdlo, [base, offset]; LDR rdhi, [base, offset+4]
6714            ArmOp::I64Ldr { rdlo, rdhi, addr } => {
6715                let mut bytes = Vec::new();
6716                // #372/#382: a memory `i64.load` carries an index register
6717                // (`reg_imm(R11, addr_reg, offset)` = R11 + addr + offset). The
6718                // immediate `encode_thumb32_ldr` below uses only base+offset and
6719                // would SILENTLY DROP `offset_reg` — the #206 defect, here for
6720                // i64. `i64_effective_base` materializes the effective base into
6721                // `ip` (and, when `offset+4 > 0xFFF`, folds the offset in too so
6722                // the function is NOT skipped — #382), returning the residual
6723                // imm12 for the two halves. Frame i64 loads (no `offset_reg`, e.g.
6724                // a spilled local at `[SP, #off]`) keep the plain `[base,#off]`
6725                // form unchanged — so existing output is byte-identical.
6726                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6727                bytes.extend_from_slice(&self.encode_thumb32_ldr(rdlo, &base, offset)?);
6728                bytes.extend_from_slice(&self.encode_thumb32_ldr(
6729                    rdhi,
6730                    &base,
6731                    offset.wrapping_add(4),
6732                )?);
6733                Ok(bytes)
6734            }
6735
6736            // I64Str: STR rdlo, [base, offset]; STR rdhi, [base, offset+4]
6737            ArmOp::I64Str { rdlo, rdhi, addr } => {
6738                let mut bytes = Vec::new();
6739                // #372/#382: same index-materialization + large-offset fold as
6740                // I64Ldr (see above).
6741                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6742                bytes.extend_from_slice(&self.encode_thumb32_str(rdlo, &base, offset)?);
6743                bytes.extend_from_slice(&self.encode_thumb32_str(
6744                    rdhi,
6745                    &base,
6746                    offset.wrapping_add(4),
6747                )?);
6748                Ok(bytes)
6749            }
6750
6751            // I64ExtendI32S: MOV rdlo, rn; ASR rdhi, rdlo, #31 (sign-extend)
6752            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
6753                let mut bytes = Vec::new();
6754                if rdlo != rn {
6755                    // MOV rdlo, rn (16-bit)
6756                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6757                        rd: *rdlo,
6758                        op2: Operand2::Reg(*rn),
6759                    })?);
6760                }
6761                // ASR rdhi, rdlo, #31 (sign-extend: fill high word with sign bit)
6762                bytes.extend_from_slice(
6763                    &self.encode_thumb32_shift(rdhi, rdlo, 31, 0b10)?, // ASR type
6764                );
6765                Ok(bytes)
6766            }
6767
6768            // I64ExtendI32U: MOV rdlo, rn; MOV rdhi, #0
6769            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
6770                let mut bytes = Vec::new();
6771                if rdlo != rn {
6772                    // MOV rdlo, rn
6773                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6774                        rd: *rdlo,
6775                        op2: Operand2::Reg(*rn),
6776                    })?);
6777                }
6778                // MOV rdhi, #0 (#916: MOV.W for rdhi >= R8). Unconditional
6779                // site with no branches in the expansion — before the fix this
6780                // emitted the literal two-instruction stream [4608, 2800], half
6781                // of which was `CMP r0,#0` rather than the high-word clear, so
6782                // every i64.extend_i32_u into a high pair leaked stale bits.
6783                emit_thumb_zero_fill(&mut bytes, reg_to_bits(rdhi));
6784                Ok(bytes)
6785            }
6786
6787            // I32WrapI64: MOV rd, rnlo (just take low 32 bits)
6788            ArmOp::I32WrapI64 { rd, rnlo } => {
6789                if rd == rnlo {
6790                    // No-op: already in the right register
6791                    let instr: u16 = 0xBF00; // NOP
6792                    Ok(instr.to_le_bytes().to_vec())
6793                } else {
6794                    // MOV rd, rnlo
6795                    self.encode_thumb(&ArmOp::Mov {
6796                        rd: *rd,
6797                        op2: Operand2::Reg(*rnlo),
6798                    })
6799                }
6800            }
6801
6802            // ===== Helium MVE operations (Thumb-2 encoding) =====
6803            ArmOp::MveLoad { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vldrw(qd, addr))),
6804            ArmOp::MveStore { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vstrw(qd, addr))),
6805            ArmOp::MveConst { qd, bytes } => self.encode_thumb_mve_const(qd, bytes),
6806            ArmOp::MveAnd { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6807                0xEF000150, qd, qn, qm,
6808            ))),
6809            ArmOp::MveOrr { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6810                0xEF200150, qd, qn, qm,
6811            ))),
6812            ArmOp::MveEor { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6813                0xFF000150, qd, qn, qm,
6814            ))),
6815            ArmOp::MveMvn { qd, qm } => {
6816                // VMVN Qd, Qm: 0xFFB005C0 | Qd<<12 | Qm
6817                let qd_enc = qreg_to_num(qd);
6818                let qm_enc = qreg_to_num(qm);
6819                let instr: u32 = 0xFFB005C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6820                Ok(vfp_to_thumb_bytes(instr))
6821            }
6822            ArmOp::MveBic { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6823                0xEF100150, qd, qn, qm,
6824            ))),
6825            ArmOp::MveAddI { qd, qn, qm, size } => {
6826                let sz = mve_size_bits(size);
6827                let base: u32 = 0xEF000840 | (sz << 20);
6828                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6829            }
6830            ArmOp::MveSubI { qd, qn, qm, size } => {
6831                let sz = mve_size_bits(size);
6832                let base: u32 = 0xFF000840 | (sz << 20);
6833                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6834            }
6835            ArmOp::MveMulI { qd, qn, qm, size } => {
6836                let sz = mve_size_bits(size);
6837                let base: u32 = 0xEF000950 | (sz << 20);
6838                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6839            }
6840            ArmOp::MveNegI { qd, qm, size } => {
6841                let sz = mve_size_bits(size);
6842                // VNEG.Sx Qd, Qm
6843                let qd_enc = qreg_to_num(qd);
6844                let qm_enc = qreg_to_num(qm);
6845                let base: u32 = 0xFFB103C0 | (sz << 18);
6846                let instr = base | ((qd_enc * 2) << 12) | (qm_enc * 2);
6847                Ok(vfp_to_thumb_bytes(instr))
6848            }
6849            ArmOp::MveDup { qd, rn, size } => {
6850                let sz = mve_size_bits(size);
6851                let qd_enc = qreg_to_num(qd);
6852                let rn_bits = reg_to_bits(rn);
6853                // VDUP.sz Qd, Rn: EEA0 0B10 variant
6854                // size encoding: 00=32, 01=16, 10=8
6855                let be = match sz {
6856                    0 => 0b00u32, // 8-bit
6857                    1 => 0b01,    // 16-bit
6858                    _ => 0b00,    // 32-bit (default)
6859                };
6860                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12) | (be << 5);
6861                Ok(vfp_to_thumb_bytes(instr))
6862            }
6863            ArmOp::MveExtractLane { rd, qn, lane, size } => {
6864                let qn_enc = qreg_to_num(qn);
6865                let rd_bits = reg_to_bits(rd);
6866                // VMOV.sz Rd, Dn[x] — extract from Q-register lane
6867                // For 32-bit: VMOV Rd, Dn — where Dn is the appropriate D-register
6868                let d_reg = qn_enc * 2 + ((*lane as u32) >> 1);
6869                let lane_in_d = (*lane as u32) & 1;
6870                let _sz = mve_size_bits(size);
6871                // VMOV Rd, Dn[x]: EE10 0B10 for 32-bit
6872                let instr: u32 = 0xEE100B10 | (d_reg << 16) | (rd_bits << 12) | (lane_in_d << 21);
6873                Ok(vfp_to_thumb_bytes(instr))
6874            }
6875            ArmOp::MveInsertLane { qd, rn, lane, size } => {
6876                let qd_enc = qreg_to_num(qd);
6877                let rn_bits = reg_to_bits(rn);
6878                let d_reg = qd_enc * 2 + ((*lane as u32) >> 1);
6879                let lane_in_d = (*lane as u32) & 1;
6880                let _sz = mve_size_bits(size);
6881                // VMOV Dn[x], Rn: EE00 0B10 for 32-bit
6882                let instr: u32 = 0xEE000B10 | (d_reg << 16) | (rn_bits << 12) | (lane_in_d << 21);
6883                Ok(vfp_to_thumb_bytes(instr))
6884            }
6885
6886            // MVE float comparisons — emit VCMP + VPSEL sequence (simplified: just VCMP)
6887            ArmOp::MveCmpEqI { qd, qn, qm, size }
6888            | ArmOp::MveCmpNeI { qd, qn, qm, size }
6889            | ArmOp::MveCmpLtS { qd, qn, qm, size }
6890            | ArmOp::MveCmpLtU { qd, qn, qm, size }
6891            | ArmOp::MveCmpGtS { qd, qn, qm, size }
6892            | ArmOp::MveCmpGtU { qd, qn, qm, size }
6893            | ArmOp::MveCmpLeS { qd, qn, qm, size }
6894            | ArmOp::MveCmpLeU { qd, qn, qm, size }
6895            | ArmOp::MveCmpGeS { qd, qn, qm, size }
6896            | ArmOp::MveCmpGeU { qd, qn, qm, size } => {
6897                // Encode as VADD (placeholder encoding — real implementation
6898                // would use VCMP + VPSEL pair)
6899                let sz = mve_size_bits(size);
6900                let base: u32 = 0xEF000840 | (sz << 20);
6901                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6902            }
6903
6904            // f32x4 MVE arithmetic
6905            ArmOp::MveAddF32 { qd, qn, qm } => {
6906                // VADD.F32 Qd, Qn, Qm (MVE): 0xEF000D40
6907                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6908            }
6909            ArmOp::MveSubF32 { qd, qn, qm } => {
6910                // VSUB.F32 Qd, Qn, Qm (MVE): 0xEF200D40
6911                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF200D40, qd, qn, qm)))
6912            }
6913            ArmOp::MveMulF32 { qd, qn, qm } => {
6914                // VMUL.F32 Qd, Qn, Qm (MVE): 0xFF000D50
6915                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xFF000D50, qd, qn, qm)))
6916            }
6917            ArmOp::MveNegF32 { qd, qm } => {
6918                let qd_enc = qreg_to_num(qd);
6919                let qm_enc = qreg_to_num(qm);
6920                // VNEG.F32 Qd, Qm: FFB907C0
6921                let instr: u32 = 0xFFB907C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6922                Ok(vfp_to_thumb_bytes(instr))
6923            }
6924            ArmOp::MveAbsF32 { qd, qm } => {
6925                let qd_enc = qreg_to_num(qd);
6926                let qm_enc = qreg_to_num(qm);
6927                // VABS.F32 Qd, Qm: FFB90740
6928                let instr: u32 = 0xFFB90740 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6929                Ok(vfp_to_thumb_bytes(instr))
6930            }
6931            ArmOp::MveCmpEqF32 { qd, qn, qm }
6932            | ArmOp::MveCmpNeF32 { qd, qn, qm }
6933            | ArmOp::MveCmpLtF32 { qd, qn, qm }
6934            | ArmOp::MveCmpLeF32 { qd, qn, qm }
6935            | ArmOp::MveCmpGtF32 { qd, qn, qm }
6936            | ArmOp::MveCmpGeF32 { qd, qn, qm } => {
6937                // Placeholder: encode as VADD.F32 (real impl needs VCMP.F32 + VPSEL)
6938                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6939            }
6940            ArmOp::MveDupF32 { qd, rn } => {
6941                let qd_enc = qreg_to_num(qd);
6942                let rn_bits = reg_to_bits(rn);
6943                // VDUP.32 Qd, Rn (same encoding as integer VDUP.32)
6944                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12);
6945                Ok(vfp_to_thumb_bytes(instr))
6946            }
6947            ArmOp::MveExtractLaneF32 { rd, qn, lane } => {
6948                let qn_enc = qreg_to_num(qn);
6949                let rd_bits = reg_to_bits(rd);
6950                // VMOV Rd, Sn where Sn = Q*4 + lane
6951                let s_num = qn_enc * 4 + (*lane as u32);
6952                let (vn, n) = encode_sreg(s_num);
6953                let instr: u32 = 0xEE100A10 | (vn << 16) | (rd_bits << 12) | (n << 7);
6954                Ok(vfp_to_thumb_bytes(instr))
6955            }
6956            ArmOp::MveReplaceLaneF32 { qd, rn, lane } => {
6957                let qd_enc = qreg_to_num(qd);
6958                let rn_bits = reg_to_bits(rn);
6959                // VMOV Sn, Rn where Sn = Q*4 + lane
6960                let s_num = qd_enc * 4 + (*lane as u32);
6961                let (vn, n) = encode_sreg(s_num);
6962                let instr: u32 = 0xEE000A10 | (vn << 16) | (rn_bits << 12) | (n << 7);
6963                Ok(vfp_to_thumb_bytes(instr))
6964            }
6965            ArmOp::MveDivF32 { qd, qn, qm } => {
6966                // Lane-wise: extract 4 S-regs, VDIV, insert back
6967                self.encode_thumb_mve_lane_wise_f32_binop(qd, qn, qm, 0xEE800A00)
6968            }
6969            ArmOp::MveSqrtF32 { qd, qm } => {
6970                // Lane-wise: extract 4 S-regs, VSQRT, insert back
6971                self.encode_thumb_mve_lane_wise_f32_sqrt(qd, qm)
6972            }
6973
6974            // Catch-all for any remaining ops
6975            _ => {
6976                let instr: u16 = 0xBF00; // NOP
6977                Ok(instr.to_le_bytes().to_vec())
6978            }
6979        }
6980    }
6981
6982    // === Thumb-2 VFP multi-instruction helpers ===
6983
6984    /// Encode F32 comparison as Thumb-2: VCMP.F32 + VMRS + MOVS rd,#0 + IT + MOV rd,#1
6985    fn encode_thumb_f32_compare(
6986        &self,
6987        rd: &Reg,
6988        sn: &VfpReg,
6989        sm: &VfpReg,
6990        cond_code: u32,
6991    ) -> Result<Vec<u8>> {
6992        let mut bytes = Vec::new();
6993        let rd_bits = reg_to_bits(rd);
6994
6995        // #709 (bug found under #708/#709): the `MOVS Rd,#0` below is a
6996        // FLAG-SETTING 16-bit move. Emitting it AFTER `VMRS APSR_nzcv, FPSCR`
6997        // (as the original code did) clobbered the N/Z/C/V flags the VMRS just
6998        // transferred from the VFP compare, so the following `IT<cond>` read
6999        // stale flags and every f32 comparison silently returned 0 (verified:
7000        // `flt(1.0,2.0)` → 0 on Cortex-M4F). The 619 harness never caught it
7001        // because it deliberately skipped compare EXECUTION on a false premise
7002        // (unicorn DOES model VMRS→APSR). Fix: materialize the `#0` FIRST, then
7003        // VCMP+VMRS set the flags the `IT` consumes. Instruction sizes are
7004        // unchanged (pure reorder), so the estimator↔encoder oracle (#511) is
7005        // untouched — only the byte ORDER differs.
7006
7007        // MOVS Rd, #0 (16-bit): 0010 0 Rd(3) 0000 0000 — its flag side effect
7008        // is immediately overwritten by the VMRS below.
7009        if rd_bits < 8 {
7010            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
7011            bytes.extend_from_slice(&movs_zero.to_le_bytes());
7012        } else {
7013            // MOV.W Rd, #0 (32-bit Thumb-2)
7014            let hw1: u16 = 0xF04F;
7015            let hw2: u16 = (rd_bits as u16) << 8;
7016            bytes.extend_from_slice(&hw1.to_le_bytes());
7017            bytes.extend_from_slice(&hw2.to_le_bytes());
7018        }
7019
7020        // VCMP.F32 Sn, Sm
7021        let sn_num = vfp_sreg_to_num(sn)?;
7022        let sm_num = vfp_sreg_to_num(sm)?;
7023        let (vd, d) = encode_sreg(sn_num);
7024        let (vm, m) = encode_sreg(sm_num);
7025        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7026        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7027
7028        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10 (sets the flags IT consumes)
7029        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7030
7031        // IT<cond> — If-Then for conditional MOV
7032        // IT encoding: 1011 1111 cond(4) mask(4)
7033        // mask = 0x8 for single "then" (IT)
7034        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7035        bytes.extend_from_slice(&it.to_le_bytes());
7036
7037        // MOV Rd, #1 (16-bit, conditional due to IT): 0010 0 Rd(3) 0000 0001
7038        if rd_bits < 8 {
7039            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7040            bytes.extend_from_slice(&mov_one.to_le_bytes());
7041        } else {
7042            // MOV.W Rd, #1 (32-bit)
7043            let hw1: u16 = 0xF04F;
7044            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7045            bytes.extend_from_slice(&hw1.to_le_bytes());
7046            bytes.extend_from_slice(&hw2.to_le_bytes());
7047        }
7048
7049        Ok(bytes)
7050    }
7051
7052    /// Encode F32 constant load as Thumb-2: MOVW + MOVT + VMOV
7053    fn encode_thumb_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
7054        let mut bytes = Vec::new();
7055        let bits = value.to_bits();
7056        let rt: u32 = 12; // R12/IP as temp
7057
7058        // MOVW R12, #lo16
7059        // Thumb-2 MOVW: 11110 i 10 0100 imm4 | 0 imm3 Rd imm8
7060        let lo16 = bits & 0xFFFF;
7061        let imm4 = (lo16 >> 12) & 0xF;
7062        let i_bit = (lo16 >> 11) & 1;
7063        let imm3 = (lo16 >> 8) & 0x7;
7064        let imm8 = lo16 & 0xFF;
7065        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7066        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
7067        bytes.extend_from_slice(&hw1.to_le_bytes());
7068        bytes.extend_from_slice(&hw2.to_le_bytes());
7069
7070        // MOVT R12, #hi16
7071        let hi16 = (bits >> 16) & 0xFFFF;
7072        let imm4 = (hi16 >> 12) & 0xF;
7073        let i_bit = (hi16 >> 11) & 1;
7074        let imm3 = (hi16 >> 8) & 0x7;
7075        let imm8 = hi16 & 0xFF;
7076        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
7077        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
7078        bytes.extend_from_slice(&hw1.to_le_bytes());
7079        bytes.extend_from_slice(&hw2.to_le_bytes());
7080
7081        // VMOV Sd, R12
7082        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
7083        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7084
7085        Ok(bytes)
7086    }
7087
7088    /// Encode VMOV + VCVT.F32.xS32 as Thumb-2
7089    fn encode_thumb_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7090        let mut bytes = Vec::new();
7091
7092        // VMOV Sd, Rm
7093        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
7094        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7095
7096        // VCVT.F32.S32/U32 Sd, Sd. Bit 7 (op) = 1 for signed (S32), 0 for
7097        // unsigned (U32): signed = 0xEEB80AC0, unsigned = 0xEEB80A40
7098        // (GI-FPU-002: previously swapped — see the ARM32 twin).
7099        let sd_num = vfp_sreg_to_num(sd)?;
7100        let (vd, d) = encode_sreg(sd_num);
7101        let (vm, m) = encode_sreg(sd_num);
7102        let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
7103        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7104        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7105
7106        Ok(bytes)
7107    }
7108
7109    /// Encode F32 rounding pseudo-op as Thumb-2 via VCVT to integer and back
7110    /// Encode F32 rounding as Thumb-2.
7111    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
7112    ///
7113    /// For trunc: uses VCVTR.S32.F32 (always truncates).
7114    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant),
7115    /// then restores FPSCR.
7116    fn encode_thumb_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7117        let mut bytes = Vec::new();
7118        let sm_num = vfp_sreg_to_num(sm)?;
7119        let sd_num = vfp_sreg_to_num(sd)?;
7120        let (vd_s, d_s) = encode_sreg(sd_num);
7121        let (vm_s, m_s) = encode_sreg(sm_num);
7122
7123        if mode == 0b11 {
7124            // Trunc (toward zero): VCVTR.S32.F32 — bit[7]=1, always truncates
7125            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7126            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7127        } else {
7128            // ceil/floor/nearest: manipulate FPSCR rounding mode
7129            let rt: u32 = 12; // R12/IP as temp
7130
7131            // VMRS R12, FPSCR
7132            let vmrs = 0xEEF10A10 | (rt << 12);
7133            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7134
7135            // BIC.W R12, R12, #(3 << 22) — clear RMode bits [23:22]
7136            // Thumb-2 modified immediate for 3<<22 = 0x00C00000:
7137            // BIC.W encoding: 11110 i 0 0001 S Rn | 0 imm3 Rd imm8
7138            // 0x00C00000 = 0x03 shifted left by 22 => Thumb mod-imm: i=0, imm3=0b101, imm8=0x03
7139            let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF); // BIC, Rn=R12
7140            let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
7141            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7142            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7143
7144            // ORR.W R12, R12, #(mode << 22)
7145            if mode != 0 {
7146                let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF); // ORR, Rn=R12
7147                let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
7148                bytes.extend_from_slice(&orr_hw1.to_le_bytes());
7149                bytes.extend_from_slice(&orr_hw2.to_le_bytes());
7150            }
7151
7152            // VMSR FPSCR, R12
7153            let vmsr = 0xEEE10A10 | (rt << 12);
7154            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7155
7156            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rmode
7157            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7158            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7159
7160            // Restore FPSCR: clear rmode bits back to nearest (default)
7161            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7162            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7163            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7164            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7165        }
7166
7167        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
7168        let (vd2, d2) = encode_sreg(sd_num);
7169        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
7170        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
7171
7172        Ok(bytes)
7173    }
7174
7175    /// Encode F32 min/max as Thumb-2: VMOV + VCMP + VMRS + IT + VMOV
7176    fn encode_thumb_f32_minmax(
7177        &self,
7178        sd: &VfpReg,
7179        sn: &VfpReg,
7180        sm: &VfpReg,
7181        is_min: bool,
7182    ) -> Result<Vec<u8>> {
7183        let mut bytes = Vec::new();
7184        let sn_num = vfp_sreg_to_num(sn)?;
7185        let sm_num = vfp_sreg_to_num(sm)?;
7186        let sd_num = vfp_sreg_to_num(sd)?;
7187
7188        // VMOV.F32 Sd, Sn
7189        let (vd, d) = encode_sreg(sd_num);
7190        let (vn, n) = encode_sreg(sn_num);
7191        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
7192        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sn));
7193
7194        // VCMP.F32 Sn, Sm
7195        let (vm, m) = encode_sreg(sm_num);
7196        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7197        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7198
7199        // VMRS APSR_nzcv, FPSCR
7200        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7201
7202        // IT GT (for min) or IT MI (for max)
7203        let cond: u16 = if is_min { 0xC } else { 0x4 };
7204        let it: u16 = 0xBF00 | (cond << 4) | 0x8;
7205        bytes.extend_from_slice(&it.to_le_bytes());
7206
7207        // VMOV{cond}.F32 Sd, Sm — conditional VMOV in IT block
7208        let vmov_sm = 0xEEB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7209        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sm));
7210
7211        Ok(bytes)
7212    }
7213
7214    /// Encode F32 copysign as Thumb-2
7215    /// Encode F32 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7216    /// encoder scratch, #212), the flags, and Sd:
7217    ///
7218    ///   VMOV R12, Sm ; CMP R12, #0    (N flag = the sign bit)
7219    ///   VABS.F32 Sd, Sn               (magnitude, sign cleared)
7220    ///   IT MI ; VNEG.F32(MI) Sd, Sd
7221    ///
7222    /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7223    /// The R12 capture happens BEFORE Sd is written, so Sd aliasing Sn or Sm
7224    /// is safe. (The previous sequence staged the magnitude through R0 —
7225    /// clobbering a live allocator-owned value, the #615 class; caught while
7226    /// composing the F64 twin for #369.)
7227    fn encode_thumb_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7228        let mut bytes = Vec::new();
7229
7230        // VMOV R12, Sm (sign source bits)
7231        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
7232            false,
7233            sm,
7234            &Reg::R12,
7235        )?));
7236        // CMP.W R12, #0 — N = bit31 (the sign, incl. -0.0 / -NaN).
7237        bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7238        bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7239        // VABS.F32 Sd, Sn
7240        let sd_num = vfp_sreg_to_num(sd)?;
7241        let sn_num = vfp_sreg_to_num(sn)?;
7242        let (vd, d) = encode_sreg(sd_num);
7243        let (vn, n) = encode_sreg(sn_num);
7244        let vabs = 0xEEB00AC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7245        bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7246        // IT MI ; VNEG.F32(MI) Sd, Sd
7247        bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7248        let vneg = 0xEEB10A40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7249        bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7250
7251        Ok(bytes)
7252    }
7253
7254    /// Encode F64 comparison as Thumb-2: VCMP.F64 + VMRS + MOV #0 + IT + MOV #1
7255    fn encode_thumb_f64_compare(
7256        &self,
7257        rd: &Reg,
7258        dn: &VfpReg,
7259        dm: &VfpReg,
7260        cond_code: u32,
7261    ) -> Result<Vec<u8>> {
7262        let mut bytes = Vec::new();
7263        let rd_bits = reg_to_bits(rd);
7264
7265        // #712-class fix (found at f64-phase-2 wiring, #369): the 16-bit
7266        // `MOVS Rd,#0` is FLAG-SETTING. The original order emitted it AFTER
7267        // `VMRS APSR_nzcv, FPSCR`, clobbering the N/Z/C/V flags the VMRS just
7268        // transferred, so the following `IT<cond>` read stale flags and every
7269        // f64 comparison silently returned 0 — the exact bug the f32 compare
7270        // encoder shipped with and #712 fixed. Same fix: materialize the `#0`
7271        // FIRST (its flag side effect is overwritten by the VMRS), then
7272        // VCMP+VMRS set the flags the IT consumes. Pure reorder — sizes
7273        // unchanged.
7274
7275        // MOVS Rd, #0
7276        if rd_bits < 8 {
7277            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
7278            bytes.extend_from_slice(&movs_zero.to_le_bytes());
7279        } else {
7280            let hw1: u16 = 0xF04F;
7281            let hw2: u16 = (rd_bits as u16) << 8;
7282            bytes.extend_from_slice(&hw1.to_le_bytes());
7283            bytes.extend_from_slice(&hw2.to_le_bytes());
7284        }
7285
7286        // VCMP.F64 Dn, Dm
7287        let dn_num = vfp_dreg_to_num(dn)?;
7288        let dm_num = vfp_dreg_to_num(dm)?;
7289        let (vd, d) = encode_dreg(dn_num);
7290        let (vm, m) = encode_dreg(dm_num);
7291        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7292        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7293
7294        // VMRS APSR_nzcv, FPSCR (sets the flags the IT consumes)
7295        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7296
7297        // IT<cond>
7298        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7299        bytes.extend_from_slice(&it.to_le_bytes());
7300
7301        // MOV Rd, #1
7302        if rd_bits < 8 {
7303            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7304            bytes.extend_from_slice(&mov_one.to_le_bytes());
7305        } else {
7306            let hw1: u16 = 0xF04F;
7307            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7308            bytes.extend_from_slice(&hw1.to_le_bytes());
7309            bytes.extend_from_slice(&hw2.to_le_bytes());
7310        }
7311
7312        Ok(bytes)
7313    }
7314
7315    /// Encode F64 constant load as Thumb-2: MOVW+MOVT (lo32 into R0) + MOVW+MOVT (hi32 into R12) + VMOV Dd, R0, R12
7316    fn encode_thumb_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
7317        let mut bytes = Vec::new();
7318        let bits = value.to_bits();
7319        let lo32 = bits as u32;
7320        let hi32 = (bits >> 32) as u32;
7321
7322        // MOVW R0, #lo16(lo32)
7323        let lo16 = lo32 & 0xFFFF;
7324        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(0, lo16)?);
7325
7326        // MOVT R0, #hi16(lo32)
7327        let hi16 = (lo32 >> 16) & 0xFFFF;
7328        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(0, hi16)?);
7329
7330        // MOVW R12, #lo16(hi32)
7331        let lo16 = hi32 & 0xFFFF;
7332        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
7333
7334        // MOVT R12, #hi16(hi32)
7335        let hi16 = (hi32 >> 16) & 0xFFFF;
7336        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
7337
7338        // VMOV Dd, R0, R12
7339        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
7340        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7341
7342        Ok(bytes)
7343    }
7344
7345    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as Thumb-2
7346    /// Encode i32 → f64 conversion as Thumb-2. The integer stages through the
7347    /// DESTINATION's own low S-alias (`S(2d)`) — allocator-owned by
7348    /// definition — never S0 (which may hold a live value; the previous
7349    /// pseudo-op's S0 staging was the #615 class). Also fixes the SWAPPED
7350    /// signed/unsigned VCVT bases (bit7 = 1 is SIGNED — the same swap the f32
7351    /// twin had; latent here because f64.convert_i32_* was decode-dropped
7352    /// until #369): clang-verified vcvt.f64.s32 d1,s2 = eeb8 1bc1,
7353    /// vcvt.f64.u32 d1,s2 = eeb8 1b41.
7354    fn encode_thumb_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7355        let dd_num = vfp_dreg_to_num(dd)?;
7356        if dd_num > 7 {
7357            return Err(synth_core::Error::synthesis(format!(
7358                "F64ConvertI32: destination {dd:?} has no S-register alias \
7359                 (D8..D15) — the selector allocates only D0..D7"
7360            )));
7361        }
7362        let mut bytes = Vec::new();
7363
7364        // VMOV S(2d), Rm — stage the integer in the destination's low word.
7365        let (vn_s, n_s) = encode_sreg(2 * dd_num);
7366        let rt = reg_to_bits(rm);
7367        let vmov = 0xEE000A10 | (vn_s << 16) | (rt << 12) | (n_s << 7);
7368        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7369
7370        // VCVT.F64.S32/U32 Dd, S(2d)
7371        let (vd, d) = encode_dreg(dd_num);
7372        let (vm, m) = encode_sreg(2 * dd_num);
7373        let base = if signed { 0xEEB80BC0 } else { 0xEEB80B40 };
7374        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7375        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7376
7377        Ok(bytes)
7378    }
7379
7380    /// Encode VCVT.F64.F32 Dd, Sm as Thumb-2
7381    fn encode_thumb_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7382        let dd_num = vfp_dreg_to_num(dd)?;
7383        let sm_num = vfp_sreg_to_num(sm)?;
7384        let (vd, d) = encode_dreg(dd_num);
7385        let (vm, m) = encode_sreg(sm_num);
7386
7387        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7388        Ok(vfp_to_thumb_bytes(vcvt))
7389    }
7390
7391    /// Encode VCVT.F32.F64 Sd, Dm (f32.demote_f64) as Thumb-2 — single
7392    /// instruction, round-to-nearest-even per FPSCR default, exactly WASM
7393    /// §4.3.3 demote (clang-verified: vcvt.f32.f64 s1,d2 = eef7 0bc2).
7394    fn encode_thumb_f32_demote_f64(&self, sd: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7395        let sd_num = vfp_sreg_to_num(sd)?;
7396        let dm_num = vfp_dreg_to_num(dm)?;
7397        let (vd, d) = encode_sreg(sd_num);
7398        let (vm, m) = encode_dreg(dm_num);
7399
7400        let vcvt = 0xEEB70BC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7401        Ok(vfp_to_thumb_bytes(vcvt))
7402    }
7403
7404    /// Encode f64 → i32 truncation as Thumb-2 (round-toward-zero VCVT).
7405    ///
7406    /// The 32-bit result stages through the SOURCE's own low S-alias, `S(2m)`,
7407    /// clobbering half of an operand the selector has already popped. The
7408    /// overlapping write is well-defined: VCVT reads its source operand before
7409    /// writing (compilers emit `vcvt.f32.f64 s0, d0` routinely).
7410    ///
7411    /// # The one precondition, and who actually provides it
7412    ///
7413    /// `dm` must be a DEAD TEMP — never a pinned param/local home. That is the
7414    /// whole safety argument, and it is worth naming the guarantor precisely
7415    /// (#946): **`select_with_stack`** provides it, by copying a home into a
7416    /// fresh D-temp first. Visible in the shipped output for
7417    /// `(func (param f64) (result i32) (i32.trunc_f64_s (local.get 0)))`:
7418    ///
7419    /// ```text
7420    ///   vmov r1, r2, d0     ; read the param out of its AAPCS-VFP home D0
7421    ///   vmov d1, r1, r2     ; ...into a fresh D-temp
7422    ///   vcvt.s32.f64 s2, d1 ; convert from the TEMP, staging into its own S2
7423    /// ```
7424    ///
7425    /// `InstructionSelector::select` / `select_default` do NOT provide it —
7426    /// `alloc_vfp_dreg` is a bare round-robin `(n + 1) % 16` with no liveness
7427    /// or home test. That path is not reachable from `synth compile`
7428    /// (`arm_backend.rs` calls `select_with_stack` exclusively; the only
7429    /// non-test caller of `select` is `examples/compile_add.rs`), so this is
7430    /// not a live miscompile — but a caller reaching that `pub` API directly
7431    /// gets no such guarantee.
7432    ///
7433    /// # What this deliberately does NOT claim
7434    ///
7435    /// An earlier version of this comment said the staging register is "never
7436    /// S0, which may hold an unrelated live value (the #615 class)". **That is
7437    /// false**, and measurably so: for
7438    /// `(func (result i32) (i32.trunc_f64_s (f64.const 3.7)))` the shipped
7439    /// compiler emits `vcvt.s32.f64 s0, d0`.
7440    ///
7441    /// It is also unnecessary. S0 is only dangerous as an *unrelated* scratch;
7442    /// here it is always the low half of `dm` itself, which the precondition
7443    /// above already makes dead. Naming a guard the code does not have — and
7444    /// does not need — invites a future reader to lean on it. The dead-temp
7445    /// precondition is the only thing holding this up.
7446    fn encode_thumb_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7447        let dm_num = vfp_dreg_to_num(dm)?;
7448        if dm_num > 7 {
7449            return Err(synth_core::Error::synthesis(format!(
7450                "I32TruncF64: source {dm:?} has no S-register alias \
7451                 (D8..D15) — the selector allocates only D0..D7"
7452            )));
7453        }
7454        let mut bytes = Vec::new();
7455
7456        // VCVT.S32/U32.F64 S(2m), Dm (clang-verified:
7457        // vcvt.s32.f64 s1,d2 = eefd 0bc2 ; vcvt.u32.f64 s1,d2 = eefc 0bc2)
7458        let (vm, m) = encode_dreg(dm_num);
7459        let (vd_s, d_s) = encode_sreg(2 * dm_num);
7460        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
7461        let vcvt = base | (d_s << 22) | (vd_s << 12) | (m << 5) | vm;
7462        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7463
7464        // VMOV Rd, S(2m)
7465        let rt = reg_to_bits(rd);
7466        let vmov = 0xEE100A10 | (vd_s << 16) | (rt << 12) | (d_s << 7);
7467        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7468
7469        Ok(bytes)
7470    }
7471
7472    /// Encode F64 rounding as a SINGLE Thumb-2 VRINT (FPv5 / cortex-m7dp).
7473    /// `mode` keeps the legacy FPSCR-RMode numbering of the callers —
7474    /// 0b00=nearest(ties-to-even)→VRINTN, 0b01=+inf(ceil)→VRINTP,
7475    /// 0b10=-inf(floor)→VRINTM, 0b11=zero(trunc)→VRINTZ — but the rounding
7476    /// mode is now ENCODED in the instruction, not smuggled through FPSCR.
7477    /// (The previous pseudo-op round-tripped through a 32-bit integer in S0:
7478    /// wrong for |x| >= 2^31, NaN/±inf collapsed to 0, -0.0 lost, and it
7479    /// CLOBBERED S0/R12 behind the allocator's back — the #615 class.)
7480    /// VRINT quietens an sNaN and preserves the sign of ±0.0/NaN per IEEE 754
7481    /// roundToIntegral, which is exactly WASM Core §4.3.3 f64.ceil/floor/
7482    /// trunc/nearest. VRINTN/P/M live in the FE "always-execute" space (never
7483    /// IT-conditional; none of these sequences emits them inside an IT block).
7484    fn encode_thumb_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7485        let dd_num = vfp_dreg_to_num(dd)?;
7486        let dm_num = vfp_dreg_to_num(dm)?;
7487        let (vd, d) = encode_dreg(dd_num);
7488        let (vm, m) = encode_dreg(dm_num);
7489        // clang-verified bases (thumbv7em, fpv5-d16):
7490        //   vrintn.f64 d1,d2 = feb9 1b42 ; vrintp = feba 1b42
7491        //   vrintm.f64 d1,d2 = febb 1b42 ; vrintz = eeb6 1bc2
7492        let base: u32 = match mode {
7493            0b00 => 0xFEB90B40, // VRINTN.F64 (round to nearest, ties to even)
7494            0b01 => 0xFEBA0B40, // VRINTP.F64 (round toward +inf)
7495            0b10 => 0xFEBB0B40, // VRINTM.F64 (round toward -inf)
7496            _ => 0xEEB60BC0,    // VRINTZ.F64 (round toward zero)
7497        };
7498        Ok(vfp_to_thumb_bytes(
7499            base | (d << 22) | (vd << 12) | (m << 5) | vm,
7500        ))
7501    }
7502
7503    /// Encode F64 min/max as Thumb-2 with WASM Core §4.3.3 semantics:
7504    ///
7505    ///   VCMP.F64 Dn, Dm ; VMRS APSR_nzcv, FPSCR
7506    ///   VMINNM.F64/VMAXNM.F64 Dd, Dn, Dm      (FPv5; -0.0 < +0.0 ordered)
7507    ///   IT VS ; VADD.F64(VS) Dd, Dn, Dm       (unordered ⇒ NaN-propagating)
7508    ///
7509    /// VMINNM/VMAXNM alone are IEEE minNum/maxNum, which return the NUMBER
7510    /// when exactly one operand is NaN — WASM requires NaN. The VS-guarded
7511    /// VADD overwrites the result with a quiet NaN whenever the compare was
7512    /// unordered (either operand NaN); on the ordered path VMINNM/VMAXNM
7513    /// order -0.0 below +0.0, matching WASM's min(+0,-0) = -0 / max = +0.
7514    /// Clobbers ONLY Dd and the flags (the previous pseudo-op's ordered IT
7515    /// GT/MI select returned the WRONG operand for NaN and ±0 mixes).
7516    ///
7517    /// Ok-or-Err: `dd` must not alias `dn`/`dm` — the VS fix-up reads them
7518    /// AFTER VMINNM wrote `dd` (the selector always allocates a fresh
7519    /// destination while both sources are still marked live).
7520    fn encode_thumb_f64_minmax(
7521        &self,
7522        dd: &VfpReg,
7523        dn: &VfpReg,
7524        dm: &VfpReg,
7525        is_min: bool,
7526    ) -> Result<Vec<u8>> {
7527        if dd == dn || dd == dm {
7528            return Err(synth_core::Error::synthesis(format!(
7529                "F64{}: destination {dd:?} aliases a source ({dn:?},{dm:?}) — \
7530                 the unordered NaN fix-up would read a clobbered operand \
7531                 (compiler bug: the selector must allocate a fresh D-temp)",
7532                if is_min { "Min" } else { "Max" },
7533            )));
7534        }
7535        let mut bytes = Vec::new();
7536        let dd_num = vfp_dreg_to_num(dd)?;
7537        let dn_num = vfp_dreg_to_num(dn)?;
7538        let dm_num = vfp_dreg_to_num(dm)?;
7539        let (vd, d) = encode_dreg(dd_num);
7540        let (vn, n) = encode_dreg(dn_num);
7541        let (vm, m) = encode_dreg(dm_num);
7542
7543        // VCMP.F64 Dn, Dm (clang-verified: vcmp.f64 d2,d3 = eeb4 2b43)
7544        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7545        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7546        // VMRS APSR_nzcv, FPSCR
7547        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7548        // VMINNM.F64 / VMAXNM.F64 Dd, Dn, Dm (clang-verified:
7549        // vminnm.f64 d1,d2,d3 = fe82 1b43 ; vmaxnm = fe82 1b03)
7550        let base: u32 = if is_min { 0xFE800B40 } else { 0xFE800B00 };
7551        let vnm = base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7552        bytes.extend_from_slice(&vfp_to_thumb_bytes(vnm));
7553        // IT VS (unordered ⇒ at least one NaN operand)
7554        bytes.extend_from_slice(&0xBF68_u16.to_le_bytes());
7555        // VADD.F64(VS) Dd, Dn, Dm — NaN + x propagates a quiet NaN
7556        let vadd = 0xEE300B00 | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7557        bytes.extend_from_slice(&vfp_to_thumb_bytes(vadd));
7558
7559        Ok(bytes)
7560    }
7561
7562    /// Encode F64 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7563    /// encoder scratch, #212), the flags, and Dd:
7564    ///
7565    ///   VMOV R12, S(2m+1)   (high word of the SIGN source Dm)
7566    ///   CMP  R12, #0        (N flag = the sign bit)
7567    ///   VABS.F64 Dd, Dn     (magnitude, sign cleared)
7568    ///   IT MI ; VNEG.F64(MI) Dd, Dd
7569    ///
7570    /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7571    /// The R12 capture happens BEFORE Dd is written, so Dd aliasing Dn or Dm
7572    /// is safe. (The previous pseudo-op clobbered R0/R1/R2 behind the
7573    /// allocator's back — the #615 class.)
7574    fn encode_thumb_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7575        let dm_num = vfp_dreg_to_num(dm)?;
7576        if dm_num > 7 {
7577            return Err(synth_core::Error::synthesis(format!(
7578                "F64Copysign: sign source {dm:?} has no S-register alias \
7579                 (D8..D15) — the selector allocates only D0..D7"
7580            )));
7581        }
7582        let mut bytes = Vec::new();
7583        // VMOV R12, S(2m+1) — the sign source's high word.
7584        let (vn_s, n_s) = encode_sreg(2 * dm_num + 1);
7585        let vmov = 0xEE100A10 | (vn_s << 16) | (12 << 12) | (n_s << 7);
7586        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7587        // CMP R12, #0 (T2: CMP.W R12, #0) — N = bit31 of the sign word.
7588        bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7589        bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7590        // VABS.F64 Dd, Dn
7591        let dd_num = vfp_dreg_to_num(dd)?;
7592        let dn_num = vfp_dreg_to_num(dn)?;
7593        let (vd, d) = encode_dreg(dd_num);
7594        let (vn, n) = encode_dreg(dn_num);
7595        let vabs = 0xEEB00BC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7596        bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7597        // IT MI ; VNEG.F64(MI) Dd, Dd
7598        bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7599        let vneg = 0xEEB10B40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7600        bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7601
7602        Ok(bytes)
7603    }
7604
7605    /// Encode VCVT.S32/U32.F32 + VMOV as Thumb-2
7606    fn encode_thumb_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7607        let mut bytes = Vec::new();
7608
7609        let sm_num = vfp_sreg_to_num(sm)?;
7610        let (vd, d) = encode_sreg(sm_num);
7611        let (vm, m) = encode_sreg(sm_num);
7612        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
7613        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7614        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7615
7616        // VMOV Rd, Sm
7617        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
7618        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7619
7620        Ok(bytes)
7621    }
7622
7623    // === Thumb-2 32-bit encoding helpers ===
7624
7625    /// Encode Thumb-2 32-bit ADD with immediate
7626    fn encode_thumb32_add(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7627        let rd_bits = reg_to_bits(rd);
7628        let rn_bits = reg_to_bits(rn);
7629
7630        // The `i:imm3:imm8` field is split the same way for both forms.
7631        let i_bit = (imm >> 11) & 1;
7632        let imm3 = (imm >> 8) & 0x7;
7633        let imm8 = imm & 0xFF;
7634
7635        let hw1_base = if imm <= 0xFF {
7636            // ADD.W (T3): the field is a ThumbExpandImm modified immediate. For
7637            // imm <= 0xFF (i:imm3 = 0000) it is the zero-extended byte, which is
7638            // correct — keep this form so existing encodings stay bit-identical.
7639            0xF100
7640        } else if imm <= 0xFFF {
7641            // ADDW (T4): a PLAIN 12-bit immediate (0..4095) — no ThumbExpandImm.
7642            // This is what makes `add sp, sp, #frame` correct for frame sizes
7643            // >= 256, which ADD.W (T3) would silently mis-encode (e.g. #256 -> #0).
7644            0xF200
7645        } else {
7646            return Err(synth_core::Error::synthesis(
7647                "ADD immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7648            ));
7649        };
7650
7651        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7652        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7653
7654        let mut bytes = hw1.to_le_bytes().to_vec();
7655        bytes.extend_from_slice(&hw2.to_le_bytes());
7656        Ok(bytes)
7657    }
7658
7659    /// Encode Thumb-2 32-bit SUB with immediate
7660    fn encode_thumb32_sub(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7661        let rd_bits = reg_to_bits(rd);
7662        let rn_bits = reg_to_bits(rn);
7663
7664        let i_bit = (imm >> 11) & 1;
7665        let imm3 = (imm >> 8) & 0x7;
7666        let imm8 = imm & 0xFF;
7667
7668        let hw1_base = if imm <= 0xFF {
7669            // SUB.W (T3) modified immediate — correct for the zero-extended byte
7670            // (imm <= 0xFF). Kept bit-identical for existing encodings.
7671            0xF1A0
7672        } else if imm <= 0xFFF {
7673            // SUBW (T4): plain 12-bit immediate (0..4095). Makes
7674            // `sub sp, sp, #frame` correct for frame sizes >= 256.
7675            0xF2A0
7676        } else {
7677            return Err(synth_core::Error::synthesis(
7678                "SUB immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7679            ));
7680        };
7681
7682        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7683        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7684
7685        let mut bytes = hw1.to_le_bytes().to_vec();
7686        bytes.extend_from_slice(&hw2.to_le_bytes());
7687        Ok(bytes)
7688    }
7689
7690    /// Encode Thumb-2 32-bit ADDS with immediate (sets flags)
7691    fn encode_thumb32_adds(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7692        let rd_bits = reg_to_bits(rd);
7693        let rn_bits = reg_to_bits(rn);
7694
7695        // ADDS.W (flag-setting) has only the modified-immediate form — error on
7696        // an un-encodable value rather than silently add the wrong constant.
7697        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7698            synth_core::Error::synthesis(
7699                "ADDS immediate is not a valid ThumbExpandImm — materialize into a register",
7700            )
7701        })?;
7702        let i_bit = (field >> 11) & 1;
7703        let imm3 = (field >> 8) & 0x7;
7704        let imm8 = field & 0xFF;
7705
7706        // ADDS.W Rd, Rn, #imm (with S=1)
7707        // First halfword: 1111 0 i 0 1000 1 Rn = F110 | i<<10 | Rn
7708        let hw1: u16 = (0xF110 | (i_bit << 10) | rn_bits) as u16;
7709        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7710
7711        let mut bytes = hw1.to_le_bytes().to_vec();
7712        bytes.extend_from_slice(&hw2.to_le_bytes());
7713        Ok(bytes)
7714    }
7715
7716    /// Encode Thumb-2 32-bit SUBS with immediate (sets flags)
7717    fn encode_thumb32_subs(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7718        let rd_bits = reg_to_bits(rd);
7719        let rn_bits = reg_to_bits(rn);
7720
7721        // SUBS.W (flag-setting) has only the modified-immediate form — error on
7722        // an un-encodable value rather than silently subtract the wrong constant.
7723        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7724            synth_core::Error::synthesis(
7725                "SUBS immediate is not a valid ThumbExpandImm — materialize into a register",
7726            )
7727        })?;
7728        let i_bit = (field >> 11) & 1;
7729        let imm3 = (field >> 8) & 0x7;
7730        let imm8 = field & 0xFF;
7731
7732        // SUBS.W Rd, Rn, #imm (with S=1)
7733        // First halfword: 1111 0 i 0 1101 1 Rn = F1B0 | i<<10 | Rn
7734        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7735        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7736
7737        let mut bytes = hw1.to_le_bytes().to_vec();
7738        bytes.extend_from_slice(&hw2.to_le_bytes());
7739        Ok(bytes)
7740    }
7741
7742    /// Encode Thumb-2 32-bit MOVW (16-bit immediate)
7743    ///
7744    /// # Contract (Verus-style)
7745    /// ```text
7746    /// requires rd <= R14
7747    /// ensures result.len() == 4
7748    /// ensures (imm & 0xFFFF) can be reconstructed from the encoding
7749    /// ```
7750    fn encode_thumb32_movw(&self, rd: &Reg, imm: u32) -> Result<Vec<u8>> {
7751        let rd_bits = reg_to_bits(rd);
7752        reg_bits_checked(rd_bits)?;
7753        let imm16 = imm & 0xFFFF;
7754
7755        // MOVW Rd, #imm16
7756        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7757        let imm4 = (imm16 >> 12) & 0xF;
7758        let i_bit = (imm16 >> 11) & 1;
7759        let imm3 = (imm16 >> 8) & 0x7;
7760        let imm8 = imm16 & 0xFF;
7761
7762        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7763        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7764
7765        let mut bytes = hw1.to_le_bytes().to_vec();
7766        bytes.extend_from_slice(&hw2.to_le_bytes());
7767        encoding_contracts::verify_thumb32(&bytes);
7768        Ok(bytes)
7769    }
7770
7771    /// Encode Thumb-2 32-bit shift with immediate
7772    ///
7773    /// # Contract (Verus-style)
7774    /// ```text
7775    /// requires rd <= R14, rm <= R14
7776    /// ensures result.len() == 4
7777    /// ```
7778    fn encode_thumb32_shift(
7779        &self,
7780        rd: &Reg,
7781        rm: &Reg,
7782        shift: u32,
7783        shift_type: u8,
7784    ) -> Result<Vec<u8>> {
7785        let rd_bits = reg_to_bits(rd);
7786        let rm_bits = reg_to_bits(rm);
7787        reg_bits_checked(rd_bits)?;
7788        reg_bits_checked(rm_bits)?;
7789        let imm5 = shift & 0x1F;
7790        let imm2 = imm5 & 0x3;
7791        let imm3 = (imm5 >> 2) & 0x7;
7792
7793        // MOV.W Rd, Rm, <shift> #imm
7794        // EA4F 0 imm3 Rd imm2 type Rm
7795        let hw1: u16 = 0xEA4F;
7796        let hw2: u16 =
7797            ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | ((shift_type as u32) << 4) | rm_bits)
7798                as u16;
7799
7800        let mut bytes = hw1.to_le_bytes().to_vec();
7801        bytes.extend_from_slice(&hw2.to_le_bytes());
7802        Ok(bytes)
7803    }
7804
7805    /// Encode Thumb-2 32-bit shift by register
7806    /// Encoding: 11111010 0xx0 Rn | 1111 Rd 0000 Rm
7807    /// shift_type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
7808    fn encode_thumb32_shift_reg(
7809        &self,
7810        rd: &Reg,
7811        rn: &Reg,
7812        rm: &Reg,
7813        shift_type: u8,
7814    ) -> Result<Vec<u8>> {
7815        let rd_bits = reg_to_bits(rd);
7816        let rn_bits = reg_to_bits(rn);
7817        let rm_bits = reg_to_bits(rm);
7818
7819        // hw1: 1111 1010 0xx0 Rn
7820        let hw1: u16 = (0xFA00 | ((shift_type as u32) << 5) | rn_bits) as u16;
7821        // hw2: 1111 Rd 0000 Rm
7822        let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
7823
7824        let mut bytes = hw1.to_le_bytes().to_vec();
7825        bytes.extend_from_slice(&hw2.to_le_bytes());
7826        Ok(bytes)
7827    }
7828
7829    /// Encode Thumb-2 32-bit CMP with immediate
7830    fn encode_thumb32_cmp_imm(&self, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7831        let rn_bits = reg_to_bits(rn);
7832
7833        // CMP.W has only the modified-immediate form (no plain-imm12 like ADDW),
7834        // so an un-encodable immediate MUST be materialized into a register by
7835        // the selector. Error rather than silently compare the wrong constant.
7836        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7837            synth_core::Error::synthesis(
7838                "CMP immediate is not a valid ThumbExpandImm — materialize into a register",
7839            )
7840        })?;
7841        let i_bit = (field >> 11) & 1;
7842        let imm3 = (field >> 8) & 0x7;
7843        let imm8 = field & 0xFF;
7844
7845        // CMP.W Rn, #imm
7846        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7847        let hw2: u16 = ((imm3 << 12) | 0x0F00 | imm8) as u16;
7848
7849        let mut bytes = hw1.to_le_bytes().to_vec();
7850        bytes.extend_from_slice(&hw2.to_le_bytes());
7851        Ok(bytes)
7852    }
7853
7854    /// #372/#382: resolve the base register AND residual immediate offset for an
7855    /// `I64Ldr`/`I64Str` whose address may carry an index register. Returns
7856    /// `(base, low_offset)`; the caller accesses the halves at `[base,
7857    /// #low_offset]` and `[base, #low_offset + 4]`.
7858    ///
7859    /// - Frame access (no `offset_reg`, e.g. a spilled local at `[SP, #off]`):
7860    ///   returns `(addr.base, off)` and emits NOTHING — byte-identical.
7861    /// - Memory access (`reg_imm(R11, addr, offset)` = `R11 + addr + offset`)
7862    ///   with `offset + 4 <= 0xFFF`: emits `ADD.W ip, base, index` and returns
7863    ///   `(ip, offset)`, folding `offset`/`offset+4` into the halves' imm12.
7864    ///   Byte-identical to the pre-#382 (#372) behavior.
7865    /// - Memory access with `offset + 4 > 0xFFF`: the imm12 form cannot hold the
7866    ///   high half's offset, so `encode_thumb32_ldr`'s `check_ldst_imm12` (#259)
7867    ///   rightly refused it and the WHOLE function was skipped (#382). Instead
7868    ///   MATERIALIZE the offset into the base: `ADD ip, index, #offset` (against
7869    ///   the read-only INDEX register, so `encode_thumb32_add_imm` never trips its
7870    ///   `rd==rn==R12` alias trap), then `ADD.W ip, ip, base` (+ R11), and return
7871    ///   `(ip, 0)` so the halves use `[ip, #0]` / `[ip, #4]`.
7872    ///
7873    /// The effective address is fully materialized into `ip` BEFORE the halves
7874    /// are accessed, so an `rdlo` aliasing the index register is safe.
7875    ///
7876    /// RQ-63-ARMI64OFF (#1165): the wasm memarg is unsigned and reaches the
7877    /// encoder through the selector's `as i32` cast, so a memarg `>= 2^31`
7878    /// arrives NEGATIVE. It is read back as `u32` (lossless) and the
7879    /// materialization carries the full 32 bits — `base + index + offset (mod
7880    /// 2^32)`, the arithmetic the Thumb-2 word path (`encode_thumb32_add_imm`)
7881    /// and the software bounds guard already use for the same memarg. This
7882    /// used to CLAMP a negative offset to 0, so `i64.load offset=0xfffffff8`
7883    /// silently read `[R11 + addr]` — a wrong address, and a divergence from
7884    /// the i32 form of the same memarg. A FRAME access (no index) with an
7885    /// out-of-range or negative offset is refused by `check_ldst_imm12` in the
7886    /// halves — loud, never clamped.
7887    fn i64_effective_base(&self, bytes: &mut Vec<u8>, addr: &MemAddr) -> Result<(Reg, u32)> {
7888        let offset = addr.offset as u32;
7889        match addr.offset_reg {
7890            Some(idx) => {
7891                let ip = Reg::R12;
7892                // The high half sits at +4, which must itself fit imm12.
7893                if offset > 0xFFB {
7894                    // Large static offset (#382): fold it (and R11) into ip so the
7895                    // imm12 halves stay in range instead of skipping the function.
7896                    // ADD ip, index, #offset  (index != ip → no add_imm alias trap)
7897                    bytes.extend_from_slice(&self.encode_thumb32_add_imm(&ip, &idx, offset)?);
7898                    // ADD.W ip, ip, base  (+ R11)
7899                    bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
7900                        reg_to_bits(&ip),
7901                        reg_to_bits(&ip),
7902                        reg_to_bits(&addr.base),
7903                    )?);
7904                    Ok((ip, 0))
7905                } else {
7906                    // ADD.W ip, addr.base, idx  (Thumb-2, byte-verified vs as)
7907                    let hw1: u16 = 0xEB00 | reg_to_bits(&addr.base) as u16;
7908                    let hw2: u16 = 0x0C00 | reg_to_bits(&idx) as u16;
7909                    bytes.extend_from_slice(&hw1.to_le_bytes());
7910                    bytes.extend_from_slice(&hw2.to_le_bytes());
7911                    Ok((ip, offset))
7912                }
7913            }
7914            None => Ok((addr.base, offset)),
7915        }
7916    }
7917
7918    /// Encode Thumb-2 32-bit LDR
7919    fn encode_thumb32_ldr(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7920        let rd_bits = reg_to_bits(rd);
7921        let base_bits = reg_to_bits(base);
7922
7923        // LDR.W Rd, [Rn, #imm12]
7924        check_ldst_imm12(offset)?;
7925        let hw1: u16 = (0xF8D0 | base_bits) as u16;
7926        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7927
7928        let mut bytes = hw1.to_le_bytes().to_vec();
7929        bytes.extend_from_slice(&hw2.to_le_bytes());
7930        Ok(bytes)
7931    }
7932
7933    /// Encode Thumb-2 32-bit STR
7934    fn encode_thumb32_str(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7935        let rd_bits = reg_to_bits(rd);
7936        let base_bits = reg_to_bits(base);
7937
7938        // STR.W Rd, [Rn, #imm12]
7939        check_ldst_imm12(offset)?;
7940        let hw1: u16 = (0xF8C0 | base_bits) as u16;
7941        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7942
7943        let mut bytes = hw1.to_le_bytes().to_vec();
7944        bytes.extend_from_slice(&hw2.to_le_bytes());
7945        Ok(bytes)
7946    }
7947
7948    /// Encode Thumb-2 32-bit LDR with register offset: LDR.W Rd, [Rn, Rm]
7949    fn encode_thumb32_ldr_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7950        let rd_bits = reg_to_bits(rd);
7951        let base_bits = reg_to_bits(base);
7952        let rm_bits = reg_to_bits(offset_reg);
7953
7954        // LDR.W Rd, [Rn, Rm, LSL #0]
7955        // Encoding: 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
7956        // imm2 = 00 for no shift (LSL #0)
7957        let hw1: u16 = (0xF850 | base_bits) as u16;
7958        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7959
7960        let mut bytes = hw1.to_le_bytes().to_vec();
7961        bytes.extend_from_slice(&hw2.to_le_bytes());
7962        Ok(bytes)
7963    }
7964
7965    /// Encode Thumb-2 32-bit STR with register offset: STR.W Rd, [Rn, Rm]
7966    fn encode_thumb32_str_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7967        let rd_bits = reg_to_bits(rd);
7968        let base_bits = reg_to_bits(base);
7969        let rm_bits = reg_to_bits(offset_reg);
7970
7971        // STR.W Rd, [Rn, Rm, LSL #0]
7972        // Encoding: 1111 1000 0100 Rn | Rt 0000 00 imm2 Rm
7973        // imm2 = 00 for no shift (LSL #0)
7974        let hw1: u16 = (0xF840 | base_bits) as u16;
7975        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7976
7977        let mut bytes = hw1.to_le_bytes().to_vec();
7978        bytes.extend_from_slice(&hw2.to_le_bytes());
7979        Ok(bytes)
7980    }
7981
7982    // === Sub-word load/store Thumb-2 encoding helpers ===
7983
7984    /// Encode Thumb-2 32-bit LDRB with immediate: LDRB.W Rd, [Rn, #imm12]
7985    fn encode_thumb32_ldrb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7986        let rd_bits = reg_to_bits(rd);
7987        let base_bits = reg_to_bits(base);
7988        // LDRB.W Rd, [Rn, #imm12]: 1111 1000 1001 Rn | Rt imm12
7989        check_ldst_imm12(offset)?;
7990        let hw1: u16 = (0xF890 | base_bits) as u16;
7991        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7992        let mut bytes = hw1.to_le_bytes().to_vec();
7993        bytes.extend_from_slice(&hw2.to_le_bytes());
7994        Ok(bytes)
7995    }
7996
7997    /// Encode Thumb-2 32-bit LDRB with register: LDRB.W Rd, [Rn, Rm]
7998    fn encode_thumb32_ldrb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7999        let rd_bits = reg_to_bits(rd);
8000        let base_bits = reg_to_bits(base);
8001        let rm_bits = reg_to_bits(offset_reg);
8002        // LDRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0001 Rn | Rt 0000 00 imm2 Rm
8003        let hw1: u16 = (0xF810 | base_bits) as u16;
8004        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8005        let mut bytes = hw1.to_le_bytes().to_vec();
8006        bytes.extend_from_slice(&hw2.to_le_bytes());
8007        Ok(bytes)
8008    }
8009
8010    /// Encode Thumb-2 32-bit LDRSB with immediate: LDRSB.W Rd, [Rn, #imm12]
8011    fn encode_thumb32_ldrsb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8012        let rd_bits = reg_to_bits(rd);
8013        let base_bits = reg_to_bits(base);
8014        // LDRSB.W Rd, [Rn, #imm12]: 1111 1001 1001 Rn | Rt imm12
8015        check_ldst_imm12(offset)?;
8016        let hw1: u16 = (0xF990 | base_bits) as u16;
8017        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8018        let mut bytes = hw1.to_le_bytes().to_vec();
8019        bytes.extend_from_slice(&hw2.to_le_bytes());
8020        Ok(bytes)
8021    }
8022
8023    /// Encode Thumb-2 32-bit LDRSB with register: LDRSB.W Rd, [Rn, Rm]
8024    fn encode_thumb32_ldrsb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8025        let rd_bits = reg_to_bits(rd);
8026        let base_bits = reg_to_bits(base);
8027        let rm_bits = reg_to_bits(offset_reg);
8028        // LDRSB.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0001 Rn | Rt 0000 00 imm2 Rm
8029        let hw1: u16 = (0xF910 | base_bits) as u16;
8030        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8031        let mut bytes = hw1.to_le_bytes().to_vec();
8032        bytes.extend_from_slice(&hw2.to_le_bytes());
8033        Ok(bytes)
8034    }
8035
8036    /// Encode Thumb-2 32-bit LDRH with immediate: LDRH.W Rd, [Rn, #imm12]
8037    fn encode_thumb32_ldrh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8038        let rd_bits = reg_to_bits(rd);
8039        let base_bits = reg_to_bits(base);
8040        // LDRH.W Rd, [Rn, #imm12]: 1111 1000 1011 Rn | Rt imm12
8041        check_ldst_imm12(offset)?;
8042        let hw1: u16 = (0xF8B0 | base_bits) as u16;
8043        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8044        let mut bytes = hw1.to_le_bytes().to_vec();
8045        bytes.extend_from_slice(&hw2.to_le_bytes());
8046        Ok(bytes)
8047    }
8048
8049    /// Encode Thumb-2 32-bit LDRH with register: LDRH.W Rd, [Rn, Rm]
8050    fn encode_thumb32_ldrh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8051        let rd_bits = reg_to_bits(rd);
8052        let base_bits = reg_to_bits(base);
8053        let rm_bits = reg_to_bits(offset_reg);
8054        // LDRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0011 Rn | Rt 0000 00 imm2 Rm
8055        let hw1: u16 = (0xF830 | base_bits) as u16;
8056        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8057        let mut bytes = hw1.to_le_bytes().to_vec();
8058        bytes.extend_from_slice(&hw2.to_le_bytes());
8059        Ok(bytes)
8060    }
8061
8062    /// Encode Thumb-2 32-bit LDRSH with immediate: LDRSH.W Rd, [Rn, #imm12]
8063    fn encode_thumb32_ldrsh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8064        let rd_bits = reg_to_bits(rd);
8065        let base_bits = reg_to_bits(base);
8066        // LDRSH.W Rd, [Rn, #imm12]: 1111 1001 1011 Rn | Rt imm12
8067        check_ldst_imm12(offset)?;
8068        let hw1: u16 = (0xF9B0 | base_bits) as u16;
8069        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8070        let mut bytes = hw1.to_le_bytes().to_vec();
8071        bytes.extend_from_slice(&hw2.to_le_bytes());
8072        Ok(bytes)
8073    }
8074
8075    /// Encode Thumb-2 32-bit LDRSH with register: LDRSH.W Rd, [Rn, Rm]
8076    fn encode_thumb32_ldrsh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8077        let rd_bits = reg_to_bits(rd);
8078        let base_bits = reg_to_bits(base);
8079        let rm_bits = reg_to_bits(offset_reg);
8080        // LDRSH.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0011 Rn | Rt 0000 00 imm2 Rm
8081        let hw1: u16 = (0xF930 | base_bits) as u16;
8082        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8083        let mut bytes = hw1.to_le_bytes().to_vec();
8084        bytes.extend_from_slice(&hw2.to_le_bytes());
8085        Ok(bytes)
8086    }
8087
8088    /// Encode Thumb-2 32-bit STRB with immediate: STRB.W Rd, [Rn, #imm12]
8089    fn encode_thumb32_strb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8090        let rd_bits = reg_to_bits(rd);
8091        let base_bits = reg_to_bits(base);
8092        // STRB.W Rd, [Rn, #imm12]: 1111 1000 1000 Rn | Rt imm12
8093        check_ldst_imm12(offset)?;
8094        let hw1: u16 = (0xF880 | base_bits) as u16;
8095        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8096        let mut bytes = hw1.to_le_bytes().to_vec();
8097        bytes.extend_from_slice(&hw2.to_le_bytes());
8098        Ok(bytes)
8099    }
8100
8101    /// Encode Thumb-2 32-bit STRB with register: STRB.W Rd, [Rn, Rm]
8102    fn encode_thumb32_strb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8103        let rd_bits = reg_to_bits(rd);
8104        let base_bits = reg_to_bits(base);
8105        let rm_bits = reg_to_bits(offset_reg);
8106        // STRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0000 Rn | Rt 0000 00 imm2 Rm
8107        let hw1: u16 = (0xF800 | base_bits) as u16;
8108        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8109        let mut bytes = hw1.to_le_bytes().to_vec();
8110        bytes.extend_from_slice(&hw2.to_le_bytes());
8111        Ok(bytes)
8112    }
8113
8114    /// Encode Thumb-2 32-bit STRH with immediate: STRH.W Rd, [Rn, #imm12]
8115    fn encode_thumb32_strh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8116        let rd_bits = reg_to_bits(rd);
8117        let base_bits = reg_to_bits(base);
8118        // STRH.W Rd, [Rn, #imm12]: 1111 1000 1010 Rn | Rt imm12
8119        check_ldst_imm12(offset)?;
8120        let hw1: u16 = (0xF8A0 | base_bits) as u16;
8121        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8122        let mut bytes = hw1.to_le_bytes().to_vec();
8123        bytes.extend_from_slice(&hw2.to_le_bytes());
8124        Ok(bytes)
8125    }
8126
8127    /// Encode Thumb-2 32-bit STRH with register: STRH.W Rd, [Rn, Rm]
8128    fn encode_thumb32_strh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8129        let rd_bits = reg_to_bits(rd);
8130        let base_bits = reg_to_bits(base);
8131        let rm_bits = reg_to_bits(offset_reg);
8132        // STRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0010 Rn | Rt 0000 00 imm2 Rm
8133        let hw1: u16 = (0xF820 | base_bits) as u16;
8134        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8135        let mut bytes = hw1.to_le_bytes().to_vec();
8136        bytes.extend_from_slice(&hw2.to_le_bytes());
8137        Ok(bytes)
8138    }
8139
8140    /// Encode Thumb-2 32-bit ADD with immediate: ADD.W Rd, Rn, #imm
8141    fn encode_thumb32_add_imm(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
8142        let rd_bits = reg_to_bits(rd);
8143        let rn_bits = reg_to_bits(rn);
8144
8145        // In-range immediates (<= 0xFFF) delegate to `encode_thumb32_add`,
8146        // which picks the correct form per value:
8147        //   - imm <= 0xFF  -> ADD.W (T3). Its `i:imm3:imm8` field is a
8148        //     ThumbExpandImm MODIFIED immediate — raw == expanded only here.
8149        //   - 0x100..=0xFFF -> ADDW (T4, 0xF200): a PLAIN 12-bit immediate.
8150        //
8151        // #681: this function used to pack the raw value into the T3 field for
8152        // ALL imm <= 0xFFF. ThumbExpandImm(0x200) = 0 and ThumbExpandImm(0x400)
8153        // = 0x8000_0000, so every dynamic-address load/store with a static
8154        // offset in 0x100..=0xFFF silently computed a WRONG address — and in
8155        // --safety-bounds software the guard checked the intended address while
8156        // the access used the mis-encoded one (bounds bypass). Same
8157        // ThumbExpandImm raw-packing class as #253/#255, reached via #382.
8158        if imm <= 0xFFF {
8159            self.encode_thumb32_add(rd, rn, imm)
8160        } else {
8161            // Out-of-range immediate (> 0xFFF): materialize it into a scratch
8162            // register, then ADD.W Rd, Rn, scratch. This is the #180/#185
8163            // "encoder must produce a legal sequence, not assert" class — see #350.
8164            //
8165            // Scratch choice (must NEVER equal Rn, or Rn would be clobbered before
8166            // the ADD reads it):
8167            //   - rd != rn  => use rd itself (rn is untouched, since rd != rn).
8168            //   - rd == rn  => use R12/IP (the reserved encoder scratch). rd/rn are
8169            //                  never R12 (R12 is non-allocatable), so it can't alias.
8170            //
8171            // The materialized value is the same whether or not MOVT is emitted, so
8172            // the byte length depends only on `imm` (and rd==rn) — the size probe and
8173            // the final emit therefore agree (mandatory: the function is encoded twice).
8174            let scratch: u32 = if rd_bits == rn_bits {
8175                12 // R12/IP — in-place add, can't use rd because rd == rn
8176            } else {
8177                rd_bits // rn is preserved because rd != rn
8178            };
8179            // Invariant: the scratch must never alias Rn (would clobber it before
8180            // the ADD reads it). Unreachable in real codegen (rd/rn are never R12,
8181            // which is reserved encoder scratch), but the encoder is also driven by
8182            // the `encoder_no_panic` fuzz harness with ARBITRARY registers — incl.
8183            // rd==rn==R12, which makes scratch (R12) alias Rn. The encoder contract
8184            // (#180/#185) is Ok-or-Err, never a panic, so return a typed error
8185            // instead of asserting. #350 follow-up.
8186            if scratch == rn_bits {
8187                return Err(synth_core::Error::synthesis(format!(
8188                    "ADD #imm: cannot lower #{imm:#x} for Rd==Rn==R12 — no free scratch \
8189                     register (R12 is the reserved encoder scratch and aliases Rn here)"
8190                )));
8191            }
8192
8193            let lo16 = imm & 0xFFFF;
8194            let hi16 = (imm >> 16) & 0xFFFF;
8195
8196            let mut bytes = self.encode_thumb32_movw_raw(scratch, lo16)?;
8197            if hi16 != 0 {
8198                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(scratch, hi16)?);
8199            }
8200            bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rn_bits, scratch)?);
8201            Ok(bytes)
8202        }
8203    }
8204
8205    // === Raw encoding helpers for POPCNT (take register numbers directly) ===
8206
8207    /// Encode Thumb-2 32-bit MOVW (16-bit immediate) - raw version
8208    ///
8209    /// # Contract (Verus-style)
8210    /// ```text
8211    /// requires rd <= 14, imm16 <= 0xFFFF
8212    /// ensures result.len() == 4
8213    /// ```
8214    fn encode_thumb32_movw_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8215        reg_bits_checked(rd)?;
8216        encoding_contracts::verify_imm16(imm16);
8217        // MOVW Rd, #imm16
8218        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
8219        let imm16 = imm16 & 0xFFFF;
8220        let imm4 = (imm16 >> 12) & 0xF;
8221        let i_bit = (imm16 >> 11) & 1;
8222        let imm3 = (imm16 >> 8) & 0x7;
8223        let imm8 = imm16 & 0xFF;
8224
8225        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
8226        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8227
8228        let mut bytes = hw1.to_le_bytes().to_vec();
8229        bytes.extend_from_slice(&hw2.to_le_bytes());
8230        encoding_contracts::verify_thumb32(&bytes);
8231        Ok(bytes)
8232    }
8233
8234    /// Encode Thumb-2 32-bit MOVT (move top 16 bits) - raw version
8235    ///
8236    /// # Contract (Verus-style)
8237    /// ```text
8238    /// requires rd <= 14, imm16 <= 0xFFFF
8239    /// ensures result.len() == 4
8240    /// ```
8241    fn encode_thumb32_movt_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8242        reg_bits_checked(rd)?;
8243        encoding_contracts::verify_imm16(imm16);
8244        // MOVT Rd, #imm16
8245        // 1111 0 i 10 1 1 0 0 imm4 | 0 imm3 Rd imm8
8246        let imm16 = imm16 & 0xFFFF;
8247        let imm4 = (imm16 >> 12) & 0xF;
8248        let i_bit = (imm16 >> 11) & 1;
8249        let imm3 = (imm16 >> 8) & 0x7;
8250        let imm8 = imm16 & 0xFF;
8251
8252        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
8253        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8254
8255        let mut bytes = hw1.to_le_bytes().to_vec();
8256        bytes.extend_from_slice(&hw2.to_le_bytes());
8257        encoding_contracts::verify_thumb32(&bytes);
8258        Ok(bytes)
8259    }
8260
8261    /// Encode Thumb-2 32-bit LSR (logical shift right) with immediate - raw version
8262    fn encode_thumb32_lsr_raw(&self, rd: u32, rm: u32, shift: u32) -> Result<Vec<u8>> {
8263        // MOV.W Rd, Rm, LSR #imm
8264        // EA4F 0 imm3 Rd imm2 01 Rm
8265        let imm5 = shift & 0x1F;
8266        let imm2 = imm5 & 0x3;
8267        let imm3 = (imm5 >> 2) & 0x7;
8268
8269        let hw1: u16 = 0xEA4F;
8270        let hw2: u16 = ((imm3 << 12) | (rd << 8) | (imm2 << 6) | (0b01 << 4) | rm) as u16;
8271
8272        let mut bytes = hw1.to_le_bytes().to_vec();
8273        bytes.extend_from_slice(&hw2.to_le_bytes());
8274        Ok(bytes)
8275    }
8276
8277    /// Encode Thumb-2 32-bit AND with immediate - raw version
8278    fn encode_thumb32_and_imm_raw(&self, rd: u32, rn: u32, imm: u32) -> Result<Vec<u8>> {
8279        // AND.W Rd, Rn, #<modified_immediate>
8280        // F0 00 Rn | 0 imm3 Rd imm8
8281        //
8282        // #681 class audit: the field is a ThumbExpandImm modified immediate,
8283        // not a raw value. The only current caller (POPCNT final mask) passes
8284        // 0x3F, which expands to itself — the gate is byte-identical today and
8285        // closes the raw-packing landmine for any future caller.
8286        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
8287            synth_core::Error::synthesis(
8288                "AND immediate is not a valid ThumbExpandImm — materialize into a register",
8289            )
8290        })?;
8291        let i_bit = (field >> 11) & 1;
8292        let imm3 = (field >> 8) & 0x7;
8293        let imm8 = field & 0xFF;
8294
8295        let hw1: u16 = (0xF000 | (i_bit << 10) | rn) as u16;
8296        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8297
8298        let mut bytes = hw1.to_le_bytes().to_vec();
8299        bytes.extend_from_slice(&hw2.to_le_bytes());
8300        Ok(bytes)
8301    }
8302
8303    /// Encode Thumb-2 32-bit SUB (register) - raw version
8304    fn encode_thumb32_sub_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8305        // SUB.W Rd, Rn, Rm
8306        // EBA0 Rn | 0 Rd 00 00 Rm
8307        let hw1: u16 = (0xEBA0 | rn) as u16;
8308        let hw2: u16 = ((rd << 8) | rm) as u16;
8309
8310        let mut bytes = hw1.to_le_bytes().to_vec();
8311        bytes.extend_from_slice(&hw2.to_le_bytes());
8312        Ok(bytes)
8313    }
8314
8315    /// Encode Thumb-2 32-bit ADD (register) - raw version
8316    fn encode_thumb32_add_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8317        // ADD.W Rd, Rn, Rm
8318        // EB00 Rn | 0 Rd 00 00 Rm
8319        let hw1: u16 = (0xEB00 | rn) as u16;
8320        let hw2: u16 = ((rd << 8) | rm) as u16;
8321
8322        let mut bytes = hw1.to_le_bytes().to_vec();
8323        bytes.extend_from_slice(&hw2.to_le_bytes());
8324        Ok(bytes)
8325    }
8326
8327    /// Encode Thumb-2 32-bit ADDS (register, flag-setting) - raw version.
8328    /// Used as the high-register fallback for `ArmOp::Adds` (i64 low-word add)
8329    /// so R8-R11 pair operands don't overflow the 16-bit field — #178/#180.
8330    fn encode_thumb32_adds_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8331        // ADDS.W Rd, Rn, Rm (T3, S=1): EB10 Rn | 0 Rd 00 00 Rm
8332        let hw1: u16 = (0xEB10 | rn) as u16;
8333        let hw2: u16 = ((rd << 8) | rm) as u16;
8334        let mut bytes = hw1.to_le_bytes().to_vec();
8335        bytes.extend_from_slice(&hw2.to_le_bytes());
8336        Ok(bytes)
8337    }
8338
8339    /// Encode Thumb-2 32-bit SUBS (register, flag-setting) - raw version.
8340    /// High-register fallback for `ArmOp::Subs` (i64 low-word subtract) — #178/#180.
8341    fn encode_thumb32_subs_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8342        // SUBS.W Rd, Rn, Rm (T3, S=1): EBB0 Rn | 0 Rd 00 00 Rm
8343        let hw1: u16 = (0xEBB0 | rn) as u16;
8344        let hw2: u16 = ((rd << 8) | rm) as u16;
8345        let mut bytes = hw1.to_le_bytes().to_vec();
8346        bytes.extend_from_slice(&hw2.to_le_bytes());
8347        Ok(bytes)
8348    }
8349
8350    /// Encode a sequence of ARM instructions
8351    pub fn encode_sequence(&self, ops: &[ArmOp]) -> Result<Vec<u8>> {
8352        let mut code = Vec::new();
8353
8354        for op in ops {
8355            let encoded = self.encode(op)?;
8356            code.extend_from_slice(&encoded);
8357        }
8358
8359        Ok(code)
8360    }
8361}
8362
8363/// Convert register to bit encoding (0-15)
8364/// Reverse of the ARMv7-M `ThumbExpandImm`: given a 32-bit immediate, return the
8365/// 12-bit `i:imm3:imm8` field if it is a representable modified immediate, else
8366/// `None` (the caller must materialize the value into a register). This is the
8367/// shared correct path for the data-processing immediate encoders — without it
8368/// they pack raw bits and silently mis-encode any value `> 0xFF` that isn't a
8369/// modified immediate (the silent-miscompile class behind #251/#253/#255).
8370fn try_thumb_expand_imm(value: u32) -> Option<u32> {
8371    // i:imm3 = 0000 → 8-bit value, zero-extended (00000000 00000000 00000000 XY).
8372    if value <= 0xFF {
8373        return Some(value);
8374    }
8375    let b0 = value & 0xFF; // byte 0
8376    let b1 = (value >> 8) & 0xFF; // byte 1
8377    // 0x00XY00XY (i:imm3 = 0001) — XY in bytes 0 and 2
8378    if value == (b0 << 16) | b0 {
8379        return Some(0x100 | b0);
8380    }
8381    // 0xXY00XY00 (i:imm3 = 0010) — XY in bytes 1 and 3
8382    if value == (b1 << 24) | (b1 << 8) {
8383        return Some(0x200 | b1);
8384    }
8385    // 0xXYXYXYXY (i:imm3 = 0011) — XY in all four bytes
8386    if value == (b0 << 24) | (b0 << 16) | (b0 << 8) | b0 {
8387        return Some(0x300 | b0);
8388    }
8389    // An 8-bit value with bit 7 set, rotated right by 8..=31. `rotate_left(rot)`
8390    // undoes the encoded right rotation; if the result is `1bbbbbbb` (0x80..=0xFF)
8391    // the value is representable. imm12[11:7] = rot, imm12[6:0] = low 7 bits.
8392    for rot in 8..=31u32 {
8393        let unrot = value.rotate_left(rot);
8394        if (0x80..=0xFF).contains(&unrot) {
8395            return Some((rot << 7) | (unrot & 0x7F));
8396        }
8397    }
8398    None
8399}
8400
8401/// Guard a Thumb-2 `LDR/STR Rd, [Rn, #imm12]` offset. The imm12 form supports
8402/// `0..=4095`; a larger offset must be materialized into a register by the
8403/// selector (register-offset addressing). Returning `Err` rather than silently
8404/// masking `offset & 0xFFF` closes the wrong-address miscompile class (#259,
8405/// the load/store sibling of #253/#255).
8406fn check_ldst_imm12(offset: u32) -> Result<()> {
8407    if offset > 0xFFF {
8408        Err(synth_core::Error::synthesis(
8409            "load/store immediate offset > 0xFFF (4095) — materialize the offset into a register",
8410        ))
8411    } else {
8412        Ok(())
8413    }
8414}
8415
8416/// #916 — emit `Rd = 0` in Thumb-2, correctly for EVERY destination register.
8417///
8418/// The 16-bit `MOVS Rd, #imm8` (T1) is `0010 0 Rd(3) imm8` — the Rd field is
8419/// **three bits**. For R8-R12 `reg_to_bits` yields 8..12, so `rd_bits << 8`
8420/// overflows into bit 11 and `0x2000 | 0x0800` is `0x2800` = `CMP r0, #0`:
8421/// not a move at all. The destination is never written (it keeps stale data)
8422/// and the flags are clobbered. Same class as #180 / H-CODE-9, and the same
8423/// defect #311 fixed for `I64SetCond`.
8424///
8425/// High registers therefore take the 32-bit `MOV.W Rd, #imm8` (T2,
8426/// `F04F 0000 | Rd<<8 | imm8`), whose Rd field is four bits. `MOV.W` with S=0
8427/// does not set flags, which is what these zero-fill sites want anyway.
8428///
8429/// **Callers with branches must consult [`thumb_zero_fill_halfwords`].** This
8430/// emits 1 halfword for R0-R7 and 2 for R8-R12; any branch whose target lies
8431/// PAST this instruction moves when it widens and its displacement has to be
8432/// derived rather than hard-coded. (A branch targeting this instruction's own
8433/// address is unaffected — an instruction cannot move itself.)
8434fn emit_thumb_zero_fill(bytes: &mut Vec<u8>, rd_bits: u32) {
8435    if rd_bits < 8 {
8436        let movs: u16 = 0x2000 | ((rd_bits as u16) << 8);
8437        bytes.extend_from_slice(&movs.to_le_bytes());
8438    } else {
8439        bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
8440        bytes.extend_from_slice(&((rd_bits as u16) << 8).to_le_bytes());
8441    }
8442}
8443
8444/// Halfword length of the encoding [`emit_thumb_zero_fill`] picks for
8445/// `rd_bits`. Branch displacements spanning the zero-fill derive from this so
8446/// the encoder cannot drift from itself (#916; the byte-size estimator mirrors
8447/// it in `synth_synthesis::estimate_arm_byte_size`, pinned by the #498
8448/// `estimator_encoder_agreement` oracle).
8449fn thumb_zero_fill_halfwords(rd_bits: u32) -> u16 {
8450    if rd_bits < 8 { 1 } else { 2 }
8451}
8452
8453fn reg_to_bits(reg: &Reg) -> u32 {
8454    match reg {
8455        Reg::R0 => 0,
8456        Reg::R1 => 1,
8457        Reg::R2 => 2,
8458        Reg::R3 => 3,
8459        Reg::R4 => 4,
8460        Reg::R5 => 5,
8461        Reg::R6 => 6,
8462        Reg::R7 => 7,
8463        Reg::R8 => 8,
8464        Reg::R9 => 9,
8465        Reg::R10 => 10,
8466        Reg::R11 => 11,
8467        Reg::R12 => 12,
8468        Reg::SP => 13,
8469        Reg::LR => 14,
8470        Reg::PC => 15,
8471    }
8472}
8473
8474// ======================================================================
8475// #610 — i64 fixed-ABI expansion wrappers.
8476//
8477// The hand-written multi-instruction i64 cores (rotl/rotr and the div/rem
8478// shift-subtract loops) compute in FIXED low registers. Before #610 the
8479// div/rem arms ignored their operand fields outright (hardcoded R0:R1 /
8480// R2:R3 in, result to R0:R1) and the rot arms used R3/R4 scratch that
8481// collided with selector-assigned registers — then restored the saved
8482// scratch OVER the result (`POP {R4}` with rd_lo == R4), so the op
8483// returned the caller's stale register: 0 for every input under qemu.
8484//
8485// These wrappers make each core honor its register parameters:
8486//   1. save R0-R3,
8487//   2. marshal the operand registers into the core's fixed input regs via
8488//      the stack (permutation-safe: every source is read before any fixed
8489//      register is written),
8490//   3. run the fixed-reg core (self-preserving for R4+; R12 is encoder
8491//      scratch and never allocatable, #212),
8492//   4. MOV the result pair from R0:R1 into the selector's rd pair,
8493//   5. restore R0-R3, skipping any register the result now occupies.
8494//
8495// All emitted lengths are register-independent so the optimized path's
8496// byte-size estimator (`estimate_arm_byte_size`, pinned by the
8497// estimator↔encoder agreement oracle #498/#511) stays a constant per op.
8498// ======================================================================
8499
8500/// Steps 1+2: `PUSH {R0-R3}`, then marshal `srcs` (operand registers, any of
8501/// R0-R12) into `R0..R<n>` via individual stack pushes. Sources are all read
8502/// before any destination register is written, so arbitrary source/target
8503/// permutations (including operands living in R0-R3) are safe.
8504fn emit_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8505    debug_assert!(srcs.len() <= 4);
8506    // PUSH {R0-R3} — save the caller-visible low registers.
8507    bytes.extend_from_slice(&0xB40Fu16.to_le_bytes());
8508    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8509    for src in srcs.iter().rev() {
8510        let rt = reg_to_bits(src) as u16;
8511        bytes.extend_from_slice(&0xF84Du16.to_le_bytes());
8512        bytes.extend_from_slice(&((rt << 12) | 0x0D04).to_le_bytes());
8513    }
8514    // POP {Ri} — Ri := srcs[i].
8515    for i in 0..srcs.len() as u16 {
8516        bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes());
8517    }
8518}
8519
8520/// Steps 4+5: move the core's R0:R1 result into the selector's rd pair, then
8521/// restore the R0-R3 saved by [`emit_i64_fixed_abi_entry`], skipping any
8522/// register the result now lives in (its saved caller word is discarded).
8523fn emit_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8524    let lo = reg_to_bits(rdlo);
8525    let hi = reg_to_bits(rdhi);
8526    if lo == 1 && hi == 0 {
8527        // A fully swapped pair would clobber one half in either MOV order.
8528        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8529        return Err(synth_core::Error::synthesis(
8530            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8531        ));
8532    }
8533    let mov16 = |bytes: &mut Vec<u8>, rd: u32, rm: u32| {
8534        let d = ((rd >> 3) & 1) as u16;
8535        bytes.extend_from_slice(
8536            &(0x4600u16 | (d << 7) | ((rm as u16) << 3) | ((rd & 7) as u16)).to_le_bytes(),
8537        );
8538    };
8539    if hi == 0 {
8540        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8541        mov16(bytes, lo, 0);
8542        mov16(bytes, hi, 1);
8543    } else {
8544        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8545        mov16(bytes, hi, 1);
8546        mov16(bytes, lo, 0);
8547    }
8548    for i in 0..4u32 {
8549        if i == lo || i == hi {
8550            // The result lives here — drop the saved caller word.
8551            bytes.extend_from_slice(&0xB001u16.to_le_bytes()); // ADD SP, #4
8552        } else {
8553            bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes()); // POP {Ri}
8554        }
8555    }
8556    Ok(())
8557}
8558
8559/// WASM `i64.div_*` / `i64.rem_*` by zero must trap, matching the i32 path's
8560/// cmp/bne/udf guard. Emitted after marshaling, when the divisor pair is in
8561/// R2:R3: `ORRS R12, R2, R3` — `BNE` over a `UDF #0` when nonzero.
8562fn emit_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8563    bytes.extend_from_slice(&0xEA52u16.to_le_bytes()); // ORRS.W R12, R2, R3
8564    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8565    bytes.extend_from_slice(&0xD100u16.to_le_bytes()); // BNE.N +0 (skip the UDF)
8566    bytes.extend_from_slice(&0xDE00u16.to_le_bytes()); // UDF #0 — divide by zero
8567}
8568
8569/// WASM `i64.div_s(INT64_MIN, -1)` must trap (Core §4.3.2 `idiv_s`: the
8570/// quotient +2^63 is unrepresentable), matching the i32 path's overflow
8571/// guard — #633: without it the core negated INT64_MIN onto itself and
8572/// silently returned INT64_MIN. Emitted after marshaling, when the dividend
8573/// pair is in R0:R1 and the divisor pair in R2:R3; R12 is encoder scratch.
8574///
8575/// div_s ONLY — `i64.rem_s(INT64_MIN, -1)` is defined as 0 and must NOT
8576/// trap (`irem_s`), so the I64RemS arm never calls this. 22 bytes,
8577/// register-independent (estimator contract, #498/#511).
8578fn emit_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8579    // AND.W R12, R2, R3 — R12 == 0xFFFFFFFF iff divisor == -1
8580    bytes.extend_from_slice(&0xEA02u16.to_le_bytes());
8581    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8582    // CMN.W R12, #1 — EQ iff both divisor words are all-ones
8583    bytes.extend_from_slice(&0xF11Cu16.to_le_bytes());
8584    bytes.extend_from_slice(&0x0F01u16.to_le_bytes());
8585    // BNE .no_trap
8586    bytes.extend_from_slice(&0xD105u16.to_le_bytes());
8587    // CMP R0, #0 — dividend lo word of INT64_MIN
8588    bytes.extend_from_slice(&0x2800u16.to_le_bytes());
8589    // BNE .no_trap
8590    bytes.extend_from_slice(&0xD103u16.to_le_bytes());
8591    // CMP.W R1, #0x80000000 — dividend hi word of INT64_MIN
8592    bytes.extend_from_slice(&0xF1B1u16.to_le_bytes());
8593    bytes.extend_from_slice(&0x4F00u16.to_le_bytes());
8594    // BNE .no_trap
8595    bytes.extend_from_slice(&0xD100u16.to_le_bytes());
8596    // UDF #0 — signed-division overflow
8597    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
8598    // .no_trap:
8599}
8600
8601// ======================================================================
8602// #615 — A32 (ARM-mode) twins of the #610 i64 fixed-ABI wrappers above.
8603// Identical register contract, A32 encodings: the multi-instruction i64
8604// cores (rotl/rotr, div/rem) compute in fixed low registers (value/dividend
8605// R0:R1, amount R2 / divisor R2:R3, result to R0:R1); the wrappers marshal
8606// the selector-assigned operand registers in and the result out, saving and
8607// restoring the caller-visible R0-R3 around the core.
8608// ======================================================================
8609
8610/// A32 steps 1+2: `STMDB SP!, {R0-R3}`, then marshal `srcs` into `R0..R<n>`
8611/// via individual stack pushes (`STR src, [SP, #-4]!` in reverse order, then
8612/// `LDR Ri, [SP], #4`). Every source is read before any fixed register is
8613/// written, so arbitrary source/target permutations are safe.
8614fn emit_a32_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8615    debug_assert!(srcs.len() <= 4);
8616    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8617    // PUSH {R0-R3} — save the caller-visible low registers.
8618    w(bytes, 0xE92D_000F);
8619    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8620    for src in srcs.iter().rev() {
8621        w(bytes, 0xE52D_0004 | (reg_to_bits(src) << 12));
8622    }
8623    // LDR Ri, [SP], #4 — Ri := srcs[i].
8624    for i in 0..srcs.len() as u32 {
8625        w(bytes, 0xE49D_0004 | (i << 12));
8626    }
8627}
8628
8629/// A32 steps 4+5: move the core's R0:R1 result into the selector's rd pair,
8630/// then restore the R0-R3 saved by [`emit_a32_i64_fixed_abi_entry`], skipping
8631/// any register the result now lives in (its saved caller word is discarded).
8632fn emit_a32_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8633    let lo = reg_to_bits(rdlo);
8634    let hi = reg_to_bits(rdhi);
8635    if lo == 1 && hi == 0 {
8636        // A fully swapped pair would clobber one half in either MOV order.
8637        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8638        return Err(synth_core::Error::synthesis(
8639            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8640        ));
8641    }
8642    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8643    let mov = |bytes: &mut Vec<u8>, rd: u32, rm: u32| w(bytes, 0xE1A0_0000 | (rd << 12) | rm);
8644    if hi == 0 {
8645        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8646        mov(bytes, lo, 0);
8647        mov(bytes, hi, 1);
8648    } else {
8649        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8650        mov(bytes, hi, 1);
8651        mov(bytes, lo, 0);
8652    }
8653    for i in 0..4u32 {
8654        if i == lo || i == hi {
8655            // The result lives here — drop the saved caller word.
8656            w(bytes, 0xE28D_D004); // ADD SP, SP, #4
8657        } else {
8658            w(bytes, 0xE49D_0004 | (i << 12)); // LDR Ri, [SP], #4
8659        }
8660    }
8661    Ok(())
8662}
8663
8664/// A32 zero-divisor trap, emitted after marshaling when the divisor pair is
8665/// in R2:R3: `ORRS R12, R2, R3` sets Z iff the divisor is zero; `BNE` skips a
8666/// `UDF #0` (WASM div/rem-by-zero must trap, matching the Thumb-2 twin).
8667fn emit_a32_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8668    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8669    w(bytes, 0xE192_C003); // ORRS R12, R2, R3
8670    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8671    w(bytes, 0xE7F0_00F0); // UDF #0 — divide by zero
8672}
8673
8674/// A32 twin of [`emit_i64_divs_overflow_trap`] (#633): trap on
8675/// `i64.div_s(INT64_MIN, -1)`. Conditional execution replaces the Thumb
8676/// branches — the CMPEQ chain leaves EQ set only when divisor == -1 AND
8677/// dividend == INT64_MIN. div_s only; rem_s must keep returning 0.
8678fn emit_a32_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8679    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8680    w(bytes, 0xE002_C003); // AND   R12, R2, R3 (== 0xFFFFFFFF iff divisor == -1)
8681    w(bytes, 0xE37C_0001); // CMN   R12, #1     (EQ iff divisor == -1)
8682    w(bytes, 0x0350_0000); // CMPEQ R0, #0      (EQ iff also dividend lo == 0)
8683    w(bytes, 0x0351_0102); // CMPEQ R1, #0x80000000 (EQ iff dividend == INT64_MIN)
8684    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8685    w(bytes, 0xE7F0_00F0); // UDF #0 — signed-division overflow
8686}
8687
8688/// Fallible form of the `verify_reg_bits` contract. PC (R15) is not a valid
8689/// data operand for the Thumb-2 encodings that use this guard (SDIV/UDIV/MLS/…
8690/// are UNPREDICTABLE with PC). Synth's own codegen never emits PC there, but
8691/// the encoder must stay *total* over arbitrary `ArmOp` inputs — the fuzz
8692/// harness (`encoder_no_panic`) requires Ok-or-Err, never a panic. Pre-fix, the
8693/// `debug_assert` in `verify_reg_bits` aborted under `-Cdebug-assertions`.
8694/// Returns a typed Err instead. See #185.
8695fn reg_bits_checked(bits: u32) -> Result<()> {
8696    if bits > 14 {
8697        return Err(synth_core::Error::synthesis(format!(
8698            "register bits {bits} (PC/R15) is not a valid operand for this Thumb-2 encoding"
8699        )));
8700    }
8701    Ok(())
8702}
8703
8704/// Try to encode a 32-bit value as an ARM rotated immediate (imm8 ROR 2*rot4).
8705/// Returns Some((encoded_bits, 1)) if representable, None otherwise.
8706fn try_encode_rotated_imm(val: u32) -> Option<(u32, u32)> {
8707    if val == 0 {
8708        return Some((0, 1));
8709    }
8710    for rot in 0..16u32 {
8711        let shift = rot * 2;
8712        // Rotate left by shift (undo the ROR) to see if result fits in 8 bits
8713        let unrotated = val.rotate_left(shift);
8714        if unrotated <= 0xFF {
8715            // Encoded as: rot4(4 bits) | imm8(8 bits) = rotate_imm << 8 | imm8
8716            return Some(((rot << 8) | unrotated, 1));
8717        }
8718    }
8719    None
8720}
8721
8722/// Encode operand2 field and return (bits, immediate_flag).
8723/// For ARM32 mode, immediates use the rotated-immediate encoding (imm8 ROR 2*rot4).
8724/// Panics if an immediate value cannot be represented. Callers that need large
8725/// immediates should use MOVW/MOVT instead of Operand2::Imm.
8726fn encode_operand2(op2: &Operand2) -> Result<(u32, u32)> {
8727    match op2 {
8728        Operand2::Imm(val) => {
8729            let uval = *val as u32;
8730            // Attempt rotated-immediate encoding (ARM32 Operand2)
8731            if let Some(encoded) = try_encode_rotated_imm(uval) {
8732                Ok(encoded)
8733            } else {
8734                // #378-class honesty: an immediate that can't be expressed as an
8735                // ARM32 rotated immediate is an INTERNAL selector bug — large
8736                // constants must be materialized via MOVW/MOVT, not passed here.
8737                // FAIL HONESTLY with an Err rather than silently masking to
8738                // `uval & 0xFF` and emitting a WRONG immediate. The encoder is
8739                // Ok-or-Err, never corrupt (#180/#185); a loud Err is also why
8740                // this is an Err and not a panic (the `encoder_no_panic` fuzz
8741                // contract — malformed/oversized input must degrade, not crash).
8742                Err(synth_core::Error::synthesis(format!(
8743                    "encode_operand2: immediate {uval:#x} ({val}) is not an ARM32 \
8744                     rotated immediate — the selector must materialize large \
8745                     constants via MOVW/MOVT"
8746                )))
8747            }
8748        }
8749
8750        Operand2::Reg(reg) => {
8751            let reg_bits = reg_to_bits(reg);
8752            Ok((reg_bits, 0)) // I=0 for register
8753        }
8754
8755        Operand2::RegShift {
8756            rm,
8757            shift: _,
8758            amount,
8759        } => {
8760            // Simplified encoding with shift
8761            let rm_bits = reg_to_bits(rm);
8762            let shift_bits = (*amount & 0x1F) << 7;
8763            Ok((shift_bits | rm_bits, 0))
8764        }
8765    }
8766}
8767
8768/// Largest immediate an A32 `LDR`/`STR`/`LDRB`/`STRB` `[Rn, #imm12]` form holds.
8769const A32_LDST_IMM12_MAX: u32 = 0xFFF;
8770/// Largest immediate an A32 `LDRH`/`STRH`/`LDRSB`/`LDRSH` `[Rn, #imm8]`
8771/// (`imm4H:imm4L`) form holds.
8772const A32_LDST_IMM8_MAX: u32 = 0xFF;
8773/// Largest LOW-half immediate an A32 `I64Ldr`/`I64Str` pair can fold: the high
8774/// half sits at `+4`, which must itself fit imm12 (`0xFFB + 4 = 0xFFF`).
8775const A32_I64_PAIR_IMM12_MAX: u32 = 0xFFB;
8776
8777/// RQ-63-ARMI64OFF (#1165): resolve the A32 base register and residual
8778/// immediate for a load/store whose static offset may exceed the form's
8779/// immediate field — the A32 counterpart of the Thumb-2 `i64_effective_base`
8780/// (#372/#382). Returns `(base, residual)`; the caller encodes `[base,
8781/// #residual]` (and `#residual + 4` for the i64 high half).
8782///
8783/// The offset is the wasm memarg round-tripped through the selector's `as i32`
8784/// cast (a memarg `>= 2^31` arrives negative), so it is read back as `u32` and
8785/// the effective address is `base + index + offset (mod 2^32)` — the same
8786/// arithmetic the Thumb-2 word path (`encode_thumb32_add_imm`) and the
8787/// `--safety-bounds software` guard (`software_bounds_guard`, which round-trips
8788/// the same cast) already use. A clamp or a mask here is a WRONG ADDRESS, i.e.
8789/// the silent memory miscompile this artifact exists to refuse: before this
8790/// helper the A32 immediate arms masked `& 0xFFF` (word/byte) and `& 0xFF`
8791/// (halfword/signed), so `i32.load offset=5000` on the `-b arm` default target
8792/// silently read `[ip, #904]`, and the i64 pair arm declined outright (46 of
8793/// the 110 core-module declines in v0.62's ARM census).
8794///
8795/// - `offset <= imm_max`, no index: emits nothing, returns `(addr.base, offset)`
8796///   — byte-identical to the pre-#1165 immediate form.
8797/// - `offset <= imm_max`, index `rm`: emits `ADD ip, base, rm`, returns
8798///   `(ip, offset)` — byte-identical to the #206 register-offset form.
8799/// - `offset > imm_max`: MATERIALIZES the whole effective base into `ip`:
8800///   `MOVW ip, #lo16 ; [MOVT ip, #hi16 — only when non-zero] ; ADD ip, ip, base
8801///   ; [ADD ip, ip, rm]`, returns `(ip, 0)`. `ip` is fully computed BEFORE the
8802///   access, so a destination register aliasing `base` or `rm` is safe.
8803///
8804/// `ip` (R12) is the reserved encoder scratch — never allocated (pool R0–R8;
8805/// R9/R10/R11 are the globals/size/base contract registers). If `base` or `rm`
8806/// IS R12 the materialization would clobber its own input, so that case is a
8807/// typed `Err` — a loud decline, never a wrong address (the alias trap
8808/// `encode_thumb32_add_imm` carries for `rd == rn == R12`).
8809///
8810/// Bytes verified against `arm-none-eabi-as` in the `test_1165_a32_*` tests
8811/// and executed by `scripts/repro/a32_ldst_offset_1165_differential.py`.
8812fn a32_effective_base(bytes: &mut Vec<u8>, addr: &MemAddr, imm_max: u32) -> Result<(Reg, u32)> {
8813    let ip = Reg::R12;
8814    let ip_bits = reg_to_bits(&ip);
8815    let offset = addr.offset as u32;
8816    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8817    // ADD rd, rn, rm (cond=AL, opcode=ADD, S=0, register operand2).
8818    let add_reg = |rd: u32, rn: u32, rm: u32| 0xE080_0000 | (rn << 16) | (rd << 12) | rm;
8819    if offset <= imm_max {
8820        return Ok(match addr.offset_reg {
8821            Some(rm) => {
8822                w(
8823                    bytes,
8824                    add_reg(ip_bits, reg_to_bits(&addr.base), reg_to_bits(&rm)),
8825                );
8826                (ip, offset)
8827            }
8828            None => (addr.base, offset),
8829        });
8830    }
8831    if addr.base == ip || addr.offset_reg == Some(ip) {
8832        return Err(synth_core::Error::synthesis(format!(
8833            "A32 load/store offset {offset:#x} exceeds the immediate field (max {imm_max:#x}) \
8834             and the address already uses the R12 scratch — no free register to \
8835             materialize it (RQ-63-ARMI64OFF)"
8836        )));
8837    }
8838    // MOVW ip, #lo16 ; MOVT ip, #hi16 (elided when zero, mirroring I64Const).
8839    let lo16 = offset & 0xFFFF;
8840    let hi16 = offset >> 16;
8841    w(
8842        bytes,
8843        0xE300_0000 | ((lo16 >> 12) << 16) | (ip_bits << 12) | (lo16 & 0xFFF),
8844    );
8845    if hi16 != 0 {
8846        w(
8847            bytes,
8848            0xE340_0000 | ((hi16 >> 12) << 16) | (ip_bits << 12) | (hi16 & 0xFFF),
8849        );
8850    }
8851    w(bytes, add_reg(ip_bits, ip_bits, reg_to_bits(&addr.base)));
8852    if let Some(rm) = addr.offset_reg {
8853        w(bytes, add_reg(ip_bits, ip_bits, reg_to_bits(&rm)));
8854    }
8855    Ok((ip, 0))
8856}
8857
8858/// Encode an A32 immediate-form memory address to `(base_reg, imm12)`.
8859///
8860/// RQ-63-ARMI64OFF (#1165): this used to mask `& 0xFFF`, silently re-targeting
8861/// any offset past the field (the #259 class, closed on Thumb-2 by
8862/// `check_ldst_imm12` but never here). The immediate arms are only reached with
8863/// an in-range, index-free address — `encode_arm_reg_offset_mem` materializes
8864/// everything else into IP first — so this is a TRIPWIRE: a typed error, never
8865/// a wrong address, should a future arm bypass that pre-pass.
8866fn encode_mem_addr(addr: &MemAddr) -> Result<(u32, u32)> {
8867    check_a32_imm_addr(addr, A32_LDST_IMM12_MAX)
8868}
8869
8870/// `encode_mem_addr` for the `imm4H:imm4L` (8-bit) forms — `LDRH`/`STRH`/
8871/// `LDRSB`/`LDRSH`. Same tripwire contract; the former `& 0xFF` mask is gone.
8872fn encode_mem_addr_imm8(addr: &MemAddr) -> Result<(u32, u32)> {
8873    check_a32_imm_addr(addr, A32_LDST_IMM8_MAX)
8874}
8875
8876fn check_a32_imm_addr(addr: &MemAddr, imm_max: u32) -> Result<(u32, u32)> {
8877    if addr.offset_reg.is_some() {
8878        return Err(synth_core::Error::synthesis(
8879            "internal: A32 immediate-form load/store reached with a register offset — \
8880             encode_arm_reg_offset_mem must materialize it first (#206)",
8881        ));
8882    }
8883    let offset = addr.offset as u32;
8884    if offset > imm_max {
8885        return Err(synth_core::Error::synthesis(format!(
8886            "internal: A32 load/store immediate offset {offset:#x} exceeds the {imm_max:#x} \
8887             field — encode_arm_reg_offset_mem must materialize it first (RQ-63-ARMI64OFF)"
8888        )));
8889    }
8890    Ok((reg_to_bits(&addr.base), offset))
8891}
8892
8893/// S-register number: S0=0, S1=1, ..., S31=31
8894fn vfp_sreg_to_num(reg: &VfpReg) -> Result<u32> {
8895    match reg {
8896        VfpReg::S0 => Ok(0),
8897        VfpReg::S1 => Ok(1),
8898        VfpReg::S2 => Ok(2),
8899        VfpReg::S3 => Ok(3),
8900        VfpReg::S4 => Ok(4),
8901        VfpReg::S5 => Ok(5),
8902        VfpReg::S6 => Ok(6),
8903        VfpReg::S7 => Ok(7),
8904        VfpReg::S8 => Ok(8),
8905        VfpReg::S9 => Ok(9),
8906        VfpReg::S10 => Ok(10),
8907        VfpReg::S11 => Ok(11),
8908        VfpReg::S12 => Ok(12),
8909        VfpReg::S13 => Ok(13),
8910        VfpReg::S14 => Ok(14),
8911        VfpReg::S15 => Ok(15),
8912        VfpReg::S16 => Ok(16),
8913        VfpReg::S17 => Ok(17),
8914        VfpReg::S18 => Ok(18),
8915        VfpReg::S19 => Ok(19),
8916        VfpReg::S20 => Ok(20),
8917        VfpReg::S21 => Ok(21),
8918        VfpReg::S22 => Ok(22),
8919        VfpReg::S23 => Ok(23),
8920        VfpReg::S24 => Ok(24),
8921        VfpReg::S25 => Ok(25),
8922        VfpReg::S26 => Ok(26),
8923        VfpReg::S27 => Ok(27),
8924        VfpReg::S28 => Ok(28),
8925        VfpReg::S29 => Ok(29),
8926        VfpReg::S30 => Ok(30),
8927        VfpReg::S31 => Ok(31),
8928        // D-registers are not used in F32 single-precision encodings
8929        _ => Err(synth_core::Error::SynthesisError(
8930            "D-register not supported in single-precision VFP encoding".to_string(),
8931        )),
8932    }
8933}
8934
8935/// D-register number: D0=0, D1=1, ..., D15=15
8936fn vfp_dreg_to_num(reg: &VfpReg) -> Result<u32> {
8937    match reg {
8938        VfpReg::D0 => Ok(0),
8939        VfpReg::D1 => Ok(1),
8940        VfpReg::D2 => Ok(2),
8941        VfpReg::D3 => Ok(3),
8942        VfpReg::D4 => Ok(4),
8943        VfpReg::D5 => Ok(5),
8944        VfpReg::D6 => Ok(6),
8945        VfpReg::D7 => Ok(7),
8946        VfpReg::D8 => Ok(8),
8947        VfpReg::D9 => Ok(9),
8948        VfpReg::D10 => Ok(10),
8949        VfpReg::D11 => Ok(11),
8950        VfpReg::D12 => Ok(12),
8951        VfpReg::D13 => Ok(13),
8952        VfpReg::D14 => Ok(14),
8953        VfpReg::D15 => Ok(15),
8954        // S-registers are not used in F64 double-precision encodings
8955        _ => Err(synth_core::Error::SynthesisError(
8956            "S-register not supported in double-precision VFP encoding".to_string(),
8957        )),
8958    }
8959}
8960
8961/// Split S-register into (Vx[3:0], qualifier_bit) for VFP encoding.
8962/// For an S-register number s: Vx = s >> 1, qualifier = s & 1.
8963/// The qualifier bit goes to D (bit 22), N (bit 7), or M (bit 5) depending on role.
8964fn encode_sreg(s: u32) -> (u32, u32) {
8965    (s >> 1, s & 1)
8966}
8967
8968/// Split D-register into (Vx[3:0], qualifier_bit) for VFP double-precision encoding.
8969/// For a D-register number d: Vx = d & 0xF, qualifier = (d >> 4) & 1.
8970/// For D0-D15, qualifier is always 0.
8971fn encode_dreg(d: u32) -> (u32, u32) {
8972    (d & 0xF, (d >> 4) & 1)
8973}
8974
8975/// Encode a VFP 3-register arithmetic instruction (VADD.F32, VSUB.F32, VMUL.F32, VDIV.F32).
8976/// Returns the full 32-bit instruction word.
8977///
8978/// VFP encoding: [cond 1110] [D opc1 Vn] [Vd 101 sz] [N opc2 M 0 Vm]
8979/// For single-precision (sz=0), coprocessor = 0xA (bits[11:8]).
8980fn encode_vfp_3reg(base: u32, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<u32> {
8981    let sd_num = vfp_sreg_to_num(sd)?;
8982    let sn_num = vfp_sreg_to_num(sn)?;
8983    let sm_num = vfp_sreg_to_num(sm)?;
8984    let (vd, d) = encode_sreg(sd_num);
8985    let (vn, n) = encode_sreg(sn_num);
8986    let (vm, m) = encode_sreg(sm_num);
8987
8988    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8989}
8990
8991/// Encode a VFP 2-register instruction (VNEG.F32, VABS.F32, VSQRT.F32).
8992/// Returns the full 32-bit instruction word.
8993fn encode_vfp_2reg(base: u32, sd: &VfpReg, sm: &VfpReg) -> Result<u32> {
8994    let sd_num = vfp_sreg_to_num(sd)?;
8995    let sm_num = vfp_sreg_to_num(sm)?;
8996    let (vd, d) = encode_sreg(sd_num);
8997    let (vm, m) = encode_sreg(sm_num);
8998
8999    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
9000}
9001
9002/// Encode a VFP load/store (VLDR.F32 / VSTR.F32).
9003/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
9004/// U bit (bit 23) controls add/subtract offset.
9005fn encode_vfp_ldst(base: u32, sd: &VfpReg, addr: &MemAddr) -> Result<u32> {
9006    let sd_num = vfp_sreg_to_num(sd)?;
9007    let (vd, d) = encode_sreg(sd_num);
9008    let rn = reg_to_bits(&addr.base);
9009
9010    let offset = addr.offset;
9011    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9012    let abs_offset = offset.unsigned_abs();
9013    let imm8 = (abs_offset / 4) & 0xFF;
9014
9015    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
9016}
9017
9018/// Encode VMOV between core register and S-register.
9019/// VMOV Sn, Rt: 0xEE00_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
9020/// VMOV Rt, Sn: 0xEE10_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
9021fn encode_vmov_core_sreg(to_sreg: bool, sreg: &VfpReg, core: &Reg) -> Result<u32> {
9022    let s_num = vfp_sreg_to_num(sreg)?;
9023    let (vn, n) = encode_sreg(s_num);
9024    let rt = reg_to_bits(core);
9025
9026    let base = if to_sreg { 0xEE000A10 } else { 0xEE100A10 };
9027    Ok(base | (vn << 16) | (rt << 12) | (n << 7))
9028}
9029
9030/// Encode a VFP 3-register double-precision instruction (VADD.F64, VSUB.F64, etc.).
9031/// For double-precision (sz=1), coprocessor = 0xB (bits[11:8]).
9032/// The base should have bit 8 = 1 for F64 (0xB suffix instead of 0xA).
9033fn encode_vfp_3reg_f64(base: u32, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<u32> {
9034    let dd_num = vfp_dreg_to_num(dd)?;
9035    let dn_num = vfp_dreg_to_num(dn)?;
9036    let dm_num = vfp_dreg_to_num(dm)?;
9037    let (vd, d) = encode_dreg(dd_num);
9038    let (vn, n) = encode_dreg(dn_num);
9039    let (vm, m) = encode_dreg(dm_num);
9040
9041    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
9042}
9043
9044/// Encode a VFP 2-register double-precision instruction (VNEG.F64, VABS.F64, VSQRT.F64).
9045fn encode_vfp_2reg_f64(base: u32, dd: &VfpReg, dm: &VfpReg) -> Result<u32> {
9046    let dd_num = vfp_dreg_to_num(dd)?;
9047    let dm_num = vfp_dreg_to_num(dm)?;
9048    let (vd, d) = encode_dreg(dd_num);
9049    let (vm, m) = encode_dreg(dm_num);
9050
9051    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
9052}
9053
9054/// Encode a VFP load/store for double-precision (VLDR.64 / VSTR.64).
9055/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
9056fn encode_vfp_ldst_f64(base: u32, dd: &VfpReg, addr: &MemAddr) -> Result<u32> {
9057    let dd_num = vfp_dreg_to_num(dd)?;
9058    let (vd, d) = encode_dreg(dd_num);
9059    let rn = reg_to_bits(&addr.base);
9060
9061    let offset = addr.offset;
9062    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9063    let abs_offset = offset.unsigned_abs();
9064    let imm8 = (abs_offset / 4) & 0xFF;
9065
9066    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
9067}
9068
9069/// Encode VMOV between two core registers and a D-register.
9070/// VMOV Dm, Rt, Rt2: 0xEC40_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
9071/// VMOV Rt, Rt2, Dm: 0xEC50_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
9072fn encode_vmov_core_dreg(
9073    to_dreg: bool,
9074    dreg: &VfpReg,
9075    core_lo: &Reg,
9076    core_hi: &Reg,
9077) -> Result<u32> {
9078    let d_num = vfp_dreg_to_num(dreg)?;
9079    let (vm, m) = encode_dreg(d_num);
9080    let rt = reg_to_bits(core_lo);
9081    let rt2 = reg_to_bits(core_hi);
9082
9083    let base = if to_dreg { 0xEC400B10 } else { 0xEC500B10 };
9084    Ok(base | (rt2 << 16) | (rt << 12) | (m << 5) | vm)
9085}
9086
9087/// Emit a VFP 32-bit instruction as Thumb-2 bytes (two LE halfwords).
9088fn vfp_to_thumb_bytes(instr: u32) -> Vec<u8> {
9089    let hw1 = ((instr >> 16) & 0xFFFF) as u16;
9090    let hw2 = (instr & 0xFFFF) as u16;
9091    let mut bytes = hw1.to_le_bytes().to_vec();
9092    bytes.extend_from_slice(&hw2.to_le_bytes());
9093    bytes
9094}
9095
9096// ============================================================================
9097// Helium MVE encoding helpers
9098// ============================================================================
9099
9100/// Q-register number: Q0=0, Q1=1, ..., Q7=7
9101fn qreg_to_num(reg: &QReg) -> u32 {
9102    match reg {
9103        QReg::Q0 => 0,
9104        QReg::Q1 => 1,
9105        QReg::Q2 => 2,
9106        QReg::Q3 => 3,
9107        QReg::Q4 => 4,
9108        QReg::Q5 => 5,
9109        QReg::Q6 => 6,
9110        QReg::Q7 => 7,
9111    }
9112}
9113
9114/// MVE element size to encoding bits: S8=0b00, S16=0b01, S32=0b10
9115fn mve_size_bits(size: &MveSize) -> u32 {
9116    match size {
9117        MveSize::S8 => 0b00,
9118        MveSize::S16 => 0b01,
9119        MveSize::S32 => 0b10,
9120    }
9121}
9122
9123/// Encode MVE 3-register instruction.
9124/// Q-registers are encoded as D-register pairs: Q0=D0:D1, Q1=D2:D3, etc.
9125/// In NEON/MVE encoding, the Q-register uses D-register number = Qn * 2.
9126fn encode_mve_3reg(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
9127    let d = qreg_to_num(qd) * 2;
9128    let n = qreg_to_num(qn) * 2;
9129    let m = qreg_to_num(qm) * 2;
9130
9131    // Standard NEON/MVE 3-register encoding:
9132    // D bit (bit 22) = Vd[4], Vd[3:0] = bits [15:12]
9133    // N bit (bit 7)  = Vn[4], Vn[3:0] = bits [19:16]
9134    // M bit (bit 5)  = Vm[4], Vm[3:0] = bits [3:0]
9135    let vd = d & 0xF;
9136    let d_bit = (d >> 4) & 1;
9137    let vn = n & 0xF;
9138    let n_bit = (n >> 4) & 1;
9139    let vm = m & 0xF;
9140    let m_bit = (m >> 4) & 1;
9141
9142    base | (d_bit << 22) | (vn << 16) | (vd << 12) | (n_bit << 7) | (m_bit << 5) | vm
9143}
9144
9145/// Encode MVE 3-register bitwise instruction (VAND, VORR, VEOR, VBIC).
9146fn encode_mve_3reg_bitwise(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
9147    encode_mve_3reg(base, qd, qn, qm)
9148}
9149
9150/// Encode MVE VLDRW.32 Qd, [Rn, #offset]
9151/// Format: EC9x xxxx - contiguous load, word-sized elements
9152fn encode_mve_vldrw(qd: &QReg, addr: &MemAddr) -> u32 {
9153    let qd_enc = qreg_to_num(qd) * 2;
9154    let rn = reg_to_bits(&addr.base);
9155    let offset = addr.offset;
9156    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9157    let abs_offset = offset.unsigned_abs();
9158    let imm7 = (abs_offset / 4) & 0x7F; // 7-bit word-aligned offset
9159
9160    // VLDRW.32 Qd, [Rn, #imm]: ED10 xx80 variant
9161    0xED100E80
9162        | (u_bit << 23)
9163        | ((qd_enc >> 4) << 22)
9164        | (rn << 16)
9165        | ((qd_enc & 0xF) << 12)
9166        | (imm7 & 0x7F)
9167}
9168
9169/// Encode MVE VSTRW.32 Qd, [Rn, #offset]
9170fn encode_mve_vstrw(qd: &QReg, addr: &MemAddr) -> u32 {
9171    let qd_enc = qreg_to_num(qd) * 2;
9172    let rn = reg_to_bits(&addr.base);
9173    let offset = addr.offset;
9174    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9175    let abs_offset = offset.unsigned_abs();
9176    let imm7 = (abs_offset / 4) & 0x7F;
9177
9178    0xED000E80
9179        | (u_bit << 23)
9180        | ((qd_enc >> 4) << 22)
9181        | (rn << 16)
9182        | ((qd_enc & 0xF) << 12)
9183        | (imm7 & 0x7F)
9184}
9185
9186impl ArmEncoder {
9187    /// Encode MVE constant load: MOVW+MOVT+VMOV for each 32-bit word, then assemble Q-register
9188    fn encode_thumb_mve_const(&self, qd: &QReg, bytes: &[u8; 16]) -> Result<Vec<u8>> {
9189        let mut result = Vec::new();
9190        let qd_num = qreg_to_num(qd);
9191
9192        // Load each 32-bit word into R12 (temp) then VMOV into S-register
9193        for i in 0..4 {
9194            let word = u32::from_le_bytes([
9195                bytes[i * 4],
9196                bytes[i * 4 + 1],
9197                bytes[i * 4 + 2],
9198                bytes[i * 4 + 3],
9199            ]);
9200            let lo16 = word & 0xFFFF;
9201            let hi16 = (word >> 16) & 0xFFFF;
9202
9203            // MOVW R12, #lo16
9204            result.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
9205            // MOVT R12, #hi16
9206            if hi16 != 0 {
9207                result.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
9208            }
9209
9210            // VMOV Sn, R12 where Sn = Qd*4 + i
9211            let s_num = qd_num * 4 + i as u32;
9212            let (vn, n) = encode_sreg(s_num);
9213            let vmov: u32 = 0xEE000A10 | (vn << 16) | (12 << 12) | (n << 7);
9214            result.extend_from_slice(&vfp_to_thumb_bytes(vmov));
9215        }
9216
9217        Ok(result)
9218    }
9219
9220    /// Encode lane-wise f32 binary operation (VDIV, etc.) via S-register extraction
9221    fn encode_thumb_mve_lane_wise_f32_binop(
9222        &self,
9223        qd: &QReg,
9224        qn: &QReg,
9225        qm: &QReg,
9226        vfp_base: u32,
9227    ) -> Result<Vec<u8>> {
9228        let mut result = Vec::new();
9229        let qd_num = qreg_to_num(qd);
9230        let qn_num = qreg_to_num(qn);
9231        let qm_num = qreg_to_num(qm);
9232
9233        // For each lane 0..3: use S-registers directly (Q aliasing)
9234        for i in 0..4u32 {
9235            let sd = qd_num * 4 + i;
9236            let sn = qn_num * 4 + i;
9237            let sm = qm_num * 4 + i;
9238
9239            let (vd, d) = encode_sreg(sd);
9240            let (vn, n) = encode_sreg(sn);
9241            let (vm, m) = encode_sreg(sm);
9242
9243            let instr = vfp_base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
9244            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9245        }
9246
9247        Ok(result)
9248    }
9249
9250    /// Encode lane-wise f32 VSQRT via S-register extraction
9251    fn encode_thumb_mve_lane_wise_f32_sqrt(&self, qd: &QReg, qm: &QReg) -> Result<Vec<u8>> {
9252        let mut result = Vec::new();
9253        let qd_num = qreg_to_num(qd);
9254        let qm_num = qreg_to_num(qm);
9255
9256        // VSQRT.F32 base: 0xEEB10AC0
9257        for i in 0..4u32 {
9258            let sd = qd_num * 4 + i;
9259            let sm = qm_num * 4 + i;
9260
9261            let (vd, d) = encode_sreg(sd);
9262            let (vm, m) = encode_sreg(sm);
9263
9264            let instr: u32 = 0xEEB10AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
9265            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9266        }
9267
9268        Ok(result)
9269    }
9270}
9271
9272/// VCR-TIER-001 (#1021/#1048) — the SCRATCH CONTRACT of an `ArmOp`'s encoder
9273/// expansion: the registers, beyond the op's declared RESULT registers and the
9274/// globally sanctioned R12/IP encoder scratch, that the expansion may leave
9275/// modified when it completes. Transient-but-restored traffic (push/pop through
9276/// the expansion's own stack red-zone, SP restored on exit) is not "modified".
9277///
9278/// This is the SINGLE declaration site for that contract — the one place a
9279/// future expansion that must borrow a register says so (the repo rule: one
9280/// declaration site, no duplicate copy that can silently drift). It is deliberately NOT derived
9281/// from the expansion's observed behavior: a contract read off the bytes would
9282/// rubber-stamp any clobber. Intent is declared here; the canary gate
9283/// (`scripts/repro/expansion_canary_gate_1021.py`) executes the REAL emitted
9284/// bytes of every variant the shipped rule table emits, on both backends, with
9285/// every non-contract register holding a distinctive canary, and fails on any
9286/// undeclared write — and on any declared register the expansion never
9287/// actually writes, so an over-broad declaration cannot hollow the gate.
9288///
9289/// The default is the STRICTEST reading — result registers only — which is
9290/// exactly the silent claim the atomic `ArmSemantics` pseudo-op model already
9291/// makes (#1021: an atomic model of a multi-instruction expansion is a silent
9292/// claim that the expansion is scratch-free). Today the table is EMPTY: #1039
9293/// reworked `i32.popcnt` off R11 (the linear-memory base) and #1048 reworked
9294/// the i64 shifts and bit-counts off their own operand registers, so every
9295/// expansion of every rule-emitted variant is R12-only on both Thumb-2 and
9296/// A32. Backend-independent for the same reason; if a backend's expansion ever
9297/// diverges, this signature grows a backend parameter in the same PR.
9298pub fn expansion_scratch_contract(op: &ArmOp) -> &'static [Reg] {
9299    // No variant currently borrows any register beyond R12. A new declaration
9300    // is added as a `match op { .. }` arm here — nowhere else.
9301    let _ = op;
9302    &[]
9303}
9304
9305#[cfg(test)]
9306mod tests {
9307    use super::*;
9308
9309    #[test]
9310    fn test_encoder_creation() {
9311        let encoder_arm = ArmEncoder::new_arm32();
9312        assert!(!encoder_arm.thumb_mode);
9313
9314        let encoder_thumb = ArmEncoder::new_thumb2();
9315        assert!(encoder_thumb.thumb_mode);
9316    }
9317
9318    /// #204 WAKE-path regression: `SetCond` materialized 0/1 with the 16-bit
9319    /// `MOVS Rd,#imm` (T1), whose Rd field is 3 bits (R0–R7). For a high Rd
9320    /// (R8–R12) `rd_bits << 8` overflows bit 11, flipping the opcode MOVS→CMP
9321    /// (`0x2c00`), so the boolean was never written — gale's `has_waiter` kept a
9322    /// stale value and the binary-sem WAKE dispatch read garbage. High Rd must
9323    /// use the 32-bit `MOV.W` (T2). Verify the bytes, not the IR.
9324    /// #311: the SAME high-Rd MOVS→CMP transmutation as #204, but in the
9325    /// i64 comparison expansions (I64SetCond / I64SetCondZ) — missed by the
9326    /// #204 hardening. With rd=R8 the boolean died in the flags
9327    /// (`ite eq; cmpeq r0,#1; cmpne r0,#0`), so gale's packed-u64 select
9328    /// read a stale register on silicon. High Rd must take MOV.W / CMP.W.
9329    #[test]
9330    fn test_encode_i64setcond_high_reg_uses_mov_w_311() {
9331        use synth_synthesis::{ArmOp, Condition, Reg};
9332        let enc = ArmEncoder::new_thumb2();
9333        let bytes = enc
9334            .encode(&ArmOp::I64SetCond {
9335                rd: Reg::R8,
9336                rn_lo: Reg::R2,
9337                rn_hi: Reg::R3,
9338                rm_lo: Reg::R6,
9339                rm_hi: Reg::R7,
9340                cond: Condition::EQ,
9341            })
9342            .unwrap();
9343        // The 32-bit MOV.W immediate (T2) first halfword is 0xF04F; the
9344        // 16-bit transmuted forms would contain 0x2801/0x2800 (CMP r0,#1/#0).
9345        let halfwords: Vec<u16> = bytes
9346            .chunks(2)
9347            .map(|c| u16::from_le_bytes([c[0], c[1]]))
9348            .collect();
9349        assert!(
9350            halfwords.iter().filter(|&&h| h == 0xF04F).count() == 2,
9351            "high rd must use two MOV.W (T2) encodings, got {halfwords:04x?}"
9352        );
9353        assert!(
9354            !halfwords.contains(&0x2801) && !halfwords.contains(&0x2800),
9355            "no transmuted 16-bit CMP imm: {halfwords:04x?}"
9356        );
9357
9358        let bytes_z = enc
9359            .encode(&ArmOp::I64SetCondZ {
9360                rd: Reg::R8,
9361                rn_lo: Reg::R2,
9362                rn_hi: Reg::R3,
9363            })
9364            .unwrap();
9365        let hw_z: Vec<u16> = bytes_z
9366            .chunks(2)
9367            .map(|c| u16::from_le_bytes([c[0], c[1]]))
9368            .collect();
9369        assert!(
9370            hw_z.iter().filter(|&&h| h == 0xF04F).count() == 2,
9371            "SetCondZ high rd MOV.W: {hw_z:04x?}"
9372        );
9373        // CMP.W rd,#0 (T2) first halfword: 0xF1B0 | rd
9374        assert!(
9375            hw_z.contains(&(0xF1B0 | 8)),
9376            "SetCondZ high rd must use CMP.W: {hw_z:04x?}"
9377        );
9378    }
9379
9380    #[test]
9381    fn test_encode_setcond_high_reg_uses_mov_w_204() {
9382        use synth_synthesis::{ArmOp, Condition, Reg};
9383        let enc = ArmEncoder::new_thumb2();
9384        // R12 (high): must be ITE + MOV.W #1 + MOV.W #0, never a 16-bit MOVS/CMP.
9385        let hi = enc
9386            .encode(&ArmOp::SetCond {
9387                rd: Reg::R12,
9388                cond: Condition::NE,
9389            })
9390            .unwrap();
9391        assert_eq!(hi.len(), 10, "ITE(2) + MOV.W(4) + MOV.W(4): {hi:02x?}");
9392        // both value halfwords are MOV.W (0xF04F) — NOT the corrupt CMP (0x2c..).
9393        assert_eq!(&hi[2..4], &[0x4F, 0xF0], "then = MOV.W: {hi:02x?}");
9394        assert_eq!(&hi[6..8], &[0x4F, 0xF0], "else = MOV.W: {hi:02x?}");
9395        assert_eq!(hi[4] & 0x0F, 0x01, "then imm = #1");
9396        assert_eq!(hi[8] & 0x0F, 0x00, "else imm = #0");
9397        // Low Rd keeps the compact 16-bit MOVS form.
9398        let lo = enc
9399            .encode(&ArmOp::SetCond {
9400                rd: Reg::R0,
9401                cond: Condition::NE,
9402            })
9403            .unwrap();
9404        assert_eq!(lo.len(), 6, "ITE(2) + MOVS(2) + MOVS(2): {lo:02x?}");
9405        assert_eq!(lo[2..4], [0x01, 0x20], "then = MOVS R0,#1");
9406        assert_eq!(lo[4..6], [0x00, 0x20], "else = MOVS R0,#0");
9407    }
9408
9409    /// #209 Opt 1b: UMULL RdLo, RdHi, Rn, Rm encodes correctly on both ISAs.
9410    /// Thumb-2 T1: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm.
9411    /// A32:        cond 0000 1000 RdHi RdLo Rm 1001 Rn.
9412    #[test]
9413    fn test_encode_umull_209b() {
9414        use synth_synthesis::{ArmOp, Reg};
9415        let op = ArmOp::Umull {
9416            rdlo: Reg::R4,
9417            rdhi: Reg::R5,
9418            rn: Reg::R0,
9419            rm: Reg::R3,
9420        };
9421        // Thumb-2: hw1 = 0xFBA0 | 0 = 0xFBA0; hw2 = (4<<12)|(5<<8)|3 = 0x4503.
9422        let t = ArmEncoder::new_thumb2().encode(&op).unwrap();
9423        assert_eq!(
9424            t,
9425            vec![0xA0, 0xFB, 0x03, 0x45],
9426            "umull r4,r5,r0,r3 (T2): {t:02x?}"
9427        );
9428        // A32: 0xE0800090 | (5<<16) | (4<<12) | (3<<8) | 0 = 0xE0854390.
9429        let a = ArmEncoder::new_arm32().encode(&op).unwrap();
9430        assert_eq!(
9431            a,
9432            0xE085_4390u32.to_le_bytes().to_vec(),
9433            "umull (A32): {a:02x?}"
9434        );
9435    }
9436
9437    /// #206 regression: the ARM32 (A32) `Ldr`/`Str` encoders fed `addr` through
9438    /// `encode_mem_addr`, which returns only the 12-bit immediate — so a register
9439    /// offset (`[rn, rm, #off]`) was silently dropped to `[rn, #off]`, sending
9440    /// the access to the wrong runtime address (silent miscompile on the default
9441    /// `--target arm`). A register offset must materialize `ip = rn + rm` and
9442    /// load from `[ip, #off]`. Verify the bytes.
9443    #[test]
9444    fn test_encode_arm32_indexed_load_keeps_index_206() {
9445        use synth_synthesis::{ArmOp, MemAddr, Reg};
9446        let enc = ArmEncoder::new_arm32();
9447        // ldr r0, [r11, r1, #8]  must NOT collapse to a single immediate ldr.
9448        let bytes = enc
9449            .encode(&ArmOp::Ldr {
9450                rd: Reg::R0,
9451                addr: MemAddr::reg_imm(Reg::R11, Reg::R1, 8),
9452            })
9453            .unwrap();
9454        assert_eq!(
9455            bytes.len(),
9456            8,
9457            "expected ADD ip + LDR (2 words): {bytes:02x?}"
9458        );
9459        let add = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9460        let ldr = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9461        // ADD ip, r11, r1  = 0xE08BC001
9462        assert_eq!(add, 0xE08B_C001, "ADD ip,r11,r1: {add:#010x}");
9463        // LDR r0, [ip, #8] = 0xE59C0008
9464        assert_eq!(ldr, 0xE59C_0008, "LDR r0,[ip,#8]: {ldr:#010x}");
9465        // A bare immediate ldr (the bug) would be 0xE59B0008 (base=r11) — reject.
9466        assert_ne!(ldr, 0xE59B_0008, "index must not be dropped");
9467    }
9468
9469    /// #594 regression: `call_indirect` on the A32 path (`--target cortex-r5`)
9470    /// was encoded as a literal NOP (0xE1A00000) — the call never happened and
9471    /// the function silently returned the leftover table-index value. The A32
9472    /// encoder must emit a real dispatch expansion, since #642 guarded by an
9473    /// inline bounds check:
9474    /// `MOVW r12, #size; CMP idx, r12; BLO +1; UDF;
9475    ///  MOV r12, idx, LSL #2; LDR r12, [r11, r12]; BLX r12`.
9476    #[test]
9477    fn test_encode_arm32_call_indirect_is_real_call_594() {
9478        use synth_synthesis::{ArmOp, Reg};
9479        let enc = ArmEncoder::new_arm32();
9480        let bytes = enc
9481            .encode(&ArmOp::CallIndirect {
9482                rd: Reg::R0,
9483                type_idx: 0,
9484                table_index_reg: Reg::R0,
9485                table_size: 4,
9486                table_byte_offset: 0,
9487                null_check: false,
9488                type_check: None,
9489            })
9490            .unwrap();
9491        assert_eq!(
9492            bytes.len(),
9493            28,
9494            "expected MOVW + CMP + BLO + UDF + MOV + LDR + BLX (7 words): {bytes:02x?}"
9495        );
9496        let words: Vec<u32> = bytes
9497            .as_chunks::<4>()
9498            .0
9499            .iter()
9500            .map(|&w| u32::from_le_bytes(w))
9501            .collect();
9502        // #642 bounds guard: MOVW r12, #4; CMP r0, r12; BLO +1; UDF
9503        assert_eq!(words[0], 0xE300_C004, "MOVW r12,#4: {:#010x}", words[0]);
9504        assert_eq!(words[1], 0xE150_000C, "CMP r0,r12: {:#010x}", words[1]);
9505        assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9506        assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9507        // MOV r12, r0, LSL #2 = 0xE1A0C100
9508        assert_eq!(
9509            words[4], 0xE1A0_C100,
9510            "MOV r12,r0,LSL#2: {:#010x}",
9511            words[4]
9512        );
9513        // LDR r12, [r11, r12] = 0xE79BC00C
9514        assert_eq!(
9515            words[5], 0xE79B_C00C,
9516            "LDR r12,[r11,r12]: {:#010x}",
9517            words[5]
9518        );
9519        // BLX r12 = 0xE12FFF3C
9520        assert_eq!(words[6], 0xE12F_FF3C, "BLX r12: {:#010x}", words[6]);
9521        // The bug: a single NOP word. Must never come back.
9522        assert!(
9523            !bytes
9524                .as_chunks::<4>()
9525                .0
9526                .iter()
9527                .any(|&w| w == 0xE1A0_0000u32.to_le_bytes()),
9528            "call_indirect must not contain a NOP (#594): {bytes:02x?}"
9529        );
9530
9531        // A non-R0 index register lands in the MOV's Rm and CMP's Rn fields.
9532        let bytes = enc
9533            .encode(&ArmOp::CallIndirect {
9534                rd: Reg::R0,
9535                type_idx: 0,
9536                table_index_reg: Reg::R4,
9537                table_size: 4,
9538                table_byte_offset: 0,
9539                null_check: false,
9540                type_check: None,
9541            })
9542            .unwrap();
9543        let cmp = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9544        assert_eq!(cmp, 0xE154_000C, "CMP r4,r12: {cmp:#010x}");
9545        let mov = u32::from_le_bytes(bytes[16..20].try_into().unwrap());
9546        assert_eq!(mov, 0xE1A0_C104, "MOV r12,r4,LSL#2: {mov:#010x}");
9547    }
9548
9549    /// #642: a table size above 16 bits must not be silently truncated by the
9550    /// MOVW — the A32 guard adds a MOVT for the high half.
9551    #[test]
9552    fn test_encode_arm32_call_indirect_wide_table_size_642() {
9553        use synth_synthesis::{ArmOp, Reg};
9554        let enc = ArmEncoder::new_arm32();
9555        let bytes = enc
9556            .encode(&ArmOp::CallIndirect {
9557                rd: Reg::R0,
9558                type_idx: 0,
9559                table_index_reg: Reg::R0,
9560                table_size: 0x0002_0003,
9561                table_byte_offset: 0,
9562                null_check: false,
9563                type_check: None,
9564            })
9565            .unwrap();
9566        assert_eq!(bytes.len(), 32, "MOVT arm adds one word: {bytes:02x?}");
9567        let movw = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9568        let movt = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9569        assert_eq!(movw, 0xE300_C003, "MOVW r12,#3: {movw:#010x}");
9570        assert_eq!(movt, 0xE340_C002, "MOVT r12,#2: {movt:#010x}");
9571    }
9572
9573    /// #597 anchor (justified correctness RE-PIN of the #594-era freeze): the
9574    /// Thumb-2 `CallIndirect` expansion is `mov.w ip, rm, LSL #2; ldr.w ip,
9575    /// [r11, ip]; blx ip`.
9576    ///
9577    /// The #594 PR froze the then-current bytes `4F EA 20 0C ...` whose first
9578    /// word decodes as `mov.w ip, rm, ASR #32` — the intended `LSL #2` had
9579    /// its shift amount in the TYPE field (bits 5:4) instead of imm2 (bits
9580    /// 7:6), so the index was destroyed and every call_indirect dispatched
9581    /// table entry 0 (shipped miscompile, masked by index-0 probes). #597
9582    /// corrects the encoding; new bytes `4F EA 80 0C ...` were
9583    /// execution-validated under unicorn against the wasmtime oracle on a
9584    /// multi-entry table (indexes 0, 1, 3 —
9585    /// scripts/repro/call_indirect_597_differential.py) before this pin was
9586    /// replaced. Old pin: [4F EA 20 0C, 5B F8 0C C0, E0 47] (ASR #32 — must
9587    /// never come back).
9588    #[test]
9589    fn test_encode_thumb_call_indirect_lsl2_597() {
9590        use synth_synthesis::{ArmOp, Reg};
9591        let enc = ArmEncoder::new_thumb2();
9592        let bytes = enc
9593            .encode(&ArmOp::CallIndirect {
9594                rd: Reg::R0,
9595                type_idx: 0,
9596                table_index_reg: Reg::R0,
9597                table_size: 4,
9598                table_byte_offset: 0,
9599                null_check: false,
9600                type_check: None,
9601            })
9602            .unwrap();
9603        assert_eq!(
9604            bytes,
9605            vec![
9606                // #642 bounds guard: movw ip,#4; cmp r0,ip; blo +1; udf #0
9607                0x40, 0xF2, 0x04, 0x0C, // movw ip, #4
9608                0x60, 0x45, // cmp r0, ip
9609                0x00, 0xD3, // blo .+4 (skip the udf)
9610                0x00, 0xDE, // udf #0 — OOB index trap (WASM §4.4.8)
9611                // #597-pinned dispatch
9612                0x4F, 0xEA, 0x80, 0x0C, // mov.w ip, r0, lsl #2
9613                0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9614                0xE0, 0x47, // blx ip
9615            ],
9616            "Thumb-2 CallIndirect: bounds guard + mov.w/ldr.w/blx dispatch: {bytes:02x?}"
9617        );
9618        // The #597 bug bytes (ASR #32 dispatch first word) must never come back.
9619        assert!(
9620            !bytes.windows(4).any(|w| w == [0x4F, 0xEA, 0x20, 0x0C]),
9621            "mov.w ip, rm, ASR #32 — the #597 type-field bug"
9622        );
9623
9624        // A non-R0 index register lands in the mov.w's Rm field (hw2 bits 3:0)
9625        // and the cmp's Rn field.
9626        let bytes = enc
9627            .encode(&ArmOp::CallIndirect {
9628                rd: Reg::R0,
9629                type_idx: 0,
9630                table_index_reg: Reg::R4,
9631                table_size: 4,
9632                table_byte_offset: 0,
9633                null_check: false,
9634                type_check: None,
9635            })
9636            .unwrap();
9637        assert_eq!(&bytes[4..6], &[0x64, 0x45], "cmp r4, ip: {bytes:02x?}");
9638        assert_eq!(
9639            &bytes[10..14],
9640            &[0x4F, 0xEA, 0x84, 0x0C],
9641            "mov.w ip, r4, LSL #2: {bytes:02x?}"
9642        );
9643    }
9644
9645    /// #642: the Thumb-2 bounds guard for a high-register index (R8 — the top
9646    /// of the allocatable pool) uses the high-reg-capable 16-bit CMP (T2) with
9647    /// the N bit set; a table size above 16 bits adds a MOVT.
9648    #[test]
9649    fn test_encode_thumb_call_indirect_guard_shapes_642() {
9650        use synth_synthesis::{ArmOp, Reg};
9651        let enc = ArmEncoder::new_thumb2();
9652        let bytes = enc
9653            .encode(&ArmOp::CallIndirect {
9654                rd: Reg::R0,
9655                type_idx: 0,
9656                table_index_reg: Reg::R8,
9657                table_size: 3,
9658                table_byte_offset: 0,
9659                null_check: false,
9660                type_check: None,
9661            })
9662            .unwrap();
9663        // cmp r8, ip — T2: 0x4500 | N(1)<<7 | Rm(12)<<3 | Rn(0) = 0x45E0
9664        assert_eq!(&bytes[4..6], &[0xE0, 0x45], "cmp r8, ip: {bytes:02x?}");
9665
9666        let bytes = enc
9667            .encode(&ArmOp::CallIndirect {
9668                rd: Reg::R0,
9669                type_idx: 0,
9670                table_index_reg: Reg::R0,
9671                table_size: 0x0002_0003,
9672                table_byte_offset: 0,
9673                null_check: false,
9674                type_check: None,
9675            })
9676            .unwrap();
9677        // movw ip,#3 then movt ip,#2 — the size must not be truncated.
9678        assert_eq!(
9679            &bytes[0..8],
9680            &[0x40, 0xF2, 0x03, 0x0C, 0xC0, 0xF2, 0x02, 0x0C],
9681            "movw ip,#3; movt ip,#2: {bytes:02x?}"
9682        );
9683    }
9684
9685    /// #650: a non-zero table base offset (table N of the contiguous R11
9686    /// region) routes the Thumb-2 pointer load through
9687    /// `add.w ip, r11, ip; ldr.w ip, [ip, #offset]` — and offset 0 keeps the
9688    /// pre-#650 single-load bytes IDENTICAL (the by-construction pin).
9689    #[test]
9690    fn test_encode_thumb_call_indirect_table_offset_650() {
9691        use synth_synthesis::{ArmOp, Reg};
9692        let enc = ArmEncoder::new_thumb2();
9693        // falcon's fused-component shape: table 0 has 7 entries, so table 1
9694        // sits at byte offset 28.
9695        let bytes = enc
9696            .encode(&ArmOp::CallIndirect {
9697                rd: Reg::R0,
9698                type_idx: 0,
9699                table_index_reg: Reg::R1,
9700                table_size: 41,
9701                table_byte_offset: 28,
9702                null_check: false,
9703                type_check: None,
9704            })
9705            .unwrap();
9706        assert_eq!(
9707            bytes,
9708            vec![
9709                // #642 bounds guard against TABLE 1's OWN size (41)
9710                0x40, 0xF2, 0x29, 0x0C, // movw ip, #41
9711                0x61, 0x45, // cmp r1, ip
9712                0x00, 0xD3, // blo .+4 (skip the udf)
9713                0x00, 0xDE, // udf #0 — OOB trap (WASM §4.4.8)
9714                // dispatch through table 1's base (R11 + 28)
9715                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9716                0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9717                0xDC, 0xF8, 0x1C, 0xC0, // ldr.w ip, [ip, #28]
9718                0xE0, 0x47, // blx ip
9719            ],
9720            "Thumb-2 table-1 dispatch (#650): {bytes:02x?}"
9721        );
9722
9723        // Offset 0 must stay the #597-pinned single-load form (no add.w, no
9724        // imm-form ldr) — single-table byte identity by construction.
9725        let zero = enc
9726            .encode(&ArmOp::CallIndirect {
9727                rd: Reg::R0,
9728                type_idx: 0,
9729                table_index_reg: Reg::R1,
9730                table_size: 41,
9731                table_byte_offset: 0,
9732                null_check: false,
9733                type_check: None,
9734            })
9735            .unwrap();
9736        assert_eq!(
9737            &zero[10..],
9738            &[
9739                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9740                0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9741                0xE0, 0x47, // blx ip
9742            ],
9743            "offset 0 keeps the pre-#650 dispatch bytes: {zero:02x?}"
9744        );
9745    }
9746
9747    /// #650: the A32 twin — `add r12, r11, r12; ldr r12, [r12, #offset]` for
9748    /// a non-zero table base offset; offset 0 keeps the #594/#642 form.
9749    #[test]
9750    fn test_encode_arm32_call_indirect_table_offset_650() {
9751        use synth_synthesis::{ArmOp, Reg};
9752        let enc = ArmEncoder::new_arm32();
9753        let bytes = enc
9754            .encode(&ArmOp::CallIndirect {
9755                rd: Reg::R0,
9756                type_idx: 0,
9757                table_index_reg: Reg::R1,
9758                table_size: 41,
9759                table_byte_offset: 28,
9760                null_check: false,
9761                type_check: None,
9762            })
9763            .unwrap();
9764        let words: Vec<u32> = bytes
9765            .as_chunks::<4>()
9766            .0
9767            .iter()
9768            .map(|&w| u32::from_le_bytes(w))
9769            .collect();
9770        assert_eq!(words[0], 0xE300_C029, "MOVW r12,#41: {:#010x}", words[0]);
9771        assert_eq!(words[1], 0xE151_000C, "CMP r1,r12: {:#010x}", words[1]);
9772        assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9773        assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9774        assert_eq!(
9775            words[4], 0xE1A0_C101,
9776            "MOV r12,r1,LSL#2: {:#010x}",
9777            words[4]
9778        );
9779        assert_eq!(
9780            words[5], 0xE08B_C00C,
9781            "ADD r12,r11,r12 (#650): {:#010x}",
9782            words[5]
9783        );
9784        assert_eq!(
9785            words[6], 0xE59C_C01C,
9786            "LDR r12,[r12,#28] (#650): {:#010x}",
9787            words[6]
9788        );
9789        assert_eq!(words[7], 0xE12F_FF3C, "BLX r12: {:#010x}", words[7]);
9790    }
9791
9792    /// #664: `null_check` inserts a null-funcref trap between the Thumb-2
9793    /// pointer load and the `BLX` (`cmp.w ip, #0; bne .+4; udf #0`) — a
9794    /// zero-linked (uninitialized) slot must TRAP (WASM §4.4.8), never
9795    /// branch to address 0. `null_check: false` keeps the expansion
9796    /// byte-identical to the pre-#664 form (by-construction pin).
9797    #[test]
9798    fn test_encode_thumb_call_indirect_null_check_664() {
9799        use synth_synthesis::{ArmOp, Reg};
9800        let enc = ArmEncoder::new_thumb2();
9801        let op = |null_check| ArmOp::CallIndirect {
9802            rd: Reg::R0,
9803            type_idx: 0,
9804            table_index_reg: Reg::R1,
9805            table_size: 4,
9806            table_byte_offset: 0,
9807            null_check,
9808            type_check: None,
9809        };
9810        let with = enc.encode(&op(true)).unwrap();
9811        let without = enc.encode(&op(false)).unwrap();
9812        // The checked form = the unchecked form with EXACTLY the three-insn
9813        // null check spliced in before the final BLX (byte identity of the
9814        // shared prefix/suffix — nothing else may move).
9815        assert_eq!(
9816            with.len(),
9817            without.len() + 8,
9818            "cmp.w (4) + bne (2) + udf (2): {with:02x?}"
9819        );
9820        let blx_at = without.len() - 2;
9821        assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9822        assert_eq!(
9823            &with[blx_at..],
9824            &[
9825                0xBC, 0xF1, 0x00, 0x0F, // cmp.w ip, #0
9826                0x00, 0xD1, // bne .+4 (skip the udf)
9827                0x00, 0xDE, // udf #0 — null-funcref trap (#664)
9828                0xE0, 0x47, // blx ip
9829            ],
9830            "null check precedes the BLX: {with:02x?}"
9831        );
9832        assert_eq!(&with[with.len() - 2..], &without[blx_at..], "same BLX");
9833    }
9834
9835    /// #664: the A32 twin — `cmp r12, #0; bne .+8; udf` before the `BLX`;
9836    /// `null_check: false` keeps the #594/#642/#650 bytes identical.
9837    #[test]
9838    fn test_encode_arm32_call_indirect_null_check_664() {
9839        use synth_synthesis::{ArmOp, Reg};
9840        let enc = ArmEncoder::new_arm32();
9841        let op = |null_check| ArmOp::CallIndirect {
9842            rd: Reg::R0,
9843            type_idx: 0,
9844            table_index_reg: Reg::R1,
9845            table_size: 4,
9846            table_byte_offset: 0,
9847            null_check,
9848            type_check: None,
9849        };
9850        let with = enc.encode(&op(true)).unwrap();
9851        let without = enc.encode(&op(false)).unwrap();
9852        assert_eq!(with.len(), without.len() + 12, "3 A32 words: {with:02x?}");
9853        let blx_at = without.len() - 4;
9854        assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9855        let words: Vec<u32> = with[blx_at..]
9856            .as_chunks::<4>()
9857            .0
9858            .iter()
9859            .map(|&w| u32::from_le_bytes(w))
9860            .collect();
9861        assert_eq!(words[0], 0xE35C_0000, "CMP r12,#0: {:#010x}", words[0]);
9862        assert_eq!(words[1], 0x1A00_0000, "BNE +1 insn: {:#010x}", words[1]);
9863        assert_eq!(words[2], 0xE7F0_00F0, "UDF (null trap): {:#010x}", words[2]);
9864        assert_eq!(words[3], 0xE12F_FF3C, "BLX r12: {:#010x}", words[3]);
9865    }
9866
9867    /// #676: `type_check` splices the runtime type check — scale the index,
9868    /// load the slot's structural class id from the type-id sidecar
9869    /// (`ldr.w ip, [ip, #type_off]`), compare against the expected class id
9870    /// and trap on mismatch (WASM §4.4.8) — between the bounds guard and
9871    /// the dispatch tail. `type_check: None` keeps the expansion
9872    /// byte-identical to the pre-#676 form (by-construction pin, the same
9873    /// trick as #650 offset-0 / #664 `null_check: false`).
9874    #[test]
9875    fn test_encode_thumb_call_indirect_type_check_676() {
9876        use synth_synthesis::{ArmOp, Reg};
9877        let enc = ArmEncoder::new_thumb2();
9878        let op = |type_check| ArmOp::CallIndirect {
9879            rd: Reg::R0,
9880            type_idx: 1,
9881            table_index_reg: Reg::R1,
9882            table_size: 5,
9883            table_byte_offset: 0,
9884            null_check: false,
9885            type_check,
9886        };
9887        let with = enc.encode(&op(Some((2, 20)))).unwrap();
9888        let without = enc.encode(&op(None)).unwrap();
9889        // The checked form = the unchecked form with EXACTLY the six-insn
9890        // type check spliced in after the bounds guard (byte identity of
9891        // the shared prefix/suffix — nothing else may move).
9892        assert_eq!(
9893            with.len(),
9894            without.len() + 20,
9895            "lsl.w(4)+add.w(4)+ldr.w(4)+cmp.w(4)+beq(2)+udf(2): {with:02x?}"
9896        );
9897        // Bounds guard: movw(4) + cmp(2) + blo(2) + udf(2) = 10 bytes.
9898        let guard_end = 10;
9899        assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
9900        assert_eq!(
9901            &with[guard_end..guard_end + 20],
9902            &[
9903                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9904                0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9905                0xDC, 0xF8, 0x14, 0xC0, // ldr.w ip, [ip, #20] — sidecar slot id
9906                0xBC, 0xF1, 0x02, 0x0F, // cmp.w ip, #2 — expected class id
9907                0x00, 0xD0, // beq .+4 (skip the udf on a match)
9908                0x00, 0xDE, // udf #0 — §4.4.8 type-mismatch trap (#676)
9909            ],
9910            "type check follows the bounds guard: {with:02x?}"
9911        );
9912        assert_eq!(
9913            &with[guard_end + 20..],
9914            &without[guard_end..],
9915            "dispatch tail unchanged (idx*4 recomputed)"
9916        );
9917    }
9918
9919    /// #676: the A32 twin — `mov r12, idx, lsl #2; add r12, r11, r12;
9920    /// ldr r12, [r12, #type_off]; cmp r12, #id; beq .+8; udf` after the
9921    /// bounds guard; `type_check: None` keeps the #594/#642/#650/#664
9922    /// bytes identical.
9923    #[test]
9924    fn test_encode_arm32_call_indirect_type_check_676() {
9925        use synth_synthesis::{ArmOp, Reg};
9926        let enc = ArmEncoder::new_arm32();
9927        let op = |type_check| ArmOp::CallIndirect {
9928            rd: Reg::R0,
9929            type_idx: 1,
9930            table_index_reg: Reg::R1,
9931            table_size: 5,
9932            table_byte_offset: 0,
9933            null_check: false,
9934            type_check,
9935        };
9936        let with = enc.encode(&op(Some((2, 20)))).unwrap();
9937        let without = enc.encode(&op(None)).unwrap();
9938        assert_eq!(with.len(), without.len() + 24, "6 A32 words: {with:02x?}");
9939        // Bounds guard: movw + cmp + blo + udf = 4 words = 16 bytes.
9940        let guard_end = 16;
9941        assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
9942        let words: Vec<u32> = with[guard_end..guard_end + 24]
9943            .as_chunks::<4>()
9944            .0
9945            .iter()
9946            .map(|&w| u32::from_le_bytes(w))
9947            .collect();
9948        assert_eq!(
9949            words[0], 0xE1A0_C101,
9950            "MOV r12,r1,LSL#2: {:#010x}",
9951            words[0]
9952        );
9953        assert_eq!(words[1], 0xE08B_C00C, "ADD r12,r11,r12: {:#010x}", words[1]);
9954        assert_eq!(
9955            words[2], 0xE59C_C014,
9956            "LDR r12,[r12,#20] (sidecar): {:#010x}",
9957            words[2]
9958        );
9959        assert_eq!(
9960            words[3], 0xE35C_0002,
9961            "CMP r12,#2 (expected class id): {:#010x}",
9962            words[3]
9963        );
9964        assert_eq!(words[4], 0x0A00_0000, "BEQ +1 insn: {:#010x}", words[4]);
9965        assert_eq!(
9966            words[5], 0xE7F0_00F0,
9967            "UDF (type-mismatch trap): {:#010x}",
9968            words[5]
9969        );
9970        assert_eq!(
9971            &with[guard_end + 24..],
9972            &without[guard_end..],
9973            "dispatch tail unchanged"
9974        );
9975    }
9976
9977    /// #178/#180 regression: the Thumb `Add`/`Adds`/`Subs` reg-forms used the
9978    /// 16-bit encoding unconditionally. For high registers (R12 base scratch,
9979    /// R8-R11 i64 pairs) the 3-bit register fields overflow and corrupt the
9980    /// operands — `add ip,ip,r0` came out as `adds r4,r5,r1` (0x186C), silently
9981    /// dropping the address operand and miscompiling every optimized memory
9982    /// access. High registers must use the 32-bit `.W` forms.
9983    #[test]
9984    fn test_encode_thumb_add_high_reg_uses_add_w_178_180() {
9985        let encoder = ArmEncoder::new_thumb2();
9986
9987        // add ip, ip, r0  — the exact MemLoad/MemStore base+addr op.
9988        let code = encoder
9989            .encode(&ArmOp::Add {
9990                rd: Reg::R12,
9991                rn: Reg::R12,
9992                op2: Operand2::Reg(Reg::R0),
9993            })
9994            .unwrap();
9995        // ADD.W ip, ip, r0 = EB0C 0C00 (little-endian halfwords).
9996        assert_eq!(
9997            code,
9998            vec![0x0C, 0xEB, 0x00, 0x0C],
9999            "high-reg Thumb ADD must be 32-bit ADD.W (EB0C 0C00), not corrupt 16-bit; got {code:02X?}"
10000        );
10001        // Must NOT be the buggy 16-bit 0x186C (`adds r4,r5,r1`).
10002        assert_ne!(code, vec![0x6C, 0x18], "regressed to corrupt 16-bit ADDS");
10003
10004        // Low-register add stays 16-bit (no regression for the common case).
10005        let lo = encoder
10006            .encode(&ArmOp::Add {
10007                rd: Reg::R1,
10008                rn: Reg::R2,
10009                op2: Operand2::Reg(Reg::R3),
10010            })
10011            .unwrap();
10012        assert_eq!(
10013            lo.len(),
10014            2,
10015            "low-reg ADD should remain 16-bit, got {lo:02X?}"
10016        );
10017    }
10018
10019    /// #178/#180 sibling: i64 low-word `Adds`/`Subs` can land in R8-R11 pairs;
10020    /// those must fall back to 32-bit ADDS.W/SUBS.W (flag-setting preserved).
10021    #[test]
10022    fn test_encode_thumb_adds_subs_high_reg_use_32bit_178_180() {
10023        let encoder = ArmEncoder::new_thumb2();
10024
10025        // adds r10, r10, r8  → ADDS.W = EB1A 0A08
10026        let adds = encoder
10027            .encode(&ArmOp::Adds {
10028                rd: Reg::R10,
10029                rn: Reg::R10,
10030                op2: Operand2::Reg(Reg::R8),
10031            })
10032            .unwrap();
10033        assert_eq!(
10034            adds,
10035            vec![0x1A, 0xEB, 0x08, 0x0A],
10036            "high-reg ADDS must be 32-bit ADDS.W (EB1A 0A08); got {adds:02X?}"
10037        );
10038
10039        // subs r10, r10, r8  → SUBS.W = EBBA 0A08
10040        let subs = encoder
10041            .encode(&ArmOp::Subs {
10042                rd: Reg::R10,
10043                rn: Reg::R10,
10044                op2: Operand2::Reg(Reg::R8),
10045            })
10046            .unwrap();
10047        assert_eq!(
10048            subs,
10049            vec![0xBA, 0xEB, 0x08, 0x0A],
10050            "high-reg SUBS must be 32-bit SUBS.W (EBBA 0A08); got {subs:02X?}"
10051        );
10052    }
10053
10054    /// #184 (sibling of #180): 16-bit CMN (T1) only encodes R0-R7. High registers
10055    /// must use 32-bit CMN.W, not the corrupt truncated 16-bit form.
10056    #[test]
10057    fn test_encode_thumb_cmn_high_reg_uses_cmn_w_184() {
10058        let encoder = ArmEncoder::new_thumb2();
10059
10060        // cmn r10, r8  → CMN.W = EB1A 0F08 (ADD.W S=1, Rd=PC discarded).
10061        let cmn = encoder
10062            .encode(&ArmOp::Cmn {
10063                rn: Reg::R10,
10064                op2: Operand2::Reg(Reg::R8),
10065            })
10066            .unwrap();
10067        assert_eq!(
10068            cmn,
10069            vec![0x1A, 0xEB, 0x08, 0x0F],
10070            "high-reg CMN must be 32-bit CMN.W (EB1A 0F08); got {cmn:02X?}"
10071        );
10072
10073        // Low registers stay 16-bit: cmn r1, r2 = 0x42D1.
10074        let lo = encoder
10075            .encode(&ArmOp::Cmn {
10076                rn: Reg::R1,
10077                op2: Operand2::Reg(Reg::R2),
10078            })
10079            .unwrap();
10080        assert_eq!(
10081            lo.len(),
10082            2,
10083            "low-reg CMN should remain 16-bit, got {lo:02X?}"
10084        );
10085        assert_eq!(lo, vec![0xD1, 0x42], "low-reg CMN bytes wrong: {lo:02X?}");
10086    }
10087
10088    /// #185 regression: feeding PC (R15) as a data operand to a Thumb-2 op that
10089    /// guards its registers must return Err, not panic under debug-assertions.
10090    /// (Synth never emits PC here; the fuzz harness requires encode() be total.)
10091    #[test]
10092    fn test_encode_pc_operand_returns_err_not_panic_185() {
10093        let encoder = ArmEncoder::new_thumb2();
10094        for op in [
10095            ArmOp::Sdiv {
10096                rd: Reg::PC,
10097                rn: Reg::R0,
10098                rm: Reg::R1,
10099            },
10100            ArmOp::Udiv {
10101                rd: Reg::R0,
10102                rn: Reg::PC,
10103                rm: Reg::R1,
10104            },
10105            ArmOp::Sdiv {
10106                rd: Reg::R0,
10107                rn: Reg::R1,
10108                rm: Reg::PC,
10109            },
10110        ] {
10111            let r = encoder.encode(&op);
10112            assert!(
10113                r.is_err(),
10114                "encode({op:?}) must return Err for a PC operand, got {r:?}"
10115            );
10116        }
10117        // Valid registers still encode fine (no false rejection).
10118        assert!(
10119            encoder
10120                .encode(&ArmOp::Sdiv {
10121                    rd: Reg::R0,
10122                    rn: Reg::R1,
10123                    rm: Reg::R2
10124                })
10125                .is_ok()
10126        );
10127    }
10128
10129    #[test]
10130    fn test_encode_nop_arm32() {
10131        let encoder = ArmEncoder::new_arm32();
10132        let code = encoder.encode(&ArmOp::Nop).unwrap();
10133
10134        assert_eq!(code.len(), 4); // ARM32 instructions are 4 bytes
10135        assert_eq!(code, vec![0x00, 0x00, 0xA0, 0xE1]); // MOV R0, R0
10136    }
10137
10138    #[test]
10139    fn test_encode_nop_thumb() {
10140        let encoder = ArmEncoder::new_thumb2();
10141        let code = encoder.encode(&ArmOp::Nop).unwrap();
10142
10143        assert_eq!(code.len(), 2); // Thumb instructions are 2 bytes
10144        assert_eq!(code, vec![0x00, 0xBF]); // NOP
10145    }
10146
10147    #[test]
10148    fn test_encode_mov_immediate_arm32() {
10149        let encoder = ArmEncoder::new_arm32();
10150        let op = ArmOp::Mov {
10151            rd: Reg::R0,
10152            op2: Operand2::Imm(42),
10153        };
10154
10155        let code = encoder.encode(&op).unwrap();
10156        assert_eq!(code.len(), 4);
10157
10158        // Verify it's a MOV instruction (bits should have immediate flag set)
10159        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10160        assert_eq!(instr & 0x0E000000, 0x02000000); // Check I bit is set
10161    }
10162
10163    #[test]
10164    fn test_encode_add_registers_arm32() {
10165        let encoder = ArmEncoder::new_arm32();
10166        let op = ArmOp::Add {
10167            rd: Reg::R0,
10168            rn: Reg::R1,
10169            op2: Operand2::Reg(Reg::R2),
10170        };
10171
10172        let code = encoder.encode(&op).unwrap();
10173        assert_eq!(code.len(), 4);
10174
10175        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10176        // Verify it's an ADD instruction with correct opcode
10177        assert_eq!(instr & 0x0FE00000, 0x00800000);
10178    }
10179
10180    /// #350 — `encode_thumb32_add_imm` must lower an out-of-range immediate
10181    /// (> 0xFFF) to a legal MOVW(/MOVT) + ADD.W-register sequence instead of
10182    /// erroring. The small-imm fast path (imm <= 0xFFF) stays byte-identical.
10183    #[test]
10184    fn test_encode_add_imm_large_350() {
10185        let enc = ArmEncoder::new_thumb2();
10186
10187        // --- Fast path: imm <= 0xFFF is a single 4-byte instruction, and the
10188        // VALUE must be right (#681: this test used to assert only the length,
10189        // letting the raw-packed T3 mis-encoding of 0x123 pass CI). 0x123 is
10190        // not ThumbExpandImm-representable, so it must be ADDW (T4, plain
10191        // imm12): clang `addw r0, r1, #0x123` = f201 0023.
10192        let small = enc
10193            .encode_thumb32_add_imm(&Reg::R0, &Reg::R1, 0x123)
10194            .unwrap();
10195        assert_eq!(small, vec![0x01, 0xF2, 0x23, 0x10], "ADDW r0, r1, #0x123");
10196
10197        // helper: decode a Thumb-2 MOVW/MOVT halfword pair back to its imm16
10198        fn movx_imm16(b: &[u8]) -> u32 {
10199            let hw1 = u16::from_le_bytes([b[0], b[1]]) as u32;
10200            let hw2 = u16::from_le_bytes([b[2], b[3]]) as u32;
10201            let imm4 = hw1 & 0xF;
10202            let i = (hw1 >> 10) & 1;
10203            let imm3 = (hw2 >> 12) & 0x7;
10204            let imm8 = hw2 & 0xFF;
10205            (imm4 << 12) | (i << 11) | (imm3 << 8) | imm8
10206        }
10207        fn movx_rd(b: &[u8]) -> u32 {
10208            (u16::from_le_bytes([b[2], b[3]]) as u32 >> 8) & 0xF
10209        }
10210
10211        // --- rd != rn: scratch is rd. imm = 70000 = 0x11170 needs MOVW+MOVT. ---
10212        // 0x11170: lo16 = 0x1170, hi16 = 0x0001
10213        let seq = enc
10214            .encode_thumb32_add_imm(&Reg::R12, &Reg::R0, 70000)
10215            .unwrap();
10216        assert_eq!(seq.len(), 12, "MOVW + MOVT + ADD = 12 bytes");
10217        // MOVW r12, #0x1170
10218        assert_eq!(u16::from_le_bytes([seq[0], seq[1]]) & 0xFBF0, 0xF240);
10219        assert_eq!(movx_rd(&seq[0..4]), 12);
10220        assert_eq!(movx_imm16(&seq[0..4]), 0x1170);
10221        // MOVT r12, #0x0001
10222        assert_eq!(u16::from_le_bytes([seq[4], seq[5]]) & 0xFBF0, 0xF2C0);
10223        assert_eq!(movx_rd(&seq[4..8]), 12);
10224        assert_eq!(movx_imm16(&seq[4..8]), 0x0001);
10225        // ADD.W r12, r0, r12  (EB00 | rn=0 ; rd=12, rm=12)
10226        let add1 = u16::from_le_bytes([seq[8], seq[9]]) as u32;
10227        let add2 = u16::from_le_bytes([seq[10], seq[11]]) as u32;
10228        assert_eq!(add1 & 0xFFF0, 0xEB00);
10229        assert_eq!(add1 & 0xF, 0); // rn = r0
10230        assert_eq!((add2 >> 8) & 0xF, 12); // rd = r12
10231        assert_eq!(add2 & 0xF, 12); // rm = scratch = r12
10232        // The materialized scratch must reconstruct exactly 70000.
10233        assert_eq!(
10234            (movx_imm16(&seq[4..8]) << 16) | movx_imm16(&seq[0..4]),
10235            70000
10236        );
10237
10238        // --- imm <= 0xFFFF: MOVT is skipped (MOVW + ADD = 8 bytes). ---
10239        let seq16 = enc
10240            .encode_thumb32_add_imm(&Reg::R3, &Reg::R0, 0xABCD)
10241            .unwrap();
10242        assert_eq!(seq16.len(), 8, "imm <= 0xFFFF skips MOVT");
10243        assert_eq!(movx_imm16(&seq16[0..4]), 0xABCD);
10244        assert_eq!(movx_rd(&seq16[0..4]), 3); // scratch = rd = r3
10245
10246        // --- rd == rn (in-place add): scratch must be R12, not rd. ---
10247        // imm = 0x12345: lo16 = 0x2345, hi16 = 0x0001
10248        let inplace = enc
10249            .encode_thumb32_add_imm(&Reg::R5, &Reg::R5, 0x12345)
10250            .unwrap();
10251        assert_eq!(inplace.len(), 12);
10252        assert_eq!(movx_rd(&inplace[0..4]), 12, "rd==rn must use R12 scratch");
10253        assert_eq!(
10254            (movx_imm16(&inplace[4..8]) << 16) | movx_imm16(&inplace[0..4]),
10255            0x12345
10256        );
10257        // ADD.W r5, r5, r12 — rm must be the scratch (12), never rn.
10258        let ip_add2 = u16::from_le_bytes([inplace[10], inplace[11]]) as u32;
10259        assert_eq!(ip_add2 & 0xF, 12);
10260        assert_eq!((ip_add2 >> 8) & 0xF, 5);
10261    }
10262
10263    /// #681 — `encode_thumb32_add_imm` packed a RAW immediate into the T3
10264    /// ADD.W `i:imm3:imm8` field, which is a ThumbExpandImm MODIFIED immediate:
10265    /// ThumbExpandImm(0x200) = 0, ThumbExpandImm(0x400) = 0x8000_0000. Every
10266    /// dynamic-address load/store with a static offset in 0x100..=0xFFF
10267    /// computed a wrong address (and bypassed --safety-bounds software: the
10268    /// guard checked the intended address, the access used the mis-encoded
10269    /// one). Fix: imm <= 0xFF keeps T3 (raw == expanded there, bit-identical);
10270    /// 0x100..=0xFFF uses ADDW (T4, plain imm12) — same lowering
10271    /// `encode_thumb32_add` already uses per #253.
10272    ///
10273    /// Every expected byte sequence below is pinned against clang
10274    /// (`-target thumbv7m-none-eabi`) output, bit-for-bit (#544 pattern).
10275    #[test]
10276    fn test_encode_add_imm_thumb_expand_681() {
10277        let enc = ArmEncoder::new_thumb2();
10278        let add = |rd: &Reg, rn: &Reg, imm: u32| enc.encode_thumb32_add_imm(rd, rn, imm).unwrap();
10279
10280        // imm <= 0xFF stays T3 ADD.W (raw == ThumbExpandImm-expanded):
10281        // clang: add.w r12, r0, #0xff  = f100 0cff
10282        assert_eq!(add(&Reg::R12, &Reg::R0, 0xFF), vec![0x00, 0xF1, 0xFF, 0x0C]);
10283
10284        // 0x100..=0xFFF must be ADDW (T4, plain imm12). The old T3 raw packing
10285        // decoded as +0 (0x100/0x200), +0x80000000 (0x400), etc.
10286        // clang: addw r12, r0, #0x100 = f200 1c00
10287        assert_eq!(
10288            add(&Reg::R12, &Reg::R0, 0x100),
10289            vec![0x00, 0xF2, 0x00, 0x1C]
10290        );
10291        // clang: addw r12, r0, #0x104 = f200 1c04
10292        assert_eq!(
10293            add(&Reg::R12, &Reg::R0, 0x104),
10294            vec![0x00, 0xF2, 0x04, 0x1C]
10295        );
10296        // clang: addw r12, r0, #0x200 = f200 2c00
10297        assert_eq!(
10298            add(&Reg::R12, &Reg::R0, 0x200),
10299            vec![0x00, 0xF2, 0x00, 0x2C]
10300        );
10301        // clang: addw r12, r0, #0x3fc = f200 3cfc
10302        assert_eq!(
10303            add(&Reg::R12, &Reg::R0, 0x3FC),
10304            vec![0x00, 0xF2, 0xFC, 0x3C]
10305        );
10306        // clang: addw r12, r0, #0x400 = f200 4c00
10307        assert_eq!(
10308            add(&Reg::R12, &Reg::R0, 0x400),
10309            vec![0x00, 0xF2, 0x00, 0x4C]
10310        );
10311        // clang: addw r12, r0, #0xfff = f600 7cff
10312        assert_eq!(
10313            add(&Reg::R12, &Reg::R0, 0xFFF),
10314            vec![0x00, 0xF6, 0xFF, 0x7C]
10315        );
10316        // Non-scratch rd/rn — clang: addw r1, r2, #0x104 = f202 1104
10317        assert_eq!(add(&Reg::R1, &Reg::R2, 0x104), vec![0x02, 0xF2, 0x04, 0x11]);
10318    }
10319
10320    /// #681 class audit — the T2 RSB and AND.W immediate fields are also
10321    /// ThumbExpandImm-coded and were raw-packed. Neither has a plain-imm12
10322    /// (T4-style) form, so a non-representable immediate must Err loudly
10323    /// (#253/#255/#378 class: never silently encode a different constant).
10324    /// Existing emitters only use representable values (RSB #32, AND #0x3F),
10325    /// pinned here bit-for-bit against clang.
10326    #[test]
10327    fn test_rsb_and_imm_thumb_expand_gate_681() {
10328        let enc = ArmEncoder::new_thumb2();
10329
10330        // clang: rsb.w r3, r2, #0x20 = f1c2 0320 — byte-identical to before.
10331        let rsb = enc
10332            .encode(&ArmOp::Rsb {
10333                rd: Reg::R3,
10334                rn: Reg::R2,
10335                imm: 32,
10336            })
10337            .unwrap();
10338        assert_eq!(rsb, vec![0xC2, 0xF1, 0x20, 0x03]);
10339
10340        // 0x101 is not ThumbExpandImm-representable -> must Err, not mis-encode.
10341        assert!(
10342            enc.encode(&ArmOp::Rsb {
10343                rd: Reg::R3,
10344                rn: Reg::R2,
10345                imm: 0x101,
10346            })
10347            .is_err(),
10348            "non-ThumbExpandImm RSB immediate must Err"
10349        );
10350
10351        // clang: and r4, r4, #0x3f = f004 043f — byte-identical to before.
10352        let and = enc.encode_thumb32_and_imm_raw(4, 4, 0x3F).unwrap();
10353        assert_eq!(and, vec![0x04, 0xF0, 0x3F, 0x04]);
10354        assert!(
10355            enc.encode_thumb32_and_imm_raw(4, 4, 0x101).is_err(),
10356            "non-ThumbExpandImm AND immediate must Err"
10357        );
10358
10359        // A32 RSB: imm12 is a rotate:imm8 modified immediate; > 0xFF used to be
10360        // silently masked to `imm & 0xFF` (#378 masking class) -> must Err.
10361        let a32 = ArmEncoder::new_arm32();
10362        assert!(
10363            a32.encode(&ArmOp::Rsb {
10364                rd: Reg::R3,
10365                rn: Reg::R2,
10366                imm: 0x120,
10367            })
10368            .is_err(),
10369            "A32 RSB immediate > 0xFF must Err, not mask"
10370        );
10371        // imm 32 (the only value real codegen emits) still encodes.
10372        assert!(
10373            a32.encode(&ArmOp::Rsb {
10374                rd: Reg::R3,
10375                rn: Reg::R2,
10376                imm: 32,
10377            })
10378            .is_ok()
10379        );
10380    }
10381
10382    /// #350 follow-up — the `encoder_no_panic` fuzz harness drives the encoder
10383    /// with ARBITRARY registers, including the one case the in-place lowering
10384    /// cannot serve: rd==rn==R12. There the scratch (R12, the reserved encoder
10385    /// register) would alias Rn and clobber it before the ADD reads it. The
10386    /// encoder contract (#180/#185) is Ok-or-Err, never a panic — so this must
10387    /// return Err, not assert. (Real codegen never emits rd==rn==R12 because R12
10388    /// is non-allocatable; this guards only the fuzz/adversarial path.)
10389    #[test]
10390    fn test_encode_add_imm_large_rd_rn_r12_errs_not_panics_350() {
10391        let enc = ArmEncoder::new_thumb2();
10392        // Out-of-range imm with rd==rn==R12: no free scratch -> Err.
10393        let r = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 70000);
10394        assert!(
10395            r.is_err(),
10396            "rd==rn==R12 with out-of-range imm must Err (no free scratch), got {r:?}"
10397        );
10398        // Small imm with rd==rn==R12 still takes the single-instruction fast path
10399        // (no scratch needed) and must succeed — the guard is scoped to the
10400        // out-of-range lowering only.
10401        let small = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 0x10);
10402        assert!(small.is_ok(), "small imm needs no scratch, must stay Ok");
10403    }
10404
10405    /// #378 — `encode_operand2` (ARM32 data-processing operand) must FAIL
10406    /// HONESTLY on an immediate that is not a valid rotated immediate, rather
10407    /// than silently masking it to `imm & 0xFF` and emitting a WRONG
10408    /// instruction. `0x1FF` has 9 set bits, so it cannot come from rotating an
10409    /// 8-bit imm8 — non-encodable. Real codegen materializes large constants via
10410    /// MOVW/MOVT; this guards the encoder's Ok-or-Err contract (#180/#185)
10411    /// directly. It is an Err (not a panic) so the `encoder_no_panic` fuzz
10412    /// harness — which drives arbitrary operands — still passes.
10413    #[test]
10414    fn test_encode_operand2_non_rotatable_imm_errs_not_masks_378() {
10415        let enc = ArmEncoder::new_arm32();
10416        let bad = enc.encode(&ArmOp::Add {
10417            rd: Reg::R0,
10418            rn: Reg::R1,
10419            op2: Operand2::Imm(0x1FF),
10420        });
10421        assert!(
10422            bad.is_err(),
10423            "non-rotatable ARM32 immediate 0x1FF must Err (was silently masked \
10424             to 0xFF), got {bad:?}"
10425        );
10426        // A representable rotated immediate still encodes fine (regression guard).
10427        let ok = enc.encode(&ArmOp::Add {
10428            rd: Reg::R0,
10429            rn: Reg::R1,
10430            op2: Operand2::Imm(0xFF),
10431        });
10432        assert!(
10433            ok.is_ok(),
10434            "0xFF is a valid rotated immediate, must stay Ok"
10435        );
10436    }
10437
10438    #[test]
10439    fn test_encode_ldr_arm32() {
10440        let encoder = ArmEncoder::new_arm32();
10441        let op = ArmOp::Ldr {
10442            rd: Reg::R0,
10443            addr: MemAddr::imm(Reg::R1, 4),
10444        };
10445
10446        let code = encoder.encode(&op).unwrap();
10447        assert_eq!(code.len(), 4);
10448
10449        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10450        // Verify load bit is set
10451        assert_eq!(instr & 0x00100000, 0x00100000);
10452    }
10453
10454    #[test]
10455    fn test_encode_str_arm32() {
10456        let encoder = ArmEncoder::new_arm32();
10457        let op = ArmOp::Str {
10458            rd: Reg::R0,
10459            addr: MemAddr::imm(Reg::SP, 0),
10460        };
10461
10462        let code = encoder.encode(&op).unwrap();
10463        assert_eq!(code.len(), 4);
10464    }
10465
10466    #[test]
10467    fn test_encode_branch_arm32() {
10468        let encoder = ArmEncoder::new_arm32();
10469        let op = ArmOp::Bl {
10470            label: "main".to_string(),
10471        };
10472
10473        let code = encoder.encode(&op).unwrap();
10474        assert_eq!(code.len(), 4);
10475
10476        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10477        // Verify BL opcode
10478        assert_eq!(instr & 0x0F000000, 0x0B000000);
10479    }
10480
10481    /// #1040: the A32 BL relocatable placeholder must carry an embedded addend
10482    /// of -8 so an R_ARM_CALL nets to exactly the symbol S — the A32 twin of
10483    /// the Thumb #167/#174 test above. A32 `BL` computes
10484    /// `target = P + 8 + (imm24 << 2)`, so:
10485    ///   - `eb000000` (imm24 = 0, addend 0) lands at S+8, two instructions
10486    ///     past the callee entry. This is what synth emitted before #1040, and
10487    ///     combined with the mislabelled R_ARM_THM_CALL a real linker
10488    ///     (`arm-none-eabi-ld`) corrupted the word to `eaca0000` — opcode
10489    ///     `eb` (BL) flipped to `ea` (B), so LR was never set at all.
10490    ///   - `ebfffffe` (imm24 = -2, offset -8) is `bl <self>` and nets to S.
10491    ///     This is exactly what `arm-none-eabi-as -march=armv7-r` emits for
10492    ///     `bl <extern>`, verified directly rather than derived from the ABI.
10493    #[test]
10494    fn test_encode_arm32_bl_placeholder_addend_1040() {
10495        let encoder = ArmEncoder::new_arm32();
10496        let code = encoder
10497            .encode(&ArmOp::Bl {
10498                label: "callee".to_string(),
10499            })
10500            .unwrap();
10501        assert_eq!(code.len(), 4, "A32 BL is one 32-bit word");
10502        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10503        assert_eq!(
10504            instr, 0xEBFF_FFFE,
10505            "A32 BL placeholder must be `ebfffffe` (gas's `bl <extern>`), not              `eb000000` — a 0 addend resolves two instructions past the callee              entry (#1040)"
10506        );
10507        // Spell the addend out so a future edit cannot satisfy the literal by
10508        // accident: sign-extended imm24, word-scaled, plus the +8 pipeline
10509        // bias, must be exactly 0 (branch-to-self).
10510        let imm24 = instr & 0x00FF_FFFF;
10511        let signed = ((imm24 as i32) << 8) >> 8;
10512        assert_eq!(
10513            8 + (signed << 2),
10514            0,
10515            "placeholder must branch to itself so the REL addend is -8"
10516        );
10517    }
10518
10519    /// Regression test for #167 + #174: the Thumb-2 BL relocatable placeholder
10520    /// must carry a -4 addend so an R_ARM_THM_CALL nets to exactly the symbol S.
10521    /// The correct encoding is what `gas` emits for `bl <extern>`: f7ff fffe
10522    /// (hw1=0xF7FF, hw2=0xFFFE), little-endian bytes FF F7 FE FF.
10523    ///   - 0xD000 (J1=J2=0) → ~+0x600000 garbage addend: `bl c0000c` / truncated
10524    ///     to fit (#167).
10525    ///   - 0xF800 (addend 0) → lands at S+4, one instruction past the callee
10526    ///     entry (#174).
10527    ///   - 0xFFFE (addend -4) → lands at S. Correct.
10528    #[test]
10529    fn test_encode_thumb_bl_placeholder_addend_167_174() {
10530        let encoder = ArmEncoder::new_thumb2();
10531        let op = ArmOp::Bl {
10532            label: "callee".to_string(),
10533        };
10534
10535        let code = encoder.encode(&op).unwrap();
10536        assert_eq!(code.len(), 4, "Thumb-2 BL is 32-bit");
10537
10538        let hw1 = u16::from_le_bytes([code[0], code[1]]);
10539        let hw2 = u16::from_le_bytes([code[2], code[3]]);
10540        assert_eq!(hw1, 0xF7FF, "BL first halfword (matches gas `bl <extern>`)");
10541        assert_eq!(
10542            hw2, 0xFFFE,
10543            "BL second halfword must be 0xFFFE (-4 addend → nets to S), not 0xF800 (→ S+4, #174) or 0xD000 (#167)"
10544        );
10545        assert_ne!(hw2, 0xF800, "0xF800 (addend 0) lands at S+4 (#174)");
10546        assert_ne!(hw2, 0xD000, "0xD000 bakes in a ~+0x600000 addend (#167)");
10547    }
10548
10549    /// #740: the Thumb-2 32-bit B<cond>.W (encoding T3) must pack the
10550    /// HALFWORD offset directly into S:J2:J1:imm6:imm11 — the byte offset is
10551    /// SignExtend(S:J2:J1:imm6:imm11:'0'). The old arm packed
10552    /// `halfword_offset >> 1`, HALVING every wide conditional branch's
10553    /// displacement: gust_poll's loop-head `br_if` to an outer block end
10554    /// landed mid-shape (a spurious state write + spurious calls on the
10555    /// empty-budget path). Narrow (16-bit) B<cond> was unaffected — only
10556    /// spans > 254 bytes hit the bug. Bytes cross-checked against the llvm
10557    /// disassembler (`bne.w #0x224` = f040 8112).
10558    #[test]
10559    fn test_encode_thumb_bcond_wide_t3_halfword_offset_740() {
10560        use synth_synthesis::Condition;
10561        let encoder = ArmEncoder::new_thumb2();
10562
10563        // gust_poll's loop-head edge: NE, +0x112 halfwords (+0x224 bytes).
10564        let code = encoder
10565            .encode(&ArmOp::BCondOffset {
10566                cond: Condition::NE,
10567                offset: 0x112,
10568            })
10569            .unwrap();
10570        assert_eq!(code.len(), 4, "offset beyond ±127 halfwords must be wide");
10571        let hw1 = u16::from_le_bytes([code[0], code[1]]);
10572        let hw2 = u16::from_le_bytes([code[2], code[3]]);
10573        assert_eq!(hw1, 0xF040, "T3 hw1: 1111 0 S=0 cond=NE imm6=0");
10574        assert_eq!(
10575            hw2, 0x8112,
10576            "T3 hw2 imm11 must carry halfword offset bits [10:0] directly — \
10577             0x8089 (offset>>1) is the halved #740 miscompile"
10578        );
10579
10580        // Backward wide branch: EQ, -0x100 halfwords. S=1, J2=J1=1,
10581        // imm6=0b111111, imm11=0x700 → f43f af00.
10582        let code = encoder
10583            .encode(&ArmOp::BCondOffset {
10584                cond: Condition::EQ,
10585                offset: -0x100,
10586            })
10587            .unwrap();
10588        assert_eq!(code.len(), 4);
10589        let hw1 = u16::from_le_bytes([code[0], code[1]]);
10590        let hw2 = u16::from_le_bytes([code[2], code[3]]);
10591        assert_eq!(hw1, 0xF43F, "T3 hw1: S=1, cond=EQ, imm6=0x3F");
10592        assert_eq!(hw2, 0xAF00, "T3 hw2: J1=1 J2=1 imm11=0x700");
10593
10594        // Narrow encoding stays byte-identical (in-range offsets untouched).
10595        let code = encoder
10596            .encode(&ArmOp::BCondOffset {
10597                cond: Condition::EQ,
10598                offset: 5,
10599            })
10600            .unwrap();
10601        assert_eq!(code, vec![0x05, 0xD0], "narrow B<cond> unchanged");
10602
10603        // Out of the signed 20-bit T3 range: loud Err, never a truncated jump.
10604        assert!(
10605            encoder
10606                .encode(&ArmOp::BCondOffset {
10607                    cond: Condition::NE,
10608                    offset: 1 << 19,
10609                })
10610                .is_err(),
10611            "out-of-range T3 offset must be a loud decline"
10612        );
10613    }
10614
10615    #[test]
10616    fn test_encode_sequence() {
10617        let encoder = ArmEncoder::new_arm32();
10618        let ops = vec![
10619            ArmOp::Mov {
10620                rd: Reg::R0,
10621                op2: Operand2::Imm(42),
10622            },
10623            ArmOp::Mov {
10624                rd: Reg::R1,
10625                op2: Operand2::Imm(10),
10626            },
10627            ArmOp::Add {
10628                rd: Reg::R2,
10629                rn: Reg::R0,
10630                op2: Operand2::Reg(Reg::R1),
10631            },
10632        ];
10633
10634        let code = encoder.encode_sequence(&ops).unwrap();
10635        assert_eq!(code.len(), 12); // 3 instructions * 4 bytes
10636    }
10637
10638    #[test]
10639    fn test_reg_to_bits() {
10640        assert_eq!(reg_to_bits(&Reg::R0), 0);
10641        assert_eq!(reg_to_bits(&Reg::R7), 7);
10642        assert_eq!(reg_to_bits(&Reg::SP), 13);
10643        assert_eq!(reg_to_bits(&Reg::LR), 14);
10644        assert_eq!(reg_to_bits(&Reg::PC), 15);
10645    }
10646
10647    #[test]
10648    fn test_encode_bitwise_operations() {
10649        let encoder = ArmEncoder::new_arm32();
10650
10651        let and_op = ArmOp::And {
10652            rd: Reg::R0,
10653            rn: Reg::R1,
10654            op2: Operand2::Reg(Reg::R2),
10655        };
10656        let and_code = encoder.encode(&and_op).unwrap();
10657        assert_eq!(and_code.len(), 4);
10658
10659        let orr_op = ArmOp::Orr {
10660            rd: Reg::R0,
10661            rn: Reg::R1,
10662            op2: Operand2::Reg(Reg::R2),
10663        };
10664        let orr_code = encoder.encode(&orr_op).unwrap();
10665        assert_eq!(orr_code.len(), 4);
10666
10667        let eor_op = ArmOp::Eor {
10668            rd: Reg::R0,
10669            rn: Reg::R1,
10670            op2: Operand2::Reg(Reg::R2),
10671        };
10672        let eor_code = encoder.encode(&eor_op).unwrap();
10673        assert_eq!(eor_code.len(), 4);
10674    }
10675
10676    // === Thumb-2 32-bit encoding tests ===
10677
10678    #[test]
10679    fn test_encode_sdiv_thumb2() {
10680        let encoder = ArmEncoder::new_thumb2();
10681        let op = ArmOp::Sdiv {
10682            rd: Reg::R0,
10683            rn: Reg::R1,
10684            rm: Reg::R2,
10685        };
10686
10687        let code = encoder.encode(&op).unwrap();
10688        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10689
10690        // SDIV R0, R1, R2: 0xFB91 0xF0F2
10691        // First halfword: 0xFB90 | Rn(1) = 0xFB91
10692        // Second halfword: 0xF0F0 | Rd(0)<<8 | Rm(2) = 0xF0F2
10693        // Little-endian: [0x91, 0xFB, 0xF2, 0xF0]
10694        assert_eq!(code[0], 0x91);
10695        assert_eq!(code[1], 0xFB);
10696        assert_eq!(code[2], 0xF2);
10697        assert_eq!(code[3], 0xF0);
10698    }
10699
10700    #[test]
10701    fn test_encode_udiv_thumb2() {
10702        let encoder = ArmEncoder::new_thumb2();
10703        let op = ArmOp::Udiv {
10704            rd: Reg::R0,
10705            rn: Reg::R1,
10706            rm: Reg::R2,
10707        };
10708
10709        let code = encoder.encode(&op).unwrap();
10710        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10711
10712        // UDIV R0, R1, R2: 0xFBB1 0xF0F2
10713        // Little-endian: [0xB1, 0xFB, 0xF2, 0xF0]
10714        assert_eq!(code[0], 0xB1);
10715        assert_eq!(code[1], 0xFB);
10716        assert_eq!(code[2], 0xF2);
10717        assert_eq!(code[3], 0xF0);
10718    }
10719
10720    #[test]
10721    fn test_encode_mul_thumb2() {
10722        let encoder = ArmEncoder::new_thumb2();
10723        let op = ArmOp::Mul {
10724            rd: Reg::R0,
10725            rn: Reg::R1,
10726            rm: Reg::R2,
10727        };
10728
10729        let code = encoder.encode(&op).unwrap();
10730        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10731    }
10732
10733    #[test]
10734    fn test_encode_and_thumb2() {
10735        let encoder = ArmEncoder::new_thumb2();
10736        let op = ArmOp::And {
10737            rd: Reg::R0,
10738            rn: Reg::R1,
10739            op2: Operand2::Reg(Reg::R2),
10740        };
10741
10742        let code = encoder.encode(&op).unwrap();
10743        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10744    }
10745
10746    #[test]
10747    fn test_encode_lsl_thumb2_low_regs() {
10748        let encoder = ArmEncoder::new_thumb2();
10749        let op = ArmOp::Lsl {
10750            rd: Reg::R0,
10751            rn: Reg::R1,
10752            shift: 5,
10753        };
10754
10755        let code = encoder.encode(&op).unwrap();
10756        assert_eq!(code.len(), 2); // 16-bit for low registers
10757    }
10758
10759    #[test]
10760    fn test_encode_clz_thumb2() {
10761        let encoder = ArmEncoder::new_thumb2();
10762        let op = ArmOp::Clz {
10763            rd: Reg::R0,
10764            rm: Reg::R1,
10765        };
10766
10767        let code = encoder.encode(&op).unwrap();
10768        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10769    }
10770
10771    #[test]
10772    fn test_encode_bx_thumb2() {
10773        let encoder = ArmEncoder::new_thumb2();
10774        let op = ArmOp::Bx { rm: Reg::LR };
10775
10776        let code = encoder.encode(&op).unwrap();
10777        assert_eq!(code.len(), 2); // 16-bit instruction
10778
10779        // BX LR: 0x4770
10780        assert_eq!(code, vec![0x70, 0x47]);
10781    }
10782
10783    // ========================================================================
10784    // f32 pseudo-op encoding tests
10785    // ========================================================================
10786
10787    #[test]
10788    fn test_encode_f32_abs_arm32() {
10789        let encoder = ArmEncoder::new_arm32();
10790        let op = ArmOp::F32Abs {
10791            sd: VfpReg::S0,
10792            sm: VfpReg::S2,
10793        };
10794        let code = encoder.encode(&op).unwrap();
10795        assert_eq!(code.len(), 4); // Single VFP instruction
10796    }
10797
10798    #[test]
10799    fn test_encode_f32_neg_arm32() {
10800        let encoder = ArmEncoder::new_arm32();
10801        let op = ArmOp::F32Neg {
10802            sd: VfpReg::S0,
10803            sm: VfpReg::S2,
10804        };
10805        let code = encoder.encode(&op).unwrap();
10806        assert_eq!(code.len(), 4);
10807    }
10808
10809    #[test]
10810    fn test_encode_f32_sqrt_arm32() {
10811        let encoder = ArmEncoder::new_arm32();
10812        let op = ArmOp::F32Sqrt {
10813            sd: VfpReg::S0,
10814            sm: VfpReg::S2,
10815        };
10816        let code = encoder.encode(&op).unwrap();
10817        assert_eq!(code.len(), 4);
10818    }
10819
10820    #[test]
10821    fn test_encode_f32_ceil_arm32() {
10822        let encoder = ArmEncoder::new_arm32();
10823        let op = ArmOp::F32Ceil {
10824            sd: VfpReg::S0,
10825            sm: VfpReg::S2,
10826        };
10827        let code = encoder.encode(&op).unwrap();
10828        // VMRS + BIC + ORR + VMSR + VCVT.S32.F32 + VMRS + BIC + VMSR + VCVT.F32.S32
10829        assert_eq!(code.len(), 36);
10830    }
10831
10832    #[test]
10833    fn test_encode_f32_floor_thumb2() {
10834        let encoder = ArmEncoder::new_thumb2();
10835        let op = ArmOp::F32Floor {
10836            sd: VfpReg::S0,
10837            sm: VfpReg::S2,
10838        };
10839        let code = encoder.encode(&op).unwrap();
10840        // VMRS + BIC.W + ORR.W + VMSR + VCVT + VMRS + BIC.W + VMSR + VCVT.F32.S32
10841        assert_eq!(code.len(), 36);
10842    }
10843
10844    #[test]
10845    fn test_encode_f32_min_arm32() {
10846        let encoder = ArmEncoder::new_arm32();
10847        let op = ArmOp::F32Min {
10848            sd: VfpReg::S0,
10849            sn: VfpReg::S2,
10850            sm: VfpReg::S4,
10851        };
10852        let code = encoder.encode(&op).unwrap();
10853        assert_eq!(code.len(), 16); // VMOV + VCMP + VMRS + conditional VMOV
10854    }
10855
10856    #[test]
10857    fn test_encode_f32_max_thumb2() {
10858        let encoder = ArmEncoder::new_thumb2();
10859        let op = ArmOp::F32Max {
10860            sd: VfpReg::S0,
10861            sn: VfpReg::S2,
10862            sm: VfpReg::S4,
10863        };
10864        let code = encoder.encode(&op).unwrap();
10865        // VMOV(4) + VCMP(4) + VMRS(4) + IT(2) + VMOV(4) = 18
10866        assert_eq!(code.len(), 18);
10867    }
10868
10869    #[test]
10870    fn test_encode_f32_copysign_arm32() {
10871        let encoder = ArmEncoder::new_arm32();
10872        let op = ArmOp::F32Copysign {
10873            sd: VfpReg::S0,
10874            sn: VfpReg::S2,
10875            sm: VfpReg::S4,
10876        };
10877        let code = encoder.encode(&op).unwrap();
10878        // VMOV + VMOV + AND + BIC + ORR + VMOV = 6 * 4 = 24
10879        assert_eq!(code.len(), 24);
10880    }
10881
10882    // ========================================================================
10883    // f64 encoding tests
10884    // ========================================================================
10885
10886    #[test]
10887    fn test_encode_f64_add_arm32() {
10888        let encoder = ArmEncoder::new_arm32();
10889        let op = ArmOp::F64Add {
10890            dd: VfpReg::D0,
10891            dn: VfpReg::D1,
10892            dm: VfpReg::D2,
10893        };
10894        let code = encoder.encode(&op).unwrap();
10895        assert_eq!(code.len(), 4);
10896        // VADD.F64 D0, D1, D2: check coprocessor is cp11 (0xB)
10897        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10898        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
10899    }
10900
10901    #[test]
10902    fn test_encode_f64_sub_thumb2() {
10903        let encoder = ArmEncoder::new_thumb2();
10904        let op = ArmOp::F64Sub {
10905            dd: VfpReg::D0,
10906            dn: VfpReg::D1,
10907            dm: VfpReg::D2,
10908        };
10909        let code = encoder.encode(&op).unwrap();
10910        assert_eq!(code.len(), 4); // 32-bit VFP as two Thumb halfwords
10911    }
10912
10913    #[test]
10914    fn test_encode_f64_mul_arm32() {
10915        let encoder = ArmEncoder::new_arm32();
10916        let op = ArmOp::F64Mul {
10917            dd: VfpReg::D0,
10918            dn: VfpReg::D1,
10919            dm: VfpReg::D2,
10920        };
10921        let code = encoder.encode(&op).unwrap();
10922        assert_eq!(code.len(), 4);
10923    }
10924
10925    #[test]
10926    fn test_encode_f64_div_arm32() {
10927        let encoder = ArmEncoder::new_arm32();
10928        let op = ArmOp::F64Div {
10929            dd: VfpReg::D0,
10930            dn: VfpReg::D1,
10931            dm: VfpReg::D2,
10932        };
10933        let code = encoder.encode(&op).unwrap();
10934        assert_eq!(code.len(), 4);
10935    }
10936
10937    #[test]
10938    fn test_encode_f64_abs_arm32() {
10939        let encoder = ArmEncoder::new_arm32();
10940        let op = ArmOp::F64Abs {
10941            dd: VfpReg::D0,
10942            dm: VfpReg::D2,
10943        };
10944        let code = encoder.encode(&op).unwrap();
10945        assert_eq!(code.len(), 4);
10946    }
10947
10948    #[test]
10949    fn test_encode_f64_neg_arm32() {
10950        let encoder = ArmEncoder::new_arm32();
10951        let op = ArmOp::F64Neg {
10952            dd: VfpReg::D0,
10953            dm: VfpReg::D2,
10954        };
10955        let code = encoder.encode(&op).unwrap();
10956        assert_eq!(code.len(), 4);
10957    }
10958
10959    #[test]
10960    fn test_encode_f64_sqrt_arm32() {
10961        let encoder = ArmEncoder::new_arm32();
10962        let op = ArmOp::F64Sqrt {
10963            dd: VfpReg::D0,
10964            dm: VfpReg::D2,
10965        };
10966        let code = encoder.encode(&op).unwrap();
10967        assert_eq!(code.len(), 4);
10968    }
10969
10970    #[test]
10971    fn test_encode_f64_load_arm32() {
10972        let encoder = ArmEncoder::new_arm32();
10973        let op = ArmOp::F64Load {
10974            dd: VfpReg::D0,
10975            addr: MemAddr::imm(Reg::R0, 8),
10976        };
10977        let code = encoder.encode(&op).unwrap();
10978        assert_eq!(code.len(), 4);
10979        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10980        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11 for F64
10981        assert_eq!(instr & 0xFF, 2); // offset 8 / 4 = 2
10982    }
10983
10984    #[test]
10985    fn test_encode_f64_store_thumb2() {
10986        let encoder = ArmEncoder::new_thumb2();
10987        let op = ArmOp::F64Store {
10988            dd: VfpReg::D0,
10989            addr: MemAddr::imm(Reg::SP, 0),
10990        };
10991        let code = encoder.encode(&op).unwrap();
10992        assert_eq!(code.len(), 4);
10993    }
10994
10995    #[test]
10996    fn test_encode_f64_compare_arm32() {
10997        let encoder = ArmEncoder::new_arm32();
10998        let op = ArmOp::F64Eq {
10999            rd: Reg::R0,
11000            dn: VfpReg::D0,
11001            dm: VfpReg::D1,
11002        };
11003        let code = encoder.encode(&op).unwrap();
11004        assert_eq!(code.len(), 16); // VCMP + VMRS + MOV #0 + MOVcond #1
11005    }
11006
11007    #[test]
11008    fn test_encode_f64_compare_thumb2() {
11009        let encoder = ArmEncoder::new_thumb2();
11010        let op = ArmOp::F64Lt {
11011            rd: Reg::R0,
11012            dn: VfpReg::D0,
11013            dm: VfpReg::D1,
11014        };
11015        let code = encoder.encode(&op).unwrap();
11016        // VCMP(4) + VMRS(4) + MOVS(2) + IT(2) + MOV(2) = 14
11017        assert_eq!(code.len(), 14);
11018    }
11019
11020    #[test]
11021    fn test_encode_f64_const_arm32() {
11022        let encoder = ArmEncoder::new_arm32();
11023        let op = ArmOp::F64Const {
11024            dd: VfpReg::D0,
11025            value: 3.125,
11026        };
11027        let code = encoder.encode(&op).unwrap();
11028        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
11029        assert_eq!(code.len(), 20);
11030    }
11031
11032    #[test]
11033    fn test_encode_f64_const_thumb2() {
11034        let encoder = ArmEncoder::new_thumb2();
11035        let op = ArmOp::F64Const {
11036            dd: VfpReg::D0,
11037            value: 2.5,
11038        };
11039        let code = encoder.encode(&op).unwrap();
11040        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
11041        assert_eq!(code.len(), 20);
11042    }
11043
11044    #[test]
11045    fn test_encode_f64_convert_i32s_arm32() {
11046        let encoder = ArmEncoder::new_arm32();
11047        let op = ArmOp::F64ConvertI32S {
11048            dd: VfpReg::D0,
11049            rm: Reg::R0,
11050        };
11051        let code = encoder.encode(&op).unwrap();
11052        // VMOV(4) + VCVT(4) = 8
11053        assert_eq!(code.len(), 8);
11054    }
11055
11056    #[test]
11057    fn test_encode_f64_promote_f32_arm32() {
11058        let encoder = ArmEncoder::new_arm32();
11059        let op = ArmOp::F64PromoteF32 {
11060            dd: VfpReg::D0,
11061            sm: VfpReg::S0,
11062        };
11063        let code = encoder.encode(&op).unwrap();
11064        assert_eq!(code.len(), 4); // Single VCVT.F64.F32 instruction
11065    }
11066
11067    #[test]
11068    fn test_encode_f64_promote_f32_thumb2() {
11069        let encoder = ArmEncoder::new_thumb2();
11070        let op = ArmOp::F64PromoteF32 {
11071            dd: VfpReg::D0,
11072            sm: VfpReg::S0,
11073        };
11074        let code = encoder.encode(&op).unwrap();
11075        assert_eq!(code.len(), 4);
11076    }
11077
11078    #[test]
11079    fn test_encode_i32_trunc_f64s_arm32() {
11080        let encoder = ArmEncoder::new_arm32();
11081        let op = ArmOp::I32TruncF64S {
11082            rd: Reg::R0,
11083            dm: VfpReg::D0,
11084        };
11085        let code = encoder.encode(&op).unwrap();
11086        // VCVT(4) + VMOV(4) = 8
11087        assert_eq!(code.len(), 8);
11088    }
11089
11090    #[test]
11091    fn test_encode_f64_reinterpret_i64_arm32() {
11092        let encoder = ArmEncoder::new_arm32();
11093        let op = ArmOp::F64ReinterpretI64 {
11094            dd: VfpReg::D0,
11095            rmlo: Reg::R0,
11096            rmhi: Reg::R1,
11097        };
11098        let code = encoder.encode(&op).unwrap();
11099        assert_eq!(code.len(), 4); // Single VMOV instruction
11100    }
11101
11102    #[test]
11103    fn test_encode_i64_reinterpret_f64_thumb2() {
11104        let encoder = ArmEncoder::new_thumb2();
11105        let op = ArmOp::I64ReinterpretF64 {
11106            rdlo: Reg::R0,
11107            rdhi: Reg::R1,
11108            dm: VfpReg::D0,
11109        };
11110        let code = encoder.encode(&op).unwrap();
11111        assert_eq!(code.len(), 4);
11112    }
11113
11114    #[test]
11115    fn test_encode_f64_trunc_thumb2() {
11116        let encoder = ArmEncoder::new_thumb2();
11117        let op = ArmOp::F64Trunc {
11118            dd: VfpReg::D0,
11119            dm: VfpReg::D1,
11120        };
11121        let code = encoder.encode(&op).unwrap();
11122        // GI-FPU-002 phase 3 (#369): a single VRINTZ.F64 (clang-verified
11123        // vrintz.f64 d0,d1 base) — no more FPSCR dance / S0 clobber.
11124        assert_eq!(code.len(), 4);
11125        assert_eq!(code, vec![0xb6, 0xee, 0xc1, 0x0b]);
11126    }
11127
11128    /// GI-FPU-002 phase 3 (#369): the rewritten f64 tail sequences, byte-exact
11129    /// against clang (`-target thumbv7em-none-eabi -mfpu=fpv5-d16`). Each
11130    /// clobbers ONLY its destination (+R12/flags where noted) — the previous
11131    /// pseudo-ops staged through live S0/R0-R2 (the #615 class) and the
11132    /// min/max/rounding semantics were wrong (ordered IT select returned the
11133    /// wrong operand on NaN/±0; rounding round-tripped through a 32-bit int).
11134    #[test]
11135    fn test_369_f64_tail_thumb2_encodings_match_clang() {
11136        let enc = ArmEncoder::new_thumb2();
11137        // vrintn/vrintp/vrintm.f64 d1, d2 (FE space, never IT'd).
11138        for (op, want) in [
11139            (
11140                ArmOp::F64Nearest {
11141                    dd: VfpReg::D1,
11142                    dm: VfpReg::D2,
11143                },
11144                vec![0xb9, 0xfe, 0x42, 0x1b],
11145            ),
11146            (
11147                ArmOp::F64Ceil {
11148                    dd: VfpReg::D1,
11149                    dm: VfpReg::D2,
11150                },
11151                vec![0xba, 0xfe, 0x42, 0x1b],
11152            ),
11153            (
11154                ArmOp::F64Floor {
11155                    dd: VfpReg::D1,
11156                    dm: VfpReg::D2,
11157                },
11158                vec![0xbb, 0xfe, 0x42, 0x1b],
11159            ),
11160        ] {
11161            assert_eq!(enc.encode(&op).unwrap(), want, "{op:?}");
11162        }
11163        // vcmp.f64 d1,d2 ; vmrs ; vminnm.f64 d0,d1,d2 ; it vs ; vaddvs.f64
11164        let min = enc
11165            .encode(&ArmOp::F64Min {
11166                dd: VfpReg::D0,
11167                dn: VfpReg::D1,
11168                dm: VfpReg::D2,
11169            })
11170            .unwrap();
11171        assert_eq!(
11172            min,
11173            vec![
11174                0xb4, 0xee, 0x42, 0x1b, // vcmp.f64 d1, d2
11175                0xf1, 0xee, 0x10, 0xfa, // vmrs APSR_nzcv, fpscr
11176                0x81, 0xfe, 0x42, 0x0b, // vminnm.f64 d0, d1, d2
11177                0x68, 0xbf, // it vs
11178                0x31, 0xee, 0x02, 0x0b, // vaddvs.f64 d0, d1, d2
11179            ]
11180        );
11181        // vmaxnm variant flips only bit6 of the VMINNM word.
11182        let max = enc
11183            .encode(&ArmOp::F64Max {
11184                dd: VfpReg::D0,
11185                dn: VfpReg::D1,
11186                dm: VfpReg::D2,
11187            })
11188            .unwrap();
11189        assert_eq!(&max[8..12], &[0x81, 0xfe, 0x02, 0x0b]);
11190        // Destination aliasing a source must ERR (the NaN fix-up would read
11191        // a clobbered operand), never encode.
11192        assert!(
11193            enc.encode(&ArmOp::F64Min {
11194                dd: VfpReg::D1,
11195                dn: VfpReg::D1,
11196                dm: VfpReg::D2,
11197            })
11198            .is_err()
11199        );
11200        // copysign d0,(mag)d1,(sign)d2:
11201        // vmov r12,s5 ; cmp.w r12,#0 ; vabs.f64 d0,d1 ; it mi ; vnegmi.f64 d0,d0
11202        let cs = enc
11203            .encode(&ArmOp::F64Copysign {
11204                dd: VfpReg::D0,
11205                dn: VfpReg::D1,
11206                dm: VfpReg::D2,
11207            })
11208            .unwrap();
11209        assert_eq!(
11210            cs,
11211            vec![
11212                0x12, 0xee, 0x90, 0xca, // vmov r12, s5
11213                0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
11214                0xb0, 0xee, 0xc1, 0x0b, // vabs.f64 d0, d1
11215                0x48, 0xbf, // it mi
11216                0xb1, 0xee, 0x40, 0x0b, // vnegmi.f64 d0, d0
11217            ]
11218        );
11219        // f32 copysign s0,(mag)s1,(sign)s2 — the R0-clobber-free rewrite:
11220        // vmov r12,s2 ; cmp.w r12,#0 ; vabs.f32 s0,s1 ; it mi ; vnegmi.f32
11221        let cs32 = enc
11222            .encode(&ArmOp::F32Copysign {
11223                sd: VfpReg::S0,
11224                sn: VfpReg::S1,
11225                sm: VfpReg::S2,
11226            })
11227            .unwrap();
11228        assert_eq!(
11229            cs32,
11230            vec![
11231                0x11, 0xee, 0x10, 0xca, // vmov r12, s2
11232                0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
11233                0xb0, 0xee, 0xe0, 0x0a, // vabs.f32 s0, s1
11234                0x48, 0xbf, // it mi
11235                0xb1, 0xee, 0x40, 0x0a, // vnegmi.f32 s0, s0
11236            ]
11237        );
11238        // i32 -> f64 stages through the DESTINATION's S-alias (never S0) and
11239        // uses the CORRECT signed/unsigned VCVT bases (previously swapped):
11240        // vmov s0,r3 ; vcvt.f64.s32 d0,s0
11241        let conv_s = enc
11242            .encode(&ArmOp::F64ConvertI32S {
11243                dd: VfpReg::D0,
11244                rm: Reg::R3,
11245            })
11246            .unwrap();
11247        assert_eq!(
11248            conv_s,
11249            vec![
11250                0x00, 0xee, 0x10, 0x3a, // vmov s0, r3
11251                0xb8, 0xee, 0xc0, 0x0b, // vcvt.f64.s32 d0, s0
11252            ]
11253        );
11254        let conv_u = enc
11255            .encode(&ArmOp::F64ConvertI32U {
11256                dd: VfpReg::D0,
11257                rm: Reg::R3,
11258            })
11259            .unwrap();
11260        assert_eq!(&conv_u[4..8], &[0xb8, 0xee, 0x40, 0x0b]); // vcvt.f64.u32
11261        // f64 -> i32 stages through the SOURCE's S-alias (never S0):
11262        // vcvt.s32.f64 s2,d1 ; vmov r3,s2
11263        let trunc_s = enc
11264            .encode(&ArmOp::I32TruncF64S {
11265                rd: Reg::R3,
11266                dm: VfpReg::D1,
11267            })
11268            .unwrap();
11269        assert_eq!(
11270            trunc_s,
11271            vec![
11272                0xbd, 0xee, 0xc1, 0x1b, // vcvt.s32.f64 s2, d1
11273                0x11, 0xee, 0x10, 0x3a, // vmov r3, s2
11274            ]
11275        );
11276        let trunc_u = enc
11277            .encode(&ArmOp::I32TruncF64U {
11278                rd: Reg::R3,
11279                dm: VfpReg::D1,
11280            })
11281            .unwrap();
11282        assert_eq!(&trunc_u[0..4], &[0xbc, 0xee, 0xc1, 0x1b]); // vcvt.u32.f64
11283        // f32.demote_f64: vcvt.f32.f64 s1, d2
11284        let demote = enc
11285            .encode(&ArmOp::F32DemoteF64 {
11286                sd: VfpReg::S1,
11287                dm: VfpReg::D2,
11288            })
11289            .unwrap();
11290        assert_eq!(demote, vec![0xf7, 0xee, 0xc2, 0x0b]);
11291    }
11292
11293    #[test]
11294    fn test_encode_f64_min_arm32() {
11295        let encoder = ArmEncoder::new_arm32();
11296        let op = ArmOp::F64Min {
11297            dd: VfpReg::D0,
11298            dn: VfpReg::D1,
11299            dm: VfpReg::D2,
11300        };
11301        let code = encoder.encode(&op).unwrap();
11302        // VMOV + VCMP + VMRS + conditional VMOV = 16
11303        assert_eq!(code.len(), 16);
11304    }
11305
11306    #[test]
11307    fn test_f64_cp11_encoding() {
11308        // Verify that F64 instructions use coprocessor 11 (0xB), not 10 (0xA)
11309        let encoder = ArmEncoder::new_arm32();
11310
11311        // F64Add
11312        let code = encoder
11313            .encode(&ArmOp::F64Add {
11314                dd: VfpReg::D0,
11315                dn: VfpReg::D0,
11316                dm: VfpReg::D0,
11317            })
11318            .unwrap();
11319        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11320        assert_eq!((instr >> 8) & 0xF, 0xB, "F64 should use cp11");
11321
11322        // F32Add for comparison
11323        let code = encoder
11324            .encode(&ArmOp::F32Add {
11325                sd: VfpReg::S0,
11326                sn: VfpReg::S0,
11327                sm: VfpReg::S0,
11328            })
11329            .unwrap();
11330        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11331        assert_eq!((instr >> 8) & 0xF, 0xA, "F32 should use cp10");
11332    }
11333
11334    #[test]
11335    fn test_dreg_encoding_higher_registers() {
11336        let encoder = ArmEncoder::new_arm32();
11337
11338        // Test with D15 (highest register)
11339        let op = ArmOp::F64Add {
11340            dd: VfpReg::D15,
11341            dn: VfpReg::D14,
11342            dm: VfpReg::D13,
11343        };
11344        let code = encoder.encode(&op).unwrap();
11345        assert_eq!(code.len(), 4);
11346
11347        // Verify the register encoding worked (instruction is valid)
11348        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11349        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
11350    }
11351
11352    // ========================================================================
11353    // Control flow encoding tests
11354    // ========================================================================
11355
11356    #[test]
11357    fn test_encode_label_emits_no_bytes() {
11358        let encoder = ArmEncoder::new_thumb2();
11359        let op = ArmOp::Label {
11360            name: ".Lblock_end_0".to_string(),
11361        };
11362        let code = encoder.encode(&op).unwrap();
11363        assert!(code.is_empty(), "Label should emit zero bytes");
11364
11365        let encoder32 = ArmEncoder::new_arm32();
11366        let code32 = encoder32.encode(&op).unwrap();
11367        assert!(
11368            code32.is_empty(),
11369            "Label should emit zero bytes in ARM32 too"
11370        );
11371    }
11372
11373    #[test]
11374    fn test_encode_bcc_eq_thumb2() {
11375        use synth_synthesis::Condition;
11376        let encoder = ArmEncoder::new_thumb2();
11377        let op = ArmOp::Bcc {
11378            cond: Condition::EQ,
11379            label: "target".to_string(),
11380        };
11381        let code = encoder.encode(&op).unwrap();
11382        assert_eq!(code.len(), 2); // 16-bit conditional branch
11383
11384        // BEQ with offset 0: 0xD000 in little-endian
11385        assert_eq!(code, vec![0x00, 0xD0]);
11386    }
11387
11388    #[test]
11389    fn test_encode_bcc_ne_thumb2() {
11390        use synth_synthesis::Condition;
11391        let encoder = ArmEncoder::new_thumb2();
11392        let op = ArmOp::Bcc {
11393            cond: Condition::NE,
11394            label: "target".to_string(),
11395        };
11396        let code = encoder.encode(&op).unwrap();
11397        assert_eq!(code.len(), 2);
11398
11399        // BNE with offset 0: 0xD100 in little-endian
11400        assert_eq!(code, vec![0x00, 0xD1]);
11401    }
11402
11403    #[test]
11404    fn test_encode_bcc_arm32() {
11405        use synth_synthesis::Condition;
11406        let encoder = ArmEncoder::new_arm32();
11407        let op = ArmOp::Bcc {
11408            cond: Condition::EQ,
11409            label: "target".to_string(),
11410        };
11411        let code = encoder.encode(&op).unwrap();
11412        assert_eq!(code.len(), 4); // 32-bit ARM instruction
11413
11414        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11415        // BEQ: cond=0x0, opcode=0xA, offset=0
11416        assert_eq!(instr & 0xF0000000, 0x00000000); // EQ condition
11417        assert_eq!(instr & 0x0F000000, 0x0A000000); // Branch opcode
11418    }
11419
11420    #[test]
11421    fn test_encode_udf_thumb2() {
11422        let encoder = ArmEncoder::new_thumb2();
11423        let op = ArmOp::Udf { imm: 0 };
11424        let code = encoder.encode(&op).unwrap();
11425        assert_eq!(code.len(), 2); // 16-bit
11426
11427        // UDF #0: 0xDE00 in little-endian
11428        assert_eq!(code, vec![0x00, 0xDE]);
11429    }
11430
11431    /// #610: the i64 rot/div/rem expansions must land the result in the
11432    /// selector-assigned rd pair and leave R0-R3 preserved (restored from the
11433    /// fixed-ABI wrapper's save area) — pre-#610 the rot expansion's own
11434    /// `POP {R4}` restored stale scratch OVER the result (rd_lo == R4) and
11435    /// the div/rem expansions ignored their register fields outright.
11436    #[test]
11437    fn test_610_i64_rot_expansion_ends_with_rd_movs_and_restore() {
11438        let encoder = ArmEncoder::new_thumb2();
11439        for op in [
11440            ArmOp::I64Rotl {
11441                rdlo: Reg::R4,
11442                rdhi: Reg::R5,
11443                rnlo: Reg::R0,
11444                rnhi: Reg::R1,
11445                shift: Reg::R2,
11446            },
11447            ArmOp::I64Rotr {
11448                rdlo: Reg::R4,
11449                rdhi: Reg::R5,
11450                rnlo: Reg::R0,
11451                rnhi: Reg::R1,
11452                shift: Reg::R2,
11453            },
11454        ] {
11455            let code = encoder.encode(&op).unwrap();
11456            assert_eq!(code.len(), 102, "register-independent size (estimator pin)");
11457            // Tail: MOV r5, r1 (0x460D); MOV r4, r0 (0x4604); POP {r0..r3}
11458            // (rd pair r4:r5 does not overlap the save area — all 4 restored).
11459            let tail: Vec<u16> = code[code.len() - 12..]
11460                .chunks(2)
11461                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11462                .collect();
11463            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11464        }
11465    }
11466
11467    /// #610: div/rem expansions honor rd and carry the divide-by-zero trap
11468    /// guard (`ORRS R12, R2, R3; BNE +0; UDF #0`) after operand marshaling.
11469    #[test]
11470    fn test_610_i64_div_rem_expansion_guard_and_rd() {
11471        let encoder = ArmEncoder::new_thumb2();
11472        let mk = |which: u8| {
11473            let (rdlo, rdhi, rnlo, rnhi, rmlo, rmhi) =
11474                (Reg::R4, Reg::R5, Reg::R0, Reg::R1, Reg::R2, Reg::R3);
11475            match which {
11476                0 => ArmOp::I64DivU {
11477                    rdlo,
11478                    rdhi,
11479                    rnlo,
11480                    rnhi,
11481                    rmlo,
11482                    rmhi,
11483                    elide_zero_guard: false,
11484                },
11485                1 => ArmOp::I64RemU {
11486                    rdlo,
11487                    rdhi,
11488                    rnlo,
11489                    rnhi,
11490                    rmlo,
11491                    rmhi,
11492                    elide_zero_guard: false,
11493                },
11494                2 => ArmOp::I64DivS {
11495                    rdlo,
11496                    rdhi,
11497                    rnlo,
11498                    rnhi,
11499                    rmlo,
11500                    rmhi,
11501                    elide_zero_guard: false,
11502                    elide_overflow_guard: false,
11503                },
11504                _ => ArmOp::I64RemS {
11505                    rdlo,
11506                    rdhi,
11507                    rnlo,
11508                    rnhi,
11509                    rmlo,
11510                    rmhi,
11511                    elide_zero_guard: false,
11512                },
11513            }
11514        };
11515        for which in 0..4u8 {
11516            let code = encoder.encode(&mk(which)).unwrap();
11517            // Zero-divisor trap guard right after the 26-byte marshal prologue.
11518            let guard: Vec<u16> = code[26..34]
11519                .chunks(2)
11520                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11521                .collect();
11522            assert_eq!(
11523                guard,
11524                vec![0xEA52, 0x0C03, 0xD100, 0xDE00],
11525                "ORRS R12,R2,R3; BNE +0; UDF #0"
11526            );
11527            // Tail: result into rd pair (r5:r4), then restore all of R0-R3.
11528            let tail: Vec<u16> = code[code.len() - 12..]
11529                .chunks(2)
11530                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11531                .collect();
11532            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11533        }
11534    }
11535
11536    /// #610: when rd overlaps R0-R3 the restore must SKIP the result
11537    /// registers (drop the saved caller word) instead of popping over them.
11538    #[test]
11539    fn test_610_i64_divu_rd_in_r0_r1_skips_restore() {
11540        let encoder = ArmEncoder::new_thumb2();
11541        let code = encoder
11542            .encode(&ArmOp::I64DivU {
11543                rdlo: Reg::R0,
11544                rdhi: Reg::R1,
11545                rnlo: Reg::R0,
11546                rnhi: Reg::R1,
11547                rmlo: Reg::R2,
11548                rmhi: Reg::R3,
11549                elide_zero_guard: false,
11550            })
11551            .unwrap();
11552        let tail: Vec<u16> = code[code.len() - 12..]
11553            .chunks(2)
11554            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11555            .collect();
11556        // MOV r1,r1 / MOV r0,r0 (no-ops, size-stable), ADD SP,#4 twice
11557        // (discard saved r0/r1 — the result lives there), POP {r2}, POP {r3}.
11558        assert_eq!(tail, vec![0x4609, 0x4600, 0xB001, 0xB001, 0xBC04, 0xBC08]);
11559    }
11560
11561    /// #610: a fully swapped rd pair (rd_lo=R1, rd_hi=R0) cannot be
11562    /// materialized by two MOVs in either order — must be a loud Err, never
11563    /// silent corruption. (Selector pairs are consecutive, so unreachable.)
11564    #[test]
11565    fn test_610_i64_swapped_rd_pair_rejected() {
11566        let encoder = ArmEncoder::new_thumb2();
11567        let result = encoder.encode(&ArmOp::I64RemU {
11568            rdlo: Reg::R1,
11569            rdhi: Reg::R0,
11570            rnlo: Reg::R2,
11571            rnhi: Reg::R3,
11572            rmlo: Reg::R4,
11573            rmhi: Reg::R5,
11574            elide_zero_guard: false,
11575        });
11576        assert!(result.is_err(), "swapped rd pair must be rejected loudly");
11577    }
11578
11579    /// #632: the I64Popcnt expansion's own scratch restore (`POP {R3,R4,R5}`)
11580    /// must not clobber the result. Pre-fix the total was materialized with
11581    /// `ADDS rd, R4, R5` BEFORE the pop, so any allocator-assigned
11582    /// rd ∈ {R3,R4,R5} received stale stack garbage. Post-fix the count is
11583    /// carried across the restore in R12 (never allocatable, never restored)
11584    /// and moved into rd only after the pop — structurally rd-independent.
11585    #[test]
11586    fn test_632_i64_popcnt_result_survives_scratch_restore() {
11587        let encoder = ArmEncoder::new_thumb2();
11588        // Every allocatable rd, including the restore set {R3,R4,R5} and R8.
11589        for rd in [
11590            Reg::R0,
11591            Reg::R2,
11592            Reg::R3,
11593            Reg::R4,
11594            Reg::R5,
11595            Reg::R6,
11596            Reg::R8,
11597        ] {
11598            let code = encoder
11599                .encode(&ArmOp::I64Popcnt {
11600                    rd,
11601                    rnlo: Reg::R6,
11602                    rnhi: Reg::R7,
11603                })
11604                .unwrap();
11605            assert_eq!(code.len(), 176, "register-independent size (estimator pin)");
11606            let hw: Vec<u16> = code
11607                .chunks(2)
11608                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11609                .collect();
11610            let pop = hw
11611                .iter()
11612                .position(|&h| h == 0xBC38)
11613                .expect("POP {R3,R4,R5} present");
11614            // Immediately before the POP: ADD.W R12, R4, R5 (the total lives
11615            // in R12, which the POP cannot touch).
11616            assert_eq!(
11617                &hw[pop - 2..pop],
11618                &[0xEB04, 0x0C05],
11619                "total must be carried in R12 across the restore"
11620            );
11621            // Immediately after the POP: MOV rd, R12.
11622            let rd_bits = match rd {
11623                Reg::R8 => 8u16,
11624                Reg::R6 => 6,
11625                Reg::R5 => 5,
11626                Reg::R4 => 4,
11627                Reg::R3 => 3,
11628                Reg::R2 => 2,
11629                _ => 0,
11630            };
11631            let expect_mov = 0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7);
11632            assert_eq!(hw[pop + 1], expect_mov, "MOV rd, R12 after the restore");
11633            // No write into rd between the PUSH and the POP (the old
11634            // pre-restore ADDS is gone).
11635            assert!(
11636                !hw[..pop].contains(&(0x1800 | (5 << 6) | (4 << 3) | rd_bits)),
11637                "no ADDS rd, R4, R5 before the restore pop"
11638            );
11639        }
11640    }
11641
11642    /// #632 audit: the entry marshal must be permutation-safe. Pre-fix
11643    /// `MOV R4, rnlo; MOV R5, rnhi` read a clobbered R4 when the operand
11644    /// pair lived at (R3, R4). Post-fix rnlo routes through R12.
11645    #[test]
11646    fn test_632_i64_popcnt_marshal_pair_at_r3_r4() {
11647        let encoder = ArmEncoder::new_thumb2();
11648        let code = encoder
11649            .encode(&ArmOp::I64Popcnt {
11650                rd: Reg::R0,
11651                rnlo: Reg::R3,
11652                rnhi: Reg::R4,
11653            })
11654            .unwrap();
11655        let hw: Vec<u16> = code
11656            .chunks(2)
11657            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11658            .collect();
11659        // PUSH {R3,R4,R5}; MOV R12, R3; MOV R5, R4 (rnhi read BEFORE any
11660        // write to R4); MOV R4, R12.
11661        assert_eq!(hw[0], 0xB438);
11662        assert_eq!(hw[1], 0x4600 | (1 << 7) | (3 << 3) | 4, "MOV R12, rnlo");
11663        assert_eq!(hw[2], 0x4600 | (4 << 3) | 5, "MOV R5, rnhi");
11664        assert_eq!(hw[3], 0x4664, "MOV R4, R12");
11665    }
11666
11667    /// #632: A32 twin — same structural fix on the ARM-mode path
11668    /// (`--target cortex-r5`): total carried in R12 across the restore.
11669    #[test]
11670    fn test_632_a32_i64_popcnt_result_survives_scratch_restore() {
11671        let encoder = ArmEncoder::new_arm32();
11672        for rd in [Reg::R0, Reg::R3, Reg::R4, Reg::R5, Reg::R8] {
11673            let code = encoder
11674                .encode(&ArmOp::I64Popcnt {
11675                    rd,
11676                    rnlo: Reg::R6,
11677                    rnhi: Reg::R7,
11678                })
11679                .unwrap();
11680            let words: Vec<u32> = code
11681                .chunks(4)
11682                .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11683                .collect();
11684            let pop = words
11685                .iter()
11686                .position(|&w| w == 0xE8BD_0038)
11687                .expect("POP {R3,R4,R5} present");
11688            assert_eq!(words[pop - 1], 0xE084_C005, "ADD R12, R4, R5 before POP");
11689            let rd_bits = match rd {
11690                Reg::R8 => 8u32,
11691                Reg::R5 => 5,
11692                Reg::R4 => 4,
11693                Reg::R3 => 3,
11694                _ => 0,
11695            };
11696            assert_eq!(
11697                words[pop + 1],
11698                0xE1A0_0000 | (rd_bits << 12) | 12,
11699                "MOV rd, R12 after the restore"
11700            );
11701        }
11702    }
11703
11704    /// #633: I64DivS must carry the INT64_MIN/-1 overflow guard (mirroring
11705    /// the i32 path) right after the zero-divisor guard — dividend in R0:R1,
11706    /// divisor in R2:R3 on the #610/#613 fixed-ABI wrapper path.
11707    #[test]
11708    fn test_633_i64_divs_overflow_guard_emitted() {
11709        let encoder = ArmEncoder::new_thumb2();
11710        let code = encoder
11711            .encode(&ArmOp::I64DivS {
11712                rdlo: Reg::R4,
11713                rdhi: Reg::R5,
11714                rnlo: Reg::R0,
11715                rnhi: Reg::R1,
11716                rmlo: Reg::R2,
11717                rmhi: Reg::R3,
11718                elide_zero_guard: false,
11719                elide_overflow_guard: false,
11720            })
11721            .unwrap();
11722        // 26-byte marshal + 8-byte zero-trap, then the 22-byte overflow guard.
11723        let guard: Vec<u16> = code[34..56]
11724            .chunks(2)
11725            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11726            .collect();
11727        assert_eq!(
11728            guard,
11729            vec![
11730                0xEA02, 0x0C03, // AND.W R12, R2, R3
11731                0xF11C, 0x0F01, // CMN.W R12, #1
11732                0xD105, // BNE .no_trap
11733                0x2800, // CMP R0, #0
11734                0xD103, // BNE .no_trap
11735                0xF1B1, 0x4F00, // CMP.W R1, #0x80000000
11736                0xD100, // BNE .no_trap
11737                0xDE00, // UDF #0 — signed-division overflow
11738            ],
11739            "INT64_MIN/-1 overflow guard after the zero-divisor guard"
11740        );
11741    }
11742
11743    /// #633 fix-guard twin: I64RemS must NOT carry the overflow guard —
11744    /// rem_s(INT64_MIN, -1) is defined as 0 and must not trap. Exactly one
11745    /// UDF (the zero-divisor trap) in the whole expansion.
11746    #[test]
11747    fn test_633_i64_rems_has_no_overflow_guard() {
11748        let encoder = ArmEncoder::new_thumb2();
11749        for (is_rem_s, op) in [
11750            (
11751                true,
11752                ArmOp::I64RemS {
11753                    rdlo: Reg::R4,
11754                    rdhi: Reg::R5,
11755                    rnlo: Reg::R0,
11756                    rnhi: Reg::R1,
11757                    rmlo: Reg::R2,
11758                    rmhi: Reg::R3,
11759                    elide_zero_guard: false,
11760                },
11761            ),
11762            (
11763                false,
11764                ArmOp::I64DivS {
11765                    rdlo: Reg::R4,
11766                    rdhi: Reg::R5,
11767                    rnlo: Reg::R0,
11768                    rnhi: Reg::R1,
11769                    rmlo: Reg::R2,
11770                    rmhi: Reg::R3,
11771                    elide_zero_guard: false,
11772                    elide_overflow_guard: false,
11773                },
11774            ),
11775        ] {
11776            let code = encoder.encode(&op).unwrap();
11777            let udfs = code
11778                .chunks(2)
11779                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11780                .count();
11781            let want = if is_rem_s { 1 } else { 2 };
11782            assert_eq!(
11783                udfs, want,
11784                "rem_s: zero-trap only; div_s: zero-trap + overflow trap"
11785            );
11786        }
11787    }
11788
11789    /// #494 phase 2b: `elide_zero_guard` drops EXACTLY the 8-byte fused
11790    /// zero-trap (`ORRS.W R12,R2,R3; BNE; UDF #0`) and nothing else — the
11791    /// rest of the expansion is byte-identical (splice check).
11792    #[test]
11793    fn test_494_i64_zero_guard_elision_is_exact_splice() {
11794        let encoder = ArmEncoder::new_thumb2();
11795        let mk = |elide_zero_guard: bool| {
11796            encoder
11797                .encode(&ArmOp::I64DivU {
11798                    rdlo: Reg::R4,
11799                    rdhi: Reg::R5,
11800                    rnlo: Reg::R0,
11801                    rnhi: Reg::R1,
11802                    rmlo: Reg::R2,
11803                    rmhi: Reg::R3,
11804                    elide_zero_guard,
11805                })
11806                .unwrap()
11807        };
11808        let full = mk(false);
11809        let elided = mk(true);
11810        assert_eq!(full.len(), elided.len() + 8, "zero guard is 8 bytes");
11811        // Marshal prologue (26 B) unchanged, guard (8 B) gone, tail identical.
11812        assert_eq!(&full[..26], &elided[..26]);
11813        assert_eq!(
11814            &full[26..34],
11815            &[0x52, 0xEA, 0x03, 0x0C, 0x00, 0xD1, 0x00, 0xDE],
11816            "the spliced-out bytes are exactly ORRS.W; BNE; UDF #0"
11817        );
11818        assert_eq!(&full[34..], &elided[26..]);
11819    }
11820
11821    /// #494 phase 2b two-guard distinction (the #633/#634 synergy): a
11822    /// divisor-nonzero fact elides ONLY the zero guard — the INT64_MIN/-1
11823    /// OVERFLOW guard is a separate obligation and must survive
11824    /// `elide_zero_guard: true`. Pinned on div_s in all flag states.
11825    #[test]
11826    fn test_494_i64_divs_overflow_guard_retained_when_only_zero_elided() {
11827        let encoder = ArmEncoder::new_thumb2();
11828        let mk = |zero: bool, ovf: bool| {
11829            encoder
11830                .encode(&ArmOp::I64DivS {
11831                    rdlo: Reg::R4,
11832                    rdhi: Reg::R5,
11833                    rnlo: Reg::R0,
11834                    rnhi: Reg::R1,
11835                    rmlo: Reg::R2,
11836                    rmhi: Reg::R3,
11837                    elide_zero_guard: zero,
11838                    elide_overflow_guard: ovf,
11839                })
11840                .unwrap()
11841        };
11842        let udf_count = |code: &[u8]| {
11843            code.chunks(2)
11844                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11845                .count()
11846        };
11847        let full = mk(false, false);
11848        let zero_only = mk(true, false);
11849        let both = mk(true, true);
11850        assert_eq!(udf_count(&full), 2, "baseline: zero trap + overflow trap");
11851        assert_eq!(
11852            udf_count(&zero_only),
11853            1,
11854            "divisor-nonzero elides the zero trap ONLY — the #633 overflow \
11855             guard must be retained"
11856        );
11857        // The retained guard is the 22-byte overflow sequence, now right
11858        // after the 26-byte marshal prologue.
11859        let guard: Vec<u16> = zero_only[26..48]
11860            .chunks(2)
11861            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11862            .collect();
11863        assert_eq!(
11864            guard,
11865            vec![
11866                0xEA02, 0x0C03, 0xF11C, 0x0F01, 0xD105, 0x2800, 0xD103, 0xF1B1, 0x4F00, 0xD100,
11867                0xDE00,
11868            ],
11869            "the surviving guard is the INT64_MIN/-1 overflow trap"
11870        );
11871        assert_eq!(full.len(), zero_only.len() + 8);
11872        assert_eq!(zero_only.len(), both.len() + 22);
11873        assert_eq!(udf_count(&both), 0, "both obligations discharged ⇒ no UDF");
11874    }
11875
11876    /// #494 phase 2b A32 twin: zero-guard elision is an exact 12-byte splice
11877    /// and the A32 overflow guard survives a zero-only elision.
11878    #[test]
11879    fn test_494_a32_i64_guard_elision() {
11880        let encoder = ArmEncoder::new_arm32();
11881        let mk = |zero: bool, ovf: bool| {
11882            encoder
11883                .encode(&ArmOp::I64DivS {
11884                    rdlo: Reg::R4,
11885                    rdhi: Reg::R5,
11886                    rnlo: Reg::R0,
11887                    rnhi: Reg::R1,
11888                    rmlo: Reg::R2,
11889                    rmhi: Reg::R3,
11890                    elide_zero_guard: zero,
11891                    elide_overflow_guard: ovf,
11892                })
11893                .unwrap()
11894        };
11895        let full = mk(false, false);
11896        let zero_only = mk(true, false);
11897        let both = mk(true, true);
11898        // A32 zero guard = 3 words (ORRS/BNE/UDF), overflow guard = 6 words.
11899        assert_eq!(full.len(), zero_only.len() + 12);
11900        assert_eq!(zero_only.len(), both.len() + 24);
11901        let udf_count = |code: &[u8]| {
11902            code.chunks(4)
11903                .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
11904                .count()
11905        };
11906        assert_eq!(udf_count(&full), 2);
11907        assert_eq!(
11908            udf_count(&zero_only),
11909            1,
11910            "A32: overflow guard retained under zero-only elision"
11911        );
11912        assert_eq!(udf_count(&both), 0);
11913    }
11914
11915    /// #633: A32 twin — the conditional-execution overflow guard on the
11916    /// ARM-mode I64DivS, and its absence from I64RemS.
11917    #[test]
11918    fn test_633_a32_i64_divs_overflow_guard() {
11919        let encoder = ArmEncoder::new_arm32();
11920        let mk_divs = ArmOp::I64DivS {
11921            rdlo: Reg::R4,
11922            rdhi: Reg::R5,
11923            rnlo: Reg::R0,
11924            rnhi: Reg::R1,
11925            rmlo: Reg::R2,
11926            rmhi: Reg::R3,
11927            elide_zero_guard: false,
11928            elide_overflow_guard: false,
11929        };
11930        let code = encoder.encode(&mk_divs).unwrap();
11931        let words: Vec<u32> = code
11932            .chunks(4)
11933            .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11934            .collect();
11935        let guard = [
11936            0xE002_C003u32, // AND   R12, R2, R3
11937            0xE37C_0001,    // CMN   R12, #1
11938            0x0350_0000,    // CMPEQ R0, #0
11939            0x0351_0102,    // CMPEQ R1, #0x80000000
11940            0x1A00_0000,    // BNE +1 insn
11941            0xE7F0_00F0,    // UDF #0
11942        ];
11943        assert!(
11944            words.windows(6).any(|w| w == guard),
11945            "A32 I64DivS carries the INT64_MIN/-1 overflow guard"
11946        );
11947        let rems = encoder
11948            .encode(&ArmOp::I64RemS {
11949                rdlo: Reg::R4,
11950                rdhi: Reg::R5,
11951                rnlo: Reg::R0,
11952                rnhi: Reg::R1,
11953                rmlo: Reg::R2,
11954                rmhi: Reg::R3,
11955                elide_zero_guard: false,
11956            })
11957            .unwrap();
11958        let rems_udfs = rems
11959            .chunks(4)
11960            .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
11961            .count();
11962        assert_eq!(rems_udfs, 1, "A32 I64RemS keeps only the zero-divisor trap");
11963    }
11964
11965    #[test]
11966    fn test_encode_nop_thumb2() {
11967        let encoder = ArmEncoder::new_thumb2();
11968        let op = ArmOp::Nop;
11969        let code = encoder.encode(&op).unwrap();
11970        assert_eq!(code.len(), 2); // 16-bit
11971
11972        // NOP: 0xBF00 in little-endian
11973        assert_eq!(code, vec![0x00, 0xBF]);
11974    }
11975
11976    // =========================================================================
11977    // i64 Thumb-2 encoding tests
11978    // =========================================================================
11979
11980    #[test]
11981    fn test_encode_i64_add_thumb2() {
11982        let encoder = ArmEncoder::new_thumb2();
11983        let op = ArmOp::I64Add {
11984            rdlo: Reg::R0,
11985            rdhi: Reg::R1,
11986            rnlo: Reg::R0,
11987            rnhi: Reg::R1,
11988            rmlo: Reg::R2,
11989            rmhi: Reg::R3,
11990        };
11991        let code = encoder.encode(&op).unwrap();
11992        // Should emit ADDS (2 bytes) + ADC.W (4 bytes) = 6 bytes
11993        assert_eq!(code.len(), 6, "I64Add should be 6 bytes (ADDS + ADC.W)");
11994    }
11995
11996    #[test]
11997    fn test_encode_i64_sub_thumb2() {
11998        let encoder = ArmEncoder::new_thumb2();
11999        let op = ArmOp::I64Sub {
12000            rdlo: Reg::R0,
12001            rdhi: Reg::R1,
12002            rnlo: Reg::R0,
12003            rnhi: Reg::R1,
12004            rmlo: Reg::R2,
12005            rmhi: Reg::R3,
12006        };
12007        let code = encoder.encode(&op).unwrap();
12008        // Should emit SUBS (2 bytes) + SBC.W (4 bytes) = 6 bytes
12009        assert_eq!(code.len(), 6, "I64Sub should be 6 bytes (SUBS + SBC.W)");
12010    }
12011
12012    #[test]
12013    fn test_encode_i64_and_thumb2() {
12014        let encoder = ArmEncoder::new_thumb2();
12015        let op = ArmOp::I64And {
12016            rdlo: Reg::R0,
12017            rdhi: Reg::R1,
12018            rnlo: Reg::R0,
12019            rnhi: Reg::R1,
12020            rmlo: Reg::R2,
12021            rmhi: Reg::R3,
12022        };
12023        let code = encoder.encode(&op).unwrap();
12024        // AND.W (4 bytes) + AND.W (4 bytes) = 8 bytes
12025        assert!(code.len() >= 4, "I64And should emit at least 4 bytes");
12026    }
12027
12028    #[test]
12029    fn test_encode_i64_or_thumb2() {
12030        let encoder = ArmEncoder::new_thumb2();
12031        let op = ArmOp::I64Or {
12032            rdlo: Reg::R0,
12033            rdhi: Reg::R1,
12034            rnlo: Reg::R0,
12035            rnhi: Reg::R1,
12036            rmlo: Reg::R2,
12037            rmhi: Reg::R3,
12038        };
12039        let code = encoder.encode(&op).unwrap();
12040        assert!(code.len() >= 4, "I64Or should emit at least 4 bytes");
12041    }
12042
12043    #[test]
12044    fn test_encode_i64_xor_thumb2() {
12045        let encoder = ArmEncoder::new_thumb2();
12046        let op = ArmOp::I64Xor {
12047            rdlo: Reg::R0,
12048            rdhi: Reg::R1,
12049            rnlo: Reg::R0,
12050            rnhi: Reg::R1,
12051            rmlo: Reg::R2,
12052            rmhi: Reg::R3,
12053        };
12054        let code = encoder.encode(&op).unwrap();
12055        assert!(code.len() >= 4, "I64Xor should emit at least 4 bytes");
12056    }
12057
12058    #[test]
12059    fn test_encode_i64_const_small_thumb2() {
12060        let encoder = ArmEncoder::new_thumb2();
12061        // Small constant: only needs MOVW for each half
12062        let op = ArmOp::I64Const {
12063            rdlo: Reg::R0,
12064            rdhi: Reg::R1,
12065            value: 42,
12066        };
12067        let code = encoder.encode(&op).unwrap();
12068        // MOVW R0, #42 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes minimum
12069        assert!(code.len() >= 8, "I64Const should emit at least 8 bytes");
12070    }
12071
12072    #[test]
12073    fn test_encode_i64_const_large_thumb2() {
12074        let encoder = ArmEncoder::new_thumb2();
12075        // Large constant: needs MOVW+MOVT for each half
12076        let op = ArmOp::I64Const {
12077            rdlo: Reg::R0,
12078            rdhi: Reg::R1,
12079            value: 0x1234_5678_9ABC_DEF0_u64 as i64,
12080        };
12081        let code = encoder.encode(&op).unwrap();
12082        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
12083        assert_eq!(
12084            code.len(),
12085            16,
12086            "I64Const with large value should be 16 bytes"
12087        );
12088    }
12089
12090    #[test]
12091    fn test_encode_i64_extend_i32_s_thumb2() {
12092        let encoder = ArmEncoder::new_thumb2();
12093        let op = ArmOp::I64ExtendI32S {
12094            rdlo: Reg::R0,
12095            rdhi: Reg::R1,
12096            rn: Reg::R0,
12097        };
12098        let code = encoder.encode(&op).unwrap();
12099        // When rdlo == rn, only ASR (4 bytes) is emitted
12100        assert_eq!(
12101            code.len(),
12102            4,
12103            "I64ExtendI32S (same reg) should be 4 bytes (ASR only)"
12104        );
12105    }
12106
12107    #[test]
12108    fn test_encode_i64_extend_i32_s_diff_reg_thumb2() {
12109        let encoder = ArmEncoder::new_thumb2();
12110        let op = ArmOp::I64ExtendI32S {
12111            rdlo: Reg::R0,
12112            rdhi: Reg::R1,
12113            rn: Reg::R2,
12114        };
12115        let code = encoder.encode(&op).unwrap();
12116        // MOV rdlo, rn (2 bytes for low regs) + ASR rdhi, rdlo, #31 (4 bytes) = 6 bytes
12117        assert!(
12118            code.len() >= 6,
12119            "I64ExtendI32S (diff reg) should be at least 6 bytes"
12120        );
12121    }
12122
12123    #[test]
12124    fn test_encode_i64_extend_i32_u_thumb2() {
12125        let encoder = ArmEncoder::new_thumb2();
12126        let op = ArmOp::I64ExtendI32U {
12127            rdlo: Reg::R0,
12128            rdhi: Reg::R1,
12129            rn: Reg::R0,
12130        };
12131        let code = encoder.encode(&op).unwrap();
12132        // When rdlo == rn, only MOV rdhi, #0 (2 bytes) is emitted
12133        assert_eq!(
12134            code.len(),
12135            2,
12136            "I64ExtendI32U (same reg) should be 2 bytes (MOV #0 only)"
12137        );
12138    }
12139
12140    #[test]
12141    fn test_encode_i32_wrap_i64_nop_thumb2() {
12142        let encoder = ArmEncoder::new_thumb2();
12143        // When rd == rnlo, should be a NOP
12144        let op = ArmOp::I32WrapI64 {
12145            rd: Reg::R0,
12146            rnlo: Reg::R0,
12147        };
12148        let code = encoder.encode(&op).unwrap();
12149        assert_eq!(code.len(), 2, "I32WrapI64 same reg should be NOP (2 bytes)");
12150        assert_eq!(code, vec![0x00, 0xBF]); // NOP
12151    }
12152
12153    #[test]
12154    fn test_encode_i32_wrap_i64_diff_reg_thumb2() {
12155        let encoder = ArmEncoder::new_thumb2();
12156        let op = ArmOp::I32WrapI64 {
12157            rd: Reg::R2,
12158            rnlo: Reg::R0,
12159        };
12160        let code = encoder.encode(&op).unwrap();
12161        // MOV R2, R0 (2 or 4 bytes)
12162        assert!(
12163            code.len() >= 2,
12164            "I32WrapI64 diff reg should emit at least 2 bytes"
12165        );
12166    }
12167
12168    #[test]
12169    fn test_encode_i64_eqz_thumb2() {
12170        let encoder = ArmEncoder::new_thumb2();
12171        let op = ArmOp::I64Eqz {
12172            rd: Reg::R0,
12173            rnlo: Reg::R0,
12174            rnhi: Reg::R1,
12175        };
12176        let code = encoder.encode(&op).unwrap();
12177        // Delegates to I64SetCondZ which is already encoded
12178        assert!(
12179            code.len() >= 6,
12180            "I64Eqz should emit at least 6 bytes for ORR+ITE+MOV+MOV"
12181        );
12182    }
12183
12184    #[test]
12185    fn test_encode_i64_eq_thumb2() {
12186        let encoder = ArmEncoder::new_thumb2();
12187        let op = ArmOp::I64Eq {
12188            rd: Reg::R0,
12189            rnlo: Reg::R0,
12190            rnhi: Reg::R1,
12191            rmlo: Reg::R2,
12192            rmhi: Reg::R3,
12193        };
12194        let code = encoder.encode(&op).unwrap();
12195        // Delegates to I64SetCond EQ: CMP lo + IT EQ + CMPEQ hi + ITE EQ + MOV 1 + MOV 0
12196        assert!(code.len() >= 10, "I64Eq should emit at least 10 bytes");
12197    }
12198
12199    #[test]
12200    fn test_encode_i64_ldr_thumb2() {
12201        let encoder = ArmEncoder::new_thumb2();
12202        let op = ArmOp::I64Ldr {
12203            rdlo: Reg::R0,
12204            rdhi: Reg::R1,
12205            addr: MemAddr::imm(Reg::SP, 0),
12206        };
12207        let code = encoder.encode(&op).unwrap();
12208        // Two LDR instructions (lo at offset, hi at offset+4)
12209        assert!(code.len() >= 4, "I64Ldr should emit at least 4 bytes");
12210    }
12211
12212    #[test]
12213    fn test_372_i64_ldr_indexed_materializes_address() {
12214        // #372: a memory i64.load carries an index register (R11 + addr + off).
12215        // The encoder must materialize `ip = base + index` (ADD.W) and load via
12216        // `[ip,#off]` — NOT drop the index. A frame (non-indexed) i64.load must
12217        // stay byte-identical (plain `[base,#off]`, no ADD).
12218        let encoder = ArmEncoder::new_thumb2();
12219        let indexed = encoder
12220            .encode(&ArmOp::I64Ldr {
12221                rdlo: Reg::R0,
12222                rdhi: Reg::R1,
12223                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
12224            })
12225            .unwrap();
12226        // ADD.W ip, fp, r0 = eb0b 0c00 (byte-verified vs arm-none-eabi-as).
12227        assert_eq!(
12228            &indexed[0..4],
12229            &[0x0b, 0xeb, 0x00, 0x0c],
12230            "indexed I64Ldr must start with ADD.W ip, base, index"
12231        );
12232        let frame = encoder
12233            .encode(&ArmOp::I64Ldr {
12234                rdlo: Reg::R0,
12235                rdhi: Reg::R1,
12236                addr: MemAddr::imm(Reg::SP, 8),
12237            })
12238            .unwrap();
12239        // No index -> no ADD.W prefix (byte-identical frame access).
12240        assert_ne!(
12241            &frame[0..2],
12242            &[0x0b, 0xeb],
12243            "frame (non-indexed) I64Ldr must NOT emit an ADD.W"
12244        );
12245    }
12246
12247    #[test]
12248    fn test_382_i64_ldst_large_offset_materializes_not_skips() {
12249        // #382: an indexed i64.load/store whose static offset > 0xFFF must
12250        // MATERIALIZE the offset into the base — NOT return Err (skip the fn).
12251        // Sequence for reg_imm(R11, R0, 5000): MOVW ip,#5000 ; ADD ip,r0,ip ;
12252        // ADD ip,ip,fp ; LDR/STR halves at [ip,#0] / [ip,#4]. Byte-verified tail
12253        // vs arm-none-eabi-as.
12254        let encoder = ArmEncoder::new_thumb2();
12255        // 0x1388 > 0xFFF (MemAddr is not Copy, so build it per use).
12256
12257        let ld = encoder
12258            .encode(&ArmOp::I64Ldr {
12259                rdlo: Reg::R0,
12260                rdhi: Reg::R1,
12261                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
12262            })
12263            .expect("large-offset i64.load must lower, not skip");
12264        // MOVW ip,#0x1388 (4) + ADD ip,r0,ip (4) + ADD ip,ip,fp (4) + 2 LDR (8).
12265        assert_eq!(ld.len(), 20, "expected MOVW + 2×ADD + 2×LDR");
12266        // Must NOT be the small-offset `ADD.W ip, fp, r0` (0x0b 0xeb) prefix —
12267        // that path can only reach imm12 offsets.
12268        assert_ne!(
12269            &ld[0..2],
12270            &[0x0b, 0xeb],
12271            "must materialize the large offset"
12272        );
12273        // Effective base built in ip, then halves at [ip,#0] / [ip,#4].
12274        assert_eq!(
12275            &ld[4..20],
12276            &[
12277                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
12278                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
12279                0xdc, 0xf8, 0x00, 0x00, // LDR.W r0, [ip, #0]
12280                0xdc, 0xf8, 0x04, 0x10, // LDR.W r1, [ip, #4]
12281            ],
12282            "large-offset i64.load must fold offset into ip and access [ip,#0]/[ip,#4]"
12283        );
12284
12285        // Store: same base materialization, STR halves.
12286        let st = encoder
12287            .encode(&ArmOp::I64Str {
12288                rdlo: Reg::R2,
12289                rdhi: Reg::R3,
12290                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
12291            })
12292            .expect("large-offset i64.store must lower, not skip");
12293        assert_eq!(st.len(), 20);
12294        assert_eq!(
12295            &st[4..20],
12296            &[
12297                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
12298                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
12299                0xcc, 0xf8, 0x00, 0x20, // STR.W r2, [ip, #0]
12300                0xcc, 0xf8, 0x04, 0x30, // STR.W r3, [ip, #4]
12301            ],
12302            "large-offset i64.store must fold offset into ip and access [ip,#0]/[ip,#4]"
12303        );
12304
12305        // Small-offset (imm12) indexed access stays byte-identical (#372): the
12306        // effective base is a single `ADD.W ip, fp, r0` and the halves keep the
12307        // folded immediates — NO extra MOVW/ADD.
12308        let small = encoder
12309            .encode(&ArmOp::I64Ldr {
12310                rdlo: Reg::R0,
12311                rdhi: Reg::R1,
12312                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 8),
12313            })
12314            .unwrap();
12315        assert_eq!(
12316            &small[0..4],
12317            &[0x0b, 0xeb, 0x00, 0x0c],
12318            "small-offset indexed i64 must keep the single ADD.W ip, fp, r0"
12319        );
12320        assert_eq!(small.len(), 12, "ADD.W + 2×LDR.W (offset folded in imm12)");
12321    }
12322
12323    /// RQ-63-ARMI64OFF (#1165): A32 `I64Ldr`/`I64Str` at and past the pair
12324    /// form's immediate boundary. Bytes verified against `arm-none-eabi-as`
12325    /// (`.arch armv7-r`, `.arm`). 4091 is the last offset the pair folds (the
12326    /// high half sits at 4095 = imm12 max); 4092 is the straddle the v0.62
12327    /// census declined on (low half fits, high half does not).
12328    #[test]
12329    fn test_1165_a32_i64_ldst_offset_boundary_materializes() {
12330        let enc = ArmEncoder::new_arm32();
12331        let le = |ws: &[u32]| ws.iter().flat_map(|w| w.to_le_bytes()).collect::<Vec<u8>>();
12332        let ld = |off: i32| {
12333            enc.encode(&ArmOp::I64Ldr {
12334                rdlo: Reg::R0,
12335                rdhi: Reg::R1,
12336                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, off),
12337            })
12338        };
12339        // AT the boundary (4091): folded, byte-identical to the pre-#1165 form.
12340        assert_eq!(
12341            ld(4091).unwrap(),
12342            le(&[0xE08BC000, 0xE59C0FFB, 0xE59C1FFF]),
12343            "4091: add ip,fp,r0 ; ldr r0,[ip,#4091] ; ldr r1,[ip,#4095]"
12344        );
12345        // ONE ABOVE (4092): the high half would need #4096 → materialize.
12346        assert_eq!(
12347            ld(4092).unwrap(),
12348            le(&[0xE300CFFC, 0xE08CC00B, 0xE08CC000, 0xE59C0000, 0xE59C1004]),
12349            "4092: movw ip,#4092 ; add ip,ip,fp ; add ip,ip,r0 ; ldr r0,[ip] ; ldr r1,[ip,#4]"
12350        );
12351        // 4095 (low half at the imm12 max), 4096 (both out), 5000.
12352        assert_eq!(ld(4095).unwrap()[..4], le(&[0xE300CFFF])[..]);
12353        assert_eq!(ld(4096).unwrap()[..4], le(&[0xE301C000])[..]);
12354        assert_eq!(
12355            ld(5000).unwrap(),
12356            le(&[0xE301C388, 0xE08CC00B, 0xE08CC000, 0xE59C0000, 0xE59C1004])
12357        );
12358        // 70000 needs MOVT; store form.
12359        assert_eq!(
12360            enc.encode(&ArmOp::I64Str {
12361                rdlo: Reg::R2,
12362                rdhi: Reg::R3,
12363                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 70000),
12364            })
12365            .unwrap(),
12366            le(&[
12367                0xE301C170, 0xE340C001, 0xE08CC00B, 0xE08CC000, 0xE58C2000, 0xE58C3004
12368            ]),
12369            "70000: movw ip,#0x1170 ; movt ip,#1 ; add ip,ip,fp ; add ip,ip,r0 ; str r2,[ip] ; str r3,[ip,#4]"
12370        );
12371        // A memarg >= 2^31 arrives negative (0xFFFFFFF8 as i32 = -8): the full
12372        // 32 bits are materialized, mod 2^32 — never clamped, never masked.
12373        assert_eq!(
12374            ld(-8).unwrap(),
12375            le(&[
12376                0xE30FCFF8, 0xE34FCFFF, 0xE08CC00B, 0xE08CC000, 0xE59C0000, 0xE59C1004
12377            ]),
12378            "0xfffffff8: movw ip,#0xfff8 ; movt ip,#0xffff ; add ; add ; ldr ; ldr"
12379        );
12380        // No index register (frame-style base), out of range: MOVW ; ADD ip,ip,base.
12381        assert_eq!(
12382            enc.encode(&ArmOp::I64Ldr {
12383                rdlo: Reg::R0,
12384                rdhi: Reg::R1,
12385                addr: MemAddr::imm(Reg::SP, 5000),
12386            })
12387            .unwrap(),
12388            le(&[0xE301C388, 0xE08CC00D, 0xE59C0000, 0xE59C1004])
12389        );
12390        // The base IS the scratch: no free register → typed error, not a clobber.
12391        assert!(
12392            enc.encode(&ArmOp::I64Ldr {
12393                rdlo: Reg::R0,
12394                rdhi: Reg::R1,
12395                addr: MemAddr::imm(Reg::R12, 5000),
12396            })
12397            .is_err(),
12398            "materializing over an R12 base must be refused, not clobber it"
12399        );
12400    }
12401
12402    /// RQ-63-ARMI64OFF (#1165): the A32 WORD and SUB-WORD immediate arms used
12403    /// to MASK an out-of-range offset (`& 0xFFF` / `& 0xFF`) — a silent wrong
12404    /// address (`i32.load offset=5000` → `ldr r0,[ip,#904]`, `i32.load16_u
12405    /// offset=256` → `ldrh r0,[ip]`). At the field maximum the bytes are
12406    /// unchanged; one above, the offset is materialized. Bytes verified
12407    /// against `arm-none-eabi-as`.
12408    #[test]
12409    fn test_1165_a32_word_subword_offset_boundary_materializes_not_masks() {
12410        let enc = ArmEncoder::new_arm32();
12411        let le = |ws: &[u32]| ws.iter().flat_map(|w| w.to_le_bytes()).collect::<Vec<u8>>();
12412        let idx = |off: i32| MemAddr::reg_imm(Reg::R11, Reg::R0, off);
12413        let ldr = |addr| enc.encode(&ArmOp::Ldr { rd: Reg::R0, addr }).unwrap();
12414        let ldrh = |addr| enc.encode(&ArmOp::Ldrh { rd: Reg::R0, addr }).unwrap();
12415        let ldrsb = |addr| enc.encode(&ArmOp::Ldrsb { rd: Reg::R0, addr }).unwrap();
12416        let strh = |addr| enc.encode(&ArmOp::Strh { rd: Reg::R1, addr }).unwrap();
12417        let ldrb = |addr| enc.encode(&ArmOp::Ldrb { rd: Reg::R0, addr }).unwrap();
12418        // LDR imm12: 4095 at, 4096 above.
12419        assert_eq!(ldr(idx(4095)), le(&[0xE08BC000, 0xE59C0FFF]));
12420        assert_eq!(
12421            ldr(idx(4096)),
12422            le(&[0xE301C000, 0xE08CC00B, 0xE08CC000, 0xE59C0000]),
12423            "ldr 4096 must materialize, not mask to #0"
12424        );
12425        // LDRH imm8: 255 at, 256 above (the pre-fix bytes were `ldrh r0,[ip]`).
12426        assert_eq!(ldrh(idx(255)), le(&[0xE08BC000, 0xE1DC0FBF]));
12427        assert_eq!(
12428            ldrh(idx(256)),
12429            le(&[0xE300C100, 0xE08CC00B, 0xE08CC000, 0xE1DC00B0]),
12430            "ldrh 256 must materialize, not mask to #0"
12431        );
12432        // LDRSB imm8 (256 above), STRH imm8 (300 above).
12433        assert_eq!(
12434            ldrsb(idx(256)),
12435            le(&[0xE300C100, 0xE08CC00B, 0xE08CC000, 0xE1DC00D0])
12436        );
12437        assert_eq!(
12438            strh(idx(300)),
12439            le(&[0xE300C12C, 0xE08CC00B, 0xE08CC000, 0xE1CC10B0])
12440        );
12441        // LDRB is an imm12 form: 4095 at, 4096 above.
12442        assert_eq!(ldrb(idx(4095)), le(&[0xE08BC000, 0xE5DC0FFF]));
12443        assert_eq!(
12444            ldrb(idx(4096)),
12445            le(&[0xE301C000, 0xE08CC00B, 0xE08CC000, 0xE5DC0000])
12446        );
12447        // No index, out of range: MOVW ; ADD ip,ip,base ; access [ip].
12448        assert_eq!(
12449            ldr(MemAddr::imm(Reg::R0, 5000)),
12450            le(&[0xE301C388, 0xE08CC000, 0xE59C0000])
12451        );
12452        // The immediate arms are now a TRIPWIRE, never a mask.
12453        assert!(encode_mem_addr(&MemAddr::imm(Reg::R1, 0xFFF)).is_ok());
12454        assert!(
12455            encode_mem_addr(&MemAddr::imm(Reg::R1, 0x1000)).is_err(),
12456            "imm12 arm must refuse 0x1000, not mask it to #0"
12457        );
12458        assert!(encode_mem_addr_imm8(&MemAddr::imm(Reg::R1, 0xFF)).is_ok());
12459        assert!(
12460            encode_mem_addr_imm8(&MemAddr::imm(Reg::R1, 0x100)).is_err(),
12461            "imm8 arm must refuse 0x100, not mask it to #0"
12462        );
12463        // Base is the scratch: refused, not clobbered.
12464        assert!(
12465            enc.encode(&ArmOp::Ldr {
12466                rd: Reg::R0,
12467                addr: MemAddr::imm(Reg::R12, 5000),
12468            })
12469            .is_err()
12470        );
12471    }
12472
12473    /// RQ-63-ARMI64OFF (#1165): Thumb-2 `i64_effective_base` used to CLAMP a
12474    /// negative offset (a memarg >= 2^31 after the selector's `as i32` cast)
12475    /// to 0 — `i64.load offset=0xfffffff8` read `[R11+addr]`. It now
12476    /// materializes the full 32 bits (MOVW+MOVT), the arithmetic the i32 word
12477    /// path uses for the same memarg. Bytes verified against
12478    /// `arm-none-eabi-as` (`.arch armv7e-m`, `.thumb`). A FRAME access with a
12479    /// negative offset (no index register) is refused loudly by
12480    /// `check_ldst_imm12`, never clamped.
12481    #[test]
12482    fn test_1165_thumb2_i64_negative_offset_materializes_not_clamps() {
12483        let enc = ArmEncoder::new_thumb2();
12484        let ld = enc
12485            .encode(&ArmOp::I64Ldr {
12486                rdlo: Reg::R0,
12487                rdhi: Reg::R1,
12488                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, -8),
12489            })
12490            .unwrap();
12491        assert_eq!(
12492            ld,
12493            vec![
12494                0x4f, 0xf6, 0xf8, 0x7c, // movw ip, #0xfff8
12495                0xcf, 0xf6, 0xff, 0x7c, // movt ip, #0xffff
12496                0x00, 0xeb, 0x0c, 0x0c, // add.w ip, r0, ip
12497                0x0c, 0xeb, 0x0b, 0x0c, // add.w ip, ip, fp
12498                0xdc, 0xf8, 0x00, 0x00, // ldr.w r0, [ip]
12499                0xdc, 0xf8, 0x04, 0x10, // ldr.w r1, [ip, #4]
12500            ],
12501            "0xfffffff8 must be materialized in full, not clamped to [ip,#0]"
12502        );
12503        let st = enc
12504            .encode(&ArmOp::I64Str {
12505                rdlo: Reg::R2,
12506                rdhi: Reg::R3,
12507                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, i32::MIN),
12508            })
12509            .unwrap();
12510        assert_eq!(
12511            st,
12512            vec![
12513                0x40, 0xf2, 0x00, 0x0c, // movw ip, #0
12514                0xc8, 0xf2, 0x00, 0x0c, // movt ip, #0x8000
12515                0x00, 0xeb, 0x0c, 0x0c, // add.w ip, r0, ip
12516                0x0c, 0xeb, 0x0b, 0x0c, // add.w ip, ip, fp
12517                0xcc, 0xf8, 0x00, 0x20, // str.w r2, [ip]
12518                0xcc, 0xf8, 0x04, 0x30, // str.w r3, [ip, #4]
12519            ]
12520        );
12521        assert!(
12522            enc.encode(&ArmOp::I64Ldr {
12523                rdlo: Reg::R0,
12524                rdhi: Reg::R1,
12525                addr: MemAddr::imm(Reg::SP, -8),
12526            })
12527            .is_err(),
12528            "a negative frame offset must be refused, not clamped to [sp,#0]"
12529        );
12530    }
12531
12532    #[test]
12533    fn test_encode_i64_str_thumb2() {
12534        let encoder = ArmEncoder::new_thumb2();
12535        let op = ArmOp::I64Str {
12536            rdlo: Reg::R0,
12537            rdhi: Reg::R1,
12538            addr: MemAddr::imm(Reg::SP, 0),
12539        };
12540        let code = encoder.encode(&op).unwrap();
12541        // Two STR instructions (lo at offset, hi at offset+4)
12542        assert!(code.len() >= 4, "I64Str should emit at least 4 bytes");
12543    }
12544
12545    #[test]
12546    fn test_encode_i64_all_comparisons_thumb2() {
12547        let encoder = ArmEncoder::new_thumb2();
12548
12549        let ops = vec![
12550            ArmOp::I64Ne {
12551                rd: Reg::R0,
12552                rnlo: Reg::R0,
12553                rnhi: Reg::R1,
12554                rmlo: Reg::R2,
12555                rmhi: Reg::R3,
12556            },
12557            ArmOp::I64LtS {
12558                rd: Reg::R0,
12559                rnlo: Reg::R0,
12560                rnhi: Reg::R1,
12561                rmlo: Reg::R2,
12562                rmhi: Reg::R3,
12563            },
12564            ArmOp::I64LtU {
12565                rd: Reg::R0,
12566                rnlo: Reg::R0,
12567                rnhi: Reg::R1,
12568                rmlo: Reg::R2,
12569                rmhi: Reg::R3,
12570            },
12571            ArmOp::I64LeS {
12572                rd: Reg::R0,
12573                rnlo: Reg::R0,
12574                rnhi: Reg::R1,
12575                rmlo: Reg::R2,
12576                rmhi: Reg::R3,
12577            },
12578            ArmOp::I64LeU {
12579                rd: Reg::R0,
12580                rnlo: Reg::R0,
12581                rnhi: Reg::R1,
12582                rmlo: Reg::R2,
12583                rmhi: Reg::R3,
12584            },
12585            ArmOp::I64GtS {
12586                rd: Reg::R0,
12587                rnlo: Reg::R0,
12588                rnhi: Reg::R1,
12589                rmlo: Reg::R2,
12590                rmhi: Reg::R3,
12591            },
12592            ArmOp::I64GtU {
12593                rd: Reg::R0,
12594                rnlo: Reg::R0,
12595                rnhi: Reg::R1,
12596                rmlo: Reg::R2,
12597                rmhi: Reg::R3,
12598            },
12599            ArmOp::I64GeS {
12600                rd: Reg::R0,
12601                rnlo: Reg::R0,
12602                rnhi: Reg::R1,
12603                rmlo: Reg::R2,
12604                rmhi: Reg::R3,
12605            },
12606            ArmOp::I64GeU {
12607                rd: Reg::R0,
12608                rnlo: Reg::R0,
12609                rnhi: Reg::R1,
12610                rmlo: Reg::R2,
12611                rmhi: Reg::R3,
12612            },
12613        ];
12614
12615        for op in &ops {
12616            let code = encoder.encode(op).unwrap();
12617            assert!(
12618                code.len() >= 8,
12619                "i64 comparison {:?} should emit at least 8 bytes, got {}",
12620                op,
12621                code.len()
12622            );
12623        }
12624    }
12625
12626    #[test]
12627    fn test_encode_i64_const_zero_thumb2() {
12628        let encoder = ArmEncoder::new_thumb2();
12629        let op = ArmOp::I64Const {
12630            rdlo: Reg::R0,
12631            rdhi: Reg::R1,
12632            value: 0,
12633        };
12634        let code = encoder.encode(&op).unwrap();
12635        // MOVW R0, #0 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes
12636        assert_eq!(code.len(), 8, "I64Const(0) should be 8 bytes");
12637    }
12638
12639    #[test]
12640    fn test_encode_i64_const_negative_one_thumb2() {
12641        let encoder = ArmEncoder::new_thumb2();
12642        let op = ArmOp::I64Const {
12643            rdlo: Reg::R0,
12644            rdhi: Reg::R1,
12645            value: -1, // 0xFFFF_FFFF_FFFF_FFFF
12646        };
12647        let code = encoder.encode(&op).unwrap();
12648        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
12649        assert_eq!(code.len(), 16, "I64Const(-1) should be 16 bytes");
12650    }
12651
12652    // =========================================================================
12653    // Sub-word load/store encoding tests
12654    // =========================================================================
12655
12656    #[test]
12657    fn test_encode_ldrb_arm32() {
12658        let encoder = ArmEncoder::new_arm32();
12659        let op = ArmOp::Ldrb {
12660            rd: Reg::R0,
12661            addr: MemAddr::imm(Reg::R1, 4),
12662        };
12663        let code = encoder.encode(&op).unwrap();
12664        assert_eq!(code.len(), 4, "ARM32 LDRB should be 4 bytes");
12665        // LDRB R0, [R1, #4] = 0xE5D10004
12666        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12667        assert_eq!(encoded, 0xE5D10004, "Should encode LDRB R0, [R1, #4]");
12668    }
12669
12670    #[test]
12671    fn test_encode_strb_arm32() {
12672        let encoder = ArmEncoder::new_arm32();
12673        let op = ArmOp::Strb {
12674            rd: Reg::R0,
12675            addr: MemAddr::imm(Reg::R1, 0),
12676        };
12677        let code = encoder.encode(&op).unwrap();
12678        assert_eq!(code.len(), 4, "ARM32 STRB should be 4 bytes");
12679        // STRB R0, [R1, #0] = 0xE5C10000
12680        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12681        assert_eq!(encoded, 0xE5C10000, "Should encode STRB R0, [R1, #0]");
12682    }
12683
12684    #[test]
12685    fn test_encode_ldrh_arm32() {
12686        let encoder = ArmEncoder::new_arm32();
12687        let op = ArmOp::Ldrh {
12688            rd: Reg::R0,
12689            addr: MemAddr::imm(Reg::R1, 2),
12690        };
12691        let code = encoder.encode(&op).unwrap();
12692        assert_eq!(code.len(), 4, "ARM32 LDRH should be 4 bytes");
12693    }
12694
12695    #[test]
12696    fn test_encode_strh_arm32() {
12697        let encoder = ArmEncoder::new_arm32();
12698        let op = ArmOp::Strh {
12699            rd: Reg::R0,
12700            addr: MemAddr::imm(Reg::R1, 0),
12701        };
12702        let code = encoder.encode(&op).unwrap();
12703        assert_eq!(code.len(), 4, "ARM32 STRH should be 4 bytes");
12704    }
12705
12706    #[test]
12707    fn test_encode_ldrsb_arm32() {
12708        let encoder = ArmEncoder::new_arm32();
12709        let op = ArmOp::Ldrsb {
12710            rd: Reg::R0,
12711            addr: MemAddr::imm(Reg::R1, 0),
12712        };
12713        let code = encoder.encode(&op).unwrap();
12714        assert_eq!(code.len(), 4, "ARM32 LDRSB should be 4 bytes");
12715    }
12716
12717    #[test]
12718    fn test_encode_ldrsh_arm32() {
12719        let encoder = ArmEncoder::new_arm32();
12720        let op = ArmOp::Ldrsh {
12721            rd: Reg::R0,
12722            addr: MemAddr::imm(Reg::R1, 0),
12723        };
12724        let code = encoder.encode(&op).unwrap();
12725        assert_eq!(code.len(), 4, "ARM32 LDRSH should be 4 bytes");
12726    }
12727
12728    #[test]
12729    fn test_encode_ldrb_thumb2_16bit() {
12730        let encoder = ArmEncoder::new_thumb2();
12731        let op = ArmOp::Ldrb {
12732            rd: Reg::R0,
12733            addr: MemAddr::imm(Reg::R1, 4),
12734        };
12735        let code = encoder.encode(&op).unwrap();
12736        // Low registers + small offset -> 16-bit encoding
12737        assert_eq!(
12738            code.len(),
12739            2,
12740            "Thumb-2 LDRB with small offset should be 16-bit"
12741        );
12742    }
12743
12744    #[test]
12745    fn test_encode_ldrb_thumb2_32bit() {
12746        let encoder = ArmEncoder::new_thumb2();
12747        let op = ArmOp::Ldrb {
12748            rd: Reg::R0,
12749            addr: MemAddr::imm(Reg::R1, 100), // offset > 31 needs 32-bit
12750        };
12751        let code = encoder.encode(&op).unwrap();
12752        assert_eq!(
12753            code.len(),
12754            4,
12755            "Thumb-2 LDRB with large offset should be 32-bit"
12756        );
12757    }
12758
12759    #[test]
12760    fn test_encode_strb_thumb2_16bit() {
12761        let encoder = ArmEncoder::new_thumb2();
12762        let op = ArmOp::Strb {
12763            rd: Reg::R0,
12764            addr: MemAddr::imm(Reg::R1, 10),
12765        };
12766        let code = encoder.encode(&op).unwrap();
12767        assert_eq!(
12768            code.len(),
12769            2,
12770            "Thumb-2 STRB with small offset should be 16-bit"
12771        );
12772    }
12773
12774    #[test]
12775    fn test_encode_ldrh_thumb2_16bit() {
12776        let encoder = ArmEncoder::new_thumb2();
12777        let op = ArmOp::Ldrh {
12778            rd: Reg::R0,
12779            addr: MemAddr::imm(Reg::R1, 4), // offset aligned to 2, <= 62
12780        };
12781        let code = encoder.encode(&op).unwrap();
12782        assert_eq!(
12783            code.len(),
12784            2,
12785            "Thumb-2 LDRH with small aligned offset should be 16-bit"
12786        );
12787    }
12788
12789    #[test]
12790    fn test_encode_strh_thumb2_16bit() {
12791        let encoder = ArmEncoder::new_thumb2();
12792        let op = ArmOp::Strh {
12793            rd: Reg::R0,
12794            addr: MemAddr::imm(Reg::R1, 4),
12795        };
12796        let code = encoder.encode(&op).unwrap();
12797        assert_eq!(
12798            code.len(),
12799            2,
12800            "Thumb-2 STRH with small aligned offset should be 16-bit"
12801        );
12802    }
12803
12804    #[test]
12805    fn test_encode_ldrsb_thumb2() {
12806        let encoder = ArmEncoder::new_thumb2();
12807        let op = ArmOp::Ldrsb {
12808            rd: Reg::R0,
12809            addr: MemAddr::imm(Reg::R1, 0),
12810        };
12811        let code = encoder.encode(&op).unwrap();
12812        // LDRSB has no 16-bit immediate form, always 32-bit
12813        assert_eq!(code.len(), 4, "Thumb-2 LDRSB should be 32-bit");
12814    }
12815
12816    #[test]
12817    fn test_encode_ldrsh_thumb2() {
12818        let encoder = ArmEncoder::new_thumb2();
12819        let op = ArmOp::Ldrsh {
12820            rd: Reg::R0,
12821            addr: MemAddr::imm(Reg::R1, 0),
12822        };
12823        let code = encoder.encode(&op).unwrap();
12824        assert_eq!(code.len(), 4, "Thumb-2 LDRSH should be 32-bit");
12825    }
12826
12827    #[test]
12828    fn test_encode_memory_size_thumb2() {
12829        let encoder = ArmEncoder::new_thumb2();
12830        let op = ArmOp::MemorySize { rd: Reg::R0 };
12831        let code = encoder.encode(&op).unwrap();
12832        // R0 and R10 are not both low registers, so this needs careful handling
12833        assert!(!code.is_empty(), "MemorySize should produce code");
12834    }
12835
12836    #[test]
12837    fn test_encode_memory_grow_thumb2() {
12838        let encoder = ArmEncoder::new_thumb2();
12839        let op = ArmOp::MemoryGrow {
12840            rd: Reg::R0,
12841            rn: Reg::R0,
12842        };
12843        let code = encoder.encode(&op).unwrap();
12844        assert_eq!(code.len(), 4, "MemoryGrow (MVN) should be 32-bit Thumb-2");
12845    }
12846
12847    #[test]
12848    fn test_encode_subword_reg_offset_thumb2() {
12849        let encoder = ArmEncoder::new_thumb2();
12850
12851        // LDRB with register offset
12852        let op = ArmOp::Ldrb {
12853            rd: Reg::R0,
12854            addr: MemAddr::reg(Reg::R1, Reg::R2),
12855        };
12856        let code = encoder.encode(&op).unwrap();
12857        assert_eq!(
12858            code.len(),
12859            4,
12860            "Thumb-2 LDRB with reg offset should be 32-bit"
12861        );
12862
12863        // STRB with register offset
12864        let op = ArmOp::Strb {
12865            rd: Reg::R0,
12866            addr: MemAddr::reg(Reg::R1, Reg::R2),
12867        };
12868        let code = encoder.encode(&op).unwrap();
12869        assert_eq!(
12870            code.len(),
12871            4,
12872            "Thumb-2 STRB with reg offset should be 32-bit"
12873        );
12874
12875        // LDRH with register offset
12876        let op = ArmOp::Ldrh {
12877            rd: Reg::R0,
12878            addr: MemAddr::reg(Reg::R1, Reg::R2),
12879        };
12880        let code = encoder.encode(&op).unwrap();
12881        assert_eq!(
12882            code.len(),
12883            4,
12884            "Thumb-2 LDRH with reg offset should be 32-bit"
12885        );
12886
12887        // STRH with register offset
12888        let op = ArmOp::Strh {
12889            rd: Reg::R0,
12890            addr: MemAddr::reg(Reg::R1, Reg::R2),
12891        };
12892        let code = encoder.encode(&op).unwrap();
12893        assert_eq!(
12894            code.len(),
12895            4,
12896            "Thumb-2 STRH with reg offset should be 32-bit"
12897        );
12898    }
12899
12900    #[test]
12901    fn test_encode_subword_reg_imm_offset_thumb2() {
12902        let encoder = ArmEncoder::new_thumb2();
12903
12904        // LDRB with both register and immediate offset
12905        let op = ArmOp::Ldrb {
12906            rd: Reg::R0,
12907            addr: MemAddr::reg_imm(Reg::R1, Reg::R2, 4),
12908        };
12909        let code = encoder.encode(&op).unwrap();
12910        // ADD R12, R2, #4 (4 bytes) + LDRB R0, [R1, R12] (4 bytes) = 8 bytes
12911        assert_eq!(
12912            code.len(),
12913            8,
12914            "Thumb-2 LDRB with reg+imm offset should be 8 bytes"
12915        );
12916    }
12917
12918    // ========================================================================
12919    // Helium MVE encoding tests
12920    // ========================================================================
12921
12922    #[test]
12923    fn test_encode_mve_addi32_thumb2() {
12924        let encoder = ArmEncoder::new_thumb2();
12925        let op = ArmOp::MveAddI {
12926            qd: QReg::Q0,
12927            qn: QReg::Q1,
12928            qm: QReg::Q2,
12929            size: MveSize::S32,
12930        };
12931        let code = encoder.encode(&op).unwrap();
12932        assert_eq!(
12933            code.len(),
12934            4,
12935            "MVE VADD.I32 should be 4 bytes (Thumb-2 32-bit)"
12936        );
12937    }
12938
12939    #[test]
12940    fn test_encode_mve_subi16_thumb2() {
12941        let encoder = ArmEncoder::new_thumb2();
12942        let op = ArmOp::MveSubI {
12943            qd: QReg::Q0,
12944            qn: QReg::Q1,
12945            qm: QReg::Q2,
12946            size: MveSize::S16,
12947        };
12948        let code = encoder.encode(&op).unwrap();
12949        assert_eq!(code.len(), 4, "MVE VSUB.I16 should be 4 bytes");
12950    }
12951
12952    #[test]
12953    fn test_encode_mve_muli8_thumb2() {
12954        let encoder = ArmEncoder::new_thumb2();
12955        let op = ArmOp::MveMulI {
12956            qd: QReg::Q0,
12957            qn: QReg::Q1,
12958            qm: QReg::Q2,
12959            size: MveSize::S8,
12960        };
12961        let code = encoder.encode(&op).unwrap();
12962        assert_eq!(code.len(), 4, "MVE VMUL.I8 should be 4 bytes");
12963    }
12964
12965    #[test]
12966    fn test_encode_mve_bitwise_thumb2() {
12967        let encoder = ArmEncoder::new_thumb2();
12968
12969        let ops = vec![
12970            ArmOp::MveAnd {
12971                qd: QReg::Q0,
12972                qn: QReg::Q1,
12973                qm: QReg::Q2,
12974            },
12975            ArmOp::MveOrr {
12976                qd: QReg::Q0,
12977                qn: QReg::Q1,
12978                qm: QReg::Q2,
12979            },
12980            ArmOp::MveEor {
12981                qd: QReg::Q0,
12982                qn: QReg::Q1,
12983                qm: QReg::Q2,
12984            },
12985            ArmOp::MveBic {
12986                qd: QReg::Q0,
12987                qn: QReg::Q1,
12988                qm: QReg::Q2,
12989            },
12990        ];
12991        for op in ops {
12992            let code = encoder.encode(&op).unwrap();
12993            assert_eq!(code.len(), 4, "MVE bitwise op should be 4 bytes");
12994        }
12995    }
12996
12997    #[test]
12998    fn test_encode_mve_mvn_thumb2() {
12999        let encoder = ArmEncoder::new_thumb2();
13000        let op = ArmOp::MveMvn {
13001            qd: QReg::Q0,
13002            qm: QReg::Q1,
13003        };
13004        let code = encoder.encode(&op).unwrap();
13005        assert_eq!(code.len(), 4, "MVE VMVN should be 4 bytes");
13006    }
13007
13008    #[test]
13009    fn test_encode_mve_load_store_thumb2() {
13010        let encoder = ArmEncoder::new_thumb2();
13011
13012        let load = ArmOp::MveLoad {
13013            qd: QReg::Q0,
13014            addr: MemAddr::imm(Reg::R0, 16),
13015        };
13016        let code = encoder.encode(&load).unwrap();
13017        assert_eq!(code.len(), 4, "MVE VLDRW.32 should be 4 bytes");
13018
13019        let store = ArmOp::MveStore {
13020            qd: QReg::Q1,
13021            addr: MemAddr::imm(Reg::R1, 0),
13022        };
13023        let code = encoder.encode(&store).unwrap();
13024        assert_eq!(code.len(), 4, "MVE VSTRW.32 should be 4 bytes");
13025    }
13026
13027    #[test]
13028    fn test_encode_mve_const_thumb2() {
13029        let encoder = ArmEncoder::new_thumb2();
13030        let op = ArmOp::MveConst {
13031            qd: QReg::Q0,
13032            bytes: [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0],
13033        };
13034        let code = encoder.encode(&op).unwrap();
13035        // Should be 4 words of (MOVW R12 + VMOV Sn) = 4 * (4+4) = 32 bytes min
13036        // Some words with hi16=0 skip MOVT, so length varies
13037        assert!(
13038            code.len() >= 24,
13039            "MVE const should produce multiple instructions"
13040        );
13041    }
13042
13043    #[test]
13044    fn test_encode_mve_dup_thumb2() {
13045        let encoder = ArmEncoder::new_thumb2();
13046        let op = ArmOp::MveDup {
13047            qd: QReg::Q0,
13048            rn: Reg::R0,
13049            size: MveSize::S32,
13050        };
13051        let code = encoder.encode(&op).unwrap();
13052        assert_eq!(code.len(), 4, "MVE VDUP.32 should be 4 bytes");
13053    }
13054
13055    #[test]
13056    fn test_encode_mve_extract_lane_thumb2() {
13057        let encoder = ArmEncoder::new_thumb2();
13058        let op = ArmOp::MveExtractLane {
13059            rd: Reg::R0,
13060            qn: QReg::Q1,
13061            lane: 2,
13062            size: MveSize::S32,
13063        };
13064        let code = encoder.encode(&op).unwrap();
13065        assert_eq!(code.len(), 4, "MVE extract lane should be 4 bytes");
13066    }
13067
13068    #[test]
13069    fn test_encode_mve_insert_lane_thumb2() {
13070        let encoder = ArmEncoder::new_thumb2();
13071        let op = ArmOp::MveInsertLane {
13072            qd: QReg::Q0,
13073            rn: Reg::R1,
13074            lane: 3,
13075            size: MveSize::S32,
13076        };
13077        let code = encoder.encode(&op).unwrap();
13078        assert_eq!(code.len(), 4, "MVE insert lane should be 4 bytes");
13079    }
13080
13081    #[test]
13082    fn test_encode_mve_addf32_thumb2() {
13083        let encoder = ArmEncoder::new_thumb2();
13084        let op = ArmOp::MveAddF32 {
13085            qd: QReg::Q0,
13086            qn: QReg::Q1,
13087            qm: QReg::Q2,
13088        };
13089        let code = encoder.encode(&op).unwrap();
13090        assert_eq!(code.len(), 4, "MVE VADD.F32 should be 4 bytes");
13091    }
13092
13093    #[test]
13094    fn test_encode_mve_divf32_thumb2() {
13095        let encoder = ArmEncoder::new_thumb2();
13096        let op = ArmOp::MveDivF32 {
13097            qd: QReg::Q0,
13098            qn: QReg::Q1,
13099            qm: QReg::Q2,
13100        };
13101        let code = encoder.encode(&op).unwrap();
13102        // Lane-wise: 4 x VDIV.F32 = 4 x 4 = 16 bytes
13103        assert_eq!(
13104            code.len(),
13105            16,
13106            "MVE VDIV.F32 (lane-wise) should be 16 bytes"
13107        );
13108    }
13109
13110    #[test]
13111    fn test_encode_mve_sqrtf32_thumb2() {
13112        let encoder = ArmEncoder::new_thumb2();
13113        let op = ArmOp::MveSqrtF32 {
13114            qd: QReg::Q0,
13115            qm: QReg::Q1,
13116        };
13117        let code = encoder.encode(&op).unwrap();
13118        // Lane-wise: 4 x VSQRT.F32 = 4 x 4 = 16 bytes
13119        assert_eq!(
13120            code.len(),
13121            16,
13122            "MVE VSQRT.F32 (lane-wise) should be 16 bytes"
13123        );
13124    }
13125
13126    #[test]
13127    fn test_encode_mve_negf32_thumb2() {
13128        let encoder = ArmEncoder::new_thumb2();
13129        let op = ArmOp::MveNegF32 {
13130            qd: QReg::Q0,
13131            qm: QReg::Q1,
13132        };
13133        let code = encoder.encode(&op).unwrap();
13134        assert_eq!(code.len(), 4, "MVE VNEG.F32 should be 4 bytes");
13135    }
13136
13137    #[test]
13138    fn test_encode_mve_absf32_thumb2() {
13139        let encoder = ArmEncoder::new_thumb2();
13140        let op = ArmOp::MveAbsF32 {
13141            qd: QReg::Q0,
13142            qm: QReg::Q1,
13143        };
13144        let code = encoder.encode(&op).unwrap();
13145        assert_eq!(code.len(), 4, "MVE VABS.F32 should be 4 bytes");
13146    }
13147
13148    /// VCR-RA-001 / immediate-folding precondition: pins the Thumb-2 `AND`
13149    /// immediate encoding for the byte range and documents its bound.
13150    ///
13151    /// The `And { Operand2::Imm }` encoder packs the low 12 bits straight into
13152    /// the `i:imm3:imm8` field WITHOUT applying ThumbExpandImm (the modified-
13153    /// immediate expansion). For `imm <= 0xFF` (e.g. gale's int8 clamps
13154    /// `#0x7e` / `#0x7f`) that is correct — `i:imm3 = 0000` means "imm8
13155    /// zero-extended". So `and r2, r0, #0x7e` encodes to the canonical
13156    /// `00 f0 7e 02`. For `imm >= 0x100` the field would need a true
13157    /// ThumbExpandImm pattern (rotation / replication), which is NOT
13158    /// implemented here — so **immediate folding must gate on `imm <= 0xFF`**
13159    /// until the encoder is hardened to ThumbExpandImm/Ok-or-Err (the
13160    /// "encoder must be Ok-or-Err, never silently wrong" principle, #180/#185).
13161    /// This bound covers the measured `flat_flight` waste (#209).
13162    #[test]
13163    fn and_immediate_encodes_correctly_in_byte_range_documents_fold_bound() {
13164        let encoder = ArmEncoder::new_thumb2();
13165        let op = ArmOp::And {
13166            rd: Reg::R2,
13167            rn: Reg::R0,
13168            op2: Operand2::Imm(0x7e),
13169        };
13170        let code = encoder.encode(&op).unwrap();
13171        assert_eq!(
13172            code,
13173            vec![0x00, 0xf0, 0x7e, 0x02],
13174            "and r2, r0, #0x7e must encode to the canonical AND.W T1 (imm8=0x7e)"
13175        );
13176    }
13177
13178    /// #255: the shared ThumbExpandImm reverse-encoder underpinning the
13179    /// data-processing immediate fix. Encodable modified immediates round-trip to
13180    /// the expected `i:imm3:imm8` field; a genuinely non-modified value is `None`
13181    /// (caller must materialize into a register). Note `1000 = 0xFA ror 30` *is*
13182    /// representable (field 0xF7A) — the old encoder mis-encoded it (raw 0x3E8);
13183    /// this encodes it correctly.
13184    #[test]
13185    fn try_thumb_expand_imm_encodes_modified_immediates() {
13186        assert_eq!(try_thumb_expand_imm(0x7e), Some(0x07e)); // zero-extended byte
13187        assert_eq!(try_thumb_expand_imm(0xff), Some(0x0ff));
13188        assert_eq!(try_thumb_expand_imm(0x0001_0001), Some(0x101)); // 0x00XY00XY
13189        assert_eq!(try_thumb_expand_imm(0xff00_ff00), Some(0x2ff)); // 0xXY00XY00
13190        assert_eq!(try_thumb_expand_imm(0xffff_ffff), Some(0x3ff)); // 0xXYXYXYXY
13191        assert_eq!(try_thumb_expand_imm(0x100), Some(0xf80)); // 0x80 ror 31
13192        assert_eq!(try_thumb_expand_imm(0x8000_0000), Some(0x400)); // 0x80 ror 8
13193        assert_eq!(try_thumb_expand_imm(1000), Some(0xf7a)); // 0xFA ror 30
13194        // Genuinely unrepresentable (bits too far apart for an 8-bit window).
13195        assert_eq!(try_thumb_expand_imm(0x101), None);
13196        assert_eq!(try_thumb_expand_imm(0x12345), None);
13197    }
13198
13199    /// #255: CMP/ADDS/SUBS encode any valid modified immediate correctly, and
13200    /// ERROR (not silently mis-encode) on a genuinely unrepresentable one,
13201    /// forcing the selector to materialize into a register — closing the
13202    /// silent-miscompile class of #251/#253.
13203    #[test]
13204    fn cmp_adds_subs_immediate_error_on_non_modified_imm() {
13205        let encoder = ArmEncoder::new_thumb2();
13206        // cmp r0, #0xff → valid → Ok; cmp r0, #1000 → valid (0xFA ror 30) → Ok.
13207        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 0xff).is_ok());
13208        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 1000).is_ok());
13209        // cmp r0, #0x101 → NOT a modified immediate → Err (materialize-reg).
13210        assert!(
13211            encoder.encode_thumb32_cmp_imm(&Reg::R0, 0x101).is_err(),
13212            "cmp #0x101 must error, not compare the wrong constant"
13213        );
13214        assert!(
13215            encoder
13216                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x101)
13217                .is_err()
13218        );
13219        assert!(
13220            encoder
13221                .encode_thumb32_subs(&Reg::R0, &Reg::R0, 0x101)
13222                .is_err()
13223        );
13224        // ...but a valid modified immediate still encodes.
13225        assert!(
13226            encoder
13227                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x80)
13228                .is_ok()
13229        );
13230    }
13231
13232    /// #257: MLA (multiply-accumulate) encodes as MLS without the bit-4 op flag.
13233    /// `mla r2, r3, r4, r8` (rd=r2, rn=r3, rm=r4, ra=r8) → Thumb-2 `03 fb 04 82`.
13234    #[test]
13235    fn mla_thumb2_encodes_correctly() {
13236        let encoder = ArmEncoder::new_thumb2();
13237        let code = encoder
13238            .encode(&ArmOp::Mla {
13239                rd: Reg::R2,
13240                rn: Reg::R3,
13241                rm: Reg::R4,
13242                ra: Reg::R8,
13243            })
13244            .unwrap();
13245        // hw1 = 0xFB03, hw2 = (8<<12)|(2<<8)|4 = 0x8204
13246        assert_eq!(code, vec![0x03, 0xfb, 0x04, 0x82]);
13247    }
13248
13249    /// #259: LDR/STR (and sub-word) immediate-offset encoders truncated
13250    /// `offset & 0xFFF`, silently targeting the wrong address for offset >= 4096.
13251    /// They now error (the selector must use register-offset addressing) — the
13252    /// load/store sibling of the #253/#255 class. Offsets <= 4095 still encode.
13253    #[test]
13254    fn ldst_imm12_offset_errors_when_out_of_range() {
13255        let encoder = ArmEncoder::new_thumb2();
13256        // offset 0xFFF (4095): valid → Ok; ldr r0, [r1, #4095].
13257        assert!(
13258            encoder
13259                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0xFFF)
13260                .is_ok()
13261        );
13262        // offset 0x1000 (4096): out of imm12 range → Err (not & 0xFFF → #0).
13263        assert!(
13264            encoder
13265                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0x1000)
13266                .is_err(),
13267            "ldr offset 4096 must error, not wrap to 0"
13268        );
13269        assert!(
13270            encoder
13271                .encode_thumb32_str(&Reg::R0, &Reg::R1, 0x1000)
13272                .is_err()
13273        );
13274        assert!(
13275            encoder
13276                .encode_thumb32_ldrb_imm(&Reg::R0, &Reg::R1, 5000)
13277                .is_err()
13278        );
13279        assert!(
13280            encoder
13281                .encode_thumb32_strh_imm(&Reg::R0, &Reg::R1, 5000)
13282                .is_err()
13283        );
13284    }
13285
13286    /// Latent miscompile fix: ADD/SUB with a >0xFF immediate (e.g.
13287    /// `add sp, sp, #frame` for a >=256-byte frame) used ADD.W (T3), whose
13288    /// `i:imm3:imm8` is a ThumbExpandImm modified immediate — so `#256` silently
13289    /// encoded as `#0` (stack corruption). Use ADDW/SUBW (T4), a PLAIN 12-bit
13290    /// immediate, for 0x100..=0xFFF; keep T3 for <=0xFF (bit-identical); error
13291    /// beyond 4095.
13292    #[test]
13293    fn add_sub_large_immediate_use_addw_subw_not_misencoded() {
13294        let encoder = ArmEncoder::new_thumb2();
13295        // add sp, sp, #256  →  ADDW (T4) SP, SP, #256  =  0d f2 00 1d
13296        assert_eq!(
13297            encoder
13298                .encode(&ArmOp::Add {
13299                    rd: Reg::SP,
13300                    rn: Reg::SP,
13301                    op2: Operand2::Imm(256),
13302                })
13303                .unwrap(),
13304            vec![0x0d, 0xf2, 0x00, 0x1d],
13305            "add sp,sp,#256 must be ADDW (plain imm12), not a mis-encoded ADD.W"
13306        );
13307        // sub sp, sp, #256  →  SUBW (T4) SP, SP, #256  =  ad f2 00 1d
13308        assert_eq!(
13309            encoder
13310                .encode(&ArmOp::Sub {
13311                    rd: Reg::SP,
13312                    rn: Reg::SP,
13313                    op2: Operand2::Imm(256),
13314                })
13315                .unwrap(),
13316            vec![0xad, 0xf2, 0x00, 0x1d],
13317        );
13318        // > 4095 has no single-instruction encoding → error, not silent wrong.
13319        assert!(
13320            encoder
13321                .encode(&ArmOp::Add {
13322                    rd: Reg::SP,
13323                    rn: Reg::SP,
13324                    op2: Operand2::Imm(5000),
13325                })
13326                .is_err(),
13327            "add #5000 must error (no single ADDW), not mis-encode"
13328        );
13329    }
13330
13331    /// Closes the data-proc immediate class: AND and CMN now go through
13332    /// `try_thumb_expand_imm` like ORR/EOR/CMP — correct for any modified
13333    /// immediate, `Err` (not raw-pack / NOP) on an un-encodable one. The byte
13334    /// range stays bit-identical (`and r2,r0,#0x7e` is unchanged).
13335    #[test]
13336    fn and_cmn_immediate_thumb_expand_else_error() {
13337        let encoder = ArmEncoder::new_thumb2();
13338        // byte range unchanged (bit-identical with the pre-retrofit encoding)
13339        assert_eq!(
13340            encoder
13341                .encode(&ArmOp::And {
13342                    rd: Reg::R2,
13343                    rn: Reg::R0,
13344                    op2: Operand2::Imm(0x7e),
13345                })
13346                .unwrap(),
13347            vec![0x00, 0xf0, 0x7e, 0x02],
13348        );
13349        // a valid replicated modified immediate now encodes (was silently wrong)
13350        assert!(
13351            encoder
13352                .encode(&ArmOp::And {
13353                    rd: Reg::R2,
13354                    rn: Reg::R0,
13355                    op2: Operand2::Imm(0xff00ff00u32 as i32),
13356                })
13357                .is_ok()
13358        );
13359        // a genuinely un-encodable immediate errors (AND was raw-pack; CMN NOP)
13360        assert!(
13361            encoder
13362                .encode(&ArmOp::And {
13363                    rd: Reg::R2,
13364                    rn: Reg::R0,
13365                    op2: Operand2::Imm(0x101),
13366                })
13367                .is_err()
13368        );
13369        assert!(
13370            encoder
13371                .encode(&ArmOp::Cmn {
13372                    rn: Reg::R0,
13373                    op2: Operand2::Imm(0x101),
13374                })
13375                .is_err(),
13376            "CMN #0x101 must error, not emit a NOP"
13377        );
13378    }
13379
13380    /// VCR-RA-001: ORR/EOR with a small immediate must encode the real
13381    /// instruction (not a silent `0xBF00` NOP). Pins the byte range and the
13382    /// Ok-or-Err bound that makes future Or/Eor immediate folding safe.
13383    #[test]
13384    fn orr_eor_immediate_encode_in_byte_range_else_error() {
13385        let encoder = ArmEncoder::new_thumb2();
13386        // orr r2, r0, #0x7e  →  ORR.W T1, imm8=0x7e
13387        assert_eq!(
13388            encoder
13389                .encode(&ArmOp::Orr {
13390                    rd: Reg::R2,
13391                    rn: Reg::R0,
13392                    op2: Operand2::Imm(0x7e),
13393                })
13394                .unwrap(),
13395            vec![0x40, 0xf0, 0x7e, 0x02],
13396        );
13397        // eor r2, r0, #0x7e  →  EOR.W T1, imm8=0x7e
13398        assert_eq!(
13399            encoder
13400                .encode(&ArmOp::Eor {
13401                    rd: Reg::R2,
13402                    rn: Reg::R0,
13403                    op2: Operand2::Imm(0x7e),
13404                })
13405                .unwrap(),
13406            vec![0x80, 0xf0, 0x7e, 0x02],
13407        );
13408        // Out-of-range immediates error rather than silently mis-encode / NOP.
13409        assert!(
13410            encoder
13411                .encode(&ArmOp::Orr {
13412                    rd: Reg::R2,
13413                    rn: Reg::R0,
13414                    op2: Operand2::Imm(0x140),
13415                })
13416                .is_err(),
13417            "ORR #0x140 must error, not emit a NOP"
13418        );
13419    }
13420
13421    #[test]
13422    fn test_encode_mve_different_qregs() {
13423        let encoder = ArmEncoder::new_thumb2();
13424
13425        // Test that different Q-register numbers produce different encodings
13426        let op1 = ArmOp::MveAddI {
13427            qd: QReg::Q0,
13428            qn: QReg::Q0,
13429            qm: QReg::Q0,
13430            size: MveSize::S32,
13431        };
13432        let op2 = ArmOp::MveAddI {
13433            qd: QReg::Q3,
13434            qn: QReg::Q5,
13435            qm: QReg::Q7,
13436            size: MveSize::S32,
13437        };
13438        let code1 = encoder.encode(&op1).unwrap();
13439        let code2 = encoder.encode(&op2).unwrap();
13440        assert_ne!(
13441            code1, code2,
13442            "Different Q-registers should produce different encodings"
13443        );
13444    }
13445
13446    #[test]
13447    fn test_encode_mve_arm32_loud_err() {
13448        // #615: MVE (Helium) is Thumb-2-only. The ARM32 encoder used to emit
13449        // a silent NOP here (dropping the vector op); it must now be a typed
13450        // Err so a broken "MVE implies Thumb" invariant fails loudly.
13451        let encoder = ArmEncoder::new_arm32();
13452        let op = ArmOp::MveAddI {
13453            qd: QReg::Q0,
13454            qn: QReg::Q1,
13455            qm: QReg::Q2,
13456            size: MveSize::S32,
13457        };
13458        let err = encoder
13459            .encode(&op)
13460            .expect_err("ARM32 MVE must be a loud Err, not a silent NOP (#615)");
13461        assert!(
13462            err.to_string().contains("Thumb-2 only"),
13463            "unexpected error message: {err}"
13464        );
13465    }
13466}