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            // RQ-67-VFPREACH (#1267): the callee-saved VFP save/restore is
2061            // emitted only by the Cortex-M (Thumb-2) VFP-pressure rung. The A32
2062            // path has no such rung, so reaching here means a caller built the
2063            // op for the wrong profile — refuse LOUDLY rather than encode
2064            // something plausible (#615: expand-or-loud-reject, never a silent
2065            // NOP).
2066            ArmOp::VPushCalleeSavedVfp | ArmOp::VPopCalleeSavedVfp => {
2067                return Err(synth_core::Error::synthesis(
2068                    "VPUSH/VPOP {d8-d15} is Thumb-2 only in this backend (RQ-67-VFPREACH, #1267)",
2069                ));
2070            }
2071            ArmOp::F32ReinterpretI32 { sd, rm } => encode_vmov_core_sreg(true, sd, rm)?,
2072            ArmOp::I32ReinterpretF32 { rd, sm } => encode_vmov_core_sreg(false, sm, rd)?,
2073            ArmOp::I32TruncF32S { rd, sm } => {
2074                return self.encode_arm_i32_trunc_f32(rd, sm, true);
2075            }
2076            ArmOp::I32TruncF32U { rd, sm } => {
2077                return self.encode_arm_i32_trunc_f32(rd, sm, false);
2078            }
2079
2080            // f64 VFP double-precision instructions (ARM32)
2081            // F64 arithmetic: same as F32 but with sz=1 (bit 8 = 1, cp11 = 0xB)
2082            ArmOp::F64Add { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B00, dd, dn, dm)?,
2083            ArmOp::F64Sub { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B40, dd, dn, dm)?,
2084            ArmOp::F64Mul { dd, dn, dm } => encode_vfp_3reg_f64(0xEE200B00, dd, dn, dm)?,
2085            ArmOp::F64Div { dd, dn, dm } => encode_vfp_3reg_f64(0xEE800B00, dd, dn, dm)?,
2086            ArmOp::F64Abs { dd, dm } => encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?,
2087            ArmOp::F64Neg { dd, dm } => encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?,
2088            ArmOp::F64Sqrt { dd, dm } => encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?,
2089
2090            // f64 pseudo-ops
2091            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
2092            ArmOp::F64Ceil { dd, dm } => {
2093                return self.encode_arm_f64_rounding(dd, dm, 0b01);
2094            }
2095            ArmOp::F64Floor { dd, dm } => {
2096                return self.encode_arm_f64_rounding(dd, dm, 0b10);
2097            }
2098            ArmOp::F64Trunc { dd, dm } => {
2099                return self.encode_arm_f64_rounding(dd, dm, 0b11);
2100            }
2101            ArmOp::F64Nearest { dd, dm } => {
2102                return self.encode_arm_f64_rounding(dd, dm, 0b00);
2103            }
2104            ArmOp::F64Min { dd, dn, dm } => {
2105                return self.encode_arm_f64_minmax(dd, dn, dm, true);
2106            }
2107            ArmOp::F64Max { dd, dn, dm } => {
2108                return self.encode_arm_f64_minmax(dd, dn, dm, false);
2109            }
2110            ArmOp::F64Copysign { dd, dn, dm } => {
2111                return self.encode_arm_f64_copysign(dd, dn, dm);
2112            }
2113
2114            // f64 comparisons
2115            ArmOp::F64Eq { rd, dn, dm } => {
2116                return self.encode_arm_f64_compare(rd, dn, dm, 0x0);
2117            }
2118            ArmOp::F64Ne { rd, dn, dm } => {
2119                return self.encode_arm_f64_compare(rd, dn, dm, 0x1);
2120            }
2121            ArmOp::F64Lt { rd, dn, dm } => {
2122                return self.encode_arm_f64_compare(rd, dn, dm, 0x4);
2123            }
2124            ArmOp::F64Le { rd, dn, dm } => {
2125                return self.encode_arm_f64_compare(rd, dn, dm, 0x9);
2126            }
2127            ArmOp::F64Gt { rd, dn, dm } => {
2128                return self.encode_arm_f64_compare(rd, dn, dm, 0xC);
2129            }
2130            ArmOp::F64Ge { rd, dn, dm } => {
2131                return self.encode_arm_f64_compare(rd, dn, dm, 0xA);
2132            }
2133
2134            ArmOp::F64Const { dd, value } => {
2135                return self.encode_arm_f64_const(dd, *value);
2136            }
2137
2138            ArmOp::F64Load { dd, addr } => encode_vfp_ldst_f64(0xED900B00, dd, addr)?,
2139            ArmOp::F64Store { dd, addr } => encode_vfp_ldst_f64(0xED800B00, dd, addr)?,
2140
2141            ArmOp::F64ConvertI32S { dd, rm } => {
2142                return self.encode_arm_f64_convert_i32(dd, rm, true);
2143            }
2144            ArmOp::F64ConvertI32U { dd, rm } => {
2145                return self.encode_arm_f64_convert_i32(dd, rm, false);
2146            }
2147            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
2148                return Err(synth_core::Error::synthesis(
2149                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2150                ));
2151            }
2152            ArmOp::F64PromoteF32 { dd, sm } => {
2153                return self.encode_arm_f64_promote_f32(dd, sm);
2154            }
2155            // GI-FPU-002 (#369): no synth A32 target carries an FPU (cortex-r5
2156            // has none — the selector declines every float op there), so the
2157            // A32 encoder refuses loudly instead of shipping an untested
2158            // encoding (#615: never a silent wrong byte).
2159            ArmOp::F32DemoteF64 { .. } => {
2160                return Err(synth_core::Error::synthesis(
2161                    "F32DemoteF64 has no A32 encoding (no A32 target has an FPU)",
2162                ));
2163            }
2164            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => {
2165                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?
2166            }
2167            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => {
2168                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?
2169            }
2170            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
2171                return Err(synth_core::Error::synthesis(
2172                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
2173                ));
2174            }
2175            ArmOp::I32TruncF64S { rd, dm } => {
2176                return self.encode_arm_i32_trunc_f64(rd, dm, true);
2177            }
2178            ArmOp::I32TruncF64U { rd, dm } => {
2179                return self.encode_arm_i32_trunc_f64(rd, dm, false);
2180            }
2181            // #615: multi-instruction i64 sequences — expanded to real A32 by
2182            // `encode_arm_expanded`, no longer "Thumb-2 only" NOPs.
2183            ArmOp::I64SetCond { .. }
2184            | ArmOp::I64SetCondZ { .. }
2185            | ArmOp::I64Mul { .. }
2186            | ArmOp::I64Shl { .. }
2187            | ArmOp::I64ShrS { .. }
2188            | ArmOp::I64ShrU { .. }
2189            | ArmOp::I64Rotl { .. }
2190            | ArmOp::I64Rotr { .. } => {
2191                unreachable!("handled by encode_arm_expanded (#615)")
2192            }
2193
2194            // MVE instructions — Thumb-2 only (Cortex-M55 is always Thumb-2)
2195            ArmOp::MveLoad { .. }
2196            | ArmOp::MveStore { .. }
2197            | ArmOp::MveConst { .. }
2198            | ArmOp::MveAnd { .. }
2199            | ArmOp::MveOrr { .. }
2200            | ArmOp::MveEor { .. }
2201            | ArmOp::MveMvn { .. }
2202            | ArmOp::MveBic { .. }
2203            | ArmOp::MveAddI { .. }
2204            | ArmOp::MveSubI { .. }
2205            | ArmOp::MveMulI { .. }
2206            | ArmOp::MveNegI { .. }
2207            | ArmOp::MveCmpEqI { .. }
2208            | ArmOp::MveCmpNeI { .. }
2209            | ArmOp::MveCmpLtS { .. }
2210            | ArmOp::MveCmpLtU { .. }
2211            | ArmOp::MveCmpGtS { .. }
2212            | ArmOp::MveCmpGtU { .. }
2213            | ArmOp::MveCmpLeS { .. }
2214            | ArmOp::MveCmpLeU { .. }
2215            | ArmOp::MveCmpGeS { .. }
2216            | ArmOp::MveCmpGeU { .. }
2217            | ArmOp::MveDup { .. }
2218            | ArmOp::MveExtractLane { .. }
2219            | ArmOp::MveInsertLane { .. }
2220            | ArmOp::MveAddF32 { .. }
2221            | ArmOp::MveSubF32 { .. }
2222            | ArmOp::MveMulF32 { .. }
2223            | ArmOp::MveNegF32 { .. }
2224            | ArmOp::MveAbsF32 { .. }
2225            | ArmOp::MveCmpEqF32 { .. }
2226            | ArmOp::MveCmpNeF32 { .. }
2227            | ArmOp::MveCmpLtF32 { .. }
2228            | ArmOp::MveCmpLeF32 { .. }
2229            | ArmOp::MveCmpGtF32 { .. }
2230            | ArmOp::MveCmpGeF32 { .. }
2231            | ArmOp::MveDupF32 { .. }
2232            | ArmOp::MveExtractLaneF32 { .. }
2233            | ArmOp::MveReplaceLaneF32 { .. }
2234            | ArmOp::MveDivF32 { .. }
2235            | ArmOp::MveSqrtF32 { .. } => {
2236                // MVE (Helium) is a Thumb-2-only extension (Cortex-M55); there
2237                // is no A32 encoding. The selector only emits MVE ops for
2238                // Thumb targets — a NOP here silently dropped the vector op
2239                // if that invariant ever broke (#615 class). Err keeps the
2240                // encoder total and the failure loud.
2241                return Err(synth_core::Error::synthesis(format!(
2242                    "MVE op {op:?} has no A32 (ARM-mode) encoding — MVE is Thumb-2 only (#615)"
2243                )));
2244            }
2245        };
2246
2247        // ARM32 instructions are little-endian
2248        Ok(instr.to_le_bytes().to_vec())
2249    }
2250
2251    // === ARM32 VFP multi-instruction helpers ===
2252
2253    /// Encode F32 comparison as ARM32: VCMP.F32 + VMRS + MOV rd,#0 + MOVcond rd,#1
2254    fn encode_arm_f32_compare(
2255        &self,
2256        rd: &Reg,
2257        sn: &VfpReg,
2258        sm: &VfpReg,
2259        cond_code: u32,
2260    ) -> Result<Vec<u8>> {
2261        let mut bytes = Vec::new();
2262
2263        // VCMP.F32 Sn, Sm: 0xEEB40A40 with Sn in Vd position, Sm in Vm position
2264        let sn_num = vfp_sreg_to_num(sn)?;
2265        let sm_num = vfp_sreg_to_num(sm)?;
2266        let (vd, d) = encode_sreg(sn_num);
2267        let (vm, m) = encode_sreg(sm_num);
2268        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2269        bytes.extend_from_slice(&vcmp.to_le_bytes());
2270
2271        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
2272        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2273
2274        // MOV rd, #0: 0xE3A0_0000 | (rd << 12)
2275        let rd_bits = reg_to_bits(rd);
2276        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2277        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2278
2279        // MOVcond rd, #1: cond(4) | 0011 1010 0000 rd(4) 0000 0000 0001
2280        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2281        bytes.extend_from_slice(&mov_one.to_le_bytes());
2282
2283        Ok(bytes)
2284    }
2285
2286    /// Encode F32 constant load as ARM32: MOVW Rt,#lo16 + MOVT Rt,#hi16 + VMOV Sd,Rt
2287    fn encode_arm_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
2288        let mut bytes = Vec::new();
2289        let bits = value.to_bits();
2290
2291        // Use R12 as temp register for constant loading
2292        let rt: u32 = 12; // R12/IP
2293
2294        // MOVW R12, #lo16: 0xE300_C000 | (imm4 << 16) | imm12
2295        let lo16 = bits & 0xFFFF;
2296        let movw = 0xE3000000 | (rt << 12) | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2297        bytes.extend_from_slice(&movw.to_le_bytes());
2298
2299        // MOVT R12, #hi16: 0xE340_C000 | (imm4 << 16) | imm12
2300        let hi16 = (bits >> 16) & 0xFFFF;
2301        let movt = 0xE3400000 | (rt << 12) | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2302        bytes.extend_from_slice(&movt.to_le_bytes());
2303
2304        // VMOV Sd, R12
2305        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
2306        bytes.extend_from_slice(&vmov.to_le_bytes());
2307
2308        Ok(bytes)
2309    }
2310
2311    /// Encode VMOV + VCVT.F32.S32/U32 as ARM32
2312    fn encode_arm_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2313        let mut bytes = Vec::new();
2314
2315        // VMOV Sd, Rm — move integer to VFP register
2316        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
2317        bytes.extend_from_slice(&vmov.to_le_bytes());
2318
2319        // VCVT.F32.S32 Sd, Sd (signed) or VCVT.F32.U32 Sd, Sd (unsigned).
2320        // The "op" bit (bit 7) selects signedness: 1 = signed (S32), 0 =
2321        // unsigned (U32). So signed = 0xEEB80AC0, unsigned = 0xEEB80A40 —
2322        // objdump confirms 0xEEB80A40 decodes to `vcvt.f32.u32` (GI-FPU-002:
2323        // the two were previously swapped, silently making `convert_i32_s`
2324        // an unsigned conversion).
2325        let sd_num = vfp_sreg_to_num(sd)?;
2326        let (vd, d) = encode_sreg(sd_num);
2327        let (vm, m) = encode_sreg(sd_num); // same register as source
2328        let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
2329        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2330        bytes.extend_from_slice(&vcvt.to_le_bytes());
2331
2332        Ok(bytes)
2333    }
2334
2335    /// Encode F32 rounding pseudo-op as ARM32 via VCVT to integer and back.
2336    /// mode: 0b00=nearest, 0b01=floor(-Inf), 0b10=ceil(+Inf), 0b11=trunc(zero)
2337    /// Strategy: VCVT.S32.F32 Sd, Sm (toward zero), then VCVT.F32.S32 Sd, Sd
2338    /// For ceil/floor/nearest, we use VCVTR (round toward mode) + convert back.
2339    /// Simplified: convert to int (toward zero for trunc) then back to float.
2340    /// Encode F32 rounding as ARM32.
2341    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2342    ///
2343    /// For trunc (mode=0b11): uses VCVTR.S32.F32 (always rounds toward zero).
2344    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant
2345    /// which honours FPSCR rmode), then restores FPSCR.
2346    fn encode_arm_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2347        let mut bytes = Vec::new();
2348        let sm_num = vfp_sreg_to_num(sm)?;
2349        let sd_num = vfp_sreg_to_num(sd)?;
2350        let (vd_s, d_s) = encode_sreg(sd_num);
2351        let (vm_s, m_s) = encode_sreg(sm_num);
2352
2353        if mode == 0b11 {
2354            // Trunc (toward zero): VCVTR.S32.F32 — the "R" variant always truncates.
2355            // 0xEEBD0AC0: bit[7]=1 => round toward zero regardless of FPSCR
2356            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2357            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2358        } else {
2359            // ceil/floor/nearest: manipulate FPSCR rounding mode
2360            let rt: u32 = 12; // R12/IP as temp
2361
2362            // VMRS R12, FPSCR
2363            let vmrs = 0xEEF10A10 | (rt << 12);
2364            bytes.extend_from_slice(&vmrs.to_le_bytes());
2365
2366            // BIC R12, R12, #(3 << 22) — clear RMode bits [23:22]
2367            // 3<<22 = 0x00C00000. ARM rotated imm: 0x03 ror 10 (rotation=5, imm8=0x03)
2368            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2369            bytes.extend_from_slice(&bic.to_le_bytes());
2370
2371            // ORR R12, R12, #(mode << 22) — set desired rounding mode
2372            if mode != 0 {
2373                // mode<<22: rotation=5, imm8=mode
2374                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2375                bytes.extend_from_slice(&orr.to_le_bytes());
2376            }
2377
2378            // VMSR FPSCR, R12
2379            let vmsr = 0xEEE10A10 | (rt << 12);
2380            bytes.extend_from_slice(&vmsr.to_le_bytes());
2381
2382            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rounding mode
2383            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2384            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2385
2386            // Restore FPSCR: clear rmode bits back to nearest (default)
2387            bytes.extend_from_slice(&vmrs.to_le_bytes());
2388            bytes.extend_from_slice(&bic.to_le_bytes());
2389            bytes.extend_from_slice(&vmsr.to_le_bytes());
2390        }
2391
2392        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
2393        let (vd2, d2) = encode_sreg(sd_num);
2394        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
2395        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2396
2397        Ok(bytes)
2398    }
2399
2400    /// Encode F32 min/max as ARM32: VCMP + VMRS + conditional VMOV
2401    fn encode_arm_f32_minmax(
2402        &self,
2403        sd: &VfpReg,
2404        sn: &VfpReg,
2405        sm: &VfpReg,
2406        is_min: bool,
2407    ) -> Result<Vec<u8>> {
2408        let mut bytes = Vec::new();
2409        let sn_num = vfp_sreg_to_num(sn)?;
2410        let sm_num = vfp_sreg_to_num(sm)?;
2411        let sd_num = vfp_sreg_to_num(sd)?;
2412
2413        // VMOV Sd, Sn (start with first operand)
2414        let (vd, d) = encode_sreg(sd_num);
2415        let (vn, n) = encode_sreg(sn_num);
2416        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2417        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2418
2419        // VCMP.F32 Sn, Sm
2420        let (vm, m) = encode_sreg(sm_num);
2421        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2422        bytes.extend_from_slice(&vcmp.to_le_bytes());
2423
2424        // VMRS APSR_nzcv, FPSCR
2425        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2426
2427        // For min: if Sn > Sm (GT), use Sm. Condition = GT (0xC)
2428        // For max: if Sn < Sm (MI/LT), use Sm. Condition = MI (0x4)
2429        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2430
2431        // VMOV{cond} Sd, Sm — conditional VMOV
2432        let vmov_cond = (cond << 28) | 0x0EB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2433        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2434
2435        Ok(bytes)
2436    }
2437
2438    /// Encode F32 copysign as ARM32: extract sign from Sm, magnitude from Sn
2439    fn encode_arm_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2440        let mut bytes = Vec::new();
2441
2442        // VMOV R12, Sm (get sign source bits)
2443        let vmov_sm = encode_vmov_core_sreg(false, sm, &Reg::R12)?;
2444        bytes.extend_from_slice(&vmov_sm.to_le_bytes());
2445
2446        // VMOV R0, Sn (get magnitude source bits) — use R0 as temp
2447        let vmov_sn = encode_vmov_core_sreg(false, sn, &Reg::R0)?;
2448        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2449
2450        // AND R12, R12, #0x80000000 (keep only sign bit)
2451        // Thumb-2 constant 0x80000000 needs special encoding; in ARM32 use rotated imm
2452        // 0x80000000 = 0x02 rotated right by 2 (rotation=1, imm8=0x02)
2453        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2454        bytes.extend_from_slice(&and_sign.to_le_bytes());
2455
2456        // BIC R0, R0, #0x80000000 (clear sign bit from magnitude)
2457        // R0 = register 0, so Rn and Rd fields are 0
2458        let bic_sign = 0xE3C00000u32 | (1 << 8) | 0x02;
2459        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2460
2461        // ORR R0, R0, R12 (combine sign + magnitude)
2462        // R0 = register 0, so Rn and Rd fields are 0
2463        let orr = 0xE1800000u32 | 12;
2464        bytes.extend_from_slice(&orr.to_le_bytes());
2465
2466        // VMOV Sd, R0
2467        let vmov_result = encode_vmov_core_sreg(true, sd, &Reg::R0)?;
2468        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2469
2470        Ok(bytes)
2471    }
2472
2473    /// Encode F64 comparison as ARM32: VCMP.F64 + VMRS + MOV rd,#0 + MOVcond rd,#1
2474    fn encode_arm_f64_compare(
2475        &self,
2476        rd: &Reg,
2477        dn: &VfpReg,
2478        dm: &VfpReg,
2479        cond_code: u32,
2480    ) -> Result<Vec<u8>> {
2481        let mut bytes = Vec::new();
2482
2483        // VCMP.F64 Dn, Dm: 0xEEB40B40 with Dn in Vd position, Dm in Vm position
2484        let dn_num = vfp_dreg_to_num(dn)?;
2485        let dm_num = vfp_dreg_to_num(dm)?;
2486        let (vd, d) = encode_dreg(dn_num);
2487        let (vm, m) = encode_dreg(dm_num);
2488        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2489        bytes.extend_from_slice(&vcmp.to_le_bytes());
2490
2491        // VMRS APSR_nzcv, FPSCR
2492        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2493
2494        // MOV rd, #0
2495        let rd_bits = reg_to_bits(rd);
2496        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2497        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2498
2499        // MOVcond rd, #1
2500        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2501        bytes.extend_from_slice(&mov_one.to_le_bytes());
2502
2503        Ok(bytes)
2504    }
2505
2506    /// Encode F64 constant load as ARM32: MOVW + MOVT + MOVW + MOVT + VMOV
2507    fn encode_arm_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
2508        let mut bytes = Vec::new();
2509        let bits = value.to_bits();
2510        let lo32 = bits as u32;
2511        let hi32 = (bits >> 32) as u32;
2512
2513        // Load low 32 bits into R0 (Rd field = 0 for R0)
2514        let lo16 = lo32 & 0xFFFF;
2515        let movw_r0 = 0xE3000000 | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2516        bytes.extend_from_slice(&movw_r0.to_le_bytes());
2517        let hi16 = (lo32 >> 16) & 0xFFFF;
2518        let movt_r0 = 0xE3400000 | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2519        bytes.extend_from_slice(&movt_r0.to_le_bytes());
2520
2521        // Load high 32 bits into R12
2522        let lo16 = hi32 & 0xFFFF;
2523        let movw_r12 = 0xE3000000 | ((lo16 >> 12) << 16) | (12 << 12) | (lo16 & 0xFFF);
2524        bytes.extend_from_slice(&movw_r12.to_le_bytes());
2525        let hi16 = (hi32 >> 16) & 0xFFFF;
2526        let movt_r12 = 0xE3400000 | ((hi16 >> 12) << 16) | (12 << 12) | (hi16 & 0xFFF);
2527        bytes.extend_from_slice(&movt_r12.to_le_bytes());
2528
2529        // VMOV Dd, R0, R12
2530        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
2531        bytes.extend_from_slice(&vmov.to_le_bytes());
2532
2533        Ok(bytes)
2534    }
2535
2536    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as ARM32
2537    fn encode_arm_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2538        let mut bytes = Vec::new();
2539
2540        // Use S0 as intermediate: VMOV S0, Rm
2541        let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
2542        bytes.extend_from_slice(&vmov.to_le_bytes());
2543
2544        // VCVT.F64.S32 Dd, S0 (signed) or VCVT.F64.U32 Dd, S0 (unsigned)
2545        // Base: 0xEEB80B40 (signed) or 0xEEB80BC0 (unsigned)
2546        let dd_num = vfp_dreg_to_num(dd)?;
2547        let (vd, d) = encode_dreg(dd_num);
2548        let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
2549        // S0 is register 0: Vm=0, M=0
2550        let vcvt = base | (d << 22) | (vd << 12);
2551        bytes.extend_from_slice(&vcvt.to_le_bytes());
2552
2553        Ok(bytes)
2554    }
2555
2556    /// Encode VCVT.F64.F32 Dd, Sm as ARM32 (f32 to f64 promotion)
2557    fn encode_arm_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2558        let dd_num = vfp_dreg_to_num(dd)?;
2559        let sm_num = vfp_sreg_to_num(sm)?;
2560        let (vd, d) = encode_dreg(dd_num);
2561        let (vm, m) = encode_sreg(sm_num);
2562
2563        // VCVT.F64.F32 Dd, Sm: 0xEEB70AC0
2564        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
2565        Ok(vcvt.to_le_bytes().to_vec())
2566    }
2567
2568    /// Encode VCVT.S32/U32.F64 Sd, Dm + VMOV Rd, Sd as ARM32
2569    fn encode_arm_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2570        let mut bytes = Vec::new();
2571        let dm_num = vfp_dreg_to_num(dm)?;
2572        let (vm, m) = encode_dreg(dm_num);
2573
2574        // VCVT.S32.F64 S0, Dm (toward zero) or VCVT.U32.F64 S0, Dm
2575        // S0: Vd=0, D=0
2576        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
2577        let vcvt = base | (m << 5) | vm;
2578        bytes.extend_from_slice(&vcvt.to_le_bytes());
2579
2580        // VMOV Rd, S0
2581        let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
2582        bytes.extend_from_slice(&vmov.to_le_bytes());
2583
2584        Ok(bytes)
2585    }
2586
2587    /// Encode F64 rounding pseudo-op as ARM32 via VCVT to integer and back.
2588    /// Encode F64 rounding as ARM32.
2589    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2590    ///
2591    /// For trunc: uses VCVTR.S32.F64 (always truncates).
2592    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F64 (non-R variant),
2593    /// then restores FPSCR.
2594    fn encode_arm_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2595        let mut bytes = Vec::new();
2596        let dm_num = vfp_dreg_to_num(dm)?;
2597        let dd_num = vfp_dreg_to_num(dd)?;
2598        let (vm, m) = encode_dreg(dm_num);
2599        let (vd, d) = encode_dreg(dd_num);
2600
2601        if mode == 0b11 {
2602            // Trunc (toward zero): VCVTR.S32.F64 — bit[7]=1, always truncates
2603            let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
2604            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2605        } else {
2606            // ceil/floor/nearest: manipulate FPSCR rounding mode
2607            let rt: u32 = 12;
2608
2609            // VMRS R12, FPSCR
2610            let vmrs = 0xEEF10A10 | (rt << 12);
2611            bytes.extend_from_slice(&vmrs.to_le_bytes());
2612
2613            // BIC R12, R12, #(3 << 22)
2614            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2615            bytes.extend_from_slice(&bic.to_le_bytes());
2616
2617            // ORR R12, R12, #(mode << 22)
2618            if mode != 0 {
2619                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2620                bytes.extend_from_slice(&orr.to_le_bytes());
2621            }
2622
2623            // VMSR FPSCR, R12
2624            let vmsr = 0xEEE10A10 | (rt << 12);
2625            bytes.extend_from_slice(&vmsr.to_le_bytes());
2626
2627            // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0), uses FPSCR rmode
2628            let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
2629            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2630
2631            // Restore FPSCR
2632            bytes.extend_from_slice(&vmrs.to_le_bytes());
2633            bytes.extend_from_slice(&bic.to_le_bytes());
2634            bytes.extend_from_slice(&vmsr.to_le_bytes());
2635        }
2636
2637        // VCVT.F64.S32 Dd, S0 (convert back to double)
2638        let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
2639        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2640
2641        Ok(bytes)
2642    }
2643
2644    /// Encode F64 min/max as ARM32: VMOV + VCMP + VMRS + conditional VMOV
2645    fn encode_arm_f64_minmax(
2646        &self,
2647        dd: &VfpReg,
2648        dn: &VfpReg,
2649        dm: &VfpReg,
2650        is_min: bool,
2651    ) -> Result<Vec<u8>> {
2652        let mut bytes = Vec::new();
2653        let dn_num = vfp_dreg_to_num(dn)?;
2654        let dm_num = vfp_dreg_to_num(dm)?;
2655        let dd_num = vfp_dreg_to_num(dd)?;
2656
2657        // VMOV.F64 Dd, Dn (start with first operand)
2658        let (vd, d) = encode_dreg(dd_num);
2659        let (vn, n) = encode_dreg(dn_num);
2660        let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2661        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2662
2663        // VCMP.F64 Dn, Dm
2664        let (vm, m) = encode_dreg(dm_num);
2665        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2666        bytes.extend_from_slice(&vcmp.to_le_bytes());
2667
2668        // VMRS APSR_nzcv, FPSCR
2669        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2670
2671        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2672        let vmov_cond = (cond << 28) | 0x0EB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2673        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2674
2675        Ok(bytes)
2676    }
2677
2678    /// Encode F64 copysign as ARM32
2679    fn encode_arm_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
2680        let mut bytes = Vec::new();
2681
2682        // VMOV R0, R12, Dm (get sign source bits)
2683        let vmov_dm = encode_vmov_core_dreg(false, dm, &Reg::R0, &Reg::R12)?;
2684        bytes.extend_from_slice(&vmov_dm.to_le_bytes());
2685
2686        // VMOV R1, R2, Dn (get magnitude source bits)
2687        // We use R1 (lo) and R2 (hi) for the magnitude
2688        let vmov_dn = encode_vmov_core_dreg(false, dn, &Reg::R1, &Reg::R2)?;
2689        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2690
2691        // AND R12, R12, #0x80000000 (keep only sign bit from hi word)
2692        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2693        bytes.extend_from_slice(&and_sign.to_le_bytes());
2694
2695        // BIC R2, R2, #0x80000000 (clear sign bit from magnitude hi word)
2696        let bic_sign = 0xE3C00000u32 | (2 << 16) | (2 << 12) | (1 << 8) | 0x02;
2697        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2698
2699        // ORR R2, R2, R12 (combine sign + magnitude)
2700        let orr = 0xE1800000u32 | (2 << 16) | (2 << 12) | 12;
2701        bytes.extend_from_slice(&orr.to_le_bytes());
2702
2703        // VMOV Dd, R1, R2
2704        let vmov_result = encode_vmov_core_dreg(true, dd, &Reg::R1, &Reg::R2)?;
2705        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2706
2707        Ok(bytes)
2708    }
2709
2710    /// Encode VCVT.S32/U32.F32 + VMOV as ARM32
2711    fn encode_arm_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2712        let mut bytes = Vec::new();
2713
2714        // VCVT.S32.F32 Sd, Sm (toward zero) or VCVT.U32.F32 Sd, Sm
2715        // We use Sm as both source and destination for the intermediate result
2716        let sm_num = vfp_sreg_to_num(sm)?;
2717        let (vd, d) = encode_sreg(sm_num);
2718        let (vm, m) = encode_sreg(sm_num);
2719        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
2720        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2721        bytes.extend_from_slice(&vcvt.to_le_bytes());
2722
2723        // VMOV Rd, Sm — move result back to core register
2724        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
2725        bytes.extend_from_slice(&vmov.to_le_bytes());
2726
2727        Ok(bytes)
2728    }
2729
2730    /// Encode an ARM instruction in Thumb-2 mode (16-bit or 32-bit instructions)
2731    fn encode_thumb(&self, op: &ArmOp) -> Result<Vec<u8>> {
2732        // Thumb-2 supports both 16-bit and 32-bit instructions
2733        // 32-bit instructions are encoded as two 16-bit halfwords (big-endian order)
2734        match op {
2735            // === 16-bit Thumb encodings ===
2736            ArmOp::Add { rd, rn, op2 } => {
2737                let rd_bits = reg_to_bits(rd) as u16;
2738                let rn_bits = reg_to_bits(rn) as u16;
2739
2740                if let Operand2::Reg(rm) = op2 {
2741                    let rm_bits = reg_to_bits(rm) as u16;
2742                    // 16-bit ADDS only has 3-bit register fields (R0-R7). For
2743                    // high registers (e.g. R12, the MemLoad/MemStore base
2744                    // scratch) the bits overflow into adjacent fields, silently
2745                    // corrupting the operands — issue #178/#180: `add ip,ip,r0`
2746                    // was emitted as `adds r4,r5,r1`. Guard on all three regs
2747                    // being low and fall back to 32-bit ADD.W otherwise, exactly
2748                    // as the Sub handler below does.
2749                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2750                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2751                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2752                        Ok(instr.to_le_bytes().to_vec())
2753                    } else {
2754                        // ADD.W Rd, Rn, Rm (32-bit) for high registers
2755                        self.encode_thumb32_add_reg_raw(
2756                            rd_bits as u32,
2757                            rn_bits as u32,
2758                            rm_bits as u32,
2759                        )
2760                    }
2761                } else if let Operand2::Imm(imm) = op2 {
2762                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2763                        // ADDS Rd, Rn, #imm3 (16-bit): 0001 110 imm3 Rn Rd
2764                        let instr: u16 = 0x1C00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2765                        Ok(instr.to_le_bytes().to_vec())
2766                    } else {
2767                        // Use 32-bit ADD for larger immediates
2768                        self.encode_thumb32_add(rd, rn, *imm as u32)
2769                    }
2770                } else {
2771                    // Fallback to 32-bit encoding
2772                    self.encode_thumb32_add(rd, rn, 0)
2773                }
2774            }
2775
2776            ArmOp::Sub { rd, rn, op2 } => {
2777                let rd_bits = reg_to_bits(rd) as u16;
2778                let rn_bits = reg_to_bits(rn) as u16;
2779
2780                if let Operand2::Reg(rm) = op2 {
2781                    let rm_bits = reg_to_bits(rm) as u16;
2782                    // 16-bit SUBS can only use low registers (R0-R7)
2783                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2784                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2785                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2786                        Ok(instr.to_le_bytes().to_vec())
2787                    } else {
2788                        // Use 32-bit SUB.W for high registers
2789                        self.encode_thumb32_sub_reg_raw(
2790                            rd_bits as u32,
2791                            rn_bits as u32,
2792                            rm_bits as u32,
2793                        )
2794                    }
2795                } else if let Operand2::Imm(imm) = op2 {
2796                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2797                        // SUBS Rd, Rn, #imm3 (16-bit): 0001 111 imm3 Rn Rd
2798                        let instr: u16 = 0x1E00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2799                        Ok(instr.to_le_bytes().to_vec())
2800                    } else {
2801                        self.encode_thumb32_sub(rd, rn, *imm as u32)
2802                    }
2803                } else {
2804                    self.encode_thumb32_sub(rd, rn, 0)
2805                }
2806            }
2807
2808            ArmOp::Mov { rd, op2 } => {
2809                let rd_bits = reg_to_bits(rd) as u16;
2810
2811                if let Operand2::Imm(imm) = op2 {
2812                    // #498: the old test here was the SIGNED `*imm <= 255`,
2813                    // so a negative immediate (e.g. -1) fell into the 16-bit
2814                    // MOVS arm and encoded the wrong VALUE (#(imm & 0xFF) =
2815                    // #0xFF). A positive imm above 0xFFFF was equally wrong:
2816                    // MOVW truncates to 16 bits. Split on the UNSIGNED value:
2817                    // imm8 → MOVS, imm16 → MOVW, anything wider (negative or
2818                    // >0xFFFF) → the full-value MOVW+MOVT pair. No emitter
2819                    // produces the wide shape today (both selectors
2820                    // materialize wide constants as explicit Movw/Movt or
2821                    // Movw+Mvn), so this is byte-identical on shipped paths —
2822                    // it retires the latent wrong-value encodings the
2823                    // `estimator_encoder_agreement` oracle had pinned.
2824                    let uimm = *imm as u32;
2825                    if uimm <= 255 && rd_bits < 8 {
2826                        // MOVS Rd, #imm8 (16-bit): 0010 0 Rd imm8
2827                        let imm_bits = (*imm as u16) & 0xFF;
2828                        let instr: u16 = 0x2000 | (rd_bits << 8) | imm_bits;
2829                        Ok(instr.to_le_bytes().to_vec())
2830                    } else if uimm <= 0xFFFF {
2831                        // Use 32-bit MOVW for 16-bit immediates
2832                        self.encode_thumb32_movw(rd, uimm)
2833                    } else {
2834                        // Full 32-bit value: MOVW low16 + MOVT high16
2835                        let mut bytes = self.encode_thumb32_movw(rd, uimm & 0xFFFF)?;
2836                        bytes.extend(self.encode_thumb32_movt_raw(reg_to_bits(rd), uimm >> 16)?);
2837                        Ok(bytes)
2838                    }
2839                } else if let Operand2::Reg(rm) = op2 {
2840                    let rm_bits = reg_to_bits(rm) as u16;
2841                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
2842                    // D = Rd[3], Rd[2:0] in lower bits
2843                    let d_bit = (rd_bits >> 3) & 1;
2844                    let instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
2845                    Ok(instr.to_le_bytes().to_vec())
2846                } else {
2847                    let instr: u16 = 0xBF00; // NOP fallback
2848                    Ok(instr.to_le_bytes().to_vec())
2849                }
2850            }
2851
2852            ArmOp::Push { regs } => {
2853                // Thumb-2 PUSH encoding:
2854                // If all regs in R0-R7 + LR, use 16-bit: 1011 010 M rrrrrrrr
2855                // Otherwise use 32-bit: STMDB SP!, {regs} = 1110 1001 0010 1101 | 0M0 reglist(13)
2856                let mut reg_list: u16 = 0;
2857                let mut need_32bit = false;
2858                for r in regs {
2859                    let bit = reg_to_bits(r);
2860                    if bit >= 8 && *r != Reg::LR {
2861                        need_32bit = true;
2862                    }
2863                    reg_list |= 1 << bit;
2864                }
2865                if !need_32bit {
2866                    // 16-bit PUSH: 1011 010 M rrrrrrrr
2867                    let m_bit = if reg_list & (1 << 14) != 0 {
2868                        1u16
2869                    } else {
2870                        0u16
2871                    };
2872                    let low_regs = reg_list & 0xFF;
2873                    let instr: u16 = 0xB400 | (m_bit << 8) | low_regs;
2874                    Ok(instr.to_le_bytes().to_vec())
2875                } else {
2876                    // 32-bit STMDB SP!, {regs}: E92D | reglist(16)
2877                    let hw1: u16 = 0xE92D;
2878                    let hw2: u16 = reg_list;
2879                    let mut bytes = hw1.to_le_bytes().to_vec();
2880                    bytes.extend_from_slice(&hw2.to_le_bytes());
2881                    Ok(bytes)
2882                }
2883            }
2884
2885            ArmOp::Pop { regs } => {
2886                // Thumb-2 POP encoding:
2887                // If all regs in R0-R7 + PC, use 16-bit: 1011 110 P rrrrrrrr
2888                // Otherwise use 32-bit: LDMIA SP!, {regs} = 1110 1000 1011 1101 | PM0 reglist(13)
2889                let mut reg_list: u16 = 0;
2890                let mut need_32bit = false;
2891                for r in regs {
2892                    let bit = reg_to_bits(r);
2893                    if bit >= 8 && *r != Reg::PC {
2894                        need_32bit = true;
2895                    }
2896                    reg_list |= 1 << bit;
2897                }
2898                if !need_32bit {
2899                    // 16-bit POP: 1011 110 P rrrrrrrr
2900                    let p_bit = if reg_list & (1 << 15) != 0 {
2901                        1u16
2902                    } else {
2903                        0u16
2904                    };
2905                    let low_regs = reg_list & 0xFF;
2906                    let instr: u16 = 0xBC00 | (p_bit << 8) | low_regs;
2907                    Ok(instr.to_le_bytes().to_vec())
2908                } else {
2909                    // 32-bit LDMIA SP!, {regs}: E8BD | reglist(16)
2910                    let hw1: u16 = 0xE8BD;
2911                    let hw2: u16 = reg_list;
2912                    let mut bytes = hw1.to_le_bytes().to_vec();
2913                    bytes.extend_from_slice(&hw2.to_le_bytes());
2914                    Ok(bytes)
2915                }
2916            }
2917
2918            ArmOp::Nop => {
2919                let instr: u16 = 0xBF00; // NOP in Thumb-2
2920                Ok(instr.to_le_bytes().to_vec())
2921            }
2922
2923            ArmOp::Udf { imm } => {
2924                // UDF (Undefined) in Thumb-2: 16-bit encoding is 0xDE00 | imm8
2925                // This triggers UsageFault/HardFault, used for WASM traps
2926                let instr: u16 = 0xDE00 | (*imm as u16);
2927                let bytes = instr.to_le_bytes().to_vec();
2928                encoding_contracts::verify_thumb16(&bytes);
2929                Ok(bytes)
2930            }
2931
2932            // i64 support: ADDS, ADC, SUBS, SBC for register pair arithmetic
2933            // ADDS sets flags (carry), ADC uses carry from previous ADDS
2934            ArmOp::Adds { rd, rn, op2 } => {
2935                let rd_bits = reg_to_bits(rd) as u16;
2936                let rn_bits = reg_to_bits(rn) as u16;
2937
2938                if let Operand2::Reg(rm) = op2 {
2939                    let rm_bits = reg_to_bits(rm) as u16;
2940                    // 16-bit ADDS is R0-R7 only; i64 pair allocation can place
2941                    // operands in R8-R11, which would overflow the 3-bit fields
2942                    // and corrupt the operands (#178/#180 class). Guard and fall
2943                    // back to 32-bit ADDS.W for high registers.
2944                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2945                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2946                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2947                        Ok(instr.to_le_bytes().to_vec())
2948                    } else {
2949                        self.encode_thumb32_adds_reg_raw(
2950                            rd_bits as u32,
2951                            rn_bits as u32,
2952                            rm_bits as u32,
2953                        )
2954                    }
2955                } else {
2956                    // 32-bit Thumb-2 ADDS with immediate
2957                    self.encode_thumb32_adds(rd, rn, 0)
2958                }
2959            }
2960
2961            // ADC: Add with Carry (Thumb-2 32-bit)
2962            // ADC.W Rd, Rn, Rm: EB40 Rn | 00 Rd 00 Rm
2963            ArmOp::Adc { rd, rn, op2 } => {
2964                let rd_bits = reg_to_bits(rd);
2965                let rn_bits = reg_to_bits(rn);
2966
2967                if let Operand2::Reg(rm) = op2 {
2968                    let rm_bits = reg_to_bits(rm);
2969                    // ADC.W Rd, Rn, Rm (T2): 1110 1011 0100 Rn | 0 000 Rd 00 00 Rm
2970                    let hw1: u16 = (0xEB40 | rn_bits) as u16;
2971                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2972
2973                    let mut bytes = hw1.to_le_bytes().to_vec();
2974                    bytes.extend_from_slice(&hw2.to_le_bytes());
2975                    Ok(bytes)
2976                } else {
2977                    // ADC with immediate - use 32-bit encoding
2978                    let hw1: u16 = (0xF140 | rn_bits) as u16;
2979                    let hw2: u16 = (rd_bits << 8) as u16;
2980                    let mut bytes = hw1.to_le_bytes().to_vec();
2981                    bytes.extend_from_slice(&hw2.to_le_bytes());
2982                    Ok(bytes)
2983                }
2984            }
2985
2986            // SUBS sets flags (borrow), SBC uses borrow from previous SUBS
2987            ArmOp::Subs { rd, rn, op2 } => {
2988                let rd_bits = reg_to_bits(rd) as u16;
2989                let rn_bits = reg_to_bits(rn) as u16;
2990
2991                if let Operand2::Reg(rm) = op2 {
2992                    let rm_bits = reg_to_bits(rm) as u16;
2993                    // 16-bit SUBS is R0-R7 only; high-register i64 pair operands
2994                    // would overflow the 3-bit fields (#178/#180 class). Guard
2995                    // and fall back to 32-bit SUBS.W for high registers.
2996                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2997                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2998                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2999                        Ok(instr.to_le_bytes().to_vec())
3000                    } else {
3001                        self.encode_thumb32_subs_reg_raw(
3002                            rd_bits as u32,
3003                            rn_bits as u32,
3004                            rm_bits as u32,
3005                        )
3006                    }
3007                } else {
3008                    // 32-bit Thumb-2 SUBS with immediate
3009                    self.encode_thumb32_subs(rd, rn, 0)
3010                }
3011            }
3012
3013            // SBC: Subtract with Carry (Thumb-2 32-bit)
3014            // SBC.W Rd, Rn, Rm: EB60 Rn | 00 Rd 00 Rm
3015            ArmOp::Sbc { rd, rn, op2 } => {
3016                let rd_bits = reg_to_bits(rd);
3017                let rn_bits = reg_to_bits(rn);
3018
3019                if let Operand2::Reg(rm) = op2 {
3020                    let rm_bits = reg_to_bits(rm);
3021                    // SBC.W Rd, Rn, Rm (T2): 1110 1011 0110 Rn | 0 000 Rd 00 00 Rm
3022                    let hw1: u16 = (0xEB60 | rn_bits) as u16;
3023                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3024
3025                    let mut bytes = hw1.to_le_bytes().to_vec();
3026                    bytes.extend_from_slice(&hw2.to_le_bytes());
3027                    Ok(bytes)
3028                } else {
3029                    // SBC with immediate - use 32-bit encoding
3030                    let hw1: u16 = (0xF160 | rn_bits) as u16;
3031                    let hw2: u16 = (rd_bits << 8) as u16;
3032                    let mut bytes = hw1.to_le_bytes().to_vec();
3033                    bytes.extend_from_slice(&hw2.to_le_bytes());
3034                    Ok(bytes)
3035                }
3036            }
3037
3038            // === 32-bit Thumb-2 encodings ===
3039
3040            // SDIV: 11111011 1001 Rn 1111 Rd 1111 Rm
3041            ArmOp::Sdiv { rd, rn, rm } => {
3042                let rd_bits = reg_to_bits(rd);
3043                let rn_bits = reg_to_bits(rn);
3044                let rm_bits = reg_to_bits(rm);
3045                reg_bits_checked(rd_bits)?;
3046                reg_bits_checked(rn_bits)?;
3047                reg_bits_checked(rm_bits)?;
3048
3049                // Thumb-2 SDIV: FB90 F0F0 | Rn<<16 | Rd<<8 | Rm
3050                // First halfword: 1111 1011 1001 Rn = 0xFB90 | Rn
3051                // Second halfword: 1111 Rd 1111 Rm = 0xF0F0 | Rd<<8 | Rm
3052                let hw1: u16 = (0xFB90 | rn_bits) as u16;
3053                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
3054
3055                // Thumb-2 32-bit instructions: first halfword, then second halfword (little-endian each)
3056                let mut bytes = hw1.to_le_bytes().to_vec();
3057                bytes.extend_from_slice(&hw2.to_le_bytes());
3058                encoding_contracts::verify_thumb32(&bytes);
3059                Ok(bytes)
3060            }
3061
3062            // UDIV: 11111011 1011 Rn 1111 Rd 1111 Rm
3063            ArmOp::Udiv { rd, rn, rm } => {
3064                let rd_bits = reg_to_bits(rd);
3065                let rn_bits = reg_to_bits(rn);
3066                let rm_bits = reg_to_bits(rm);
3067                reg_bits_checked(rd_bits)?;
3068                reg_bits_checked(rn_bits)?;
3069                reg_bits_checked(rm_bits)?;
3070
3071                // Thumb-2 UDIV: FBB0 F0F0 | Rn<<16 | Rd<<8 | Rm
3072                let hw1: u16 = (0xFBB0 | rn_bits) as u16;
3073                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
3074
3075                let mut bytes = hw1.to_le_bytes().to_vec();
3076                bytes.extend_from_slice(&hw2.to_le_bytes());
3077                encoding_contracts::verify_thumb32(&bytes);
3078                Ok(bytes)
3079            }
3080
3081            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
3082                let rdlo_bits = reg_to_bits(rdlo);
3083                let rdhi_bits = reg_to_bits(rdhi);
3084                let rn_bits = reg_to_bits(rn);
3085                let rm_bits = reg_to_bits(rm);
3086                reg_bits_checked(rdlo_bits)?;
3087                reg_bits_checked(rdhi_bits)?;
3088                reg_bits_checked(rn_bits)?;
3089                reg_bits_checked(rm_bits)?;
3090
3091                // Thumb-2 UMULL: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm
3092                let hw1: u16 = (0xFBA0 | rn_bits) as u16;
3093                let hw2: u16 = ((rdlo_bits << 12) | (rdhi_bits << 8) | rm_bits) as u16;
3094
3095                let mut bytes = hw1.to_le_bytes().to_vec();
3096                bytes.extend_from_slice(&hw2.to_le_bytes());
3097                encoding_contracts::verify_thumb32(&bytes);
3098                Ok(bytes)
3099            }
3100
3101            // MUL (Thumb-2 32-bit): MUL Rd, Rn, Rm
3102            ArmOp::Mul { rd, rn, rm } => {
3103                let rd_bits = reg_to_bits(rd);
3104                let rn_bits = reg_to_bits(rn);
3105                let rm_bits = reg_to_bits(rm);
3106
3107                // Thumb-2 MUL: FB00 F000 | Rn | Rd<<8 | Rm
3108                // 11111011 0000 Rn | 1111 Rd 0000 Rm
3109                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3110                let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
3111
3112                let mut bytes = hw1.to_le_bytes().to_vec();
3113                bytes.extend_from_slice(&hw2.to_le_bytes());
3114                Ok(bytes)
3115            }
3116
3117            // MLS: Rd = Ra - Rn * Rm
3118            ArmOp::Mls { rd, rn, rm, ra } => {
3119                let rd_bits = reg_to_bits(rd);
3120                let rn_bits = reg_to_bits(rn);
3121                let rm_bits = reg_to_bits(rm);
3122                let ra_bits = reg_to_bits(ra);
3123
3124                // Thumb-2 MLS: FB00 Rn | Ra Rd 0001 Rm
3125                // 11111011 0000 Rn | Ra Rd 0001 Rm
3126                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3127                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | 0x10 | rm_bits) as u16;
3128
3129                let mut bytes = hw1.to_le_bytes().to_vec();
3130                bytes.extend_from_slice(&hw2.to_le_bytes());
3131                Ok(bytes)
3132            }
3133
3134            ArmOp::Mla { rd, rn, rm, ra } => {
3135                let rd_bits = reg_to_bits(rd);
3136                let rn_bits = reg_to_bits(rn);
3137                let rm_bits = reg_to_bits(rm);
3138                let ra_bits = reg_to_bits(ra);
3139
3140                // Thumb-2 MLA: FB00 Rn | Ra Rd 0000 Rm — same as MLS without the
3141                // bit-4 (0x10) op flag. rd = ra + rn*rm.
3142                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3143                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | rm_bits) as u16;
3144
3145                let mut bytes = hw1.to_le_bytes().to_vec();
3146                bytes.extend_from_slice(&hw2.to_le_bytes());
3147                Ok(bytes)
3148            }
3149
3150            // AND (Thumb-2 32-bit)
3151            ArmOp::And { rd, rn, op2 } => {
3152                if let Operand2::Reg(rm) = op2 {
3153                    let rd_bits = reg_to_bits(rd);
3154                    let rn_bits = reg_to_bits(rn);
3155                    let rm_bits = reg_to_bits(rm);
3156
3157                    // Thumb-2 AND register: EA00 Rn | 0 Rd 00 00 Rm
3158                    let hw1: u16 = (0xEA00 | rn_bits) as u16;
3159                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3160
3161                    let mut bytes = hw1.to_le_bytes().to_vec();
3162                    bytes.extend_from_slice(&hw2.to_le_bytes());
3163                    Ok(bytes)
3164                } else if let Operand2::Imm(imm) = op2 {
3165                    let rd_bits = reg_to_bits(rd);
3166                    let rn_bits = reg_to_bits(rn);
3167
3168                    // Thumb-2 AND.W immediate T1: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8.
3169                    // The i:imm3:imm8 field is a ThumbExpandImm modified immediate —
3170                    // encode it correctly (or error on an un-encodable value)
3171                    // rather than packing raw bits, closing the silent-miscompile
3172                    // class for AND alongside ORR/EOR (#251) / ADD/SUB (#253) /
3173                    // CMP (#255).
3174                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3175                        synth_core::Error::synthesis(
3176                            "AND immediate is not a valid ThumbExpandImm — materialize into a register",
3177                        )
3178                    })?;
3179                    let i_bit = (field >> 11) & 1;
3180                    let imm3 = (field >> 8) & 0x7;
3181                    let imm8 = field & 0xFF;
3182
3183                    let hw1: u16 = (0xF000 | (i_bit << 10) | rn_bits) as u16;
3184                    let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3185
3186                    let mut bytes = hw1.to_le_bytes().to_vec();
3187                    bytes.extend_from_slice(&hw2.to_le_bytes());
3188                    Ok(bytes)
3189                } else {
3190                    // RegShift variant - fallback to NOP
3191                    let instr: u16 = 0xBF00;
3192                    Ok(instr.to_le_bytes().to_vec())
3193                }
3194            }
3195
3196            // ORR (Thumb-2 32-bit)
3197            ArmOp::Orr { rd, rn, op2 } => {
3198                if let Operand2::Reg(rm) = op2 {
3199                    let rd_bits = reg_to_bits(rd);
3200                    let rn_bits = reg_to_bits(rn);
3201                    let rm_bits = reg_to_bits(rm);
3202
3203                    // Thumb-2 ORR: EA40 Rn | 0 Rd 00 00 Rm
3204                    let hw1: u16 = (0xEA40 | rn_bits) as u16;
3205                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3206
3207                    let mut bytes = hw1.to_le_bytes().to_vec();
3208                    bytes.extend_from_slice(&hw2.to_le_bytes());
3209                    Ok(bytes)
3210                } else if let Operand2::Imm(imm) = op2 {
3211                    // ORR.W immediate T1: 11110 i 0 0010 S Rn | 0 imm3 Rd imm8.
3212                    // Only the zero-extended byte form (imm <= 0xFF) is encoded;
3213                    // larger modified immediates need ThumbExpandImm — return an
3214                    // error rather than silently emit a NOP (Ok-or-Err, #180/#185).
3215                    let imm_val = *imm as u32;
3216                    if imm_val > 0xFF {
3217                        return Err(synth_core::Error::synthesis(
3218                            "ORR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3219                        ));
3220                    }
3221                    let rd_bits = reg_to_bits(rd);
3222                    let rn_bits = reg_to_bits(rn);
3223                    let hw1: u16 = (0xF040 | rn_bits) as u16;
3224                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3225                    let mut bytes = hw1.to_le_bytes().to_vec();
3226                    bytes.extend_from_slice(&hw2.to_le_bytes());
3227                    Ok(bytes)
3228                } else {
3229                    let instr: u16 = 0xBF00;
3230                    Ok(instr.to_le_bytes().to_vec())
3231                }
3232            }
3233
3234            // EOR (Thumb-2 32-bit)
3235            ArmOp::Eor { rd, rn, op2 } => {
3236                if let Operand2::Reg(rm) = op2 {
3237                    let rd_bits = reg_to_bits(rd);
3238                    let rn_bits = reg_to_bits(rn);
3239                    let rm_bits = reg_to_bits(rm);
3240
3241                    // Thumb-2 EOR: EA80 Rn | 0 Rd 00 00 Rm
3242                    let hw1: u16 = (0xEA80 | rn_bits) as u16;
3243                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3244
3245                    let mut bytes = hw1.to_le_bytes().to_vec();
3246                    bytes.extend_from_slice(&hw2.to_le_bytes());
3247                    Ok(bytes)
3248                } else if let Operand2::Imm(imm) = op2 {
3249                    // EOR.W immediate T1: 11110 i 0 0100 S Rn | 0 imm3 Rd imm8.
3250                    // Byte form only (imm <= 0xFF); larger needs ThumbExpandImm —
3251                    // error, not a silent NOP (Ok-or-Err, #180/#185).
3252                    let imm_val = *imm as u32;
3253                    if imm_val > 0xFF {
3254                        return Err(synth_core::Error::synthesis(
3255                            "EOR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3256                        ));
3257                    }
3258                    let rd_bits = reg_to_bits(rd);
3259                    let rn_bits = reg_to_bits(rn);
3260                    let hw1: u16 = (0xF080 | rn_bits) as u16;
3261                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3262                    let mut bytes = hw1.to_le_bytes().to_vec();
3263                    bytes.extend_from_slice(&hw2.to_le_bytes());
3264                    Ok(bytes)
3265                } else {
3266                    let instr: u16 = 0xBF00;
3267                    Ok(instr.to_le_bytes().to_vec())
3268                }
3269            }
3270
3271            // Shift operations (16-bit for low registers)
3272            ArmOp::Lsl { rd, rn, shift } => {
3273                let rd_bits = reg_to_bits(rd) as u16;
3274                let rn_bits = reg_to_bits(rn) as u16;
3275                let shift_bits = (*shift as u16) & 0x1F;
3276
3277                if rd_bits < 8 && rn_bits < 8 {
3278                    // LSLS Rd, Rm, #imm5 (16-bit): 0000 0 imm5 Rm Rd
3279                    let instr: u16 = (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3280                    Ok(instr.to_le_bytes().to_vec())
3281                } else {
3282                    // Use 32-bit encoding for high registers
3283                    self.encode_thumb32_shift(rd, rn, *shift, 0b00) // LSL type
3284                }
3285            }
3286
3287            ArmOp::Lsr { rd, rn, shift } => {
3288                let rd_bits = reg_to_bits(rd) as u16;
3289                let rn_bits = reg_to_bits(rn) as u16;
3290                let shift_bits = (*shift as u16) & 0x1F;
3291
3292                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3293                    // LSRS Rd, Rm, #imm5 (16-bit): 0000 1 imm5 Rm Rd
3294                    let instr: u16 = 0x0800 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3295                    Ok(instr.to_le_bytes().to_vec())
3296                } else {
3297                    self.encode_thumb32_shift(rd, rn, *shift, 0b01) // LSR type
3298                }
3299            }
3300
3301            ArmOp::Asr { rd, rn, shift } => {
3302                let rd_bits = reg_to_bits(rd) as u16;
3303                let rn_bits = reg_to_bits(rn) as u16;
3304                let shift_bits = (*shift as u16) & 0x1F;
3305
3306                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3307                    // ASRS Rd, Rm, #imm5 (16-bit): 0001 0 imm5 Rm Rd
3308                    let instr: u16 = 0x1000 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3309                    Ok(instr.to_le_bytes().to_vec())
3310                } else {
3311                    self.encode_thumb32_shift(rd, rn, *shift, 0b10) // ASR type
3312                }
3313            }
3314
3315            ArmOp::Ror { rd, rn, shift } => {
3316                // ROR doesn't have a 16-bit immediate form, use 32-bit
3317                self.encode_thumb32_shift(rd, rn, *shift, 0b11) // ROR type
3318            }
3319
3320            // Register-based shifts (Thumb-2 32-bit)
3321            // Encoding: 11111010 0xxS Rn 1111 Rd 0000 Rm
3322            // xx = shift type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
3323            ArmOp::LslReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b00),
3324            ArmOp::LsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b01),
3325            ArmOp::AsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b10),
3326            ArmOp::RorReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b11),
3327
3328            // RSB (Reverse Subtract): Rd = imm - Rn
3329            // Thumb-2 T2 encoding: 11110 i 0 1110 S Rn | 0 imm3 Rd imm8
3330            ArmOp::Rsb { rd, rn, imm } => {
3331                let rd_bits = reg_to_bits(rd);
3332                let rn_bits = reg_to_bits(rn);
3333
3334                // #681 class audit: the T2 `i:imm3:imm8` field is a
3335                // ThumbExpandImm modified immediate and RSB has NO plain-imm12
3336                // (T4-style) form — packing a raw value > 0xFF silently encodes
3337                // a different constant (#253/#255 class). All current emitters
3338                // use imm 32 (shift complement), which expands to itself, so
3339                // this gate is byte-identical for existing codegen.
3340                let field = try_thumb_expand_imm(*imm).ok_or_else(|| {
3341                    synth_core::Error::synthesis(
3342                        "RSB immediate is not a valid ThumbExpandImm — materialize into a register",
3343                    )
3344                })?;
3345                let i_bit = (field >> 11) & 1;
3346                let imm3 = (field >> 8) & 0x7;
3347                let imm8 = field & 0xFF;
3348
3349                // hw1: 11110 i 01110 0 Rn  (S=0)
3350                let hw1: u16 = (0xF1C0 | (i_bit << 10) | rn_bits) as u16;
3351                // hw2: 0 imm3 Rd imm8
3352                let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3353
3354                let mut bytes = hw1.to_le_bytes().to_vec();
3355                bytes.extend_from_slice(&hw2.to_le_bytes());
3356                Ok(bytes)
3357            }
3358
3359            // CLZ (Thumb-2 32-bit)
3360            ArmOp::Clz { rd, rm } => {
3361                let rd_bits = reg_to_bits(rd);
3362                let rm_bits = reg_to_bits(rm);
3363
3364                // Thumb-2 CLZ: FAB0 Rm | F8 Rd Rm
3365                // 11111010 1011 Rm | 1111 1000 Rd Rm
3366                let hw1: u16 = (0xFAB0 | rm_bits) as u16;
3367                let hw2: u16 = (0xF080 | (rd_bits << 8) | rm_bits) as u16;
3368
3369                let mut bytes = hw1.to_le_bytes().to_vec();
3370                bytes.extend_from_slice(&hw2.to_le_bytes());
3371                Ok(bytes)
3372            }
3373
3374            // RBIT (Thumb-2 32-bit)
3375            ArmOp::Rbit { rd, rm } => {
3376                let rd_bits = reg_to_bits(rd);
3377                let rm_bits = reg_to_bits(rm);
3378
3379                // Thumb-2 RBIT: FA90 Rm | F0 Rd A0 Rm
3380                // 11111010 1001 Rm | 1111 Rd 1010 Rm
3381                let hw1: u16 = (0xFA90 | rm_bits) as u16;
3382                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rm_bits) as u16;
3383
3384                let mut bytes = hw1.to_le_bytes().to_vec();
3385                bytes.extend_from_slice(&hw2.to_le_bytes());
3386                Ok(bytes)
3387            }
3388
3389            // SXTB (16-bit for low registers)
3390            ArmOp::Sxtb { rd, rm } => {
3391                let rd_bits = reg_to_bits(rd) as u16;
3392                let rm_bits = reg_to_bits(rm) as u16;
3393
3394                if rd_bits < 8 && rm_bits < 8 {
3395                    // SXTB Rd, Rm (16-bit): 1011 0010 01 Rm Rd
3396                    let instr: u16 = 0xB240 | (rm_bits << 3) | rd_bits;
3397                    Ok(instr.to_le_bytes().to_vec())
3398                } else {
3399                    // Thumb-2 SXTB.W: FA4F F(rd)80 (rm)
3400                    // 11111010 0100 1111 | 1111 Rd 10 rotate Rm
3401                    let rd_bits32 = rd_bits as u32;
3402                    let rm_bits32 = rm_bits as u32;
3403                    let hw1: u16 = 0xFA4F;
3404                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3405                    let mut bytes = hw1.to_le_bytes().to_vec();
3406                    bytes.extend_from_slice(&hw2.to_le_bytes());
3407                    Ok(bytes)
3408                }
3409            }
3410
3411            // SXTH (16-bit for low registers)
3412            ArmOp::Sxth { rd, rm } => {
3413                let rd_bits = reg_to_bits(rd) as u16;
3414                let rm_bits = reg_to_bits(rm) as u16;
3415
3416                if rd_bits < 8 && rm_bits < 8 {
3417                    // SXTH Rd, Rm (16-bit): 1011 0010 00 Rm Rd
3418                    let instr: u16 = 0xB200 | (rm_bits << 3) | rd_bits;
3419                    Ok(instr.to_le_bytes().to_vec())
3420                } else {
3421                    // Thumb-2 SXTH.W: FA0F F(rd)80 (rm)
3422                    // 11111010 0000 1111 | 1111 Rd 10 rotate Rm
3423                    let rd_bits32 = rd_bits as u32;
3424                    let rm_bits32 = rm_bits as u32;
3425                    let hw1: u16 = 0xFA0F;
3426                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3427                    let mut bytes = hw1.to_le_bytes().to_vec();
3428                    bytes.extend_from_slice(&hw2.to_le_bytes());
3429                    Ok(bytes)
3430                }
3431            }
3432
3433            // UXTB Rd,Rm — zero-extend byte (rd = rm & 0xff)
3434            ArmOp::Uxtb { rd, rm } => {
3435                let rd_bits = reg_to_bits(rd) as u16;
3436                let rm_bits = reg_to_bits(rm) as u16;
3437                if rd_bits < 8 && rm_bits < 8 {
3438                    // UXTB Rd, Rm (16-bit): 1011 0010 11 Rm Rd
3439                    let instr: u16 = 0xB2C0 | (rm_bits << 3) | rd_bits;
3440                    Ok(instr.to_le_bytes().to_vec())
3441                } else {
3442                    // Thumb-2 UXTB.W: FA5F F(rd)80 (rm)
3443                    let hw1: u16 = 0xFA5F;
3444                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3445                    let mut bytes = hw1.to_le_bytes().to_vec();
3446                    bytes.extend_from_slice(&hw2.to_le_bytes());
3447                    Ok(bytes)
3448                }
3449            }
3450
3451            // UXTH Rd,Rm — zero-extend halfword (rd = rm & 0xffff)
3452            ArmOp::Uxth { rd, rm } => {
3453                let rd_bits = reg_to_bits(rd) as u16;
3454                let rm_bits = reg_to_bits(rm) as u16;
3455                if rd_bits < 8 && rm_bits < 8 {
3456                    // UXTH Rd, Rm (16-bit): 1011 0010 10 Rm Rd
3457                    let instr: u16 = 0xB280 | (rm_bits << 3) | rd_bits;
3458                    Ok(instr.to_le_bytes().to_vec())
3459                } else {
3460                    // Thumb-2 UXTH.W: FA1F F(rd)80 (rm)
3461                    let hw1: u16 = 0xFA1F;
3462                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3463                    let mut bytes = hw1.to_le_bytes().to_vec();
3464                    bytes.extend_from_slice(&hw2.to_le_bytes());
3465                    Ok(bytes)
3466                }
3467            }
3468
3469            // CMP (can be 16-bit for low registers)
3470            ArmOp::Cmp { rn, op2 } => {
3471                let rn_bits = reg_to_bits(rn) as u16;
3472
3473                if let Operand2::Imm(imm) = op2 {
3474                    // Only use 16-bit encoding for non-negative immediates 0-255
3475                    // Negative immediates must use 32-bit encoding
3476                    if *imm >= 0 && *imm <= 255 && rn_bits < 8 {
3477                        // CMP Rn, #imm8 (16-bit): 0010 1 Rn imm8
3478                        let instr: u16 = 0x2800 | (rn_bits << 8) | (*imm as u16 & 0xFF);
3479                        Ok(instr.to_le_bytes().to_vec())
3480                    } else {
3481                        self.encode_thumb32_cmp_imm(rn, *imm as u32)
3482                    }
3483                } else if let Operand2::Reg(rm) = op2 {
3484                    let rm_bits = reg_to_bits(rm) as u16;
3485                    if rn_bits < 8 && rm_bits < 8 {
3486                        // CMP Rn, Rm (16-bit low): 0100 0010 10 Rm Rn
3487                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
3488                        Ok(instr.to_le_bytes().to_vec())
3489                    } else {
3490                        // CMP Rn, Rm (16-bit high): 0100 0101 N Rm Rn[2:0]
3491                        let n_bit = (rn_bits >> 3) & 1;
3492                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
3493                        Ok(instr.to_le_bytes().to_vec())
3494                    }
3495                } else {
3496                    let instr: u16 = 0xBF00;
3497                    Ok(instr.to_le_bytes().to_vec())
3498                }
3499            }
3500
3501            // CMN (Compare Negative) - computes Rn + op2 and sets flags
3502            // CMN Rn, #1 sets Z flag if Rn == -1 (since -1 + 1 = 0)
3503            ArmOp::Cmn { rn, op2 } => {
3504                let rn_bits = reg_to_bits(rn) as u16;
3505
3506                if let Operand2::Imm(imm) = op2 {
3507                    // CMN.W Rn, #imm (32-bit): i:imm3:imm8 is a ThumbExpandImm
3508                    // modified immediate (the field sits in imm3=hw2[14:12],
3509                    // imm8=hw2[7:0], i=hw1[10]). Encode it correctly, or error on
3510                    // an un-encodable value — replacing the old silent `0xBF00`
3511                    // NOP (the last of the silent-miscompile data-proc encoders).
3512                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3513                        synth_core::Error::synthesis(
3514                            "CMN immediate is not a valid ThumbExpandImm — materialize into a register",
3515                        )
3516                    })?;
3517                    let i_bit = (field >> 11) & 1;
3518                    let imm3 = (field >> 8) & 0x7;
3519                    let imm8 = field & 0xFF;
3520                    let hw1: u16 = (0xF110 | (i_bit << 10) as u16) | rn_bits;
3521                    let hw2: u16 = (imm3 << 12) as u16 | 0x0F00 | imm8 as u16;
3522                    let mut bytes = hw1.to_le_bytes().to_vec();
3523                    bytes.extend_from_slice(&hw2.to_le_bytes());
3524                    Ok(bytes)
3525                } else if let Operand2::Reg(rm) = op2 {
3526                    let rm_bits = reg_to_bits(rm) as u16;
3527                    // 16-bit CMN (T1) only encodes R0-R7; high registers overflow
3528                    // the 3-bit fields and corrupt the operands (#184, the #180
3529                    // class). CMN has no high-register 16-bit form, so fall back
3530                    // to 32-bit CMN.W (T2): EB10 Rn | 0F00 Rm (ADD.W with S=1 and
3531                    // Rd discarded as PC/1111).
3532                    if rn_bits < 8 && rm_bits < 8 {
3533                        // CMN Rn, Rm (16-bit): 0100 0010 11 Rm Rn
3534                        let instr: u16 = 0x42C0 | (rm_bits << 3) | rn_bits;
3535                        Ok(instr.to_le_bytes().to_vec())
3536                    } else {
3537                        let hw1: u16 = 0xEB10 | rn_bits;
3538                        let hw2: u16 = 0x0F00 | rm_bits;
3539                        let mut bytes = hw1.to_le_bytes().to_vec();
3540                        bytes.extend_from_slice(&hw2.to_le_bytes());
3541                        Ok(bytes)
3542                    }
3543                } else {
3544                    Ok(vec![0xBF, 0x00])
3545                }
3546            }
3547
3548            // LDR (can be 16-bit for simple cases)
3549            ArmOp::Ldr { rd, addr } => {
3550                let rd_bits = reg_to_bits(rd);
3551                let base_bits = reg_to_bits(&addr.base);
3552
3553                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3554                if let Some(offset_reg) = &addr.offset_reg {
3555                    let rm_bits = reg_to_bits(offset_reg);
3556
3557                    // If there's also an immediate offset, we need to ADD it first
3558                    if addr.offset != 0 {
3559                        // Use R12 (IP) as scratch to avoid clobbering the address register
3560                        // ADD R12, Rm, #offset; LDR Rd, [base, R12]
3561                        let scratch = Reg::R12;
3562                        let mut bytes =
3563                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3564                        bytes.extend(self.encode_thumb32_ldr_reg(rd, &addr.base, &scratch)?);
3565                        return Ok(bytes);
3566                    }
3567
3568                    // Simple register offset: LDR Rd, [Rn, Rm]
3569                    // 16-bit: only if Rd, Rn, Rm < R8
3570                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3571                        // LDR Rd, [Rn, Rm] (16-bit): 0101 100 Rm Rn Rd
3572                        let instr: u16 = 0x5800
3573                            | ((rm_bits as u16) << 6)
3574                            | ((base_bits as u16) << 3)
3575                            | (rd_bits as u16);
3576                        return Ok(instr.to_le_bytes().to_vec());
3577                    }
3578
3579                    // 32-bit register offset
3580                    return self.encode_thumb32_ldr_reg(rd, &addr.base, offset_reg);
3581                }
3582
3583                // Immediate offset mode [base, #imm]
3584                let offset = addr.offset as u32;
3585
3586                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3587                    // LDR Rd, [Rn, #imm5*4] (16-bit): 0110 1 imm5 Rn Rd
3588                    let imm5 = (offset >> 2) as u16;
3589                    let instr: u16 =
3590                        0x6800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3591                    Ok(instr.to_le_bytes().to_vec())
3592                } else {
3593                    self.encode_thumb32_ldr(rd, &addr.base, offset)
3594                }
3595            }
3596
3597            // STR (can be 16-bit for simple cases)
3598            ArmOp::Str { rd, addr } => {
3599                let rd_bits = reg_to_bits(rd);
3600                let base_bits = reg_to_bits(&addr.base);
3601
3602                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3603                if let Some(offset_reg) = &addr.offset_reg {
3604                    let rm_bits = reg_to_bits(offset_reg);
3605
3606                    // If there's also an immediate offset, we need to ADD it first
3607                    if addr.offset != 0 {
3608                        // Use R12 (IP) as scratch to avoid clobbering the address register
3609                        // ADD R12, Rm, #offset; STR Rd, [base, R12]
3610                        let scratch = Reg::R12;
3611                        let mut bytes =
3612                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3613                        bytes.extend(self.encode_thumb32_str_reg(rd, &addr.base, &scratch)?);
3614                        return Ok(bytes);
3615                    }
3616
3617                    // Simple register offset: STR Rd, [Rn, Rm]
3618                    // 16-bit: only if Rd, Rn, Rm < R8
3619                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3620                        // STR Rd, [Rn, Rm] (16-bit): 0101 000 Rm Rn Rd
3621                        let instr: u16 = 0x5000
3622                            | ((rm_bits as u16) << 6)
3623                            | ((base_bits as u16) << 3)
3624                            | (rd_bits as u16);
3625                        return Ok(instr.to_le_bytes().to_vec());
3626                    }
3627
3628                    // 32-bit register offset
3629                    return self.encode_thumb32_str_reg(rd, &addr.base, offset_reg);
3630                }
3631
3632                // Immediate offset mode [base, #imm]
3633                let offset = addr.offset as u32;
3634
3635                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3636                    // STR Rd, [Rn, #imm5*4] (16-bit): 0110 0 imm5 Rn Rd
3637                    let imm5 = (offset >> 2) as u16;
3638                    let instr: u16 =
3639                        0x6000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3640                    Ok(instr.to_le_bytes().to_vec())
3641                } else {
3642                    self.encode_thumb32_str(rd, &addr.base, offset)
3643                }
3644            }
3645
3646            // LDRB (Thumb-2)
3647            ArmOp::Ldrb { rd, addr } => {
3648                let rd_bits = reg_to_bits(rd);
3649                let base_bits = reg_to_bits(&addr.base);
3650
3651                if let Some(offset_reg) = &addr.offset_reg {
3652                    if addr.offset != 0 {
3653                        let scratch = Reg::R12;
3654                        let mut bytes =
3655                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3656                        bytes.extend(self.encode_thumb32_ldrb_reg(rd, &addr.base, &scratch)?);
3657                        return Ok(bytes);
3658                    }
3659                    return self.encode_thumb32_ldrb_reg(rd, &addr.base, offset_reg);
3660                }
3661
3662                let offset = addr.offset as u32;
3663                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3664                    // LDRB Rd, [Rn, #imm5] (16-bit): 0111 1 imm5 Rn Rd
3665                    let instr: u16 = 0x7800
3666                        | ((offset as u16) << 6)
3667                        | ((base_bits as u16) << 3)
3668                        | (rd_bits as u16);
3669                    Ok(instr.to_le_bytes().to_vec())
3670                } else {
3671                    self.encode_thumb32_ldrb_imm(rd, &addr.base, offset)
3672                }
3673            }
3674
3675            // LDRSB (Thumb-2)
3676            ArmOp::Ldrsb { rd, addr } => {
3677                let rd_bits = reg_to_bits(rd);
3678                let base_bits = reg_to_bits(&addr.base);
3679
3680                if let Some(offset_reg) = &addr.offset_reg {
3681                    if addr.offset != 0 {
3682                        let scratch = Reg::R12;
3683                        let mut bytes =
3684                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3685                        bytes.extend(self.encode_thumb32_ldrsb_reg(rd, &addr.base, &scratch)?);
3686                        return Ok(bytes);
3687                    }
3688                    return self.encode_thumb32_ldrsb_reg(rd, &addr.base, offset_reg);
3689                }
3690
3691                let offset = addr.offset as u32;
3692                // LDRSB has no 16-bit immediate form (only register)
3693                // For 16-bit reg form: only if Rd, Rn, Rm < R8
3694                if rd_bits < 8 && base_bits < 8 && offset == 0 {
3695                    // No immediate 16-bit encoding for LDRSB; use 32-bit
3696                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3697                } else {
3698                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3699                }
3700            }
3701
3702            // LDRH (Thumb-2)
3703            ArmOp::Ldrh { rd, addr } => {
3704                let rd_bits = reg_to_bits(rd);
3705                let base_bits = reg_to_bits(&addr.base);
3706
3707                if let Some(offset_reg) = &addr.offset_reg {
3708                    if addr.offset != 0 {
3709                        let scratch = Reg::R12;
3710                        let mut bytes =
3711                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3712                        bytes.extend(self.encode_thumb32_ldrh_reg(rd, &addr.base, &scratch)?);
3713                        return Ok(bytes);
3714                    }
3715                    return self.encode_thumb32_ldrh_reg(rd, &addr.base, offset_reg);
3716                }
3717
3718                let offset = addr.offset as u32;
3719                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3720                    // LDRH Rd, [Rn, #imm5*2] (16-bit): 1000 1 imm5 Rn Rd
3721                    let imm5 = (offset >> 1) as u16;
3722                    let instr: u16 =
3723                        0x8800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3724                    Ok(instr.to_le_bytes().to_vec())
3725                } else {
3726                    self.encode_thumb32_ldrh_imm(rd, &addr.base, offset)
3727                }
3728            }
3729
3730            // LDRSH (Thumb-2)
3731            ArmOp::Ldrsh { rd, addr } => {
3732                if let Some(offset_reg) = &addr.offset_reg {
3733                    if addr.offset != 0 {
3734                        let scratch = Reg::R12;
3735                        let mut bytes =
3736                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3737                        bytes.extend(self.encode_thumb32_ldrsh_reg(rd, &addr.base, &scratch)?);
3738                        return Ok(bytes);
3739                    }
3740                    return self.encode_thumb32_ldrsh_reg(rd, &addr.base, offset_reg);
3741                }
3742
3743                let offset = addr.offset as u32;
3744                self.encode_thumb32_ldrsh_imm(rd, &addr.base, offset)
3745            }
3746
3747            // STRB (Thumb-2)
3748            ArmOp::Strb { rd, addr } => {
3749                let rd_bits = reg_to_bits(rd);
3750                let base_bits = reg_to_bits(&addr.base);
3751
3752                if let Some(offset_reg) = &addr.offset_reg {
3753                    if addr.offset != 0 {
3754                        let scratch = Reg::R12;
3755                        let mut bytes =
3756                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3757                        bytes.extend(self.encode_thumb32_strb_reg(rd, &addr.base, &scratch)?);
3758                        return Ok(bytes);
3759                    }
3760                    return self.encode_thumb32_strb_reg(rd, &addr.base, offset_reg);
3761                }
3762
3763                let offset = addr.offset as u32;
3764                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3765                    // STRB Rd, [Rn, #imm5] (16-bit): 0111 0 imm5 Rn Rd
3766                    let instr: u16 = 0x7000
3767                        | ((offset as u16) << 6)
3768                        | ((base_bits as u16) << 3)
3769                        | (rd_bits as u16);
3770                    Ok(instr.to_le_bytes().to_vec())
3771                } else {
3772                    self.encode_thumb32_strb_imm(rd, &addr.base, offset)
3773                }
3774            }
3775
3776            // STRH (Thumb-2)
3777            ArmOp::Strh { rd, addr } => {
3778                let rd_bits = reg_to_bits(rd);
3779                let base_bits = reg_to_bits(&addr.base);
3780
3781                if let Some(offset_reg) = &addr.offset_reg {
3782                    if addr.offset != 0 {
3783                        let scratch = Reg::R12;
3784                        let mut bytes =
3785                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3786                        bytes.extend(self.encode_thumb32_strh_reg(rd, &addr.base, &scratch)?);
3787                        return Ok(bytes);
3788                    }
3789                    return self.encode_thumb32_strh_reg(rd, &addr.base, offset_reg);
3790                }
3791
3792                let offset = addr.offset as u32;
3793                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3794                    // STRH Rd, [Rn, #imm5*2] (16-bit): 1000 0 imm5 Rn Rd
3795                    let imm5 = (offset >> 1) as u16;
3796                    let instr: u16 =
3797                        0x8000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3798                    Ok(instr.to_le_bytes().to_vec())
3799                } else {
3800                    self.encode_thumb32_strh_imm(rd, &addr.base, offset)
3801                }
3802            }
3803
3804            // MemorySize (Thumb-2)
3805            ArmOp::MemorySize { rd } => {
3806                // LSR rd, R10, #16 — memory size in bytes / 65536 = pages
3807                // Thumb-2 16-bit: LSRS Rd, Rm, #imm5 — 0000 1 imm5 Rm Rd
3808                let rd_bits = reg_to_bits(rd);
3809                let r10_bits = reg_to_bits(&Reg::R10);
3810                if rd_bits < 8 && r10_bits < 8 {
3811                    let instr: u16 =
3812                        0x0800 | (16u16 << 6) | ((r10_bits as u16) << 3) | (rd_bits as u16);
3813                    Ok(instr.to_le_bytes().to_vec())
3814                } else {
3815                    // Thumb-2 32-bit LSR: 1110 1010 010 0 1111 | 0 imm3 Rd imm2 01 Rm
3816                    let imm5: u32 = 16;
3817                    let imm3 = (imm5 >> 2) & 0x7;
3818                    let imm2 = imm5 & 0x3;
3819                    let hw1: u16 = 0xEA4F;
3820                    let hw2: u16 =
3821                        ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | 0x10 | r10_bits) as u16;
3822                    let mut bytes = hw1.to_le_bytes().to_vec();
3823                    bytes.extend_from_slice(&hw2.to_le_bytes());
3824                    Ok(bytes)
3825                }
3826            }
3827
3828            // MemoryGrow (Thumb-2)
3829            ArmOp::MemoryGrow { rd, .. } => {
3830                // On embedded with fixed memory, always return -1 (failure)
3831                // MVN rd, #0 → MOV rd, #-1
3832                // Thumb-2 32-bit: MVN: 1111 0 i 0 0 0 1 1 0 1111 | 0 imm3 Rd imm8
3833                let rd_bits = reg_to_bits(rd);
3834                let hw1: u16 = 0xF06F; // MVN with i=0
3835                let hw2: u16 = (rd_bits << 8) as u16; // imm8=0 → ~0 = 0xFFFFFFFF = -1
3836                let mut bytes = hw1.to_le_bytes().to_vec();
3837                bytes.extend_from_slice(&hw2.to_le_bytes());
3838                Ok(bytes)
3839            }
3840
3841            // BX (16-bit)
3842            ArmOp::Bx { rm } => {
3843                let rm_bits = reg_to_bits(rm) as u16;
3844                // BX Rm (16-bit): 0100 0111 0 Rm 000
3845                let instr: u16 = 0x4700 | (rm_bits << 3);
3846                Ok(instr.to_le_bytes().to_vec())
3847            }
3848
3849            // BLX (16-bit) - Branch with Link and Exchange
3850            // BLX Rm: 0100 0111 1 Rm 000
3851            ArmOp::Blx { rm } => {
3852                let rm_bits = reg_to_bits(rm) as u16;
3853                let instr: u16 = 0x4780 | (rm_bits << 3);
3854                Ok(instr.to_le_bytes().to_vec())
3855            }
3856
3857            // CallIndirect - indirect function call via table lookup
3858            // table_index_reg contains the table index
3859            // Generates (#642): MOVW ip,#size [; MOVT]; CMP idx,ip; BLO +1;
3860            //                   UDF #0; LSL R12,idx,#2; LDR R12,[R11,R12]; BLX R12
3861            // #650, table_byte_offset != 0 (a non-zero table in the contiguous
3862            // R11 region): the pointer load becomes
3863            //                   ADD R12,R11,R12; LDR R12,[R12,#offset]
3864            // #664, null_check (the table has null slots, linked as ZERO
3865            // words): the loaded pointer is null-checked before the BLX —
3866            //                   CMP.W R12,#0; BNE +1; UDF #0
3867            // #676, type_check (heterogeneous table — runtime §4.4.8 type
3868            // check against the type-id sidecar at R11+off): after the
3869            // bounds guard —
3870            //                   LSL R12,idx,#2; ADD R12,R11,R12;
3871            //                   LDR R12,[R12,#type_off]; CMP.W R12,#id;
3872            //                   BEQ +1; UDF #0
3873            // (the dispatch tail then recomputes idx*4 — idx stays live).
3874            ArmOp::CallIndirect {
3875                rd: _,
3876                type_idx: _,
3877                table_index_reg,
3878                table_size,
3879                table_byte_offset,
3880                null_check,
3881                type_check,
3882            } => {
3883                let idx_reg = reg_to_bits(table_index_reg);
3884                let mut bytes = Vec::new();
3885
3886                // The expansion:
3887                // 1. Bounds guard (#642): trap (UDF #0, WASM Core §4.4.8) when
3888                //    index >= table size. Without it an out-of-bounds index
3889                //    reads past the table and BLXes whatever word lies there —
3890                //    an uncontrolled indirect branch instead of a trap.
3891                // 2. Multiplies index by 4 (function pointer size)
3892                // 3. Loads function pointer from table (table base in R11)
3893                // 4. Calls the function via BLX
3894                //
3895                // Table base setup must be done by caller/runtime. The type
3896                // check §4.4.8 also requires is discharged at COMPILE time:
3897                // the selector only emits this op after verifying the closed-
3898                // world property that every table entry's signature equals the
3899                // expected type (the raw code-pointer table carries no runtime
3900                // type ids to compare) — see the #642 selector guard.
3901
3902                // MOVW R12, #(size & 0xFFFF) — Thumb-2 T3:
3903                // 11110 i 100100 imm4 | 0 imm3 Rd imm8 (Rd=R12).
3904                let size_lo = *table_size & 0xFFFF;
3905                let hw1: u16 =
3906                    (0xF240 | (((size_lo >> 11) & 1) << 10) | ((size_lo >> 12) & 0xF)) as u16;
3907                let hw2: u16 =
3908                    ((((size_lo >> 8) & 0x7) << 12) | (12 << 8) | (size_lo & 0xFF)) as u16;
3909                bytes.extend_from_slice(&hw1.to_le_bytes());
3910                bytes.extend_from_slice(&hw2.to_le_bytes());
3911                // MOVT R12, #(size >> 16) — only when the table size exceeds
3912                // 16 bits (never in practice, but the guard must not compare
3913                // against a truncated size).
3914                let size_hi = *table_size >> 16;
3915                if size_hi != 0 {
3916                    let hw1: u16 =
3917                        (0xF2C0 | (((size_hi >> 11) & 1) << 10) | ((size_hi >> 12) & 0xF)) as u16;
3918                    let hw2: u16 =
3919                        ((((size_hi >> 8) & 0x7) << 12) | (12 << 8) | (size_hi & 0xFF)) as u16;
3920                    bytes.extend_from_slice(&hw1.to_le_bytes());
3921                    bytes.extend_from_slice(&hw2.to_le_bytes());
3922                }
3923                // CMP idx, R12 — 16-bit T2 (high-register capable):
3924                // 010001 01 N Rm(4) Rn(3), Rn full = N:Rn3.
3925                let cmp: u16 = (0x4500 | ((idx_reg & 8) << 4) | (12 << 3) | (idx_reg & 7)) as u16;
3926                bytes.extend_from_slice(&cmp.to_le_bytes());
3927                // BLO +1 insn (skip the UDF when index < size) — B<cond>.N
3928                // imm8=0: target = branch + 4. LO = unsigned lower.
3929                bytes.extend_from_slice(&0xD300u16.to_le_bytes());
3930                // UDF #0 — call_indirect out-of-bounds trap (same trap idiom as
3931                // the div-by-zero guards).
3932                bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3933
3934                // #676: runtime type check — ONLY for a heterogeneous table
3935                // (mixed signatures, closed-world verdict impossible). Load
3936                // the indexed slot's structural class id from the type-id
3937                // sidecar (`R11 + type_off + idx*4`; `type_off` = sidecar
3938                // base + this table's base offset, a compile-time constant)
3939                // and compare it against the expected type's class id — a
3940                // mismatch is the WASM Core §4.4.8 type trap. Null slots
3941                // carry the reserved id 0, so this compare subsumes the
3942                // #664 null trap (the selector passes `null_check: false`).
3943                // `None` emits NOTHING: every homogeneous table keeps the
3944                // pre-#676 bytes identical BY CONSTRUCTION. R12 stays the
3945                // only scratch (#212); the dispatch tail below recomputes
3946                // idx*4 — the index register is never clobbered here.
3947                if let Some((expected_id, type_off)) = type_check {
3948                    // RQ-61-IMMRANGE (#1072): compiled out in release, where
3949                    // the masks below silently TRUNCATE (id 256 compares as
3950                    // 0 — a NULL slot would pass the §4.4.8 check). The
3951                    // enforcement claim is DEMONSTRATED: the sole `Some`
3952                    // producer, `resolve_runtime_type_check`, loud-declines
3953                    // id > 255 / offset > 4095, tripped by
3954                    // `test_676_call_indirect_runtime_check_range_declines`
3955                    // (mutation-checked — see the A32 twin above).
3956                    debug_assert!(*expected_id <= 255, "selector enforces the CMP imm8 range");
3957                    debug_assert!(*type_off <= 4095, "selector enforces the LDR imm12 range");
3958                    // MOV.W R12, idx, LSL #2 (same encoding as the dispatch
3959                    // tail's index scale below).
3960                    bytes.extend_from_slice(&0xEA4Fu16.to_le_bytes());
3961                    bytes.extend_from_slice(
3962                        &(((0x0C00 | (0b10 << 6)) | idx_reg) as u16).to_le_bytes(),
3963                    );
3964                    // ADD.W R12, R11, R12 (the #650 base-add form).
3965                    bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
3966                    bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
3967                    // LDR.W R12, [R12, #type_off] — T3 LDR (immediate):
3968                    // 1111 1000 1101 Rn=1100 | Rt=1100 imm12.
3969                    bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
3970                    bytes.extend_from_slice(
3971                        &(0xC000u16 | (*type_off as u16 & 0x0FFF)).to_le_bytes(),
3972                    );
3973                    // CMP.W R12, #expected_id — T2 CMP (immediate), imm8
3974                    // (same form as the #664 null check's CMP.W R12, #0).
3975                    bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
3976                    bytes.extend_from_slice(
3977                        &(0x0F00u16 | (*expected_id as u16 & 0xFF)).to_le_bytes(),
3978                    );
3979                    // BEQ +1 insn (skip the UDF when the class id matches) —
3980                    // B<cond>.N imm8=0: target = branch + 4. EQ.
3981                    bytes.extend_from_slice(&0xD000u16.to_le_bytes());
3982                    // UDF #0 — the §4.4.8 type-mismatch trap (same idiom as
3983                    // the bounds guard above).
3984                    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3985                }
3986
3987                // LSL R12, idx_reg, #2 (multiply index by 4)
3988                // Thumb-2 MOV with shift: 11101010 010 S 1111 | 0 imm3 Rd imm2 type Rm
3989                // LSL: type=00 (bits 5:4), imm5=2 -> imm3=000, imm2=10 (bits 7:6)
3990                // #597: the shift amount was previously shifted into bits 5:4 —
3991                // the TYPE field — encoding `mov.w ip, rm, ASR #32`, which
3992                // destroyed the index and dispatched table entry 0 for every
3993                // call. imm2 lives at bits 7:6.
3994                let hw1: u16 = 0xEA4F_u16; // MOV.W R12, Rm, LSL #2
3995                let hw2: u16 = ((0x0C00 | (0b10 << 6)) | idx_reg) as u16;
3996                bytes.extend_from_slice(&hw1.to_le_bytes());
3997                bytes.extend_from_slice(&hw2.to_le_bytes());
3998
3999                if *table_byte_offset == 0 {
4000                    // Table 0 (base = R11 itself): the pre-#650 single-load
4001                    // form — a single-table module's bytes stay identical BY
4002                    // CONSTRUCTION.
4003                    // LDR R12, [R11, R12] - load function pointer
4004                    // Thumb-2 LDR (register): 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
4005                    // Rn=R11, Rt=R12, Rm=R12, imm2=00 (no shift)
4006                    let ldr_hw1: u16 = 0xF85B; // LDR.W Rt, [R11, Rm]
4007                    let ldr_hw2: u16 = 0xC00C; // Rt=R12, imm2=00, Rm=R12
4008                    bytes.extend_from_slice(&ldr_hw1.to_le_bytes());
4009                    bytes.extend_from_slice(&ldr_hw2.to_le_bytes());
4010                } else {
4011                    // #650: table N of the contiguous R11 region — fold the
4012                    // compile-time base offset into the pointer load via the
4013                    // LDR imm12 form (R12 stays the only scratch, per the
4014                    // #212 convention).
4015                    assert!(
4016                        *table_byte_offset <= 4095,
4017                        "call_indirect table base offset {table_byte_offset} exceeds \
4018                         LDR imm12 — the selector must have declined this (#650)"
4019                    );
4020                    // ADD.W R12, R11, R12 — T3 ADD (register):
4021                    // 11101011000 S=0 Rn=1011 | 0 imm3=000 Rd=1100 imm2=00 type=00 Rm=1100
4022                    bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
4023                    bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
4024                    // LDR.W R12, [R12, #offset] — T3 LDR (immediate):
4025                    // 1111 1000 1101 Rn=1100 | Rt=1100 imm12
4026                    bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
4027                    bytes.extend_from_slice(
4028                        &((0xC000u16) | (*table_byte_offset as u16 & 0x0FFF)).to_le_bytes(),
4029                    );
4030                }
4031
4032                // #664: null-slot trap — ONLY when the table image carries
4033                // null (uninitialized) slots, which the layout contract
4034                // requires to be linked as ZERO words. A fully-initialized
4035                // table skips this branch entirely, keeping the pre-#664
4036                // expansion byte-identical BY CONSTRUCTION (the #650
4037                // offset-0 trick).
4038                if *null_check {
4039                    // CMP.W R12, #0 — T2 CMP (immediate): 11110 i 0 1101 1
4040                    // Rn(4) | 0 imm3 1111 imm8, Rn=R12, imm=0.
4041                    bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
4042                    bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
4043                    // BNE +1 insn (skip the UDF when the pointer is non-null)
4044                    // — B<cond>.N imm8=0: target = branch + 4. NE.
4045                    bytes.extend_from_slice(&0xD100u16.to_le_bytes());
4046                    // UDF #0 — call_indirect null-funcref trap (WASM Core
4047                    // §4.4.8: calling an uninitialized element traps; same
4048                    // trap idiom as the bounds guard above).
4049                    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
4050                }
4051
4052                // BLX R12 (call function indirectly)
4053                // BLX Rm (16-bit): 0100 0111 1 Rm 000
4054                let blx: u16 = 0x47E0; // BLX R12
4055                bytes.extend_from_slice(&blx.to_le_bytes());
4056
4057                Ok(bytes)
4058            }
4059
4060            // Label pseudo-instruction: emits no machine code
4061            ArmOp::Label { .. } => Ok(Vec::new()),
4062
4063            // Conditional branch to label (generic) - offset 0, will be patched
4064            ArmOp::Bcc { cond, label: _ } => {
4065                use synth_synthesis::Condition;
4066                let cond_bits: u16 = match cond {
4067                    Condition::EQ => 0x0,
4068                    Condition::NE => 0x1,
4069                    Condition::HS => 0x2,
4070                    Condition::LO => 0x3,
4071                    Condition::HI => 0x8,
4072                    Condition::LS => 0x9,
4073                    Condition::GE => 0xA,
4074                    Condition::LT => 0xB,
4075                    Condition::GT => 0xC,
4076                    Condition::LE => 0xD,
4077                };
4078                // 16-bit B<cond> with offset 0: 1101 cond imm8
4079                let instr: u16 = 0xD000 | (cond_bits << 8);
4080                Ok(instr.to_le_bytes().to_vec())
4081            }
4082
4083            // Branch instructions
4084            ArmOp::B { label: _ } => {
4085                // Simplified: B.N with offset 0
4086                // For real usage, would need label resolution
4087                let instr: u16 = 0xE000; // B.N #0
4088                Ok(instr.to_le_bytes().to_vec())
4089            }
4090
4091            // BHS (Branch if Higher or Same) - used for bounds checking
4092            // Condition code: 0x2 (C set)
4093            ArmOp::Bhs { label: _ } => {
4094                // 16-bit B<cond> with offset 0: 1101 cond imm8
4095                // cond = 0x2 (HS)
4096                let instr: u16 = 0xD200; // BHS.N #0
4097                Ok(instr.to_le_bytes().to_vec())
4098            }
4099
4100            // BLO (Branch if Lower) - complementary to BHS
4101            // Condition code: 0x3 (C clear)
4102            ArmOp::Blo { label: _ } => {
4103                // 16-bit B<cond> with offset 0: 1101 cond imm8
4104                // cond = 0x3 (LO)
4105                let instr: u16 = 0xD300; // BLO.N #0
4106                Ok(instr.to_le_bytes().to_vec())
4107            }
4108
4109            // Branch with numeric offset (Thumb-2)
4110            // Thumb-2 B.W instruction: 32-bit with +-16MB range
4111            ArmOp::BOffset { offset } => {
4112                // offset is already the halfword displacement: (target - branch - 4) / 2
4113                // This is the raw encoded value, accounting for variable-length instructions
4114                let halfword_offset = *offset;
4115
4116                // 16-bit B.N encoding: 1110 0 imm11 (11-bit signed halfword offset)
4117                // Range: -1024 to +1022 halfwords
4118                if (-1024..=1022).contains(&halfword_offset) {
4119                    // 16-bit B.N encoding: 1110 0 imm11
4120                    let imm11 = (halfword_offset as u16) & 0x7FF;
4121                    let instr: u16 = 0xE000 | imm11;
4122                    Ok(instr.to_le_bytes().to_vec())
4123                } else {
4124                    // 32-bit B.W encoding for larger offsets
4125                    // First halfword: 1111 0 S imm10
4126                    // Second halfword: 10 J1 0 J2 imm11
4127                    // Total offset = SignExtend(S:I1:I2:imm10:imm11:0)
4128                    // where I1 = NOT(J1 XOR S), I2 = NOT(J2 XOR S)
4129
4130                    // The B.W (T4) encoding packs the signed offset as:
4131                    //   S:I1:I2:imm10:imm11:0  (25-bit signed, halfword-aligned)
4132                    // where J1 = NOT(I1 XOR S), J2 = NOT(I2 XOR S)
4133                    // Input halfword_offset already equals (target - PC - 4) / 2,
4134                    // so the full byte offset = halfword_offset << 1.
4135                    // The encoding fields split that 25-bit signed value (including the
4136                    // implicit trailing zero) as: S | imm10 | imm11
4137                    // with I1 = bit 23 and I2 = bit 22 of the signed offset.
4138                    let signed_offset = halfword_offset << 1; // byte offset
4139                    let s = if signed_offset < 0 { 1u32 } else { 0u32 };
4140                    let uoffset = signed_offset as u32;
4141                    let imm10 = (uoffset >> 12) & 0x3FF; // bits [21:12]
4142                    let imm11 = (uoffset >> 1) & 0x7FF; // bits [11:1]
4143                    let i1 = (uoffset >> 23) & 1; // bit 23
4144                    let i2 = (uoffset >> 22) & 1; // bit 22
4145                    let j1 = (!(i1 ^ s)) & 1; // J1 = NOT(I1 XOR S)
4146                    let j2 = (!(i2 ^ s)) & 1; // J2 = NOT(I2 XOR S)
4147
4148                    let hw1: u16 = (0xF000 | (s << 10) | imm10) as u16;
4149                    let hw2: u16 = (0x9000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4150
4151                    let mut bytes = hw1.to_le_bytes().to_vec();
4152                    bytes.extend_from_slice(&hw2.to_le_bytes());
4153                    Ok(bytes)
4154                }
4155            }
4156
4157            // Conditional branch with numeric offset (Thumb-2)
4158            ArmOp::BCondOffset { cond, offset } => {
4159                use synth_synthesis::Condition;
4160                let cond_bits: u16 = match cond {
4161                    Condition::EQ => 0x0,
4162                    Condition::NE => 0x1,
4163                    Condition::HS => 0x2,
4164                    Condition::LO => 0x3,
4165                    Condition::HI => 0x8,
4166                    Condition::LS => 0x9,
4167                    Condition::GE => 0xA,
4168                    Condition::LT => 0xB,
4169                    Condition::GT => 0xC,
4170                    Condition::LE => 0xD,
4171                };
4172
4173                // offset is already the halfword displacement: (target - branch - 4) / 2
4174                // This is the raw imm8 value for 16-bit B<cond> encoding
4175                let halfword_offset = *offset;
4176
4177                // 16-bit B<cond> encoding: 1101 cond imm8
4178                // Range: -256 to +254 halfwords (imm8 is sign-extended and shifted left 1)
4179                if (-128..=127).contains(&halfword_offset) {
4180                    let imm8 = (halfword_offset as u16) & 0xFF;
4181                    let instr: u16 = 0xD000 | (cond_bits << 8) | imm8;
4182                    Ok(instr.to_le_bytes().to_vec())
4183                } else {
4184                    // 32-bit B<cond>.W (encoding T3) for larger offsets
4185                    // First halfword: 1111 0 S cond(4) imm6
4186                    // Second halfword: 10 J1 0 J2 imm11
4187                    //
4188                    // Per ARMv7-M, the branch BYTE offset is
4189                    // SignExtend(S:J2:J1:imm6:imm11:'0'), i.e. the field value
4190                    // S:J2:J1:imm6:imm11 IS the signed 20-bit HALFWORD offset —
4191                    // imm11/imm6/J1/J2/S take `halfword_offset` bits [10:0],
4192                    // [16:11], 17, 18 and 19 directly (mirroring the T4
4193                    // unconditional arm above).
4194                    //
4195                    // #740: this arm previously packed `halfword_offset >> 1`
4196                    // into imm6:imm11 — HALVING the displacement — so every
4197                    // wide conditional branch (span > 254 bytes) landed at half
4198                    // its intended offset: gust_poll's loop-head `br_if` to an
4199                    // outer block end jumped mid-shape. Narrow (16-bit) B<cond>
4200                    // encodings were unaffected, which is why short-range CF
4201                    // fixtures never caught it.
4202                    if !(-(1 << 19)..(1 << 19)).contains(&halfword_offset) {
4203                        return Err(synth_core::Error::synthesis(format!(
4204                            "B<cond>.W (T3) halfword offset {halfword_offset} exceeds \
4205                             the signed 20-bit encoding range (±1 MB) — refusing to \
4206                             emit a truncated branch"
4207                        )));
4208                    }
4209                    let u = halfword_offset as u32;
4210                    let imm11 = u & 0x7FF; // halfword offset bits [10:0]
4211                    let imm6 = (u >> 11) & 0x3F; // bits [16:11]
4212                    let j1 = (u >> 17) & 1; // bit 17
4213                    let j2 = (u >> 18) & 1; // bit 18
4214                    let s = (u >> 19) & 1; // sign (range-checked above)
4215
4216                    let hw1: u16 = (0xF000 | (s << 10) | ((cond_bits as u32) << 6) | imm6) as u16;
4217                    let hw2: u16 = (0x8000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4218
4219                    let mut bytes = hw1.to_le_bytes().to_vec();
4220                    bytes.extend_from_slice(&hw2.to_le_bytes());
4221                    Ok(bytes)
4222                }
4223            }
4224
4225            ArmOp::Bl { label: _ } => {
4226                // BL is always 32-bit in Thumb-2, encoded here as a relocatable
4227                // placeholder; an R_ARM_THM_CALL relocation patches the target
4228                // (see arm_backend.rs). The placeholder must carry an embedded
4229                // addend of -4 so the relocation nets to exactly the symbol S.
4230                //
4231                // Thumb BL computes `target = (P + 4) + signed_offset`. Under
4232                // R_ARM_THM_CALL the linker resolves using the in-place addend;
4233                // a 0xF800 placeholder (addend 0) lands at S+4 — every call one
4234                // instruction past the callee entry (#174). The correct
4235                // placeholder is what `gas` emits for `bl <extern>`:
4236                //   f7ff fffe  ->  `bl <self>`  (S=1, J1=J2=1, imm = -4 addend),
4237                // i.e. hw1=0xF7FF, hw2=0xFFFE. This nets to S, not S+4.
4238                // (The earlier 0xD000 was worse still — a ~+0x600000 addend,
4239                // the garbage `bl c0000c` and "truncated to fit" of #167.)
4240                let hw1: u16 = 0xF7FF;
4241                let hw2: u16 = 0xFFFE;
4242                let mut bytes = hw1.to_le_bytes().to_vec();
4243                bytes.extend_from_slice(&hw2.to_le_bytes());
4244                Ok(bytes)
4245            }
4246
4247            // MVN
4248            ArmOp::Mvn { rd, op2 } => {
4249                if let Operand2::Reg(rm) = op2 {
4250                    let rd_bits = reg_to_bits(rd) as u16;
4251                    let rm_bits = reg_to_bits(rm) as u16;
4252
4253                    if rd_bits < 8 && rm_bits < 8 {
4254                        // MVNS Rd, Rm (16-bit): 0100 0011 11 Rm Rd
4255                        let instr: u16 = 0x43C0 | (rm_bits << 3) | rd_bits;
4256                        Ok(instr.to_le_bytes().to_vec())
4257                    } else {
4258                        // 32-bit MVN
4259                        let hw1: u16 = 0xEA6F_u16;
4260                        let hw2: u16 = ((reg_to_bits(rd) << 8) | reg_to_bits(rm)) as u16;
4261                        let mut bytes = hw1.to_le_bytes().to_vec();
4262                        bytes.extend_from_slice(&hw2.to_le_bytes());
4263                        Ok(bytes)
4264                    }
4265                } else {
4266                    let instr: u16 = 0xBF00;
4267                    Ok(instr.to_le_bytes().to_vec())
4268                }
4269            }
4270
4271            // MOVW - Move Wide (Thumb-2 32-bit)
4272            ArmOp::Movw { rd, imm16 } => {
4273                self.encode_thumb32_movw_raw(reg_to_bits(rd), *imm16 as u32)
4274            }
4275
4276            // MOVT - Move Top (Thumb-2 32-bit)
4277            ArmOp::Movt { rd, imm16 } => {
4278                self.encode_thumb32_movt_raw(reg_to_bits(rd), *imm16 as u32)
4279            }
4280
4281            // #237: symbol-relative MOVW/MOVT. Encode the addend's low/high 16
4282            // bits in place; the backend records an R_ARM_MOVW_ABS_NC /
4283            // R_ARM_MOVT_ABS relocation against `symbol`, so the linker adds the
4284            // symbol's final address to the in-place addend (REL semantics).
4285            ArmOp::MovwSym { rd, addend, .. } => {
4286                self.encode_thumb32_movw_raw(reg_to_bits(rd), (*addend as u32) & 0xffff)
4287            }
4288            ArmOp::MovtSym { rd, addend, .. } => {
4289                self.encode_thumb32_movt_raw(reg_to_bits(rd), ((*addend as u32) >> 16) & 0xffff)
4290            }
4291
4292            // #345: literal-pool address load — emit a PLACEHOLDER `LDR.W rd,
4293            // [pc, #0]` (U=1, imm12=0). The backend (arm_backend.rs) places the
4294            // 4-byte pool word at the end of the function, records the R_ARM_ABS32
4295            // relocation against `symbol+addend`, and patches the imm12 with the
4296            // real PC-relative distance once the pool offset is known.
4297            // Encoding T2: 1111 1000 1101 1111 | Rt(4) imm12(12), with the literal
4298            // base = Align(PC,4) and PC = address of this instruction + 4.
4299            ArmOp::LdrSym { rd, .. } => {
4300                let rt = reg_to_bits(rd) as u16;
4301                let hw1: u16 = 0xF8DF; // LDR.W (literal), U=1
4302                let hw2: u16 = rt << 12; // imm12 = 0 placeholder
4303                let mut bytes = Vec::with_capacity(4);
4304                bytes.extend_from_slice(&hw1.to_le_bytes());
4305                bytes.extend_from_slice(&hw2.to_le_bytes());
4306                Ok(bytes)
4307            }
4308
4309            // SetCond: Materialize condition flag into register (0 or 1)
4310            // Strategy: ITE <cond>; MOV Rd, #1; MOV Rd, #0
4311            // IMPORTANT: Must use ITE (If-Then-Else) because 16-bit Thumb MOV
4312            // always sets flags (MOVS). We need to evaluate the condition BEFORE
4313            // any MOV instruction clobbers the flags from CMP.
4314            ArmOp::SetCond { rd, cond } => {
4315                let rd_bits = reg_to_bits(rd) as u16;
4316
4317                // Condition code encoding for IT block
4318                use synth_synthesis::Condition;
4319                let cond_bits: u16 = match cond {
4320                    Condition::EQ => 0x0,
4321                    Condition::NE => 0x1,
4322                    Condition::LT => 0xB,
4323                    Condition::LE => 0xD,
4324                    Condition::GT => 0xC,
4325                    Condition::GE => 0xA,
4326                    Condition::LO => 0x3, // CC/LO (unsigned <)
4327                    Condition::LS => 0x9, // LS (unsigned <=)
4328                    Condition::HI => 0x8, // HI (unsigned >)
4329                    Condition::HS => 0x2, // CS/HS (unsigned >=)
4330                };
4331
4332                // ITE <cond>: encodes If-Then-Else block
4333                // The mask field depends on firstcond[0]:
4334                // - If firstcond[0] = 0: mask = 0xC for TE pattern (ITE EQ = BF0C)
4335                // - If firstcond[0] = 1: mask = 0x4 for TE pattern (ITE NE = BF14)
4336                let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4337                let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4338
4339                // Materialize 0/1 into Rd. The 16-bit MOVS (T1) encodes Rd in a
4340                // 3-bit field (bits[10:8]) — only R0–R7. For a high register
4341                // (R8–R12) `rd_bits << 8` overflows into bit 11 and silently
4342                // turns MOVS into CMP (00100 → 00101), corrupting the result
4343                // (this mis-materialized gale's `has_waiter`, so its `local.set`
4344                // stored a stale register → the binary-sem WAKE dispatch read
4345                // garbage). Use the 32-bit MOV.W (T2) for high registers, which
4346                // has a 4-bit Rd field. MOV.W with S=0 doesn't set flags, which
4347                // is fine inside the ITE (the materialized value is the result;
4348                // the flags are not consumed afterwards).
4349                let mut bytes = ite_instr.to_le_bytes().to_vec();
4350                let push_mov = |bytes: &mut Vec<u8>, imm: u16| {
4351                    if rd_bits <= 7 {
4352                        let m: u16 = 0x2000 | (rd_bits << 8) | imm; // 16-bit MOVS Rd,#imm
4353                        bytes.extend_from_slice(&m.to_le_bytes());
4354                    } else {
4355                        // 32-bit MOV.W Rd, #imm (T2): F04F | (Rd<<8) | imm8
4356                        let hw1: u16 = 0xF04F;
4357                        let hw2: u16 = (rd_bits << 8) | imm;
4358                        bytes.extend_from_slice(&hw1.to_le_bytes());
4359                        bytes.extend_from_slice(&hw2.to_le_bytes());
4360                    }
4361                };
4362                push_mov(&mut bytes, 1); // Then branch (condition true)  → 1
4363                push_mov(&mut bytes, 0); // Else branch (condition false) → 0
4364                Ok(bytes)
4365            }
4366
4367            // I64SetCond: Compare two i64 register pairs, result 0/1 in rd
4368            // EQ/NE: CMP lo,lo; IT EQ; CMPEQ hi,hi; ITE <cond>; MOV 1; MOV 0
4369            // LT: CMP lo,lo; SBCS rd,hi,hi; ITE LT; MOV 1; MOV 0
4370            // GT: CMP lo,lo (swapped); SBCS rd,hi,hi (swapped); ITE LT; MOV 1; MOV 0
4371            ArmOp::I64SetCond {
4372                rd,
4373                rn_lo,
4374                rn_hi,
4375                rm_lo,
4376                rm_hi,
4377                cond,
4378            } => {
4379                use synth_synthesis::Condition;
4380                let rd_bits = reg_to_bits(rd) as u16;
4381                let mut bytes = Vec::new();
4382
4383                // Helper: encode CMP Rn, Rm (16-bit)
4384                let encode_cmp_reg = |rn: &synth_synthesis::Reg,
4385                                      rm: &synth_synthesis::Reg|
4386                 -> Vec<u8> {
4387                    let rn_bits = reg_to_bits(rn) as u16;
4388                    let rm_bits = reg_to_bits(rm) as u16;
4389                    if rn_bits < 8 && rm_bits < 8 {
4390                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
4391                        instr.to_le_bytes().to_vec()
4392                    } else {
4393                        let n_bit = (rn_bits >> 3) & 1;
4394                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
4395                        instr.to_le_bytes().to_vec()
4396                    }
4397                };
4398
4399                // Helper: encode ITE <cond> (2 bytes)
4400                let encode_ite = |cond_bits: u16| -> Vec<u8> {
4401                    let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4402                    let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4403                    ite_instr.to_le_bytes().to_vec()
4404                };
4405
4406                // Helper: encode SetCond (ITE + MOV #1 + MOV #0) for given condition
4407                let encode_setcond = |cond_bits: u16, rd_bits: u16| -> Vec<u8> {
4408                    let mut b = encode_ite(cond_bits);
4409                    if rd_bits < 8 {
4410                        let mov_one: u16 = 0x2001 | (rd_bits << 8);
4411                        let mov_zero: u16 = 0x2000 | (rd_bits << 8);
4412                        b.extend_from_slice(&mov_one.to_le_bytes());
4413                        b.extend_from_slice(&mov_zero.to_le_bytes());
4414                    } else {
4415                        // #311: rd >= R8 — the 16-bit MOV imm8 form has a 3-bit
4416                        // rd field; rd_bits<<8 overflows into bit 11 and
4417                        // TRANSMUTES the MOV into CMP (0x2001|0x0800 = 0x2801 =
4418                        // CMP r0,#1): the boolean dies in the flags and the
4419                        // consumer reads a stale register. Use the 32-bit
4420                        // MOV.W (T2: F04F 0000|rd<<8|imm8) — IT-legal,
4421                        // flag-preserving. Same class as H-CODE-9 / #180.
4422                        for imm in [1u16, 0u16] {
4423                            let hw1: u16 = 0xF04F;
4424                            let hw2: u16 = (rd_bits << 8) | imm;
4425                            b.extend_from_slice(&hw1.to_le_bytes());
4426                            b.extend_from_slice(&hw2.to_le_bytes());
4427                        }
4428                    }
4429                    b
4430                };
4431
4432                match cond {
4433                    Condition::EQ | Condition::NE => {
4434                        // CMP rn_lo, rm_lo (compare low words)
4435                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4436
4437                        // IT EQ (execute next instruction only if Z=1)
4438                        let it_eq: u16 = 0xBF08; // IT EQ: cond=0000, mask=1000
4439                        bytes.extend_from_slice(&it_eq.to_le_bytes());
4440
4441                        // CMPEQ rn_hi, rm_hi (compare high words, only if low equal)
4442                        bytes.extend_from_slice(&encode_cmp_reg(rn_hi, rm_hi));
4443
4444                        // ITE <cond>; MOV rd, #1; MOV rd, #0
4445                        let cond_bits: u16 = match cond {
4446                            Condition::EQ => 0x0,
4447                            Condition::NE => 0x1,
4448                            _ => unreachable!(),
4449                        };
4450                        bytes.extend_from_slice(&encode_setcond(cond_bits, rd_bits));
4451                    }
4452
4453                    Condition::LT => {
4454                        // CMP rn_lo, rm_lo (sets C flag for borrow)
4455                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4456
4457                        // SBCS rd, rn_hi, rm_hi (subtract with carry, sets N,V flags)
4458                        // SBCS.W Rd, Rn, Rm: EB70 Rn | 0000 Rd 0000 Rm
4459                        let rn_hi_bits = reg_to_bits(rn_hi);
4460                        let rm_hi_bits = reg_to_bits(rm_hi);
4461                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4462                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4463                        bytes.extend_from_slice(&hw1.to_le_bytes());
4464                        bytes.extend_from_slice(&hw2.to_le_bytes());
4465
4466                        // ITE LT; MOV rd, #1; MOV rd, #0
4467                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4468                    }
4469
4470                    Condition::GT => {
4471                        // GT(a,b) = LT(b,a): swap operands
4472                        // CMP rm_lo, rn_lo (swapped)
4473                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4474
4475                        // SBCS rd, rm_hi, rn_hi (swapped)
4476                        let rm_hi_bits = reg_to_bits(rm_hi);
4477                        let rn_hi_bits = reg_to_bits(rn_hi);
4478                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4479                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4480                        bytes.extend_from_slice(&hw1.to_le_bytes());
4481                        bytes.extend_from_slice(&hw2.to_le_bytes());
4482
4483                        // ITE LT; MOV rd, #1; MOV rd, #0
4484                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4485                    }
4486
4487                    Condition::LE => {
4488                        // LE(a,b) = !GT(a,b): use GT logic but invert result
4489                        // GT(a,b) = LT(b,a): so we do CMP(b,a) and check LT, then invert
4490                        // CMP rm_lo, rn_lo (swapped, same as GT)
4491                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4492
4493                        // SBCS rd, rm_hi, rn_hi (swapped)
4494                        let rm_hi_bits = reg_to_bits(rm_hi);
4495                        let rn_hi_bits = reg_to_bits(rn_hi);
4496                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4497                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4498                        bytes.extend_from_slice(&hw1.to_le_bytes());
4499                        bytes.extend_from_slice(&hw2.to_le_bytes());
4500
4501                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT, so inverting GT result)
4502                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4503                    }
4504
4505                    Condition::GE => {
4506                        // GE(a,b) = !LT(a,b): use LT logic but invert result
4507                        // CMP rn_lo, rm_lo (same as LT)
4508                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4509
4510                        // SBCS rd, rn_hi, rm_hi (same as LT)
4511                        let rn_hi_bits = reg_to_bits(rn_hi);
4512                        let rm_hi_bits = reg_to_bits(rm_hi);
4513                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4514                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4515                        bytes.extend_from_slice(&hw1.to_le_bytes());
4516                        bytes.extend_from_slice(&hw2.to_le_bytes());
4517
4518                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT)
4519                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4520                    }
4521
4522                    // Unsigned comparisons - same instruction sequence, different conditions
4523                    Condition::LO => {
4524                        // LO (unsigned LT): CMP lo, SBCS hi, check C=0
4525                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4526                        let rn_hi_bits = reg_to_bits(rn_hi);
4527                        let rm_hi_bits = reg_to_bits(rm_hi);
4528                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4529                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4530                        bytes.extend_from_slice(&hw1.to_le_bytes());
4531                        bytes.extend_from_slice(&hw2.to_le_bytes());
4532                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4533                    }
4534
4535                    Condition::HI => {
4536                        // HI (unsigned GT): swap operands and check LO
4537                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4538                        let rm_hi_bits = reg_to_bits(rm_hi);
4539                        let rn_hi_bits = reg_to_bits(rn_hi);
4540                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4541                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4542                        bytes.extend_from_slice(&hw1.to_le_bytes());
4543                        bytes.extend_from_slice(&hw2.to_le_bytes());
4544                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4545                    }
4546
4547                    Condition::LS => {
4548                        // LS (unsigned LE): !(a > b) = !(HI), so do HI and invert
4549                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4550                        let rm_hi_bits = reg_to_bits(rm_hi);
4551                        let rn_hi_bits = reg_to_bits(rn_hi);
4552                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4553                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4554                        bytes.extend_from_slice(&hw1.to_le_bytes());
4555                        bytes.extend_from_slice(&hw2.to_le_bytes());
4556                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4557                    }
4558
4559                    Condition::HS => {
4560                        // HS (unsigned GE): !(a < b) = !(LO)
4561                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4562                        let rn_hi_bits = reg_to_bits(rn_hi);
4563                        let rm_hi_bits = reg_to_bits(rm_hi);
4564                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4565                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4566                        bytes.extend_from_slice(&hw1.to_le_bytes());
4567                        bytes.extend_from_slice(&hw2.to_le_bytes());
4568                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4569                    }
4570                }
4571
4572                Ok(bytes)
4573            }
4574
4575            // I64SetCondZ: Test if i64 register pair is zero, result 0/1 in rd
4576            // ORR.W rd, rn_lo, rn_hi; CMP rd, #0; ITE EQ; MOV 1; MOV 0
4577            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
4578                let rd_bits = reg_to_bits(rd);
4579                let rn_lo_bits = reg_to_bits(rn_lo);
4580                let rn_hi_bits = reg_to_bits(rn_hi);
4581                let mut bytes = Vec::new();
4582
4583                // ORR.W rd, rn_lo, rn_hi: EA40 rn_lo | 0000 rd 0000 rn_hi
4584                let hw1: u16 = (0xEA40 | rn_lo_bits) as u16;
4585                let hw2: u16 = ((rd_bits << 8) | rn_hi_bits) as u16;
4586                bytes.extend_from_slice(&hw1.to_le_bytes());
4587                bytes.extend_from_slice(&hw2.to_le_bytes());
4588
4589                // CMP rd, #0 — 16-bit form only for r0-r7 (3-bit rd field);
4590                // high registers take CMP.W (T2: F1B0|rn 0F00|imm8). This was
4591                // H-CODE-9: rd_bits<<8 overflowing the field compared the
4592                // WRONG register. Same hardening as the #311 SetCond fix.
4593                if rd_bits < 8 {
4594                    let cmp_instr: u16 = 0x2800 | ((rd_bits as u16) << 8);
4595                    bytes.extend_from_slice(&cmp_instr.to_le_bytes());
4596                } else {
4597                    let hw1: u16 = 0xF1B0 | (rd_bits as u16);
4598                    let hw2: u16 = 0x0F00;
4599                    bytes.extend_from_slice(&hw1.to_le_bytes());
4600                    bytes.extend_from_slice(&hw2.to_le_bytes());
4601                }
4602
4603                // ITE EQ; MOV rd, #1; MOV rd, #0 (32-bit MOV.W for rd >= R8,
4604                // #311 — see I64SetCond)
4605                let mask = 0xC_u16; // ITE EQ mask: firstcond[0]=0, mask=0xC
4606                let ite_instr: u16 = 0xBF00 | mask;
4607                bytes.extend_from_slice(&ite_instr.to_le_bytes());
4608                if rd_bits < 8 {
4609                    let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
4610                    let mov_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
4611                    bytes.extend_from_slice(&mov_one.to_le_bytes());
4612                    bytes.extend_from_slice(&mov_zero.to_le_bytes());
4613                } else {
4614                    for imm in [1u16, 0u16] {
4615                        let hw1: u16 = 0xF04F;
4616                        let hw2: u16 = ((rd_bits as u16) << 8) | imm;
4617                        bytes.extend_from_slice(&hw1.to_le_bytes());
4618                        bytes.extend_from_slice(&hw2.to_le_bytes());
4619                    }
4620                }
4621
4622                Ok(bytes)
4623            }
4624
4625            // I64Mul: 64-bit multiply using UMULL + MLA cross products
4626            // Formula: result = (a_lo * b_lo) + ((a_lo * b_hi + a_hi * b_lo) << 32)
4627            // Uses R12 as scratch register
4628            ArmOp::I64Mul {
4629                rd_lo,
4630                rd_hi,
4631                rn_lo,
4632                rn_hi,
4633                rm_lo,
4634                rm_hi,
4635            } => {
4636                let rd_lo_bits = reg_to_bits(rd_lo);
4637                let rd_hi_bits = reg_to_bits(rd_hi);
4638                let rn_lo_bits = reg_to_bits(rn_lo);
4639                let rn_hi_bits = reg_to_bits(rn_hi);
4640                let rm_lo_bits = reg_to_bits(rm_lo);
4641                let rm_hi_bits = reg_to_bits(rm_hi);
4642                let r12: u32 = 12; // IP scratch register
4643                let mut bytes = Vec::new();
4644
4645                // 1. MUL R12, rn_lo, rm_hi  (R12 = a_lo * b_hi)
4646                // Thumb-2 MUL: hw1=0xFB00|Rn, hw2=0xF000|(Rd<<8)|Rm
4647                let hw1: u16 = (0xFB00 | rn_lo_bits) as u16;
4648                let hw2: u16 = (0xF000 | (r12 << 8) | rm_hi_bits) as u16;
4649                bytes.extend_from_slice(&hw1.to_le_bytes());
4650                bytes.extend_from_slice(&hw2.to_le_bytes());
4651
4652                // 2. MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
4653                // Thumb-2 MLA: hw1=0xFB00|Rn, hw2=(Ra<<12)|(Rd<<8)|Rm
4654                let hw1: u16 = (0xFB00 | rn_hi_bits) as u16;
4655                let hw2: u16 = ((r12 << 12) | (r12 << 8) | rm_lo_bits) as u16;
4656                bytes.extend_from_slice(&hw1.to_le_bytes());
4657                bytes.extend_from_slice(&hw2.to_le_bytes());
4658
4659                // 3. UMULL rd_lo, rd_hi, rn_lo, rm_lo  (rd_lo:rd_hi = a_lo * b_lo)
4660                // Thumb-2 UMULL: hw1=0xFBA0|Rn, hw2=(RdLo<<12)|(RdHi<<8)|Rm
4661                let hw1: u16 = (0xFBA0 | rn_lo_bits) as u16;
4662                let hw2: u16 = ((rd_lo_bits << 12) | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4663                bytes.extend_from_slice(&hw1.to_le_bytes());
4664                bytes.extend_from_slice(&hw2.to_le_bytes());
4665
4666                // 4. ADD rd_hi, R12  (rd_hi += cross products)
4667                // 16-bit high reg ADD: 01000100 D Rm Rdn[2:0]
4668                let d_bit = (rd_hi_bits >> 3) & 1;
4669                let add_instr: u16 =
4670                    (0x4400 | (d_bit << 7) | (r12 << 3) | (rd_hi_bits & 0x7)) as u16;
4671                bytes.extend_from_slice(&add_instr.to_le_bytes());
4672
4673                Ok(bytes)
4674            }
4675
4676            // I64Shl: 64-bit shift left with branch for n<32 vs n>=32.
4677            //
4678            // #1048: the expansion must NEVER write its own input operands.
4679            // The pre-#1048 sequence masked the amount IN PLACE
4680            // (`AND.W rm_lo, rm_lo, #63`) and used the amount's home high
4681            // register `rm_hi` as scratch (`SUBS.W rm_hi, rm_lo, #32`, RSB,
4682            // LSR) — so re-reading the amount after the shift returned a
4683            // mangled value (amt=64 read back 0, amt=67 read back 3). The
4684            // rewrite uses R12 — encoder scratch, never allocatable (#212) —
4685            // as the ONLY temporary, re-deriving the masked amount from the
4686            // untouched rm_lo whenever a second live temp would otherwise be
4687            // needed. This matches the Rocq/SMT pseudo-op models
4688            // (I64ShlPseudo writes rd_lo/rd_hi ONLY), which were proven over
4689            // exactly this non-clobbering contract all along.
4690            ArmOp::I64Shl {
4691                rd_lo,
4692                rd_hi,
4693                rn_lo,
4694                rn_hi,
4695                rm_lo,
4696                rm_hi: _,
4697            } => {
4698                let rd_lo_bits = reg_to_bits(rd_lo);
4699                let rd_hi_bits = reg_to_bits(rd_hi);
4700                let rn_lo_bits = reg_to_bits(rn_lo);
4701                let rn_hi_bits = reg_to_bits(rn_hi);
4702                let rm_lo_bits = reg_to_bits(rm_lo);
4703                let r12: u32 = 12; // the only scratch — never allocatable
4704                let mut bytes = Vec::new();
4705
4706                // #1039 house style: refuse a destination that would collide
4707                // with an input still needed after the destination is first
4708                // written, loudly — never misassemble. rd_hi is written before
4709                // rn_lo and rm_lo are last read; the in-place form
4710                // rd == rn (select_default) has rd_hi == rn_hi and stays legal.
4711                if rd_hi_bits == rn_lo_bits || rd_hi_bits == rm_lo_bits {
4712                    return Err(synth_core::Error::synthesis(format!(
4713                        "I64Shl: rd_hi {rd_hi:?} aliases an input ({rn_lo:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4714                    )));
4715                }
4716
4717                // AND.W R12, rm_lo, #63  (n — the amount register is only READ)
4718                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4719                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4720                bytes.extend_from_slice(&hw1.to_le_bytes());
4721                bytes.extend_from_slice(&hw2.to_le_bytes());
4722
4723                // SUBS.W R12, R12, #32  (R12 = n-32, sets flags)
4724                let hw1: u16 = (0xF1B0 | r12) as u16;
4725                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4726                bytes.extend_from_slice(&hw1.to_le_bytes());
4727                bytes.extend_from_slice(&hw2.to_le_bytes());
4728
4729                // BPL .large (branch if n >= 32, offset = +14 halfwords)
4730                let bpl: u16 = 0xD50E;
4731                bytes.extend_from_slice(&bpl.to_le_bytes());
4732
4733                // --- Small shift (n < 32) ---
4734                // AND.W R12, rm_lo, #63  (n again — R12 held n-32)
4735                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4736                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4737                bytes.extend_from_slice(&hw1.to_le_bytes());
4738                bytes.extend_from_slice(&hw2.to_le_bytes());
4739
4740                // LSL.W rd_hi, rn_hi, R12  (hi << n; rn_hi's last read)
4741                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4742                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4743                bytes.extend_from_slice(&hw1.to_le_bytes());
4744                bytes.extend_from_slice(&hw2.to_le_bytes());
4745
4746                // RSB.W R12, R12, #32  (R12 = 32-n)
4747                let hw1: u16 = (0xF1C0 | r12) as u16;
4748                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4749                bytes.extend_from_slice(&hw1.to_le_bytes());
4750                bytes.extend_from_slice(&hw2.to_le_bytes());
4751
4752                // LSR.W R12, rn_lo, R12  (overflow = lo >> (32-n); n=0 gives
4753                // a register shift by 32 which yields 0 — exact)
4754                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4755                let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4756                bytes.extend_from_slice(&hw1.to_le_bytes());
4757                bytes.extend_from_slice(&hw2.to_le_bytes());
4758
4759                // ORR.W rd_hi, rd_hi, R12  (hi |= overflow bits from lo)
4760                let hw1: u16 = (0xEA40 | rd_hi_bits) as u16;
4761                let hw2: u16 = ((rd_hi_bits << 8) | r12) as u16;
4762                bytes.extend_from_slice(&hw1.to_le_bytes());
4763                bytes.extend_from_slice(&hw2.to_le_bytes());
4764
4765                // AND.W R12, rm_lo, #63  (n once more for the low half)
4766                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4767                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4768                bytes.extend_from_slice(&hw1.to_le_bytes());
4769                bytes.extend_from_slice(&hw2.to_le_bytes());
4770
4771                // LSL.W rd_lo, rn_lo, R12  (lo << n)
4772                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4773                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4774                bytes.extend_from_slice(&hw1.to_le_bytes());
4775                bytes.extend_from_slice(&hw2.to_le_bytes());
4776
4777                // B .done — `.done` is the END of the expansion, i.e. PAST the
4778                // large-shift arm's trailing zero-fill. #916: that zero-fill is
4779                // 1 halfword for a low rd_lo but 2 for R8-R12 (MOV.W), so the
4780                // displacement is DERIVED from its real width instead of the
4781                // hard-coded 0xE002 — widening the MOV without this would
4782                // overshoot `.done` and turn a data miscompile into a
4783                // control-flow one. Thumb `B` reads PC as its own address + 4
4784                // (= +2 halfwords), so imm11 = (large-arm halfwords) - 1.
4785                let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_lo_bits);
4786                let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4787                bytes.extend_from_slice(&b_done.to_le_bytes());
4788
4789                // --- Large shift (n >= 32) --- (R12 still holds n-32)
4790                // LSL.W rd_hi, rn_lo, R12  (hi = lo << (n-32))
4791                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4792                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4793                bytes.extend_from_slice(&hw1.to_le_bytes());
4794                bytes.extend_from_slice(&hw2.to_le_bytes());
4795
4796                // MOV rd_lo, #0 (#916: MOV.W for rd_lo >= R8). NOTE the order
4797                // is load-bearing — zeroing rd_lo BEFORE the LSL.W would
4798                // destroy rn_lo in the in-place case rd_lo == rn_lo, so this
4799                // cannot be reordered to dodge the displacement change.
4800                emit_thumb_zero_fill(&mut bytes, rd_lo_bits);
4801
4802                Ok(bytes) // 46 bytes (48 when rd_lo >= R8 takes MOV.W)
4803            }
4804
4805            // I64ShrU: 64-bit logical shift right with branch for n<32 vs
4806            // n>=32. #1048: R12-only scratch, operands never written — see
4807            // the I64Shl comment for the full rationale.
4808            ArmOp::I64ShrU {
4809                rd_lo,
4810                rd_hi,
4811                rn_lo,
4812                rn_hi,
4813                rm_lo,
4814                rm_hi: _,
4815            } => {
4816                let rd_lo_bits = reg_to_bits(rd_lo);
4817                let rd_hi_bits = reg_to_bits(rd_hi);
4818                let rn_lo_bits = reg_to_bits(rn_lo);
4819                let rn_hi_bits = reg_to_bits(rn_hi);
4820                let rm_lo_bits = reg_to_bits(rm_lo);
4821                let r12: u32 = 12; // the only scratch — never allocatable
4822                let mut bytes = Vec::new();
4823
4824                // #1039 house style: rd_lo is written before rn_hi and rm_lo
4825                // are last read — refuse the collision loudly. The in-place
4826                // form rd == rn (select_default) has rd_lo == rn_lo and stays
4827                // legal.
4828                if rd_lo_bits == rn_hi_bits || rd_lo_bits == rm_lo_bits {
4829                    return Err(synth_core::Error::synthesis(format!(
4830                        "I64ShrU: rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4831                    )));
4832                }
4833
4834                // AND.W R12, rm_lo, #63  (n — the amount register is only READ)
4835                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4836                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4837                bytes.extend_from_slice(&hw1.to_le_bytes());
4838                bytes.extend_from_slice(&hw2.to_le_bytes());
4839
4840                // SUBS.W R12, R12, #32  (R12 = n-32, sets flags)
4841                let hw1: u16 = (0xF1B0 | r12) as u16;
4842                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4843                bytes.extend_from_slice(&hw1.to_le_bytes());
4844                bytes.extend_from_slice(&hw2.to_le_bytes());
4845
4846                // BPL .large (+14 halfwords)
4847                let bpl: u16 = 0xD50E;
4848                bytes.extend_from_slice(&bpl.to_le_bytes());
4849
4850                // --- Small shift (n < 32) ---
4851                // AND.W R12, rm_lo, #63  (n again — R12 held n-32)
4852                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4853                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4854                bytes.extend_from_slice(&hw1.to_le_bytes());
4855                bytes.extend_from_slice(&hw2.to_le_bytes());
4856
4857                // LSR.W rd_lo, rn_lo, R12  (lo >> n; rn_lo's last read)
4858                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4859                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4860                bytes.extend_from_slice(&hw1.to_le_bytes());
4861                bytes.extend_from_slice(&hw2.to_le_bytes());
4862
4863                // RSB.W R12, R12, #32  (R12 = 32-n)
4864                let hw1: u16 = (0xF1C0 | r12) as u16;
4865                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4866                bytes.extend_from_slice(&hw1.to_le_bytes());
4867                bytes.extend_from_slice(&hw2.to_le_bytes());
4868
4869                // LSL.W R12, rn_hi, R12  (overflow = hi << (32-n); n=0 gives
4870                // a register shift by 32 which yields 0 — exact)
4871                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4872                let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4873                bytes.extend_from_slice(&hw1.to_le_bytes());
4874                bytes.extend_from_slice(&hw2.to_le_bytes());
4875
4876                // ORR.W rd_lo, rd_lo, R12  (lo |= overflow from hi)
4877                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4878                let hw2: u16 = ((rd_lo_bits << 8) | r12) as u16;
4879                bytes.extend_from_slice(&hw1.to_le_bytes());
4880                bytes.extend_from_slice(&hw2.to_le_bytes());
4881
4882                // AND.W R12, rm_lo, #63  (n once more for the high half)
4883                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4884                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4885                bytes.extend_from_slice(&hw1.to_le_bytes());
4886                bytes.extend_from_slice(&hw2.to_le_bytes());
4887
4888                // LSR.W rd_hi, rn_hi, R12  (hi >> n, logical)
4889                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4890                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4891                bytes.extend_from_slice(&hw1.to_le_bytes());
4892                bytes.extend_from_slice(&hw2.to_le_bytes());
4893
4894                // B .done — see I64Shl: `.done` is the END of the expansion,
4895                // past the trailing zero-fill, so the displacement is derived
4896                // from that zero-fill's real width (#916).
4897                let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_hi_bits);
4898                let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4899                bytes.extend_from_slice(&b_done.to_le_bytes());
4900
4901                // --- Large shift (n >= 32) --- (R12 still holds n-32)
4902                // LSR.W rd_lo, rn_hi, R12  (lo = hi >> (n-32))
4903                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4904                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4905                bytes.extend_from_slice(&hw1.to_le_bytes());
4906                bytes.extend_from_slice(&hw2.to_le_bytes());
4907
4908                // MOV rd_hi, #0 (#916: MOV.W for rd_hi >= R8). Order is
4909                // load-bearing: the LSR.W above reads rn_hi, which may BE
4910                // rd_hi in the in-place case.
4911                emit_thumb_zero_fill(&mut bytes, rd_hi_bits);
4912
4913                Ok(bytes) // 46 bytes (48 when rd_hi >= R8 takes MOV.W)
4914            }
4915
4916            // I64ShrS: 64-bit arithmetic shift right with branch for n<32 vs
4917            // n>=32. #1048: R12-only scratch, operands never written — see
4918            // the I64Shl comment for the full rationale.
4919            ArmOp::I64ShrS {
4920                rd_lo,
4921                rd_hi,
4922                rn_lo,
4923                rn_hi,
4924                rm_lo,
4925                rm_hi: _,
4926            } => {
4927                let rd_lo_bits = reg_to_bits(rd_lo);
4928                let rd_hi_bits = reg_to_bits(rd_hi);
4929                let rn_lo_bits = reg_to_bits(rn_lo);
4930                let rn_hi_bits = reg_to_bits(rn_hi);
4931                let rm_lo_bits = reg_to_bits(rm_lo);
4932                let r12: u32 = 12; // the only scratch — never allocatable
4933                let mut bytes = Vec::new();
4934
4935                // #1039 house style: rd_lo is written before rn_hi and rm_lo
4936                // are last read (on BOTH arms of the diamond — the large arm's
4937                // trailing `ASR rd_hi, rn_hi, #31` also reads rn_hi after
4938                // rd_lo is written). The in-place form rd == rn stays legal.
4939                if rd_lo_bits == rn_hi_bits || rd_lo_bits == rm_lo_bits {
4940                    return Err(synth_core::Error::synthesis(format!(
4941                        "I64ShrS: rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4942                    )));
4943                }
4944
4945                // AND.W R12, rm_lo, #63  (n — the amount register is only READ)
4946                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4947                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4948                bytes.extend_from_slice(&hw1.to_le_bytes());
4949                bytes.extend_from_slice(&hw2.to_le_bytes());
4950
4951                // SUBS.W R12, R12, #32  (R12 = n-32, sets flags)
4952                let hw1: u16 = (0xF1B0 | r12) as u16;
4953                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4954                bytes.extend_from_slice(&hw1.to_le_bytes());
4955                bytes.extend_from_slice(&hw2.to_le_bytes());
4956
4957                // BPL .large (+14 halfwords)
4958                let bpl: u16 = 0xD50E;
4959                bytes.extend_from_slice(&bpl.to_le_bytes());
4960
4961                // --- Small shift (n < 32) ---
4962                // AND.W R12, rm_lo, #63  (n again — R12 held n-32)
4963                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4964                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4965                bytes.extend_from_slice(&hw1.to_le_bytes());
4966                bytes.extend_from_slice(&hw2.to_le_bytes());
4967
4968                // LSR.W rd_lo, rn_lo, R12  (lo >> n, logical for lo word)
4969                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4970                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4971                bytes.extend_from_slice(&hw1.to_le_bytes());
4972                bytes.extend_from_slice(&hw2.to_le_bytes());
4973
4974                // RSB.W R12, R12, #32  (R12 = 32-n)
4975                let hw1: u16 = (0xF1C0 | r12) as u16;
4976                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4977                bytes.extend_from_slice(&hw1.to_le_bytes());
4978                bytes.extend_from_slice(&hw2.to_le_bytes());
4979
4980                // LSL.W R12, rn_hi, R12  (overflow = hi << (32-n); n=0 gives
4981                // a register shift by 32 which yields 0 — exact)
4982                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4983                let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4984                bytes.extend_from_slice(&hw1.to_le_bytes());
4985                bytes.extend_from_slice(&hw2.to_le_bytes());
4986
4987                // ORR.W rd_lo, rd_lo, R12  (lo |= overflow from hi)
4988                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4989                let hw2: u16 = ((rd_lo_bits << 8) | r12) as u16;
4990                bytes.extend_from_slice(&hw1.to_le_bytes());
4991                bytes.extend_from_slice(&hw2.to_le_bytes());
4992
4993                // AND.W R12, rm_lo, #63  (n once more for the high half)
4994                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4995                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4996                bytes.extend_from_slice(&hw1.to_le_bytes());
4997                bytes.extend_from_slice(&hw2.to_le_bytes());
4998
4999                // ASR.W rd_hi, rn_hi, R12  (hi >> n, arithmetic/sign-extending)
5000                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
5001                let hw2: u16 = (0xF000 | (rd_hi_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                // B .done (+3 halfwords, large shift is 8 bytes)
5006                let b_done: u16 = 0xE003;
5007                bytes.extend_from_slice(&b_done.to_le_bytes());
5008
5009                // --- Large shift (n >= 32) --- (R12 still holds n-32)
5010                // ASR.W rd_lo, rn_hi, R12  (lo = hi >>> (n-32))
5011                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
5012                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
5013                bytes.extend_from_slice(&hw1.to_le_bytes());
5014                bytes.extend_from_slice(&hw2.to_le_bytes());
5015
5016                // ASR.W rd_hi, rn_hi, #31  (hi = sign extension, all 0s or all 1s)
5017                // Thumb-2 ASR immediate: hw1=0xEA4F, hw2=imm3:Rd:imm2:10:Rm
5018                // imm5=31=11111 → imm3=111, imm2=11
5019                let hw1: u16 = 0xEA4F;
5020                let hw2: u16 = (0x7000 | (rd_hi_bits << 8) | 0x00E0 | rn_hi_bits) as u16;
5021                bytes.extend_from_slice(&hw1.to_le_bytes());
5022                bytes.extend_from_slice(&hw2.to_le_bytes());
5023
5024                Ok(bytes) // Total: 48 bytes
5025            }
5026
5027            // I64Rotl: 64-bit rotate left (#610 rewrite).
5028            // For n < 32: new_hi = (hi << n) | (lo >> (32-n)), new_lo = (lo << n) | (hi >> (32-n))
5029            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
5030            //
5031            // Fixed-reg core: value in R0:R1, amount in R2, scratch R3 + R12
5032            // (all four saved/marshaled by the #610 fixed-ABI wrapper; the
5033            // pre-#610 expansion wrote through the selector's registers with
5034            // colliding R3/R4 scratch and restored the saved R4 OVER the
5035            // result). Relies on ARM register-shift semantics: amounts >= 32
5036            // yield 0 for LSL/LSR, which makes n = 0 and n = 32 exact.
5037            ArmOp::I64Rotl {
5038                rdlo,
5039                rdhi,
5040                rnlo,
5041                rnhi,
5042                shift,
5043            } => {
5044                let mut bytes = Vec::new();
5045                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
5046
5047                let core: [u16; 35] = [
5048                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
5049                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
5050                    0xD50E, //         BPL    .large        (n >= 32)
5051                    // --- small rotation (n < 32) ---
5052                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
5053                    0xFA20, 0xFC03, // LSR.W  R12, R0, R3   (lo >> (32-n))
5054                    0xFA21, 0xF303, // LSR.W  R3, R1, R3    (hi >> (32-n))
5055                    0xFA01, 0xF102, // LSL.W  R1, R1, R2    (hi << n)
5056                    0xEA41, 0x010C, // ORR.W  R1, R1, R12   (new_hi)
5057                    0xFA00, 0xF002, // LSL.W  R0, R0, R2    (lo << n)
5058                    0xEA40, 0x0003, // ORR.W  R0, R0, R3    (new_lo)
5059                    0xE00E, //         B      .done
5060                    // --- large rotation (n >= 32), R3 = m = n-32 ---
5061                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
5062                    0xFA21, 0xFC02, // LSR.W  R12, R1, R2   (hi >> (64-n))
5063                    0xFA20, 0xF202, // LSR.W  R2, R0, R2    (lo >> (64-n))
5064                    0xFA00, 0xF003, // LSL.W  R0, R0, R3    (lo << m)
5065                    0xFA01, 0xF103, // LSL.W  R1, R1, R3    (hi << m)
5066                    0xEA40, 0x0C0C, // ORR.W  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
5067                    0xEA41, 0x0002, // ORR.W  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
5068                    0x4661, //         MOV    R1, R12       (new_hi into place)
5069                            // .done: result in R0:R1
5070                ];
5071                for hw in core {
5072                    bytes.extend_from_slice(&hw.to_le_bytes());
5073                }
5074
5075                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5076                Ok(bytes) // Total: 102 bytes
5077            }
5078
5079            // I64Rotr: 64-bit rotate right (#610 rewrite).
5080            // For n < 32: new_lo = (lo >> n) | (hi << (32-n)), new_hi = (hi >> n) | (lo << (32-n))
5081            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
5082            //
5083            // Same fixed-reg core contract as I64Rotl: value in R0:R1, amount
5084            // in R2, scratch R3 + R12, all covered by the fixed-ABI wrapper.
5085            ArmOp::I64Rotr {
5086                rdlo,
5087                rdhi,
5088                rnlo,
5089                rnhi,
5090                shift,
5091            } => {
5092                let mut bytes = Vec::new();
5093                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
5094
5095                let core: [u16; 35] = [
5096                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
5097                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
5098                    0xD50E, //         BPL    .large        (n >= 32)
5099                    // --- small rotation (n < 32) ---
5100                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
5101                    0xFA01, 0xFC03, // LSL.W  R12, R1, R3   (hi << (32-n))
5102                    0xFA00, 0xF303, // LSL.W  R3, R0, R3    (lo << (32-n))
5103                    0xFA20, 0xF002, // LSR.W  R0, R0, R2    (lo >> n)
5104                    0xEA40, 0x000C, // ORR.W  R0, R0, R12   (new_lo)
5105                    0xFA21, 0xF102, // LSR.W  R1, R1, R2    (hi >> n)
5106                    0xEA41, 0x0103, // ORR.W  R1, R1, R3    (new_hi)
5107                    0xE00E, //         B      .done
5108                    // --- large rotation (n >= 32), R3 = m = n-32 ---
5109                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
5110                    0xFA00, 0xFC02, // LSL.W  R12, R0, R2   (lo << (64-n))
5111                    0xFA01, 0xF202, // LSL.W  R2, R1, R2    (hi << (64-n))
5112                    0xFA21, 0xF103, // LSR.W  R1, R1, R3    (hi >> m)
5113                    0xEA41, 0x0C0C, // ORR.W  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
5114                    0xFA20, 0xF103, // LSR.W  R1, R0, R3    (lo >> m)
5115                    0xEA41, 0x0102, // ORR.W  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
5116                    0x4660, //         MOV    R0, R12       (new_lo into place)
5117                            // .done: result in R0:R1
5118                ];
5119                for hw in core {
5120                    bytes.extend_from_slice(&hw.to_le_bytes());
5121                }
5122
5123                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5124                Ok(bytes) // Total: 102 bytes
5125            }
5126
5127            // I64Clz: Count leading zeros in 64-bit value
5128            // If hi != 0: result = CLZ(hi)
5129            // If hi == 0: result = 32 + CLZ(lo)
5130            //
5131            // Layout (using CMP+BNE approach for consistency):
5132            // 0: CMP.W rnhi, #0 (4 bytes)
5133            // 4: BEQ .hi_zero (2 bytes) - branch forward to offset 14
5134            // 6: CLZ.W rd, rnhi (4 bytes)
5135            // 10: B .done (2 bytes) - branch forward to offset 22
5136            // 12: NOP (2 bytes) - padding for alignment
5137            // 14: .hi_zero: CLZ.W rd, rnlo (4 bytes)
5138            // 18: ADD.W rd, rd, #32 (4 bytes)
5139            // 22: .done
5140            ArmOp::I64Clz { rd, rnlo, rnhi } => {
5141                let rd_bits = reg_to_bits(rd);
5142                let rn_lo_bits = reg_to_bits(rnlo);
5143                let rn_hi_bits = reg_to_bits(rnhi);
5144                let mut bytes = Vec::new();
5145
5146                // CMP.W rnhi, #0 (4 bytes at offset 0)
5147                let hw1: u16 = (0xF1B0 | rn_hi_bits) as u16;
5148                let hw2: u16 = 0x0F00;
5149                bytes.extend_from_slice(&hw1.to_le_bytes());
5150                bytes.extend_from_slice(&hw2.to_le_bytes());
5151
5152                // BEQ .hi_zero (2 bytes at offset 4)
5153                // PC = 4 + 4 = 8, target = 14, offset = 6, imm8 = 3
5154                let beq: u16 = 0xD003;
5155                bytes.extend_from_slice(&beq.to_le_bytes());
5156
5157                // CLZ.W rd, rnhi (4 bytes at offset 6)
5158                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5159                let hw1: u16 = (0xFAB0 | rn_hi_bits) as u16;
5160                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_hi_bits) as u16;
5161                bytes.extend_from_slice(&hw1.to_le_bytes());
5162                bytes.extend_from_slice(&hw2.to_le_bytes());
5163
5164                // B .done (2 bytes at offset 10)
5165                // PC = 10 + 4 = 14, target = 22, offset = 8, imm11 = 4
5166                let b_done: u16 = 0xE004;
5167                bytes.extend_from_slice(&b_done.to_le_bytes());
5168
5169                // NOP (2 bytes at offset 12) - padding
5170                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5171
5172                // .hi_zero: (offset 14)
5173                // CLZ.W rd, rnlo (4 bytes)
5174                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5175                let hw1: u16 = (0xFAB0 | rn_lo_bits) as u16;
5176                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_lo_bits) as u16;
5177                bytes.extend_from_slice(&hw1.to_le_bytes());
5178                bytes.extend_from_slice(&hw2.to_le_bytes());
5179
5180                // ADD.W rd, rd, #32 (4 bytes at offset 18)
5181                let hw1: u16 = (0xF100 | rd_bits) as u16;
5182                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5183                bytes.extend_from_slice(&hw1.to_le_bytes());
5184                bytes.extend_from_slice(&hw2.to_le_bytes());
5185
5186                // .done: (offset 22 — the end of the expansion)
5187                //
5188                // #1048: the former trailing hi-word clear (`MOV rnhi, #0`)
5189                // is GONE. It was aimed at the RESULT's high half but wrote
5190                // the OPERAND's home high register — a real executed
5191                // miscompile on the direct selector, which allocates a fresh
5192                // destination pair and zeroes its own dst_hi, leaving the
5193                // operand's hi limb destroyed for any later re-read. The
5194                // Rocq/SMT models of I64ClzPseudo always said "writes rd
5195                // ONLY"; the callers that relied on the implicit clear
5196                // (select_default, optimizer_bridge) now emit their own
5197                // explicit hi-zero op. `B .done` above targets offset 22 =
5198                // past-the-end, `BEQ` targets offset 14 — no displacement
5199                // moves.
5200
5201                Ok(bytes) // 22 bytes, register-independent
5202            }
5203
5204            // I64Ctz: Count trailing zeros in 64-bit value
5205            // If lo != 0: result = CTZ(lo) = CLZ(RBIT(lo))
5206            // If lo == 0: result = 32 + CTZ(hi) = 32 + CLZ(RBIT(hi))
5207            //
5208            // Layout:
5209            // 0: CMP.W rnlo, #0 (4 bytes)
5210            // 4: BEQ .lo_zero (2 bytes) - branch to offset 18
5211            // 6: RBIT.W rd, rnlo (4 bytes)
5212            // 10: CLZ.W rd, rd (4 bytes)
5213            // 14: B .done (2 bytes) - branch to offset 30
5214            // 16: NOP (2 bytes) - padding
5215            // 18: .lo_zero: RBIT.W rd, rnhi (4 bytes)
5216            // 22: CLZ.W rd, rd (4 bytes)
5217            // 26: ADD.W rd, rd, #32 (4 bytes)
5218            // 30: .done
5219            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
5220                let rd_bits = reg_to_bits(rd);
5221                let rn_lo_bits = reg_to_bits(rnlo);
5222                let rn_hi_bits = reg_to_bits(rnhi);
5223                let mut bytes = Vec::new();
5224
5225                // CMP.W rnlo, #0 (4 bytes at offset 0)
5226                let hw1: u16 = (0xF1B0 | rn_lo_bits) as u16;
5227                let hw2: u16 = 0x0F00;
5228                bytes.extend_from_slice(&hw1.to_le_bytes());
5229                bytes.extend_from_slice(&hw2.to_le_bytes());
5230
5231                // BEQ .lo_zero (2 bytes at offset 4)
5232                // PC = 4 + 4 = 8, target = 18, offset = 10, imm8 = 5
5233                let beq: u16 = 0xD005;
5234                bytes.extend_from_slice(&beq.to_le_bytes());
5235
5236                // RBIT.W rd, rnlo (4 bytes at offset 6)
5237                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5238                let hw1: u16 = (0xFA90 | rn_lo_bits) as u16;
5239                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_lo_bits) as u16;
5240                bytes.extend_from_slice(&hw1.to_le_bytes());
5241                bytes.extend_from_slice(&hw2.to_le_bytes());
5242
5243                // CLZ.W rd, rd (4 bytes at offset 10)
5244                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5245                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5246                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5247                bytes.extend_from_slice(&hw1.to_le_bytes());
5248                bytes.extend_from_slice(&hw2.to_le_bytes());
5249
5250                // B .done (2 bytes at offset 14)
5251                // PC = 14 + 4 = 18, target = 30, offset = 12, imm11 = 6
5252                let b_done: u16 = 0xE006;
5253                bytes.extend_from_slice(&b_done.to_le_bytes());
5254
5255                // NOP (2 bytes at offset 16) - padding
5256                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5257
5258                // .lo_zero: (offset 18)
5259                // RBIT.W rd, rnhi (4 bytes)
5260                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5261                let hw1: u16 = (0xFA90 | rn_hi_bits) as u16;
5262                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_hi_bits) as u16;
5263                bytes.extend_from_slice(&hw1.to_le_bytes());
5264                bytes.extend_from_slice(&hw2.to_le_bytes());
5265
5266                // CLZ.W rd, rd (4 bytes at offset 22)
5267                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5268                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5269                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5270                bytes.extend_from_slice(&hw1.to_le_bytes());
5271                bytes.extend_from_slice(&hw2.to_le_bytes());
5272
5273                // ADD.W rd, rd, #32 (4 bytes at offset 26)
5274                let hw1: u16 = (0xF100 | rd_bits) as u16;
5275                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5276                bytes.extend_from_slice(&hw1.to_le_bytes());
5277                bytes.extend_from_slice(&hw2.to_le_bytes());
5278
5279                // .done: (offset 30 — the end of the expansion)
5280                // #1048: the former trailing `MOV rnhi, #0` is GONE — it
5281                // wrote the OPERAND's home high register (see the I64Clz
5282                // comment above). `B .done` targets offset 30 = past-the-end,
5283                // `BEQ` targets offset 18 — no displacement moves.
5284
5285                Ok(bytes) // 30 bytes, register-independent
5286            }
5287
5288            // I64Popcnt: Population count of 64-bit value
5289            // result = POPCNT(lo) + POPCNT(hi)
5290            // Using SIMD-style parallel bit counting algorithm
5291            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
5292                let rd_bits = reg_to_bits(rd);
5293                let rn_lo_bits = reg_to_bits(rnlo);
5294                let rn_hi_bits = reg_to_bits(rnhi);
5295                let r12: u32 = 12; // IP scratch
5296                let r3: u32 = 3; // Scratch for hi popcnt result
5297                let mut bytes = Vec::new();
5298
5299                // PUSH {R3, R4, R5} - save scratch registers
5300                bytes.extend_from_slice(&0xB438u16.to_le_bytes());
5301
5302                // Strategy: compute popcnt(lo) -> R4, popcnt(hi) -> R5, add them -> rd
5303                // Using lookup table approach for each byte would be too large
5304                // Using shift-and-add approach instead
5305
5306                // For simplicity and correctness, use the efficient parallel algorithm
5307                // but implement it as a series of inline operations
5308
5309                // Marshal the operand pair into the fixed scratch regs, routing
5310                // rnlo through R12 (#632 audit): writing R4 first corrupted the
5311                // rnhi read for a pair living at (R3,R4) — every source is read
5312                // before any scratch register it could occupy is written.
5313                // MOV R12, rnlo
5314                let mov: u16 = (0x4600 | (1 << 7) | (rn_lo_bits << 3) | 4) as u16;
5315                bytes.extend_from_slice(&mov.to_le_bytes());
5316                // MOV R5, rnhi (R4 untouched so far; rnhi == R5 is a no-op)
5317                let mov: u16 = (0x4600 | (rn_hi_bits << 3) | 5) as u16;
5318                bytes.extend_from_slice(&mov.to_le_bytes());
5319                // MOV R4, R12
5320                bytes.extend_from_slice(&0x4664u16.to_le_bytes());
5321
5322                // --- POPCNT for R4 (lo word) ---
5323                // Step 1: x = x - ((x >> 1) & 0x55555555)
5324                // LSR.W R12, R4, #1
5325                let hw1: u16 = 0xEA4F;
5326                let hw2: u16 = ((r12 << 8) | 0x50 | 4) as u16;
5327                bytes.extend_from_slice(&hw1.to_le_bytes());
5328                bytes.extend_from_slice(&hw2.to_le_bytes());
5329
5330                // Load 0x55555555 into R3 using MOVW/MOVT
5331                // MOVW R3, #0x5555
5332                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5333                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5334                // MOVT R3, #0x5555
5335                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5336                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5337
5338                // AND.W R12, R12, R3
5339                let hw1: u16 = (0xEA00 | r12) as u16;
5340                let hw2: u16 = ((r12 << 8) | r3) as u16;
5341                bytes.extend_from_slice(&hw1.to_le_bytes());
5342                bytes.extend_from_slice(&hw2.to_le_bytes());
5343
5344                // SUB.W R4, R4, R12
5345                let hw1: u16 = (0xEBA0 | 4) as u16;
5346                let hw2: u16 = ((4 << 8) | r12) as u16;
5347                bytes.extend_from_slice(&hw1.to_le_bytes());
5348                bytes.extend_from_slice(&hw2.to_le_bytes());
5349
5350                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5351                // Load 0x33333333 into R3
5352                // MOVW R3, #0x3333
5353                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5354                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5355                // MOVT R3, #0x3333
5356                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5357                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5358
5359                // AND.W R12, R4, R3
5360                let hw1: u16 = (0xEA00 | 4) as u16;
5361                let hw2: u16 = ((r12 << 8) | r3) as u16;
5362                bytes.extend_from_slice(&hw1.to_le_bytes());
5363                bytes.extend_from_slice(&hw2.to_le_bytes());
5364
5365                // LSR.W R4, R4, #2
5366                let hw1: u16 = 0xEA4F;
5367                let hw2: u16 = ((4 << 8) | 0x90 | 4) as u16;
5368                bytes.extend_from_slice(&hw1.to_le_bytes());
5369                bytes.extend_from_slice(&hw2.to_le_bytes());
5370
5371                // AND.W R4, R4, R3
5372                let hw1: u16 = (0xEA00 | 4) as u16;
5373                let hw2: u16 = ((4 << 8) | r3) as u16;
5374                bytes.extend_from_slice(&hw1.to_le_bytes());
5375                bytes.extend_from_slice(&hw2.to_le_bytes());
5376
5377                // ADD.W R4, R4, R12
5378                let hw1: u16 = (0xEB00 | 4) as u16;
5379                let hw2: u16 = ((4 << 8) | r12) as u16;
5380                bytes.extend_from_slice(&hw1.to_le_bytes());
5381                bytes.extend_from_slice(&hw2.to_le_bytes());
5382
5383                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5384                // LSR.W R12, R4, #4
5385                // hw2 = (imm3 << 12) | (Rd << 8) | (imm2 << 6) | (type << 4) | Rm
5386                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5387                let hw1: u16 = 0xEA4F;
5388                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 4) as u16;
5389                bytes.extend_from_slice(&hw1.to_le_bytes());
5390                bytes.extend_from_slice(&hw2.to_le_bytes());
5391
5392                // ADD.W R4, R4, R12
5393                let hw1: u16 = (0xEB00 | 4) as u16;
5394                let hw2: u16 = ((4 << 8) | r12) as u16;
5395                bytes.extend_from_slice(&hw1.to_le_bytes());
5396                bytes.extend_from_slice(&hw2.to_le_bytes());
5397
5398                // Load 0x0F0F0F0F into R3
5399                // MOVW R3, #0x0F0F (imm4=0, i=1, imm3=7, imm8=0x0F)
5400                // hw1 = 11110 1 10 0100 0000 = 0xF640
5401                // hw2 = 0 111 0011 00001111 = 0x730F
5402                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5403                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5404                // MOVT R3, #0x0F0F
5405                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5406                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5407
5408                // AND.W R4, R4, R3
5409                let hw1: u16 = (0xEA00 | 4) as u16;
5410                let hw2: u16 = ((4 << 8) | r3) as u16;
5411                bytes.extend_from_slice(&hw1.to_le_bytes());
5412                bytes.extend_from_slice(&hw2.to_le_bytes());
5413
5414                // Step 4: x = x * 0x01010101 >> 24
5415                // Load 0x01010101 into R3
5416                // MOVW R3, #0x0101
5417                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5418                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5419                // MOVT R3, #0x0101
5420                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5421                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5422
5423                // MUL R4, R4, R3
5424                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5425                let hw1: u16 = (0xFB00 | 4) as u16;
5426                let hw2: u16 = (0xF000 | (4 << 8) | r3) as u16;
5427                bytes.extend_from_slice(&hw1.to_le_bytes());
5428                bytes.extend_from_slice(&hw2.to_le_bytes());
5429
5430                // LSR.W R4, R4, #24
5431                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5432                let hw1: u16 = 0xEA4F;
5433                let hw2: u16 = (0x6000 | (4 << 8) | 0x10 | 4) as u16;
5434                bytes.extend_from_slice(&hw1.to_le_bytes());
5435                bytes.extend_from_slice(&hw2.to_le_bytes());
5436
5437                // --- POPCNT for R5 (hi word) - same algorithm ---
5438                // Step 1
5439                let hw1: u16 = 0xEA4F;
5440                let hw2: u16 = ((r12 << 8) | 0x50 | 5) as u16;
5441                bytes.extend_from_slice(&hw1.to_le_bytes());
5442                bytes.extend_from_slice(&hw2.to_le_bytes());
5443
5444                // Load 0x55555555 into R3
5445                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5446                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5447                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5448                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5449
5450                let hw1: u16 = (0xEA00 | r12) as u16;
5451                let hw2: u16 = ((r12 << 8) | r3) as u16;
5452                bytes.extend_from_slice(&hw1.to_le_bytes());
5453                bytes.extend_from_slice(&hw2.to_le_bytes());
5454
5455                let hw1: u16 = (0xEBA0 | 5) as u16;
5456                let hw2: u16 = ((5 << 8) | r12) as u16;
5457                bytes.extend_from_slice(&hw1.to_le_bytes());
5458                bytes.extend_from_slice(&hw2.to_le_bytes());
5459
5460                // Step 2
5461                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5462                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5463                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5464                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5465
5466                let hw1: u16 = (0xEA00 | 5) as u16;
5467                let hw2: u16 = ((r12 << 8) | r3) as u16;
5468                bytes.extend_from_slice(&hw1.to_le_bytes());
5469                bytes.extend_from_slice(&hw2.to_le_bytes());
5470
5471                let hw1: u16 = 0xEA4F;
5472                let hw2: u16 = ((5 << 8) | 0x90 | 5) as u16;
5473                bytes.extend_from_slice(&hw1.to_le_bytes());
5474                bytes.extend_from_slice(&hw2.to_le_bytes());
5475
5476                let hw1: u16 = (0xEA00 | 5) as u16;
5477                let hw2: u16 = ((5 << 8) | r3) as u16;
5478                bytes.extend_from_slice(&hw1.to_le_bytes());
5479                bytes.extend_from_slice(&hw2.to_le_bytes());
5480
5481                let hw1: u16 = (0xEB00 | 5) as u16;
5482                let hw2: u16 = ((5 << 8) | r12) as u16;
5483                bytes.extend_from_slice(&hw1.to_le_bytes());
5484                bytes.extend_from_slice(&hw2.to_le_bytes());
5485
5486                // Step 3: LSR.W R12, R5, #4
5487                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5488                let hw1: u16 = 0xEA4F;
5489                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 5) as u16;
5490                bytes.extend_from_slice(&hw1.to_le_bytes());
5491                bytes.extend_from_slice(&hw2.to_le_bytes());
5492
5493                let hw1: u16 = (0xEB00 | 5) as u16;
5494                let hw2: u16 = ((5 << 8) | r12) as u16;
5495                bytes.extend_from_slice(&hw1.to_le_bytes());
5496                bytes.extend_from_slice(&hw2.to_le_bytes());
5497
5498                // Load 0x0F0F0F0F into R3 (for hi-word)
5499                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5500                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5501                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5502                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5503
5504                let hw1: u16 = (0xEA00 | 5) as u16;
5505                let hw2: u16 = ((5 << 8) | r3) as u16;
5506                bytes.extend_from_slice(&hw1.to_le_bytes());
5507                bytes.extend_from_slice(&hw2.to_le_bytes());
5508
5509                // Step 4
5510                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5511                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5512                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5513                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5514
5515                // MUL R5, R5, R3
5516                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5517                let hw1: u16 = (0xFB00 | 5) as u16;
5518                let hw2: u16 = (0xF000 | (5 << 8) | r3) as u16;
5519                bytes.extend_from_slice(&hw1.to_le_bytes());
5520                bytes.extend_from_slice(&hw2.to_le_bytes());
5521
5522                // LSR.W R5, R5, #24
5523                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5524                let hw1: u16 = 0xEA4F;
5525                let hw2: u16 = (0x6000 | (5 << 8) | 0x10 | 5) as u16;
5526                bytes.extend_from_slice(&hw1.to_le_bytes());
5527                bytes.extend_from_slice(&hw2.to_le_bytes());
5528
5529                // #632: the count must be carried ACROSS the scratch restore
5530                // in a register the POP cannot touch. rd is allocator-assigned
5531                // (any of R0-R8) and can land inside the {R3,R4,R5} restore set
5532                // — the old `ADDS rd, R4, R5; POP {R3,R4,R5}` destroyed the
5533                // result one instruction after computing it (0 for every input
5534                // under qemu). R12 is encoder scratch: never allocatable (#212)
5535                // and never in a restore set, so no choice of rd can collide.
5536                // ADD.W R12, R4, R5
5537                bytes.extend_from_slice(&0xEB04u16.to_le_bytes());
5538                bytes.extend_from_slice(&0x0C05u16.to_le_bytes());
5539
5540                // POP {R3, R4, R5}
5541                bytes.extend_from_slice(&0xBC38u16.to_le_bytes());
5542
5543                // MOV rd, R12 — after the restore. The 4-bit Rd (D:rd) form is
5544                // also total over rd = R8, where the old ADDS T1 3-bit field
5545                // silently corrupted the encoding (#178/#180 class).
5546                let mov: u16 =
5547                    (0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7)) as u16;
5548                bytes.extend_from_slice(&mov.to_le_bytes());
5549
5550                // #1048: the former trailing `MOV.W rnhi, #0` hi-word clear
5551                // is GONE — it wrote the OPERAND's home high register (see
5552                // the I64Clz comment). Callers that relied on the implicit
5553                // clear emit their own explicit hi-zero op.
5554
5555                Ok(bytes)
5556            }
5557
5558            // I64Extend8S: Sign-extend low 8 bits to 64 bits
5559            // Result: rdlo = sign_extend_8(rnlo), rdhi = rdlo >> 31
5560            ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
5561                let rdlo_bits = reg_to_bits(rdlo);
5562                let rdhi_bits = reg_to_bits(rdhi);
5563                let rnlo_bits = reg_to_bits(rnlo);
5564                let mut bytes = Vec::new();
5565
5566                // SXTB.W rdlo, rnlo (sign-extend byte to 32-bit)
5567                // SXTB T2: hw1 = 0xFA4F, hw2 = 0xF0<Rd><Rm>
5568                let hw1: u16 = 0xFA4F_u16;
5569                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5570                bytes.extend_from_slice(&hw1.to_le_bytes());
5571                bytes.extend_from_slice(&hw2.to_le_bytes());
5572
5573                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5574                // ASR (immediate): hw1 = 0xEA4F, hw2 = imm3:Rd:imm2:type:Rm
5575                // For imm5=31: imm3=111, imm2=11, type=10 (ASR)
5576                // hw2 = (7 << 12) | (rdhi << 8) | (3 << 6) | (2 << 4) | rdlo
5577                let hw1: u16 = 0xEA4F;
5578                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5579                bytes.extend_from_slice(&hw1.to_le_bytes());
5580                bytes.extend_from_slice(&hw2.to_le_bytes());
5581
5582                Ok(bytes)
5583            }
5584
5585            // I64Extend16S: Sign-extend low 16 bits to 64 bits
5586            // Result: rdlo = sign_extend_16(rnlo), rdhi = rdlo >> 31
5587            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
5588                let rdlo_bits = reg_to_bits(rdlo);
5589                let rdhi_bits = reg_to_bits(rdhi);
5590                let rnlo_bits = reg_to_bits(rnlo);
5591                let mut bytes = Vec::new();
5592
5593                // SXTH.W rdlo, rnlo (sign-extend halfword to 32-bit)
5594                // SXTH T2: hw1 = 0xFA0F, hw2 = 0xF0<Rd><Rm>
5595                let hw1: u16 = 0xFA0F_u16;
5596                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5597                bytes.extend_from_slice(&hw1.to_le_bytes());
5598                bytes.extend_from_slice(&hw2.to_le_bytes());
5599
5600                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5601                let hw1: u16 = 0xEA4F;
5602                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5603                bytes.extend_from_slice(&hw1.to_le_bytes());
5604                bytes.extend_from_slice(&hw2.to_le_bytes());
5605
5606                Ok(bytes)
5607            }
5608
5609            // I64Extend32S: Sign-extend low 32 bits to 64 bits
5610            // Result: rdlo = rnlo, rdhi = rnlo >> 31
5611            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
5612                let rdlo_bits = reg_to_bits(rdlo);
5613                let rdhi_bits = reg_to_bits(rdhi);
5614                let rnlo_bits = reg_to_bits(rnlo);
5615                let mut bytes = Vec::new();
5616
5617                // MOV rdlo, rnlo (if different)
5618                if rdlo_bits != rnlo_bits {
5619                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5620                    let d_bit = ((rdlo_bits >> 3) & 1) as u16;
5621                    let mov: u16 = 0x4600
5622                        | (d_bit << 7)
5623                        | ((rnlo_bits as u16) << 3)
5624                        | ((rdlo_bits & 0x7) as u16);
5625                    bytes.extend_from_slice(&mov.to_le_bytes());
5626                }
5627
5628                // ASR.W rdhi, rnlo, #31 (sign-extend to high word)
5629                let hw1: u16 = 0xEA4F;
5630                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rnlo_bits) as u16;
5631                bytes.extend_from_slice(&hw1.to_le_bytes());
5632                bytes.extend_from_slice(&hw2.to_le_bytes());
5633
5634                Ok(bytes)
5635            }
5636
5637            // SelectMove: IT <cond>; MOV{cond} rd, rm
5638            // Conditional move: only execute MOV if condition is true
5639            ArmOp::SelectMove { rd, rm, cond } => {
5640                let rd_bits = reg_to_bits(rd) as u16;
5641                let rm_bits = reg_to_bits(rm) as u16;
5642
5643                // Condition code encoding for IT block
5644                use synth_synthesis::Condition;
5645                let cond_bits: u16 = match cond {
5646                    Condition::EQ => 0x0, // Equal
5647                    Condition::NE => 0x1, // Not equal
5648                    Condition::HS => 0x2, // Higher or same (unsigned >=)
5649                    Condition::LO => 0x3, // Lower (unsigned <)
5650                    Condition::HI => 0x8, // Higher (unsigned >)
5651                    Condition::LS => 0x9, // Lower or same (unsigned <=)
5652                    Condition::GE => 0xA, // Greater or equal (signed)
5653                    Condition::LT => 0xB, // Less than (signed)
5654                    Condition::GT => 0xC, // Greater than (signed)
5655                    Condition::LE => 0xD, // Less or equal (signed)
5656                };
5657
5658                // IT <cond>: single Then block (mask = 0x8 for T only)
5659                // IT instruction: 1011 1111 firstcond mask
5660                let it_instr: u16 = 0xBF00 | (cond_bits << 4) | 0x8;
5661
5662                // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5663                // This MOV will only execute if condition is true due to IT block
5664                let d_bit = (rd_bits >> 3) & 1;
5665                let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5666
5667                // Emit: IT <cond>, MOV rd, rm
5668                let mut bytes = it_instr.to_le_bytes().to_vec();
5669                bytes.extend_from_slice(&mov_instr.to_le_bytes());
5670                Ok(bytes)
5671            }
5672
5673            // Popcnt: Population count (count set bits)
5674            // ARM Cortex-M has no native POPCNT, so we implement the bit manipulation algorithm:
5675            // x = x - ((x >> 1) & 0x55555555);
5676            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
5677            // x = (x + (x >> 4)) & 0x0F0F0F0F;
5678            // x = x + (x >> 8);
5679            // x = x + (x >> 16);
5680            // return x & 0x3F;
5681            //
5682            // #1021: R12 (IP, never allocatable) is the ONLY scratch. The
5683            // previous expansion borrowed R11 as a second temp — but R11 is
5684            // the WASM linear-memory base, materialized at entry and read by
5685            // every later LDR/STR, and it is NOT in the pushed set, so the
5686            // clobber leaked to the CALLER too (a live memory-safety
5687            // miscompile: loads through `base = x >> 16`). The second temp is
5688            // eliminated the way the healthy i64.popcnt discipline implies —
5689            // never touch an unsaved register — but without its PUSH/POP
5690            // wrapper: the SWAR masks 0x55555555 / 0x33333333 / 0x0F0F0F0F
5691            // are all `0xXYXYXYXY` ThumbExpandImm modified immediates, so
5692            // each AND takes its mask from the instruction itself and R12
5693            // alone carries every intermediate. Straight-line, no branches,
5694            // no stack traffic — nothing to skip on a trap edge.
5695            ArmOp::Popcnt { rd, rm } => {
5696                let rd_bits = reg_to_bits(rd);
5697                // Defensive (#1021): rd = R11/R12/SP/PC would silently
5698                // corrupt the linear-memory base, the expansion's own
5699                // scratch, or the stack. The selector never assigns them
5700                // (pool R0-R8); refuse loudly if that ever changes.
5701                if rd_bits >= 11 {
5702                    return Err(synth_core::Error::synthesis(
5703                        "Popcnt destination must be R0-R10: R11 is the linear-memory \
5704                         base and R12 is the expansion's scratch (#1021)",
5705                    ));
5706                }
5707                let mut bytes = Vec::new();
5708
5709                // First, move rm to rd if they're different
5710                if rd != rm {
5711                    let rm_bits = reg_to_bits(rm) as u16;
5712                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5713                    let d_bit = ((rd_bits as u16) >> 3) & 1;
5714                    let mov_instr: u16 =
5715                        0x4600 | (d_bit << 7) | (rm_bits << 3) | ((rd_bits as u16) & 0x7);
5716                    bytes.extend_from_slice(&mov_instr.to_le_bytes());
5717                }
5718
5719                // Step 1: x = x - ((x >> 1) & 0x55555555)
5720                // R12 = rd >> 1
5721                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 1)?);
5722                // R12 = R12 & 0x55555555 (modified immediate, no constant reg)
5723                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(12, 12, 0x5555_5555)?);
5724                // rd = rd - R12
5725                bytes.extend_from_slice(&self.encode_thumb32_sub_reg_raw(rd_bits, rd_bits, 12)?);
5726
5727                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5728                // R12 = rd & 0x33333333
5729                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5730                    12,
5731                    rd_bits,
5732                    0x3333_3333,
5733                )?);
5734                // rd = rd >> 2
5735                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(rd_bits, rd_bits, 2)?);
5736                // rd = rd & 0x33333333
5737                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5738                    rd_bits,
5739                    rd_bits,
5740                    0x3333_3333,
5741                )?);
5742                // rd = rd + R12
5743                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5744
5745                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5746                // R12 = rd >> 4
5747                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 4)?);
5748                // rd = rd + R12
5749                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5750                // rd = rd & 0x0F0F0F0F
5751                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5752                    rd_bits,
5753                    rd_bits,
5754                    0x0F0F_0F0F,
5755                )?);
5756
5757                // Step 4: x = x + (x >> 8)
5758                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 8)?);
5759                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5760
5761                // Step 5: x = x + (x >> 16)
5762                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 16)?);
5763                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5764
5765                // Step 6: return x & 0x3F
5766                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(rd_bits, rd_bits, 0x3F)?);
5767
5768                Ok(bytes)
5769            }
5770
5771            // I64DivU: 64-bit unsigned division using binary long division
5772            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = quotient
5773            // Uses: R4-R7, R12 as loop counter (avoid R8 for Renode compatibility)
5774            //
5775            // #610: the fixed-ABI wrapper marshals the selector-assigned
5776            // operand registers into the core's fixed regs and lands the
5777            // result in rd — pre-#610 this arm IGNORED its register fields,
5778            // so the selector read its rd pair (e.g. R4:R5) after the core's
5779            // own POP restored the stale caller values over it: 0 for every
5780            // input. A zero divisor now traps (UDF #0), per WASM semantics.
5781            ArmOp::I64DivU {
5782                rdlo,
5783                rdhi,
5784                rnlo,
5785                rnhi,
5786                rmlo,
5787                rmhi,
5788                elide_zero_guard,
5789            } => {
5790                let mut bytes = Vec::new();
5791                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5792                // #494 phase 2b: elided only under a certificate-discharged
5793                // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
5794                if !elide_zero_guard {
5795                    emit_i64_divisor_zero_trap(&mut bytes);
5796                }
5797
5798                // PUSH {R4-R7} - save scratch registers (NO LR — this is inline code)
5799                // 16-bit PUSH: 1011 010 M rrrrrrrr where M=0 (no LR), r=R4-R7 = 0xF0
5800                // Encoding: 1011 0100 1111 0000 = 0xB4F0
5801                bytes.extend_from_slice(&0xB4F0u16.to_le_bytes());
5802
5803                // Initialize quotient (R4:R5) = 0
5804                bytes.extend_from_slice(&0x2400u16.to_le_bytes()); // MOV R4, #0
5805                bytes.extend_from_slice(&0x2500u16.to_le_bytes()); // MOV R5, #0
5806
5807                // Initialize remainder (R6:R7) = 0
5808                bytes.extend_from_slice(&0x2600u16.to_le_bytes()); // MOV R6, #0
5809                bytes.extend_from_slice(&0x2700u16.to_le_bytes()); // MOV R7, #0
5810
5811                // Initialize loop counter R12 = 64 (use R12 scratch instead of R8)
5812                // MOV.W R12, #64: F04F 0C40
5813                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5814                bytes.extend_from_slice(&0x0C40u16.to_le_bytes());
5815
5816                // Loop start
5817                let loop_start = bytes.len();
5818
5819                // === Loop body: process one bit ===
5820
5821                // 1. Shift quotient R4:R5 left by 1
5822                // LSLS R5, R5, #1 (16-bit: 0000 0010 1010 1101 = 0x006D -> actually 0x002D for LSL R5,R5,#1)
5823                // LSL Rd, Rm, #imm5: 000 00 imm5 Rm Rd = 000 00 00001 101 101 = 0x006D
5824                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5825                // Get carry from R4 into R5: ORR R5, R5, R4 LSR #31
5826                // Thumb-2 ORR with shifted register: EA45 75D4 = ORR.W R5, R5, R4, LSR #31
5827                // 11101010 010 S Rn | 0 imm3 Rd imm2 type Rm
5828                // type=01 (LSR), imm5=31 (imm3=111, imm2=11)
5829                bytes.extend_from_slice(&0xEA45u16.to_le_bytes());
5830                bytes.extend_from_slice(&0x75D4u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5831                // LSLS R4, R4, #1: 000 00 00001 100 100 = 0x0064
5832                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5833
5834                // 2. Shift remainder R6:R7 left by 1, OR in MSB of dividend R1
5835                // LSLS R7, R7, #1
5836                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5837                // ORR.W R7, R7, R6, LSR #31
5838                bytes.extend_from_slice(&0xEA47u16.to_le_bytes());
5839                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5840                // LSLS R6, R6, #1
5841                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5842                // ORR.W R6, R6, R1, LSR #31 (bring in MSB of dividend high)
5843                bytes.extend_from_slice(&0xEA46u16.to_le_bytes());
5844                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5845
5846                // 3. Shift dividend R0:R1 left by 1
5847                // LSLS R1, R1, #1
5848                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5849                // ORR.W R1, R1, R0, LSR #31
5850                bytes.extend_from_slice(&0xEA41u16.to_le_bytes());
5851                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5852                // LSLS R0, R0, #1
5853                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5854
5855                // 4. Compare remainder >= divisor (64-bit unsigned comparison)
5856                // Compare high words first: CMP R7, R3
5857                // CMP Rn, Rm encoding: 0x4280 | (Rm << 3) | Rn
5858                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3 (16-bit)
5859                // BHI means R7 > R3 (unsigned) - definitely subtract
5860                // BLO means R7 < R3 - definitely don't subtract
5861                // BEQ means need to check low words
5862
5863                // If high > divisor high: branch to subtract (forward +offset)
5864                // BHI.N +6 (skip CMP, skip BLO, do subtract)
5865                // BHI: 1101 1000 offset8 where cond=1000 (HI)
5866                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4 (to subtract block)
5867
5868                // If high < divisor high: branch past subtract
5869                // BLO.N +10 (skip to decrement)
5870                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BLO/BCC +12 (past subtract)
5871
5872                // High words equal, compare low: CMP R6, R2
5873                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2 (16-bit)
5874                // BLO/BCC past subtract (skip SUBS+SBC.W+ORR.W = 10 bytes = 4 halfwords from PC+4)
5875                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords (past subtract)
5876
5877                // === Subtract block: remainder -= divisor, quotient |= 1 ===
5878                // SUBS R6, R6, R2
5879                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2 (16-bit)
5880                // SBC R7, R7, R3 (with borrow)
5881                // Thumb-2 SBC.W: EB67 0703 = SBC.W R7, R7, R3
5882                bytes.extend_from_slice(&0xEB67u16.to_le_bytes());
5883                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5884                // ORR R4, R4, #1 (set bit 0 of quotient low)
5885                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5886                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5887
5888                // === Decrement counter and loop ===
5889                // SUBS.W R12, R12, #1 (decrement loop counter)
5890                // SUBS.W R12, R12, #1: F1BC 0C01
5891                bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
5892                bytes.extend_from_slice(&0x0C01u16.to_le_bytes());
5893
5894                // BNE back to loop_start
5895                let branch_offset_bytes = bytes.len() - loop_start + 4; // +4 for pipeline
5896                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5897                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5898                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5899
5900                // === Loop done, move quotient to R0:R1 ===
5901                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5902                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5903
5904                // POP {R4-R7} - restore scratch registers (NO PC — inline code continues)
5905                // 16-bit POP: 1011 110 P rrrrrrrr where P=0 (no PC), r=R4-R7 = 0xF0
5906                // Encoding: 1011 1100 1111 0000 = 0xBCF0
5907                bytes.extend_from_slice(&0xBCF0u16.to_le_bytes());
5908
5909                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5910                Ok(bytes)
5911            }
5912
5913            // I64DivS: 64-bit signed division
5914            // Converts to unsigned, divides, then applies sign
5915            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5916            //   ->  R0:R1 = quotient (signed)
5917            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5918            ArmOp::I64DivS {
5919                rdlo,
5920                rdhi,
5921                rnlo,
5922                rnhi,
5923                rmlo,
5924                rmhi,
5925                elide_zero_guard,
5926                elide_overflow_guard,
5927            } => {
5928                let mut bytes = Vec::new();
5929                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5930                // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
5931                // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
5932                // the #633 overflow guard falls ONLY to
5933                // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
5934                // divisor-nonzero fact alone must keep it.
5935                if !elide_zero_guard {
5936                    emit_i64_divisor_zero_trap(&mut bytes);
5937                }
5938                if !elide_overflow_guard {
5939                    // #633: INT64_MIN / -1 overflows — trap like the i32 path
5940                    // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
5941                    emit_i64_divs_overflow_trap(&mut bytes);
5942                }
5943
5944                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5945                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5946                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5947
5948                // Save result sign in R9: R9 = R1 XOR R3 (sign bit = MSB)
5949                // EOR.W R9, R1, R3
5950                bytes.extend_from_slice(&0xEA81u16.to_le_bytes());
5951                bytes.extend_from_slice(&0x0903u16.to_le_bytes());
5952
5953                // If dividend negative (R1 MSB set), negate it
5954                // TST R1, R1 (check sign)
5955                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5956                // BPL skip_neg_dividend (+10 bytes = 5 halfwords)
5957                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5958
5959                // Negate R0:R1 (64-bit): RSBS R0, R0, #0; SBC R1, R1, R1 LSL #1
5960                // Actually: MVN R0, R0; MVN R1, R1; ADDS R0, R0, #1; ADC R1, R1, #0
5961                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5962                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5963                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5964                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5965                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5966
5967                // If divisor negative (R3 MSB set), negate it
5968                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5969                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5970
5971                // Negate R2:R3
5972                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
5973                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
5974                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
5975                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
5976                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
5977
5978                // === Now do unsigned division (same as I64DivU) ===
5979                // Initialize quotient (R4:R5) = 0
5980                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5981                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5982                // Initialize remainder (R6:R7) = 0
5983                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5984                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5985                // Initialize loop counter R8 = 64
5986                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5987                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5988
5989                let loop_start = bytes.len();
5990
5991                // Shift quotient left
5992                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5993                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5994                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5995                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5996
5997                // Shift remainder left, OR in MSB of dividend
5998                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5999                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6000                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6001                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6002                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6003                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6004
6005                // Shift dividend left
6006                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6007                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6008                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6009                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6010
6011                // Compare and conditionally subtract
6012                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6013                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6014                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6015                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6016                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6017
6018                // Subtract and set quotient bit
6019                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6020                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6021                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6022                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6023                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6024
6025                // Decrement and loop
6026                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6027                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6028
6029                let branch_offset_bytes = bytes.len() - loop_start + 4;
6030                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6031                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6032                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6033
6034                // Move quotient to R0:R1
6035                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
6036                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
6037
6038                // If result should be negative (R9 MSB set), negate R0:R1
6039                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9 (check MSB)
6040                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6041                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8 (skip negation)
6042
6043                // Negate result R0:R1
6044                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6045                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6046                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6047                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6048                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6049
6050                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6051                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6052                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6053
6054                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6055                Ok(bytes)
6056            }
6057
6058            // I64RemU: 64-bit unsigned remainder using binary long division
6059            // Same algorithm as I64DivU but returns remainder instead of quotient
6060            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = remainder
6061            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6062            ArmOp::I64RemU {
6063                rdlo,
6064                rdhi,
6065                rnlo,
6066                rnhi,
6067                rmlo,
6068                rmhi,
6069                elide_zero_guard,
6070            } => {
6071                let mut bytes = Vec::new();
6072                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6073                if !elide_zero_guard {
6074                    emit_i64_divisor_zero_trap(&mut bytes);
6075                }
6076
6077                // PUSH {R4-R8} - save scratch registers (NO LR — inline code)
6078                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6079                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6080
6081                // Initialize quotient (R4:R5) = 0 (computed but not returned)
6082                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6083                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6084                // Initialize remainder (R6:R7) = 0
6085                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6086                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6087                // Initialize loop counter R8 = 64
6088                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6089                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6090
6091                let loop_start = bytes.len();
6092
6093                // Shift quotient left (not needed for result, but keeps algorithm same)
6094                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6095                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6096                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6097                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6098
6099                // Shift remainder left, OR in MSB of dividend
6100                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6101                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6102                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6103                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6104                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6105                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6106
6107                // Shift dividend left
6108                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6109                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6110                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6111                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6112
6113                // Compare and conditionally subtract
6114                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6115                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6116                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6117                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6118                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6119
6120                // Subtract and set quotient bit
6121                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6122                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6123                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6124                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6125                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6126
6127                // Decrement and loop
6128                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6129                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6130
6131                let branch_offset_bytes = bytes.len() - loop_start + 4;
6132                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6133                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6134                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6135
6136                // Move REMAINDER to R0:R1 (difference from I64DivU)
6137                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6138                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6139
6140                // POP {R4-R8} - restore scratch registers (NO PC — inline code continues)
6141                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6142                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6143
6144                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6145                Ok(bytes)
6146            }
6147
6148            // I64RemS: 64-bit signed remainder
6149            // Remainder sign follows dividend sign (not quotient rule)
6150            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
6151            //   ->  R0:R1 = remainder (signed, same sign as dividend)
6152            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6153            ArmOp::I64RemS {
6154                rdlo,
6155                rdhi,
6156                rnlo,
6157                rnhi,
6158                rmlo,
6159                rmhi,
6160                elide_zero_guard,
6161            } => {
6162                let mut bytes = Vec::new();
6163                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6164                if !elide_zero_guard {
6165                    emit_i64_divisor_zero_trap(&mut bytes);
6166                }
6167
6168                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
6169                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6170                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6171
6172                // Save dividend sign in R9 (remainder sign = dividend sign)
6173                // MOV R9, R1 (just need the sign bit)
6174                bytes.extend_from_slice(&0x4689u16.to_le_bytes()); // MOV R9, R1
6175
6176                // If dividend negative (R1 MSB set), negate it
6177                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
6178                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6179
6180                // Negate R0:R1
6181                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6182                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6183                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6184                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6185                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6186
6187                // If divisor negative (R3 MSB set), negate it
6188                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
6189                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6190
6191                // Negate R2:R3
6192                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
6193                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
6194                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
6195                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
6196                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
6197
6198                // === Unsigned division algorithm ===
6199                // Initialize quotient (R4:R5) = 0
6200                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6201                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6202                // Initialize remainder (R6:R7) = 0
6203                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6204                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6205                // Initialize loop counter R8 = 64
6206                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6207                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6208
6209                let loop_start = bytes.len();
6210
6211                // Shift quotient left
6212                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6213                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6214                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6215                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6216
6217                // Shift remainder left, OR in MSB of dividend
6218                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6219                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6220                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6221                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6222                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6223                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6224
6225                // Shift dividend left
6226                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6227                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6228                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6229                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6230
6231                // Compare and conditionally subtract
6232                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6233                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6234                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6235                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6236                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6237
6238                // Subtract and set quotient bit
6239                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6240                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6241                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6242                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6243                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6244
6245                // Decrement and loop
6246                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6247                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6248
6249                let branch_offset_bytes = bytes.len() - loop_start + 4;
6250                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6251                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6252                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6253
6254                // Move remainder to R0:R1
6255                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6256                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6257
6258                // If original dividend was negative (R9 MSB set), negate remainder
6259                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9
6260                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6261                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6262
6263                // Negate result R0:R1
6264                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6265                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6266                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6267                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6268                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6269
6270                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6271                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6272                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6273
6274                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6275                Ok(bytes)
6276            }
6277
6278            // === F32 VFP single-precision Thumb-2 encodings ===
6279            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6280            ArmOp::F32Add { sd, sn, sm } => {
6281                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A00, sd, sn, sm)?))
6282            }
6283            ArmOp::F32Sub { sd, sn, sm } => {
6284                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A40, sd, sn, sm)?))
6285            }
6286            ArmOp::F32Mul { sd, sn, sm } => {
6287                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE200A00, sd, sn, sm)?))
6288            }
6289            ArmOp::F32Div { sd, sn, sm } => {
6290                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE800A00, sd, sn, sm)?))
6291            }
6292            ArmOp::F32Abs { sd, sm } => {
6293                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB00AC0, sd, sm)?))
6294            }
6295            ArmOp::F32Neg { sd, sm } => {
6296                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10A40, sd, sm)?))
6297            }
6298            ArmOp::F32Sqrt { sd, sm } => {
6299                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10AC0, sd, sm)?))
6300            }
6301
6302            // f32 pseudo-ops — multi-instruction sequences
6303            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6304            ArmOp::F32Ceil { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b01),
6305            ArmOp::F32Floor { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b10),
6306            ArmOp::F32Trunc { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b11),
6307            ArmOp::F32Nearest { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b00),
6308            ArmOp::F32Min { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, true),
6309            ArmOp::F32Max { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, false),
6310            ArmOp::F32Copysign { sd, sn, sm } => self.encode_thumb_f32_copysign(sd, sn, sm),
6311
6312            // f32 comparisons — VCMP + VMRS + MOV #0 + IT + MOV #1
6313            ArmOp::F32Eq { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x0),
6314            ArmOp::F32Ne { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x1),
6315            ArmOp::F32Lt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x4),
6316            ArmOp::F32Le { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x9),
6317            ArmOp::F32Gt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xC),
6318            ArmOp::F32Ge { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xA),
6319
6320            ArmOp::F32Const { sd, value } => self.encode_thumb_f32_const(sd, *value),
6321
6322            ArmOp::F32Load { sd, addr } => {
6323                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED900A00, sd, addr)?))
6324            }
6325            ArmOp::F32Store { sd, addr } => {
6326                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED800A00, sd, addr)?))
6327            }
6328
6329            ArmOp::F32ConvertI32S { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, true),
6330            ArmOp::F32ConvertI32U { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, false),
6331            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
6332                Err(synth_core::Error::synthesis(
6333                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6334                ))
6335            }
6336            ArmOp::F32ReinterpretI32 { sd, rm } => {
6337                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(true, sd, rm)?))
6338            }
6339            ArmOp::I32ReinterpretF32 { rd, sm } => {
6340                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(false, sm, rd)?))
6341            }
6342            ArmOp::I32TruncF32S { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, true),
6343            ArmOp::I32TruncF32U { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, false),
6344
6345            // === F64 VFP double-precision Thumb-2 encodings ===
6346            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6347            ArmOp::F64Add { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6348                0xEE300B00, dd, dn, dm,
6349            )?)),
6350            ArmOp::F64Sub { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6351                0xEE300B40, dd, dn, dm,
6352            )?)),
6353            ArmOp::F64Mul { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6354                0xEE200B00, dd, dn, dm,
6355            )?)),
6356            ArmOp::F64Div { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6357                0xEE800B00, dd, dn, dm,
6358            )?)),
6359            ArmOp::F64Abs { dd, dm } => {
6360                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?))
6361            }
6362            ArmOp::F64Neg { dd, dm } => {
6363                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?))
6364            }
6365            ArmOp::F64Sqrt { dd, dm } => {
6366                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?))
6367            }
6368
6369            // f64 pseudo-ops
6370            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6371            ArmOp::F64Ceil { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b01),
6372            ArmOp::F64Floor { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b10),
6373            ArmOp::F64Trunc { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b11),
6374            ArmOp::F64Nearest { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b00),
6375            ArmOp::F64Min { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, true),
6376            ArmOp::F64Max { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, false),
6377            ArmOp::F64Copysign { dd, dn, dm } => self.encode_thumb_f64_copysign(dd, dn, dm),
6378
6379            // f64 comparisons
6380            ArmOp::F64Eq { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x0),
6381            ArmOp::F64Ne { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x1),
6382            ArmOp::F64Lt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x4),
6383            ArmOp::F64Le { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x9),
6384            ArmOp::F64Gt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xC),
6385            ArmOp::F64Ge { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xA),
6386
6387            ArmOp::F64Const { dd, value } => self.encode_thumb_f64_const(dd, *value),
6388
6389            ArmOp::F64Load { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6390                0xED900B00, dd, addr,
6391            )?)),
6392            ArmOp::F64Store { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6393                0xED800B00, dd, addr,
6394            )?)),
6395
6396            ArmOp::F64ConvertI32S { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, true),
6397            ArmOp::F64ConvertI32U { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, false),
6398            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
6399                Err(synth_core::Error::synthesis(
6400                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6401                ))
6402            }
6403            ArmOp::F64PromoteF32 { dd, sm } => self.encode_thumb_f64_promote_f32(dd, sm),
6404            ArmOp::F32DemoteF64 { sd, dm } => self.encode_thumb_f32_demote_f64(sd, dm),
6405            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => Ok(vfp_to_thumb_bytes(
6406                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?,
6407            )),
6408            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => Ok(vfp_to_thumb_bytes(
6409                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?,
6410            )),
6411            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
6412                Err(synth_core::Error::synthesis(
6413                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
6414                ))
6415            }
6416            ArmOp::I32TruncF64S { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, true),
6417            ArmOp::I32TruncF64U { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, false),
6418
6419            // ===== i64 operations: encode as multi-instruction Thumb-2 sequences =====
6420
6421            // I64Add: ADDS rdlo, rnlo, rmlo; ADC.W rdhi, rnhi, rmhi
6422            ArmOp::I64Add {
6423                rdlo,
6424                rdhi,
6425                rnlo,
6426                rnhi,
6427                rmlo,
6428                rmhi,
6429            } => {
6430                let mut bytes = Vec::new();
6431                // ADDS rdlo, rnlo, rmlo (16-bit)
6432                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adds {
6433                    rd: *rdlo,
6434                    rn: *rnlo,
6435                    op2: Operand2::Reg(*rmlo),
6436                })?);
6437                // ADC.W rdhi, rnhi, rmhi (32-bit)
6438                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adc {
6439                    rd: *rdhi,
6440                    rn: *rnhi,
6441                    op2: Operand2::Reg(*rmhi),
6442                })?);
6443                Ok(bytes)
6444            }
6445
6446            // I64Sub: SUBS rdlo, rnlo, rmlo; SBC.W rdhi, rnhi, rmhi
6447            ArmOp::I64Sub {
6448                rdlo,
6449                rdhi,
6450                rnlo,
6451                rnhi,
6452                rmlo,
6453                rmhi,
6454            } => {
6455                let mut bytes = Vec::new();
6456                // SUBS rdlo, rnlo, rmlo (16-bit)
6457                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Subs {
6458                    rd: *rdlo,
6459                    rn: *rnlo,
6460                    op2: Operand2::Reg(*rmlo),
6461                })?);
6462                // SBC.W rdhi, rnhi, rmhi (32-bit)
6463                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Sbc {
6464                    rd: *rdhi,
6465                    rn: *rnhi,
6466                    op2: Operand2::Reg(*rmhi),
6467                })?);
6468                Ok(bytes)
6469            }
6470
6471            // I64And: AND rdlo, rnlo, rmlo; AND rdhi, rnhi, rmhi
6472            ArmOp::I64And {
6473                rdlo,
6474                rdhi,
6475                rnlo,
6476                rnhi,
6477                rmlo,
6478                rmhi,
6479            } => {
6480                let mut bytes = Vec::new();
6481                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6482                    rd: *rdlo,
6483                    rn: *rnlo,
6484                    op2: Operand2::Reg(*rmlo),
6485                })?);
6486                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6487                    rd: *rdhi,
6488                    rn: *rnhi,
6489                    op2: Operand2::Reg(*rmhi),
6490                })?);
6491                Ok(bytes)
6492            }
6493
6494            // I64Or: ORR rdlo, rnlo, rmlo; ORR rdhi, rnhi, rmhi
6495            ArmOp::I64Or {
6496                rdlo,
6497                rdhi,
6498                rnlo,
6499                rnhi,
6500                rmlo,
6501                rmhi,
6502            } => {
6503                let mut bytes = Vec::new();
6504                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6505                    rd: *rdlo,
6506                    rn: *rnlo,
6507                    op2: Operand2::Reg(*rmlo),
6508                })?);
6509                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6510                    rd: *rdhi,
6511                    rn: *rnhi,
6512                    op2: Operand2::Reg(*rmhi),
6513                })?);
6514                Ok(bytes)
6515            }
6516
6517            // I64Xor: EOR rdlo, rnlo, rmlo; EOR rdhi, rnhi, rmhi
6518            ArmOp::I64Xor {
6519                rdlo,
6520                rdhi,
6521                rnlo,
6522                rnhi,
6523                rmlo,
6524                rmhi,
6525            } => {
6526                let mut bytes = Vec::new();
6527                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6528                    rd: *rdlo,
6529                    rn: *rnlo,
6530                    op2: Operand2::Reg(*rmlo),
6531                })?);
6532                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6533                    rd: *rdhi,
6534                    rn: *rnhi,
6535                    op2: Operand2::Reg(*rmhi),
6536                })?);
6537                Ok(bytes)
6538            }
6539
6540            // I64Eqz: ORR scratch, lo, hi; ITE EQ; MOV rd, #1; MOV rd, #0
6541            ArmOp::I64Eqz { rd, rnlo, rnhi } => self.encode_thumb(&ArmOp::I64SetCondZ {
6542                rd: *rd,
6543                rn_lo: *rnlo,
6544                rn_hi: *rnhi,
6545            }),
6546
6547            // I64 comparisons: delegate to I64SetCond
6548            ArmOp::I64Eq {
6549                rd,
6550                rnlo,
6551                rnhi,
6552                rmlo,
6553                rmhi,
6554            } => self.encode_thumb(&ArmOp::I64SetCond {
6555                rd: *rd,
6556                rn_lo: *rnlo,
6557                rn_hi: *rnhi,
6558                rm_lo: *rmlo,
6559                rm_hi: *rmhi,
6560                cond: synth_synthesis::Condition::EQ,
6561            }),
6562
6563            ArmOp::I64Ne {
6564                rd,
6565                rnlo,
6566                rnhi,
6567                rmlo,
6568                rmhi,
6569            } => self.encode_thumb(&ArmOp::I64SetCond {
6570                rd: *rd,
6571                rn_lo: *rnlo,
6572                rn_hi: *rnhi,
6573                rm_lo: *rmlo,
6574                rm_hi: *rmhi,
6575                cond: synth_synthesis::Condition::NE,
6576            }),
6577
6578            ArmOp::I64LtS {
6579                rd,
6580                rnlo,
6581                rnhi,
6582                rmlo,
6583                rmhi,
6584            } => self.encode_thumb(&ArmOp::I64SetCond {
6585                rd: *rd,
6586                rn_lo: *rnlo,
6587                rn_hi: *rnhi,
6588                rm_lo: *rmlo,
6589                rm_hi: *rmhi,
6590                cond: synth_synthesis::Condition::LT,
6591            }),
6592
6593            ArmOp::I64LtU {
6594                rd,
6595                rnlo,
6596                rnhi,
6597                rmlo,
6598                rmhi,
6599            } => self.encode_thumb(&ArmOp::I64SetCond {
6600                rd: *rd,
6601                rn_lo: *rnlo,
6602                rn_hi: *rnhi,
6603                rm_lo: *rmlo,
6604                rm_hi: *rmhi,
6605                cond: synth_synthesis::Condition::LO,
6606            }),
6607
6608            ArmOp::I64LeS {
6609                rd,
6610                rnlo,
6611                rnhi,
6612                rmlo,
6613                rmhi,
6614            } => self.encode_thumb(&ArmOp::I64SetCond {
6615                rd: *rd,
6616                rn_lo: *rnlo,
6617                rn_hi: *rnhi,
6618                rm_lo: *rmlo,
6619                rm_hi: *rmhi,
6620                cond: synth_synthesis::Condition::LE,
6621            }),
6622
6623            ArmOp::I64LeU {
6624                rd,
6625                rnlo,
6626                rnhi,
6627                rmlo,
6628                rmhi,
6629            } => self.encode_thumb(&ArmOp::I64SetCond {
6630                rd: *rd,
6631                rn_lo: *rnlo,
6632                rn_hi: *rnhi,
6633                rm_lo: *rmlo,
6634                rm_hi: *rmhi,
6635                cond: synth_synthesis::Condition::LS,
6636            }),
6637
6638            ArmOp::I64GtS {
6639                rd,
6640                rnlo,
6641                rnhi,
6642                rmlo,
6643                rmhi,
6644            } => self.encode_thumb(&ArmOp::I64SetCond {
6645                rd: *rd,
6646                rn_lo: *rnlo,
6647                rn_hi: *rnhi,
6648                rm_lo: *rmlo,
6649                rm_hi: *rmhi,
6650                cond: synth_synthesis::Condition::GT,
6651            }),
6652
6653            ArmOp::I64GtU {
6654                rd,
6655                rnlo,
6656                rnhi,
6657                rmlo,
6658                rmhi,
6659            } => self.encode_thumb(&ArmOp::I64SetCond {
6660                rd: *rd,
6661                rn_lo: *rnlo,
6662                rn_hi: *rnhi,
6663                rm_lo: *rmlo,
6664                rm_hi: *rmhi,
6665                cond: synth_synthesis::Condition::HI,
6666            }),
6667
6668            ArmOp::I64GeS {
6669                rd,
6670                rnlo,
6671                rnhi,
6672                rmlo,
6673                rmhi,
6674            } => self.encode_thumb(&ArmOp::I64SetCond {
6675                rd: *rd,
6676                rn_lo: *rnlo,
6677                rn_hi: *rnhi,
6678                rm_lo: *rmlo,
6679                rm_hi: *rmhi,
6680                cond: synth_synthesis::Condition::GE,
6681            }),
6682
6683            ArmOp::I64GeU {
6684                rd,
6685                rnlo,
6686                rnhi,
6687                rmlo,
6688                rmhi,
6689            } => self.encode_thumb(&ArmOp::I64SetCond {
6690                rd: *rd,
6691                rn_lo: *rnlo,
6692                rn_hi: *rnhi,
6693                rm_lo: *rmlo,
6694                rm_hi: *rmhi,
6695                cond: synth_synthesis::Condition::HS,
6696            }),
6697
6698            // I64Const: MOVW rdlo, lo16; MOVT rdlo, hi16; MOVW rdhi, lo16_hi; MOVT rdhi, hi16_hi
6699            ArmOp::I64Const { rdlo, rdhi, value } => {
6700                let lo32 = *value as u32;
6701                let hi32 = (*value >> 32) as u32;
6702                let mut bytes = Vec::new();
6703                // Load low 32 bits into rdlo
6704                bytes.extend_from_slice(
6705                    &self.encode_thumb32_movw_raw(reg_to_bits(rdlo), lo32 & 0xFFFF)?,
6706                );
6707                if lo32 > 0xFFFF {
6708                    bytes.extend_from_slice(
6709                        &self.encode_thumb32_movt_raw(reg_to_bits(rdlo), lo32 >> 16)?,
6710                    );
6711                }
6712                // Load high 32 bits into rdhi
6713                bytes.extend_from_slice(
6714                    &self.encode_thumb32_movw_raw(reg_to_bits(rdhi), hi32 & 0xFFFF)?,
6715                );
6716                if hi32 > 0xFFFF {
6717                    bytes.extend_from_slice(
6718                        &self.encode_thumb32_movt_raw(reg_to_bits(rdhi), hi32 >> 16)?,
6719                    );
6720                }
6721                Ok(bytes)
6722            }
6723
6724            // I64Ldr: LDR rdlo, [base, offset]; LDR rdhi, [base, offset+4]
6725            ArmOp::I64Ldr { rdlo, rdhi, addr } => {
6726                let mut bytes = Vec::new();
6727                // #372/#382: a memory `i64.load` carries an index register
6728                // (`reg_imm(R11, addr_reg, offset)` = R11 + addr + offset). The
6729                // immediate `encode_thumb32_ldr` below uses only base+offset and
6730                // would SILENTLY DROP `offset_reg` — the #206 defect, here for
6731                // i64. `i64_effective_base` materializes the effective base into
6732                // `ip` (and, when `offset+4 > 0xFFF`, folds the offset in too so
6733                // the function is NOT skipped — #382), returning the residual
6734                // imm12 for the two halves. Frame i64 loads (no `offset_reg`, e.g.
6735                // a spilled local at `[SP, #off]`) keep the plain `[base,#off]`
6736                // form unchanged — so existing output is byte-identical.
6737                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6738                bytes.extend_from_slice(&self.encode_thumb32_ldr(rdlo, &base, offset)?);
6739                bytes.extend_from_slice(&self.encode_thumb32_ldr(
6740                    rdhi,
6741                    &base,
6742                    offset.wrapping_add(4),
6743                )?);
6744                Ok(bytes)
6745            }
6746
6747            // I64Str: STR rdlo, [base, offset]; STR rdhi, [base, offset+4]
6748            ArmOp::I64Str { rdlo, rdhi, addr } => {
6749                let mut bytes = Vec::new();
6750                // #372/#382: same index-materialization + large-offset fold as
6751                // I64Ldr (see above).
6752                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6753                bytes.extend_from_slice(&self.encode_thumb32_str(rdlo, &base, offset)?);
6754                bytes.extend_from_slice(&self.encode_thumb32_str(
6755                    rdhi,
6756                    &base,
6757                    offset.wrapping_add(4),
6758                )?);
6759                Ok(bytes)
6760            }
6761
6762            // I64ExtendI32S: MOV rdlo, rn; ASR rdhi, rdlo, #31 (sign-extend)
6763            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
6764                let mut bytes = Vec::new();
6765                if rdlo != rn {
6766                    // MOV rdlo, rn (16-bit)
6767                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6768                        rd: *rdlo,
6769                        op2: Operand2::Reg(*rn),
6770                    })?);
6771                }
6772                // ASR rdhi, rdlo, #31 (sign-extend: fill high word with sign bit)
6773                bytes.extend_from_slice(
6774                    &self.encode_thumb32_shift(rdhi, rdlo, 31, 0b10)?, // ASR type
6775                );
6776                Ok(bytes)
6777            }
6778
6779            // I64ExtendI32U: MOV rdlo, rn; MOV rdhi, #0
6780            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
6781                let mut bytes = Vec::new();
6782                if rdlo != rn {
6783                    // MOV rdlo, rn
6784                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6785                        rd: *rdlo,
6786                        op2: Operand2::Reg(*rn),
6787                    })?);
6788                }
6789                // MOV rdhi, #0 (#916: MOV.W for rdhi >= R8). Unconditional
6790                // site with no branches in the expansion — before the fix this
6791                // emitted the literal two-instruction stream [4608, 2800], half
6792                // of which was `CMP r0,#0` rather than the high-word clear, so
6793                // every i64.extend_i32_u into a high pair leaked stale bits.
6794                emit_thumb_zero_fill(&mut bytes, reg_to_bits(rdhi));
6795                Ok(bytes)
6796            }
6797
6798            // I32WrapI64: MOV rd, rnlo (just take low 32 bits)
6799            ArmOp::I32WrapI64 { rd, rnlo } => {
6800                if rd == rnlo {
6801                    // No-op: already in the right register
6802                    let instr: u16 = 0xBF00; // NOP
6803                    Ok(instr.to_le_bytes().to_vec())
6804                } else {
6805                    // MOV rd, rnlo
6806                    self.encode_thumb(&ArmOp::Mov {
6807                        rd: *rd,
6808                        op2: Operand2::Reg(*rnlo),
6809                    })
6810                }
6811            }
6812
6813            // ===== Helium MVE operations (Thumb-2 encoding) =====
6814            ArmOp::MveLoad { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vldrw(qd, addr))),
6815            ArmOp::MveStore { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vstrw(qd, addr))),
6816            ArmOp::MveConst { qd, bytes } => self.encode_thumb_mve_const(qd, bytes),
6817            ArmOp::MveAnd { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6818                0xEF000150, qd, qn, qm,
6819            ))),
6820            ArmOp::MveOrr { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6821                0xEF200150, qd, qn, qm,
6822            ))),
6823            ArmOp::MveEor { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6824                0xFF000150, qd, qn, qm,
6825            ))),
6826            ArmOp::MveMvn { qd, qm } => {
6827                // VMVN Qd, Qm: 0xFFB005C0 | Qd<<12 | Qm
6828                let qd_enc = qreg_to_num(qd);
6829                let qm_enc = qreg_to_num(qm);
6830                let instr: u32 = 0xFFB005C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6831                Ok(vfp_to_thumb_bytes(instr))
6832            }
6833            ArmOp::MveBic { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6834                0xEF100150, qd, qn, qm,
6835            ))),
6836            ArmOp::MveAddI { qd, qn, qm, size } => {
6837                let sz = mve_size_bits(size);
6838                let base: u32 = 0xEF000840 | (sz << 20);
6839                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6840            }
6841            ArmOp::MveSubI { qd, qn, qm, size } => {
6842                let sz = mve_size_bits(size);
6843                let base: u32 = 0xFF000840 | (sz << 20);
6844                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6845            }
6846            ArmOp::MveMulI { qd, qn, qm, size } => {
6847                let sz = mve_size_bits(size);
6848                let base: u32 = 0xEF000950 | (sz << 20);
6849                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6850            }
6851            ArmOp::MveNegI { qd, qm, size } => {
6852                let sz = mve_size_bits(size);
6853                // VNEG.Sx Qd, Qm
6854                let qd_enc = qreg_to_num(qd);
6855                let qm_enc = qreg_to_num(qm);
6856                let base: u32 = 0xFFB103C0 | (sz << 18);
6857                let instr = base | ((qd_enc * 2) << 12) | (qm_enc * 2);
6858                Ok(vfp_to_thumb_bytes(instr))
6859            }
6860            ArmOp::MveDup { qd, rn, size } => {
6861                let sz = mve_size_bits(size);
6862                let qd_enc = qreg_to_num(qd);
6863                let rn_bits = reg_to_bits(rn);
6864                // VDUP.sz Qd, Rn: EEA0 0B10 variant
6865                // size encoding: 00=32, 01=16, 10=8
6866                let be = match sz {
6867                    0 => 0b00u32, // 8-bit
6868                    1 => 0b01,    // 16-bit
6869                    _ => 0b00,    // 32-bit (default)
6870                };
6871                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12) | (be << 5);
6872                Ok(vfp_to_thumb_bytes(instr))
6873            }
6874            ArmOp::MveExtractLane { rd, qn, lane, size } => {
6875                let qn_enc = qreg_to_num(qn);
6876                let rd_bits = reg_to_bits(rd);
6877                // VMOV.sz Rd, Dn[x] — extract from Q-register lane
6878                // For 32-bit: VMOV Rd, Dn — where Dn is the appropriate D-register
6879                let d_reg = qn_enc * 2 + ((*lane as u32) >> 1);
6880                let lane_in_d = (*lane as u32) & 1;
6881                let _sz = mve_size_bits(size);
6882                // VMOV Rd, Dn[x]: EE10 0B10 for 32-bit
6883                let instr: u32 = 0xEE100B10 | (d_reg << 16) | (rd_bits << 12) | (lane_in_d << 21);
6884                Ok(vfp_to_thumb_bytes(instr))
6885            }
6886            ArmOp::MveInsertLane { qd, rn, lane, size } => {
6887                let qd_enc = qreg_to_num(qd);
6888                let rn_bits = reg_to_bits(rn);
6889                let d_reg = qd_enc * 2 + ((*lane as u32) >> 1);
6890                let lane_in_d = (*lane as u32) & 1;
6891                let _sz = mve_size_bits(size);
6892                // VMOV Dn[x], Rn: EE00 0B10 for 32-bit
6893                let instr: u32 = 0xEE000B10 | (d_reg << 16) | (rn_bits << 12) | (lane_in_d << 21);
6894                Ok(vfp_to_thumb_bytes(instr))
6895            }
6896
6897            // MVE float comparisons — emit VCMP + VPSEL sequence (simplified: just VCMP)
6898            ArmOp::MveCmpEqI { qd, qn, qm, size }
6899            | ArmOp::MveCmpNeI { qd, qn, qm, size }
6900            | ArmOp::MveCmpLtS { qd, qn, qm, size }
6901            | ArmOp::MveCmpLtU { qd, qn, qm, size }
6902            | ArmOp::MveCmpGtS { qd, qn, qm, size }
6903            | ArmOp::MveCmpGtU { qd, qn, qm, size }
6904            | ArmOp::MveCmpLeS { qd, qn, qm, size }
6905            | ArmOp::MveCmpLeU { qd, qn, qm, size }
6906            | ArmOp::MveCmpGeS { qd, qn, qm, size }
6907            | ArmOp::MveCmpGeU { qd, qn, qm, size } => {
6908                // Encode as VADD (placeholder encoding — real implementation
6909                // would use VCMP + VPSEL pair)
6910                let sz = mve_size_bits(size);
6911                let base: u32 = 0xEF000840 | (sz << 20);
6912                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6913            }
6914
6915            // f32x4 MVE arithmetic
6916            ArmOp::MveAddF32 { qd, qn, qm } => {
6917                // VADD.F32 Qd, Qn, Qm (MVE): 0xEF000D40
6918                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6919            }
6920            ArmOp::MveSubF32 { qd, qn, qm } => {
6921                // VSUB.F32 Qd, Qn, Qm (MVE): 0xEF200D40
6922                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF200D40, qd, qn, qm)))
6923            }
6924            ArmOp::MveMulF32 { qd, qn, qm } => {
6925                // VMUL.F32 Qd, Qn, Qm (MVE): 0xFF000D50
6926                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xFF000D50, qd, qn, qm)))
6927            }
6928            ArmOp::MveNegF32 { qd, qm } => {
6929                let qd_enc = qreg_to_num(qd);
6930                let qm_enc = qreg_to_num(qm);
6931                // VNEG.F32 Qd, Qm: FFB907C0
6932                let instr: u32 = 0xFFB907C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6933                Ok(vfp_to_thumb_bytes(instr))
6934            }
6935            ArmOp::MveAbsF32 { qd, qm } => {
6936                let qd_enc = qreg_to_num(qd);
6937                let qm_enc = qreg_to_num(qm);
6938                // VABS.F32 Qd, Qm: FFB90740
6939                let instr: u32 = 0xFFB90740 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6940                Ok(vfp_to_thumb_bytes(instr))
6941            }
6942            ArmOp::MveCmpEqF32 { qd, qn, qm }
6943            | ArmOp::MveCmpNeF32 { qd, qn, qm }
6944            | ArmOp::MveCmpLtF32 { qd, qn, qm }
6945            | ArmOp::MveCmpLeF32 { qd, qn, qm }
6946            | ArmOp::MveCmpGtF32 { qd, qn, qm }
6947            | ArmOp::MveCmpGeF32 { qd, qn, qm } => {
6948                // Placeholder: encode as VADD.F32 (real impl needs VCMP.F32 + VPSEL)
6949                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6950            }
6951            ArmOp::MveDupF32 { qd, rn } => {
6952                let qd_enc = qreg_to_num(qd);
6953                let rn_bits = reg_to_bits(rn);
6954                // VDUP.32 Qd, Rn (same encoding as integer VDUP.32)
6955                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12);
6956                Ok(vfp_to_thumb_bytes(instr))
6957            }
6958            ArmOp::MveExtractLaneF32 { rd, qn, lane } => {
6959                let qn_enc = qreg_to_num(qn);
6960                let rd_bits = reg_to_bits(rd);
6961                // VMOV Rd, Sn where Sn = Q*4 + lane
6962                let s_num = qn_enc * 4 + (*lane as u32);
6963                let (vn, n) = encode_sreg(s_num);
6964                let instr: u32 = 0xEE100A10 | (vn << 16) | (rd_bits << 12) | (n << 7);
6965                Ok(vfp_to_thumb_bytes(instr))
6966            }
6967            ArmOp::MveReplaceLaneF32 { qd, rn, lane } => {
6968                let qd_enc = qreg_to_num(qd);
6969                let rn_bits = reg_to_bits(rn);
6970                // VMOV Sn, Rn where Sn = Q*4 + lane
6971                let s_num = qd_enc * 4 + (*lane as u32);
6972                let (vn, n) = encode_sreg(s_num);
6973                let instr: u32 = 0xEE000A10 | (vn << 16) | (rn_bits << 12) | (n << 7);
6974                Ok(vfp_to_thumb_bytes(instr))
6975            }
6976            ArmOp::MveDivF32 { qd, qn, qm } => {
6977                // Lane-wise: extract 4 S-regs, VDIV, insert back
6978                self.encode_thumb_mve_lane_wise_f32_binop(qd, qn, qm, 0xEE800A00)
6979            }
6980            ArmOp::MveSqrtF32 { qd, qm } => {
6981                // Lane-wise: extract 4 S-regs, VSQRT, insert back
6982                self.encode_thumb_mve_lane_wise_f32_sqrt(qd, qm)
6983            }
6984
6985            // RQ-67-VFPREACH (#1267): the AAPCS callee-saved VFP half.
6986            //
6987            // Encodings taken from llvm-mc, not derived by hand:
6988            //   llvm-mc -triple=thumbv7em-none-eabi -mattr=+vfp4 -show-encoding
6989            //     vpush {d8-d15}  ->  [0x2d,0xed,0x10,0x8b]   (ED2D 8B10)
6990            //     vpop  {d8-d15}  ->  [0xbd,0xec,0x10,0x8b]   (ECBD 8B10)
6991            //   imm8 is 2 x the double count (0x10 = 16 = 2 x 8), and the
6992            //   `8B` nibble selects the 64-bit form — `vpush {d8}` is 8B02,
6993            //   which is how that field was confirmed rather than assumed.
6994            //
6995            // NOTE FOR ANYONE ADDING AN OP BELOW THIS POINT: the catch-all that
6996            // follows returns a NOP, so an op that reaches it is encoded as
6997            // nothing at all. These two arms exist ABOVE it deliberately.
6998            ArmOp::VPushCalleeSavedVfp => Ok(vec![0x2d, 0xed, 0x10, 0x8b]),
6999            ArmOp::VPopCalleeSavedVfp => Ok(vec![0xbd, 0xec, 0x10, 0x8b]),
7000
7001            // Catch-all for any remaining ops
7002            //
7003            // #615 FIXED THIS CLASS ON THE A32 PATH AND NOT HERE. Thumb-2 is
7004            // the DEFAULT target, and this arm silently encodes any unhandled
7005            // op as `NOP`. RQ-67-VFPREACH hit it directly: the two VFP
7006            // save/restore ops above compiled clean and would have emitted a
7007            // save that does nothing. Filed rather than widened in this lane —
7008            // turning it into a typed Err is a behaviour change across every
7009            // op that currently lands here, which needs its own byte-identity
7010            // evidence.
7011            _ => {
7012                let instr: u16 = 0xBF00; // NOP
7013                Ok(instr.to_le_bytes().to_vec())
7014            }
7015        }
7016    }
7017
7018    // === Thumb-2 VFP multi-instruction helpers ===
7019
7020    /// Encode F32 comparison as Thumb-2: VCMP.F32 + VMRS + MOVS rd,#0 + IT + MOV rd,#1
7021    fn encode_thumb_f32_compare(
7022        &self,
7023        rd: &Reg,
7024        sn: &VfpReg,
7025        sm: &VfpReg,
7026        cond_code: u32,
7027    ) -> Result<Vec<u8>> {
7028        let mut bytes = Vec::new();
7029        let rd_bits = reg_to_bits(rd);
7030
7031        // #709 (bug found under #708/#709): the `MOVS Rd,#0` below is a
7032        // FLAG-SETTING 16-bit move. Emitting it AFTER `VMRS APSR_nzcv, FPSCR`
7033        // (as the original code did) clobbered the N/Z/C/V flags the VMRS just
7034        // transferred from the VFP compare, so the following `IT<cond>` read
7035        // stale flags and every f32 comparison silently returned 0 (verified:
7036        // `flt(1.0,2.0)` → 0 on Cortex-M4F). The 619 harness never caught it
7037        // because it deliberately skipped compare EXECUTION on a false premise
7038        // (unicorn DOES model VMRS→APSR). Fix: materialize the `#0` FIRST, then
7039        // VCMP+VMRS set the flags the `IT` consumes. Instruction sizes are
7040        // unchanged (pure reorder), so the estimator↔encoder oracle (#511) is
7041        // untouched — only the byte ORDER differs.
7042
7043        // MOVS Rd, #0 (16-bit): 0010 0 Rd(3) 0000 0000 — its flag side effect
7044        // is immediately overwritten by the VMRS below.
7045        if rd_bits < 8 {
7046            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
7047            bytes.extend_from_slice(&movs_zero.to_le_bytes());
7048        } else {
7049            // MOV.W Rd, #0 (32-bit Thumb-2)
7050            let hw1: u16 = 0xF04F;
7051            let hw2: u16 = (rd_bits as u16) << 8;
7052            bytes.extend_from_slice(&hw1.to_le_bytes());
7053            bytes.extend_from_slice(&hw2.to_le_bytes());
7054        }
7055
7056        // VCMP.F32 Sn, Sm
7057        let sn_num = vfp_sreg_to_num(sn)?;
7058        let sm_num = vfp_sreg_to_num(sm)?;
7059        let (vd, d) = encode_sreg(sn_num);
7060        let (vm, m) = encode_sreg(sm_num);
7061        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7062        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7063
7064        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10 (sets the flags IT consumes)
7065        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7066
7067        // IT<cond> — If-Then for conditional MOV
7068        // IT encoding: 1011 1111 cond(4) mask(4)
7069        // mask = 0x8 for single "then" (IT)
7070        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7071        bytes.extend_from_slice(&it.to_le_bytes());
7072
7073        // MOV Rd, #1 (16-bit, conditional due to IT): 0010 0 Rd(3) 0000 0001
7074        if rd_bits < 8 {
7075            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7076            bytes.extend_from_slice(&mov_one.to_le_bytes());
7077        } else {
7078            // MOV.W Rd, #1 (32-bit)
7079            let hw1: u16 = 0xF04F;
7080            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7081            bytes.extend_from_slice(&hw1.to_le_bytes());
7082            bytes.extend_from_slice(&hw2.to_le_bytes());
7083        }
7084
7085        Ok(bytes)
7086    }
7087
7088    /// Encode F32 constant load as Thumb-2: MOVW + MOVT + VMOV
7089    fn encode_thumb_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
7090        let mut bytes = Vec::new();
7091        let bits = value.to_bits();
7092        let rt: u32 = 12; // R12/IP as temp
7093
7094        // MOVW R12, #lo16
7095        // Thumb-2 MOVW: 11110 i 10 0100 imm4 | 0 imm3 Rd imm8
7096        let lo16 = bits & 0xFFFF;
7097        let imm4 = (lo16 >> 12) & 0xF;
7098        let i_bit = (lo16 >> 11) & 1;
7099        let imm3 = (lo16 >> 8) & 0x7;
7100        let imm8 = lo16 & 0xFF;
7101        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7102        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
7103        bytes.extend_from_slice(&hw1.to_le_bytes());
7104        bytes.extend_from_slice(&hw2.to_le_bytes());
7105
7106        // MOVT R12, #hi16
7107        let hi16 = (bits >> 16) & 0xFFFF;
7108        let imm4 = (hi16 >> 12) & 0xF;
7109        let i_bit = (hi16 >> 11) & 1;
7110        let imm3 = (hi16 >> 8) & 0x7;
7111        let imm8 = hi16 & 0xFF;
7112        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
7113        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
7114        bytes.extend_from_slice(&hw1.to_le_bytes());
7115        bytes.extend_from_slice(&hw2.to_le_bytes());
7116
7117        // VMOV Sd, R12
7118        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
7119        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7120
7121        Ok(bytes)
7122    }
7123
7124    /// Encode VMOV + VCVT.F32.xS32 as Thumb-2
7125    fn encode_thumb_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7126        let mut bytes = Vec::new();
7127
7128        // VMOV Sd, Rm
7129        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
7130        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7131
7132        // VCVT.F32.S32/U32 Sd, Sd. Bit 7 (op) = 1 for signed (S32), 0 for
7133        // unsigned (U32): signed = 0xEEB80AC0, unsigned = 0xEEB80A40
7134        // (GI-FPU-002: previously swapped — see the ARM32 twin).
7135        let sd_num = vfp_sreg_to_num(sd)?;
7136        let (vd, d) = encode_sreg(sd_num);
7137        let (vm, m) = encode_sreg(sd_num);
7138        let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
7139        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7140        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7141
7142        Ok(bytes)
7143    }
7144
7145    /// Encode F32 rounding pseudo-op as Thumb-2 via VCVT to integer and back
7146    /// Encode F32 rounding as Thumb-2.
7147    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
7148    ///
7149    /// For trunc: uses VCVTR.S32.F32 (always truncates).
7150    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant),
7151    /// then restores FPSCR.
7152    fn encode_thumb_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7153        let mut bytes = Vec::new();
7154        let sm_num = vfp_sreg_to_num(sm)?;
7155        let sd_num = vfp_sreg_to_num(sd)?;
7156        let (vd_s, d_s) = encode_sreg(sd_num);
7157        let (vm_s, m_s) = encode_sreg(sm_num);
7158
7159        if mode == 0b11 {
7160            // Trunc (toward zero): VCVTR.S32.F32 — bit[7]=1, always truncates
7161            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7162            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7163        } else {
7164            // ceil/floor/nearest: manipulate FPSCR rounding mode
7165            let rt: u32 = 12; // R12/IP as temp
7166
7167            // VMRS R12, FPSCR
7168            let vmrs = 0xEEF10A10 | (rt << 12);
7169            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7170
7171            // BIC.W R12, R12, #(3 << 22) — clear RMode bits [23:22]
7172            // Thumb-2 modified immediate for 3<<22 = 0x00C00000:
7173            // BIC.W encoding: 11110 i 0 0001 S Rn | 0 imm3 Rd imm8
7174            // 0x00C00000 = 0x03 shifted left by 22 => Thumb mod-imm: i=0, imm3=0b101, imm8=0x03
7175            let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF); // BIC, Rn=R12
7176            let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
7177            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7178            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7179
7180            // ORR.W R12, R12, #(mode << 22)
7181            if mode != 0 {
7182                let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF); // ORR, Rn=R12
7183                let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
7184                bytes.extend_from_slice(&orr_hw1.to_le_bytes());
7185                bytes.extend_from_slice(&orr_hw2.to_le_bytes());
7186            }
7187
7188            // VMSR FPSCR, R12
7189            let vmsr = 0xEEE10A10 | (rt << 12);
7190            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7191
7192            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rmode
7193            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7194            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7195
7196            // Restore FPSCR: clear rmode bits back to nearest (default)
7197            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7198            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7199            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7200            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7201        }
7202
7203        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
7204        let (vd2, d2) = encode_sreg(sd_num);
7205        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
7206        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
7207
7208        Ok(bytes)
7209    }
7210
7211    /// Encode F32 min/max as Thumb-2: VMOV + VCMP + VMRS + IT + VMOV
7212    fn encode_thumb_f32_minmax(
7213        &self,
7214        sd: &VfpReg,
7215        sn: &VfpReg,
7216        sm: &VfpReg,
7217        is_min: bool,
7218    ) -> Result<Vec<u8>> {
7219        let mut bytes = Vec::new();
7220        let sn_num = vfp_sreg_to_num(sn)?;
7221        let sm_num = vfp_sreg_to_num(sm)?;
7222        let sd_num = vfp_sreg_to_num(sd)?;
7223
7224        // VMOV.F32 Sd, Sn
7225        let (vd, d) = encode_sreg(sd_num);
7226        let (vn, n) = encode_sreg(sn_num);
7227        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
7228        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sn));
7229
7230        // VCMP.F32 Sn, Sm
7231        let (vm, m) = encode_sreg(sm_num);
7232        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7233        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7234
7235        // VMRS APSR_nzcv, FPSCR
7236        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7237
7238        // IT GT (for min) or IT MI (for max)
7239        let cond: u16 = if is_min { 0xC } else { 0x4 };
7240        let it: u16 = 0xBF00 | (cond << 4) | 0x8;
7241        bytes.extend_from_slice(&it.to_le_bytes());
7242
7243        // VMOV{cond}.F32 Sd, Sm — conditional VMOV in IT block
7244        let vmov_sm = 0xEEB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7245        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sm));
7246
7247        Ok(bytes)
7248    }
7249
7250    /// Encode F32 copysign as Thumb-2
7251    /// Encode F32 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7252    /// encoder scratch, #212), the flags, and Sd:
7253    ///
7254    ///   VMOV R12, Sm ; CMP R12, #0    (N flag = the sign bit)
7255    ///   VABS.F32 Sd, Sn               (magnitude, sign cleared)
7256    ///   IT MI ; VNEG.F32(MI) Sd, Sd
7257    ///
7258    /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7259    /// The R12 capture happens BEFORE Sd is written, so Sd aliasing Sn or Sm
7260    /// is safe. (The previous sequence staged the magnitude through R0 —
7261    /// clobbering a live allocator-owned value, the #615 class; caught while
7262    /// composing the F64 twin for #369.)
7263    fn encode_thumb_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7264        let mut bytes = Vec::new();
7265
7266        // VMOV R12, Sm (sign source bits)
7267        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
7268            false,
7269            sm,
7270            &Reg::R12,
7271        )?));
7272        // CMP.W R12, #0 — N = bit31 (the sign, incl. -0.0 / -NaN).
7273        bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7274        bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7275        // VABS.F32 Sd, Sn
7276        let sd_num = vfp_sreg_to_num(sd)?;
7277        let sn_num = vfp_sreg_to_num(sn)?;
7278        let (vd, d) = encode_sreg(sd_num);
7279        let (vn, n) = encode_sreg(sn_num);
7280        let vabs = 0xEEB00AC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7281        bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7282        // IT MI ; VNEG.F32(MI) Sd, Sd
7283        bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7284        let vneg = 0xEEB10A40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7285        bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7286
7287        Ok(bytes)
7288    }
7289
7290    /// Encode F64 comparison as Thumb-2: VCMP.F64 + VMRS + MOV #0 + IT + MOV #1
7291    fn encode_thumb_f64_compare(
7292        &self,
7293        rd: &Reg,
7294        dn: &VfpReg,
7295        dm: &VfpReg,
7296        cond_code: u32,
7297    ) -> Result<Vec<u8>> {
7298        let mut bytes = Vec::new();
7299        let rd_bits = reg_to_bits(rd);
7300
7301        // #712-class fix (found at f64-phase-2 wiring, #369): the 16-bit
7302        // `MOVS Rd,#0` is FLAG-SETTING. The original order emitted it AFTER
7303        // `VMRS APSR_nzcv, FPSCR`, clobbering the N/Z/C/V flags the VMRS just
7304        // transferred, so the following `IT<cond>` read stale flags and every
7305        // f64 comparison silently returned 0 — the exact bug the f32 compare
7306        // encoder shipped with and #712 fixed. Same fix: materialize the `#0`
7307        // FIRST (its flag side effect is overwritten by the VMRS), then
7308        // VCMP+VMRS set the flags the IT consumes. Pure reorder — sizes
7309        // unchanged.
7310
7311        // MOVS Rd, #0
7312        if rd_bits < 8 {
7313            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
7314            bytes.extend_from_slice(&movs_zero.to_le_bytes());
7315        } else {
7316            let hw1: u16 = 0xF04F;
7317            let hw2: u16 = (rd_bits as u16) << 8;
7318            bytes.extend_from_slice(&hw1.to_le_bytes());
7319            bytes.extend_from_slice(&hw2.to_le_bytes());
7320        }
7321
7322        // VCMP.F64 Dn, Dm
7323        let dn_num = vfp_dreg_to_num(dn)?;
7324        let dm_num = vfp_dreg_to_num(dm)?;
7325        let (vd, d) = encode_dreg(dn_num);
7326        let (vm, m) = encode_dreg(dm_num);
7327        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7328        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7329
7330        // VMRS APSR_nzcv, FPSCR (sets the flags the IT consumes)
7331        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7332
7333        // IT<cond>
7334        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7335        bytes.extend_from_slice(&it.to_le_bytes());
7336
7337        // MOV Rd, #1
7338        if rd_bits < 8 {
7339            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7340            bytes.extend_from_slice(&mov_one.to_le_bytes());
7341        } else {
7342            let hw1: u16 = 0xF04F;
7343            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7344            bytes.extend_from_slice(&hw1.to_le_bytes());
7345            bytes.extend_from_slice(&hw2.to_le_bytes());
7346        }
7347
7348        Ok(bytes)
7349    }
7350
7351    /// Encode F64 constant load as Thumb-2: MOVW+MOVT (lo32 into R0) + MOVW+MOVT (hi32 into R12) + VMOV Dd, R0, R12
7352    fn encode_thumb_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
7353        let mut bytes = Vec::new();
7354        let bits = value.to_bits();
7355        let lo32 = bits as u32;
7356        let hi32 = (bits >> 32) as u32;
7357
7358        // MOVW R0, #lo16(lo32)
7359        let lo16 = lo32 & 0xFFFF;
7360        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(0, lo16)?);
7361
7362        // MOVT R0, #hi16(lo32)
7363        let hi16 = (lo32 >> 16) & 0xFFFF;
7364        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(0, hi16)?);
7365
7366        // MOVW R12, #lo16(hi32)
7367        let lo16 = hi32 & 0xFFFF;
7368        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
7369
7370        // MOVT R12, #hi16(hi32)
7371        let hi16 = (hi32 >> 16) & 0xFFFF;
7372        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
7373
7374        // VMOV Dd, R0, R12
7375        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
7376        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7377
7378        Ok(bytes)
7379    }
7380
7381    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as Thumb-2
7382    /// Encode i32 → f64 conversion as Thumb-2. The integer stages through the
7383    /// DESTINATION's own low S-alias (`S(2d)`) — allocator-owned by
7384    /// definition — never S0 (which may hold a live value; the previous
7385    /// pseudo-op's S0 staging was the #615 class). Also fixes the SWAPPED
7386    /// signed/unsigned VCVT bases (bit7 = 1 is SIGNED — the same swap the f32
7387    /// twin had; latent here because f64.convert_i32_* was decode-dropped
7388    /// until #369): clang-verified vcvt.f64.s32 d1,s2 = eeb8 1bc1,
7389    /// vcvt.f64.u32 d1,s2 = eeb8 1b41.
7390    fn encode_thumb_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7391        let dd_num = vfp_dreg_to_num(dd)?;
7392        if dd_num > 7 {
7393            return Err(synth_core::Error::synthesis(format!(
7394                "F64ConvertI32: destination {dd:?} has no S-register alias \
7395                 (D8..D15) — the selector allocates only D0..D7"
7396            )));
7397        }
7398        let mut bytes = Vec::new();
7399
7400        // VMOV S(2d), Rm — stage the integer in the destination's low word.
7401        let (vn_s, n_s) = encode_sreg(2 * dd_num);
7402        let rt = reg_to_bits(rm);
7403        let vmov = 0xEE000A10 | (vn_s << 16) | (rt << 12) | (n_s << 7);
7404        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7405
7406        // VCVT.F64.S32/U32 Dd, S(2d)
7407        let (vd, d) = encode_dreg(dd_num);
7408        let (vm, m) = encode_sreg(2 * dd_num);
7409        let base = if signed { 0xEEB80BC0 } else { 0xEEB80B40 };
7410        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7411        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7412
7413        Ok(bytes)
7414    }
7415
7416    /// Encode VCVT.F64.F32 Dd, Sm as Thumb-2
7417    fn encode_thumb_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7418        let dd_num = vfp_dreg_to_num(dd)?;
7419        let sm_num = vfp_sreg_to_num(sm)?;
7420        let (vd, d) = encode_dreg(dd_num);
7421        let (vm, m) = encode_sreg(sm_num);
7422
7423        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7424        Ok(vfp_to_thumb_bytes(vcvt))
7425    }
7426
7427    /// Encode VCVT.F32.F64 Sd, Dm (f32.demote_f64) as Thumb-2 — single
7428    /// instruction, round-to-nearest-even per FPSCR default, exactly WASM
7429    /// §4.3.3 demote (clang-verified: vcvt.f32.f64 s1,d2 = eef7 0bc2).
7430    fn encode_thumb_f32_demote_f64(&self, sd: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7431        let sd_num = vfp_sreg_to_num(sd)?;
7432        let dm_num = vfp_dreg_to_num(dm)?;
7433        let (vd, d) = encode_sreg(sd_num);
7434        let (vm, m) = encode_dreg(dm_num);
7435
7436        let vcvt = 0xEEB70BC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7437        Ok(vfp_to_thumb_bytes(vcvt))
7438    }
7439
7440    /// Encode f64 → i32 truncation as Thumb-2 (round-toward-zero VCVT).
7441    ///
7442    /// The 32-bit result stages through the SOURCE's own low S-alias, `S(2m)`,
7443    /// clobbering half of an operand the selector has already popped. The
7444    /// overlapping write is well-defined: VCVT reads its source operand before
7445    /// writing (compilers emit `vcvt.f32.f64 s0, d0` routinely).
7446    ///
7447    /// # The one precondition, and who actually provides it
7448    ///
7449    /// `dm` must be a DEAD TEMP — never a pinned param/local home. That is the
7450    /// whole safety argument, and it is worth naming the guarantor precisely
7451    /// (#946): **`select_with_stack`** provides it, by copying a home into a
7452    /// fresh D-temp first. Visible in the shipped output for
7453    /// `(func (param f64) (result i32) (i32.trunc_f64_s (local.get 0)))`:
7454    ///
7455    /// ```text
7456    ///   vmov r1, r2, d0     ; read the param out of its AAPCS-VFP home D0
7457    ///   vmov d1, r1, r2     ; ...into a fresh D-temp
7458    ///   vcvt.s32.f64 s2, d1 ; convert from the TEMP, staging into its own S2
7459    /// ```
7460    ///
7461    /// `InstructionSelector::select` / `select_default` do NOT provide it —
7462    /// `alloc_vfp_dreg` is a bare round-robin `(n + 1) % 16` with no liveness
7463    /// or home test. That path is not reachable from `synth compile`
7464    /// (`arm_backend.rs` calls `select_with_stack` exclusively; the only
7465    /// non-test caller of `select` is `examples/compile_add.rs`), so this is
7466    /// not a live miscompile — but a caller reaching that `pub` API directly
7467    /// gets no such guarantee.
7468    ///
7469    /// # What this deliberately does NOT claim
7470    ///
7471    /// An earlier version of this comment said the staging register is "never
7472    /// S0, which may hold an unrelated live value (the #615 class)". **That is
7473    /// false**, and measurably so: for
7474    /// `(func (result i32) (i32.trunc_f64_s (f64.const 3.7)))` the shipped
7475    /// compiler emits `vcvt.s32.f64 s0, d0`.
7476    ///
7477    /// It is also unnecessary. S0 is only dangerous as an *unrelated* scratch;
7478    /// here it is always the low half of `dm` itself, which the precondition
7479    /// above already makes dead. Naming a guard the code does not have — and
7480    /// does not need — invites a future reader to lean on it. The dead-temp
7481    /// precondition is the only thing holding this up.
7482    fn encode_thumb_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7483        let dm_num = vfp_dreg_to_num(dm)?;
7484        if dm_num > 7 {
7485            return Err(synth_core::Error::synthesis(format!(
7486                "I32TruncF64: source {dm:?} has no S-register alias \
7487                 (D8..D15) — the selector allocates only D0..D7"
7488            )));
7489        }
7490        let mut bytes = Vec::new();
7491
7492        // VCVT.S32/U32.F64 S(2m), Dm (clang-verified:
7493        // vcvt.s32.f64 s1,d2 = eefd 0bc2 ; vcvt.u32.f64 s1,d2 = eefc 0bc2)
7494        let (vm, m) = encode_dreg(dm_num);
7495        let (vd_s, d_s) = encode_sreg(2 * dm_num);
7496        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
7497        let vcvt = base | (d_s << 22) | (vd_s << 12) | (m << 5) | vm;
7498        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7499
7500        // VMOV Rd, S(2m)
7501        let rt = reg_to_bits(rd);
7502        let vmov = 0xEE100A10 | (vd_s << 16) | (rt << 12) | (d_s << 7);
7503        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7504
7505        Ok(bytes)
7506    }
7507
7508    /// Encode F64 rounding as a SINGLE Thumb-2 VRINT (FPv5 / cortex-m7dp).
7509    /// `mode` keeps the legacy FPSCR-RMode numbering of the callers —
7510    /// 0b00=nearest(ties-to-even)→VRINTN, 0b01=+inf(ceil)→VRINTP,
7511    /// 0b10=-inf(floor)→VRINTM, 0b11=zero(trunc)→VRINTZ — but the rounding
7512    /// mode is now ENCODED in the instruction, not smuggled through FPSCR.
7513    /// (The previous pseudo-op round-tripped through a 32-bit integer in S0:
7514    /// wrong for |x| >= 2^31, NaN/±inf collapsed to 0, -0.0 lost, and it
7515    /// CLOBBERED S0/R12 behind the allocator's back — the #615 class.)
7516    /// VRINT quietens an sNaN and preserves the sign of ±0.0/NaN per IEEE 754
7517    /// roundToIntegral, which is exactly WASM Core §4.3.3 f64.ceil/floor/
7518    /// trunc/nearest. VRINTN/P/M live in the FE "always-execute" space (never
7519    /// IT-conditional; none of these sequences emits them inside an IT block).
7520    fn encode_thumb_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7521        let dd_num = vfp_dreg_to_num(dd)?;
7522        let dm_num = vfp_dreg_to_num(dm)?;
7523        let (vd, d) = encode_dreg(dd_num);
7524        let (vm, m) = encode_dreg(dm_num);
7525        // clang-verified bases (thumbv7em, fpv5-d16):
7526        //   vrintn.f64 d1,d2 = feb9 1b42 ; vrintp = feba 1b42
7527        //   vrintm.f64 d1,d2 = febb 1b42 ; vrintz = eeb6 1bc2
7528        let base: u32 = match mode {
7529            0b00 => 0xFEB90B40, // VRINTN.F64 (round to nearest, ties to even)
7530            0b01 => 0xFEBA0B40, // VRINTP.F64 (round toward +inf)
7531            0b10 => 0xFEBB0B40, // VRINTM.F64 (round toward -inf)
7532            _ => 0xEEB60BC0,    // VRINTZ.F64 (round toward zero)
7533        };
7534        Ok(vfp_to_thumb_bytes(
7535            base | (d << 22) | (vd << 12) | (m << 5) | vm,
7536        ))
7537    }
7538
7539    /// Encode F64 min/max as Thumb-2 with WASM Core §4.3.3 semantics:
7540    ///
7541    ///   VCMP.F64 Dn, Dm ; VMRS APSR_nzcv, FPSCR
7542    ///   VMINNM.F64/VMAXNM.F64 Dd, Dn, Dm      (FPv5; -0.0 < +0.0 ordered)
7543    ///   IT VS ; VADD.F64(VS) Dd, Dn, Dm       (unordered ⇒ NaN-propagating)
7544    ///
7545    /// VMINNM/VMAXNM alone are IEEE minNum/maxNum, which return the NUMBER
7546    /// when exactly one operand is NaN — WASM requires NaN. The VS-guarded
7547    /// VADD overwrites the result with a quiet NaN whenever the compare was
7548    /// unordered (either operand NaN); on the ordered path VMINNM/VMAXNM
7549    /// order -0.0 below +0.0, matching WASM's min(+0,-0) = -0 / max = +0.
7550    /// Clobbers ONLY Dd and the flags (the previous pseudo-op's ordered IT
7551    /// GT/MI select returned the WRONG operand for NaN and ±0 mixes).
7552    ///
7553    /// Ok-or-Err: `dd` must not alias `dn`/`dm` — the VS fix-up reads them
7554    /// AFTER VMINNM wrote `dd` (the selector always allocates a fresh
7555    /// destination while both sources are still marked live).
7556    fn encode_thumb_f64_minmax(
7557        &self,
7558        dd: &VfpReg,
7559        dn: &VfpReg,
7560        dm: &VfpReg,
7561        is_min: bool,
7562    ) -> Result<Vec<u8>> {
7563        if dd == dn || dd == dm {
7564            return Err(synth_core::Error::synthesis(format!(
7565                "F64{}: destination {dd:?} aliases a source ({dn:?},{dm:?}) — \
7566                 the unordered NaN fix-up would read a clobbered operand \
7567                 (compiler bug: the selector must allocate a fresh D-temp)",
7568                if is_min { "Min" } else { "Max" },
7569            )));
7570        }
7571        let mut bytes = Vec::new();
7572        let dd_num = vfp_dreg_to_num(dd)?;
7573        let dn_num = vfp_dreg_to_num(dn)?;
7574        let dm_num = vfp_dreg_to_num(dm)?;
7575        let (vd, d) = encode_dreg(dd_num);
7576        let (vn, n) = encode_dreg(dn_num);
7577        let (vm, m) = encode_dreg(dm_num);
7578
7579        // VCMP.F64 Dn, Dm (clang-verified: vcmp.f64 d2,d3 = eeb4 2b43)
7580        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7581        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7582        // VMRS APSR_nzcv, FPSCR
7583        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7584        // VMINNM.F64 / VMAXNM.F64 Dd, Dn, Dm (clang-verified:
7585        // vminnm.f64 d1,d2,d3 = fe82 1b43 ; vmaxnm = fe82 1b03)
7586        let base: u32 = if is_min { 0xFE800B40 } else { 0xFE800B00 };
7587        let vnm = base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7588        bytes.extend_from_slice(&vfp_to_thumb_bytes(vnm));
7589        // IT VS (unordered ⇒ at least one NaN operand)
7590        bytes.extend_from_slice(&0xBF68_u16.to_le_bytes());
7591        // VADD.F64(VS) Dd, Dn, Dm — NaN + x propagates a quiet NaN
7592        let vadd = 0xEE300B00 | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7593        bytes.extend_from_slice(&vfp_to_thumb_bytes(vadd));
7594
7595        Ok(bytes)
7596    }
7597
7598    /// Encode F64 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7599    /// encoder scratch, #212), the flags, and Dd:
7600    ///
7601    ///   VMOV R12, S(2m+1)   (high word of the SIGN source Dm)
7602    ///   CMP  R12, #0        (N flag = the sign bit)
7603    ///   VABS.F64 Dd, Dn     (magnitude, sign cleared)
7604    ///   IT MI ; VNEG.F64(MI) Dd, Dd
7605    ///
7606    /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7607    /// The R12 capture happens BEFORE Dd is written, so Dd aliasing Dn or Dm
7608    /// is safe. (The previous pseudo-op clobbered R0/R1/R2 behind the
7609    /// allocator's back — the #615 class.)
7610    fn encode_thumb_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7611        let dm_num = vfp_dreg_to_num(dm)?;
7612        if dm_num > 7 {
7613            return Err(synth_core::Error::synthesis(format!(
7614                "F64Copysign: sign source {dm:?} has no S-register alias \
7615                 (D8..D15) — the selector allocates only D0..D7"
7616            )));
7617        }
7618        let mut bytes = Vec::new();
7619        // VMOV R12, S(2m+1) — the sign source's high word.
7620        let (vn_s, n_s) = encode_sreg(2 * dm_num + 1);
7621        let vmov = 0xEE100A10 | (vn_s << 16) | (12 << 12) | (n_s << 7);
7622        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7623        // CMP R12, #0 (T2: CMP.W R12, #0) — N = bit31 of the sign word.
7624        bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7625        bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7626        // VABS.F64 Dd, Dn
7627        let dd_num = vfp_dreg_to_num(dd)?;
7628        let dn_num = vfp_dreg_to_num(dn)?;
7629        let (vd, d) = encode_dreg(dd_num);
7630        let (vn, n) = encode_dreg(dn_num);
7631        let vabs = 0xEEB00BC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7632        bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7633        // IT MI ; VNEG.F64(MI) Dd, Dd
7634        bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7635        let vneg = 0xEEB10B40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7636        bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7637
7638        Ok(bytes)
7639    }
7640
7641    /// Encode VCVT.S32/U32.F32 + VMOV as Thumb-2
7642    fn encode_thumb_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7643        let mut bytes = Vec::new();
7644
7645        let sm_num = vfp_sreg_to_num(sm)?;
7646        let (vd, d) = encode_sreg(sm_num);
7647        let (vm, m) = encode_sreg(sm_num);
7648        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
7649        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7650        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7651
7652        // VMOV Rd, Sm
7653        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
7654        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7655
7656        Ok(bytes)
7657    }
7658
7659    // === Thumb-2 32-bit encoding helpers ===
7660
7661    /// Encode Thumb-2 32-bit ADD with immediate
7662    fn encode_thumb32_add(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7663        let rd_bits = reg_to_bits(rd);
7664        let rn_bits = reg_to_bits(rn);
7665
7666        // The `i:imm3:imm8` field is split the same way for both forms.
7667        let i_bit = (imm >> 11) & 1;
7668        let imm3 = (imm >> 8) & 0x7;
7669        let imm8 = imm & 0xFF;
7670
7671        let hw1_base = if imm <= 0xFF {
7672            // ADD.W (T3): the field is a ThumbExpandImm modified immediate. For
7673            // imm <= 0xFF (i:imm3 = 0000) it is the zero-extended byte, which is
7674            // correct — keep this form so existing encodings stay bit-identical.
7675            0xF100
7676        } else if imm <= 0xFFF {
7677            // ADDW (T4): a PLAIN 12-bit immediate (0..4095) — no ThumbExpandImm.
7678            // This is what makes `add sp, sp, #frame` correct for frame sizes
7679            // >= 256, which ADD.W (T3) would silently mis-encode (e.g. #256 -> #0).
7680            0xF200
7681        } else {
7682            return Err(synth_core::Error::synthesis(
7683                "ADD immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7684            ));
7685        };
7686
7687        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7688        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7689
7690        let mut bytes = hw1.to_le_bytes().to_vec();
7691        bytes.extend_from_slice(&hw2.to_le_bytes());
7692        Ok(bytes)
7693    }
7694
7695    /// Encode Thumb-2 32-bit SUB with immediate
7696    fn encode_thumb32_sub(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7697        let rd_bits = reg_to_bits(rd);
7698        let rn_bits = reg_to_bits(rn);
7699
7700        let i_bit = (imm >> 11) & 1;
7701        let imm3 = (imm >> 8) & 0x7;
7702        let imm8 = imm & 0xFF;
7703
7704        let hw1_base = if imm <= 0xFF {
7705            // SUB.W (T3) modified immediate — correct for the zero-extended byte
7706            // (imm <= 0xFF). Kept bit-identical for existing encodings.
7707            0xF1A0
7708        } else if imm <= 0xFFF {
7709            // SUBW (T4): plain 12-bit immediate (0..4095). Makes
7710            // `sub sp, sp, #frame` correct for frame sizes >= 256.
7711            0xF2A0
7712        } else {
7713            return Err(synth_core::Error::synthesis(
7714                "SUB immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7715            ));
7716        };
7717
7718        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7719        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7720
7721        let mut bytes = hw1.to_le_bytes().to_vec();
7722        bytes.extend_from_slice(&hw2.to_le_bytes());
7723        Ok(bytes)
7724    }
7725
7726    /// Encode Thumb-2 32-bit ADDS with immediate (sets flags)
7727    fn encode_thumb32_adds(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7728        let rd_bits = reg_to_bits(rd);
7729        let rn_bits = reg_to_bits(rn);
7730
7731        // ADDS.W (flag-setting) has only the modified-immediate form — error on
7732        // an un-encodable value rather than silently add the wrong constant.
7733        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7734            synth_core::Error::synthesis(
7735                "ADDS immediate is not a valid ThumbExpandImm — materialize into a register",
7736            )
7737        })?;
7738        let i_bit = (field >> 11) & 1;
7739        let imm3 = (field >> 8) & 0x7;
7740        let imm8 = field & 0xFF;
7741
7742        // ADDS.W Rd, Rn, #imm (with S=1)
7743        // First halfword: 1111 0 i 0 1000 1 Rn = F110 | i<<10 | Rn
7744        let hw1: u16 = (0xF110 | (i_bit << 10) | rn_bits) as u16;
7745        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7746
7747        let mut bytes = hw1.to_le_bytes().to_vec();
7748        bytes.extend_from_slice(&hw2.to_le_bytes());
7749        Ok(bytes)
7750    }
7751
7752    /// Encode Thumb-2 32-bit SUBS with immediate (sets flags)
7753    fn encode_thumb32_subs(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7754        let rd_bits = reg_to_bits(rd);
7755        let rn_bits = reg_to_bits(rn);
7756
7757        // SUBS.W (flag-setting) has only the modified-immediate form — error on
7758        // an un-encodable value rather than silently subtract the wrong constant.
7759        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7760            synth_core::Error::synthesis(
7761                "SUBS immediate is not a valid ThumbExpandImm — materialize into a register",
7762            )
7763        })?;
7764        let i_bit = (field >> 11) & 1;
7765        let imm3 = (field >> 8) & 0x7;
7766        let imm8 = field & 0xFF;
7767
7768        // SUBS.W Rd, Rn, #imm (with S=1)
7769        // First halfword: 1111 0 i 0 1101 1 Rn = F1B0 | i<<10 | Rn
7770        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7771        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7772
7773        let mut bytes = hw1.to_le_bytes().to_vec();
7774        bytes.extend_from_slice(&hw2.to_le_bytes());
7775        Ok(bytes)
7776    }
7777
7778    /// Encode Thumb-2 32-bit MOVW (16-bit immediate)
7779    ///
7780    /// # Contract (Verus-style)
7781    /// ```text
7782    /// requires rd <= R14
7783    /// ensures result.len() == 4
7784    /// ensures (imm & 0xFFFF) can be reconstructed from the encoding
7785    /// ```
7786    fn encode_thumb32_movw(&self, rd: &Reg, imm: u32) -> Result<Vec<u8>> {
7787        let rd_bits = reg_to_bits(rd);
7788        reg_bits_checked(rd_bits)?;
7789        let imm16 = imm & 0xFFFF;
7790
7791        // MOVW Rd, #imm16
7792        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7793        let imm4 = (imm16 >> 12) & 0xF;
7794        let i_bit = (imm16 >> 11) & 1;
7795        let imm3 = (imm16 >> 8) & 0x7;
7796        let imm8 = imm16 & 0xFF;
7797
7798        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7799        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7800
7801        let mut bytes = hw1.to_le_bytes().to_vec();
7802        bytes.extend_from_slice(&hw2.to_le_bytes());
7803        encoding_contracts::verify_thumb32(&bytes);
7804        Ok(bytes)
7805    }
7806
7807    /// Encode Thumb-2 32-bit shift with immediate
7808    ///
7809    /// # Contract (Verus-style)
7810    /// ```text
7811    /// requires rd <= R14, rm <= R14
7812    /// ensures result.len() == 4
7813    /// ```
7814    fn encode_thumb32_shift(
7815        &self,
7816        rd: &Reg,
7817        rm: &Reg,
7818        shift: u32,
7819        shift_type: u8,
7820    ) -> Result<Vec<u8>> {
7821        let rd_bits = reg_to_bits(rd);
7822        let rm_bits = reg_to_bits(rm);
7823        reg_bits_checked(rd_bits)?;
7824        reg_bits_checked(rm_bits)?;
7825        let imm5 = shift & 0x1F;
7826        let imm2 = imm5 & 0x3;
7827        let imm3 = (imm5 >> 2) & 0x7;
7828
7829        // MOV.W Rd, Rm, <shift> #imm
7830        // EA4F 0 imm3 Rd imm2 type Rm
7831        let hw1: u16 = 0xEA4F;
7832        let hw2: u16 =
7833            ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | ((shift_type as u32) << 4) | rm_bits)
7834                as u16;
7835
7836        let mut bytes = hw1.to_le_bytes().to_vec();
7837        bytes.extend_from_slice(&hw2.to_le_bytes());
7838        Ok(bytes)
7839    }
7840
7841    /// Encode Thumb-2 32-bit shift by register
7842    /// Encoding: 11111010 0xx0 Rn | 1111 Rd 0000 Rm
7843    /// shift_type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
7844    fn encode_thumb32_shift_reg(
7845        &self,
7846        rd: &Reg,
7847        rn: &Reg,
7848        rm: &Reg,
7849        shift_type: u8,
7850    ) -> Result<Vec<u8>> {
7851        let rd_bits = reg_to_bits(rd);
7852        let rn_bits = reg_to_bits(rn);
7853        let rm_bits = reg_to_bits(rm);
7854
7855        // hw1: 1111 1010 0xx0 Rn
7856        let hw1: u16 = (0xFA00 | ((shift_type as u32) << 5) | rn_bits) as u16;
7857        // hw2: 1111 Rd 0000 Rm
7858        let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
7859
7860        let mut bytes = hw1.to_le_bytes().to_vec();
7861        bytes.extend_from_slice(&hw2.to_le_bytes());
7862        Ok(bytes)
7863    }
7864
7865    /// Encode Thumb-2 32-bit CMP with immediate
7866    fn encode_thumb32_cmp_imm(&self, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7867        let rn_bits = reg_to_bits(rn);
7868
7869        // CMP.W has only the modified-immediate form (no plain-imm12 like ADDW),
7870        // so an un-encodable immediate MUST be materialized into a register by
7871        // the selector. Error rather than silently compare the wrong constant.
7872        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7873            synth_core::Error::synthesis(
7874                "CMP immediate is not a valid ThumbExpandImm — materialize into a register",
7875            )
7876        })?;
7877        let i_bit = (field >> 11) & 1;
7878        let imm3 = (field >> 8) & 0x7;
7879        let imm8 = field & 0xFF;
7880
7881        // CMP.W Rn, #imm
7882        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7883        let hw2: u16 = ((imm3 << 12) | 0x0F00 | imm8) as u16;
7884
7885        let mut bytes = hw1.to_le_bytes().to_vec();
7886        bytes.extend_from_slice(&hw2.to_le_bytes());
7887        Ok(bytes)
7888    }
7889
7890    /// #372/#382: resolve the base register AND residual immediate offset for an
7891    /// `I64Ldr`/`I64Str` whose address may carry an index register. Returns
7892    /// `(base, low_offset)`; the caller accesses the halves at `[base,
7893    /// #low_offset]` and `[base, #low_offset + 4]`.
7894    ///
7895    /// - Frame access (no `offset_reg`, e.g. a spilled local at `[SP, #off]`):
7896    ///   returns `(addr.base, off)` and emits NOTHING — byte-identical.
7897    /// - Memory access (`reg_imm(R11, addr, offset)` = `R11 + addr + offset`)
7898    ///   with `offset + 4 <= 0xFFF`: emits `ADD.W ip, base, index` and returns
7899    ///   `(ip, offset)`, folding `offset`/`offset+4` into the halves' imm12.
7900    ///   Byte-identical to the pre-#382 (#372) behavior.
7901    /// - Memory access with `offset + 4 > 0xFFF`: the imm12 form cannot hold the
7902    ///   high half's offset, so `encode_thumb32_ldr`'s `check_ldst_imm12` (#259)
7903    ///   rightly refused it and the WHOLE function was skipped (#382). Instead
7904    ///   MATERIALIZE the offset into the base: `ADD ip, index, #offset` (against
7905    ///   the read-only INDEX register, so `encode_thumb32_add_imm` never trips its
7906    ///   `rd==rn==R12` alias trap), then `ADD.W ip, ip, base` (+ R11), and return
7907    ///   `(ip, 0)` so the halves use `[ip, #0]` / `[ip, #4]`.
7908    ///
7909    /// The effective address is fully materialized into `ip` BEFORE the halves
7910    /// are accessed, so an `rdlo` aliasing the index register is safe.
7911    ///
7912    /// RQ-63-ARMI64OFF (#1165): the wasm memarg is unsigned and reaches the
7913    /// encoder through the selector's `as i32` cast, so a memarg `>= 2^31`
7914    /// arrives NEGATIVE. It is read back as `u32` (lossless) and the
7915    /// materialization carries the full 32 bits — `base + index + offset (mod
7916    /// 2^32)`, the arithmetic the Thumb-2 word path (`encode_thumb32_add_imm`)
7917    /// and the software bounds guard already use for the same memarg. This
7918    /// used to CLAMP a negative offset to 0, so `i64.load offset=0xfffffff8`
7919    /// silently read `[R11 + addr]` — a wrong address, and a divergence from
7920    /// the i32 form of the same memarg. A FRAME access (no index) with an
7921    /// out-of-range or negative offset is refused by `check_ldst_imm12` in the
7922    /// halves — loud, never clamped.
7923    fn i64_effective_base(&self, bytes: &mut Vec<u8>, addr: &MemAddr) -> Result<(Reg, u32)> {
7924        let offset = addr.offset as u32;
7925        match addr.offset_reg {
7926            Some(idx) => {
7927                let ip = Reg::R12;
7928                // The high half sits at +4, which must itself fit imm12.
7929                if offset > 0xFFB {
7930                    // Large static offset (#382): fold it (and R11) into ip so the
7931                    // imm12 halves stay in range instead of skipping the function.
7932                    // ADD ip, index, #offset  (index != ip → no add_imm alias trap)
7933                    bytes.extend_from_slice(&self.encode_thumb32_add_imm(&ip, &idx, offset)?);
7934                    // ADD.W ip, ip, base  (+ R11)
7935                    bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
7936                        reg_to_bits(&ip),
7937                        reg_to_bits(&ip),
7938                        reg_to_bits(&addr.base),
7939                    )?);
7940                    Ok((ip, 0))
7941                } else {
7942                    // ADD.W ip, addr.base, idx  (Thumb-2, byte-verified vs as)
7943                    let hw1: u16 = 0xEB00 | reg_to_bits(&addr.base) as u16;
7944                    let hw2: u16 = 0x0C00 | reg_to_bits(&idx) as u16;
7945                    bytes.extend_from_slice(&hw1.to_le_bytes());
7946                    bytes.extend_from_slice(&hw2.to_le_bytes());
7947                    Ok((ip, offset))
7948                }
7949            }
7950            None => Ok((addr.base, offset)),
7951        }
7952    }
7953
7954    /// Encode Thumb-2 32-bit LDR
7955    fn encode_thumb32_ldr(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7956        let rd_bits = reg_to_bits(rd);
7957        let base_bits = reg_to_bits(base);
7958
7959        // LDR.W Rd, [Rn, #imm12]
7960        check_ldst_imm12(offset)?;
7961        let hw1: u16 = (0xF8D0 | base_bits) as u16;
7962        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7963
7964        let mut bytes = hw1.to_le_bytes().to_vec();
7965        bytes.extend_from_slice(&hw2.to_le_bytes());
7966        Ok(bytes)
7967    }
7968
7969    /// Encode Thumb-2 32-bit STR
7970    fn encode_thumb32_str(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7971        let rd_bits = reg_to_bits(rd);
7972        let base_bits = reg_to_bits(base);
7973
7974        // STR.W Rd, [Rn, #imm12]
7975        check_ldst_imm12(offset)?;
7976        let hw1: u16 = (0xF8C0 | base_bits) as u16;
7977        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7978
7979        let mut bytes = hw1.to_le_bytes().to_vec();
7980        bytes.extend_from_slice(&hw2.to_le_bytes());
7981        Ok(bytes)
7982    }
7983
7984    /// Encode Thumb-2 32-bit LDR with register offset: LDR.W Rd, [Rn, Rm]
7985    fn encode_thumb32_ldr_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7986        let rd_bits = reg_to_bits(rd);
7987        let base_bits = reg_to_bits(base);
7988        let rm_bits = reg_to_bits(offset_reg);
7989
7990        // LDR.W Rd, [Rn, Rm, LSL #0]
7991        // Encoding: 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
7992        // imm2 = 00 for no shift (LSL #0)
7993        let hw1: u16 = (0xF850 | base_bits) as u16;
7994        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7995
7996        let mut bytes = hw1.to_le_bytes().to_vec();
7997        bytes.extend_from_slice(&hw2.to_le_bytes());
7998        Ok(bytes)
7999    }
8000
8001    /// Encode Thumb-2 32-bit STR with register offset: STR.W Rd, [Rn, Rm]
8002    fn encode_thumb32_str_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8003        let rd_bits = reg_to_bits(rd);
8004        let base_bits = reg_to_bits(base);
8005        let rm_bits = reg_to_bits(offset_reg);
8006
8007        // STR.W Rd, [Rn, Rm, LSL #0]
8008        // Encoding: 1111 1000 0100 Rn | Rt 0000 00 imm2 Rm
8009        // imm2 = 00 for no shift (LSL #0)
8010        let hw1: u16 = (0xF840 | base_bits) as u16;
8011        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8012
8013        let mut bytes = hw1.to_le_bytes().to_vec();
8014        bytes.extend_from_slice(&hw2.to_le_bytes());
8015        Ok(bytes)
8016    }
8017
8018    // === Sub-word load/store Thumb-2 encoding helpers ===
8019
8020    /// Encode Thumb-2 32-bit LDRB with immediate: LDRB.W Rd, [Rn, #imm12]
8021    fn encode_thumb32_ldrb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8022        let rd_bits = reg_to_bits(rd);
8023        let base_bits = reg_to_bits(base);
8024        // LDRB.W Rd, [Rn, #imm12]: 1111 1000 1001 Rn | Rt imm12
8025        check_ldst_imm12(offset)?;
8026        let hw1: u16 = (0xF890 | base_bits) as u16;
8027        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8028        let mut bytes = hw1.to_le_bytes().to_vec();
8029        bytes.extend_from_slice(&hw2.to_le_bytes());
8030        Ok(bytes)
8031    }
8032
8033    /// Encode Thumb-2 32-bit LDRB with register: LDRB.W Rd, [Rn, Rm]
8034    fn encode_thumb32_ldrb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8035        let rd_bits = reg_to_bits(rd);
8036        let base_bits = reg_to_bits(base);
8037        let rm_bits = reg_to_bits(offset_reg);
8038        // LDRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0001 Rn | Rt 0000 00 imm2 Rm
8039        let hw1: u16 = (0xF810 | base_bits) as u16;
8040        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8041        let mut bytes = hw1.to_le_bytes().to_vec();
8042        bytes.extend_from_slice(&hw2.to_le_bytes());
8043        Ok(bytes)
8044    }
8045
8046    /// Encode Thumb-2 32-bit LDRSB with immediate: LDRSB.W Rd, [Rn, #imm12]
8047    fn encode_thumb32_ldrsb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8048        let rd_bits = reg_to_bits(rd);
8049        let base_bits = reg_to_bits(base);
8050        // LDRSB.W Rd, [Rn, #imm12]: 1111 1001 1001 Rn | Rt imm12
8051        check_ldst_imm12(offset)?;
8052        let hw1: u16 = (0xF990 | base_bits) as u16;
8053        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8054        let mut bytes = hw1.to_le_bytes().to_vec();
8055        bytes.extend_from_slice(&hw2.to_le_bytes());
8056        Ok(bytes)
8057    }
8058
8059    /// Encode Thumb-2 32-bit LDRSB with register: LDRSB.W Rd, [Rn, Rm]
8060    fn encode_thumb32_ldrsb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8061        let rd_bits = reg_to_bits(rd);
8062        let base_bits = reg_to_bits(base);
8063        let rm_bits = reg_to_bits(offset_reg);
8064        // LDRSB.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0001 Rn | Rt 0000 00 imm2 Rm
8065        let hw1: u16 = (0xF910 | base_bits) as u16;
8066        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8067        let mut bytes = hw1.to_le_bytes().to_vec();
8068        bytes.extend_from_slice(&hw2.to_le_bytes());
8069        Ok(bytes)
8070    }
8071
8072    /// Encode Thumb-2 32-bit LDRH with immediate: LDRH.W Rd, [Rn, #imm12]
8073    fn encode_thumb32_ldrh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8074        let rd_bits = reg_to_bits(rd);
8075        let base_bits = reg_to_bits(base);
8076        // LDRH.W Rd, [Rn, #imm12]: 1111 1000 1011 Rn | Rt imm12
8077        check_ldst_imm12(offset)?;
8078        let hw1: u16 = (0xF8B0 | base_bits) as u16;
8079        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8080        let mut bytes = hw1.to_le_bytes().to_vec();
8081        bytes.extend_from_slice(&hw2.to_le_bytes());
8082        Ok(bytes)
8083    }
8084
8085    /// Encode Thumb-2 32-bit LDRH with register: LDRH.W Rd, [Rn, Rm]
8086    fn encode_thumb32_ldrh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8087        let rd_bits = reg_to_bits(rd);
8088        let base_bits = reg_to_bits(base);
8089        let rm_bits = reg_to_bits(offset_reg);
8090        // LDRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0011 Rn | Rt 0000 00 imm2 Rm
8091        let hw1: u16 = (0xF830 | base_bits) as u16;
8092        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8093        let mut bytes = hw1.to_le_bytes().to_vec();
8094        bytes.extend_from_slice(&hw2.to_le_bytes());
8095        Ok(bytes)
8096    }
8097
8098    /// Encode Thumb-2 32-bit LDRSH with immediate: LDRSH.W Rd, [Rn, #imm12]
8099    fn encode_thumb32_ldrsh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8100        let rd_bits = reg_to_bits(rd);
8101        let base_bits = reg_to_bits(base);
8102        // LDRSH.W Rd, [Rn, #imm12]: 1111 1001 1011 Rn | Rt imm12
8103        check_ldst_imm12(offset)?;
8104        let hw1: u16 = (0xF9B0 | base_bits) as u16;
8105        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8106        let mut bytes = hw1.to_le_bytes().to_vec();
8107        bytes.extend_from_slice(&hw2.to_le_bytes());
8108        Ok(bytes)
8109    }
8110
8111    /// Encode Thumb-2 32-bit LDRSH with register: LDRSH.W Rd, [Rn, Rm]
8112    fn encode_thumb32_ldrsh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8113        let rd_bits = reg_to_bits(rd);
8114        let base_bits = reg_to_bits(base);
8115        let rm_bits = reg_to_bits(offset_reg);
8116        // LDRSH.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0011 Rn | Rt 0000 00 imm2 Rm
8117        let hw1: u16 = (0xF930 | base_bits) as u16;
8118        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8119        let mut bytes = hw1.to_le_bytes().to_vec();
8120        bytes.extend_from_slice(&hw2.to_le_bytes());
8121        Ok(bytes)
8122    }
8123
8124    /// Encode Thumb-2 32-bit STRB with immediate: STRB.W Rd, [Rn, #imm12]
8125    fn encode_thumb32_strb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8126        let rd_bits = reg_to_bits(rd);
8127        let base_bits = reg_to_bits(base);
8128        // STRB.W Rd, [Rn, #imm12]: 1111 1000 1000 Rn | Rt imm12
8129        check_ldst_imm12(offset)?;
8130        let hw1: u16 = (0xF880 | base_bits) as u16;
8131        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8132        let mut bytes = hw1.to_le_bytes().to_vec();
8133        bytes.extend_from_slice(&hw2.to_le_bytes());
8134        Ok(bytes)
8135    }
8136
8137    /// Encode Thumb-2 32-bit STRB with register: STRB.W Rd, [Rn, Rm]
8138    fn encode_thumb32_strb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8139        let rd_bits = reg_to_bits(rd);
8140        let base_bits = reg_to_bits(base);
8141        let rm_bits = reg_to_bits(offset_reg);
8142        // STRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0000 Rn | Rt 0000 00 imm2 Rm
8143        let hw1: u16 = (0xF800 | base_bits) as u16;
8144        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8145        let mut bytes = hw1.to_le_bytes().to_vec();
8146        bytes.extend_from_slice(&hw2.to_le_bytes());
8147        Ok(bytes)
8148    }
8149
8150    /// Encode Thumb-2 32-bit STRH with immediate: STRH.W Rd, [Rn, #imm12]
8151    fn encode_thumb32_strh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8152        let rd_bits = reg_to_bits(rd);
8153        let base_bits = reg_to_bits(base);
8154        // STRH.W Rd, [Rn, #imm12]: 1111 1000 1010 Rn | Rt imm12
8155        check_ldst_imm12(offset)?;
8156        let hw1: u16 = (0xF8A0 | base_bits) as u16;
8157        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8158        let mut bytes = hw1.to_le_bytes().to_vec();
8159        bytes.extend_from_slice(&hw2.to_le_bytes());
8160        Ok(bytes)
8161    }
8162
8163    /// Encode Thumb-2 32-bit STRH with register: STRH.W Rd, [Rn, Rm]
8164    fn encode_thumb32_strh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8165        let rd_bits = reg_to_bits(rd);
8166        let base_bits = reg_to_bits(base);
8167        let rm_bits = reg_to_bits(offset_reg);
8168        // STRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0010 Rn | Rt 0000 00 imm2 Rm
8169        let hw1: u16 = (0xF820 | base_bits) as u16;
8170        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8171        let mut bytes = hw1.to_le_bytes().to_vec();
8172        bytes.extend_from_slice(&hw2.to_le_bytes());
8173        Ok(bytes)
8174    }
8175
8176    /// Encode Thumb-2 32-bit ADD with immediate: ADD.W Rd, Rn, #imm
8177    fn encode_thumb32_add_imm(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
8178        let rd_bits = reg_to_bits(rd);
8179        let rn_bits = reg_to_bits(rn);
8180
8181        // In-range immediates (<= 0xFFF) delegate to `encode_thumb32_add`,
8182        // which picks the correct form per value:
8183        //   - imm <= 0xFF  -> ADD.W (T3). Its `i:imm3:imm8` field is a
8184        //     ThumbExpandImm MODIFIED immediate — raw == expanded only here.
8185        //   - 0x100..=0xFFF -> ADDW (T4, 0xF200): a PLAIN 12-bit immediate.
8186        //
8187        // #681: this function used to pack the raw value into the T3 field for
8188        // ALL imm <= 0xFFF. ThumbExpandImm(0x200) = 0 and ThumbExpandImm(0x400)
8189        // = 0x8000_0000, so every dynamic-address load/store with a static
8190        // offset in 0x100..=0xFFF silently computed a WRONG address — and in
8191        // --safety-bounds software the guard checked the intended address while
8192        // the access used the mis-encoded one (bounds bypass). Same
8193        // ThumbExpandImm raw-packing class as #253/#255, reached via #382.
8194        if imm <= 0xFFF {
8195            self.encode_thumb32_add(rd, rn, imm)
8196        } else {
8197            // Out-of-range immediate (> 0xFFF): materialize it into a scratch
8198            // register, then ADD.W Rd, Rn, scratch. This is the #180/#185
8199            // "encoder must produce a legal sequence, not assert" class — see #350.
8200            //
8201            // Scratch choice (must NEVER equal Rn, or Rn would be clobbered before
8202            // the ADD reads it):
8203            //   - rd != rn  => use rd itself (rn is untouched, since rd != rn).
8204            //   - rd == rn  => use R12/IP (the reserved encoder scratch). rd/rn are
8205            //                  never R12 (R12 is non-allocatable), so it can't alias.
8206            //
8207            // The materialized value is the same whether or not MOVT is emitted, so
8208            // the byte length depends only on `imm` (and rd==rn) — the size probe and
8209            // the final emit therefore agree (mandatory: the function is encoded twice).
8210            let scratch: u32 = if rd_bits == rn_bits {
8211                12 // R12/IP — in-place add, can't use rd because rd == rn
8212            } else {
8213                rd_bits // rn is preserved because rd != rn
8214            };
8215            // Invariant: the scratch must never alias Rn (would clobber it before
8216            // the ADD reads it). Unreachable in real codegen (rd/rn are never R12,
8217            // which is reserved encoder scratch), but the encoder is also driven by
8218            // the `encoder_no_panic` fuzz harness with ARBITRARY registers — incl.
8219            // rd==rn==R12, which makes scratch (R12) alias Rn. The encoder contract
8220            // (#180/#185) is Ok-or-Err, never a panic, so return a typed error
8221            // instead of asserting. #350 follow-up.
8222            if scratch == rn_bits {
8223                return Err(synth_core::Error::synthesis(format!(
8224                    "ADD #imm: cannot lower #{imm:#x} for Rd==Rn==R12 — no free scratch \
8225                     register (R12 is the reserved encoder scratch and aliases Rn here)"
8226                )));
8227            }
8228
8229            let lo16 = imm & 0xFFFF;
8230            let hi16 = (imm >> 16) & 0xFFFF;
8231
8232            let mut bytes = self.encode_thumb32_movw_raw(scratch, lo16)?;
8233            if hi16 != 0 {
8234                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(scratch, hi16)?);
8235            }
8236            bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rn_bits, scratch)?);
8237            Ok(bytes)
8238        }
8239    }
8240
8241    // === Raw encoding helpers for POPCNT (take register numbers directly) ===
8242
8243    /// Encode Thumb-2 32-bit MOVW (16-bit immediate) - raw version
8244    ///
8245    /// # Contract (Verus-style)
8246    /// ```text
8247    /// requires rd <= 14, imm16 <= 0xFFFF
8248    /// ensures result.len() == 4
8249    /// ```
8250    fn encode_thumb32_movw_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8251        reg_bits_checked(rd)?;
8252        encoding_contracts::verify_imm16(imm16);
8253        // MOVW Rd, #imm16
8254        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
8255        let imm16 = imm16 & 0xFFFF;
8256        let imm4 = (imm16 >> 12) & 0xF;
8257        let i_bit = (imm16 >> 11) & 1;
8258        let imm3 = (imm16 >> 8) & 0x7;
8259        let imm8 = imm16 & 0xFF;
8260
8261        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
8262        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8263
8264        let mut bytes = hw1.to_le_bytes().to_vec();
8265        bytes.extend_from_slice(&hw2.to_le_bytes());
8266        encoding_contracts::verify_thumb32(&bytes);
8267        Ok(bytes)
8268    }
8269
8270    /// Encode Thumb-2 32-bit MOVT (move top 16 bits) - raw version
8271    ///
8272    /// # Contract (Verus-style)
8273    /// ```text
8274    /// requires rd <= 14, imm16 <= 0xFFFF
8275    /// ensures result.len() == 4
8276    /// ```
8277    fn encode_thumb32_movt_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8278        reg_bits_checked(rd)?;
8279        encoding_contracts::verify_imm16(imm16);
8280        // MOVT Rd, #imm16
8281        // 1111 0 i 10 1 1 0 0 imm4 | 0 imm3 Rd imm8
8282        let imm16 = imm16 & 0xFFFF;
8283        let imm4 = (imm16 >> 12) & 0xF;
8284        let i_bit = (imm16 >> 11) & 1;
8285        let imm3 = (imm16 >> 8) & 0x7;
8286        let imm8 = imm16 & 0xFF;
8287
8288        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
8289        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8290
8291        let mut bytes = hw1.to_le_bytes().to_vec();
8292        bytes.extend_from_slice(&hw2.to_le_bytes());
8293        encoding_contracts::verify_thumb32(&bytes);
8294        Ok(bytes)
8295    }
8296
8297    /// Encode Thumb-2 32-bit LSR (logical shift right) with immediate - raw version
8298    fn encode_thumb32_lsr_raw(&self, rd: u32, rm: u32, shift: u32) -> Result<Vec<u8>> {
8299        // MOV.W Rd, Rm, LSR #imm
8300        // EA4F 0 imm3 Rd imm2 01 Rm
8301        let imm5 = shift & 0x1F;
8302        let imm2 = imm5 & 0x3;
8303        let imm3 = (imm5 >> 2) & 0x7;
8304
8305        let hw1: u16 = 0xEA4F;
8306        let hw2: u16 = ((imm3 << 12) | (rd << 8) | (imm2 << 6) | (0b01 << 4) | rm) as u16;
8307
8308        let mut bytes = hw1.to_le_bytes().to_vec();
8309        bytes.extend_from_slice(&hw2.to_le_bytes());
8310        Ok(bytes)
8311    }
8312
8313    /// Encode Thumb-2 32-bit AND with immediate - raw version
8314    fn encode_thumb32_and_imm_raw(&self, rd: u32, rn: u32, imm: u32) -> Result<Vec<u8>> {
8315        // AND.W Rd, Rn, #<modified_immediate>
8316        // F0 00 Rn | 0 imm3 Rd imm8
8317        //
8318        // #681 class audit: the field is a ThumbExpandImm modified immediate,
8319        // not a raw value. The only current caller (POPCNT final mask) passes
8320        // 0x3F, which expands to itself — the gate is byte-identical today and
8321        // closes the raw-packing landmine for any future caller.
8322        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
8323            synth_core::Error::synthesis(
8324                "AND immediate is not a valid ThumbExpandImm — materialize into a register",
8325            )
8326        })?;
8327        let i_bit = (field >> 11) & 1;
8328        let imm3 = (field >> 8) & 0x7;
8329        let imm8 = field & 0xFF;
8330
8331        let hw1: u16 = (0xF000 | (i_bit << 10) | rn) as u16;
8332        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8333
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 SUB (register) - raw version
8340    fn encode_thumb32_sub_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8341        // SUB.W Rd, Rn, Rm
8342        // EBA0 Rn | 0 Rd 00 00 Rm
8343        let hw1: u16 = (0xEBA0 | rn) as u16;
8344        let hw2: u16 = ((rd << 8) | rm) as u16;
8345
8346        let mut bytes = hw1.to_le_bytes().to_vec();
8347        bytes.extend_from_slice(&hw2.to_le_bytes());
8348        Ok(bytes)
8349    }
8350
8351    /// Encode Thumb-2 32-bit ADD (register) - raw version
8352    fn encode_thumb32_add_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8353        // ADD.W Rd, Rn, Rm
8354        // EB00 Rn | 0 Rd 00 00 Rm
8355        let hw1: u16 = (0xEB00 | rn) as u16;
8356        let hw2: u16 = ((rd << 8) | rm) as u16;
8357
8358        let mut bytes = hw1.to_le_bytes().to_vec();
8359        bytes.extend_from_slice(&hw2.to_le_bytes());
8360        Ok(bytes)
8361    }
8362
8363    /// Encode Thumb-2 32-bit ADDS (register, flag-setting) - raw version.
8364    /// Used as the high-register fallback for `ArmOp::Adds` (i64 low-word add)
8365    /// so R8-R11 pair operands don't overflow the 16-bit field — #178/#180.
8366    fn encode_thumb32_adds_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8367        // ADDS.W Rd, Rn, Rm (T3, S=1): EB10 Rn | 0 Rd 00 00 Rm
8368        let hw1: u16 = (0xEB10 | rn) as u16;
8369        let hw2: u16 = ((rd << 8) | rm) as u16;
8370        let mut bytes = hw1.to_le_bytes().to_vec();
8371        bytes.extend_from_slice(&hw2.to_le_bytes());
8372        Ok(bytes)
8373    }
8374
8375    /// Encode Thumb-2 32-bit SUBS (register, flag-setting) - raw version.
8376    /// High-register fallback for `ArmOp::Subs` (i64 low-word subtract) — #178/#180.
8377    fn encode_thumb32_subs_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8378        // SUBS.W Rd, Rn, Rm (T3, S=1): EBB0 Rn | 0 Rd 00 00 Rm
8379        let hw1: u16 = (0xEBB0 | rn) as u16;
8380        let hw2: u16 = ((rd << 8) | rm) as u16;
8381        let mut bytes = hw1.to_le_bytes().to_vec();
8382        bytes.extend_from_slice(&hw2.to_le_bytes());
8383        Ok(bytes)
8384    }
8385
8386    /// Encode a sequence of ARM instructions
8387    pub fn encode_sequence(&self, ops: &[ArmOp]) -> Result<Vec<u8>> {
8388        let mut code = Vec::new();
8389
8390        for op in ops {
8391            let encoded = self.encode(op)?;
8392            code.extend_from_slice(&encoded);
8393        }
8394
8395        Ok(code)
8396    }
8397}
8398
8399/// Convert register to bit encoding (0-15)
8400/// Reverse of the ARMv7-M `ThumbExpandImm`: given a 32-bit immediate, return the
8401/// 12-bit `i:imm3:imm8` field if it is a representable modified immediate, else
8402/// `None` (the caller must materialize the value into a register). This is the
8403/// shared correct path for the data-processing immediate encoders — without it
8404/// they pack raw bits and silently mis-encode any value `> 0xFF` that isn't a
8405/// modified immediate (the silent-miscompile class behind #251/#253/#255).
8406fn try_thumb_expand_imm(value: u32) -> Option<u32> {
8407    // i:imm3 = 0000 → 8-bit value, zero-extended (00000000 00000000 00000000 XY).
8408    if value <= 0xFF {
8409        return Some(value);
8410    }
8411    let b0 = value & 0xFF; // byte 0
8412    let b1 = (value >> 8) & 0xFF; // byte 1
8413    // 0x00XY00XY (i:imm3 = 0001) — XY in bytes 0 and 2
8414    if value == (b0 << 16) | b0 {
8415        return Some(0x100 | b0);
8416    }
8417    // 0xXY00XY00 (i:imm3 = 0010) — XY in bytes 1 and 3
8418    if value == (b1 << 24) | (b1 << 8) {
8419        return Some(0x200 | b1);
8420    }
8421    // 0xXYXYXYXY (i:imm3 = 0011) — XY in all four bytes
8422    if value == (b0 << 24) | (b0 << 16) | (b0 << 8) | b0 {
8423        return Some(0x300 | b0);
8424    }
8425    // An 8-bit value with bit 7 set, rotated right by 8..=31. `rotate_left(rot)`
8426    // undoes the encoded right rotation; if the result is `1bbbbbbb` (0x80..=0xFF)
8427    // the value is representable. imm12[11:7] = rot, imm12[6:0] = low 7 bits.
8428    for rot in 8..=31u32 {
8429        let unrot = value.rotate_left(rot);
8430        if (0x80..=0xFF).contains(&unrot) {
8431            return Some((rot << 7) | (unrot & 0x7F));
8432        }
8433    }
8434    None
8435}
8436
8437/// Guard a Thumb-2 `LDR/STR Rd, [Rn, #imm12]` offset. The imm12 form supports
8438/// `0..=4095`; a larger offset must be materialized into a register by the
8439/// selector (register-offset addressing). Returning `Err` rather than silently
8440/// masking `offset & 0xFFF` closes the wrong-address miscompile class (#259,
8441/// the load/store sibling of #253/#255).
8442fn check_ldst_imm12(offset: u32) -> Result<()> {
8443    if offset > 0xFFF {
8444        Err(synth_core::Error::synthesis(
8445            "load/store immediate offset > 0xFFF (4095) — materialize the offset into a register",
8446        ))
8447    } else {
8448        Ok(())
8449    }
8450}
8451
8452/// #916 — emit `Rd = 0` in Thumb-2, correctly for EVERY destination register.
8453///
8454/// The 16-bit `MOVS Rd, #imm8` (T1) is `0010 0 Rd(3) imm8` — the Rd field is
8455/// **three bits**. For R8-R12 `reg_to_bits` yields 8..12, so `rd_bits << 8`
8456/// overflows into bit 11 and `0x2000 | 0x0800` is `0x2800` = `CMP r0, #0`:
8457/// not a move at all. The destination is never written (it keeps stale data)
8458/// and the flags are clobbered. Same class as #180 / H-CODE-9, and the same
8459/// defect #311 fixed for `I64SetCond`.
8460///
8461/// High registers therefore take the 32-bit `MOV.W Rd, #imm8` (T2,
8462/// `F04F 0000 | Rd<<8 | imm8`), whose Rd field is four bits. `MOV.W` with S=0
8463/// does not set flags, which is what these zero-fill sites want anyway.
8464///
8465/// **Callers with branches must consult [`thumb_zero_fill_halfwords`].** This
8466/// emits 1 halfword for R0-R7 and 2 for R8-R12; any branch whose target lies
8467/// PAST this instruction moves when it widens and its displacement has to be
8468/// derived rather than hard-coded. (A branch targeting this instruction's own
8469/// address is unaffected — an instruction cannot move itself.)
8470fn emit_thumb_zero_fill(bytes: &mut Vec<u8>, rd_bits: u32) {
8471    if rd_bits < 8 {
8472        let movs: u16 = 0x2000 | ((rd_bits as u16) << 8);
8473        bytes.extend_from_slice(&movs.to_le_bytes());
8474    } else {
8475        bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
8476        bytes.extend_from_slice(&((rd_bits as u16) << 8).to_le_bytes());
8477    }
8478}
8479
8480/// Halfword length of the encoding [`emit_thumb_zero_fill`] picks for
8481/// `rd_bits`. Branch displacements spanning the zero-fill derive from this so
8482/// the encoder cannot drift from itself (#916; the byte-size estimator mirrors
8483/// it in `synth_synthesis::estimate_arm_byte_size`, pinned by the #498
8484/// `estimator_encoder_agreement` oracle).
8485fn thumb_zero_fill_halfwords(rd_bits: u32) -> u16 {
8486    if rd_bits < 8 { 1 } else { 2 }
8487}
8488
8489fn reg_to_bits(reg: &Reg) -> u32 {
8490    match reg {
8491        Reg::R0 => 0,
8492        Reg::R1 => 1,
8493        Reg::R2 => 2,
8494        Reg::R3 => 3,
8495        Reg::R4 => 4,
8496        Reg::R5 => 5,
8497        Reg::R6 => 6,
8498        Reg::R7 => 7,
8499        Reg::R8 => 8,
8500        Reg::R9 => 9,
8501        Reg::R10 => 10,
8502        Reg::R11 => 11,
8503        Reg::R12 => 12,
8504        Reg::SP => 13,
8505        Reg::LR => 14,
8506        Reg::PC => 15,
8507    }
8508}
8509
8510// ======================================================================
8511// #610 — i64 fixed-ABI expansion wrappers.
8512//
8513// The hand-written multi-instruction i64 cores (rotl/rotr and the div/rem
8514// shift-subtract loops) compute in FIXED low registers. Before #610 the
8515// div/rem arms ignored their operand fields outright (hardcoded R0:R1 /
8516// R2:R3 in, result to R0:R1) and the rot arms used R3/R4 scratch that
8517// collided with selector-assigned registers — then restored the saved
8518// scratch OVER the result (`POP {R4}` with rd_lo == R4), so the op
8519// returned the caller's stale register: 0 for every input under qemu.
8520//
8521// These wrappers make each core honor its register parameters:
8522//   1. save R0-R3,
8523//   2. marshal the operand registers into the core's fixed input regs via
8524//      the stack (permutation-safe: every source is read before any fixed
8525//      register is written),
8526//   3. run the fixed-reg core (self-preserving for R4+; R12 is encoder
8527//      scratch and never allocatable, #212),
8528//   4. MOV the result pair from R0:R1 into the selector's rd pair,
8529//   5. restore R0-R3, skipping any register the result now occupies.
8530//
8531// All emitted lengths are register-independent so the optimized path's
8532// byte-size estimator (`estimate_arm_byte_size`, pinned by the
8533// estimator↔encoder agreement oracle #498/#511) stays a constant per op.
8534// ======================================================================
8535
8536/// Steps 1+2: `PUSH {R0-R3}`, then marshal `srcs` (operand registers, any of
8537/// R0-R12) into `R0..R<n>` via individual stack pushes. Sources are all read
8538/// before any destination register is written, so arbitrary source/target
8539/// permutations (including operands living in R0-R3) are safe.
8540fn emit_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8541    debug_assert!(srcs.len() <= 4);
8542    // PUSH {R0-R3} — save the caller-visible low registers.
8543    bytes.extend_from_slice(&0xB40Fu16.to_le_bytes());
8544    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8545    for src in srcs.iter().rev() {
8546        let rt = reg_to_bits(src) as u16;
8547        bytes.extend_from_slice(&0xF84Du16.to_le_bytes());
8548        bytes.extend_from_slice(&((rt << 12) | 0x0D04).to_le_bytes());
8549    }
8550    // POP {Ri} — Ri := srcs[i].
8551    for i in 0..srcs.len() as u16 {
8552        bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes());
8553    }
8554}
8555
8556/// Steps 4+5: move the core's R0:R1 result into the selector's rd pair, then
8557/// restore the R0-R3 saved by [`emit_i64_fixed_abi_entry`], skipping any
8558/// register the result now lives in (its saved caller word is discarded).
8559fn emit_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8560    let lo = reg_to_bits(rdlo);
8561    let hi = reg_to_bits(rdhi);
8562    if lo == 1 && hi == 0 {
8563        // A fully swapped pair would clobber one half in either MOV order.
8564        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8565        return Err(synth_core::Error::synthesis(
8566            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8567        ));
8568    }
8569    let mov16 = |bytes: &mut Vec<u8>, rd: u32, rm: u32| {
8570        let d = ((rd >> 3) & 1) as u16;
8571        bytes.extend_from_slice(
8572            &(0x4600u16 | (d << 7) | ((rm as u16) << 3) | ((rd & 7) as u16)).to_le_bytes(),
8573        );
8574    };
8575    if hi == 0 {
8576        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8577        mov16(bytes, lo, 0);
8578        mov16(bytes, hi, 1);
8579    } else {
8580        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8581        mov16(bytes, hi, 1);
8582        mov16(bytes, lo, 0);
8583    }
8584    for i in 0..4u32 {
8585        if i == lo || i == hi {
8586            // The result lives here — drop the saved caller word.
8587            bytes.extend_from_slice(&0xB001u16.to_le_bytes()); // ADD SP, #4
8588        } else {
8589            bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes()); // POP {Ri}
8590        }
8591    }
8592    Ok(())
8593}
8594
8595/// WASM `i64.div_*` / `i64.rem_*` by zero must trap, matching the i32 path's
8596/// cmp/bne/udf guard. Emitted after marshaling, when the divisor pair is in
8597/// R2:R3: `ORRS R12, R2, R3` — `BNE` over a `UDF #0` when nonzero.
8598fn emit_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8599    bytes.extend_from_slice(&0xEA52u16.to_le_bytes()); // ORRS.W R12, R2, R3
8600    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8601    bytes.extend_from_slice(&0xD100u16.to_le_bytes()); // BNE.N +0 (skip the UDF)
8602    bytes.extend_from_slice(&0xDE00u16.to_le_bytes()); // UDF #0 — divide by zero
8603}
8604
8605/// WASM `i64.div_s(INT64_MIN, -1)` must trap (Core §4.3.2 `idiv_s`: the
8606/// quotient +2^63 is unrepresentable), matching the i32 path's overflow
8607/// guard — #633: without it the core negated INT64_MIN onto itself and
8608/// silently returned INT64_MIN. Emitted after marshaling, when the dividend
8609/// pair is in R0:R1 and the divisor pair in R2:R3; R12 is encoder scratch.
8610///
8611/// div_s ONLY — `i64.rem_s(INT64_MIN, -1)` is defined as 0 and must NOT
8612/// trap (`irem_s`), so the I64RemS arm never calls this. 22 bytes,
8613/// register-independent (estimator contract, #498/#511).
8614fn emit_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8615    // AND.W R12, R2, R3 — R12 == 0xFFFFFFFF iff divisor == -1
8616    bytes.extend_from_slice(&0xEA02u16.to_le_bytes());
8617    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8618    // CMN.W R12, #1 — EQ iff both divisor words are all-ones
8619    bytes.extend_from_slice(&0xF11Cu16.to_le_bytes());
8620    bytes.extend_from_slice(&0x0F01u16.to_le_bytes());
8621    // BNE .no_trap
8622    bytes.extend_from_slice(&0xD105u16.to_le_bytes());
8623    // CMP R0, #0 — dividend lo word of INT64_MIN
8624    bytes.extend_from_slice(&0x2800u16.to_le_bytes());
8625    // BNE .no_trap
8626    bytes.extend_from_slice(&0xD103u16.to_le_bytes());
8627    // CMP.W R1, #0x80000000 — dividend hi word of INT64_MIN
8628    bytes.extend_from_slice(&0xF1B1u16.to_le_bytes());
8629    bytes.extend_from_slice(&0x4F00u16.to_le_bytes());
8630    // BNE .no_trap
8631    bytes.extend_from_slice(&0xD100u16.to_le_bytes());
8632    // UDF #0 — signed-division overflow
8633    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
8634    // .no_trap:
8635}
8636
8637// ======================================================================
8638// #615 — A32 (ARM-mode) twins of the #610 i64 fixed-ABI wrappers above.
8639// Identical register contract, A32 encodings: the multi-instruction i64
8640// cores (rotl/rotr, div/rem) compute in fixed low registers (value/dividend
8641// R0:R1, amount R2 / divisor R2:R3, result to R0:R1); the wrappers marshal
8642// the selector-assigned operand registers in and the result out, saving and
8643// restoring the caller-visible R0-R3 around the core.
8644// ======================================================================
8645
8646/// A32 steps 1+2: `STMDB SP!, {R0-R3}`, then marshal `srcs` into `R0..R<n>`
8647/// via individual stack pushes (`STR src, [SP, #-4]!` in reverse order, then
8648/// `LDR Ri, [SP], #4`). Every source is read before any fixed register is
8649/// written, so arbitrary source/target permutations are safe.
8650fn emit_a32_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8651    debug_assert!(srcs.len() <= 4);
8652    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8653    // PUSH {R0-R3} — save the caller-visible low registers.
8654    w(bytes, 0xE92D_000F);
8655    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8656    for src in srcs.iter().rev() {
8657        w(bytes, 0xE52D_0004 | (reg_to_bits(src) << 12));
8658    }
8659    // LDR Ri, [SP], #4 — Ri := srcs[i].
8660    for i in 0..srcs.len() as u32 {
8661        w(bytes, 0xE49D_0004 | (i << 12));
8662    }
8663}
8664
8665/// A32 steps 4+5: move the core's R0:R1 result into the selector's rd pair,
8666/// then restore the R0-R3 saved by [`emit_a32_i64_fixed_abi_entry`], skipping
8667/// any register the result now lives in (its saved caller word is discarded).
8668fn emit_a32_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8669    let lo = reg_to_bits(rdlo);
8670    let hi = reg_to_bits(rdhi);
8671    if lo == 1 && hi == 0 {
8672        // A fully swapped pair would clobber one half in either MOV order.
8673        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8674        return Err(synth_core::Error::synthesis(
8675            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8676        ));
8677    }
8678    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8679    let mov = |bytes: &mut Vec<u8>, rd: u32, rm: u32| w(bytes, 0xE1A0_0000 | (rd << 12) | rm);
8680    if hi == 0 {
8681        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8682        mov(bytes, lo, 0);
8683        mov(bytes, hi, 1);
8684    } else {
8685        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8686        mov(bytes, hi, 1);
8687        mov(bytes, lo, 0);
8688    }
8689    for i in 0..4u32 {
8690        if i == lo || i == hi {
8691            // The result lives here — drop the saved caller word.
8692            w(bytes, 0xE28D_D004); // ADD SP, SP, #4
8693        } else {
8694            w(bytes, 0xE49D_0004 | (i << 12)); // LDR Ri, [SP], #4
8695        }
8696    }
8697    Ok(())
8698}
8699
8700/// A32 zero-divisor trap, emitted after marshaling when the divisor pair is
8701/// in R2:R3: `ORRS R12, R2, R3` sets Z iff the divisor is zero; `BNE` skips a
8702/// `UDF #0` (WASM div/rem-by-zero must trap, matching the Thumb-2 twin).
8703fn emit_a32_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8704    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8705    w(bytes, 0xE192_C003); // ORRS R12, R2, R3
8706    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8707    w(bytes, 0xE7F0_00F0); // UDF #0 — divide by zero
8708}
8709
8710/// A32 twin of [`emit_i64_divs_overflow_trap`] (#633): trap on
8711/// `i64.div_s(INT64_MIN, -1)`. Conditional execution replaces the Thumb
8712/// branches — the CMPEQ chain leaves EQ set only when divisor == -1 AND
8713/// dividend == INT64_MIN. div_s only; rem_s must keep returning 0.
8714fn emit_a32_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8715    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8716    w(bytes, 0xE002_C003); // AND   R12, R2, R3 (== 0xFFFFFFFF iff divisor == -1)
8717    w(bytes, 0xE37C_0001); // CMN   R12, #1     (EQ iff divisor == -1)
8718    w(bytes, 0x0350_0000); // CMPEQ R0, #0      (EQ iff also dividend lo == 0)
8719    w(bytes, 0x0351_0102); // CMPEQ R1, #0x80000000 (EQ iff dividend == INT64_MIN)
8720    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8721    w(bytes, 0xE7F0_00F0); // UDF #0 — signed-division overflow
8722}
8723
8724/// Fallible form of the `verify_reg_bits` contract. PC (R15) is not a valid
8725/// data operand for the Thumb-2 encodings that use this guard (SDIV/UDIV/MLS/…
8726/// are UNPREDICTABLE with PC). Synth's own codegen never emits PC there, but
8727/// the encoder must stay *total* over arbitrary `ArmOp` inputs — the fuzz
8728/// harness (`encoder_no_panic`) requires Ok-or-Err, never a panic. Pre-fix, the
8729/// `debug_assert` in `verify_reg_bits` aborted under `-Cdebug-assertions`.
8730/// Returns a typed Err instead. See #185.
8731fn reg_bits_checked(bits: u32) -> Result<()> {
8732    if bits > 14 {
8733        return Err(synth_core::Error::synthesis(format!(
8734            "register bits {bits} (PC/R15) is not a valid operand for this Thumb-2 encoding"
8735        )));
8736    }
8737    Ok(())
8738}
8739
8740/// Try to encode a 32-bit value as an ARM rotated immediate (imm8 ROR 2*rot4).
8741/// Returns Some((encoded_bits, 1)) if representable, None otherwise.
8742fn try_encode_rotated_imm(val: u32) -> Option<(u32, u32)> {
8743    if val == 0 {
8744        return Some((0, 1));
8745    }
8746    for rot in 0..16u32 {
8747        let shift = rot * 2;
8748        // Rotate left by shift (undo the ROR) to see if result fits in 8 bits
8749        let unrotated = val.rotate_left(shift);
8750        if unrotated <= 0xFF {
8751            // Encoded as: rot4(4 bits) | imm8(8 bits) = rotate_imm << 8 | imm8
8752            return Some(((rot << 8) | unrotated, 1));
8753        }
8754    }
8755    None
8756}
8757
8758/// Encode operand2 field and return (bits, immediate_flag).
8759/// For ARM32 mode, immediates use the rotated-immediate encoding (imm8 ROR 2*rot4).
8760/// Panics if an immediate value cannot be represented. Callers that need large
8761/// immediates should use MOVW/MOVT instead of Operand2::Imm.
8762fn encode_operand2(op2: &Operand2) -> Result<(u32, u32)> {
8763    match op2 {
8764        Operand2::Imm(val) => {
8765            let uval = *val as u32;
8766            // Attempt rotated-immediate encoding (ARM32 Operand2)
8767            if let Some(encoded) = try_encode_rotated_imm(uval) {
8768                Ok(encoded)
8769            } else {
8770                // #378-class honesty: an immediate that can't be expressed as an
8771                // ARM32 rotated immediate is an INTERNAL selector bug — large
8772                // constants must be materialized via MOVW/MOVT, not passed here.
8773                // FAIL HONESTLY with an Err rather than silently masking to
8774                // `uval & 0xFF` and emitting a WRONG immediate. The encoder is
8775                // Ok-or-Err, never corrupt (#180/#185); a loud Err is also why
8776                // this is an Err and not a panic (the `encoder_no_panic` fuzz
8777                // contract — malformed/oversized input must degrade, not crash).
8778                Err(synth_core::Error::synthesis(format!(
8779                    "encode_operand2: immediate {uval:#x} ({val}) is not an ARM32 \
8780                     rotated immediate — the selector must materialize large \
8781                     constants via MOVW/MOVT"
8782                )))
8783            }
8784        }
8785
8786        Operand2::Reg(reg) => {
8787            let reg_bits = reg_to_bits(reg);
8788            Ok((reg_bits, 0)) // I=0 for register
8789        }
8790
8791        Operand2::RegShift {
8792            rm,
8793            shift: _,
8794            amount,
8795        } => {
8796            // Simplified encoding with shift
8797            let rm_bits = reg_to_bits(rm);
8798            let shift_bits = (*amount & 0x1F) << 7;
8799            Ok((shift_bits | rm_bits, 0))
8800        }
8801    }
8802}
8803
8804/// Largest immediate an A32 `LDR`/`STR`/`LDRB`/`STRB` `[Rn, #imm12]` form holds.
8805const A32_LDST_IMM12_MAX: u32 = 0xFFF;
8806/// Largest immediate an A32 `LDRH`/`STRH`/`LDRSB`/`LDRSH` `[Rn, #imm8]`
8807/// (`imm4H:imm4L`) form holds.
8808const A32_LDST_IMM8_MAX: u32 = 0xFF;
8809/// Largest LOW-half immediate an A32 `I64Ldr`/`I64Str` pair can fold: the high
8810/// half sits at `+4`, which must itself fit imm12 (`0xFFB + 4 = 0xFFF`).
8811const A32_I64_PAIR_IMM12_MAX: u32 = 0xFFB;
8812
8813/// RQ-63-ARMI64OFF (#1165): resolve the A32 base register and residual
8814/// immediate for a load/store whose static offset may exceed the form's
8815/// immediate field — the A32 counterpart of the Thumb-2 `i64_effective_base`
8816/// (#372/#382). Returns `(base, residual)`; the caller encodes `[base,
8817/// #residual]` (and `#residual + 4` for the i64 high half).
8818///
8819/// The offset is the wasm memarg round-tripped through the selector's `as i32`
8820/// cast (a memarg `>= 2^31` arrives negative), so it is read back as `u32` and
8821/// the effective address is `base + index + offset (mod 2^32)` — the same
8822/// arithmetic the Thumb-2 word path (`encode_thumb32_add_imm`) and the
8823/// `--safety-bounds software` guard (`software_bounds_guard`, which round-trips
8824/// the same cast) already use. A clamp or a mask here is a WRONG ADDRESS, i.e.
8825/// the silent memory miscompile this artifact exists to refuse: before this
8826/// helper the A32 immediate arms masked `& 0xFFF` (word/byte) and `& 0xFF`
8827/// (halfword/signed), so `i32.load offset=5000` on the `-b arm` default target
8828/// silently read `[ip, #904]`, and the i64 pair arm declined outright (46 of
8829/// the 110 core-module declines in v0.62's ARM census).
8830///
8831/// - `offset <= imm_max`, no index: emits nothing, returns `(addr.base, offset)`
8832///   — byte-identical to the pre-#1165 immediate form.
8833/// - `offset <= imm_max`, index `rm`: emits `ADD ip, base, rm`, returns
8834///   `(ip, offset)` — byte-identical to the #206 register-offset form.
8835/// - `offset > imm_max`: MATERIALIZES the whole effective base into `ip`:
8836///   `MOVW ip, #lo16 ; [MOVT ip, #hi16 — only when non-zero] ; ADD ip, ip, base
8837///   ; [ADD ip, ip, rm]`, returns `(ip, 0)`. `ip` is fully computed BEFORE the
8838///   access, so a destination register aliasing `base` or `rm` is safe.
8839///
8840/// `ip` (R12) is the reserved encoder scratch — never allocated (pool R0–R8;
8841/// R9/R10/R11 are the globals/size/base contract registers). If `base` or `rm`
8842/// IS R12 the materialization would clobber its own input, so that case is a
8843/// typed `Err` — a loud decline, never a wrong address (the alias trap
8844/// `encode_thumb32_add_imm` carries for `rd == rn == R12`).
8845///
8846/// Bytes verified against `arm-none-eabi-as` in the `test_1165_a32_*` tests
8847/// and executed by `scripts/repro/a32_ldst_offset_1165_differential.py`.
8848fn a32_effective_base(bytes: &mut Vec<u8>, addr: &MemAddr, imm_max: u32) -> Result<(Reg, u32)> {
8849    let ip = Reg::R12;
8850    let ip_bits = reg_to_bits(&ip);
8851    let offset = addr.offset as u32;
8852    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8853    // ADD rd, rn, rm (cond=AL, opcode=ADD, S=0, register operand2).
8854    let add_reg = |rd: u32, rn: u32, rm: u32| 0xE080_0000 | (rn << 16) | (rd << 12) | rm;
8855    if offset <= imm_max {
8856        return Ok(match addr.offset_reg {
8857            Some(rm) => {
8858                w(
8859                    bytes,
8860                    add_reg(ip_bits, reg_to_bits(&addr.base), reg_to_bits(&rm)),
8861                );
8862                (ip, offset)
8863            }
8864            None => (addr.base, offset),
8865        });
8866    }
8867    if addr.base == ip || addr.offset_reg == Some(ip) {
8868        return Err(synth_core::Error::synthesis(format!(
8869            "A32 load/store offset {offset:#x} exceeds the immediate field (max {imm_max:#x}) \
8870             and the address already uses the R12 scratch — no free register to \
8871             materialize it (RQ-63-ARMI64OFF)"
8872        )));
8873    }
8874    // MOVW ip, #lo16 ; MOVT ip, #hi16 (elided when zero, mirroring I64Const).
8875    let lo16 = offset & 0xFFFF;
8876    let hi16 = offset >> 16;
8877    w(
8878        bytes,
8879        0xE300_0000 | ((lo16 >> 12) << 16) | (ip_bits << 12) | (lo16 & 0xFFF),
8880    );
8881    if hi16 != 0 {
8882        w(
8883            bytes,
8884            0xE340_0000 | ((hi16 >> 12) << 16) | (ip_bits << 12) | (hi16 & 0xFFF),
8885        );
8886    }
8887    w(bytes, add_reg(ip_bits, ip_bits, reg_to_bits(&addr.base)));
8888    if let Some(rm) = addr.offset_reg {
8889        w(bytes, add_reg(ip_bits, ip_bits, reg_to_bits(&rm)));
8890    }
8891    Ok((ip, 0))
8892}
8893
8894/// Encode an A32 immediate-form memory address to `(base_reg, imm12)`.
8895///
8896/// RQ-63-ARMI64OFF (#1165): this used to mask `& 0xFFF`, silently re-targeting
8897/// any offset past the field (the #259 class, closed on Thumb-2 by
8898/// `check_ldst_imm12` but never here). The immediate arms are only reached with
8899/// an in-range, index-free address — `encode_arm_reg_offset_mem` materializes
8900/// everything else into IP first — so this is a TRIPWIRE: a typed error, never
8901/// a wrong address, should a future arm bypass that pre-pass.
8902fn encode_mem_addr(addr: &MemAddr) -> Result<(u32, u32)> {
8903    check_a32_imm_addr(addr, A32_LDST_IMM12_MAX)
8904}
8905
8906/// `encode_mem_addr` for the `imm4H:imm4L` (8-bit) forms — `LDRH`/`STRH`/
8907/// `LDRSB`/`LDRSH`. Same tripwire contract; the former `& 0xFF` mask is gone.
8908fn encode_mem_addr_imm8(addr: &MemAddr) -> Result<(u32, u32)> {
8909    check_a32_imm_addr(addr, A32_LDST_IMM8_MAX)
8910}
8911
8912fn check_a32_imm_addr(addr: &MemAddr, imm_max: u32) -> Result<(u32, u32)> {
8913    if addr.offset_reg.is_some() {
8914        return Err(synth_core::Error::synthesis(
8915            "internal: A32 immediate-form load/store reached with a register offset — \
8916             encode_arm_reg_offset_mem must materialize it first (#206)",
8917        ));
8918    }
8919    let offset = addr.offset as u32;
8920    if offset > imm_max {
8921        return Err(synth_core::Error::synthesis(format!(
8922            "internal: A32 load/store immediate offset {offset:#x} exceeds the {imm_max:#x} \
8923             field — encode_arm_reg_offset_mem must materialize it first (RQ-63-ARMI64OFF)"
8924        )));
8925    }
8926    Ok((reg_to_bits(&addr.base), offset))
8927}
8928
8929/// S-register number: S0=0, S1=1, ..., S31=31
8930fn vfp_sreg_to_num(reg: &VfpReg) -> Result<u32> {
8931    match reg {
8932        VfpReg::S0 => Ok(0),
8933        VfpReg::S1 => Ok(1),
8934        VfpReg::S2 => Ok(2),
8935        VfpReg::S3 => Ok(3),
8936        VfpReg::S4 => Ok(4),
8937        VfpReg::S5 => Ok(5),
8938        VfpReg::S6 => Ok(6),
8939        VfpReg::S7 => Ok(7),
8940        VfpReg::S8 => Ok(8),
8941        VfpReg::S9 => Ok(9),
8942        VfpReg::S10 => Ok(10),
8943        VfpReg::S11 => Ok(11),
8944        VfpReg::S12 => Ok(12),
8945        VfpReg::S13 => Ok(13),
8946        VfpReg::S14 => Ok(14),
8947        VfpReg::S15 => Ok(15),
8948        VfpReg::S16 => Ok(16),
8949        VfpReg::S17 => Ok(17),
8950        VfpReg::S18 => Ok(18),
8951        VfpReg::S19 => Ok(19),
8952        VfpReg::S20 => Ok(20),
8953        VfpReg::S21 => Ok(21),
8954        VfpReg::S22 => Ok(22),
8955        VfpReg::S23 => Ok(23),
8956        VfpReg::S24 => Ok(24),
8957        VfpReg::S25 => Ok(25),
8958        VfpReg::S26 => Ok(26),
8959        VfpReg::S27 => Ok(27),
8960        VfpReg::S28 => Ok(28),
8961        VfpReg::S29 => Ok(29),
8962        VfpReg::S30 => Ok(30),
8963        VfpReg::S31 => Ok(31),
8964        // D-registers are not used in F32 single-precision encodings
8965        _ => Err(synth_core::Error::SynthesisError(
8966            "D-register not supported in single-precision VFP encoding".to_string(),
8967        )),
8968    }
8969}
8970
8971/// D-register number: D0=0, D1=1, ..., D15=15
8972fn vfp_dreg_to_num(reg: &VfpReg) -> Result<u32> {
8973    match reg {
8974        VfpReg::D0 => Ok(0),
8975        VfpReg::D1 => Ok(1),
8976        VfpReg::D2 => Ok(2),
8977        VfpReg::D3 => Ok(3),
8978        VfpReg::D4 => Ok(4),
8979        VfpReg::D5 => Ok(5),
8980        VfpReg::D6 => Ok(6),
8981        VfpReg::D7 => Ok(7),
8982        VfpReg::D8 => Ok(8),
8983        VfpReg::D9 => Ok(9),
8984        VfpReg::D10 => Ok(10),
8985        VfpReg::D11 => Ok(11),
8986        VfpReg::D12 => Ok(12),
8987        VfpReg::D13 => Ok(13),
8988        VfpReg::D14 => Ok(14),
8989        VfpReg::D15 => Ok(15),
8990        // S-registers are not used in F64 double-precision encodings
8991        _ => Err(synth_core::Error::SynthesisError(
8992            "S-register not supported in double-precision VFP encoding".to_string(),
8993        )),
8994    }
8995}
8996
8997/// Split S-register into (Vx[3:0], qualifier_bit) for VFP encoding.
8998/// For an S-register number s: Vx = s >> 1, qualifier = s & 1.
8999/// The qualifier bit goes to D (bit 22), N (bit 7), or M (bit 5) depending on role.
9000fn encode_sreg(s: u32) -> (u32, u32) {
9001    (s >> 1, s & 1)
9002}
9003
9004/// Split D-register into (Vx[3:0], qualifier_bit) for VFP double-precision encoding.
9005/// For a D-register number d: Vx = d & 0xF, qualifier = (d >> 4) & 1.
9006/// For D0-D15, qualifier is always 0.
9007fn encode_dreg(d: u32) -> (u32, u32) {
9008    (d & 0xF, (d >> 4) & 1)
9009}
9010
9011/// Encode a VFP 3-register arithmetic instruction (VADD.F32, VSUB.F32, VMUL.F32, VDIV.F32).
9012/// Returns the full 32-bit instruction word.
9013///
9014/// VFP encoding: [cond 1110] [D opc1 Vn] [Vd 101 sz] [N opc2 M 0 Vm]
9015/// For single-precision (sz=0), coprocessor = 0xA (bits[11:8]).
9016fn encode_vfp_3reg(base: u32, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<u32> {
9017    let sd_num = vfp_sreg_to_num(sd)?;
9018    let sn_num = vfp_sreg_to_num(sn)?;
9019    let sm_num = vfp_sreg_to_num(sm)?;
9020    let (vd, d) = encode_sreg(sd_num);
9021    let (vn, n) = encode_sreg(sn_num);
9022    let (vm, m) = encode_sreg(sm_num);
9023
9024    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
9025}
9026
9027/// Encode a VFP 2-register instruction (VNEG.F32, VABS.F32, VSQRT.F32).
9028/// Returns the full 32-bit instruction word.
9029fn encode_vfp_2reg(base: u32, sd: &VfpReg, sm: &VfpReg) -> Result<u32> {
9030    let sd_num = vfp_sreg_to_num(sd)?;
9031    let sm_num = vfp_sreg_to_num(sm)?;
9032    let (vd, d) = encode_sreg(sd_num);
9033    let (vm, m) = encode_sreg(sm_num);
9034
9035    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
9036}
9037
9038/// Encode a VFP load/store (VLDR.F32 / VSTR.F32).
9039/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
9040/// U bit (bit 23) controls add/subtract offset.
9041fn encode_vfp_ldst(base: u32, sd: &VfpReg, addr: &MemAddr) -> Result<u32> {
9042    let sd_num = vfp_sreg_to_num(sd)?;
9043    let (vd, d) = encode_sreg(sd_num);
9044    let rn = reg_to_bits(&addr.base);
9045
9046    let offset = addr.offset;
9047    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9048    let abs_offset = offset.unsigned_abs();
9049    let imm8 = (abs_offset / 4) & 0xFF;
9050
9051    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
9052}
9053
9054/// Encode VMOV between core register and S-register.
9055/// VMOV Sn, Rt: 0xEE00_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
9056/// VMOV Rt, Sn: 0xEE10_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
9057fn encode_vmov_core_sreg(to_sreg: bool, sreg: &VfpReg, core: &Reg) -> Result<u32> {
9058    let s_num = vfp_sreg_to_num(sreg)?;
9059    let (vn, n) = encode_sreg(s_num);
9060    let rt = reg_to_bits(core);
9061
9062    let base = if to_sreg { 0xEE000A10 } else { 0xEE100A10 };
9063    Ok(base | (vn << 16) | (rt << 12) | (n << 7))
9064}
9065
9066/// Encode a VFP 3-register double-precision instruction (VADD.F64, VSUB.F64, etc.).
9067/// For double-precision (sz=1), coprocessor = 0xB (bits[11:8]).
9068/// The base should have bit 8 = 1 for F64 (0xB suffix instead of 0xA).
9069fn encode_vfp_3reg_f64(base: u32, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<u32> {
9070    let dd_num = vfp_dreg_to_num(dd)?;
9071    let dn_num = vfp_dreg_to_num(dn)?;
9072    let dm_num = vfp_dreg_to_num(dm)?;
9073    let (vd, d) = encode_dreg(dd_num);
9074    let (vn, n) = encode_dreg(dn_num);
9075    let (vm, m) = encode_dreg(dm_num);
9076
9077    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
9078}
9079
9080/// Encode a VFP 2-register double-precision instruction (VNEG.F64, VABS.F64, VSQRT.F64).
9081fn encode_vfp_2reg_f64(base: u32, dd: &VfpReg, dm: &VfpReg) -> Result<u32> {
9082    let dd_num = vfp_dreg_to_num(dd)?;
9083    let dm_num = vfp_dreg_to_num(dm)?;
9084    let (vd, d) = encode_dreg(dd_num);
9085    let (vm, m) = encode_dreg(dm_num);
9086
9087    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
9088}
9089
9090/// Encode a VFP load/store for double-precision (VLDR.64 / VSTR.64).
9091/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
9092fn encode_vfp_ldst_f64(base: u32, dd: &VfpReg, addr: &MemAddr) -> Result<u32> {
9093    let dd_num = vfp_dreg_to_num(dd)?;
9094    let (vd, d) = encode_dreg(dd_num);
9095    let rn = reg_to_bits(&addr.base);
9096
9097    let offset = addr.offset;
9098    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9099    let abs_offset = offset.unsigned_abs();
9100    let imm8 = (abs_offset / 4) & 0xFF;
9101
9102    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
9103}
9104
9105/// Encode VMOV between two core registers and a D-register.
9106/// VMOV Dm, Rt, Rt2: 0xEC40_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
9107/// VMOV Rt, Rt2, Dm: 0xEC50_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
9108fn encode_vmov_core_dreg(
9109    to_dreg: bool,
9110    dreg: &VfpReg,
9111    core_lo: &Reg,
9112    core_hi: &Reg,
9113) -> Result<u32> {
9114    let d_num = vfp_dreg_to_num(dreg)?;
9115    let (vm, m) = encode_dreg(d_num);
9116    let rt = reg_to_bits(core_lo);
9117    let rt2 = reg_to_bits(core_hi);
9118
9119    let base = if to_dreg { 0xEC400B10 } else { 0xEC500B10 };
9120    Ok(base | (rt2 << 16) | (rt << 12) | (m << 5) | vm)
9121}
9122
9123/// Emit a VFP 32-bit instruction as Thumb-2 bytes (two LE halfwords).
9124fn vfp_to_thumb_bytes(instr: u32) -> Vec<u8> {
9125    let hw1 = ((instr >> 16) & 0xFFFF) as u16;
9126    let hw2 = (instr & 0xFFFF) as u16;
9127    let mut bytes = hw1.to_le_bytes().to_vec();
9128    bytes.extend_from_slice(&hw2.to_le_bytes());
9129    bytes
9130}
9131
9132// ============================================================================
9133// Helium MVE encoding helpers
9134// ============================================================================
9135
9136/// Q-register number: Q0=0, Q1=1, ..., Q7=7
9137fn qreg_to_num(reg: &QReg) -> u32 {
9138    match reg {
9139        QReg::Q0 => 0,
9140        QReg::Q1 => 1,
9141        QReg::Q2 => 2,
9142        QReg::Q3 => 3,
9143        QReg::Q4 => 4,
9144        QReg::Q5 => 5,
9145        QReg::Q6 => 6,
9146        QReg::Q7 => 7,
9147    }
9148}
9149
9150/// MVE element size to encoding bits: S8=0b00, S16=0b01, S32=0b10
9151fn mve_size_bits(size: &MveSize) -> u32 {
9152    match size {
9153        MveSize::S8 => 0b00,
9154        MveSize::S16 => 0b01,
9155        MveSize::S32 => 0b10,
9156    }
9157}
9158
9159/// Encode MVE 3-register instruction.
9160/// Q-registers are encoded as D-register pairs: Q0=D0:D1, Q1=D2:D3, etc.
9161/// In NEON/MVE encoding, the Q-register uses D-register number = Qn * 2.
9162fn encode_mve_3reg(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
9163    let d = qreg_to_num(qd) * 2;
9164    let n = qreg_to_num(qn) * 2;
9165    let m = qreg_to_num(qm) * 2;
9166
9167    // Standard NEON/MVE 3-register encoding:
9168    // D bit (bit 22) = Vd[4], Vd[3:0] = bits [15:12]
9169    // N bit (bit 7)  = Vn[4], Vn[3:0] = bits [19:16]
9170    // M bit (bit 5)  = Vm[4], Vm[3:0] = bits [3:0]
9171    let vd = d & 0xF;
9172    let d_bit = (d >> 4) & 1;
9173    let vn = n & 0xF;
9174    let n_bit = (n >> 4) & 1;
9175    let vm = m & 0xF;
9176    let m_bit = (m >> 4) & 1;
9177
9178    base | (d_bit << 22) | (vn << 16) | (vd << 12) | (n_bit << 7) | (m_bit << 5) | vm
9179}
9180
9181/// Encode MVE 3-register bitwise instruction (VAND, VORR, VEOR, VBIC).
9182fn encode_mve_3reg_bitwise(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
9183    encode_mve_3reg(base, qd, qn, qm)
9184}
9185
9186/// Encode MVE VLDRW.32 Qd, [Rn, #offset]
9187/// Format: EC9x xxxx - contiguous load, word-sized elements
9188fn encode_mve_vldrw(qd: &QReg, addr: &MemAddr) -> u32 {
9189    let qd_enc = qreg_to_num(qd) * 2;
9190    let rn = reg_to_bits(&addr.base);
9191    let offset = addr.offset;
9192    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9193    let abs_offset = offset.unsigned_abs();
9194    let imm7 = (abs_offset / 4) & 0x7F; // 7-bit word-aligned offset
9195
9196    // VLDRW.32 Qd, [Rn, #imm]: ED10 xx80 variant
9197    0xED100E80
9198        | (u_bit << 23)
9199        | ((qd_enc >> 4) << 22)
9200        | (rn << 16)
9201        | ((qd_enc & 0xF) << 12)
9202        | (imm7 & 0x7F)
9203}
9204
9205/// Encode MVE VSTRW.32 Qd, [Rn, #offset]
9206fn encode_mve_vstrw(qd: &QReg, addr: &MemAddr) -> u32 {
9207    let qd_enc = qreg_to_num(qd) * 2;
9208    let rn = reg_to_bits(&addr.base);
9209    let offset = addr.offset;
9210    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9211    let abs_offset = offset.unsigned_abs();
9212    let imm7 = (abs_offset / 4) & 0x7F;
9213
9214    0xED000E80
9215        | (u_bit << 23)
9216        | ((qd_enc >> 4) << 22)
9217        | (rn << 16)
9218        | ((qd_enc & 0xF) << 12)
9219        | (imm7 & 0x7F)
9220}
9221
9222impl ArmEncoder {
9223    /// Encode MVE constant load: MOVW+MOVT+VMOV for each 32-bit word, then assemble Q-register
9224    fn encode_thumb_mve_const(&self, qd: &QReg, bytes: &[u8; 16]) -> Result<Vec<u8>> {
9225        let mut result = Vec::new();
9226        let qd_num = qreg_to_num(qd);
9227
9228        // Load each 32-bit word into R12 (temp) then VMOV into S-register
9229        for i in 0..4 {
9230            let word = u32::from_le_bytes([
9231                bytes[i * 4],
9232                bytes[i * 4 + 1],
9233                bytes[i * 4 + 2],
9234                bytes[i * 4 + 3],
9235            ]);
9236            let lo16 = word & 0xFFFF;
9237            let hi16 = (word >> 16) & 0xFFFF;
9238
9239            // MOVW R12, #lo16
9240            result.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
9241            // MOVT R12, #hi16
9242            if hi16 != 0 {
9243                result.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
9244            }
9245
9246            // VMOV Sn, R12 where Sn = Qd*4 + i
9247            let s_num = qd_num * 4 + i as u32;
9248            let (vn, n) = encode_sreg(s_num);
9249            let vmov: u32 = 0xEE000A10 | (vn << 16) | (12 << 12) | (n << 7);
9250            result.extend_from_slice(&vfp_to_thumb_bytes(vmov));
9251        }
9252
9253        Ok(result)
9254    }
9255
9256    /// Encode lane-wise f32 binary operation (VDIV, etc.) via S-register extraction
9257    fn encode_thumb_mve_lane_wise_f32_binop(
9258        &self,
9259        qd: &QReg,
9260        qn: &QReg,
9261        qm: &QReg,
9262        vfp_base: u32,
9263    ) -> Result<Vec<u8>> {
9264        let mut result = Vec::new();
9265        let qd_num = qreg_to_num(qd);
9266        let qn_num = qreg_to_num(qn);
9267        let qm_num = qreg_to_num(qm);
9268
9269        // For each lane 0..3: use S-registers directly (Q aliasing)
9270        for i in 0..4u32 {
9271            let sd = qd_num * 4 + i;
9272            let sn = qn_num * 4 + i;
9273            let sm = qm_num * 4 + i;
9274
9275            let (vd, d) = encode_sreg(sd);
9276            let (vn, n) = encode_sreg(sn);
9277            let (vm, m) = encode_sreg(sm);
9278
9279            let instr = vfp_base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
9280            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9281        }
9282
9283        Ok(result)
9284    }
9285
9286    /// Encode lane-wise f32 VSQRT via S-register extraction
9287    fn encode_thumb_mve_lane_wise_f32_sqrt(&self, qd: &QReg, qm: &QReg) -> Result<Vec<u8>> {
9288        let mut result = Vec::new();
9289        let qd_num = qreg_to_num(qd);
9290        let qm_num = qreg_to_num(qm);
9291
9292        // VSQRT.F32 base: 0xEEB10AC0
9293        for i in 0..4u32 {
9294            let sd = qd_num * 4 + i;
9295            let sm = qm_num * 4 + i;
9296
9297            let (vd, d) = encode_sreg(sd);
9298            let (vm, m) = encode_sreg(sm);
9299
9300            let instr: u32 = 0xEEB10AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
9301            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9302        }
9303
9304        Ok(result)
9305    }
9306}
9307
9308/// VCR-TIER-001 (#1021/#1048) — the SCRATCH CONTRACT of an `ArmOp`'s encoder
9309/// expansion: the registers, beyond the op's declared RESULT registers and the
9310/// globally sanctioned R12/IP encoder scratch, that the expansion may leave
9311/// modified when it completes. Transient-but-restored traffic (push/pop through
9312/// the expansion's own stack red-zone, SP restored on exit) is not "modified".
9313///
9314/// This is the SINGLE declaration site for that contract — the one place a
9315/// future expansion that must borrow a register says so (the repo rule: one
9316/// declaration site, no duplicate copy that can silently drift). It is deliberately NOT derived
9317/// from the expansion's observed behavior: a contract read off the bytes would
9318/// rubber-stamp any clobber. Intent is declared here; the canary gate
9319/// (`scripts/repro/expansion_canary_gate_1021.py`) executes the REAL emitted
9320/// bytes of every variant the shipped rule table emits, on both backends, with
9321/// every non-contract register holding a distinctive canary, and fails on any
9322/// undeclared write — and on any declared register the expansion never
9323/// actually writes, so an over-broad declaration cannot hollow the gate.
9324///
9325/// The default is the STRICTEST reading — result registers only — which is
9326/// exactly the silent claim the atomic `ArmSemantics` pseudo-op model already
9327/// makes (#1021: an atomic model of a multi-instruction expansion is a silent
9328/// claim that the expansion is scratch-free). Today the table is EMPTY: #1039
9329/// reworked `i32.popcnt` off R11 (the linear-memory base) and #1048 reworked
9330/// the i64 shifts and bit-counts off their own operand registers, so every
9331/// expansion of every rule-emitted variant is R12-only on both Thumb-2 and
9332/// A32. Backend-independent for the same reason; if a backend's expansion ever
9333/// diverges, this signature grows a backend parameter in the same PR.
9334pub fn expansion_scratch_contract(op: &ArmOp) -> &'static [Reg] {
9335    // No variant currently borrows any register beyond R12. A new declaration
9336    // is added as a `match op { .. }` arm here — nowhere else.
9337    let _ = op;
9338    &[]
9339}
9340
9341#[cfg(test)]
9342mod tests {
9343    use super::*;
9344
9345    #[test]
9346    fn test_encoder_creation() {
9347        let encoder_arm = ArmEncoder::new_arm32();
9348        assert!(!encoder_arm.thumb_mode);
9349
9350        let encoder_thumb = ArmEncoder::new_thumb2();
9351        assert!(encoder_thumb.thumb_mode);
9352    }
9353
9354    /// #204 WAKE-path regression: `SetCond` materialized 0/1 with the 16-bit
9355    /// `MOVS Rd,#imm` (T1), whose Rd field is 3 bits (R0–R7). For a high Rd
9356    /// (R8–R12) `rd_bits << 8` overflows bit 11, flipping the opcode MOVS→CMP
9357    /// (`0x2c00`), so the boolean was never written — gale's `has_waiter` kept a
9358    /// stale value and the binary-sem WAKE dispatch read garbage. High Rd must
9359    /// use the 32-bit `MOV.W` (T2). Verify the bytes, not the IR.
9360    /// #311: the SAME high-Rd MOVS→CMP transmutation as #204, but in the
9361    /// i64 comparison expansions (I64SetCond / I64SetCondZ) — missed by the
9362    /// #204 hardening. With rd=R8 the boolean died in the flags
9363    /// (`ite eq; cmpeq r0,#1; cmpne r0,#0`), so gale's packed-u64 select
9364    /// read a stale register on silicon. High Rd must take MOV.W / CMP.W.
9365    #[test]
9366    fn test_encode_i64setcond_high_reg_uses_mov_w_311() {
9367        use synth_synthesis::{ArmOp, Condition, Reg};
9368        let enc = ArmEncoder::new_thumb2();
9369        let bytes = enc
9370            .encode(&ArmOp::I64SetCond {
9371                rd: Reg::R8,
9372                rn_lo: Reg::R2,
9373                rn_hi: Reg::R3,
9374                rm_lo: Reg::R6,
9375                rm_hi: Reg::R7,
9376                cond: Condition::EQ,
9377            })
9378            .unwrap();
9379        // The 32-bit MOV.W immediate (T2) first halfword is 0xF04F; the
9380        // 16-bit transmuted forms would contain 0x2801/0x2800 (CMP r0,#1/#0).
9381        let halfwords: Vec<u16> = bytes
9382            .chunks(2)
9383            .map(|c| u16::from_le_bytes([c[0], c[1]]))
9384            .collect();
9385        assert!(
9386            halfwords.iter().filter(|&&h| h == 0xF04F).count() == 2,
9387            "high rd must use two MOV.W (T2) encodings, got {halfwords:04x?}"
9388        );
9389        assert!(
9390            !halfwords.contains(&0x2801) && !halfwords.contains(&0x2800),
9391            "no transmuted 16-bit CMP imm: {halfwords:04x?}"
9392        );
9393
9394        let bytes_z = enc
9395            .encode(&ArmOp::I64SetCondZ {
9396                rd: Reg::R8,
9397                rn_lo: Reg::R2,
9398                rn_hi: Reg::R3,
9399            })
9400            .unwrap();
9401        let hw_z: Vec<u16> = bytes_z
9402            .chunks(2)
9403            .map(|c| u16::from_le_bytes([c[0], c[1]]))
9404            .collect();
9405        assert!(
9406            hw_z.iter().filter(|&&h| h == 0xF04F).count() == 2,
9407            "SetCondZ high rd MOV.W: {hw_z:04x?}"
9408        );
9409        // CMP.W rd,#0 (T2) first halfword: 0xF1B0 | rd
9410        assert!(
9411            hw_z.contains(&(0xF1B0 | 8)),
9412            "SetCondZ high rd must use CMP.W: {hw_z:04x?}"
9413        );
9414    }
9415
9416    #[test]
9417    fn test_encode_setcond_high_reg_uses_mov_w_204() {
9418        use synth_synthesis::{ArmOp, Condition, Reg};
9419        let enc = ArmEncoder::new_thumb2();
9420        // R12 (high): must be ITE + MOV.W #1 + MOV.W #0, never a 16-bit MOVS/CMP.
9421        let hi = enc
9422            .encode(&ArmOp::SetCond {
9423                rd: Reg::R12,
9424                cond: Condition::NE,
9425            })
9426            .unwrap();
9427        assert_eq!(hi.len(), 10, "ITE(2) + MOV.W(4) + MOV.W(4): {hi:02x?}");
9428        // both value halfwords are MOV.W (0xF04F) — NOT the corrupt CMP (0x2c..).
9429        assert_eq!(&hi[2..4], &[0x4F, 0xF0], "then = MOV.W: {hi:02x?}");
9430        assert_eq!(&hi[6..8], &[0x4F, 0xF0], "else = MOV.W: {hi:02x?}");
9431        assert_eq!(hi[4] & 0x0F, 0x01, "then imm = #1");
9432        assert_eq!(hi[8] & 0x0F, 0x00, "else imm = #0");
9433        // Low Rd keeps the compact 16-bit MOVS form.
9434        let lo = enc
9435            .encode(&ArmOp::SetCond {
9436                rd: Reg::R0,
9437                cond: Condition::NE,
9438            })
9439            .unwrap();
9440        assert_eq!(lo.len(), 6, "ITE(2) + MOVS(2) + MOVS(2): {lo:02x?}");
9441        assert_eq!(lo[2..4], [0x01, 0x20], "then = MOVS R0,#1");
9442        assert_eq!(lo[4..6], [0x00, 0x20], "else = MOVS R0,#0");
9443    }
9444
9445    /// #209 Opt 1b: UMULL RdLo, RdHi, Rn, Rm encodes correctly on both ISAs.
9446    /// Thumb-2 T1: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm.
9447    /// A32:        cond 0000 1000 RdHi RdLo Rm 1001 Rn.
9448    #[test]
9449    fn test_encode_umull_209b() {
9450        use synth_synthesis::{ArmOp, Reg};
9451        let op = ArmOp::Umull {
9452            rdlo: Reg::R4,
9453            rdhi: Reg::R5,
9454            rn: Reg::R0,
9455            rm: Reg::R3,
9456        };
9457        // Thumb-2: hw1 = 0xFBA0 | 0 = 0xFBA0; hw2 = (4<<12)|(5<<8)|3 = 0x4503.
9458        let t = ArmEncoder::new_thumb2().encode(&op).unwrap();
9459        assert_eq!(
9460            t,
9461            vec![0xA0, 0xFB, 0x03, 0x45],
9462            "umull r4,r5,r0,r3 (T2): {t:02x?}"
9463        );
9464        // A32: 0xE0800090 | (5<<16) | (4<<12) | (3<<8) | 0 = 0xE0854390.
9465        let a = ArmEncoder::new_arm32().encode(&op).unwrap();
9466        assert_eq!(
9467            a,
9468            0xE085_4390u32.to_le_bytes().to_vec(),
9469            "umull (A32): {a:02x?}"
9470        );
9471    }
9472
9473    /// #206 regression: the ARM32 (A32) `Ldr`/`Str` encoders fed `addr` through
9474    /// `encode_mem_addr`, which returns only the 12-bit immediate — so a register
9475    /// offset (`[rn, rm, #off]`) was silently dropped to `[rn, #off]`, sending
9476    /// the access to the wrong runtime address (silent miscompile on the default
9477    /// `--target arm`). A register offset must materialize `ip = rn + rm` and
9478    /// load from `[ip, #off]`. Verify the bytes.
9479    #[test]
9480    fn test_encode_arm32_indexed_load_keeps_index_206() {
9481        use synth_synthesis::{ArmOp, MemAddr, Reg};
9482        let enc = ArmEncoder::new_arm32();
9483        // ldr r0, [r11, r1, #8]  must NOT collapse to a single immediate ldr.
9484        let bytes = enc
9485            .encode(&ArmOp::Ldr {
9486                rd: Reg::R0,
9487                addr: MemAddr::reg_imm(Reg::R11, Reg::R1, 8),
9488            })
9489            .unwrap();
9490        assert_eq!(
9491            bytes.len(),
9492            8,
9493            "expected ADD ip + LDR (2 words): {bytes:02x?}"
9494        );
9495        let add = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9496        let ldr = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9497        // ADD ip, r11, r1  = 0xE08BC001
9498        assert_eq!(add, 0xE08B_C001, "ADD ip,r11,r1: {add:#010x}");
9499        // LDR r0, [ip, #8] = 0xE59C0008
9500        assert_eq!(ldr, 0xE59C_0008, "LDR r0,[ip,#8]: {ldr:#010x}");
9501        // A bare immediate ldr (the bug) would be 0xE59B0008 (base=r11) — reject.
9502        assert_ne!(ldr, 0xE59B_0008, "index must not be dropped");
9503    }
9504
9505    /// #594 regression: `call_indirect` on the A32 path (`--target cortex-r5`)
9506    /// was encoded as a literal NOP (0xE1A00000) — the call never happened and
9507    /// the function silently returned the leftover table-index value. The A32
9508    /// encoder must emit a real dispatch expansion, since #642 guarded by an
9509    /// inline bounds check:
9510    /// `MOVW r12, #size; CMP idx, r12; BLO +1; UDF;
9511    ///  MOV r12, idx, LSL #2; LDR r12, [r11, r12]; BLX r12`.
9512    #[test]
9513    fn test_encode_arm32_call_indirect_is_real_call_594() {
9514        use synth_synthesis::{ArmOp, Reg};
9515        let enc = ArmEncoder::new_arm32();
9516        let bytes = enc
9517            .encode(&ArmOp::CallIndirect {
9518                rd: Reg::R0,
9519                type_idx: 0,
9520                table_index_reg: Reg::R0,
9521                table_size: 4,
9522                table_byte_offset: 0,
9523                null_check: false,
9524                type_check: None,
9525            })
9526            .unwrap();
9527        assert_eq!(
9528            bytes.len(),
9529            28,
9530            "expected MOVW + CMP + BLO + UDF + MOV + LDR + BLX (7 words): {bytes:02x?}"
9531        );
9532        let words: Vec<u32> = bytes
9533            .as_chunks::<4>()
9534            .0
9535            .iter()
9536            .map(|&w| u32::from_le_bytes(w))
9537            .collect();
9538        // #642 bounds guard: MOVW r12, #4; CMP r0, r12; BLO +1; UDF
9539        assert_eq!(words[0], 0xE300_C004, "MOVW r12,#4: {:#010x}", words[0]);
9540        assert_eq!(words[1], 0xE150_000C, "CMP r0,r12: {:#010x}", words[1]);
9541        assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9542        assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9543        // MOV r12, r0, LSL #2 = 0xE1A0C100
9544        assert_eq!(
9545            words[4], 0xE1A0_C100,
9546            "MOV r12,r0,LSL#2: {:#010x}",
9547            words[4]
9548        );
9549        // LDR r12, [r11, r12] = 0xE79BC00C
9550        assert_eq!(
9551            words[5], 0xE79B_C00C,
9552            "LDR r12,[r11,r12]: {:#010x}",
9553            words[5]
9554        );
9555        // BLX r12 = 0xE12FFF3C
9556        assert_eq!(words[6], 0xE12F_FF3C, "BLX r12: {:#010x}", words[6]);
9557        // The bug: a single NOP word. Must never come back.
9558        assert!(
9559            !bytes
9560                .as_chunks::<4>()
9561                .0
9562                .iter()
9563                .any(|&w| w == 0xE1A0_0000u32.to_le_bytes()),
9564            "call_indirect must not contain a NOP (#594): {bytes:02x?}"
9565        );
9566
9567        // A non-R0 index register lands in the MOV's Rm and CMP's Rn fields.
9568        let bytes = enc
9569            .encode(&ArmOp::CallIndirect {
9570                rd: Reg::R0,
9571                type_idx: 0,
9572                table_index_reg: Reg::R4,
9573                table_size: 4,
9574                table_byte_offset: 0,
9575                null_check: false,
9576                type_check: None,
9577            })
9578            .unwrap();
9579        let cmp = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9580        assert_eq!(cmp, 0xE154_000C, "CMP r4,r12: {cmp:#010x}");
9581        let mov = u32::from_le_bytes(bytes[16..20].try_into().unwrap());
9582        assert_eq!(mov, 0xE1A0_C104, "MOV r12,r4,LSL#2: {mov:#010x}");
9583    }
9584
9585    /// #642: a table size above 16 bits must not be silently truncated by the
9586    /// MOVW — the A32 guard adds a MOVT for the high half.
9587    #[test]
9588    fn test_encode_arm32_call_indirect_wide_table_size_642() {
9589        use synth_synthesis::{ArmOp, Reg};
9590        let enc = ArmEncoder::new_arm32();
9591        let bytes = enc
9592            .encode(&ArmOp::CallIndirect {
9593                rd: Reg::R0,
9594                type_idx: 0,
9595                table_index_reg: Reg::R0,
9596                table_size: 0x0002_0003,
9597                table_byte_offset: 0,
9598                null_check: false,
9599                type_check: None,
9600            })
9601            .unwrap();
9602        assert_eq!(bytes.len(), 32, "MOVT arm adds one word: {bytes:02x?}");
9603        let movw = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9604        let movt = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9605        assert_eq!(movw, 0xE300_C003, "MOVW r12,#3: {movw:#010x}");
9606        assert_eq!(movt, 0xE340_C002, "MOVT r12,#2: {movt:#010x}");
9607    }
9608
9609    /// #597 anchor (justified correctness RE-PIN of the #594-era freeze): the
9610    /// Thumb-2 `CallIndirect` expansion is `mov.w ip, rm, LSL #2; ldr.w ip,
9611    /// [r11, ip]; blx ip`.
9612    ///
9613    /// The #594 PR froze the then-current bytes `4F EA 20 0C ...` whose first
9614    /// word decodes as `mov.w ip, rm, ASR #32` — the intended `LSL #2` had
9615    /// its shift amount in the TYPE field (bits 5:4) instead of imm2 (bits
9616    /// 7:6), so the index was destroyed and every call_indirect dispatched
9617    /// table entry 0 (shipped miscompile, masked by index-0 probes). #597
9618    /// corrects the encoding; new bytes `4F EA 80 0C ...` were
9619    /// execution-validated under unicorn against the wasmtime oracle on a
9620    /// multi-entry table (indexes 0, 1, 3 —
9621    /// scripts/repro/call_indirect_597_differential.py) before this pin was
9622    /// replaced. Old pin: [4F EA 20 0C, 5B F8 0C C0, E0 47] (ASR #32 — must
9623    /// never come back).
9624    #[test]
9625    fn test_encode_thumb_call_indirect_lsl2_597() {
9626        use synth_synthesis::{ArmOp, Reg};
9627        let enc = ArmEncoder::new_thumb2();
9628        let bytes = enc
9629            .encode(&ArmOp::CallIndirect {
9630                rd: Reg::R0,
9631                type_idx: 0,
9632                table_index_reg: Reg::R0,
9633                table_size: 4,
9634                table_byte_offset: 0,
9635                null_check: false,
9636                type_check: None,
9637            })
9638            .unwrap();
9639        assert_eq!(
9640            bytes,
9641            vec![
9642                // #642 bounds guard: movw ip,#4; cmp r0,ip; blo +1; udf #0
9643                0x40, 0xF2, 0x04, 0x0C, // movw ip, #4
9644                0x60, 0x45, // cmp r0, ip
9645                0x00, 0xD3, // blo .+4 (skip the udf)
9646                0x00, 0xDE, // udf #0 — OOB index trap (WASM §4.4.8)
9647                // #597-pinned dispatch
9648                0x4F, 0xEA, 0x80, 0x0C, // mov.w ip, r0, lsl #2
9649                0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9650                0xE0, 0x47, // blx ip
9651            ],
9652            "Thumb-2 CallIndirect: bounds guard + mov.w/ldr.w/blx dispatch: {bytes:02x?}"
9653        );
9654        // The #597 bug bytes (ASR #32 dispatch first word) must never come back.
9655        assert!(
9656            !bytes.windows(4).any(|w| w == [0x4F, 0xEA, 0x20, 0x0C]),
9657            "mov.w ip, rm, ASR #32 — the #597 type-field bug"
9658        );
9659
9660        // A non-R0 index register lands in the mov.w's Rm field (hw2 bits 3:0)
9661        // and the cmp's Rn field.
9662        let bytes = enc
9663            .encode(&ArmOp::CallIndirect {
9664                rd: Reg::R0,
9665                type_idx: 0,
9666                table_index_reg: Reg::R4,
9667                table_size: 4,
9668                table_byte_offset: 0,
9669                null_check: false,
9670                type_check: None,
9671            })
9672            .unwrap();
9673        assert_eq!(&bytes[4..6], &[0x64, 0x45], "cmp r4, ip: {bytes:02x?}");
9674        assert_eq!(
9675            &bytes[10..14],
9676            &[0x4F, 0xEA, 0x84, 0x0C],
9677            "mov.w ip, r4, LSL #2: {bytes:02x?}"
9678        );
9679    }
9680
9681    /// #642: the Thumb-2 bounds guard for a high-register index (R8 — the top
9682    /// of the allocatable pool) uses the high-reg-capable 16-bit CMP (T2) with
9683    /// the N bit set; a table size above 16 bits adds a MOVT.
9684    #[test]
9685    fn test_encode_thumb_call_indirect_guard_shapes_642() {
9686        use synth_synthesis::{ArmOp, Reg};
9687        let enc = ArmEncoder::new_thumb2();
9688        let bytes = enc
9689            .encode(&ArmOp::CallIndirect {
9690                rd: Reg::R0,
9691                type_idx: 0,
9692                table_index_reg: Reg::R8,
9693                table_size: 3,
9694                table_byte_offset: 0,
9695                null_check: false,
9696                type_check: None,
9697            })
9698            .unwrap();
9699        // cmp r8, ip — T2: 0x4500 | N(1)<<7 | Rm(12)<<3 | Rn(0) = 0x45E0
9700        assert_eq!(&bytes[4..6], &[0xE0, 0x45], "cmp r8, ip: {bytes:02x?}");
9701
9702        let bytes = enc
9703            .encode(&ArmOp::CallIndirect {
9704                rd: Reg::R0,
9705                type_idx: 0,
9706                table_index_reg: Reg::R0,
9707                table_size: 0x0002_0003,
9708                table_byte_offset: 0,
9709                null_check: false,
9710                type_check: None,
9711            })
9712            .unwrap();
9713        // movw ip,#3 then movt ip,#2 — the size must not be truncated.
9714        assert_eq!(
9715            &bytes[0..8],
9716            &[0x40, 0xF2, 0x03, 0x0C, 0xC0, 0xF2, 0x02, 0x0C],
9717            "movw ip,#3; movt ip,#2: {bytes:02x?}"
9718        );
9719    }
9720
9721    /// #650: a non-zero table base offset (table N of the contiguous R11
9722    /// region) routes the Thumb-2 pointer load through
9723    /// `add.w ip, r11, ip; ldr.w ip, [ip, #offset]` — and offset 0 keeps the
9724    /// pre-#650 single-load bytes IDENTICAL (the by-construction pin).
9725    #[test]
9726    fn test_encode_thumb_call_indirect_table_offset_650() {
9727        use synth_synthesis::{ArmOp, Reg};
9728        let enc = ArmEncoder::new_thumb2();
9729        // falcon's fused-component shape: table 0 has 7 entries, so table 1
9730        // sits at byte offset 28.
9731        let bytes = enc
9732            .encode(&ArmOp::CallIndirect {
9733                rd: Reg::R0,
9734                type_idx: 0,
9735                table_index_reg: Reg::R1,
9736                table_size: 41,
9737                table_byte_offset: 28,
9738                null_check: false,
9739                type_check: None,
9740            })
9741            .unwrap();
9742        assert_eq!(
9743            bytes,
9744            vec![
9745                // #642 bounds guard against TABLE 1's OWN size (41)
9746                0x40, 0xF2, 0x29, 0x0C, // movw ip, #41
9747                0x61, 0x45, // cmp r1, ip
9748                0x00, 0xD3, // blo .+4 (skip the udf)
9749                0x00, 0xDE, // udf #0 — OOB trap (WASM §4.4.8)
9750                // dispatch through table 1's base (R11 + 28)
9751                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9752                0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9753                0xDC, 0xF8, 0x1C, 0xC0, // ldr.w ip, [ip, #28]
9754                0xE0, 0x47, // blx ip
9755            ],
9756            "Thumb-2 table-1 dispatch (#650): {bytes:02x?}"
9757        );
9758
9759        // Offset 0 must stay the #597-pinned single-load form (no add.w, no
9760        // imm-form ldr) — single-table byte identity by construction.
9761        let zero = enc
9762            .encode(&ArmOp::CallIndirect {
9763                rd: Reg::R0,
9764                type_idx: 0,
9765                table_index_reg: Reg::R1,
9766                table_size: 41,
9767                table_byte_offset: 0,
9768                null_check: false,
9769                type_check: None,
9770            })
9771            .unwrap();
9772        assert_eq!(
9773            &zero[10..],
9774            &[
9775                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9776                0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9777                0xE0, 0x47, // blx ip
9778            ],
9779            "offset 0 keeps the pre-#650 dispatch bytes: {zero:02x?}"
9780        );
9781    }
9782
9783    /// #650: the A32 twin — `add r12, r11, r12; ldr r12, [r12, #offset]` for
9784    /// a non-zero table base offset; offset 0 keeps the #594/#642 form.
9785    #[test]
9786    fn test_encode_arm32_call_indirect_table_offset_650() {
9787        use synth_synthesis::{ArmOp, Reg};
9788        let enc = ArmEncoder::new_arm32();
9789        let bytes = enc
9790            .encode(&ArmOp::CallIndirect {
9791                rd: Reg::R0,
9792                type_idx: 0,
9793                table_index_reg: Reg::R1,
9794                table_size: 41,
9795                table_byte_offset: 28,
9796                null_check: false,
9797                type_check: None,
9798            })
9799            .unwrap();
9800        let words: Vec<u32> = bytes
9801            .as_chunks::<4>()
9802            .0
9803            .iter()
9804            .map(|&w| u32::from_le_bytes(w))
9805            .collect();
9806        assert_eq!(words[0], 0xE300_C029, "MOVW r12,#41: {:#010x}", words[0]);
9807        assert_eq!(words[1], 0xE151_000C, "CMP r1,r12: {:#010x}", words[1]);
9808        assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9809        assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9810        assert_eq!(
9811            words[4], 0xE1A0_C101,
9812            "MOV r12,r1,LSL#2: {:#010x}",
9813            words[4]
9814        );
9815        assert_eq!(
9816            words[5], 0xE08B_C00C,
9817            "ADD r12,r11,r12 (#650): {:#010x}",
9818            words[5]
9819        );
9820        assert_eq!(
9821            words[6], 0xE59C_C01C,
9822            "LDR r12,[r12,#28] (#650): {:#010x}",
9823            words[6]
9824        );
9825        assert_eq!(words[7], 0xE12F_FF3C, "BLX r12: {:#010x}", words[7]);
9826    }
9827
9828    /// #664: `null_check` inserts a null-funcref trap between the Thumb-2
9829    /// pointer load and the `BLX` (`cmp.w ip, #0; bne .+4; udf #0`) — a
9830    /// zero-linked (uninitialized) slot must TRAP (WASM §4.4.8), never
9831    /// branch to address 0. `null_check: false` keeps the expansion
9832    /// byte-identical to the pre-#664 form (by-construction pin).
9833    #[test]
9834    fn test_encode_thumb_call_indirect_null_check_664() {
9835        use synth_synthesis::{ArmOp, Reg};
9836        let enc = ArmEncoder::new_thumb2();
9837        let op = |null_check| ArmOp::CallIndirect {
9838            rd: Reg::R0,
9839            type_idx: 0,
9840            table_index_reg: Reg::R1,
9841            table_size: 4,
9842            table_byte_offset: 0,
9843            null_check,
9844            type_check: None,
9845        };
9846        let with = enc.encode(&op(true)).unwrap();
9847        let without = enc.encode(&op(false)).unwrap();
9848        // The checked form = the unchecked form with EXACTLY the three-insn
9849        // null check spliced in before the final BLX (byte identity of the
9850        // shared prefix/suffix — nothing else may move).
9851        assert_eq!(
9852            with.len(),
9853            without.len() + 8,
9854            "cmp.w (4) + bne (2) + udf (2): {with:02x?}"
9855        );
9856        let blx_at = without.len() - 2;
9857        assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9858        assert_eq!(
9859            &with[blx_at..],
9860            &[
9861                0xBC, 0xF1, 0x00, 0x0F, // cmp.w ip, #0
9862                0x00, 0xD1, // bne .+4 (skip the udf)
9863                0x00, 0xDE, // udf #0 — null-funcref trap (#664)
9864                0xE0, 0x47, // blx ip
9865            ],
9866            "null check precedes the BLX: {with:02x?}"
9867        );
9868        assert_eq!(&with[with.len() - 2..], &without[blx_at..], "same BLX");
9869    }
9870
9871    /// #664: the A32 twin — `cmp r12, #0; bne .+8; udf` before the `BLX`;
9872    /// `null_check: false` keeps the #594/#642/#650 bytes identical.
9873    #[test]
9874    fn test_encode_arm32_call_indirect_null_check_664() {
9875        use synth_synthesis::{ArmOp, Reg};
9876        let enc = ArmEncoder::new_arm32();
9877        let op = |null_check| ArmOp::CallIndirect {
9878            rd: Reg::R0,
9879            type_idx: 0,
9880            table_index_reg: Reg::R1,
9881            table_size: 4,
9882            table_byte_offset: 0,
9883            null_check,
9884            type_check: None,
9885        };
9886        let with = enc.encode(&op(true)).unwrap();
9887        let without = enc.encode(&op(false)).unwrap();
9888        assert_eq!(with.len(), without.len() + 12, "3 A32 words: {with:02x?}");
9889        let blx_at = without.len() - 4;
9890        assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9891        let words: Vec<u32> = with[blx_at..]
9892            .as_chunks::<4>()
9893            .0
9894            .iter()
9895            .map(|&w| u32::from_le_bytes(w))
9896            .collect();
9897        assert_eq!(words[0], 0xE35C_0000, "CMP r12,#0: {:#010x}", words[0]);
9898        assert_eq!(words[1], 0x1A00_0000, "BNE +1 insn: {:#010x}", words[1]);
9899        assert_eq!(words[2], 0xE7F0_00F0, "UDF (null trap): {:#010x}", words[2]);
9900        assert_eq!(words[3], 0xE12F_FF3C, "BLX r12: {:#010x}", words[3]);
9901    }
9902
9903    /// #676: `type_check` splices the runtime type check — scale the index,
9904    /// load the slot's structural class id from the type-id sidecar
9905    /// (`ldr.w ip, [ip, #type_off]`), compare against the expected class id
9906    /// and trap on mismatch (WASM §4.4.8) — between the bounds guard and
9907    /// the dispatch tail. `type_check: None` keeps the expansion
9908    /// byte-identical to the pre-#676 form (by-construction pin, the same
9909    /// trick as #650 offset-0 / #664 `null_check: false`).
9910    #[test]
9911    fn test_encode_thumb_call_indirect_type_check_676() {
9912        use synth_synthesis::{ArmOp, Reg};
9913        let enc = ArmEncoder::new_thumb2();
9914        let op = |type_check| ArmOp::CallIndirect {
9915            rd: Reg::R0,
9916            type_idx: 1,
9917            table_index_reg: Reg::R1,
9918            table_size: 5,
9919            table_byte_offset: 0,
9920            null_check: false,
9921            type_check,
9922        };
9923        let with = enc.encode(&op(Some((2, 20)))).unwrap();
9924        let without = enc.encode(&op(None)).unwrap();
9925        // The checked form = the unchecked form with EXACTLY the six-insn
9926        // type check spliced in after the bounds guard (byte identity of
9927        // the shared prefix/suffix — nothing else may move).
9928        assert_eq!(
9929            with.len(),
9930            without.len() + 20,
9931            "lsl.w(4)+add.w(4)+ldr.w(4)+cmp.w(4)+beq(2)+udf(2): {with:02x?}"
9932        );
9933        // Bounds guard: movw(4) + cmp(2) + blo(2) + udf(2) = 10 bytes.
9934        let guard_end = 10;
9935        assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
9936        assert_eq!(
9937            &with[guard_end..guard_end + 20],
9938            &[
9939                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9940                0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9941                0xDC, 0xF8, 0x14, 0xC0, // ldr.w ip, [ip, #20] — sidecar slot id
9942                0xBC, 0xF1, 0x02, 0x0F, // cmp.w ip, #2 — expected class id
9943                0x00, 0xD0, // beq .+4 (skip the udf on a match)
9944                0x00, 0xDE, // udf #0 — §4.4.8 type-mismatch trap (#676)
9945            ],
9946            "type check follows the bounds guard: {with:02x?}"
9947        );
9948        assert_eq!(
9949            &with[guard_end + 20..],
9950            &without[guard_end..],
9951            "dispatch tail unchanged (idx*4 recomputed)"
9952        );
9953    }
9954
9955    /// #676: the A32 twin — `mov r12, idx, lsl #2; add r12, r11, r12;
9956    /// ldr r12, [r12, #type_off]; cmp r12, #id; beq .+8; udf` after the
9957    /// bounds guard; `type_check: None` keeps the #594/#642/#650/#664
9958    /// bytes identical.
9959    #[test]
9960    fn test_encode_arm32_call_indirect_type_check_676() {
9961        use synth_synthesis::{ArmOp, Reg};
9962        let enc = ArmEncoder::new_arm32();
9963        let op = |type_check| ArmOp::CallIndirect {
9964            rd: Reg::R0,
9965            type_idx: 1,
9966            table_index_reg: Reg::R1,
9967            table_size: 5,
9968            table_byte_offset: 0,
9969            null_check: false,
9970            type_check,
9971        };
9972        let with = enc.encode(&op(Some((2, 20)))).unwrap();
9973        let without = enc.encode(&op(None)).unwrap();
9974        assert_eq!(with.len(), without.len() + 24, "6 A32 words: {with:02x?}");
9975        // Bounds guard: movw + cmp + blo + udf = 4 words = 16 bytes.
9976        let guard_end = 16;
9977        assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
9978        let words: Vec<u32> = with[guard_end..guard_end + 24]
9979            .as_chunks::<4>()
9980            .0
9981            .iter()
9982            .map(|&w| u32::from_le_bytes(w))
9983            .collect();
9984        assert_eq!(
9985            words[0], 0xE1A0_C101,
9986            "MOV r12,r1,LSL#2: {:#010x}",
9987            words[0]
9988        );
9989        assert_eq!(words[1], 0xE08B_C00C, "ADD r12,r11,r12: {:#010x}", words[1]);
9990        assert_eq!(
9991            words[2], 0xE59C_C014,
9992            "LDR r12,[r12,#20] (sidecar): {:#010x}",
9993            words[2]
9994        );
9995        assert_eq!(
9996            words[3], 0xE35C_0002,
9997            "CMP r12,#2 (expected class id): {:#010x}",
9998            words[3]
9999        );
10000        assert_eq!(words[4], 0x0A00_0000, "BEQ +1 insn: {:#010x}", words[4]);
10001        assert_eq!(
10002            words[5], 0xE7F0_00F0,
10003            "UDF (type-mismatch trap): {:#010x}",
10004            words[5]
10005        );
10006        assert_eq!(
10007            &with[guard_end + 24..],
10008            &without[guard_end..],
10009            "dispatch tail unchanged"
10010        );
10011    }
10012
10013    /// #178/#180 regression: the Thumb `Add`/`Adds`/`Subs` reg-forms used the
10014    /// 16-bit encoding unconditionally. For high registers (R12 base scratch,
10015    /// R8-R11 i64 pairs) the 3-bit register fields overflow and corrupt the
10016    /// operands — `add ip,ip,r0` came out as `adds r4,r5,r1` (0x186C), silently
10017    /// dropping the address operand and miscompiling every optimized memory
10018    /// access. High registers must use the 32-bit `.W` forms.
10019    #[test]
10020    fn test_encode_thumb_add_high_reg_uses_add_w_178_180() {
10021        let encoder = ArmEncoder::new_thumb2();
10022
10023        // add ip, ip, r0  — the exact MemLoad/MemStore base+addr op.
10024        let code = encoder
10025            .encode(&ArmOp::Add {
10026                rd: Reg::R12,
10027                rn: Reg::R12,
10028                op2: Operand2::Reg(Reg::R0),
10029            })
10030            .unwrap();
10031        // ADD.W ip, ip, r0 = EB0C 0C00 (little-endian halfwords).
10032        assert_eq!(
10033            code,
10034            vec![0x0C, 0xEB, 0x00, 0x0C],
10035            "high-reg Thumb ADD must be 32-bit ADD.W (EB0C 0C00), not corrupt 16-bit; got {code:02X?}"
10036        );
10037        // Must NOT be the buggy 16-bit 0x186C (`adds r4,r5,r1`).
10038        assert_ne!(code, vec![0x6C, 0x18], "regressed to corrupt 16-bit ADDS");
10039
10040        // Low-register add stays 16-bit (no regression for the common case).
10041        let lo = encoder
10042            .encode(&ArmOp::Add {
10043                rd: Reg::R1,
10044                rn: Reg::R2,
10045                op2: Operand2::Reg(Reg::R3),
10046            })
10047            .unwrap();
10048        assert_eq!(
10049            lo.len(),
10050            2,
10051            "low-reg ADD should remain 16-bit, got {lo:02X?}"
10052        );
10053    }
10054
10055    /// #178/#180 sibling: i64 low-word `Adds`/`Subs` can land in R8-R11 pairs;
10056    /// those must fall back to 32-bit ADDS.W/SUBS.W (flag-setting preserved).
10057    #[test]
10058    fn test_encode_thumb_adds_subs_high_reg_use_32bit_178_180() {
10059        let encoder = ArmEncoder::new_thumb2();
10060
10061        // adds r10, r10, r8  → ADDS.W = EB1A 0A08
10062        let adds = encoder
10063            .encode(&ArmOp::Adds {
10064                rd: Reg::R10,
10065                rn: Reg::R10,
10066                op2: Operand2::Reg(Reg::R8),
10067            })
10068            .unwrap();
10069        assert_eq!(
10070            adds,
10071            vec![0x1A, 0xEB, 0x08, 0x0A],
10072            "high-reg ADDS must be 32-bit ADDS.W (EB1A 0A08); got {adds:02X?}"
10073        );
10074
10075        // subs r10, r10, r8  → SUBS.W = EBBA 0A08
10076        let subs = encoder
10077            .encode(&ArmOp::Subs {
10078                rd: Reg::R10,
10079                rn: Reg::R10,
10080                op2: Operand2::Reg(Reg::R8),
10081            })
10082            .unwrap();
10083        assert_eq!(
10084            subs,
10085            vec![0xBA, 0xEB, 0x08, 0x0A],
10086            "high-reg SUBS must be 32-bit SUBS.W (EBBA 0A08); got {subs:02X?}"
10087        );
10088    }
10089
10090    /// #184 (sibling of #180): 16-bit CMN (T1) only encodes R0-R7. High registers
10091    /// must use 32-bit CMN.W, not the corrupt truncated 16-bit form.
10092    #[test]
10093    fn test_encode_thumb_cmn_high_reg_uses_cmn_w_184() {
10094        let encoder = ArmEncoder::new_thumb2();
10095
10096        // cmn r10, r8  → CMN.W = EB1A 0F08 (ADD.W S=1, Rd=PC discarded).
10097        let cmn = encoder
10098            .encode(&ArmOp::Cmn {
10099                rn: Reg::R10,
10100                op2: Operand2::Reg(Reg::R8),
10101            })
10102            .unwrap();
10103        assert_eq!(
10104            cmn,
10105            vec![0x1A, 0xEB, 0x08, 0x0F],
10106            "high-reg CMN must be 32-bit CMN.W (EB1A 0F08); got {cmn:02X?}"
10107        );
10108
10109        // Low registers stay 16-bit: cmn r1, r2 = 0x42D1.
10110        let lo = encoder
10111            .encode(&ArmOp::Cmn {
10112                rn: Reg::R1,
10113                op2: Operand2::Reg(Reg::R2),
10114            })
10115            .unwrap();
10116        assert_eq!(
10117            lo.len(),
10118            2,
10119            "low-reg CMN should remain 16-bit, got {lo:02X?}"
10120        );
10121        assert_eq!(lo, vec![0xD1, 0x42], "low-reg CMN bytes wrong: {lo:02X?}");
10122    }
10123
10124    /// #185 regression: feeding PC (R15) as a data operand to a Thumb-2 op that
10125    /// guards its registers must return Err, not panic under debug-assertions.
10126    /// (Synth never emits PC here; the fuzz harness requires encode() be total.)
10127    #[test]
10128    fn test_encode_pc_operand_returns_err_not_panic_185() {
10129        let encoder = ArmEncoder::new_thumb2();
10130        for op in [
10131            ArmOp::Sdiv {
10132                rd: Reg::PC,
10133                rn: Reg::R0,
10134                rm: Reg::R1,
10135            },
10136            ArmOp::Udiv {
10137                rd: Reg::R0,
10138                rn: Reg::PC,
10139                rm: Reg::R1,
10140            },
10141            ArmOp::Sdiv {
10142                rd: Reg::R0,
10143                rn: Reg::R1,
10144                rm: Reg::PC,
10145            },
10146        ] {
10147            let r = encoder.encode(&op);
10148            assert!(
10149                r.is_err(),
10150                "encode({op:?}) must return Err for a PC operand, got {r:?}"
10151            );
10152        }
10153        // Valid registers still encode fine (no false rejection).
10154        assert!(
10155            encoder
10156                .encode(&ArmOp::Sdiv {
10157                    rd: Reg::R0,
10158                    rn: Reg::R1,
10159                    rm: Reg::R2
10160                })
10161                .is_ok()
10162        );
10163    }
10164
10165    #[test]
10166    fn test_encode_nop_arm32() {
10167        let encoder = ArmEncoder::new_arm32();
10168        let code = encoder.encode(&ArmOp::Nop).unwrap();
10169
10170        assert_eq!(code.len(), 4); // ARM32 instructions are 4 bytes
10171        assert_eq!(code, vec![0x00, 0x00, 0xA0, 0xE1]); // MOV R0, R0
10172    }
10173
10174    #[test]
10175    fn test_encode_nop_thumb() {
10176        let encoder = ArmEncoder::new_thumb2();
10177        let code = encoder.encode(&ArmOp::Nop).unwrap();
10178
10179        assert_eq!(code.len(), 2); // Thumb instructions are 2 bytes
10180        assert_eq!(code, vec![0x00, 0xBF]); // NOP
10181    }
10182
10183    #[test]
10184    fn test_encode_mov_immediate_arm32() {
10185        let encoder = ArmEncoder::new_arm32();
10186        let op = ArmOp::Mov {
10187            rd: Reg::R0,
10188            op2: Operand2::Imm(42),
10189        };
10190
10191        let code = encoder.encode(&op).unwrap();
10192        assert_eq!(code.len(), 4);
10193
10194        // Verify it's a MOV instruction (bits should have immediate flag set)
10195        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10196        assert_eq!(instr & 0x0E000000, 0x02000000); // Check I bit is set
10197    }
10198
10199    #[test]
10200    fn test_encode_add_registers_arm32() {
10201        let encoder = ArmEncoder::new_arm32();
10202        let op = ArmOp::Add {
10203            rd: Reg::R0,
10204            rn: Reg::R1,
10205            op2: Operand2::Reg(Reg::R2),
10206        };
10207
10208        let code = encoder.encode(&op).unwrap();
10209        assert_eq!(code.len(), 4);
10210
10211        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10212        // Verify it's an ADD instruction with correct opcode
10213        assert_eq!(instr & 0x0FE00000, 0x00800000);
10214    }
10215
10216    /// #350 — `encode_thumb32_add_imm` must lower an out-of-range immediate
10217    /// (> 0xFFF) to a legal MOVW(/MOVT) + ADD.W-register sequence instead of
10218    /// erroring. The small-imm fast path (imm <= 0xFFF) stays byte-identical.
10219    #[test]
10220    fn test_encode_add_imm_large_350() {
10221        let enc = ArmEncoder::new_thumb2();
10222
10223        // --- Fast path: imm <= 0xFFF is a single 4-byte instruction, and the
10224        // VALUE must be right (#681: this test used to assert only the length,
10225        // letting the raw-packed T3 mis-encoding of 0x123 pass CI). 0x123 is
10226        // not ThumbExpandImm-representable, so it must be ADDW (T4, plain
10227        // imm12): clang `addw r0, r1, #0x123` = f201 0023.
10228        let small = enc
10229            .encode_thumb32_add_imm(&Reg::R0, &Reg::R1, 0x123)
10230            .unwrap();
10231        assert_eq!(small, vec![0x01, 0xF2, 0x23, 0x10], "ADDW r0, r1, #0x123");
10232
10233        // helper: decode a Thumb-2 MOVW/MOVT halfword pair back to its imm16
10234        fn movx_imm16(b: &[u8]) -> u32 {
10235            let hw1 = u16::from_le_bytes([b[0], b[1]]) as u32;
10236            let hw2 = u16::from_le_bytes([b[2], b[3]]) as u32;
10237            let imm4 = hw1 & 0xF;
10238            let i = (hw1 >> 10) & 1;
10239            let imm3 = (hw2 >> 12) & 0x7;
10240            let imm8 = hw2 & 0xFF;
10241            (imm4 << 12) | (i << 11) | (imm3 << 8) | imm8
10242        }
10243        fn movx_rd(b: &[u8]) -> u32 {
10244            (u16::from_le_bytes([b[2], b[3]]) as u32 >> 8) & 0xF
10245        }
10246
10247        // --- rd != rn: scratch is rd. imm = 70000 = 0x11170 needs MOVW+MOVT. ---
10248        // 0x11170: lo16 = 0x1170, hi16 = 0x0001
10249        let seq = enc
10250            .encode_thumb32_add_imm(&Reg::R12, &Reg::R0, 70000)
10251            .unwrap();
10252        assert_eq!(seq.len(), 12, "MOVW + MOVT + ADD = 12 bytes");
10253        // MOVW r12, #0x1170
10254        assert_eq!(u16::from_le_bytes([seq[0], seq[1]]) & 0xFBF0, 0xF240);
10255        assert_eq!(movx_rd(&seq[0..4]), 12);
10256        assert_eq!(movx_imm16(&seq[0..4]), 0x1170);
10257        // MOVT r12, #0x0001
10258        assert_eq!(u16::from_le_bytes([seq[4], seq[5]]) & 0xFBF0, 0xF2C0);
10259        assert_eq!(movx_rd(&seq[4..8]), 12);
10260        assert_eq!(movx_imm16(&seq[4..8]), 0x0001);
10261        // ADD.W r12, r0, r12  (EB00 | rn=0 ; rd=12, rm=12)
10262        let add1 = u16::from_le_bytes([seq[8], seq[9]]) as u32;
10263        let add2 = u16::from_le_bytes([seq[10], seq[11]]) as u32;
10264        assert_eq!(add1 & 0xFFF0, 0xEB00);
10265        assert_eq!(add1 & 0xF, 0); // rn = r0
10266        assert_eq!((add2 >> 8) & 0xF, 12); // rd = r12
10267        assert_eq!(add2 & 0xF, 12); // rm = scratch = r12
10268        // The materialized scratch must reconstruct exactly 70000.
10269        assert_eq!(
10270            (movx_imm16(&seq[4..8]) << 16) | movx_imm16(&seq[0..4]),
10271            70000
10272        );
10273
10274        // --- imm <= 0xFFFF: MOVT is skipped (MOVW + ADD = 8 bytes). ---
10275        let seq16 = enc
10276            .encode_thumb32_add_imm(&Reg::R3, &Reg::R0, 0xABCD)
10277            .unwrap();
10278        assert_eq!(seq16.len(), 8, "imm <= 0xFFFF skips MOVT");
10279        assert_eq!(movx_imm16(&seq16[0..4]), 0xABCD);
10280        assert_eq!(movx_rd(&seq16[0..4]), 3); // scratch = rd = r3
10281
10282        // --- rd == rn (in-place add): scratch must be R12, not rd. ---
10283        // imm = 0x12345: lo16 = 0x2345, hi16 = 0x0001
10284        let inplace = enc
10285            .encode_thumb32_add_imm(&Reg::R5, &Reg::R5, 0x12345)
10286            .unwrap();
10287        assert_eq!(inplace.len(), 12);
10288        assert_eq!(movx_rd(&inplace[0..4]), 12, "rd==rn must use R12 scratch");
10289        assert_eq!(
10290            (movx_imm16(&inplace[4..8]) << 16) | movx_imm16(&inplace[0..4]),
10291            0x12345
10292        );
10293        // ADD.W r5, r5, r12 — rm must be the scratch (12), never rn.
10294        let ip_add2 = u16::from_le_bytes([inplace[10], inplace[11]]) as u32;
10295        assert_eq!(ip_add2 & 0xF, 12);
10296        assert_eq!((ip_add2 >> 8) & 0xF, 5);
10297    }
10298
10299    /// #681 — `encode_thumb32_add_imm` packed a RAW immediate into the T3
10300    /// ADD.W `i:imm3:imm8` field, which is a ThumbExpandImm MODIFIED immediate:
10301    /// ThumbExpandImm(0x200) = 0, ThumbExpandImm(0x400) = 0x8000_0000. Every
10302    /// dynamic-address load/store with a static offset in 0x100..=0xFFF
10303    /// computed a wrong address (and bypassed --safety-bounds software: the
10304    /// guard checked the intended address, the access used the mis-encoded
10305    /// one). Fix: imm <= 0xFF keeps T3 (raw == expanded there, bit-identical);
10306    /// 0x100..=0xFFF uses ADDW (T4, plain imm12) — same lowering
10307    /// `encode_thumb32_add` already uses per #253.
10308    ///
10309    /// Every expected byte sequence below is pinned against clang
10310    /// (`-target thumbv7m-none-eabi`) output, bit-for-bit (#544 pattern).
10311    #[test]
10312    fn test_encode_add_imm_thumb_expand_681() {
10313        let enc = ArmEncoder::new_thumb2();
10314        let add = |rd: &Reg, rn: &Reg, imm: u32| enc.encode_thumb32_add_imm(rd, rn, imm).unwrap();
10315
10316        // imm <= 0xFF stays T3 ADD.W (raw == ThumbExpandImm-expanded):
10317        // clang: add.w r12, r0, #0xff  = f100 0cff
10318        assert_eq!(add(&Reg::R12, &Reg::R0, 0xFF), vec![0x00, 0xF1, 0xFF, 0x0C]);
10319
10320        // 0x100..=0xFFF must be ADDW (T4, plain imm12). The old T3 raw packing
10321        // decoded as +0 (0x100/0x200), +0x80000000 (0x400), etc.
10322        // clang: addw r12, r0, #0x100 = f200 1c00
10323        assert_eq!(
10324            add(&Reg::R12, &Reg::R0, 0x100),
10325            vec![0x00, 0xF2, 0x00, 0x1C]
10326        );
10327        // clang: addw r12, r0, #0x104 = f200 1c04
10328        assert_eq!(
10329            add(&Reg::R12, &Reg::R0, 0x104),
10330            vec![0x00, 0xF2, 0x04, 0x1C]
10331        );
10332        // clang: addw r12, r0, #0x200 = f200 2c00
10333        assert_eq!(
10334            add(&Reg::R12, &Reg::R0, 0x200),
10335            vec![0x00, 0xF2, 0x00, 0x2C]
10336        );
10337        // clang: addw r12, r0, #0x3fc = f200 3cfc
10338        assert_eq!(
10339            add(&Reg::R12, &Reg::R0, 0x3FC),
10340            vec![0x00, 0xF2, 0xFC, 0x3C]
10341        );
10342        // clang: addw r12, r0, #0x400 = f200 4c00
10343        assert_eq!(
10344            add(&Reg::R12, &Reg::R0, 0x400),
10345            vec![0x00, 0xF2, 0x00, 0x4C]
10346        );
10347        // clang: addw r12, r0, #0xfff = f600 7cff
10348        assert_eq!(
10349            add(&Reg::R12, &Reg::R0, 0xFFF),
10350            vec![0x00, 0xF6, 0xFF, 0x7C]
10351        );
10352        // Non-scratch rd/rn — clang: addw r1, r2, #0x104 = f202 1104
10353        assert_eq!(add(&Reg::R1, &Reg::R2, 0x104), vec![0x02, 0xF2, 0x04, 0x11]);
10354    }
10355
10356    /// #681 class audit — the T2 RSB and AND.W immediate fields are also
10357    /// ThumbExpandImm-coded and were raw-packed. Neither has a plain-imm12
10358    /// (T4-style) form, so a non-representable immediate must Err loudly
10359    /// (#253/#255/#378 class: never silently encode a different constant).
10360    /// Existing emitters only use representable values (RSB #32, AND #0x3F),
10361    /// pinned here bit-for-bit against clang.
10362    #[test]
10363    fn test_rsb_and_imm_thumb_expand_gate_681() {
10364        let enc = ArmEncoder::new_thumb2();
10365
10366        // clang: rsb.w r3, r2, #0x20 = f1c2 0320 — byte-identical to before.
10367        let rsb = enc
10368            .encode(&ArmOp::Rsb {
10369                rd: Reg::R3,
10370                rn: Reg::R2,
10371                imm: 32,
10372            })
10373            .unwrap();
10374        assert_eq!(rsb, vec![0xC2, 0xF1, 0x20, 0x03]);
10375
10376        // 0x101 is not ThumbExpandImm-representable -> must Err, not mis-encode.
10377        assert!(
10378            enc.encode(&ArmOp::Rsb {
10379                rd: Reg::R3,
10380                rn: Reg::R2,
10381                imm: 0x101,
10382            })
10383            .is_err(),
10384            "non-ThumbExpandImm RSB immediate must Err"
10385        );
10386
10387        // clang: and r4, r4, #0x3f = f004 043f — byte-identical to before.
10388        let and = enc.encode_thumb32_and_imm_raw(4, 4, 0x3F).unwrap();
10389        assert_eq!(and, vec![0x04, 0xF0, 0x3F, 0x04]);
10390        assert!(
10391            enc.encode_thumb32_and_imm_raw(4, 4, 0x101).is_err(),
10392            "non-ThumbExpandImm AND immediate must Err"
10393        );
10394
10395        // A32 RSB: imm12 is a rotate:imm8 modified immediate; > 0xFF used to be
10396        // silently masked to `imm & 0xFF` (#378 masking class) -> must Err.
10397        let a32 = ArmEncoder::new_arm32();
10398        assert!(
10399            a32.encode(&ArmOp::Rsb {
10400                rd: Reg::R3,
10401                rn: Reg::R2,
10402                imm: 0x120,
10403            })
10404            .is_err(),
10405            "A32 RSB immediate > 0xFF must Err, not mask"
10406        );
10407        // imm 32 (the only value real codegen emits) still encodes.
10408        assert!(
10409            a32.encode(&ArmOp::Rsb {
10410                rd: Reg::R3,
10411                rn: Reg::R2,
10412                imm: 32,
10413            })
10414            .is_ok()
10415        );
10416    }
10417
10418    /// #350 follow-up — the `encoder_no_panic` fuzz harness drives the encoder
10419    /// with ARBITRARY registers, including the one case the in-place lowering
10420    /// cannot serve: rd==rn==R12. There the scratch (R12, the reserved encoder
10421    /// register) would alias Rn and clobber it before the ADD reads it. The
10422    /// encoder contract (#180/#185) is Ok-or-Err, never a panic — so this must
10423    /// return Err, not assert. (Real codegen never emits rd==rn==R12 because R12
10424    /// is non-allocatable; this guards only the fuzz/adversarial path.)
10425    #[test]
10426    fn test_encode_add_imm_large_rd_rn_r12_errs_not_panics_350() {
10427        let enc = ArmEncoder::new_thumb2();
10428        // Out-of-range imm with rd==rn==R12: no free scratch -> Err.
10429        let r = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 70000);
10430        assert!(
10431            r.is_err(),
10432            "rd==rn==R12 with out-of-range imm must Err (no free scratch), got {r:?}"
10433        );
10434        // Small imm with rd==rn==R12 still takes the single-instruction fast path
10435        // (no scratch needed) and must succeed — the guard is scoped to the
10436        // out-of-range lowering only.
10437        let small = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 0x10);
10438        assert!(small.is_ok(), "small imm needs no scratch, must stay Ok");
10439    }
10440
10441    /// #378 — `encode_operand2` (ARM32 data-processing operand) must FAIL
10442    /// HONESTLY on an immediate that is not a valid rotated immediate, rather
10443    /// than silently masking it to `imm & 0xFF` and emitting a WRONG
10444    /// instruction. `0x1FF` has 9 set bits, so it cannot come from rotating an
10445    /// 8-bit imm8 — non-encodable. Real codegen materializes large constants via
10446    /// MOVW/MOVT; this guards the encoder's Ok-or-Err contract (#180/#185)
10447    /// directly. It is an Err (not a panic) so the `encoder_no_panic` fuzz
10448    /// harness — which drives arbitrary operands — still passes.
10449    #[test]
10450    fn test_encode_operand2_non_rotatable_imm_errs_not_masks_378() {
10451        let enc = ArmEncoder::new_arm32();
10452        let bad = enc.encode(&ArmOp::Add {
10453            rd: Reg::R0,
10454            rn: Reg::R1,
10455            op2: Operand2::Imm(0x1FF),
10456        });
10457        assert!(
10458            bad.is_err(),
10459            "non-rotatable ARM32 immediate 0x1FF must Err (was silently masked \
10460             to 0xFF), got {bad:?}"
10461        );
10462        // A representable rotated immediate still encodes fine (regression guard).
10463        let ok = enc.encode(&ArmOp::Add {
10464            rd: Reg::R0,
10465            rn: Reg::R1,
10466            op2: Operand2::Imm(0xFF),
10467        });
10468        assert!(
10469            ok.is_ok(),
10470            "0xFF is a valid rotated immediate, must stay Ok"
10471        );
10472    }
10473
10474    #[test]
10475    fn test_encode_ldr_arm32() {
10476        let encoder = ArmEncoder::new_arm32();
10477        let op = ArmOp::Ldr {
10478            rd: Reg::R0,
10479            addr: MemAddr::imm(Reg::R1, 4),
10480        };
10481
10482        let code = encoder.encode(&op).unwrap();
10483        assert_eq!(code.len(), 4);
10484
10485        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10486        // Verify load bit is set
10487        assert_eq!(instr & 0x00100000, 0x00100000);
10488    }
10489
10490    #[test]
10491    fn test_encode_str_arm32() {
10492        let encoder = ArmEncoder::new_arm32();
10493        let op = ArmOp::Str {
10494            rd: Reg::R0,
10495            addr: MemAddr::imm(Reg::SP, 0),
10496        };
10497
10498        let code = encoder.encode(&op).unwrap();
10499        assert_eq!(code.len(), 4);
10500    }
10501
10502    #[test]
10503    fn test_encode_branch_arm32() {
10504        let encoder = ArmEncoder::new_arm32();
10505        let op = ArmOp::Bl {
10506            label: "main".to_string(),
10507        };
10508
10509        let code = encoder.encode(&op).unwrap();
10510        assert_eq!(code.len(), 4);
10511
10512        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10513        // Verify BL opcode
10514        assert_eq!(instr & 0x0F000000, 0x0B000000);
10515    }
10516
10517    /// #1040: the A32 BL relocatable placeholder must carry an embedded addend
10518    /// of -8 so an R_ARM_CALL nets to exactly the symbol S — the A32 twin of
10519    /// the Thumb #167/#174 test above. A32 `BL` computes
10520    /// `target = P + 8 + (imm24 << 2)`, so:
10521    ///   - `eb000000` (imm24 = 0, addend 0) lands at S+8, two instructions
10522    ///     past the callee entry. This is what synth emitted before #1040, and
10523    ///     combined with the mislabelled R_ARM_THM_CALL a real linker
10524    ///     (`arm-none-eabi-ld`) corrupted the word to `eaca0000` — opcode
10525    ///     `eb` (BL) flipped to `ea` (B), so LR was never set at all.
10526    ///   - `ebfffffe` (imm24 = -2, offset -8) is `bl <self>` and nets to S.
10527    ///     This is exactly what `arm-none-eabi-as -march=armv7-r` emits for
10528    ///     `bl <extern>`, verified directly rather than derived from the ABI.
10529    #[test]
10530    fn test_encode_arm32_bl_placeholder_addend_1040() {
10531        let encoder = ArmEncoder::new_arm32();
10532        let code = encoder
10533            .encode(&ArmOp::Bl {
10534                label: "callee".to_string(),
10535            })
10536            .unwrap();
10537        assert_eq!(code.len(), 4, "A32 BL is one 32-bit word");
10538        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10539        assert_eq!(
10540            instr, 0xEBFF_FFFE,
10541            "A32 BL placeholder must be `ebfffffe` (gas's `bl <extern>`), not              `eb000000` — a 0 addend resolves two instructions past the callee              entry (#1040)"
10542        );
10543        // Spell the addend out so a future edit cannot satisfy the literal by
10544        // accident: sign-extended imm24, word-scaled, plus the +8 pipeline
10545        // bias, must be exactly 0 (branch-to-self).
10546        let imm24 = instr & 0x00FF_FFFF;
10547        let signed = ((imm24 as i32) << 8) >> 8;
10548        assert_eq!(
10549            8 + (signed << 2),
10550            0,
10551            "placeholder must branch to itself so the REL addend is -8"
10552        );
10553    }
10554
10555    /// Regression test for #167 + #174: the Thumb-2 BL relocatable placeholder
10556    /// must carry a -4 addend so an R_ARM_THM_CALL nets to exactly the symbol S.
10557    /// The correct encoding is what `gas` emits for `bl <extern>`: f7ff fffe
10558    /// (hw1=0xF7FF, hw2=0xFFFE), little-endian bytes FF F7 FE FF.
10559    ///   - 0xD000 (J1=J2=0) → ~+0x600000 garbage addend: `bl c0000c` / truncated
10560    ///     to fit (#167).
10561    ///   - 0xF800 (addend 0) → lands at S+4, one instruction past the callee
10562    ///     entry (#174).
10563    ///   - 0xFFFE (addend -4) → lands at S. Correct.
10564    #[test]
10565    fn test_encode_thumb_bl_placeholder_addend_167_174() {
10566        let encoder = ArmEncoder::new_thumb2();
10567        let op = ArmOp::Bl {
10568            label: "callee".to_string(),
10569        };
10570
10571        let code = encoder.encode(&op).unwrap();
10572        assert_eq!(code.len(), 4, "Thumb-2 BL is 32-bit");
10573
10574        let hw1 = u16::from_le_bytes([code[0], code[1]]);
10575        let hw2 = u16::from_le_bytes([code[2], code[3]]);
10576        assert_eq!(hw1, 0xF7FF, "BL first halfword (matches gas `bl <extern>`)");
10577        assert_eq!(
10578            hw2, 0xFFFE,
10579            "BL second halfword must be 0xFFFE (-4 addend → nets to S), not 0xF800 (→ S+4, #174) or 0xD000 (#167)"
10580        );
10581        assert_ne!(hw2, 0xF800, "0xF800 (addend 0) lands at S+4 (#174)");
10582        assert_ne!(hw2, 0xD000, "0xD000 bakes in a ~+0x600000 addend (#167)");
10583    }
10584
10585    /// #740: the Thumb-2 32-bit B<cond>.W (encoding T3) must pack the
10586    /// HALFWORD offset directly into S:J2:J1:imm6:imm11 — the byte offset is
10587    /// SignExtend(S:J2:J1:imm6:imm11:'0'). The old arm packed
10588    /// `halfword_offset >> 1`, HALVING every wide conditional branch's
10589    /// displacement: gust_poll's loop-head `br_if` to an outer block end
10590    /// landed mid-shape (a spurious state write + spurious calls on the
10591    /// empty-budget path). Narrow (16-bit) B<cond> was unaffected — only
10592    /// spans > 254 bytes hit the bug. Bytes cross-checked against the llvm
10593    /// disassembler (`bne.w #0x224` = f040 8112).
10594    #[test]
10595    fn test_encode_thumb_bcond_wide_t3_halfword_offset_740() {
10596        use synth_synthesis::Condition;
10597        let encoder = ArmEncoder::new_thumb2();
10598
10599        // gust_poll's loop-head edge: NE, +0x112 halfwords (+0x224 bytes).
10600        let code = encoder
10601            .encode(&ArmOp::BCondOffset {
10602                cond: Condition::NE,
10603                offset: 0x112,
10604            })
10605            .unwrap();
10606        assert_eq!(code.len(), 4, "offset beyond ±127 halfwords must be wide");
10607        let hw1 = u16::from_le_bytes([code[0], code[1]]);
10608        let hw2 = u16::from_le_bytes([code[2], code[3]]);
10609        assert_eq!(hw1, 0xF040, "T3 hw1: 1111 0 S=0 cond=NE imm6=0");
10610        assert_eq!(
10611            hw2, 0x8112,
10612            "T3 hw2 imm11 must carry halfword offset bits [10:0] directly — \
10613             0x8089 (offset>>1) is the halved #740 miscompile"
10614        );
10615
10616        // Backward wide branch: EQ, -0x100 halfwords. S=1, J2=J1=1,
10617        // imm6=0b111111, imm11=0x700 → f43f af00.
10618        let code = encoder
10619            .encode(&ArmOp::BCondOffset {
10620                cond: Condition::EQ,
10621                offset: -0x100,
10622            })
10623            .unwrap();
10624        assert_eq!(code.len(), 4);
10625        let hw1 = u16::from_le_bytes([code[0], code[1]]);
10626        let hw2 = u16::from_le_bytes([code[2], code[3]]);
10627        assert_eq!(hw1, 0xF43F, "T3 hw1: S=1, cond=EQ, imm6=0x3F");
10628        assert_eq!(hw2, 0xAF00, "T3 hw2: J1=1 J2=1 imm11=0x700");
10629
10630        // Narrow encoding stays byte-identical (in-range offsets untouched).
10631        let code = encoder
10632            .encode(&ArmOp::BCondOffset {
10633                cond: Condition::EQ,
10634                offset: 5,
10635            })
10636            .unwrap();
10637        assert_eq!(code, vec![0x05, 0xD0], "narrow B<cond> unchanged");
10638
10639        // Out of the signed 20-bit T3 range: loud Err, never a truncated jump.
10640        assert!(
10641            encoder
10642                .encode(&ArmOp::BCondOffset {
10643                    cond: Condition::NE,
10644                    offset: 1 << 19,
10645                })
10646                .is_err(),
10647            "out-of-range T3 offset must be a loud decline"
10648        );
10649    }
10650
10651    #[test]
10652    fn test_encode_sequence() {
10653        let encoder = ArmEncoder::new_arm32();
10654        let ops = vec![
10655            ArmOp::Mov {
10656                rd: Reg::R0,
10657                op2: Operand2::Imm(42),
10658            },
10659            ArmOp::Mov {
10660                rd: Reg::R1,
10661                op2: Operand2::Imm(10),
10662            },
10663            ArmOp::Add {
10664                rd: Reg::R2,
10665                rn: Reg::R0,
10666                op2: Operand2::Reg(Reg::R1),
10667            },
10668        ];
10669
10670        let code = encoder.encode_sequence(&ops).unwrap();
10671        assert_eq!(code.len(), 12); // 3 instructions * 4 bytes
10672    }
10673
10674    #[test]
10675    fn test_reg_to_bits() {
10676        assert_eq!(reg_to_bits(&Reg::R0), 0);
10677        assert_eq!(reg_to_bits(&Reg::R7), 7);
10678        assert_eq!(reg_to_bits(&Reg::SP), 13);
10679        assert_eq!(reg_to_bits(&Reg::LR), 14);
10680        assert_eq!(reg_to_bits(&Reg::PC), 15);
10681    }
10682
10683    #[test]
10684    fn test_encode_bitwise_operations() {
10685        let encoder = ArmEncoder::new_arm32();
10686
10687        let and_op = ArmOp::And {
10688            rd: Reg::R0,
10689            rn: Reg::R1,
10690            op2: Operand2::Reg(Reg::R2),
10691        };
10692        let and_code = encoder.encode(&and_op).unwrap();
10693        assert_eq!(and_code.len(), 4);
10694
10695        let orr_op = ArmOp::Orr {
10696            rd: Reg::R0,
10697            rn: Reg::R1,
10698            op2: Operand2::Reg(Reg::R2),
10699        };
10700        let orr_code = encoder.encode(&orr_op).unwrap();
10701        assert_eq!(orr_code.len(), 4);
10702
10703        let eor_op = ArmOp::Eor {
10704            rd: Reg::R0,
10705            rn: Reg::R1,
10706            op2: Operand2::Reg(Reg::R2),
10707        };
10708        let eor_code = encoder.encode(&eor_op).unwrap();
10709        assert_eq!(eor_code.len(), 4);
10710    }
10711
10712    // === Thumb-2 32-bit encoding tests ===
10713
10714    #[test]
10715    fn test_encode_sdiv_thumb2() {
10716        let encoder = ArmEncoder::new_thumb2();
10717        let op = ArmOp::Sdiv {
10718            rd: Reg::R0,
10719            rn: Reg::R1,
10720            rm: Reg::R2,
10721        };
10722
10723        let code = encoder.encode(&op).unwrap();
10724        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10725
10726        // SDIV R0, R1, R2: 0xFB91 0xF0F2
10727        // First halfword: 0xFB90 | Rn(1) = 0xFB91
10728        // Second halfword: 0xF0F0 | Rd(0)<<8 | Rm(2) = 0xF0F2
10729        // Little-endian: [0x91, 0xFB, 0xF2, 0xF0]
10730        assert_eq!(code[0], 0x91);
10731        assert_eq!(code[1], 0xFB);
10732        assert_eq!(code[2], 0xF2);
10733        assert_eq!(code[3], 0xF0);
10734    }
10735
10736    #[test]
10737    fn test_encode_udiv_thumb2() {
10738        let encoder = ArmEncoder::new_thumb2();
10739        let op = ArmOp::Udiv {
10740            rd: Reg::R0,
10741            rn: Reg::R1,
10742            rm: Reg::R2,
10743        };
10744
10745        let code = encoder.encode(&op).unwrap();
10746        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10747
10748        // UDIV R0, R1, R2: 0xFBB1 0xF0F2
10749        // Little-endian: [0xB1, 0xFB, 0xF2, 0xF0]
10750        assert_eq!(code[0], 0xB1);
10751        assert_eq!(code[1], 0xFB);
10752        assert_eq!(code[2], 0xF2);
10753        assert_eq!(code[3], 0xF0);
10754    }
10755
10756    #[test]
10757    fn test_encode_mul_thumb2() {
10758        let encoder = ArmEncoder::new_thumb2();
10759        let op = ArmOp::Mul {
10760            rd: Reg::R0,
10761            rn: Reg::R1,
10762            rm: Reg::R2,
10763        };
10764
10765        let code = encoder.encode(&op).unwrap();
10766        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10767    }
10768
10769    #[test]
10770    fn test_encode_and_thumb2() {
10771        let encoder = ArmEncoder::new_thumb2();
10772        let op = ArmOp::And {
10773            rd: Reg::R0,
10774            rn: Reg::R1,
10775            op2: Operand2::Reg(Reg::R2),
10776        };
10777
10778        let code = encoder.encode(&op).unwrap();
10779        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10780    }
10781
10782    #[test]
10783    fn test_encode_lsl_thumb2_low_regs() {
10784        let encoder = ArmEncoder::new_thumb2();
10785        let op = ArmOp::Lsl {
10786            rd: Reg::R0,
10787            rn: Reg::R1,
10788            shift: 5,
10789        };
10790
10791        let code = encoder.encode(&op).unwrap();
10792        assert_eq!(code.len(), 2); // 16-bit for low registers
10793    }
10794
10795    #[test]
10796    fn test_encode_clz_thumb2() {
10797        let encoder = ArmEncoder::new_thumb2();
10798        let op = ArmOp::Clz {
10799            rd: Reg::R0,
10800            rm: Reg::R1,
10801        };
10802
10803        let code = encoder.encode(&op).unwrap();
10804        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10805    }
10806
10807    #[test]
10808    fn test_encode_bx_thumb2() {
10809        let encoder = ArmEncoder::new_thumb2();
10810        let op = ArmOp::Bx { rm: Reg::LR };
10811
10812        let code = encoder.encode(&op).unwrap();
10813        assert_eq!(code.len(), 2); // 16-bit instruction
10814
10815        // BX LR: 0x4770
10816        assert_eq!(code, vec![0x70, 0x47]);
10817    }
10818
10819    // ========================================================================
10820    // f32 pseudo-op encoding tests
10821    // ========================================================================
10822
10823    #[test]
10824    fn test_encode_f32_abs_arm32() {
10825        let encoder = ArmEncoder::new_arm32();
10826        let op = ArmOp::F32Abs {
10827            sd: VfpReg::S0,
10828            sm: VfpReg::S2,
10829        };
10830        let code = encoder.encode(&op).unwrap();
10831        assert_eq!(code.len(), 4); // Single VFP instruction
10832    }
10833
10834    #[test]
10835    fn test_encode_f32_neg_arm32() {
10836        let encoder = ArmEncoder::new_arm32();
10837        let op = ArmOp::F32Neg {
10838            sd: VfpReg::S0,
10839            sm: VfpReg::S2,
10840        };
10841        let code = encoder.encode(&op).unwrap();
10842        assert_eq!(code.len(), 4);
10843    }
10844
10845    #[test]
10846    fn test_encode_f32_sqrt_arm32() {
10847        let encoder = ArmEncoder::new_arm32();
10848        let op = ArmOp::F32Sqrt {
10849            sd: VfpReg::S0,
10850            sm: VfpReg::S2,
10851        };
10852        let code = encoder.encode(&op).unwrap();
10853        assert_eq!(code.len(), 4);
10854    }
10855
10856    #[test]
10857    fn test_encode_f32_ceil_arm32() {
10858        let encoder = ArmEncoder::new_arm32();
10859        let op = ArmOp::F32Ceil {
10860            sd: VfpReg::S0,
10861            sm: VfpReg::S2,
10862        };
10863        let code = encoder.encode(&op).unwrap();
10864        // VMRS + BIC + ORR + VMSR + VCVT.S32.F32 + VMRS + BIC + VMSR + VCVT.F32.S32
10865        assert_eq!(code.len(), 36);
10866    }
10867
10868    #[test]
10869    fn test_encode_f32_floor_thumb2() {
10870        let encoder = ArmEncoder::new_thumb2();
10871        let op = ArmOp::F32Floor {
10872            sd: VfpReg::S0,
10873            sm: VfpReg::S2,
10874        };
10875        let code = encoder.encode(&op).unwrap();
10876        // VMRS + BIC.W + ORR.W + VMSR + VCVT + VMRS + BIC.W + VMSR + VCVT.F32.S32
10877        assert_eq!(code.len(), 36);
10878    }
10879
10880    #[test]
10881    fn test_encode_f32_min_arm32() {
10882        let encoder = ArmEncoder::new_arm32();
10883        let op = ArmOp::F32Min {
10884            sd: VfpReg::S0,
10885            sn: VfpReg::S2,
10886            sm: VfpReg::S4,
10887        };
10888        let code = encoder.encode(&op).unwrap();
10889        assert_eq!(code.len(), 16); // VMOV + VCMP + VMRS + conditional VMOV
10890    }
10891
10892    #[test]
10893    fn test_encode_f32_max_thumb2() {
10894        let encoder = ArmEncoder::new_thumb2();
10895        let op = ArmOp::F32Max {
10896            sd: VfpReg::S0,
10897            sn: VfpReg::S2,
10898            sm: VfpReg::S4,
10899        };
10900        let code = encoder.encode(&op).unwrap();
10901        // VMOV(4) + VCMP(4) + VMRS(4) + IT(2) + VMOV(4) = 18
10902        assert_eq!(code.len(), 18);
10903    }
10904
10905    #[test]
10906    fn test_encode_f32_copysign_arm32() {
10907        let encoder = ArmEncoder::new_arm32();
10908        let op = ArmOp::F32Copysign {
10909            sd: VfpReg::S0,
10910            sn: VfpReg::S2,
10911            sm: VfpReg::S4,
10912        };
10913        let code = encoder.encode(&op).unwrap();
10914        // VMOV + VMOV + AND + BIC + ORR + VMOV = 6 * 4 = 24
10915        assert_eq!(code.len(), 24);
10916    }
10917
10918    // ========================================================================
10919    // f64 encoding tests
10920    // ========================================================================
10921
10922    #[test]
10923    fn test_encode_f64_add_arm32() {
10924        let encoder = ArmEncoder::new_arm32();
10925        let op = ArmOp::F64Add {
10926            dd: VfpReg::D0,
10927            dn: VfpReg::D1,
10928            dm: VfpReg::D2,
10929        };
10930        let code = encoder.encode(&op).unwrap();
10931        assert_eq!(code.len(), 4);
10932        // VADD.F64 D0, D1, D2: check coprocessor is cp11 (0xB)
10933        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10934        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
10935    }
10936
10937    #[test]
10938    fn test_encode_f64_sub_thumb2() {
10939        let encoder = ArmEncoder::new_thumb2();
10940        let op = ArmOp::F64Sub {
10941            dd: VfpReg::D0,
10942            dn: VfpReg::D1,
10943            dm: VfpReg::D2,
10944        };
10945        let code = encoder.encode(&op).unwrap();
10946        assert_eq!(code.len(), 4); // 32-bit VFP as two Thumb halfwords
10947    }
10948
10949    #[test]
10950    fn test_encode_f64_mul_arm32() {
10951        let encoder = ArmEncoder::new_arm32();
10952        let op = ArmOp::F64Mul {
10953            dd: VfpReg::D0,
10954            dn: VfpReg::D1,
10955            dm: VfpReg::D2,
10956        };
10957        let code = encoder.encode(&op).unwrap();
10958        assert_eq!(code.len(), 4);
10959    }
10960
10961    #[test]
10962    fn test_encode_f64_div_arm32() {
10963        let encoder = ArmEncoder::new_arm32();
10964        let op = ArmOp::F64Div {
10965            dd: VfpReg::D0,
10966            dn: VfpReg::D1,
10967            dm: VfpReg::D2,
10968        };
10969        let code = encoder.encode(&op).unwrap();
10970        assert_eq!(code.len(), 4);
10971    }
10972
10973    #[test]
10974    fn test_encode_f64_abs_arm32() {
10975        let encoder = ArmEncoder::new_arm32();
10976        let op = ArmOp::F64Abs {
10977            dd: VfpReg::D0,
10978            dm: VfpReg::D2,
10979        };
10980        let code = encoder.encode(&op).unwrap();
10981        assert_eq!(code.len(), 4);
10982    }
10983
10984    #[test]
10985    fn test_encode_f64_neg_arm32() {
10986        let encoder = ArmEncoder::new_arm32();
10987        let op = ArmOp::F64Neg {
10988            dd: VfpReg::D0,
10989            dm: VfpReg::D2,
10990        };
10991        let code = encoder.encode(&op).unwrap();
10992        assert_eq!(code.len(), 4);
10993    }
10994
10995    #[test]
10996    fn test_encode_f64_sqrt_arm32() {
10997        let encoder = ArmEncoder::new_arm32();
10998        let op = ArmOp::F64Sqrt {
10999            dd: VfpReg::D0,
11000            dm: VfpReg::D2,
11001        };
11002        let code = encoder.encode(&op).unwrap();
11003        assert_eq!(code.len(), 4);
11004    }
11005
11006    #[test]
11007    fn test_encode_f64_load_arm32() {
11008        let encoder = ArmEncoder::new_arm32();
11009        let op = ArmOp::F64Load {
11010            dd: VfpReg::D0,
11011            addr: MemAddr::imm(Reg::R0, 8),
11012        };
11013        let code = encoder.encode(&op).unwrap();
11014        assert_eq!(code.len(), 4);
11015        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11016        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11 for F64
11017        assert_eq!(instr & 0xFF, 2); // offset 8 / 4 = 2
11018    }
11019
11020    #[test]
11021    fn test_encode_f64_store_thumb2() {
11022        let encoder = ArmEncoder::new_thumb2();
11023        let op = ArmOp::F64Store {
11024            dd: VfpReg::D0,
11025            addr: MemAddr::imm(Reg::SP, 0),
11026        };
11027        let code = encoder.encode(&op).unwrap();
11028        assert_eq!(code.len(), 4);
11029    }
11030
11031    #[test]
11032    fn test_encode_f64_compare_arm32() {
11033        let encoder = ArmEncoder::new_arm32();
11034        let op = ArmOp::F64Eq {
11035            rd: Reg::R0,
11036            dn: VfpReg::D0,
11037            dm: VfpReg::D1,
11038        };
11039        let code = encoder.encode(&op).unwrap();
11040        assert_eq!(code.len(), 16); // VCMP + VMRS + MOV #0 + MOVcond #1
11041    }
11042
11043    #[test]
11044    fn test_encode_f64_compare_thumb2() {
11045        let encoder = ArmEncoder::new_thumb2();
11046        let op = ArmOp::F64Lt {
11047            rd: Reg::R0,
11048            dn: VfpReg::D0,
11049            dm: VfpReg::D1,
11050        };
11051        let code = encoder.encode(&op).unwrap();
11052        // VCMP(4) + VMRS(4) + MOVS(2) + IT(2) + MOV(2) = 14
11053        assert_eq!(code.len(), 14);
11054    }
11055
11056    #[test]
11057    fn test_encode_f64_const_arm32() {
11058        let encoder = ArmEncoder::new_arm32();
11059        let op = ArmOp::F64Const {
11060            dd: VfpReg::D0,
11061            value: 3.125,
11062        };
11063        let code = encoder.encode(&op).unwrap();
11064        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
11065        assert_eq!(code.len(), 20);
11066    }
11067
11068    #[test]
11069    fn test_encode_f64_const_thumb2() {
11070        let encoder = ArmEncoder::new_thumb2();
11071        let op = ArmOp::F64Const {
11072            dd: VfpReg::D0,
11073            value: 2.5,
11074        };
11075        let code = encoder.encode(&op).unwrap();
11076        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
11077        assert_eq!(code.len(), 20);
11078    }
11079
11080    #[test]
11081    fn test_encode_f64_convert_i32s_arm32() {
11082        let encoder = ArmEncoder::new_arm32();
11083        let op = ArmOp::F64ConvertI32S {
11084            dd: VfpReg::D0,
11085            rm: Reg::R0,
11086        };
11087        let code = encoder.encode(&op).unwrap();
11088        // VMOV(4) + VCVT(4) = 8
11089        assert_eq!(code.len(), 8);
11090    }
11091
11092    #[test]
11093    fn test_encode_f64_promote_f32_arm32() {
11094        let encoder = ArmEncoder::new_arm32();
11095        let op = ArmOp::F64PromoteF32 {
11096            dd: VfpReg::D0,
11097            sm: VfpReg::S0,
11098        };
11099        let code = encoder.encode(&op).unwrap();
11100        assert_eq!(code.len(), 4); // Single VCVT.F64.F32 instruction
11101    }
11102
11103    #[test]
11104    fn test_encode_f64_promote_f32_thumb2() {
11105        let encoder = ArmEncoder::new_thumb2();
11106        let op = ArmOp::F64PromoteF32 {
11107            dd: VfpReg::D0,
11108            sm: VfpReg::S0,
11109        };
11110        let code = encoder.encode(&op).unwrap();
11111        assert_eq!(code.len(), 4);
11112    }
11113
11114    #[test]
11115    fn test_encode_i32_trunc_f64s_arm32() {
11116        let encoder = ArmEncoder::new_arm32();
11117        let op = ArmOp::I32TruncF64S {
11118            rd: Reg::R0,
11119            dm: VfpReg::D0,
11120        };
11121        let code = encoder.encode(&op).unwrap();
11122        // VCVT(4) + VMOV(4) = 8
11123        assert_eq!(code.len(), 8);
11124    }
11125
11126    #[test]
11127    fn test_encode_f64_reinterpret_i64_arm32() {
11128        let encoder = ArmEncoder::new_arm32();
11129        let op = ArmOp::F64ReinterpretI64 {
11130            dd: VfpReg::D0,
11131            rmlo: Reg::R0,
11132            rmhi: Reg::R1,
11133        };
11134        let code = encoder.encode(&op).unwrap();
11135        assert_eq!(code.len(), 4); // Single VMOV instruction
11136    }
11137
11138    #[test]
11139    fn test_encode_i64_reinterpret_f64_thumb2() {
11140        let encoder = ArmEncoder::new_thumb2();
11141        let op = ArmOp::I64ReinterpretF64 {
11142            rdlo: Reg::R0,
11143            rdhi: Reg::R1,
11144            dm: VfpReg::D0,
11145        };
11146        let code = encoder.encode(&op).unwrap();
11147        assert_eq!(code.len(), 4);
11148    }
11149
11150    #[test]
11151    fn test_encode_f64_trunc_thumb2() {
11152        let encoder = ArmEncoder::new_thumb2();
11153        let op = ArmOp::F64Trunc {
11154            dd: VfpReg::D0,
11155            dm: VfpReg::D1,
11156        };
11157        let code = encoder.encode(&op).unwrap();
11158        // GI-FPU-002 phase 3 (#369): a single VRINTZ.F64 (clang-verified
11159        // vrintz.f64 d0,d1 base) — no more FPSCR dance / S0 clobber.
11160        assert_eq!(code.len(), 4);
11161        assert_eq!(code, vec![0xb6, 0xee, 0xc1, 0x0b]);
11162    }
11163
11164    /// GI-FPU-002 phase 3 (#369): the rewritten f64 tail sequences, byte-exact
11165    /// against clang (`-target thumbv7em-none-eabi -mfpu=fpv5-d16`). Each
11166    /// clobbers ONLY its destination (+R12/flags where noted) — the previous
11167    /// pseudo-ops staged through live S0/R0-R2 (the #615 class) and the
11168    /// min/max/rounding semantics were wrong (ordered IT select returned the
11169    /// wrong operand on NaN/±0; rounding round-tripped through a 32-bit int).
11170    #[test]
11171    fn test_369_f64_tail_thumb2_encodings_match_clang() {
11172        let enc = ArmEncoder::new_thumb2();
11173        // vrintn/vrintp/vrintm.f64 d1, d2 (FE space, never IT'd).
11174        for (op, want) in [
11175            (
11176                ArmOp::F64Nearest {
11177                    dd: VfpReg::D1,
11178                    dm: VfpReg::D2,
11179                },
11180                vec![0xb9, 0xfe, 0x42, 0x1b],
11181            ),
11182            (
11183                ArmOp::F64Ceil {
11184                    dd: VfpReg::D1,
11185                    dm: VfpReg::D2,
11186                },
11187                vec![0xba, 0xfe, 0x42, 0x1b],
11188            ),
11189            (
11190                ArmOp::F64Floor {
11191                    dd: VfpReg::D1,
11192                    dm: VfpReg::D2,
11193                },
11194                vec![0xbb, 0xfe, 0x42, 0x1b],
11195            ),
11196        ] {
11197            assert_eq!(enc.encode(&op).unwrap(), want, "{op:?}");
11198        }
11199        // vcmp.f64 d1,d2 ; vmrs ; vminnm.f64 d0,d1,d2 ; it vs ; vaddvs.f64
11200        let min = enc
11201            .encode(&ArmOp::F64Min {
11202                dd: VfpReg::D0,
11203                dn: VfpReg::D1,
11204                dm: VfpReg::D2,
11205            })
11206            .unwrap();
11207        assert_eq!(
11208            min,
11209            vec![
11210                0xb4, 0xee, 0x42, 0x1b, // vcmp.f64 d1, d2
11211                0xf1, 0xee, 0x10, 0xfa, // vmrs APSR_nzcv, fpscr
11212                0x81, 0xfe, 0x42, 0x0b, // vminnm.f64 d0, d1, d2
11213                0x68, 0xbf, // it vs
11214                0x31, 0xee, 0x02, 0x0b, // vaddvs.f64 d0, d1, d2
11215            ]
11216        );
11217        // vmaxnm variant flips only bit6 of the VMINNM word.
11218        let max = enc
11219            .encode(&ArmOp::F64Max {
11220                dd: VfpReg::D0,
11221                dn: VfpReg::D1,
11222                dm: VfpReg::D2,
11223            })
11224            .unwrap();
11225        assert_eq!(&max[8..12], &[0x81, 0xfe, 0x02, 0x0b]);
11226        // Destination aliasing a source must ERR (the NaN fix-up would read
11227        // a clobbered operand), never encode.
11228        assert!(
11229            enc.encode(&ArmOp::F64Min {
11230                dd: VfpReg::D1,
11231                dn: VfpReg::D1,
11232                dm: VfpReg::D2,
11233            })
11234            .is_err()
11235        );
11236        // copysign d0,(mag)d1,(sign)d2:
11237        // vmov r12,s5 ; cmp.w r12,#0 ; vabs.f64 d0,d1 ; it mi ; vnegmi.f64 d0,d0
11238        let cs = enc
11239            .encode(&ArmOp::F64Copysign {
11240                dd: VfpReg::D0,
11241                dn: VfpReg::D1,
11242                dm: VfpReg::D2,
11243            })
11244            .unwrap();
11245        assert_eq!(
11246            cs,
11247            vec![
11248                0x12, 0xee, 0x90, 0xca, // vmov r12, s5
11249                0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
11250                0xb0, 0xee, 0xc1, 0x0b, // vabs.f64 d0, d1
11251                0x48, 0xbf, // it mi
11252                0xb1, 0xee, 0x40, 0x0b, // vnegmi.f64 d0, d0
11253            ]
11254        );
11255        // f32 copysign s0,(mag)s1,(sign)s2 — the R0-clobber-free rewrite:
11256        // vmov r12,s2 ; cmp.w r12,#0 ; vabs.f32 s0,s1 ; it mi ; vnegmi.f32
11257        let cs32 = enc
11258            .encode(&ArmOp::F32Copysign {
11259                sd: VfpReg::S0,
11260                sn: VfpReg::S1,
11261                sm: VfpReg::S2,
11262            })
11263            .unwrap();
11264        assert_eq!(
11265            cs32,
11266            vec![
11267                0x11, 0xee, 0x10, 0xca, // vmov r12, s2
11268                0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
11269                0xb0, 0xee, 0xe0, 0x0a, // vabs.f32 s0, s1
11270                0x48, 0xbf, // it mi
11271                0xb1, 0xee, 0x40, 0x0a, // vnegmi.f32 s0, s0
11272            ]
11273        );
11274        // i32 -> f64 stages through the DESTINATION's S-alias (never S0) and
11275        // uses the CORRECT signed/unsigned VCVT bases (previously swapped):
11276        // vmov s0,r3 ; vcvt.f64.s32 d0,s0
11277        let conv_s = enc
11278            .encode(&ArmOp::F64ConvertI32S {
11279                dd: VfpReg::D0,
11280                rm: Reg::R3,
11281            })
11282            .unwrap();
11283        assert_eq!(
11284            conv_s,
11285            vec![
11286                0x00, 0xee, 0x10, 0x3a, // vmov s0, r3
11287                0xb8, 0xee, 0xc0, 0x0b, // vcvt.f64.s32 d0, s0
11288            ]
11289        );
11290        let conv_u = enc
11291            .encode(&ArmOp::F64ConvertI32U {
11292                dd: VfpReg::D0,
11293                rm: Reg::R3,
11294            })
11295            .unwrap();
11296        assert_eq!(&conv_u[4..8], &[0xb8, 0xee, 0x40, 0x0b]); // vcvt.f64.u32
11297        // f64 -> i32 stages through the SOURCE's S-alias (never S0):
11298        // vcvt.s32.f64 s2,d1 ; vmov r3,s2
11299        let trunc_s = enc
11300            .encode(&ArmOp::I32TruncF64S {
11301                rd: Reg::R3,
11302                dm: VfpReg::D1,
11303            })
11304            .unwrap();
11305        assert_eq!(
11306            trunc_s,
11307            vec![
11308                0xbd, 0xee, 0xc1, 0x1b, // vcvt.s32.f64 s2, d1
11309                0x11, 0xee, 0x10, 0x3a, // vmov r3, s2
11310            ]
11311        );
11312        let trunc_u = enc
11313            .encode(&ArmOp::I32TruncF64U {
11314                rd: Reg::R3,
11315                dm: VfpReg::D1,
11316            })
11317            .unwrap();
11318        assert_eq!(&trunc_u[0..4], &[0xbc, 0xee, 0xc1, 0x1b]); // vcvt.u32.f64
11319        // f32.demote_f64: vcvt.f32.f64 s1, d2
11320        let demote = enc
11321            .encode(&ArmOp::F32DemoteF64 {
11322                sd: VfpReg::S1,
11323                dm: VfpReg::D2,
11324            })
11325            .unwrap();
11326        assert_eq!(demote, vec![0xf7, 0xee, 0xc2, 0x0b]);
11327    }
11328
11329    #[test]
11330    fn test_encode_f64_min_arm32() {
11331        let encoder = ArmEncoder::new_arm32();
11332        let op = ArmOp::F64Min {
11333            dd: VfpReg::D0,
11334            dn: VfpReg::D1,
11335            dm: VfpReg::D2,
11336        };
11337        let code = encoder.encode(&op).unwrap();
11338        // VMOV + VCMP + VMRS + conditional VMOV = 16
11339        assert_eq!(code.len(), 16);
11340    }
11341
11342    #[test]
11343    fn test_f64_cp11_encoding() {
11344        // Verify that F64 instructions use coprocessor 11 (0xB), not 10 (0xA)
11345        let encoder = ArmEncoder::new_arm32();
11346
11347        // F64Add
11348        let code = encoder
11349            .encode(&ArmOp::F64Add {
11350                dd: VfpReg::D0,
11351                dn: VfpReg::D0,
11352                dm: VfpReg::D0,
11353            })
11354            .unwrap();
11355        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11356        assert_eq!((instr >> 8) & 0xF, 0xB, "F64 should use cp11");
11357
11358        // F32Add for comparison
11359        let code = encoder
11360            .encode(&ArmOp::F32Add {
11361                sd: VfpReg::S0,
11362                sn: VfpReg::S0,
11363                sm: VfpReg::S0,
11364            })
11365            .unwrap();
11366        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11367        assert_eq!((instr >> 8) & 0xF, 0xA, "F32 should use cp10");
11368    }
11369
11370    #[test]
11371    fn test_dreg_encoding_higher_registers() {
11372        let encoder = ArmEncoder::new_arm32();
11373
11374        // Test with D15 (highest register)
11375        let op = ArmOp::F64Add {
11376            dd: VfpReg::D15,
11377            dn: VfpReg::D14,
11378            dm: VfpReg::D13,
11379        };
11380        let code = encoder.encode(&op).unwrap();
11381        assert_eq!(code.len(), 4);
11382
11383        // Verify the register encoding worked (instruction is valid)
11384        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11385        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
11386    }
11387
11388    // ========================================================================
11389    // Control flow encoding tests
11390    // ========================================================================
11391
11392    #[test]
11393    fn test_encode_label_emits_no_bytes() {
11394        let encoder = ArmEncoder::new_thumb2();
11395        let op = ArmOp::Label {
11396            name: ".Lblock_end_0".to_string(),
11397        };
11398        let code = encoder.encode(&op).unwrap();
11399        assert!(code.is_empty(), "Label should emit zero bytes");
11400
11401        let encoder32 = ArmEncoder::new_arm32();
11402        let code32 = encoder32.encode(&op).unwrap();
11403        assert!(
11404            code32.is_empty(),
11405            "Label should emit zero bytes in ARM32 too"
11406        );
11407    }
11408
11409    #[test]
11410    fn test_encode_bcc_eq_thumb2() {
11411        use synth_synthesis::Condition;
11412        let encoder = ArmEncoder::new_thumb2();
11413        let op = ArmOp::Bcc {
11414            cond: Condition::EQ,
11415            label: "target".to_string(),
11416        };
11417        let code = encoder.encode(&op).unwrap();
11418        assert_eq!(code.len(), 2); // 16-bit conditional branch
11419
11420        // BEQ with offset 0: 0xD000 in little-endian
11421        assert_eq!(code, vec![0x00, 0xD0]);
11422    }
11423
11424    #[test]
11425    fn test_encode_bcc_ne_thumb2() {
11426        use synth_synthesis::Condition;
11427        let encoder = ArmEncoder::new_thumb2();
11428        let op = ArmOp::Bcc {
11429            cond: Condition::NE,
11430            label: "target".to_string(),
11431        };
11432        let code = encoder.encode(&op).unwrap();
11433        assert_eq!(code.len(), 2);
11434
11435        // BNE with offset 0: 0xD100 in little-endian
11436        assert_eq!(code, vec![0x00, 0xD1]);
11437    }
11438
11439    #[test]
11440    fn test_encode_bcc_arm32() {
11441        use synth_synthesis::Condition;
11442        let encoder = ArmEncoder::new_arm32();
11443        let op = ArmOp::Bcc {
11444            cond: Condition::EQ,
11445            label: "target".to_string(),
11446        };
11447        let code = encoder.encode(&op).unwrap();
11448        assert_eq!(code.len(), 4); // 32-bit ARM instruction
11449
11450        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11451        // BEQ: cond=0x0, opcode=0xA, offset=0
11452        assert_eq!(instr & 0xF0000000, 0x00000000); // EQ condition
11453        assert_eq!(instr & 0x0F000000, 0x0A000000); // Branch opcode
11454    }
11455
11456    #[test]
11457    fn test_encode_udf_thumb2() {
11458        let encoder = ArmEncoder::new_thumb2();
11459        let op = ArmOp::Udf { imm: 0 };
11460        let code = encoder.encode(&op).unwrap();
11461        assert_eq!(code.len(), 2); // 16-bit
11462
11463        // UDF #0: 0xDE00 in little-endian
11464        assert_eq!(code, vec![0x00, 0xDE]);
11465    }
11466
11467    /// #610: the i64 rot/div/rem expansions must land the result in the
11468    /// selector-assigned rd pair and leave R0-R3 preserved (restored from the
11469    /// fixed-ABI wrapper's save area) — pre-#610 the rot expansion's own
11470    /// `POP {R4}` restored stale scratch OVER the result (rd_lo == R4) and
11471    /// the div/rem expansions ignored their register fields outright.
11472    #[test]
11473    fn test_610_i64_rot_expansion_ends_with_rd_movs_and_restore() {
11474        let encoder = ArmEncoder::new_thumb2();
11475        for op in [
11476            ArmOp::I64Rotl {
11477                rdlo: Reg::R4,
11478                rdhi: Reg::R5,
11479                rnlo: Reg::R0,
11480                rnhi: Reg::R1,
11481                shift: Reg::R2,
11482            },
11483            ArmOp::I64Rotr {
11484                rdlo: Reg::R4,
11485                rdhi: Reg::R5,
11486                rnlo: Reg::R0,
11487                rnhi: Reg::R1,
11488                shift: Reg::R2,
11489            },
11490        ] {
11491            let code = encoder.encode(&op).unwrap();
11492            assert_eq!(code.len(), 102, "register-independent size (estimator pin)");
11493            // Tail: MOV r5, r1 (0x460D); MOV r4, r0 (0x4604); POP {r0..r3}
11494            // (rd pair r4:r5 does not overlap the save area — all 4 restored).
11495            let tail: Vec<u16> = code[code.len() - 12..]
11496                .chunks(2)
11497                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11498                .collect();
11499            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11500        }
11501    }
11502
11503    /// #610: div/rem expansions honor rd and carry the divide-by-zero trap
11504    /// guard (`ORRS R12, R2, R3; BNE +0; UDF #0`) after operand marshaling.
11505    #[test]
11506    fn test_610_i64_div_rem_expansion_guard_and_rd() {
11507        let encoder = ArmEncoder::new_thumb2();
11508        let mk = |which: u8| {
11509            let (rdlo, rdhi, rnlo, rnhi, rmlo, rmhi) =
11510                (Reg::R4, Reg::R5, Reg::R0, Reg::R1, Reg::R2, Reg::R3);
11511            match which {
11512                0 => ArmOp::I64DivU {
11513                    rdlo,
11514                    rdhi,
11515                    rnlo,
11516                    rnhi,
11517                    rmlo,
11518                    rmhi,
11519                    elide_zero_guard: false,
11520                },
11521                1 => ArmOp::I64RemU {
11522                    rdlo,
11523                    rdhi,
11524                    rnlo,
11525                    rnhi,
11526                    rmlo,
11527                    rmhi,
11528                    elide_zero_guard: false,
11529                },
11530                2 => ArmOp::I64DivS {
11531                    rdlo,
11532                    rdhi,
11533                    rnlo,
11534                    rnhi,
11535                    rmlo,
11536                    rmhi,
11537                    elide_zero_guard: false,
11538                    elide_overflow_guard: false,
11539                },
11540                _ => ArmOp::I64RemS {
11541                    rdlo,
11542                    rdhi,
11543                    rnlo,
11544                    rnhi,
11545                    rmlo,
11546                    rmhi,
11547                    elide_zero_guard: false,
11548                },
11549            }
11550        };
11551        for which in 0..4u8 {
11552            let code = encoder.encode(&mk(which)).unwrap();
11553            // Zero-divisor trap guard right after the 26-byte marshal prologue.
11554            let guard: Vec<u16> = code[26..34]
11555                .chunks(2)
11556                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11557                .collect();
11558            assert_eq!(
11559                guard,
11560                vec![0xEA52, 0x0C03, 0xD100, 0xDE00],
11561                "ORRS R12,R2,R3; BNE +0; UDF #0"
11562            );
11563            // Tail: result into rd pair (r5:r4), then restore all of R0-R3.
11564            let tail: Vec<u16> = code[code.len() - 12..]
11565                .chunks(2)
11566                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11567                .collect();
11568            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11569        }
11570    }
11571
11572    /// #610: when rd overlaps R0-R3 the restore must SKIP the result
11573    /// registers (drop the saved caller word) instead of popping over them.
11574    #[test]
11575    fn test_610_i64_divu_rd_in_r0_r1_skips_restore() {
11576        let encoder = ArmEncoder::new_thumb2();
11577        let code = encoder
11578            .encode(&ArmOp::I64DivU {
11579                rdlo: Reg::R0,
11580                rdhi: Reg::R1,
11581                rnlo: Reg::R0,
11582                rnhi: Reg::R1,
11583                rmlo: Reg::R2,
11584                rmhi: Reg::R3,
11585                elide_zero_guard: false,
11586            })
11587            .unwrap();
11588        let tail: Vec<u16> = code[code.len() - 12..]
11589            .chunks(2)
11590            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11591            .collect();
11592        // MOV r1,r1 / MOV r0,r0 (no-ops, size-stable), ADD SP,#4 twice
11593        // (discard saved r0/r1 — the result lives there), POP {r2}, POP {r3}.
11594        assert_eq!(tail, vec![0x4609, 0x4600, 0xB001, 0xB001, 0xBC04, 0xBC08]);
11595    }
11596
11597    /// #610: a fully swapped rd pair (rd_lo=R1, rd_hi=R0) cannot be
11598    /// materialized by two MOVs in either order — must be a loud Err, never
11599    /// silent corruption. (Selector pairs are consecutive, so unreachable.)
11600    #[test]
11601    fn test_610_i64_swapped_rd_pair_rejected() {
11602        let encoder = ArmEncoder::new_thumb2();
11603        let result = encoder.encode(&ArmOp::I64RemU {
11604            rdlo: Reg::R1,
11605            rdhi: Reg::R0,
11606            rnlo: Reg::R2,
11607            rnhi: Reg::R3,
11608            rmlo: Reg::R4,
11609            rmhi: Reg::R5,
11610            elide_zero_guard: false,
11611        });
11612        assert!(result.is_err(), "swapped rd pair must be rejected loudly");
11613    }
11614
11615    /// #632: the I64Popcnt expansion's own scratch restore (`POP {R3,R4,R5}`)
11616    /// must not clobber the result. Pre-fix the total was materialized with
11617    /// `ADDS rd, R4, R5` BEFORE the pop, so any allocator-assigned
11618    /// rd ∈ {R3,R4,R5} received stale stack garbage. Post-fix the count is
11619    /// carried across the restore in R12 (never allocatable, never restored)
11620    /// and moved into rd only after the pop — structurally rd-independent.
11621    #[test]
11622    fn test_632_i64_popcnt_result_survives_scratch_restore() {
11623        let encoder = ArmEncoder::new_thumb2();
11624        // Every allocatable rd, including the restore set {R3,R4,R5} and R8.
11625        for rd in [
11626            Reg::R0,
11627            Reg::R2,
11628            Reg::R3,
11629            Reg::R4,
11630            Reg::R5,
11631            Reg::R6,
11632            Reg::R8,
11633        ] {
11634            let code = encoder
11635                .encode(&ArmOp::I64Popcnt {
11636                    rd,
11637                    rnlo: Reg::R6,
11638                    rnhi: Reg::R7,
11639                })
11640                .unwrap();
11641            assert_eq!(code.len(), 176, "register-independent size (estimator pin)");
11642            let hw: Vec<u16> = code
11643                .chunks(2)
11644                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11645                .collect();
11646            let pop = hw
11647                .iter()
11648                .position(|&h| h == 0xBC38)
11649                .expect("POP {R3,R4,R5} present");
11650            // Immediately before the POP: ADD.W R12, R4, R5 (the total lives
11651            // in R12, which the POP cannot touch).
11652            assert_eq!(
11653                &hw[pop - 2..pop],
11654                &[0xEB04, 0x0C05],
11655                "total must be carried in R12 across the restore"
11656            );
11657            // Immediately after the POP: MOV rd, R12.
11658            let rd_bits = match rd {
11659                Reg::R8 => 8u16,
11660                Reg::R6 => 6,
11661                Reg::R5 => 5,
11662                Reg::R4 => 4,
11663                Reg::R3 => 3,
11664                Reg::R2 => 2,
11665                _ => 0,
11666            };
11667            let expect_mov = 0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7);
11668            assert_eq!(hw[pop + 1], expect_mov, "MOV rd, R12 after the restore");
11669            // No write into rd between the PUSH and the POP (the old
11670            // pre-restore ADDS is gone).
11671            assert!(
11672                !hw[..pop].contains(&(0x1800 | (5 << 6) | (4 << 3) | rd_bits)),
11673                "no ADDS rd, R4, R5 before the restore pop"
11674            );
11675        }
11676    }
11677
11678    /// #632 audit: the entry marshal must be permutation-safe. Pre-fix
11679    /// `MOV R4, rnlo; MOV R5, rnhi` read a clobbered R4 when the operand
11680    /// pair lived at (R3, R4). Post-fix rnlo routes through R12.
11681    #[test]
11682    fn test_632_i64_popcnt_marshal_pair_at_r3_r4() {
11683        let encoder = ArmEncoder::new_thumb2();
11684        let code = encoder
11685            .encode(&ArmOp::I64Popcnt {
11686                rd: Reg::R0,
11687                rnlo: Reg::R3,
11688                rnhi: Reg::R4,
11689            })
11690            .unwrap();
11691        let hw: Vec<u16> = code
11692            .chunks(2)
11693            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11694            .collect();
11695        // PUSH {R3,R4,R5}; MOV R12, R3; MOV R5, R4 (rnhi read BEFORE any
11696        // write to R4); MOV R4, R12.
11697        assert_eq!(hw[0], 0xB438);
11698        assert_eq!(hw[1], 0x4600 | (1 << 7) | (3 << 3) | 4, "MOV R12, rnlo");
11699        assert_eq!(hw[2], 0x4600 | (4 << 3) | 5, "MOV R5, rnhi");
11700        assert_eq!(hw[3], 0x4664, "MOV R4, R12");
11701    }
11702
11703    /// #632: A32 twin — same structural fix on the ARM-mode path
11704    /// (`--target cortex-r5`): total carried in R12 across the restore.
11705    #[test]
11706    fn test_632_a32_i64_popcnt_result_survives_scratch_restore() {
11707        let encoder = ArmEncoder::new_arm32();
11708        for rd in [Reg::R0, Reg::R3, Reg::R4, Reg::R5, Reg::R8] {
11709            let code = encoder
11710                .encode(&ArmOp::I64Popcnt {
11711                    rd,
11712                    rnlo: Reg::R6,
11713                    rnhi: Reg::R7,
11714                })
11715                .unwrap();
11716            let words: Vec<u32> = code
11717                .chunks(4)
11718                .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11719                .collect();
11720            let pop = words
11721                .iter()
11722                .position(|&w| w == 0xE8BD_0038)
11723                .expect("POP {R3,R4,R5} present");
11724            assert_eq!(words[pop - 1], 0xE084_C005, "ADD R12, R4, R5 before POP");
11725            let rd_bits = match rd {
11726                Reg::R8 => 8u32,
11727                Reg::R5 => 5,
11728                Reg::R4 => 4,
11729                Reg::R3 => 3,
11730                _ => 0,
11731            };
11732            assert_eq!(
11733                words[pop + 1],
11734                0xE1A0_0000 | (rd_bits << 12) | 12,
11735                "MOV rd, R12 after the restore"
11736            );
11737        }
11738    }
11739
11740    /// #633: I64DivS must carry the INT64_MIN/-1 overflow guard (mirroring
11741    /// the i32 path) right after the zero-divisor guard — dividend in R0:R1,
11742    /// divisor in R2:R3 on the #610/#613 fixed-ABI wrapper path.
11743    #[test]
11744    fn test_633_i64_divs_overflow_guard_emitted() {
11745        let encoder = ArmEncoder::new_thumb2();
11746        let code = encoder
11747            .encode(&ArmOp::I64DivS {
11748                rdlo: Reg::R4,
11749                rdhi: Reg::R5,
11750                rnlo: Reg::R0,
11751                rnhi: Reg::R1,
11752                rmlo: Reg::R2,
11753                rmhi: Reg::R3,
11754                elide_zero_guard: false,
11755                elide_overflow_guard: false,
11756            })
11757            .unwrap();
11758        // 26-byte marshal + 8-byte zero-trap, then the 22-byte overflow guard.
11759        let guard: Vec<u16> = code[34..56]
11760            .chunks(2)
11761            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11762            .collect();
11763        assert_eq!(
11764            guard,
11765            vec![
11766                0xEA02, 0x0C03, // AND.W R12, R2, R3
11767                0xF11C, 0x0F01, // CMN.W R12, #1
11768                0xD105, // BNE .no_trap
11769                0x2800, // CMP R0, #0
11770                0xD103, // BNE .no_trap
11771                0xF1B1, 0x4F00, // CMP.W R1, #0x80000000
11772                0xD100, // BNE .no_trap
11773                0xDE00, // UDF #0 — signed-division overflow
11774            ],
11775            "INT64_MIN/-1 overflow guard after the zero-divisor guard"
11776        );
11777    }
11778
11779    /// #633 fix-guard twin: I64RemS must NOT carry the overflow guard —
11780    /// rem_s(INT64_MIN, -1) is defined as 0 and must not trap. Exactly one
11781    /// UDF (the zero-divisor trap) in the whole expansion.
11782    #[test]
11783    fn test_633_i64_rems_has_no_overflow_guard() {
11784        let encoder = ArmEncoder::new_thumb2();
11785        for (is_rem_s, op) in [
11786            (
11787                true,
11788                ArmOp::I64RemS {
11789                    rdlo: Reg::R4,
11790                    rdhi: Reg::R5,
11791                    rnlo: Reg::R0,
11792                    rnhi: Reg::R1,
11793                    rmlo: Reg::R2,
11794                    rmhi: Reg::R3,
11795                    elide_zero_guard: false,
11796                },
11797            ),
11798            (
11799                false,
11800                ArmOp::I64DivS {
11801                    rdlo: Reg::R4,
11802                    rdhi: Reg::R5,
11803                    rnlo: Reg::R0,
11804                    rnhi: Reg::R1,
11805                    rmlo: Reg::R2,
11806                    rmhi: Reg::R3,
11807                    elide_zero_guard: false,
11808                    elide_overflow_guard: false,
11809                },
11810            ),
11811        ] {
11812            let code = encoder.encode(&op).unwrap();
11813            let udfs = code
11814                .chunks(2)
11815                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11816                .count();
11817            let want = if is_rem_s { 1 } else { 2 };
11818            assert_eq!(
11819                udfs, want,
11820                "rem_s: zero-trap only; div_s: zero-trap + overflow trap"
11821            );
11822        }
11823    }
11824
11825    /// #494 phase 2b: `elide_zero_guard` drops EXACTLY the 8-byte fused
11826    /// zero-trap (`ORRS.W R12,R2,R3; BNE; UDF #0`) and nothing else — the
11827    /// rest of the expansion is byte-identical (splice check).
11828    #[test]
11829    fn test_494_i64_zero_guard_elision_is_exact_splice() {
11830        let encoder = ArmEncoder::new_thumb2();
11831        let mk = |elide_zero_guard: bool| {
11832            encoder
11833                .encode(&ArmOp::I64DivU {
11834                    rdlo: Reg::R4,
11835                    rdhi: Reg::R5,
11836                    rnlo: Reg::R0,
11837                    rnhi: Reg::R1,
11838                    rmlo: Reg::R2,
11839                    rmhi: Reg::R3,
11840                    elide_zero_guard,
11841                })
11842                .unwrap()
11843        };
11844        let full = mk(false);
11845        let elided = mk(true);
11846        assert_eq!(full.len(), elided.len() + 8, "zero guard is 8 bytes");
11847        // Marshal prologue (26 B) unchanged, guard (8 B) gone, tail identical.
11848        assert_eq!(&full[..26], &elided[..26]);
11849        assert_eq!(
11850            &full[26..34],
11851            &[0x52, 0xEA, 0x03, 0x0C, 0x00, 0xD1, 0x00, 0xDE],
11852            "the spliced-out bytes are exactly ORRS.W; BNE; UDF #0"
11853        );
11854        assert_eq!(&full[34..], &elided[26..]);
11855    }
11856
11857    /// #494 phase 2b two-guard distinction (the #633/#634 synergy): a
11858    /// divisor-nonzero fact elides ONLY the zero guard — the INT64_MIN/-1
11859    /// OVERFLOW guard is a separate obligation and must survive
11860    /// `elide_zero_guard: true`. Pinned on div_s in all flag states.
11861    #[test]
11862    fn test_494_i64_divs_overflow_guard_retained_when_only_zero_elided() {
11863        let encoder = ArmEncoder::new_thumb2();
11864        let mk = |zero: bool, ovf: bool| {
11865            encoder
11866                .encode(&ArmOp::I64DivS {
11867                    rdlo: Reg::R4,
11868                    rdhi: Reg::R5,
11869                    rnlo: Reg::R0,
11870                    rnhi: Reg::R1,
11871                    rmlo: Reg::R2,
11872                    rmhi: Reg::R3,
11873                    elide_zero_guard: zero,
11874                    elide_overflow_guard: ovf,
11875                })
11876                .unwrap()
11877        };
11878        let udf_count = |code: &[u8]| {
11879            code.chunks(2)
11880                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11881                .count()
11882        };
11883        let full = mk(false, false);
11884        let zero_only = mk(true, false);
11885        let both = mk(true, true);
11886        assert_eq!(udf_count(&full), 2, "baseline: zero trap + overflow trap");
11887        assert_eq!(
11888            udf_count(&zero_only),
11889            1,
11890            "divisor-nonzero elides the zero trap ONLY — the #633 overflow \
11891             guard must be retained"
11892        );
11893        // The retained guard is the 22-byte overflow sequence, now right
11894        // after the 26-byte marshal prologue.
11895        let guard: Vec<u16> = zero_only[26..48]
11896            .chunks(2)
11897            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11898            .collect();
11899        assert_eq!(
11900            guard,
11901            vec![
11902                0xEA02, 0x0C03, 0xF11C, 0x0F01, 0xD105, 0x2800, 0xD103, 0xF1B1, 0x4F00, 0xD100,
11903                0xDE00,
11904            ],
11905            "the surviving guard is the INT64_MIN/-1 overflow trap"
11906        );
11907        assert_eq!(full.len(), zero_only.len() + 8);
11908        assert_eq!(zero_only.len(), both.len() + 22);
11909        assert_eq!(udf_count(&both), 0, "both obligations discharged ⇒ no UDF");
11910    }
11911
11912    /// #494 phase 2b A32 twin: zero-guard elision is an exact 12-byte splice
11913    /// and the A32 overflow guard survives a zero-only elision.
11914    #[test]
11915    fn test_494_a32_i64_guard_elision() {
11916        let encoder = ArmEncoder::new_arm32();
11917        let mk = |zero: bool, ovf: bool| {
11918            encoder
11919                .encode(&ArmOp::I64DivS {
11920                    rdlo: Reg::R4,
11921                    rdhi: Reg::R5,
11922                    rnlo: Reg::R0,
11923                    rnhi: Reg::R1,
11924                    rmlo: Reg::R2,
11925                    rmhi: Reg::R3,
11926                    elide_zero_guard: zero,
11927                    elide_overflow_guard: ovf,
11928                })
11929                .unwrap()
11930        };
11931        let full = mk(false, false);
11932        let zero_only = mk(true, false);
11933        let both = mk(true, true);
11934        // A32 zero guard = 3 words (ORRS/BNE/UDF), overflow guard = 6 words.
11935        assert_eq!(full.len(), zero_only.len() + 12);
11936        assert_eq!(zero_only.len(), both.len() + 24);
11937        let udf_count = |code: &[u8]| {
11938            code.chunks(4)
11939                .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
11940                .count()
11941        };
11942        assert_eq!(udf_count(&full), 2);
11943        assert_eq!(
11944            udf_count(&zero_only),
11945            1,
11946            "A32: overflow guard retained under zero-only elision"
11947        );
11948        assert_eq!(udf_count(&both), 0);
11949    }
11950
11951    /// #633: A32 twin — the conditional-execution overflow guard on the
11952    /// ARM-mode I64DivS, and its absence from I64RemS.
11953    #[test]
11954    fn test_633_a32_i64_divs_overflow_guard() {
11955        let encoder = ArmEncoder::new_arm32();
11956        let mk_divs = ArmOp::I64DivS {
11957            rdlo: Reg::R4,
11958            rdhi: Reg::R5,
11959            rnlo: Reg::R0,
11960            rnhi: Reg::R1,
11961            rmlo: Reg::R2,
11962            rmhi: Reg::R3,
11963            elide_zero_guard: false,
11964            elide_overflow_guard: false,
11965        };
11966        let code = encoder.encode(&mk_divs).unwrap();
11967        let words: Vec<u32> = code
11968            .chunks(4)
11969            .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11970            .collect();
11971        let guard = [
11972            0xE002_C003u32, // AND   R12, R2, R3
11973            0xE37C_0001,    // CMN   R12, #1
11974            0x0350_0000,    // CMPEQ R0, #0
11975            0x0351_0102,    // CMPEQ R1, #0x80000000
11976            0x1A00_0000,    // BNE +1 insn
11977            0xE7F0_00F0,    // UDF #0
11978        ];
11979        assert!(
11980            words.windows(6).any(|w| w == guard),
11981            "A32 I64DivS carries the INT64_MIN/-1 overflow guard"
11982        );
11983        let rems = encoder
11984            .encode(&ArmOp::I64RemS {
11985                rdlo: Reg::R4,
11986                rdhi: Reg::R5,
11987                rnlo: Reg::R0,
11988                rnhi: Reg::R1,
11989                rmlo: Reg::R2,
11990                rmhi: Reg::R3,
11991                elide_zero_guard: false,
11992            })
11993            .unwrap();
11994        let rems_udfs = rems
11995            .chunks(4)
11996            .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
11997            .count();
11998        assert_eq!(rems_udfs, 1, "A32 I64RemS keeps only the zero-divisor trap");
11999    }
12000
12001    #[test]
12002    fn test_encode_nop_thumb2() {
12003        let encoder = ArmEncoder::new_thumb2();
12004        let op = ArmOp::Nop;
12005        let code = encoder.encode(&op).unwrap();
12006        assert_eq!(code.len(), 2); // 16-bit
12007
12008        // NOP: 0xBF00 in little-endian
12009        assert_eq!(code, vec![0x00, 0xBF]);
12010    }
12011
12012    // =========================================================================
12013    // i64 Thumb-2 encoding tests
12014    // =========================================================================
12015
12016    #[test]
12017    fn test_encode_i64_add_thumb2() {
12018        let encoder = ArmEncoder::new_thumb2();
12019        let op = ArmOp::I64Add {
12020            rdlo: Reg::R0,
12021            rdhi: Reg::R1,
12022            rnlo: Reg::R0,
12023            rnhi: Reg::R1,
12024            rmlo: Reg::R2,
12025            rmhi: Reg::R3,
12026        };
12027        let code = encoder.encode(&op).unwrap();
12028        // Should emit ADDS (2 bytes) + ADC.W (4 bytes) = 6 bytes
12029        assert_eq!(code.len(), 6, "I64Add should be 6 bytes (ADDS + ADC.W)");
12030    }
12031
12032    #[test]
12033    fn test_encode_i64_sub_thumb2() {
12034        let encoder = ArmEncoder::new_thumb2();
12035        let op = ArmOp::I64Sub {
12036            rdlo: Reg::R0,
12037            rdhi: Reg::R1,
12038            rnlo: Reg::R0,
12039            rnhi: Reg::R1,
12040            rmlo: Reg::R2,
12041            rmhi: Reg::R3,
12042        };
12043        let code = encoder.encode(&op).unwrap();
12044        // Should emit SUBS (2 bytes) + SBC.W (4 bytes) = 6 bytes
12045        assert_eq!(code.len(), 6, "I64Sub should be 6 bytes (SUBS + SBC.W)");
12046    }
12047
12048    #[test]
12049    fn test_encode_i64_and_thumb2() {
12050        let encoder = ArmEncoder::new_thumb2();
12051        let op = ArmOp::I64And {
12052            rdlo: Reg::R0,
12053            rdhi: Reg::R1,
12054            rnlo: Reg::R0,
12055            rnhi: Reg::R1,
12056            rmlo: Reg::R2,
12057            rmhi: Reg::R3,
12058        };
12059        let code = encoder.encode(&op).unwrap();
12060        // AND.W (4 bytes) + AND.W (4 bytes) = 8 bytes
12061        assert!(code.len() >= 4, "I64And should emit at least 4 bytes");
12062    }
12063
12064    #[test]
12065    fn test_encode_i64_or_thumb2() {
12066        let encoder = ArmEncoder::new_thumb2();
12067        let op = ArmOp::I64Or {
12068            rdlo: Reg::R0,
12069            rdhi: Reg::R1,
12070            rnlo: Reg::R0,
12071            rnhi: Reg::R1,
12072            rmlo: Reg::R2,
12073            rmhi: Reg::R3,
12074        };
12075        let code = encoder.encode(&op).unwrap();
12076        assert!(code.len() >= 4, "I64Or should emit at least 4 bytes");
12077    }
12078
12079    #[test]
12080    fn test_encode_i64_xor_thumb2() {
12081        let encoder = ArmEncoder::new_thumb2();
12082        let op = ArmOp::I64Xor {
12083            rdlo: Reg::R0,
12084            rdhi: Reg::R1,
12085            rnlo: Reg::R0,
12086            rnhi: Reg::R1,
12087            rmlo: Reg::R2,
12088            rmhi: Reg::R3,
12089        };
12090        let code = encoder.encode(&op).unwrap();
12091        assert!(code.len() >= 4, "I64Xor should emit at least 4 bytes");
12092    }
12093
12094    #[test]
12095    fn test_encode_i64_const_small_thumb2() {
12096        let encoder = ArmEncoder::new_thumb2();
12097        // Small constant: only needs MOVW for each half
12098        let op = ArmOp::I64Const {
12099            rdlo: Reg::R0,
12100            rdhi: Reg::R1,
12101            value: 42,
12102        };
12103        let code = encoder.encode(&op).unwrap();
12104        // MOVW R0, #42 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes minimum
12105        assert!(code.len() >= 8, "I64Const should emit at least 8 bytes");
12106    }
12107
12108    #[test]
12109    fn test_encode_i64_const_large_thumb2() {
12110        let encoder = ArmEncoder::new_thumb2();
12111        // Large constant: needs MOVW+MOVT for each half
12112        let op = ArmOp::I64Const {
12113            rdlo: Reg::R0,
12114            rdhi: Reg::R1,
12115            value: 0x1234_5678_9ABC_DEF0_u64 as i64,
12116        };
12117        let code = encoder.encode(&op).unwrap();
12118        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
12119        assert_eq!(
12120            code.len(),
12121            16,
12122            "I64Const with large value should be 16 bytes"
12123        );
12124    }
12125
12126    #[test]
12127    fn test_encode_i64_extend_i32_s_thumb2() {
12128        let encoder = ArmEncoder::new_thumb2();
12129        let op = ArmOp::I64ExtendI32S {
12130            rdlo: Reg::R0,
12131            rdhi: Reg::R1,
12132            rn: Reg::R0,
12133        };
12134        let code = encoder.encode(&op).unwrap();
12135        // When rdlo == rn, only ASR (4 bytes) is emitted
12136        assert_eq!(
12137            code.len(),
12138            4,
12139            "I64ExtendI32S (same reg) should be 4 bytes (ASR only)"
12140        );
12141    }
12142
12143    #[test]
12144    fn test_encode_i64_extend_i32_s_diff_reg_thumb2() {
12145        let encoder = ArmEncoder::new_thumb2();
12146        let op = ArmOp::I64ExtendI32S {
12147            rdlo: Reg::R0,
12148            rdhi: Reg::R1,
12149            rn: Reg::R2,
12150        };
12151        let code = encoder.encode(&op).unwrap();
12152        // MOV rdlo, rn (2 bytes for low regs) + ASR rdhi, rdlo, #31 (4 bytes) = 6 bytes
12153        assert!(
12154            code.len() >= 6,
12155            "I64ExtendI32S (diff reg) should be at least 6 bytes"
12156        );
12157    }
12158
12159    #[test]
12160    fn test_encode_i64_extend_i32_u_thumb2() {
12161        let encoder = ArmEncoder::new_thumb2();
12162        let op = ArmOp::I64ExtendI32U {
12163            rdlo: Reg::R0,
12164            rdhi: Reg::R1,
12165            rn: Reg::R0,
12166        };
12167        let code = encoder.encode(&op).unwrap();
12168        // When rdlo == rn, only MOV rdhi, #0 (2 bytes) is emitted
12169        assert_eq!(
12170            code.len(),
12171            2,
12172            "I64ExtendI32U (same reg) should be 2 bytes (MOV #0 only)"
12173        );
12174    }
12175
12176    #[test]
12177    fn test_encode_i32_wrap_i64_nop_thumb2() {
12178        let encoder = ArmEncoder::new_thumb2();
12179        // When rd == rnlo, should be a NOP
12180        let op = ArmOp::I32WrapI64 {
12181            rd: Reg::R0,
12182            rnlo: Reg::R0,
12183        };
12184        let code = encoder.encode(&op).unwrap();
12185        assert_eq!(code.len(), 2, "I32WrapI64 same reg should be NOP (2 bytes)");
12186        assert_eq!(code, vec![0x00, 0xBF]); // NOP
12187    }
12188
12189    #[test]
12190    fn test_encode_i32_wrap_i64_diff_reg_thumb2() {
12191        let encoder = ArmEncoder::new_thumb2();
12192        let op = ArmOp::I32WrapI64 {
12193            rd: Reg::R2,
12194            rnlo: Reg::R0,
12195        };
12196        let code = encoder.encode(&op).unwrap();
12197        // MOV R2, R0 (2 or 4 bytes)
12198        assert!(
12199            code.len() >= 2,
12200            "I32WrapI64 diff reg should emit at least 2 bytes"
12201        );
12202    }
12203
12204    #[test]
12205    fn test_encode_i64_eqz_thumb2() {
12206        let encoder = ArmEncoder::new_thumb2();
12207        let op = ArmOp::I64Eqz {
12208            rd: Reg::R0,
12209            rnlo: Reg::R0,
12210            rnhi: Reg::R1,
12211        };
12212        let code = encoder.encode(&op).unwrap();
12213        // Delegates to I64SetCondZ which is already encoded
12214        assert!(
12215            code.len() >= 6,
12216            "I64Eqz should emit at least 6 bytes for ORR+ITE+MOV+MOV"
12217        );
12218    }
12219
12220    #[test]
12221    fn test_encode_i64_eq_thumb2() {
12222        let encoder = ArmEncoder::new_thumb2();
12223        let op = ArmOp::I64Eq {
12224            rd: Reg::R0,
12225            rnlo: Reg::R0,
12226            rnhi: Reg::R1,
12227            rmlo: Reg::R2,
12228            rmhi: Reg::R3,
12229        };
12230        let code = encoder.encode(&op).unwrap();
12231        // Delegates to I64SetCond EQ: CMP lo + IT EQ + CMPEQ hi + ITE EQ + MOV 1 + MOV 0
12232        assert!(code.len() >= 10, "I64Eq should emit at least 10 bytes");
12233    }
12234
12235    #[test]
12236    fn test_encode_i64_ldr_thumb2() {
12237        let encoder = ArmEncoder::new_thumb2();
12238        let op = ArmOp::I64Ldr {
12239            rdlo: Reg::R0,
12240            rdhi: Reg::R1,
12241            addr: MemAddr::imm(Reg::SP, 0),
12242        };
12243        let code = encoder.encode(&op).unwrap();
12244        // Two LDR instructions (lo at offset, hi at offset+4)
12245        assert!(code.len() >= 4, "I64Ldr should emit at least 4 bytes");
12246    }
12247
12248    #[test]
12249    fn test_372_i64_ldr_indexed_materializes_address() {
12250        // #372: a memory i64.load carries an index register (R11 + addr + off).
12251        // The encoder must materialize `ip = base + index` (ADD.W) and load via
12252        // `[ip,#off]` — NOT drop the index. A frame (non-indexed) i64.load must
12253        // stay byte-identical (plain `[base,#off]`, no ADD).
12254        let encoder = ArmEncoder::new_thumb2();
12255        let indexed = encoder
12256            .encode(&ArmOp::I64Ldr {
12257                rdlo: Reg::R0,
12258                rdhi: Reg::R1,
12259                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
12260            })
12261            .unwrap();
12262        // ADD.W ip, fp, r0 = eb0b 0c00 (byte-verified vs arm-none-eabi-as).
12263        assert_eq!(
12264            &indexed[0..4],
12265            &[0x0b, 0xeb, 0x00, 0x0c],
12266            "indexed I64Ldr must start with ADD.W ip, base, index"
12267        );
12268        let frame = encoder
12269            .encode(&ArmOp::I64Ldr {
12270                rdlo: Reg::R0,
12271                rdhi: Reg::R1,
12272                addr: MemAddr::imm(Reg::SP, 8),
12273            })
12274            .unwrap();
12275        // No index -> no ADD.W prefix (byte-identical frame access).
12276        assert_ne!(
12277            &frame[0..2],
12278            &[0x0b, 0xeb],
12279            "frame (non-indexed) I64Ldr must NOT emit an ADD.W"
12280        );
12281    }
12282
12283    #[test]
12284    fn test_382_i64_ldst_large_offset_materializes_not_skips() {
12285        // #382: an indexed i64.load/store whose static offset > 0xFFF must
12286        // MATERIALIZE the offset into the base — NOT return Err (skip the fn).
12287        // Sequence for reg_imm(R11, R0, 5000): MOVW ip,#5000 ; ADD ip,r0,ip ;
12288        // ADD ip,ip,fp ; LDR/STR halves at [ip,#0] / [ip,#4]. Byte-verified tail
12289        // vs arm-none-eabi-as.
12290        let encoder = ArmEncoder::new_thumb2();
12291        // 0x1388 > 0xFFF (MemAddr is not Copy, so build it per use).
12292
12293        let ld = encoder
12294            .encode(&ArmOp::I64Ldr {
12295                rdlo: Reg::R0,
12296                rdhi: Reg::R1,
12297                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
12298            })
12299            .expect("large-offset i64.load must lower, not skip");
12300        // MOVW ip,#0x1388 (4) + ADD ip,r0,ip (4) + ADD ip,ip,fp (4) + 2 LDR (8).
12301        assert_eq!(ld.len(), 20, "expected MOVW + 2×ADD + 2×LDR");
12302        // Must NOT be the small-offset `ADD.W ip, fp, r0` (0x0b 0xeb) prefix —
12303        // that path can only reach imm12 offsets.
12304        assert_ne!(
12305            &ld[0..2],
12306            &[0x0b, 0xeb],
12307            "must materialize the large offset"
12308        );
12309        // Effective base built in ip, then halves at [ip,#0] / [ip,#4].
12310        assert_eq!(
12311            &ld[4..20],
12312            &[
12313                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
12314                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
12315                0xdc, 0xf8, 0x00, 0x00, // LDR.W r0, [ip, #0]
12316                0xdc, 0xf8, 0x04, 0x10, // LDR.W r1, [ip, #4]
12317            ],
12318            "large-offset i64.load must fold offset into ip and access [ip,#0]/[ip,#4]"
12319        );
12320
12321        // Store: same base materialization, STR halves.
12322        let st = encoder
12323            .encode(&ArmOp::I64Str {
12324                rdlo: Reg::R2,
12325                rdhi: Reg::R3,
12326                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
12327            })
12328            .expect("large-offset i64.store must lower, not skip");
12329        assert_eq!(st.len(), 20);
12330        assert_eq!(
12331            &st[4..20],
12332            &[
12333                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
12334                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
12335                0xcc, 0xf8, 0x00, 0x20, // STR.W r2, [ip, #0]
12336                0xcc, 0xf8, 0x04, 0x30, // STR.W r3, [ip, #4]
12337            ],
12338            "large-offset i64.store must fold offset into ip and access [ip,#0]/[ip,#4]"
12339        );
12340
12341        // Small-offset (imm12) indexed access stays byte-identical (#372): the
12342        // effective base is a single `ADD.W ip, fp, r0` and the halves keep the
12343        // folded immediates — NO extra MOVW/ADD.
12344        let small = encoder
12345            .encode(&ArmOp::I64Ldr {
12346                rdlo: Reg::R0,
12347                rdhi: Reg::R1,
12348                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 8),
12349            })
12350            .unwrap();
12351        assert_eq!(
12352            &small[0..4],
12353            &[0x0b, 0xeb, 0x00, 0x0c],
12354            "small-offset indexed i64 must keep the single ADD.W ip, fp, r0"
12355        );
12356        assert_eq!(small.len(), 12, "ADD.W + 2×LDR.W (offset folded in imm12)");
12357    }
12358
12359    /// RQ-63-ARMI64OFF (#1165): A32 `I64Ldr`/`I64Str` at and past the pair
12360    /// form's immediate boundary. Bytes verified against `arm-none-eabi-as`
12361    /// (`.arch armv7-r`, `.arm`). 4091 is the last offset the pair folds (the
12362    /// high half sits at 4095 = imm12 max); 4092 is the straddle the v0.62
12363    /// census declined on (low half fits, high half does not).
12364    #[test]
12365    fn test_1165_a32_i64_ldst_offset_boundary_materializes() {
12366        let enc = ArmEncoder::new_arm32();
12367        let le = |ws: &[u32]| ws.iter().flat_map(|w| w.to_le_bytes()).collect::<Vec<u8>>();
12368        let ld = |off: i32| {
12369            enc.encode(&ArmOp::I64Ldr {
12370                rdlo: Reg::R0,
12371                rdhi: Reg::R1,
12372                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, off),
12373            })
12374        };
12375        // AT the boundary (4091): folded, byte-identical to the pre-#1165 form.
12376        assert_eq!(
12377            ld(4091).unwrap(),
12378            le(&[0xE08BC000, 0xE59C0FFB, 0xE59C1FFF]),
12379            "4091: add ip,fp,r0 ; ldr r0,[ip,#4091] ; ldr r1,[ip,#4095]"
12380        );
12381        // ONE ABOVE (4092): the high half would need #4096 → materialize.
12382        assert_eq!(
12383            ld(4092).unwrap(),
12384            le(&[0xE300CFFC, 0xE08CC00B, 0xE08CC000, 0xE59C0000, 0xE59C1004]),
12385            "4092: movw ip,#4092 ; add ip,ip,fp ; add ip,ip,r0 ; ldr r0,[ip] ; ldr r1,[ip,#4]"
12386        );
12387        // 4095 (low half at the imm12 max), 4096 (both out), 5000.
12388        assert_eq!(ld(4095).unwrap()[..4], le(&[0xE300CFFF])[..]);
12389        assert_eq!(ld(4096).unwrap()[..4], le(&[0xE301C000])[..]);
12390        assert_eq!(
12391            ld(5000).unwrap(),
12392            le(&[0xE301C388, 0xE08CC00B, 0xE08CC000, 0xE59C0000, 0xE59C1004])
12393        );
12394        // 70000 needs MOVT; store form.
12395        assert_eq!(
12396            enc.encode(&ArmOp::I64Str {
12397                rdlo: Reg::R2,
12398                rdhi: Reg::R3,
12399                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 70000),
12400            })
12401            .unwrap(),
12402            le(&[
12403                0xE301C170, 0xE340C001, 0xE08CC00B, 0xE08CC000, 0xE58C2000, 0xE58C3004
12404            ]),
12405            "70000: movw ip,#0x1170 ; movt ip,#1 ; add ip,ip,fp ; add ip,ip,r0 ; str r2,[ip] ; str r3,[ip,#4]"
12406        );
12407        // A memarg >= 2^31 arrives negative (0xFFFFFFF8 as i32 = -8): the full
12408        // 32 bits are materialized, mod 2^32 — never clamped, never masked.
12409        assert_eq!(
12410            ld(-8).unwrap(),
12411            le(&[
12412                0xE30FCFF8, 0xE34FCFFF, 0xE08CC00B, 0xE08CC000, 0xE59C0000, 0xE59C1004
12413            ]),
12414            "0xfffffff8: movw ip,#0xfff8 ; movt ip,#0xffff ; add ; add ; ldr ; ldr"
12415        );
12416        // No index register (frame-style base), out of range: MOVW ; ADD ip,ip,base.
12417        assert_eq!(
12418            enc.encode(&ArmOp::I64Ldr {
12419                rdlo: Reg::R0,
12420                rdhi: Reg::R1,
12421                addr: MemAddr::imm(Reg::SP, 5000),
12422            })
12423            .unwrap(),
12424            le(&[0xE301C388, 0xE08CC00D, 0xE59C0000, 0xE59C1004])
12425        );
12426        // The base IS the scratch: no free register → typed error, not a clobber.
12427        assert!(
12428            enc.encode(&ArmOp::I64Ldr {
12429                rdlo: Reg::R0,
12430                rdhi: Reg::R1,
12431                addr: MemAddr::imm(Reg::R12, 5000),
12432            })
12433            .is_err(),
12434            "materializing over an R12 base must be refused, not clobber it"
12435        );
12436    }
12437
12438    /// RQ-63-ARMI64OFF (#1165): the A32 WORD and SUB-WORD immediate arms used
12439    /// to MASK an out-of-range offset (`& 0xFFF` / `& 0xFF`) — a silent wrong
12440    /// address (`i32.load offset=5000` → `ldr r0,[ip,#904]`, `i32.load16_u
12441    /// offset=256` → `ldrh r0,[ip]`). At the field maximum the bytes are
12442    /// unchanged; one above, the offset is materialized. Bytes verified
12443    /// against `arm-none-eabi-as`.
12444    #[test]
12445    fn test_1165_a32_word_subword_offset_boundary_materializes_not_masks() {
12446        let enc = ArmEncoder::new_arm32();
12447        let le = |ws: &[u32]| ws.iter().flat_map(|w| w.to_le_bytes()).collect::<Vec<u8>>();
12448        let idx = |off: i32| MemAddr::reg_imm(Reg::R11, Reg::R0, off);
12449        let ldr = |addr| enc.encode(&ArmOp::Ldr { rd: Reg::R0, addr }).unwrap();
12450        let ldrh = |addr| enc.encode(&ArmOp::Ldrh { rd: Reg::R0, addr }).unwrap();
12451        let ldrsb = |addr| enc.encode(&ArmOp::Ldrsb { rd: Reg::R0, addr }).unwrap();
12452        let strh = |addr| enc.encode(&ArmOp::Strh { rd: Reg::R1, addr }).unwrap();
12453        let ldrb = |addr| enc.encode(&ArmOp::Ldrb { rd: Reg::R0, addr }).unwrap();
12454        // LDR imm12: 4095 at, 4096 above.
12455        assert_eq!(ldr(idx(4095)), le(&[0xE08BC000, 0xE59C0FFF]));
12456        assert_eq!(
12457            ldr(idx(4096)),
12458            le(&[0xE301C000, 0xE08CC00B, 0xE08CC000, 0xE59C0000]),
12459            "ldr 4096 must materialize, not mask to #0"
12460        );
12461        // LDRH imm8: 255 at, 256 above (the pre-fix bytes were `ldrh r0,[ip]`).
12462        assert_eq!(ldrh(idx(255)), le(&[0xE08BC000, 0xE1DC0FBF]));
12463        assert_eq!(
12464            ldrh(idx(256)),
12465            le(&[0xE300C100, 0xE08CC00B, 0xE08CC000, 0xE1DC00B0]),
12466            "ldrh 256 must materialize, not mask to #0"
12467        );
12468        // LDRSB imm8 (256 above), STRH imm8 (300 above).
12469        assert_eq!(
12470            ldrsb(idx(256)),
12471            le(&[0xE300C100, 0xE08CC00B, 0xE08CC000, 0xE1DC00D0])
12472        );
12473        assert_eq!(
12474            strh(idx(300)),
12475            le(&[0xE300C12C, 0xE08CC00B, 0xE08CC000, 0xE1CC10B0])
12476        );
12477        // LDRB is an imm12 form: 4095 at, 4096 above.
12478        assert_eq!(ldrb(idx(4095)), le(&[0xE08BC000, 0xE5DC0FFF]));
12479        assert_eq!(
12480            ldrb(idx(4096)),
12481            le(&[0xE301C000, 0xE08CC00B, 0xE08CC000, 0xE5DC0000])
12482        );
12483        // No index, out of range: MOVW ; ADD ip,ip,base ; access [ip].
12484        assert_eq!(
12485            ldr(MemAddr::imm(Reg::R0, 5000)),
12486            le(&[0xE301C388, 0xE08CC000, 0xE59C0000])
12487        );
12488        // The immediate arms are now a TRIPWIRE, never a mask.
12489        assert!(encode_mem_addr(&MemAddr::imm(Reg::R1, 0xFFF)).is_ok());
12490        assert!(
12491            encode_mem_addr(&MemAddr::imm(Reg::R1, 0x1000)).is_err(),
12492            "imm12 arm must refuse 0x1000, not mask it to #0"
12493        );
12494        assert!(encode_mem_addr_imm8(&MemAddr::imm(Reg::R1, 0xFF)).is_ok());
12495        assert!(
12496            encode_mem_addr_imm8(&MemAddr::imm(Reg::R1, 0x100)).is_err(),
12497            "imm8 arm must refuse 0x100, not mask it to #0"
12498        );
12499        // Base is the scratch: refused, not clobbered.
12500        assert!(
12501            enc.encode(&ArmOp::Ldr {
12502                rd: Reg::R0,
12503                addr: MemAddr::imm(Reg::R12, 5000),
12504            })
12505            .is_err()
12506        );
12507    }
12508
12509    /// RQ-63-ARMI64OFF (#1165): Thumb-2 `i64_effective_base` used to CLAMP a
12510    /// negative offset (a memarg >= 2^31 after the selector's `as i32` cast)
12511    /// to 0 — `i64.load offset=0xfffffff8` read `[R11+addr]`. It now
12512    /// materializes the full 32 bits (MOVW+MOVT), the arithmetic the i32 word
12513    /// path uses for the same memarg. Bytes verified against
12514    /// `arm-none-eabi-as` (`.arch armv7e-m`, `.thumb`). A FRAME access with a
12515    /// negative offset (no index register) is refused loudly by
12516    /// `check_ldst_imm12`, never clamped.
12517    #[test]
12518    fn test_1165_thumb2_i64_negative_offset_materializes_not_clamps() {
12519        let enc = ArmEncoder::new_thumb2();
12520        let ld = enc
12521            .encode(&ArmOp::I64Ldr {
12522                rdlo: Reg::R0,
12523                rdhi: Reg::R1,
12524                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, -8),
12525            })
12526            .unwrap();
12527        assert_eq!(
12528            ld,
12529            vec![
12530                0x4f, 0xf6, 0xf8, 0x7c, // movw ip, #0xfff8
12531                0xcf, 0xf6, 0xff, 0x7c, // movt ip, #0xffff
12532                0x00, 0xeb, 0x0c, 0x0c, // add.w ip, r0, ip
12533                0x0c, 0xeb, 0x0b, 0x0c, // add.w ip, ip, fp
12534                0xdc, 0xf8, 0x00, 0x00, // ldr.w r0, [ip]
12535                0xdc, 0xf8, 0x04, 0x10, // ldr.w r1, [ip, #4]
12536            ],
12537            "0xfffffff8 must be materialized in full, not clamped to [ip,#0]"
12538        );
12539        let st = enc
12540            .encode(&ArmOp::I64Str {
12541                rdlo: Reg::R2,
12542                rdhi: Reg::R3,
12543                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, i32::MIN),
12544            })
12545            .unwrap();
12546        assert_eq!(
12547            st,
12548            vec![
12549                0x40, 0xf2, 0x00, 0x0c, // movw ip, #0
12550                0xc8, 0xf2, 0x00, 0x0c, // movt ip, #0x8000
12551                0x00, 0xeb, 0x0c, 0x0c, // add.w ip, r0, ip
12552                0x0c, 0xeb, 0x0b, 0x0c, // add.w ip, ip, fp
12553                0xcc, 0xf8, 0x00, 0x20, // str.w r2, [ip]
12554                0xcc, 0xf8, 0x04, 0x30, // str.w r3, [ip, #4]
12555            ]
12556        );
12557        assert!(
12558            enc.encode(&ArmOp::I64Ldr {
12559                rdlo: Reg::R0,
12560                rdhi: Reg::R1,
12561                addr: MemAddr::imm(Reg::SP, -8),
12562            })
12563            .is_err(),
12564            "a negative frame offset must be refused, not clamped to [sp,#0]"
12565        );
12566    }
12567
12568    #[test]
12569    fn test_encode_i64_str_thumb2() {
12570        let encoder = ArmEncoder::new_thumb2();
12571        let op = ArmOp::I64Str {
12572            rdlo: Reg::R0,
12573            rdhi: Reg::R1,
12574            addr: MemAddr::imm(Reg::SP, 0),
12575        };
12576        let code = encoder.encode(&op).unwrap();
12577        // Two STR instructions (lo at offset, hi at offset+4)
12578        assert!(code.len() >= 4, "I64Str should emit at least 4 bytes");
12579    }
12580
12581    #[test]
12582    fn test_encode_i64_all_comparisons_thumb2() {
12583        let encoder = ArmEncoder::new_thumb2();
12584
12585        let ops = vec![
12586            ArmOp::I64Ne {
12587                rd: Reg::R0,
12588                rnlo: Reg::R0,
12589                rnhi: Reg::R1,
12590                rmlo: Reg::R2,
12591                rmhi: Reg::R3,
12592            },
12593            ArmOp::I64LtS {
12594                rd: Reg::R0,
12595                rnlo: Reg::R0,
12596                rnhi: Reg::R1,
12597                rmlo: Reg::R2,
12598                rmhi: Reg::R3,
12599            },
12600            ArmOp::I64LtU {
12601                rd: Reg::R0,
12602                rnlo: Reg::R0,
12603                rnhi: Reg::R1,
12604                rmlo: Reg::R2,
12605                rmhi: Reg::R3,
12606            },
12607            ArmOp::I64LeS {
12608                rd: Reg::R0,
12609                rnlo: Reg::R0,
12610                rnhi: Reg::R1,
12611                rmlo: Reg::R2,
12612                rmhi: Reg::R3,
12613            },
12614            ArmOp::I64LeU {
12615                rd: Reg::R0,
12616                rnlo: Reg::R0,
12617                rnhi: Reg::R1,
12618                rmlo: Reg::R2,
12619                rmhi: Reg::R3,
12620            },
12621            ArmOp::I64GtS {
12622                rd: Reg::R0,
12623                rnlo: Reg::R0,
12624                rnhi: Reg::R1,
12625                rmlo: Reg::R2,
12626                rmhi: Reg::R3,
12627            },
12628            ArmOp::I64GtU {
12629                rd: Reg::R0,
12630                rnlo: Reg::R0,
12631                rnhi: Reg::R1,
12632                rmlo: Reg::R2,
12633                rmhi: Reg::R3,
12634            },
12635            ArmOp::I64GeS {
12636                rd: Reg::R0,
12637                rnlo: Reg::R0,
12638                rnhi: Reg::R1,
12639                rmlo: Reg::R2,
12640                rmhi: Reg::R3,
12641            },
12642            ArmOp::I64GeU {
12643                rd: Reg::R0,
12644                rnlo: Reg::R0,
12645                rnhi: Reg::R1,
12646                rmlo: Reg::R2,
12647                rmhi: Reg::R3,
12648            },
12649        ];
12650
12651        for op in &ops {
12652            let code = encoder.encode(op).unwrap();
12653            assert!(
12654                code.len() >= 8,
12655                "i64 comparison {:?} should emit at least 8 bytes, got {}",
12656                op,
12657                code.len()
12658            );
12659        }
12660    }
12661
12662    #[test]
12663    fn test_encode_i64_const_zero_thumb2() {
12664        let encoder = ArmEncoder::new_thumb2();
12665        let op = ArmOp::I64Const {
12666            rdlo: Reg::R0,
12667            rdhi: Reg::R1,
12668            value: 0,
12669        };
12670        let code = encoder.encode(&op).unwrap();
12671        // MOVW R0, #0 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes
12672        assert_eq!(code.len(), 8, "I64Const(0) should be 8 bytes");
12673    }
12674
12675    #[test]
12676    fn test_encode_i64_const_negative_one_thumb2() {
12677        let encoder = ArmEncoder::new_thumb2();
12678        let op = ArmOp::I64Const {
12679            rdlo: Reg::R0,
12680            rdhi: Reg::R1,
12681            value: -1, // 0xFFFF_FFFF_FFFF_FFFF
12682        };
12683        let code = encoder.encode(&op).unwrap();
12684        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
12685        assert_eq!(code.len(), 16, "I64Const(-1) should be 16 bytes");
12686    }
12687
12688    // =========================================================================
12689    // Sub-word load/store encoding tests
12690    // =========================================================================
12691
12692    #[test]
12693    fn test_encode_ldrb_arm32() {
12694        let encoder = ArmEncoder::new_arm32();
12695        let op = ArmOp::Ldrb {
12696            rd: Reg::R0,
12697            addr: MemAddr::imm(Reg::R1, 4),
12698        };
12699        let code = encoder.encode(&op).unwrap();
12700        assert_eq!(code.len(), 4, "ARM32 LDRB should be 4 bytes");
12701        // LDRB R0, [R1, #4] = 0xE5D10004
12702        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12703        assert_eq!(encoded, 0xE5D10004, "Should encode LDRB R0, [R1, #4]");
12704    }
12705
12706    #[test]
12707    fn test_encode_strb_arm32() {
12708        let encoder = ArmEncoder::new_arm32();
12709        let op = ArmOp::Strb {
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 STRB should be 4 bytes");
12715        // STRB R0, [R1, #0] = 0xE5C10000
12716        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12717        assert_eq!(encoded, 0xE5C10000, "Should encode STRB R0, [R1, #0]");
12718    }
12719
12720    #[test]
12721    fn test_encode_ldrh_arm32() {
12722        let encoder = ArmEncoder::new_arm32();
12723        let op = ArmOp::Ldrh {
12724            rd: Reg::R0,
12725            addr: MemAddr::imm(Reg::R1, 2),
12726        };
12727        let code = encoder.encode(&op).unwrap();
12728        assert_eq!(code.len(), 4, "ARM32 LDRH should be 4 bytes");
12729    }
12730
12731    #[test]
12732    fn test_encode_strh_arm32() {
12733        let encoder = ArmEncoder::new_arm32();
12734        let op = ArmOp::Strh {
12735            rd: Reg::R0,
12736            addr: MemAddr::imm(Reg::R1, 0),
12737        };
12738        let code = encoder.encode(&op).unwrap();
12739        assert_eq!(code.len(), 4, "ARM32 STRH should be 4 bytes");
12740    }
12741
12742    #[test]
12743    fn test_encode_ldrsb_arm32() {
12744        let encoder = ArmEncoder::new_arm32();
12745        let op = ArmOp::Ldrsb {
12746            rd: Reg::R0,
12747            addr: MemAddr::imm(Reg::R1, 0),
12748        };
12749        let code = encoder.encode(&op).unwrap();
12750        assert_eq!(code.len(), 4, "ARM32 LDRSB should be 4 bytes");
12751    }
12752
12753    #[test]
12754    fn test_encode_ldrsh_arm32() {
12755        let encoder = ArmEncoder::new_arm32();
12756        let op = ArmOp::Ldrsh {
12757            rd: Reg::R0,
12758            addr: MemAddr::imm(Reg::R1, 0),
12759        };
12760        let code = encoder.encode(&op).unwrap();
12761        assert_eq!(code.len(), 4, "ARM32 LDRSH should be 4 bytes");
12762    }
12763
12764    #[test]
12765    fn test_encode_ldrb_thumb2_16bit() {
12766        let encoder = ArmEncoder::new_thumb2();
12767        let op = ArmOp::Ldrb {
12768            rd: Reg::R0,
12769            addr: MemAddr::imm(Reg::R1, 4),
12770        };
12771        let code = encoder.encode(&op).unwrap();
12772        // Low registers + small offset -> 16-bit encoding
12773        assert_eq!(
12774            code.len(),
12775            2,
12776            "Thumb-2 LDRB with small offset should be 16-bit"
12777        );
12778    }
12779
12780    #[test]
12781    fn test_encode_ldrb_thumb2_32bit() {
12782        let encoder = ArmEncoder::new_thumb2();
12783        let op = ArmOp::Ldrb {
12784            rd: Reg::R0,
12785            addr: MemAddr::imm(Reg::R1, 100), // offset > 31 needs 32-bit
12786        };
12787        let code = encoder.encode(&op).unwrap();
12788        assert_eq!(
12789            code.len(),
12790            4,
12791            "Thumb-2 LDRB with large offset should be 32-bit"
12792        );
12793    }
12794
12795    #[test]
12796    fn test_encode_strb_thumb2_16bit() {
12797        let encoder = ArmEncoder::new_thumb2();
12798        let op = ArmOp::Strb {
12799            rd: Reg::R0,
12800            addr: MemAddr::imm(Reg::R1, 10),
12801        };
12802        let code = encoder.encode(&op).unwrap();
12803        assert_eq!(
12804            code.len(),
12805            2,
12806            "Thumb-2 STRB with small offset should be 16-bit"
12807        );
12808    }
12809
12810    #[test]
12811    fn test_encode_ldrh_thumb2_16bit() {
12812        let encoder = ArmEncoder::new_thumb2();
12813        let op = ArmOp::Ldrh {
12814            rd: Reg::R0,
12815            addr: MemAddr::imm(Reg::R1, 4), // offset aligned to 2, <= 62
12816        };
12817        let code = encoder.encode(&op).unwrap();
12818        assert_eq!(
12819            code.len(),
12820            2,
12821            "Thumb-2 LDRH with small aligned offset should be 16-bit"
12822        );
12823    }
12824
12825    #[test]
12826    fn test_encode_strh_thumb2_16bit() {
12827        let encoder = ArmEncoder::new_thumb2();
12828        let op = ArmOp::Strh {
12829            rd: Reg::R0,
12830            addr: MemAddr::imm(Reg::R1, 4),
12831        };
12832        let code = encoder.encode(&op).unwrap();
12833        assert_eq!(
12834            code.len(),
12835            2,
12836            "Thumb-2 STRH with small aligned offset should be 16-bit"
12837        );
12838    }
12839
12840    #[test]
12841    fn test_encode_ldrsb_thumb2() {
12842        let encoder = ArmEncoder::new_thumb2();
12843        let op = ArmOp::Ldrsb {
12844            rd: Reg::R0,
12845            addr: MemAddr::imm(Reg::R1, 0),
12846        };
12847        let code = encoder.encode(&op).unwrap();
12848        // LDRSB has no 16-bit immediate form, always 32-bit
12849        assert_eq!(code.len(), 4, "Thumb-2 LDRSB should be 32-bit");
12850    }
12851
12852    #[test]
12853    fn test_encode_ldrsh_thumb2() {
12854        let encoder = ArmEncoder::new_thumb2();
12855        let op = ArmOp::Ldrsh {
12856            rd: Reg::R0,
12857            addr: MemAddr::imm(Reg::R1, 0),
12858        };
12859        let code = encoder.encode(&op).unwrap();
12860        assert_eq!(code.len(), 4, "Thumb-2 LDRSH should be 32-bit");
12861    }
12862
12863    #[test]
12864    fn test_encode_memory_size_thumb2() {
12865        let encoder = ArmEncoder::new_thumb2();
12866        let op = ArmOp::MemorySize { rd: Reg::R0 };
12867        let code = encoder.encode(&op).unwrap();
12868        // R0 and R10 are not both low registers, so this needs careful handling
12869        assert!(!code.is_empty(), "MemorySize should produce code");
12870    }
12871
12872    #[test]
12873    fn test_encode_memory_grow_thumb2() {
12874        let encoder = ArmEncoder::new_thumb2();
12875        let op = ArmOp::MemoryGrow {
12876            rd: Reg::R0,
12877            rn: Reg::R0,
12878        };
12879        let code = encoder.encode(&op).unwrap();
12880        assert_eq!(code.len(), 4, "MemoryGrow (MVN) should be 32-bit Thumb-2");
12881    }
12882
12883    #[test]
12884    fn test_encode_subword_reg_offset_thumb2() {
12885        let encoder = ArmEncoder::new_thumb2();
12886
12887        // LDRB with register offset
12888        let op = ArmOp::Ldrb {
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 LDRB with reg offset should be 32-bit"
12897        );
12898
12899        // STRB with register offset
12900        let op = ArmOp::Strb {
12901            rd: Reg::R0,
12902            addr: MemAddr::reg(Reg::R1, Reg::R2),
12903        };
12904        let code = encoder.encode(&op).unwrap();
12905        assert_eq!(
12906            code.len(),
12907            4,
12908            "Thumb-2 STRB with reg offset should be 32-bit"
12909        );
12910
12911        // LDRH with register offset
12912        let op = ArmOp::Ldrh {
12913            rd: Reg::R0,
12914            addr: MemAddr::reg(Reg::R1, Reg::R2),
12915        };
12916        let code = encoder.encode(&op).unwrap();
12917        assert_eq!(
12918            code.len(),
12919            4,
12920            "Thumb-2 LDRH with reg offset should be 32-bit"
12921        );
12922
12923        // STRH with register offset
12924        let op = ArmOp::Strh {
12925            rd: Reg::R0,
12926            addr: MemAddr::reg(Reg::R1, Reg::R2),
12927        };
12928        let code = encoder.encode(&op).unwrap();
12929        assert_eq!(
12930            code.len(),
12931            4,
12932            "Thumb-2 STRH with reg offset should be 32-bit"
12933        );
12934    }
12935
12936    #[test]
12937    fn test_encode_subword_reg_imm_offset_thumb2() {
12938        let encoder = ArmEncoder::new_thumb2();
12939
12940        // LDRB with both register and immediate offset
12941        let op = ArmOp::Ldrb {
12942            rd: Reg::R0,
12943            addr: MemAddr::reg_imm(Reg::R1, Reg::R2, 4),
12944        };
12945        let code = encoder.encode(&op).unwrap();
12946        // ADD R12, R2, #4 (4 bytes) + LDRB R0, [R1, R12] (4 bytes) = 8 bytes
12947        assert_eq!(
12948            code.len(),
12949            8,
12950            "Thumb-2 LDRB with reg+imm offset should be 8 bytes"
12951        );
12952    }
12953
12954    // ========================================================================
12955    // Helium MVE encoding tests
12956    // ========================================================================
12957
12958    #[test]
12959    fn test_encode_mve_addi32_thumb2() {
12960        let encoder = ArmEncoder::new_thumb2();
12961        let op = ArmOp::MveAddI {
12962            qd: QReg::Q0,
12963            qn: QReg::Q1,
12964            qm: QReg::Q2,
12965            size: MveSize::S32,
12966        };
12967        let code = encoder.encode(&op).unwrap();
12968        assert_eq!(
12969            code.len(),
12970            4,
12971            "MVE VADD.I32 should be 4 bytes (Thumb-2 32-bit)"
12972        );
12973    }
12974
12975    #[test]
12976    fn test_encode_mve_subi16_thumb2() {
12977        let encoder = ArmEncoder::new_thumb2();
12978        let op = ArmOp::MveSubI {
12979            qd: QReg::Q0,
12980            qn: QReg::Q1,
12981            qm: QReg::Q2,
12982            size: MveSize::S16,
12983        };
12984        let code = encoder.encode(&op).unwrap();
12985        assert_eq!(code.len(), 4, "MVE VSUB.I16 should be 4 bytes");
12986    }
12987
12988    #[test]
12989    fn test_encode_mve_muli8_thumb2() {
12990        let encoder = ArmEncoder::new_thumb2();
12991        let op = ArmOp::MveMulI {
12992            qd: QReg::Q0,
12993            qn: QReg::Q1,
12994            qm: QReg::Q2,
12995            size: MveSize::S8,
12996        };
12997        let code = encoder.encode(&op).unwrap();
12998        assert_eq!(code.len(), 4, "MVE VMUL.I8 should be 4 bytes");
12999    }
13000
13001    #[test]
13002    fn test_encode_mve_bitwise_thumb2() {
13003        let encoder = ArmEncoder::new_thumb2();
13004
13005        let ops = vec![
13006            ArmOp::MveAnd {
13007                qd: QReg::Q0,
13008                qn: QReg::Q1,
13009                qm: QReg::Q2,
13010            },
13011            ArmOp::MveOrr {
13012                qd: QReg::Q0,
13013                qn: QReg::Q1,
13014                qm: QReg::Q2,
13015            },
13016            ArmOp::MveEor {
13017                qd: QReg::Q0,
13018                qn: QReg::Q1,
13019                qm: QReg::Q2,
13020            },
13021            ArmOp::MveBic {
13022                qd: QReg::Q0,
13023                qn: QReg::Q1,
13024                qm: QReg::Q2,
13025            },
13026        ];
13027        for op in ops {
13028            let code = encoder.encode(&op).unwrap();
13029            assert_eq!(code.len(), 4, "MVE bitwise op should be 4 bytes");
13030        }
13031    }
13032
13033    #[test]
13034    fn test_encode_mve_mvn_thumb2() {
13035        let encoder = ArmEncoder::new_thumb2();
13036        let op = ArmOp::MveMvn {
13037            qd: QReg::Q0,
13038            qm: QReg::Q1,
13039        };
13040        let code = encoder.encode(&op).unwrap();
13041        assert_eq!(code.len(), 4, "MVE VMVN should be 4 bytes");
13042    }
13043
13044    #[test]
13045    fn test_encode_mve_load_store_thumb2() {
13046        let encoder = ArmEncoder::new_thumb2();
13047
13048        let load = ArmOp::MveLoad {
13049            qd: QReg::Q0,
13050            addr: MemAddr::imm(Reg::R0, 16),
13051        };
13052        let code = encoder.encode(&load).unwrap();
13053        assert_eq!(code.len(), 4, "MVE VLDRW.32 should be 4 bytes");
13054
13055        let store = ArmOp::MveStore {
13056            qd: QReg::Q1,
13057            addr: MemAddr::imm(Reg::R1, 0),
13058        };
13059        let code = encoder.encode(&store).unwrap();
13060        assert_eq!(code.len(), 4, "MVE VSTRW.32 should be 4 bytes");
13061    }
13062
13063    #[test]
13064    fn test_encode_mve_const_thumb2() {
13065        let encoder = ArmEncoder::new_thumb2();
13066        let op = ArmOp::MveConst {
13067            qd: QReg::Q0,
13068            bytes: [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0],
13069        };
13070        let code = encoder.encode(&op).unwrap();
13071        // Should be 4 words of (MOVW R12 + VMOV Sn) = 4 * (4+4) = 32 bytes min
13072        // Some words with hi16=0 skip MOVT, so length varies
13073        assert!(
13074            code.len() >= 24,
13075            "MVE const should produce multiple instructions"
13076        );
13077    }
13078
13079    #[test]
13080    fn test_encode_mve_dup_thumb2() {
13081        let encoder = ArmEncoder::new_thumb2();
13082        let op = ArmOp::MveDup {
13083            qd: QReg::Q0,
13084            rn: Reg::R0,
13085            size: MveSize::S32,
13086        };
13087        let code = encoder.encode(&op).unwrap();
13088        assert_eq!(code.len(), 4, "MVE VDUP.32 should be 4 bytes");
13089    }
13090
13091    #[test]
13092    fn test_encode_mve_extract_lane_thumb2() {
13093        let encoder = ArmEncoder::new_thumb2();
13094        let op = ArmOp::MveExtractLane {
13095            rd: Reg::R0,
13096            qn: QReg::Q1,
13097            lane: 2,
13098            size: MveSize::S32,
13099        };
13100        let code = encoder.encode(&op).unwrap();
13101        assert_eq!(code.len(), 4, "MVE extract lane should be 4 bytes");
13102    }
13103
13104    #[test]
13105    fn test_encode_mve_insert_lane_thumb2() {
13106        let encoder = ArmEncoder::new_thumb2();
13107        let op = ArmOp::MveInsertLane {
13108            qd: QReg::Q0,
13109            rn: Reg::R1,
13110            lane: 3,
13111            size: MveSize::S32,
13112        };
13113        let code = encoder.encode(&op).unwrap();
13114        assert_eq!(code.len(), 4, "MVE insert lane should be 4 bytes");
13115    }
13116
13117    #[test]
13118    fn test_encode_mve_addf32_thumb2() {
13119        let encoder = ArmEncoder::new_thumb2();
13120        let op = ArmOp::MveAddF32 {
13121            qd: QReg::Q0,
13122            qn: QReg::Q1,
13123            qm: QReg::Q2,
13124        };
13125        let code = encoder.encode(&op).unwrap();
13126        assert_eq!(code.len(), 4, "MVE VADD.F32 should be 4 bytes");
13127    }
13128
13129    #[test]
13130    fn test_encode_mve_divf32_thumb2() {
13131        let encoder = ArmEncoder::new_thumb2();
13132        let op = ArmOp::MveDivF32 {
13133            qd: QReg::Q0,
13134            qn: QReg::Q1,
13135            qm: QReg::Q2,
13136        };
13137        let code = encoder.encode(&op).unwrap();
13138        // Lane-wise: 4 x VDIV.F32 = 4 x 4 = 16 bytes
13139        assert_eq!(
13140            code.len(),
13141            16,
13142            "MVE VDIV.F32 (lane-wise) should be 16 bytes"
13143        );
13144    }
13145
13146    #[test]
13147    fn test_encode_mve_sqrtf32_thumb2() {
13148        let encoder = ArmEncoder::new_thumb2();
13149        let op = ArmOp::MveSqrtF32 {
13150            qd: QReg::Q0,
13151            qm: QReg::Q1,
13152        };
13153        let code = encoder.encode(&op).unwrap();
13154        // Lane-wise: 4 x VSQRT.F32 = 4 x 4 = 16 bytes
13155        assert_eq!(
13156            code.len(),
13157            16,
13158            "MVE VSQRT.F32 (lane-wise) should be 16 bytes"
13159        );
13160    }
13161
13162    #[test]
13163    fn test_encode_mve_negf32_thumb2() {
13164        let encoder = ArmEncoder::new_thumb2();
13165        let op = ArmOp::MveNegF32 {
13166            qd: QReg::Q0,
13167            qm: QReg::Q1,
13168        };
13169        let code = encoder.encode(&op).unwrap();
13170        assert_eq!(code.len(), 4, "MVE VNEG.F32 should be 4 bytes");
13171    }
13172
13173    #[test]
13174    fn test_encode_mve_absf32_thumb2() {
13175        let encoder = ArmEncoder::new_thumb2();
13176        let op = ArmOp::MveAbsF32 {
13177            qd: QReg::Q0,
13178            qm: QReg::Q1,
13179        };
13180        let code = encoder.encode(&op).unwrap();
13181        assert_eq!(code.len(), 4, "MVE VABS.F32 should be 4 bytes");
13182    }
13183
13184    /// VCR-RA-001 / immediate-folding precondition: pins the Thumb-2 `AND`
13185    /// immediate encoding for the byte range and documents its bound.
13186    ///
13187    /// The `And { Operand2::Imm }` encoder packs the low 12 bits straight into
13188    /// the `i:imm3:imm8` field WITHOUT applying ThumbExpandImm (the modified-
13189    /// immediate expansion). For `imm <= 0xFF` (e.g. gale's int8 clamps
13190    /// `#0x7e` / `#0x7f`) that is correct — `i:imm3 = 0000` means "imm8
13191    /// zero-extended". So `and r2, r0, #0x7e` encodes to the canonical
13192    /// `00 f0 7e 02`. For `imm >= 0x100` the field would need a true
13193    /// ThumbExpandImm pattern (rotation / replication), which is NOT
13194    /// implemented here — so **immediate folding must gate on `imm <= 0xFF`**
13195    /// until the encoder is hardened to ThumbExpandImm/Ok-or-Err (the
13196    /// "encoder must be Ok-or-Err, never silently wrong" principle, #180/#185).
13197    /// This bound covers the measured `flat_flight` waste (#209).
13198    #[test]
13199    fn and_immediate_encodes_correctly_in_byte_range_documents_fold_bound() {
13200        let encoder = ArmEncoder::new_thumb2();
13201        let op = ArmOp::And {
13202            rd: Reg::R2,
13203            rn: Reg::R0,
13204            op2: Operand2::Imm(0x7e),
13205        };
13206        let code = encoder.encode(&op).unwrap();
13207        assert_eq!(
13208            code,
13209            vec![0x00, 0xf0, 0x7e, 0x02],
13210            "and r2, r0, #0x7e must encode to the canonical AND.W T1 (imm8=0x7e)"
13211        );
13212    }
13213
13214    /// #255: the shared ThumbExpandImm reverse-encoder underpinning the
13215    /// data-processing immediate fix. Encodable modified immediates round-trip to
13216    /// the expected `i:imm3:imm8` field; a genuinely non-modified value is `None`
13217    /// (caller must materialize into a register). Note `1000 = 0xFA ror 30` *is*
13218    /// representable (field 0xF7A) — the old encoder mis-encoded it (raw 0x3E8);
13219    /// this encodes it correctly.
13220    #[test]
13221    fn try_thumb_expand_imm_encodes_modified_immediates() {
13222        assert_eq!(try_thumb_expand_imm(0x7e), Some(0x07e)); // zero-extended byte
13223        assert_eq!(try_thumb_expand_imm(0xff), Some(0x0ff));
13224        assert_eq!(try_thumb_expand_imm(0x0001_0001), Some(0x101)); // 0x00XY00XY
13225        assert_eq!(try_thumb_expand_imm(0xff00_ff00), Some(0x2ff)); // 0xXY00XY00
13226        assert_eq!(try_thumb_expand_imm(0xffff_ffff), Some(0x3ff)); // 0xXYXYXYXY
13227        assert_eq!(try_thumb_expand_imm(0x100), Some(0xf80)); // 0x80 ror 31
13228        assert_eq!(try_thumb_expand_imm(0x8000_0000), Some(0x400)); // 0x80 ror 8
13229        assert_eq!(try_thumb_expand_imm(1000), Some(0xf7a)); // 0xFA ror 30
13230        // Genuinely unrepresentable (bits too far apart for an 8-bit window).
13231        assert_eq!(try_thumb_expand_imm(0x101), None);
13232        assert_eq!(try_thumb_expand_imm(0x12345), None);
13233    }
13234
13235    /// #255: CMP/ADDS/SUBS encode any valid modified immediate correctly, and
13236    /// ERROR (not silently mis-encode) on a genuinely unrepresentable one,
13237    /// forcing the selector to materialize into a register — closing the
13238    /// silent-miscompile class of #251/#253.
13239    #[test]
13240    fn cmp_adds_subs_immediate_error_on_non_modified_imm() {
13241        let encoder = ArmEncoder::new_thumb2();
13242        // cmp r0, #0xff → valid → Ok; cmp r0, #1000 → valid (0xFA ror 30) → Ok.
13243        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 0xff).is_ok());
13244        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 1000).is_ok());
13245        // cmp r0, #0x101 → NOT a modified immediate → Err (materialize-reg).
13246        assert!(
13247            encoder.encode_thumb32_cmp_imm(&Reg::R0, 0x101).is_err(),
13248            "cmp #0x101 must error, not compare the wrong constant"
13249        );
13250        assert!(
13251            encoder
13252                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x101)
13253                .is_err()
13254        );
13255        assert!(
13256            encoder
13257                .encode_thumb32_subs(&Reg::R0, &Reg::R0, 0x101)
13258                .is_err()
13259        );
13260        // ...but a valid modified immediate still encodes.
13261        assert!(
13262            encoder
13263                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x80)
13264                .is_ok()
13265        );
13266    }
13267
13268    /// #257: MLA (multiply-accumulate) encodes as MLS without the bit-4 op flag.
13269    /// `mla r2, r3, r4, r8` (rd=r2, rn=r3, rm=r4, ra=r8) → Thumb-2 `03 fb 04 82`.
13270    #[test]
13271    fn mla_thumb2_encodes_correctly() {
13272        let encoder = ArmEncoder::new_thumb2();
13273        let code = encoder
13274            .encode(&ArmOp::Mla {
13275                rd: Reg::R2,
13276                rn: Reg::R3,
13277                rm: Reg::R4,
13278                ra: Reg::R8,
13279            })
13280            .unwrap();
13281        // hw1 = 0xFB03, hw2 = (8<<12)|(2<<8)|4 = 0x8204
13282        assert_eq!(code, vec![0x03, 0xfb, 0x04, 0x82]);
13283    }
13284
13285    /// #259: LDR/STR (and sub-word) immediate-offset encoders truncated
13286    /// `offset & 0xFFF`, silently targeting the wrong address for offset >= 4096.
13287    /// They now error (the selector must use register-offset addressing) — the
13288    /// load/store sibling of the #253/#255 class. Offsets <= 4095 still encode.
13289    #[test]
13290    fn ldst_imm12_offset_errors_when_out_of_range() {
13291        let encoder = ArmEncoder::new_thumb2();
13292        // offset 0xFFF (4095): valid → Ok; ldr r0, [r1, #4095].
13293        assert!(
13294            encoder
13295                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0xFFF)
13296                .is_ok()
13297        );
13298        // offset 0x1000 (4096): out of imm12 range → Err (not & 0xFFF → #0).
13299        assert!(
13300            encoder
13301                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0x1000)
13302                .is_err(),
13303            "ldr offset 4096 must error, not wrap to 0"
13304        );
13305        assert!(
13306            encoder
13307                .encode_thumb32_str(&Reg::R0, &Reg::R1, 0x1000)
13308                .is_err()
13309        );
13310        assert!(
13311            encoder
13312                .encode_thumb32_ldrb_imm(&Reg::R0, &Reg::R1, 5000)
13313                .is_err()
13314        );
13315        assert!(
13316            encoder
13317                .encode_thumb32_strh_imm(&Reg::R0, &Reg::R1, 5000)
13318                .is_err()
13319        );
13320    }
13321
13322    /// Latent miscompile fix: ADD/SUB with a >0xFF immediate (e.g.
13323    /// `add sp, sp, #frame` for a >=256-byte frame) used ADD.W (T3), whose
13324    /// `i:imm3:imm8` is a ThumbExpandImm modified immediate — so `#256` silently
13325    /// encoded as `#0` (stack corruption). Use ADDW/SUBW (T4), a PLAIN 12-bit
13326    /// immediate, for 0x100..=0xFFF; keep T3 for <=0xFF (bit-identical); error
13327    /// beyond 4095.
13328    #[test]
13329    fn add_sub_large_immediate_use_addw_subw_not_misencoded() {
13330        let encoder = ArmEncoder::new_thumb2();
13331        // add sp, sp, #256  →  ADDW (T4) SP, SP, #256  =  0d f2 00 1d
13332        assert_eq!(
13333            encoder
13334                .encode(&ArmOp::Add {
13335                    rd: Reg::SP,
13336                    rn: Reg::SP,
13337                    op2: Operand2::Imm(256),
13338                })
13339                .unwrap(),
13340            vec![0x0d, 0xf2, 0x00, 0x1d],
13341            "add sp,sp,#256 must be ADDW (plain imm12), not a mis-encoded ADD.W"
13342        );
13343        // sub sp, sp, #256  →  SUBW (T4) SP, SP, #256  =  ad f2 00 1d
13344        assert_eq!(
13345            encoder
13346                .encode(&ArmOp::Sub {
13347                    rd: Reg::SP,
13348                    rn: Reg::SP,
13349                    op2: Operand2::Imm(256),
13350                })
13351                .unwrap(),
13352            vec![0xad, 0xf2, 0x00, 0x1d],
13353        );
13354        // > 4095 has no single-instruction encoding → error, not silent wrong.
13355        assert!(
13356            encoder
13357                .encode(&ArmOp::Add {
13358                    rd: Reg::SP,
13359                    rn: Reg::SP,
13360                    op2: Operand2::Imm(5000),
13361                })
13362                .is_err(),
13363            "add #5000 must error (no single ADDW), not mis-encode"
13364        );
13365    }
13366
13367    /// Closes the data-proc immediate class: AND and CMN now go through
13368    /// `try_thumb_expand_imm` like ORR/EOR/CMP — correct for any modified
13369    /// immediate, `Err` (not raw-pack / NOP) on an un-encodable one. The byte
13370    /// range stays bit-identical (`and r2,r0,#0x7e` is unchanged).
13371    #[test]
13372    fn and_cmn_immediate_thumb_expand_else_error() {
13373        let encoder = ArmEncoder::new_thumb2();
13374        // byte range unchanged (bit-identical with the pre-retrofit encoding)
13375        assert_eq!(
13376            encoder
13377                .encode(&ArmOp::And {
13378                    rd: Reg::R2,
13379                    rn: Reg::R0,
13380                    op2: Operand2::Imm(0x7e),
13381                })
13382                .unwrap(),
13383            vec![0x00, 0xf0, 0x7e, 0x02],
13384        );
13385        // a valid replicated modified immediate now encodes (was silently wrong)
13386        assert!(
13387            encoder
13388                .encode(&ArmOp::And {
13389                    rd: Reg::R2,
13390                    rn: Reg::R0,
13391                    op2: Operand2::Imm(0xff00ff00u32 as i32),
13392                })
13393                .is_ok()
13394        );
13395        // a genuinely un-encodable immediate errors (AND was raw-pack; CMN NOP)
13396        assert!(
13397            encoder
13398                .encode(&ArmOp::And {
13399                    rd: Reg::R2,
13400                    rn: Reg::R0,
13401                    op2: Operand2::Imm(0x101),
13402                })
13403                .is_err()
13404        );
13405        assert!(
13406            encoder
13407                .encode(&ArmOp::Cmn {
13408                    rn: Reg::R0,
13409                    op2: Operand2::Imm(0x101),
13410                })
13411                .is_err(),
13412            "CMN #0x101 must error, not emit a NOP"
13413        );
13414    }
13415
13416    /// VCR-RA-001: ORR/EOR with a small immediate must encode the real
13417    /// instruction (not a silent `0xBF00` NOP). Pins the byte range and the
13418    /// Ok-or-Err bound that makes future Or/Eor immediate folding safe.
13419    #[test]
13420    fn orr_eor_immediate_encode_in_byte_range_else_error() {
13421        let encoder = ArmEncoder::new_thumb2();
13422        // orr r2, r0, #0x7e  →  ORR.W T1, imm8=0x7e
13423        assert_eq!(
13424            encoder
13425                .encode(&ArmOp::Orr {
13426                    rd: Reg::R2,
13427                    rn: Reg::R0,
13428                    op2: Operand2::Imm(0x7e),
13429                })
13430                .unwrap(),
13431            vec![0x40, 0xf0, 0x7e, 0x02],
13432        );
13433        // eor r2, r0, #0x7e  →  EOR.W T1, imm8=0x7e
13434        assert_eq!(
13435            encoder
13436                .encode(&ArmOp::Eor {
13437                    rd: Reg::R2,
13438                    rn: Reg::R0,
13439                    op2: Operand2::Imm(0x7e),
13440                })
13441                .unwrap(),
13442            vec![0x80, 0xf0, 0x7e, 0x02],
13443        );
13444        // Out-of-range immediates error rather than silently mis-encode / NOP.
13445        assert!(
13446            encoder
13447                .encode(&ArmOp::Orr {
13448                    rd: Reg::R2,
13449                    rn: Reg::R0,
13450                    op2: Operand2::Imm(0x140),
13451                })
13452                .is_err(),
13453            "ORR #0x140 must error, not emit a NOP"
13454        );
13455    }
13456
13457    #[test]
13458    fn test_encode_mve_different_qregs() {
13459        let encoder = ArmEncoder::new_thumb2();
13460
13461        // Test that different Q-register numbers produce different encodings
13462        let op1 = ArmOp::MveAddI {
13463            qd: QReg::Q0,
13464            qn: QReg::Q0,
13465            qm: QReg::Q0,
13466            size: MveSize::S32,
13467        };
13468        let op2 = ArmOp::MveAddI {
13469            qd: QReg::Q3,
13470            qn: QReg::Q5,
13471            qm: QReg::Q7,
13472            size: MveSize::S32,
13473        };
13474        let code1 = encoder.encode(&op1).unwrap();
13475        let code2 = encoder.encode(&op2).unwrap();
13476        assert_ne!(
13477            code1, code2,
13478            "Different Q-registers should produce different encodings"
13479        );
13480    }
13481
13482    #[test]
13483    fn test_encode_mve_arm32_loud_err() {
13484        // #615: MVE (Helium) is Thumb-2-only. The ARM32 encoder used to emit
13485        // a silent NOP here (dropping the vector op); it must now be a typed
13486        // Err so a broken "MVE implies Thumb" invariant fails loudly.
13487        let encoder = ArmEncoder::new_arm32();
13488        let op = ArmOp::MveAddI {
13489            qd: QReg::Q0,
13490            qn: QReg::Q1,
13491            qm: QReg::Q2,
13492            size: MveSize::S32,
13493        };
13494        let err = encoder
13495            .encode(&op)
13496            .expect_err("ARM32 MVE must be a loud Err, not a silent NOP (#615)");
13497        assert!(
13498            err.to_string().contains("Thumb-2 only"),
13499            "unexpected error message: {err}"
13500        );
13501    }
13502}