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, page_log2 } => {
1757                let rd_bits = reg_to_bits(rd);
1758                let r10_bits = reg_to_bits(&Reg::R10);
1759                // R10 holds the size in BYTES; `memory.size` answers in DECLARED
1760                // pages, so shift right by log2(page size) (#1441).
1761                //
1762                // SHIFT 0 IS NOT AN LSR, and this is the trap the fix had to
1763                // avoid: in `cond|000|1101|S|0000|Rd|shift5|type|0|Rm` an
1764                // `shift5` of 0 with `type=01` (LSR) means SHIFT BY 32, not by
1765                // zero. `(pagesize 1)` therefore encodes as LSL #0 — the
1766                // canonical `MOV rd, R10` — and a naive "shift by page_log2"
1767                // would have emitted a shift of 32 and returned 0. The #180/#185
1768                // encoder rule applies: Ok-or-Err, never a silent wrong word.
1769                if *page_log2 == 0 {
1770                    // LSL #0 == MOV rd, R10
1771                    0xE1A00000 | (rd_bits << 12) | r10_bits
1772                } else if *page_log2 < 32 {
1773                    // LSR #page_log2: shift5=page_log2, type=01
1774                    0xE1A00000 | (rd_bits << 12) | (page_log2 << 7) | (0b01 << 5) | r10_bits
1775                } else {
1776                    return Err(synth_core::Error::synthesis(format!(
1777                        "#1441 internal: memory.size page_log2 = {page_log2} is \
1778                         not encodable as an ARM32 shift (0..=31) — refusing \
1779                         rather than emitting a shift of 32"
1780                    )));
1781                }
1782            }
1783
1784            ArmOp::MemoryGrow { rd, .. } => {
1785                let rd_bits = reg_to_bits(rd);
1786                // On embedded, always fail: MOV rd, #-1
1787                0xE3E00000 | (rd_bits << 12) // MVN rd, #0 = MOV rd, #-1
1788            }
1789
1790            // Label pseudo-instruction: emits no machine code
1791            ArmOp::Label { .. } => {
1792                return Ok(Vec::new());
1793            }
1794
1795            // Branch instructions
1796            ArmOp::B { label: _ } => {
1797                // B encoding: cond(4) | 1010 | offset(24)
1798                // Simplified: branch to offset 0 (will be patched by linker/resolver)
1799                0xEA000000
1800            }
1801
1802            // Conditional branch to label (generic)
1803            ArmOp::Bcc { cond, label: _ } => {
1804                use synth_synthesis::Condition;
1805                let cond_bits: u32 = match cond {
1806                    Condition::EQ => 0x0,
1807                    Condition::NE => 0x1,
1808                    Condition::HS => 0x2,
1809                    Condition::LO => 0x3,
1810                    Condition::HI => 0x8,
1811                    Condition::LS => 0x9,
1812                    Condition::GE => 0xA,
1813                    Condition::LT => 0xB,
1814                    Condition::GT => 0xC,
1815                    Condition::LE => 0xD,
1816                };
1817                // B<cond> with offset 0 (will be patched)
1818                (cond_bits << 28) | 0x0A000000
1819            }
1820
1821            // BHS (Branch if Higher or Same) - used for bounds checking
1822            ArmOp::Bhs { label: _ } => {
1823                // BHS encoding: cond(2=HS) | 1010 | offset(24)
1824                0x2A000000 // BHS with offset 0
1825            }
1826
1827            // BLO (Branch if Lower) - complementary to BHS
1828            ArmOp::Blo { label: _ } => {
1829                // BLO encoding: cond(3=LO) | 1010 | offset(24)
1830                0x3A000000 // BLO with offset 0
1831            }
1832
1833            // Branch with numeric offset (in instructions)
1834            // ARM32 B instruction: offset is in instructions, stored as words
1835            // The offset is relative to PC+8 (due to ARM pipeline)
1836            ArmOp::BOffset { offset } => {
1837                // B encoding: cond(4) | 1010 | offset(24)
1838                // Offset is signed, in words (4-byte units)
1839                // ARM adds PC+8 to the offset, so we need to adjust:
1840                // target = PC + 8 + (offset * 4)
1841                // For backward branch of N instructions: offset = -(N + 2)
1842                // wrapping_sub keeps the encoder total under fuzzing (#186): an
1843                // extreme i32::MIN offset would otherwise overflow-panic; for any
1844                // real branch offset this is identical to `- 2`.
1845                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1846                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1847                0xEA000000 | offset_bits
1848            }
1849
1850            // Conditional branch with numeric offset
1851            ArmOp::BCondOffset { cond, offset } => {
1852                use synth_synthesis::Condition;
1853                let cond_bits: u32 = match cond {
1854                    Condition::EQ => 0x0,
1855                    Condition::NE => 0x1,
1856                    Condition::HS => 0x2,
1857                    Condition::LO => 0x3,
1858                    Condition::HI => 0x8,
1859                    Condition::LS => 0x9,
1860                    Condition::GE => 0xA,
1861                    Condition::LT => 0xB,
1862                    Condition::GT => 0xC,
1863                    Condition::LE => 0xD,
1864                };
1865                // B<cond> encoding: cond(4) | 1010 | offset(24)
1866                // wrapping_sub: total under fuzzing (#186), identical for real offsets.
1867                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1868                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1869                (cond_bits << 28) | 0x0A000000 | offset_bits
1870            }
1871
1872            ArmOp::Bl { label: _ } => {
1873                // BL encoding: cond(4) | 1011 | offset(24). Relocatable
1874                // placeholder; an R_ARM_CALL relocation patches the target.
1875                //
1876                // #1040: the placeholder must carry an embedded addend of -8,
1877                // not 0. A32 `BL` computes `target = P + 8 + (imm24 << 2)`, so
1878                // under REL semantics a 0 addend (`eb000000`) resolves two
1879                // instructions PAST the callee entry — the A32 twin of the
1880                // Thumb #174 bug. The correct word is what `gas` emits for
1881                // `bl <extern>` in ARM mode:
1882                //   ebfffffe  ->  `bl <self>`  (imm24 = -2, offset = -8),
1883                // which nets to exactly S. Verified against
1884                // `arm-none-eabi-as -march=armv7-r`, which emits `ebfffffe`
1885                // with an R_ARM_CALL relocation.
1886                0xEBFFFFFE
1887            }
1888
1889            ArmOp::Bx { rm } => {
1890                let rm_bits = reg_to_bits(rm);
1891
1892                // BX encoding: cond(4) | 000100101111111111110001 | Rm(4)
1893                0xE12FFF10 | rm_bits
1894            }
1895
1896            ArmOp::Blx { rm } => {
1897                let rm_bits = reg_to_bits(rm);
1898
1899                // BLX (register) encoding: cond(4) | 000100101111111111110011 | Rm(4)
1900                0xE12FFF30 | rm_bits
1901            }
1902
1903            ArmOp::Push { regs } => {
1904                // STMDB SP!, {regs} encoding: cond(4) | 100100 | 10 | 1101 | register_list(16)
1905                let mut reg_list: u32 = 0;
1906                for r in regs {
1907                    reg_list |= 1 << reg_to_bits(r);
1908                }
1909                0xE92D0000 | reg_list
1910            }
1911
1912            ArmOp::Pop { regs } => {
1913                // LDMIA SP!, {regs} encoding: cond(4) | 100010 | 11 | 1101 | register_list(16)
1914                let mut reg_list: u32 = 0;
1915                for r in regs {
1916                    reg_list |= 1 << reg_to_bits(r);
1917                }
1918                0xE8BD0000 | reg_list
1919            }
1920
1921            ArmOp::Nop => {
1922                // NOP encoding: MOV R0, R0
1923                0xE1A00000
1924            }
1925
1926            ArmOp::Udf { imm } => {
1927                // UDF (Undefined) encoding in ARM: 0xE7F000F0 | (imm12_hi << 8) | imm4_lo
1928                // We only use imm8, so split into imm4_hi and imm4_lo
1929                let imm8 = *imm as u32;
1930                0xE7F000F0 | ((imm8 & 0xF0) << 4) | (imm8 & 0x0F)
1931            }
1932
1933            // #615: handled by the `encode_arm_expanded` early return at the
1934            // top of this function — a real MOV{cond}/MOV pair now, never a
1935            // silent NOP again.
1936            ArmOp::Popcnt { .. } | ArmOp::SetCond { .. } | ArmOp::SelectMove { .. } => {
1937                unreachable!("handled by encode_arm_expanded (#615)")
1938            }
1939
1940            // Verification-only pseudo-ops: `synth-verify`'s ArmSemantics
1941            // models these, but NO codegen path constructs them (the selector
1942            // lowers select/locals/globals/br_table/call to real instruction
1943            // sequences before the encoder). Encoding one as a NOP silently
1944            // dropped the operation (#615 class); a typed Err keeps the
1945            // encoder total (Ok-or-Err, the `encoder_no_panic` contract)
1946            // while making any future reachability LOUD.
1947            ArmOp::Select { .. }
1948            | ArmOp::LocalGet { .. }
1949            | ArmOp::LocalSet { .. }
1950            | ArmOp::LocalTee { .. }
1951            | ArmOp::GlobalGet { .. }
1952            | ArmOp::GlobalSet { .. }
1953            | ArmOp::BrTable { .. }
1954            | ArmOp::Call { .. } => {
1955                return Err(synth_core::Error::synthesis(format!(
1956                    "verification-only pseudo-op {op:?} reached the A32 encoder — \
1957                     codegen lowers it before encoding; refusing to emit a silent NOP (#615)"
1958                )));
1959            }
1960
1961            // #594: CallIndirect is expanded to a real multi-instruction
1962            // sequence by the early return at the top of this function —
1963            // it must NEVER fall through to a silent NOP again.
1964            ArmOp::CallIndirect { .. } => {
1965                unreachable!("CallIndirect handled by encode_arm_call_indirect (#594)")
1966            }
1967
1968            // #615: every i64 op (and I32WrapI64) is expanded to a real A32
1969            // multi-instruction sequence by `encode_arm_expanded` — the
1970            // "encode as NOP for now" era ended with the value silently
1971            // vanishing on `--target cortex-r5`.
1972            ArmOp::I64Add { .. }
1973            | ArmOp::I64Sub { .. }
1974            | ArmOp::I64DivS { .. }
1975            | ArmOp::I64DivU { .. }
1976            | ArmOp::I64RemS { .. }
1977            | ArmOp::I64RemU { .. }
1978            | ArmOp::I64Clz { .. }
1979            | ArmOp::I64Ctz { .. }
1980            | ArmOp::I64Popcnt { .. }
1981            | ArmOp::I64And { .. }
1982            | ArmOp::I64Or { .. }
1983            | ArmOp::I64Xor { .. }
1984            | ArmOp::I64Eqz { .. }
1985            | ArmOp::I64Eq { .. }
1986            | ArmOp::I64Ne { .. }
1987            | ArmOp::I64LtS { .. }
1988            | ArmOp::I64LtU { .. }
1989            | ArmOp::I64LeS { .. }
1990            | ArmOp::I64LeU { .. }
1991            | ArmOp::I64GtS { .. }
1992            | ArmOp::I64GtU { .. }
1993            | ArmOp::I64GeS { .. }
1994            | ArmOp::I64GeU { .. }
1995            | ArmOp::I64Const { .. }
1996            | ArmOp::I64Ldr { .. }
1997            | ArmOp::I64Str { .. }
1998            | ArmOp::I64ExtendI32S { .. }
1999            | ArmOp::I64ExtendI32U { .. }
2000            | ArmOp::I64Extend8S { .. }
2001            | ArmOp::I64Extend16S { .. }
2002            | ArmOp::I64Extend32S { .. }
2003            | ArmOp::I32WrapI64 { .. } => {
2004                unreachable!("handled by encode_arm_expanded (#615)")
2005            }
2006
2007            // f32 VFP single-precision instructions
2008            ArmOp::F32Add { sd, sn, sm } => encode_vfp_3reg(0xEE300A00, sd, sn, sm)?,
2009            ArmOp::F32Sub { sd, sn, sm } => encode_vfp_3reg(0xEE300A40, sd, sn, sm)?,
2010            ArmOp::F32Mul { sd, sn, sm } => encode_vfp_3reg(0xEE200A00, sd, sn, sm)?,
2011            ArmOp::F32Div { sd, sn, sm } => encode_vfp_3reg(0xEE800A00, sd, sn, sm)?,
2012            ArmOp::F32Abs { sd, sm } => encode_vfp_2reg(0xEEB00AC0, sd, sm)?,
2013            ArmOp::F32Neg { sd, sm } => encode_vfp_2reg(0xEEB10A40, sd, sm)?,
2014            ArmOp::F32Sqrt { sd, sm } => encode_vfp_2reg(0xEEB10AC0, sd, sm)?,
2015
2016            // f32 pseudo-ops — multi-instruction sequences
2017            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
2018            ArmOp::F32Ceil { sd, sm } => {
2019                return self.encode_arm_f32_rounding(sd, sm, 0b01); // Round toward +Inf
2020            }
2021            ArmOp::F32Floor { sd, sm } => {
2022                return self.encode_arm_f32_rounding(sd, sm, 0b10); // Round toward -Inf
2023            }
2024            ArmOp::F32Trunc { sd, sm } => {
2025                return self.encode_arm_f32_rounding(sd, sm, 0b11); // VCVT toward zero
2026            }
2027            ArmOp::F32Nearest { sd, sm } => {
2028                return self.encode_arm_f32_rounding(sd, sm, 0b00); // VCVT to nearest
2029            }
2030            ArmOp::F32Min { sd, sn, sm } => {
2031                return self.encode_arm_f32_minmax(sd, sn, sm, true);
2032            }
2033            ArmOp::F32Max { sd, sn, sm } => {
2034                return self.encode_arm_f32_minmax(sd, sn, sm, false);
2035            }
2036            ArmOp::F32Copysign { sd, sn, sm } => {
2037                return self.encode_arm_f32_copysign(sd, sn, sm);
2038            }
2039
2040            // f32 comparisons — multi-instruction: VCMP + VMRS + conditional MOV
2041            ArmOp::F32Eq { rd, sn, sm } => {
2042                return self.encode_arm_f32_compare(rd, sn, sm, 0x0); // EQ
2043            }
2044            ArmOp::F32Ne { rd, sn, sm } => {
2045                return self.encode_arm_f32_compare(rd, sn, sm, 0x1); // NE
2046            }
2047            ArmOp::F32Lt { rd, sn, sm } => {
2048                return self.encode_arm_f32_compare(rd, sn, sm, 0x4); // MI (less than)
2049            }
2050            ArmOp::F32Le { rd, sn, sm } => {
2051                return self.encode_arm_f32_compare(rd, sn, sm, 0x9); // LS (less or same)
2052            }
2053            ArmOp::F32Gt { rd, sn, sm } => {
2054                return self.encode_arm_f32_compare(rd, sn, sm, 0xC); // GT
2055            }
2056            ArmOp::F32Ge { rd, sn, sm } => {
2057                return self.encode_arm_f32_compare(rd, sn, sm, 0xA); // GE
2058            }
2059
2060            // f32 const — multi-instruction: MOVW + MOVT + VMOV
2061            ArmOp::F32Const { sd, value } => {
2062                return self.encode_arm_f32_const(sd, *value);
2063            }
2064
2065            ArmOp::F32Load { sd, addr } => encode_vfp_ldst(0xED900A00, sd, addr)?,
2066            ArmOp::F32Store { sd, addr } => encode_vfp_ldst(0xED800A00, sd, addr)?,
2067
2068            // f32 conversions — multi-instruction sequences
2069            ArmOp::F32ConvertI32S { sd, rm } => {
2070                return self.encode_arm_f32_convert_i32(sd, rm, true);
2071            }
2072            ArmOp::F32ConvertI32U { sd, rm } => {
2073                return self.encode_arm_f32_convert_i32(sd, rm, false);
2074            }
2075            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
2076                return Err(synth_core::Error::synthesis(
2077                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2078                ));
2079            }
2080            // RQ-67-VFPREACH (#1267): the callee-saved VFP save/restore is
2081            // emitted only by the Cortex-M (Thumb-2) VFP-pressure rung. The A32
2082            // path has no such rung, so reaching here means a caller built the
2083            // op for the wrong profile — refuse LOUDLY rather than encode
2084            // something plausible (#615: expand-or-loud-reject, never a silent
2085            // NOP).
2086            ArmOp::VPushCalleeSavedVfp | ArmOp::VPopCalleeSavedVfp => {
2087                return Err(synth_core::Error::synthesis(
2088                    "VPUSH/VPOP {d8-d15} is Thumb-2 only in this backend (RQ-67-VFPREACH, #1267)",
2089                ));
2090            }
2091            ArmOp::F32ReinterpretI32 { sd, rm } => encode_vmov_core_sreg(true, sd, rm)?,
2092            ArmOp::I32ReinterpretF32 { rd, sm } => encode_vmov_core_sreg(false, sm, rd)?,
2093            ArmOp::I32TruncF32S { rd, sm } => {
2094                return self.encode_arm_i32_trunc_f32(rd, sm, true);
2095            }
2096            ArmOp::I32TruncF32U { rd, sm } => {
2097                return self.encode_arm_i32_trunc_f32(rd, sm, false);
2098            }
2099
2100            // f64 VFP double-precision instructions (ARM32)
2101            // F64 arithmetic: same as F32 but with sz=1 (bit 8 = 1, cp11 = 0xB)
2102            ArmOp::F64Add { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B00, dd, dn, dm)?,
2103            ArmOp::F64Sub { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B40, dd, dn, dm)?,
2104            ArmOp::F64Mul { dd, dn, dm } => encode_vfp_3reg_f64(0xEE200B00, dd, dn, dm)?,
2105            ArmOp::F64Div { dd, dn, dm } => encode_vfp_3reg_f64(0xEE800B00, dd, dn, dm)?,
2106            ArmOp::F64Abs { dd, dm } => encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?,
2107            ArmOp::F64Neg { dd, dm } => encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?,
2108            ArmOp::F64Sqrt { dd, dm } => encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?,
2109
2110            // f64 pseudo-ops
2111            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
2112            ArmOp::F64Ceil { dd, dm } => {
2113                return self.encode_arm_f64_rounding(dd, dm, 0b01);
2114            }
2115            ArmOp::F64Floor { dd, dm } => {
2116                return self.encode_arm_f64_rounding(dd, dm, 0b10);
2117            }
2118            ArmOp::F64Trunc { dd, dm } => {
2119                return self.encode_arm_f64_rounding(dd, dm, 0b11);
2120            }
2121            ArmOp::F64Nearest { dd, dm } => {
2122                return self.encode_arm_f64_rounding(dd, dm, 0b00);
2123            }
2124            ArmOp::F64Min { dd, dn, dm } => {
2125                return self.encode_arm_f64_minmax(dd, dn, dm, true);
2126            }
2127            ArmOp::F64Max { dd, dn, dm } => {
2128                return self.encode_arm_f64_minmax(dd, dn, dm, false);
2129            }
2130            ArmOp::F64Copysign { dd, dn, dm } => {
2131                return self.encode_arm_f64_copysign(dd, dn, dm);
2132            }
2133
2134            // f64 comparisons
2135            ArmOp::F64Eq { rd, dn, dm } => {
2136                return self.encode_arm_f64_compare(rd, dn, dm, 0x0);
2137            }
2138            ArmOp::F64Ne { rd, dn, dm } => {
2139                return self.encode_arm_f64_compare(rd, dn, dm, 0x1);
2140            }
2141            ArmOp::F64Lt { rd, dn, dm } => {
2142                return self.encode_arm_f64_compare(rd, dn, dm, 0x4);
2143            }
2144            ArmOp::F64Le { rd, dn, dm } => {
2145                return self.encode_arm_f64_compare(rd, dn, dm, 0x9);
2146            }
2147            ArmOp::F64Gt { rd, dn, dm } => {
2148                return self.encode_arm_f64_compare(rd, dn, dm, 0xC);
2149            }
2150            ArmOp::F64Ge { rd, dn, dm } => {
2151                return self.encode_arm_f64_compare(rd, dn, dm, 0xA);
2152            }
2153
2154            ArmOp::F64Const { dd, value } => {
2155                return self.encode_arm_f64_const(dd, *value);
2156            }
2157
2158            ArmOp::F64Load { dd, addr } => encode_vfp_ldst_f64(0xED900B00, dd, addr)?,
2159            ArmOp::F64Store { dd, addr } => encode_vfp_ldst_f64(0xED800B00, dd, addr)?,
2160
2161            ArmOp::F64ConvertI32S { dd, rm } => {
2162                return self.encode_arm_f64_convert_i32(dd, rm, true);
2163            }
2164            ArmOp::F64ConvertI32U { dd, rm } => {
2165                return self.encode_arm_f64_convert_i32(dd, rm, false);
2166            }
2167            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
2168                return Err(synth_core::Error::synthesis(
2169                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2170                ));
2171            }
2172            ArmOp::F64PromoteF32 { dd, sm } => {
2173                return self.encode_arm_f64_promote_f32(dd, sm);
2174            }
2175            // GI-FPU-002 (#369): no synth A32 target carries an FPU (cortex-r5
2176            // has none — the selector declines every float op there), so the
2177            // A32 encoder refuses loudly instead of shipping an untested
2178            // encoding (#615: never a silent wrong byte).
2179            ArmOp::F32DemoteF64 { .. } => {
2180                return Err(synth_core::Error::synthesis(
2181                    "F32DemoteF64 has no A32 encoding (no A32 target has an FPU)",
2182                ));
2183            }
2184            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => {
2185                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?
2186            }
2187            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => {
2188                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?
2189            }
2190            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
2191                return Err(synth_core::Error::synthesis(
2192                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
2193                ));
2194            }
2195            ArmOp::I32TruncF64S { rd, dm } => {
2196                return self.encode_arm_i32_trunc_f64(rd, dm, true);
2197            }
2198            ArmOp::I32TruncF64U { rd, dm } => {
2199                return self.encode_arm_i32_trunc_f64(rd, dm, false);
2200            }
2201            // #615: multi-instruction i64 sequences — expanded to real A32 by
2202            // `encode_arm_expanded`, no longer "Thumb-2 only" NOPs.
2203            ArmOp::I64SetCond { .. }
2204            | ArmOp::I64SetCondZ { .. }
2205            | ArmOp::I64Mul { .. }
2206            | ArmOp::I64Shl { .. }
2207            | ArmOp::I64ShrS { .. }
2208            | ArmOp::I64ShrU { .. }
2209            | ArmOp::I64Rotl { .. }
2210            | ArmOp::I64Rotr { .. } => {
2211                unreachable!("handled by encode_arm_expanded (#615)")
2212            }
2213
2214            // MVE instructions — Thumb-2 only (Cortex-M55 is always Thumb-2)
2215            ArmOp::MveLoad { .. }
2216            | ArmOp::MveStore { .. }
2217            | ArmOp::MveConst { .. }
2218            | ArmOp::MveAnd { .. }
2219            | ArmOp::MveOrr { .. }
2220            | ArmOp::MveEor { .. }
2221            | ArmOp::MveMvn { .. }
2222            | ArmOp::MveBic { .. }
2223            | ArmOp::MveAddI { .. }
2224            | ArmOp::MveSubI { .. }
2225            | ArmOp::MveMulI { .. }
2226            | ArmOp::MveNegI { .. }
2227            | ArmOp::MveCmpEqI { .. }
2228            | ArmOp::MveCmpNeI { .. }
2229            | ArmOp::MveCmpLtS { .. }
2230            | ArmOp::MveCmpLtU { .. }
2231            | ArmOp::MveCmpGtS { .. }
2232            | ArmOp::MveCmpGtU { .. }
2233            | ArmOp::MveCmpLeS { .. }
2234            | ArmOp::MveCmpLeU { .. }
2235            | ArmOp::MveCmpGeS { .. }
2236            | ArmOp::MveCmpGeU { .. }
2237            | ArmOp::MveDup { .. }
2238            | ArmOp::MveExtractLane { .. }
2239            | ArmOp::MveInsertLane { .. }
2240            | ArmOp::MveAddF32 { .. }
2241            | ArmOp::MveSubF32 { .. }
2242            | ArmOp::MveMulF32 { .. }
2243            | ArmOp::MveNegF32 { .. }
2244            | ArmOp::MveAbsF32 { .. }
2245            | ArmOp::MveCmpEqF32 { .. }
2246            | ArmOp::MveCmpNeF32 { .. }
2247            | ArmOp::MveCmpLtF32 { .. }
2248            | ArmOp::MveCmpLeF32 { .. }
2249            | ArmOp::MveCmpGtF32 { .. }
2250            | ArmOp::MveCmpGeF32 { .. }
2251            | ArmOp::MveDupF32 { .. }
2252            | ArmOp::MveExtractLaneF32 { .. }
2253            | ArmOp::MveReplaceLaneF32 { .. }
2254            | ArmOp::MveDivF32 { .. }
2255            | ArmOp::MveSqrtF32 { .. } => {
2256                // MVE (Helium) is a Thumb-2-only extension (Cortex-M55); there
2257                // is no A32 encoding. The selector only emits MVE ops for
2258                // Thumb targets — a NOP here silently dropped the vector op
2259                // if that invariant ever broke (#615 class). Err keeps the
2260                // encoder total and the failure loud.
2261                return Err(synth_core::Error::synthesis(format!(
2262                    "MVE op {op:?} has no A32 (ARM-mode) encoding — MVE is Thumb-2 only (#615)"
2263                )));
2264            }
2265        };
2266
2267        // ARM32 instructions are little-endian
2268        Ok(instr.to_le_bytes().to_vec())
2269    }
2270
2271    // === ARM32 VFP multi-instruction helpers ===
2272
2273    /// Encode F32 comparison as ARM32: VCMP.F32 + VMRS + MOV rd,#0 + MOVcond rd,#1
2274    fn encode_arm_f32_compare(
2275        &self,
2276        rd: &Reg,
2277        sn: &VfpReg,
2278        sm: &VfpReg,
2279        cond_code: u32,
2280    ) -> Result<Vec<u8>> {
2281        let mut bytes = Vec::new();
2282
2283        // VCMP.F32 Sn, Sm: 0xEEB40A40 with Sn in Vd position, Sm in Vm position
2284        let sn_num = vfp_sreg_to_num(sn)?;
2285        let sm_num = vfp_sreg_to_num(sm)?;
2286        let (vd, d) = encode_sreg(sn_num);
2287        let (vm, m) = encode_sreg(sm_num);
2288        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2289        bytes.extend_from_slice(&vcmp.to_le_bytes());
2290
2291        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
2292        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2293
2294        // MOV rd, #0: 0xE3A0_0000 | (rd << 12)
2295        let rd_bits = reg_to_bits(rd);
2296        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2297        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2298
2299        // MOVcond rd, #1: cond(4) | 0011 1010 0000 rd(4) 0000 0000 0001
2300        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2301        bytes.extend_from_slice(&mov_one.to_le_bytes());
2302
2303        Ok(bytes)
2304    }
2305
2306    /// Encode F32 constant load as ARM32: MOVW Rt,#lo16 + MOVT Rt,#hi16 + VMOV Sd,Rt
2307    fn encode_arm_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
2308        let mut bytes = Vec::new();
2309        let bits = value.to_bits();
2310
2311        // Use R12 as temp register for constant loading
2312        let rt: u32 = 12; // R12/IP
2313
2314        // MOVW R12, #lo16: 0xE300_C000 | (imm4 << 16) | imm12
2315        let lo16 = bits & 0xFFFF;
2316        let movw = 0xE3000000 | (rt << 12) | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2317        bytes.extend_from_slice(&movw.to_le_bytes());
2318
2319        // MOVT R12, #hi16: 0xE340_C000 | (imm4 << 16) | imm12
2320        let hi16 = (bits >> 16) & 0xFFFF;
2321        let movt = 0xE3400000 | (rt << 12) | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2322        bytes.extend_from_slice(&movt.to_le_bytes());
2323
2324        // VMOV Sd, R12
2325        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
2326        bytes.extend_from_slice(&vmov.to_le_bytes());
2327
2328        Ok(bytes)
2329    }
2330
2331    /// Encode VMOV + VCVT.F32.S32/U32 as ARM32
2332    fn encode_arm_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2333        let mut bytes = Vec::new();
2334
2335        // VMOV Sd, Rm — move integer to VFP register
2336        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
2337        bytes.extend_from_slice(&vmov.to_le_bytes());
2338
2339        // VCVT.F32.S32 Sd, Sd (signed) or VCVT.F32.U32 Sd, Sd (unsigned).
2340        // The "op" bit (bit 7) selects signedness: 1 = signed (S32), 0 =
2341        // unsigned (U32). So signed = 0xEEB80AC0, unsigned = 0xEEB80A40 —
2342        // objdump confirms 0xEEB80A40 decodes to `vcvt.f32.u32` (GI-FPU-002:
2343        // the two were previously swapped, silently making `convert_i32_s`
2344        // an unsigned conversion).
2345        let sd_num = vfp_sreg_to_num(sd)?;
2346        let (vd, d) = encode_sreg(sd_num);
2347        let (vm, m) = encode_sreg(sd_num); // same register as source
2348        let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
2349        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2350        bytes.extend_from_slice(&vcvt.to_le_bytes());
2351
2352        Ok(bytes)
2353    }
2354
2355    /// Encode F32 rounding pseudo-op as ARM32 via VCVT to integer and back.
2356    /// mode: 0b00=nearest, 0b01=floor(-Inf), 0b10=ceil(+Inf), 0b11=trunc(zero)
2357    /// Strategy: VCVT.S32.F32 Sd, Sm (toward zero), then VCVT.F32.S32 Sd, Sd
2358    /// For ceil/floor/nearest, we use VCVTR (round toward mode) + convert back.
2359    /// Simplified: convert to int (toward zero for trunc) then back to float.
2360    /// Encode F32 rounding as ARM32.
2361    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2362    ///
2363    /// For trunc (mode=0b11): uses VCVTR.S32.F32 (always rounds toward zero).
2364    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant
2365    /// which honours FPSCR rmode), then restores FPSCR.
2366    fn encode_arm_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2367        let mut bytes = Vec::new();
2368        let sm_num = vfp_sreg_to_num(sm)?;
2369        let sd_num = vfp_sreg_to_num(sd)?;
2370        let (vd_s, d_s) = encode_sreg(sd_num);
2371        let (vm_s, m_s) = encode_sreg(sm_num);
2372
2373        if mode == 0b11 {
2374            // Trunc (toward zero): VCVTR.S32.F32 — the "R" variant always truncates.
2375            // 0xEEBD0AC0: bit[7]=1 => round toward zero regardless of FPSCR
2376            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2377            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2378        } else {
2379            // ceil/floor/nearest: manipulate FPSCR rounding mode
2380            let rt: u32 = 12; // R12/IP as temp
2381
2382            // VMRS R12, FPSCR
2383            let vmrs = 0xEEF10A10 | (rt << 12);
2384            bytes.extend_from_slice(&vmrs.to_le_bytes());
2385
2386            // BIC R12, R12, #(3 << 22) — clear RMode bits [23:22]
2387            // 3<<22 = 0x00C00000. ARM rotated imm: 0x03 ror 10 (rotation=5, imm8=0x03)
2388            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2389            bytes.extend_from_slice(&bic.to_le_bytes());
2390
2391            // ORR R12, R12, #(mode << 22) — set desired rounding mode
2392            if mode != 0 {
2393                // mode<<22: rotation=5, imm8=mode
2394                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2395                bytes.extend_from_slice(&orr.to_le_bytes());
2396            }
2397
2398            // VMSR FPSCR, R12
2399            let vmsr = 0xEEE10A10 | (rt << 12);
2400            bytes.extend_from_slice(&vmsr.to_le_bytes());
2401
2402            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rounding mode
2403            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2404            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2405
2406            // Restore FPSCR: clear rmode bits back to nearest (default)
2407            bytes.extend_from_slice(&vmrs.to_le_bytes());
2408            bytes.extend_from_slice(&bic.to_le_bytes());
2409            bytes.extend_from_slice(&vmsr.to_le_bytes());
2410        }
2411
2412        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
2413        let (vd2, d2) = encode_sreg(sd_num);
2414        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
2415        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2416
2417        Ok(bytes)
2418    }
2419
2420    /// Encode F32 min/max as ARM32: VCMP + VMRS + conditional VMOV
2421    fn encode_arm_f32_minmax(
2422        &self,
2423        sd: &VfpReg,
2424        sn: &VfpReg,
2425        sm: &VfpReg,
2426        is_min: bool,
2427    ) -> Result<Vec<u8>> {
2428        let mut bytes = Vec::new();
2429        let sn_num = vfp_sreg_to_num(sn)?;
2430        let sm_num = vfp_sreg_to_num(sm)?;
2431        let sd_num = vfp_sreg_to_num(sd)?;
2432
2433        // VMOV Sd, Sn (start with first operand)
2434        let (vd, d) = encode_sreg(sd_num);
2435        let (vn, n) = encode_sreg(sn_num);
2436        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2437        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2438
2439        // VCMP.F32 Sn, Sm
2440        let (vm, m) = encode_sreg(sm_num);
2441        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2442        bytes.extend_from_slice(&vcmp.to_le_bytes());
2443
2444        // VMRS APSR_nzcv, FPSCR
2445        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2446
2447        // For min: if Sn > Sm (GT), use Sm. Condition = GT (0xC)
2448        // For max: if Sn < Sm (MI/LT), use Sm. Condition = MI (0x4)
2449        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2450
2451        // VMOV{cond} Sd, Sm — conditional VMOV
2452        let vmov_cond = (cond << 28) | 0x0EB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2453        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2454
2455        Ok(bytes)
2456    }
2457
2458    /// Encode F32 copysign as ARM32: extract sign from Sm, magnitude from Sn
2459    fn encode_arm_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2460        let mut bytes = Vec::new();
2461
2462        // VMOV R12, Sm (get sign source bits)
2463        let vmov_sm = encode_vmov_core_sreg(false, sm, &Reg::R12)?;
2464        bytes.extend_from_slice(&vmov_sm.to_le_bytes());
2465
2466        // VMOV R0, Sn (get magnitude source bits) — use R0 as temp
2467        let vmov_sn = encode_vmov_core_sreg(false, sn, &Reg::R0)?;
2468        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2469
2470        // AND R12, R12, #0x80000000 (keep only sign bit)
2471        // Thumb-2 constant 0x80000000 needs special encoding; in ARM32 use rotated imm
2472        // 0x80000000 = 0x02 rotated right by 2 (rotation=1, imm8=0x02)
2473        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2474        bytes.extend_from_slice(&and_sign.to_le_bytes());
2475
2476        // BIC R0, R0, #0x80000000 (clear sign bit from magnitude)
2477        // R0 = register 0, so Rn and Rd fields are 0
2478        let bic_sign = 0xE3C00000u32 | (1 << 8) | 0x02;
2479        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2480
2481        // ORR R0, R0, R12 (combine sign + magnitude)
2482        // R0 = register 0, so Rn and Rd fields are 0
2483        let orr = 0xE1800000u32 | 12;
2484        bytes.extend_from_slice(&orr.to_le_bytes());
2485
2486        // VMOV Sd, R0
2487        let vmov_result = encode_vmov_core_sreg(true, sd, &Reg::R0)?;
2488        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2489
2490        Ok(bytes)
2491    }
2492
2493    /// Encode F64 comparison as ARM32: VCMP.F64 + VMRS + MOV rd,#0 + MOVcond rd,#1
2494    fn encode_arm_f64_compare(
2495        &self,
2496        rd: &Reg,
2497        dn: &VfpReg,
2498        dm: &VfpReg,
2499        cond_code: u32,
2500    ) -> Result<Vec<u8>> {
2501        let mut bytes = Vec::new();
2502
2503        // VCMP.F64 Dn, Dm: 0xEEB40B40 with Dn in Vd position, Dm in Vm position
2504        let dn_num = vfp_dreg_to_num(dn)?;
2505        let dm_num = vfp_dreg_to_num(dm)?;
2506        let (vd, d) = encode_dreg(dn_num);
2507        let (vm, m) = encode_dreg(dm_num);
2508        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2509        bytes.extend_from_slice(&vcmp.to_le_bytes());
2510
2511        // VMRS APSR_nzcv, FPSCR
2512        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2513
2514        // MOV rd, #0
2515        let rd_bits = reg_to_bits(rd);
2516        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2517        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2518
2519        // MOVcond rd, #1
2520        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2521        bytes.extend_from_slice(&mov_one.to_le_bytes());
2522
2523        Ok(bytes)
2524    }
2525
2526    /// Encode F64 constant load as ARM32: MOVW + MOVT + MOVW + MOVT + VMOV
2527    fn encode_arm_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
2528        let mut bytes = Vec::new();
2529        let bits = value.to_bits();
2530        let lo32 = bits as u32;
2531        let hi32 = (bits >> 32) as u32;
2532
2533        // Load low 32 bits into R0 (Rd field = 0 for R0)
2534        let lo16 = lo32 & 0xFFFF;
2535        let movw_r0 = 0xE3000000 | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2536        bytes.extend_from_slice(&movw_r0.to_le_bytes());
2537        let hi16 = (lo32 >> 16) & 0xFFFF;
2538        let movt_r0 = 0xE3400000 | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2539        bytes.extend_from_slice(&movt_r0.to_le_bytes());
2540
2541        // Load high 32 bits into R12
2542        let lo16 = hi32 & 0xFFFF;
2543        let movw_r12 = 0xE3000000 | ((lo16 >> 12) << 16) | (12 << 12) | (lo16 & 0xFFF);
2544        bytes.extend_from_slice(&movw_r12.to_le_bytes());
2545        let hi16 = (hi32 >> 16) & 0xFFFF;
2546        let movt_r12 = 0xE3400000 | ((hi16 >> 12) << 16) | (12 << 12) | (hi16 & 0xFFF);
2547        bytes.extend_from_slice(&movt_r12.to_le_bytes());
2548
2549        // VMOV Dd, R0, R12
2550        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
2551        bytes.extend_from_slice(&vmov.to_le_bytes());
2552
2553        Ok(bytes)
2554    }
2555
2556    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as ARM32
2557    fn encode_arm_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2558        let mut bytes = Vec::new();
2559
2560        // Use S0 as intermediate: VMOV S0, Rm
2561        let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
2562        bytes.extend_from_slice(&vmov.to_le_bytes());
2563
2564        // VCVT.F64.S32 Dd, S0 (signed) or VCVT.F64.U32 Dd, S0 (unsigned)
2565        // Base: 0xEEB80B40 (signed) or 0xEEB80BC0 (unsigned)
2566        let dd_num = vfp_dreg_to_num(dd)?;
2567        let (vd, d) = encode_dreg(dd_num);
2568        let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
2569        // S0 is register 0: Vm=0, M=0
2570        let vcvt = base | (d << 22) | (vd << 12);
2571        bytes.extend_from_slice(&vcvt.to_le_bytes());
2572
2573        Ok(bytes)
2574    }
2575
2576    /// Encode VCVT.F64.F32 Dd, Sm as ARM32 (f32 to f64 promotion)
2577    fn encode_arm_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2578        let dd_num = vfp_dreg_to_num(dd)?;
2579        let sm_num = vfp_sreg_to_num(sm)?;
2580        let (vd, d) = encode_dreg(dd_num);
2581        let (vm, m) = encode_sreg(sm_num);
2582
2583        // VCVT.F64.F32 Dd, Sm: 0xEEB70AC0
2584        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
2585        Ok(vcvt.to_le_bytes().to_vec())
2586    }
2587
2588    /// Encode VCVT.S32/U32.F64 Sd, Dm + VMOV Rd, Sd as ARM32
2589    fn encode_arm_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2590        let mut bytes = Vec::new();
2591        let dm_num = vfp_dreg_to_num(dm)?;
2592        let (vm, m) = encode_dreg(dm_num);
2593
2594        // VCVT.S32.F64 S0, Dm (toward zero) or VCVT.U32.F64 S0, Dm
2595        // S0: Vd=0, D=0
2596        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
2597        let vcvt = base | (m << 5) | vm;
2598        bytes.extend_from_slice(&vcvt.to_le_bytes());
2599
2600        // VMOV Rd, S0
2601        let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
2602        bytes.extend_from_slice(&vmov.to_le_bytes());
2603
2604        Ok(bytes)
2605    }
2606
2607    /// Encode F64 rounding pseudo-op as ARM32 via VCVT to integer and back.
2608    /// Encode F64 rounding as ARM32.
2609    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2610    ///
2611    /// For trunc: uses VCVTR.S32.F64 (always truncates).
2612    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F64 (non-R variant),
2613    /// then restores FPSCR.
2614    fn encode_arm_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2615        let mut bytes = Vec::new();
2616        let dm_num = vfp_dreg_to_num(dm)?;
2617        let dd_num = vfp_dreg_to_num(dd)?;
2618        let (vm, m) = encode_dreg(dm_num);
2619        let (vd, d) = encode_dreg(dd_num);
2620
2621        if mode == 0b11 {
2622            // Trunc (toward zero): VCVTR.S32.F64 — bit[7]=1, always truncates
2623            let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
2624            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2625        } else {
2626            // ceil/floor/nearest: manipulate FPSCR rounding mode
2627            let rt: u32 = 12;
2628
2629            // VMRS R12, FPSCR
2630            let vmrs = 0xEEF10A10 | (rt << 12);
2631            bytes.extend_from_slice(&vmrs.to_le_bytes());
2632
2633            // BIC R12, R12, #(3 << 22)
2634            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2635            bytes.extend_from_slice(&bic.to_le_bytes());
2636
2637            // ORR R12, R12, #(mode << 22)
2638            if mode != 0 {
2639                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2640                bytes.extend_from_slice(&orr.to_le_bytes());
2641            }
2642
2643            // VMSR FPSCR, R12
2644            let vmsr = 0xEEE10A10 | (rt << 12);
2645            bytes.extend_from_slice(&vmsr.to_le_bytes());
2646
2647            // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0), uses FPSCR rmode
2648            let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
2649            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2650
2651            // Restore FPSCR
2652            bytes.extend_from_slice(&vmrs.to_le_bytes());
2653            bytes.extend_from_slice(&bic.to_le_bytes());
2654            bytes.extend_from_slice(&vmsr.to_le_bytes());
2655        }
2656
2657        // VCVT.F64.S32 Dd, S0 (convert back to double)
2658        let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
2659        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2660
2661        Ok(bytes)
2662    }
2663
2664    /// Encode F64 min/max as ARM32: VMOV + VCMP + VMRS + conditional VMOV
2665    fn encode_arm_f64_minmax(
2666        &self,
2667        dd: &VfpReg,
2668        dn: &VfpReg,
2669        dm: &VfpReg,
2670        is_min: bool,
2671    ) -> Result<Vec<u8>> {
2672        let mut bytes = Vec::new();
2673        let dn_num = vfp_dreg_to_num(dn)?;
2674        let dm_num = vfp_dreg_to_num(dm)?;
2675        let dd_num = vfp_dreg_to_num(dd)?;
2676
2677        // VMOV.F64 Dd, Dn (start with first operand)
2678        let (vd, d) = encode_dreg(dd_num);
2679        let (vn, n) = encode_dreg(dn_num);
2680        let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2681        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2682
2683        // VCMP.F64 Dn, Dm
2684        let (vm, m) = encode_dreg(dm_num);
2685        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2686        bytes.extend_from_slice(&vcmp.to_le_bytes());
2687
2688        // VMRS APSR_nzcv, FPSCR
2689        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2690
2691        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2692        let vmov_cond = (cond << 28) | 0x0EB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2693        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2694
2695        Ok(bytes)
2696    }
2697
2698    /// Encode F64 copysign as ARM32
2699    fn encode_arm_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
2700        let mut bytes = Vec::new();
2701
2702        // VMOV R0, R12, Dm (get sign source bits)
2703        let vmov_dm = encode_vmov_core_dreg(false, dm, &Reg::R0, &Reg::R12)?;
2704        bytes.extend_from_slice(&vmov_dm.to_le_bytes());
2705
2706        // VMOV R1, R2, Dn (get magnitude source bits)
2707        // We use R1 (lo) and R2 (hi) for the magnitude
2708        let vmov_dn = encode_vmov_core_dreg(false, dn, &Reg::R1, &Reg::R2)?;
2709        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2710
2711        // AND R12, R12, #0x80000000 (keep only sign bit from hi word)
2712        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2713        bytes.extend_from_slice(&and_sign.to_le_bytes());
2714
2715        // BIC R2, R2, #0x80000000 (clear sign bit from magnitude hi word)
2716        let bic_sign = 0xE3C00000u32 | (2 << 16) | (2 << 12) | (1 << 8) | 0x02;
2717        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2718
2719        // ORR R2, R2, R12 (combine sign + magnitude)
2720        let orr = 0xE1800000u32 | (2 << 16) | (2 << 12) | 12;
2721        bytes.extend_from_slice(&orr.to_le_bytes());
2722
2723        // VMOV Dd, R1, R2
2724        let vmov_result = encode_vmov_core_dreg(true, dd, &Reg::R1, &Reg::R2)?;
2725        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2726
2727        Ok(bytes)
2728    }
2729
2730    /// Encode VCVT.S32/U32.F32 + VMOV as ARM32
2731    fn encode_arm_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2732        let mut bytes = Vec::new();
2733
2734        // VCVT.S32.F32 Sd, Sm (toward zero) or VCVT.U32.F32 Sd, Sm
2735        // We use Sm as both source and destination for the intermediate result
2736        let sm_num = vfp_sreg_to_num(sm)?;
2737        let (vd, d) = encode_sreg(sm_num);
2738        let (vm, m) = encode_sreg(sm_num);
2739        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
2740        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2741        bytes.extend_from_slice(&vcvt.to_le_bytes());
2742
2743        // VMOV Rd, Sm — move result back to core register
2744        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
2745        bytes.extend_from_slice(&vmov.to_le_bytes());
2746
2747        Ok(bytes)
2748    }
2749
2750    /// Encode an ARM instruction in Thumb-2 mode (16-bit or 32-bit instructions)
2751    fn encode_thumb(&self, op: &ArmOp) -> Result<Vec<u8>> {
2752        // Thumb-2 supports both 16-bit and 32-bit instructions
2753        // 32-bit instructions are encoded as two 16-bit halfwords (big-endian order)
2754        match op {
2755            // === 16-bit Thumb encodings ===
2756            ArmOp::Add { rd, rn, op2 } => {
2757                let rd_bits = reg_to_bits(rd) as u16;
2758                let rn_bits = reg_to_bits(rn) as u16;
2759
2760                if let Operand2::Reg(rm) = op2 {
2761                    let rm_bits = reg_to_bits(rm) as u16;
2762                    // 16-bit ADDS only has 3-bit register fields (R0-R7). For
2763                    // high registers (e.g. R12, the MemLoad/MemStore base
2764                    // scratch) the bits overflow into adjacent fields, silently
2765                    // corrupting the operands — issue #178/#180: `add ip,ip,r0`
2766                    // was emitted as `adds r4,r5,r1`. Guard on all three regs
2767                    // being low and fall back to 32-bit ADD.W otherwise, exactly
2768                    // as the Sub handler below does.
2769                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2770                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2771                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2772                        Ok(instr.to_le_bytes().to_vec())
2773                    } else {
2774                        // ADD.W Rd, Rn, Rm (32-bit) for high registers
2775                        self.encode_thumb32_add_reg_raw(
2776                            rd_bits as u32,
2777                            rn_bits as u32,
2778                            rm_bits as u32,
2779                        )
2780                    }
2781                } else if let Operand2::Imm(imm) = op2 {
2782                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2783                        // ADDS Rd, Rn, #imm3 (16-bit): 0001 110 imm3 Rn Rd
2784                        let instr: u16 = 0x1C00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2785                        Ok(instr.to_le_bytes().to_vec())
2786                    } else {
2787                        // Use 32-bit ADD for larger immediates
2788                        self.encode_thumb32_add(rd, rn, *imm as u32)
2789                    }
2790                } else {
2791                    // Fallback to 32-bit encoding
2792                    self.encode_thumb32_add(rd, rn, 0)
2793                }
2794            }
2795
2796            ArmOp::Sub { rd, rn, op2 } => {
2797                let rd_bits = reg_to_bits(rd) as u16;
2798                let rn_bits = reg_to_bits(rn) as u16;
2799
2800                if let Operand2::Reg(rm) = op2 {
2801                    let rm_bits = reg_to_bits(rm) as u16;
2802                    // 16-bit SUBS can only use low registers (R0-R7)
2803                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2804                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2805                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2806                        Ok(instr.to_le_bytes().to_vec())
2807                    } else {
2808                        // Use 32-bit SUB.W for high registers
2809                        self.encode_thumb32_sub_reg_raw(
2810                            rd_bits as u32,
2811                            rn_bits as u32,
2812                            rm_bits as u32,
2813                        )
2814                    }
2815                } else if let Operand2::Imm(imm) = op2 {
2816                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2817                        // SUBS Rd, Rn, #imm3 (16-bit): 0001 111 imm3 Rn Rd
2818                        let instr: u16 = 0x1E00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2819                        Ok(instr.to_le_bytes().to_vec())
2820                    } else {
2821                        self.encode_thumb32_sub(rd, rn, *imm as u32)
2822                    }
2823                } else {
2824                    self.encode_thumb32_sub(rd, rn, 0)
2825                }
2826            }
2827
2828            ArmOp::Mov { rd, op2 } => {
2829                let rd_bits = reg_to_bits(rd) as u16;
2830
2831                if let Operand2::Imm(imm) = op2 {
2832                    // #498: the old test here was the SIGNED `*imm <= 255`,
2833                    // so a negative immediate (e.g. -1) fell into the 16-bit
2834                    // MOVS arm and encoded the wrong VALUE (#(imm & 0xFF) =
2835                    // #0xFF). A positive imm above 0xFFFF was equally wrong:
2836                    // MOVW truncates to 16 bits. Split on the UNSIGNED value:
2837                    // imm8 → MOVS, imm16 → MOVW, anything wider (negative or
2838                    // >0xFFFF) → the full-value MOVW+MOVT pair. No emitter
2839                    // produces the wide shape today (both selectors
2840                    // materialize wide constants as explicit Movw/Movt or
2841                    // Movw+Mvn), so this is byte-identical on shipped paths —
2842                    // it retires the latent wrong-value encodings the
2843                    // `estimator_encoder_agreement` oracle had pinned.
2844                    let uimm = *imm as u32;
2845                    if uimm <= 255 && rd_bits < 8 {
2846                        // MOVS Rd, #imm8 (16-bit): 0010 0 Rd imm8
2847                        let imm_bits = (*imm as u16) & 0xFF;
2848                        let instr: u16 = 0x2000 | (rd_bits << 8) | imm_bits;
2849                        Ok(instr.to_le_bytes().to_vec())
2850                    } else if uimm <= 0xFFFF {
2851                        // Use 32-bit MOVW for 16-bit immediates
2852                        self.encode_thumb32_movw(rd, uimm)
2853                    } else {
2854                        // Full 32-bit value: MOVW low16 + MOVT high16
2855                        let mut bytes = self.encode_thumb32_movw(rd, uimm & 0xFFFF)?;
2856                        bytes.extend(self.encode_thumb32_movt_raw(reg_to_bits(rd), uimm >> 16)?);
2857                        Ok(bytes)
2858                    }
2859                } else if let Operand2::Reg(rm) = op2 {
2860                    let rm_bits = reg_to_bits(rm) as u16;
2861                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
2862                    // D = Rd[3], Rd[2:0] in lower bits
2863                    let d_bit = (rd_bits >> 3) & 1;
2864                    let instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
2865                    Ok(instr.to_le_bytes().to_vec())
2866                } else {
2867                    // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
2868                    // operand form the arm does not encode (Imm and Reg only) -- #615's
2869                    // silent-miscompile class, on the DEFAULT target. Refuse instead.
2870                    Err(synth_core::Error::synthesis(format!(
2871                        "Thumb-2 MOV with operand {op2:?} has no Thumb-2 encoding here (Imm and Reg \
2872                         are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
2873                    )))
2874                }
2875            }
2876
2877            ArmOp::Push { regs } => {
2878                // Thumb-2 PUSH encoding:
2879                // If all regs in R0-R7 + LR, use 16-bit: 1011 010 M rrrrrrrr
2880                // Otherwise use 32-bit: STMDB SP!, {regs} = 1110 1001 0010 1101 | 0M0 reglist(13)
2881                let mut reg_list: u16 = 0;
2882                let mut need_32bit = false;
2883                for r in regs {
2884                    let bit = reg_to_bits(r);
2885                    if bit >= 8 && *r != Reg::LR {
2886                        need_32bit = true;
2887                    }
2888                    reg_list |= 1 << bit;
2889                }
2890                if !need_32bit {
2891                    // 16-bit PUSH: 1011 010 M rrrrrrrr
2892                    let m_bit = if reg_list & (1 << 14) != 0 {
2893                        1u16
2894                    } else {
2895                        0u16
2896                    };
2897                    let low_regs = reg_list & 0xFF;
2898                    let instr: u16 = 0xB400 | (m_bit << 8) | low_regs;
2899                    Ok(instr.to_le_bytes().to_vec())
2900                } else {
2901                    // 32-bit STMDB SP!, {regs}: E92D | reglist(16)
2902                    let hw1: u16 = 0xE92D;
2903                    let hw2: u16 = reg_list;
2904                    let mut bytes = hw1.to_le_bytes().to_vec();
2905                    bytes.extend_from_slice(&hw2.to_le_bytes());
2906                    Ok(bytes)
2907                }
2908            }
2909
2910            ArmOp::Pop { regs } => {
2911                // Thumb-2 POP encoding:
2912                // If all regs in R0-R7 + PC, use 16-bit: 1011 110 P rrrrrrrr
2913                // Otherwise use 32-bit: LDMIA SP!, {regs} = 1110 1000 1011 1101 | PM0 reglist(13)
2914                let mut reg_list: u16 = 0;
2915                let mut need_32bit = false;
2916                for r in regs {
2917                    let bit = reg_to_bits(r);
2918                    if bit >= 8 && *r != Reg::PC {
2919                        need_32bit = true;
2920                    }
2921                    reg_list |= 1 << bit;
2922                }
2923                if !need_32bit {
2924                    // 16-bit POP: 1011 110 P rrrrrrrr
2925                    let p_bit = if reg_list & (1 << 15) != 0 {
2926                        1u16
2927                    } else {
2928                        0u16
2929                    };
2930                    let low_regs = reg_list & 0xFF;
2931                    let instr: u16 = 0xBC00 | (p_bit << 8) | low_regs;
2932                    Ok(instr.to_le_bytes().to_vec())
2933                } else {
2934                    // 32-bit LDMIA SP!, {regs}: E8BD | reglist(16)
2935                    let hw1: u16 = 0xE8BD;
2936                    let hw2: u16 = reg_list;
2937                    let mut bytes = hw1.to_le_bytes().to_vec();
2938                    bytes.extend_from_slice(&hw2.to_le_bytes());
2939                    Ok(bytes)
2940                }
2941            }
2942
2943            ArmOp::Nop => {
2944                let instr: u16 = 0xBF00; // NOP in Thumb-2
2945                Ok(instr.to_le_bytes().to_vec())
2946            }
2947
2948            ArmOp::Udf { imm } => {
2949                // UDF (Undefined) in Thumb-2: 16-bit encoding is 0xDE00 | imm8
2950                // This triggers UsageFault/HardFault, used for WASM traps
2951                let instr: u16 = 0xDE00 | (*imm as u16);
2952                let bytes = instr.to_le_bytes().to_vec();
2953                encoding_contracts::verify_thumb16(&bytes);
2954                Ok(bytes)
2955            }
2956
2957            // i64 support: ADDS, ADC, SUBS, SBC for register pair arithmetic
2958            // ADDS sets flags (carry), ADC uses carry from previous ADDS
2959            ArmOp::Adds { rd, rn, op2 } => {
2960                let rd_bits = reg_to_bits(rd) as u16;
2961                let rn_bits = reg_to_bits(rn) as u16;
2962
2963                if let Operand2::Reg(rm) = op2 {
2964                    let rm_bits = reg_to_bits(rm) as u16;
2965                    // 16-bit ADDS is R0-R7 only; i64 pair allocation can place
2966                    // operands in R8-R11, which would overflow the 3-bit fields
2967                    // and corrupt the operands (#178/#180 class). Guard and fall
2968                    // back to 32-bit ADDS.W for high registers.
2969                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2970                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2971                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2972                        Ok(instr.to_le_bytes().to_vec())
2973                    } else {
2974                        self.encode_thumb32_adds_reg_raw(
2975                            rd_bits as u32,
2976                            rn_bits as u32,
2977                            rm_bits as u32,
2978                        )
2979                    }
2980                } else {
2981                    // 32-bit Thumb-2 ADDS with immediate
2982                    self.encode_thumb32_adds(rd, rn, 0)
2983                }
2984            }
2985
2986            // ADC: Add with Carry (Thumb-2 32-bit)
2987            // ADC.W Rd, Rn, Rm: EB40 Rn | 00 Rd 00 Rm
2988            ArmOp::Adc { rd, rn, op2 } => {
2989                let rd_bits = reg_to_bits(rd);
2990                let rn_bits = reg_to_bits(rn);
2991
2992                if let Operand2::Reg(rm) = op2 {
2993                    let rm_bits = reg_to_bits(rm);
2994                    // ADC.W Rd, Rn, Rm (T2): 1110 1011 0100 Rn | 0 000 Rd 00 00 Rm
2995                    let hw1: u16 = (0xEB40 | rn_bits) as u16;
2996                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2997
2998                    let mut bytes = hw1.to_le_bytes().to_vec();
2999                    bytes.extend_from_slice(&hw2.to_le_bytes());
3000                    Ok(bytes)
3001                } else {
3002                    // ADC with immediate - use 32-bit encoding
3003                    let hw1: u16 = (0xF140 | rn_bits) as u16;
3004                    let hw2: u16 = (rd_bits << 8) as u16;
3005                    let mut bytes = hw1.to_le_bytes().to_vec();
3006                    bytes.extend_from_slice(&hw2.to_le_bytes());
3007                    Ok(bytes)
3008                }
3009            }
3010
3011            // SUBS sets flags (borrow), SBC uses borrow from previous SUBS
3012            ArmOp::Subs { rd, rn, op2 } => {
3013                let rd_bits = reg_to_bits(rd) as u16;
3014                let rn_bits = reg_to_bits(rn) as u16;
3015
3016                if let Operand2::Reg(rm) = op2 {
3017                    let rm_bits = reg_to_bits(rm) as u16;
3018                    // 16-bit SUBS is R0-R7 only; high-register i64 pair operands
3019                    // would overflow the 3-bit fields (#178/#180 class). Guard
3020                    // and fall back to 32-bit SUBS.W for high registers.
3021                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
3022                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
3023                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
3024                        Ok(instr.to_le_bytes().to_vec())
3025                    } else {
3026                        self.encode_thumb32_subs_reg_raw(
3027                            rd_bits as u32,
3028                            rn_bits as u32,
3029                            rm_bits as u32,
3030                        )
3031                    }
3032                } else {
3033                    // 32-bit Thumb-2 SUBS with immediate
3034                    self.encode_thumb32_subs(rd, rn, 0)
3035                }
3036            }
3037
3038            // SBC: Subtract with Carry (Thumb-2 32-bit)
3039            // SBC.W Rd, Rn, Rm: EB60 Rn | 00 Rd 00 Rm
3040            ArmOp::Sbc { rd, rn, op2 } => {
3041                let rd_bits = reg_to_bits(rd);
3042                let rn_bits = reg_to_bits(rn);
3043
3044                if let Operand2::Reg(rm) = op2 {
3045                    let rm_bits = reg_to_bits(rm);
3046                    // SBC.W Rd, Rn, Rm (T2): 1110 1011 0110 Rn | 0 000 Rd 00 00 Rm
3047                    let hw1: u16 = (0xEB60 | rn_bits) as u16;
3048                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3049
3050                    let mut bytes = hw1.to_le_bytes().to_vec();
3051                    bytes.extend_from_slice(&hw2.to_le_bytes());
3052                    Ok(bytes)
3053                } else {
3054                    // SBC with immediate - use 32-bit encoding
3055                    let hw1: u16 = (0xF160 | rn_bits) as u16;
3056                    let hw2: u16 = (rd_bits << 8) as u16;
3057                    let mut bytes = hw1.to_le_bytes().to_vec();
3058                    bytes.extend_from_slice(&hw2.to_le_bytes());
3059                    Ok(bytes)
3060                }
3061            }
3062
3063            // === 32-bit Thumb-2 encodings ===
3064
3065            // SDIV: 11111011 1001 Rn 1111 Rd 1111 Rm
3066            ArmOp::Sdiv { rd, rn, rm } => {
3067                let rd_bits = reg_to_bits(rd);
3068                let rn_bits = reg_to_bits(rn);
3069                let rm_bits = reg_to_bits(rm);
3070                reg_bits_checked(rd_bits)?;
3071                reg_bits_checked(rn_bits)?;
3072                reg_bits_checked(rm_bits)?;
3073
3074                // Thumb-2 SDIV: FB90 F0F0 | Rn<<16 | Rd<<8 | Rm
3075                // First halfword: 1111 1011 1001 Rn = 0xFB90 | Rn
3076                // Second halfword: 1111 Rd 1111 Rm = 0xF0F0 | Rd<<8 | Rm
3077                let hw1: u16 = (0xFB90 | rn_bits) as u16;
3078                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
3079
3080                // Thumb-2 32-bit instructions: first halfword, then second halfword (little-endian each)
3081                let mut bytes = hw1.to_le_bytes().to_vec();
3082                bytes.extend_from_slice(&hw2.to_le_bytes());
3083                encoding_contracts::verify_thumb32(&bytes);
3084                Ok(bytes)
3085            }
3086
3087            // UDIV: 11111011 1011 Rn 1111 Rd 1111 Rm
3088            ArmOp::Udiv { rd, rn, rm } => {
3089                let rd_bits = reg_to_bits(rd);
3090                let rn_bits = reg_to_bits(rn);
3091                let rm_bits = reg_to_bits(rm);
3092                reg_bits_checked(rd_bits)?;
3093                reg_bits_checked(rn_bits)?;
3094                reg_bits_checked(rm_bits)?;
3095
3096                // Thumb-2 UDIV: FBB0 F0F0 | Rn<<16 | Rd<<8 | Rm
3097                let hw1: u16 = (0xFBB0 | rn_bits) as u16;
3098                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
3099
3100                let mut bytes = hw1.to_le_bytes().to_vec();
3101                bytes.extend_from_slice(&hw2.to_le_bytes());
3102                encoding_contracts::verify_thumb32(&bytes);
3103                Ok(bytes)
3104            }
3105
3106            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
3107                let rdlo_bits = reg_to_bits(rdlo);
3108                let rdhi_bits = reg_to_bits(rdhi);
3109                let rn_bits = reg_to_bits(rn);
3110                let rm_bits = reg_to_bits(rm);
3111                reg_bits_checked(rdlo_bits)?;
3112                reg_bits_checked(rdhi_bits)?;
3113                reg_bits_checked(rn_bits)?;
3114                reg_bits_checked(rm_bits)?;
3115
3116                // Thumb-2 UMULL: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm
3117                let hw1: u16 = (0xFBA0 | rn_bits) as u16;
3118                let hw2: u16 = ((rdlo_bits << 12) | (rdhi_bits << 8) | rm_bits) as u16;
3119
3120                let mut bytes = hw1.to_le_bytes().to_vec();
3121                bytes.extend_from_slice(&hw2.to_le_bytes());
3122                encoding_contracts::verify_thumb32(&bytes);
3123                Ok(bytes)
3124            }
3125
3126            // MUL (Thumb-2 32-bit): MUL Rd, Rn, Rm
3127            ArmOp::Mul { rd, rn, rm } => {
3128                let rd_bits = reg_to_bits(rd);
3129                let rn_bits = reg_to_bits(rn);
3130                let rm_bits = reg_to_bits(rm);
3131
3132                // Thumb-2 MUL: FB00 F000 | Rn | Rd<<8 | Rm
3133                // 11111011 0000 Rn | 1111 Rd 0000 Rm
3134                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3135                let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
3136
3137                let mut bytes = hw1.to_le_bytes().to_vec();
3138                bytes.extend_from_slice(&hw2.to_le_bytes());
3139                Ok(bytes)
3140            }
3141
3142            // MLS: Rd = Ra - Rn * Rm
3143            ArmOp::Mls { rd, rn, rm, ra } => {
3144                let rd_bits = reg_to_bits(rd);
3145                let rn_bits = reg_to_bits(rn);
3146                let rm_bits = reg_to_bits(rm);
3147                let ra_bits = reg_to_bits(ra);
3148
3149                // Thumb-2 MLS: FB00 Rn | Ra Rd 0001 Rm
3150                // 11111011 0000 Rn | Ra Rd 0001 Rm
3151                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3152                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | 0x10 | rm_bits) as u16;
3153
3154                let mut bytes = hw1.to_le_bytes().to_vec();
3155                bytes.extend_from_slice(&hw2.to_le_bytes());
3156                Ok(bytes)
3157            }
3158
3159            ArmOp::Mla { rd, rn, rm, ra } => {
3160                let rd_bits = reg_to_bits(rd);
3161                let rn_bits = reg_to_bits(rn);
3162                let rm_bits = reg_to_bits(rm);
3163                let ra_bits = reg_to_bits(ra);
3164
3165                // Thumb-2 MLA: FB00 Rn | Ra Rd 0000 Rm — same as MLS without the
3166                // bit-4 (0x10) op flag. rd = ra + rn*rm.
3167                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3168                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | rm_bits) as u16;
3169
3170                let mut bytes = hw1.to_le_bytes().to_vec();
3171                bytes.extend_from_slice(&hw2.to_le_bytes());
3172                Ok(bytes)
3173            }
3174
3175            // AND (Thumb-2 32-bit)
3176            ArmOp::And { rd, rn, op2 } => {
3177                if let Operand2::Reg(rm) = op2 {
3178                    let rd_bits = reg_to_bits(rd);
3179                    let rn_bits = reg_to_bits(rn);
3180                    let rm_bits = reg_to_bits(rm);
3181
3182                    // Thumb-2 AND register: EA00 Rn | 0 Rd 00 00 Rm
3183                    let hw1: u16 = (0xEA00 | rn_bits) as u16;
3184                    let hw2: u16 = ((rd_bits << 8) | rm_bits) 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 if let Operand2::Imm(imm) = op2 {
3190                    let rd_bits = reg_to_bits(rd);
3191                    let rn_bits = reg_to_bits(rn);
3192
3193                    // Thumb-2 AND.W immediate T1: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8.
3194                    // The i:imm3:imm8 field is a ThumbExpandImm modified immediate —
3195                    // encode it correctly (or error on an un-encodable value)
3196                    // rather than packing raw bits, closing the silent-miscompile
3197                    // class for AND alongside ORR/EOR (#251) / ADD/SUB (#253) /
3198                    // CMP (#255).
3199                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3200                        synth_core::Error::synthesis(
3201                            "AND immediate is not a valid ThumbExpandImm — materialize into a register",
3202                        )
3203                    })?;
3204                    let i_bit = (field >> 11) & 1;
3205                    let imm3 = (field >> 8) & 0x7;
3206                    let imm8 = field & 0xFF;
3207
3208                    let hw1: u16 = (0xF000 | (i_bit << 10) | rn_bits) as u16;
3209                    let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3210
3211                    let mut bytes = hw1.to_le_bytes().to_vec();
3212                    bytes.extend_from_slice(&hw2.to_le_bytes());
3213                    Ok(bytes)
3214                } else {
3215                    // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
3216                    // operand form the arm does not encode (Reg and Imm only) -- #615's
3217                    // silent-miscompile class, on the DEFAULT target. Refuse instead.
3218                    Err(synth_core::Error::synthesis(format!(
3219                        "Thumb-2 AND with operand {op2:?} has no Thumb-2 encoding here (Reg and Imm \
3220                         are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
3221                    )))
3222                }
3223            }
3224
3225            // ORR (Thumb-2 32-bit)
3226            ArmOp::Orr { rd, rn, op2 } => {
3227                if let Operand2::Reg(rm) = op2 {
3228                    let rd_bits = reg_to_bits(rd);
3229                    let rn_bits = reg_to_bits(rn);
3230                    let rm_bits = reg_to_bits(rm);
3231
3232                    // Thumb-2 ORR: EA40 Rn | 0 Rd 00 00 Rm
3233                    let hw1: u16 = (0xEA40 | rn_bits) as u16;
3234                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3235
3236                    let mut bytes = hw1.to_le_bytes().to_vec();
3237                    bytes.extend_from_slice(&hw2.to_le_bytes());
3238                    Ok(bytes)
3239                } else if let Operand2::Imm(imm) = op2 {
3240                    // ORR.W immediate T1: 11110 i 0 0010 S Rn | 0 imm3 Rd imm8.
3241                    // Only the zero-extended byte form (imm <= 0xFF) is encoded;
3242                    // larger modified immediates need ThumbExpandImm — return an
3243                    // error rather than silently emit a NOP (Ok-or-Err, #180/#185).
3244                    let imm_val = *imm as u32;
3245                    if imm_val > 0xFF {
3246                        return Err(synth_core::Error::synthesis(
3247                            "ORR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3248                        ));
3249                    }
3250                    let rd_bits = reg_to_bits(rd);
3251                    let rn_bits = reg_to_bits(rn);
3252                    let hw1: u16 = (0xF040 | rn_bits) as u16;
3253                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3254                    let mut bytes = hw1.to_le_bytes().to_vec();
3255                    bytes.extend_from_slice(&hw2.to_le_bytes());
3256                    Ok(bytes)
3257                } else {
3258                    // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
3259                    // operand form the arm does not encode (Reg and Imm only) -- #615's
3260                    // silent-miscompile class, on the DEFAULT target. Refuse instead.
3261                    Err(synth_core::Error::synthesis(format!(
3262                        "Thumb-2 ORR with operand {op2:?} has no Thumb-2 encoding here (Reg and Imm \
3263                         are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
3264                    )))
3265                }
3266            }
3267
3268            // EOR (Thumb-2 32-bit)
3269            ArmOp::Eor { rd, rn, op2 } => {
3270                if let Operand2::Reg(rm) = op2 {
3271                    let rd_bits = reg_to_bits(rd);
3272                    let rn_bits = reg_to_bits(rn);
3273                    let rm_bits = reg_to_bits(rm);
3274
3275                    // Thumb-2 EOR: EA80 Rn | 0 Rd 00 00 Rm
3276                    let hw1: u16 = (0xEA80 | rn_bits) as u16;
3277                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3278
3279                    let mut bytes = hw1.to_le_bytes().to_vec();
3280                    bytes.extend_from_slice(&hw2.to_le_bytes());
3281                    Ok(bytes)
3282                } else if let Operand2::Imm(imm) = op2 {
3283                    // EOR.W immediate T1: 11110 i 0 0100 S Rn | 0 imm3 Rd imm8.
3284                    // Byte form only (imm <= 0xFF); larger needs ThumbExpandImm —
3285                    // error, not a silent NOP (Ok-or-Err, #180/#185).
3286                    let imm_val = *imm as u32;
3287                    if imm_val > 0xFF {
3288                        return Err(synth_core::Error::synthesis(
3289                            "EOR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3290                        ));
3291                    }
3292                    let rd_bits = reg_to_bits(rd);
3293                    let rn_bits = reg_to_bits(rn);
3294                    let hw1: u16 = (0xF080 | rn_bits) as u16;
3295                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3296                    let mut bytes = hw1.to_le_bytes().to_vec();
3297                    bytes.extend_from_slice(&hw2.to_le_bytes());
3298                    Ok(bytes)
3299                } else {
3300                    // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
3301                    // operand form the arm does not encode (Reg and Imm only) -- #615's
3302                    // silent-miscompile class, on the DEFAULT target. Refuse instead.
3303                    Err(synth_core::Error::synthesis(format!(
3304                        "Thumb-2 EOR with operand {op2:?} has no Thumb-2 encoding here (Reg and Imm \
3305                         are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
3306                    )))
3307                }
3308            }
3309
3310            // Shift operations (16-bit for low registers)
3311            ArmOp::Lsl { rd, rn, shift } => {
3312                let rd_bits = reg_to_bits(rd) as u16;
3313                let rn_bits = reg_to_bits(rn) as u16;
3314                let shift_bits = (*shift as u16) & 0x1F;
3315
3316                if rd_bits < 8 && rn_bits < 8 {
3317                    // LSLS Rd, Rm, #imm5 (16-bit): 0000 0 imm5 Rm Rd
3318                    let instr: u16 = (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3319                    Ok(instr.to_le_bytes().to_vec())
3320                } else {
3321                    // Use 32-bit encoding for high registers
3322                    self.encode_thumb32_shift(rd, rn, *shift, 0b00) // LSL type
3323                }
3324            }
3325
3326            ArmOp::Lsr { rd, rn, shift } => {
3327                let rd_bits = reg_to_bits(rd) as u16;
3328                let rn_bits = reg_to_bits(rn) as u16;
3329                let shift_bits = (*shift as u16) & 0x1F;
3330
3331                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3332                    // LSRS Rd, Rm, #imm5 (16-bit): 0000 1 imm5 Rm Rd
3333                    let instr: u16 = 0x0800 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3334                    Ok(instr.to_le_bytes().to_vec())
3335                } else {
3336                    self.encode_thumb32_shift(rd, rn, *shift, 0b01) // LSR type
3337                }
3338            }
3339
3340            ArmOp::Asr { rd, rn, shift } => {
3341                let rd_bits = reg_to_bits(rd) as u16;
3342                let rn_bits = reg_to_bits(rn) as u16;
3343                let shift_bits = (*shift as u16) & 0x1F;
3344
3345                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3346                    // ASRS Rd, Rm, #imm5 (16-bit): 0001 0 imm5 Rm Rd
3347                    let instr: u16 = 0x1000 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3348                    Ok(instr.to_le_bytes().to_vec())
3349                } else {
3350                    self.encode_thumb32_shift(rd, rn, *shift, 0b10) // ASR type
3351                }
3352            }
3353
3354            ArmOp::Ror { rd, rn, shift } => {
3355                // ROR doesn't have a 16-bit immediate form, use 32-bit
3356                self.encode_thumb32_shift(rd, rn, *shift, 0b11) // ROR type
3357            }
3358
3359            // Register-based shifts (Thumb-2 32-bit)
3360            // Encoding: 11111010 0xxS Rn 1111 Rd 0000 Rm
3361            // xx = shift type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
3362            ArmOp::LslReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b00),
3363            ArmOp::LsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b01),
3364            ArmOp::AsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b10),
3365            ArmOp::RorReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b11),
3366
3367            // RSB (Reverse Subtract): Rd = imm - Rn
3368            // Thumb-2 T2 encoding: 11110 i 0 1110 S Rn | 0 imm3 Rd imm8
3369            ArmOp::Rsb { rd, rn, imm } => {
3370                let rd_bits = reg_to_bits(rd);
3371                let rn_bits = reg_to_bits(rn);
3372
3373                // #681 class audit: the T2 `i:imm3:imm8` field is a
3374                // ThumbExpandImm modified immediate and RSB has NO plain-imm12
3375                // (T4-style) form — packing a raw value > 0xFF silently encodes
3376                // a different constant (#253/#255 class). All current emitters
3377                // use imm 32 (shift complement), which expands to itself, so
3378                // this gate is byte-identical for existing codegen.
3379                let field = try_thumb_expand_imm(*imm).ok_or_else(|| {
3380                    synth_core::Error::synthesis(
3381                        "RSB immediate is not a valid ThumbExpandImm — materialize into a register",
3382                    )
3383                })?;
3384                let i_bit = (field >> 11) & 1;
3385                let imm3 = (field >> 8) & 0x7;
3386                let imm8 = field & 0xFF;
3387
3388                // hw1: 11110 i 01110 0 Rn  (S=0)
3389                let hw1: u16 = (0xF1C0 | (i_bit << 10) | rn_bits) as u16;
3390                // hw2: 0 imm3 Rd imm8
3391                let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3392
3393                let mut bytes = hw1.to_le_bytes().to_vec();
3394                bytes.extend_from_slice(&hw2.to_le_bytes());
3395                Ok(bytes)
3396            }
3397
3398            // CLZ (Thumb-2 32-bit)
3399            ArmOp::Clz { rd, rm } => {
3400                let rd_bits = reg_to_bits(rd);
3401                let rm_bits = reg_to_bits(rm);
3402
3403                // Thumb-2 CLZ: FAB0 Rm | F8 Rd Rm
3404                // 11111010 1011 Rm | 1111 1000 Rd Rm
3405                let hw1: u16 = (0xFAB0 | rm_bits) as u16;
3406                let hw2: u16 = (0xF080 | (rd_bits << 8) | rm_bits) as u16;
3407
3408                let mut bytes = hw1.to_le_bytes().to_vec();
3409                bytes.extend_from_slice(&hw2.to_le_bytes());
3410                Ok(bytes)
3411            }
3412
3413            // RBIT (Thumb-2 32-bit)
3414            ArmOp::Rbit { rd, rm } => {
3415                let rd_bits = reg_to_bits(rd);
3416                let rm_bits = reg_to_bits(rm);
3417
3418                // Thumb-2 RBIT: FA90 Rm | F0 Rd A0 Rm
3419                // 11111010 1001 Rm | 1111 Rd 1010 Rm
3420                let hw1: u16 = (0xFA90 | rm_bits) as u16;
3421                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rm_bits) as u16;
3422
3423                let mut bytes = hw1.to_le_bytes().to_vec();
3424                bytes.extend_from_slice(&hw2.to_le_bytes());
3425                Ok(bytes)
3426            }
3427
3428            // SXTB (16-bit for low registers)
3429            ArmOp::Sxtb { rd, rm } => {
3430                let rd_bits = reg_to_bits(rd) as u16;
3431                let rm_bits = reg_to_bits(rm) as u16;
3432
3433                if rd_bits < 8 && rm_bits < 8 {
3434                    // SXTB Rd, Rm (16-bit): 1011 0010 01 Rm Rd
3435                    let instr: u16 = 0xB240 | (rm_bits << 3) | rd_bits;
3436                    Ok(instr.to_le_bytes().to_vec())
3437                } else {
3438                    // Thumb-2 SXTB.W: FA4F F(rd)80 (rm)
3439                    // 11111010 0100 1111 | 1111 Rd 10 rotate Rm
3440                    let rd_bits32 = rd_bits as u32;
3441                    let rm_bits32 = rm_bits as u32;
3442                    let hw1: u16 = 0xFA4F;
3443                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3444                    let mut bytes = hw1.to_le_bytes().to_vec();
3445                    bytes.extend_from_slice(&hw2.to_le_bytes());
3446                    Ok(bytes)
3447                }
3448            }
3449
3450            // SXTH (16-bit for low registers)
3451            ArmOp::Sxth { rd, rm } => {
3452                let rd_bits = reg_to_bits(rd) as u16;
3453                let rm_bits = reg_to_bits(rm) as u16;
3454
3455                if rd_bits < 8 && rm_bits < 8 {
3456                    // SXTH Rd, Rm (16-bit): 1011 0010 00 Rm Rd
3457                    let instr: u16 = 0xB200 | (rm_bits << 3) | rd_bits;
3458                    Ok(instr.to_le_bytes().to_vec())
3459                } else {
3460                    // Thumb-2 SXTH.W: FA0F F(rd)80 (rm)
3461                    // 11111010 0000 1111 | 1111 Rd 10 rotate Rm
3462                    let rd_bits32 = rd_bits as u32;
3463                    let rm_bits32 = rm_bits as u32;
3464                    let hw1: u16 = 0xFA0F;
3465                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3466                    let mut bytes = hw1.to_le_bytes().to_vec();
3467                    bytes.extend_from_slice(&hw2.to_le_bytes());
3468                    Ok(bytes)
3469                }
3470            }
3471
3472            // UXTB Rd,Rm — zero-extend byte (rd = rm & 0xff)
3473            ArmOp::Uxtb { rd, rm } => {
3474                let rd_bits = reg_to_bits(rd) as u16;
3475                let rm_bits = reg_to_bits(rm) as u16;
3476                if rd_bits < 8 && rm_bits < 8 {
3477                    // UXTB Rd, Rm (16-bit): 1011 0010 11 Rm Rd
3478                    let instr: u16 = 0xB2C0 | (rm_bits << 3) | rd_bits;
3479                    Ok(instr.to_le_bytes().to_vec())
3480                } else {
3481                    // Thumb-2 UXTB.W: FA5F F(rd)80 (rm)
3482                    let hw1: u16 = 0xFA5F;
3483                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3484                    let mut bytes = hw1.to_le_bytes().to_vec();
3485                    bytes.extend_from_slice(&hw2.to_le_bytes());
3486                    Ok(bytes)
3487                }
3488            }
3489
3490            // UXTH Rd,Rm — zero-extend halfword (rd = rm & 0xffff)
3491            ArmOp::Uxth { rd, rm } => {
3492                let rd_bits = reg_to_bits(rd) as u16;
3493                let rm_bits = reg_to_bits(rm) as u16;
3494                if rd_bits < 8 && rm_bits < 8 {
3495                    // UXTH Rd, Rm (16-bit): 1011 0010 10 Rm Rd
3496                    let instr: u16 = 0xB280 | (rm_bits << 3) | rd_bits;
3497                    Ok(instr.to_le_bytes().to_vec())
3498                } else {
3499                    // Thumb-2 UXTH.W: FA1F F(rd)80 (rm)
3500                    let hw1: u16 = 0xFA1F;
3501                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3502                    let mut bytes = hw1.to_le_bytes().to_vec();
3503                    bytes.extend_from_slice(&hw2.to_le_bytes());
3504                    Ok(bytes)
3505                }
3506            }
3507
3508            // CMP (can be 16-bit for low registers)
3509            ArmOp::Cmp { rn, op2 } => {
3510                let rn_bits = reg_to_bits(rn) as u16;
3511
3512                if let Operand2::Imm(imm) = op2 {
3513                    // Only use 16-bit encoding for non-negative immediates 0-255
3514                    // Negative immediates must use 32-bit encoding
3515                    if *imm >= 0 && *imm <= 255 && rn_bits < 8 {
3516                        // CMP Rn, #imm8 (16-bit): 0010 1 Rn imm8
3517                        let instr: u16 = 0x2800 | (rn_bits << 8) | (*imm as u16 & 0xFF);
3518                        Ok(instr.to_le_bytes().to_vec())
3519                    } else {
3520                        self.encode_thumb32_cmp_imm(rn, *imm as u32)
3521                    }
3522                } else if let Operand2::Reg(rm) = op2 {
3523                    let rm_bits = reg_to_bits(rm) as u16;
3524                    if rn_bits < 8 && rm_bits < 8 {
3525                        // CMP Rn, Rm (16-bit low): 0100 0010 10 Rm Rn
3526                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
3527                        Ok(instr.to_le_bytes().to_vec())
3528                    } else {
3529                        // CMP Rn, Rm (16-bit high): 0100 0101 N Rm Rn[2:0]
3530                        let n_bit = (rn_bits >> 3) & 1;
3531                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
3532                        Ok(instr.to_le_bytes().to_vec())
3533                    }
3534                } else {
3535                    // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
3536                    // operand form the arm does not encode (Imm and Reg only) -- #615's
3537                    // silent-miscompile class, on the DEFAULT target. Refuse instead.
3538                    Err(synth_core::Error::synthesis(format!(
3539                        "Thumb-2 CMP with operand {op2:?} has no Thumb-2 encoding here (Imm and Reg \
3540                         are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
3541                    )))
3542                }
3543            }
3544
3545            // CMN (Compare Negative) - computes Rn + op2 and sets flags
3546            // CMN Rn, #1 sets Z flag if Rn == -1 (since -1 + 1 = 0)
3547            ArmOp::Cmn { rn, op2 } => {
3548                let rn_bits = reg_to_bits(rn) as u16;
3549
3550                if let Operand2::Imm(imm) = op2 {
3551                    // CMN.W Rn, #imm (32-bit): i:imm3:imm8 is a ThumbExpandImm
3552                    // modified immediate (the field sits in imm3=hw2[14:12],
3553                    // imm8=hw2[7:0], i=hw1[10]). Encode it correctly, or error on
3554                    // an un-encodable value — replacing the old silent `0xBF00`
3555                    // NOP (the last of the silent-miscompile data-proc encoders).
3556                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3557                        synth_core::Error::synthesis(
3558                            "CMN immediate is not a valid ThumbExpandImm — materialize into a register",
3559                        )
3560                    })?;
3561                    let i_bit = (field >> 11) & 1;
3562                    let imm3 = (field >> 8) & 0x7;
3563                    let imm8 = field & 0xFF;
3564                    let hw1: u16 = (0xF110 | (i_bit << 10) as u16) | rn_bits;
3565                    let hw2: u16 = (imm3 << 12) as u16 | 0x0F00 | imm8 as u16;
3566                    let mut bytes = hw1.to_le_bytes().to_vec();
3567                    bytes.extend_from_slice(&hw2.to_le_bytes());
3568                    Ok(bytes)
3569                } else if let Operand2::Reg(rm) = op2 {
3570                    let rm_bits = reg_to_bits(rm) as u16;
3571                    // 16-bit CMN (T1) only encodes R0-R7; high registers overflow
3572                    // the 3-bit fields and corrupt the operands (#184, the #180
3573                    // class). CMN has no high-register 16-bit form, so fall back
3574                    // to 32-bit CMN.W (T2): EB10 Rn | 0F00 Rm (ADD.W with S=1 and
3575                    // Rd discarded as PC/1111).
3576                    if rn_bits < 8 && rm_bits < 8 {
3577                        // CMN Rn, Rm (16-bit): 0100 0010 11 Rm Rn
3578                        let instr: u16 = 0x42C0 | (rm_bits << 3) | rn_bits;
3579                        Ok(instr.to_le_bytes().to_vec())
3580                    } else {
3581                        let hw1: u16 = 0xEB10 | rn_bits;
3582                        let hw2: u16 = 0x0F00 | rm_bits;
3583                        let mut bytes = hw1.to_le_bytes().to_vec();
3584                        bytes.extend_from_slice(&hw2.to_le_bytes());
3585                        Ok(bytes)
3586                    }
3587                } else {
3588                    Ok(vec![0xBF, 0x00])
3589                }
3590            }
3591
3592            // LDR (can be 16-bit for simple cases)
3593            ArmOp::Ldr { rd, addr } => {
3594                let rd_bits = reg_to_bits(rd);
3595                let base_bits = reg_to_bits(&addr.base);
3596
3597                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3598                if let Some(offset_reg) = &addr.offset_reg {
3599                    let rm_bits = reg_to_bits(offset_reg);
3600
3601                    // If there's also an immediate offset, we need to ADD it first
3602                    if addr.offset != 0 {
3603                        // Use R12 (IP) as scratch to avoid clobbering the address register
3604                        // ADD R12, Rm, #offset; LDR Rd, [base, R12]
3605                        let scratch = Reg::R12;
3606                        let mut bytes =
3607                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3608                        bytes.extend(self.encode_thumb32_ldr_reg(rd, &addr.base, &scratch)?);
3609                        return Ok(bytes);
3610                    }
3611
3612                    // Simple register offset: LDR Rd, [Rn, Rm]
3613                    // 16-bit: only if Rd, Rn, Rm < R8
3614                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3615                        // LDR Rd, [Rn, Rm] (16-bit): 0101 100 Rm Rn Rd
3616                        let instr: u16 = 0x5800
3617                            | ((rm_bits as u16) << 6)
3618                            | ((base_bits as u16) << 3)
3619                            | (rd_bits as u16);
3620                        return Ok(instr.to_le_bytes().to_vec());
3621                    }
3622
3623                    // 32-bit register offset
3624                    return self.encode_thumb32_ldr_reg(rd, &addr.base, offset_reg);
3625                }
3626
3627                // Immediate offset mode [base, #imm]
3628                let offset = addr.offset as u32;
3629
3630                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3631                    // LDR Rd, [Rn, #imm5*4] (16-bit): 0110 1 imm5 Rn Rd
3632                    let imm5 = (offset >> 2) as u16;
3633                    let instr: u16 =
3634                        0x6800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3635                    Ok(instr.to_le_bytes().to_vec())
3636                } else {
3637                    self.encode_thumb32_ldr(rd, &addr.base, offset)
3638                }
3639            }
3640
3641            // STR (can be 16-bit for simple cases)
3642            ArmOp::Str { rd, addr } => {
3643                let rd_bits = reg_to_bits(rd);
3644                let base_bits = reg_to_bits(&addr.base);
3645
3646                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3647                if let Some(offset_reg) = &addr.offset_reg {
3648                    let rm_bits = reg_to_bits(offset_reg);
3649
3650                    // If there's also an immediate offset, we need to ADD it first
3651                    if addr.offset != 0 {
3652                        // Use R12 (IP) as scratch to avoid clobbering the address register
3653                        // ADD R12, Rm, #offset; STR Rd, [base, R12]
3654                        let scratch = Reg::R12;
3655                        let mut bytes =
3656                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3657                        bytes.extend(self.encode_thumb32_str_reg(rd, &addr.base, &scratch)?);
3658                        return Ok(bytes);
3659                    }
3660
3661                    // Simple register offset: STR Rd, [Rn, Rm]
3662                    // 16-bit: only if Rd, Rn, Rm < R8
3663                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3664                        // STR Rd, [Rn, Rm] (16-bit): 0101 000 Rm Rn Rd
3665                        let instr: u16 = 0x5000
3666                            | ((rm_bits as u16) << 6)
3667                            | ((base_bits as u16) << 3)
3668                            | (rd_bits as u16);
3669                        return Ok(instr.to_le_bytes().to_vec());
3670                    }
3671
3672                    // 32-bit register offset
3673                    return self.encode_thumb32_str_reg(rd, &addr.base, offset_reg);
3674                }
3675
3676                // Immediate offset mode [base, #imm]
3677                let offset = addr.offset as u32;
3678
3679                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3680                    // STR Rd, [Rn, #imm5*4] (16-bit): 0110 0 imm5 Rn Rd
3681                    let imm5 = (offset >> 2) as u16;
3682                    let instr: u16 =
3683                        0x6000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3684                    Ok(instr.to_le_bytes().to_vec())
3685                } else {
3686                    self.encode_thumb32_str(rd, &addr.base, offset)
3687                }
3688            }
3689
3690            // LDRB (Thumb-2)
3691            ArmOp::Ldrb { rd, addr } => {
3692                let rd_bits = reg_to_bits(rd);
3693                let base_bits = reg_to_bits(&addr.base);
3694
3695                if let Some(offset_reg) = &addr.offset_reg {
3696                    if addr.offset != 0 {
3697                        let scratch = Reg::R12;
3698                        let mut bytes =
3699                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3700                        bytes.extend(self.encode_thumb32_ldrb_reg(rd, &addr.base, &scratch)?);
3701                        return Ok(bytes);
3702                    }
3703                    return self.encode_thumb32_ldrb_reg(rd, &addr.base, offset_reg);
3704                }
3705
3706                let offset = addr.offset as u32;
3707                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3708                    // LDRB Rd, [Rn, #imm5] (16-bit): 0111 1 imm5 Rn Rd
3709                    let instr: u16 = 0x7800
3710                        | ((offset as u16) << 6)
3711                        | ((base_bits as u16) << 3)
3712                        | (rd_bits as u16);
3713                    Ok(instr.to_le_bytes().to_vec())
3714                } else {
3715                    self.encode_thumb32_ldrb_imm(rd, &addr.base, offset)
3716                }
3717            }
3718
3719            // LDRSB (Thumb-2)
3720            ArmOp::Ldrsb { rd, addr } => {
3721                let rd_bits = reg_to_bits(rd);
3722                let base_bits = reg_to_bits(&addr.base);
3723
3724                if let Some(offset_reg) = &addr.offset_reg {
3725                    if addr.offset != 0 {
3726                        let scratch = Reg::R12;
3727                        let mut bytes =
3728                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3729                        bytes.extend(self.encode_thumb32_ldrsb_reg(rd, &addr.base, &scratch)?);
3730                        return Ok(bytes);
3731                    }
3732                    return self.encode_thumb32_ldrsb_reg(rd, &addr.base, offset_reg);
3733                }
3734
3735                let offset = addr.offset as u32;
3736                // LDRSB has no 16-bit immediate form (only register)
3737                // For 16-bit reg form: only if Rd, Rn, Rm < R8
3738                if rd_bits < 8 && base_bits < 8 && offset == 0 {
3739                    // No immediate 16-bit encoding for LDRSB; use 32-bit
3740                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3741                } else {
3742                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3743                }
3744            }
3745
3746            // LDRH (Thumb-2)
3747            ArmOp::Ldrh { rd, addr } => {
3748                let rd_bits = reg_to_bits(rd);
3749                let base_bits = reg_to_bits(&addr.base);
3750
3751                if let Some(offset_reg) = &addr.offset_reg {
3752                    if addr.offset != 0 {
3753                        let scratch = Reg::R12;
3754                        let mut bytes =
3755                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3756                        bytes.extend(self.encode_thumb32_ldrh_reg(rd, &addr.base, &scratch)?);
3757                        return Ok(bytes);
3758                    }
3759                    return self.encode_thumb32_ldrh_reg(rd, &addr.base, offset_reg);
3760                }
3761
3762                let offset = addr.offset as u32;
3763                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3764                    // LDRH Rd, [Rn, #imm5*2] (16-bit): 1000 1 imm5 Rn Rd
3765                    let imm5 = (offset >> 1) as u16;
3766                    let instr: u16 =
3767                        0x8800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3768                    Ok(instr.to_le_bytes().to_vec())
3769                } else {
3770                    self.encode_thumb32_ldrh_imm(rd, &addr.base, offset)
3771                }
3772            }
3773
3774            // LDRSH (Thumb-2)
3775            ArmOp::Ldrsh { rd, addr } => {
3776                if let Some(offset_reg) = &addr.offset_reg {
3777                    if addr.offset != 0 {
3778                        let scratch = Reg::R12;
3779                        let mut bytes =
3780                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3781                        bytes.extend(self.encode_thumb32_ldrsh_reg(rd, &addr.base, &scratch)?);
3782                        return Ok(bytes);
3783                    }
3784                    return self.encode_thumb32_ldrsh_reg(rd, &addr.base, offset_reg);
3785                }
3786
3787                let offset = addr.offset as u32;
3788                self.encode_thumb32_ldrsh_imm(rd, &addr.base, offset)
3789            }
3790
3791            // STRB (Thumb-2)
3792            ArmOp::Strb { rd, addr } => {
3793                let rd_bits = reg_to_bits(rd);
3794                let base_bits = reg_to_bits(&addr.base);
3795
3796                if let Some(offset_reg) = &addr.offset_reg {
3797                    if addr.offset != 0 {
3798                        let scratch = Reg::R12;
3799                        let mut bytes =
3800                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3801                        bytes.extend(self.encode_thumb32_strb_reg(rd, &addr.base, &scratch)?);
3802                        return Ok(bytes);
3803                    }
3804                    return self.encode_thumb32_strb_reg(rd, &addr.base, offset_reg);
3805                }
3806
3807                let offset = addr.offset as u32;
3808                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3809                    // STRB Rd, [Rn, #imm5] (16-bit): 0111 0 imm5 Rn Rd
3810                    let instr: u16 = 0x7000
3811                        | ((offset as u16) << 6)
3812                        | ((base_bits as u16) << 3)
3813                        | (rd_bits as u16);
3814                    Ok(instr.to_le_bytes().to_vec())
3815                } else {
3816                    self.encode_thumb32_strb_imm(rd, &addr.base, offset)
3817                }
3818            }
3819
3820            // STRH (Thumb-2)
3821            ArmOp::Strh { rd, addr } => {
3822                let rd_bits = reg_to_bits(rd);
3823                let base_bits = reg_to_bits(&addr.base);
3824
3825                if let Some(offset_reg) = &addr.offset_reg {
3826                    if addr.offset != 0 {
3827                        let scratch = Reg::R12;
3828                        let mut bytes =
3829                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3830                        bytes.extend(self.encode_thumb32_strh_reg(rd, &addr.base, &scratch)?);
3831                        return Ok(bytes);
3832                    }
3833                    return self.encode_thumb32_strh_reg(rd, &addr.base, offset_reg);
3834                }
3835
3836                let offset = addr.offset as u32;
3837                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3838                    // STRH Rd, [Rn, #imm5*2] (16-bit): 1000 0 imm5 Rn Rd
3839                    let imm5 = (offset >> 1) as u16;
3840                    let instr: u16 =
3841                        0x8000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3842                    Ok(instr.to_le_bytes().to_vec())
3843                } else {
3844                    self.encode_thumb32_strh_imm(rd, &addr.base, offset)
3845                }
3846            }
3847
3848            // MemorySize (Thumb-2)
3849            ArmOp::MemorySize { rd, page_log2 } => {
3850                // R10 holds the size in BYTES; `memory.size` answers in DECLARED
3851                // pages, so shift right by log2(page size) (#1441). 16 for the
3852                // default 64 KiB page, 0 for `(pagesize 1)`.
3853                let rd_bits = reg_to_bits(rd);
3854                let r10_bits = reg_to_bits(&Reg::R10);
3855                if *page_log2 >= 32 {
3856                    return Err(synth_core::Error::synthesis(format!(
3857                        "#1441 internal: memory.size page_log2 = {page_log2} is \
3858                         not encodable as a Thumb-2 shift (0..=31) — refusing \
3859                         rather than emitting a shift of 32"
3860                    )));
3861                }
3862                // SHIFT 0 IS NOT AN LSR. In the 16-bit form `0000 1 imm5 Rm Rd`
3863                // an `imm5` of 0 is not "LSR #0" at all, and in the 32-bit form
3864                // `type=01` with a zero immediate means SHIFT BY 32. So
3865                // `(pagesize 1)` must encode as a MOV. Emitting the shift
3866                // unconditionally would have returned 0 for every
3867                // `memory.size` on a byte-paged memory — a silent wrong answer
3868                // produced by the fix itself.
3869                if rd_bits < 8 && r10_bits < 8 && *page_log2 != 0 {
3870                    // 16-bit LSRS Rd, Rm, #imm5 (unreachable for R10, which is
3871                    // a high register — kept because the guard, not the
3872                    // register allocation, is what makes it safe).
3873                    let instr: u16 = 0x0800
3874                        | ((*page_log2 as u16) << 6)
3875                        | ((r10_bits as u16) << 3)
3876                        | (rd_bits as u16);
3877                    Ok(instr.to_le_bytes().to_vec())
3878                } else {
3879                    // Thumb-2 32-bit: 1110 1010 010 0 1111 | 0 imm3 Rd imm2 tt Rm
3880                    // `tt` = 01 for LSR, 00 for LSL. LSL #0 is the canonical
3881                    // MOV.W Rd, Rm.
3882                    let imm5: u32 = *page_log2;
3883                    let imm3 = (imm5 >> 2) & 0x7;
3884                    let imm2 = imm5 & 0x3;
3885                    let shift_type: u32 = if imm5 == 0 { 0x00 } else { 0x10 };
3886                    let hw1: u16 = 0xEA4F;
3887                    let hw2: u16 =
3888                        ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | shift_type | r10_bits)
3889                            as u16;
3890                    let mut bytes = hw1.to_le_bytes().to_vec();
3891                    bytes.extend_from_slice(&hw2.to_le_bytes());
3892                    Ok(bytes)
3893                }
3894            }
3895
3896            // MemoryGrow (Thumb-2)
3897            ArmOp::MemoryGrow { rd, .. } => {
3898                // On embedded with fixed memory, always return -1 (failure)
3899                // MVN rd, #0 → MOV rd, #-1
3900                // Thumb-2 32-bit: MVN: 1111 0 i 0 0 0 1 1 0 1111 | 0 imm3 Rd imm8
3901                let rd_bits = reg_to_bits(rd);
3902                let hw1: u16 = 0xF06F; // MVN with i=0
3903                let hw2: u16 = (rd_bits << 8) as u16; // imm8=0 → ~0 = 0xFFFFFFFF = -1
3904                let mut bytes = hw1.to_le_bytes().to_vec();
3905                bytes.extend_from_slice(&hw2.to_le_bytes());
3906                Ok(bytes)
3907            }
3908
3909            // BX (16-bit)
3910            ArmOp::Bx { rm } => {
3911                let rm_bits = reg_to_bits(rm) as u16;
3912                // BX Rm (16-bit): 0100 0111 0 Rm 000
3913                let instr: u16 = 0x4700 | (rm_bits << 3);
3914                Ok(instr.to_le_bytes().to_vec())
3915            }
3916
3917            // BLX (16-bit) - Branch with Link and Exchange
3918            // BLX Rm: 0100 0111 1 Rm 000
3919            ArmOp::Blx { rm } => {
3920                let rm_bits = reg_to_bits(rm) as u16;
3921                let instr: u16 = 0x4780 | (rm_bits << 3);
3922                Ok(instr.to_le_bytes().to_vec())
3923            }
3924
3925            // CallIndirect - indirect function call via table lookup
3926            // table_index_reg contains the table index
3927            // Generates (#642): MOVW ip,#size [; MOVT]; CMP idx,ip; BLO +1;
3928            //                   UDF #0; LSL R12,idx,#2; LDR R12,[R11,R12]; BLX R12
3929            // #650, table_byte_offset != 0 (a non-zero table in the contiguous
3930            // R11 region): the pointer load becomes
3931            //                   ADD R12,R11,R12; LDR R12,[R12,#offset]
3932            // #664, null_check (the table has null slots, linked as ZERO
3933            // words): the loaded pointer is null-checked before the BLX —
3934            //                   CMP.W R12,#0; BNE +1; UDF #0
3935            // #676, type_check (heterogeneous table — runtime §4.4.8 type
3936            // check against the type-id sidecar at R11+off): after the
3937            // bounds guard —
3938            //                   LSL R12,idx,#2; ADD R12,R11,R12;
3939            //                   LDR R12,[R12,#type_off]; CMP.W R12,#id;
3940            //                   BEQ +1; UDF #0
3941            // (the dispatch tail then recomputes idx*4 — idx stays live).
3942            ArmOp::CallIndirect {
3943                rd: _,
3944                type_idx: _,
3945                table_index_reg,
3946                table_size,
3947                table_byte_offset,
3948                null_check,
3949                type_check,
3950            } => {
3951                let idx_reg = reg_to_bits(table_index_reg);
3952                let mut bytes = Vec::new();
3953
3954                // The expansion:
3955                // 1. Bounds guard (#642): trap (UDF #0, WASM Core §4.4.8) when
3956                //    index >= table size. Without it an out-of-bounds index
3957                //    reads past the table and BLXes whatever word lies there —
3958                //    an uncontrolled indirect branch instead of a trap.
3959                // 2. Multiplies index by 4 (function pointer size)
3960                // 3. Loads function pointer from table (table base in R11)
3961                // 4. Calls the function via BLX
3962                //
3963                // Table base setup must be done by caller/runtime. The type
3964                // check §4.4.8 also requires is discharged at COMPILE time:
3965                // the selector only emits this op after verifying the closed-
3966                // world property that every table entry's signature equals the
3967                // expected type (the raw code-pointer table carries no runtime
3968                // type ids to compare) — see the #642 selector guard.
3969
3970                // MOVW R12, #(size & 0xFFFF) — Thumb-2 T3:
3971                // 11110 i 100100 imm4 | 0 imm3 Rd imm8 (Rd=R12).
3972                let size_lo = *table_size & 0xFFFF;
3973                let hw1: u16 =
3974                    (0xF240 | (((size_lo >> 11) & 1) << 10) | ((size_lo >> 12) & 0xF)) as u16;
3975                let hw2: u16 =
3976                    ((((size_lo >> 8) & 0x7) << 12) | (12 << 8) | (size_lo & 0xFF)) as u16;
3977                bytes.extend_from_slice(&hw1.to_le_bytes());
3978                bytes.extend_from_slice(&hw2.to_le_bytes());
3979                // MOVT R12, #(size >> 16) — only when the table size exceeds
3980                // 16 bits (never in practice, but the guard must not compare
3981                // against a truncated size).
3982                let size_hi = *table_size >> 16;
3983                if size_hi != 0 {
3984                    let hw1: u16 =
3985                        (0xF2C0 | (((size_hi >> 11) & 1) << 10) | ((size_hi >> 12) & 0xF)) as u16;
3986                    let hw2: u16 =
3987                        ((((size_hi >> 8) & 0x7) << 12) | (12 << 8) | (size_hi & 0xFF)) as u16;
3988                    bytes.extend_from_slice(&hw1.to_le_bytes());
3989                    bytes.extend_from_slice(&hw2.to_le_bytes());
3990                }
3991                // CMP idx, R12 — 16-bit T2 (high-register capable):
3992                // 010001 01 N Rm(4) Rn(3), Rn full = N:Rn3.
3993                let cmp: u16 = (0x4500 | ((idx_reg & 8) << 4) | (12 << 3) | (idx_reg & 7)) as u16;
3994                bytes.extend_from_slice(&cmp.to_le_bytes());
3995                // BLO +1 insn (skip the UDF when index < size) — B<cond>.N
3996                // imm8=0: target = branch + 4. LO = unsigned lower.
3997                bytes.extend_from_slice(&0xD300u16.to_le_bytes());
3998                // UDF #0 — call_indirect out-of-bounds trap (same trap idiom as
3999                // the div-by-zero guards).
4000                bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
4001
4002                // #676: runtime type check — ONLY for a heterogeneous table
4003                // (mixed signatures, closed-world verdict impossible). Load
4004                // the indexed slot's structural class id from the type-id
4005                // sidecar (`R11 + type_off + idx*4`; `type_off` = sidecar
4006                // base + this table's base offset, a compile-time constant)
4007                // and compare it against the expected type's class id — a
4008                // mismatch is the WASM Core §4.4.8 type trap. Null slots
4009                // carry the reserved id 0, so this compare subsumes the
4010                // #664 null trap (the selector passes `null_check: false`).
4011                // `None` emits NOTHING: every homogeneous table keeps the
4012                // pre-#676 bytes identical BY CONSTRUCTION. R12 stays the
4013                // only scratch (#212); the dispatch tail below recomputes
4014                // idx*4 — the index register is never clobbered here.
4015                if let Some((expected_id, type_off)) = type_check {
4016                    // RQ-61-IMMRANGE (#1072): compiled out in release, where
4017                    // the masks below silently TRUNCATE (id 256 compares as
4018                    // 0 — a NULL slot would pass the §4.4.8 check). The
4019                    // enforcement claim is DEMONSTRATED: the sole `Some`
4020                    // producer, `resolve_runtime_type_check`, loud-declines
4021                    // id > 255 / offset > 4095, tripped by
4022                    // `test_676_call_indirect_runtime_check_range_declines`
4023                    // (mutation-checked — see the A32 twin above).
4024                    debug_assert!(*expected_id <= 255, "selector enforces the CMP imm8 range");
4025                    debug_assert!(*type_off <= 4095, "selector enforces the LDR imm12 range");
4026                    // MOV.W R12, idx, LSL #2 (same encoding as the dispatch
4027                    // tail's index scale below).
4028                    bytes.extend_from_slice(&0xEA4Fu16.to_le_bytes());
4029                    bytes.extend_from_slice(
4030                        &(((0x0C00 | (0b10 << 6)) | idx_reg) as u16).to_le_bytes(),
4031                    );
4032                    // ADD.W R12, R11, R12 (the #650 base-add form).
4033                    bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
4034                    bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
4035                    // LDR.W R12, [R12, #type_off] — T3 LDR (immediate):
4036                    // 1111 1000 1101 Rn=1100 | Rt=1100 imm12.
4037                    bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
4038                    bytes.extend_from_slice(
4039                        &(0xC000u16 | (*type_off as u16 & 0x0FFF)).to_le_bytes(),
4040                    );
4041                    // CMP.W R12, #expected_id — T2 CMP (immediate), imm8
4042                    // (same form as the #664 null check's CMP.W R12, #0).
4043                    bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
4044                    bytes.extend_from_slice(
4045                        &(0x0F00u16 | (*expected_id as u16 & 0xFF)).to_le_bytes(),
4046                    );
4047                    // BEQ +1 insn (skip the UDF when the class id matches) —
4048                    // B<cond>.N imm8=0: target = branch + 4. EQ.
4049                    bytes.extend_from_slice(&0xD000u16.to_le_bytes());
4050                    // UDF #0 — the §4.4.8 type-mismatch trap (same idiom as
4051                    // the bounds guard above).
4052                    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
4053                }
4054
4055                // LSL R12, idx_reg, #2 (multiply index by 4)
4056                // Thumb-2 MOV with shift: 11101010 010 S 1111 | 0 imm3 Rd imm2 type Rm
4057                // LSL: type=00 (bits 5:4), imm5=2 -> imm3=000, imm2=10 (bits 7:6)
4058                // #597: the shift amount was previously shifted into bits 5:4 —
4059                // the TYPE field — encoding `mov.w ip, rm, ASR #32`, which
4060                // destroyed the index and dispatched table entry 0 for every
4061                // call. imm2 lives at bits 7:6.
4062                let hw1: u16 = 0xEA4F_u16; // MOV.W R12, Rm, LSL #2
4063                let hw2: u16 = ((0x0C00 | (0b10 << 6)) | idx_reg) as u16;
4064                bytes.extend_from_slice(&hw1.to_le_bytes());
4065                bytes.extend_from_slice(&hw2.to_le_bytes());
4066
4067                if *table_byte_offset == 0 {
4068                    // Table 0 (base = R11 itself): the pre-#650 single-load
4069                    // form — a single-table module's bytes stay identical BY
4070                    // CONSTRUCTION.
4071                    // LDR R12, [R11, R12] - load function pointer
4072                    // Thumb-2 LDR (register): 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
4073                    // Rn=R11, Rt=R12, Rm=R12, imm2=00 (no shift)
4074                    let ldr_hw1: u16 = 0xF85B; // LDR.W Rt, [R11, Rm]
4075                    let ldr_hw2: u16 = 0xC00C; // Rt=R12, imm2=00, Rm=R12
4076                    bytes.extend_from_slice(&ldr_hw1.to_le_bytes());
4077                    bytes.extend_from_slice(&ldr_hw2.to_le_bytes());
4078                } else {
4079                    // #650: table N of the contiguous R11 region — fold the
4080                    // compile-time base offset into the pointer load via the
4081                    // LDR imm12 form (R12 stays the only scratch, per the
4082                    // #212 convention).
4083                    assert!(
4084                        *table_byte_offset <= 4095,
4085                        "call_indirect table base offset {table_byte_offset} exceeds \
4086                         LDR imm12 — the selector must have declined this (#650)"
4087                    );
4088                    // ADD.W R12, R11, R12 — T3 ADD (register):
4089                    // 11101011000 S=0 Rn=1011 | 0 imm3=000 Rd=1100 imm2=00 type=00 Rm=1100
4090                    bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
4091                    bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
4092                    // LDR.W R12, [R12, #offset] — T3 LDR (immediate):
4093                    // 1111 1000 1101 Rn=1100 | Rt=1100 imm12
4094                    bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
4095                    bytes.extend_from_slice(
4096                        &((0xC000u16) | (*table_byte_offset as u16 & 0x0FFF)).to_le_bytes(),
4097                    );
4098                }
4099
4100                // #664: null-slot trap — ONLY when the table image carries
4101                // null (uninitialized) slots, which the layout contract
4102                // requires to be linked as ZERO words. A fully-initialized
4103                // table skips this branch entirely, keeping the pre-#664
4104                // expansion byte-identical BY CONSTRUCTION (the #650
4105                // offset-0 trick).
4106                if *null_check {
4107                    // CMP.W R12, #0 — T2 CMP (immediate): 11110 i 0 1101 1
4108                    // Rn(4) | 0 imm3 1111 imm8, Rn=R12, imm=0.
4109                    bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
4110                    bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
4111                    // BNE +1 insn (skip the UDF when the pointer is non-null)
4112                    // — B<cond>.N imm8=0: target = branch + 4. NE.
4113                    bytes.extend_from_slice(&0xD100u16.to_le_bytes());
4114                    // UDF #0 — call_indirect null-funcref trap (WASM Core
4115                    // §4.4.8: calling an uninitialized element traps; same
4116                    // trap idiom as the bounds guard above).
4117                    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
4118                }
4119
4120                // BLX R12 (call function indirectly)
4121                // BLX Rm (16-bit): 0100 0111 1 Rm 000
4122                let blx: u16 = 0x47E0; // BLX R12
4123                bytes.extend_from_slice(&blx.to_le_bytes());
4124
4125                Ok(bytes)
4126            }
4127
4128            // Label pseudo-instruction: emits no machine code
4129            ArmOp::Label { .. } => Ok(Vec::new()),
4130
4131            // Conditional branch to label (generic) - offset 0, will be patched
4132            ArmOp::Bcc { cond, label: _ } => {
4133                use synth_synthesis::Condition;
4134                let cond_bits: u16 = match cond {
4135                    Condition::EQ => 0x0,
4136                    Condition::NE => 0x1,
4137                    Condition::HS => 0x2,
4138                    Condition::LO => 0x3,
4139                    Condition::HI => 0x8,
4140                    Condition::LS => 0x9,
4141                    Condition::GE => 0xA,
4142                    Condition::LT => 0xB,
4143                    Condition::GT => 0xC,
4144                    Condition::LE => 0xD,
4145                };
4146                // 16-bit B<cond> with offset 0: 1101 cond imm8
4147                let instr: u16 = 0xD000 | (cond_bits << 8);
4148                Ok(instr.to_le_bytes().to_vec())
4149            }
4150
4151            // Branch instructions
4152            ArmOp::B { label: _ } => {
4153                // Simplified: B.N with offset 0
4154                // For real usage, would need label resolution
4155                let instr: u16 = 0xE000; // B.N #0
4156                Ok(instr.to_le_bytes().to_vec())
4157            }
4158
4159            // BHS (Branch if Higher or Same) - used for bounds checking
4160            // Condition code: 0x2 (C set)
4161            ArmOp::Bhs { label: _ } => {
4162                // 16-bit B<cond> with offset 0: 1101 cond imm8
4163                // cond = 0x2 (HS)
4164                let instr: u16 = 0xD200; // BHS.N #0
4165                Ok(instr.to_le_bytes().to_vec())
4166            }
4167
4168            // BLO (Branch if Lower) - complementary to BHS
4169            // Condition code: 0x3 (C clear)
4170            ArmOp::Blo { label: _ } => {
4171                // 16-bit B<cond> with offset 0: 1101 cond imm8
4172                // cond = 0x3 (LO)
4173                let instr: u16 = 0xD300; // BLO.N #0
4174                Ok(instr.to_le_bytes().to_vec())
4175            }
4176
4177            // Branch with numeric offset (Thumb-2)
4178            // Thumb-2 B.W instruction: 32-bit with +-16MB range
4179            ArmOp::BOffset { offset } => {
4180                // offset is already the halfword displacement: (target - branch - 4) / 2
4181                // This is the raw encoded value, accounting for variable-length instructions
4182                let halfword_offset = *offset;
4183
4184                // 16-bit B.N encoding: 1110 0 imm11 (11-bit signed halfword offset)
4185                // Range: -1024 to +1022 halfwords
4186                if (-1024..=1022).contains(&halfword_offset) {
4187                    // 16-bit B.N encoding: 1110 0 imm11
4188                    let imm11 = (halfword_offset as u16) & 0x7FF;
4189                    let instr: u16 = 0xE000 | imm11;
4190                    Ok(instr.to_le_bytes().to_vec())
4191                } else {
4192                    // 32-bit B.W encoding for larger offsets
4193                    // First halfword: 1111 0 S imm10
4194                    // Second halfword: 10 J1 0 J2 imm11
4195                    // Total offset = SignExtend(S:I1:I2:imm10:imm11:0)
4196                    // where I1 = NOT(J1 XOR S), I2 = NOT(J2 XOR S)
4197
4198                    // The B.W (T4) encoding packs the signed offset as:
4199                    //   S:I1:I2:imm10:imm11:0  (25-bit signed, halfword-aligned)
4200                    // where J1 = NOT(I1 XOR S), J2 = NOT(I2 XOR S)
4201                    // Input halfword_offset already equals (target - PC - 4) / 2,
4202                    // so the full byte offset = halfword_offset << 1.
4203                    // The encoding fields split that 25-bit signed value (including the
4204                    // implicit trailing zero) as: S | imm10 | imm11
4205                    // with I1 = bit 23 and I2 = bit 22 of the signed offset.
4206                    let signed_offset = halfword_offset << 1; // byte offset
4207                    let s = if signed_offset < 0 { 1u32 } else { 0u32 };
4208                    let uoffset = signed_offset as u32;
4209                    let imm10 = (uoffset >> 12) & 0x3FF; // bits [21:12]
4210                    let imm11 = (uoffset >> 1) & 0x7FF; // bits [11:1]
4211                    let i1 = (uoffset >> 23) & 1; // bit 23
4212                    let i2 = (uoffset >> 22) & 1; // bit 22
4213                    let j1 = (!(i1 ^ s)) & 1; // J1 = NOT(I1 XOR S)
4214                    let j2 = (!(i2 ^ s)) & 1; // J2 = NOT(I2 XOR S)
4215
4216                    let hw1: u16 = (0xF000 | (s << 10) | imm10) as u16;
4217                    let hw2: u16 = (0x9000 | (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            // Conditional branch with numeric offset (Thumb-2)
4226            ArmOp::BCondOffset { cond, offset } => {
4227                use synth_synthesis::Condition;
4228                let cond_bits: u16 = match cond {
4229                    Condition::EQ => 0x0,
4230                    Condition::NE => 0x1,
4231                    Condition::HS => 0x2,
4232                    Condition::LO => 0x3,
4233                    Condition::HI => 0x8,
4234                    Condition::LS => 0x9,
4235                    Condition::GE => 0xA,
4236                    Condition::LT => 0xB,
4237                    Condition::GT => 0xC,
4238                    Condition::LE => 0xD,
4239                };
4240
4241                // offset is already the halfword displacement: (target - branch - 4) / 2
4242                // This is the raw imm8 value for 16-bit B<cond> encoding
4243                let halfword_offset = *offset;
4244
4245                // 16-bit B<cond> encoding: 1101 cond imm8
4246                // Range: -256 to +254 halfwords (imm8 is sign-extended and shifted left 1)
4247                if (-128..=127).contains(&halfword_offset) {
4248                    let imm8 = (halfword_offset as u16) & 0xFF;
4249                    let instr: u16 = 0xD000 | (cond_bits << 8) | imm8;
4250                    Ok(instr.to_le_bytes().to_vec())
4251                } else {
4252                    // 32-bit B<cond>.W (encoding T3) for larger offsets
4253                    // First halfword: 1111 0 S cond(4) imm6
4254                    // Second halfword: 10 J1 0 J2 imm11
4255                    //
4256                    // Per ARMv7-M, the branch BYTE offset is
4257                    // SignExtend(S:J2:J1:imm6:imm11:'0'), i.e. the field value
4258                    // S:J2:J1:imm6:imm11 IS the signed 20-bit HALFWORD offset —
4259                    // imm11/imm6/J1/J2/S take `halfword_offset` bits [10:0],
4260                    // [16:11], 17, 18 and 19 directly (mirroring the T4
4261                    // unconditional arm above).
4262                    //
4263                    // #740: this arm previously packed `halfword_offset >> 1`
4264                    // into imm6:imm11 — HALVING the displacement — so every
4265                    // wide conditional branch (span > 254 bytes) landed at half
4266                    // its intended offset: gust_poll's loop-head `br_if` to an
4267                    // outer block end jumped mid-shape. Narrow (16-bit) B<cond>
4268                    // encodings were unaffected, which is why short-range CF
4269                    // fixtures never caught it.
4270                    if !(-(1 << 19)..(1 << 19)).contains(&halfword_offset) {
4271                        return Err(synth_core::Error::synthesis(format!(
4272                            "B<cond>.W (T3) halfword offset {halfword_offset} exceeds \
4273                             the signed 20-bit encoding range (±1 MB) — refusing to \
4274                             emit a truncated branch"
4275                        )));
4276                    }
4277                    let u = halfword_offset as u32;
4278                    let imm11 = u & 0x7FF; // halfword offset bits [10:0]
4279                    let imm6 = (u >> 11) & 0x3F; // bits [16:11]
4280                    let j1 = (u >> 17) & 1; // bit 17
4281                    let j2 = (u >> 18) & 1; // bit 18
4282                    let s = (u >> 19) & 1; // sign (range-checked above)
4283
4284                    let hw1: u16 = (0xF000 | (s << 10) | ((cond_bits as u32) << 6) | imm6) as u16;
4285                    let hw2: u16 = (0x8000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4286
4287                    let mut bytes = hw1.to_le_bytes().to_vec();
4288                    bytes.extend_from_slice(&hw2.to_le_bytes());
4289                    Ok(bytes)
4290                }
4291            }
4292
4293            ArmOp::Bl { label: _ } => {
4294                // BL is always 32-bit in Thumb-2, encoded here as a relocatable
4295                // placeholder; an R_ARM_THM_CALL relocation patches the target
4296                // (see arm_backend.rs). The placeholder must carry an embedded
4297                // addend of -4 so the relocation nets to exactly the symbol S.
4298                //
4299                // Thumb BL computes `target = (P + 4) + signed_offset`. Under
4300                // R_ARM_THM_CALL the linker resolves using the in-place addend;
4301                // a 0xF800 placeholder (addend 0) lands at S+4 — every call one
4302                // instruction past the callee entry (#174). The correct
4303                // placeholder is what `gas` emits for `bl <extern>`:
4304                //   f7ff fffe  ->  `bl <self>`  (S=1, J1=J2=1, imm = -4 addend),
4305                // i.e. hw1=0xF7FF, hw2=0xFFFE. This nets to S, not S+4.
4306                // (The earlier 0xD000 was worse still — a ~+0x600000 addend,
4307                // the garbage `bl c0000c` and "truncated to fit" of #167.)
4308                let hw1: u16 = 0xF7FF;
4309                let hw2: u16 = 0xFFFE;
4310                let mut bytes = hw1.to_le_bytes().to_vec();
4311                bytes.extend_from_slice(&hw2.to_le_bytes());
4312                Ok(bytes)
4313            }
4314
4315            // MVN
4316            ArmOp::Mvn { rd, op2 } => {
4317                if let Operand2::Reg(rm) = op2 {
4318                    let rd_bits = reg_to_bits(rd) as u16;
4319                    let rm_bits = reg_to_bits(rm) as u16;
4320
4321                    if rd_bits < 8 && rm_bits < 8 {
4322                        // MVNS Rd, Rm (16-bit): 0100 0011 11 Rm Rd
4323                        let instr: u16 = 0x43C0 | (rm_bits << 3) | rd_bits;
4324                        Ok(instr.to_le_bytes().to_vec())
4325                    } else {
4326                        // 32-bit MVN
4327                        let hw1: u16 = 0xEA6F_u16;
4328                        let hw2: u16 = ((reg_to_bits(rd) << 8) | reg_to_bits(rm)) as u16;
4329                        let mut bytes = hw1.to_le_bytes().to_vec();
4330                        bytes.extend_from_slice(&hw2.to_le_bytes());
4331                        Ok(bytes)
4332                    }
4333                } else {
4334                    // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
4335                    // operand form the arm does not encode (Reg only) -- #615's
4336                    // silent-miscompile class, on the DEFAULT target. Refuse instead.
4337                    Err(synth_core::Error::synthesis(format!(
4338                        "Thumb-2 MVN with operand {op2:?} has no Thumb-2 encoding here (Reg \
4339                         are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
4340                    )))
4341                }
4342            }
4343
4344            // MOVW - Move Wide (Thumb-2 32-bit)
4345            ArmOp::Movw { rd, imm16 } => {
4346                self.encode_thumb32_movw_raw(reg_to_bits(rd), *imm16 as u32)
4347            }
4348
4349            // MOVT - Move Top (Thumb-2 32-bit)
4350            ArmOp::Movt { rd, imm16 } => {
4351                self.encode_thumb32_movt_raw(reg_to_bits(rd), *imm16 as u32)
4352            }
4353
4354            // #237: symbol-relative MOVW/MOVT. Encode the addend's low/high 16
4355            // bits in place; the backend records an R_ARM_MOVW_ABS_NC /
4356            // R_ARM_MOVT_ABS relocation against `symbol`, so the linker adds the
4357            // symbol's final address to the in-place addend (REL semantics).
4358            ArmOp::MovwSym { rd, addend, .. } => {
4359                self.encode_thumb32_movw_raw(reg_to_bits(rd), (*addend as u32) & 0xffff)
4360            }
4361            ArmOp::MovtSym { rd, addend, .. } => {
4362                self.encode_thumb32_movt_raw(reg_to_bits(rd), ((*addend as u32) >> 16) & 0xffff)
4363            }
4364
4365            // #345: literal-pool address load — emit a PLACEHOLDER `LDR.W rd,
4366            // [pc, #0]` (U=1, imm12=0). The backend (arm_backend.rs) places the
4367            // 4-byte pool word at the end of the function, records the R_ARM_ABS32
4368            // relocation against `symbol+addend`, and patches the imm12 with the
4369            // real PC-relative distance once the pool offset is known.
4370            // Encoding T2: 1111 1000 1101 1111 | Rt(4) imm12(12), with the literal
4371            // base = Align(PC,4) and PC = address of this instruction + 4.
4372            ArmOp::LdrSym { rd, .. } => {
4373                let rt = reg_to_bits(rd) as u16;
4374                let hw1: u16 = 0xF8DF; // LDR.W (literal), U=1
4375                let hw2: u16 = rt << 12; // imm12 = 0 placeholder
4376                let mut bytes = Vec::with_capacity(4);
4377                bytes.extend_from_slice(&hw1.to_le_bytes());
4378                bytes.extend_from_slice(&hw2.to_le_bytes());
4379                Ok(bytes)
4380            }
4381
4382            // SetCond: Materialize condition flag into register (0 or 1)
4383            // Strategy: ITE <cond>; MOV Rd, #1; MOV Rd, #0
4384            // IMPORTANT: Must use ITE (If-Then-Else) because 16-bit Thumb MOV
4385            // always sets flags (MOVS). We need to evaluate the condition BEFORE
4386            // any MOV instruction clobbers the flags from CMP.
4387            ArmOp::SetCond { rd, cond } => {
4388                let rd_bits = reg_to_bits(rd) as u16;
4389
4390                // Condition code encoding for IT block
4391                use synth_synthesis::Condition;
4392                let cond_bits: u16 = match cond {
4393                    Condition::EQ => 0x0,
4394                    Condition::NE => 0x1,
4395                    Condition::LT => 0xB,
4396                    Condition::LE => 0xD,
4397                    Condition::GT => 0xC,
4398                    Condition::GE => 0xA,
4399                    Condition::LO => 0x3, // CC/LO (unsigned <)
4400                    Condition::LS => 0x9, // LS (unsigned <=)
4401                    Condition::HI => 0x8, // HI (unsigned >)
4402                    Condition::HS => 0x2, // CS/HS (unsigned >=)
4403                };
4404
4405                // ITE <cond>: encodes If-Then-Else block
4406                // The mask field depends on firstcond[0]:
4407                // - If firstcond[0] = 0: mask = 0xC for TE pattern (ITE EQ = BF0C)
4408                // - If firstcond[0] = 1: mask = 0x4 for TE pattern (ITE NE = BF14)
4409                let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4410                let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4411
4412                // Materialize 0/1 into Rd. The 16-bit MOVS (T1) encodes Rd in a
4413                // 3-bit field (bits[10:8]) — only R0–R7. For a high register
4414                // (R8–R12) `rd_bits << 8` overflows into bit 11 and silently
4415                // turns MOVS into CMP (00100 → 00101), corrupting the result
4416                // (this mis-materialized gale's `has_waiter`, so its `local.set`
4417                // stored a stale register → the binary-sem WAKE dispatch read
4418                // garbage). Use the 32-bit MOV.W (T2) for high registers, which
4419                // has a 4-bit Rd field. MOV.W with S=0 doesn't set flags, which
4420                // is fine inside the ITE (the materialized value is the result;
4421                // the flags are not consumed afterwards).
4422                let mut bytes = ite_instr.to_le_bytes().to_vec();
4423                let push_mov = |bytes: &mut Vec<u8>, imm: u16| {
4424                    if rd_bits <= 7 {
4425                        let m: u16 = 0x2000 | (rd_bits << 8) | imm; // 16-bit MOVS Rd,#imm
4426                        bytes.extend_from_slice(&m.to_le_bytes());
4427                    } else {
4428                        // 32-bit MOV.W Rd, #imm (T2): F04F | (Rd<<8) | imm8
4429                        let hw1: u16 = 0xF04F;
4430                        let hw2: u16 = (rd_bits << 8) | imm;
4431                        bytes.extend_from_slice(&hw1.to_le_bytes());
4432                        bytes.extend_from_slice(&hw2.to_le_bytes());
4433                    }
4434                };
4435                push_mov(&mut bytes, 1); // Then branch (condition true)  → 1
4436                push_mov(&mut bytes, 0); // Else branch (condition false) → 0
4437                Ok(bytes)
4438            }
4439
4440            // I64SetCond: Compare two i64 register pairs, result 0/1 in rd
4441            // EQ/NE: CMP lo,lo; IT EQ; CMPEQ hi,hi; ITE <cond>; MOV 1; MOV 0
4442            // LT: CMP lo,lo; SBCS rd,hi,hi; ITE LT; MOV 1; MOV 0
4443            // GT: CMP lo,lo (swapped); SBCS rd,hi,hi (swapped); ITE LT; MOV 1; MOV 0
4444            ArmOp::I64SetCond {
4445                rd,
4446                rn_lo,
4447                rn_hi,
4448                rm_lo,
4449                rm_hi,
4450                cond,
4451            } => {
4452                use synth_synthesis::Condition;
4453                let rd_bits = reg_to_bits(rd) as u16;
4454                let mut bytes = Vec::new();
4455
4456                // Helper: encode CMP Rn, Rm (16-bit)
4457                let encode_cmp_reg = |rn: &synth_synthesis::Reg,
4458                                      rm: &synth_synthesis::Reg|
4459                 -> Vec<u8> {
4460                    let rn_bits = reg_to_bits(rn) as u16;
4461                    let rm_bits = reg_to_bits(rm) as u16;
4462                    if rn_bits < 8 && rm_bits < 8 {
4463                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
4464                        instr.to_le_bytes().to_vec()
4465                    } else {
4466                        let n_bit = (rn_bits >> 3) & 1;
4467                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
4468                        instr.to_le_bytes().to_vec()
4469                    }
4470                };
4471
4472                // Helper: encode ITE <cond> (2 bytes)
4473                let encode_ite = |cond_bits: u16| -> Vec<u8> {
4474                    let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4475                    let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4476                    ite_instr.to_le_bytes().to_vec()
4477                };
4478
4479                // Helper: encode SetCond (ITE + MOV #1 + MOV #0) for given condition
4480                let encode_setcond = |cond_bits: u16, rd_bits: u16| -> Vec<u8> {
4481                    let mut b = encode_ite(cond_bits);
4482                    if rd_bits < 8 {
4483                        let mov_one: u16 = 0x2001 | (rd_bits << 8);
4484                        let mov_zero: u16 = 0x2000 | (rd_bits << 8);
4485                        b.extend_from_slice(&mov_one.to_le_bytes());
4486                        b.extend_from_slice(&mov_zero.to_le_bytes());
4487                    } else {
4488                        // #311: rd >= R8 — the 16-bit MOV imm8 form has a 3-bit
4489                        // rd field; rd_bits<<8 overflows into bit 11 and
4490                        // TRANSMUTES the MOV into CMP (0x2001|0x0800 = 0x2801 =
4491                        // CMP r0,#1): the boolean dies in the flags and the
4492                        // consumer reads a stale register. Use the 32-bit
4493                        // MOV.W (T2: F04F 0000|rd<<8|imm8) — IT-legal,
4494                        // flag-preserving. Same class as H-CODE-9 / #180.
4495                        for imm in [1u16, 0u16] {
4496                            let hw1: u16 = 0xF04F;
4497                            let hw2: u16 = (rd_bits << 8) | imm;
4498                            b.extend_from_slice(&hw1.to_le_bytes());
4499                            b.extend_from_slice(&hw2.to_le_bytes());
4500                        }
4501                    }
4502                    b
4503                };
4504
4505                match cond {
4506                    Condition::EQ | Condition::NE => {
4507                        // CMP rn_lo, rm_lo (compare low words)
4508                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4509
4510                        // IT EQ (execute next instruction only if Z=1)
4511                        let it_eq: u16 = 0xBF08; // IT EQ: cond=0000, mask=1000
4512                        bytes.extend_from_slice(&it_eq.to_le_bytes());
4513
4514                        // CMPEQ rn_hi, rm_hi (compare high words, only if low equal)
4515                        bytes.extend_from_slice(&encode_cmp_reg(rn_hi, rm_hi));
4516
4517                        // ITE <cond>; MOV rd, #1; MOV rd, #0
4518                        let cond_bits: u16 = match cond {
4519                            Condition::EQ => 0x0,
4520                            Condition::NE => 0x1,
4521                            _ => unreachable!(),
4522                        };
4523                        bytes.extend_from_slice(&encode_setcond(cond_bits, rd_bits));
4524                    }
4525
4526                    Condition::LT => {
4527                        // CMP rn_lo, rm_lo (sets C flag for borrow)
4528                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4529
4530                        // SBCS rd, rn_hi, rm_hi (subtract with carry, sets N,V flags)
4531                        // SBCS.W Rd, Rn, Rm: EB70 Rn | 0000 Rd 0000 Rm
4532                        let rn_hi_bits = reg_to_bits(rn_hi);
4533                        let rm_hi_bits = reg_to_bits(rm_hi);
4534                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4535                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4536                        bytes.extend_from_slice(&hw1.to_le_bytes());
4537                        bytes.extend_from_slice(&hw2.to_le_bytes());
4538
4539                        // ITE LT; MOV rd, #1; MOV rd, #0
4540                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4541                    }
4542
4543                    Condition::GT => {
4544                        // GT(a,b) = LT(b,a): swap operands
4545                        // CMP rm_lo, rn_lo (swapped)
4546                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4547
4548                        // SBCS rd, rm_hi, rn_hi (swapped)
4549                        let rm_hi_bits = reg_to_bits(rm_hi);
4550                        let rn_hi_bits = reg_to_bits(rn_hi);
4551                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4552                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4553                        bytes.extend_from_slice(&hw1.to_le_bytes());
4554                        bytes.extend_from_slice(&hw2.to_le_bytes());
4555
4556                        // ITE LT; MOV rd, #1; MOV rd, #0
4557                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4558                    }
4559
4560                    Condition::LE => {
4561                        // LE(a,b) = !GT(a,b): use GT logic but invert result
4562                        // GT(a,b) = LT(b,a): so we do CMP(b,a) and check LT, then invert
4563                        // CMP rm_lo, rn_lo (swapped, same as GT)
4564                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4565
4566                        // SBCS rd, rm_hi, rn_hi (swapped)
4567                        let rm_hi_bits = reg_to_bits(rm_hi);
4568                        let rn_hi_bits = reg_to_bits(rn_hi);
4569                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4570                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4571                        bytes.extend_from_slice(&hw1.to_le_bytes());
4572                        bytes.extend_from_slice(&hw2.to_le_bytes());
4573
4574                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT, so inverting GT result)
4575                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4576                    }
4577
4578                    Condition::GE => {
4579                        // GE(a,b) = !LT(a,b): use LT logic but invert result
4580                        // CMP rn_lo, rm_lo (same as LT)
4581                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4582
4583                        // SBCS rd, rn_hi, rm_hi (same as LT)
4584                        let rn_hi_bits = reg_to_bits(rn_hi);
4585                        let rm_hi_bits = reg_to_bits(rm_hi);
4586                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4587                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4588                        bytes.extend_from_slice(&hw1.to_le_bytes());
4589                        bytes.extend_from_slice(&hw2.to_le_bytes());
4590
4591                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT)
4592                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4593                    }
4594
4595                    // Unsigned comparisons - same instruction sequence, different conditions
4596                    Condition::LO => {
4597                        // LO (unsigned LT): CMP lo, SBCS hi, check C=0
4598                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4599                        let rn_hi_bits = reg_to_bits(rn_hi);
4600                        let rm_hi_bits = reg_to_bits(rm_hi);
4601                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4602                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4603                        bytes.extend_from_slice(&hw1.to_le_bytes());
4604                        bytes.extend_from_slice(&hw2.to_le_bytes());
4605                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4606                    }
4607
4608                    Condition::HI => {
4609                        // HI (unsigned GT): swap operands and check LO
4610                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4611                        let rm_hi_bits = reg_to_bits(rm_hi);
4612                        let rn_hi_bits = reg_to_bits(rn_hi);
4613                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4614                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4615                        bytes.extend_from_slice(&hw1.to_le_bytes());
4616                        bytes.extend_from_slice(&hw2.to_le_bytes());
4617                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4618                    }
4619
4620                    Condition::LS => {
4621                        // LS (unsigned LE): !(a > b) = !(HI), so do HI and invert
4622                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4623                        let rm_hi_bits = reg_to_bits(rm_hi);
4624                        let rn_hi_bits = reg_to_bits(rn_hi);
4625                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4626                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4627                        bytes.extend_from_slice(&hw1.to_le_bytes());
4628                        bytes.extend_from_slice(&hw2.to_le_bytes());
4629                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4630                    }
4631
4632                    Condition::HS => {
4633                        // HS (unsigned GE): !(a < b) = !(LO)
4634                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4635                        let rn_hi_bits = reg_to_bits(rn_hi);
4636                        let rm_hi_bits = reg_to_bits(rm_hi);
4637                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4638                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4639                        bytes.extend_from_slice(&hw1.to_le_bytes());
4640                        bytes.extend_from_slice(&hw2.to_le_bytes());
4641                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4642                    }
4643                }
4644
4645                Ok(bytes)
4646            }
4647
4648            // I64SetCondZ: Test if i64 register pair is zero, result 0/1 in rd
4649            // ORR.W rd, rn_lo, rn_hi; CMP rd, #0; ITE EQ; MOV 1; MOV 0
4650            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
4651                let rd_bits = reg_to_bits(rd);
4652                let rn_lo_bits = reg_to_bits(rn_lo);
4653                let rn_hi_bits = reg_to_bits(rn_hi);
4654                let mut bytes = Vec::new();
4655
4656                // ORR.W rd, rn_lo, rn_hi: EA40 rn_lo | 0000 rd 0000 rn_hi
4657                let hw1: u16 = (0xEA40 | rn_lo_bits) as u16;
4658                let hw2: u16 = ((rd_bits << 8) | rn_hi_bits) as u16;
4659                bytes.extend_from_slice(&hw1.to_le_bytes());
4660                bytes.extend_from_slice(&hw2.to_le_bytes());
4661
4662                // CMP rd, #0 — 16-bit form only for r0-r7 (3-bit rd field);
4663                // high registers take CMP.W (T2: F1B0|rn 0F00|imm8). This was
4664                // H-CODE-9: rd_bits<<8 overflowing the field compared the
4665                // WRONG register. Same hardening as the #311 SetCond fix.
4666                if rd_bits < 8 {
4667                    let cmp_instr: u16 = 0x2800 | ((rd_bits as u16) << 8);
4668                    bytes.extend_from_slice(&cmp_instr.to_le_bytes());
4669                } else {
4670                    let hw1: u16 = 0xF1B0 | (rd_bits as u16);
4671                    let hw2: u16 = 0x0F00;
4672                    bytes.extend_from_slice(&hw1.to_le_bytes());
4673                    bytes.extend_from_slice(&hw2.to_le_bytes());
4674                }
4675
4676                // ITE EQ; MOV rd, #1; MOV rd, #0 (32-bit MOV.W for rd >= R8,
4677                // #311 — see I64SetCond)
4678                let mask = 0xC_u16; // ITE EQ mask: firstcond[0]=0, mask=0xC
4679                let ite_instr: u16 = 0xBF00 | mask;
4680                bytes.extend_from_slice(&ite_instr.to_le_bytes());
4681                if rd_bits < 8 {
4682                    let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
4683                    let mov_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
4684                    bytes.extend_from_slice(&mov_one.to_le_bytes());
4685                    bytes.extend_from_slice(&mov_zero.to_le_bytes());
4686                } else {
4687                    for imm in [1u16, 0u16] {
4688                        let hw1: u16 = 0xF04F;
4689                        let hw2: u16 = ((rd_bits as u16) << 8) | imm;
4690                        bytes.extend_from_slice(&hw1.to_le_bytes());
4691                        bytes.extend_from_slice(&hw2.to_le_bytes());
4692                    }
4693                }
4694
4695                Ok(bytes)
4696            }
4697
4698            // I64Mul: 64-bit multiply using UMULL + MLA cross products
4699            // Formula: result = (a_lo * b_lo) + ((a_lo * b_hi + a_hi * b_lo) << 32)
4700            // Uses R12 as scratch register
4701            ArmOp::I64Mul {
4702                rd_lo,
4703                rd_hi,
4704                rn_lo,
4705                rn_hi,
4706                rm_lo,
4707                rm_hi,
4708            } => {
4709                let rd_lo_bits = reg_to_bits(rd_lo);
4710                let rd_hi_bits = reg_to_bits(rd_hi);
4711                let rn_lo_bits = reg_to_bits(rn_lo);
4712                let rn_hi_bits = reg_to_bits(rn_hi);
4713                let rm_lo_bits = reg_to_bits(rm_lo);
4714                let rm_hi_bits = reg_to_bits(rm_hi);
4715                let r12: u32 = 12; // IP scratch register
4716                let mut bytes = Vec::new();
4717
4718                // 1. MUL R12, rn_lo, rm_hi  (R12 = a_lo * b_hi)
4719                // Thumb-2 MUL: hw1=0xFB00|Rn, hw2=0xF000|(Rd<<8)|Rm
4720                let hw1: u16 = (0xFB00 | rn_lo_bits) as u16;
4721                let hw2: u16 = (0xF000 | (r12 << 8) | rm_hi_bits) as u16;
4722                bytes.extend_from_slice(&hw1.to_le_bytes());
4723                bytes.extend_from_slice(&hw2.to_le_bytes());
4724
4725                // 2. MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
4726                // Thumb-2 MLA: hw1=0xFB00|Rn, hw2=(Ra<<12)|(Rd<<8)|Rm
4727                let hw1: u16 = (0xFB00 | rn_hi_bits) as u16;
4728                let hw2: u16 = ((r12 << 12) | (r12 << 8) | rm_lo_bits) as u16;
4729                bytes.extend_from_slice(&hw1.to_le_bytes());
4730                bytes.extend_from_slice(&hw2.to_le_bytes());
4731
4732                // 3. UMULL rd_lo, rd_hi, rn_lo, rm_lo  (rd_lo:rd_hi = a_lo * b_lo)
4733                // Thumb-2 UMULL: hw1=0xFBA0|Rn, hw2=(RdLo<<12)|(RdHi<<8)|Rm
4734                let hw1: u16 = (0xFBA0 | rn_lo_bits) as u16;
4735                let hw2: u16 = ((rd_lo_bits << 12) | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4736                bytes.extend_from_slice(&hw1.to_le_bytes());
4737                bytes.extend_from_slice(&hw2.to_le_bytes());
4738
4739                // 4. ADD rd_hi, R12  (rd_hi += cross products)
4740                // 16-bit high reg ADD: 01000100 D Rm Rdn[2:0]
4741                let d_bit = (rd_hi_bits >> 3) & 1;
4742                let add_instr: u16 =
4743                    (0x4400 | (d_bit << 7) | (r12 << 3) | (rd_hi_bits & 0x7)) as u16;
4744                bytes.extend_from_slice(&add_instr.to_le_bytes());
4745
4746                Ok(bytes)
4747            }
4748
4749            // I64Shl: 64-bit shift left with branch for n<32 vs n>=32.
4750            //
4751            // #1048: the expansion must NEVER write its own input operands.
4752            // The pre-#1048 sequence masked the amount IN PLACE
4753            // (`AND.W rm_lo, rm_lo, #63`) and used the amount's home high
4754            // register `rm_hi` as scratch (`SUBS.W rm_hi, rm_lo, #32`, RSB,
4755            // LSR) — so re-reading the amount after the shift returned a
4756            // mangled value (amt=64 read back 0, amt=67 read back 3). The
4757            // rewrite uses R12 — encoder scratch, never allocatable (#212) —
4758            // as the ONLY temporary, re-deriving the masked amount from the
4759            // untouched rm_lo whenever a second live temp would otherwise be
4760            // needed. This matches the Rocq/SMT pseudo-op models
4761            // (I64ShlPseudo writes rd_lo/rd_hi ONLY), which were proven over
4762            // exactly this non-clobbering contract all along.
4763            ArmOp::I64Shl {
4764                rd_lo,
4765                rd_hi,
4766                rn_lo,
4767                rn_hi,
4768                rm_lo,
4769                rm_hi: _,
4770            } => {
4771                let rd_lo_bits = reg_to_bits(rd_lo);
4772                let rd_hi_bits = reg_to_bits(rd_hi);
4773                let rn_lo_bits = reg_to_bits(rn_lo);
4774                let rn_hi_bits = reg_to_bits(rn_hi);
4775                let rm_lo_bits = reg_to_bits(rm_lo);
4776                let r12: u32 = 12; // the only scratch — never allocatable
4777                let mut bytes = Vec::new();
4778
4779                // #1039 house style: refuse a destination that would collide
4780                // with an input still needed after the destination is first
4781                // written, loudly — never misassemble. rd_hi is written before
4782                // rn_lo and rm_lo are last read; the in-place form
4783                // rd == rn (select_default) has rd_hi == rn_hi and stays legal.
4784                if rd_hi_bits == rn_lo_bits || rd_hi_bits == rm_lo_bits {
4785                    return Err(synth_core::Error::synthesis(format!(
4786                        "I64Shl: rd_hi {rd_hi:?} aliases an input ({rn_lo:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4787                    )));
4788                }
4789
4790                // AND.W R12, rm_lo, #63  (n — the amount register is only READ)
4791                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4792                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4793                bytes.extend_from_slice(&hw1.to_le_bytes());
4794                bytes.extend_from_slice(&hw2.to_le_bytes());
4795
4796                // SUBS.W R12, R12, #32  (R12 = n-32, sets flags)
4797                let hw1: u16 = (0xF1B0 | r12) as u16;
4798                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4799                bytes.extend_from_slice(&hw1.to_le_bytes());
4800                bytes.extend_from_slice(&hw2.to_le_bytes());
4801
4802                // BPL .large (branch if n >= 32, offset = +14 halfwords)
4803                let bpl: u16 = 0xD50E;
4804                bytes.extend_from_slice(&bpl.to_le_bytes());
4805
4806                // --- Small shift (n < 32) ---
4807                // AND.W R12, rm_lo, #63  (n again — R12 held n-32)
4808                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4809                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4810                bytes.extend_from_slice(&hw1.to_le_bytes());
4811                bytes.extend_from_slice(&hw2.to_le_bytes());
4812
4813                // LSL.W rd_hi, rn_hi, R12  (hi << n; rn_hi's last read)
4814                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4815                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4816                bytes.extend_from_slice(&hw1.to_le_bytes());
4817                bytes.extend_from_slice(&hw2.to_le_bytes());
4818
4819                // RSB.W R12, R12, #32  (R12 = 32-n)
4820                let hw1: u16 = (0xF1C0 | r12) as u16;
4821                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4822                bytes.extend_from_slice(&hw1.to_le_bytes());
4823                bytes.extend_from_slice(&hw2.to_le_bytes());
4824
4825                // LSR.W R12, rn_lo, R12  (overflow = lo >> (32-n); n=0 gives
4826                // a register shift by 32 which yields 0 — exact)
4827                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4828                let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4829                bytes.extend_from_slice(&hw1.to_le_bytes());
4830                bytes.extend_from_slice(&hw2.to_le_bytes());
4831
4832                // ORR.W rd_hi, rd_hi, R12  (hi |= overflow bits from lo)
4833                let hw1: u16 = (0xEA40 | rd_hi_bits) as u16;
4834                let hw2: u16 = ((rd_hi_bits << 8) | r12) as u16;
4835                bytes.extend_from_slice(&hw1.to_le_bytes());
4836                bytes.extend_from_slice(&hw2.to_le_bytes());
4837
4838                // AND.W R12, rm_lo, #63  (n once more for the low half)
4839                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4840                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4841                bytes.extend_from_slice(&hw1.to_le_bytes());
4842                bytes.extend_from_slice(&hw2.to_le_bytes());
4843
4844                // LSL.W rd_lo, rn_lo, R12  (lo << n)
4845                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4846                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4847                bytes.extend_from_slice(&hw1.to_le_bytes());
4848                bytes.extend_from_slice(&hw2.to_le_bytes());
4849
4850                // B .done — `.done` is the END of the expansion, i.e. PAST the
4851                // large-shift arm's trailing zero-fill. #916: that zero-fill is
4852                // 1 halfword for a low rd_lo but 2 for R8-R12 (MOV.W), so the
4853                // displacement is DERIVED from its real width instead of the
4854                // hard-coded 0xE002 — widening the MOV without this would
4855                // overshoot `.done` and turn a data miscompile into a
4856                // control-flow one. Thumb `B` reads PC as its own address + 4
4857                // (= +2 halfwords), so imm11 = (large-arm halfwords) - 1.
4858                let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_lo_bits);
4859                let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4860                bytes.extend_from_slice(&b_done.to_le_bytes());
4861
4862                // --- Large shift (n >= 32) --- (R12 still holds n-32)
4863                // LSL.W rd_hi, rn_lo, R12  (hi = lo << (n-32))
4864                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4865                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4866                bytes.extend_from_slice(&hw1.to_le_bytes());
4867                bytes.extend_from_slice(&hw2.to_le_bytes());
4868
4869                // MOV rd_lo, #0 (#916: MOV.W for rd_lo >= R8). NOTE the order
4870                // is load-bearing — zeroing rd_lo BEFORE the LSL.W would
4871                // destroy rn_lo in the in-place case rd_lo == rn_lo, so this
4872                // cannot be reordered to dodge the displacement change.
4873                emit_thumb_zero_fill(&mut bytes, rd_lo_bits);
4874
4875                Ok(bytes) // 46 bytes (48 when rd_lo >= R8 takes MOV.W)
4876            }
4877
4878            // I64ShrU: 64-bit logical shift right with branch for n<32 vs
4879            // n>=32. #1048: R12-only scratch, operands never written — see
4880            // the I64Shl comment for the full rationale.
4881            ArmOp::I64ShrU {
4882                rd_lo,
4883                rd_hi,
4884                rn_lo,
4885                rn_hi,
4886                rm_lo,
4887                rm_hi: _,
4888            } => {
4889                let rd_lo_bits = reg_to_bits(rd_lo);
4890                let rd_hi_bits = reg_to_bits(rd_hi);
4891                let rn_lo_bits = reg_to_bits(rn_lo);
4892                let rn_hi_bits = reg_to_bits(rn_hi);
4893                let rm_lo_bits = reg_to_bits(rm_lo);
4894                let r12: u32 = 12; // the only scratch — never allocatable
4895                let mut bytes = Vec::new();
4896
4897                // #1039 house style: rd_lo is written before rn_hi and rm_lo
4898                // are last read — refuse the collision loudly. The in-place
4899                // form rd == rn (select_default) has rd_lo == rn_lo and stays
4900                // legal.
4901                if rd_lo_bits == rn_hi_bits || rd_lo_bits == rm_lo_bits {
4902                    return Err(synth_core::Error::synthesis(format!(
4903                        "I64ShrU: rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4904                    )));
4905                }
4906
4907                // AND.W R12, rm_lo, #63  (n — the amount register is only READ)
4908                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4909                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4910                bytes.extend_from_slice(&hw1.to_le_bytes());
4911                bytes.extend_from_slice(&hw2.to_le_bytes());
4912
4913                // SUBS.W R12, R12, #32  (R12 = n-32, sets flags)
4914                let hw1: u16 = (0xF1B0 | r12) as u16;
4915                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4916                bytes.extend_from_slice(&hw1.to_le_bytes());
4917                bytes.extend_from_slice(&hw2.to_le_bytes());
4918
4919                // BPL .large (+14 halfwords)
4920                let bpl: u16 = 0xD50E;
4921                bytes.extend_from_slice(&bpl.to_le_bytes());
4922
4923                // --- Small shift (n < 32) ---
4924                // AND.W R12, rm_lo, #63  (n again — R12 held n-32)
4925                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4926                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4927                bytes.extend_from_slice(&hw1.to_le_bytes());
4928                bytes.extend_from_slice(&hw2.to_le_bytes());
4929
4930                // LSR.W rd_lo, rn_lo, R12  (lo >> n; rn_lo's last read)
4931                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4932                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4933                bytes.extend_from_slice(&hw1.to_le_bytes());
4934                bytes.extend_from_slice(&hw2.to_le_bytes());
4935
4936                // RSB.W R12, R12, #32  (R12 = 32-n)
4937                let hw1: u16 = (0xF1C0 | r12) as u16;
4938                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4939                bytes.extend_from_slice(&hw1.to_le_bytes());
4940                bytes.extend_from_slice(&hw2.to_le_bytes());
4941
4942                // LSL.W R12, rn_hi, R12  (overflow = hi << (32-n); n=0 gives
4943                // a register shift by 32 which yields 0 — exact)
4944                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4945                let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4946                bytes.extend_from_slice(&hw1.to_le_bytes());
4947                bytes.extend_from_slice(&hw2.to_le_bytes());
4948
4949                // ORR.W rd_lo, rd_lo, R12  (lo |= overflow from hi)
4950                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4951                let hw2: u16 = ((rd_lo_bits << 8) | r12) as u16;
4952                bytes.extend_from_slice(&hw1.to_le_bytes());
4953                bytes.extend_from_slice(&hw2.to_le_bytes());
4954
4955                // AND.W R12, rm_lo, #63  (n once more for the high half)
4956                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4957                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4958                bytes.extend_from_slice(&hw1.to_le_bytes());
4959                bytes.extend_from_slice(&hw2.to_le_bytes());
4960
4961                // LSR.W rd_hi, rn_hi, R12  (hi >> n, logical)
4962                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4963                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4964                bytes.extend_from_slice(&hw1.to_le_bytes());
4965                bytes.extend_from_slice(&hw2.to_le_bytes());
4966
4967                // B .done — see I64Shl: `.done` is the END of the expansion,
4968                // past the trailing zero-fill, so the displacement is derived
4969                // from that zero-fill's real width (#916).
4970                let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_hi_bits);
4971                let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4972                bytes.extend_from_slice(&b_done.to_le_bytes());
4973
4974                // --- Large shift (n >= 32) --- (R12 still holds n-32)
4975                // LSR.W rd_lo, rn_hi, R12  (lo = hi >> (n-32))
4976                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4977                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4978                bytes.extend_from_slice(&hw1.to_le_bytes());
4979                bytes.extend_from_slice(&hw2.to_le_bytes());
4980
4981                // MOV rd_hi, #0 (#916: MOV.W for rd_hi >= R8). Order is
4982                // load-bearing: the LSR.W above reads rn_hi, which may BE
4983                // rd_hi in the in-place case.
4984                emit_thumb_zero_fill(&mut bytes, rd_hi_bits);
4985
4986                Ok(bytes) // 46 bytes (48 when rd_hi >= R8 takes MOV.W)
4987            }
4988
4989            // I64ShrS: 64-bit arithmetic shift right with branch for n<32 vs
4990            // n>=32. #1048: R12-only scratch, operands never written — see
4991            // the I64Shl comment for the full rationale.
4992            ArmOp::I64ShrS {
4993                rd_lo,
4994                rd_hi,
4995                rn_lo,
4996                rn_hi,
4997                rm_lo,
4998                rm_hi: _,
4999            } => {
5000                let rd_lo_bits = reg_to_bits(rd_lo);
5001                let rd_hi_bits = reg_to_bits(rd_hi);
5002                let rn_lo_bits = reg_to_bits(rn_lo);
5003                let rn_hi_bits = reg_to_bits(rn_hi);
5004                let rm_lo_bits = reg_to_bits(rm_lo);
5005                let r12: u32 = 12; // the only scratch — never allocatable
5006                let mut bytes = Vec::new();
5007
5008                // #1039 house style: rd_lo is written before rn_hi and rm_lo
5009                // are last read (on BOTH arms of the diamond — the large arm's
5010                // trailing `ASR rd_hi, rn_hi, #31` also reads rn_hi after
5011                // rd_lo is written). The in-place form rd == rn stays legal.
5012                if rd_lo_bits == rn_hi_bits || rd_lo_bits == rm_lo_bits {
5013                    return Err(synth_core::Error::synthesis(format!(
5014                        "I64ShrS: rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
5015                    )));
5016                }
5017
5018                // AND.W R12, rm_lo, #63  (n — the amount register is only READ)
5019                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
5020                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
5021                bytes.extend_from_slice(&hw1.to_le_bytes());
5022                bytes.extend_from_slice(&hw2.to_le_bytes());
5023
5024                // SUBS.W R12, R12, #32  (R12 = n-32, sets flags)
5025                let hw1: u16 = (0xF1B0 | r12) as u16;
5026                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
5027                bytes.extend_from_slice(&hw1.to_le_bytes());
5028                bytes.extend_from_slice(&hw2.to_le_bytes());
5029
5030                // BPL .large (+14 halfwords)
5031                let bpl: u16 = 0xD50E;
5032                bytes.extend_from_slice(&bpl.to_le_bytes());
5033
5034                // --- Small shift (n < 32) ---
5035                // AND.W R12, rm_lo, #63  (n again — R12 held n-32)
5036                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
5037                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
5038                bytes.extend_from_slice(&hw1.to_le_bytes());
5039                bytes.extend_from_slice(&hw2.to_le_bytes());
5040
5041                // LSR.W rd_lo, rn_lo, R12  (lo >> n, logical for lo word)
5042                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
5043                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
5044                bytes.extend_from_slice(&hw1.to_le_bytes());
5045                bytes.extend_from_slice(&hw2.to_le_bytes());
5046
5047                // RSB.W R12, R12, #32  (R12 = 32-n)
5048                let hw1: u16 = (0xF1C0 | r12) as u16;
5049                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
5050                bytes.extend_from_slice(&hw1.to_le_bytes());
5051                bytes.extend_from_slice(&hw2.to_le_bytes());
5052
5053                // LSL.W R12, rn_hi, R12  (overflow = hi << (32-n); n=0 gives
5054                // a register shift by 32 which yields 0 — exact)
5055                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
5056                let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
5057                bytes.extend_from_slice(&hw1.to_le_bytes());
5058                bytes.extend_from_slice(&hw2.to_le_bytes());
5059
5060                // ORR.W rd_lo, rd_lo, R12  (lo |= overflow from hi)
5061                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
5062                let hw2: u16 = ((rd_lo_bits << 8) | r12) as u16;
5063                bytes.extend_from_slice(&hw1.to_le_bytes());
5064                bytes.extend_from_slice(&hw2.to_le_bytes());
5065
5066                // AND.W R12, rm_lo, #63  (n once more for the high half)
5067                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
5068                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
5069                bytes.extend_from_slice(&hw1.to_le_bytes());
5070                bytes.extend_from_slice(&hw2.to_le_bytes());
5071
5072                // ASR.W rd_hi, rn_hi, R12  (hi >> n, arithmetic/sign-extending)
5073                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
5074                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
5075                bytes.extend_from_slice(&hw1.to_le_bytes());
5076                bytes.extend_from_slice(&hw2.to_le_bytes());
5077
5078                // B .done (+3 halfwords, large shift is 8 bytes)
5079                let b_done: u16 = 0xE003;
5080                bytes.extend_from_slice(&b_done.to_le_bytes());
5081
5082                // --- Large shift (n >= 32) --- (R12 still holds n-32)
5083                // ASR.W rd_lo, rn_hi, R12  (lo = hi >>> (n-32))
5084                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
5085                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
5086                bytes.extend_from_slice(&hw1.to_le_bytes());
5087                bytes.extend_from_slice(&hw2.to_le_bytes());
5088
5089                // ASR.W rd_hi, rn_hi, #31  (hi = sign extension, all 0s or all 1s)
5090                // Thumb-2 ASR immediate: hw1=0xEA4F, hw2=imm3:Rd:imm2:10:Rm
5091                // imm5=31=11111 → imm3=111, imm2=11
5092                let hw1: u16 = 0xEA4F;
5093                let hw2: u16 = (0x7000 | (rd_hi_bits << 8) | 0x00E0 | rn_hi_bits) as u16;
5094                bytes.extend_from_slice(&hw1.to_le_bytes());
5095                bytes.extend_from_slice(&hw2.to_le_bytes());
5096
5097                Ok(bytes) // Total: 48 bytes
5098            }
5099
5100            // I64Rotl: 64-bit rotate left (#610 rewrite).
5101            // For n < 32: new_hi = (hi << n) | (lo >> (32-n)), new_lo = (lo << n) | (hi >> (32-n))
5102            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
5103            //
5104            // Fixed-reg core: value in R0:R1, amount in R2, scratch R3 + R12
5105            // (all four saved/marshaled by the #610 fixed-ABI wrapper; the
5106            // pre-#610 expansion wrote through the selector's registers with
5107            // colliding R3/R4 scratch and restored the saved R4 OVER the
5108            // result). Relies on ARM register-shift semantics: amounts >= 32
5109            // yield 0 for LSL/LSR, which makes n = 0 and n = 32 exact.
5110            ArmOp::I64Rotl {
5111                rdlo,
5112                rdhi,
5113                rnlo,
5114                rnhi,
5115                shift,
5116            } => {
5117                let mut bytes = Vec::new();
5118                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
5119
5120                let core: [u16; 35] = [
5121                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
5122                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
5123                    0xD50E, //         BPL    .large        (n >= 32)
5124                    // --- small rotation (n < 32) ---
5125                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
5126                    0xFA20, 0xFC03, // LSR.W  R12, R0, R3   (lo >> (32-n))
5127                    0xFA21, 0xF303, // LSR.W  R3, R1, R3    (hi >> (32-n))
5128                    0xFA01, 0xF102, // LSL.W  R1, R1, R2    (hi << n)
5129                    0xEA41, 0x010C, // ORR.W  R1, R1, R12   (new_hi)
5130                    0xFA00, 0xF002, // LSL.W  R0, R0, R2    (lo << n)
5131                    0xEA40, 0x0003, // ORR.W  R0, R0, R3    (new_lo)
5132                    0xE00E, //         B      .done
5133                    // --- large rotation (n >= 32), R3 = m = n-32 ---
5134                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
5135                    0xFA21, 0xFC02, // LSR.W  R12, R1, R2   (hi >> (64-n))
5136                    0xFA20, 0xF202, // LSR.W  R2, R0, R2    (lo >> (64-n))
5137                    0xFA00, 0xF003, // LSL.W  R0, R0, R3    (lo << m)
5138                    0xFA01, 0xF103, // LSL.W  R1, R1, R3    (hi << m)
5139                    0xEA40, 0x0C0C, // ORR.W  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
5140                    0xEA41, 0x0002, // ORR.W  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
5141                    0x4661, //         MOV    R1, R12       (new_hi into place)
5142                            // .done: result in R0:R1
5143                ];
5144                for hw in core {
5145                    bytes.extend_from_slice(&hw.to_le_bytes());
5146                }
5147
5148                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5149                Ok(bytes) // Total: 102 bytes
5150            }
5151
5152            // I64Rotr: 64-bit rotate right (#610 rewrite).
5153            // For n < 32: new_lo = (lo >> n) | (hi << (32-n)), new_hi = (hi >> n) | (lo << (32-n))
5154            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
5155            //
5156            // Same fixed-reg core contract as I64Rotl: value in R0:R1, amount
5157            // in R2, scratch R3 + R12, all covered by the fixed-ABI wrapper.
5158            ArmOp::I64Rotr {
5159                rdlo,
5160                rdhi,
5161                rnlo,
5162                rnhi,
5163                shift,
5164            } => {
5165                let mut bytes = Vec::new();
5166                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
5167
5168                let core: [u16; 35] = [
5169                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
5170                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
5171                    0xD50E, //         BPL    .large        (n >= 32)
5172                    // --- small rotation (n < 32) ---
5173                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
5174                    0xFA01, 0xFC03, // LSL.W  R12, R1, R3   (hi << (32-n))
5175                    0xFA00, 0xF303, // LSL.W  R3, R0, R3    (lo << (32-n))
5176                    0xFA20, 0xF002, // LSR.W  R0, R0, R2    (lo >> n)
5177                    0xEA40, 0x000C, // ORR.W  R0, R0, R12   (new_lo)
5178                    0xFA21, 0xF102, // LSR.W  R1, R1, R2    (hi >> n)
5179                    0xEA41, 0x0103, // ORR.W  R1, R1, R3    (new_hi)
5180                    0xE00E, //         B      .done
5181                    // --- large rotation (n >= 32), R3 = m = n-32 ---
5182                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
5183                    0xFA00, 0xFC02, // LSL.W  R12, R0, R2   (lo << (64-n))
5184                    0xFA01, 0xF202, // LSL.W  R2, R1, R2    (hi << (64-n))
5185                    0xFA21, 0xF103, // LSR.W  R1, R1, R3    (hi >> m)
5186                    0xEA41, 0x0C0C, // ORR.W  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
5187                    0xFA20, 0xF103, // LSR.W  R1, R0, R3    (lo >> m)
5188                    0xEA41, 0x0102, // ORR.W  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
5189                    0x4660, //         MOV    R0, R12       (new_lo into place)
5190                            // .done: result in R0:R1
5191                ];
5192                for hw in core {
5193                    bytes.extend_from_slice(&hw.to_le_bytes());
5194                }
5195
5196                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5197                Ok(bytes) // Total: 102 bytes
5198            }
5199
5200            // I64Clz: Count leading zeros in 64-bit value
5201            // If hi != 0: result = CLZ(hi)
5202            // If hi == 0: result = 32 + CLZ(lo)
5203            //
5204            // Layout (using CMP+BNE approach for consistency):
5205            // 0: CMP.W rnhi, #0 (4 bytes)
5206            // 4: BEQ .hi_zero (2 bytes) - branch forward to offset 14
5207            // 6: CLZ.W rd, rnhi (4 bytes)
5208            // 10: B .done (2 bytes) - branch forward to offset 22
5209            // 12: NOP (2 bytes) - padding for alignment
5210            // 14: .hi_zero: CLZ.W rd, rnlo (4 bytes)
5211            // 18: ADD.W rd, rd, #32 (4 bytes)
5212            // 22: .done
5213            ArmOp::I64Clz { rd, rnlo, rnhi } => {
5214                let rd_bits = reg_to_bits(rd);
5215                let rn_lo_bits = reg_to_bits(rnlo);
5216                let rn_hi_bits = reg_to_bits(rnhi);
5217                let mut bytes = Vec::new();
5218
5219                // CMP.W rnhi, #0 (4 bytes at offset 0)
5220                let hw1: u16 = (0xF1B0 | rn_hi_bits) as u16;
5221                let hw2: u16 = 0x0F00;
5222                bytes.extend_from_slice(&hw1.to_le_bytes());
5223                bytes.extend_from_slice(&hw2.to_le_bytes());
5224
5225                // BEQ .hi_zero (2 bytes at offset 4)
5226                // PC = 4 + 4 = 8, target = 14, offset = 6, imm8 = 3
5227                let beq: u16 = 0xD003;
5228                bytes.extend_from_slice(&beq.to_le_bytes());
5229
5230                // CLZ.W rd, rnhi (4 bytes at offset 6)
5231                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5232                let hw1: u16 = (0xFAB0 | rn_hi_bits) as u16;
5233                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_hi_bits) as u16;
5234                bytes.extend_from_slice(&hw1.to_le_bytes());
5235                bytes.extend_from_slice(&hw2.to_le_bytes());
5236
5237                // B .done (2 bytes at offset 10)
5238                // PC = 10 + 4 = 14, target = 22, offset = 8, imm11 = 4
5239                let b_done: u16 = 0xE004;
5240                bytes.extend_from_slice(&b_done.to_le_bytes());
5241
5242                // NOP (2 bytes at offset 12) - padding
5243                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5244
5245                // .hi_zero: (offset 14)
5246                // CLZ.W rd, rnlo (4 bytes)
5247                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5248                let hw1: u16 = (0xFAB0 | rn_lo_bits) as u16;
5249                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_lo_bits) as u16;
5250                bytes.extend_from_slice(&hw1.to_le_bytes());
5251                bytes.extend_from_slice(&hw2.to_le_bytes());
5252
5253                // ADD.W rd, rd, #32 (4 bytes at offset 18)
5254                let hw1: u16 = (0xF100 | rd_bits) as u16;
5255                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5256                bytes.extend_from_slice(&hw1.to_le_bytes());
5257                bytes.extend_from_slice(&hw2.to_le_bytes());
5258
5259                // .done: (offset 22 — the end of the expansion)
5260                //
5261                // #1048: the former trailing hi-word clear (`MOV rnhi, #0`)
5262                // is GONE. It was aimed at the RESULT's high half but wrote
5263                // the OPERAND's home high register — a real executed
5264                // miscompile on the direct selector, which allocates a fresh
5265                // destination pair and zeroes its own dst_hi, leaving the
5266                // operand's hi limb destroyed for any later re-read. The
5267                // Rocq/SMT models of I64ClzPseudo always said "writes rd
5268                // ONLY"; the callers that relied on the implicit clear
5269                // (select_default, optimizer_bridge) now emit their own
5270                // explicit hi-zero op. `B .done` above targets offset 22 =
5271                // past-the-end, `BEQ` targets offset 14 — no displacement
5272                // moves.
5273
5274                Ok(bytes) // 22 bytes, register-independent
5275            }
5276
5277            // I64Ctz: Count trailing zeros in 64-bit value
5278            // If lo != 0: result = CTZ(lo) = CLZ(RBIT(lo))
5279            // If lo == 0: result = 32 + CTZ(hi) = 32 + CLZ(RBIT(hi))
5280            //
5281            // Layout:
5282            // 0: CMP.W rnlo, #0 (4 bytes)
5283            // 4: BEQ .lo_zero (2 bytes) - branch to offset 18
5284            // 6: RBIT.W rd, rnlo (4 bytes)
5285            // 10: CLZ.W rd, rd (4 bytes)
5286            // 14: B .done (2 bytes) - branch to offset 30
5287            // 16: NOP (2 bytes) - padding
5288            // 18: .lo_zero: RBIT.W rd, rnhi (4 bytes)
5289            // 22: CLZ.W rd, rd (4 bytes)
5290            // 26: ADD.W rd, rd, #32 (4 bytes)
5291            // 30: .done
5292            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
5293                let rd_bits = reg_to_bits(rd);
5294                let rn_lo_bits = reg_to_bits(rnlo);
5295                let rn_hi_bits = reg_to_bits(rnhi);
5296                let mut bytes = Vec::new();
5297
5298                // CMP.W rnlo, #0 (4 bytes at offset 0)
5299                let hw1: u16 = (0xF1B0 | rn_lo_bits) as u16;
5300                let hw2: u16 = 0x0F00;
5301                bytes.extend_from_slice(&hw1.to_le_bytes());
5302                bytes.extend_from_slice(&hw2.to_le_bytes());
5303
5304                // BEQ .lo_zero (2 bytes at offset 4)
5305                // PC = 4 + 4 = 8, target = 18, offset = 10, imm8 = 5
5306                let beq: u16 = 0xD005;
5307                bytes.extend_from_slice(&beq.to_le_bytes());
5308
5309                // RBIT.W rd, rnlo (4 bytes at offset 6)
5310                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5311                let hw1: u16 = (0xFA90 | rn_lo_bits) as u16;
5312                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_lo_bits) as u16;
5313                bytes.extend_from_slice(&hw1.to_le_bytes());
5314                bytes.extend_from_slice(&hw2.to_le_bytes());
5315
5316                // CLZ.W rd, rd (4 bytes at offset 10)
5317                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5318                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5319                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5320                bytes.extend_from_slice(&hw1.to_le_bytes());
5321                bytes.extend_from_slice(&hw2.to_le_bytes());
5322
5323                // B .done (2 bytes at offset 14)
5324                // PC = 14 + 4 = 18, target = 30, offset = 12, imm11 = 6
5325                let b_done: u16 = 0xE006;
5326                bytes.extend_from_slice(&b_done.to_le_bytes());
5327
5328                // NOP (2 bytes at offset 16) - padding
5329                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5330
5331                // .lo_zero: (offset 18)
5332                // RBIT.W rd, rnhi (4 bytes)
5333                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5334                let hw1: u16 = (0xFA90 | rn_hi_bits) as u16;
5335                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_hi_bits) as u16;
5336                bytes.extend_from_slice(&hw1.to_le_bytes());
5337                bytes.extend_from_slice(&hw2.to_le_bytes());
5338
5339                // CLZ.W rd, rd (4 bytes at offset 22)
5340                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5341                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5342                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5343                bytes.extend_from_slice(&hw1.to_le_bytes());
5344                bytes.extend_from_slice(&hw2.to_le_bytes());
5345
5346                // ADD.W rd, rd, #32 (4 bytes at offset 26)
5347                let hw1: u16 = (0xF100 | rd_bits) as u16;
5348                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5349                bytes.extend_from_slice(&hw1.to_le_bytes());
5350                bytes.extend_from_slice(&hw2.to_le_bytes());
5351
5352                // .done: (offset 30 — the end of the expansion)
5353                // #1048: the former trailing `MOV rnhi, #0` is GONE — it
5354                // wrote the OPERAND's home high register (see the I64Clz
5355                // comment above). `B .done` targets offset 30 = past-the-end,
5356                // `BEQ` targets offset 18 — no displacement moves.
5357
5358                Ok(bytes) // 30 bytes, register-independent
5359            }
5360
5361            // I64Popcnt: Population count of 64-bit value
5362            // result = POPCNT(lo) + POPCNT(hi)
5363            // Using SIMD-style parallel bit counting algorithm
5364            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
5365                let rd_bits = reg_to_bits(rd);
5366                let rn_lo_bits = reg_to_bits(rnlo);
5367                let rn_hi_bits = reg_to_bits(rnhi);
5368                let r12: u32 = 12; // IP scratch
5369                let r3: u32 = 3; // Scratch for hi popcnt result
5370                let mut bytes = Vec::new();
5371
5372                // PUSH {R3, R4, R5} - save scratch registers
5373                bytes.extend_from_slice(&0xB438u16.to_le_bytes());
5374
5375                // Strategy: compute popcnt(lo) -> R4, popcnt(hi) -> R5, add them -> rd
5376                // Using lookup table approach for each byte would be too large
5377                // Using shift-and-add approach instead
5378
5379                // For simplicity and correctness, use the efficient parallel algorithm
5380                // but implement it as a series of inline operations
5381
5382                // Marshal the operand pair into the fixed scratch regs, routing
5383                // rnlo through R12 (#632 audit): writing R4 first corrupted the
5384                // rnhi read for a pair living at (R3,R4) — every source is read
5385                // before any scratch register it could occupy is written.
5386                // MOV R12, rnlo
5387                let mov: u16 = (0x4600 | (1 << 7) | (rn_lo_bits << 3) | 4) as u16;
5388                bytes.extend_from_slice(&mov.to_le_bytes());
5389                // MOV R5, rnhi (R4 untouched so far; rnhi == R5 is a no-op)
5390                let mov: u16 = (0x4600 | (rn_hi_bits << 3) | 5) as u16;
5391                bytes.extend_from_slice(&mov.to_le_bytes());
5392                // MOV R4, R12
5393                bytes.extend_from_slice(&0x4664u16.to_le_bytes());
5394
5395                // --- POPCNT for R4 (lo word) ---
5396                // Step 1: x = x - ((x >> 1) & 0x55555555)
5397                // LSR.W R12, R4, #1
5398                let hw1: u16 = 0xEA4F;
5399                let hw2: u16 = ((r12 << 8) | 0x50 | 4) as u16;
5400                bytes.extend_from_slice(&hw1.to_le_bytes());
5401                bytes.extend_from_slice(&hw2.to_le_bytes());
5402
5403                // Load 0x55555555 into R3 using MOVW/MOVT
5404                // MOVW R3, #0x5555
5405                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5406                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5407                // MOVT R3, #0x5555
5408                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5409                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5410
5411                // AND.W R12, R12, R3
5412                let hw1: u16 = (0xEA00 | r12) as u16;
5413                let hw2: u16 = ((r12 << 8) | r3) as u16;
5414                bytes.extend_from_slice(&hw1.to_le_bytes());
5415                bytes.extend_from_slice(&hw2.to_le_bytes());
5416
5417                // SUB.W R4, R4, R12
5418                let hw1: u16 = (0xEBA0 | 4) as u16;
5419                let hw2: u16 = ((4 << 8) | r12) as u16;
5420                bytes.extend_from_slice(&hw1.to_le_bytes());
5421                bytes.extend_from_slice(&hw2.to_le_bytes());
5422
5423                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5424                // Load 0x33333333 into R3
5425                // MOVW R3, #0x3333
5426                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5427                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5428                // MOVT R3, #0x3333
5429                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5430                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5431
5432                // AND.W R12, R4, R3
5433                let hw1: u16 = (0xEA00 | 4) as u16;
5434                let hw2: u16 = ((r12 << 8) | r3) as u16;
5435                bytes.extend_from_slice(&hw1.to_le_bytes());
5436                bytes.extend_from_slice(&hw2.to_le_bytes());
5437
5438                // LSR.W R4, R4, #2
5439                let hw1: u16 = 0xEA4F;
5440                let hw2: u16 = ((4 << 8) | 0x90 | 4) as u16;
5441                bytes.extend_from_slice(&hw1.to_le_bytes());
5442                bytes.extend_from_slice(&hw2.to_le_bytes());
5443
5444                // AND.W R4, R4, R3
5445                let hw1: u16 = (0xEA00 | 4) as u16;
5446                let hw2: u16 = ((4 << 8) | r3) as u16;
5447                bytes.extend_from_slice(&hw1.to_le_bytes());
5448                bytes.extend_from_slice(&hw2.to_le_bytes());
5449
5450                // ADD.W R4, R4, R12
5451                let hw1: u16 = (0xEB00 | 4) as u16;
5452                let hw2: u16 = ((4 << 8) | r12) as u16;
5453                bytes.extend_from_slice(&hw1.to_le_bytes());
5454                bytes.extend_from_slice(&hw2.to_le_bytes());
5455
5456                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5457                // LSR.W R12, R4, #4
5458                // hw2 = (imm3 << 12) | (Rd << 8) | (imm2 << 6) | (type << 4) | Rm
5459                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5460                let hw1: u16 = 0xEA4F;
5461                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 4) as u16;
5462                bytes.extend_from_slice(&hw1.to_le_bytes());
5463                bytes.extend_from_slice(&hw2.to_le_bytes());
5464
5465                // ADD.W R4, R4, R12
5466                let hw1: u16 = (0xEB00 | 4) as u16;
5467                let hw2: u16 = ((4 << 8) | r12) as u16;
5468                bytes.extend_from_slice(&hw1.to_le_bytes());
5469                bytes.extend_from_slice(&hw2.to_le_bytes());
5470
5471                // Load 0x0F0F0F0F into R3
5472                // MOVW R3, #0x0F0F (imm4=0, i=1, imm3=7, imm8=0x0F)
5473                // hw1 = 11110 1 10 0100 0000 = 0xF640
5474                // hw2 = 0 111 0011 00001111 = 0x730F
5475                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5476                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5477                // MOVT R3, #0x0F0F
5478                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5479                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5480
5481                // AND.W R4, R4, R3
5482                let hw1: u16 = (0xEA00 | 4) as u16;
5483                let hw2: u16 = ((4 << 8) | r3) as u16;
5484                bytes.extend_from_slice(&hw1.to_le_bytes());
5485                bytes.extend_from_slice(&hw2.to_le_bytes());
5486
5487                // Step 4: x = x * 0x01010101 >> 24
5488                // Load 0x01010101 into R3
5489                // MOVW R3, #0x0101
5490                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5491                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5492                // MOVT R3, #0x0101
5493                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5494                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5495
5496                // MUL R4, R4, R3
5497                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5498                let hw1: u16 = (0xFB00 | 4) as u16;
5499                let hw2: u16 = (0xF000 | (4 << 8) | r3) as u16;
5500                bytes.extend_from_slice(&hw1.to_le_bytes());
5501                bytes.extend_from_slice(&hw2.to_le_bytes());
5502
5503                // LSR.W R4, R4, #24
5504                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5505                let hw1: u16 = 0xEA4F;
5506                let hw2: u16 = (0x6000 | (4 << 8) | 0x10 | 4) as u16;
5507                bytes.extend_from_slice(&hw1.to_le_bytes());
5508                bytes.extend_from_slice(&hw2.to_le_bytes());
5509
5510                // --- POPCNT for R5 (hi word) - same algorithm ---
5511                // Step 1
5512                let hw1: u16 = 0xEA4F;
5513                let hw2: u16 = ((r12 << 8) | 0x50 | 5) as u16;
5514                bytes.extend_from_slice(&hw1.to_le_bytes());
5515                bytes.extend_from_slice(&hw2.to_le_bytes());
5516
5517                // Load 0x55555555 into R3
5518                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5519                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5520                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5521                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5522
5523                let hw1: u16 = (0xEA00 | r12) as u16;
5524                let hw2: u16 = ((r12 << 8) | r3) as u16;
5525                bytes.extend_from_slice(&hw1.to_le_bytes());
5526                bytes.extend_from_slice(&hw2.to_le_bytes());
5527
5528                let hw1: u16 = (0xEBA0 | 5) as u16;
5529                let hw2: u16 = ((5 << 8) | r12) as u16;
5530                bytes.extend_from_slice(&hw1.to_le_bytes());
5531                bytes.extend_from_slice(&hw2.to_le_bytes());
5532
5533                // Step 2
5534                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5535                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5536                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5537                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5538
5539                let hw1: u16 = (0xEA00 | 5) as u16;
5540                let hw2: u16 = ((r12 << 8) | r3) as u16;
5541                bytes.extend_from_slice(&hw1.to_le_bytes());
5542                bytes.extend_from_slice(&hw2.to_le_bytes());
5543
5544                let hw1: u16 = 0xEA4F;
5545                let hw2: u16 = ((5 << 8) | 0x90 | 5) as u16;
5546                bytes.extend_from_slice(&hw1.to_le_bytes());
5547                bytes.extend_from_slice(&hw2.to_le_bytes());
5548
5549                let hw1: u16 = (0xEA00 | 5) as u16;
5550                let hw2: u16 = ((5 << 8) | r3) as u16;
5551                bytes.extend_from_slice(&hw1.to_le_bytes());
5552                bytes.extend_from_slice(&hw2.to_le_bytes());
5553
5554                let hw1: u16 = (0xEB00 | 5) as u16;
5555                let hw2: u16 = ((5 << 8) | r12) as u16;
5556                bytes.extend_from_slice(&hw1.to_le_bytes());
5557                bytes.extend_from_slice(&hw2.to_le_bytes());
5558
5559                // Step 3: LSR.W R12, R5, #4
5560                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5561                let hw1: u16 = 0xEA4F;
5562                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 5) as u16;
5563                bytes.extend_from_slice(&hw1.to_le_bytes());
5564                bytes.extend_from_slice(&hw2.to_le_bytes());
5565
5566                let hw1: u16 = (0xEB00 | 5) as u16;
5567                let hw2: u16 = ((5 << 8) | r12) as u16;
5568                bytes.extend_from_slice(&hw1.to_le_bytes());
5569                bytes.extend_from_slice(&hw2.to_le_bytes());
5570
5571                // Load 0x0F0F0F0F into R3 (for hi-word)
5572                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5573                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5574                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5575                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5576
5577                let hw1: u16 = (0xEA00 | 5) as u16;
5578                let hw2: u16 = ((5 << 8) | r3) as u16;
5579                bytes.extend_from_slice(&hw1.to_le_bytes());
5580                bytes.extend_from_slice(&hw2.to_le_bytes());
5581
5582                // Step 4
5583                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5584                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5585                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5586                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5587
5588                // MUL R5, R5, R3
5589                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5590                let hw1: u16 = (0xFB00 | 5) as u16;
5591                let hw2: u16 = (0xF000 | (5 << 8) | r3) as u16;
5592                bytes.extend_from_slice(&hw1.to_le_bytes());
5593                bytes.extend_from_slice(&hw2.to_le_bytes());
5594
5595                // LSR.W R5, R5, #24
5596                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5597                let hw1: u16 = 0xEA4F;
5598                let hw2: u16 = (0x6000 | (5 << 8) | 0x10 | 5) as u16;
5599                bytes.extend_from_slice(&hw1.to_le_bytes());
5600                bytes.extend_from_slice(&hw2.to_le_bytes());
5601
5602                // #632: the count must be carried ACROSS the scratch restore
5603                // in a register the POP cannot touch. rd is allocator-assigned
5604                // (any of R0-R8) and can land inside the {R3,R4,R5} restore set
5605                // — the old `ADDS rd, R4, R5; POP {R3,R4,R5}` destroyed the
5606                // result one instruction after computing it (0 for every input
5607                // under qemu). R12 is encoder scratch: never allocatable (#212)
5608                // and never in a restore set, so no choice of rd can collide.
5609                // ADD.W R12, R4, R5
5610                bytes.extend_from_slice(&0xEB04u16.to_le_bytes());
5611                bytes.extend_from_slice(&0x0C05u16.to_le_bytes());
5612
5613                // POP {R3, R4, R5}
5614                bytes.extend_from_slice(&0xBC38u16.to_le_bytes());
5615
5616                // MOV rd, R12 — after the restore. The 4-bit Rd (D:rd) form is
5617                // also total over rd = R8, where the old ADDS T1 3-bit field
5618                // silently corrupted the encoding (#178/#180 class).
5619                let mov: u16 =
5620                    (0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7)) as u16;
5621                bytes.extend_from_slice(&mov.to_le_bytes());
5622
5623                // #1048: the former trailing `MOV.W rnhi, #0` hi-word clear
5624                // is GONE — it wrote the OPERAND's home high register (see
5625                // the I64Clz comment). Callers that relied on the implicit
5626                // clear emit their own explicit hi-zero op.
5627
5628                Ok(bytes)
5629            }
5630
5631            // I64Extend8S: Sign-extend low 8 bits to 64 bits
5632            // Result: rdlo = sign_extend_8(rnlo), rdhi = rdlo >> 31
5633            ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
5634                let rdlo_bits = reg_to_bits(rdlo);
5635                let rdhi_bits = reg_to_bits(rdhi);
5636                let rnlo_bits = reg_to_bits(rnlo);
5637                let mut bytes = Vec::new();
5638
5639                // SXTB.W rdlo, rnlo (sign-extend byte to 32-bit)
5640                // SXTB T2: hw1 = 0xFA4F, hw2 = 0xF0<Rd><Rm>
5641                let hw1: u16 = 0xFA4F_u16;
5642                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5643                bytes.extend_from_slice(&hw1.to_le_bytes());
5644                bytes.extend_from_slice(&hw2.to_le_bytes());
5645
5646                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5647                // ASR (immediate): hw1 = 0xEA4F, hw2 = imm3:Rd:imm2:type:Rm
5648                // For imm5=31: imm3=111, imm2=11, type=10 (ASR)
5649                // hw2 = (7 << 12) | (rdhi << 8) | (3 << 6) | (2 << 4) | rdlo
5650                let hw1: u16 = 0xEA4F;
5651                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5652                bytes.extend_from_slice(&hw1.to_le_bytes());
5653                bytes.extend_from_slice(&hw2.to_le_bytes());
5654
5655                Ok(bytes)
5656            }
5657
5658            // I64Extend16S: Sign-extend low 16 bits to 64 bits
5659            // Result: rdlo = sign_extend_16(rnlo), rdhi = rdlo >> 31
5660            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
5661                let rdlo_bits = reg_to_bits(rdlo);
5662                let rdhi_bits = reg_to_bits(rdhi);
5663                let rnlo_bits = reg_to_bits(rnlo);
5664                let mut bytes = Vec::new();
5665
5666                // SXTH.W rdlo, rnlo (sign-extend halfword to 32-bit)
5667                // SXTH T2: hw1 = 0xFA0F, hw2 = 0xF0<Rd><Rm>
5668                let hw1: u16 = 0xFA0F_u16;
5669                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5670                bytes.extend_from_slice(&hw1.to_le_bytes());
5671                bytes.extend_from_slice(&hw2.to_le_bytes());
5672
5673                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5674                let hw1: u16 = 0xEA4F;
5675                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5676                bytes.extend_from_slice(&hw1.to_le_bytes());
5677                bytes.extend_from_slice(&hw2.to_le_bytes());
5678
5679                Ok(bytes)
5680            }
5681
5682            // I64Extend32S: Sign-extend low 32 bits to 64 bits
5683            // Result: rdlo = rnlo, rdhi = rnlo >> 31
5684            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
5685                let rdlo_bits = reg_to_bits(rdlo);
5686                let rdhi_bits = reg_to_bits(rdhi);
5687                let rnlo_bits = reg_to_bits(rnlo);
5688                let mut bytes = Vec::new();
5689
5690                // MOV rdlo, rnlo (if different)
5691                if rdlo_bits != rnlo_bits {
5692                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5693                    let d_bit = ((rdlo_bits >> 3) & 1) as u16;
5694                    let mov: u16 = 0x4600
5695                        | (d_bit << 7)
5696                        | ((rnlo_bits as u16) << 3)
5697                        | ((rdlo_bits & 0x7) as u16);
5698                    bytes.extend_from_slice(&mov.to_le_bytes());
5699                }
5700
5701                // ASR.W rdhi, rnlo, #31 (sign-extend to high word)
5702                let hw1: u16 = 0xEA4F;
5703                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rnlo_bits) as u16;
5704                bytes.extend_from_slice(&hw1.to_le_bytes());
5705                bytes.extend_from_slice(&hw2.to_le_bytes());
5706
5707                Ok(bytes)
5708            }
5709
5710            // SelectMove: IT <cond>; MOV{cond} rd, rm
5711            // Conditional move: only execute MOV if condition is true
5712            ArmOp::SelectMove { rd, rm, cond } => {
5713                let rd_bits = reg_to_bits(rd) as u16;
5714                let rm_bits = reg_to_bits(rm) as u16;
5715
5716                // Condition code encoding for IT block
5717                use synth_synthesis::Condition;
5718                let cond_bits: u16 = match cond {
5719                    Condition::EQ => 0x0, // Equal
5720                    Condition::NE => 0x1, // Not equal
5721                    Condition::HS => 0x2, // Higher or same (unsigned >=)
5722                    Condition::LO => 0x3, // Lower (unsigned <)
5723                    Condition::HI => 0x8, // Higher (unsigned >)
5724                    Condition::LS => 0x9, // Lower or same (unsigned <=)
5725                    Condition::GE => 0xA, // Greater or equal (signed)
5726                    Condition::LT => 0xB, // Less than (signed)
5727                    Condition::GT => 0xC, // Greater than (signed)
5728                    Condition::LE => 0xD, // Less or equal (signed)
5729                };
5730
5731                // IT <cond>: single Then block (mask = 0x8 for T only)
5732                // IT instruction: 1011 1111 firstcond mask
5733                let it_instr: u16 = 0xBF00 | (cond_bits << 4) | 0x8;
5734
5735                // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5736                // This MOV will only execute if condition is true due to IT block
5737                let d_bit = (rd_bits >> 3) & 1;
5738                let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5739
5740                // Emit: IT <cond>, MOV rd, rm
5741                let mut bytes = it_instr.to_le_bytes().to_vec();
5742                bytes.extend_from_slice(&mov_instr.to_le_bytes());
5743                Ok(bytes)
5744            }
5745
5746            // Popcnt: Population count (count set bits)
5747            // ARM Cortex-M has no native POPCNT, so we implement the bit manipulation algorithm:
5748            // x = x - ((x >> 1) & 0x55555555);
5749            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
5750            // x = (x + (x >> 4)) & 0x0F0F0F0F;
5751            // x = x + (x >> 8);
5752            // x = x + (x >> 16);
5753            // return x & 0x3F;
5754            //
5755            // #1021: R12 (IP, never allocatable) is the ONLY scratch. The
5756            // previous expansion borrowed R11 as a second temp — but R11 is
5757            // the WASM linear-memory base, materialized at entry and read by
5758            // every later LDR/STR, and it is NOT in the pushed set, so the
5759            // clobber leaked to the CALLER too (a live memory-safety
5760            // miscompile: loads through `base = x >> 16`). The second temp is
5761            // eliminated the way the healthy i64.popcnt discipline implies —
5762            // never touch an unsaved register — but without its PUSH/POP
5763            // wrapper: the SWAR masks 0x55555555 / 0x33333333 / 0x0F0F0F0F
5764            // are all `0xXYXYXYXY` ThumbExpandImm modified immediates, so
5765            // each AND takes its mask from the instruction itself and R12
5766            // alone carries every intermediate. Straight-line, no branches,
5767            // no stack traffic — nothing to skip on a trap edge.
5768            ArmOp::Popcnt { rd, rm } => {
5769                let rd_bits = reg_to_bits(rd);
5770                // Defensive (#1021): rd = R11/R12/SP/PC would silently
5771                // corrupt the linear-memory base, the expansion's own
5772                // scratch, or the stack. The selector never assigns them
5773                // (pool R0-R8); refuse loudly if that ever changes.
5774                if rd_bits >= 11 {
5775                    return Err(synth_core::Error::synthesis(
5776                        "Popcnt destination must be R0-R10: R11 is the linear-memory \
5777                         base and R12 is the expansion's scratch (#1021)",
5778                    ));
5779                }
5780                let mut bytes = Vec::new();
5781
5782                // First, move rm to rd if they're different
5783                if rd != rm {
5784                    let rm_bits = reg_to_bits(rm) as u16;
5785                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5786                    let d_bit = ((rd_bits as u16) >> 3) & 1;
5787                    let mov_instr: u16 =
5788                        0x4600 | (d_bit << 7) | (rm_bits << 3) | ((rd_bits as u16) & 0x7);
5789                    bytes.extend_from_slice(&mov_instr.to_le_bytes());
5790                }
5791
5792                // Step 1: x = x - ((x >> 1) & 0x55555555)
5793                // R12 = rd >> 1
5794                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 1)?);
5795                // R12 = R12 & 0x55555555 (modified immediate, no constant reg)
5796                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(12, 12, 0x5555_5555)?);
5797                // rd = rd - R12
5798                bytes.extend_from_slice(&self.encode_thumb32_sub_reg_raw(rd_bits, rd_bits, 12)?);
5799
5800                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5801                // R12 = rd & 0x33333333
5802                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5803                    12,
5804                    rd_bits,
5805                    0x3333_3333,
5806                )?);
5807                // rd = rd >> 2
5808                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(rd_bits, rd_bits, 2)?);
5809                // rd = rd & 0x33333333
5810                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5811                    rd_bits,
5812                    rd_bits,
5813                    0x3333_3333,
5814                )?);
5815                // rd = rd + R12
5816                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5817
5818                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5819                // R12 = rd >> 4
5820                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 4)?);
5821                // rd = rd + R12
5822                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5823                // rd = rd & 0x0F0F0F0F
5824                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5825                    rd_bits,
5826                    rd_bits,
5827                    0x0F0F_0F0F,
5828                )?);
5829
5830                // Step 4: x = x + (x >> 8)
5831                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 8)?);
5832                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5833
5834                // Step 5: x = x + (x >> 16)
5835                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 16)?);
5836                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5837
5838                // Step 6: return x & 0x3F
5839                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(rd_bits, rd_bits, 0x3F)?);
5840
5841                Ok(bytes)
5842            }
5843
5844            // I64DivU: 64-bit unsigned division using binary long division
5845            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = quotient
5846            // Uses: R4-R7, R12 as loop counter (avoid R8 for Renode compatibility)
5847            //
5848            // #610: the fixed-ABI wrapper marshals the selector-assigned
5849            // operand registers into the core's fixed regs and lands the
5850            // result in rd — pre-#610 this arm IGNORED its register fields,
5851            // so the selector read its rd pair (e.g. R4:R5) after the core's
5852            // own POP restored the stale caller values over it: 0 for every
5853            // input. A zero divisor now traps (UDF #0), per WASM semantics.
5854            ArmOp::I64DivU {
5855                rdlo,
5856                rdhi,
5857                rnlo,
5858                rnhi,
5859                rmlo,
5860                rmhi,
5861                elide_zero_guard,
5862            } => {
5863                let mut bytes = Vec::new();
5864                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5865                // #494 phase 2b: elided only under a certificate-discharged
5866                // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
5867                if !elide_zero_guard {
5868                    emit_i64_divisor_zero_trap(&mut bytes);
5869                }
5870
5871                // PUSH {R4-R7} - save scratch registers (NO LR — this is inline code)
5872                // 16-bit PUSH: 1011 010 M rrrrrrrr where M=0 (no LR), r=R4-R7 = 0xF0
5873                // Encoding: 1011 0100 1111 0000 = 0xB4F0
5874                bytes.extend_from_slice(&0xB4F0u16.to_le_bytes());
5875
5876                // Initialize quotient (R4:R5) = 0
5877                bytes.extend_from_slice(&0x2400u16.to_le_bytes()); // MOV R4, #0
5878                bytes.extend_from_slice(&0x2500u16.to_le_bytes()); // MOV R5, #0
5879
5880                // Initialize remainder (R6:R7) = 0
5881                bytes.extend_from_slice(&0x2600u16.to_le_bytes()); // MOV R6, #0
5882                bytes.extend_from_slice(&0x2700u16.to_le_bytes()); // MOV R7, #0
5883
5884                // Initialize loop counter R12 = 64 (use R12 scratch instead of R8)
5885                // MOV.W R12, #64: F04F 0C40
5886                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5887                bytes.extend_from_slice(&0x0C40u16.to_le_bytes());
5888
5889                // Loop start
5890                let loop_start = bytes.len();
5891
5892                // === Loop body: process one bit ===
5893
5894                // 1. Shift quotient R4:R5 left by 1
5895                // LSLS R5, R5, #1 (16-bit: 0000 0010 1010 1101 = 0x006D -> actually 0x002D for LSL R5,R5,#1)
5896                // LSL Rd, Rm, #imm5: 000 00 imm5 Rm Rd = 000 00 00001 101 101 = 0x006D
5897                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5898                // Get carry from R4 into R5: ORR R5, R5, R4 LSR #31
5899                // Thumb-2 ORR with shifted register: EA45 75D4 = ORR.W R5, R5, R4, LSR #31
5900                // 11101010 010 S Rn | 0 imm3 Rd imm2 type Rm
5901                // type=01 (LSR), imm5=31 (imm3=111, imm2=11)
5902                bytes.extend_from_slice(&0xEA45u16.to_le_bytes());
5903                bytes.extend_from_slice(&0x75D4u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5904                // LSLS R4, R4, #1: 000 00 00001 100 100 = 0x0064
5905                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5906
5907                // 2. Shift remainder R6:R7 left by 1, OR in MSB of dividend R1
5908                // LSLS R7, R7, #1
5909                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5910                // ORR.W R7, R7, R6, LSR #31
5911                bytes.extend_from_slice(&0xEA47u16.to_le_bytes());
5912                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5913                // LSLS R6, R6, #1
5914                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5915                // ORR.W R6, R6, R1, LSR #31 (bring in MSB of dividend high)
5916                bytes.extend_from_slice(&0xEA46u16.to_le_bytes());
5917                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5918
5919                // 3. Shift dividend R0:R1 left by 1
5920                // LSLS R1, R1, #1
5921                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5922                // ORR.W R1, R1, R0, LSR #31
5923                bytes.extend_from_slice(&0xEA41u16.to_le_bytes());
5924                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5925                // LSLS R0, R0, #1
5926                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5927
5928                // 4. Compare remainder >= divisor (64-bit unsigned comparison)
5929                // Compare high words first: CMP R7, R3
5930                // CMP Rn, Rm encoding: 0x4280 | (Rm << 3) | Rn
5931                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3 (16-bit)
5932                // BHI means R7 > R3 (unsigned) - definitely subtract
5933                // BLO means R7 < R3 - definitely don't subtract
5934                // BEQ means need to check low words
5935
5936                // If high > divisor high: branch to subtract (forward +offset)
5937                // BHI.N +6 (skip CMP, skip BLO, do subtract)
5938                // BHI: 1101 1000 offset8 where cond=1000 (HI)
5939                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4 (to subtract block)
5940
5941                // If high < divisor high: branch past subtract
5942                // BLO.N +10 (skip to decrement)
5943                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BLO/BCC +12 (past subtract)
5944
5945                // High words equal, compare low: CMP R6, R2
5946                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2 (16-bit)
5947                // BLO/BCC past subtract (skip SUBS+SBC.W+ORR.W = 10 bytes = 4 halfwords from PC+4)
5948                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords (past subtract)
5949
5950                // === Subtract block: remainder -= divisor, quotient |= 1 ===
5951                // SUBS R6, R6, R2
5952                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2 (16-bit)
5953                // SBC R7, R7, R3 (with borrow)
5954                // Thumb-2 SBC.W: EB67 0703 = SBC.W R7, R7, R3
5955                bytes.extend_from_slice(&0xEB67u16.to_le_bytes());
5956                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5957                // ORR R4, R4, #1 (set bit 0 of quotient low)
5958                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5959                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5960
5961                // === Decrement counter and loop ===
5962                // SUBS.W R12, R12, #1 (decrement loop counter)
5963                // SUBS.W R12, R12, #1: F1BC 0C01
5964                bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
5965                bytes.extend_from_slice(&0x0C01u16.to_le_bytes());
5966
5967                // BNE back to loop_start
5968                let branch_offset_bytes = bytes.len() - loop_start + 4; // +4 for pipeline
5969                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5970                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5971                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5972
5973                // === Loop done, move quotient to R0:R1 ===
5974                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5975                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5976
5977                // POP {R4-R7} - restore scratch registers (NO PC — inline code continues)
5978                // 16-bit POP: 1011 110 P rrrrrrrr where P=0 (no PC), r=R4-R7 = 0xF0
5979                // Encoding: 1011 1100 1111 0000 = 0xBCF0
5980                bytes.extend_from_slice(&0xBCF0u16.to_le_bytes());
5981
5982                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5983                Ok(bytes)
5984            }
5985
5986            // I64DivS: 64-bit signed division
5987            // Converts to unsigned, divides, then applies sign
5988            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5989            //   ->  R0:R1 = quotient (signed)
5990            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5991            ArmOp::I64DivS {
5992                rdlo,
5993                rdhi,
5994                rnlo,
5995                rnhi,
5996                rmlo,
5997                rmhi,
5998                elide_zero_guard,
5999                elide_overflow_guard,
6000            } => {
6001                let mut bytes = Vec::new();
6002                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6003                // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
6004                // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
6005                // the #633 overflow guard falls ONLY to
6006                // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
6007                // divisor-nonzero fact alone must keep it.
6008                if !elide_zero_guard {
6009                    emit_i64_divisor_zero_trap(&mut bytes);
6010                }
6011                if !elide_overflow_guard {
6012                    // #633: INT64_MIN / -1 overflows — trap like the i32 path
6013                    // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
6014                    emit_i64_divs_overflow_trap(&mut bytes);
6015                }
6016
6017                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
6018                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6019                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6020
6021                // Save result sign in R9: R9 = R1 XOR R3 (sign bit = MSB)
6022                // EOR.W R9, R1, R3
6023                bytes.extend_from_slice(&0xEA81u16.to_le_bytes());
6024                bytes.extend_from_slice(&0x0903u16.to_le_bytes());
6025
6026                // If dividend negative (R1 MSB set), negate it
6027                // TST R1, R1 (check sign)
6028                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
6029                // BPL skip_neg_dividend (+10 bytes = 5 halfwords)
6030                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6031
6032                // Negate R0:R1 (64-bit): RSBS R0, R0, #0; SBC R1, R1, R1 LSL #1
6033                // Actually: MVN R0, R0; MVN R1, R1; ADDS R0, R0, #1; ADC R1, R1, #0
6034                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6035                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6036                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6037                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6038                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6039
6040                // If divisor negative (R3 MSB set), negate it
6041                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
6042                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6043
6044                // Negate R2:R3
6045                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
6046                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
6047                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
6048                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
6049                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
6050
6051                // === Now do unsigned division (same as I64DivU) ===
6052                // Initialize quotient (R4:R5) = 0
6053                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6054                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6055                // Initialize remainder (R6:R7) = 0
6056                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6057                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6058                // Initialize loop counter R8 = 64
6059                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6060                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6061
6062                let loop_start = bytes.len();
6063
6064                // Shift quotient left
6065                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6066                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6067                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6068                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6069
6070                // Shift remainder left, OR in MSB of dividend
6071                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6072                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6073                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6074                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6075                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6076                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6077
6078                // Shift dividend left
6079                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6080                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6081                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6082                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6083
6084                // Compare and conditionally subtract
6085                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6086                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6087                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6088                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6089                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6090
6091                // Subtract and set quotient bit
6092                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6093                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6094                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6095                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6096                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6097
6098                // Decrement and loop
6099                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6100                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6101
6102                let branch_offset_bytes = bytes.len() - loop_start + 4;
6103                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6104                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6105                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6106
6107                // Move quotient to R0:R1
6108                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
6109                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
6110
6111                // If result should be negative (R9 MSB set), negate R0:R1
6112                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9 (check MSB)
6113                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6114                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8 (skip negation)
6115
6116                // Negate result R0:R1
6117                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6118                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6119                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6120                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6121                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6122
6123                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6124                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6125                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6126
6127                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6128                Ok(bytes)
6129            }
6130
6131            // I64RemU: 64-bit unsigned remainder using binary long division
6132            // Same algorithm as I64DivU but returns remainder instead of quotient
6133            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = remainder
6134            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6135            ArmOp::I64RemU {
6136                rdlo,
6137                rdhi,
6138                rnlo,
6139                rnhi,
6140                rmlo,
6141                rmhi,
6142                elide_zero_guard,
6143            } => {
6144                let mut bytes = Vec::new();
6145                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6146                if !elide_zero_guard {
6147                    emit_i64_divisor_zero_trap(&mut bytes);
6148                }
6149
6150                // PUSH {R4-R8} - save scratch registers (NO LR — inline code)
6151                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6152                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6153
6154                // Initialize quotient (R4:R5) = 0 (computed but not returned)
6155                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6156                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6157                // Initialize remainder (R6:R7) = 0
6158                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6159                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6160                // Initialize loop counter R8 = 64
6161                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6162                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6163
6164                let loop_start = bytes.len();
6165
6166                // Shift quotient left (not needed for result, but keeps algorithm same)
6167                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6168                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6169                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6170                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6171
6172                // Shift remainder left, OR in MSB of dividend
6173                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6174                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6175                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6176                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6177                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6178                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6179
6180                // Shift dividend left
6181                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6182                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6183                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6184                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6185
6186                // Compare and conditionally subtract
6187                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6188                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6189                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6190                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6191                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6192
6193                // Subtract and set quotient bit
6194                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6195                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6196                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6197                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6198                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6199
6200                // Decrement and loop
6201                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6202                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6203
6204                let branch_offset_bytes = bytes.len() - loop_start + 4;
6205                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6206                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6207                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6208
6209                // Move REMAINDER to R0:R1 (difference from I64DivU)
6210                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6211                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6212
6213                // POP {R4-R8} - restore scratch registers (NO PC — inline code continues)
6214                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6215                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6216
6217                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6218                Ok(bytes)
6219            }
6220
6221            // I64RemS: 64-bit signed remainder
6222            // Remainder sign follows dividend sign (not quotient rule)
6223            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
6224            //   ->  R0:R1 = remainder (signed, same sign as dividend)
6225            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6226            ArmOp::I64RemS {
6227                rdlo,
6228                rdhi,
6229                rnlo,
6230                rnhi,
6231                rmlo,
6232                rmhi,
6233                elide_zero_guard,
6234            } => {
6235                let mut bytes = Vec::new();
6236                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6237                if !elide_zero_guard {
6238                    emit_i64_divisor_zero_trap(&mut bytes);
6239                }
6240
6241                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
6242                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6243                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6244
6245                // Save dividend sign in R9 (remainder sign = dividend sign)
6246                // MOV R9, R1 (just need the sign bit)
6247                bytes.extend_from_slice(&0x4689u16.to_le_bytes()); // MOV R9, R1
6248
6249                // If dividend negative (R1 MSB set), negate it
6250                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
6251                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6252
6253                // Negate R0:R1
6254                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6255                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6256                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6257                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6258                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6259
6260                // If divisor negative (R3 MSB set), negate it
6261                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
6262                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6263
6264                // Negate R2:R3
6265                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
6266                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
6267                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
6268                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
6269                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
6270
6271                // === Unsigned division algorithm ===
6272                // Initialize quotient (R4:R5) = 0
6273                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6274                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6275                // Initialize remainder (R6:R7) = 0
6276                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6277                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6278                // Initialize loop counter R8 = 64
6279                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6280                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6281
6282                let loop_start = bytes.len();
6283
6284                // Shift quotient left
6285                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6286                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6287                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6288                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6289
6290                // Shift remainder left, OR in MSB of dividend
6291                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6292                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6293                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6294                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6295                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6296                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6297
6298                // Shift dividend left
6299                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6300                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6301                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6302                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6303
6304                // Compare and conditionally subtract
6305                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6306                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6307                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6308                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6309                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6310
6311                // Subtract and set quotient bit
6312                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6313                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6314                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6315                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6316                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6317
6318                // Decrement and loop
6319                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6320                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6321
6322                let branch_offset_bytes = bytes.len() - loop_start + 4;
6323                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6324                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6325                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6326
6327                // Move remainder to R0:R1
6328                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6329                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6330
6331                // If original dividend was negative (R9 MSB set), negate remainder
6332                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9
6333                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6334                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6335
6336                // Negate result R0:R1
6337                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6338                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6339                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6340                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6341                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6342
6343                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6344                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6345                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6346
6347                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6348                Ok(bytes)
6349            }
6350
6351            // === F32 VFP single-precision Thumb-2 encodings ===
6352            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6353            ArmOp::F32Add { sd, sn, sm } => {
6354                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A00, sd, sn, sm)?))
6355            }
6356            ArmOp::F32Sub { sd, sn, sm } => {
6357                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A40, sd, sn, sm)?))
6358            }
6359            ArmOp::F32Mul { sd, sn, sm } => {
6360                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE200A00, sd, sn, sm)?))
6361            }
6362            ArmOp::F32Div { sd, sn, sm } => {
6363                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE800A00, sd, sn, sm)?))
6364            }
6365            ArmOp::F32Abs { sd, sm } => {
6366                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB00AC0, sd, sm)?))
6367            }
6368            ArmOp::F32Neg { sd, sm } => {
6369                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10A40, sd, sm)?))
6370            }
6371            ArmOp::F32Sqrt { sd, sm } => {
6372                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10AC0, sd, sm)?))
6373            }
6374
6375            // f32 pseudo-ops — multi-instruction sequences
6376            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6377            ArmOp::F32Ceil { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b01),
6378            ArmOp::F32Floor { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b10),
6379            ArmOp::F32Trunc { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b11),
6380            ArmOp::F32Nearest { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b00),
6381            ArmOp::F32Min { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, true),
6382            ArmOp::F32Max { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, false),
6383            ArmOp::F32Copysign { sd, sn, sm } => self.encode_thumb_f32_copysign(sd, sn, sm),
6384
6385            // f32 comparisons — VCMP + VMRS + MOV #0 + IT + MOV #1
6386            ArmOp::F32Eq { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x0),
6387            ArmOp::F32Ne { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x1),
6388            ArmOp::F32Lt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x4),
6389            ArmOp::F32Le { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x9),
6390            ArmOp::F32Gt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xC),
6391            ArmOp::F32Ge { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xA),
6392
6393            ArmOp::F32Const { sd, value } => self.encode_thumb_f32_const(sd, *value),
6394
6395            ArmOp::F32Load { sd, addr } => {
6396                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED900A00, sd, addr)?))
6397            }
6398            ArmOp::F32Store { sd, addr } => {
6399                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED800A00, sd, addr)?))
6400            }
6401
6402            ArmOp::F32ConvertI32S { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, true),
6403            ArmOp::F32ConvertI32U { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, false),
6404            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
6405                Err(synth_core::Error::synthesis(
6406                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6407                ))
6408            }
6409            ArmOp::F32ReinterpretI32 { sd, rm } => {
6410                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(true, sd, rm)?))
6411            }
6412            ArmOp::I32ReinterpretF32 { rd, sm } => {
6413                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(false, sm, rd)?))
6414            }
6415            ArmOp::I32TruncF32S { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, true),
6416            ArmOp::I32TruncF32U { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, false),
6417
6418            // === F64 VFP double-precision Thumb-2 encodings ===
6419            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6420            ArmOp::F64Add { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6421                0xEE300B00, dd, dn, dm,
6422            )?)),
6423            ArmOp::F64Sub { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6424                0xEE300B40, dd, dn, dm,
6425            )?)),
6426            ArmOp::F64Mul { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6427                0xEE200B00, dd, dn, dm,
6428            )?)),
6429            ArmOp::F64Div { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6430                0xEE800B00, dd, dn, dm,
6431            )?)),
6432            ArmOp::F64Abs { dd, dm } => {
6433                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?))
6434            }
6435            ArmOp::F64Neg { dd, dm } => {
6436                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?))
6437            }
6438            ArmOp::F64Sqrt { dd, dm } => {
6439                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?))
6440            }
6441
6442            // f64 pseudo-ops
6443            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6444            ArmOp::F64Ceil { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b01),
6445            ArmOp::F64Floor { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b10),
6446            ArmOp::F64Trunc { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b11),
6447            ArmOp::F64Nearest { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b00),
6448            ArmOp::F64Min { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, true),
6449            ArmOp::F64Max { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, false),
6450            ArmOp::F64Copysign { dd, dn, dm } => self.encode_thumb_f64_copysign(dd, dn, dm),
6451
6452            // f64 comparisons
6453            ArmOp::F64Eq { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x0),
6454            ArmOp::F64Ne { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x1),
6455            ArmOp::F64Lt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x4),
6456            ArmOp::F64Le { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x9),
6457            ArmOp::F64Gt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xC),
6458            ArmOp::F64Ge { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xA),
6459
6460            ArmOp::F64Const { dd, value } => self.encode_thumb_f64_const(dd, *value),
6461
6462            ArmOp::F64Load { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6463                0xED900B00, dd, addr,
6464            )?)),
6465            ArmOp::F64Store { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6466                0xED800B00, dd, addr,
6467            )?)),
6468
6469            ArmOp::F64ConvertI32S { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, true),
6470            ArmOp::F64ConvertI32U { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, false),
6471            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
6472                Err(synth_core::Error::synthesis(
6473                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6474                ))
6475            }
6476            ArmOp::F64PromoteF32 { dd, sm } => self.encode_thumb_f64_promote_f32(dd, sm),
6477            ArmOp::F32DemoteF64 { sd, dm } => self.encode_thumb_f32_demote_f64(sd, dm),
6478            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => Ok(vfp_to_thumb_bytes(
6479                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?,
6480            )),
6481            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => Ok(vfp_to_thumb_bytes(
6482                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?,
6483            )),
6484            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
6485                Err(synth_core::Error::synthesis(
6486                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
6487                ))
6488            }
6489            ArmOp::I32TruncF64S { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, true),
6490            ArmOp::I32TruncF64U { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, false),
6491
6492            // ===== i64 operations: encode as multi-instruction Thumb-2 sequences =====
6493
6494            // I64Add: ADDS rdlo, rnlo, rmlo; ADC.W rdhi, rnhi, rmhi
6495            ArmOp::I64Add {
6496                rdlo,
6497                rdhi,
6498                rnlo,
6499                rnhi,
6500                rmlo,
6501                rmhi,
6502            } => {
6503                let mut bytes = Vec::new();
6504                // ADDS rdlo, rnlo, rmlo (16-bit)
6505                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adds {
6506                    rd: *rdlo,
6507                    rn: *rnlo,
6508                    op2: Operand2::Reg(*rmlo),
6509                })?);
6510                // ADC.W rdhi, rnhi, rmhi (32-bit)
6511                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adc {
6512                    rd: *rdhi,
6513                    rn: *rnhi,
6514                    op2: Operand2::Reg(*rmhi),
6515                })?);
6516                Ok(bytes)
6517            }
6518
6519            // I64Sub: SUBS rdlo, rnlo, rmlo; SBC.W rdhi, rnhi, rmhi
6520            ArmOp::I64Sub {
6521                rdlo,
6522                rdhi,
6523                rnlo,
6524                rnhi,
6525                rmlo,
6526                rmhi,
6527            } => {
6528                let mut bytes = Vec::new();
6529                // SUBS rdlo, rnlo, rmlo (16-bit)
6530                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Subs {
6531                    rd: *rdlo,
6532                    rn: *rnlo,
6533                    op2: Operand2::Reg(*rmlo),
6534                })?);
6535                // SBC.W rdhi, rnhi, rmhi (32-bit)
6536                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Sbc {
6537                    rd: *rdhi,
6538                    rn: *rnhi,
6539                    op2: Operand2::Reg(*rmhi),
6540                })?);
6541                Ok(bytes)
6542            }
6543
6544            // I64And: AND rdlo, rnlo, rmlo; AND rdhi, rnhi, rmhi
6545            ArmOp::I64And {
6546                rdlo,
6547                rdhi,
6548                rnlo,
6549                rnhi,
6550                rmlo,
6551                rmhi,
6552            } => {
6553                let mut bytes = Vec::new();
6554                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6555                    rd: *rdlo,
6556                    rn: *rnlo,
6557                    op2: Operand2::Reg(*rmlo),
6558                })?);
6559                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6560                    rd: *rdhi,
6561                    rn: *rnhi,
6562                    op2: Operand2::Reg(*rmhi),
6563                })?);
6564                Ok(bytes)
6565            }
6566
6567            // I64Or: ORR rdlo, rnlo, rmlo; ORR rdhi, rnhi, rmhi
6568            ArmOp::I64Or {
6569                rdlo,
6570                rdhi,
6571                rnlo,
6572                rnhi,
6573                rmlo,
6574                rmhi,
6575            } => {
6576                let mut bytes = Vec::new();
6577                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6578                    rd: *rdlo,
6579                    rn: *rnlo,
6580                    op2: Operand2::Reg(*rmlo),
6581                })?);
6582                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6583                    rd: *rdhi,
6584                    rn: *rnhi,
6585                    op2: Operand2::Reg(*rmhi),
6586                })?);
6587                Ok(bytes)
6588            }
6589
6590            // I64Xor: EOR rdlo, rnlo, rmlo; EOR rdhi, rnhi, rmhi
6591            ArmOp::I64Xor {
6592                rdlo,
6593                rdhi,
6594                rnlo,
6595                rnhi,
6596                rmlo,
6597                rmhi,
6598            } => {
6599                let mut bytes = Vec::new();
6600                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6601                    rd: *rdlo,
6602                    rn: *rnlo,
6603                    op2: Operand2::Reg(*rmlo),
6604                })?);
6605                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6606                    rd: *rdhi,
6607                    rn: *rnhi,
6608                    op2: Operand2::Reg(*rmhi),
6609                })?);
6610                Ok(bytes)
6611            }
6612
6613            // I64Eqz: ORR scratch, lo, hi; ITE EQ; MOV rd, #1; MOV rd, #0
6614            ArmOp::I64Eqz { rd, rnlo, rnhi } => self.encode_thumb(&ArmOp::I64SetCondZ {
6615                rd: *rd,
6616                rn_lo: *rnlo,
6617                rn_hi: *rnhi,
6618            }),
6619
6620            // I64 comparisons: delegate to I64SetCond
6621            ArmOp::I64Eq {
6622                rd,
6623                rnlo,
6624                rnhi,
6625                rmlo,
6626                rmhi,
6627            } => self.encode_thumb(&ArmOp::I64SetCond {
6628                rd: *rd,
6629                rn_lo: *rnlo,
6630                rn_hi: *rnhi,
6631                rm_lo: *rmlo,
6632                rm_hi: *rmhi,
6633                cond: synth_synthesis::Condition::EQ,
6634            }),
6635
6636            ArmOp::I64Ne {
6637                rd,
6638                rnlo,
6639                rnhi,
6640                rmlo,
6641                rmhi,
6642            } => self.encode_thumb(&ArmOp::I64SetCond {
6643                rd: *rd,
6644                rn_lo: *rnlo,
6645                rn_hi: *rnhi,
6646                rm_lo: *rmlo,
6647                rm_hi: *rmhi,
6648                cond: synth_synthesis::Condition::NE,
6649            }),
6650
6651            ArmOp::I64LtS {
6652                rd,
6653                rnlo,
6654                rnhi,
6655                rmlo,
6656                rmhi,
6657            } => self.encode_thumb(&ArmOp::I64SetCond {
6658                rd: *rd,
6659                rn_lo: *rnlo,
6660                rn_hi: *rnhi,
6661                rm_lo: *rmlo,
6662                rm_hi: *rmhi,
6663                cond: synth_synthesis::Condition::LT,
6664            }),
6665
6666            ArmOp::I64LtU {
6667                rd,
6668                rnlo,
6669                rnhi,
6670                rmlo,
6671                rmhi,
6672            } => self.encode_thumb(&ArmOp::I64SetCond {
6673                rd: *rd,
6674                rn_lo: *rnlo,
6675                rn_hi: *rnhi,
6676                rm_lo: *rmlo,
6677                rm_hi: *rmhi,
6678                cond: synth_synthesis::Condition::LO,
6679            }),
6680
6681            ArmOp::I64LeS {
6682                rd,
6683                rnlo,
6684                rnhi,
6685                rmlo,
6686                rmhi,
6687            } => self.encode_thumb(&ArmOp::I64SetCond {
6688                rd: *rd,
6689                rn_lo: *rnlo,
6690                rn_hi: *rnhi,
6691                rm_lo: *rmlo,
6692                rm_hi: *rmhi,
6693                cond: synth_synthesis::Condition::LE,
6694            }),
6695
6696            ArmOp::I64LeU {
6697                rd,
6698                rnlo,
6699                rnhi,
6700                rmlo,
6701                rmhi,
6702            } => self.encode_thumb(&ArmOp::I64SetCond {
6703                rd: *rd,
6704                rn_lo: *rnlo,
6705                rn_hi: *rnhi,
6706                rm_lo: *rmlo,
6707                rm_hi: *rmhi,
6708                cond: synth_synthesis::Condition::LS,
6709            }),
6710
6711            ArmOp::I64GtS {
6712                rd,
6713                rnlo,
6714                rnhi,
6715                rmlo,
6716                rmhi,
6717            } => self.encode_thumb(&ArmOp::I64SetCond {
6718                rd: *rd,
6719                rn_lo: *rnlo,
6720                rn_hi: *rnhi,
6721                rm_lo: *rmlo,
6722                rm_hi: *rmhi,
6723                cond: synth_synthesis::Condition::GT,
6724            }),
6725
6726            ArmOp::I64GtU {
6727                rd,
6728                rnlo,
6729                rnhi,
6730                rmlo,
6731                rmhi,
6732            } => self.encode_thumb(&ArmOp::I64SetCond {
6733                rd: *rd,
6734                rn_lo: *rnlo,
6735                rn_hi: *rnhi,
6736                rm_lo: *rmlo,
6737                rm_hi: *rmhi,
6738                cond: synth_synthesis::Condition::HI,
6739            }),
6740
6741            ArmOp::I64GeS {
6742                rd,
6743                rnlo,
6744                rnhi,
6745                rmlo,
6746                rmhi,
6747            } => self.encode_thumb(&ArmOp::I64SetCond {
6748                rd: *rd,
6749                rn_lo: *rnlo,
6750                rn_hi: *rnhi,
6751                rm_lo: *rmlo,
6752                rm_hi: *rmhi,
6753                cond: synth_synthesis::Condition::GE,
6754            }),
6755
6756            ArmOp::I64GeU {
6757                rd,
6758                rnlo,
6759                rnhi,
6760                rmlo,
6761                rmhi,
6762            } => self.encode_thumb(&ArmOp::I64SetCond {
6763                rd: *rd,
6764                rn_lo: *rnlo,
6765                rn_hi: *rnhi,
6766                rm_lo: *rmlo,
6767                rm_hi: *rmhi,
6768                cond: synth_synthesis::Condition::HS,
6769            }),
6770
6771            // I64Const: MOVW rdlo, lo16; MOVT rdlo, hi16; MOVW rdhi, lo16_hi; MOVT rdhi, hi16_hi
6772            ArmOp::I64Const { rdlo, rdhi, value } => {
6773                let lo32 = *value as u32;
6774                let hi32 = (*value >> 32) as u32;
6775                let mut bytes = Vec::new();
6776                // Load low 32 bits into rdlo
6777                bytes.extend_from_slice(
6778                    &self.encode_thumb32_movw_raw(reg_to_bits(rdlo), lo32 & 0xFFFF)?,
6779                );
6780                if lo32 > 0xFFFF {
6781                    bytes.extend_from_slice(
6782                        &self.encode_thumb32_movt_raw(reg_to_bits(rdlo), lo32 >> 16)?,
6783                    );
6784                }
6785                // Load high 32 bits into rdhi
6786                bytes.extend_from_slice(
6787                    &self.encode_thumb32_movw_raw(reg_to_bits(rdhi), hi32 & 0xFFFF)?,
6788                );
6789                if hi32 > 0xFFFF {
6790                    bytes.extend_from_slice(
6791                        &self.encode_thumb32_movt_raw(reg_to_bits(rdhi), hi32 >> 16)?,
6792                    );
6793                }
6794                Ok(bytes)
6795            }
6796
6797            // I64Ldr: LDR rdlo, [base, offset]; LDR rdhi, [base, offset+4]
6798            ArmOp::I64Ldr { rdlo, rdhi, addr } => {
6799                let mut bytes = Vec::new();
6800                // #372/#382: a memory `i64.load` carries an index register
6801                // (`reg_imm(R11, addr_reg, offset)` = R11 + addr + offset). The
6802                // immediate `encode_thumb32_ldr` below uses only base+offset and
6803                // would SILENTLY DROP `offset_reg` — the #206 defect, here for
6804                // i64. `i64_effective_base` materializes the effective base into
6805                // `ip` (and, when `offset+4 > 0xFFF`, folds the offset in too so
6806                // the function is NOT skipped — #382), returning the residual
6807                // imm12 for the two halves. Frame i64 loads (no `offset_reg`, e.g.
6808                // a spilled local at `[SP, #off]`) keep the plain `[base,#off]`
6809                // form unchanged — so existing output is byte-identical.
6810                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6811                bytes.extend_from_slice(&self.encode_thumb32_ldr(rdlo, &base, offset)?);
6812                bytes.extend_from_slice(&self.encode_thumb32_ldr(
6813                    rdhi,
6814                    &base,
6815                    offset.wrapping_add(4),
6816                )?);
6817                Ok(bytes)
6818            }
6819
6820            // I64Str: STR rdlo, [base, offset]; STR rdhi, [base, offset+4]
6821            ArmOp::I64Str { rdlo, rdhi, addr } => {
6822                let mut bytes = Vec::new();
6823                // #372/#382: same index-materialization + large-offset fold as
6824                // I64Ldr (see above).
6825                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6826                bytes.extend_from_slice(&self.encode_thumb32_str(rdlo, &base, offset)?);
6827                bytes.extend_from_slice(&self.encode_thumb32_str(
6828                    rdhi,
6829                    &base,
6830                    offset.wrapping_add(4),
6831                )?);
6832                Ok(bytes)
6833            }
6834
6835            // I64ExtendI32S: MOV rdlo, rn; ASR rdhi, rdlo, #31 (sign-extend)
6836            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
6837                let mut bytes = Vec::new();
6838                if rdlo != rn {
6839                    // MOV rdlo, rn (16-bit)
6840                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6841                        rd: *rdlo,
6842                        op2: Operand2::Reg(*rn),
6843                    })?);
6844                }
6845                // ASR rdhi, rdlo, #31 (sign-extend: fill high word with sign bit)
6846                bytes.extend_from_slice(
6847                    &self.encode_thumb32_shift(rdhi, rdlo, 31, 0b10)?, // ASR type
6848                );
6849                Ok(bytes)
6850            }
6851
6852            // I64ExtendI32U: MOV rdlo, rn; MOV rdhi, #0
6853            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
6854                let mut bytes = Vec::new();
6855                if rdlo != rn {
6856                    // MOV rdlo, rn
6857                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6858                        rd: *rdlo,
6859                        op2: Operand2::Reg(*rn),
6860                    })?);
6861                }
6862                // MOV rdhi, #0 (#916: MOV.W for rdhi >= R8). Unconditional
6863                // site with no branches in the expansion — before the fix this
6864                // emitted the literal two-instruction stream [4608, 2800], half
6865                // of which was `CMP r0,#0` rather than the high-word clear, so
6866                // every i64.extend_i32_u into a high pair leaked stale bits.
6867                emit_thumb_zero_fill(&mut bytes, reg_to_bits(rdhi));
6868                Ok(bytes)
6869            }
6870
6871            // I32WrapI64: MOV rd, rnlo (just take low 32 bits)
6872            ArmOp::I32WrapI64 { rd, rnlo } => {
6873                if rd == rnlo {
6874                    // No-op: already in the right register
6875                    let instr: u16 = 0xBF00; // NOP
6876                    Ok(instr.to_le_bytes().to_vec())
6877                } else {
6878                    // MOV rd, rnlo
6879                    self.encode_thumb(&ArmOp::Mov {
6880                        rd: *rd,
6881                        op2: Operand2::Reg(*rnlo),
6882                    })
6883                }
6884            }
6885
6886            // ===== Helium MVE operations (Thumb-2 encoding) =====
6887            ArmOp::MveLoad { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vldrw(qd, addr))),
6888            ArmOp::MveStore { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vstrw(qd, addr))),
6889            ArmOp::MveConst { qd, bytes } => self.encode_thumb_mve_const(qd, bytes),
6890            ArmOp::MveAnd { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6891                0xEF000150, qd, qn, qm,
6892            ))),
6893            ArmOp::MveOrr { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6894                0xEF200150, qd, qn, qm,
6895            ))),
6896            ArmOp::MveEor { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6897                0xFF000150, qd, qn, qm,
6898            ))),
6899            ArmOp::MveMvn { qd, qm } => {
6900                // VMVN Qd, Qm: 0xFFB005C0 | Qd<<12 | Qm
6901                let qd_enc = qreg_to_num(qd);
6902                let qm_enc = qreg_to_num(qm);
6903                let instr: u32 = 0xFFB005C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6904                Ok(vfp_to_thumb_bytes(instr))
6905            }
6906            ArmOp::MveBic { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6907                0xEF100150, qd, qn, qm,
6908            ))),
6909            ArmOp::MveAddI { qd, qn, qm, size } => {
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            ArmOp::MveSubI { qd, qn, qm, size } => {
6915                let sz = mve_size_bits(size);
6916                let base: u32 = 0xFF000840 | (sz << 20);
6917                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6918            }
6919            ArmOp::MveMulI { qd, qn, qm, size } => {
6920                let sz = mve_size_bits(size);
6921                let base: u32 = 0xEF000950 | (sz << 20);
6922                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6923            }
6924            ArmOp::MveNegI { qd, qm, size } => {
6925                let sz = mve_size_bits(size);
6926                // VNEG.Sx Qd, Qm
6927                let qd_enc = qreg_to_num(qd);
6928                let qm_enc = qreg_to_num(qm);
6929                let base: u32 = 0xFFB103C0 | (sz << 18);
6930                let instr = base | ((qd_enc * 2) << 12) | (qm_enc * 2);
6931                Ok(vfp_to_thumb_bytes(instr))
6932            }
6933            ArmOp::MveDup { qd, rn, size } => {
6934                let sz = mve_size_bits(size);
6935                let qd_enc = qreg_to_num(qd);
6936                let rn_bits = reg_to_bits(rn);
6937                // VDUP.sz Qd, Rn: EEA0 0B10 variant
6938                // size encoding: 00=32, 01=16, 10=8
6939                let be = match sz {
6940                    0 => 0b00u32, // 8-bit
6941                    1 => 0b01,    // 16-bit
6942                    _ => 0b00,    // 32-bit (default)
6943                };
6944                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12) | (be << 5);
6945                Ok(vfp_to_thumb_bytes(instr))
6946            }
6947            ArmOp::MveExtractLane { rd, qn, lane, size } => {
6948                let qn_enc = qreg_to_num(qn);
6949                let rd_bits = reg_to_bits(rd);
6950                // VMOV.sz Rd, Dn[x] — extract from Q-register lane
6951                // For 32-bit: VMOV Rd, Dn — where Dn is the appropriate D-register
6952                let d_reg = qn_enc * 2 + ((*lane as u32) >> 1);
6953                let lane_in_d = (*lane as u32) & 1;
6954                let _sz = mve_size_bits(size);
6955                // VMOV Rd, Dn[x]: EE10 0B10 for 32-bit
6956                let instr: u32 = 0xEE100B10 | (d_reg << 16) | (rd_bits << 12) | (lane_in_d << 21);
6957                Ok(vfp_to_thumb_bytes(instr))
6958            }
6959            ArmOp::MveInsertLane { qd, rn, lane, size } => {
6960                let qd_enc = qreg_to_num(qd);
6961                let rn_bits = reg_to_bits(rn);
6962                let d_reg = qd_enc * 2 + ((*lane as u32) >> 1);
6963                let lane_in_d = (*lane as u32) & 1;
6964                let _sz = mve_size_bits(size);
6965                // VMOV Dn[x], Rn: EE00 0B10 for 32-bit
6966                let instr: u32 = 0xEE000B10 | (d_reg << 16) | (rn_bits << 12) | (lane_in_d << 21);
6967                Ok(vfp_to_thumb_bytes(instr))
6968            }
6969
6970            // MVE float comparisons — emit VCMP + VPSEL sequence (simplified: just VCMP)
6971            ArmOp::MveCmpEqI { qd, qn, qm, size }
6972            | ArmOp::MveCmpNeI { qd, qn, qm, size }
6973            | ArmOp::MveCmpLtS { qd, qn, qm, size }
6974            | ArmOp::MveCmpLtU { qd, qn, qm, size }
6975            | ArmOp::MveCmpGtS { qd, qn, qm, size }
6976            | ArmOp::MveCmpGtU { qd, qn, qm, size }
6977            | ArmOp::MveCmpLeS { qd, qn, qm, size }
6978            | ArmOp::MveCmpLeU { qd, qn, qm, size }
6979            | ArmOp::MveCmpGeS { qd, qn, qm, size }
6980            | ArmOp::MveCmpGeU { qd, qn, qm, size } => {
6981                // Encode as VADD (placeholder encoding — real implementation
6982                // would use VCMP + VPSEL pair)
6983                let sz = mve_size_bits(size);
6984                let base: u32 = 0xEF000840 | (sz << 20);
6985                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6986            }
6987
6988            // f32x4 MVE arithmetic
6989            ArmOp::MveAddF32 { qd, qn, qm } => {
6990                // VADD.F32 Qd, Qn, Qm (MVE): 0xEF000D40
6991                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6992            }
6993            ArmOp::MveSubF32 { qd, qn, qm } => {
6994                // VSUB.F32 Qd, Qn, Qm (MVE): 0xEF200D40
6995                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF200D40, qd, qn, qm)))
6996            }
6997            ArmOp::MveMulF32 { qd, qn, qm } => {
6998                // VMUL.F32 Qd, Qn, Qm (MVE): 0xFF000D50
6999                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xFF000D50, qd, qn, qm)))
7000            }
7001            ArmOp::MveNegF32 { qd, qm } => {
7002                let qd_enc = qreg_to_num(qd);
7003                let qm_enc = qreg_to_num(qm);
7004                // VNEG.F32 Qd, Qm: FFB907C0
7005                let instr: u32 = 0xFFB907C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
7006                Ok(vfp_to_thumb_bytes(instr))
7007            }
7008            ArmOp::MveAbsF32 { qd, qm } => {
7009                let qd_enc = qreg_to_num(qd);
7010                let qm_enc = qreg_to_num(qm);
7011                // VABS.F32 Qd, Qm: FFB90740
7012                let instr: u32 = 0xFFB90740 | ((qd_enc * 2) << 12) | (qm_enc * 2);
7013                Ok(vfp_to_thumb_bytes(instr))
7014            }
7015            ArmOp::MveCmpEqF32 { qd, qn, qm }
7016            | ArmOp::MveCmpNeF32 { qd, qn, qm }
7017            | ArmOp::MveCmpLtF32 { qd, qn, qm }
7018            | ArmOp::MveCmpLeF32 { qd, qn, qm }
7019            | ArmOp::MveCmpGtF32 { qd, qn, qm }
7020            | ArmOp::MveCmpGeF32 { qd, qn, qm } => {
7021                // Placeholder: encode as VADD.F32 (real impl needs VCMP.F32 + VPSEL)
7022                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
7023            }
7024            ArmOp::MveDupF32 { qd, rn } => {
7025                let qd_enc = qreg_to_num(qd);
7026                let rn_bits = reg_to_bits(rn);
7027                // VDUP.32 Qd, Rn (same encoding as integer VDUP.32)
7028                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12);
7029                Ok(vfp_to_thumb_bytes(instr))
7030            }
7031            ArmOp::MveExtractLaneF32 { rd, qn, lane } => {
7032                let qn_enc = qreg_to_num(qn);
7033                let rd_bits = reg_to_bits(rd);
7034                // VMOV Rd, Sn where Sn = Q*4 + lane
7035                let s_num = qn_enc * 4 + (*lane as u32);
7036                let (vn, n) = encode_sreg(s_num);
7037                let instr: u32 = 0xEE100A10 | (vn << 16) | (rd_bits << 12) | (n << 7);
7038                Ok(vfp_to_thumb_bytes(instr))
7039            }
7040            ArmOp::MveReplaceLaneF32 { qd, rn, lane } => {
7041                let qd_enc = qreg_to_num(qd);
7042                let rn_bits = reg_to_bits(rn);
7043                // VMOV Sn, Rn where Sn = Q*4 + lane
7044                let s_num = qd_enc * 4 + (*lane as u32);
7045                let (vn, n) = encode_sreg(s_num);
7046                let instr: u32 = 0xEE000A10 | (vn << 16) | (rn_bits << 12) | (n << 7);
7047                Ok(vfp_to_thumb_bytes(instr))
7048            }
7049            ArmOp::MveDivF32 { qd, qn, qm } => {
7050                // Lane-wise: extract 4 S-regs, VDIV, insert back
7051                self.encode_thumb_mve_lane_wise_f32_binop(qd, qn, qm, 0xEE800A00)
7052            }
7053            ArmOp::MveSqrtF32 { qd, qm } => {
7054                // Lane-wise: extract 4 S-regs, VSQRT, insert back
7055                self.encode_thumb_mve_lane_wise_f32_sqrt(qd, qm)
7056            }
7057
7058            // RQ-67-VFPREACH (#1267): the AAPCS callee-saved VFP half.
7059            //
7060            // Encodings taken from llvm-mc, not derived by hand:
7061            //   llvm-mc -triple=thumbv7em-none-eabi -mattr=+vfp4 -show-encoding
7062            //     vpush {d8-d15}  ->  [0x2d,0xed,0x10,0x8b]   (ED2D 8B10)
7063            //     vpop  {d8-d15}  ->  [0xbd,0xec,0x10,0x8b]   (ECBD 8B10)
7064            //   imm8 is 2 x the double count (0x10 = 16 = 2 x 8), and the
7065            //   `8B` nibble selects the 64-bit form — `vpush {d8}` is 8B02,
7066            //   which is how that field was confirmed rather than assumed.
7067            //
7068            // NOTE FOR ANYONE ADDING AN OP BELOW THIS POINT: the catch-all that
7069            // follows returns a NOP, so an op that reaches it is encoded as
7070            // nothing at all. These two arms exist ABOVE it deliberately.
7071            ArmOp::VPushCalleeSavedVfp => Ok(vec![0x2d, 0xed, 0x10, 0x8b]),
7072            ArmOp::VPopCalleeSavedVfp => Ok(vec![0xbd, 0xec, 0x10, 0x8b]),
7073
7074            // Catch-all for any remaining ops
7075            //
7076            // #615 FIXED THIS CLASS ON THE A32 PATH AND NOT HERE. Thumb-2 is
7077            // the DEFAULT target, and this arm silently encodes any unhandled
7078            // op as `NOP`. RQ-67-VFPREACH hit it directly: the two VFP
7079            // save/restore ops above compiled clean and would have emitted a
7080            // save that does nothing. Filed rather than widened in this lane —
7081            // turning it into a typed Err is a behaviour change across every
7082            // op that currently lands here, which needs its own byte-identity
7083            // evidence.
7084            other => Err(synth_core::Error::synthesis(format!(
7085                "{other:?} reached the Thumb-2 encoder with no Thumb-2 encoding — \
7086                 refusing to emit a silent NOP (#1272; #615 closed this class on A32 \
7087                 only, and Thumb-2 is the default target)"
7088            ))),
7089        }
7090    }
7091
7092    // === Thumb-2 VFP multi-instruction helpers ===
7093
7094    /// Encode F32 comparison as Thumb-2: VCMP.F32 + VMRS + MOVS rd,#0 + IT + MOV rd,#1
7095    fn encode_thumb_f32_compare(
7096        &self,
7097        rd: &Reg,
7098        sn: &VfpReg,
7099        sm: &VfpReg,
7100        cond_code: u32,
7101    ) -> Result<Vec<u8>> {
7102        let mut bytes = Vec::new();
7103        let rd_bits = reg_to_bits(rd);
7104
7105        // #709 (bug found under #708/#709): the `MOVS Rd,#0` below is a
7106        // FLAG-SETTING 16-bit move. Emitting it AFTER `VMRS APSR_nzcv, FPSCR`
7107        // (as the original code did) clobbered the N/Z/C/V flags the VMRS just
7108        // transferred from the VFP compare, so the following `IT<cond>` read
7109        // stale flags and every f32 comparison silently returned 0 (verified:
7110        // `flt(1.0,2.0)` → 0 on Cortex-M4F). The 619 harness never caught it
7111        // because it deliberately skipped compare EXECUTION on a false premise
7112        // (unicorn DOES model VMRS→APSR). Fix: materialize the `#0` FIRST, then
7113        // VCMP+VMRS set the flags the `IT` consumes. Instruction sizes are
7114        // unchanged (pure reorder), so the estimator↔encoder oracle (#511) is
7115        // untouched — only the byte ORDER differs.
7116
7117        // MOVS Rd, #0 (16-bit): 0010 0 Rd(3) 0000 0000 — its flag side effect
7118        // is immediately overwritten by the VMRS below.
7119        if rd_bits < 8 {
7120            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
7121            bytes.extend_from_slice(&movs_zero.to_le_bytes());
7122        } else {
7123            // MOV.W Rd, #0 (32-bit Thumb-2)
7124            let hw1: u16 = 0xF04F;
7125            let hw2: u16 = (rd_bits as u16) << 8;
7126            bytes.extend_from_slice(&hw1.to_le_bytes());
7127            bytes.extend_from_slice(&hw2.to_le_bytes());
7128        }
7129
7130        // VCMP.F32 Sn, Sm
7131        let sn_num = vfp_sreg_to_num(sn)?;
7132        let sm_num = vfp_sreg_to_num(sm)?;
7133        let (vd, d) = encode_sreg(sn_num);
7134        let (vm, m) = encode_sreg(sm_num);
7135        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7136        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7137
7138        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10 (sets the flags IT consumes)
7139        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7140
7141        // IT<cond> — If-Then for conditional MOV
7142        // IT encoding: 1011 1111 cond(4) mask(4)
7143        // mask = 0x8 for single "then" (IT)
7144        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7145        bytes.extend_from_slice(&it.to_le_bytes());
7146
7147        // MOV Rd, #1 (16-bit, conditional due to IT): 0010 0 Rd(3) 0000 0001
7148        if rd_bits < 8 {
7149            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7150            bytes.extend_from_slice(&mov_one.to_le_bytes());
7151        } else {
7152            // MOV.W Rd, #1 (32-bit)
7153            let hw1: u16 = 0xF04F;
7154            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7155            bytes.extend_from_slice(&hw1.to_le_bytes());
7156            bytes.extend_from_slice(&hw2.to_le_bytes());
7157        }
7158
7159        Ok(bytes)
7160    }
7161
7162    /// Encode F32 constant load as Thumb-2: MOVW + MOVT + VMOV
7163    fn encode_thumb_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
7164        let mut bytes = Vec::new();
7165        let bits = value.to_bits();
7166        let rt: u32 = 12; // R12/IP as temp
7167
7168        // MOVW R12, #lo16
7169        // Thumb-2 MOVW: 11110 i 10 0100 imm4 | 0 imm3 Rd imm8
7170        let lo16 = bits & 0xFFFF;
7171        let imm4 = (lo16 >> 12) & 0xF;
7172        let i_bit = (lo16 >> 11) & 1;
7173        let imm3 = (lo16 >> 8) & 0x7;
7174        let imm8 = lo16 & 0xFF;
7175        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7176        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
7177        bytes.extend_from_slice(&hw1.to_le_bytes());
7178        bytes.extend_from_slice(&hw2.to_le_bytes());
7179
7180        // MOVT R12, #hi16
7181        let hi16 = (bits >> 16) & 0xFFFF;
7182        let imm4 = (hi16 >> 12) & 0xF;
7183        let i_bit = (hi16 >> 11) & 1;
7184        let imm3 = (hi16 >> 8) & 0x7;
7185        let imm8 = hi16 & 0xFF;
7186        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
7187        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
7188        bytes.extend_from_slice(&hw1.to_le_bytes());
7189        bytes.extend_from_slice(&hw2.to_le_bytes());
7190
7191        // VMOV Sd, R12
7192        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
7193        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7194
7195        Ok(bytes)
7196    }
7197
7198    /// Encode VMOV + VCVT.F32.xS32 as Thumb-2
7199    fn encode_thumb_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7200        let mut bytes = Vec::new();
7201
7202        // VMOV Sd, Rm
7203        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
7204        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7205
7206        // VCVT.F32.S32/U32 Sd, Sd. Bit 7 (op) = 1 for signed (S32), 0 for
7207        // unsigned (U32): signed = 0xEEB80AC0, unsigned = 0xEEB80A40
7208        // (GI-FPU-002: previously swapped — see the ARM32 twin).
7209        let sd_num = vfp_sreg_to_num(sd)?;
7210        let (vd, d) = encode_sreg(sd_num);
7211        let (vm, m) = encode_sreg(sd_num);
7212        let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
7213        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7214        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7215
7216        Ok(bytes)
7217    }
7218
7219    /// Encode F32 rounding pseudo-op as Thumb-2 via VCVT to integer and back
7220    /// Encode F32 rounding as Thumb-2.
7221    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
7222    ///
7223    /// For trunc: uses VCVTR.S32.F32 (always truncates).
7224    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant),
7225    /// then restores FPSCR.
7226    fn encode_thumb_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7227        let mut bytes = Vec::new();
7228        let sm_num = vfp_sreg_to_num(sm)?;
7229        let sd_num = vfp_sreg_to_num(sd)?;
7230        let (vd_s, d_s) = encode_sreg(sd_num);
7231        let (vm_s, m_s) = encode_sreg(sm_num);
7232
7233        if mode == 0b11 {
7234            // Trunc (toward zero): VCVTR.S32.F32 — bit[7]=1, always truncates
7235            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7236            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7237        } else {
7238            // ceil/floor/nearest: manipulate FPSCR rounding mode
7239            let rt: u32 = 12; // R12/IP as temp
7240
7241            // VMRS R12, FPSCR
7242            let vmrs = 0xEEF10A10 | (rt << 12);
7243            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7244
7245            // BIC.W R12, R12, #(3 << 22) — clear RMode bits [23:22]
7246            // Thumb-2 modified immediate for 3<<22 = 0x00C00000:
7247            // BIC.W encoding: 11110 i 0 0001 S Rn | 0 imm3 Rd imm8
7248            // 0x00C00000 = 0x03 shifted left by 22 => Thumb mod-imm: i=0, imm3=0b101, imm8=0x03
7249            let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF); // BIC, Rn=R12
7250            let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
7251            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7252            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7253
7254            // ORR.W R12, R12, #(mode << 22)
7255            if mode != 0 {
7256                let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF); // ORR, Rn=R12
7257                let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
7258                bytes.extend_from_slice(&orr_hw1.to_le_bytes());
7259                bytes.extend_from_slice(&orr_hw2.to_le_bytes());
7260            }
7261
7262            // VMSR FPSCR, R12
7263            let vmsr = 0xEEE10A10 | (rt << 12);
7264            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7265
7266            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rmode
7267            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7268            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7269
7270            // Restore FPSCR: clear rmode bits back to nearest (default)
7271            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7272            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7273            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7274            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7275        }
7276
7277        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
7278        let (vd2, d2) = encode_sreg(sd_num);
7279        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
7280        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
7281
7282        Ok(bytes)
7283    }
7284
7285    /// Encode F32 min/max as Thumb-2: VMOV + VCMP + VMRS + IT + VMOV
7286    fn encode_thumb_f32_minmax(
7287        &self,
7288        sd: &VfpReg,
7289        sn: &VfpReg,
7290        sm: &VfpReg,
7291        is_min: bool,
7292    ) -> Result<Vec<u8>> {
7293        let mut bytes = Vec::new();
7294        let sn_num = vfp_sreg_to_num(sn)?;
7295        let sm_num = vfp_sreg_to_num(sm)?;
7296        let sd_num = vfp_sreg_to_num(sd)?;
7297
7298        // VMOV.F32 Sd, Sn
7299        let (vd, d) = encode_sreg(sd_num);
7300        let (vn, n) = encode_sreg(sn_num);
7301        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
7302        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sn));
7303
7304        // VCMP.F32 Sn, Sm
7305        let (vm, m) = encode_sreg(sm_num);
7306        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7307        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7308
7309        // VMRS APSR_nzcv, FPSCR
7310        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7311
7312        // IT GT (for min) or IT MI (for max)
7313        let cond: u16 = if is_min { 0xC } else { 0x4 };
7314        let it: u16 = 0xBF00 | (cond << 4) | 0x8;
7315        bytes.extend_from_slice(&it.to_le_bytes());
7316
7317        // VMOV{cond}.F32 Sd, Sm — conditional VMOV in IT block
7318        let vmov_sm = 0xEEB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7319        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sm));
7320
7321        Ok(bytes)
7322    }
7323
7324    /// Encode F32 copysign as Thumb-2
7325    /// Encode F32 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7326    /// encoder scratch, #212), the flags, and Sd:
7327    ///
7328    ///   VMOV R12, Sm ; CMP R12, #0    (N flag = the sign bit)
7329    ///   VABS.F32 Sd, Sn               (magnitude, sign cleared)
7330    ///   IT MI ; VNEG.F32(MI) Sd, Sd
7331    ///
7332    /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7333    /// The R12 capture happens BEFORE Sd is written, so Sd aliasing Sn or Sm
7334    /// is safe. (The previous sequence staged the magnitude through R0 —
7335    /// clobbering a live allocator-owned value, the #615 class; caught while
7336    /// composing the F64 twin for #369.)
7337    fn encode_thumb_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7338        let mut bytes = Vec::new();
7339
7340        // VMOV R12, Sm (sign source bits)
7341        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
7342            false,
7343            sm,
7344            &Reg::R12,
7345        )?));
7346        // CMP.W R12, #0 — N = bit31 (the sign, incl. -0.0 / -NaN).
7347        bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7348        bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7349        // VABS.F32 Sd, Sn
7350        let sd_num = vfp_sreg_to_num(sd)?;
7351        let sn_num = vfp_sreg_to_num(sn)?;
7352        let (vd, d) = encode_sreg(sd_num);
7353        let (vn, n) = encode_sreg(sn_num);
7354        let vabs = 0xEEB00AC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7355        bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7356        // IT MI ; VNEG.F32(MI) Sd, Sd
7357        bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7358        let vneg = 0xEEB10A40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7359        bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7360
7361        Ok(bytes)
7362    }
7363
7364    /// Encode F64 comparison as Thumb-2: VCMP.F64 + VMRS + MOV #0 + IT + MOV #1
7365    fn encode_thumb_f64_compare(
7366        &self,
7367        rd: &Reg,
7368        dn: &VfpReg,
7369        dm: &VfpReg,
7370        cond_code: u32,
7371    ) -> Result<Vec<u8>> {
7372        let mut bytes = Vec::new();
7373        let rd_bits = reg_to_bits(rd);
7374
7375        // #712-class fix (found at f64-phase-2 wiring, #369): the 16-bit
7376        // `MOVS Rd,#0` is FLAG-SETTING. The original order emitted it AFTER
7377        // `VMRS APSR_nzcv, FPSCR`, clobbering the N/Z/C/V flags the VMRS just
7378        // transferred, so the following `IT<cond>` read stale flags and every
7379        // f64 comparison silently returned 0 — the exact bug the f32 compare
7380        // encoder shipped with and #712 fixed. Same fix: materialize the `#0`
7381        // FIRST (its flag side effect is overwritten by the VMRS), then
7382        // VCMP+VMRS set the flags the IT consumes. Pure reorder — sizes
7383        // unchanged.
7384
7385        // MOVS Rd, #0
7386        if rd_bits < 8 {
7387            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
7388            bytes.extend_from_slice(&movs_zero.to_le_bytes());
7389        } else {
7390            let hw1: u16 = 0xF04F;
7391            let hw2: u16 = (rd_bits as u16) << 8;
7392            bytes.extend_from_slice(&hw1.to_le_bytes());
7393            bytes.extend_from_slice(&hw2.to_le_bytes());
7394        }
7395
7396        // VCMP.F64 Dn, Dm
7397        let dn_num = vfp_dreg_to_num(dn)?;
7398        let dm_num = vfp_dreg_to_num(dm)?;
7399        let (vd, d) = encode_dreg(dn_num);
7400        let (vm, m) = encode_dreg(dm_num);
7401        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7402        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7403
7404        // VMRS APSR_nzcv, FPSCR (sets the flags the IT consumes)
7405        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7406
7407        // IT<cond>
7408        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7409        bytes.extend_from_slice(&it.to_le_bytes());
7410
7411        // MOV Rd, #1
7412        if rd_bits < 8 {
7413            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7414            bytes.extend_from_slice(&mov_one.to_le_bytes());
7415        } else {
7416            let hw1: u16 = 0xF04F;
7417            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7418            bytes.extend_from_slice(&hw1.to_le_bytes());
7419            bytes.extend_from_slice(&hw2.to_le_bytes());
7420        }
7421
7422        Ok(bytes)
7423    }
7424
7425    /// Encode F64 constant load as Thumb-2: MOVW+MOVT (lo32 into R0) + MOVW+MOVT (hi32 into R12) + VMOV Dd, R0, R12
7426    fn encode_thumb_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
7427        let mut bytes = Vec::new();
7428        let bits = value.to_bits();
7429        let lo32 = bits as u32;
7430        let hi32 = (bits >> 32) as u32;
7431
7432        // MOVW R0, #lo16(lo32)
7433        let lo16 = lo32 & 0xFFFF;
7434        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(0, lo16)?);
7435
7436        // MOVT R0, #hi16(lo32)
7437        let hi16 = (lo32 >> 16) & 0xFFFF;
7438        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(0, hi16)?);
7439
7440        // MOVW R12, #lo16(hi32)
7441        let lo16 = hi32 & 0xFFFF;
7442        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
7443
7444        // MOVT R12, #hi16(hi32)
7445        let hi16 = (hi32 >> 16) & 0xFFFF;
7446        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
7447
7448        // VMOV Dd, R0, R12
7449        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
7450        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7451
7452        Ok(bytes)
7453    }
7454
7455    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as Thumb-2
7456    /// Encode i32 → f64 conversion as Thumb-2. The integer stages through the
7457    /// DESTINATION's own low S-alias (`S(2d)`) — allocator-owned by
7458    /// definition — never S0 (which may hold a live value; the previous
7459    /// pseudo-op's S0 staging was the #615 class). Also fixes the SWAPPED
7460    /// signed/unsigned VCVT bases (bit7 = 1 is SIGNED — the same swap the f32
7461    /// twin had; latent here because f64.convert_i32_* was decode-dropped
7462    /// until #369): clang-verified vcvt.f64.s32 d1,s2 = eeb8 1bc1,
7463    /// vcvt.f64.u32 d1,s2 = eeb8 1b41.
7464    fn encode_thumb_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7465        let dd_num = vfp_dreg_to_num(dd)?;
7466        if dd_num > 7 {
7467            return Err(synth_core::Error::synthesis(format!(
7468                "F64ConvertI32: destination {dd:?} has no S-register alias \
7469                 (D8..D15) — the selector allocates only D0..D7"
7470            )));
7471        }
7472        let mut bytes = Vec::new();
7473
7474        // VMOV S(2d), Rm — stage the integer in the destination's low word.
7475        let (vn_s, n_s) = encode_sreg(2 * dd_num);
7476        let rt = reg_to_bits(rm);
7477        let vmov = 0xEE000A10 | (vn_s << 16) | (rt << 12) | (n_s << 7);
7478        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7479
7480        // VCVT.F64.S32/U32 Dd, S(2d)
7481        let (vd, d) = encode_dreg(dd_num);
7482        let (vm, m) = encode_sreg(2 * dd_num);
7483        let base = if signed { 0xEEB80BC0 } else { 0xEEB80B40 };
7484        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7485        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7486
7487        Ok(bytes)
7488    }
7489
7490    /// Encode VCVT.F64.F32 Dd, Sm as Thumb-2
7491    fn encode_thumb_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7492        let dd_num = vfp_dreg_to_num(dd)?;
7493        let sm_num = vfp_sreg_to_num(sm)?;
7494        let (vd, d) = encode_dreg(dd_num);
7495        let (vm, m) = encode_sreg(sm_num);
7496
7497        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7498        Ok(vfp_to_thumb_bytes(vcvt))
7499    }
7500
7501    /// Encode VCVT.F32.F64 Sd, Dm (f32.demote_f64) as Thumb-2 — single
7502    /// instruction, round-to-nearest-even per FPSCR default, exactly WASM
7503    /// §4.3.3 demote (clang-verified: vcvt.f32.f64 s1,d2 = eef7 0bc2).
7504    fn encode_thumb_f32_demote_f64(&self, sd: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7505        let sd_num = vfp_sreg_to_num(sd)?;
7506        let dm_num = vfp_dreg_to_num(dm)?;
7507        let (vd, d) = encode_sreg(sd_num);
7508        let (vm, m) = encode_dreg(dm_num);
7509
7510        let vcvt = 0xEEB70BC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7511        Ok(vfp_to_thumb_bytes(vcvt))
7512    }
7513
7514    /// Encode f64 → i32 truncation as Thumb-2 (round-toward-zero VCVT).
7515    ///
7516    /// The 32-bit result stages through the SOURCE's own low S-alias, `S(2m)`,
7517    /// clobbering half of an operand the selector has already popped. The
7518    /// overlapping write is well-defined: VCVT reads its source operand before
7519    /// writing (compilers emit `vcvt.f32.f64 s0, d0` routinely).
7520    ///
7521    /// # The one precondition, and who actually provides it
7522    ///
7523    /// `dm` must be a DEAD TEMP — never a pinned param/local home. That is the
7524    /// whole safety argument, and it is worth naming the guarantor precisely
7525    /// (#946): **`select_with_stack`** provides it, by copying a home into a
7526    /// fresh D-temp first. Visible in the shipped output for
7527    /// `(func (param f64) (result i32) (i32.trunc_f64_s (local.get 0)))`:
7528    ///
7529    /// ```text
7530    ///   vmov r1, r2, d0     ; read the param out of its AAPCS-VFP home D0
7531    ///   vmov d1, r1, r2     ; ...into a fresh D-temp
7532    ///   vcvt.s32.f64 s2, d1 ; convert from the TEMP, staging into its own S2
7533    /// ```
7534    ///
7535    /// `InstructionSelector::select` / `select_default` do NOT provide it —
7536    /// `alloc_vfp_dreg` is a bare round-robin `(n + 1) % 16` with no liveness
7537    /// or home test. That path is not reachable from `synth compile`
7538    /// (`arm_backend.rs` calls `select_with_stack` exclusively; the only
7539    /// non-test caller of `select` is `examples/compile_add.rs`), so this is
7540    /// not a live miscompile — but a caller reaching that `pub` API directly
7541    /// gets no such guarantee.
7542    ///
7543    /// # What this deliberately does NOT claim
7544    ///
7545    /// An earlier version of this comment said the staging register is "never
7546    /// S0, which may hold an unrelated live value (the #615 class)". **That is
7547    /// false**, and measurably so: for
7548    /// `(func (result i32) (i32.trunc_f64_s (f64.const 3.7)))` the shipped
7549    /// compiler emits `vcvt.s32.f64 s0, d0`.
7550    ///
7551    /// It is also unnecessary. S0 is only dangerous as an *unrelated* scratch;
7552    /// here it is always the low half of `dm` itself, which the precondition
7553    /// above already makes dead. Naming a guard the code does not have — and
7554    /// does not need — invites a future reader to lean on it. The dead-temp
7555    /// precondition is the only thing holding this up.
7556    fn encode_thumb_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7557        let dm_num = vfp_dreg_to_num(dm)?;
7558        if dm_num > 7 {
7559            return Err(synth_core::Error::synthesis(format!(
7560                "I32TruncF64: source {dm:?} has no S-register alias \
7561                 (D8..D15) — the selector allocates only D0..D7"
7562            )));
7563        }
7564        let mut bytes = Vec::new();
7565
7566        // VCVT.S32/U32.F64 S(2m), Dm (clang-verified:
7567        // vcvt.s32.f64 s1,d2 = eefd 0bc2 ; vcvt.u32.f64 s1,d2 = eefc 0bc2)
7568        let (vm, m) = encode_dreg(dm_num);
7569        let (vd_s, d_s) = encode_sreg(2 * dm_num);
7570        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
7571        let vcvt = base | (d_s << 22) | (vd_s << 12) | (m << 5) | vm;
7572        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7573
7574        // VMOV Rd, S(2m)
7575        let rt = reg_to_bits(rd);
7576        let vmov = 0xEE100A10 | (vd_s << 16) | (rt << 12) | (d_s << 7);
7577        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7578
7579        Ok(bytes)
7580    }
7581
7582    /// Encode F64 rounding as a SINGLE Thumb-2 VRINT (FPv5 / cortex-m7dp).
7583    /// `mode` keeps the legacy FPSCR-RMode numbering of the callers —
7584    /// 0b00=nearest(ties-to-even)→VRINTN, 0b01=+inf(ceil)→VRINTP,
7585    /// 0b10=-inf(floor)→VRINTM, 0b11=zero(trunc)→VRINTZ — but the rounding
7586    /// mode is now ENCODED in the instruction, not smuggled through FPSCR.
7587    /// (The previous pseudo-op round-tripped through a 32-bit integer in S0:
7588    /// wrong for |x| >= 2^31, NaN/±inf collapsed to 0, -0.0 lost, and it
7589    /// CLOBBERED S0/R12 behind the allocator's back — the #615 class.)
7590    /// VRINT quietens an sNaN and preserves the sign of ±0.0/NaN per IEEE 754
7591    /// roundToIntegral, which is exactly WASM Core §4.3.3 f64.ceil/floor/
7592    /// trunc/nearest. VRINTN/P/M live in the FE "always-execute" space (never
7593    /// IT-conditional; none of these sequences emits them inside an IT block).
7594    fn encode_thumb_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7595        let dd_num = vfp_dreg_to_num(dd)?;
7596        let dm_num = vfp_dreg_to_num(dm)?;
7597        let (vd, d) = encode_dreg(dd_num);
7598        let (vm, m) = encode_dreg(dm_num);
7599        // clang-verified bases (thumbv7em, fpv5-d16):
7600        //   vrintn.f64 d1,d2 = feb9 1b42 ; vrintp = feba 1b42
7601        //   vrintm.f64 d1,d2 = febb 1b42 ; vrintz = eeb6 1bc2
7602        let base: u32 = match mode {
7603            0b00 => 0xFEB90B40, // VRINTN.F64 (round to nearest, ties to even)
7604            0b01 => 0xFEBA0B40, // VRINTP.F64 (round toward +inf)
7605            0b10 => 0xFEBB0B40, // VRINTM.F64 (round toward -inf)
7606            _ => 0xEEB60BC0,    // VRINTZ.F64 (round toward zero)
7607        };
7608        Ok(vfp_to_thumb_bytes(
7609            base | (d << 22) | (vd << 12) | (m << 5) | vm,
7610        ))
7611    }
7612
7613    /// Encode F64 min/max as Thumb-2 with WASM Core §4.3.3 semantics:
7614    ///
7615    ///   VCMP.F64 Dn, Dm ; VMRS APSR_nzcv, FPSCR
7616    ///   VMINNM.F64/VMAXNM.F64 Dd, Dn, Dm      (FPv5; -0.0 < +0.0 ordered)
7617    ///   IT VS ; VADD.F64(VS) Dd, Dn, Dm       (unordered ⇒ NaN-propagating)
7618    ///
7619    /// VMINNM/VMAXNM alone are IEEE minNum/maxNum, which return the NUMBER
7620    /// when exactly one operand is NaN — WASM requires NaN. The VS-guarded
7621    /// VADD overwrites the result with a quiet NaN whenever the compare was
7622    /// unordered (either operand NaN); on the ordered path VMINNM/VMAXNM
7623    /// order -0.0 below +0.0, matching WASM's min(+0,-0) = -0 / max = +0.
7624    /// Clobbers ONLY Dd and the flags (the previous pseudo-op's ordered IT
7625    /// GT/MI select returned the WRONG operand for NaN and ±0 mixes).
7626    ///
7627    /// Ok-or-Err: `dd` must not alias `dn`/`dm` — the VS fix-up reads them
7628    /// AFTER VMINNM wrote `dd` (the selector always allocates a fresh
7629    /// destination while both sources are still marked live).
7630    fn encode_thumb_f64_minmax(
7631        &self,
7632        dd: &VfpReg,
7633        dn: &VfpReg,
7634        dm: &VfpReg,
7635        is_min: bool,
7636    ) -> Result<Vec<u8>> {
7637        if dd == dn || dd == dm {
7638            return Err(synth_core::Error::synthesis(format!(
7639                "F64{}: destination {dd:?} aliases a source ({dn:?},{dm:?}) — \
7640                 the unordered NaN fix-up would read a clobbered operand \
7641                 (compiler bug: the selector must allocate a fresh D-temp)",
7642                if is_min { "Min" } else { "Max" },
7643            )));
7644        }
7645        let mut bytes = Vec::new();
7646        let dd_num = vfp_dreg_to_num(dd)?;
7647        let dn_num = vfp_dreg_to_num(dn)?;
7648        let dm_num = vfp_dreg_to_num(dm)?;
7649        let (vd, d) = encode_dreg(dd_num);
7650        let (vn, n) = encode_dreg(dn_num);
7651        let (vm, m) = encode_dreg(dm_num);
7652
7653        // VCMP.F64 Dn, Dm (clang-verified: vcmp.f64 d2,d3 = eeb4 2b43)
7654        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7655        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7656        // VMRS APSR_nzcv, FPSCR
7657        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7658        // VMINNM.F64 / VMAXNM.F64 Dd, Dn, Dm (clang-verified:
7659        // vminnm.f64 d1,d2,d3 = fe82 1b43 ; vmaxnm = fe82 1b03)
7660        let base: u32 = if is_min { 0xFE800B40 } else { 0xFE800B00 };
7661        let vnm = base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7662        bytes.extend_from_slice(&vfp_to_thumb_bytes(vnm));
7663        // IT VS (unordered ⇒ at least one NaN operand)
7664        bytes.extend_from_slice(&0xBF68_u16.to_le_bytes());
7665        // VADD.F64(VS) Dd, Dn, Dm — NaN + x propagates a quiet NaN
7666        let vadd = 0xEE300B00 | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7667        bytes.extend_from_slice(&vfp_to_thumb_bytes(vadd));
7668
7669        Ok(bytes)
7670    }
7671
7672    /// Encode F64 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7673    /// encoder scratch, #212), the flags, and Dd:
7674    ///
7675    ///   VMOV R12, S(2m+1)   (high word of the SIGN source Dm)
7676    ///   CMP  R12, #0        (N flag = the sign bit)
7677    ///   VABS.F64 Dd, Dn     (magnitude, sign cleared)
7678    ///   IT MI ; VNEG.F64(MI) Dd, Dd
7679    ///
7680    /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7681    /// The R12 capture happens BEFORE Dd is written, so Dd aliasing Dn or Dm
7682    /// is safe. (The previous pseudo-op clobbered R0/R1/R2 behind the
7683    /// allocator's back — the #615 class.)
7684    fn encode_thumb_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7685        let dm_num = vfp_dreg_to_num(dm)?;
7686        if dm_num > 7 {
7687            return Err(synth_core::Error::synthesis(format!(
7688                "F64Copysign: sign source {dm:?} has no S-register alias \
7689                 (D8..D15) — the selector allocates only D0..D7"
7690            )));
7691        }
7692        let mut bytes = Vec::new();
7693        // VMOV R12, S(2m+1) — the sign source's high word.
7694        let (vn_s, n_s) = encode_sreg(2 * dm_num + 1);
7695        let vmov = 0xEE100A10 | (vn_s << 16) | (12 << 12) | (n_s << 7);
7696        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7697        // CMP R12, #0 (T2: CMP.W R12, #0) — N = bit31 of the sign word.
7698        bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7699        bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7700        // VABS.F64 Dd, Dn
7701        let dd_num = vfp_dreg_to_num(dd)?;
7702        let dn_num = vfp_dreg_to_num(dn)?;
7703        let (vd, d) = encode_dreg(dd_num);
7704        let (vn, n) = encode_dreg(dn_num);
7705        let vabs = 0xEEB00BC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7706        bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7707        // IT MI ; VNEG.F64(MI) Dd, Dd
7708        bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7709        let vneg = 0xEEB10B40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7710        bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7711
7712        Ok(bytes)
7713    }
7714
7715    /// Encode VCVT.S32/U32.F32 + VMOV as Thumb-2
7716    fn encode_thumb_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7717        let mut bytes = Vec::new();
7718
7719        let sm_num = vfp_sreg_to_num(sm)?;
7720        let (vd, d) = encode_sreg(sm_num);
7721        let (vm, m) = encode_sreg(sm_num);
7722        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
7723        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7724        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7725
7726        // VMOV Rd, Sm
7727        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
7728        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7729
7730        Ok(bytes)
7731    }
7732
7733    // === Thumb-2 32-bit encoding helpers ===
7734
7735    /// Encode Thumb-2 32-bit ADD with immediate
7736    fn encode_thumb32_add(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7737        let rd_bits = reg_to_bits(rd);
7738        let rn_bits = reg_to_bits(rn);
7739
7740        // The `i:imm3:imm8` field is split the same way for both forms.
7741        let i_bit = (imm >> 11) & 1;
7742        let imm3 = (imm >> 8) & 0x7;
7743        let imm8 = imm & 0xFF;
7744
7745        let hw1_base = if imm <= 0xFF {
7746            // ADD.W (T3): the field is a ThumbExpandImm modified immediate. For
7747            // imm <= 0xFF (i:imm3 = 0000) it is the zero-extended byte, which is
7748            // correct — keep this form so existing encodings stay bit-identical.
7749            0xF100
7750        } else if imm <= 0xFFF {
7751            // ADDW (T4): a PLAIN 12-bit immediate (0..4095) — no ThumbExpandImm.
7752            // This is what makes `add sp, sp, #frame` correct for frame sizes
7753            // >= 256, which ADD.W (T3) would silently mis-encode (e.g. #256 -> #0).
7754            0xF200
7755        } else {
7756            return Err(synth_core::Error::synthesis(
7757                "ADD immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7758            ));
7759        };
7760
7761        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7762        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7763
7764        let mut bytes = hw1.to_le_bytes().to_vec();
7765        bytes.extend_from_slice(&hw2.to_le_bytes());
7766        Ok(bytes)
7767    }
7768
7769    /// Encode Thumb-2 32-bit SUB with immediate
7770    fn encode_thumb32_sub(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7771        let rd_bits = reg_to_bits(rd);
7772        let rn_bits = reg_to_bits(rn);
7773
7774        let i_bit = (imm >> 11) & 1;
7775        let imm3 = (imm >> 8) & 0x7;
7776        let imm8 = imm & 0xFF;
7777
7778        let hw1_base = if imm <= 0xFF {
7779            // SUB.W (T3) modified immediate — correct for the zero-extended byte
7780            // (imm <= 0xFF). Kept bit-identical for existing encodings.
7781            0xF1A0
7782        } else if imm <= 0xFFF {
7783            // SUBW (T4): plain 12-bit immediate (0..4095). Makes
7784            // `sub sp, sp, #frame` correct for frame sizes >= 256.
7785            0xF2A0
7786        } else {
7787            return Err(synth_core::Error::synthesis(
7788                "SUB immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7789            ));
7790        };
7791
7792        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7793        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7794
7795        let mut bytes = hw1.to_le_bytes().to_vec();
7796        bytes.extend_from_slice(&hw2.to_le_bytes());
7797        Ok(bytes)
7798    }
7799
7800    /// Encode Thumb-2 32-bit ADDS with immediate (sets flags)
7801    fn encode_thumb32_adds(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7802        let rd_bits = reg_to_bits(rd);
7803        let rn_bits = reg_to_bits(rn);
7804
7805        // ADDS.W (flag-setting) has only the modified-immediate form — error on
7806        // an un-encodable value rather than silently add the wrong constant.
7807        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7808            synth_core::Error::synthesis(
7809                "ADDS immediate is not a valid ThumbExpandImm — materialize into a register",
7810            )
7811        })?;
7812        let i_bit = (field >> 11) & 1;
7813        let imm3 = (field >> 8) & 0x7;
7814        let imm8 = field & 0xFF;
7815
7816        // ADDS.W Rd, Rn, #imm (with S=1)
7817        // First halfword: 1111 0 i 0 1000 1 Rn = F110 | i<<10 | Rn
7818        let hw1: u16 = (0xF110 | (i_bit << 10) | rn_bits) as u16;
7819        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7820
7821        let mut bytes = hw1.to_le_bytes().to_vec();
7822        bytes.extend_from_slice(&hw2.to_le_bytes());
7823        Ok(bytes)
7824    }
7825
7826    /// Encode Thumb-2 32-bit SUBS with immediate (sets flags)
7827    fn encode_thumb32_subs(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7828        let rd_bits = reg_to_bits(rd);
7829        let rn_bits = reg_to_bits(rn);
7830
7831        // SUBS.W (flag-setting) has only the modified-immediate form — error on
7832        // an un-encodable value rather than silently subtract the wrong constant.
7833        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7834            synth_core::Error::synthesis(
7835                "SUBS immediate is not a valid ThumbExpandImm — materialize into a register",
7836            )
7837        })?;
7838        let i_bit = (field >> 11) & 1;
7839        let imm3 = (field >> 8) & 0x7;
7840        let imm8 = field & 0xFF;
7841
7842        // SUBS.W Rd, Rn, #imm (with S=1)
7843        // First halfword: 1111 0 i 0 1101 1 Rn = F1B0 | i<<10 | Rn
7844        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7845        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7846
7847        let mut bytes = hw1.to_le_bytes().to_vec();
7848        bytes.extend_from_slice(&hw2.to_le_bytes());
7849        Ok(bytes)
7850    }
7851
7852    /// Encode Thumb-2 32-bit MOVW (16-bit immediate)
7853    ///
7854    /// # Contract (Verus-style)
7855    /// ```text
7856    /// requires rd <= R14
7857    /// ensures result.len() == 4
7858    /// ensures (imm & 0xFFFF) can be reconstructed from the encoding
7859    /// ```
7860    fn encode_thumb32_movw(&self, rd: &Reg, imm: u32) -> Result<Vec<u8>> {
7861        let rd_bits = reg_to_bits(rd);
7862        reg_bits_checked(rd_bits)?;
7863        let imm16 = imm & 0xFFFF;
7864
7865        // MOVW Rd, #imm16
7866        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7867        let imm4 = (imm16 >> 12) & 0xF;
7868        let i_bit = (imm16 >> 11) & 1;
7869        let imm3 = (imm16 >> 8) & 0x7;
7870        let imm8 = imm16 & 0xFF;
7871
7872        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7873        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7874
7875        let mut bytes = hw1.to_le_bytes().to_vec();
7876        bytes.extend_from_slice(&hw2.to_le_bytes());
7877        encoding_contracts::verify_thumb32(&bytes);
7878        Ok(bytes)
7879    }
7880
7881    /// Encode Thumb-2 32-bit shift with immediate
7882    ///
7883    /// # Contract (Verus-style)
7884    /// ```text
7885    /// requires rd <= R14, rm <= R14
7886    /// ensures result.len() == 4
7887    /// ```
7888    fn encode_thumb32_shift(
7889        &self,
7890        rd: &Reg,
7891        rm: &Reg,
7892        shift: u32,
7893        shift_type: u8,
7894    ) -> Result<Vec<u8>> {
7895        let rd_bits = reg_to_bits(rd);
7896        let rm_bits = reg_to_bits(rm);
7897        reg_bits_checked(rd_bits)?;
7898        reg_bits_checked(rm_bits)?;
7899        let imm5 = shift & 0x1F;
7900        let imm2 = imm5 & 0x3;
7901        let imm3 = (imm5 >> 2) & 0x7;
7902
7903        // MOV.W Rd, Rm, <shift> #imm
7904        // EA4F 0 imm3 Rd imm2 type Rm
7905        let hw1: u16 = 0xEA4F;
7906        let hw2: u16 =
7907            ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | ((shift_type as u32) << 4) | rm_bits)
7908                as u16;
7909
7910        let mut bytes = hw1.to_le_bytes().to_vec();
7911        bytes.extend_from_slice(&hw2.to_le_bytes());
7912        Ok(bytes)
7913    }
7914
7915    /// Encode Thumb-2 32-bit shift by register
7916    /// Encoding: 11111010 0xx0 Rn | 1111 Rd 0000 Rm
7917    /// shift_type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
7918    fn encode_thumb32_shift_reg(
7919        &self,
7920        rd: &Reg,
7921        rn: &Reg,
7922        rm: &Reg,
7923        shift_type: u8,
7924    ) -> Result<Vec<u8>> {
7925        let rd_bits = reg_to_bits(rd);
7926        let rn_bits = reg_to_bits(rn);
7927        let rm_bits = reg_to_bits(rm);
7928
7929        // hw1: 1111 1010 0xx0 Rn
7930        let hw1: u16 = (0xFA00 | ((shift_type as u32) << 5) | rn_bits) as u16;
7931        // hw2: 1111 Rd 0000 Rm
7932        let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
7933
7934        let mut bytes = hw1.to_le_bytes().to_vec();
7935        bytes.extend_from_slice(&hw2.to_le_bytes());
7936        Ok(bytes)
7937    }
7938
7939    /// Encode Thumb-2 32-bit CMP with immediate
7940    fn encode_thumb32_cmp_imm(&self, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7941        let rn_bits = reg_to_bits(rn);
7942
7943        // CMP.W has only the modified-immediate form (no plain-imm12 like ADDW),
7944        // so an un-encodable immediate MUST be materialized into a register by
7945        // the selector. Error rather than silently compare the wrong constant.
7946        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7947            synth_core::Error::synthesis(
7948                "CMP immediate is not a valid ThumbExpandImm — materialize into a register",
7949            )
7950        })?;
7951        let i_bit = (field >> 11) & 1;
7952        let imm3 = (field >> 8) & 0x7;
7953        let imm8 = field & 0xFF;
7954
7955        // CMP.W Rn, #imm
7956        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7957        let hw2: u16 = ((imm3 << 12) | 0x0F00 | imm8) as u16;
7958
7959        let mut bytes = hw1.to_le_bytes().to_vec();
7960        bytes.extend_from_slice(&hw2.to_le_bytes());
7961        Ok(bytes)
7962    }
7963
7964    /// #372/#382: resolve the base register AND residual immediate offset for an
7965    /// `I64Ldr`/`I64Str` whose address may carry an index register. Returns
7966    /// `(base, low_offset)`; the caller accesses the halves at `[base,
7967    /// #low_offset]` and `[base, #low_offset + 4]`.
7968    ///
7969    /// - Frame access (no `offset_reg`, e.g. a spilled local at `[SP, #off]`):
7970    ///   returns `(addr.base, off)` and emits NOTHING — byte-identical.
7971    /// - Memory access (`reg_imm(R11, addr, offset)` = `R11 + addr + offset`)
7972    ///   with `offset + 4 <= 0xFFF`: emits `ADD.W ip, base, index` and returns
7973    ///   `(ip, offset)`, folding `offset`/`offset+4` into the halves' imm12.
7974    ///   Byte-identical to the pre-#382 (#372) behavior.
7975    /// - Memory access with `offset + 4 > 0xFFF`: the imm12 form cannot hold the
7976    ///   high half's offset, so `encode_thumb32_ldr`'s `check_ldst_imm12` (#259)
7977    ///   rightly refused it and the WHOLE function was skipped (#382). Instead
7978    ///   MATERIALIZE the offset into the base: `ADD ip, index, #offset` (against
7979    ///   the read-only INDEX register, so `encode_thumb32_add_imm` never trips its
7980    ///   `rd==rn==R12` alias trap), then `ADD.W ip, ip, base` (+ R11), and return
7981    ///   `(ip, 0)` so the halves use `[ip, #0]` / `[ip, #4]`.
7982    ///
7983    /// The effective address is fully materialized into `ip` BEFORE the halves
7984    /// are accessed, so an `rdlo` aliasing the index register is safe.
7985    ///
7986    /// RQ-63-ARMI64OFF (#1165): the wasm memarg is unsigned and reaches the
7987    /// encoder through the selector's `as i32` cast, so a memarg `>= 2^31`
7988    /// arrives NEGATIVE. It is read back as `u32` (lossless) and the
7989    /// materialization carries the full 32 bits — `base + index + offset (mod
7990    /// 2^32)`, the arithmetic the Thumb-2 word path (`encode_thumb32_add_imm`)
7991    /// and the software bounds guard already use for the same memarg. This
7992    /// used to CLAMP a negative offset to 0, so `i64.load offset=0xfffffff8`
7993    /// silently read `[R11 + addr]` — a wrong address, and a divergence from
7994    /// the i32 form of the same memarg. A FRAME access (no index) with an
7995    /// out-of-range or negative offset is refused by `check_ldst_imm12` in the
7996    /// halves — loud, never clamped.
7997    fn i64_effective_base(&self, bytes: &mut Vec<u8>, addr: &MemAddr) -> Result<(Reg, u32)> {
7998        let offset = addr.offset as u32;
7999        match addr.offset_reg {
8000            Some(idx) => {
8001                let ip = Reg::R12;
8002                // The high half sits at +4, which must itself fit imm12.
8003                if offset > 0xFFB {
8004                    // Large static offset (#382): fold it (and R11) into ip so the
8005                    // imm12 halves stay in range instead of skipping the function.
8006                    // ADD ip, index, #offset  (index != ip → no add_imm alias trap)
8007                    bytes.extend_from_slice(&self.encode_thumb32_add_imm(&ip, &idx, offset)?);
8008                    // ADD.W ip, ip, base  (+ R11)
8009                    bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
8010                        reg_to_bits(&ip),
8011                        reg_to_bits(&ip),
8012                        reg_to_bits(&addr.base),
8013                    )?);
8014                    Ok((ip, 0))
8015                } else {
8016                    // ADD.W ip, addr.base, idx  (Thumb-2, byte-verified vs as)
8017                    let hw1: u16 = 0xEB00 | reg_to_bits(&addr.base) as u16;
8018                    let hw2: u16 = 0x0C00 | reg_to_bits(&idx) as u16;
8019                    bytes.extend_from_slice(&hw1.to_le_bytes());
8020                    bytes.extend_from_slice(&hw2.to_le_bytes());
8021                    Ok((ip, offset))
8022                }
8023            }
8024            None => Ok((addr.base, offset)),
8025        }
8026    }
8027
8028    /// Encode Thumb-2 32-bit LDR
8029    fn encode_thumb32_ldr(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8030        let rd_bits = reg_to_bits(rd);
8031        let base_bits = reg_to_bits(base);
8032
8033        // LDR.W Rd, [Rn, #imm12]
8034        check_ldst_imm12(offset)?;
8035        let hw1: u16 = (0xF8D0 | base_bits) as u16;
8036        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8037
8038        let mut bytes = hw1.to_le_bytes().to_vec();
8039        bytes.extend_from_slice(&hw2.to_le_bytes());
8040        Ok(bytes)
8041    }
8042
8043    /// Encode Thumb-2 32-bit STR
8044    fn encode_thumb32_str(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8045        let rd_bits = reg_to_bits(rd);
8046        let base_bits = reg_to_bits(base);
8047
8048        // STR.W Rd, [Rn, #imm12]
8049        check_ldst_imm12(offset)?;
8050        let hw1: u16 = (0xF8C0 | base_bits) as u16;
8051        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8052
8053        let mut bytes = hw1.to_le_bytes().to_vec();
8054        bytes.extend_from_slice(&hw2.to_le_bytes());
8055        Ok(bytes)
8056    }
8057
8058    /// Encode Thumb-2 32-bit LDR with register offset: LDR.W Rd, [Rn, Rm]
8059    fn encode_thumb32_ldr_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8060        let rd_bits = reg_to_bits(rd);
8061        let base_bits = reg_to_bits(base);
8062        let rm_bits = reg_to_bits(offset_reg);
8063
8064        // LDR.W Rd, [Rn, Rm, LSL #0]
8065        // Encoding: 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
8066        // imm2 = 00 for no shift (LSL #0)
8067        let hw1: u16 = (0xF850 | base_bits) as u16;
8068        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8069
8070        let mut bytes = hw1.to_le_bytes().to_vec();
8071        bytes.extend_from_slice(&hw2.to_le_bytes());
8072        Ok(bytes)
8073    }
8074
8075    /// Encode Thumb-2 32-bit STR with register offset: STR.W Rd, [Rn, Rm]
8076    fn encode_thumb32_str_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8077        let rd_bits = reg_to_bits(rd);
8078        let base_bits = reg_to_bits(base);
8079        let rm_bits = reg_to_bits(offset_reg);
8080
8081        // STR.W Rd, [Rn, Rm, LSL #0]
8082        // Encoding: 1111 1000 0100 Rn | Rt 0000 00 imm2 Rm
8083        // imm2 = 00 for no shift (LSL #0)
8084        let hw1: u16 = (0xF840 | base_bits) as u16;
8085        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8086
8087        let mut bytes = hw1.to_le_bytes().to_vec();
8088        bytes.extend_from_slice(&hw2.to_le_bytes());
8089        Ok(bytes)
8090    }
8091
8092    // === Sub-word load/store Thumb-2 encoding helpers ===
8093
8094    /// Encode Thumb-2 32-bit LDRB with immediate: LDRB.W Rd, [Rn, #imm12]
8095    fn encode_thumb32_ldrb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8096        let rd_bits = reg_to_bits(rd);
8097        let base_bits = reg_to_bits(base);
8098        // LDRB.W Rd, [Rn, #imm12]: 1111 1000 1001 Rn | Rt imm12
8099        check_ldst_imm12(offset)?;
8100        let hw1: u16 = (0xF890 | base_bits) as u16;
8101        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8102        let mut bytes = hw1.to_le_bytes().to_vec();
8103        bytes.extend_from_slice(&hw2.to_le_bytes());
8104        Ok(bytes)
8105    }
8106
8107    /// Encode Thumb-2 32-bit LDRB with register: LDRB.W Rd, [Rn, Rm]
8108    fn encode_thumb32_ldrb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8109        let rd_bits = reg_to_bits(rd);
8110        let base_bits = reg_to_bits(base);
8111        let rm_bits = reg_to_bits(offset_reg);
8112        // LDRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0001 Rn | Rt 0000 00 imm2 Rm
8113        let hw1: u16 = (0xF810 | base_bits) as u16;
8114        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8115        let mut bytes = hw1.to_le_bytes().to_vec();
8116        bytes.extend_from_slice(&hw2.to_le_bytes());
8117        Ok(bytes)
8118    }
8119
8120    /// Encode Thumb-2 32-bit LDRSB with immediate: LDRSB.W Rd, [Rn, #imm12]
8121    fn encode_thumb32_ldrsb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8122        let rd_bits = reg_to_bits(rd);
8123        let base_bits = reg_to_bits(base);
8124        // LDRSB.W Rd, [Rn, #imm12]: 1111 1001 1001 Rn | Rt imm12
8125        check_ldst_imm12(offset)?;
8126        let hw1: u16 = (0xF990 | base_bits) as u16;
8127        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8128        let mut bytes = hw1.to_le_bytes().to_vec();
8129        bytes.extend_from_slice(&hw2.to_le_bytes());
8130        Ok(bytes)
8131    }
8132
8133    /// Encode Thumb-2 32-bit LDRSB with register: LDRSB.W Rd, [Rn, Rm]
8134    fn encode_thumb32_ldrsb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8135        let rd_bits = reg_to_bits(rd);
8136        let base_bits = reg_to_bits(base);
8137        let rm_bits = reg_to_bits(offset_reg);
8138        // LDRSB.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0001 Rn | Rt 0000 00 imm2 Rm
8139        let hw1: u16 = (0xF910 | base_bits) as u16;
8140        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8141        let mut bytes = hw1.to_le_bytes().to_vec();
8142        bytes.extend_from_slice(&hw2.to_le_bytes());
8143        Ok(bytes)
8144    }
8145
8146    /// Encode Thumb-2 32-bit LDRH with immediate: LDRH.W Rd, [Rn, #imm12]
8147    fn encode_thumb32_ldrh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8148        let rd_bits = reg_to_bits(rd);
8149        let base_bits = reg_to_bits(base);
8150        // LDRH.W Rd, [Rn, #imm12]: 1111 1000 1011 Rn | Rt imm12
8151        check_ldst_imm12(offset)?;
8152        let hw1: u16 = (0xF8B0 | base_bits) as u16;
8153        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8154        let mut bytes = hw1.to_le_bytes().to_vec();
8155        bytes.extend_from_slice(&hw2.to_le_bytes());
8156        Ok(bytes)
8157    }
8158
8159    /// Encode Thumb-2 32-bit LDRH with register: LDRH.W Rd, [Rn, Rm]
8160    fn encode_thumb32_ldrh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8161        let rd_bits = reg_to_bits(rd);
8162        let base_bits = reg_to_bits(base);
8163        let rm_bits = reg_to_bits(offset_reg);
8164        // LDRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0011 Rn | Rt 0000 00 imm2 Rm
8165        let hw1: u16 = (0xF830 | base_bits) as u16;
8166        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8167        let mut bytes = hw1.to_le_bytes().to_vec();
8168        bytes.extend_from_slice(&hw2.to_le_bytes());
8169        Ok(bytes)
8170    }
8171
8172    /// Encode Thumb-2 32-bit LDRSH with immediate: LDRSH.W Rd, [Rn, #imm12]
8173    fn encode_thumb32_ldrsh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8174        let rd_bits = reg_to_bits(rd);
8175        let base_bits = reg_to_bits(base);
8176        // LDRSH.W Rd, [Rn, #imm12]: 1111 1001 1011 Rn | Rt imm12
8177        check_ldst_imm12(offset)?;
8178        let hw1: u16 = (0xF9B0 | base_bits) as u16;
8179        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8180        let mut bytes = hw1.to_le_bytes().to_vec();
8181        bytes.extend_from_slice(&hw2.to_le_bytes());
8182        Ok(bytes)
8183    }
8184
8185    /// Encode Thumb-2 32-bit LDRSH with register: LDRSH.W Rd, [Rn, Rm]
8186    fn encode_thumb32_ldrsh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8187        let rd_bits = reg_to_bits(rd);
8188        let base_bits = reg_to_bits(base);
8189        let rm_bits = reg_to_bits(offset_reg);
8190        // LDRSH.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0011 Rn | Rt 0000 00 imm2 Rm
8191        let hw1: u16 = (0xF930 | base_bits) as u16;
8192        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8193        let mut bytes = hw1.to_le_bytes().to_vec();
8194        bytes.extend_from_slice(&hw2.to_le_bytes());
8195        Ok(bytes)
8196    }
8197
8198    /// Encode Thumb-2 32-bit STRB with immediate: STRB.W Rd, [Rn, #imm12]
8199    fn encode_thumb32_strb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8200        let rd_bits = reg_to_bits(rd);
8201        let base_bits = reg_to_bits(base);
8202        // STRB.W Rd, [Rn, #imm12]: 1111 1000 1000 Rn | Rt imm12
8203        check_ldst_imm12(offset)?;
8204        let hw1: u16 = (0xF880 | base_bits) as u16;
8205        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8206        let mut bytes = hw1.to_le_bytes().to_vec();
8207        bytes.extend_from_slice(&hw2.to_le_bytes());
8208        Ok(bytes)
8209    }
8210
8211    /// Encode Thumb-2 32-bit STRB with register: STRB.W Rd, [Rn, Rm]
8212    fn encode_thumb32_strb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8213        let rd_bits = reg_to_bits(rd);
8214        let base_bits = reg_to_bits(base);
8215        let rm_bits = reg_to_bits(offset_reg);
8216        // STRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0000 Rn | Rt 0000 00 imm2 Rm
8217        let hw1: u16 = (0xF800 | base_bits) as u16;
8218        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8219        let mut bytes = hw1.to_le_bytes().to_vec();
8220        bytes.extend_from_slice(&hw2.to_le_bytes());
8221        Ok(bytes)
8222    }
8223
8224    /// Encode Thumb-2 32-bit STRH with immediate: STRH.W Rd, [Rn, #imm12]
8225    fn encode_thumb32_strh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8226        let rd_bits = reg_to_bits(rd);
8227        let base_bits = reg_to_bits(base);
8228        // STRH.W Rd, [Rn, #imm12]: 1111 1000 1010 Rn | Rt imm12
8229        check_ldst_imm12(offset)?;
8230        let hw1: u16 = (0xF8A0 | base_bits) as u16;
8231        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8232        let mut bytes = hw1.to_le_bytes().to_vec();
8233        bytes.extend_from_slice(&hw2.to_le_bytes());
8234        Ok(bytes)
8235    }
8236
8237    /// Encode Thumb-2 32-bit STRH with register: STRH.W Rd, [Rn, Rm]
8238    fn encode_thumb32_strh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8239        let rd_bits = reg_to_bits(rd);
8240        let base_bits = reg_to_bits(base);
8241        let rm_bits = reg_to_bits(offset_reg);
8242        // STRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0010 Rn | Rt 0000 00 imm2 Rm
8243        let hw1: u16 = (0xF820 | base_bits) as u16;
8244        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8245        let mut bytes = hw1.to_le_bytes().to_vec();
8246        bytes.extend_from_slice(&hw2.to_le_bytes());
8247        Ok(bytes)
8248    }
8249
8250    /// Encode Thumb-2 32-bit ADD with immediate: ADD.W Rd, Rn, #imm
8251    fn encode_thumb32_add_imm(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
8252        let rd_bits = reg_to_bits(rd);
8253        let rn_bits = reg_to_bits(rn);
8254
8255        // In-range immediates (<= 0xFFF) delegate to `encode_thumb32_add`,
8256        // which picks the correct form per value:
8257        //   - imm <= 0xFF  -> ADD.W (T3). Its `i:imm3:imm8` field is a
8258        //     ThumbExpandImm MODIFIED immediate — raw == expanded only here.
8259        //   - 0x100..=0xFFF -> ADDW (T4, 0xF200): a PLAIN 12-bit immediate.
8260        //
8261        // #681: this function used to pack the raw value into the T3 field for
8262        // ALL imm <= 0xFFF. ThumbExpandImm(0x200) = 0 and ThumbExpandImm(0x400)
8263        // = 0x8000_0000, so every dynamic-address load/store with a static
8264        // offset in 0x100..=0xFFF silently computed a WRONG address — and in
8265        // --safety-bounds software the guard checked the intended address while
8266        // the access used the mis-encoded one (bounds bypass). Same
8267        // ThumbExpandImm raw-packing class as #253/#255, reached via #382.
8268        if imm <= 0xFFF {
8269            self.encode_thumb32_add(rd, rn, imm)
8270        } else {
8271            // Out-of-range immediate (> 0xFFF): materialize it into a scratch
8272            // register, then ADD.W Rd, Rn, scratch. This is the #180/#185
8273            // "encoder must produce a legal sequence, not assert" class — see #350.
8274            //
8275            // Scratch choice (must NEVER equal Rn, or Rn would be clobbered before
8276            // the ADD reads it):
8277            //   - rd != rn  => use rd itself (rn is untouched, since rd != rn).
8278            //   - rd == rn  => use R12/IP (the reserved encoder scratch). rd/rn are
8279            //                  never R12 (R12 is non-allocatable), so it can't alias.
8280            //
8281            // The materialized value is the same whether or not MOVT is emitted, so
8282            // the byte length depends only on `imm` (and rd==rn) — the size probe and
8283            // the final emit therefore agree (mandatory: the function is encoded twice).
8284            let scratch: u32 = if rd_bits == rn_bits {
8285                12 // R12/IP — in-place add, can't use rd because rd == rn
8286            } else {
8287                rd_bits // rn is preserved because rd != rn
8288            };
8289            // Invariant: the scratch must never alias Rn (would clobber it before
8290            // the ADD reads it). Unreachable in real codegen (rd/rn are never R12,
8291            // which is reserved encoder scratch), but the encoder is also driven by
8292            // the `encoder_no_panic` fuzz harness with ARBITRARY registers — incl.
8293            // rd==rn==R12, which makes scratch (R12) alias Rn. The encoder contract
8294            // (#180/#185) is Ok-or-Err, never a panic, so return a typed error
8295            // instead of asserting. #350 follow-up.
8296            if scratch == rn_bits {
8297                return Err(synth_core::Error::synthesis(format!(
8298                    "ADD #imm: cannot lower #{imm:#x} for Rd==Rn==R12 — no free scratch \
8299                     register (R12 is the reserved encoder scratch and aliases Rn here)"
8300                )));
8301            }
8302
8303            let lo16 = imm & 0xFFFF;
8304            let hi16 = (imm >> 16) & 0xFFFF;
8305
8306            let mut bytes = self.encode_thumb32_movw_raw(scratch, lo16)?;
8307            if hi16 != 0 {
8308                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(scratch, hi16)?);
8309            }
8310            bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rn_bits, scratch)?);
8311            Ok(bytes)
8312        }
8313    }
8314
8315    // === Raw encoding helpers for POPCNT (take register numbers directly) ===
8316
8317    /// Encode Thumb-2 32-bit MOVW (16-bit immediate) - raw version
8318    ///
8319    /// # Contract (Verus-style)
8320    /// ```text
8321    /// requires rd <= 14, imm16 <= 0xFFFF
8322    /// ensures result.len() == 4
8323    /// ```
8324    fn encode_thumb32_movw_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8325        reg_bits_checked(rd)?;
8326        encoding_contracts::verify_imm16(imm16);
8327        // MOVW Rd, #imm16
8328        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
8329        let imm16 = imm16 & 0xFFFF;
8330        let imm4 = (imm16 >> 12) & 0xF;
8331        let i_bit = (imm16 >> 11) & 1;
8332        let imm3 = (imm16 >> 8) & 0x7;
8333        let imm8 = imm16 & 0xFF;
8334
8335        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
8336        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8337
8338        let mut bytes = hw1.to_le_bytes().to_vec();
8339        bytes.extend_from_slice(&hw2.to_le_bytes());
8340        encoding_contracts::verify_thumb32(&bytes);
8341        Ok(bytes)
8342    }
8343
8344    /// Encode Thumb-2 32-bit MOVT (move top 16 bits) - raw version
8345    ///
8346    /// # Contract (Verus-style)
8347    /// ```text
8348    /// requires rd <= 14, imm16 <= 0xFFFF
8349    /// ensures result.len() == 4
8350    /// ```
8351    fn encode_thumb32_movt_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8352        reg_bits_checked(rd)?;
8353        encoding_contracts::verify_imm16(imm16);
8354        // MOVT Rd, #imm16
8355        // 1111 0 i 10 1 1 0 0 imm4 | 0 imm3 Rd imm8
8356        let imm16 = imm16 & 0xFFFF;
8357        let imm4 = (imm16 >> 12) & 0xF;
8358        let i_bit = (imm16 >> 11) & 1;
8359        let imm3 = (imm16 >> 8) & 0x7;
8360        let imm8 = imm16 & 0xFF;
8361
8362        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
8363        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8364
8365        let mut bytes = hw1.to_le_bytes().to_vec();
8366        bytes.extend_from_slice(&hw2.to_le_bytes());
8367        encoding_contracts::verify_thumb32(&bytes);
8368        Ok(bytes)
8369    }
8370
8371    /// Encode Thumb-2 32-bit LSR (logical shift right) with immediate - raw version
8372    fn encode_thumb32_lsr_raw(&self, rd: u32, rm: u32, shift: u32) -> Result<Vec<u8>> {
8373        // MOV.W Rd, Rm, LSR #imm
8374        // EA4F 0 imm3 Rd imm2 01 Rm
8375        let imm5 = shift & 0x1F;
8376        let imm2 = imm5 & 0x3;
8377        let imm3 = (imm5 >> 2) & 0x7;
8378
8379        let hw1: u16 = 0xEA4F;
8380        let hw2: u16 = ((imm3 << 12) | (rd << 8) | (imm2 << 6) | (0b01 << 4) | rm) as u16;
8381
8382        let mut bytes = hw1.to_le_bytes().to_vec();
8383        bytes.extend_from_slice(&hw2.to_le_bytes());
8384        Ok(bytes)
8385    }
8386
8387    /// Encode Thumb-2 32-bit AND with immediate - raw version
8388    fn encode_thumb32_and_imm_raw(&self, rd: u32, rn: u32, imm: u32) -> Result<Vec<u8>> {
8389        // AND.W Rd, Rn, #<modified_immediate>
8390        // F0 00 Rn | 0 imm3 Rd imm8
8391        //
8392        // #681 class audit: the field is a ThumbExpandImm modified immediate,
8393        // not a raw value. The only current caller (POPCNT final mask) passes
8394        // 0x3F, which expands to itself — the gate is byte-identical today and
8395        // closes the raw-packing landmine for any future caller.
8396        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
8397            synth_core::Error::synthesis(
8398                "AND immediate is not a valid ThumbExpandImm — materialize into a register",
8399            )
8400        })?;
8401        let i_bit = (field >> 11) & 1;
8402        let imm3 = (field >> 8) & 0x7;
8403        let imm8 = field & 0xFF;
8404
8405        let hw1: u16 = (0xF000 | (i_bit << 10) | rn) as u16;
8406        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8407
8408        let mut bytes = hw1.to_le_bytes().to_vec();
8409        bytes.extend_from_slice(&hw2.to_le_bytes());
8410        Ok(bytes)
8411    }
8412
8413    /// Encode Thumb-2 32-bit SUB (register) - raw version
8414    fn encode_thumb32_sub_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8415        // SUB.W Rd, Rn, Rm
8416        // EBA0 Rn | 0 Rd 00 00 Rm
8417        let hw1: u16 = (0xEBA0 | rn) as u16;
8418        let hw2: u16 = ((rd << 8) | rm) as u16;
8419
8420        let mut bytes = hw1.to_le_bytes().to_vec();
8421        bytes.extend_from_slice(&hw2.to_le_bytes());
8422        Ok(bytes)
8423    }
8424
8425    /// Encode Thumb-2 32-bit ADD (register) - raw version
8426    fn encode_thumb32_add_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8427        // ADD.W Rd, Rn, Rm
8428        // EB00 Rn | 0 Rd 00 00 Rm
8429        let hw1: u16 = (0xEB00 | rn) as u16;
8430        let hw2: u16 = ((rd << 8) | rm) as u16;
8431
8432        let mut bytes = hw1.to_le_bytes().to_vec();
8433        bytes.extend_from_slice(&hw2.to_le_bytes());
8434        Ok(bytes)
8435    }
8436
8437    /// Encode Thumb-2 32-bit ADDS (register, flag-setting) - raw version.
8438    /// Used as the high-register fallback for `ArmOp::Adds` (i64 low-word add)
8439    /// so R8-R11 pair operands don't overflow the 16-bit field — #178/#180.
8440    fn encode_thumb32_adds_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8441        // ADDS.W Rd, Rn, Rm (T3, S=1): EB10 Rn | 0 Rd 00 00 Rm
8442        let hw1: u16 = (0xEB10 | rn) as u16;
8443        let hw2: u16 = ((rd << 8) | rm) as u16;
8444        let mut bytes = hw1.to_le_bytes().to_vec();
8445        bytes.extend_from_slice(&hw2.to_le_bytes());
8446        Ok(bytes)
8447    }
8448
8449    /// Encode Thumb-2 32-bit SUBS (register, flag-setting) - raw version.
8450    /// High-register fallback for `ArmOp::Subs` (i64 low-word subtract) — #178/#180.
8451    fn encode_thumb32_subs_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8452        // SUBS.W Rd, Rn, Rm (T3, S=1): EBB0 Rn | 0 Rd 00 00 Rm
8453        let hw1: u16 = (0xEBB0 | rn) as u16;
8454        let hw2: u16 = ((rd << 8) | rm) as u16;
8455        let mut bytes = hw1.to_le_bytes().to_vec();
8456        bytes.extend_from_slice(&hw2.to_le_bytes());
8457        Ok(bytes)
8458    }
8459
8460    /// Encode a sequence of ARM instructions
8461    pub fn encode_sequence(&self, ops: &[ArmOp]) -> Result<Vec<u8>> {
8462        let mut code = Vec::new();
8463
8464        for op in ops {
8465            let encoded = self.encode(op)?;
8466            code.extend_from_slice(&encoded);
8467        }
8468
8469        Ok(code)
8470    }
8471}
8472
8473/// Convert register to bit encoding (0-15)
8474/// Reverse of the ARMv7-M `ThumbExpandImm`: given a 32-bit immediate, return the
8475/// 12-bit `i:imm3:imm8` field if it is a representable modified immediate, else
8476/// `None` (the caller must materialize the value into a register). This is the
8477/// shared correct path for the data-processing immediate encoders — without it
8478/// they pack raw bits and silently mis-encode any value `> 0xFF` that isn't a
8479/// modified immediate (the silent-miscompile class behind #251/#253/#255).
8480fn try_thumb_expand_imm(value: u32) -> Option<u32> {
8481    // i:imm3 = 0000 → 8-bit value, zero-extended (00000000 00000000 00000000 XY).
8482    if value <= 0xFF {
8483        return Some(value);
8484    }
8485    let b0 = value & 0xFF; // byte 0
8486    let b1 = (value >> 8) & 0xFF; // byte 1
8487    // 0x00XY00XY (i:imm3 = 0001) — XY in bytes 0 and 2
8488    if value == (b0 << 16) | b0 {
8489        return Some(0x100 | b0);
8490    }
8491    // 0xXY00XY00 (i:imm3 = 0010) — XY in bytes 1 and 3
8492    if value == (b1 << 24) | (b1 << 8) {
8493        return Some(0x200 | b1);
8494    }
8495    // 0xXYXYXYXY (i:imm3 = 0011) — XY in all four bytes
8496    if value == (b0 << 24) | (b0 << 16) | (b0 << 8) | b0 {
8497        return Some(0x300 | b0);
8498    }
8499    // An 8-bit value with bit 7 set, rotated right by 8..=31. `rotate_left(rot)`
8500    // undoes the encoded right rotation; if the result is `1bbbbbbb` (0x80..=0xFF)
8501    // the value is representable. imm12[11:7] = rot, imm12[6:0] = low 7 bits.
8502    for rot in 8..=31u32 {
8503        let unrot = value.rotate_left(rot);
8504        if (0x80..=0xFF).contains(&unrot) {
8505            return Some((rot << 7) | (unrot & 0x7F));
8506        }
8507    }
8508    None
8509}
8510
8511/// Guard a Thumb-2 `LDR/STR Rd, [Rn, #imm12]` offset. The imm12 form supports
8512/// `0..=4095`; a larger offset must be materialized into a register by the
8513/// selector (register-offset addressing). Returning `Err` rather than silently
8514/// masking `offset & 0xFFF` closes the wrong-address miscompile class (#259,
8515/// the load/store sibling of #253/#255).
8516fn check_ldst_imm12(offset: u32) -> Result<()> {
8517    if offset > 0xFFF {
8518        Err(synth_core::Error::synthesis(
8519            "load/store immediate offset > 0xFFF (4095) — materialize the offset into a register",
8520        ))
8521    } else {
8522        Ok(())
8523    }
8524}
8525
8526/// #916 — emit `Rd = 0` in Thumb-2, correctly for EVERY destination register.
8527///
8528/// The 16-bit `MOVS Rd, #imm8` (T1) is `0010 0 Rd(3) imm8` — the Rd field is
8529/// **three bits**. For R8-R12 `reg_to_bits` yields 8..12, so `rd_bits << 8`
8530/// overflows into bit 11 and `0x2000 | 0x0800` is `0x2800` = `CMP r0, #0`:
8531/// not a move at all. The destination is never written (it keeps stale data)
8532/// and the flags are clobbered. Same class as #180 / H-CODE-9, and the same
8533/// defect #311 fixed for `I64SetCond`.
8534///
8535/// High registers therefore take the 32-bit `MOV.W Rd, #imm8` (T2,
8536/// `F04F 0000 | Rd<<8 | imm8`), whose Rd field is four bits. `MOV.W` with S=0
8537/// does not set flags, which is what these zero-fill sites want anyway.
8538///
8539/// **Callers with branches must consult [`thumb_zero_fill_halfwords`].** This
8540/// emits 1 halfword for R0-R7 and 2 for R8-R12; any branch whose target lies
8541/// PAST this instruction moves when it widens and its displacement has to be
8542/// derived rather than hard-coded. (A branch targeting this instruction's own
8543/// address is unaffected — an instruction cannot move itself.)
8544fn emit_thumb_zero_fill(bytes: &mut Vec<u8>, rd_bits: u32) {
8545    if rd_bits < 8 {
8546        let movs: u16 = 0x2000 | ((rd_bits as u16) << 8);
8547        bytes.extend_from_slice(&movs.to_le_bytes());
8548    } else {
8549        bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
8550        bytes.extend_from_slice(&((rd_bits as u16) << 8).to_le_bytes());
8551    }
8552}
8553
8554/// Halfword length of the encoding [`emit_thumb_zero_fill`] picks for
8555/// `rd_bits`. Branch displacements spanning the zero-fill derive from this so
8556/// the encoder cannot drift from itself (#916; the byte-size estimator mirrors
8557/// it in `synth_synthesis::estimate_arm_byte_size`, pinned by the #498
8558/// `estimator_encoder_agreement` oracle).
8559fn thumb_zero_fill_halfwords(rd_bits: u32) -> u16 {
8560    if rd_bits < 8 { 1 } else { 2 }
8561}
8562
8563fn reg_to_bits(reg: &Reg) -> u32 {
8564    match reg {
8565        Reg::R0 => 0,
8566        Reg::R1 => 1,
8567        Reg::R2 => 2,
8568        Reg::R3 => 3,
8569        Reg::R4 => 4,
8570        Reg::R5 => 5,
8571        Reg::R6 => 6,
8572        Reg::R7 => 7,
8573        Reg::R8 => 8,
8574        Reg::R9 => 9,
8575        Reg::R10 => 10,
8576        Reg::R11 => 11,
8577        Reg::R12 => 12,
8578        Reg::SP => 13,
8579        Reg::LR => 14,
8580        Reg::PC => 15,
8581    }
8582}
8583
8584// ======================================================================
8585// #610 — i64 fixed-ABI expansion wrappers.
8586//
8587// The hand-written multi-instruction i64 cores (rotl/rotr and the div/rem
8588// shift-subtract loops) compute in FIXED low registers. Before #610 the
8589// div/rem arms ignored their operand fields outright (hardcoded R0:R1 /
8590// R2:R3 in, result to R0:R1) and the rot arms used R3/R4 scratch that
8591// collided with selector-assigned registers — then restored the saved
8592// scratch OVER the result (`POP {R4}` with rd_lo == R4), so the op
8593// returned the caller's stale register: 0 for every input under qemu.
8594//
8595// These wrappers make each core honor its register parameters:
8596//   1. save R0-R3,
8597//   2. marshal the operand registers into the core's fixed input regs via
8598//      the stack (permutation-safe: every source is read before any fixed
8599//      register is written),
8600//   3. run the fixed-reg core (self-preserving for R4+; R12 is encoder
8601//      scratch and never allocatable, #212),
8602//   4. MOV the result pair from R0:R1 into the selector's rd pair,
8603//   5. restore R0-R3, skipping any register the result now occupies.
8604//
8605// All emitted lengths are register-independent so the optimized path's
8606// byte-size estimator (`estimate_arm_byte_size`, pinned by the
8607// estimator↔encoder agreement oracle #498/#511) stays a constant per op.
8608// ======================================================================
8609
8610/// Steps 1+2: `PUSH {R0-R3}`, then marshal `srcs` (operand registers, any of
8611/// R0-R12) into `R0..R<n>` via individual stack pushes. Sources are all read
8612/// before any destination register is written, so arbitrary source/target
8613/// permutations (including operands living in R0-R3) are safe.
8614fn emit_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8615    debug_assert!(srcs.len() <= 4);
8616    // PUSH {R0-R3} — save the caller-visible low registers.
8617    bytes.extend_from_slice(&0xB40Fu16.to_le_bytes());
8618    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8619    for src in srcs.iter().rev() {
8620        let rt = reg_to_bits(src) as u16;
8621        bytes.extend_from_slice(&0xF84Du16.to_le_bytes());
8622        bytes.extend_from_slice(&((rt << 12) | 0x0D04).to_le_bytes());
8623    }
8624    // POP {Ri} — Ri := srcs[i].
8625    for i in 0..srcs.len() as u16 {
8626        bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes());
8627    }
8628}
8629
8630/// Steps 4+5: move the core's R0:R1 result into the selector's rd pair, then
8631/// restore the R0-R3 saved by [`emit_i64_fixed_abi_entry`], skipping any
8632/// register the result now lives in (its saved caller word is discarded).
8633fn emit_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8634    let lo = reg_to_bits(rdlo);
8635    let hi = reg_to_bits(rdhi);
8636    if lo == 1 && hi == 0 {
8637        // A fully swapped pair would clobber one half in either MOV order.
8638        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8639        return Err(synth_core::Error::synthesis(
8640            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8641        ));
8642    }
8643    let mov16 = |bytes: &mut Vec<u8>, rd: u32, rm: u32| {
8644        let d = ((rd >> 3) & 1) as u16;
8645        bytes.extend_from_slice(
8646            &(0x4600u16 | (d << 7) | ((rm as u16) << 3) | ((rd & 7) as u16)).to_le_bytes(),
8647        );
8648    };
8649    if hi == 0 {
8650        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8651        mov16(bytes, lo, 0);
8652        mov16(bytes, hi, 1);
8653    } else {
8654        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8655        mov16(bytes, hi, 1);
8656        mov16(bytes, lo, 0);
8657    }
8658    for i in 0..4u32 {
8659        if i == lo || i == hi {
8660            // The result lives here — drop the saved caller word.
8661            bytes.extend_from_slice(&0xB001u16.to_le_bytes()); // ADD SP, #4
8662        } else {
8663            bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes()); // POP {Ri}
8664        }
8665    }
8666    Ok(())
8667}
8668
8669/// WASM `i64.div_*` / `i64.rem_*` by zero must trap, matching the i32 path's
8670/// cmp/bne/udf guard. Emitted after marshaling, when the divisor pair is in
8671/// R2:R3: `ORRS R12, R2, R3` — `BNE` over a `UDF #0` when nonzero.
8672fn emit_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8673    bytes.extend_from_slice(&0xEA52u16.to_le_bytes()); // ORRS.W R12, R2, R3
8674    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8675    bytes.extend_from_slice(&0xD100u16.to_le_bytes()); // BNE.N +0 (skip the UDF)
8676    bytes.extend_from_slice(&0xDE00u16.to_le_bytes()); // UDF #0 — divide by zero
8677}
8678
8679/// WASM `i64.div_s(INT64_MIN, -1)` must trap (Core §4.3.2 `idiv_s`: the
8680/// quotient +2^63 is unrepresentable), matching the i32 path's overflow
8681/// guard — #633: without it the core negated INT64_MIN onto itself and
8682/// silently returned INT64_MIN. Emitted after marshaling, when the dividend
8683/// pair is in R0:R1 and the divisor pair in R2:R3; R12 is encoder scratch.
8684///
8685/// div_s ONLY — `i64.rem_s(INT64_MIN, -1)` is defined as 0 and must NOT
8686/// trap (`irem_s`), so the I64RemS arm never calls this. 22 bytes,
8687/// register-independent (estimator contract, #498/#511).
8688fn emit_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8689    // AND.W R12, R2, R3 — R12 == 0xFFFFFFFF iff divisor == -1
8690    bytes.extend_from_slice(&0xEA02u16.to_le_bytes());
8691    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8692    // CMN.W R12, #1 — EQ iff both divisor words are all-ones
8693    bytes.extend_from_slice(&0xF11Cu16.to_le_bytes());
8694    bytes.extend_from_slice(&0x0F01u16.to_le_bytes());
8695    // BNE .no_trap
8696    bytes.extend_from_slice(&0xD105u16.to_le_bytes());
8697    // CMP R0, #0 — dividend lo word of INT64_MIN
8698    bytes.extend_from_slice(&0x2800u16.to_le_bytes());
8699    // BNE .no_trap
8700    bytes.extend_from_slice(&0xD103u16.to_le_bytes());
8701    // CMP.W R1, #0x80000000 — dividend hi word of INT64_MIN
8702    bytes.extend_from_slice(&0xF1B1u16.to_le_bytes());
8703    bytes.extend_from_slice(&0x4F00u16.to_le_bytes());
8704    // BNE .no_trap
8705    bytes.extend_from_slice(&0xD100u16.to_le_bytes());
8706    // UDF #0 — signed-division overflow
8707    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
8708    // .no_trap:
8709}
8710
8711// ======================================================================
8712// #615 — A32 (ARM-mode) twins of the #610 i64 fixed-ABI wrappers above.
8713// Identical register contract, A32 encodings: the multi-instruction i64
8714// cores (rotl/rotr, div/rem) compute in fixed low registers (value/dividend
8715// R0:R1, amount R2 / divisor R2:R3, result to R0:R1); the wrappers marshal
8716// the selector-assigned operand registers in and the result out, saving and
8717// restoring the caller-visible R0-R3 around the core.
8718// ======================================================================
8719
8720/// A32 steps 1+2: `STMDB SP!, {R0-R3}`, then marshal `srcs` into `R0..R<n>`
8721/// via individual stack pushes (`STR src, [SP, #-4]!` in reverse order, then
8722/// `LDR Ri, [SP], #4`). Every source is read before any fixed register is
8723/// written, so arbitrary source/target permutations are safe.
8724fn emit_a32_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8725    debug_assert!(srcs.len() <= 4);
8726    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8727    // PUSH {R0-R3} — save the caller-visible low registers.
8728    w(bytes, 0xE92D_000F);
8729    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8730    for src in srcs.iter().rev() {
8731        w(bytes, 0xE52D_0004 | (reg_to_bits(src) << 12));
8732    }
8733    // LDR Ri, [SP], #4 — Ri := srcs[i].
8734    for i in 0..srcs.len() as u32 {
8735        w(bytes, 0xE49D_0004 | (i << 12));
8736    }
8737}
8738
8739/// A32 steps 4+5: move the core's R0:R1 result into the selector's rd pair,
8740/// then restore the R0-R3 saved by [`emit_a32_i64_fixed_abi_entry`], skipping
8741/// any register the result now lives in (its saved caller word is discarded).
8742fn emit_a32_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8743    let lo = reg_to_bits(rdlo);
8744    let hi = reg_to_bits(rdhi);
8745    if lo == 1 && hi == 0 {
8746        // A fully swapped pair would clobber one half in either MOV order.
8747        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8748        return Err(synth_core::Error::synthesis(
8749            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8750        ));
8751    }
8752    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8753    let mov = |bytes: &mut Vec<u8>, rd: u32, rm: u32| w(bytes, 0xE1A0_0000 | (rd << 12) | rm);
8754    if hi == 0 {
8755        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8756        mov(bytes, lo, 0);
8757        mov(bytes, hi, 1);
8758    } else {
8759        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8760        mov(bytes, hi, 1);
8761        mov(bytes, lo, 0);
8762    }
8763    for i in 0..4u32 {
8764        if i == lo || i == hi {
8765            // The result lives here — drop the saved caller word.
8766            w(bytes, 0xE28D_D004); // ADD SP, SP, #4
8767        } else {
8768            w(bytes, 0xE49D_0004 | (i << 12)); // LDR Ri, [SP], #4
8769        }
8770    }
8771    Ok(())
8772}
8773
8774/// A32 zero-divisor trap, emitted after marshaling when the divisor pair is
8775/// in R2:R3: `ORRS R12, R2, R3` sets Z iff the divisor is zero; `BNE` skips a
8776/// `UDF #0` (WASM div/rem-by-zero must trap, matching the Thumb-2 twin).
8777fn emit_a32_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8778    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8779    w(bytes, 0xE192_C003); // ORRS R12, R2, R3
8780    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8781    w(bytes, 0xE7F0_00F0); // UDF #0 — divide by zero
8782}
8783
8784/// A32 twin of [`emit_i64_divs_overflow_trap`] (#633): trap on
8785/// `i64.div_s(INT64_MIN, -1)`. Conditional execution replaces the Thumb
8786/// branches — the CMPEQ chain leaves EQ set only when divisor == -1 AND
8787/// dividend == INT64_MIN. div_s only; rem_s must keep returning 0.
8788fn emit_a32_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8789    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8790    w(bytes, 0xE002_C003); // AND   R12, R2, R3 (== 0xFFFFFFFF iff divisor == -1)
8791    w(bytes, 0xE37C_0001); // CMN   R12, #1     (EQ iff divisor == -1)
8792    w(bytes, 0x0350_0000); // CMPEQ R0, #0      (EQ iff also dividend lo == 0)
8793    w(bytes, 0x0351_0102); // CMPEQ R1, #0x80000000 (EQ iff dividend == INT64_MIN)
8794    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8795    w(bytes, 0xE7F0_00F0); // UDF #0 — signed-division overflow
8796}
8797
8798/// Fallible form of the `verify_reg_bits` contract. PC (R15) is not a valid
8799/// data operand for the Thumb-2 encodings that use this guard (SDIV/UDIV/MLS/…
8800/// are UNPREDICTABLE with PC). Synth's own codegen never emits PC there, but
8801/// the encoder must stay *total* over arbitrary `ArmOp` inputs — the fuzz
8802/// harness (`encoder_no_panic`) requires Ok-or-Err, never a panic. Pre-fix, the
8803/// `debug_assert` in `verify_reg_bits` aborted under `-Cdebug-assertions`.
8804/// Returns a typed Err instead. See #185.
8805fn reg_bits_checked(bits: u32) -> Result<()> {
8806    if bits > 14 {
8807        return Err(synth_core::Error::synthesis(format!(
8808            "register bits {bits} (PC/R15) is not a valid operand for this Thumb-2 encoding"
8809        )));
8810    }
8811    Ok(())
8812}
8813
8814/// Try to encode a 32-bit value as an ARM rotated immediate (imm8 ROR 2*rot4).
8815/// Returns Some((encoded_bits, 1)) if representable, None otherwise.
8816fn try_encode_rotated_imm(val: u32) -> Option<(u32, u32)> {
8817    if val == 0 {
8818        return Some((0, 1));
8819    }
8820    for rot in 0..16u32 {
8821        let shift = rot * 2;
8822        // Rotate left by shift (undo the ROR) to see if result fits in 8 bits
8823        let unrotated = val.rotate_left(shift);
8824        if unrotated <= 0xFF {
8825            // Encoded as: rot4(4 bits) | imm8(8 bits) = rotate_imm << 8 | imm8
8826            return Some(((rot << 8) | unrotated, 1));
8827        }
8828    }
8829    None
8830}
8831
8832/// Encode operand2 field and return (bits, immediate_flag).
8833/// For ARM32 mode, immediates use the rotated-immediate encoding (imm8 ROR 2*rot4).
8834/// Panics if an immediate value cannot be represented. Callers that need large
8835/// immediates should use MOVW/MOVT instead of Operand2::Imm.
8836fn encode_operand2(op2: &Operand2) -> Result<(u32, u32)> {
8837    match op2 {
8838        Operand2::Imm(val) => {
8839            let uval = *val as u32;
8840            // Attempt rotated-immediate encoding (ARM32 Operand2)
8841            if let Some(encoded) = try_encode_rotated_imm(uval) {
8842                Ok(encoded)
8843            } else {
8844                // #378-class honesty: an immediate that can't be expressed as an
8845                // ARM32 rotated immediate is an INTERNAL selector bug — large
8846                // constants must be materialized via MOVW/MOVT, not passed here.
8847                // FAIL HONESTLY with an Err rather than silently masking to
8848                // `uval & 0xFF` and emitting a WRONG immediate. The encoder is
8849                // Ok-or-Err, never corrupt (#180/#185); a loud Err is also why
8850                // this is an Err and not a panic (the `encoder_no_panic` fuzz
8851                // contract — malformed/oversized input must degrade, not crash).
8852                Err(synth_core::Error::synthesis(format!(
8853                    "encode_operand2: immediate {uval:#x} ({val}) is not an ARM32 \
8854                     rotated immediate — the selector must materialize large \
8855                     constants via MOVW/MOVT"
8856                )))
8857            }
8858        }
8859
8860        Operand2::Reg(reg) => {
8861            let reg_bits = reg_to_bits(reg);
8862            Ok((reg_bits, 0)) // I=0 for register
8863        }
8864
8865        Operand2::RegShift {
8866            rm,
8867            shift: _,
8868            amount,
8869        } => {
8870            // Simplified encoding with shift
8871            let rm_bits = reg_to_bits(rm);
8872            let shift_bits = (*amount & 0x1F) << 7;
8873            Ok((shift_bits | rm_bits, 0))
8874        }
8875    }
8876}
8877
8878/// Largest immediate an A32 `LDR`/`STR`/`LDRB`/`STRB` `[Rn, #imm12]` form holds.
8879const A32_LDST_IMM12_MAX: u32 = 0xFFF;
8880/// Largest immediate an A32 `LDRH`/`STRH`/`LDRSB`/`LDRSH` `[Rn, #imm8]`
8881/// (`imm4H:imm4L`) form holds.
8882const A32_LDST_IMM8_MAX: u32 = 0xFF;
8883/// Largest LOW-half immediate an A32 `I64Ldr`/`I64Str` pair can fold: the high
8884/// half sits at `+4`, which must itself fit imm12 (`0xFFB + 4 = 0xFFF`).
8885const A32_I64_PAIR_IMM12_MAX: u32 = 0xFFB;
8886
8887/// RQ-63-ARMI64OFF (#1165): resolve the A32 base register and residual
8888/// immediate for a load/store whose static offset may exceed the form's
8889/// immediate field — the A32 counterpart of the Thumb-2 `i64_effective_base`
8890/// (#372/#382). Returns `(base, residual)`; the caller encodes `[base,
8891/// #residual]` (and `#residual + 4` for the i64 high half).
8892///
8893/// The offset is the wasm memarg round-tripped through the selector's `as i32`
8894/// cast (a memarg `>= 2^31` arrives negative), so it is read back as `u32` and
8895/// the effective address is `base + index + offset (mod 2^32)` — the same
8896/// arithmetic the Thumb-2 word path (`encode_thumb32_add_imm`) and the
8897/// `--safety-bounds software` guard (`software_bounds_guard`, which round-trips
8898/// the same cast) already use. A clamp or a mask here is a WRONG ADDRESS, i.e.
8899/// the silent memory miscompile this artifact exists to refuse: before this
8900/// helper the A32 immediate arms masked `& 0xFFF` (word/byte) and `& 0xFF`
8901/// (halfword/signed), so `i32.load offset=5000` on the `-b arm` default target
8902/// silently read `[ip, #904]`, and the i64 pair arm declined outright (46 of
8903/// the 110 core-module declines in v0.62's ARM census).
8904///
8905/// - `offset <= imm_max`, no index: emits nothing, returns `(addr.base, offset)`
8906///   — byte-identical to the pre-#1165 immediate form.
8907/// - `offset <= imm_max`, index `rm`: emits `ADD ip, base, rm`, returns
8908///   `(ip, offset)` — byte-identical to the #206 register-offset form.
8909/// - `offset > imm_max`: MATERIALIZES the whole effective base into `ip`:
8910///   `MOVW ip, #lo16 ; [MOVT ip, #hi16 — only when non-zero] ; ADD ip, ip, base
8911///   ; [ADD ip, ip, rm]`, returns `(ip, 0)`. `ip` is fully computed BEFORE the
8912///   access, so a destination register aliasing `base` or `rm` is safe.
8913///
8914/// `ip` (R12) is the reserved encoder scratch — never allocated (pool R0–R8;
8915/// R9/R10/R11 are the globals/size/base contract registers). If `base` or `rm`
8916/// IS R12 the materialization would clobber its own input, so that case is a
8917/// typed `Err` — a loud decline, never a wrong address (the alias trap
8918/// `encode_thumb32_add_imm` carries for `rd == rn == R12`).
8919///
8920/// Bytes verified against `arm-none-eabi-as` in the `test_1165_a32_*` tests
8921/// and executed by `scripts/repro/a32_ldst_offset_1165_differential.py`.
8922fn a32_effective_base(bytes: &mut Vec<u8>, addr: &MemAddr, imm_max: u32) -> Result<(Reg, u32)> {
8923    let ip = Reg::R12;
8924    let ip_bits = reg_to_bits(&ip);
8925    let offset = addr.offset as u32;
8926    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8927    // ADD rd, rn, rm (cond=AL, opcode=ADD, S=0, register operand2).
8928    let add_reg = |rd: u32, rn: u32, rm: u32| 0xE080_0000 | (rn << 16) | (rd << 12) | rm;
8929    if offset <= imm_max {
8930        return Ok(match addr.offset_reg {
8931            Some(rm) => {
8932                w(
8933                    bytes,
8934                    add_reg(ip_bits, reg_to_bits(&addr.base), reg_to_bits(&rm)),
8935                );
8936                (ip, offset)
8937            }
8938            None => (addr.base, offset),
8939        });
8940    }
8941    if addr.base == ip || addr.offset_reg == Some(ip) {
8942        return Err(synth_core::Error::synthesis(format!(
8943            "A32 load/store offset {offset:#x} exceeds the immediate field (max {imm_max:#x}) \
8944             and the address already uses the R12 scratch — no free register to \
8945             materialize it (RQ-63-ARMI64OFF)"
8946        )));
8947    }
8948    // MOVW ip, #lo16 ; MOVT ip, #hi16 (elided when zero, mirroring I64Const).
8949    let lo16 = offset & 0xFFFF;
8950    let hi16 = offset >> 16;
8951    w(
8952        bytes,
8953        0xE300_0000 | ((lo16 >> 12) << 16) | (ip_bits << 12) | (lo16 & 0xFFF),
8954    );
8955    if hi16 != 0 {
8956        w(
8957            bytes,
8958            0xE340_0000 | ((hi16 >> 12) << 16) | (ip_bits << 12) | (hi16 & 0xFFF),
8959        );
8960    }
8961    w(bytes, add_reg(ip_bits, ip_bits, reg_to_bits(&addr.base)));
8962    if let Some(rm) = addr.offset_reg {
8963        w(bytes, add_reg(ip_bits, ip_bits, reg_to_bits(&rm)));
8964    }
8965    Ok((ip, 0))
8966}
8967
8968/// Encode an A32 immediate-form memory address to `(base_reg, imm12)`.
8969///
8970/// RQ-63-ARMI64OFF (#1165): this used to mask `& 0xFFF`, silently re-targeting
8971/// any offset past the field (the #259 class, closed on Thumb-2 by
8972/// `check_ldst_imm12` but never here). The immediate arms are only reached with
8973/// an in-range, index-free address — `encode_arm_reg_offset_mem` materializes
8974/// everything else into IP first — so this is a TRIPWIRE: a typed error, never
8975/// a wrong address, should a future arm bypass that pre-pass.
8976fn encode_mem_addr(addr: &MemAddr) -> Result<(u32, u32)> {
8977    check_a32_imm_addr(addr, A32_LDST_IMM12_MAX)
8978}
8979
8980/// `encode_mem_addr` for the `imm4H:imm4L` (8-bit) forms — `LDRH`/`STRH`/
8981/// `LDRSB`/`LDRSH`. Same tripwire contract; the former `& 0xFF` mask is gone.
8982fn encode_mem_addr_imm8(addr: &MemAddr) -> Result<(u32, u32)> {
8983    check_a32_imm_addr(addr, A32_LDST_IMM8_MAX)
8984}
8985
8986fn check_a32_imm_addr(addr: &MemAddr, imm_max: u32) -> Result<(u32, u32)> {
8987    if addr.offset_reg.is_some() {
8988        return Err(synth_core::Error::synthesis(
8989            "internal: A32 immediate-form load/store reached with a register offset — \
8990             encode_arm_reg_offset_mem must materialize it first (#206)",
8991        ));
8992    }
8993    let offset = addr.offset as u32;
8994    if offset > imm_max {
8995        return Err(synth_core::Error::synthesis(format!(
8996            "internal: A32 load/store immediate offset {offset:#x} exceeds the {imm_max:#x} \
8997             field — encode_arm_reg_offset_mem must materialize it first (RQ-63-ARMI64OFF)"
8998        )));
8999    }
9000    Ok((reg_to_bits(&addr.base), offset))
9001}
9002
9003/// S-register number: S0=0, S1=1, ..., S31=31
9004fn vfp_sreg_to_num(reg: &VfpReg) -> Result<u32> {
9005    match reg {
9006        VfpReg::S0 => Ok(0),
9007        VfpReg::S1 => Ok(1),
9008        VfpReg::S2 => Ok(2),
9009        VfpReg::S3 => Ok(3),
9010        VfpReg::S4 => Ok(4),
9011        VfpReg::S5 => Ok(5),
9012        VfpReg::S6 => Ok(6),
9013        VfpReg::S7 => Ok(7),
9014        VfpReg::S8 => Ok(8),
9015        VfpReg::S9 => Ok(9),
9016        VfpReg::S10 => Ok(10),
9017        VfpReg::S11 => Ok(11),
9018        VfpReg::S12 => Ok(12),
9019        VfpReg::S13 => Ok(13),
9020        VfpReg::S14 => Ok(14),
9021        VfpReg::S15 => Ok(15),
9022        VfpReg::S16 => Ok(16),
9023        VfpReg::S17 => Ok(17),
9024        VfpReg::S18 => Ok(18),
9025        VfpReg::S19 => Ok(19),
9026        VfpReg::S20 => Ok(20),
9027        VfpReg::S21 => Ok(21),
9028        VfpReg::S22 => Ok(22),
9029        VfpReg::S23 => Ok(23),
9030        VfpReg::S24 => Ok(24),
9031        VfpReg::S25 => Ok(25),
9032        VfpReg::S26 => Ok(26),
9033        VfpReg::S27 => Ok(27),
9034        VfpReg::S28 => Ok(28),
9035        VfpReg::S29 => Ok(29),
9036        VfpReg::S30 => Ok(30),
9037        VfpReg::S31 => Ok(31),
9038        // D-registers are not used in F32 single-precision encodings
9039        _ => Err(synth_core::Error::SynthesisError(
9040            "D-register not supported in single-precision VFP encoding".to_string(),
9041        )),
9042    }
9043}
9044
9045/// D-register number: D0=0, D1=1, ..., D15=15
9046fn vfp_dreg_to_num(reg: &VfpReg) -> Result<u32> {
9047    match reg {
9048        VfpReg::D0 => Ok(0),
9049        VfpReg::D1 => Ok(1),
9050        VfpReg::D2 => Ok(2),
9051        VfpReg::D3 => Ok(3),
9052        VfpReg::D4 => Ok(4),
9053        VfpReg::D5 => Ok(5),
9054        VfpReg::D6 => Ok(6),
9055        VfpReg::D7 => Ok(7),
9056        VfpReg::D8 => Ok(8),
9057        VfpReg::D9 => Ok(9),
9058        VfpReg::D10 => Ok(10),
9059        VfpReg::D11 => Ok(11),
9060        VfpReg::D12 => Ok(12),
9061        VfpReg::D13 => Ok(13),
9062        VfpReg::D14 => Ok(14),
9063        VfpReg::D15 => Ok(15),
9064        // S-registers are not used in F64 double-precision encodings
9065        _ => Err(synth_core::Error::SynthesisError(
9066            "S-register not supported in double-precision VFP encoding".to_string(),
9067        )),
9068    }
9069}
9070
9071/// Split S-register into (Vx[3:0], qualifier_bit) for VFP encoding.
9072/// For an S-register number s: Vx = s >> 1, qualifier = s & 1.
9073/// The qualifier bit goes to D (bit 22), N (bit 7), or M (bit 5) depending on role.
9074fn encode_sreg(s: u32) -> (u32, u32) {
9075    (s >> 1, s & 1)
9076}
9077
9078/// Split D-register into (Vx[3:0], qualifier_bit) for VFP double-precision encoding.
9079/// For a D-register number d: Vx = d & 0xF, qualifier = (d >> 4) & 1.
9080/// For D0-D15, qualifier is always 0.
9081fn encode_dreg(d: u32) -> (u32, u32) {
9082    (d & 0xF, (d >> 4) & 1)
9083}
9084
9085/// Encode a VFP 3-register arithmetic instruction (VADD.F32, VSUB.F32, VMUL.F32, VDIV.F32).
9086/// Returns the full 32-bit instruction word.
9087///
9088/// VFP encoding: [cond 1110] [D opc1 Vn] [Vd 101 sz] [N opc2 M 0 Vm]
9089/// For single-precision (sz=0), coprocessor = 0xA (bits[11:8]).
9090fn encode_vfp_3reg(base: u32, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<u32> {
9091    let sd_num = vfp_sreg_to_num(sd)?;
9092    let sn_num = vfp_sreg_to_num(sn)?;
9093    let sm_num = vfp_sreg_to_num(sm)?;
9094    let (vd, d) = encode_sreg(sd_num);
9095    let (vn, n) = encode_sreg(sn_num);
9096    let (vm, m) = encode_sreg(sm_num);
9097
9098    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
9099}
9100
9101/// Encode a VFP 2-register instruction (VNEG.F32, VABS.F32, VSQRT.F32).
9102/// Returns the full 32-bit instruction word.
9103fn encode_vfp_2reg(base: u32, sd: &VfpReg, sm: &VfpReg) -> Result<u32> {
9104    let sd_num = vfp_sreg_to_num(sd)?;
9105    let sm_num = vfp_sreg_to_num(sm)?;
9106    let (vd, d) = encode_sreg(sd_num);
9107    let (vm, m) = encode_sreg(sm_num);
9108
9109    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
9110}
9111
9112/// Encode a VFP load/store (VLDR.F32 / VSTR.F32).
9113/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
9114/// U bit (bit 23) controls add/subtract offset.
9115fn encode_vfp_ldst(base: u32, sd: &VfpReg, addr: &MemAddr) -> Result<u32> {
9116    let sd_num = vfp_sreg_to_num(sd)?;
9117    let (vd, d) = encode_sreg(sd_num);
9118    let rn = reg_to_bits(&addr.base);
9119
9120    let offset = addr.offset;
9121    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9122    let abs_offset = offset.unsigned_abs();
9123    let imm8 = (abs_offset / 4) & 0xFF;
9124
9125    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
9126}
9127
9128/// Encode VMOV between core register and S-register.
9129/// VMOV Sn, Rt: 0xEE00_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
9130/// VMOV Rt, Sn: 0xEE10_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
9131fn encode_vmov_core_sreg(to_sreg: bool, sreg: &VfpReg, core: &Reg) -> Result<u32> {
9132    let s_num = vfp_sreg_to_num(sreg)?;
9133    let (vn, n) = encode_sreg(s_num);
9134    let rt = reg_to_bits(core);
9135
9136    let base = if to_sreg { 0xEE000A10 } else { 0xEE100A10 };
9137    Ok(base | (vn << 16) | (rt << 12) | (n << 7))
9138}
9139
9140/// Encode a VFP 3-register double-precision instruction (VADD.F64, VSUB.F64, etc.).
9141/// For double-precision (sz=1), coprocessor = 0xB (bits[11:8]).
9142/// The base should have bit 8 = 1 for F64 (0xB suffix instead of 0xA).
9143fn encode_vfp_3reg_f64(base: u32, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<u32> {
9144    let dd_num = vfp_dreg_to_num(dd)?;
9145    let dn_num = vfp_dreg_to_num(dn)?;
9146    let dm_num = vfp_dreg_to_num(dm)?;
9147    let (vd, d) = encode_dreg(dd_num);
9148    let (vn, n) = encode_dreg(dn_num);
9149    let (vm, m) = encode_dreg(dm_num);
9150
9151    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
9152}
9153
9154/// Encode a VFP 2-register double-precision instruction (VNEG.F64, VABS.F64, VSQRT.F64).
9155fn encode_vfp_2reg_f64(base: u32, dd: &VfpReg, dm: &VfpReg) -> Result<u32> {
9156    let dd_num = vfp_dreg_to_num(dd)?;
9157    let dm_num = vfp_dreg_to_num(dm)?;
9158    let (vd, d) = encode_dreg(dd_num);
9159    let (vm, m) = encode_dreg(dm_num);
9160
9161    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
9162}
9163
9164/// Encode a VFP load/store for double-precision (VLDR.64 / VSTR.64).
9165/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
9166fn encode_vfp_ldst_f64(base: u32, dd: &VfpReg, addr: &MemAddr) -> Result<u32> {
9167    let dd_num = vfp_dreg_to_num(dd)?;
9168    let (vd, d) = encode_dreg(dd_num);
9169    let rn = reg_to_bits(&addr.base);
9170
9171    let offset = addr.offset;
9172    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9173    let abs_offset = offset.unsigned_abs();
9174    let imm8 = (abs_offset / 4) & 0xFF;
9175
9176    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
9177}
9178
9179/// Encode VMOV between two core registers and a D-register.
9180/// VMOV Dm, Rt, Rt2: 0xEC40_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
9181/// VMOV Rt, Rt2, Dm: 0xEC50_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
9182fn encode_vmov_core_dreg(
9183    to_dreg: bool,
9184    dreg: &VfpReg,
9185    core_lo: &Reg,
9186    core_hi: &Reg,
9187) -> Result<u32> {
9188    let d_num = vfp_dreg_to_num(dreg)?;
9189    let (vm, m) = encode_dreg(d_num);
9190    let rt = reg_to_bits(core_lo);
9191    let rt2 = reg_to_bits(core_hi);
9192
9193    let base = if to_dreg { 0xEC400B10 } else { 0xEC500B10 };
9194    Ok(base | (rt2 << 16) | (rt << 12) | (m << 5) | vm)
9195}
9196
9197/// Emit a VFP 32-bit instruction as Thumb-2 bytes (two LE halfwords).
9198fn vfp_to_thumb_bytes(instr: u32) -> Vec<u8> {
9199    let hw1 = ((instr >> 16) & 0xFFFF) as u16;
9200    let hw2 = (instr & 0xFFFF) as u16;
9201    let mut bytes = hw1.to_le_bytes().to_vec();
9202    bytes.extend_from_slice(&hw2.to_le_bytes());
9203    bytes
9204}
9205
9206// ============================================================================
9207// Helium MVE encoding helpers
9208// ============================================================================
9209
9210/// Q-register number: Q0=0, Q1=1, ..., Q7=7
9211fn qreg_to_num(reg: &QReg) -> u32 {
9212    match reg {
9213        QReg::Q0 => 0,
9214        QReg::Q1 => 1,
9215        QReg::Q2 => 2,
9216        QReg::Q3 => 3,
9217        QReg::Q4 => 4,
9218        QReg::Q5 => 5,
9219        QReg::Q6 => 6,
9220        QReg::Q7 => 7,
9221    }
9222}
9223
9224/// MVE element size to encoding bits: S8=0b00, S16=0b01, S32=0b10
9225fn mve_size_bits(size: &MveSize) -> u32 {
9226    match size {
9227        MveSize::S8 => 0b00,
9228        MveSize::S16 => 0b01,
9229        MveSize::S32 => 0b10,
9230    }
9231}
9232
9233/// Encode MVE 3-register instruction.
9234/// Q-registers are encoded as D-register pairs: Q0=D0:D1, Q1=D2:D3, etc.
9235/// In NEON/MVE encoding, the Q-register uses D-register number = Qn * 2.
9236fn encode_mve_3reg(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
9237    let d = qreg_to_num(qd) * 2;
9238    let n = qreg_to_num(qn) * 2;
9239    let m = qreg_to_num(qm) * 2;
9240
9241    // Standard NEON/MVE 3-register encoding:
9242    // D bit (bit 22) = Vd[4], Vd[3:0] = bits [15:12]
9243    // N bit (bit 7)  = Vn[4], Vn[3:0] = bits [19:16]
9244    // M bit (bit 5)  = Vm[4], Vm[3:0] = bits [3:0]
9245    let vd = d & 0xF;
9246    let d_bit = (d >> 4) & 1;
9247    let vn = n & 0xF;
9248    let n_bit = (n >> 4) & 1;
9249    let vm = m & 0xF;
9250    let m_bit = (m >> 4) & 1;
9251
9252    base | (d_bit << 22) | (vn << 16) | (vd << 12) | (n_bit << 7) | (m_bit << 5) | vm
9253}
9254
9255/// Encode MVE 3-register bitwise instruction (VAND, VORR, VEOR, VBIC).
9256fn encode_mve_3reg_bitwise(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
9257    encode_mve_3reg(base, qd, qn, qm)
9258}
9259
9260/// Encode MVE VLDRW.32 Qd, [Rn, #offset]
9261/// Format: EC9x xxxx - contiguous load, word-sized elements
9262fn encode_mve_vldrw(qd: &QReg, addr: &MemAddr) -> u32 {
9263    let qd_enc = qreg_to_num(qd) * 2;
9264    let rn = reg_to_bits(&addr.base);
9265    let offset = addr.offset;
9266    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9267    let abs_offset = offset.unsigned_abs();
9268    let imm7 = (abs_offset / 4) & 0x7F; // 7-bit word-aligned offset
9269
9270    // VLDRW.32 Qd, [Rn, #imm]: ED10 xx80 variant
9271    0xED100E80
9272        | (u_bit << 23)
9273        | ((qd_enc >> 4) << 22)
9274        | (rn << 16)
9275        | ((qd_enc & 0xF) << 12)
9276        | (imm7 & 0x7F)
9277}
9278
9279/// Encode MVE VSTRW.32 Qd, [Rn, #offset]
9280fn encode_mve_vstrw(qd: &QReg, addr: &MemAddr) -> u32 {
9281    let qd_enc = qreg_to_num(qd) * 2;
9282    let rn = reg_to_bits(&addr.base);
9283    let offset = addr.offset;
9284    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9285    let abs_offset = offset.unsigned_abs();
9286    let imm7 = (abs_offset / 4) & 0x7F;
9287
9288    0xED000E80
9289        | (u_bit << 23)
9290        | ((qd_enc >> 4) << 22)
9291        | (rn << 16)
9292        | ((qd_enc & 0xF) << 12)
9293        | (imm7 & 0x7F)
9294}
9295
9296impl ArmEncoder {
9297    /// Encode MVE constant load: MOVW+MOVT+VMOV for each 32-bit word, then assemble Q-register
9298    fn encode_thumb_mve_const(&self, qd: &QReg, bytes: &[u8; 16]) -> Result<Vec<u8>> {
9299        let mut result = Vec::new();
9300        let qd_num = qreg_to_num(qd);
9301
9302        // Load each 32-bit word into R12 (temp) then VMOV into S-register
9303        for i in 0..4 {
9304            let word = u32::from_le_bytes([
9305                bytes[i * 4],
9306                bytes[i * 4 + 1],
9307                bytes[i * 4 + 2],
9308                bytes[i * 4 + 3],
9309            ]);
9310            let lo16 = word & 0xFFFF;
9311            let hi16 = (word >> 16) & 0xFFFF;
9312
9313            // MOVW R12, #lo16
9314            result.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
9315            // MOVT R12, #hi16
9316            if hi16 != 0 {
9317                result.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
9318            }
9319
9320            // VMOV Sn, R12 where Sn = Qd*4 + i
9321            let s_num = qd_num * 4 + i as u32;
9322            let (vn, n) = encode_sreg(s_num);
9323            let vmov: u32 = 0xEE000A10 | (vn << 16) | (12 << 12) | (n << 7);
9324            result.extend_from_slice(&vfp_to_thumb_bytes(vmov));
9325        }
9326
9327        Ok(result)
9328    }
9329
9330    /// Encode lane-wise f32 binary operation (VDIV, etc.) via S-register extraction
9331    fn encode_thumb_mve_lane_wise_f32_binop(
9332        &self,
9333        qd: &QReg,
9334        qn: &QReg,
9335        qm: &QReg,
9336        vfp_base: u32,
9337    ) -> Result<Vec<u8>> {
9338        let mut result = Vec::new();
9339        let qd_num = qreg_to_num(qd);
9340        let qn_num = qreg_to_num(qn);
9341        let qm_num = qreg_to_num(qm);
9342
9343        // For each lane 0..3: use S-registers directly (Q aliasing)
9344        for i in 0..4u32 {
9345            let sd = qd_num * 4 + i;
9346            let sn = qn_num * 4 + i;
9347            let sm = qm_num * 4 + i;
9348
9349            let (vd, d) = encode_sreg(sd);
9350            let (vn, n) = encode_sreg(sn);
9351            let (vm, m) = encode_sreg(sm);
9352
9353            let instr = vfp_base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
9354            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9355        }
9356
9357        Ok(result)
9358    }
9359
9360    /// Encode lane-wise f32 VSQRT via S-register extraction
9361    fn encode_thumb_mve_lane_wise_f32_sqrt(&self, qd: &QReg, qm: &QReg) -> Result<Vec<u8>> {
9362        let mut result = Vec::new();
9363        let qd_num = qreg_to_num(qd);
9364        let qm_num = qreg_to_num(qm);
9365
9366        // VSQRT.F32 base: 0xEEB10AC0
9367        for i in 0..4u32 {
9368            let sd = qd_num * 4 + i;
9369            let sm = qm_num * 4 + i;
9370
9371            let (vd, d) = encode_sreg(sd);
9372            let (vm, m) = encode_sreg(sm);
9373
9374            let instr: u32 = 0xEEB10AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
9375            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9376        }
9377
9378        Ok(result)
9379    }
9380}
9381
9382/// VCR-TIER-001 (#1021/#1048) — the SCRATCH CONTRACT of an `ArmOp`'s encoder
9383/// expansion: the registers, beyond the op's declared RESULT registers and the
9384/// globally sanctioned R12/IP encoder scratch, that the expansion may leave
9385/// modified when it completes. Transient-but-restored traffic (push/pop through
9386/// the expansion's own stack red-zone, SP restored on exit) is not "modified".
9387///
9388/// This is the SINGLE declaration site for that contract — the one place a
9389/// future expansion that must borrow a register says so (the repo rule: one
9390/// declaration site, no duplicate copy that can silently drift). It is deliberately NOT derived
9391/// from the expansion's observed behavior: a contract read off the bytes would
9392/// rubber-stamp any clobber. Intent is declared here; the canary gate
9393/// (`scripts/repro/expansion_canary_gate_1021.py`) executes the REAL emitted
9394/// bytes of every variant the shipped rule table emits, on both backends, with
9395/// every non-contract register holding a distinctive canary, and fails on any
9396/// undeclared write — and on any declared register the expansion never
9397/// actually writes, so an over-broad declaration cannot hollow the gate.
9398///
9399/// The default is the STRICTEST reading — result registers only — which is
9400/// exactly the silent claim the atomic `ArmSemantics` pseudo-op model already
9401/// makes (#1021: an atomic model of a multi-instruction expansion is a silent
9402/// claim that the expansion is scratch-free). Today the table is EMPTY: #1039
9403/// reworked `i32.popcnt` off R11 (the linear-memory base) and #1048 reworked
9404/// the i64 shifts and bit-counts off their own operand registers, so every
9405/// expansion of every rule-emitted variant is R12-only on both Thumb-2 and
9406/// A32. Backend-independent for the same reason; if a backend's expansion ever
9407/// diverges, this signature grows a backend parameter in the same PR.
9408pub fn expansion_scratch_contract(op: &ArmOp) -> &'static [Reg] {
9409    // No variant currently borrows any register beyond R12. A new declaration
9410    // is added as a `match op { .. }` arm here — nowhere else.
9411    let _ = op;
9412    &[]
9413}
9414
9415#[cfg(test)]
9416mod tests {
9417    use super::*;
9418
9419    #[test]
9420    fn test_encoder_creation() {
9421        let encoder_arm = ArmEncoder::new_arm32();
9422        assert!(!encoder_arm.thumb_mode);
9423
9424        let encoder_thumb = ArmEncoder::new_thumb2();
9425        assert!(encoder_thumb.thumb_mode);
9426    }
9427
9428    /// #204 WAKE-path regression: `SetCond` materialized 0/1 with the 16-bit
9429    /// `MOVS Rd,#imm` (T1), whose Rd field is 3 bits (R0–R7). For a high Rd
9430    /// (R8–R12) `rd_bits << 8` overflows bit 11, flipping the opcode MOVS→CMP
9431    /// (`0x2c00`), so the boolean was never written — gale's `has_waiter` kept a
9432    /// stale value and the binary-sem WAKE dispatch read garbage. High Rd must
9433    /// use the 32-bit `MOV.W` (T2). Verify the bytes, not the IR.
9434    /// #311: the SAME high-Rd MOVS→CMP transmutation as #204, but in the
9435    /// i64 comparison expansions (I64SetCond / I64SetCondZ) — missed by the
9436    /// #204 hardening. With rd=R8 the boolean died in the flags
9437    /// (`ite eq; cmpeq r0,#1; cmpne r0,#0`), so gale's packed-u64 select
9438    /// read a stale register on silicon. High Rd must take MOV.W / CMP.W.
9439    #[test]
9440    fn test_encode_i64setcond_high_reg_uses_mov_w_311() {
9441        use synth_synthesis::{ArmOp, Condition, Reg};
9442        let enc = ArmEncoder::new_thumb2();
9443        let bytes = enc
9444            .encode(&ArmOp::I64SetCond {
9445                rd: Reg::R8,
9446                rn_lo: Reg::R2,
9447                rn_hi: Reg::R3,
9448                rm_lo: Reg::R6,
9449                rm_hi: Reg::R7,
9450                cond: Condition::EQ,
9451            })
9452            .unwrap();
9453        // The 32-bit MOV.W immediate (T2) first halfword is 0xF04F; the
9454        // 16-bit transmuted forms would contain 0x2801/0x2800 (CMP r0,#1/#0).
9455        let halfwords: Vec<u16> = bytes
9456            .chunks(2)
9457            .map(|c| u16::from_le_bytes([c[0], c[1]]))
9458            .collect();
9459        assert!(
9460            halfwords.iter().filter(|&&h| h == 0xF04F).count() == 2,
9461            "high rd must use two MOV.W (T2) encodings, got {halfwords:04x?}"
9462        );
9463        assert!(
9464            !halfwords.contains(&0x2801) && !halfwords.contains(&0x2800),
9465            "no transmuted 16-bit CMP imm: {halfwords:04x?}"
9466        );
9467
9468        let bytes_z = enc
9469            .encode(&ArmOp::I64SetCondZ {
9470                rd: Reg::R8,
9471                rn_lo: Reg::R2,
9472                rn_hi: Reg::R3,
9473            })
9474            .unwrap();
9475        let hw_z: Vec<u16> = bytes_z
9476            .chunks(2)
9477            .map(|c| u16::from_le_bytes([c[0], c[1]]))
9478            .collect();
9479        assert!(
9480            hw_z.iter().filter(|&&h| h == 0xF04F).count() == 2,
9481            "SetCondZ high rd MOV.W: {hw_z:04x?}"
9482        );
9483        // CMP.W rd,#0 (T2) first halfword: 0xF1B0 | rd
9484        assert!(
9485            hw_z.contains(&(0xF1B0 | 8)),
9486            "SetCondZ high rd must use CMP.W: {hw_z:04x?}"
9487        );
9488    }
9489
9490    #[test]
9491    fn test_encode_setcond_high_reg_uses_mov_w_204() {
9492        use synth_synthesis::{ArmOp, Condition, Reg};
9493        let enc = ArmEncoder::new_thumb2();
9494        // R12 (high): must be ITE + MOV.W #1 + MOV.W #0, never a 16-bit MOVS/CMP.
9495        let hi = enc
9496            .encode(&ArmOp::SetCond {
9497                rd: Reg::R12,
9498                cond: Condition::NE,
9499            })
9500            .unwrap();
9501        assert_eq!(hi.len(), 10, "ITE(2) + MOV.W(4) + MOV.W(4): {hi:02x?}");
9502        // both value halfwords are MOV.W (0xF04F) — NOT the corrupt CMP (0x2c..).
9503        assert_eq!(&hi[2..4], &[0x4F, 0xF0], "then = MOV.W: {hi:02x?}");
9504        assert_eq!(&hi[6..8], &[0x4F, 0xF0], "else = MOV.W: {hi:02x?}");
9505        assert_eq!(hi[4] & 0x0F, 0x01, "then imm = #1");
9506        assert_eq!(hi[8] & 0x0F, 0x00, "else imm = #0");
9507        // Low Rd keeps the compact 16-bit MOVS form.
9508        let lo = enc
9509            .encode(&ArmOp::SetCond {
9510                rd: Reg::R0,
9511                cond: Condition::NE,
9512            })
9513            .unwrap();
9514        assert_eq!(lo.len(), 6, "ITE(2) + MOVS(2) + MOVS(2): {lo:02x?}");
9515        assert_eq!(lo[2..4], [0x01, 0x20], "then = MOVS R0,#1");
9516        assert_eq!(lo[4..6], [0x00, 0x20], "else = MOVS R0,#0");
9517    }
9518
9519    /// #209 Opt 1b: UMULL RdLo, RdHi, Rn, Rm encodes correctly on both ISAs.
9520    /// Thumb-2 T1: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm.
9521    /// A32:        cond 0000 1000 RdHi RdLo Rm 1001 Rn.
9522    #[test]
9523    fn test_encode_umull_209b() {
9524        use synth_synthesis::{ArmOp, Reg};
9525        let op = ArmOp::Umull {
9526            rdlo: Reg::R4,
9527            rdhi: Reg::R5,
9528            rn: Reg::R0,
9529            rm: Reg::R3,
9530        };
9531        // Thumb-2: hw1 = 0xFBA0 | 0 = 0xFBA0; hw2 = (4<<12)|(5<<8)|3 = 0x4503.
9532        let t = ArmEncoder::new_thumb2().encode(&op).unwrap();
9533        assert_eq!(
9534            t,
9535            vec![0xA0, 0xFB, 0x03, 0x45],
9536            "umull r4,r5,r0,r3 (T2): {t:02x?}"
9537        );
9538        // A32: 0xE0800090 | (5<<16) | (4<<12) | (3<<8) | 0 = 0xE0854390.
9539        let a = ArmEncoder::new_arm32().encode(&op).unwrap();
9540        assert_eq!(
9541            a,
9542            0xE085_4390u32.to_le_bytes().to_vec(),
9543            "umull (A32): {a:02x?}"
9544        );
9545    }
9546
9547    /// #206 regression: the ARM32 (A32) `Ldr`/`Str` encoders fed `addr` through
9548    /// `encode_mem_addr`, which returns only the 12-bit immediate — so a register
9549    /// offset (`[rn, rm, #off]`) was silently dropped to `[rn, #off]`, sending
9550    /// the access to the wrong runtime address (silent miscompile on the default
9551    /// `--target arm`). A register offset must materialize `ip = rn + rm` and
9552    /// load from `[ip, #off]`. Verify the bytes.
9553    #[test]
9554    fn test_encode_arm32_indexed_load_keeps_index_206() {
9555        use synth_synthesis::{ArmOp, MemAddr, Reg};
9556        let enc = ArmEncoder::new_arm32();
9557        // ldr r0, [r11, r1, #8]  must NOT collapse to a single immediate ldr.
9558        let bytes = enc
9559            .encode(&ArmOp::Ldr {
9560                rd: Reg::R0,
9561                addr: MemAddr::reg_imm(Reg::R11, Reg::R1, 8),
9562            })
9563            .unwrap();
9564        assert_eq!(
9565            bytes.len(),
9566            8,
9567            "expected ADD ip + LDR (2 words): {bytes:02x?}"
9568        );
9569        let add = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9570        let ldr = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9571        // ADD ip, r11, r1  = 0xE08BC001
9572        assert_eq!(add, 0xE08B_C001, "ADD ip,r11,r1: {add:#010x}");
9573        // LDR r0, [ip, #8] = 0xE59C0008
9574        assert_eq!(ldr, 0xE59C_0008, "LDR r0,[ip,#8]: {ldr:#010x}");
9575        // A bare immediate ldr (the bug) would be 0xE59B0008 (base=r11) — reject.
9576        assert_ne!(ldr, 0xE59B_0008, "index must not be dropped");
9577    }
9578
9579    /// #594 regression: `call_indirect` on the A32 path (`--target cortex-r5`)
9580    /// was encoded as a literal NOP (0xE1A00000) — the call never happened and
9581    /// the function silently returned the leftover table-index value. The A32
9582    /// encoder must emit a real dispatch expansion, since #642 guarded by an
9583    /// inline bounds check:
9584    /// `MOVW r12, #size; CMP idx, r12; BLO +1; UDF;
9585    ///  MOV r12, idx, LSL #2; LDR r12, [r11, r12]; BLX r12`.
9586    #[test]
9587    fn test_encode_arm32_call_indirect_is_real_call_594() {
9588        use synth_synthesis::{ArmOp, Reg};
9589        let enc = ArmEncoder::new_arm32();
9590        let bytes = enc
9591            .encode(&ArmOp::CallIndirect {
9592                rd: Reg::R0,
9593                type_idx: 0,
9594                table_index_reg: Reg::R0,
9595                table_size: 4,
9596                table_byte_offset: 0,
9597                null_check: false,
9598                type_check: None,
9599            })
9600            .unwrap();
9601        assert_eq!(
9602            bytes.len(),
9603            28,
9604            "expected MOVW + CMP + BLO + UDF + MOV + LDR + BLX (7 words): {bytes:02x?}"
9605        );
9606        let words: Vec<u32> = bytes
9607            .as_chunks::<4>()
9608            .0
9609            .iter()
9610            .map(|&w| u32::from_le_bytes(w))
9611            .collect();
9612        // #642 bounds guard: MOVW r12, #4; CMP r0, r12; BLO +1; UDF
9613        assert_eq!(words[0], 0xE300_C004, "MOVW r12,#4: {:#010x}", words[0]);
9614        assert_eq!(words[1], 0xE150_000C, "CMP r0,r12: {:#010x}", words[1]);
9615        assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9616        assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9617        // MOV r12, r0, LSL #2 = 0xE1A0C100
9618        assert_eq!(
9619            words[4], 0xE1A0_C100,
9620            "MOV r12,r0,LSL#2: {:#010x}",
9621            words[4]
9622        );
9623        // LDR r12, [r11, r12] = 0xE79BC00C
9624        assert_eq!(
9625            words[5], 0xE79B_C00C,
9626            "LDR r12,[r11,r12]: {:#010x}",
9627            words[5]
9628        );
9629        // BLX r12 = 0xE12FFF3C
9630        assert_eq!(words[6], 0xE12F_FF3C, "BLX r12: {:#010x}", words[6]);
9631        // The bug: a single NOP word. Must never come back.
9632        assert!(
9633            !bytes
9634                .as_chunks::<4>()
9635                .0
9636                .iter()
9637                .any(|&w| w == 0xE1A0_0000u32.to_le_bytes()),
9638            "call_indirect must not contain a NOP (#594): {bytes:02x?}"
9639        );
9640
9641        // A non-R0 index register lands in the MOV's Rm and CMP's Rn fields.
9642        let bytes = enc
9643            .encode(&ArmOp::CallIndirect {
9644                rd: Reg::R0,
9645                type_idx: 0,
9646                table_index_reg: Reg::R4,
9647                table_size: 4,
9648                table_byte_offset: 0,
9649                null_check: false,
9650                type_check: None,
9651            })
9652            .unwrap();
9653        let cmp = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9654        assert_eq!(cmp, 0xE154_000C, "CMP r4,r12: {cmp:#010x}");
9655        let mov = u32::from_le_bytes(bytes[16..20].try_into().unwrap());
9656        assert_eq!(mov, 0xE1A0_C104, "MOV r12,r4,LSL#2: {mov:#010x}");
9657    }
9658
9659    /// #642: a table size above 16 bits must not be silently truncated by the
9660    /// MOVW — the A32 guard adds a MOVT for the high half.
9661    #[test]
9662    fn test_encode_arm32_call_indirect_wide_table_size_642() {
9663        use synth_synthesis::{ArmOp, Reg};
9664        let enc = ArmEncoder::new_arm32();
9665        let bytes = enc
9666            .encode(&ArmOp::CallIndirect {
9667                rd: Reg::R0,
9668                type_idx: 0,
9669                table_index_reg: Reg::R0,
9670                table_size: 0x0002_0003,
9671                table_byte_offset: 0,
9672                null_check: false,
9673                type_check: None,
9674            })
9675            .unwrap();
9676        assert_eq!(bytes.len(), 32, "MOVT arm adds one word: {bytes:02x?}");
9677        let movw = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9678        let movt = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9679        assert_eq!(movw, 0xE300_C003, "MOVW r12,#3: {movw:#010x}");
9680        assert_eq!(movt, 0xE340_C002, "MOVT r12,#2: {movt:#010x}");
9681    }
9682
9683    /// #597 anchor (justified correctness RE-PIN of the #594-era freeze): the
9684    /// Thumb-2 `CallIndirect` expansion is `mov.w ip, rm, LSL #2; ldr.w ip,
9685    /// [r11, ip]; blx ip`.
9686    ///
9687    /// The #594 PR froze the then-current bytes `4F EA 20 0C ...` whose first
9688    /// word decodes as `mov.w ip, rm, ASR #32` — the intended `LSL #2` had
9689    /// its shift amount in the TYPE field (bits 5:4) instead of imm2 (bits
9690    /// 7:6), so the index was destroyed and every call_indirect dispatched
9691    /// table entry 0 (shipped miscompile, masked by index-0 probes). #597
9692    /// corrects the encoding; new bytes `4F EA 80 0C ...` were
9693    /// execution-validated under unicorn against the wasmtime oracle on a
9694    /// multi-entry table (indexes 0, 1, 3 —
9695    /// scripts/repro/call_indirect_597_differential.py) before this pin was
9696    /// replaced. Old pin: [4F EA 20 0C, 5B F8 0C C0, E0 47] (ASR #32 — must
9697    /// never come back).
9698    #[test]
9699    fn test_encode_thumb_call_indirect_lsl2_597() {
9700        use synth_synthesis::{ArmOp, Reg};
9701        let enc = ArmEncoder::new_thumb2();
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: 4,
9708                table_byte_offset: 0,
9709                null_check: false,
9710                type_check: None,
9711            })
9712            .unwrap();
9713        assert_eq!(
9714            bytes,
9715            vec![
9716                // #642 bounds guard: movw ip,#4; cmp r0,ip; blo +1; udf #0
9717                0x40, 0xF2, 0x04, 0x0C, // movw ip, #4
9718                0x60, 0x45, // cmp r0, ip
9719                0x00, 0xD3, // blo .+4 (skip the udf)
9720                0x00, 0xDE, // udf #0 — OOB index trap (WASM §4.4.8)
9721                // #597-pinned dispatch
9722                0x4F, 0xEA, 0x80, 0x0C, // mov.w ip, r0, lsl #2
9723                0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9724                0xE0, 0x47, // blx ip
9725            ],
9726            "Thumb-2 CallIndirect: bounds guard + mov.w/ldr.w/blx dispatch: {bytes:02x?}"
9727        );
9728        // The #597 bug bytes (ASR #32 dispatch first word) must never come back.
9729        assert!(
9730            !bytes.windows(4).any(|w| w == [0x4F, 0xEA, 0x20, 0x0C]),
9731            "mov.w ip, rm, ASR #32 — the #597 type-field bug"
9732        );
9733
9734        // A non-R0 index register lands in the mov.w's Rm field (hw2 bits 3:0)
9735        // and the cmp's Rn field.
9736        let bytes = enc
9737            .encode(&ArmOp::CallIndirect {
9738                rd: Reg::R0,
9739                type_idx: 0,
9740                table_index_reg: Reg::R4,
9741                table_size: 4,
9742                table_byte_offset: 0,
9743                null_check: false,
9744                type_check: None,
9745            })
9746            .unwrap();
9747        assert_eq!(&bytes[4..6], &[0x64, 0x45], "cmp r4, ip: {bytes:02x?}");
9748        assert_eq!(
9749            &bytes[10..14],
9750            &[0x4F, 0xEA, 0x84, 0x0C],
9751            "mov.w ip, r4, LSL #2: {bytes:02x?}"
9752        );
9753    }
9754
9755    /// #642: the Thumb-2 bounds guard for a high-register index (R8 — the top
9756    /// of the allocatable pool) uses the high-reg-capable 16-bit CMP (T2) with
9757    /// the N bit set; a table size above 16 bits adds a MOVT.
9758    #[test]
9759    fn test_encode_thumb_call_indirect_guard_shapes_642() {
9760        use synth_synthesis::{ArmOp, Reg};
9761        let enc = ArmEncoder::new_thumb2();
9762        let bytes = enc
9763            .encode(&ArmOp::CallIndirect {
9764                rd: Reg::R0,
9765                type_idx: 0,
9766                table_index_reg: Reg::R8,
9767                table_size: 3,
9768                table_byte_offset: 0,
9769                null_check: false,
9770                type_check: None,
9771            })
9772            .unwrap();
9773        // cmp r8, ip — T2: 0x4500 | N(1)<<7 | Rm(12)<<3 | Rn(0) = 0x45E0
9774        assert_eq!(&bytes[4..6], &[0xE0, 0x45], "cmp r8, ip: {bytes:02x?}");
9775
9776        let bytes = enc
9777            .encode(&ArmOp::CallIndirect {
9778                rd: Reg::R0,
9779                type_idx: 0,
9780                table_index_reg: Reg::R0,
9781                table_size: 0x0002_0003,
9782                table_byte_offset: 0,
9783                null_check: false,
9784                type_check: None,
9785            })
9786            .unwrap();
9787        // movw ip,#3 then movt ip,#2 — the size must not be truncated.
9788        assert_eq!(
9789            &bytes[0..8],
9790            &[0x40, 0xF2, 0x03, 0x0C, 0xC0, 0xF2, 0x02, 0x0C],
9791            "movw ip,#3; movt ip,#2: {bytes:02x?}"
9792        );
9793    }
9794
9795    /// #650: a non-zero table base offset (table N of the contiguous R11
9796    /// region) routes the Thumb-2 pointer load through
9797    /// `add.w ip, r11, ip; ldr.w ip, [ip, #offset]` — and offset 0 keeps the
9798    /// pre-#650 single-load bytes IDENTICAL (the by-construction pin).
9799    #[test]
9800    fn test_encode_thumb_call_indirect_table_offset_650() {
9801        use synth_synthesis::{ArmOp, Reg};
9802        let enc = ArmEncoder::new_thumb2();
9803        // falcon's fused-component shape: table 0 has 7 entries, so table 1
9804        // sits at byte offset 28.
9805        let bytes = enc
9806            .encode(&ArmOp::CallIndirect {
9807                rd: Reg::R0,
9808                type_idx: 0,
9809                table_index_reg: Reg::R1,
9810                table_size: 41,
9811                table_byte_offset: 28,
9812                null_check: false,
9813                type_check: None,
9814            })
9815            .unwrap();
9816        assert_eq!(
9817            bytes,
9818            vec![
9819                // #642 bounds guard against TABLE 1's OWN size (41)
9820                0x40, 0xF2, 0x29, 0x0C, // movw ip, #41
9821                0x61, 0x45, // cmp r1, ip
9822                0x00, 0xD3, // blo .+4 (skip the udf)
9823                0x00, 0xDE, // udf #0 — OOB trap (WASM §4.4.8)
9824                // dispatch through table 1's base (R11 + 28)
9825                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9826                0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9827                0xDC, 0xF8, 0x1C, 0xC0, // ldr.w ip, [ip, #28]
9828                0xE0, 0x47, // blx ip
9829            ],
9830            "Thumb-2 table-1 dispatch (#650): {bytes:02x?}"
9831        );
9832
9833        // Offset 0 must stay the #597-pinned single-load form (no add.w, no
9834        // imm-form ldr) — single-table byte identity by construction.
9835        let zero = enc
9836            .encode(&ArmOp::CallIndirect {
9837                rd: Reg::R0,
9838                type_idx: 0,
9839                table_index_reg: Reg::R1,
9840                table_size: 41,
9841                table_byte_offset: 0,
9842                null_check: false,
9843                type_check: None,
9844            })
9845            .unwrap();
9846        assert_eq!(
9847            &zero[10..],
9848            &[
9849                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9850                0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9851                0xE0, 0x47, // blx ip
9852            ],
9853            "offset 0 keeps the pre-#650 dispatch bytes: {zero:02x?}"
9854        );
9855    }
9856
9857    /// #650: the A32 twin — `add r12, r11, r12; ldr r12, [r12, #offset]` for
9858    /// a non-zero table base offset; offset 0 keeps the #594/#642 form.
9859    #[test]
9860    fn test_encode_arm32_call_indirect_table_offset_650() {
9861        use synth_synthesis::{ArmOp, Reg};
9862        let enc = ArmEncoder::new_arm32();
9863        let bytes = enc
9864            .encode(&ArmOp::CallIndirect {
9865                rd: Reg::R0,
9866                type_idx: 0,
9867                table_index_reg: Reg::R1,
9868                table_size: 41,
9869                table_byte_offset: 28,
9870                null_check: false,
9871                type_check: None,
9872            })
9873            .unwrap();
9874        let words: Vec<u32> = bytes
9875            .as_chunks::<4>()
9876            .0
9877            .iter()
9878            .map(|&w| u32::from_le_bytes(w))
9879            .collect();
9880        assert_eq!(words[0], 0xE300_C029, "MOVW r12,#41: {:#010x}", words[0]);
9881        assert_eq!(words[1], 0xE151_000C, "CMP r1,r12: {:#010x}", words[1]);
9882        assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9883        assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9884        assert_eq!(
9885            words[4], 0xE1A0_C101,
9886            "MOV r12,r1,LSL#2: {:#010x}",
9887            words[4]
9888        );
9889        assert_eq!(
9890            words[5], 0xE08B_C00C,
9891            "ADD r12,r11,r12 (#650): {:#010x}",
9892            words[5]
9893        );
9894        assert_eq!(
9895            words[6], 0xE59C_C01C,
9896            "LDR r12,[r12,#28] (#650): {:#010x}",
9897            words[6]
9898        );
9899        assert_eq!(words[7], 0xE12F_FF3C, "BLX r12: {:#010x}", words[7]);
9900    }
9901
9902    /// #664: `null_check` inserts a null-funcref trap between the Thumb-2
9903    /// pointer load and the `BLX` (`cmp.w ip, #0; bne .+4; udf #0`) — a
9904    /// zero-linked (uninitialized) slot must TRAP (WASM §4.4.8), never
9905    /// branch to address 0. `null_check: false` keeps the expansion
9906    /// byte-identical to the pre-#664 form (by-construction pin).
9907    #[test]
9908    fn test_encode_thumb_call_indirect_null_check_664() {
9909        use synth_synthesis::{ArmOp, Reg};
9910        let enc = ArmEncoder::new_thumb2();
9911        let op = |null_check| ArmOp::CallIndirect {
9912            rd: Reg::R0,
9913            type_idx: 0,
9914            table_index_reg: Reg::R1,
9915            table_size: 4,
9916            table_byte_offset: 0,
9917            null_check,
9918            type_check: None,
9919        };
9920        let with = enc.encode(&op(true)).unwrap();
9921        let without = enc.encode(&op(false)).unwrap();
9922        // The checked form = the unchecked form with EXACTLY the three-insn
9923        // null check spliced in before the final BLX (byte identity of the
9924        // shared prefix/suffix — nothing else may move).
9925        assert_eq!(
9926            with.len(),
9927            without.len() + 8,
9928            "cmp.w (4) + bne (2) + udf (2): {with:02x?}"
9929        );
9930        let blx_at = without.len() - 2;
9931        assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9932        assert_eq!(
9933            &with[blx_at..],
9934            &[
9935                0xBC, 0xF1, 0x00, 0x0F, // cmp.w ip, #0
9936                0x00, 0xD1, // bne .+4 (skip the udf)
9937                0x00, 0xDE, // udf #0 — null-funcref trap (#664)
9938                0xE0, 0x47, // blx ip
9939            ],
9940            "null check precedes the BLX: {with:02x?}"
9941        );
9942        assert_eq!(&with[with.len() - 2..], &without[blx_at..], "same BLX");
9943    }
9944
9945    /// #664: the A32 twin — `cmp r12, #0; bne .+8; udf` before the `BLX`;
9946    /// `null_check: false` keeps the #594/#642/#650 bytes identical.
9947    #[test]
9948    fn test_encode_arm32_call_indirect_null_check_664() {
9949        use synth_synthesis::{ArmOp, Reg};
9950        let enc = ArmEncoder::new_arm32();
9951        let op = |null_check| ArmOp::CallIndirect {
9952            rd: Reg::R0,
9953            type_idx: 0,
9954            table_index_reg: Reg::R1,
9955            table_size: 4,
9956            table_byte_offset: 0,
9957            null_check,
9958            type_check: None,
9959        };
9960        let with = enc.encode(&op(true)).unwrap();
9961        let without = enc.encode(&op(false)).unwrap();
9962        assert_eq!(with.len(), without.len() + 12, "3 A32 words: {with:02x?}");
9963        let blx_at = without.len() - 4;
9964        assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9965        let words: Vec<u32> = with[blx_at..]
9966            .as_chunks::<4>()
9967            .0
9968            .iter()
9969            .map(|&w| u32::from_le_bytes(w))
9970            .collect();
9971        assert_eq!(words[0], 0xE35C_0000, "CMP r12,#0: {:#010x}", words[0]);
9972        assert_eq!(words[1], 0x1A00_0000, "BNE +1 insn: {:#010x}", words[1]);
9973        assert_eq!(words[2], 0xE7F0_00F0, "UDF (null trap): {:#010x}", words[2]);
9974        assert_eq!(words[3], 0xE12F_FF3C, "BLX r12: {:#010x}", words[3]);
9975    }
9976
9977    /// #676: `type_check` splices the runtime type check — scale the index,
9978    /// load the slot's structural class id from the type-id sidecar
9979    /// (`ldr.w ip, [ip, #type_off]`), compare against the expected class id
9980    /// and trap on mismatch (WASM §4.4.8) — between the bounds guard and
9981    /// the dispatch tail. `type_check: None` keeps the expansion
9982    /// byte-identical to the pre-#676 form (by-construction pin, the same
9983    /// trick as #650 offset-0 / #664 `null_check: false`).
9984    #[test]
9985    fn test_encode_thumb_call_indirect_type_check_676() {
9986        use synth_synthesis::{ArmOp, Reg};
9987        let enc = ArmEncoder::new_thumb2();
9988        let op = |type_check| ArmOp::CallIndirect {
9989            rd: Reg::R0,
9990            type_idx: 1,
9991            table_index_reg: Reg::R1,
9992            table_size: 5,
9993            table_byte_offset: 0,
9994            null_check: false,
9995            type_check,
9996        };
9997        let with = enc.encode(&op(Some((2, 20)))).unwrap();
9998        let without = enc.encode(&op(None)).unwrap();
9999        // The checked form = the unchecked form with EXACTLY the six-insn
10000        // type check spliced in after the bounds guard (byte identity of
10001        // the shared prefix/suffix — nothing else may move).
10002        assert_eq!(
10003            with.len(),
10004            without.len() + 20,
10005            "lsl.w(4)+add.w(4)+ldr.w(4)+cmp.w(4)+beq(2)+udf(2): {with:02x?}"
10006        );
10007        // Bounds guard: movw(4) + cmp(2) + blo(2) + udf(2) = 10 bytes.
10008        let guard_end = 10;
10009        assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
10010        assert_eq!(
10011            &with[guard_end..guard_end + 20],
10012            &[
10013                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
10014                0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
10015                0xDC, 0xF8, 0x14, 0xC0, // ldr.w ip, [ip, #20] — sidecar slot id
10016                0xBC, 0xF1, 0x02, 0x0F, // cmp.w ip, #2 — expected class id
10017                0x00, 0xD0, // beq .+4 (skip the udf on a match)
10018                0x00, 0xDE, // udf #0 — §4.4.8 type-mismatch trap (#676)
10019            ],
10020            "type check follows the bounds guard: {with:02x?}"
10021        );
10022        assert_eq!(
10023            &with[guard_end + 20..],
10024            &without[guard_end..],
10025            "dispatch tail unchanged (idx*4 recomputed)"
10026        );
10027    }
10028
10029    /// #676: the A32 twin — `mov r12, idx, lsl #2; add r12, r11, r12;
10030    /// ldr r12, [r12, #type_off]; cmp r12, #id; beq .+8; udf` after the
10031    /// bounds guard; `type_check: None` keeps the #594/#642/#650/#664
10032    /// bytes identical.
10033    #[test]
10034    fn test_encode_arm32_call_indirect_type_check_676() {
10035        use synth_synthesis::{ArmOp, Reg};
10036        let enc = ArmEncoder::new_arm32();
10037        let op = |type_check| ArmOp::CallIndirect {
10038            rd: Reg::R0,
10039            type_idx: 1,
10040            table_index_reg: Reg::R1,
10041            table_size: 5,
10042            table_byte_offset: 0,
10043            null_check: false,
10044            type_check,
10045        };
10046        let with = enc.encode(&op(Some((2, 20)))).unwrap();
10047        let without = enc.encode(&op(None)).unwrap();
10048        assert_eq!(with.len(), without.len() + 24, "6 A32 words: {with:02x?}");
10049        // Bounds guard: movw + cmp + blo + udf = 4 words = 16 bytes.
10050        let guard_end = 16;
10051        assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
10052        let words: Vec<u32> = with[guard_end..guard_end + 24]
10053            .as_chunks::<4>()
10054            .0
10055            .iter()
10056            .map(|&w| u32::from_le_bytes(w))
10057            .collect();
10058        assert_eq!(
10059            words[0], 0xE1A0_C101,
10060            "MOV r12,r1,LSL#2: {:#010x}",
10061            words[0]
10062        );
10063        assert_eq!(words[1], 0xE08B_C00C, "ADD r12,r11,r12: {:#010x}", words[1]);
10064        assert_eq!(
10065            words[2], 0xE59C_C014,
10066            "LDR r12,[r12,#20] (sidecar): {:#010x}",
10067            words[2]
10068        );
10069        assert_eq!(
10070            words[3], 0xE35C_0002,
10071            "CMP r12,#2 (expected class id): {:#010x}",
10072            words[3]
10073        );
10074        assert_eq!(words[4], 0x0A00_0000, "BEQ +1 insn: {:#010x}", words[4]);
10075        assert_eq!(
10076            words[5], 0xE7F0_00F0,
10077            "UDF (type-mismatch trap): {:#010x}",
10078            words[5]
10079        );
10080        assert_eq!(
10081            &with[guard_end + 24..],
10082            &without[guard_end..],
10083            "dispatch tail unchanged"
10084        );
10085    }
10086
10087    /// #178/#180 regression: the Thumb `Add`/`Adds`/`Subs` reg-forms used the
10088    /// 16-bit encoding unconditionally. For high registers (R12 base scratch,
10089    /// R8-R11 i64 pairs) the 3-bit register fields overflow and corrupt the
10090    /// operands — `add ip,ip,r0` came out as `adds r4,r5,r1` (0x186C), silently
10091    /// dropping the address operand and miscompiling every optimized memory
10092    /// access. High registers must use the 32-bit `.W` forms.
10093    #[test]
10094    fn test_encode_thumb_add_high_reg_uses_add_w_178_180() {
10095        let encoder = ArmEncoder::new_thumb2();
10096
10097        // add ip, ip, r0  — the exact MemLoad/MemStore base+addr op.
10098        let code = encoder
10099            .encode(&ArmOp::Add {
10100                rd: Reg::R12,
10101                rn: Reg::R12,
10102                op2: Operand2::Reg(Reg::R0),
10103            })
10104            .unwrap();
10105        // ADD.W ip, ip, r0 = EB0C 0C00 (little-endian halfwords).
10106        assert_eq!(
10107            code,
10108            vec![0x0C, 0xEB, 0x00, 0x0C],
10109            "high-reg Thumb ADD must be 32-bit ADD.W (EB0C 0C00), not corrupt 16-bit; got {code:02X?}"
10110        );
10111        // Must NOT be the buggy 16-bit 0x186C (`adds r4,r5,r1`).
10112        assert_ne!(code, vec![0x6C, 0x18], "regressed to corrupt 16-bit ADDS");
10113
10114        // Low-register add stays 16-bit (no regression for the common case).
10115        let lo = encoder
10116            .encode(&ArmOp::Add {
10117                rd: Reg::R1,
10118                rn: Reg::R2,
10119                op2: Operand2::Reg(Reg::R3),
10120            })
10121            .unwrap();
10122        assert_eq!(
10123            lo.len(),
10124            2,
10125            "low-reg ADD should remain 16-bit, got {lo:02X?}"
10126        );
10127    }
10128
10129    /// #178/#180 sibling: i64 low-word `Adds`/`Subs` can land in R8-R11 pairs;
10130    /// those must fall back to 32-bit ADDS.W/SUBS.W (flag-setting preserved).
10131    #[test]
10132    fn test_encode_thumb_adds_subs_high_reg_use_32bit_178_180() {
10133        let encoder = ArmEncoder::new_thumb2();
10134
10135        // adds r10, r10, r8  → ADDS.W = EB1A 0A08
10136        let adds = encoder
10137            .encode(&ArmOp::Adds {
10138                rd: Reg::R10,
10139                rn: Reg::R10,
10140                op2: Operand2::Reg(Reg::R8),
10141            })
10142            .unwrap();
10143        assert_eq!(
10144            adds,
10145            vec![0x1A, 0xEB, 0x08, 0x0A],
10146            "high-reg ADDS must be 32-bit ADDS.W (EB1A 0A08); got {adds:02X?}"
10147        );
10148
10149        // subs r10, r10, r8  → SUBS.W = EBBA 0A08
10150        let subs = encoder
10151            .encode(&ArmOp::Subs {
10152                rd: Reg::R10,
10153                rn: Reg::R10,
10154                op2: Operand2::Reg(Reg::R8),
10155            })
10156            .unwrap();
10157        assert_eq!(
10158            subs,
10159            vec![0xBA, 0xEB, 0x08, 0x0A],
10160            "high-reg SUBS must be 32-bit SUBS.W (EBBA 0A08); got {subs:02X?}"
10161        );
10162    }
10163
10164    /// #184 (sibling of #180): 16-bit CMN (T1) only encodes R0-R7. High registers
10165    /// must use 32-bit CMN.W, not the corrupt truncated 16-bit form.
10166    #[test]
10167    fn test_encode_thumb_cmn_high_reg_uses_cmn_w_184() {
10168        let encoder = ArmEncoder::new_thumb2();
10169
10170        // cmn r10, r8  → CMN.W = EB1A 0F08 (ADD.W S=1, Rd=PC discarded).
10171        let cmn = encoder
10172            .encode(&ArmOp::Cmn {
10173                rn: Reg::R10,
10174                op2: Operand2::Reg(Reg::R8),
10175            })
10176            .unwrap();
10177        assert_eq!(
10178            cmn,
10179            vec![0x1A, 0xEB, 0x08, 0x0F],
10180            "high-reg CMN must be 32-bit CMN.W (EB1A 0F08); got {cmn:02X?}"
10181        );
10182
10183        // Low registers stay 16-bit: cmn r1, r2 = 0x42D1.
10184        let lo = encoder
10185            .encode(&ArmOp::Cmn {
10186                rn: Reg::R1,
10187                op2: Operand2::Reg(Reg::R2),
10188            })
10189            .unwrap();
10190        assert_eq!(
10191            lo.len(),
10192            2,
10193            "low-reg CMN should remain 16-bit, got {lo:02X?}"
10194        );
10195        assert_eq!(lo, vec![0xD1, 0x42], "low-reg CMN bytes wrong: {lo:02X?}");
10196    }
10197
10198    /// #185 regression: feeding PC (R15) as a data operand to a Thumb-2 op that
10199    /// guards its registers must return Err, not panic under debug-assertions.
10200    /// (Synth never emits PC here; the fuzz harness requires encode() be total.)
10201    #[test]
10202    fn test_encode_pc_operand_returns_err_not_panic_185() {
10203        let encoder = ArmEncoder::new_thumb2();
10204        for op in [
10205            ArmOp::Sdiv {
10206                rd: Reg::PC,
10207                rn: Reg::R0,
10208                rm: Reg::R1,
10209            },
10210            ArmOp::Udiv {
10211                rd: Reg::R0,
10212                rn: Reg::PC,
10213                rm: Reg::R1,
10214            },
10215            ArmOp::Sdiv {
10216                rd: Reg::R0,
10217                rn: Reg::R1,
10218                rm: Reg::PC,
10219            },
10220        ] {
10221            let r = encoder.encode(&op);
10222            assert!(
10223                r.is_err(),
10224                "encode({op:?}) must return Err for a PC operand, got {r:?}"
10225            );
10226        }
10227        // Valid registers still encode fine (no false rejection).
10228        assert!(
10229            encoder
10230                .encode(&ArmOp::Sdiv {
10231                    rd: Reg::R0,
10232                    rn: Reg::R1,
10233                    rm: Reg::R2
10234                })
10235                .is_ok()
10236        );
10237    }
10238
10239    #[test]
10240    fn test_encode_nop_arm32() {
10241        let encoder = ArmEncoder::new_arm32();
10242        let code = encoder.encode(&ArmOp::Nop).unwrap();
10243
10244        assert_eq!(code.len(), 4); // ARM32 instructions are 4 bytes
10245        assert_eq!(code, vec![0x00, 0x00, 0xA0, 0xE1]); // MOV R0, R0
10246    }
10247
10248    #[test]
10249    fn test_encode_nop_thumb() {
10250        let encoder = ArmEncoder::new_thumb2();
10251        let code = encoder.encode(&ArmOp::Nop).unwrap();
10252
10253        assert_eq!(code.len(), 2); // Thumb instructions are 2 bytes
10254        assert_eq!(code, vec![0x00, 0xBF]); // NOP
10255    }
10256
10257    #[test]
10258    fn test_encode_mov_immediate_arm32() {
10259        let encoder = ArmEncoder::new_arm32();
10260        let op = ArmOp::Mov {
10261            rd: Reg::R0,
10262            op2: Operand2::Imm(42),
10263        };
10264
10265        let code = encoder.encode(&op).unwrap();
10266        assert_eq!(code.len(), 4);
10267
10268        // Verify it's a MOV instruction (bits should have immediate flag set)
10269        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10270        assert_eq!(instr & 0x0E000000, 0x02000000); // Check I bit is set
10271    }
10272
10273    #[test]
10274    fn test_encode_add_registers_arm32() {
10275        let encoder = ArmEncoder::new_arm32();
10276        let op = ArmOp::Add {
10277            rd: Reg::R0,
10278            rn: Reg::R1,
10279            op2: Operand2::Reg(Reg::R2),
10280        };
10281
10282        let code = encoder.encode(&op).unwrap();
10283        assert_eq!(code.len(), 4);
10284
10285        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10286        // Verify it's an ADD instruction with correct opcode
10287        assert_eq!(instr & 0x0FE00000, 0x00800000);
10288    }
10289
10290    /// #350 — `encode_thumb32_add_imm` must lower an out-of-range immediate
10291    /// (> 0xFFF) to a legal MOVW(/MOVT) + ADD.W-register sequence instead of
10292    /// erroring. The small-imm fast path (imm <= 0xFFF) stays byte-identical.
10293    #[test]
10294    fn test_encode_add_imm_large_350() {
10295        let enc = ArmEncoder::new_thumb2();
10296
10297        // --- Fast path: imm <= 0xFFF is a single 4-byte instruction, and the
10298        // VALUE must be right (#681: this test used to assert only the length,
10299        // letting the raw-packed T3 mis-encoding of 0x123 pass CI). 0x123 is
10300        // not ThumbExpandImm-representable, so it must be ADDW (T4, plain
10301        // imm12): clang `addw r0, r1, #0x123` = f201 0023.
10302        let small = enc
10303            .encode_thumb32_add_imm(&Reg::R0, &Reg::R1, 0x123)
10304            .unwrap();
10305        assert_eq!(small, vec![0x01, 0xF2, 0x23, 0x10], "ADDW r0, r1, #0x123");
10306
10307        // helper: decode a Thumb-2 MOVW/MOVT halfword pair back to its imm16
10308        fn movx_imm16(b: &[u8]) -> u32 {
10309            let hw1 = u16::from_le_bytes([b[0], b[1]]) as u32;
10310            let hw2 = u16::from_le_bytes([b[2], b[3]]) as u32;
10311            let imm4 = hw1 & 0xF;
10312            let i = (hw1 >> 10) & 1;
10313            let imm3 = (hw2 >> 12) & 0x7;
10314            let imm8 = hw2 & 0xFF;
10315            (imm4 << 12) | (i << 11) | (imm3 << 8) | imm8
10316        }
10317        fn movx_rd(b: &[u8]) -> u32 {
10318            (u16::from_le_bytes([b[2], b[3]]) as u32 >> 8) & 0xF
10319        }
10320
10321        // --- rd != rn: scratch is rd. imm = 70000 = 0x11170 needs MOVW+MOVT. ---
10322        // 0x11170: lo16 = 0x1170, hi16 = 0x0001
10323        let seq = enc
10324            .encode_thumb32_add_imm(&Reg::R12, &Reg::R0, 70000)
10325            .unwrap();
10326        assert_eq!(seq.len(), 12, "MOVW + MOVT + ADD = 12 bytes");
10327        // MOVW r12, #0x1170
10328        assert_eq!(u16::from_le_bytes([seq[0], seq[1]]) & 0xFBF0, 0xF240);
10329        assert_eq!(movx_rd(&seq[0..4]), 12);
10330        assert_eq!(movx_imm16(&seq[0..4]), 0x1170);
10331        // MOVT r12, #0x0001
10332        assert_eq!(u16::from_le_bytes([seq[4], seq[5]]) & 0xFBF0, 0xF2C0);
10333        assert_eq!(movx_rd(&seq[4..8]), 12);
10334        assert_eq!(movx_imm16(&seq[4..8]), 0x0001);
10335        // ADD.W r12, r0, r12  (EB00 | rn=0 ; rd=12, rm=12)
10336        let add1 = u16::from_le_bytes([seq[8], seq[9]]) as u32;
10337        let add2 = u16::from_le_bytes([seq[10], seq[11]]) as u32;
10338        assert_eq!(add1 & 0xFFF0, 0xEB00);
10339        assert_eq!(add1 & 0xF, 0); // rn = r0
10340        assert_eq!((add2 >> 8) & 0xF, 12); // rd = r12
10341        assert_eq!(add2 & 0xF, 12); // rm = scratch = r12
10342        // The materialized scratch must reconstruct exactly 70000.
10343        assert_eq!(
10344            (movx_imm16(&seq[4..8]) << 16) | movx_imm16(&seq[0..4]),
10345            70000
10346        );
10347
10348        // --- imm <= 0xFFFF: MOVT is skipped (MOVW + ADD = 8 bytes). ---
10349        let seq16 = enc
10350            .encode_thumb32_add_imm(&Reg::R3, &Reg::R0, 0xABCD)
10351            .unwrap();
10352        assert_eq!(seq16.len(), 8, "imm <= 0xFFFF skips MOVT");
10353        assert_eq!(movx_imm16(&seq16[0..4]), 0xABCD);
10354        assert_eq!(movx_rd(&seq16[0..4]), 3); // scratch = rd = r3
10355
10356        // --- rd == rn (in-place add): scratch must be R12, not rd. ---
10357        // imm = 0x12345: lo16 = 0x2345, hi16 = 0x0001
10358        let inplace = enc
10359            .encode_thumb32_add_imm(&Reg::R5, &Reg::R5, 0x12345)
10360            .unwrap();
10361        assert_eq!(inplace.len(), 12);
10362        assert_eq!(movx_rd(&inplace[0..4]), 12, "rd==rn must use R12 scratch");
10363        assert_eq!(
10364            (movx_imm16(&inplace[4..8]) << 16) | movx_imm16(&inplace[0..4]),
10365            0x12345
10366        );
10367        // ADD.W r5, r5, r12 — rm must be the scratch (12), never rn.
10368        let ip_add2 = u16::from_le_bytes([inplace[10], inplace[11]]) as u32;
10369        assert_eq!(ip_add2 & 0xF, 12);
10370        assert_eq!((ip_add2 >> 8) & 0xF, 5);
10371    }
10372
10373    /// #681 — `encode_thumb32_add_imm` packed a RAW immediate into the T3
10374    /// ADD.W `i:imm3:imm8` field, which is a ThumbExpandImm MODIFIED immediate:
10375    /// ThumbExpandImm(0x200) = 0, ThumbExpandImm(0x400) = 0x8000_0000. Every
10376    /// dynamic-address load/store with a static offset in 0x100..=0xFFF
10377    /// computed a wrong address (and bypassed --safety-bounds software: the
10378    /// guard checked the intended address, the access used the mis-encoded
10379    /// one). Fix: imm <= 0xFF keeps T3 (raw == expanded there, bit-identical);
10380    /// 0x100..=0xFFF uses ADDW (T4, plain imm12) — same lowering
10381    /// `encode_thumb32_add` already uses per #253.
10382    ///
10383    /// Every expected byte sequence below is pinned against clang
10384    /// (`-target thumbv7m-none-eabi`) output, bit-for-bit (#544 pattern).
10385    #[test]
10386    fn test_encode_add_imm_thumb_expand_681() {
10387        let enc = ArmEncoder::new_thumb2();
10388        let add = |rd: &Reg, rn: &Reg, imm: u32| enc.encode_thumb32_add_imm(rd, rn, imm).unwrap();
10389
10390        // imm <= 0xFF stays T3 ADD.W (raw == ThumbExpandImm-expanded):
10391        // clang: add.w r12, r0, #0xff  = f100 0cff
10392        assert_eq!(add(&Reg::R12, &Reg::R0, 0xFF), vec![0x00, 0xF1, 0xFF, 0x0C]);
10393
10394        // 0x100..=0xFFF must be ADDW (T4, plain imm12). The old T3 raw packing
10395        // decoded as +0 (0x100/0x200), +0x80000000 (0x400), etc.
10396        // clang: addw r12, r0, #0x100 = f200 1c00
10397        assert_eq!(
10398            add(&Reg::R12, &Reg::R0, 0x100),
10399            vec![0x00, 0xF2, 0x00, 0x1C]
10400        );
10401        // clang: addw r12, r0, #0x104 = f200 1c04
10402        assert_eq!(
10403            add(&Reg::R12, &Reg::R0, 0x104),
10404            vec![0x00, 0xF2, 0x04, 0x1C]
10405        );
10406        // clang: addw r12, r0, #0x200 = f200 2c00
10407        assert_eq!(
10408            add(&Reg::R12, &Reg::R0, 0x200),
10409            vec![0x00, 0xF2, 0x00, 0x2C]
10410        );
10411        // clang: addw r12, r0, #0x3fc = f200 3cfc
10412        assert_eq!(
10413            add(&Reg::R12, &Reg::R0, 0x3FC),
10414            vec![0x00, 0xF2, 0xFC, 0x3C]
10415        );
10416        // clang: addw r12, r0, #0x400 = f200 4c00
10417        assert_eq!(
10418            add(&Reg::R12, &Reg::R0, 0x400),
10419            vec![0x00, 0xF2, 0x00, 0x4C]
10420        );
10421        // clang: addw r12, r0, #0xfff = f600 7cff
10422        assert_eq!(
10423            add(&Reg::R12, &Reg::R0, 0xFFF),
10424            vec![0x00, 0xF6, 0xFF, 0x7C]
10425        );
10426        // Non-scratch rd/rn — clang: addw r1, r2, #0x104 = f202 1104
10427        assert_eq!(add(&Reg::R1, &Reg::R2, 0x104), vec![0x02, 0xF2, 0x04, 0x11]);
10428    }
10429
10430    /// #681 class audit — the T2 RSB and AND.W immediate fields are also
10431    /// ThumbExpandImm-coded and were raw-packed. Neither has a plain-imm12
10432    /// (T4-style) form, so a non-representable immediate must Err loudly
10433    /// (#253/#255/#378 class: never silently encode a different constant).
10434    /// Existing emitters only use representable values (RSB #32, AND #0x3F),
10435    /// pinned here bit-for-bit against clang.
10436    #[test]
10437    fn test_rsb_and_imm_thumb_expand_gate_681() {
10438        let enc = ArmEncoder::new_thumb2();
10439
10440        // clang: rsb.w r3, r2, #0x20 = f1c2 0320 — byte-identical to before.
10441        let rsb = enc
10442            .encode(&ArmOp::Rsb {
10443                rd: Reg::R3,
10444                rn: Reg::R2,
10445                imm: 32,
10446            })
10447            .unwrap();
10448        assert_eq!(rsb, vec![0xC2, 0xF1, 0x20, 0x03]);
10449
10450        // 0x101 is not ThumbExpandImm-representable -> must Err, not mis-encode.
10451        assert!(
10452            enc.encode(&ArmOp::Rsb {
10453                rd: Reg::R3,
10454                rn: Reg::R2,
10455                imm: 0x101,
10456            })
10457            .is_err(),
10458            "non-ThumbExpandImm RSB immediate must Err"
10459        );
10460
10461        // clang: and r4, r4, #0x3f = f004 043f — byte-identical to before.
10462        let and = enc.encode_thumb32_and_imm_raw(4, 4, 0x3F).unwrap();
10463        assert_eq!(and, vec![0x04, 0xF0, 0x3F, 0x04]);
10464        assert!(
10465            enc.encode_thumb32_and_imm_raw(4, 4, 0x101).is_err(),
10466            "non-ThumbExpandImm AND immediate must Err"
10467        );
10468
10469        // A32 RSB: imm12 is a rotate:imm8 modified immediate; > 0xFF used to be
10470        // silently masked to `imm & 0xFF` (#378 masking class) -> must Err.
10471        let a32 = ArmEncoder::new_arm32();
10472        assert!(
10473            a32.encode(&ArmOp::Rsb {
10474                rd: Reg::R3,
10475                rn: Reg::R2,
10476                imm: 0x120,
10477            })
10478            .is_err(),
10479            "A32 RSB immediate > 0xFF must Err, not mask"
10480        );
10481        // imm 32 (the only value real codegen emits) still encodes.
10482        assert!(
10483            a32.encode(&ArmOp::Rsb {
10484                rd: Reg::R3,
10485                rn: Reg::R2,
10486                imm: 32,
10487            })
10488            .is_ok()
10489        );
10490    }
10491
10492    /// #350 follow-up — the `encoder_no_panic` fuzz harness drives the encoder
10493    /// with ARBITRARY registers, including the one case the in-place lowering
10494    /// cannot serve: rd==rn==R12. There the scratch (R12, the reserved encoder
10495    /// register) would alias Rn and clobber it before the ADD reads it. The
10496    /// encoder contract (#180/#185) is Ok-or-Err, never a panic — so this must
10497    /// return Err, not assert. (Real codegen never emits rd==rn==R12 because R12
10498    /// is non-allocatable; this guards only the fuzz/adversarial path.)
10499    #[test]
10500    fn test_encode_add_imm_large_rd_rn_r12_errs_not_panics_350() {
10501        let enc = ArmEncoder::new_thumb2();
10502        // Out-of-range imm with rd==rn==R12: no free scratch -> Err.
10503        let r = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 70000);
10504        assert!(
10505            r.is_err(),
10506            "rd==rn==R12 with out-of-range imm must Err (no free scratch), got {r:?}"
10507        );
10508        // Small imm with rd==rn==R12 still takes the single-instruction fast path
10509        // (no scratch needed) and must succeed — the guard is scoped to the
10510        // out-of-range lowering only.
10511        let small = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 0x10);
10512        assert!(small.is_ok(), "small imm needs no scratch, must stay Ok");
10513    }
10514
10515    /// #378 — `encode_operand2` (ARM32 data-processing operand) must FAIL
10516    /// HONESTLY on an immediate that is not a valid rotated immediate, rather
10517    /// than silently masking it to `imm & 0xFF` and emitting a WRONG
10518    /// instruction. `0x1FF` has 9 set bits, so it cannot come from rotating an
10519    /// 8-bit imm8 — non-encodable. Real codegen materializes large constants via
10520    /// MOVW/MOVT; this guards the encoder's Ok-or-Err contract (#180/#185)
10521    /// directly. It is an Err (not a panic) so the `encoder_no_panic` fuzz
10522    /// harness — which drives arbitrary operands — still passes.
10523    #[test]
10524    fn test_encode_operand2_non_rotatable_imm_errs_not_masks_378() {
10525        let enc = ArmEncoder::new_arm32();
10526        let bad = enc.encode(&ArmOp::Add {
10527            rd: Reg::R0,
10528            rn: Reg::R1,
10529            op2: Operand2::Imm(0x1FF),
10530        });
10531        assert!(
10532            bad.is_err(),
10533            "non-rotatable ARM32 immediate 0x1FF must Err (was silently masked \
10534             to 0xFF), got {bad:?}"
10535        );
10536        // A representable rotated immediate still encodes fine (regression guard).
10537        let ok = enc.encode(&ArmOp::Add {
10538            rd: Reg::R0,
10539            rn: Reg::R1,
10540            op2: Operand2::Imm(0xFF),
10541        });
10542        assert!(
10543            ok.is_ok(),
10544            "0xFF is a valid rotated immediate, must stay Ok"
10545        );
10546    }
10547
10548    #[test]
10549    fn test_encode_ldr_arm32() {
10550        let encoder = ArmEncoder::new_arm32();
10551        let op = ArmOp::Ldr {
10552            rd: Reg::R0,
10553            addr: MemAddr::imm(Reg::R1, 4),
10554        };
10555
10556        let code = encoder.encode(&op).unwrap();
10557        assert_eq!(code.len(), 4);
10558
10559        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10560        // Verify load bit is set
10561        assert_eq!(instr & 0x00100000, 0x00100000);
10562    }
10563
10564    #[test]
10565    fn test_encode_str_arm32() {
10566        let encoder = ArmEncoder::new_arm32();
10567        let op = ArmOp::Str {
10568            rd: Reg::R0,
10569            addr: MemAddr::imm(Reg::SP, 0),
10570        };
10571
10572        let code = encoder.encode(&op).unwrap();
10573        assert_eq!(code.len(), 4);
10574    }
10575
10576    #[test]
10577    fn test_encode_branch_arm32() {
10578        let encoder = ArmEncoder::new_arm32();
10579        let op = ArmOp::Bl {
10580            label: "main".to_string(),
10581        };
10582
10583        let code = encoder.encode(&op).unwrap();
10584        assert_eq!(code.len(), 4);
10585
10586        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10587        // Verify BL opcode
10588        assert_eq!(instr & 0x0F000000, 0x0B000000);
10589    }
10590
10591    /// #1040: the A32 BL relocatable placeholder must carry an embedded addend
10592    /// of -8 so an R_ARM_CALL nets to exactly the symbol S — the A32 twin of
10593    /// the Thumb #167/#174 test above. A32 `BL` computes
10594    /// `target = P + 8 + (imm24 << 2)`, so:
10595    ///   - `eb000000` (imm24 = 0, addend 0) lands at S+8, two instructions
10596    ///     past the callee entry. This is what synth emitted before #1040, and
10597    ///     combined with the mislabelled R_ARM_THM_CALL a real linker
10598    ///     (`arm-none-eabi-ld`) corrupted the word to `eaca0000` — opcode
10599    ///     `eb` (BL) flipped to `ea` (B), so LR was never set at all.
10600    ///   - `ebfffffe` (imm24 = -2, offset -8) is `bl <self>` and nets to S.
10601    ///     This is exactly what `arm-none-eabi-as -march=armv7-r` emits for
10602    ///     `bl <extern>`, verified directly rather than derived from the ABI.
10603    #[test]
10604    fn test_encode_arm32_bl_placeholder_addend_1040() {
10605        let encoder = ArmEncoder::new_arm32();
10606        let code = encoder
10607            .encode(&ArmOp::Bl {
10608                label: "callee".to_string(),
10609            })
10610            .unwrap();
10611        assert_eq!(code.len(), 4, "A32 BL is one 32-bit word");
10612        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10613        assert_eq!(
10614            instr, 0xEBFF_FFFE,
10615            "A32 BL placeholder must be `ebfffffe` (gas's `bl <extern>`), not              `eb000000` — a 0 addend resolves two instructions past the callee              entry (#1040)"
10616        );
10617        // Spell the addend out so a future edit cannot satisfy the literal by
10618        // accident: sign-extended imm24, word-scaled, plus the +8 pipeline
10619        // bias, must be exactly 0 (branch-to-self).
10620        let imm24 = instr & 0x00FF_FFFF;
10621        let signed = ((imm24 as i32) << 8) >> 8;
10622        assert_eq!(
10623            8 + (signed << 2),
10624            0,
10625            "placeholder must branch to itself so the REL addend is -8"
10626        );
10627    }
10628
10629    /// Regression test for #167 + #174: the Thumb-2 BL relocatable placeholder
10630    /// must carry a -4 addend so an R_ARM_THM_CALL nets to exactly the symbol S.
10631    /// The correct encoding is what `gas` emits for `bl <extern>`: f7ff fffe
10632    /// (hw1=0xF7FF, hw2=0xFFFE), little-endian bytes FF F7 FE FF.
10633    ///   - 0xD000 (J1=J2=0) → ~+0x600000 garbage addend: `bl c0000c` / truncated
10634    ///     to fit (#167).
10635    ///   - 0xF800 (addend 0) → lands at S+4, one instruction past the callee
10636    ///     entry (#174).
10637    ///   - 0xFFFE (addend -4) → lands at S. Correct.
10638    #[test]
10639    fn test_encode_thumb_bl_placeholder_addend_167_174() {
10640        let encoder = ArmEncoder::new_thumb2();
10641        let op = ArmOp::Bl {
10642            label: "callee".to_string(),
10643        };
10644
10645        let code = encoder.encode(&op).unwrap();
10646        assert_eq!(code.len(), 4, "Thumb-2 BL is 32-bit");
10647
10648        let hw1 = u16::from_le_bytes([code[0], code[1]]);
10649        let hw2 = u16::from_le_bytes([code[2], code[3]]);
10650        assert_eq!(hw1, 0xF7FF, "BL first halfword (matches gas `bl <extern>`)");
10651        assert_eq!(
10652            hw2, 0xFFFE,
10653            "BL second halfword must be 0xFFFE (-4 addend → nets to S), not 0xF800 (→ S+4, #174) or 0xD000 (#167)"
10654        );
10655        assert_ne!(hw2, 0xF800, "0xF800 (addend 0) lands at S+4 (#174)");
10656        assert_ne!(hw2, 0xD000, "0xD000 bakes in a ~+0x600000 addend (#167)");
10657    }
10658
10659    /// #740: the Thumb-2 32-bit B<cond>.W (encoding T3) must pack the
10660    /// HALFWORD offset directly into S:J2:J1:imm6:imm11 — the byte offset is
10661    /// SignExtend(S:J2:J1:imm6:imm11:'0'). The old arm packed
10662    /// `halfword_offset >> 1`, HALVING every wide conditional branch's
10663    /// displacement: gust_poll's loop-head `br_if` to an outer block end
10664    /// landed mid-shape (a spurious state write + spurious calls on the
10665    /// empty-budget path). Narrow (16-bit) B<cond> was unaffected — only
10666    /// spans > 254 bytes hit the bug. Bytes cross-checked against the llvm
10667    /// disassembler (`bne.w #0x224` = f040 8112).
10668    #[test]
10669    fn test_encode_thumb_bcond_wide_t3_halfword_offset_740() {
10670        use synth_synthesis::Condition;
10671        let encoder = ArmEncoder::new_thumb2();
10672
10673        // gust_poll's loop-head edge: NE, +0x112 halfwords (+0x224 bytes).
10674        let code = encoder
10675            .encode(&ArmOp::BCondOffset {
10676                cond: Condition::NE,
10677                offset: 0x112,
10678            })
10679            .unwrap();
10680        assert_eq!(code.len(), 4, "offset beyond ±127 halfwords must be wide");
10681        let hw1 = u16::from_le_bytes([code[0], code[1]]);
10682        let hw2 = u16::from_le_bytes([code[2], code[3]]);
10683        assert_eq!(hw1, 0xF040, "T3 hw1: 1111 0 S=0 cond=NE imm6=0");
10684        assert_eq!(
10685            hw2, 0x8112,
10686            "T3 hw2 imm11 must carry halfword offset bits [10:0] directly — \
10687             0x8089 (offset>>1) is the halved #740 miscompile"
10688        );
10689
10690        // Backward wide branch: EQ, -0x100 halfwords. S=1, J2=J1=1,
10691        // imm6=0b111111, imm11=0x700 → f43f af00.
10692        let code = encoder
10693            .encode(&ArmOp::BCondOffset {
10694                cond: Condition::EQ,
10695                offset: -0x100,
10696            })
10697            .unwrap();
10698        assert_eq!(code.len(), 4);
10699        let hw1 = u16::from_le_bytes([code[0], code[1]]);
10700        let hw2 = u16::from_le_bytes([code[2], code[3]]);
10701        assert_eq!(hw1, 0xF43F, "T3 hw1: S=1, cond=EQ, imm6=0x3F");
10702        assert_eq!(hw2, 0xAF00, "T3 hw2: J1=1 J2=1 imm11=0x700");
10703
10704        // Narrow encoding stays byte-identical (in-range offsets untouched).
10705        let code = encoder
10706            .encode(&ArmOp::BCondOffset {
10707                cond: Condition::EQ,
10708                offset: 5,
10709            })
10710            .unwrap();
10711        assert_eq!(code, vec![0x05, 0xD0], "narrow B<cond> unchanged");
10712
10713        // Out of the signed 20-bit T3 range: loud Err, never a truncated jump.
10714        assert!(
10715            encoder
10716                .encode(&ArmOp::BCondOffset {
10717                    cond: Condition::NE,
10718                    offset: 1 << 19,
10719                })
10720                .is_err(),
10721            "out-of-range T3 offset must be a loud decline"
10722        );
10723    }
10724
10725    #[test]
10726    fn test_encode_sequence() {
10727        let encoder = ArmEncoder::new_arm32();
10728        let ops = vec![
10729            ArmOp::Mov {
10730                rd: Reg::R0,
10731                op2: Operand2::Imm(42),
10732            },
10733            ArmOp::Mov {
10734                rd: Reg::R1,
10735                op2: Operand2::Imm(10),
10736            },
10737            ArmOp::Add {
10738                rd: Reg::R2,
10739                rn: Reg::R0,
10740                op2: Operand2::Reg(Reg::R1),
10741            },
10742        ];
10743
10744        let code = encoder.encode_sequence(&ops).unwrap();
10745        assert_eq!(code.len(), 12); // 3 instructions * 4 bytes
10746    }
10747
10748    #[test]
10749    fn test_reg_to_bits() {
10750        assert_eq!(reg_to_bits(&Reg::R0), 0);
10751        assert_eq!(reg_to_bits(&Reg::R7), 7);
10752        assert_eq!(reg_to_bits(&Reg::SP), 13);
10753        assert_eq!(reg_to_bits(&Reg::LR), 14);
10754        assert_eq!(reg_to_bits(&Reg::PC), 15);
10755    }
10756
10757    #[test]
10758    fn test_encode_bitwise_operations() {
10759        let encoder = ArmEncoder::new_arm32();
10760
10761        let and_op = ArmOp::And {
10762            rd: Reg::R0,
10763            rn: Reg::R1,
10764            op2: Operand2::Reg(Reg::R2),
10765        };
10766        let and_code = encoder.encode(&and_op).unwrap();
10767        assert_eq!(and_code.len(), 4);
10768
10769        let orr_op = ArmOp::Orr {
10770            rd: Reg::R0,
10771            rn: Reg::R1,
10772            op2: Operand2::Reg(Reg::R2),
10773        };
10774        let orr_code = encoder.encode(&orr_op).unwrap();
10775        assert_eq!(orr_code.len(), 4);
10776
10777        let eor_op = ArmOp::Eor {
10778            rd: Reg::R0,
10779            rn: Reg::R1,
10780            op2: Operand2::Reg(Reg::R2),
10781        };
10782        let eor_code = encoder.encode(&eor_op).unwrap();
10783        assert_eq!(eor_code.len(), 4);
10784    }
10785
10786    // === Thumb-2 32-bit encoding tests ===
10787
10788    #[test]
10789    fn test_encode_sdiv_thumb2() {
10790        let encoder = ArmEncoder::new_thumb2();
10791        let op = ArmOp::Sdiv {
10792            rd: Reg::R0,
10793            rn: Reg::R1,
10794            rm: Reg::R2,
10795        };
10796
10797        let code = encoder.encode(&op).unwrap();
10798        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10799
10800        // SDIV R0, R1, R2: 0xFB91 0xF0F2
10801        // First halfword: 0xFB90 | Rn(1) = 0xFB91
10802        // Second halfword: 0xF0F0 | Rd(0)<<8 | Rm(2) = 0xF0F2
10803        // Little-endian: [0x91, 0xFB, 0xF2, 0xF0]
10804        assert_eq!(code[0], 0x91);
10805        assert_eq!(code[1], 0xFB);
10806        assert_eq!(code[2], 0xF2);
10807        assert_eq!(code[3], 0xF0);
10808    }
10809
10810    #[test]
10811    fn test_encode_udiv_thumb2() {
10812        let encoder = ArmEncoder::new_thumb2();
10813        let op = ArmOp::Udiv {
10814            rd: Reg::R0,
10815            rn: Reg::R1,
10816            rm: Reg::R2,
10817        };
10818
10819        let code = encoder.encode(&op).unwrap();
10820        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10821
10822        // UDIV R0, R1, R2: 0xFBB1 0xF0F2
10823        // Little-endian: [0xB1, 0xFB, 0xF2, 0xF0]
10824        assert_eq!(code[0], 0xB1);
10825        assert_eq!(code[1], 0xFB);
10826        assert_eq!(code[2], 0xF2);
10827        assert_eq!(code[3], 0xF0);
10828    }
10829
10830    #[test]
10831    fn test_encode_mul_thumb2() {
10832        let encoder = ArmEncoder::new_thumb2();
10833        let op = ArmOp::Mul {
10834            rd: Reg::R0,
10835            rn: Reg::R1,
10836            rm: Reg::R2,
10837        };
10838
10839        let code = encoder.encode(&op).unwrap();
10840        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10841    }
10842
10843    #[test]
10844    fn test_encode_and_thumb2() {
10845        let encoder = ArmEncoder::new_thumb2();
10846        let op = ArmOp::And {
10847            rd: Reg::R0,
10848            rn: Reg::R1,
10849            op2: Operand2::Reg(Reg::R2),
10850        };
10851
10852        let code = encoder.encode(&op).unwrap();
10853        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10854    }
10855
10856    #[test]
10857    fn test_encode_lsl_thumb2_low_regs() {
10858        let encoder = ArmEncoder::new_thumb2();
10859        let op = ArmOp::Lsl {
10860            rd: Reg::R0,
10861            rn: Reg::R1,
10862            shift: 5,
10863        };
10864
10865        let code = encoder.encode(&op).unwrap();
10866        assert_eq!(code.len(), 2); // 16-bit for low registers
10867    }
10868
10869    #[test]
10870    fn test_encode_clz_thumb2() {
10871        let encoder = ArmEncoder::new_thumb2();
10872        let op = ArmOp::Clz {
10873            rd: Reg::R0,
10874            rm: Reg::R1,
10875        };
10876
10877        let code = encoder.encode(&op).unwrap();
10878        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10879    }
10880
10881    #[test]
10882    fn test_encode_bx_thumb2() {
10883        let encoder = ArmEncoder::new_thumb2();
10884        let op = ArmOp::Bx { rm: Reg::LR };
10885
10886        let code = encoder.encode(&op).unwrap();
10887        assert_eq!(code.len(), 2); // 16-bit instruction
10888
10889        // BX LR: 0x4770
10890        assert_eq!(code, vec![0x70, 0x47]);
10891    }
10892
10893    // ========================================================================
10894    // f32 pseudo-op encoding tests
10895    // ========================================================================
10896
10897    #[test]
10898    fn test_encode_f32_abs_arm32() {
10899        let encoder = ArmEncoder::new_arm32();
10900        let op = ArmOp::F32Abs {
10901            sd: VfpReg::S0,
10902            sm: VfpReg::S2,
10903        };
10904        let code = encoder.encode(&op).unwrap();
10905        assert_eq!(code.len(), 4); // Single VFP instruction
10906    }
10907
10908    #[test]
10909    fn test_encode_f32_neg_arm32() {
10910        let encoder = ArmEncoder::new_arm32();
10911        let op = ArmOp::F32Neg {
10912            sd: VfpReg::S0,
10913            sm: VfpReg::S2,
10914        };
10915        let code = encoder.encode(&op).unwrap();
10916        assert_eq!(code.len(), 4);
10917    }
10918
10919    #[test]
10920    fn test_encode_f32_sqrt_arm32() {
10921        let encoder = ArmEncoder::new_arm32();
10922        let op = ArmOp::F32Sqrt {
10923            sd: VfpReg::S0,
10924            sm: VfpReg::S2,
10925        };
10926        let code = encoder.encode(&op).unwrap();
10927        assert_eq!(code.len(), 4);
10928    }
10929
10930    #[test]
10931    fn test_encode_f32_ceil_arm32() {
10932        let encoder = ArmEncoder::new_arm32();
10933        let op = ArmOp::F32Ceil {
10934            sd: VfpReg::S0,
10935            sm: VfpReg::S2,
10936        };
10937        let code = encoder.encode(&op).unwrap();
10938        // VMRS + BIC + ORR + VMSR + VCVT.S32.F32 + VMRS + BIC + VMSR + VCVT.F32.S32
10939        assert_eq!(code.len(), 36);
10940    }
10941
10942    #[test]
10943    fn test_encode_f32_floor_thumb2() {
10944        let encoder = ArmEncoder::new_thumb2();
10945        let op = ArmOp::F32Floor {
10946            sd: VfpReg::S0,
10947            sm: VfpReg::S2,
10948        };
10949        let code = encoder.encode(&op).unwrap();
10950        // VMRS + BIC.W + ORR.W + VMSR + VCVT + VMRS + BIC.W + VMSR + VCVT.F32.S32
10951        assert_eq!(code.len(), 36);
10952    }
10953
10954    #[test]
10955    fn test_encode_f32_min_arm32() {
10956        let encoder = ArmEncoder::new_arm32();
10957        let op = ArmOp::F32Min {
10958            sd: VfpReg::S0,
10959            sn: VfpReg::S2,
10960            sm: VfpReg::S4,
10961        };
10962        let code = encoder.encode(&op).unwrap();
10963        assert_eq!(code.len(), 16); // VMOV + VCMP + VMRS + conditional VMOV
10964    }
10965
10966    #[test]
10967    fn test_encode_f32_max_thumb2() {
10968        let encoder = ArmEncoder::new_thumb2();
10969        let op = ArmOp::F32Max {
10970            sd: VfpReg::S0,
10971            sn: VfpReg::S2,
10972            sm: VfpReg::S4,
10973        };
10974        let code = encoder.encode(&op).unwrap();
10975        // VMOV(4) + VCMP(4) + VMRS(4) + IT(2) + VMOV(4) = 18
10976        assert_eq!(code.len(), 18);
10977    }
10978
10979    #[test]
10980    fn test_encode_f32_copysign_arm32() {
10981        let encoder = ArmEncoder::new_arm32();
10982        let op = ArmOp::F32Copysign {
10983            sd: VfpReg::S0,
10984            sn: VfpReg::S2,
10985            sm: VfpReg::S4,
10986        };
10987        let code = encoder.encode(&op).unwrap();
10988        // VMOV + VMOV + AND + BIC + ORR + VMOV = 6 * 4 = 24
10989        assert_eq!(code.len(), 24);
10990    }
10991
10992    // ========================================================================
10993    // f64 encoding tests
10994    // ========================================================================
10995
10996    #[test]
10997    fn test_encode_f64_add_arm32() {
10998        let encoder = ArmEncoder::new_arm32();
10999        let op = ArmOp::F64Add {
11000            dd: VfpReg::D0,
11001            dn: VfpReg::D1,
11002            dm: VfpReg::D2,
11003        };
11004        let code = encoder.encode(&op).unwrap();
11005        assert_eq!(code.len(), 4);
11006        // VADD.F64 D0, D1, D2: check coprocessor is cp11 (0xB)
11007        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11008        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
11009    }
11010
11011    #[test]
11012    fn test_encode_f64_sub_thumb2() {
11013        let encoder = ArmEncoder::new_thumb2();
11014        let op = ArmOp::F64Sub {
11015            dd: VfpReg::D0,
11016            dn: VfpReg::D1,
11017            dm: VfpReg::D2,
11018        };
11019        let code = encoder.encode(&op).unwrap();
11020        assert_eq!(code.len(), 4); // 32-bit VFP as two Thumb halfwords
11021    }
11022
11023    #[test]
11024    fn test_encode_f64_mul_arm32() {
11025        let encoder = ArmEncoder::new_arm32();
11026        let op = ArmOp::F64Mul {
11027            dd: VfpReg::D0,
11028            dn: VfpReg::D1,
11029            dm: VfpReg::D2,
11030        };
11031        let code = encoder.encode(&op).unwrap();
11032        assert_eq!(code.len(), 4);
11033    }
11034
11035    #[test]
11036    fn test_encode_f64_div_arm32() {
11037        let encoder = ArmEncoder::new_arm32();
11038        let op = ArmOp::F64Div {
11039            dd: VfpReg::D0,
11040            dn: VfpReg::D1,
11041            dm: VfpReg::D2,
11042        };
11043        let code = encoder.encode(&op).unwrap();
11044        assert_eq!(code.len(), 4);
11045    }
11046
11047    #[test]
11048    fn test_encode_f64_abs_arm32() {
11049        let encoder = ArmEncoder::new_arm32();
11050        let op = ArmOp::F64Abs {
11051            dd: VfpReg::D0,
11052            dm: VfpReg::D2,
11053        };
11054        let code = encoder.encode(&op).unwrap();
11055        assert_eq!(code.len(), 4);
11056    }
11057
11058    #[test]
11059    fn test_encode_f64_neg_arm32() {
11060        let encoder = ArmEncoder::new_arm32();
11061        let op = ArmOp::F64Neg {
11062            dd: VfpReg::D0,
11063            dm: VfpReg::D2,
11064        };
11065        let code = encoder.encode(&op).unwrap();
11066        assert_eq!(code.len(), 4);
11067    }
11068
11069    #[test]
11070    fn test_encode_f64_sqrt_arm32() {
11071        let encoder = ArmEncoder::new_arm32();
11072        let op = ArmOp::F64Sqrt {
11073            dd: VfpReg::D0,
11074            dm: VfpReg::D2,
11075        };
11076        let code = encoder.encode(&op).unwrap();
11077        assert_eq!(code.len(), 4);
11078    }
11079
11080    #[test]
11081    fn test_encode_f64_load_arm32() {
11082        let encoder = ArmEncoder::new_arm32();
11083        let op = ArmOp::F64Load {
11084            dd: VfpReg::D0,
11085            addr: MemAddr::imm(Reg::R0, 8),
11086        };
11087        let code = encoder.encode(&op).unwrap();
11088        assert_eq!(code.len(), 4);
11089        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11090        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11 for F64
11091        assert_eq!(instr & 0xFF, 2); // offset 8 / 4 = 2
11092    }
11093
11094    #[test]
11095    fn test_encode_f64_store_thumb2() {
11096        let encoder = ArmEncoder::new_thumb2();
11097        let op = ArmOp::F64Store {
11098            dd: VfpReg::D0,
11099            addr: MemAddr::imm(Reg::SP, 0),
11100        };
11101        let code = encoder.encode(&op).unwrap();
11102        assert_eq!(code.len(), 4);
11103    }
11104
11105    #[test]
11106    fn test_encode_f64_compare_arm32() {
11107        let encoder = ArmEncoder::new_arm32();
11108        let op = ArmOp::F64Eq {
11109            rd: Reg::R0,
11110            dn: VfpReg::D0,
11111            dm: VfpReg::D1,
11112        };
11113        let code = encoder.encode(&op).unwrap();
11114        assert_eq!(code.len(), 16); // VCMP + VMRS + MOV #0 + MOVcond #1
11115    }
11116
11117    #[test]
11118    fn test_encode_f64_compare_thumb2() {
11119        let encoder = ArmEncoder::new_thumb2();
11120        let op = ArmOp::F64Lt {
11121            rd: Reg::R0,
11122            dn: VfpReg::D0,
11123            dm: VfpReg::D1,
11124        };
11125        let code = encoder.encode(&op).unwrap();
11126        // VCMP(4) + VMRS(4) + MOVS(2) + IT(2) + MOV(2) = 14
11127        assert_eq!(code.len(), 14);
11128    }
11129
11130    #[test]
11131    fn test_encode_f64_const_arm32() {
11132        let encoder = ArmEncoder::new_arm32();
11133        let op = ArmOp::F64Const {
11134            dd: VfpReg::D0,
11135            value: 3.125,
11136        };
11137        let code = encoder.encode(&op).unwrap();
11138        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
11139        assert_eq!(code.len(), 20);
11140    }
11141
11142    #[test]
11143    fn test_encode_f64_const_thumb2() {
11144        let encoder = ArmEncoder::new_thumb2();
11145        let op = ArmOp::F64Const {
11146            dd: VfpReg::D0,
11147            value: 2.5,
11148        };
11149        let code = encoder.encode(&op).unwrap();
11150        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
11151        assert_eq!(code.len(), 20);
11152    }
11153
11154    #[test]
11155    fn test_encode_f64_convert_i32s_arm32() {
11156        let encoder = ArmEncoder::new_arm32();
11157        let op = ArmOp::F64ConvertI32S {
11158            dd: VfpReg::D0,
11159            rm: Reg::R0,
11160        };
11161        let code = encoder.encode(&op).unwrap();
11162        // VMOV(4) + VCVT(4) = 8
11163        assert_eq!(code.len(), 8);
11164    }
11165
11166    #[test]
11167    fn test_encode_f64_promote_f32_arm32() {
11168        let encoder = ArmEncoder::new_arm32();
11169        let op = ArmOp::F64PromoteF32 {
11170            dd: VfpReg::D0,
11171            sm: VfpReg::S0,
11172        };
11173        let code = encoder.encode(&op).unwrap();
11174        assert_eq!(code.len(), 4); // Single VCVT.F64.F32 instruction
11175    }
11176
11177    #[test]
11178    fn test_encode_f64_promote_f32_thumb2() {
11179        let encoder = ArmEncoder::new_thumb2();
11180        let op = ArmOp::F64PromoteF32 {
11181            dd: VfpReg::D0,
11182            sm: VfpReg::S0,
11183        };
11184        let code = encoder.encode(&op).unwrap();
11185        assert_eq!(code.len(), 4);
11186    }
11187
11188    #[test]
11189    fn test_encode_i32_trunc_f64s_arm32() {
11190        let encoder = ArmEncoder::new_arm32();
11191        let op = ArmOp::I32TruncF64S {
11192            rd: Reg::R0,
11193            dm: VfpReg::D0,
11194        };
11195        let code = encoder.encode(&op).unwrap();
11196        // VCVT(4) + VMOV(4) = 8
11197        assert_eq!(code.len(), 8);
11198    }
11199
11200    #[test]
11201    fn test_encode_f64_reinterpret_i64_arm32() {
11202        let encoder = ArmEncoder::new_arm32();
11203        let op = ArmOp::F64ReinterpretI64 {
11204            dd: VfpReg::D0,
11205            rmlo: Reg::R0,
11206            rmhi: Reg::R1,
11207        };
11208        let code = encoder.encode(&op).unwrap();
11209        assert_eq!(code.len(), 4); // Single VMOV instruction
11210    }
11211
11212    #[test]
11213    fn test_encode_i64_reinterpret_f64_thumb2() {
11214        let encoder = ArmEncoder::new_thumb2();
11215        let op = ArmOp::I64ReinterpretF64 {
11216            rdlo: Reg::R0,
11217            rdhi: Reg::R1,
11218            dm: VfpReg::D0,
11219        };
11220        let code = encoder.encode(&op).unwrap();
11221        assert_eq!(code.len(), 4);
11222    }
11223
11224    #[test]
11225    fn test_encode_f64_trunc_thumb2() {
11226        let encoder = ArmEncoder::new_thumb2();
11227        let op = ArmOp::F64Trunc {
11228            dd: VfpReg::D0,
11229            dm: VfpReg::D1,
11230        };
11231        let code = encoder.encode(&op).unwrap();
11232        // GI-FPU-002 phase 3 (#369): a single VRINTZ.F64 (clang-verified
11233        // vrintz.f64 d0,d1 base) — no more FPSCR dance / S0 clobber.
11234        assert_eq!(code.len(), 4);
11235        assert_eq!(code, vec![0xb6, 0xee, 0xc1, 0x0b]);
11236    }
11237
11238    /// GI-FPU-002 phase 3 (#369): the rewritten f64 tail sequences, byte-exact
11239    /// against clang (`-target thumbv7em-none-eabi -mfpu=fpv5-d16`). Each
11240    /// clobbers ONLY its destination (+R12/flags where noted) — the previous
11241    /// pseudo-ops staged through live S0/R0-R2 (the #615 class) and the
11242    /// min/max/rounding semantics were wrong (ordered IT select returned the
11243    /// wrong operand on NaN/±0; rounding round-tripped through a 32-bit int).
11244    #[test]
11245    fn test_369_f64_tail_thumb2_encodings_match_clang() {
11246        let enc = ArmEncoder::new_thumb2();
11247        // vrintn/vrintp/vrintm.f64 d1, d2 (FE space, never IT'd).
11248        for (op, want) in [
11249            (
11250                ArmOp::F64Nearest {
11251                    dd: VfpReg::D1,
11252                    dm: VfpReg::D2,
11253                },
11254                vec![0xb9, 0xfe, 0x42, 0x1b],
11255            ),
11256            (
11257                ArmOp::F64Ceil {
11258                    dd: VfpReg::D1,
11259                    dm: VfpReg::D2,
11260                },
11261                vec![0xba, 0xfe, 0x42, 0x1b],
11262            ),
11263            (
11264                ArmOp::F64Floor {
11265                    dd: VfpReg::D1,
11266                    dm: VfpReg::D2,
11267                },
11268                vec![0xbb, 0xfe, 0x42, 0x1b],
11269            ),
11270        ] {
11271            assert_eq!(enc.encode(&op).unwrap(), want, "{op:?}");
11272        }
11273        // vcmp.f64 d1,d2 ; vmrs ; vminnm.f64 d0,d1,d2 ; it vs ; vaddvs.f64
11274        let min = enc
11275            .encode(&ArmOp::F64Min {
11276                dd: VfpReg::D0,
11277                dn: VfpReg::D1,
11278                dm: VfpReg::D2,
11279            })
11280            .unwrap();
11281        assert_eq!(
11282            min,
11283            vec![
11284                0xb4, 0xee, 0x42, 0x1b, // vcmp.f64 d1, d2
11285                0xf1, 0xee, 0x10, 0xfa, // vmrs APSR_nzcv, fpscr
11286                0x81, 0xfe, 0x42, 0x0b, // vminnm.f64 d0, d1, d2
11287                0x68, 0xbf, // it vs
11288                0x31, 0xee, 0x02, 0x0b, // vaddvs.f64 d0, d1, d2
11289            ]
11290        );
11291        // vmaxnm variant flips only bit6 of the VMINNM word.
11292        let max = enc
11293            .encode(&ArmOp::F64Max {
11294                dd: VfpReg::D0,
11295                dn: VfpReg::D1,
11296                dm: VfpReg::D2,
11297            })
11298            .unwrap();
11299        assert_eq!(&max[8..12], &[0x81, 0xfe, 0x02, 0x0b]);
11300        // Destination aliasing a source must ERR (the NaN fix-up would read
11301        // a clobbered operand), never encode.
11302        assert!(
11303            enc.encode(&ArmOp::F64Min {
11304                dd: VfpReg::D1,
11305                dn: VfpReg::D1,
11306                dm: VfpReg::D2,
11307            })
11308            .is_err()
11309        );
11310        // copysign d0,(mag)d1,(sign)d2:
11311        // vmov r12,s5 ; cmp.w r12,#0 ; vabs.f64 d0,d1 ; it mi ; vnegmi.f64 d0,d0
11312        let cs = enc
11313            .encode(&ArmOp::F64Copysign {
11314                dd: VfpReg::D0,
11315                dn: VfpReg::D1,
11316                dm: VfpReg::D2,
11317            })
11318            .unwrap();
11319        assert_eq!(
11320            cs,
11321            vec![
11322                0x12, 0xee, 0x90, 0xca, // vmov r12, s5
11323                0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
11324                0xb0, 0xee, 0xc1, 0x0b, // vabs.f64 d0, d1
11325                0x48, 0xbf, // it mi
11326                0xb1, 0xee, 0x40, 0x0b, // vnegmi.f64 d0, d0
11327            ]
11328        );
11329        // f32 copysign s0,(mag)s1,(sign)s2 — the R0-clobber-free rewrite:
11330        // vmov r12,s2 ; cmp.w r12,#0 ; vabs.f32 s0,s1 ; it mi ; vnegmi.f32
11331        let cs32 = enc
11332            .encode(&ArmOp::F32Copysign {
11333                sd: VfpReg::S0,
11334                sn: VfpReg::S1,
11335                sm: VfpReg::S2,
11336            })
11337            .unwrap();
11338        assert_eq!(
11339            cs32,
11340            vec![
11341                0x11, 0xee, 0x10, 0xca, // vmov r12, s2
11342                0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
11343                0xb0, 0xee, 0xe0, 0x0a, // vabs.f32 s0, s1
11344                0x48, 0xbf, // it mi
11345                0xb1, 0xee, 0x40, 0x0a, // vnegmi.f32 s0, s0
11346            ]
11347        );
11348        // i32 -> f64 stages through the DESTINATION's S-alias (never S0) and
11349        // uses the CORRECT signed/unsigned VCVT bases (previously swapped):
11350        // vmov s0,r3 ; vcvt.f64.s32 d0,s0
11351        let conv_s = enc
11352            .encode(&ArmOp::F64ConvertI32S {
11353                dd: VfpReg::D0,
11354                rm: Reg::R3,
11355            })
11356            .unwrap();
11357        assert_eq!(
11358            conv_s,
11359            vec![
11360                0x00, 0xee, 0x10, 0x3a, // vmov s0, r3
11361                0xb8, 0xee, 0xc0, 0x0b, // vcvt.f64.s32 d0, s0
11362            ]
11363        );
11364        let conv_u = enc
11365            .encode(&ArmOp::F64ConvertI32U {
11366                dd: VfpReg::D0,
11367                rm: Reg::R3,
11368            })
11369            .unwrap();
11370        assert_eq!(&conv_u[4..8], &[0xb8, 0xee, 0x40, 0x0b]); // vcvt.f64.u32
11371        // f64 -> i32 stages through the SOURCE's S-alias (never S0):
11372        // vcvt.s32.f64 s2,d1 ; vmov r3,s2
11373        let trunc_s = enc
11374            .encode(&ArmOp::I32TruncF64S {
11375                rd: Reg::R3,
11376                dm: VfpReg::D1,
11377            })
11378            .unwrap();
11379        assert_eq!(
11380            trunc_s,
11381            vec![
11382                0xbd, 0xee, 0xc1, 0x1b, // vcvt.s32.f64 s2, d1
11383                0x11, 0xee, 0x10, 0x3a, // vmov r3, s2
11384            ]
11385        );
11386        let trunc_u = enc
11387            .encode(&ArmOp::I32TruncF64U {
11388                rd: Reg::R3,
11389                dm: VfpReg::D1,
11390            })
11391            .unwrap();
11392        assert_eq!(&trunc_u[0..4], &[0xbc, 0xee, 0xc1, 0x1b]); // vcvt.u32.f64
11393        // f32.demote_f64: vcvt.f32.f64 s1, d2
11394        let demote = enc
11395            .encode(&ArmOp::F32DemoteF64 {
11396                sd: VfpReg::S1,
11397                dm: VfpReg::D2,
11398            })
11399            .unwrap();
11400        assert_eq!(demote, vec![0xf7, 0xee, 0xc2, 0x0b]);
11401    }
11402
11403    #[test]
11404    fn test_encode_f64_min_arm32() {
11405        let encoder = ArmEncoder::new_arm32();
11406        let op = ArmOp::F64Min {
11407            dd: VfpReg::D0,
11408            dn: VfpReg::D1,
11409            dm: VfpReg::D2,
11410        };
11411        let code = encoder.encode(&op).unwrap();
11412        // VMOV + VCMP + VMRS + conditional VMOV = 16
11413        assert_eq!(code.len(), 16);
11414    }
11415
11416    #[test]
11417    fn test_f64_cp11_encoding() {
11418        // Verify that F64 instructions use coprocessor 11 (0xB), not 10 (0xA)
11419        let encoder = ArmEncoder::new_arm32();
11420
11421        // F64Add
11422        let code = encoder
11423            .encode(&ArmOp::F64Add {
11424                dd: VfpReg::D0,
11425                dn: VfpReg::D0,
11426                dm: VfpReg::D0,
11427            })
11428            .unwrap();
11429        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11430        assert_eq!((instr >> 8) & 0xF, 0xB, "F64 should use cp11");
11431
11432        // F32Add for comparison
11433        let code = encoder
11434            .encode(&ArmOp::F32Add {
11435                sd: VfpReg::S0,
11436                sn: VfpReg::S0,
11437                sm: VfpReg::S0,
11438            })
11439            .unwrap();
11440        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11441        assert_eq!((instr >> 8) & 0xF, 0xA, "F32 should use cp10");
11442    }
11443
11444    #[test]
11445    fn test_dreg_encoding_higher_registers() {
11446        let encoder = ArmEncoder::new_arm32();
11447
11448        // Test with D15 (highest register)
11449        let op = ArmOp::F64Add {
11450            dd: VfpReg::D15,
11451            dn: VfpReg::D14,
11452            dm: VfpReg::D13,
11453        };
11454        let code = encoder.encode(&op).unwrap();
11455        assert_eq!(code.len(), 4);
11456
11457        // Verify the register encoding worked (instruction is valid)
11458        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11459        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
11460    }
11461
11462    // ========================================================================
11463    // Control flow encoding tests
11464    // ========================================================================
11465
11466    #[test]
11467    fn test_encode_label_emits_no_bytes() {
11468        let encoder = ArmEncoder::new_thumb2();
11469        let op = ArmOp::Label {
11470            name: ".Lblock_end_0".to_string(),
11471        };
11472        let code = encoder.encode(&op).unwrap();
11473        assert!(code.is_empty(), "Label should emit zero bytes");
11474
11475        let encoder32 = ArmEncoder::new_arm32();
11476        let code32 = encoder32.encode(&op).unwrap();
11477        assert!(
11478            code32.is_empty(),
11479            "Label should emit zero bytes in ARM32 too"
11480        );
11481    }
11482
11483    #[test]
11484    fn test_encode_bcc_eq_thumb2() {
11485        use synth_synthesis::Condition;
11486        let encoder = ArmEncoder::new_thumb2();
11487        let op = ArmOp::Bcc {
11488            cond: Condition::EQ,
11489            label: "target".to_string(),
11490        };
11491        let code = encoder.encode(&op).unwrap();
11492        assert_eq!(code.len(), 2); // 16-bit conditional branch
11493
11494        // BEQ with offset 0: 0xD000 in little-endian
11495        assert_eq!(code, vec![0x00, 0xD0]);
11496    }
11497
11498    #[test]
11499    fn test_encode_bcc_ne_thumb2() {
11500        use synth_synthesis::Condition;
11501        let encoder = ArmEncoder::new_thumb2();
11502        let op = ArmOp::Bcc {
11503            cond: Condition::NE,
11504            label: "target".to_string(),
11505        };
11506        let code = encoder.encode(&op).unwrap();
11507        assert_eq!(code.len(), 2);
11508
11509        // BNE with offset 0: 0xD100 in little-endian
11510        assert_eq!(code, vec![0x00, 0xD1]);
11511    }
11512
11513    #[test]
11514    fn test_encode_bcc_arm32() {
11515        use synth_synthesis::Condition;
11516        let encoder = ArmEncoder::new_arm32();
11517        let op = ArmOp::Bcc {
11518            cond: Condition::EQ,
11519            label: "target".to_string(),
11520        };
11521        let code = encoder.encode(&op).unwrap();
11522        assert_eq!(code.len(), 4); // 32-bit ARM instruction
11523
11524        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11525        // BEQ: cond=0x0, opcode=0xA, offset=0
11526        assert_eq!(instr & 0xF0000000, 0x00000000); // EQ condition
11527        assert_eq!(instr & 0x0F000000, 0x0A000000); // Branch opcode
11528    }
11529
11530    #[test]
11531    fn test_encode_udf_thumb2() {
11532        let encoder = ArmEncoder::new_thumb2();
11533        let op = ArmOp::Udf { imm: 0 };
11534        let code = encoder.encode(&op).unwrap();
11535        assert_eq!(code.len(), 2); // 16-bit
11536
11537        // UDF #0: 0xDE00 in little-endian
11538        assert_eq!(code, vec![0x00, 0xDE]);
11539    }
11540
11541    /// #610: the i64 rot/div/rem expansions must land the result in the
11542    /// selector-assigned rd pair and leave R0-R3 preserved (restored from the
11543    /// fixed-ABI wrapper's save area) — pre-#610 the rot expansion's own
11544    /// `POP {R4}` restored stale scratch OVER the result (rd_lo == R4) and
11545    /// the div/rem expansions ignored their register fields outright.
11546    #[test]
11547    fn test_610_i64_rot_expansion_ends_with_rd_movs_and_restore() {
11548        let encoder = ArmEncoder::new_thumb2();
11549        for op in [
11550            ArmOp::I64Rotl {
11551                rdlo: Reg::R4,
11552                rdhi: Reg::R5,
11553                rnlo: Reg::R0,
11554                rnhi: Reg::R1,
11555                shift: Reg::R2,
11556            },
11557            ArmOp::I64Rotr {
11558                rdlo: Reg::R4,
11559                rdhi: Reg::R5,
11560                rnlo: Reg::R0,
11561                rnhi: Reg::R1,
11562                shift: Reg::R2,
11563            },
11564        ] {
11565            let code = encoder.encode(&op).unwrap();
11566            assert_eq!(code.len(), 102, "register-independent size (estimator pin)");
11567            // Tail: MOV r5, r1 (0x460D); MOV r4, r0 (0x4604); POP {r0..r3}
11568            // (rd pair r4:r5 does not overlap the save area — all 4 restored).
11569            let tail: Vec<u16> = code[code.len() - 12..]
11570                .chunks(2)
11571                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11572                .collect();
11573            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11574        }
11575    }
11576
11577    /// #610: div/rem expansions honor rd and carry the divide-by-zero trap
11578    /// guard (`ORRS R12, R2, R3; BNE +0; UDF #0`) after operand marshaling.
11579    #[test]
11580    fn test_610_i64_div_rem_expansion_guard_and_rd() {
11581        let encoder = ArmEncoder::new_thumb2();
11582        let mk = |which: u8| {
11583            let (rdlo, rdhi, rnlo, rnhi, rmlo, rmhi) =
11584                (Reg::R4, Reg::R5, Reg::R0, Reg::R1, Reg::R2, Reg::R3);
11585            match which {
11586                0 => ArmOp::I64DivU {
11587                    rdlo,
11588                    rdhi,
11589                    rnlo,
11590                    rnhi,
11591                    rmlo,
11592                    rmhi,
11593                    elide_zero_guard: false,
11594                },
11595                1 => ArmOp::I64RemU {
11596                    rdlo,
11597                    rdhi,
11598                    rnlo,
11599                    rnhi,
11600                    rmlo,
11601                    rmhi,
11602                    elide_zero_guard: false,
11603                },
11604                2 => ArmOp::I64DivS {
11605                    rdlo,
11606                    rdhi,
11607                    rnlo,
11608                    rnhi,
11609                    rmlo,
11610                    rmhi,
11611                    elide_zero_guard: false,
11612                    elide_overflow_guard: false,
11613                },
11614                _ => ArmOp::I64RemS {
11615                    rdlo,
11616                    rdhi,
11617                    rnlo,
11618                    rnhi,
11619                    rmlo,
11620                    rmhi,
11621                    elide_zero_guard: false,
11622                },
11623            }
11624        };
11625        for which in 0..4u8 {
11626            let code = encoder.encode(&mk(which)).unwrap();
11627            // Zero-divisor trap guard right after the 26-byte marshal prologue.
11628            let guard: Vec<u16> = code[26..34]
11629                .chunks(2)
11630                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11631                .collect();
11632            assert_eq!(
11633                guard,
11634                vec![0xEA52, 0x0C03, 0xD100, 0xDE00],
11635                "ORRS R12,R2,R3; BNE +0; UDF #0"
11636            );
11637            // Tail: result into rd pair (r5:r4), then restore all of R0-R3.
11638            let tail: Vec<u16> = code[code.len() - 12..]
11639                .chunks(2)
11640                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11641                .collect();
11642            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11643        }
11644    }
11645
11646    /// #610: when rd overlaps R0-R3 the restore must SKIP the result
11647    /// registers (drop the saved caller word) instead of popping over them.
11648    #[test]
11649    fn test_610_i64_divu_rd_in_r0_r1_skips_restore() {
11650        let encoder = ArmEncoder::new_thumb2();
11651        let code = encoder
11652            .encode(&ArmOp::I64DivU {
11653                rdlo: Reg::R0,
11654                rdhi: Reg::R1,
11655                rnlo: Reg::R0,
11656                rnhi: Reg::R1,
11657                rmlo: Reg::R2,
11658                rmhi: Reg::R3,
11659                elide_zero_guard: false,
11660            })
11661            .unwrap();
11662        let tail: Vec<u16> = code[code.len() - 12..]
11663            .chunks(2)
11664            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11665            .collect();
11666        // MOV r1,r1 / MOV r0,r0 (no-ops, size-stable), ADD SP,#4 twice
11667        // (discard saved r0/r1 — the result lives there), POP {r2}, POP {r3}.
11668        assert_eq!(tail, vec![0x4609, 0x4600, 0xB001, 0xB001, 0xBC04, 0xBC08]);
11669    }
11670
11671    /// #610: a fully swapped rd pair (rd_lo=R1, rd_hi=R0) cannot be
11672    /// materialized by two MOVs in either order — must be a loud Err, never
11673    /// silent corruption. (Selector pairs are consecutive, so unreachable.)
11674    #[test]
11675    fn test_610_i64_swapped_rd_pair_rejected() {
11676        let encoder = ArmEncoder::new_thumb2();
11677        let result = encoder.encode(&ArmOp::I64RemU {
11678            rdlo: Reg::R1,
11679            rdhi: Reg::R0,
11680            rnlo: Reg::R2,
11681            rnhi: Reg::R3,
11682            rmlo: Reg::R4,
11683            rmhi: Reg::R5,
11684            elide_zero_guard: false,
11685        });
11686        assert!(result.is_err(), "swapped rd pair must be rejected loudly");
11687    }
11688
11689    /// #632: the I64Popcnt expansion's own scratch restore (`POP {R3,R4,R5}`)
11690    /// must not clobber the result. Pre-fix the total was materialized with
11691    /// `ADDS rd, R4, R5` BEFORE the pop, so any allocator-assigned
11692    /// rd ∈ {R3,R4,R5} received stale stack garbage. Post-fix the count is
11693    /// carried across the restore in R12 (never allocatable, never restored)
11694    /// and moved into rd only after the pop — structurally rd-independent.
11695    #[test]
11696    fn test_632_i64_popcnt_result_survives_scratch_restore() {
11697        let encoder = ArmEncoder::new_thumb2();
11698        // Every allocatable rd, including the restore set {R3,R4,R5} and R8.
11699        for rd in [
11700            Reg::R0,
11701            Reg::R2,
11702            Reg::R3,
11703            Reg::R4,
11704            Reg::R5,
11705            Reg::R6,
11706            Reg::R8,
11707        ] {
11708            let code = encoder
11709                .encode(&ArmOp::I64Popcnt {
11710                    rd,
11711                    rnlo: Reg::R6,
11712                    rnhi: Reg::R7,
11713                })
11714                .unwrap();
11715            assert_eq!(code.len(), 176, "register-independent size (estimator pin)");
11716            let hw: Vec<u16> = code
11717                .chunks(2)
11718                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11719                .collect();
11720            let pop = hw
11721                .iter()
11722                .position(|&h| h == 0xBC38)
11723                .expect("POP {R3,R4,R5} present");
11724            // Immediately before the POP: ADD.W R12, R4, R5 (the total lives
11725            // in R12, which the POP cannot touch).
11726            assert_eq!(
11727                &hw[pop - 2..pop],
11728                &[0xEB04, 0x0C05],
11729                "total must be carried in R12 across the restore"
11730            );
11731            // Immediately after the POP: MOV rd, R12.
11732            let rd_bits = match rd {
11733                Reg::R8 => 8u16,
11734                Reg::R6 => 6,
11735                Reg::R5 => 5,
11736                Reg::R4 => 4,
11737                Reg::R3 => 3,
11738                Reg::R2 => 2,
11739                _ => 0,
11740            };
11741            let expect_mov = 0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7);
11742            assert_eq!(hw[pop + 1], expect_mov, "MOV rd, R12 after the restore");
11743            // No write into rd between the PUSH and the POP (the old
11744            // pre-restore ADDS is gone).
11745            assert!(
11746                !hw[..pop].contains(&(0x1800 | (5 << 6) | (4 << 3) | rd_bits)),
11747                "no ADDS rd, R4, R5 before the restore pop"
11748            );
11749        }
11750    }
11751
11752    /// #632 audit: the entry marshal must be permutation-safe. Pre-fix
11753    /// `MOV R4, rnlo; MOV R5, rnhi` read a clobbered R4 when the operand
11754    /// pair lived at (R3, R4). Post-fix rnlo routes through R12.
11755    #[test]
11756    fn test_632_i64_popcnt_marshal_pair_at_r3_r4() {
11757        let encoder = ArmEncoder::new_thumb2();
11758        let code = encoder
11759            .encode(&ArmOp::I64Popcnt {
11760                rd: Reg::R0,
11761                rnlo: Reg::R3,
11762                rnhi: Reg::R4,
11763            })
11764            .unwrap();
11765        let hw: Vec<u16> = code
11766            .chunks(2)
11767            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11768            .collect();
11769        // PUSH {R3,R4,R5}; MOV R12, R3; MOV R5, R4 (rnhi read BEFORE any
11770        // write to R4); MOV R4, R12.
11771        assert_eq!(hw[0], 0xB438);
11772        assert_eq!(hw[1], 0x4600 | (1 << 7) | (3 << 3) | 4, "MOV R12, rnlo");
11773        assert_eq!(hw[2], 0x4600 | (4 << 3) | 5, "MOV R5, rnhi");
11774        assert_eq!(hw[3], 0x4664, "MOV R4, R12");
11775    }
11776
11777    /// #632: A32 twin — same structural fix on the ARM-mode path
11778    /// (`--target cortex-r5`): total carried in R12 across the restore.
11779    #[test]
11780    fn test_632_a32_i64_popcnt_result_survives_scratch_restore() {
11781        let encoder = ArmEncoder::new_arm32();
11782        for rd in [Reg::R0, Reg::R3, Reg::R4, Reg::R5, Reg::R8] {
11783            let code = encoder
11784                .encode(&ArmOp::I64Popcnt {
11785                    rd,
11786                    rnlo: Reg::R6,
11787                    rnhi: Reg::R7,
11788                })
11789                .unwrap();
11790            let words: Vec<u32> = code
11791                .chunks(4)
11792                .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11793                .collect();
11794            let pop = words
11795                .iter()
11796                .position(|&w| w == 0xE8BD_0038)
11797                .expect("POP {R3,R4,R5} present");
11798            assert_eq!(words[pop - 1], 0xE084_C005, "ADD R12, R4, R5 before POP");
11799            let rd_bits = match rd {
11800                Reg::R8 => 8u32,
11801                Reg::R5 => 5,
11802                Reg::R4 => 4,
11803                Reg::R3 => 3,
11804                _ => 0,
11805            };
11806            assert_eq!(
11807                words[pop + 1],
11808                0xE1A0_0000 | (rd_bits << 12) | 12,
11809                "MOV rd, R12 after the restore"
11810            );
11811        }
11812    }
11813
11814    /// #633: I64DivS must carry the INT64_MIN/-1 overflow guard (mirroring
11815    /// the i32 path) right after the zero-divisor guard — dividend in R0:R1,
11816    /// divisor in R2:R3 on the #610/#613 fixed-ABI wrapper path.
11817    #[test]
11818    fn test_633_i64_divs_overflow_guard_emitted() {
11819        let encoder = ArmEncoder::new_thumb2();
11820        let code = encoder
11821            .encode(&ArmOp::I64DivS {
11822                rdlo: Reg::R4,
11823                rdhi: Reg::R5,
11824                rnlo: Reg::R0,
11825                rnhi: Reg::R1,
11826                rmlo: Reg::R2,
11827                rmhi: Reg::R3,
11828                elide_zero_guard: false,
11829                elide_overflow_guard: false,
11830            })
11831            .unwrap();
11832        // 26-byte marshal + 8-byte zero-trap, then the 22-byte overflow guard.
11833        let guard: Vec<u16> = code[34..56]
11834            .chunks(2)
11835            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11836            .collect();
11837        assert_eq!(
11838            guard,
11839            vec![
11840                0xEA02, 0x0C03, // AND.W R12, R2, R3
11841                0xF11C, 0x0F01, // CMN.W R12, #1
11842                0xD105, // BNE .no_trap
11843                0x2800, // CMP R0, #0
11844                0xD103, // BNE .no_trap
11845                0xF1B1, 0x4F00, // CMP.W R1, #0x80000000
11846                0xD100, // BNE .no_trap
11847                0xDE00, // UDF #0 — signed-division overflow
11848            ],
11849            "INT64_MIN/-1 overflow guard after the zero-divisor guard"
11850        );
11851    }
11852
11853    /// #633 fix-guard twin: I64RemS must NOT carry the overflow guard —
11854    /// rem_s(INT64_MIN, -1) is defined as 0 and must not trap. Exactly one
11855    /// UDF (the zero-divisor trap) in the whole expansion.
11856    #[test]
11857    fn test_633_i64_rems_has_no_overflow_guard() {
11858        let encoder = ArmEncoder::new_thumb2();
11859        for (is_rem_s, op) in [
11860            (
11861                true,
11862                ArmOp::I64RemS {
11863                    rdlo: Reg::R4,
11864                    rdhi: Reg::R5,
11865                    rnlo: Reg::R0,
11866                    rnhi: Reg::R1,
11867                    rmlo: Reg::R2,
11868                    rmhi: Reg::R3,
11869                    elide_zero_guard: false,
11870                },
11871            ),
11872            (
11873                false,
11874                ArmOp::I64DivS {
11875                    rdlo: Reg::R4,
11876                    rdhi: Reg::R5,
11877                    rnlo: Reg::R0,
11878                    rnhi: Reg::R1,
11879                    rmlo: Reg::R2,
11880                    rmhi: Reg::R3,
11881                    elide_zero_guard: false,
11882                    elide_overflow_guard: false,
11883                },
11884            ),
11885        ] {
11886            let code = encoder.encode(&op).unwrap();
11887            let udfs = code
11888                .chunks(2)
11889                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11890                .count();
11891            let want = if is_rem_s { 1 } else { 2 };
11892            assert_eq!(
11893                udfs, want,
11894                "rem_s: zero-trap only; div_s: zero-trap + overflow trap"
11895            );
11896        }
11897    }
11898
11899    /// #494 phase 2b: `elide_zero_guard` drops EXACTLY the 8-byte fused
11900    /// zero-trap (`ORRS.W R12,R2,R3; BNE; UDF #0`) and nothing else — the
11901    /// rest of the expansion is byte-identical (splice check).
11902    #[test]
11903    fn test_494_i64_zero_guard_elision_is_exact_splice() {
11904        let encoder = ArmEncoder::new_thumb2();
11905        let mk = |elide_zero_guard: bool| {
11906            encoder
11907                .encode(&ArmOp::I64DivU {
11908                    rdlo: Reg::R4,
11909                    rdhi: Reg::R5,
11910                    rnlo: Reg::R0,
11911                    rnhi: Reg::R1,
11912                    rmlo: Reg::R2,
11913                    rmhi: Reg::R3,
11914                    elide_zero_guard,
11915                })
11916                .unwrap()
11917        };
11918        let full = mk(false);
11919        let elided = mk(true);
11920        assert_eq!(full.len(), elided.len() + 8, "zero guard is 8 bytes");
11921        // Marshal prologue (26 B) unchanged, guard (8 B) gone, tail identical.
11922        assert_eq!(&full[..26], &elided[..26]);
11923        assert_eq!(
11924            &full[26..34],
11925            &[0x52, 0xEA, 0x03, 0x0C, 0x00, 0xD1, 0x00, 0xDE],
11926            "the spliced-out bytes are exactly ORRS.W; BNE; UDF #0"
11927        );
11928        assert_eq!(&full[34..], &elided[26..]);
11929    }
11930
11931    /// #494 phase 2b two-guard distinction (the #633/#634 synergy): a
11932    /// divisor-nonzero fact elides ONLY the zero guard — the INT64_MIN/-1
11933    /// OVERFLOW guard is a separate obligation and must survive
11934    /// `elide_zero_guard: true`. Pinned on div_s in all flag states.
11935    #[test]
11936    fn test_494_i64_divs_overflow_guard_retained_when_only_zero_elided() {
11937        let encoder = ArmEncoder::new_thumb2();
11938        let mk = |zero: bool, ovf: bool| {
11939            encoder
11940                .encode(&ArmOp::I64DivS {
11941                    rdlo: Reg::R4,
11942                    rdhi: Reg::R5,
11943                    rnlo: Reg::R0,
11944                    rnhi: Reg::R1,
11945                    rmlo: Reg::R2,
11946                    rmhi: Reg::R3,
11947                    elide_zero_guard: zero,
11948                    elide_overflow_guard: ovf,
11949                })
11950                .unwrap()
11951        };
11952        let udf_count = |code: &[u8]| {
11953            code.chunks(2)
11954                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11955                .count()
11956        };
11957        let full = mk(false, false);
11958        let zero_only = mk(true, false);
11959        let both = mk(true, true);
11960        assert_eq!(udf_count(&full), 2, "baseline: zero trap + overflow trap");
11961        assert_eq!(
11962            udf_count(&zero_only),
11963            1,
11964            "divisor-nonzero elides the zero trap ONLY — the #633 overflow \
11965             guard must be retained"
11966        );
11967        // The retained guard is the 22-byte overflow sequence, now right
11968        // after the 26-byte marshal prologue.
11969        let guard: Vec<u16> = zero_only[26..48]
11970            .chunks(2)
11971            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11972            .collect();
11973        assert_eq!(
11974            guard,
11975            vec![
11976                0xEA02, 0x0C03, 0xF11C, 0x0F01, 0xD105, 0x2800, 0xD103, 0xF1B1, 0x4F00, 0xD100,
11977                0xDE00,
11978            ],
11979            "the surviving guard is the INT64_MIN/-1 overflow trap"
11980        );
11981        assert_eq!(full.len(), zero_only.len() + 8);
11982        assert_eq!(zero_only.len(), both.len() + 22);
11983        assert_eq!(udf_count(&both), 0, "both obligations discharged ⇒ no UDF");
11984    }
11985
11986    /// #494 phase 2b A32 twin: zero-guard elision is an exact 12-byte splice
11987    /// and the A32 overflow guard survives a zero-only elision.
11988    #[test]
11989    fn test_494_a32_i64_guard_elision() {
11990        let encoder = ArmEncoder::new_arm32();
11991        let mk = |zero: bool, ovf: bool| {
11992            encoder
11993                .encode(&ArmOp::I64DivS {
11994                    rdlo: Reg::R4,
11995                    rdhi: Reg::R5,
11996                    rnlo: Reg::R0,
11997                    rnhi: Reg::R1,
11998                    rmlo: Reg::R2,
11999                    rmhi: Reg::R3,
12000                    elide_zero_guard: zero,
12001                    elide_overflow_guard: ovf,
12002                })
12003                .unwrap()
12004        };
12005        let full = mk(false, false);
12006        let zero_only = mk(true, false);
12007        let both = mk(true, true);
12008        // A32 zero guard = 3 words (ORRS/BNE/UDF), overflow guard = 6 words.
12009        assert_eq!(full.len(), zero_only.len() + 12);
12010        assert_eq!(zero_only.len(), both.len() + 24);
12011        let udf_count = |code: &[u8]| {
12012            code.chunks(4)
12013                .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
12014                .count()
12015        };
12016        assert_eq!(udf_count(&full), 2);
12017        assert_eq!(
12018            udf_count(&zero_only),
12019            1,
12020            "A32: overflow guard retained under zero-only elision"
12021        );
12022        assert_eq!(udf_count(&both), 0);
12023    }
12024
12025    /// #633: A32 twin — the conditional-execution overflow guard on the
12026    /// ARM-mode I64DivS, and its absence from I64RemS.
12027    #[test]
12028    fn test_633_a32_i64_divs_overflow_guard() {
12029        let encoder = ArmEncoder::new_arm32();
12030        let mk_divs = ArmOp::I64DivS {
12031            rdlo: Reg::R4,
12032            rdhi: Reg::R5,
12033            rnlo: Reg::R0,
12034            rnhi: Reg::R1,
12035            rmlo: Reg::R2,
12036            rmhi: Reg::R3,
12037            elide_zero_guard: false,
12038            elide_overflow_guard: false,
12039        };
12040        let code = encoder.encode(&mk_divs).unwrap();
12041        let words: Vec<u32> = code
12042            .chunks(4)
12043            .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
12044            .collect();
12045        let guard = [
12046            0xE002_C003u32, // AND   R12, R2, R3
12047            0xE37C_0001,    // CMN   R12, #1
12048            0x0350_0000,    // CMPEQ R0, #0
12049            0x0351_0102,    // CMPEQ R1, #0x80000000
12050            0x1A00_0000,    // BNE +1 insn
12051            0xE7F0_00F0,    // UDF #0
12052        ];
12053        assert!(
12054            words.windows(6).any(|w| w == guard),
12055            "A32 I64DivS carries the INT64_MIN/-1 overflow guard"
12056        );
12057        let rems = encoder
12058            .encode(&ArmOp::I64RemS {
12059                rdlo: Reg::R4,
12060                rdhi: Reg::R5,
12061                rnlo: Reg::R0,
12062                rnhi: Reg::R1,
12063                rmlo: Reg::R2,
12064                rmhi: Reg::R3,
12065                elide_zero_guard: false,
12066            })
12067            .unwrap();
12068        let rems_udfs = rems
12069            .chunks(4)
12070            .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
12071            .count();
12072        assert_eq!(rems_udfs, 1, "A32 I64RemS keeps only the zero-divisor trap");
12073    }
12074
12075    #[test]
12076    fn test_encode_nop_thumb2() {
12077        let encoder = ArmEncoder::new_thumb2();
12078        let op = ArmOp::Nop;
12079        let code = encoder.encode(&op).unwrap();
12080        assert_eq!(code.len(), 2); // 16-bit
12081
12082        // NOP: 0xBF00 in little-endian
12083        assert_eq!(code, vec![0x00, 0xBF]);
12084    }
12085
12086    // =========================================================================
12087    // i64 Thumb-2 encoding tests
12088    // =========================================================================
12089
12090    #[test]
12091    fn test_encode_i64_add_thumb2() {
12092        let encoder = ArmEncoder::new_thumb2();
12093        let op = ArmOp::I64Add {
12094            rdlo: Reg::R0,
12095            rdhi: Reg::R1,
12096            rnlo: Reg::R0,
12097            rnhi: Reg::R1,
12098            rmlo: Reg::R2,
12099            rmhi: Reg::R3,
12100        };
12101        let code = encoder.encode(&op).unwrap();
12102        // Should emit ADDS (2 bytes) + ADC.W (4 bytes) = 6 bytes
12103        assert_eq!(code.len(), 6, "I64Add should be 6 bytes (ADDS + ADC.W)");
12104    }
12105
12106    #[test]
12107    fn test_encode_i64_sub_thumb2() {
12108        let encoder = ArmEncoder::new_thumb2();
12109        let op = ArmOp::I64Sub {
12110            rdlo: Reg::R0,
12111            rdhi: Reg::R1,
12112            rnlo: Reg::R0,
12113            rnhi: Reg::R1,
12114            rmlo: Reg::R2,
12115            rmhi: Reg::R3,
12116        };
12117        let code = encoder.encode(&op).unwrap();
12118        // Should emit SUBS (2 bytes) + SBC.W (4 bytes) = 6 bytes
12119        assert_eq!(code.len(), 6, "I64Sub should be 6 bytes (SUBS + SBC.W)");
12120    }
12121
12122    #[test]
12123    fn test_encode_i64_and_thumb2() {
12124        let encoder = ArmEncoder::new_thumb2();
12125        let op = ArmOp::I64And {
12126            rdlo: Reg::R0,
12127            rdhi: Reg::R1,
12128            rnlo: Reg::R0,
12129            rnhi: Reg::R1,
12130            rmlo: Reg::R2,
12131            rmhi: Reg::R3,
12132        };
12133        let code = encoder.encode(&op).unwrap();
12134        // AND.W (4 bytes) + AND.W (4 bytes) = 8 bytes
12135        assert!(code.len() >= 4, "I64And should emit at least 4 bytes");
12136    }
12137
12138    #[test]
12139    fn test_encode_i64_or_thumb2() {
12140        let encoder = ArmEncoder::new_thumb2();
12141        let op = ArmOp::I64Or {
12142            rdlo: Reg::R0,
12143            rdhi: Reg::R1,
12144            rnlo: Reg::R0,
12145            rnhi: Reg::R1,
12146            rmlo: Reg::R2,
12147            rmhi: Reg::R3,
12148        };
12149        let code = encoder.encode(&op).unwrap();
12150        assert!(code.len() >= 4, "I64Or should emit at least 4 bytes");
12151    }
12152
12153    #[test]
12154    fn test_encode_i64_xor_thumb2() {
12155        let encoder = ArmEncoder::new_thumb2();
12156        let op = ArmOp::I64Xor {
12157            rdlo: Reg::R0,
12158            rdhi: Reg::R1,
12159            rnlo: Reg::R0,
12160            rnhi: Reg::R1,
12161            rmlo: Reg::R2,
12162            rmhi: Reg::R3,
12163        };
12164        let code = encoder.encode(&op).unwrap();
12165        assert!(code.len() >= 4, "I64Xor should emit at least 4 bytes");
12166    }
12167
12168    #[test]
12169    fn test_encode_i64_const_small_thumb2() {
12170        let encoder = ArmEncoder::new_thumb2();
12171        // Small constant: only needs MOVW for each half
12172        let op = ArmOp::I64Const {
12173            rdlo: Reg::R0,
12174            rdhi: Reg::R1,
12175            value: 42,
12176        };
12177        let code = encoder.encode(&op).unwrap();
12178        // MOVW R0, #42 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes minimum
12179        assert!(code.len() >= 8, "I64Const should emit at least 8 bytes");
12180    }
12181
12182    #[test]
12183    fn test_encode_i64_const_large_thumb2() {
12184        let encoder = ArmEncoder::new_thumb2();
12185        // Large constant: needs MOVW+MOVT for each half
12186        let op = ArmOp::I64Const {
12187            rdlo: Reg::R0,
12188            rdhi: Reg::R1,
12189            value: 0x1234_5678_9ABC_DEF0_u64 as i64,
12190        };
12191        let code = encoder.encode(&op).unwrap();
12192        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
12193        assert_eq!(
12194            code.len(),
12195            16,
12196            "I64Const with large value should be 16 bytes"
12197        );
12198    }
12199
12200    #[test]
12201    fn test_encode_i64_extend_i32_s_thumb2() {
12202        let encoder = ArmEncoder::new_thumb2();
12203        let op = ArmOp::I64ExtendI32S {
12204            rdlo: Reg::R0,
12205            rdhi: Reg::R1,
12206            rn: Reg::R0,
12207        };
12208        let code = encoder.encode(&op).unwrap();
12209        // When rdlo == rn, only ASR (4 bytes) is emitted
12210        assert_eq!(
12211            code.len(),
12212            4,
12213            "I64ExtendI32S (same reg) should be 4 bytes (ASR only)"
12214        );
12215    }
12216
12217    #[test]
12218    fn test_encode_i64_extend_i32_s_diff_reg_thumb2() {
12219        let encoder = ArmEncoder::new_thumb2();
12220        let op = ArmOp::I64ExtendI32S {
12221            rdlo: Reg::R0,
12222            rdhi: Reg::R1,
12223            rn: Reg::R2,
12224        };
12225        let code = encoder.encode(&op).unwrap();
12226        // MOV rdlo, rn (2 bytes for low regs) + ASR rdhi, rdlo, #31 (4 bytes) = 6 bytes
12227        assert!(
12228            code.len() >= 6,
12229            "I64ExtendI32S (diff reg) should be at least 6 bytes"
12230        );
12231    }
12232
12233    #[test]
12234    fn test_encode_i64_extend_i32_u_thumb2() {
12235        let encoder = ArmEncoder::new_thumb2();
12236        let op = ArmOp::I64ExtendI32U {
12237            rdlo: Reg::R0,
12238            rdhi: Reg::R1,
12239            rn: Reg::R0,
12240        };
12241        let code = encoder.encode(&op).unwrap();
12242        // When rdlo == rn, only MOV rdhi, #0 (2 bytes) is emitted
12243        assert_eq!(
12244            code.len(),
12245            2,
12246            "I64ExtendI32U (same reg) should be 2 bytes (MOV #0 only)"
12247        );
12248    }
12249
12250    #[test]
12251    fn test_encode_i32_wrap_i64_nop_thumb2() {
12252        let encoder = ArmEncoder::new_thumb2();
12253        // When rd == rnlo, should be a NOP
12254        let op = ArmOp::I32WrapI64 {
12255            rd: Reg::R0,
12256            rnlo: Reg::R0,
12257        };
12258        let code = encoder.encode(&op).unwrap();
12259        assert_eq!(code.len(), 2, "I32WrapI64 same reg should be NOP (2 bytes)");
12260        assert_eq!(code, vec![0x00, 0xBF]); // NOP
12261    }
12262
12263    #[test]
12264    fn test_encode_i32_wrap_i64_diff_reg_thumb2() {
12265        let encoder = ArmEncoder::new_thumb2();
12266        let op = ArmOp::I32WrapI64 {
12267            rd: Reg::R2,
12268            rnlo: Reg::R0,
12269        };
12270        let code = encoder.encode(&op).unwrap();
12271        // MOV R2, R0 (2 or 4 bytes)
12272        assert!(
12273            code.len() >= 2,
12274            "I32WrapI64 diff reg should emit at least 2 bytes"
12275        );
12276    }
12277
12278    #[test]
12279    fn test_encode_i64_eqz_thumb2() {
12280        let encoder = ArmEncoder::new_thumb2();
12281        let op = ArmOp::I64Eqz {
12282            rd: Reg::R0,
12283            rnlo: Reg::R0,
12284            rnhi: Reg::R1,
12285        };
12286        let code = encoder.encode(&op).unwrap();
12287        // Delegates to I64SetCondZ which is already encoded
12288        assert!(
12289            code.len() >= 6,
12290            "I64Eqz should emit at least 6 bytes for ORR+ITE+MOV+MOV"
12291        );
12292    }
12293
12294    #[test]
12295    fn test_encode_i64_eq_thumb2() {
12296        let encoder = ArmEncoder::new_thumb2();
12297        let op = ArmOp::I64Eq {
12298            rd: Reg::R0,
12299            rnlo: Reg::R0,
12300            rnhi: Reg::R1,
12301            rmlo: Reg::R2,
12302            rmhi: Reg::R3,
12303        };
12304        let code = encoder.encode(&op).unwrap();
12305        // Delegates to I64SetCond EQ: CMP lo + IT EQ + CMPEQ hi + ITE EQ + MOV 1 + MOV 0
12306        assert!(code.len() >= 10, "I64Eq should emit at least 10 bytes");
12307    }
12308
12309    #[test]
12310    fn test_encode_i64_ldr_thumb2() {
12311        let encoder = ArmEncoder::new_thumb2();
12312        let op = ArmOp::I64Ldr {
12313            rdlo: Reg::R0,
12314            rdhi: Reg::R1,
12315            addr: MemAddr::imm(Reg::SP, 0),
12316        };
12317        let code = encoder.encode(&op).unwrap();
12318        // Two LDR instructions (lo at offset, hi at offset+4)
12319        assert!(code.len() >= 4, "I64Ldr should emit at least 4 bytes");
12320    }
12321
12322    #[test]
12323    fn test_372_i64_ldr_indexed_materializes_address() {
12324        // #372: a memory i64.load carries an index register (R11 + addr + off).
12325        // The encoder must materialize `ip = base + index` (ADD.W) and load via
12326        // `[ip,#off]` — NOT drop the index. A frame (non-indexed) i64.load must
12327        // stay byte-identical (plain `[base,#off]`, no ADD).
12328        let encoder = ArmEncoder::new_thumb2();
12329        let indexed = encoder
12330            .encode(&ArmOp::I64Ldr {
12331                rdlo: Reg::R0,
12332                rdhi: Reg::R1,
12333                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
12334            })
12335            .unwrap();
12336        // ADD.W ip, fp, r0 = eb0b 0c00 (byte-verified vs arm-none-eabi-as).
12337        assert_eq!(
12338            &indexed[0..4],
12339            &[0x0b, 0xeb, 0x00, 0x0c],
12340            "indexed I64Ldr must start with ADD.W ip, base, index"
12341        );
12342        let frame = encoder
12343            .encode(&ArmOp::I64Ldr {
12344                rdlo: Reg::R0,
12345                rdhi: Reg::R1,
12346                addr: MemAddr::imm(Reg::SP, 8),
12347            })
12348            .unwrap();
12349        // No index -> no ADD.W prefix (byte-identical frame access).
12350        assert_ne!(
12351            &frame[0..2],
12352            &[0x0b, 0xeb],
12353            "frame (non-indexed) I64Ldr must NOT emit an ADD.W"
12354        );
12355    }
12356
12357    #[test]
12358    fn test_382_i64_ldst_large_offset_materializes_not_skips() {
12359        // #382: an indexed i64.load/store whose static offset > 0xFFF must
12360        // MATERIALIZE the offset into the base — NOT return Err (skip the fn).
12361        // Sequence for reg_imm(R11, R0, 5000): MOVW ip,#5000 ; ADD ip,r0,ip ;
12362        // ADD ip,ip,fp ; LDR/STR halves at [ip,#0] / [ip,#4]. Byte-verified tail
12363        // vs arm-none-eabi-as.
12364        let encoder = ArmEncoder::new_thumb2();
12365        // 0x1388 > 0xFFF (MemAddr is not Copy, so build it per use).
12366
12367        let ld = encoder
12368            .encode(&ArmOp::I64Ldr {
12369                rdlo: Reg::R0,
12370                rdhi: Reg::R1,
12371                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
12372            })
12373            .expect("large-offset i64.load must lower, not skip");
12374        // MOVW ip,#0x1388 (4) + ADD ip,r0,ip (4) + ADD ip,ip,fp (4) + 2 LDR (8).
12375        assert_eq!(ld.len(), 20, "expected MOVW + 2×ADD + 2×LDR");
12376        // Must NOT be the small-offset `ADD.W ip, fp, r0` (0x0b 0xeb) prefix —
12377        // that path can only reach imm12 offsets.
12378        assert_ne!(
12379            &ld[0..2],
12380            &[0x0b, 0xeb],
12381            "must materialize the large offset"
12382        );
12383        // Effective base built in ip, then halves at [ip,#0] / [ip,#4].
12384        assert_eq!(
12385            &ld[4..20],
12386            &[
12387                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
12388                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
12389                0xdc, 0xf8, 0x00, 0x00, // LDR.W r0, [ip, #0]
12390                0xdc, 0xf8, 0x04, 0x10, // LDR.W r1, [ip, #4]
12391            ],
12392            "large-offset i64.load must fold offset into ip and access [ip,#0]/[ip,#4]"
12393        );
12394
12395        // Store: same base materialization, STR halves.
12396        let st = encoder
12397            .encode(&ArmOp::I64Str {
12398                rdlo: Reg::R2,
12399                rdhi: Reg::R3,
12400                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
12401            })
12402            .expect("large-offset i64.store must lower, not skip");
12403        assert_eq!(st.len(), 20);
12404        assert_eq!(
12405            &st[4..20],
12406            &[
12407                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
12408                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
12409                0xcc, 0xf8, 0x00, 0x20, // STR.W r2, [ip, #0]
12410                0xcc, 0xf8, 0x04, 0x30, // STR.W r3, [ip, #4]
12411            ],
12412            "large-offset i64.store must fold offset into ip and access [ip,#0]/[ip,#4]"
12413        );
12414
12415        // Small-offset (imm12) indexed access stays byte-identical (#372): the
12416        // effective base is a single `ADD.W ip, fp, r0` and the halves keep the
12417        // folded immediates — NO extra MOVW/ADD.
12418        let small = encoder
12419            .encode(&ArmOp::I64Ldr {
12420                rdlo: Reg::R0,
12421                rdhi: Reg::R1,
12422                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 8),
12423            })
12424            .unwrap();
12425        assert_eq!(
12426            &small[0..4],
12427            &[0x0b, 0xeb, 0x00, 0x0c],
12428            "small-offset indexed i64 must keep the single ADD.W ip, fp, r0"
12429        );
12430        assert_eq!(small.len(), 12, "ADD.W + 2×LDR.W (offset folded in imm12)");
12431    }
12432
12433    /// RQ-63-ARMI64OFF (#1165): A32 `I64Ldr`/`I64Str` at and past the pair
12434    /// form's immediate boundary. Bytes verified against `arm-none-eabi-as`
12435    /// (`.arch armv7-r`, `.arm`). 4091 is the last offset the pair folds (the
12436    /// high half sits at 4095 = imm12 max); 4092 is the straddle the v0.62
12437    /// census declined on (low half fits, high half does not).
12438    #[test]
12439    fn test_1165_a32_i64_ldst_offset_boundary_materializes() {
12440        let enc = ArmEncoder::new_arm32();
12441        let le = |ws: &[u32]| ws.iter().flat_map(|w| w.to_le_bytes()).collect::<Vec<u8>>();
12442        let ld = |off: i32| {
12443            enc.encode(&ArmOp::I64Ldr {
12444                rdlo: Reg::R0,
12445                rdhi: Reg::R1,
12446                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, off),
12447            })
12448        };
12449        // AT the boundary (4091): folded, byte-identical to the pre-#1165 form.
12450        assert_eq!(
12451            ld(4091).unwrap(),
12452            le(&[0xE08BC000, 0xE59C0FFB, 0xE59C1FFF]),
12453            "4091: add ip,fp,r0 ; ldr r0,[ip,#4091] ; ldr r1,[ip,#4095]"
12454        );
12455        // ONE ABOVE (4092): the high half would need #4096 → materialize.
12456        assert_eq!(
12457            ld(4092).unwrap(),
12458            le(&[0xE300CFFC, 0xE08CC00B, 0xE08CC000, 0xE59C0000, 0xE59C1004]),
12459            "4092: movw ip,#4092 ; add ip,ip,fp ; add ip,ip,r0 ; ldr r0,[ip] ; ldr r1,[ip,#4]"
12460        );
12461        // 4095 (low half at the imm12 max), 4096 (both out), 5000.
12462        assert_eq!(ld(4095).unwrap()[..4], le(&[0xE300CFFF])[..]);
12463        assert_eq!(ld(4096).unwrap()[..4], le(&[0xE301C000])[..]);
12464        assert_eq!(
12465            ld(5000).unwrap(),
12466            le(&[0xE301C388, 0xE08CC00B, 0xE08CC000, 0xE59C0000, 0xE59C1004])
12467        );
12468        // 70000 needs MOVT; store form.
12469        assert_eq!(
12470            enc.encode(&ArmOp::I64Str {
12471                rdlo: Reg::R2,
12472                rdhi: Reg::R3,
12473                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 70000),
12474            })
12475            .unwrap(),
12476            le(&[
12477                0xE301C170, 0xE340C001, 0xE08CC00B, 0xE08CC000, 0xE58C2000, 0xE58C3004
12478            ]),
12479            "70000: movw ip,#0x1170 ; movt ip,#1 ; add ip,ip,fp ; add ip,ip,r0 ; str r2,[ip] ; str r3,[ip,#4]"
12480        );
12481        // A memarg >= 2^31 arrives negative (0xFFFFFFF8 as i32 = -8): the full
12482        // 32 bits are materialized, mod 2^32 — never clamped, never masked.
12483        assert_eq!(
12484            ld(-8).unwrap(),
12485            le(&[
12486                0xE30FCFF8, 0xE34FCFFF, 0xE08CC00B, 0xE08CC000, 0xE59C0000, 0xE59C1004
12487            ]),
12488            "0xfffffff8: movw ip,#0xfff8 ; movt ip,#0xffff ; add ; add ; ldr ; ldr"
12489        );
12490        // No index register (frame-style base), out of range: MOVW ; ADD ip,ip,base.
12491        assert_eq!(
12492            enc.encode(&ArmOp::I64Ldr {
12493                rdlo: Reg::R0,
12494                rdhi: Reg::R1,
12495                addr: MemAddr::imm(Reg::SP, 5000),
12496            })
12497            .unwrap(),
12498            le(&[0xE301C388, 0xE08CC00D, 0xE59C0000, 0xE59C1004])
12499        );
12500        // The base IS the scratch: no free register → typed error, not a clobber.
12501        assert!(
12502            enc.encode(&ArmOp::I64Ldr {
12503                rdlo: Reg::R0,
12504                rdhi: Reg::R1,
12505                addr: MemAddr::imm(Reg::R12, 5000),
12506            })
12507            .is_err(),
12508            "materializing over an R12 base must be refused, not clobber it"
12509        );
12510    }
12511
12512    /// RQ-63-ARMI64OFF (#1165): the A32 WORD and SUB-WORD immediate arms used
12513    /// to MASK an out-of-range offset (`& 0xFFF` / `& 0xFF`) — a silent wrong
12514    /// address (`i32.load offset=5000` → `ldr r0,[ip,#904]`, `i32.load16_u
12515    /// offset=256` → `ldrh r0,[ip]`). At the field maximum the bytes are
12516    /// unchanged; one above, the offset is materialized. Bytes verified
12517    /// against `arm-none-eabi-as`.
12518    #[test]
12519    fn test_1165_a32_word_subword_offset_boundary_materializes_not_masks() {
12520        let enc = ArmEncoder::new_arm32();
12521        let le = |ws: &[u32]| ws.iter().flat_map(|w| w.to_le_bytes()).collect::<Vec<u8>>();
12522        let idx = |off: i32| MemAddr::reg_imm(Reg::R11, Reg::R0, off);
12523        let ldr = |addr| enc.encode(&ArmOp::Ldr { rd: Reg::R0, addr }).unwrap();
12524        let ldrh = |addr| enc.encode(&ArmOp::Ldrh { rd: Reg::R0, addr }).unwrap();
12525        let ldrsb = |addr| enc.encode(&ArmOp::Ldrsb { rd: Reg::R0, addr }).unwrap();
12526        let strh = |addr| enc.encode(&ArmOp::Strh { rd: Reg::R1, addr }).unwrap();
12527        let ldrb = |addr| enc.encode(&ArmOp::Ldrb { rd: Reg::R0, addr }).unwrap();
12528        // LDR imm12: 4095 at, 4096 above.
12529        assert_eq!(ldr(idx(4095)), le(&[0xE08BC000, 0xE59C0FFF]));
12530        assert_eq!(
12531            ldr(idx(4096)),
12532            le(&[0xE301C000, 0xE08CC00B, 0xE08CC000, 0xE59C0000]),
12533            "ldr 4096 must materialize, not mask to #0"
12534        );
12535        // LDRH imm8: 255 at, 256 above (the pre-fix bytes were `ldrh r0,[ip]`).
12536        assert_eq!(ldrh(idx(255)), le(&[0xE08BC000, 0xE1DC0FBF]));
12537        assert_eq!(
12538            ldrh(idx(256)),
12539            le(&[0xE300C100, 0xE08CC00B, 0xE08CC000, 0xE1DC00B0]),
12540            "ldrh 256 must materialize, not mask to #0"
12541        );
12542        // LDRSB imm8 (256 above), STRH imm8 (300 above).
12543        assert_eq!(
12544            ldrsb(idx(256)),
12545            le(&[0xE300C100, 0xE08CC00B, 0xE08CC000, 0xE1DC00D0])
12546        );
12547        assert_eq!(
12548            strh(idx(300)),
12549            le(&[0xE300C12C, 0xE08CC00B, 0xE08CC000, 0xE1CC10B0])
12550        );
12551        // LDRB is an imm12 form: 4095 at, 4096 above.
12552        assert_eq!(ldrb(idx(4095)), le(&[0xE08BC000, 0xE5DC0FFF]));
12553        assert_eq!(
12554            ldrb(idx(4096)),
12555            le(&[0xE301C000, 0xE08CC00B, 0xE08CC000, 0xE5DC0000])
12556        );
12557        // No index, out of range: MOVW ; ADD ip,ip,base ; access [ip].
12558        assert_eq!(
12559            ldr(MemAddr::imm(Reg::R0, 5000)),
12560            le(&[0xE301C388, 0xE08CC000, 0xE59C0000])
12561        );
12562        // The immediate arms are now a TRIPWIRE, never a mask.
12563        assert!(encode_mem_addr(&MemAddr::imm(Reg::R1, 0xFFF)).is_ok());
12564        assert!(
12565            encode_mem_addr(&MemAddr::imm(Reg::R1, 0x1000)).is_err(),
12566            "imm12 arm must refuse 0x1000, not mask it to #0"
12567        );
12568        assert!(encode_mem_addr_imm8(&MemAddr::imm(Reg::R1, 0xFF)).is_ok());
12569        assert!(
12570            encode_mem_addr_imm8(&MemAddr::imm(Reg::R1, 0x100)).is_err(),
12571            "imm8 arm must refuse 0x100, not mask it to #0"
12572        );
12573        // Base is the scratch: refused, not clobbered.
12574        assert!(
12575            enc.encode(&ArmOp::Ldr {
12576                rd: Reg::R0,
12577                addr: MemAddr::imm(Reg::R12, 5000),
12578            })
12579            .is_err()
12580        );
12581    }
12582
12583    /// RQ-63-ARMI64OFF (#1165): Thumb-2 `i64_effective_base` used to CLAMP a
12584    /// negative offset (a memarg >= 2^31 after the selector's `as i32` cast)
12585    /// to 0 — `i64.load offset=0xfffffff8` read `[R11+addr]`. It now
12586    /// materializes the full 32 bits (MOVW+MOVT), the arithmetic the i32 word
12587    /// path uses for the same memarg. Bytes verified against
12588    /// `arm-none-eabi-as` (`.arch armv7e-m`, `.thumb`). A FRAME access with a
12589    /// negative offset (no index register) is refused loudly by
12590    /// `check_ldst_imm12`, never clamped.
12591    #[test]
12592    fn test_1165_thumb2_i64_negative_offset_materializes_not_clamps() {
12593        let enc = ArmEncoder::new_thumb2();
12594        let ld = enc
12595            .encode(&ArmOp::I64Ldr {
12596                rdlo: Reg::R0,
12597                rdhi: Reg::R1,
12598                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, -8),
12599            })
12600            .unwrap();
12601        assert_eq!(
12602            ld,
12603            vec![
12604                0x4f, 0xf6, 0xf8, 0x7c, // movw ip, #0xfff8
12605                0xcf, 0xf6, 0xff, 0x7c, // movt ip, #0xffff
12606                0x00, 0xeb, 0x0c, 0x0c, // add.w ip, r0, ip
12607                0x0c, 0xeb, 0x0b, 0x0c, // add.w ip, ip, fp
12608                0xdc, 0xf8, 0x00, 0x00, // ldr.w r0, [ip]
12609                0xdc, 0xf8, 0x04, 0x10, // ldr.w r1, [ip, #4]
12610            ],
12611            "0xfffffff8 must be materialized in full, not clamped to [ip,#0]"
12612        );
12613        let st = enc
12614            .encode(&ArmOp::I64Str {
12615                rdlo: Reg::R2,
12616                rdhi: Reg::R3,
12617                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, i32::MIN),
12618            })
12619            .unwrap();
12620        assert_eq!(
12621            st,
12622            vec![
12623                0x40, 0xf2, 0x00, 0x0c, // movw ip, #0
12624                0xc8, 0xf2, 0x00, 0x0c, // movt ip, #0x8000
12625                0x00, 0xeb, 0x0c, 0x0c, // add.w ip, r0, ip
12626                0x0c, 0xeb, 0x0b, 0x0c, // add.w ip, ip, fp
12627                0xcc, 0xf8, 0x00, 0x20, // str.w r2, [ip]
12628                0xcc, 0xf8, 0x04, 0x30, // str.w r3, [ip, #4]
12629            ]
12630        );
12631        assert!(
12632            enc.encode(&ArmOp::I64Ldr {
12633                rdlo: Reg::R0,
12634                rdhi: Reg::R1,
12635                addr: MemAddr::imm(Reg::SP, -8),
12636            })
12637            .is_err(),
12638            "a negative frame offset must be refused, not clamped to [sp,#0]"
12639        );
12640    }
12641
12642    #[test]
12643    fn test_encode_i64_str_thumb2() {
12644        let encoder = ArmEncoder::new_thumb2();
12645        let op = ArmOp::I64Str {
12646            rdlo: Reg::R0,
12647            rdhi: Reg::R1,
12648            addr: MemAddr::imm(Reg::SP, 0),
12649        };
12650        let code = encoder.encode(&op).unwrap();
12651        // Two STR instructions (lo at offset, hi at offset+4)
12652        assert!(code.len() >= 4, "I64Str should emit at least 4 bytes");
12653    }
12654
12655    #[test]
12656    fn test_encode_i64_all_comparisons_thumb2() {
12657        let encoder = ArmEncoder::new_thumb2();
12658
12659        let ops = vec![
12660            ArmOp::I64Ne {
12661                rd: Reg::R0,
12662                rnlo: Reg::R0,
12663                rnhi: Reg::R1,
12664                rmlo: Reg::R2,
12665                rmhi: Reg::R3,
12666            },
12667            ArmOp::I64LtS {
12668                rd: Reg::R0,
12669                rnlo: Reg::R0,
12670                rnhi: Reg::R1,
12671                rmlo: Reg::R2,
12672                rmhi: Reg::R3,
12673            },
12674            ArmOp::I64LtU {
12675                rd: Reg::R0,
12676                rnlo: Reg::R0,
12677                rnhi: Reg::R1,
12678                rmlo: Reg::R2,
12679                rmhi: Reg::R3,
12680            },
12681            ArmOp::I64LeS {
12682                rd: Reg::R0,
12683                rnlo: Reg::R0,
12684                rnhi: Reg::R1,
12685                rmlo: Reg::R2,
12686                rmhi: Reg::R3,
12687            },
12688            ArmOp::I64LeU {
12689                rd: Reg::R0,
12690                rnlo: Reg::R0,
12691                rnhi: Reg::R1,
12692                rmlo: Reg::R2,
12693                rmhi: Reg::R3,
12694            },
12695            ArmOp::I64GtS {
12696                rd: Reg::R0,
12697                rnlo: Reg::R0,
12698                rnhi: Reg::R1,
12699                rmlo: Reg::R2,
12700                rmhi: Reg::R3,
12701            },
12702            ArmOp::I64GtU {
12703                rd: Reg::R0,
12704                rnlo: Reg::R0,
12705                rnhi: Reg::R1,
12706                rmlo: Reg::R2,
12707                rmhi: Reg::R3,
12708            },
12709            ArmOp::I64GeS {
12710                rd: Reg::R0,
12711                rnlo: Reg::R0,
12712                rnhi: Reg::R1,
12713                rmlo: Reg::R2,
12714                rmhi: Reg::R3,
12715            },
12716            ArmOp::I64GeU {
12717                rd: Reg::R0,
12718                rnlo: Reg::R0,
12719                rnhi: Reg::R1,
12720                rmlo: Reg::R2,
12721                rmhi: Reg::R3,
12722            },
12723        ];
12724
12725        for op in &ops {
12726            let code = encoder.encode(op).unwrap();
12727            assert!(
12728                code.len() >= 8,
12729                "i64 comparison {:?} should emit at least 8 bytes, got {}",
12730                op,
12731                code.len()
12732            );
12733        }
12734    }
12735
12736    #[test]
12737    fn test_encode_i64_const_zero_thumb2() {
12738        let encoder = ArmEncoder::new_thumb2();
12739        let op = ArmOp::I64Const {
12740            rdlo: Reg::R0,
12741            rdhi: Reg::R1,
12742            value: 0,
12743        };
12744        let code = encoder.encode(&op).unwrap();
12745        // MOVW R0, #0 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes
12746        assert_eq!(code.len(), 8, "I64Const(0) should be 8 bytes");
12747    }
12748
12749    #[test]
12750    fn test_encode_i64_const_negative_one_thumb2() {
12751        let encoder = ArmEncoder::new_thumb2();
12752        let op = ArmOp::I64Const {
12753            rdlo: Reg::R0,
12754            rdhi: Reg::R1,
12755            value: -1, // 0xFFFF_FFFF_FFFF_FFFF
12756        };
12757        let code = encoder.encode(&op).unwrap();
12758        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
12759        assert_eq!(code.len(), 16, "I64Const(-1) should be 16 bytes");
12760    }
12761
12762    // =========================================================================
12763    // Sub-word load/store encoding tests
12764    // =========================================================================
12765
12766    #[test]
12767    fn test_encode_ldrb_arm32() {
12768        let encoder = ArmEncoder::new_arm32();
12769        let op = ArmOp::Ldrb {
12770            rd: Reg::R0,
12771            addr: MemAddr::imm(Reg::R1, 4),
12772        };
12773        let code = encoder.encode(&op).unwrap();
12774        assert_eq!(code.len(), 4, "ARM32 LDRB should be 4 bytes");
12775        // LDRB R0, [R1, #4] = 0xE5D10004
12776        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12777        assert_eq!(encoded, 0xE5D10004, "Should encode LDRB R0, [R1, #4]");
12778    }
12779
12780    #[test]
12781    fn test_encode_strb_arm32() {
12782        let encoder = ArmEncoder::new_arm32();
12783        let op = ArmOp::Strb {
12784            rd: Reg::R0,
12785            addr: MemAddr::imm(Reg::R1, 0),
12786        };
12787        let code = encoder.encode(&op).unwrap();
12788        assert_eq!(code.len(), 4, "ARM32 STRB should be 4 bytes");
12789        // STRB R0, [R1, #0] = 0xE5C10000
12790        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12791        assert_eq!(encoded, 0xE5C10000, "Should encode STRB R0, [R1, #0]");
12792    }
12793
12794    #[test]
12795    fn test_encode_ldrh_arm32() {
12796        let encoder = ArmEncoder::new_arm32();
12797        let op = ArmOp::Ldrh {
12798            rd: Reg::R0,
12799            addr: MemAddr::imm(Reg::R1, 2),
12800        };
12801        let code = encoder.encode(&op).unwrap();
12802        assert_eq!(code.len(), 4, "ARM32 LDRH should be 4 bytes");
12803    }
12804
12805    #[test]
12806    fn test_encode_strh_arm32() {
12807        let encoder = ArmEncoder::new_arm32();
12808        let op = ArmOp::Strh {
12809            rd: Reg::R0,
12810            addr: MemAddr::imm(Reg::R1, 0),
12811        };
12812        let code = encoder.encode(&op).unwrap();
12813        assert_eq!(code.len(), 4, "ARM32 STRH should be 4 bytes");
12814    }
12815
12816    #[test]
12817    fn test_encode_ldrsb_arm32() {
12818        let encoder = ArmEncoder::new_arm32();
12819        let op = ArmOp::Ldrsb {
12820            rd: Reg::R0,
12821            addr: MemAddr::imm(Reg::R1, 0),
12822        };
12823        let code = encoder.encode(&op).unwrap();
12824        assert_eq!(code.len(), 4, "ARM32 LDRSB should be 4 bytes");
12825    }
12826
12827    #[test]
12828    fn test_encode_ldrsh_arm32() {
12829        let encoder = ArmEncoder::new_arm32();
12830        let op = ArmOp::Ldrsh {
12831            rd: Reg::R0,
12832            addr: MemAddr::imm(Reg::R1, 0),
12833        };
12834        let code = encoder.encode(&op).unwrap();
12835        assert_eq!(code.len(), 4, "ARM32 LDRSH should be 4 bytes");
12836    }
12837
12838    #[test]
12839    fn test_encode_ldrb_thumb2_16bit() {
12840        let encoder = ArmEncoder::new_thumb2();
12841        let op = ArmOp::Ldrb {
12842            rd: Reg::R0,
12843            addr: MemAddr::imm(Reg::R1, 4),
12844        };
12845        let code = encoder.encode(&op).unwrap();
12846        // Low registers + small offset -> 16-bit encoding
12847        assert_eq!(
12848            code.len(),
12849            2,
12850            "Thumb-2 LDRB with small offset should be 16-bit"
12851        );
12852    }
12853
12854    #[test]
12855    fn test_encode_ldrb_thumb2_32bit() {
12856        let encoder = ArmEncoder::new_thumb2();
12857        let op = ArmOp::Ldrb {
12858            rd: Reg::R0,
12859            addr: MemAddr::imm(Reg::R1, 100), // offset > 31 needs 32-bit
12860        };
12861        let code = encoder.encode(&op).unwrap();
12862        assert_eq!(
12863            code.len(),
12864            4,
12865            "Thumb-2 LDRB with large offset should be 32-bit"
12866        );
12867    }
12868
12869    #[test]
12870    fn test_encode_strb_thumb2_16bit() {
12871        let encoder = ArmEncoder::new_thumb2();
12872        let op = ArmOp::Strb {
12873            rd: Reg::R0,
12874            addr: MemAddr::imm(Reg::R1, 10),
12875        };
12876        let code = encoder.encode(&op).unwrap();
12877        assert_eq!(
12878            code.len(),
12879            2,
12880            "Thumb-2 STRB with small offset should be 16-bit"
12881        );
12882    }
12883
12884    #[test]
12885    fn test_encode_ldrh_thumb2_16bit() {
12886        let encoder = ArmEncoder::new_thumb2();
12887        let op = ArmOp::Ldrh {
12888            rd: Reg::R0,
12889            addr: MemAddr::imm(Reg::R1, 4), // offset aligned to 2, <= 62
12890        };
12891        let code = encoder.encode(&op).unwrap();
12892        assert_eq!(
12893            code.len(),
12894            2,
12895            "Thumb-2 LDRH with small aligned offset should be 16-bit"
12896        );
12897    }
12898
12899    #[test]
12900    fn test_encode_strh_thumb2_16bit() {
12901        let encoder = ArmEncoder::new_thumb2();
12902        let op = ArmOp::Strh {
12903            rd: Reg::R0,
12904            addr: MemAddr::imm(Reg::R1, 4),
12905        };
12906        let code = encoder.encode(&op).unwrap();
12907        assert_eq!(
12908            code.len(),
12909            2,
12910            "Thumb-2 STRH with small aligned offset should be 16-bit"
12911        );
12912    }
12913
12914    #[test]
12915    fn test_encode_ldrsb_thumb2() {
12916        let encoder = ArmEncoder::new_thumb2();
12917        let op = ArmOp::Ldrsb {
12918            rd: Reg::R0,
12919            addr: MemAddr::imm(Reg::R1, 0),
12920        };
12921        let code = encoder.encode(&op).unwrap();
12922        // LDRSB has no 16-bit immediate form, always 32-bit
12923        assert_eq!(code.len(), 4, "Thumb-2 LDRSB should be 32-bit");
12924    }
12925
12926    #[test]
12927    fn test_encode_ldrsh_thumb2() {
12928        let encoder = ArmEncoder::new_thumb2();
12929        let op = ArmOp::Ldrsh {
12930            rd: Reg::R0,
12931            addr: MemAddr::imm(Reg::R1, 0),
12932        };
12933        let code = encoder.encode(&op).unwrap();
12934        assert_eq!(code.len(), 4, "Thumb-2 LDRSH should be 32-bit");
12935    }
12936
12937    #[test]
12938    fn test_encode_memory_size_thumb2() {
12939        // #1441: EXACT BYTES, both page sizes. The previous assertion was
12940        // `!code.is_empty()`, which passes for ANY encoding — including the
12941        // shift-of-32 this lane's whole risk is about.
12942        let encoder = ArmEncoder::new_thumb2();
12943
12944        // Default 64 KiB page: LSR.W R0, R10, #16.
12945        // 1110 1010 010 0 1111 | 0 imm3=100 Rd=0000 imm2=00 01 Rm=1010
12946        let dflt = encoder
12947            .encode(&ArmOp::MemorySize {
12948                rd: Reg::R0,
12949                page_log2: 16,
12950            })
12951            .unwrap();
12952        assert_eq!(
12953            dflt,
12954            vec![0x4F, 0xEA, 0x1A, 0x40],
12955            "the DEFAULT page must stay BYTE-IDENTICAL to the pre-#1441 \
12956             encoding — every frozen fixture depends on it"
12957        );
12958
12959        // `(pagesize 1)`: the byte count IS the page count, so MOV.W R0, R10.
12960        // NOT an LSR with a zero immediate, which encodes a shift BY 32 and
12961        // would make `memory.size` return 0 for every byte-paged memory.
12962        let ps1 = encoder
12963            .encode(&ArmOp::MemorySize {
12964                rd: Reg::R0,
12965                page_log2: 0,
12966            })
12967            .unwrap();
12968        assert_eq!(
12969            ps1,
12970            vec![0x4F, 0xEA, 0x0A, 0x00],
12971            "(pagesize 1) must encode MOV.W R0, R10 — a shift of 0 is not an LSR"
12972        );
12973        assert_ne!(ps1, dflt, "the two page sizes must not emit the same word");
12974
12975        // An unencodable shift REFUSES rather than emitting a wrong word
12976        // (#180/#185: the encoder is Ok-or-Err, never silently wrong).
12977        assert!(
12978            encoder
12979                .encode(&ArmOp::MemorySize {
12980                    rd: Reg::R0,
12981                    page_log2: 32,
12982                })
12983                .is_err(),
12984            "page_log2 = 32 is not an encodable shift and must be refused"
12985        );
12986    }
12987
12988    #[test]
12989    fn test_encode_memory_grow_thumb2() {
12990        let encoder = ArmEncoder::new_thumb2();
12991        let op = ArmOp::MemoryGrow {
12992            rd: Reg::R0,
12993            rn: Reg::R0,
12994        };
12995        let code = encoder.encode(&op).unwrap();
12996        assert_eq!(code.len(), 4, "MemoryGrow (MVN) should be 32-bit Thumb-2");
12997    }
12998
12999    #[test]
13000    fn test_encode_subword_reg_offset_thumb2() {
13001        let encoder = ArmEncoder::new_thumb2();
13002
13003        // LDRB with register offset
13004        let op = ArmOp::Ldrb {
13005            rd: Reg::R0,
13006            addr: MemAddr::reg(Reg::R1, Reg::R2),
13007        };
13008        let code = encoder.encode(&op).unwrap();
13009        assert_eq!(
13010            code.len(),
13011            4,
13012            "Thumb-2 LDRB with reg offset should be 32-bit"
13013        );
13014
13015        // STRB with register offset
13016        let op = ArmOp::Strb {
13017            rd: Reg::R0,
13018            addr: MemAddr::reg(Reg::R1, Reg::R2),
13019        };
13020        let code = encoder.encode(&op).unwrap();
13021        assert_eq!(
13022            code.len(),
13023            4,
13024            "Thumb-2 STRB with reg offset should be 32-bit"
13025        );
13026
13027        // LDRH with register offset
13028        let op = ArmOp::Ldrh {
13029            rd: Reg::R0,
13030            addr: MemAddr::reg(Reg::R1, Reg::R2),
13031        };
13032        let code = encoder.encode(&op).unwrap();
13033        assert_eq!(
13034            code.len(),
13035            4,
13036            "Thumb-2 LDRH with reg offset should be 32-bit"
13037        );
13038
13039        // STRH with register offset
13040        let op = ArmOp::Strh {
13041            rd: Reg::R0,
13042            addr: MemAddr::reg(Reg::R1, Reg::R2),
13043        };
13044        let code = encoder.encode(&op).unwrap();
13045        assert_eq!(
13046            code.len(),
13047            4,
13048            "Thumb-2 STRH with reg offset should be 32-bit"
13049        );
13050    }
13051
13052    #[test]
13053    fn test_encode_subword_reg_imm_offset_thumb2() {
13054        let encoder = ArmEncoder::new_thumb2();
13055
13056        // LDRB with both register and immediate offset
13057        let op = ArmOp::Ldrb {
13058            rd: Reg::R0,
13059            addr: MemAddr::reg_imm(Reg::R1, Reg::R2, 4),
13060        };
13061        let code = encoder.encode(&op).unwrap();
13062        // ADD R12, R2, #4 (4 bytes) + LDRB R0, [R1, R12] (4 bytes) = 8 bytes
13063        assert_eq!(
13064            code.len(),
13065            8,
13066            "Thumb-2 LDRB with reg+imm offset should be 8 bytes"
13067        );
13068    }
13069
13070    // ========================================================================
13071    // Helium MVE encoding tests
13072    // ========================================================================
13073
13074    #[test]
13075    fn test_encode_mve_addi32_thumb2() {
13076        let encoder = ArmEncoder::new_thumb2();
13077        let op = ArmOp::MveAddI {
13078            qd: QReg::Q0,
13079            qn: QReg::Q1,
13080            qm: QReg::Q2,
13081            size: MveSize::S32,
13082        };
13083        let code = encoder.encode(&op).unwrap();
13084        assert_eq!(
13085            code.len(),
13086            4,
13087            "MVE VADD.I32 should be 4 bytes (Thumb-2 32-bit)"
13088        );
13089    }
13090
13091    #[test]
13092    fn test_encode_mve_subi16_thumb2() {
13093        let encoder = ArmEncoder::new_thumb2();
13094        let op = ArmOp::MveSubI {
13095            qd: QReg::Q0,
13096            qn: QReg::Q1,
13097            qm: QReg::Q2,
13098            size: MveSize::S16,
13099        };
13100        let code = encoder.encode(&op).unwrap();
13101        assert_eq!(code.len(), 4, "MVE VSUB.I16 should be 4 bytes");
13102    }
13103
13104    #[test]
13105    fn test_encode_mve_muli8_thumb2() {
13106        let encoder = ArmEncoder::new_thumb2();
13107        let op = ArmOp::MveMulI {
13108            qd: QReg::Q0,
13109            qn: QReg::Q1,
13110            qm: QReg::Q2,
13111            size: MveSize::S8,
13112        };
13113        let code = encoder.encode(&op).unwrap();
13114        assert_eq!(code.len(), 4, "MVE VMUL.I8 should be 4 bytes");
13115    }
13116
13117    #[test]
13118    fn test_encode_mve_bitwise_thumb2() {
13119        let encoder = ArmEncoder::new_thumb2();
13120
13121        let ops = vec![
13122            ArmOp::MveAnd {
13123                qd: QReg::Q0,
13124                qn: QReg::Q1,
13125                qm: QReg::Q2,
13126            },
13127            ArmOp::MveOrr {
13128                qd: QReg::Q0,
13129                qn: QReg::Q1,
13130                qm: QReg::Q2,
13131            },
13132            ArmOp::MveEor {
13133                qd: QReg::Q0,
13134                qn: QReg::Q1,
13135                qm: QReg::Q2,
13136            },
13137            ArmOp::MveBic {
13138                qd: QReg::Q0,
13139                qn: QReg::Q1,
13140                qm: QReg::Q2,
13141            },
13142        ];
13143        for op in ops {
13144            let code = encoder.encode(&op).unwrap();
13145            assert_eq!(code.len(), 4, "MVE bitwise op should be 4 bytes");
13146        }
13147    }
13148
13149    #[test]
13150    fn test_encode_mve_mvn_thumb2() {
13151        let encoder = ArmEncoder::new_thumb2();
13152        let op = ArmOp::MveMvn {
13153            qd: QReg::Q0,
13154            qm: QReg::Q1,
13155        };
13156        let code = encoder.encode(&op).unwrap();
13157        assert_eq!(code.len(), 4, "MVE VMVN should be 4 bytes");
13158    }
13159
13160    #[test]
13161    fn test_encode_mve_load_store_thumb2() {
13162        let encoder = ArmEncoder::new_thumb2();
13163
13164        let load = ArmOp::MveLoad {
13165            qd: QReg::Q0,
13166            addr: MemAddr::imm(Reg::R0, 16),
13167        };
13168        let code = encoder.encode(&load).unwrap();
13169        assert_eq!(code.len(), 4, "MVE VLDRW.32 should be 4 bytes");
13170
13171        let store = ArmOp::MveStore {
13172            qd: QReg::Q1,
13173            addr: MemAddr::imm(Reg::R1, 0),
13174        };
13175        let code = encoder.encode(&store).unwrap();
13176        assert_eq!(code.len(), 4, "MVE VSTRW.32 should be 4 bytes");
13177    }
13178
13179    #[test]
13180    fn test_encode_mve_const_thumb2() {
13181        let encoder = ArmEncoder::new_thumb2();
13182        let op = ArmOp::MveConst {
13183            qd: QReg::Q0,
13184            bytes: [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0],
13185        };
13186        let code = encoder.encode(&op).unwrap();
13187        // Should be 4 words of (MOVW R12 + VMOV Sn) = 4 * (4+4) = 32 bytes min
13188        // Some words with hi16=0 skip MOVT, so length varies
13189        assert!(
13190            code.len() >= 24,
13191            "MVE const should produce multiple instructions"
13192        );
13193    }
13194
13195    #[test]
13196    fn test_encode_mve_dup_thumb2() {
13197        let encoder = ArmEncoder::new_thumb2();
13198        let op = ArmOp::MveDup {
13199            qd: QReg::Q0,
13200            rn: Reg::R0,
13201            size: MveSize::S32,
13202        };
13203        let code = encoder.encode(&op).unwrap();
13204        assert_eq!(code.len(), 4, "MVE VDUP.32 should be 4 bytes");
13205    }
13206
13207    #[test]
13208    fn test_encode_mve_extract_lane_thumb2() {
13209        let encoder = ArmEncoder::new_thumb2();
13210        let op = ArmOp::MveExtractLane {
13211            rd: Reg::R0,
13212            qn: QReg::Q1,
13213            lane: 2,
13214            size: MveSize::S32,
13215        };
13216        let code = encoder.encode(&op).unwrap();
13217        assert_eq!(code.len(), 4, "MVE extract lane should be 4 bytes");
13218    }
13219
13220    #[test]
13221    fn test_encode_mve_insert_lane_thumb2() {
13222        let encoder = ArmEncoder::new_thumb2();
13223        let op = ArmOp::MveInsertLane {
13224            qd: QReg::Q0,
13225            rn: Reg::R1,
13226            lane: 3,
13227            size: MveSize::S32,
13228        };
13229        let code = encoder.encode(&op).unwrap();
13230        assert_eq!(code.len(), 4, "MVE insert lane should be 4 bytes");
13231    }
13232
13233    #[test]
13234    fn test_encode_mve_addf32_thumb2() {
13235        let encoder = ArmEncoder::new_thumb2();
13236        let op = ArmOp::MveAddF32 {
13237            qd: QReg::Q0,
13238            qn: QReg::Q1,
13239            qm: QReg::Q2,
13240        };
13241        let code = encoder.encode(&op).unwrap();
13242        assert_eq!(code.len(), 4, "MVE VADD.F32 should be 4 bytes");
13243    }
13244
13245    #[test]
13246    fn test_encode_mve_divf32_thumb2() {
13247        let encoder = ArmEncoder::new_thumb2();
13248        let op = ArmOp::MveDivF32 {
13249            qd: QReg::Q0,
13250            qn: QReg::Q1,
13251            qm: QReg::Q2,
13252        };
13253        let code = encoder.encode(&op).unwrap();
13254        // Lane-wise: 4 x VDIV.F32 = 4 x 4 = 16 bytes
13255        assert_eq!(
13256            code.len(),
13257            16,
13258            "MVE VDIV.F32 (lane-wise) should be 16 bytes"
13259        );
13260    }
13261
13262    #[test]
13263    fn test_encode_mve_sqrtf32_thumb2() {
13264        let encoder = ArmEncoder::new_thumb2();
13265        let op = ArmOp::MveSqrtF32 {
13266            qd: QReg::Q0,
13267            qm: QReg::Q1,
13268        };
13269        let code = encoder.encode(&op).unwrap();
13270        // Lane-wise: 4 x VSQRT.F32 = 4 x 4 = 16 bytes
13271        assert_eq!(
13272            code.len(),
13273            16,
13274            "MVE VSQRT.F32 (lane-wise) should be 16 bytes"
13275        );
13276    }
13277
13278    #[test]
13279    fn test_encode_mve_negf32_thumb2() {
13280        let encoder = ArmEncoder::new_thumb2();
13281        let op = ArmOp::MveNegF32 {
13282            qd: QReg::Q0,
13283            qm: QReg::Q1,
13284        };
13285        let code = encoder.encode(&op).unwrap();
13286        assert_eq!(code.len(), 4, "MVE VNEG.F32 should be 4 bytes");
13287    }
13288
13289    #[test]
13290    fn test_encode_mve_absf32_thumb2() {
13291        let encoder = ArmEncoder::new_thumb2();
13292        let op = ArmOp::MveAbsF32 {
13293            qd: QReg::Q0,
13294            qm: QReg::Q1,
13295        };
13296        let code = encoder.encode(&op).unwrap();
13297        assert_eq!(code.len(), 4, "MVE VABS.F32 should be 4 bytes");
13298    }
13299
13300    /// VCR-RA-001 / immediate-folding precondition: pins the Thumb-2 `AND`
13301    /// immediate encoding for the byte range and documents its bound.
13302    ///
13303    /// The `And { Operand2::Imm }` encoder packs the low 12 bits straight into
13304    /// the `i:imm3:imm8` field WITHOUT applying ThumbExpandImm (the modified-
13305    /// immediate expansion). For `imm <= 0xFF` (e.g. gale's int8 clamps
13306    /// `#0x7e` / `#0x7f`) that is correct — `i:imm3 = 0000` means "imm8
13307    /// zero-extended". So `and r2, r0, #0x7e` encodes to the canonical
13308    /// `00 f0 7e 02`. For `imm >= 0x100` the field would need a true
13309    /// ThumbExpandImm pattern (rotation / replication), which is NOT
13310    /// implemented here — so **immediate folding must gate on `imm <= 0xFF`**
13311    /// until the encoder is hardened to ThumbExpandImm/Ok-or-Err (the
13312    /// "encoder must be Ok-or-Err, never silently wrong" principle, #180/#185).
13313    /// This bound covers the measured `flat_flight` waste (#209).
13314    #[test]
13315    fn and_immediate_encodes_correctly_in_byte_range_documents_fold_bound() {
13316        let encoder = ArmEncoder::new_thumb2();
13317        let op = ArmOp::And {
13318            rd: Reg::R2,
13319            rn: Reg::R0,
13320            op2: Operand2::Imm(0x7e),
13321        };
13322        let code = encoder.encode(&op).unwrap();
13323        assert_eq!(
13324            code,
13325            vec![0x00, 0xf0, 0x7e, 0x02],
13326            "and r2, r0, #0x7e must encode to the canonical AND.W T1 (imm8=0x7e)"
13327        );
13328    }
13329
13330    /// #255: the shared ThumbExpandImm reverse-encoder underpinning the
13331    /// data-processing immediate fix. Encodable modified immediates round-trip to
13332    /// the expected `i:imm3:imm8` field; a genuinely non-modified value is `None`
13333    /// (caller must materialize into a register). Note `1000 = 0xFA ror 30` *is*
13334    /// representable (field 0xF7A) — the old encoder mis-encoded it (raw 0x3E8);
13335    /// this encodes it correctly.
13336    #[test]
13337    fn try_thumb_expand_imm_encodes_modified_immediates() {
13338        assert_eq!(try_thumb_expand_imm(0x7e), Some(0x07e)); // zero-extended byte
13339        assert_eq!(try_thumb_expand_imm(0xff), Some(0x0ff));
13340        assert_eq!(try_thumb_expand_imm(0x0001_0001), Some(0x101)); // 0x00XY00XY
13341        assert_eq!(try_thumb_expand_imm(0xff00_ff00), Some(0x2ff)); // 0xXY00XY00
13342        assert_eq!(try_thumb_expand_imm(0xffff_ffff), Some(0x3ff)); // 0xXYXYXYXY
13343        assert_eq!(try_thumb_expand_imm(0x100), Some(0xf80)); // 0x80 ror 31
13344        assert_eq!(try_thumb_expand_imm(0x8000_0000), Some(0x400)); // 0x80 ror 8
13345        assert_eq!(try_thumb_expand_imm(1000), Some(0xf7a)); // 0xFA ror 30
13346        // Genuinely unrepresentable (bits too far apart for an 8-bit window).
13347        assert_eq!(try_thumb_expand_imm(0x101), None);
13348        assert_eq!(try_thumb_expand_imm(0x12345), None);
13349    }
13350
13351    /// #255: CMP/ADDS/SUBS encode any valid modified immediate correctly, and
13352    /// ERROR (not silently mis-encode) on a genuinely unrepresentable one,
13353    /// forcing the selector to materialize into a register — closing the
13354    /// silent-miscompile class of #251/#253.
13355    #[test]
13356    fn cmp_adds_subs_immediate_error_on_non_modified_imm() {
13357        let encoder = ArmEncoder::new_thumb2();
13358        // cmp r0, #0xff → valid → Ok; cmp r0, #1000 → valid (0xFA ror 30) → Ok.
13359        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 0xff).is_ok());
13360        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 1000).is_ok());
13361        // cmp r0, #0x101 → NOT a modified immediate → Err (materialize-reg).
13362        assert!(
13363            encoder.encode_thumb32_cmp_imm(&Reg::R0, 0x101).is_err(),
13364            "cmp #0x101 must error, not compare the wrong constant"
13365        );
13366        assert!(
13367            encoder
13368                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x101)
13369                .is_err()
13370        );
13371        assert!(
13372            encoder
13373                .encode_thumb32_subs(&Reg::R0, &Reg::R0, 0x101)
13374                .is_err()
13375        );
13376        // ...but a valid modified immediate still encodes.
13377        assert!(
13378            encoder
13379                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x80)
13380                .is_ok()
13381        );
13382    }
13383
13384    /// #257: MLA (multiply-accumulate) encodes as MLS without the bit-4 op flag.
13385    /// `mla r2, r3, r4, r8` (rd=r2, rn=r3, rm=r4, ra=r8) → Thumb-2 `03 fb 04 82`.
13386    #[test]
13387    fn mla_thumb2_encodes_correctly() {
13388        let encoder = ArmEncoder::new_thumb2();
13389        let code = encoder
13390            .encode(&ArmOp::Mla {
13391                rd: Reg::R2,
13392                rn: Reg::R3,
13393                rm: Reg::R4,
13394                ra: Reg::R8,
13395            })
13396            .unwrap();
13397        // hw1 = 0xFB03, hw2 = (8<<12)|(2<<8)|4 = 0x8204
13398        assert_eq!(code, vec![0x03, 0xfb, 0x04, 0x82]);
13399    }
13400
13401    /// #259: LDR/STR (and sub-word) immediate-offset encoders truncated
13402    /// `offset & 0xFFF`, silently targeting the wrong address for offset >= 4096.
13403    /// They now error (the selector must use register-offset addressing) — the
13404    /// load/store sibling of the #253/#255 class. Offsets <= 4095 still encode.
13405    #[test]
13406    fn ldst_imm12_offset_errors_when_out_of_range() {
13407        let encoder = ArmEncoder::new_thumb2();
13408        // offset 0xFFF (4095): valid → Ok; ldr r0, [r1, #4095].
13409        assert!(
13410            encoder
13411                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0xFFF)
13412                .is_ok()
13413        );
13414        // offset 0x1000 (4096): out of imm12 range → Err (not & 0xFFF → #0).
13415        assert!(
13416            encoder
13417                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0x1000)
13418                .is_err(),
13419            "ldr offset 4096 must error, not wrap to 0"
13420        );
13421        assert!(
13422            encoder
13423                .encode_thumb32_str(&Reg::R0, &Reg::R1, 0x1000)
13424                .is_err()
13425        );
13426        assert!(
13427            encoder
13428                .encode_thumb32_ldrb_imm(&Reg::R0, &Reg::R1, 5000)
13429                .is_err()
13430        );
13431        assert!(
13432            encoder
13433                .encode_thumb32_strh_imm(&Reg::R0, &Reg::R1, 5000)
13434                .is_err()
13435        );
13436    }
13437
13438    /// Latent miscompile fix: ADD/SUB with a >0xFF immediate (e.g.
13439    /// `add sp, sp, #frame` for a >=256-byte frame) used ADD.W (T3), whose
13440    /// `i:imm3:imm8` is a ThumbExpandImm modified immediate — so `#256` silently
13441    /// encoded as `#0` (stack corruption). Use ADDW/SUBW (T4), a PLAIN 12-bit
13442    /// immediate, for 0x100..=0xFFF; keep T3 for <=0xFF (bit-identical); error
13443    /// beyond 4095.
13444    #[test]
13445    fn add_sub_large_immediate_use_addw_subw_not_misencoded() {
13446        let encoder = ArmEncoder::new_thumb2();
13447        // add sp, sp, #256  →  ADDW (T4) SP, SP, #256  =  0d f2 00 1d
13448        assert_eq!(
13449            encoder
13450                .encode(&ArmOp::Add {
13451                    rd: Reg::SP,
13452                    rn: Reg::SP,
13453                    op2: Operand2::Imm(256),
13454                })
13455                .unwrap(),
13456            vec![0x0d, 0xf2, 0x00, 0x1d],
13457            "add sp,sp,#256 must be ADDW (plain imm12), not a mis-encoded ADD.W"
13458        );
13459        // sub sp, sp, #256  →  SUBW (T4) SP, SP, #256  =  ad f2 00 1d
13460        assert_eq!(
13461            encoder
13462                .encode(&ArmOp::Sub {
13463                    rd: Reg::SP,
13464                    rn: Reg::SP,
13465                    op2: Operand2::Imm(256),
13466                })
13467                .unwrap(),
13468            vec![0xad, 0xf2, 0x00, 0x1d],
13469        );
13470        // > 4095 has no single-instruction encoding → error, not silent wrong.
13471        assert!(
13472            encoder
13473                .encode(&ArmOp::Add {
13474                    rd: Reg::SP,
13475                    rn: Reg::SP,
13476                    op2: Operand2::Imm(5000),
13477                })
13478                .is_err(),
13479            "add #5000 must error (no single ADDW), not mis-encode"
13480        );
13481    }
13482
13483    /// Closes the data-proc immediate class: AND and CMN now go through
13484    /// `try_thumb_expand_imm` like ORR/EOR/CMP — correct for any modified
13485    /// immediate, `Err` (not raw-pack / NOP) on an un-encodable one. The byte
13486    /// range stays bit-identical (`and r2,r0,#0x7e` is unchanged).
13487    #[test]
13488    fn and_cmn_immediate_thumb_expand_else_error() {
13489        let encoder = ArmEncoder::new_thumb2();
13490        // byte range unchanged (bit-identical with the pre-retrofit encoding)
13491        assert_eq!(
13492            encoder
13493                .encode(&ArmOp::And {
13494                    rd: Reg::R2,
13495                    rn: Reg::R0,
13496                    op2: Operand2::Imm(0x7e),
13497                })
13498                .unwrap(),
13499            vec![0x00, 0xf0, 0x7e, 0x02],
13500        );
13501        // a valid replicated modified immediate now encodes (was silently wrong)
13502        assert!(
13503            encoder
13504                .encode(&ArmOp::And {
13505                    rd: Reg::R2,
13506                    rn: Reg::R0,
13507                    op2: Operand2::Imm(0xff00ff00u32 as i32),
13508                })
13509                .is_ok()
13510        );
13511        // a genuinely un-encodable immediate errors (AND was raw-pack; CMN NOP)
13512        assert!(
13513            encoder
13514                .encode(&ArmOp::And {
13515                    rd: Reg::R2,
13516                    rn: Reg::R0,
13517                    op2: Operand2::Imm(0x101),
13518                })
13519                .is_err()
13520        );
13521        assert!(
13522            encoder
13523                .encode(&ArmOp::Cmn {
13524                    rn: Reg::R0,
13525                    op2: Operand2::Imm(0x101),
13526                })
13527                .is_err(),
13528            "CMN #0x101 must error, not emit a NOP"
13529        );
13530    }
13531
13532    /// VCR-RA-001: ORR/EOR with a small immediate must encode the real
13533    /// instruction (not a silent `0xBF00` NOP). Pins the byte range and the
13534    /// Ok-or-Err bound that makes future Or/Eor immediate folding safe.
13535    #[test]
13536    fn orr_eor_immediate_encode_in_byte_range_else_error() {
13537        let encoder = ArmEncoder::new_thumb2();
13538        // orr r2, r0, #0x7e  →  ORR.W T1, imm8=0x7e
13539        assert_eq!(
13540            encoder
13541                .encode(&ArmOp::Orr {
13542                    rd: Reg::R2,
13543                    rn: Reg::R0,
13544                    op2: Operand2::Imm(0x7e),
13545                })
13546                .unwrap(),
13547            vec![0x40, 0xf0, 0x7e, 0x02],
13548        );
13549        // eor r2, r0, #0x7e  →  EOR.W T1, imm8=0x7e
13550        assert_eq!(
13551            encoder
13552                .encode(&ArmOp::Eor {
13553                    rd: Reg::R2,
13554                    rn: Reg::R0,
13555                    op2: Operand2::Imm(0x7e),
13556                })
13557                .unwrap(),
13558            vec![0x80, 0xf0, 0x7e, 0x02],
13559        );
13560        // Out-of-range immediates error rather than silently mis-encode / NOP.
13561        assert!(
13562            encoder
13563                .encode(&ArmOp::Orr {
13564                    rd: Reg::R2,
13565                    rn: Reg::R0,
13566                    op2: Operand2::Imm(0x140),
13567                })
13568                .is_err(),
13569            "ORR #0x140 must error, not emit a NOP"
13570        );
13571    }
13572
13573    #[test]
13574    fn test_encode_mve_different_qregs() {
13575        let encoder = ArmEncoder::new_thumb2();
13576
13577        // Test that different Q-register numbers produce different encodings
13578        let op1 = ArmOp::MveAddI {
13579            qd: QReg::Q0,
13580            qn: QReg::Q0,
13581            qm: QReg::Q0,
13582            size: MveSize::S32,
13583        };
13584        let op2 = ArmOp::MveAddI {
13585            qd: QReg::Q3,
13586            qn: QReg::Q5,
13587            qm: QReg::Q7,
13588            size: MveSize::S32,
13589        };
13590        let code1 = encoder.encode(&op1).unwrap();
13591        let code2 = encoder.encode(&op2).unwrap();
13592        assert_ne!(
13593            code1, code2,
13594            "Different Q-registers should produce different encodings"
13595        );
13596    }
13597
13598    #[test]
13599    fn test_encode_mve_arm32_loud_err() {
13600        // #615: MVE (Helium) is Thumb-2-only. The ARM32 encoder used to emit
13601        // a silent NOP here (dropping the vector op); it must now be a typed
13602        // Err so a broken "MVE implies Thumb" invariant fails loudly.
13603        let encoder = ArmEncoder::new_arm32();
13604        let op = ArmOp::MveAddI {
13605            qd: QReg::Q0,
13606            qn: QReg::Q1,
13607            qm: QReg::Q2,
13608            size: MveSize::S32,
13609        };
13610        let err = encoder
13611            .encode(&op)
13612            .expect_err("ARM32 MVE must be a loud Err, not a silent NOP (#615)");
13613        assert!(
13614            err.to_string().contains("Thumb-2 only"),
13615            "unexpected error message: {err}"
13616        );
13617    }
13618}