Skip to main content

synth_backend/
arm_encoder.rs

1//! ARM Code Encoder - Converts ARM instructions to binary machine code
2//!
3//! Generates ARM32/Thumb-2 machine code from ARM instruction structures
4
5use synth_core::Result;
6use synth_core::target::FPUPrecision;
7use synth_synthesis::contracts::encoding as encoding_contracts;
8use synth_synthesis::{ArmOp, MemAddr, MveSize, Operand2, QReg, Reg, VfpReg};
9
10/// ARM instruction encoding
11pub struct ArmEncoder {
12    /// Use Thumb mode (vs ARM mode)
13    thumb_mode: bool,
14    /// FPU capability for VFP instruction encoding
15    #[allow(dead_code)]
16    fpu: Option<FPUPrecision>,
17}
18
19impl ArmEncoder {
20    /// Create a new ARM encoder in ARM32 mode
21    pub fn new_arm32() -> Self {
22        Self {
23            thumb_mode: false,
24            fpu: None,
25        }
26    }
27
28    /// Create a new ARM encoder in Thumb-2 mode
29    pub fn new_thumb2() -> Self {
30        Self {
31            thumb_mode: true,
32            fpu: None,
33        }
34    }
35
36    /// Create a new Thumb-2 encoder with FPU capability
37    pub fn new_thumb2_with_fpu(fpu: Option<FPUPrecision>) -> Self {
38        Self {
39            thumb_mode: true,
40            fpu,
41        }
42    }
43
44    /// Encode a single ARM instruction to bytes
45    pub fn encode(&self, op: &ArmOp) -> Result<Vec<u8>> {
46        if self.thumb_mode {
47            self.encode_thumb(op)
48        } else {
49            self.encode_arm(op)
50        }
51    }
52
53    /// Encode an ARM instruction in ARM32 mode (32-bit instructions)
54    /// #206 / RQ-63-ARMI64OFF (#1165): encode an ARM32 (A32) word/sub-word
55    /// load/store whose effective base must be computed into IP — because the
56    /// address carries a register offset (`[rn, rm{, #off}]`, #206) and/or
57    /// because its static offset exceeds the form's immediate field (imm12 for
58    /// LDR/STR/LDRB/STRB, imm8 for LDRH/STRH/LDRSB/LDRSH; #1165 — the
59    /// immediate arms used to MASK such an offset to a wrong address). Returns
60    /// `None` for a plain in-range immediate access (the caller falls through
61    /// to the immediate-form arms, byte-identical). Otherwise
62    /// `a32_effective_base` materializes `ip` and the op is re-encoded against
63    /// `[ip, #residual]`, which is uniform for word/byte/halfword/signed forms.
64    /// IP (R12) is the scratch register the selector already treats as
65    /// clobberable across memory ops.
66    fn encode_arm_reg_offset_mem(&self, op: &ArmOp) -> Result<Option<Vec<u8>>> {
67        let (addr, imm_max) = match op {
68            ArmOp::Ldr { addr, .. }
69            | ArmOp::Str { addr, .. }
70            | ArmOp::Ldrb { addr, .. }
71            | ArmOp::Strb { addr, .. } => (addr, A32_LDST_IMM12_MAX),
72            ArmOp::Ldrh { addr, .. }
73            | ArmOp::Strh { addr, .. }
74            | ArmOp::Ldrsb { addr, .. }
75            | ArmOp::Ldrsh { addr, .. } => (addr, A32_LDST_IMM8_MAX),
76            _ => return Ok(None),
77        };
78        if addr.offset_reg.is_none() && (addr.offset as u32) <= imm_max {
79            return Ok(None);
80        }
81        let mut bytes = Vec::new();
82        let (base, residual) = a32_effective_base(&mut bytes, addr, imm_max)?;
83        // Re-encode the op against [base, #residual] (immediate form, in range
84        // by construction → this recursion hits the immediate arms, not this
85        // helper again).
86        let imm_addr = MemAddr::imm(base, residual as i32);
87        let imm_op = match op {
88            ArmOp::Ldr { rd, .. } => ArmOp::Ldr {
89                rd: *rd,
90                addr: imm_addr,
91            },
92            ArmOp::Str { rd, .. } => ArmOp::Str {
93                rd: *rd,
94                addr: imm_addr,
95            },
96            ArmOp::Ldrb { rd, .. } => ArmOp::Ldrb {
97                rd: *rd,
98                addr: imm_addr,
99            },
100            ArmOp::Strb { rd, .. } => ArmOp::Strb {
101                rd: *rd,
102                addr: imm_addr,
103            },
104            ArmOp::Ldrh { rd, .. } => ArmOp::Ldrh {
105                rd: *rd,
106                addr: imm_addr,
107            },
108            ArmOp::Strh { rd, .. } => ArmOp::Strh {
109                rd: *rd,
110                addr: imm_addr,
111            },
112            ArmOp::Ldrsb { rd, .. } => ArmOp::Ldrsb {
113                rd: *rd,
114                addr: imm_addr,
115            },
116            ArmOp::Ldrsh { rd, .. } => ArmOp::Ldrsh {
117                rd: *rd,
118                addr: imm_addr,
119            },
120            _ => unreachable!(),
121        };
122        bytes.extend(self.encode_arm(&imm_op)?);
123        Ok(Some(bytes))
124    }
125
126    /// #594: A32 expansion of `ArmOp::CallIndirect` — mirror of the Thumb-2
127    /// arm (same contract: R11 holds the function-pointer table base, entry
128    /// `i` is a 4-byte code address, R12 is the encoder-scratch register):
129    ///
130    /// ```text
131    /// MOVW r12, #size        ; #642: table size (compile-time immediate)
132    /// [MOVT r12, #size>>16]  ; only when size exceeds 16 bits
133    /// CMP  idx, r12          ; bounds guard: index >= size must TRAP
134    /// BLO  +1 insn           ; skip the trap when in bounds
135    /// UDF                    ; WASM Core §4.4.8 out-of-bounds trap
136    /// MOV r12, idx, LSL #2   ; table byte offset
137    /// LDR r12, [r11, r12]    ; load function pointer
138    /// BLX r12                ; indirect call
139    /// ```
140    ///
141    /// #650, `table_byte_offset != 0` (a non-zero table of the contiguous
142    /// R11 region): the pointer load becomes
143    /// `ADD r12, r11, r12; LDR r12, [r12, #offset]` — offset 0 keeps the
144    /// single-load form (single-table modules byte-identical by
145    /// construction).
146    ///
147    /// #664, `null_check` (the table has null slots, linked as ZERO words
148    /// per the layout contract): `CMP r12, #0; BNE +1; UDF` between the
149    /// pointer load and the `BLX` — a call reaching an uninitialized slot
150    /// traps (§4.4.8). `false` keeps the expansion byte-identical.
151    ///
152    /// #676, `type_check` (heterogeneous table): the §4.4.8 type check is
153    /// discharged at RUNTIME against the type-id sidecar — after the bounds
154    /// guard, `MOV r12, idx, LSL #2; ADD r12, r11, r12;
155    /// LDR r12, [r12, #type_off]; CMP r12, #expected_id; BEQ +1; UDF`
156    /// (mirror of the Thumb-2 arm; the dispatch tail recomputes `idx*4`).
157    /// Null slots carry the reserved class id 0, subsuming the #664 null
158    /// trap. `None` (every homogeneous table — the verdict discharged at
159    /// COMPILE time by the closed-world verification, see the #642 selector
160    /// guard) emits nothing and keeps the expansion byte-identical.
161    fn encode_arm_call_indirect(
162        table_index_reg: &Reg,
163        table_size: u32,
164        table_byte_offset: u32,
165        null_check: bool,
166        type_check: Option<(u32, u32)>,
167    ) -> Vec<u8> {
168        let idx = reg_to_bits(table_index_reg);
169        let mut bytes = Vec::with_capacity(32);
170        // MOVW r12, #(size & 0xFFFF) — cond=E 0011 0000 imm4 Rd imm12.
171        let size_lo = table_size & 0xFFFF;
172        let movw: u32 = 0xE300_0000 | ((size_lo >> 12) << 16) | (12 << 12) | (size_lo & 0xFFF);
173        bytes.extend_from_slice(&movw.to_le_bytes());
174        // MOVT r12, #(size >> 16) — only for a table size above 16 bits.
175        let size_hi = table_size >> 16;
176        if size_hi != 0 {
177            let movt: u32 = 0xE340_0000 | ((size_hi >> 12) << 16) | (12 << 12) | (size_hi & 0xFFF);
178            bytes.extend_from_slice(&movt.to_le_bytes());
179        }
180        // CMP idx, r12 — cond=E, opcode=1010, S=1, Rn=idx, Rm=r12.
181        let cmp: u32 = 0xE150_000C | (idx << 16);
182        bytes.extend_from_slice(&cmp.to_le_bytes());
183        // BLO +1 insn (skip the UDF when index < size) — cond=LO(0011),
184        // imm24=0: target = branch + 8.
185        bytes.extend_from_slice(&0x3A00_0000u32.to_le_bytes());
186        // UDF — permanently undefined (same trap idiom as the A32 div-by-zero
187        // guards): call_indirect out-of-bounds trap.
188        bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
189        // #676: runtime type check for a heterogeneous table — load the
190        // indexed slot's structural class id from the type-id sidecar and
191        // trap on mismatch (§4.4.8). Mirror of the Thumb-2 arm; `None`
192        // emits nothing (homogeneous tables byte-identical by construction).
193        if let Some((expected_id, type_off)) = type_check {
194            // RQ-61-IMMRANGE (#1072): these `debug_assert`s are compiled out
195            // in release, where the `& 0xFF` / `& 0xFFF` masks below would
196            // silently TRUNCATE an out-of-range value (id 256 compares as 0,
197            // letting a NULL slot pass the §4.4.8 check). The enforcement
198            // claim is DEMONSTRATED, not assumed: the sole `Some` producer is
199            // `resolve_runtime_type_check` (instruction_selector.rs), which
200            // loud-declines id > 255 and offset > 4095, and
201            // `test_676_call_indirect_runtime_check_range_declines` trips
202            // both declines (mutation-checked: disabling either turns it red).
203            debug_assert!(expected_id <= 255, "selector enforces the CMP imm8 range");
204            debug_assert!(type_off <= 4095, "selector enforces the LDR imm12 range");
205            // MOV r12, idx, LSL #2 (same as the dispatch tail's scale).
206            bytes.extend_from_slice(&(0xE1A0C000u32 | (2 << 7) | idx).to_le_bytes());
207            // ADD r12, r11, r12 — data-processing ADD (register).
208            bytes.extend_from_slice(&0xE08BC00Cu32.to_le_bytes());
209            // LDR r12, [r12, #type_off] — immediate offset, P=1 U=1 L=1.
210            bytes.extend_from_slice(&(0xE59CC000u32 | (type_off & 0xFFF)).to_le_bytes());
211            // CMP r12, #expected_id — data-processing CMP (immediate).
212            bytes.extend_from_slice(&(0xE35C_0000u32 | (expected_id & 0xFF)).to_le_bytes());
213            // BEQ +1 insn (skip the UDF when the class id matches) —
214            // cond=EQ(0000), imm24=0: target = branch + 8.
215            bytes.extend_from_slice(&0x0A00_0000u32.to_le_bytes());
216            // UDF — the §4.4.8 type-mismatch trap.
217            bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
218        }
219        // MOV r12, idx, LSL #2 — data-processing MOV, register op2 with
220        // imm5=2/LSL: cond=E, opcode=1101, S=0, Rd=r12.
221        let mov: u32 = 0xE1A0C000 | (2 << 7) | idx;
222        bytes.extend_from_slice(&mov.to_le_bytes());
223        if table_byte_offset == 0 {
224            // Table 0 (base = R11 itself): the pre-#650 single-load form.
225            // LDR r12, [r11, r12] — register offset, P=1 U=1 B=0 W=0 L=1.
226            let ldr: u32 = 0xE79BC00C;
227            bytes.extend_from_slice(&ldr.to_le_bytes());
228        } else {
229            // #650: fold the table's compile-time base offset into the
230            // pointer load via the LDR imm12 form.
231            assert!(
232                table_byte_offset <= 4095,
233                "call_indirect table base offset {table_byte_offset} exceeds \
234                 LDR imm12 — the selector must have declined this (#650)"
235            );
236            // ADD r12, r11, r12 — data-processing ADD (register).
237            bytes.extend_from_slice(&0xE08BC00Cu32.to_le_bytes());
238            // LDR r12, [r12, #offset] — immediate offset, P=1 U=1 L=1.
239            let ldr: u32 = 0xE59CC000 | (table_byte_offset & 0xFFF);
240            bytes.extend_from_slice(&ldr.to_le_bytes());
241        }
242        // #664: null-slot trap — only when the table image has null slots
243        // (zero-linked words). A fully-initialized table keeps the pre-#664
244        // bytes identical by construction.
245        if null_check {
246            // CMP r12, #0 — data-processing CMP (immediate), Rn=r12.
247            bytes.extend_from_slice(&0xE35C_0000u32.to_le_bytes());
248            // BNE +1 insn (skip the UDF when the pointer is non-null) —
249            // cond=NE(0001), imm24=0: target = branch + 8.
250            bytes.extend_from_slice(&0x1A00_0000u32.to_le_bytes());
251            // UDF — the §4.4.8 uninitialized-element trap (same idiom as
252            // the bounds guard).
253            bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
254        }
255        // BLX r12 — cond=E, 0001 0010 1111 1111 1111 0011, Rm=r12.
256        let blx: u32 = 0xE12FFF3C;
257        bytes.extend_from_slice(&blx.to_le_bytes());
258        bytes
259    }
260
261    /// #615: A32 (ARM-mode) expansions for the multi-instruction ops that the
262    /// Thumb-2 encoder expands but the A32 arm previously encoded as a single
263    /// literal NOP (`0xE1A00000`) — i64 mul / shifts / rotates / comparisons /
264    /// eqz, plus i64 const/load/store/extend/wrap and the i32 SetCond /
265    /// SelectMove pseudo-ops. Each expansion mirrors its Thumb-2 twin's
266    /// register contract and semantics exactly (A32 conditional execution
267    /// replaces the IT blocks). Returns `Ok(None)` for ops this helper does
268    /// not handle; the caller's match encodes or loudly rejects those.
269    fn encode_arm_expanded(&self, op: &ArmOp) -> Result<Option<Vec<u8>>> {
270        use synth_synthesis::Condition;
271
272        /// A32 condition-field bits (instruction bits [31:28]).
273        fn cond_bits(cond: &Condition) -> u32 {
274            match cond {
275                Condition::EQ => 0x0,
276                Condition::NE => 0x1,
277                Condition::HS => 0x2, // CS: unsigned >=
278                Condition::LO => 0x3, // CC: unsigned <
279                Condition::HI => 0x8, // unsigned >
280                Condition::LS => 0x9, // unsigned <=
281                Condition::GE => 0xA,
282                Condition::LT => 0xB,
283                Condition::GT => 0xC,
284                Condition::LE => 0xD,
285            }
286        }
287        fn w(b: &mut Vec<u8>, word: u32) {
288            b.extend_from_slice(&word.to_le_bytes());
289        }
290        /// MOV<cond> rd, #imm (rotated-immediate form; only 0/1 used here).
291        fn mov_cond_imm(b: &mut Vec<u8>, cond: u32, rd: u32, imm: u32) {
292            w(b, (cond << 28) | 0x03A0_0000 | (rd << 12) | imm);
293        }
294        /// After a flag-setting pair: MOV<cond> rd,#1 ; MOV<!cond> rd,#0.
295        fn set_cond(b: &mut Vec<u8>, cond: &Condition, rd: u32) {
296            mov_cond_imm(b, cond_bits(cond), rd, 1);
297            mov_cond_imm(b, cond_bits(&cond.invert()), rd, 0);
298        }
299        /// CMP rn, rm (register form).
300        fn cmp_reg(b: &mut Vec<u8>, rn: u32, rm: u32) {
301            w(b, 0xE150_0000 | (rn << 16) | rm);
302        }
303        /// SBCS rd, rn, rm — the 64-bit compare idiom's high-word subtract.
304        fn sbcs(b: &mut Vec<u8>, rd: u32, rn: u32, rm: u32) {
305            w(b, 0xE0D0_0000 | (rn << 16) | (rd << 12) | rm);
306        }
307        /// MOVW rd, #imm16.
308        fn movw(b: &mut Vec<u8>, rd: u32, v: u32) {
309            w(
310                b,
311                0xE300_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
312            );
313        }
314        /// MOVT rd, #imm16.
315        fn movt(b: &mut Vec<u8>, rd: u32, v: u32) {
316            w(
317                b,
318                0xE340_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
319            );
320        }
321        /// Register-controlled shift: MOV rd, rn, <LSL|LSR|ASR> rs.
322        /// `ty`: 0=LSL, 1=LSR, 2=ASR. A32 uses the bottom byte of rs;
323        /// amounts of 32 or more yield 0 (LSL/LSR) or all-sign (ASR) — same
324        /// semantics the Thumb-2 expansions rely on.
325        fn shift_reg(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, rs: u32) {
326            w(b, 0xE1A0_0010 | (rd << 12) | (rs << 8) | (ty << 5) | rn);
327        }
328        const LSL: u32 = 0;
329        const LSR: u32 = 1;
330        const ASR: u32 = 2;
331        /// Immediate-shift move: MOV rd, rn, <LSL|LSR|ASR> #imm.
332        fn shift_imm(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, imm: u32) {
333            w(
334                b,
335                0xE1A0_0000 | (rd << 12) | ((imm & 0x1F) << 7) | (ty << 5) | rn,
336            );
337        }
338        /// Data-processing register form: `base | rn<<16 | rd<<12 | rm`.
339        /// `base` carries cond/opcode/S (e.g. 0xE090_0000 = ADDS).
340        fn dp_reg(b: &mut Vec<u8>, base: u32, rd: u32, rn: u32, rm: u32) {
341            w(b, base | (rn << 16) | (rd << 12) | rm);
342        }
343        /// Data-processing with an immediate-shifted register operand:
344        /// `<op> rd, rn, rm, <LSL|LSR|ASR> #imm` — the A32 barrel shifter
345        /// folds a shift into the second operand for free. #1021 uses this to
346        /// run the popcnt SWAR fold on R12 alone (no second scratch, so R11 —
347        /// the linear-memory base — is never touched).
348        fn dp_reg_shift(b: &mut Vec<u8>, base: u32, rd: u32, rn: u32, rm: u32, ty: u32, imm: u32) {
349            w(
350                b,
351                base | (rn << 16) | (rd << 12) | ((imm & 0x1F) << 7) | (ty << 5) | rm,
352            );
353        }
354        /// ORR rd, rd, rm, LSR #31 — the carry-propagation idiom of the
355        /// shift-subtract division loop (bring rm's MSB into rd's bit 0).
356        fn orr_lsr31(b: &mut Vec<u8>, rd: u32, rm: u32) {
357            w(
358                b,
359                0xE180_0000 | (rd << 16) | (rd << 12) | (31 << 7) | (1 << 5) | rm,
360            );
361        }
362        /// 64-bit two's-complement negate of the lo:hi pair (MVN/MVN/ADDS/ADC).
363        fn negate64(b: &mut Vec<u8>, lo: u32, hi: u32) {
364            w(b, 0xE1E0_0000 | (lo << 12) | lo); //           MVN  lo, lo
365            w(b, 0xE1E0_0000 | (hi << 12) | hi); //           MVN  hi, hi
366            w(b, 0xE290_0001 | (lo << 16) | (lo << 12)); //   ADDS lo, lo, #1
367            w(b, 0xE2A0_0000 | (hi << 16) | (hi << 12)); //   ADC  hi, hi, #0
368        }
369        /// TST x, x ; BPL +4-instructions — the "skip the negate64 when the
370        /// sign bit is clear" guard of the signed div/rem arms.
371        fn skip_negate_if_positive(b: &mut Vec<u8>, x: u32) {
372            w(b, 0xE110_0000 | (x << 16) | x); // TST x, x
373            w(b, 0x5A00_0003); //                 BPL +4 insns (past negate64)
374        }
375        /// The 64-iteration shift-subtract division loop — A32 transcription
376        /// of the Thumb-2 #610 core: dividend R0:R1, divisor R2:R3, quotient
377        /// R4:R5, remainder R6:R7, loop counter in `counter` (R12 or R8).
378        fn div_loop(b: &mut Vec<u8>, counter: u32) {
379            w(b, 0xE3A0_0040 | (counter << 12)); // MOV counter, #64
380            let loop_start = b.len();
381            // quotient <<= 1
382            shift_imm(b, LSL, 5, 5, 1);
383            orr_lsr31(b, 5, 4);
384            shift_imm(b, LSL, 4, 4, 1);
385            // remainder <<= 1, OR in dividend MSB
386            shift_imm(b, LSL, 7, 7, 1);
387            orr_lsr31(b, 7, 6);
388            shift_imm(b, LSL, 6, 6, 1);
389            orr_lsr31(b, 6, 1);
390            // dividend <<= 1
391            shift_imm(b, LSL, 1, 1, 1);
392            orr_lsr31(b, 1, 0);
393            shift_imm(b, LSL, 0, 0, 1);
394            // if remainder >= divisor (64-bit unsigned): subtract, set q bit
395            w(b, 0xE157_0003); // CMP R7, R3      (high words)
396            w(b, 0x8A00_0002); // BHI .subtract   (+2 insns)
397            w(b, 0x3A00_0004); // BLO .next       (+4 insns)
398            w(b, 0xE156_0002); // CMP R6, R2      (low words, highs equal)
399            w(b, 0x3A00_0002); // BLO .next       (+2 insns)
400            w(b, 0xE056_6002); // .subtract: SUBS R6, R6, R2
401            w(b, 0xE0C7_7003); //            SBC  R7, R7, R3
402            w(b, 0xE384_4001); //            ORR  R4, R4, #1
403            // .next: decrement and loop
404            w(b, 0xE250_0001 | (counter << 16) | (counter << 12)); // SUBS counter, #1
405            let diff = (loop_start as i64) - (b.len() as i64 + 8);
406            w(b, 0x1A00_0000 | (((diff / 4) as u32) & 0x00FF_FFFF)); // BNE loop
407        }
408        /// 32-bit population count on working register `x` — A32 transcription
409        /// of the Thumb-2 I64Popcnt per-word core (mul-based fold): `c` is the
410        /// constant register, R12 the shifted temp. Both are clobbered.
411        fn popcnt_word(b: &mut Vec<u8>, x: u32, c: u32) {
412            // x = x - ((x >> 1) & 0x55555555)
413            shift_imm(b, LSR, 12, x, 1);
414            movw(b, c, 0x5555);
415            movt(b, c, 0x5555);
416            dp_reg(b, 0xE000_0000, 12, 12, c); // AND R12, R12, c
417            dp_reg(b, 0xE040_0000, x, x, 12); //  SUB x, x, R12
418            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
419            movw(b, c, 0x3333);
420            movt(b, c, 0x3333);
421            dp_reg(b, 0xE000_0000, 12, x, c); //  AND R12, x, c
422            shift_imm(b, LSR, x, x, 2);
423            dp_reg(b, 0xE000_0000, x, x, c); //   AND x, x, c
424            dp_reg(b, 0xE080_0000, x, x, 12); //  ADD x, x, R12
425            // x = (x + (x >> 4)) & 0x0F0F0F0F
426            shift_imm(b, LSR, 12, x, 4);
427            dp_reg(b, 0xE080_0000, x, x, 12); //  ADD x, x, R12
428            movw(b, c, 0x0F0F);
429            movt(b, c, 0x0F0F);
430            dp_reg(b, 0xE000_0000, x, x, c); //   AND x, x, c
431            // x = (x * 0x01010101) >> 24
432            movw(b, c, 0x0101);
433            movt(b, c, 0x0101);
434            w(b, 0xE000_0090 | (x << 16) | (c << 8) | x); // MUL x, x, c
435            shift_imm(b, LSR, x, x, 24);
436        }
437
438        let mut b: Vec<u8> = Vec::new();
439        match op {
440            // SetCond: materialize a flags-predicate as 0/1 — the A32 twin of
441            // the Thumb `ITE cond; MOV rd,#1; MOV rd,#0`.
442            ArmOp::SetCond { rd, cond } => {
443                set_cond(&mut b, cond, reg_to_bits(rd));
444            }
445
446            // SelectMove: conditional register move (Thumb: IT cond; MOV).
447            ArmOp::SelectMove { rd, rm, cond } => {
448                w(
449                    &mut b,
450                    (cond_bits(cond) << 28)
451                        | 0x01A0_0000
452                        | (reg_to_bits(rd) << 12)
453                        | reg_to_bits(rm),
454                );
455            }
456
457            // I64SetCond: compare two i64 register pairs, 0/1 into rd.
458            // EQ/NE: CMP lo,lo; CMPEQ hi,hi (only if lows equal); set.
459            // Ordered: CMP lo,lo; SBCS rd,hi,hi; set — with the same
460            // operand-swap + condition mapping as the Thumb-2 arm.
461            ArmOp::I64SetCond {
462                rd,
463                rn_lo,
464                rn_hi,
465                rm_lo,
466                rm_hi,
467                cond,
468            } => {
469                let rd_b = reg_to_bits(rd);
470                let (n_lo, n_hi, m_lo, m_hi) = (
471                    reg_to_bits(rn_lo),
472                    reg_to_bits(rn_hi),
473                    reg_to_bits(rm_lo),
474                    reg_to_bits(rm_hi),
475                );
476                match cond {
477                    Condition::EQ | Condition::NE => {
478                        cmp_reg(&mut b, n_lo, m_lo);
479                        // CMP<EQ> rn_hi, rm_hi — compare highs only if lows equal.
480                        w(&mut b, 0x0150_0000 | (n_hi << 16) | m_hi);
481                        set_cond(&mut b, cond, rd_b);
482                    }
483                    // (swap operands?, condition after SBCS) per the Thumb arm:
484                    // LT/GE/LO/HS compare (rn, rm); GT/LE/HI/LS swap to (rm, rn).
485                    Condition::LT => {
486                        cmp_reg(&mut b, n_lo, m_lo);
487                        sbcs(&mut b, rd_b, n_hi, m_hi);
488                        set_cond(&mut b, &Condition::LT, rd_b);
489                    }
490                    Condition::GE => {
491                        cmp_reg(&mut b, n_lo, m_lo);
492                        sbcs(&mut b, rd_b, n_hi, m_hi);
493                        set_cond(&mut b, &Condition::GE, rd_b);
494                    }
495                    Condition::GT => {
496                        cmp_reg(&mut b, m_lo, n_lo);
497                        sbcs(&mut b, rd_b, m_hi, n_hi);
498                        set_cond(&mut b, &Condition::LT, rd_b);
499                    }
500                    Condition::LE => {
501                        cmp_reg(&mut b, m_lo, n_lo);
502                        sbcs(&mut b, rd_b, m_hi, n_hi);
503                        set_cond(&mut b, &Condition::GE, rd_b);
504                    }
505                    Condition::LO => {
506                        cmp_reg(&mut b, n_lo, m_lo);
507                        sbcs(&mut b, rd_b, n_hi, m_hi);
508                        set_cond(&mut b, &Condition::LO, rd_b);
509                    }
510                    Condition::HS => {
511                        cmp_reg(&mut b, n_lo, m_lo);
512                        sbcs(&mut b, rd_b, n_hi, m_hi);
513                        set_cond(&mut b, &Condition::HS, rd_b);
514                    }
515                    Condition::HI => {
516                        cmp_reg(&mut b, m_lo, n_lo);
517                        sbcs(&mut b, rd_b, m_hi, n_hi);
518                        set_cond(&mut b, &Condition::LO, rd_b);
519                    }
520                    Condition::LS => {
521                        cmp_reg(&mut b, m_lo, n_lo);
522                        sbcs(&mut b, rd_b, m_hi, n_hi);
523                        set_cond(&mut b, &Condition::HS, rd_b);
524                    }
525                }
526            }
527
528            // I64SetCondZ: ORRS rd, lo, hi sets Z iff the pair is zero.
529            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
530                let rd_b = reg_to_bits(rd);
531                w(
532                    &mut b,
533                    0xE190_0000 | (reg_to_bits(rn_lo) << 16) | (rd_b << 12) | reg_to_bits(rn_hi),
534                );
535                set_cond(&mut b, &Condition::EQ, rd_b);
536            }
537
538            // i64 comparison wrappers: delegate to I64SetCond/Z, mirroring the
539            // Thumb-2 delegation arms.
540            ArmOp::I64Eqz { rd, rnlo, rnhi } => {
541                return self
542                    .encode_arm(&ArmOp::I64SetCondZ {
543                        rd: *rd,
544                        rn_lo: *rnlo,
545                        rn_hi: *rnhi,
546                    })
547                    .map(Some);
548            }
549            ArmOp::I64Eq {
550                rd,
551                rnlo,
552                rnhi,
553                rmlo,
554                rmhi,
555            }
556            | ArmOp::I64Ne {
557                rd,
558                rnlo,
559                rnhi,
560                rmlo,
561                rmhi,
562            }
563            | ArmOp::I64LtS {
564                rd,
565                rnlo,
566                rnhi,
567                rmlo,
568                rmhi,
569            }
570            | ArmOp::I64LtU {
571                rd,
572                rnlo,
573                rnhi,
574                rmlo,
575                rmhi,
576            }
577            | ArmOp::I64LeS {
578                rd,
579                rnlo,
580                rnhi,
581                rmlo,
582                rmhi,
583            }
584            | ArmOp::I64LeU {
585                rd,
586                rnlo,
587                rnhi,
588                rmlo,
589                rmhi,
590            }
591            | ArmOp::I64GtS {
592                rd,
593                rnlo,
594                rnhi,
595                rmlo,
596                rmhi,
597            }
598            | ArmOp::I64GtU {
599                rd,
600                rnlo,
601                rnhi,
602                rmlo,
603                rmhi,
604            }
605            | ArmOp::I64GeS {
606                rd,
607                rnlo,
608                rnhi,
609                rmlo,
610                rmhi,
611            }
612            | ArmOp::I64GeU {
613                rd,
614                rnlo,
615                rnhi,
616                rmlo,
617                rmhi,
618            } => {
619                let cond = match op {
620                    ArmOp::I64Eq { .. } => Condition::EQ,
621                    ArmOp::I64Ne { .. } => Condition::NE,
622                    ArmOp::I64LtS { .. } => Condition::LT,
623                    ArmOp::I64LtU { .. } => Condition::LO,
624                    ArmOp::I64LeS { .. } => Condition::LE,
625                    ArmOp::I64LeU { .. } => Condition::LS,
626                    ArmOp::I64GtS { .. } => Condition::GT,
627                    ArmOp::I64GtU { .. } => Condition::HI,
628                    ArmOp::I64GeS { .. } => Condition::GE,
629                    _ => Condition::HS,
630                };
631                return self
632                    .encode_arm(&ArmOp::I64SetCond {
633                        rd: *rd,
634                        rn_lo: *rnlo,
635                        rn_hi: *rnhi,
636                        rm_lo: *rmlo,
637                        rm_hi: *rmhi,
638                        cond,
639                    })
640                    .map(Some);
641            }
642
643            // I64Mul: cross products into R12, then UMULL — same sequence and
644            // ordering as the Thumb-2 arm (R12 is encoder scratch, #212).
645            ArmOp::I64Mul {
646                rd_lo,
647                rd_hi,
648                rn_lo,
649                rn_hi,
650                rm_lo,
651                rm_hi,
652            } => {
653                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
654                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
655                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
656                // MUL R12, rn_lo, rm_hi   (R12 = a_lo * b_hi)
657                w(&mut b, 0xE000_0090 | (12 << 16) | (mh << 8) | nl);
658                // MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
659                w(
660                    &mut b,
661                    0xE020_0090 | (12 << 16) | (12 << 12) | (ml << 8) | nh,
662                );
663                // UMULL rd_lo, rd_hi, rn_lo, rm_lo
664                w(
665                    &mut b,
666                    0xE080_0090 | (dh << 16) | (dl << 12) | (ml << 8) | nl,
667                );
668                // ADD rd_hi, rd_hi, R12
669                w(&mut b, 0xE080_0000 | (dh << 16) | (dh << 12) | 12);
670            }
671
672            // I64Shl / I64ShrU / I64ShrS: same small/large-shift structure as
673            // the Thumb-2 arms. #1048: the expansion must never write its own
674            // input operands — the pre-#1048 A32 arms masked the amount in
675            // place (`AND ml, ml, #63`) and used the amount's home high
676            // register as scratch, identically to the Thumb-2 defect. R12
677            // (encoder scratch, never allocatable, #212) is the ONLY
678            // temporary; the masked amount is re-derived from the untouched
679            // rm_lo where a second live temp would otherwise be needed.
680            // Register-controlled shifts >= 32 yield 0, which the small path
681            // relies on for n = 0. Same #1039-style loud alias guards as the
682            // Thumb-2 arms.
683            ArmOp::I64Shl {
684                rd_lo,
685                rd_hi,
686                rn_lo,
687                rn_hi,
688                rm_lo,
689                rm_hi: _,
690            } => {
691                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
692                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
693                let ml = reg_to_bits(rm_lo);
694                if dh == nl || dh == ml {
695                    return Err(synth_core::Error::synthesis(format!(
696                        "I64Shl (A32): rd_hi {rd_hi:?} aliases an input ({rn_lo:?}/{rm_lo:?}) still live inside the expansion (#1048)"
697                    )));
698                }
699                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
700                w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
701                w(&mut b, 0x5A00_0007); //                            BPL  .large
702                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
703                shift_reg(&mut b, LSL, dh, nh, 12); //               dh = hi << n
704                w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB  r12, r12, #32
705                shift_reg(&mut b, LSR, 12, nl, 12); //               r12 = lo >> (32-n)
706                w(&mut b, 0xE180_0000 | (dh << 16) | (dh << 12) | 12); // ORR dh, dh, r12
707                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
708                shift_reg(&mut b, LSL, dl, nl, 12); //               dl = lo << n
709                w(&mut b, 0xEA00_0001); //                            B    .done
710                shift_reg(&mut b, LSL, dh, nl, 12); //               .large: dh = lo << (n-32)
711                w(&mut b, 0xE3A0_0000 | (dl << 12)); //              MOV  dl, #0
712            }
713            ArmOp::I64ShrU {
714                rd_lo,
715                rd_hi,
716                rn_lo,
717                rn_hi,
718                rm_lo,
719                rm_hi: _,
720            } => {
721                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
722                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
723                let ml = reg_to_bits(rm_lo);
724                if dl == nh || dl == ml {
725                    return Err(synth_core::Error::synthesis(format!(
726                        "I64ShrU (A32): rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
727                    )));
728                }
729                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
730                w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
731                w(&mut b, 0x5A00_0007); //                            BPL  .large
732                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
733                shift_reg(&mut b, LSR, dl, nl, 12); //               dl = lo >> n
734                w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB  r12, r12, #32
735                shift_reg(&mut b, LSL, 12, nh, 12); //               r12 = hi << (32-n)
736                w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | 12); // ORR dl, dl, r12
737                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
738                shift_reg(&mut b, LSR, dh, nh, 12); //               dh = hi >> n
739                w(&mut b, 0xEA00_0001); //                            B    .done
740                shift_reg(&mut b, LSR, dl, nh, 12); //               .large: dl = hi >> (n-32)
741                w(&mut b, 0xE3A0_0000 | (dh << 12)); //              MOV  dh, #0
742            }
743            ArmOp::I64ShrS {
744                rd_lo,
745                rd_hi,
746                rn_lo,
747                rn_hi,
748                rm_lo,
749                rm_hi: _,
750            } => {
751                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
752                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
753                let ml = reg_to_bits(rm_lo);
754                if dl == nh || dl == ml {
755                    return Err(synth_core::Error::synthesis(format!(
756                        "I64ShrS (A32): rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
757                    )));
758                }
759                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
760                w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
761                w(&mut b, 0x5A00_0007); //                            BPL  .large
762                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
763                shift_reg(&mut b, LSR, dl, nl, 12); //               dl = lo >> n
764                w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB  r12, r12, #32
765                shift_reg(&mut b, LSL, 12, nh, 12); //               r12 = hi << (32-n)
766                w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | 12); // ORR dl, dl, r12
767                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
768                shift_reg(&mut b, ASR, dh, nh, 12); //               dh = hi >> n (arith)
769                w(&mut b, 0xEA00_0001); //                            B    .done
770                shift_reg(&mut b, ASR, dl, nh, 12); //               .large: dl = hi >> (n-32)
771                w(&mut b, 0xE1A0_0040 | (dh << 12) | (31 << 7) | nh); // ASR dh, nh, #31
772            }
773
774            // I64Rotl / I64Rotr: the #610 fixed-ABI wrapper (A32 form) around
775            // the same fixed-register core as the Thumb-2 arms — value in
776            // R0:R1, amount in R2, scratch R3 + R12.
777            ArmOp::I64Rotl {
778                rdlo,
779                rdhi,
780                rnlo,
781                rnhi,
782                shift,
783            } => {
784                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
785                for word in [
786                    0xE202_203Fu32, // AND  R2, R2, #63   (mask amount mod 64)
787                    0xE252_3020,    // SUBS R3, R2, #32   (R3 = n-32, sets N)
788                    0x5A00_0007,    // BPL  .large        (n >= 32)
789                    // --- small rotation (n < 32) ---
790                    0xE262_3020, // RSB  R3, R2, #32   (R3 = 32-n)
791                    0xE1A0_C330, // LSR  R12, R0, R3   (lo >> (32-n))
792                    0xE1A0_3331, // LSR  R3, R1, R3    (hi >> (32-n))
793                    0xE1A0_1211, // LSL  R1, R1, R2    (hi << n)
794                    0xE181_100C, // ORR  R1, R1, R12   (new_hi)
795                    0xE1A0_0210, // LSL  R0, R0, R2    (lo << n)
796                    0xE180_0003, // ORR  R0, R0, R3    (new_lo)
797                    0xEA00_0007, // B    .done
798                    // --- large rotation (n >= 32), R3 = m = n-32 ---
799                    0xE263_2020, // RSB  R2, R3, #32   (R2 = 32-m = 64-n)
800                    0xE1A0_C231, // LSR  R12, R1, R2   (hi >> (64-n))
801                    0xE1A0_2230, // LSR  R2, R0, R2    (lo >> (64-n))
802                    0xE1A0_0310, // LSL  R0, R0, R3    (lo << m)
803                    0xE1A0_1311, // LSL  R1, R1, R3    (hi << m)
804                    0xE180_C00C, // ORR  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
805                    0xE181_0002, // ORR  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
806                    0xE1A0_100C, // MOV  R1, R12       (new_hi into place)
807                ] {
808                    w(&mut b, word);
809                }
810                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
811            }
812            ArmOp::I64Rotr {
813                rdlo,
814                rdhi,
815                rnlo,
816                rnhi,
817                shift,
818            } => {
819                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
820                for word in [
821                    0xE202_203Fu32, // AND  R2, R2, #63   (mask amount mod 64)
822                    0xE252_3020,    // SUBS R3, R2, #32   (R3 = n-32, sets N)
823                    0x5A00_0007,    // BPL  .large        (n >= 32)
824                    // --- small rotation (n < 32) ---
825                    0xE262_3020, // RSB  R3, R2, #32   (R3 = 32-n)
826                    0xE1A0_C311, // LSL  R12, R1, R3   (hi << (32-n))
827                    0xE1A0_3310, // LSL  R3, R0, R3    (lo << (32-n))
828                    0xE1A0_0230, // LSR  R0, R0, R2    (lo >> n)
829                    0xE180_000C, // ORR  R0, R0, R12   (new_lo)
830                    0xE1A0_1231, // LSR  R1, R1, R2    (hi >> n)
831                    0xE181_1003, // ORR  R1, R1, R3    (new_hi)
832                    0xEA00_0007, // B    .done
833                    // --- large rotation (n >= 32), R3 = m = n-32 ---
834                    0xE263_2020, // RSB  R2, R3, #32   (R2 = 32-m = 64-n)
835                    0xE1A0_C210, // LSL  R12, R0, R2   (lo << (64-n))
836                    0xE1A0_2211, // LSL  R2, R1, R2    (hi << (64-n))
837                    0xE1A0_1331, // LSR  R1, R1, R3    (hi >> m)
838                    0xE181_C00C, // ORR  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
839                    0xE1A0_1330, // LSR  R1, R0, R3    (lo >> m)
840                    0xE181_1002, // ORR  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
841                    0xE1A0_000C, // MOV  R0, R12       (new_lo into place)
842                ] {
843                    w(&mut b, word);
844                }
845                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
846            }
847
848            // I64Clz: CLZ(hi), or 32 + CLZ(lo) when hi == 0. Conditional
849            // execution replaces the Thumb branches; like the Thumb arm, the
850            // high word of the result pair (rnhi) is cleared last.
851            ArmOp::I64Clz { rd, rnlo, rnhi } => {
852                let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
853                w(&mut b, 0xE350_0000 | (hi << 16)); //              CMP   rnhi, #0
854                w(&mut b, 0x116F_0F10 | (rd_b << 12) | hi); //       CLZNE rd, rnhi
855                w(&mut b, 0x016F_0F10 | (rd_b << 12) | lo); //       CLZEQ rd, rnlo
856                w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
857                // #1048: the former trailing `MOV rnhi, #0` is GONE — it
858                // wrote the OPERAND's home high register (see the Thumb-2
859                // I64Clz comment). Callers that relied on the implicit clear
860                // emit their own explicit hi-zero op.
861            }
862
863            // I64Ctz: CLZ(RBIT(lo)), or 32 + CLZ(RBIT(hi)) when lo == 0.
864            // RBIT/CLZ leave the flags intact, so the CMP's Z survives to the
865            // conditional ADD.
866            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
867                let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
868                w(&mut b, 0xE350_0000 | (lo << 16)); //              CMP    rnlo, #0
869                w(&mut b, 0x16FF_0F30 | (rd_b << 12) | lo); //       RBITNE rd, rnlo
870                w(&mut b, 0x06FF_0F30 | (rd_b << 12) | hi); //       RBITEQ rd, rnhi
871                w(&mut b, 0xE16F_0F10 | (rd_b << 12) | rd_b); //     CLZ    rd, rd
872                w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
873                // #1048: no trailing `MOV rnhi, #0` — see I64Clz above.
874            }
875
876            // I64Const: MOVW/MOVT per half (MOVT elided when the half fits in
877            // 16 bits, mirroring the Thumb-2 arm).
878            ArmOp::I64Const { rdlo, rdhi, value } => {
879                let lo32 = *value as u32;
880                let hi32 = (*value >> 32) as u32;
881                movw(&mut b, reg_to_bits(rdlo), lo32 & 0xFFFF);
882                if lo32 > 0xFFFF {
883                    movt(&mut b, reg_to_bits(rdlo), lo32 >> 16);
884                }
885                movw(&mut b, reg_to_bits(rdhi), hi32 & 0xFFFF);
886                if hi32 > 0xFFFF {
887                    movt(&mut b, reg_to_bits(rdhi), hi32 >> 16);
888                }
889            }
890
891            // I64Ldr / I64Str: two word accesses at [base, #off] / #off+4.
892            // A register offset is materialized into IP once (the #206/#372
893            // hazard: dropping it would read the wrong address).
894            // RQ-63-ARMI64OFF (#1165): an offset the pair form cannot hold
895            // (> 0xFFB, so the high half's +4 leaves imm12) used to be a loud
896            // decline here — 46 of the 110 core-module declines in v0.62's ARM
897            // census, all on the `-b arm` (Arm32) default target. It is now
898            // MATERIALIZED (MOVW/MOVT + ADD, the A32 mirror of #382's Thumb-2
899            // `i64_effective_base`); in-range offsets stay byte-identical.
900            ArmOp::I64Ldr { rdlo, rdhi, addr } | ArmOp::I64Str { rdlo, rdhi, addr } => {
901                let (base, off) = a32_effective_base(&mut b, addr, A32_I64_PAIR_IMM12_MAX)?;
902                let base = reg_to_bits(&base);
903                let opc: u32 = if matches!(op, ArmOp::I64Ldr { .. }) {
904                    0xE590_0000 // LDR
905                } else {
906                    0xE580_0000 // STR
907                };
908                w(&mut b, opc | (base << 16) | (reg_to_bits(rdlo) << 12) | off);
909                w(
910                    &mut b,
911                    opc | (base << 16) | (reg_to_bits(rdhi) << 12) | (off + 4),
912                );
913            }
914
915            // I64ExtendI32S: rdlo = rn; rdhi = rdlo >> 31 (arithmetic).
916            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
917                if rdlo != rn {
918                    w(
919                        &mut b,
920                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
921                    );
922                }
923                w(
924                    &mut b,
925                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
926                );
927            }
928
929            // I64ExtendI32U: rdlo = rn; rdhi = 0.
930            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
931                if rdlo != rn {
932                    w(
933                        &mut b,
934                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
935                    );
936                }
937                w(&mut b, 0xE3A0_0000 | (reg_to_bits(rdhi) << 12));
938            }
939
940            // I64Extend8S / I64Extend16S: SXTB/SXTH then sign-fill the high word.
941            ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
942                w(
943                    &mut b,
944                    0xE6AF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
945                );
946                w(
947                    &mut b,
948                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
949                );
950            }
951            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
952                w(
953                    &mut b,
954                    0xE6BF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
955                );
956                w(
957                    &mut b,
958                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
959                );
960            }
961            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
962                if rdlo != rnlo {
963                    w(
964                        &mut b,
965                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
966                    );
967                }
968                w(
969                    &mut b,
970                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rnlo),
971                );
972            }
973
974            // I32WrapI64: take the low word. When rd == rnlo this is a genuine
975            // no-op (the one case where a NOP word is the correct encoding).
976            ArmOp::I32WrapI64 { rd, rnlo } => {
977                w(
978                    &mut b,
979                    0xE1A0_0000 | (reg_to_bits(rd) << 12) | reg_to_bits(rnlo),
980                );
981            }
982
983            // I64Add / I64Sub: the classic pair — ADDS lo + ADC hi (SUBS/SBC).
984            // The selector emits these as separate Adds/Adc ops; the fused
985            // variants are verification-constructed, but they encode for real.
986            ArmOp::I64Add {
987                rdlo,
988                rdhi,
989                rnlo,
990                rnhi,
991                rmlo,
992                rmhi,
993            } => {
994                dp_reg(
995                    &mut b,
996                    0xE090_0000, // ADDS
997                    reg_to_bits(rdlo),
998                    reg_to_bits(rnlo),
999                    reg_to_bits(rmlo),
1000                );
1001                dp_reg(
1002                    &mut b,
1003                    0xE0A0_0000, // ADC
1004                    reg_to_bits(rdhi),
1005                    reg_to_bits(rnhi),
1006                    reg_to_bits(rmhi),
1007                );
1008            }
1009            ArmOp::I64Sub {
1010                rdlo,
1011                rdhi,
1012                rnlo,
1013                rnhi,
1014                rmlo,
1015                rmhi,
1016            } => {
1017                dp_reg(
1018                    &mut b,
1019                    0xE050_0000, // SUBS
1020                    reg_to_bits(rdlo),
1021                    reg_to_bits(rnlo),
1022                    reg_to_bits(rmlo),
1023                );
1024                dp_reg(
1025                    &mut b,
1026                    0xE0C0_0000, // SBC
1027                    reg_to_bits(rdhi),
1028                    reg_to_bits(rnhi),
1029                    reg_to_bits(rmhi),
1030                );
1031            }
1032
1033            // I64And / I64Or / I64Xor: two independent word ops.
1034            ArmOp::I64And {
1035                rdlo,
1036                rdhi,
1037                rnlo,
1038                rnhi,
1039                rmlo,
1040                rmhi,
1041            }
1042            | ArmOp::I64Or {
1043                rdlo,
1044                rdhi,
1045                rnlo,
1046                rnhi,
1047                rmlo,
1048                rmhi,
1049            }
1050            | ArmOp::I64Xor {
1051                rdlo,
1052                rdhi,
1053                rnlo,
1054                rnhi,
1055                rmlo,
1056                rmhi,
1057            } => {
1058                let base = match op {
1059                    ArmOp::I64And { .. } => 0xE000_0000, // AND
1060                    ArmOp::I64Or { .. } => 0xE180_0000,  // ORR
1061                    _ => 0xE020_0000,                    // EOR
1062                };
1063                dp_reg(
1064                    &mut b,
1065                    base,
1066                    reg_to_bits(rdlo),
1067                    reg_to_bits(rnlo),
1068                    reg_to_bits(rmlo),
1069                );
1070                dp_reg(
1071                    &mut b,
1072                    base,
1073                    reg_to_bits(rdhi),
1074                    reg_to_bits(rnhi),
1075                    reg_to_bits(rmhi),
1076                );
1077            }
1078
1079            // I64DivU: binary long division — A32 transcription of the Thumb-2
1080            // #610/#613 arm (fixed-ABI marshal, zero-divisor trap, 64-round
1081            // shift-subtract core, quotient to R0:R1, result to rd pair).
1082            ArmOp::I64DivU {
1083                rdlo,
1084                rdhi,
1085                rnlo,
1086                rnhi,
1087                rmlo,
1088                rmhi,
1089                elide_zero_guard,
1090            } => {
1091                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1092                // #494 phase 2b: elided only under a certificate-discharged
1093                // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
1094                if !elide_zero_guard {
1095                    emit_a32_i64_divisor_zero_trap(&mut b);
1096                }
1097                w(&mut b, 0xE92D_00F0); // PUSH {R4-R7}
1098                for r in 4..8u32 {
1099                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1100                }
1101                div_loop(&mut b, 12); // counter in R12 (encoder scratch)
1102                w(&mut b, 0xE1A0_0004); // MOV R0, R4 (quotient lo)
1103                w(&mut b, 0xE1A0_1005); // MOV R1, R5 (quotient hi)
1104                w(&mut b, 0xE8BD_00F0); // POP {R4-R7}
1105                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1106            }
1107
1108            // I64DivS: sign-extract, unsigned core, conditional negate —
1109            // A32 transcription of the Thumb-2 arm.
1110            ArmOp::I64DivS {
1111                rdlo,
1112                rdhi,
1113                rnlo,
1114                rnhi,
1115                rmlo,
1116                rmhi,
1117                elide_zero_guard,
1118                elide_overflow_guard,
1119            } => {
1120                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1121                // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
1122                // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
1123                // the #633 overflow guard falls ONLY to
1124                // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
1125                // divisor-nonzero fact alone must keep it.
1126                if !elide_zero_guard {
1127                    emit_a32_i64_divisor_zero_trap(&mut b);
1128                }
1129                if !elide_overflow_guard {
1130                    // #633: INT64_MIN / -1 overflows — trap like the i32 path
1131                    // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
1132                    emit_a32_i64_divs_overflow_trap(&mut b);
1133                }
1134                w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1135                w(&mut b, 0xE021_9003); // EOR R9, R1, R3 (result sign in MSB)
1136                skip_negate_if_positive(&mut b, 1);
1137                negate64(&mut b, 0, 1);
1138                skip_negate_if_positive(&mut b, 3);
1139                negate64(&mut b, 2, 3);
1140                for r in 4..8u32 {
1141                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1142                }
1143                div_loop(&mut b, 8); // counter in R8 (saved above)
1144                w(&mut b, 0xE1A0_0004); // MOV R0, R4
1145                w(&mut b, 0xE1A0_1005); // MOV R1, R5
1146                skip_negate_if_positive(&mut b, 9);
1147                negate64(&mut b, 0, 1);
1148                w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1149                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1150            }
1151
1152            // I64RemU: same core as I64DivU, returns the remainder (R6:R7).
1153            ArmOp::I64RemU {
1154                rdlo,
1155                rdhi,
1156                rnlo,
1157                rnhi,
1158                rmlo,
1159                rmhi,
1160                elide_zero_guard,
1161            } => {
1162                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1163                if !elide_zero_guard {
1164                    emit_a32_i64_divisor_zero_trap(&mut b);
1165                }
1166                w(&mut b, 0xE92D_01F0); // PUSH {R4-R8}
1167                for r in 4..8u32 {
1168                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1169                }
1170                div_loop(&mut b, 8);
1171                w(&mut b, 0xE1A0_0006); // MOV R0, R6 (remainder lo)
1172                w(&mut b, 0xE1A0_1007); // MOV R1, R7 (remainder hi)
1173                w(&mut b, 0xE8BD_01F0); // POP {R4-R8}
1174                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1175            }
1176
1177            // I64RemS: remainder takes the DIVIDEND's sign (WASM semantics).
1178            ArmOp::I64RemS {
1179                rdlo,
1180                rdhi,
1181                rnlo,
1182                rnhi,
1183                rmlo,
1184                rmhi,
1185                elide_zero_guard,
1186            } => {
1187                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1188                if !elide_zero_guard {
1189                    emit_a32_i64_divisor_zero_trap(&mut b);
1190                }
1191                w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1192                w(&mut b, 0xE1A0_9001); // MOV R9, R1 (dividend sign)
1193                skip_negate_if_positive(&mut b, 1);
1194                negate64(&mut b, 0, 1);
1195                skip_negate_if_positive(&mut b, 3);
1196                negate64(&mut b, 2, 3);
1197                for r in 4..8u32 {
1198                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1199                }
1200                div_loop(&mut b, 8);
1201                w(&mut b, 0xE1A0_0006); // MOV R0, R6
1202                w(&mut b, 0xE1A0_1007); // MOV R1, R7
1203                skip_negate_if_positive(&mut b, 9);
1204                negate64(&mut b, 0, 1);
1205                w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1206                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1207            }
1208
1209            // Popcnt (i32): bit-twiddle expansion (no native A32 popcount),
1210            // mirroring the Thumb-2 arm's #1021 register contract: R12 is the
1211            // ONLY scratch. The previous transcription copied the old Thumb
1212            // contract's R11 borrow — but R11 is the linear-memory base on
1213            // this path too, so it inherited the same live miscompile. A32
1214            // has no ThumbExpandImm for 0xXYXYXYXY masks, so instead the
1215            // barrel shifter folds each shift into the mask AND itself
1216            // (`AND R12, R12, rd, LSR #n`), and step 2 recovers `x & C` from
1217            // one term via `x - (((x >> 2) & C) << 2) = x & C` — the second
1218            // temp disappears algebraically. Straight-line, no PUSH/POP,
1219            // nothing to skip on a trap edge.
1220            ArmOp::Popcnt { rd, rm } => {
1221                let rd_b = reg_to_bits(rd);
1222                // Defensive (#1021), same contract as the Thumb-2 arm.
1223                if rd_b >= 11 {
1224                    return Err(synth_core::Error::synthesis(
1225                        "Popcnt destination must be R0-R10: R11 is the linear-memory \
1226                         base and R12 is the expansion's scratch (#1021)",
1227                    ));
1228                }
1229                if rd != rm {
1230                    w(&mut b, 0xE1A0_0000 | (rd_b << 12) | reg_to_bits(rm)); // MOV rd, rm
1231                }
1232                // x = x - ((x >> 1) & 0x55555555)
1233                movw(&mut b, 12, 0x5555);
1234                movt(&mut b, 12, 0x5555);
1235                dp_reg_shift(&mut b, 0xE000_0000, 12, 12, rd_b, LSR, 1); // AND R12, R12, rd, LSR #1
1236                dp_reg(&mut b, 0xE040_0000, rd_b, rd_b, 12); //             SUB rd, rd, R12
1237                // x = (x & 0x33333333) + ((x >> 2) & 0x33333333), one temp:
1238                //   R12 = (x >> 2) & C; x - (R12 << 2) = x & C; then + R12.
1239                movw(&mut b, 12, 0x3333);
1240                movt(&mut b, 12, 0x3333);
1241                dp_reg_shift(&mut b, 0xE000_0000, 12, 12, rd_b, LSR, 2); // AND R12, R12, rd, LSR #2
1242                dp_reg_shift(&mut b, 0xE040_0000, rd_b, rd_b, 12, LSL, 2); // SUB rd, rd, R12, LSL #2
1243                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 12); //              ADD rd, rd, R12
1244                // x = (x + (x >> 4)) & 0x0F0F0F0F
1245                dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 4); // ADD rd, rd, rd, LSR #4
1246                movw(&mut b, 12, 0x0F0F);
1247                movt(&mut b, 12, 0x0F0F);
1248                dp_reg(&mut b, 0xE000_0000, rd_b, rd_b, 12); // AND rd, rd, R12
1249                // x += x >> 8; x += x >> 16; x &= 0x3F
1250                dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 8);
1251                dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 16);
1252                w(&mut b, 0xE200_003F | (rd_b << 16) | (rd_b << 12)); // AND rd, rd, #63
1253            }
1254
1255            // I64Popcnt: POPCNT(lo) + POPCNT(hi) — A32 transcription of the
1256            // Thumb-2 arm (R3/R4/R5 saved, mul-based per-word fold, high
1257            // result word rnhi cleared last, mirroring the Thumb contract).
1258            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
1259                let hi = reg_to_bits(rnhi);
1260                w(&mut b, 0xE92D_0038); // PUSH {R3, R4, R5}
1261                // #632 audit: route rnlo through R12 so a pair living at
1262                // (R3,R4) cannot read a clobbered R4 (sources read before any
1263                // scratch register they could occupy is written).
1264                w(&mut b, 0xE1A0_C000 | reg_to_bits(rnlo)); // MOV R12, rnlo
1265                w(&mut b, 0xE1A0_5000 | hi); //                MOV R5, rnhi
1266                w(&mut b, 0xE1A0_400C); //                     MOV R4, R12
1267                popcnt_word(&mut b, 4, 3);
1268                popcnt_word(&mut b, 5, 3);
1269                // #632: carry the count across the scratch restore in R12 —
1270                // rd is allocator-assigned and can land inside {R3,R4,R5};
1271                // the old `ADD rd, R4, R5` before the POP was destroyed by
1272                // the restore. R12 is never allocatable and never restored.
1273                dp_reg(&mut b, 0xE080_0000, 12, 4, 5); // ADD R12, R4, R5
1274                w(&mut b, 0xE8BD_0038); // POP {R3, R4, R5}
1275                w(&mut b, 0xE1A0_0000 | (reg_to_bits(rd) << 12) | 12); // MOV rd, R12
1276                // #1048: no trailing `MOV rnhi, #0` — the hi-word clear wrote
1277                // the OPERAND's home high register; callers emit it explicitly.
1278            }
1279
1280            _ => return Ok(None),
1281        }
1282        Ok(Some(b))
1283    }
1284
1285    fn encode_arm(&self, op: &ArmOp) -> Result<Vec<u8>> {
1286        // #615: A32 multi-instruction expansions (i64 arithmetic/shift/rotate/
1287        // compare, SetCond/SelectMove, popcnt, ...). These ops were literal
1288        // NOPs on the A32 path — user-reachable via `--target cortex-r5` —
1289        // so the value silently vanished. Mirror of the #594 CallIndirect
1290        // early-return: if the expansion helper covers the op, its bytes are
1291        // the encoding.
1292        if let Some(bytes) = self.encode_arm_expanded(op)? {
1293            return Ok(bytes);
1294        }
1295        // #206: ARM32 register-offset loads/stores. `encode_mem_addr` only
1296        // returns the 12-bit immediate, so the immediate-form arms below
1297        // silently DROP `addr.offset_reg` — a runtime address index vanished,
1298        // turning `ldr rd,[rn,rm,#off]` into `ldr rd,[rn,#off]` (the access went
1299        // to the wrong address). Compute the effective base into IP and re-encode
1300        // against `[ip, #off]`, which is uniform for word/byte/halfword/signed.
1301        if let Some(bytes) = self.encode_arm_reg_offset_mem(op)? {
1302            return Ok(bytes);
1303        }
1304        // #594: call_indirect was encoded as a literal NOP on the A32 path
1305        // (`--target cortex-r5`) — the call never happened and the function
1306        // silently returned garbage. Emit the same three-instruction expansion
1307        // as the Thumb-2 path (R11 = function-pointer table base, R12 scratch):
1308        //   MOV r12, idx, LSL #2 ; LDR r12, [r11, r12] ; BLX r12
1309        if let ArmOp::CallIndirect {
1310            table_index_reg,
1311            table_size,
1312            table_byte_offset,
1313            null_check,
1314            type_check,
1315            ..
1316        } = op
1317        {
1318            return Ok(Self::encode_arm_call_indirect(
1319                table_index_reg,
1320                *table_size,
1321                *table_byte_offset,
1322                *null_check,
1323                *type_check,
1324            ));
1325        }
1326        let instr: u32 = match op {
1327            // Data processing instructions
1328            ArmOp::Add { rd, rn, op2 } => {
1329                let rd_bits = reg_to_bits(rd);
1330                let rn_bits = reg_to_bits(rn);
1331                let (op2_bits, i_flag) = encode_operand2(op2)?;
1332
1333                // ADD encoding: cond(4) | 00 | I(1) | 0100 | S(1) | Rn(4) | Rd(4) | operand2(12)
1334                0xE0800000 // condition=always(E), opcode=ADD(0100), S=0
1335                    | (i_flag << 25)
1336                    | (rn_bits << 16)
1337                    | (rd_bits << 12)
1338                    | op2_bits
1339            }
1340
1341            ArmOp::Sub { rd, rn, op2 } => {
1342                let rd_bits = reg_to_bits(rd);
1343                let rn_bits = reg_to_bits(rn);
1344                let (op2_bits, i_flag) = encode_operand2(op2)?;
1345
1346                // SUB encoding: opcode=0010
1347                0xE0400000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1348            }
1349
1350            // i64 support: ADDS, ADC, SUBS, SBC for ARM32
1351            ArmOp::Adds { rd, rn, op2 } => {
1352                let rd_bits = reg_to_bits(rd);
1353                let rn_bits = reg_to_bits(rn);
1354                let (op2_bits, i_flag) = encode_operand2(op2)?;
1355
1356                // ADDS encoding: opcode=0100, S=1
1357                0xE0900000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1358            }
1359
1360            ArmOp::Adc { rd, rn, op2 } => {
1361                let rd_bits = reg_to_bits(rd);
1362                let rn_bits = reg_to_bits(rn);
1363                let (op2_bits, i_flag) = encode_operand2(op2)?;
1364
1365                // ADC encoding: opcode=0101
1366                0xE0A00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1367            }
1368
1369            ArmOp::Subs { rd, rn, op2 } => {
1370                let rd_bits = reg_to_bits(rd);
1371                let rn_bits = reg_to_bits(rn);
1372                let (op2_bits, i_flag) = encode_operand2(op2)?;
1373
1374                // SUBS encoding: opcode=0010, S=1
1375                0xE0500000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1376            }
1377
1378            ArmOp::Sbc { rd, rn, op2 } => {
1379                let rd_bits = reg_to_bits(rd);
1380                let rn_bits = reg_to_bits(rn);
1381                let (op2_bits, i_flag) = encode_operand2(op2)?;
1382
1383                // SBC encoding: opcode=0110
1384                0xE0C00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1385            }
1386
1387            ArmOp::Mul { rd, rn, rm } => {
1388                let rd_bits = reg_to_bits(rd);
1389                let rn_bits = reg_to_bits(rn);
1390                let rm_bits = reg_to_bits(rm);
1391
1392                // MUL encoding: cond(4) | 000000 | A(1) | S(1) | Rd(4) | Rn(4) | Rs(4) | 1001 | Rm(4)
1393                0xE0000090 | (rd_bits << 16) | (rn_bits << 8) | rm_bits
1394            }
1395
1396            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
1397                let rdlo_bits = reg_to_bits(rdlo);
1398                let rdhi_bits = reg_to_bits(rdhi);
1399                let rn_bits = reg_to_bits(rn);
1400                let rm_bits = reg_to_bits(rm);
1401
1402                // UMULL encoding: cond(4) | 0000 1000 | RdHi(4) | RdLo(4) | Rm(4) | 1001 | Rn(4)
1403                0xE0800090 | (rdhi_bits << 16) | (rdlo_bits << 12) | (rm_bits << 8) | rn_bits
1404            }
1405
1406            ArmOp::Sdiv { rd, rn, rm } => {
1407                let rd_bits = reg_to_bits(rd);
1408                let rn_bits = reg_to_bits(rn);
1409                let rm_bits = reg_to_bits(rm);
1410
1411                // SDIV encoding: cond(4) | 01110001 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1412                // ARMv7-M and above
1413                0xE710F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1414            }
1415
1416            ArmOp::Udiv { rd, rn, rm } => {
1417                let rd_bits = reg_to_bits(rd);
1418                let rn_bits = reg_to_bits(rn);
1419                let rm_bits = reg_to_bits(rm);
1420
1421                // UDIV encoding: cond(4) | 01110011 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1422                // ARMv7-M and above
1423                0xE730F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1424            }
1425
1426            ArmOp::Mls { rd, rn, rm, ra } => {
1427                let rd_bits = reg_to_bits(rd);
1428                let rn_bits = reg_to_bits(rn);
1429                let rm_bits = reg_to_bits(rm);
1430                let ra_bits = reg_to_bits(ra);
1431
1432                // MLS encoding: cond(4) | 00000110 | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1433                // Rd = Ra - (Rn * Rm)
1434                0xE0600090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1435            }
1436
1437            ArmOp::Mla { rd, rn, rm, ra } => {
1438                let rd_bits = reg_to_bits(rd);
1439                let rn_bits = reg_to_bits(rn);
1440                let rm_bits = reg_to_bits(rm);
1441                let ra_bits = reg_to_bits(ra);
1442
1443                // MLA encoding: cond(4) | 0000001 S | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1444                // Rd = Ra + (Rn * Rm). Base 0xE0200090 (S=0).
1445                0xE0200090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1446            }
1447
1448            ArmOp::And { rd, rn, op2 } => {
1449                let rd_bits = reg_to_bits(rd);
1450                let rn_bits = reg_to_bits(rn);
1451                let (op2_bits, i_flag) = encode_operand2(op2)?;
1452
1453                // AND encoding: opcode=0000
1454                0xE0000000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1455            }
1456
1457            ArmOp::Orr { rd, rn, op2 } => {
1458                let rd_bits = reg_to_bits(rd);
1459                let rn_bits = reg_to_bits(rn);
1460                let (op2_bits, i_flag) = encode_operand2(op2)?;
1461
1462                // ORR encoding: opcode=1100
1463                0xE1800000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1464            }
1465
1466            ArmOp::Eor { rd, rn, op2 } => {
1467                let rd_bits = reg_to_bits(rd);
1468                let rn_bits = reg_to_bits(rn);
1469                let (op2_bits, i_flag) = encode_operand2(op2)?;
1470
1471                // EOR encoding: opcode=0001
1472                0xE0200000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1473            }
1474
1475            // Shift instructions
1476            ArmOp::Lsl { rd, rn, shift } => {
1477                let rd_bits = reg_to_bits(rd);
1478                let rn_bits = reg_to_bits(rn);
1479                let shift_bits = *shift & 0x1F;
1480
1481                // LSL encoding: MOV with shift
1482                0xE1A00000 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1483            }
1484
1485            ArmOp::Lsr { rd, rn, shift } => {
1486                let rd_bits = reg_to_bits(rd);
1487                let rn_bits = reg_to_bits(rn);
1488                let shift_bits = *shift & 0x1F;
1489
1490                // LSR encoding
1491                0xE1A00020 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1492            }
1493
1494            ArmOp::Asr { rd, rn, shift } => {
1495                let rd_bits = reg_to_bits(rd);
1496                let rn_bits = reg_to_bits(rn);
1497                let shift_bits = *shift & 0x1F;
1498
1499                // ASR encoding
1500                0xE1A00040 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1501            }
1502
1503            ArmOp::Ror { rd, rn, shift } => {
1504                let rd_bits = reg_to_bits(rd);
1505                let rn_bits = reg_to_bits(rn);
1506                let shift_bits = *shift & 0x1F;
1507
1508                // ROR encoding: MOV with ROR shift
1509                0xE1A00060 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1510            }
1511
1512            // Register-based shifts (ARM32)
1513            // LSL Rd, Rn, Rm: cond 0001101S 0000 Rd Rs 0001 Rn
1514            ArmOp::LslReg { rd, rn, rm } => {
1515                let rd_bits = reg_to_bits(rd);
1516                let rn_bits = reg_to_bits(rn);
1517                let rm_bits = reg_to_bits(rm);
1518                0xE1A00010 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1519            }
1520            ArmOp::LsrReg { rd, rn, rm } => {
1521                let rd_bits = reg_to_bits(rd);
1522                let rn_bits = reg_to_bits(rn);
1523                let rm_bits = reg_to_bits(rm);
1524                0xE1A00030 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1525            }
1526            ArmOp::AsrReg { rd, rn, rm } => {
1527                let rd_bits = reg_to_bits(rd);
1528                let rn_bits = reg_to_bits(rn);
1529                let rm_bits = reg_to_bits(rm);
1530                0xE1A00050 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1531            }
1532            ArmOp::RorReg { rd, rn, rm } => {
1533                let rd_bits = reg_to_bits(rd);
1534                let rn_bits = reg_to_bits(rn);
1535                let rm_bits = reg_to_bits(rm);
1536                0xE1A00070 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1537            }
1538
1539            // RSB (Reverse Subtract): Rd = imm - Rn
1540            ArmOp::Rsb { rd, rn, imm } => {
1541                let rd_bits = reg_to_bits(rd);
1542                let rn_bits = reg_to_bits(rn);
1543                // RSB encoding: cond(4) | 00 1 0011 S | Rn(4) | Rd(4) | imm12
1544                // Opcode for RSB = 0011, I=1 (immediate), S=0
1545                //
1546                // #681 class audit: the A32 imm12 is a rotate(4):imm8 modified
1547                // immediate; `*imm & 0xFF` silently encoded a WRONG constant
1548                // for imm > 0xFF (#378 masking class). All current emitters use
1549                // imm 32, so erroring here is byte-identical for real codegen.
1550                if *imm > 0xFF {
1551                    return Err(synth_core::Error::synthesis(
1552                        "A32 RSB immediate > 0xFF requires a rotated-immediate encoding \
1553                         (not supported) — materialize into a register",
1554                    ));
1555                }
1556                0xE2600000 | (rn_bits << 16) | (rd_bits << 12) | (*imm & 0xFF)
1557            }
1558
1559            // Bit manipulation instructions
1560            ArmOp::Clz { rd, rm } => {
1561                let rd_bits = reg_to_bits(rd);
1562                let rm_bits = reg_to_bits(rm);
1563
1564                // CLZ encoding: cond(4) | 00010110 | 1111 | Rd(4) | 1111 | 0001 | Rm(4)
1565                // ARMv5T and above
1566                0xE16F0F10 | (rd_bits << 12) | rm_bits
1567            }
1568
1569            ArmOp::Rbit { rd, rm } => {
1570                let rd_bits = reg_to_bits(rd);
1571                let rm_bits = reg_to_bits(rm);
1572
1573                // RBIT encoding: cond(4) | 01101111 | 1111 | Rd(4) | 1111 | 0011 | Rm(4)
1574                // ARMv6T2 and above
1575                0xE6FF0F30 | (rd_bits << 12) | rm_bits
1576            }
1577
1578            ArmOp::Sxtb { rd, rm } => {
1579                let rd_bits = reg_to_bits(rd);
1580                let rm_bits = reg_to_bits(rm);
1581
1582                // SXTB encoding: cond(4) | 01101010 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1583                // ARMv6 and above. rotate=00 for no rotation
1584                0xE6AF0070 | (rd_bits << 12) | rm_bits
1585            }
1586
1587            ArmOp::Sxth { rd, rm } => {
1588                let rd_bits = reg_to_bits(rd);
1589                let rm_bits = reg_to_bits(rm);
1590
1591                // SXTH encoding: cond(4) | 01101011 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1592                // ARMv6 and above. rotate=00 for no rotation
1593                0xE6BF0070 | (rd_bits << 12) | rm_bits
1594            }
1595
1596            ArmOp::Uxtb { rd, rm } => {
1597                let rd_bits = reg_to_bits(rd);
1598                let rm_bits = reg_to_bits(rm);
1599                // UXTB encoding: cond | 01101110 1111 Rd rotate 00 0111 Rm (rotate=00)
1600                0xE6EF0070 | (rd_bits << 12) | rm_bits
1601            }
1602
1603            ArmOp::Uxth { rd, rm } => {
1604                let rd_bits = reg_to_bits(rd);
1605                let rm_bits = reg_to_bits(rm);
1606                // UXTH encoding: cond | 01101111 1111 Rd rotate 00 0111 Rm (rotate=00)
1607                0xE6FF0070 | (rd_bits << 12) | rm_bits
1608            }
1609
1610            // Move instructions
1611            ArmOp::Mov { rd, op2 } => {
1612                let rd_bits = reg_to_bits(rd);
1613                let (op2_bits, i_flag) = encode_operand2(op2)?;
1614
1615                // MOV encoding: opcode=1101
1616                0xE1A00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1617            }
1618
1619            ArmOp::Mvn { rd, op2 } => {
1620                let rd_bits = reg_to_bits(rd);
1621                let (op2_bits, i_flag) = encode_operand2(op2)?;
1622
1623                // MVN encoding: opcode=1111
1624                0xE1E00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1625            }
1626
1627            // MOVW - Move Wide (ARM32)
1628            // Encoding: cond(4) | 0011 0000 | imm4(4) | Rd(4) | imm12(12)
1629            ArmOp::Movw { rd, imm16 } => {
1630                let rd_bits = reg_to_bits(rd);
1631                let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1632                let imm12 = (*imm16 as u32) & 0xFFF;
1633                0xE3000000 | (imm4 << 16) | (rd_bits << 12) | imm12
1634            }
1635
1636            // MOVT - Move Top (ARM32)
1637            // Encoding: cond(4) | 0011 0100 | imm4(4) | Rd(4) | imm12(12)
1638            ArmOp::Movt { rd, imm16 } => {
1639                let rd_bits = reg_to_bits(rd);
1640                let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1641                let imm12 = (*imm16 as u32) & 0xFFF;
1642                0xE3400000 | (imm4 << 16) | (rd_bits << 12) | imm12
1643            }
1644
1645            // #237: symbol-relative MOVW/MOVT (ARM mode) — addend in place, the
1646            // backend records the MOVW_ABS/MOVT_ABS relocation against `symbol`.
1647            ArmOp::MovwSym { rd, addend, .. } => {
1648                let rd_bits = reg_to_bits(rd);
1649                let v = (*addend as u32) & 0xffff;
1650                0xE3000000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1651            }
1652            ArmOp::MovtSym { rd, addend, .. } => {
1653                let rd_bits = reg_to_bits(rd);
1654                let v = ((*addend as u32) >> 16) & 0xffff;
1655                0xE3400000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1656            }
1657
1658            // #345: LdrSym is the Thumb-2 literal-pool address load. A32 mode is
1659            // not used for relocatable native-pointer objects; fail loudly rather
1660            // than miscompile if it is ever reached here.
1661            ArmOp::LdrSym { .. } => {
1662                return Err(synth_core::Error::synthesis(
1663                    "LdrSym (literal-pool address load) is Thumb-2-only",
1664                ));
1665            }
1666
1667            // Compare
1668            ArmOp::Cmp { rn, op2 } => {
1669                let rn_bits = reg_to_bits(rn);
1670                let (op2_bits, i_flag) = encode_operand2(op2)?;
1671
1672                // CMP encoding: opcode=1010, S=1
1673                0xE1500000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1674            }
1675
1676            // Compare Negative (CMN) - computes Rn + op2 and sets flags
1677            ArmOp::Cmn { rn, op2 } => {
1678                let rn_bits = reg_to_bits(rn);
1679                let (op2_bits, i_flag) = encode_operand2(op2)?;
1680
1681                // CMN encoding: opcode=1011, S=1
1682                0xE1700000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1683            }
1684
1685            // Load/Store
1686            ArmOp::Ldr { rd, addr } => {
1687                let rd_bits = reg_to_bits(rd);
1688                let (base_bits, offset_bits) = encode_mem_addr(addr)?;
1689
1690                // LDR encoding: cond(4) | 01 | I(1) | P(1) | U(1) | B(1) | W(1) | L(1) | Rn(4) | Rd(4) | offset(12)
1691                // P=1 (pre-indexed), U=1 (add offset), L=1 (load)
1692                0xE5900000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1693            }
1694
1695            ArmOp::Str { rd, addr } => {
1696                let rd_bits = reg_to_bits(rd);
1697                let (base_bits, offset_bits) = encode_mem_addr(addr)?;
1698
1699                // STR encoding: L=0 (store)
1700                0xE5800000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1701            }
1702
1703            // Sub-word loads (ARM32 encoding)
1704            ArmOp::Ldrb { rd, addr } => {
1705                let rd_bits = reg_to_bits(rd);
1706                let (base_bits, offset_bits) = encode_mem_addr(addr)?;
1707                // LDRB: LDR with B=1 (byte): cond|01|I|P|U|1|W|L|Rn|Rd|offset
1708                0xE5D00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1709            }
1710
1711            ArmOp::Ldrsb { rd, addr } => {
1712                let rd_bits = reg_to_bits(rd);
1713                let (base_bits, offset_val) = encode_mem_addr_imm8(addr)?;
1714                // LDRSB (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1101|imm4L
1715                let imm4h = (offset_val >> 4) & 0xF;
1716                let imm4l = offset_val & 0xF;
1717                0xE1D000D0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1718            }
1719
1720            ArmOp::Ldrh { rd, addr } => {
1721                let rd_bits = reg_to_bits(rd);
1722                let (base_bits, offset_val) = encode_mem_addr_imm8(addr)?;
1723                // LDRH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1011|imm4L
1724                let imm4h = (offset_val >> 4) & 0xF;
1725                let imm4l = offset_val & 0xF;
1726                0xE1D000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1727            }
1728
1729            ArmOp::Ldrsh { rd, addr } => {
1730                let rd_bits = reg_to_bits(rd);
1731                let (base_bits, offset_val) = encode_mem_addr_imm8(addr)?;
1732                // LDRSH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1111|imm4L
1733                let imm4h = (offset_val >> 4) & 0xF;
1734                let imm4l = offset_val & 0xF;
1735                0xE1D000F0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1736            }
1737
1738            // Sub-word stores (ARM32 encoding)
1739            ArmOp::Strb { rd, addr } => {
1740                let rd_bits = reg_to_bits(rd);
1741                let (base_bits, offset_bits) = encode_mem_addr(addr)?;
1742                // STRB: STR with B=1 (byte): cond|01|I|P|U|1|W|0|Rn|Rd|offset
1743                0xE5C00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1744            }
1745
1746            ArmOp::Strh { rd, addr } => {
1747                let rd_bits = reg_to_bits(rd);
1748                let (base_bits, offset_val) = encode_mem_addr_imm8(addr)?;
1749                // STRH (misc store): cond|000|P|U|1|W|0|Rn|Rd|imm4H|1011|imm4L
1750                let imm4h = (offset_val >> 4) & 0xF;
1751                let imm4l = offset_val & 0xF;
1752                0xE1C000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1753            }
1754
1755            // Memory management (ARM32 encoding)
1756            ArmOp::MemorySize { rd } => {
1757                let rd_bits = reg_to_bits(rd);
1758                // MOV rd, R10, LSR #16  (memory size in bytes / 65536 = pages)
1759                // cond|000|1101|S|0000|Rd|shift5|type|0|Rm
1760                // LSR #16: shift5=10000, type=01
1761                0xE1A00820 | (rd_bits << 12) | 0x0A // Rm=R10, shift=16, LSR
1762            }
1763
1764            ArmOp::MemoryGrow { rd, .. } => {
1765                let rd_bits = reg_to_bits(rd);
1766                // On embedded, always fail: MOV rd, #-1
1767                0xE3E00000 | (rd_bits << 12) // MVN rd, #0 = MOV rd, #-1
1768            }
1769
1770            // Label pseudo-instruction: emits no machine code
1771            ArmOp::Label { .. } => {
1772                return Ok(Vec::new());
1773            }
1774
1775            // Branch instructions
1776            ArmOp::B { label: _ } => {
1777                // B encoding: cond(4) | 1010 | offset(24)
1778                // Simplified: branch to offset 0 (will be patched by linker/resolver)
1779                0xEA000000
1780            }
1781
1782            // Conditional branch to label (generic)
1783            ArmOp::Bcc { cond, label: _ } => {
1784                use synth_synthesis::Condition;
1785                let cond_bits: u32 = match cond {
1786                    Condition::EQ => 0x0,
1787                    Condition::NE => 0x1,
1788                    Condition::HS => 0x2,
1789                    Condition::LO => 0x3,
1790                    Condition::HI => 0x8,
1791                    Condition::LS => 0x9,
1792                    Condition::GE => 0xA,
1793                    Condition::LT => 0xB,
1794                    Condition::GT => 0xC,
1795                    Condition::LE => 0xD,
1796                };
1797                // B<cond> with offset 0 (will be patched)
1798                (cond_bits << 28) | 0x0A000000
1799            }
1800
1801            // BHS (Branch if Higher or Same) - used for bounds checking
1802            ArmOp::Bhs { label: _ } => {
1803                // BHS encoding: cond(2=HS) | 1010 | offset(24)
1804                0x2A000000 // BHS with offset 0
1805            }
1806
1807            // BLO (Branch if Lower) - complementary to BHS
1808            ArmOp::Blo { label: _ } => {
1809                // BLO encoding: cond(3=LO) | 1010 | offset(24)
1810                0x3A000000 // BLO with offset 0
1811            }
1812
1813            // Branch with numeric offset (in instructions)
1814            // ARM32 B instruction: offset is in instructions, stored as words
1815            // The offset is relative to PC+8 (due to ARM pipeline)
1816            ArmOp::BOffset { offset } => {
1817                // B encoding: cond(4) | 1010 | offset(24)
1818                // Offset is signed, in words (4-byte units)
1819                // ARM adds PC+8 to the offset, so we need to adjust:
1820                // target = PC + 8 + (offset * 4)
1821                // For backward branch of N instructions: offset = -(N + 2)
1822                // wrapping_sub keeps the encoder total under fuzzing (#186): an
1823                // extreme i32::MIN offset would otherwise overflow-panic; for any
1824                // real branch offset this is identical to `- 2`.
1825                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1826                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1827                0xEA000000 | offset_bits
1828            }
1829
1830            // Conditional branch with numeric offset
1831            ArmOp::BCondOffset { cond, offset } => {
1832                use synth_synthesis::Condition;
1833                let cond_bits: u32 = match cond {
1834                    Condition::EQ => 0x0,
1835                    Condition::NE => 0x1,
1836                    Condition::HS => 0x2,
1837                    Condition::LO => 0x3,
1838                    Condition::HI => 0x8,
1839                    Condition::LS => 0x9,
1840                    Condition::GE => 0xA,
1841                    Condition::LT => 0xB,
1842                    Condition::GT => 0xC,
1843                    Condition::LE => 0xD,
1844                };
1845                // B<cond> encoding: cond(4) | 1010 | offset(24)
1846                // wrapping_sub: total under fuzzing (#186), identical for real offsets.
1847                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1848                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1849                (cond_bits << 28) | 0x0A000000 | offset_bits
1850            }
1851
1852            ArmOp::Bl { label: _ } => {
1853                // BL encoding: cond(4) | 1011 | offset(24). Relocatable
1854                // placeholder; an R_ARM_CALL relocation patches the target.
1855                //
1856                // #1040: the placeholder must carry an embedded addend of -8,
1857                // not 0. A32 `BL` computes `target = P + 8 + (imm24 << 2)`, so
1858                // under REL semantics a 0 addend (`eb000000`) resolves two
1859                // instructions PAST the callee entry — the A32 twin of the
1860                // Thumb #174 bug. The correct word is what `gas` emits for
1861                // `bl <extern>` in ARM mode:
1862                //   ebfffffe  ->  `bl <self>`  (imm24 = -2, offset = -8),
1863                // which nets to exactly S. Verified against
1864                // `arm-none-eabi-as -march=armv7-r`, which emits `ebfffffe`
1865                // with an R_ARM_CALL relocation.
1866                0xEBFFFFFE
1867            }
1868
1869            ArmOp::Bx { rm } => {
1870                let rm_bits = reg_to_bits(rm);
1871
1872                // BX encoding: cond(4) | 000100101111111111110001 | Rm(4)
1873                0xE12FFF10 | rm_bits
1874            }
1875
1876            ArmOp::Blx { rm } => {
1877                let rm_bits = reg_to_bits(rm);
1878
1879                // BLX (register) encoding: cond(4) | 000100101111111111110011 | Rm(4)
1880                0xE12FFF30 | rm_bits
1881            }
1882
1883            ArmOp::Push { regs } => {
1884                // STMDB SP!, {regs} encoding: cond(4) | 100100 | 10 | 1101 | register_list(16)
1885                let mut reg_list: u32 = 0;
1886                for r in regs {
1887                    reg_list |= 1 << reg_to_bits(r);
1888                }
1889                0xE92D0000 | reg_list
1890            }
1891
1892            ArmOp::Pop { regs } => {
1893                // LDMIA SP!, {regs} encoding: cond(4) | 100010 | 11 | 1101 | register_list(16)
1894                let mut reg_list: u32 = 0;
1895                for r in regs {
1896                    reg_list |= 1 << reg_to_bits(r);
1897                }
1898                0xE8BD0000 | reg_list
1899            }
1900
1901            ArmOp::Nop => {
1902                // NOP encoding: MOV R0, R0
1903                0xE1A00000
1904            }
1905
1906            ArmOp::Udf { imm } => {
1907                // UDF (Undefined) encoding in ARM: 0xE7F000F0 | (imm12_hi << 8) | imm4_lo
1908                // We only use imm8, so split into imm4_hi and imm4_lo
1909                let imm8 = *imm as u32;
1910                0xE7F000F0 | ((imm8 & 0xF0) << 4) | (imm8 & 0x0F)
1911            }
1912
1913            // #615: handled by the `encode_arm_expanded` early return at the
1914            // top of this function — a real MOV{cond}/MOV pair now, never a
1915            // silent NOP again.
1916            ArmOp::Popcnt { .. } | ArmOp::SetCond { .. } | ArmOp::SelectMove { .. } => {
1917                unreachable!("handled by encode_arm_expanded (#615)")
1918            }
1919
1920            // Verification-only pseudo-ops: `synth-verify`'s ArmSemantics
1921            // models these, but NO codegen path constructs them (the selector
1922            // lowers select/locals/globals/br_table/call to real instruction
1923            // sequences before the encoder). Encoding one as a NOP silently
1924            // dropped the operation (#615 class); a typed Err keeps the
1925            // encoder total (Ok-or-Err, the `encoder_no_panic` contract)
1926            // while making any future reachability LOUD.
1927            ArmOp::Select { .. }
1928            | ArmOp::LocalGet { .. }
1929            | ArmOp::LocalSet { .. }
1930            | ArmOp::LocalTee { .. }
1931            | ArmOp::GlobalGet { .. }
1932            | ArmOp::GlobalSet { .. }
1933            | ArmOp::BrTable { .. }
1934            | ArmOp::Call { .. } => {
1935                return Err(synth_core::Error::synthesis(format!(
1936                    "verification-only pseudo-op {op:?} reached the A32 encoder — \
1937                     codegen lowers it before encoding; refusing to emit a silent NOP (#615)"
1938                )));
1939            }
1940
1941            // #594: CallIndirect is expanded to a real multi-instruction
1942            // sequence by the early return at the top of this function —
1943            // it must NEVER fall through to a silent NOP again.
1944            ArmOp::CallIndirect { .. } => {
1945                unreachable!("CallIndirect handled by encode_arm_call_indirect (#594)")
1946            }
1947
1948            // #615: every i64 op (and I32WrapI64) is expanded to a real A32
1949            // multi-instruction sequence by `encode_arm_expanded` — the
1950            // "encode as NOP for now" era ended with the value silently
1951            // vanishing on `--target cortex-r5`.
1952            ArmOp::I64Add { .. }
1953            | ArmOp::I64Sub { .. }
1954            | ArmOp::I64DivS { .. }
1955            | ArmOp::I64DivU { .. }
1956            | ArmOp::I64RemS { .. }
1957            | ArmOp::I64RemU { .. }
1958            | ArmOp::I64Clz { .. }
1959            | ArmOp::I64Ctz { .. }
1960            | ArmOp::I64Popcnt { .. }
1961            | ArmOp::I64And { .. }
1962            | ArmOp::I64Or { .. }
1963            | ArmOp::I64Xor { .. }
1964            | ArmOp::I64Eqz { .. }
1965            | ArmOp::I64Eq { .. }
1966            | ArmOp::I64Ne { .. }
1967            | ArmOp::I64LtS { .. }
1968            | ArmOp::I64LtU { .. }
1969            | ArmOp::I64LeS { .. }
1970            | ArmOp::I64LeU { .. }
1971            | ArmOp::I64GtS { .. }
1972            | ArmOp::I64GtU { .. }
1973            | ArmOp::I64GeS { .. }
1974            | ArmOp::I64GeU { .. }
1975            | ArmOp::I64Const { .. }
1976            | ArmOp::I64Ldr { .. }
1977            | ArmOp::I64Str { .. }
1978            | ArmOp::I64ExtendI32S { .. }
1979            | ArmOp::I64ExtendI32U { .. }
1980            | ArmOp::I64Extend8S { .. }
1981            | ArmOp::I64Extend16S { .. }
1982            | ArmOp::I64Extend32S { .. }
1983            | ArmOp::I32WrapI64 { .. } => {
1984                unreachable!("handled by encode_arm_expanded (#615)")
1985            }
1986
1987            // f32 VFP single-precision instructions
1988            ArmOp::F32Add { sd, sn, sm } => encode_vfp_3reg(0xEE300A00, sd, sn, sm)?,
1989            ArmOp::F32Sub { sd, sn, sm } => encode_vfp_3reg(0xEE300A40, sd, sn, sm)?,
1990            ArmOp::F32Mul { sd, sn, sm } => encode_vfp_3reg(0xEE200A00, sd, sn, sm)?,
1991            ArmOp::F32Div { sd, sn, sm } => encode_vfp_3reg(0xEE800A00, sd, sn, sm)?,
1992            ArmOp::F32Abs { sd, sm } => encode_vfp_2reg(0xEEB00AC0, sd, sm)?,
1993            ArmOp::F32Neg { sd, sm } => encode_vfp_2reg(0xEEB10A40, sd, sm)?,
1994            ArmOp::F32Sqrt { sd, sm } => encode_vfp_2reg(0xEEB10AC0, sd, sm)?,
1995
1996            // f32 pseudo-ops — multi-instruction sequences
1997            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
1998            ArmOp::F32Ceil { sd, sm } => {
1999                return self.encode_arm_f32_rounding(sd, sm, 0b01); // Round toward +Inf
2000            }
2001            ArmOp::F32Floor { sd, sm } => {
2002                return self.encode_arm_f32_rounding(sd, sm, 0b10); // Round toward -Inf
2003            }
2004            ArmOp::F32Trunc { sd, sm } => {
2005                return self.encode_arm_f32_rounding(sd, sm, 0b11); // VCVT toward zero
2006            }
2007            ArmOp::F32Nearest { sd, sm } => {
2008                return self.encode_arm_f32_rounding(sd, sm, 0b00); // VCVT to nearest
2009            }
2010            ArmOp::F32Min { sd, sn, sm } => {
2011                return self.encode_arm_f32_minmax(sd, sn, sm, true);
2012            }
2013            ArmOp::F32Max { sd, sn, sm } => {
2014                return self.encode_arm_f32_minmax(sd, sn, sm, false);
2015            }
2016            ArmOp::F32Copysign { sd, sn, sm } => {
2017                return self.encode_arm_f32_copysign(sd, sn, sm);
2018            }
2019
2020            // f32 comparisons — multi-instruction: VCMP + VMRS + conditional MOV
2021            ArmOp::F32Eq { rd, sn, sm } => {
2022                return self.encode_arm_f32_compare(rd, sn, sm, 0x0); // EQ
2023            }
2024            ArmOp::F32Ne { rd, sn, sm } => {
2025                return self.encode_arm_f32_compare(rd, sn, sm, 0x1); // NE
2026            }
2027            ArmOp::F32Lt { rd, sn, sm } => {
2028                return self.encode_arm_f32_compare(rd, sn, sm, 0x4); // MI (less than)
2029            }
2030            ArmOp::F32Le { rd, sn, sm } => {
2031                return self.encode_arm_f32_compare(rd, sn, sm, 0x9); // LS (less or same)
2032            }
2033            ArmOp::F32Gt { rd, sn, sm } => {
2034                return self.encode_arm_f32_compare(rd, sn, sm, 0xC); // GT
2035            }
2036            ArmOp::F32Ge { rd, sn, sm } => {
2037                return self.encode_arm_f32_compare(rd, sn, sm, 0xA); // GE
2038            }
2039
2040            // f32 const — multi-instruction: MOVW + MOVT + VMOV
2041            ArmOp::F32Const { sd, value } => {
2042                return self.encode_arm_f32_const(sd, *value);
2043            }
2044
2045            ArmOp::F32Load { sd, addr } => encode_vfp_ldst(0xED900A00, sd, addr)?,
2046            ArmOp::F32Store { sd, addr } => encode_vfp_ldst(0xED800A00, sd, addr)?,
2047
2048            // f32 conversions — multi-instruction sequences
2049            ArmOp::F32ConvertI32S { sd, rm } => {
2050                return self.encode_arm_f32_convert_i32(sd, rm, true);
2051            }
2052            ArmOp::F32ConvertI32U { sd, rm } => {
2053                return self.encode_arm_f32_convert_i32(sd, rm, false);
2054            }
2055            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
2056                return Err(synth_core::Error::synthesis(
2057                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2058                ));
2059            }
2060            // RQ-67-VFPREACH (#1267): the callee-saved VFP save/restore is
2061            // emitted only by the Cortex-M (Thumb-2) VFP-pressure rung. The A32
2062            // path has no such rung, so reaching here means a caller built the
2063            // op for the wrong profile — refuse LOUDLY rather than encode
2064            // something plausible (#615: expand-or-loud-reject, never a silent
2065            // NOP).
2066            ArmOp::VPushCalleeSavedVfp | ArmOp::VPopCalleeSavedVfp => {
2067                return Err(synth_core::Error::synthesis(
2068                    "VPUSH/VPOP {d8-d15} is Thumb-2 only in this backend (RQ-67-VFPREACH, #1267)",
2069                ));
2070            }
2071            ArmOp::F32ReinterpretI32 { sd, rm } => encode_vmov_core_sreg(true, sd, rm)?,
2072            ArmOp::I32ReinterpretF32 { rd, sm } => encode_vmov_core_sreg(false, sm, rd)?,
2073            ArmOp::I32TruncF32S { rd, sm } => {
2074                return self.encode_arm_i32_trunc_f32(rd, sm, true);
2075            }
2076            ArmOp::I32TruncF32U { rd, sm } => {
2077                return self.encode_arm_i32_trunc_f32(rd, sm, false);
2078            }
2079
2080            // f64 VFP double-precision instructions (ARM32)
2081            // F64 arithmetic: same as F32 but with sz=1 (bit 8 = 1, cp11 = 0xB)
2082            ArmOp::F64Add { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B00, dd, dn, dm)?,
2083            ArmOp::F64Sub { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B40, dd, dn, dm)?,
2084            ArmOp::F64Mul { dd, dn, dm } => encode_vfp_3reg_f64(0xEE200B00, dd, dn, dm)?,
2085            ArmOp::F64Div { dd, dn, dm } => encode_vfp_3reg_f64(0xEE800B00, dd, dn, dm)?,
2086            ArmOp::F64Abs { dd, dm } => encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?,
2087            ArmOp::F64Neg { dd, dm } => encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?,
2088            ArmOp::F64Sqrt { dd, dm } => encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?,
2089
2090            // f64 pseudo-ops
2091            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
2092            ArmOp::F64Ceil { dd, dm } => {
2093                return self.encode_arm_f64_rounding(dd, dm, 0b01);
2094            }
2095            ArmOp::F64Floor { dd, dm } => {
2096                return self.encode_arm_f64_rounding(dd, dm, 0b10);
2097            }
2098            ArmOp::F64Trunc { dd, dm } => {
2099                return self.encode_arm_f64_rounding(dd, dm, 0b11);
2100            }
2101            ArmOp::F64Nearest { dd, dm } => {
2102                return self.encode_arm_f64_rounding(dd, dm, 0b00);
2103            }
2104            ArmOp::F64Min { dd, dn, dm } => {
2105                return self.encode_arm_f64_minmax(dd, dn, dm, true);
2106            }
2107            ArmOp::F64Max { dd, dn, dm } => {
2108                return self.encode_arm_f64_minmax(dd, dn, dm, false);
2109            }
2110            ArmOp::F64Copysign { dd, dn, dm } => {
2111                return self.encode_arm_f64_copysign(dd, dn, dm);
2112            }
2113
2114            // f64 comparisons
2115            ArmOp::F64Eq { rd, dn, dm } => {
2116                return self.encode_arm_f64_compare(rd, dn, dm, 0x0);
2117            }
2118            ArmOp::F64Ne { rd, dn, dm } => {
2119                return self.encode_arm_f64_compare(rd, dn, dm, 0x1);
2120            }
2121            ArmOp::F64Lt { rd, dn, dm } => {
2122                return self.encode_arm_f64_compare(rd, dn, dm, 0x4);
2123            }
2124            ArmOp::F64Le { rd, dn, dm } => {
2125                return self.encode_arm_f64_compare(rd, dn, dm, 0x9);
2126            }
2127            ArmOp::F64Gt { rd, dn, dm } => {
2128                return self.encode_arm_f64_compare(rd, dn, dm, 0xC);
2129            }
2130            ArmOp::F64Ge { rd, dn, dm } => {
2131                return self.encode_arm_f64_compare(rd, dn, dm, 0xA);
2132            }
2133
2134            ArmOp::F64Const { dd, value } => {
2135                return self.encode_arm_f64_const(dd, *value);
2136            }
2137
2138            ArmOp::F64Load { dd, addr } => encode_vfp_ldst_f64(0xED900B00, dd, addr)?,
2139            ArmOp::F64Store { dd, addr } => encode_vfp_ldst_f64(0xED800B00, dd, addr)?,
2140
2141            ArmOp::F64ConvertI32S { dd, rm } => {
2142                return self.encode_arm_f64_convert_i32(dd, rm, true);
2143            }
2144            ArmOp::F64ConvertI32U { dd, rm } => {
2145                return self.encode_arm_f64_convert_i32(dd, rm, false);
2146            }
2147            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
2148                return Err(synth_core::Error::synthesis(
2149                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2150                ));
2151            }
2152            ArmOp::F64PromoteF32 { dd, sm } => {
2153                return self.encode_arm_f64_promote_f32(dd, sm);
2154            }
2155            // GI-FPU-002 (#369): no synth A32 target carries an FPU (cortex-r5
2156            // has none — the selector declines every float op there), so the
2157            // A32 encoder refuses loudly instead of shipping an untested
2158            // encoding (#615: never a silent wrong byte).
2159            ArmOp::F32DemoteF64 { .. } => {
2160                return Err(synth_core::Error::synthesis(
2161                    "F32DemoteF64 has no A32 encoding (no A32 target has an FPU)",
2162                ));
2163            }
2164            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => {
2165                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?
2166            }
2167            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => {
2168                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?
2169            }
2170            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
2171                return Err(synth_core::Error::synthesis(
2172                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
2173                ));
2174            }
2175            ArmOp::I32TruncF64S { rd, dm } => {
2176                return self.encode_arm_i32_trunc_f64(rd, dm, true);
2177            }
2178            ArmOp::I32TruncF64U { rd, dm } => {
2179                return self.encode_arm_i32_trunc_f64(rd, dm, false);
2180            }
2181            // #615: multi-instruction i64 sequences — expanded to real A32 by
2182            // `encode_arm_expanded`, no longer "Thumb-2 only" NOPs.
2183            ArmOp::I64SetCond { .. }
2184            | ArmOp::I64SetCondZ { .. }
2185            | ArmOp::I64Mul { .. }
2186            | ArmOp::I64Shl { .. }
2187            | ArmOp::I64ShrS { .. }
2188            | ArmOp::I64ShrU { .. }
2189            | ArmOp::I64Rotl { .. }
2190            | ArmOp::I64Rotr { .. } => {
2191                unreachable!("handled by encode_arm_expanded (#615)")
2192            }
2193
2194            // MVE instructions — Thumb-2 only (Cortex-M55 is always Thumb-2)
2195            ArmOp::MveLoad { .. }
2196            | ArmOp::MveStore { .. }
2197            | ArmOp::MveConst { .. }
2198            | ArmOp::MveAnd { .. }
2199            | ArmOp::MveOrr { .. }
2200            | ArmOp::MveEor { .. }
2201            | ArmOp::MveMvn { .. }
2202            | ArmOp::MveBic { .. }
2203            | ArmOp::MveAddI { .. }
2204            | ArmOp::MveSubI { .. }
2205            | ArmOp::MveMulI { .. }
2206            | ArmOp::MveNegI { .. }
2207            | ArmOp::MveCmpEqI { .. }
2208            | ArmOp::MveCmpNeI { .. }
2209            | ArmOp::MveCmpLtS { .. }
2210            | ArmOp::MveCmpLtU { .. }
2211            | ArmOp::MveCmpGtS { .. }
2212            | ArmOp::MveCmpGtU { .. }
2213            | ArmOp::MveCmpLeS { .. }
2214            | ArmOp::MveCmpLeU { .. }
2215            | ArmOp::MveCmpGeS { .. }
2216            | ArmOp::MveCmpGeU { .. }
2217            | ArmOp::MveDup { .. }
2218            | ArmOp::MveExtractLane { .. }
2219            | ArmOp::MveInsertLane { .. }
2220            | ArmOp::MveAddF32 { .. }
2221            | ArmOp::MveSubF32 { .. }
2222            | ArmOp::MveMulF32 { .. }
2223            | ArmOp::MveNegF32 { .. }
2224            | ArmOp::MveAbsF32 { .. }
2225            | ArmOp::MveCmpEqF32 { .. }
2226            | ArmOp::MveCmpNeF32 { .. }
2227            | ArmOp::MveCmpLtF32 { .. }
2228            | ArmOp::MveCmpLeF32 { .. }
2229            | ArmOp::MveCmpGtF32 { .. }
2230            | ArmOp::MveCmpGeF32 { .. }
2231            | ArmOp::MveDupF32 { .. }
2232            | ArmOp::MveExtractLaneF32 { .. }
2233            | ArmOp::MveReplaceLaneF32 { .. }
2234            | ArmOp::MveDivF32 { .. }
2235            | ArmOp::MveSqrtF32 { .. } => {
2236                // MVE (Helium) is a Thumb-2-only extension (Cortex-M55); there
2237                // is no A32 encoding. The selector only emits MVE ops for
2238                // Thumb targets — a NOP here silently dropped the vector op
2239                // if that invariant ever broke (#615 class). Err keeps the
2240                // encoder total and the failure loud.
2241                return Err(synth_core::Error::synthesis(format!(
2242                    "MVE op {op:?} has no A32 (ARM-mode) encoding — MVE is Thumb-2 only (#615)"
2243                )));
2244            }
2245        };
2246
2247        // ARM32 instructions are little-endian
2248        Ok(instr.to_le_bytes().to_vec())
2249    }
2250
2251    // === ARM32 VFP multi-instruction helpers ===
2252
2253    /// Encode F32 comparison as ARM32: VCMP.F32 + VMRS + MOV rd,#0 + MOVcond rd,#1
2254    fn encode_arm_f32_compare(
2255        &self,
2256        rd: &Reg,
2257        sn: &VfpReg,
2258        sm: &VfpReg,
2259        cond_code: u32,
2260    ) -> Result<Vec<u8>> {
2261        let mut bytes = Vec::new();
2262
2263        // VCMP.F32 Sn, Sm: 0xEEB40A40 with Sn in Vd position, Sm in Vm position
2264        let sn_num = vfp_sreg_to_num(sn)?;
2265        let sm_num = vfp_sreg_to_num(sm)?;
2266        let (vd, d) = encode_sreg(sn_num);
2267        let (vm, m) = encode_sreg(sm_num);
2268        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2269        bytes.extend_from_slice(&vcmp.to_le_bytes());
2270
2271        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
2272        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2273
2274        // MOV rd, #0: 0xE3A0_0000 | (rd << 12)
2275        let rd_bits = reg_to_bits(rd);
2276        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2277        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2278
2279        // MOVcond rd, #1: cond(4) | 0011 1010 0000 rd(4) 0000 0000 0001
2280        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2281        bytes.extend_from_slice(&mov_one.to_le_bytes());
2282
2283        Ok(bytes)
2284    }
2285
2286    /// Encode F32 constant load as ARM32: MOVW Rt,#lo16 + MOVT Rt,#hi16 + VMOV Sd,Rt
2287    fn encode_arm_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
2288        let mut bytes = Vec::new();
2289        let bits = value.to_bits();
2290
2291        // Use R12 as temp register for constant loading
2292        let rt: u32 = 12; // R12/IP
2293
2294        // MOVW R12, #lo16: 0xE300_C000 | (imm4 << 16) | imm12
2295        let lo16 = bits & 0xFFFF;
2296        let movw = 0xE3000000 | (rt << 12) | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2297        bytes.extend_from_slice(&movw.to_le_bytes());
2298
2299        // MOVT R12, #hi16: 0xE340_C000 | (imm4 << 16) | imm12
2300        let hi16 = (bits >> 16) & 0xFFFF;
2301        let movt = 0xE3400000 | (rt << 12) | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2302        bytes.extend_from_slice(&movt.to_le_bytes());
2303
2304        // VMOV Sd, R12
2305        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
2306        bytes.extend_from_slice(&vmov.to_le_bytes());
2307
2308        Ok(bytes)
2309    }
2310
2311    /// Encode VMOV + VCVT.F32.S32/U32 as ARM32
2312    fn encode_arm_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2313        let mut bytes = Vec::new();
2314
2315        // VMOV Sd, Rm — move integer to VFP register
2316        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
2317        bytes.extend_from_slice(&vmov.to_le_bytes());
2318
2319        // VCVT.F32.S32 Sd, Sd (signed) or VCVT.F32.U32 Sd, Sd (unsigned).
2320        // The "op" bit (bit 7) selects signedness: 1 = signed (S32), 0 =
2321        // unsigned (U32). So signed = 0xEEB80AC0, unsigned = 0xEEB80A40 —
2322        // objdump confirms 0xEEB80A40 decodes to `vcvt.f32.u32` (GI-FPU-002:
2323        // the two were previously swapped, silently making `convert_i32_s`
2324        // an unsigned conversion).
2325        let sd_num = vfp_sreg_to_num(sd)?;
2326        let (vd, d) = encode_sreg(sd_num);
2327        let (vm, m) = encode_sreg(sd_num); // same register as source
2328        let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
2329        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2330        bytes.extend_from_slice(&vcvt.to_le_bytes());
2331
2332        Ok(bytes)
2333    }
2334
2335    /// Encode F32 rounding pseudo-op as ARM32 via VCVT to integer and back.
2336    /// mode: 0b00=nearest, 0b01=floor(-Inf), 0b10=ceil(+Inf), 0b11=trunc(zero)
2337    /// Strategy: VCVT.S32.F32 Sd, Sm (toward zero), then VCVT.F32.S32 Sd, Sd
2338    /// For ceil/floor/nearest, we use VCVTR (round toward mode) + convert back.
2339    /// Simplified: convert to int (toward zero for trunc) then back to float.
2340    /// Encode F32 rounding as ARM32.
2341    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2342    ///
2343    /// For trunc (mode=0b11): uses VCVTR.S32.F32 (always rounds toward zero).
2344    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant
2345    /// which honours FPSCR rmode), then restores FPSCR.
2346    fn encode_arm_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2347        let mut bytes = Vec::new();
2348        let sm_num = vfp_sreg_to_num(sm)?;
2349        let sd_num = vfp_sreg_to_num(sd)?;
2350        let (vd_s, d_s) = encode_sreg(sd_num);
2351        let (vm_s, m_s) = encode_sreg(sm_num);
2352
2353        if mode == 0b11 {
2354            // Trunc (toward zero): VCVTR.S32.F32 — the "R" variant always truncates.
2355            // 0xEEBD0AC0: bit[7]=1 => round toward zero regardless of FPSCR
2356            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2357            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2358        } else {
2359            // ceil/floor/nearest: manipulate FPSCR rounding mode
2360            let rt: u32 = 12; // R12/IP as temp
2361
2362            // VMRS R12, FPSCR
2363            let vmrs = 0xEEF10A10 | (rt << 12);
2364            bytes.extend_from_slice(&vmrs.to_le_bytes());
2365
2366            // BIC R12, R12, #(3 << 22) — clear RMode bits [23:22]
2367            // 3<<22 = 0x00C00000. ARM rotated imm: 0x03 ror 10 (rotation=5, imm8=0x03)
2368            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2369            bytes.extend_from_slice(&bic.to_le_bytes());
2370
2371            // ORR R12, R12, #(mode << 22) — set desired rounding mode
2372            if mode != 0 {
2373                // mode<<22: rotation=5, imm8=mode
2374                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2375                bytes.extend_from_slice(&orr.to_le_bytes());
2376            }
2377
2378            // VMSR FPSCR, R12
2379            let vmsr = 0xEEE10A10 | (rt << 12);
2380            bytes.extend_from_slice(&vmsr.to_le_bytes());
2381
2382            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rounding mode
2383            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2384            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2385
2386            // Restore FPSCR: clear rmode bits back to nearest (default)
2387            bytes.extend_from_slice(&vmrs.to_le_bytes());
2388            bytes.extend_from_slice(&bic.to_le_bytes());
2389            bytes.extend_from_slice(&vmsr.to_le_bytes());
2390        }
2391
2392        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
2393        let (vd2, d2) = encode_sreg(sd_num);
2394        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
2395        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2396
2397        Ok(bytes)
2398    }
2399
2400    /// Encode F32 min/max as ARM32: VCMP + VMRS + conditional VMOV
2401    fn encode_arm_f32_minmax(
2402        &self,
2403        sd: &VfpReg,
2404        sn: &VfpReg,
2405        sm: &VfpReg,
2406        is_min: bool,
2407    ) -> Result<Vec<u8>> {
2408        let mut bytes = Vec::new();
2409        let sn_num = vfp_sreg_to_num(sn)?;
2410        let sm_num = vfp_sreg_to_num(sm)?;
2411        let sd_num = vfp_sreg_to_num(sd)?;
2412
2413        // VMOV Sd, Sn (start with first operand)
2414        let (vd, d) = encode_sreg(sd_num);
2415        let (vn, n) = encode_sreg(sn_num);
2416        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2417        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2418
2419        // VCMP.F32 Sn, Sm
2420        let (vm, m) = encode_sreg(sm_num);
2421        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2422        bytes.extend_from_slice(&vcmp.to_le_bytes());
2423
2424        // VMRS APSR_nzcv, FPSCR
2425        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2426
2427        // For min: if Sn > Sm (GT), use Sm. Condition = GT (0xC)
2428        // For max: if Sn < Sm (MI/LT), use Sm. Condition = MI (0x4)
2429        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2430
2431        // VMOV{cond} Sd, Sm — conditional VMOV
2432        let vmov_cond = (cond << 28) | 0x0EB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2433        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2434
2435        Ok(bytes)
2436    }
2437
2438    /// Encode F32 copysign as ARM32: extract sign from Sm, magnitude from Sn
2439    fn encode_arm_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2440        let mut bytes = Vec::new();
2441
2442        // VMOV R12, Sm (get sign source bits)
2443        let vmov_sm = encode_vmov_core_sreg(false, sm, &Reg::R12)?;
2444        bytes.extend_from_slice(&vmov_sm.to_le_bytes());
2445
2446        // VMOV R0, Sn (get magnitude source bits) — use R0 as temp
2447        let vmov_sn = encode_vmov_core_sreg(false, sn, &Reg::R0)?;
2448        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2449
2450        // AND R12, R12, #0x80000000 (keep only sign bit)
2451        // Thumb-2 constant 0x80000000 needs special encoding; in ARM32 use rotated imm
2452        // 0x80000000 = 0x02 rotated right by 2 (rotation=1, imm8=0x02)
2453        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2454        bytes.extend_from_slice(&and_sign.to_le_bytes());
2455
2456        // BIC R0, R0, #0x80000000 (clear sign bit from magnitude)
2457        // R0 = register 0, so Rn and Rd fields are 0
2458        let bic_sign = 0xE3C00000u32 | (1 << 8) | 0x02;
2459        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2460
2461        // ORR R0, R0, R12 (combine sign + magnitude)
2462        // R0 = register 0, so Rn and Rd fields are 0
2463        let orr = 0xE1800000u32 | 12;
2464        bytes.extend_from_slice(&orr.to_le_bytes());
2465
2466        // VMOV Sd, R0
2467        let vmov_result = encode_vmov_core_sreg(true, sd, &Reg::R0)?;
2468        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2469
2470        Ok(bytes)
2471    }
2472
2473    /// Encode F64 comparison as ARM32: VCMP.F64 + VMRS + MOV rd,#0 + MOVcond rd,#1
2474    fn encode_arm_f64_compare(
2475        &self,
2476        rd: &Reg,
2477        dn: &VfpReg,
2478        dm: &VfpReg,
2479        cond_code: u32,
2480    ) -> Result<Vec<u8>> {
2481        let mut bytes = Vec::new();
2482
2483        // VCMP.F64 Dn, Dm: 0xEEB40B40 with Dn in Vd position, Dm in Vm position
2484        let dn_num = vfp_dreg_to_num(dn)?;
2485        let dm_num = vfp_dreg_to_num(dm)?;
2486        let (vd, d) = encode_dreg(dn_num);
2487        let (vm, m) = encode_dreg(dm_num);
2488        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2489        bytes.extend_from_slice(&vcmp.to_le_bytes());
2490
2491        // VMRS APSR_nzcv, FPSCR
2492        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2493
2494        // MOV rd, #0
2495        let rd_bits = reg_to_bits(rd);
2496        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2497        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2498
2499        // MOVcond rd, #1
2500        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2501        bytes.extend_from_slice(&mov_one.to_le_bytes());
2502
2503        Ok(bytes)
2504    }
2505
2506    /// Encode F64 constant load as ARM32: MOVW + MOVT + MOVW + MOVT + VMOV
2507    fn encode_arm_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
2508        let mut bytes = Vec::new();
2509        let bits = value.to_bits();
2510        let lo32 = bits as u32;
2511        let hi32 = (bits >> 32) as u32;
2512
2513        // Load low 32 bits into R0 (Rd field = 0 for R0)
2514        let lo16 = lo32 & 0xFFFF;
2515        let movw_r0 = 0xE3000000 | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2516        bytes.extend_from_slice(&movw_r0.to_le_bytes());
2517        let hi16 = (lo32 >> 16) & 0xFFFF;
2518        let movt_r0 = 0xE3400000 | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2519        bytes.extend_from_slice(&movt_r0.to_le_bytes());
2520
2521        // Load high 32 bits into R12
2522        let lo16 = hi32 & 0xFFFF;
2523        let movw_r12 = 0xE3000000 | ((lo16 >> 12) << 16) | (12 << 12) | (lo16 & 0xFFF);
2524        bytes.extend_from_slice(&movw_r12.to_le_bytes());
2525        let hi16 = (hi32 >> 16) & 0xFFFF;
2526        let movt_r12 = 0xE3400000 | ((hi16 >> 12) << 16) | (12 << 12) | (hi16 & 0xFFF);
2527        bytes.extend_from_slice(&movt_r12.to_le_bytes());
2528
2529        // VMOV Dd, R0, R12
2530        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
2531        bytes.extend_from_slice(&vmov.to_le_bytes());
2532
2533        Ok(bytes)
2534    }
2535
2536    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as ARM32
2537    fn encode_arm_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2538        let mut bytes = Vec::new();
2539
2540        // Use S0 as intermediate: VMOV S0, Rm
2541        let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
2542        bytes.extend_from_slice(&vmov.to_le_bytes());
2543
2544        // VCVT.F64.S32 Dd, S0 (signed) or VCVT.F64.U32 Dd, S0 (unsigned)
2545        // Base: 0xEEB80B40 (signed) or 0xEEB80BC0 (unsigned)
2546        let dd_num = vfp_dreg_to_num(dd)?;
2547        let (vd, d) = encode_dreg(dd_num);
2548        let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
2549        // S0 is register 0: Vm=0, M=0
2550        let vcvt = base | (d << 22) | (vd << 12);
2551        bytes.extend_from_slice(&vcvt.to_le_bytes());
2552
2553        Ok(bytes)
2554    }
2555
2556    /// Encode VCVT.F64.F32 Dd, Sm as ARM32 (f32 to f64 promotion)
2557    fn encode_arm_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2558        let dd_num = vfp_dreg_to_num(dd)?;
2559        let sm_num = vfp_sreg_to_num(sm)?;
2560        let (vd, d) = encode_dreg(dd_num);
2561        let (vm, m) = encode_sreg(sm_num);
2562
2563        // VCVT.F64.F32 Dd, Sm: 0xEEB70AC0
2564        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
2565        Ok(vcvt.to_le_bytes().to_vec())
2566    }
2567
2568    /// Encode VCVT.S32/U32.F64 Sd, Dm + VMOV Rd, Sd as ARM32
2569    fn encode_arm_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2570        let mut bytes = Vec::new();
2571        let dm_num = vfp_dreg_to_num(dm)?;
2572        let (vm, m) = encode_dreg(dm_num);
2573
2574        // VCVT.S32.F64 S0, Dm (toward zero) or VCVT.U32.F64 S0, Dm
2575        // S0: Vd=0, D=0
2576        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
2577        let vcvt = base | (m << 5) | vm;
2578        bytes.extend_from_slice(&vcvt.to_le_bytes());
2579
2580        // VMOV Rd, S0
2581        let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
2582        bytes.extend_from_slice(&vmov.to_le_bytes());
2583
2584        Ok(bytes)
2585    }
2586
2587    /// Encode F64 rounding pseudo-op as ARM32 via VCVT to integer and back.
2588    /// Encode F64 rounding as ARM32.
2589    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2590    ///
2591    /// For trunc: uses VCVTR.S32.F64 (always truncates).
2592    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F64 (non-R variant),
2593    /// then restores FPSCR.
2594    fn encode_arm_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2595        let mut bytes = Vec::new();
2596        let dm_num = vfp_dreg_to_num(dm)?;
2597        let dd_num = vfp_dreg_to_num(dd)?;
2598        let (vm, m) = encode_dreg(dm_num);
2599        let (vd, d) = encode_dreg(dd_num);
2600
2601        if mode == 0b11 {
2602            // Trunc (toward zero): VCVTR.S32.F64 — bit[7]=1, always truncates
2603            let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
2604            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2605        } else {
2606            // ceil/floor/nearest: manipulate FPSCR rounding mode
2607            let rt: u32 = 12;
2608
2609            // VMRS R12, FPSCR
2610            let vmrs = 0xEEF10A10 | (rt << 12);
2611            bytes.extend_from_slice(&vmrs.to_le_bytes());
2612
2613            // BIC R12, R12, #(3 << 22)
2614            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2615            bytes.extend_from_slice(&bic.to_le_bytes());
2616
2617            // ORR R12, R12, #(mode << 22)
2618            if mode != 0 {
2619                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2620                bytes.extend_from_slice(&orr.to_le_bytes());
2621            }
2622
2623            // VMSR FPSCR, R12
2624            let vmsr = 0xEEE10A10 | (rt << 12);
2625            bytes.extend_from_slice(&vmsr.to_le_bytes());
2626
2627            // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0), uses FPSCR rmode
2628            let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
2629            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2630
2631            // Restore FPSCR
2632            bytes.extend_from_slice(&vmrs.to_le_bytes());
2633            bytes.extend_from_slice(&bic.to_le_bytes());
2634            bytes.extend_from_slice(&vmsr.to_le_bytes());
2635        }
2636
2637        // VCVT.F64.S32 Dd, S0 (convert back to double)
2638        let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
2639        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2640
2641        Ok(bytes)
2642    }
2643
2644    /// Encode F64 min/max as ARM32: VMOV + VCMP + VMRS + conditional VMOV
2645    fn encode_arm_f64_minmax(
2646        &self,
2647        dd: &VfpReg,
2648        dn: &VfpReg,
2649        dm: &VfpReg,
2650        is_min: bool,
2651    ) -> Result<Vec<u8>> {
2652        let mut bytes = Vec::new();
2653        let dn_num = vfp_dreg_to_num(dn)?;
2654        let dm_num = vfp_dreg_to_num(dm)?;
2655        let dd_num = vfp_dreg_to_num(dd)?;
2656
2657        // VMOV.F64 Dd, Dn (start with first operand)
2658        let (vd, d) = encode_dreg(dd_num);
2659        let (vn, n) = encode_dreg(dn_num);
2660        let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2661        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2662
2663        // VCMP.F64 Dn, Dm
2664        let (vm, m) = encode_dreg(dm_num);
2665        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2666        bytes.extend_from_slice(&vcmp.to_le_bytes());
2667
2668        // VMRS APSR_nzcv, FPSCR
2669        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2670
2671        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2672        let vmov_cond = (cond << 28) | 0x0EB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2673        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2674
2675        Ok(bytes)
2676    }
2677
2678    /// Encode F64 copysign as ARM32
2679    fn encode_arm_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
2680        let mut bytes = Vec::new();
2681
2682        // VMOV R0, R12, Dm (get sign source bits)
2683        let vmov_dm = encode_vmov_core_dreg(false, dm, &Reg::R0, &Reg::R12)?;
2684        bytes.extend_from_slice(&vmov_dm.to_le_bytes());
2685
2686        // VMOV R1, R2, Dn (get magnitude source bits)
2687        // We use R1 (lo) and R2 (hi) for the magnitude
2688        let vmov_dn = encode_vmov_core_dreg(false, dn, &Reg::R1, &Reg::R2)?;
2689        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2690
2691        // AND R12, R12, #0x80000000 (keep only sign bit from hi word)
2692        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2693        bytes.extend_from_slice(&and_sign.to_le_bytes());
2694
2695        // BIC R2, R2, #0x80000000 (clear sign bit from magnitude hi word)
2696        let bic_sign = 0xE3C00000u32 | (2 << 16) | (2 << 12) | (1 << 8) | 0x02;
2697        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2698
2699        // ORR R2, R2, R12 (combine sign + magnitude)
2700        let orr = 0xE1800000u32 | (2 << 16) | (2 << 12) | 12;
2701        bytes.extend_from_slice(&orr.to_le_bytes());
2702
2703        // VMOV Dd, R1, R2
2704        let vmov_result = encode_vmov_core_dreg(true, dd, &Reg::R1, &Reg::R2)?;
2705        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2706
2707        Ok(bytes)
2708    }
2709
2710    /// Encode VCVT.S32/U32.F32 + VMOV as ARM32
2711    fn encode_arm_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2712        let mut bytes = Vec::new();
2713
2714        // VCVT.S32.F32 Sd, Sm (toward zero) or VCVT.U32.F32 Sd, Sm
2715        // We use Sm as both source and destination for the intermediate result
2716        let sm_num = vfp_sreg_to_num(sm)?;
2717        let (vd, d) = encode_sreg(sm_num);
2718        let (vm, m) = encode_sreg(sm_num);
2719        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
2720        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2721        bytes.extend_from_slice(&vcvt.to_le_bytes());
2722
2723        // VMOV Rd, Sm — move result back to core register
2724        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
2725        bytes.extend_from_slice(&vmov.to_le_bytes());
2726
2727        Ok(bytes)
2728    }
2729
2730    /// Encode an ARM instruction in Thumb-2 mode (16-bit or 32-bit instructions)
2731    fn encode_thumb(&self, op: &ArmOp) -> Result<Vec<u8>> {
2732        // Thumb-2 supports both 16-bit and 32-bit instructions
2733        // 32-bit instructions are encoded as two 16-bit halfwords (big-endian order)
2734        match op {
2735            // === 16-bit Thumb encodings ===
2736            ArmOp::Add { rd, rn, op2 } => {
2737                let rd_bits = reg_to_bits(rd) as u16;
2738                let rn_bits = reg_to_bits(rn) as u16;
2739
2740                if let Operand2::Reg(rm) = op2 {
2741                    let rm_bits = reg_to_bits(rm) as u16;
2742                    // 16-bit ADDS only has 3-bit register fields (R0-R7). For
2743                    // high registers (e.g. R12, the MemLoad/MemStore base
2744                    // scratch) the bits overflow into adjacent fields, silently
2745                    // corrupting the operands — issue #178/#180: `add ip,ip,r0`
2746                    // was emitted as `adds r4,r5,r1`. Guard on all three regs
2747                    // being low and fall back to 32-bit ADD.W otherwise, exactly
2748                    // as the Sub handler below does.
2749                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2750                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2751                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2752                        Ok(instr.to_le_bytes().to_vec())
2753                    } else {
2754                        // ADD.W Rd, Rn, Rm (32-bit) for high registers
2755                        self.encode_thumb32_add_reg_raw(
2756                            rd_bits as u32,
2757                            rn_bits as u32,
2758                            rm_bits as u32,
2759                        )
2760                    }
2761                } else if let Operand2::Imm(imm) = op2 {
2762                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2763                        // ADDS Rd, Rn, #imm3 (16-bit): 0001 110 imm3 Rn Rd
2764                        let instr: u16 = 0x1C00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2765                        Ok(instr.to_le_bytes().to_vec())
2766                    } else {
2767                        // Use 32-bit ADD for larger immediates
2768                        self.encode_thumb32_add(rd, rn, *imm as u32)
2769                    }
2770                } else {
2771                    // Fallback to 32-bit encoding
2772                    self.encode_thumb32_add(rd, rn, 0)
2773                }
2774            }
2775
2776            ArmOp::Sub { rd, rn, op2 } => {
2777                let rd_bits = reg_to_bits(rd) as u16;
2778                let rn_bits = reg_to_bits(rn) as u16;
2779
2780                if let Operand2::Reg(rm) = op2 {
2781                    let rm_bits = reg_to_bits(rm) as u16;
2782                    // 16-bit SUBS can only use low registers (R0-R7)
2783                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2784                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2785                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2786                        Ok(instr.to_le_bytes().to_vec())
2787                    } else {
2788                        // Use 32-bit SUB.W for high registers
2789                        self.encode_thumb32_sub_reg_raw(
2790                            rd_bits as u32,
2791                            rn_bits as u32,
2792                            rm_bits as u32,
2793                        )
2794                    }
2795                } else if let Operand2::Imm(imm) = op2 {
2796                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2797                        // SUBS Rd, Rn, #imm3 (16-bit): 0001 111 imm3 Rn Rd
2798                        let instr: u16 = 0x1E00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2799                        Ok(instr.to_le_bytes().to_vec())
2800                    } else {
2801                        self.encode_thumb32_sub(rd, rn, *imm as u32)
2802                    }
2803                } else {
2804                    self.encode_thumb32_sub(rd, rn, 0)
2805                }
2806            }
2807
2808            ArmOp::Mov { rd, op2 } => {
2809                let rd_bits = reg_to_bits(rd) as u16;
2810
2811                if let Operand2::Imm(imm) = op2 {
2812                    // #498: the old test here was the SIGNED `*imm <= 255`,
2813                    // so a negative immediate (e.g. -1) fell into the 16-bit
2814                    // MOVS arm and encoded the wrong VALUE (#(imm & 0xFF) =
2815                    // #0xFF). A positive imm above 0xFFFF was equally wrong:
2816                    // MOVW truncates to 16 bits. Split on the UNSIGNED value:
2817                    // imm8 → MOVS, imm16 → MOVW, anything wider (negative or
2818                    // >0xFFFF) → the full-value MOVW+MOVT pair. No emitter
2819                    // produces the wide shape today (both selectors
2820                    // materialize wide constants as explicit Movw/Movt or
2821                    // Movw+Mvn), so this is byte-identical on shipped paths —
2822                    // it retires the latent wrong-value encodings the
2823                    // `estimator_encoder_agreement` oracle had pinned.
2824                    let uimm = *imm as u32;
2825                    if uimm <= 255 && rd_bits < 8 {
2826                        // MOVS Rd, #imm8 (16-bit): 0010 0 Rd imm8
2827                        let imm_bits = (*imm as u16) & 0xFF;
2828                        let instr: u16 = 0x2000 | (rd_bits << 8) | imm_bits;
2829                        Ok(instr.to_le_bytes().to_vec())
2830                    } else if uimm <= 0xFFFF {
2831                        // Use 32-bit MOVW for 16-bit immediates
2832                        self.encode_thumb32_movw(rd, uimm)
2833                    } else {
2834                        // Full 32-bit value: MOVW low16 + MOVT high16
2835                        let mut bytes = self.encode_thumb32_movw(rd, uimm & 0xFFFF)?;
2836                        bytes.extend(self.encode_thumb32_movt_raw(reg_to_bits(rd), uimm >> 16)?);
2837                        Ok(bytes)
2838                    }
2839                } else if let Operand2::Reg(rm) = op2 {
2840                    let rm_bits = reg_to_bits(rm) as u16;
2841                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
2842                    // D = Rd[3], Rd[2:0] in lower bits
2843                    let d_bit = (rd_bits >> 3) & 1;
2844                    let instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
2845                    Ok(instr.to_le_bytes().to_vec())
2846                } else {
2847                    // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
2848                    // operand form the arm does not encode (Imm and Reg only) -- #615's
2849                    // silent-miscompile class, on the DEFAULT target. Refuse instead.
2850                    Err(synth_core::Error::synthesis(format!(
2851                        "Thumb-2 MOV with operand {op2:?} has no Thumb-2 encoding here (Imm and Reg \
2852                         are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
2853                    )))
2854                }
2855            }
2856
2857            ArmOp::Push { regs } => {
2858                // Thumb-2 PUSH encoding:
2859                // If all regs in R0-R7 + LR, use 16-bit: 1011 010 M rrrrrrrr
2860                // Otherwise use 32-bit: STMDB SP!, {regs} = 1110 1001 0010 1101 | 0M0 reglist(13)
2861                let mut reg_list: u16 = 0;
2862                let mut need_32bit = false;
2863                for r in regs {
2864                    let bit = reg_to_bits(r);
2865                    if bit >= 8 && *r != Reg::LR {
2866                        need_32bit = true;
2867                    }
2868                    reg_list |= 1 << bit;
2869                }
2870                if !need_32bit {
2871                    // 16-bit PUSH: 1011 010 M rrrrrrrr
2872                    let m_bit = if reg_list & (1 << 14) != 0 {
2873                        1u16
2874                    } else {
2875                        0u16
2876                    };
2877                    let low_regs = reg_list & 0xFF;
2878                    let instr: u16 = 0xB400 | (m_bit << 8) | low_regs;
2879                    Ok(instr.to_le_bytes().to_vec())
2880                } else {
2881                    // 32-bit STMDB SP!, {regs}: E92D | reglist(16)
2882                    let hw1: u16 = 0xE92D;
2883                    let hw2: u16 = reg_list;
2884                    let mut bytes = hw1.to_le_bytes().to_vec();
2885                    bytes.extend_from_slice(&hw2.to_le_bytes());
2886                    Ok(bytes)
2887                }
2888            }
2889
2890            ArmOp::Pop { regs } => {
2891                // Thumb-2 POP encoding:
2892                // If all regs in R0-R7 + PC, use 16-bit: 1011 110 P rrrrrrrr
2893                // Otherwise use 32-bit: LDMIA SP!, {regs} = 1110 1000 1011 1101 | PM0 reglist(13)
2894                let mut reg_list: u16 = 0;
2895                let mut need_32bit = false;
2896                for r in regs {
2897                    let bit = reg_to_bits(r);
2898                    if bit >= 8 && *r != Reg::PC {
2899                        need_32bit = true;
2900                    }
2901                    reg_list |= 1 << bit;
2902                }
2903                if !need_32bit {
2904                    // 16-bit POP: 1011 110 P rrrrrrrr
2905                    let p_bit = if reg_list & (1 << 15) != 0 {
2906                        1u16
2907                    } else {
2908                        0u16
2909                    };
2910                    let low_regs = reg_list & 0xFF;
2911                    let instr: u16 = 0xBC00 | (p_bit << 8) | low_regs;
2912                    Ok(instr.to_le_bytes().to_vec())
2913                } else {
2914                    // 32-bit LDMIA SP!, {regs}: E8BD | reglist(16)
2915                    let hw1: u16 = 0xE8BD;
2916                    let hw2: u16 = reg_list;
2917                    let mut bytes = hw1.to_le_bytes().to_vec();
2918                    bytes.extend_from_slice(&hw2.to_le_bytes());
2919                    Ok(bytes)
2920                }
2921            }
2922
2923            ArmOp::Nop => {
2924                let instr: u16 = 0xBF00; // NOP in Thumb-2
2925                Ok(instr.to_le_bytes().to_vec())
2926            }
2927
2928            ArmOp::Udf { imm } => {
2929                // UDF (Undefined) in Thumb-2: 16-bit encoding is 0xDE00 | imm8
2930                // This triggers UsageFault/HardFault, used for WASM traps
2931                let instr: u16 = 0xDE00 | (*imm as u16);
2932                let bytes = instr.to_le_bytes().to_vec();
2933                encoding_contracts::verify_thumb16(&bytes);
2934                Ok(bytes)
2935            }
2936
2937            // i64 support: ADDS, ADC, SUBS, SBC for register pair arithmetic
2938            // ADDS sets flags (carry), ADC uses carry from previous ADDS
2939            ArmOp::Adds { rd, rn, op2 } => {
2940                let rd_bits = reg_to_bits(rd) as u16;
2941                let rn_bits = reg_to_bits(rn) as u16;
2942
2943                if let Operand2::Reg(rm) = op2 {
2944                    let rm_bits = reg_to_bits(rm) as u16;
2945                    // 16-bit ADDS is R0-R7 only; i64 pair allocation can place
2946                    // operands in R8-R11, which would overflow the 3-bit fields
2947                    // and corrupt the operands (#178/#180 class). Guard and fall
2948                    // back to 32-bit ADDS.W for high registers.
2949                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2950                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2951                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2952                        Ok(instr.to_le_bytes().to_vec())
2953                    } else {
2954                        self.encode_thumb32_adds_reg_raw(
2955                            rd_bits as u32,
2956                            rn_bits as u32,
2957                            rm_bits as u32,
2958                        )
2959                    }
2960                } else {
2961                    // 32-bit Thumb-2 ADDS with immediate
2962                    self.encode_thumb32_adds(rd, rn, 0)
2963                }
2964            }
2965
2966            // ADC: Add with Carry (Thumb-2 32-bit)
2967            // ADC.W Rd, Rn, Rm: EB40 Rn | 00 Rd 00 Rm
2968            ArmOp::Adc { rd, rn, op2 } => {
2969                let rd_bits = reg_to_bits(rd);
2970                let rn_bits = reg_to_bits(rn);
2971
2972                if let Operand2::Reg(rm) = op2 {
2973                    let rm_bits = reg_to_bits(rm);
2974                    // ADC.W Rd, Rn, Rm (T2): 1110 1011 0100 Rn | 0 000 Rd 00 00 Rm
2975                    let hw1: u16 = (0xEB40 | rn_bits) as u16;
2976                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2977
2978                    let mut bytes = hw1.to_le_bytes().to_vec();
2979                    bytes.extend_from_slice(&hw2.to_le_bytes());
2980                    Ok(bytes)
2981                } else {
2982                    // ADC with immediate - use 32-bit encoding
2983                    let hw1: u16 = (0xF140 | rn_bits) as u16;
2984                    let hw2: u16 = (rd_bits << 8) as u16;
2985                    let mut bytes = hw1.to_le_bytes().to_vec();
2986                    bytes.extend_from_slice(&hw2.to_le_bytes());
2987                    Ok(bytes)
2988                }
2989            }
2990
2991            // SUBS sets flags (borrow), SBC uses borrow from previous SUBS
2992            ArmOp::Subs { rd, rn, op2 } => {
2993                let rd_bits = reg_to_bits(rd) as u16;
2994                let rn_bits = reg_to_bits(rn) as u16;
2995
2996                if let Operand2::Reg(rm) = op2 {
2997                    let rm_bits = reg_to_bits(rm) as u16;
2998                    // 16-bit SUBS is R0-R7 only; high-register i64 pair operands
2999                    // would overflow the 3-bit fields (#178/#180 class). Guard
3000                    // and fall back to 32-bit SUBS.W for high registers.
3001                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
3002                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
3003                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
3004                        Ok(instr.to_le_bytes().to_vec())
3005                    } else {
3006                        self.encode_thumb32_subs_reg_raw(
3007                            rd_bits as u32,
3008                            rn_bits as u32,
3009                            rm_bits as u32,
3010                        )
3011                    }
3012                } else {
3013                    // 32-bit Thumb-2 SUBS with immediate
3014                    self.encode_thumb32_subs(rd, rn, 0)
3015                }
3016            }
3017
3018            // SBC: Subtract with Carry (Thumb-2 32-bit)
3019            // SBC.W Rd, Rn, Rm: EB60 Rn | 00 Rd 00 Rm
3020            ArmOp::Sbc { rd, rn, op2 } => {
3021                let rd_bits = reg_to_bits(rd);
3022                let rn_bits = reg_to_bits(rn);
3023
3024                if let Operand2::Reg(rm) = op2 {
3025                    let rm_bits = reg_to_bits(rm);
3026                    // SBC.W Rd, Rn, Rm (T2): 1110 1011 0110 Rn | 0 000 Rd 00 00 Rm
3027                    let hw1: u16 = (0xEB60 | rn_bits) as u16;
3028                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3029
3030                    let mut bytes = hw1.to_le_bytes().to_vec();
3031                    bytes.extend_from_slice(&hw2.to_le_bytes());
3032                    Ok(bytes)
3033                } else {
3034                    // SBC with immediate - use 32-bit encoding
3035                    let hw1: u16 = (0xF160 | rn_bits) as u16;
3036                    let hw2: u16 = (rd_bits << 8) as u16;
3037                    let mut bytes = hw1.to_le_bytes().to_vec();
3038                    bytes.extend_from_slice(&hw2.to_le_bytes());
3039                    Ok(bytes)
3040                }
3041            }
3042
3043            // === 32-bit Thumb-2 encodings ===
3044
3045            // SDIV: 11111011 1001 Rn 1111 Rd 1111 Rm
3046            ArmOp::Sdiv { rd, rn, rm } => {
3047                let rd_bits = reg_to_bits(rd);
3048                let rn_bits = reg_to_bits(rn);
3049                let rm_bits = reg_to_bits(rm);
3050                reg_bits_checked(rd_bits)?;
3051                reg_bits_checked(rn_bits)?;
3052                reg_bits_checked(rm_bits)?;
3053
3054                // Thumb-2 SDIV: FB90 F0F0 | Rn<<16 | Rd<<8 | Rm
3055                // First halfword: 1111 1011 1001 Rn = 0xFB90 | Rn
3056                // Second halfword: 1111 Rd 1111 Rm = 0xF0F0 | Rd<<8 | Rm
3057                let hw1: u16 = (0xFB90 | rn_bits) as u16;
3058                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
3059
3060                // Thumb-2 32-bit instructions: first halfword, then second halfword (little-endian each)
3061                let mut bytes = hw1.to_le_bytes().to_vec();
3062                bytes.extend_from_slice(&hw2.to_le_bytes());
3063                encoding_contracts::verify_thumb32(&bytes);
3064                Ok(bytes)
3065            }
3066
3067            // UDIV: 11111011 1011 Rn 1111 Rd 1111 Rm
3068            ArmOp::Udiv { rd, rn, rm } => {
3069                let rd_bits = reg_to_bits(rd);
3070                let rn_bits = reg_to_bits(rn);
3071                let rm_bits = reg_to_bits(rm);
3072                reg_bits_checked(rd_bits)?;
3073                reg_bits_checked(rn_bits)?;
3074                reg_bits_checked(rm_bits)?;
3075
3076                // Thumb-2 UDIV: FBB0 F0F0 | Rn<<16 | Rd<<8 | Rm
3077                let hw1: u16 = (0xFBB0 | rn_bits) as u16;
3078                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
3079
3080                let mut bytes = hw1.to_le_bytes().to_vec();
3081                bytes.extend_from_slice(&hw2.to_le_bytes());
3082                encoding_contracts::verify_thumb32(&bytes);
3083                Ok(bytes)
3084            }
3085
3086            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
3087                let rdlo_bits = reg_to_bits(rdlo);
3088                let rdhi_bits = reg_to_bits(rdhi);
3089                let rn_bits = reg_to_bits(rn);
3090                let rm_bits = reg_to_bits(rm);
3091                reg_bits_checked(rdlo_bits)?;
3092                reg_bits_checked(rdhi_bits)?;
3093                reg_bits_checked(rn_bits)?;
3094                reg_bits_checked(rm_bits)?;
3095
3096                // Thumb-2 UMULL: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm
3097                let hw1: u16 = (0xFBA0 | rn_bits) as u16;
3098                let hw2: u16 = ((rdlo_bits << 12) | (rdhi_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            // MUL (Thumb-2 32-bit): MUL Rd, Rn, Rm
3107            ArmOp::Mul { rd, rn, rm } => {
3108                let rd_bits = reg_to_bits(rd);
3109                let rn_bits = reg_to_bits(rn);
3110                let rm_bits = reg_to_bits(rm);
3111
3112                // Thumb-2 MUL: FB00 F000 | Rn | Rd<<8 | Rm
3113                // 11111011 0000 Rn | 1111 Rd 0000 Rm
3114                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3115                let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
3116
3117                let mut bytes = hw1.to_le_bytes().to_vec();
3118                bytes.extend_from_slice(&hw2.to_le_bytes());
3119                Ok(bytes)
3120            }
3121
3122            // MLS: Rd = Ra - Rn * Rm
3123            ArmOp::Mls { rd, rn, rm, ra } => {
3124                let rd_bits = reg_to_bits(rd);
3125                let rn_bits = reg_to_bits(rn);
3126                let rm_bits = reg_to_bits(rm);
3127                let ra_bits = reg_to_bits(ra);
3128
3129                // Thumb-2 MLS: FB00 Rn | Ra Rd 0001 Rm
3130                // 11111011 0000 Rn | Ra Rd 0001 Rm
3131                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3132                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | 0x10 | rm_bits) as u16;
3133
3134                let mut bytes = hw1.to_le_bytes().to_vec();
3135                bytes.extend_from_slice(&hw2.to_le_bytes());
3136                Ok(bytes)
3137            }
3138
3139            ArmOp::Mla { rd, rn, rm, ra } => {
3140                let rd_bits = reg_to_bits(rd);
3141                let rn_bits = reg_to_bits(rn);
3142                let rm_bits = reg_to_bits(rm);
3143                let ra_bits = reg_to_bits(ra);
3144
3145                // Thumb-2 MLA: FB00 Rn | Ra Rd 0000 Rm — same as MLS without the
3146                // bit-4 (0x10) op flag. rd = ra + rn*rm.
3147                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3148                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | rm_bits) as u16;
3149
3150                let mut bytes = hw1.to_le_bytes().to_vec();
3151                bytes.extend_from_slice(&hw2.to_le_bytes());
3152                Ok(bytes)
3153            }
3154
3155            // AND (Thumb-2 32-bit)
3156            ArmOp::And { rd, rn, op2 } => {
3157                if let Operand2::Reg(rm) = op2 {
3158                    let rd_bits = reg_to_bits(rd);
3159                    let rn_bits = reg_to_bits(rn);
3160                    let rm_bits = reg_to_bits(rm);
3161
3162                    // Thumb-2 AND register: EA00 Rn | 0 Rd 00 00 Rm
3163                    let hw1: u16 = (0xEA00 | rn_bits) as u16;
3164                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3165
3166                    let mut bytes = hw1.to_le_bytes().to_vec();
3167                    bytes.extend_from_slice(&hw2.to_le_bytes());
3168                    Ok(bytes)
3169                } else if let Operand2::Imm(imm) = op2 {
3170                    let rd_bits = reg_to_bits(rd);
3171                    let rn_bits = reg_to_bits(rn);
3172
3173                    // Thumb-2 AND.W immediate T1: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8.
3174                    // The i:imm3:imm8 field is a ThumbExpandImm modified immediate —
3175                    // encode it correctly (or error on an un-encodable value)
3176                    // rather than packing raw bits, closing the silent-miscompile
3177                    // class for AND alongside ORR/EOR (#251) / ADD/SUB (#253) /
3178                    // CMP (#255).
3179                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3180                        synth_core::Error::synthesis(
3181                            "AND immediate is not a valid ThumbExpandImm — materialize into a register",
3182                        )
3183                    })?;
3184                    let i_bit = (field >> 11) & 1;
3185                    let imm3 = (field >> 8) & 0x7;
3186                    let imm8 = field & 0xFF;
3187
3188                    let hw1: u16 = (0xF000 | (i_bit << 10) | rn_bits) as u16;
3189                    let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3190
3191                    let mut bytes = hw1.to_le_bytes().to_vec();
3192                    bytes.extend_from_slice(&hw2.to_le_bytes());
3193                    Ok(bytes)
3194                } else {
3195                    // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
3196                    // operand form the arm does not encode (Reg and Imm only) -- #615's
3197                    // silent-miscompile class, on the DEFAULT target. Refuse instead.
3198                    Err(synth_core::Error::synthesis(format!(
3199                        "Thumb-2 AND with operand {op2:?} has no Thumb-2 encoding here (Reg and Imm \
3200                         are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
3201                    )))
3202                }
3203            }
3204
3205            // ORR (Thumb-2 32-bit)
3206            ArmOp::Orr { rd, rn, op2 } => {
3207                if let Operand2::Reg(rm) = op2 {
3208                    let rd_bits = reg_to_bits(rd);
3209                    let rn_bits = reg_to_bits(rn);
3210                    let rm_bits = reg_to_bits(rm);
3211
3212                    // Thumb-2 ORR: EA40 Rn | 0 Rd 00 00 Rm
3213                    let hw1: u16 = (0xEA40 | rn_bits) as u16;
3214                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3215
3216                    let mut bytes = hw1.to_le_bytes().to_vec();
3217                    bytes.extend_from_slice(&hw2.to_le_bytes());
3218                    Ok(bytes)
3219                } else if let Operand2::Imm(imm) = op2 {
3220                    // ORR.W immediate T1: 11110 i 0 0010 S Rn | 0 imm3 Rd imm8.
3221                    // Only the zero-extended byte form (imm <= 0xFF) is encoded;
3222                    // larger modified immediates need ThumbExpandImm — return an
3223                    // error rather than silently emit a NOP (Ok-or-Err, #180/#185).
3224                    let imm_val = *imm as u32;
3225                    if imm_val > 0xFF {
3226                        return Err(synth_core::Error::synthesis(
3227                            "ORR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3228                        ));
3229                    }
3230                    let rd_bits = reg_to_bits(rd);
3231                    let rn_bits = reg_to_bits(rn);
3232                    let hw1: u16 = (0xF040 | rn_bits) as u16;
3233                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3234                    let mut bytes = hw1.to_le_bytes().to_vec();
3235                    bytes.extend_from_slice(&hw2.to_le_bytes());
3236                    Ok(bytes)
3237                } else {
3238                    // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
3239                    // operand form the arm does not encode (Reg and Imm only) -- #615's
3240                    // silent-miscompile class, on the DEFAULT target. Refuse instead.
3241                    Err(synth_core::Error::synthesis(format!(
3242                        "Thumb-2 ORR with operand {op2:?} has no Thumb-2 encoding here (Reg and Imm \
3243                         are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
3244                    )))
3245                }
3246            }
3247
3248            // EOR (Thumb-2 32-bit)
3249            ArmOp::Eor { rd, rn, op2 } => {
3250                if let Operand2::Reg(rm) = op2 {
3251                    let rd_bits = reg_to_bits(rd);
3252                    let rn_bits = reg_to_bits(rn);
3253                    let rm_bits = reg_to_bits(rm);
3254
3255                    // Thumb-2 EOR: EA80 Rn | 0 Rd 00 00 Rm
3256                    let hw1: u16 = (0xEA80 | rn_bits) as u16;
3257                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3258
3259                    let mut bytes = hw1.to_le_bytes().to_vec();
3260                    bytes.extend_from_slice(&hw2.to_le_bytes());
3261                    Ok(bytes)
3262                } else if let Operand2::Imm(imm) = op2 {
3263                    // EOR.W immediate T1: 11110 i 0 0100 S Rn | 0 imm3 Rd imm8.
3264                    // Byte form only (imm <= 0xFF); larger needs ThumbExpandImm —
3265                    // error, not a silent NOP (Ok-or-Err, #180/#185).
3266                    let imm_val = *imm as u32;
3267                    if imm_val > 0xFF {
3268                        return Err(synth_core::Error::synthesis(
3269                            "EOR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3270                        ));
3271                    }
3272                    let rd_bits = reg_to_bits(rd);
3273                    let rn_bits = reg_to_bits(rn);
3274                    let hw1: u16 = (0xF080 | rn_bits) as u16;
3275                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3276                    let mut bytes = hw1.to_le_bytes().to_vec();
3277                    bytes.extend_from_slice(&hw2.to_le_bytes());
3278                    Ok(bytes)
3279                } else {
3280                    // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
3281                    // operand form the arm does not encode (Reg and Imm only) -- #615's
3282                    // silent-miscompile class, on the DEFAULT target. Refuse instead.
3283                    Err(synth_core::Error::synthesis(format!(
3284                        "Thumb-2 EOR with operand {op2:?} has no Thumb-2 encoding here (Reg and Imm \
3285                         are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
3286                    )))
3287                }
3288            }
3289
3290            // Shift operations (16-bit for low registers)
3291            ArmOp::Lsl { rd, rn, shift } => {
3292                let rd_bits = reg_to_bits(rd) as u16;
3293                let rn_bits = reg_to_bits(rn) as u16;
3294                let shift_bits = (*shift as u16) & 0x1F;
3295
3296                if rd_bits < 8 && rn_bits < 8 {
3297                    // LSLS Rd, Rm, #imm5 (16-bit): 0000 0 imm5 Rm Rd
3298                    let instr: u16 = (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3299                    Ok(instr.to_le_bytes().to_vec())
3300                } else {
3301                    // Use 32-bit encoding for high registers
3302                    self.encode_thumb32_shift(rd, rn, *shift, 0b00) // LSL type
3303                }
3304            }
3305
3306            ArmOp::Lsr { rd, rn, shift } => {
3307                let rd_bits = reg_to_bits(rd) as u16;
3308                let rn_bits = reg_to_bits(rn) as u16;
3309                let shift_bits = (*shift as u16) & 0x1F;
3310
3311                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3312                    // LSRS Rd, Rm, #imm5 (16-bit): 0000 1 imm5 Rm Rd
3313                    let instr: u16 = 0x0800 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3314                    Ok(instr.to_le_bytes().to_vec())
3315                } else {
3316                    self.encode_thumb32_shift(rd, rn, *shift, 0b01) // LSR type
3317                }
3318            }
3319
3320            ArmOp::Asr { rd, rn, shift } => {
3321                let rd_bits = reg_to_bits(rd) as u16;
3322                let rn_bits = reg_to_bits(rn) as u16;
3323                let shift_bits = (*shift as u16) & 0x1F;
3324
3325                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3326                    // ASRS Rd, Rm, #imm5 (16-bit): 0001 0 imm5 Rm Rd
3327                    let instr: u16 = 0x1000 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3328                    Ok(instr.to_le_bytes().to_vec())
3329                } else {
3330                    self.encode_thumb32_shift(rd, rn, *shift, 0b10) // ASR type
3331                }
3332            }
3333
3334            ArmOp::Ror { rd, rn, shift } => {
3335                // ROR doesn't have a 16-bit immediate form, use 32-bit
3336                self.encode_thumb32_shift(rd, rn, *shift, 0b11) // ROR type
3337            }
3338
3339            // Register-based shifts (Thumb-2 32-bit)
3340            // Encoding: 11111010 0xxS Rn 1111 Rd 0000 Rm
3341            // xx = shift type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
3342            ArmOp::LslReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b00),
3343            ArmOp::LsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b01),
3344            ArmOp::AsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b10),
3345            ArmOp::RorReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b11),
3346
3347            // RSB (Reverse Subtract): Rd = imm - Rn
3348            // Thumb-2 T2 encoding: 11110 i 0 1110 S Rn | 0 imm3 Rd imm8
3349            ArmOp::Rsb { rd, rn, imm } => {
3350                let rd_bits = reg_to_bits(rd);
3351                let rn_bits = reg_to_bits(rn);
3352
3353                // #681 class audit: the T2 `i:imm3:imm8` field is a
3354                // ThumbExpandImm modified immediate and RSB has NO plain-imm12
3355                // (T4-style) form — packing a raw value > 0xFF silently encodes
3356                // a different constant (#253/#255 class). All current emitters
3357                // use imm 32 (shift complement), which expands to itself, so
3358                // this gate is byte-identical for existing codegen.
3359                let field = try_thumb_expand_imm(*imm).ok_or_else(|| {
3360                    synth_core::Error::synthesis(
3361                        "RSB immediate is not a valid ThumbExpandImm — materialize into a register",
3362                    )
3363                })?;
3364                let i_bit = (field >> 11) & 1;
3365                let imm3 = (field >> 8) & 0x7;
3366                let imm8 = field & 0xFF;
3367
3368                // hw1: 11110 i 01110 0 Rn  (S=0)
3369                let hw1: u16 = (0xF1C0 | (i_bit << 10) | rn_bits) as u16;
3370                // hw2: 0 imm3 Rd imm8
3371                let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3372
3373                let mut bytes = hw1.to_le_bytes().to_vec();
3374                bytes.extend_from_slice(&hw2.to_le_bytes());
3375                Ok(bytes)
3376            }
3377
3378            // CLZ (Thumb-2 32-bit)
3379            ArmOp::Clz { rd, rm } => {
3380                let rd_bits = reg_to_bits(rd);
3381                let rm_bits = reg_to_bits(rm);
3382
3383                // Thumb-2 CLZ: FAB0 Rm | F8 Rd Rm
3384                // 11111010 1011 Rm | 1111 1000 Rd Rm
3385                let hw1: u16 = (0xFAB0 | rm_bits) as u16;
3386                let hw2: u16 = (0xF080 | (rd_bits << 8) | rm_bits) as u16;
3387
3388                let mut bytes = hw1.to_le_bytes().to_vec();
3389                bytes.extend_from_slice(&hw2.to_le_bytes());
3390                Ok(bytes)
3391            }
3392
3393            // RBIT (Thumb-2 32-bit)
3394            ArmOp::Rbit { rd, rm } => {
3395                let rd_bits = reg_to_bits(rd);
3396                let rm_bits = reg_to_bits(rm);
3397
3398                // Thumb-2 RBIT: FA90 Rm | F0 Rd A0 Rm
3399                // 11111010 1001 Rm | 1111 Rd 1010 Rm
3400                let hw1: u16 = (0xFA90 | rm_bits) as u16;
3401                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rm_bits) as u16;
3402
3403                let mut bytes = hw1.to_le_bytes().to_vec();
3404                bytes.extend_from_slice(&hw2.to_le_bytes());
3405                Ok(bytes)
3406            }
3407
3408            // SXTB (16-bit for low registers)
3409            ArmOp::Sxtb { rd, rm } => {
3410                let rd_bits = reg_to_bits(rd) as u16;
3411                let rm_bits = reg_to_bits(rm) as u16;
3412
3413                if rd_bits < 8 && rm_bits < 8 {
3414                    // SXTB Rd, Rm (16-bit): 1011 0010 01 Rm Rd
3415                    let instr: u16 = 0xB240 | (rm_bits << 3) | rd_bits;
3416                    Ok(instr.to_le_bytes().to_vec())
3417                } else {
3418                    // Thumb-2 SXTB.W: FA4F F(rd)80 (rm)
3419                    // 11111010 0100 1111 | 1111 Rd 10 rotate Rm
3420                    let rd_bits32 = rd_bits as u32;
3421                    let rm_bits32 = rm_bits as u32;
3422                    let hw1: u16 = 0xFA4F;
3423                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3424                    let mut bytes = hw1.to_le_bytes().to_vec();
3425                    bytes.extend_from_slice(&hw2.to_le_bytes());
3426                    Ok(bytes)
3427                }
3428            }
3429
3430            // SXTH (16-bit for low registers)
3431            ArmOp::Sxth { rd, rm } => {
3432                let rd_bits = reg_to_bits(rd) as u16;
3433                let rm_bits = reg_to_bits(rm) as u16;
3434
3435                if rd_bits < 8 && rm_bits < 8 {
3436                    // SXTH Rd, Rm (16-bit): 1011 0010 00 Rm Rd
3437                    let instr: u16 = 0xB200 | (rm_bits << 3) | rd_bits;
3438                    Ok(instr.to_le_bytes().to_vec())
3439                } else {
3440                    // Thumb-2 SXTH.W: FA0F F(rd)80 (rm)
3441                    // 11111010 0000 1111 | 1111 Rd 10 rotate Rm
3442                    let rd_bits32 = rd_bits as u32;
3443                    let rm_bits32 = rm_bits as u32;
3444                    let hw1: u16 = 0xFA0F;
3445                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3446                    let mut bytes = hw1.to_le_bytes().to_vec();
3447                    bytes.extend_from_slice(&hw2.to_le_bytes());
3448                    Ok(bytes)
3449                }
3450            }
3451
3452            // UXTB Rd,Rm — zero-extend byte (rd = rm & 0xff)
3453            ArmOp::Uxtb { rd, rm } => {
3454                let rd_bits = reg_to_bits(rd) as u16;
3455                let rm_bits = reg_to_bits(rm) as u16;
3456                if rd_bits < 8 && rm_bits < 8 {
3457                    // UXTB Rd, Rm (16-bit): 1011 0010 11 Rm Rd
3458                    let instr: u16 = 0xB2C0 | (rm_bits << 3) | rd_bits;
3459                    Ok(instr.to_le_bytes().to_vec())
3460                } else {
3461                    // Thumb-2 UXTB.W: FA5F F(rd)80 (rm)
3462                    let hw1: u16 = 0xFA5F;
3463                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3464                    let mut bytes = hw1.to_le_bytes().to_vec();
3465                    bytes.extend_from_slice(&hw2.to_le_bytes());
3466                    Ok(bytes)
3467                }
3468            }
3469
3470            // UXTH Rd,Rm — zero-extend halfword (rd = rm & 0xffff)
3471            ArmOp::Uxth { rd, rm } => {
3472                let rd_bits = reg_to_bits(rd) as u16;
3473                let rm_bits = reg_to_bits(rm) as u16;
3474                if rd_bits < 8 && rm_bits < 8 {
3475                    // UXTH Rd, Rm (16-bit): 1011 0010 10 Rm Rd
3476                    let instr: u16 = 0xB280 | (rm_bits << 3) | rd_bits;
3477                    Ok(instr.to_le_bytes().to_vec())
3478                } else {
3479                    // Thumb-2 UXTH.W: FA1F F(rd)80 (rm)
3480                    let hw1: u16 = 0xFA1F;
3481                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3482                    let mut bytes = hw1.to_le_bytes().to_vec();
3483                    bytes.extend_from_slice(&hw2.to_le_bytes());
3484                    Ok(bytes)
3485                }
3486            }
3487
3488            // CMP (can be 16-bit for low registers)
3489            ArmOp::Cmp { rn, op2 } => {
3490                let rn_bits = reg_to_bits(rn) as u16;
3491
3492                if let Operand2::Imm(imm) = op2 {
3493                    // Only use 16-bit encoding for non-negative immediates 0-255
3494                    // Negative immediates must use 32-bit encoding
3495                    if *imm >= 0 && *imm <= 255 && rn_bits < 8 {
3496                        // CMP Rn, #imm8 (16-bit): 0010 1 Rn imm8
3497                        let instr: u16 = 0x2800 | (rn_bits << 8) | (*imm as u16 & 0xFF);
3498                        Ok(instr.to_le_bytes().to_vec())
3499                    } else {
3500                        self.encode_thumb32_cmp_imm(rn, *imm as u32)
3501                    }
3502                } else if let Operand2::Reg(rm) = op2 {
3503                    let rm_bits = reg_to_bits(rm) as u16;
3504                    if rn_bits < 8 && rm_bits < 8 {
3505                        // CMP Rn, Rm (16-bit low): 0100 0010 10 Rm Rn
3506                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
3507                        Ok(instr.to_le_bytes().to_vec())
3508                    } else {
3509                        // CMP Rn, Rm (16-bit high): 0100 0101 N Rm Rn[2:0]
3510                        let n_bit = (rn_bits >> 3) & 1;
3511                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
3512                        Ok(instr.to_le_bytes().to_vec())
3513                    }
3514                } else {
3515                    // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
3516                    // operand form the arm does not encode (Imm and Reg only) -- #615's
3517                    // silent-miscompile class, on the DEFAULT target. Refuse instead.
3518                    Err(synth_core::Error::synthesis(format!(
3519                        "Thumb-2 CMP with operand {op2:?} has no Thumb-2 encoding here (Imm and Reg \
3520                         are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
3521                    )))
3522                }
3523            }
3524
3525            // CMN (Compare Negative) - computes Rn + op2 and sets flags
3526            // CMN Rn, #1 sets Z flag if Rn == -1 (since -1 + 1 = 0)
3527            ArmOp::Cmn { rn, op2 } => {
3528                let rn_bits = reg_to_bits(rn) as u16;
3529
3530                if let Operand2::Imm(imm) = op2 {
3531                    // CMN.W Rn, #imm (32-bit): i:imm3:imm8 is a ThumbExpandImm
3532                    // modified immediate (the field sits in imm3=hw2[14:12],
3533                    // imm8=hw2[7:0], i=hw1[10]). Encode it correctly, or error on
3534                    // an un-encodable value — replacing the old silent `0xBF00`
3535                    // NOP (the last of the silent-miscompile data-proc encoders).
3536                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3537                        synth_core::Error::synthesis(
3538                            "CMN immediate is not a valid ThumbExpandImm — materialize into a register",
3539                        )
3540                    })?;
3541                    let i_bit = (field >> 11) & 1;
3542                    let imm3 = (field >> 8) & 0x7;
3543                    let imm8 = field & 0xFF;
3544                    let hw1: u16 = (0xF110 | (i_bit << 10) as u16) | rn_bits;
3545                    let hw2: u16 = (imm3 << 12) as u16 | 0x0F00 | imm8 as u16;
3546                    let mut bytes = hw1.to_le_bytes().to_vec();
3547                    bytes.extend_from_slice(&hw2.to_le_bytes());
3548                    Ok(bytes)
3549                } else if let Operand2::Reg(rm) = op2 {
3550                    let rm_bits = reg_to_bits(rm) as u16;
3551                    // 16-bit CMN (T1) only encodes R0-R7; high registers overflow
3552                    // the 3-bit fields and corrupt the operands (#184, the #180
3553                    // class). CMN has no high-register 16-bit form, so fall back
3554                    // to 32-bit CMN.W (T2): EB10 Rn | 0F00 Rm (ADD.W with S=1 and
3555                    // Rd discarded as PC/1111).
3556                    if rn_bits < 8 && rm_bits < 8 {
3557                        // CMN Rn, Rm (16-bit): 0100 0010 11 Rm Rn
3558                        let instr: u16 = 0x42C0 | (rm_bits << 3) | rn_bits;
3559                        Ok(instr.to_le_bytes().to_vec())
3560                    } else {
3561                        let hw1: u16 = 0xEB10 | rn_bits;
3562                        let hw2: u16 = 0x0F00 | rm_bits;
3563                        let mut bytes = hw1.to_le_bytes().to_vec();
3564                        bytes.extend_from_slice(&hw2.to_le_bytes());
3565                        Ok(bytes)
3566                    }
3567                } else {
3568                    Ok(vec![0xBF, 0x00])
3569                }
3570            }
3571
3572            // LDR (can be 16-bit for simple cases)
3573            ArmOp::Ldr { rd, addr } => {
3574                let rd_bits = reg_to_bits(rd);
3575                let base_bits = reg_to_bits(&addr.base);
3576
3577                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3578                if let Some(offset_reg) = &addr.offset_reg {
3579                    let rm_bits = reg_to_bits(offset_reg);
3580
3581                    // If there's also an immediate offset, we need to ADD it first
3582                    if addr.offset != 0 {
3583                        // Use R12 (IP) as scratch to avoid clobbering the address register
3584                        // ADD R12, Rm, #offset; LDR Rd, [base, R12]
3585                        let scratch = Reg::R12;
3586                        let mut bytes =
3587                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3588                        bytes.extend(self.encode_thumb32_ldr_reg(rd, &addr.base, &scratch)?);
3589                        return Ok(bytes);
3590                    }
3591
3592                    // Simple register offset: LDR Rd, [Rn, Rm]
3593                    // 16-bit: only if Rd, Rn, Rm < R8
3594                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3595                        // LDR Rd, [Rn, Rm] (16-bit): 0101 100 Rm Rn Rd
3596                        let instr: u16 = 0x5800
3597                            | ((rm_bits as u16) << 6)
3598                            | ((base_bits as u16) << 3)
3599                            | (rd_bits as u16);
3600                        return Ok(instr.to_le_bytes().to_vec());
3601                    }
3602
3603                    // 32-bit register offset
3604                    return self.encode_thumb32_ldr_reg(rd, &addr.base, offset_reg);
3605                }
3606
3607                // Immediate offset mode [base, #imm]
3608                let offset = addr.offset as u32;
3609
3610                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3611                    // LDR Rd, [Rn, #imm5*4] (16-bit): 0110 1 imm5 Rn Rd
3612                    let imm5 = (offset >> 2) as u16;
3613                    let instr: u16 =
3614                        0x6800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3615                    Ok(instr.to_le_bytes().to_vec())
3616                } else {
3617                    self.encode_thumb32_ldr(rd, &addr.base, offset)
3618                }
3619            }
3620
3621            // STR (can be 16-bit for simple cases)
3622            ArmOp::Str { rd, addr } => {
3623                let rd_bits = reg_to_bits(rd);
3624                let base_bits = reg_to_bits(&addr.base);
3625
3626                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3627                if let Some(offset_reg) = &addr.offset_reg {
3628                    let rm_bits = reg_to_bits(offset_reg);
3629
3630                    // If there's also an immediate offset, we need to ADD it first
3631                    if addr.offset != 0 {
3632                        // Use R12 (IP) as scratch to avoid clobbering the address register
3633                        // ADD R12, Rm, #offset; STR Rd, [base, R12]
3634                        let scratch = Reg::R12;
3635                        let mut bytes =
3636                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3637                        bytes.extend(self.encode_thumb32_str_reg(rd, &addr.base, &scratch)?);
3638                        return Ok(bytes);
3639                    }
3640
3641                    // Simple register offset: STR Rd, [Rn, Rm]
3642                    // 16-bit: only if Rd, Rn, Rm < R8
3643                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3644                        // STR Rd, [Rn, Rm] (16-bit): 0101 000 Rm Rn Rd
3645                        let instr: u16 = 0x5000
3646                            | ((rm_bits as u16) << 6)
3647                            | ((base_bits as u16) << 3)
3648                            | (rd_bits as u16);
3649                        return Ok(instr.to_le_bytes().to_vec());
3650                    }
3651
3652                    // 32-bit register offset
3653                    return self.encode_thumb32_str_reg(rd, &addr.base, offset_reg);
3654                }
3655
3656                // Immediate offset mode [base, #imm]
3657                let offset = addr.offset as u32;
3658
3659                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3660                    // STR Rd, [Rn, #imm5*4] (16-bit): 0110 0 imm5 Rn Rd
3661                    let imm5 = (offset >> 2) as u16;
3662                    let instr: u16 =
3663                        0x6000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3664                    Ok(instr.to_le_bytes().to_vec())
3665                } else {
3666                    self.encode_thumb32_str(rd, &addr.base, offset)
3667                }
3668            }
3669
3670            // LDRB (Thumb-2)
3671            ArmOp::Ldrb { rd, addr } => {
3672                let rd_bits = reg_to_bits(rd);
3673                let base_bits = reg_to_bits(&addr.base);
3674
3675                if let Some(offset_reg) = &addr.offset_reg {
3676                    if addr.offset != 0 {
3677                        let scratch = Reg::R12;
3678                        let mut bytes =
3679                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3680                        bytes.extend(self.encode_thumb32_ldrb_reg(rd, &addr.base, &scratch)?);
3681                        return Ok(bytes);
3682                    }
3683                    return self.encode_thumb32_ldrb_reg(rd, &addr.base, offset_reg);
3684                }
3685
3686                let offset = addr.offset as u32;
3687                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3688                    // LDRB Rd, [Rn, #imm5] (16-bit): 0111 1 imm5 Rn Rd
3689                    let instr: u16 = 0x7800
3690                        | ((offset as u16) << 6)
3691                        | ((base_bits as u16) << 3)
3692                        | (rd_bits as u16);
3693                    Ok(instr.to_le_bytes().to_vec())
3694                } else {
3695                    self.encode_thumb32_ldrb_imm(rd, &addr.base, offset)
3696                }
3697            }
3698
3699            // LDRSB (Thumb-2)
3700            ArmOp::Ldrsb { rd, addr } => {
3701                let rd_bits = reg_to_bits(rd);
3702                let base_bits = reg_to_bits(&addr.base);
3703
3704                if let Some(offset_reg) = &addr.offset_reg {
3705                    if addr.offset != 0 {
3706                        let scratch = Reg::R12;
3707                        let mut bytes =
3708                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3709                        bytes.extend(self.encode_thumb32_ldrsb_reg(rd, &addr.base, &scratch)?);
3710                        return Ok(bytes);
3711                    }
3712                    return self.encode_thumb32_ldrsb_reg(rd, &addr.base, offset_reg);
3713                }
3714
3715                let offset = addr.offset as u32;
3716                // LDRSB has no 16-bit immediate form (only register)
3717                // For 16-bit reg form: only if Rd, Rn, Rm < R8
3718                if rd_bits < 8 && base_bits < 8 && offset == 0 {
3719                    // No immediate 16-bit encoding for LDRSB; use 32-bit
3720                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3721                } else {
3722                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3723                }
3724            }
3725
3726            // LDRH (Thumb-2)
3727            ArmOp::Ldrh { rd, addr } => {
3728                let rd_bits = reg_to_bits(rd);
3729                let base_bits = reg_to_bits(&addr.base);
3730
3731                if let Some(offset_reg) = &addr.offset_reg {
3732                    if addr.offset != 0 {
3733                        let scratch = Reg::R12;
3734                        let mut bytes =
3735                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3736                        bytes.extend(self.encode_thumb32_ldrh_reg(rd, &addr.base, &scratch)?);
3737                        return Ok(bytes);
3738                    }
3739                    return self.encode_thumb32_ldrh_reg(rd, &addr.base, offset_reg);
3740                }
3741
3742                let offset = addr.offset as u32;
3743                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3744                    // LDRH Rd, [Rn, #imm5*2] (16-bit): 1000 1 imm5 Rn Rd
3745                    let imm5 = (offset >> 1) as u16;
3746                    let instr: u16 =
3747                        0x8800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3748                    Ok(instr.to_le_bytes().to_vec())
3749                } else {
3750                    self.encode_thumb32_ldrh_imm(rd, &addr.base, offset)
3751                }
3752            }
3753
3754            // LDRSH (Thumb-2)
3755            ArmOp::Ldrsh { rd, addr } => {
3756                if let Some(offset_reg) = &addr.offset_reg {
3757                    if addr.offset != 0 {
3758                        let scratch = Reg::R12;
3759                        let mut bytes =
3760                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3761                        bytes.extend(self.encode_thumb32_ldrsh_reg(rd, &addr.base, &scratch)?);
3762                        return Ok(bytes);
3763                    }
3764                    return self.encode_thumb32_ldrsh_reg(rd, &addr.base, offset_reg);
3765                }
3766
3767                let offset = addr.offset as u32;
3768                self.encode_thumb32_ldrsh_imm(rd, &addr.base, offset)
3769            }
3770
3771            // STRB (Thumb-2)
3772            ArmOp::Strb { rd, addr } => {
3773                let rd_bits = reg_to_bits(rd);
3774                let base_bits = reg_to_bits(&addr.base);
3775
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_strb_reg(rd, &addr.base, &scratch)?);
3782                        return Ok(bytes);
3783                    }
3784                    return self.encode_thumb32_strb_reg(rd, &addr.base, offset_reg);
3785                }
3786
3787                let offset = addr.offset as u32;
3788                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3789                    // STRB Rd, [Rn, #imm5] (16-bit): 0111 0 imm5 Rn Rd
3790                    let instr: u16 = 0x7000
3791                        | ((offset as u16) << 6)
3792                        | ((base_bits as u16) << 3)
3793                        | (rd_bits as u16);
3794                    Ok(instr.to_le_bytes().to_vec())
3795                } else {
3796                    self.encode_thumb32_strb_imm(rd, &addr.base, offset)
3797                }
3798            }
3799
3800            // STRH (Thumb-2)
3801            ArmOp::Strh { rd, addr } => {
3802                let rd_bits = reg_to_bits(rd);
3803                let base_bits = reg_to_bits(&addr.base);
3804
3805                if let Some(offset_reg) = &addr.offset_reg {
3806                    if addr.offset != 0 {
3807                        let scratch = Reg::R12;
3808                        let mut bytes =
3809                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3810                        bytes.extend(self.encode_thumb32_strh_reg(rd, &addr.base, &scratch)?);
3811                        return Ok(bytes);
3812                    }
3813                    return self.encode_thumb32_strh_reg(rd, &addr.base, offset_reg);
3814                }
3815
3816                let offset = addr.offset as u32;
3817                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3818                    // STRH Rd, [Rn, #imm5*2] (16-bit): 1000 0 imm5 Rn Rd
3819                    let imm5 = (offset >> 1) as u16;
3820                    let instr: u16 =
3821                        0x8000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3822                    Ok(instr.to_le_bytes().to_vec())
3823                } else {
3824                    self.encode_thumb32_strh_imm(rd, &addr.base, offset)
3825                }
3826            }
3827
3828            // MemorySize (Thumb-2)
3829            ArmOp::MemorySize { rd } => {
3830                // LSR rd, R10, #16 — memory size in bytes / 65536 = pages
3831                // Thumb-2 16-bit: LSRS Rd, Rm, #imm5 — 0000 1 imm5 Rm Rd
3832                let rd_bits = reg_to_bits(rd);
3833                let r10_bits = reg_to_bits(&Reg::R10);
3834                if rd_bits < 8 && r10_bits < 8 {
3835                    let instr: u16 =
3836                        0x0800 | (16u16 << 6) | ((r10_bits as u16) << 3) | (rd_bits as u16);
3837                    Ok(instr.to_le_bytes().to_vec())
3838                } else {
3839                    // Thumb-2 32-bit LSR: 1110 1010 010 0 1111 | 0 imm3 Rd imm2 01 Rm
3840                    let imm5: u32 = 16;
3841                    let imm3 = (imm5 >> 2) & 0x7;
3842                    let imm2 = imm5 & 0x3;
3843                    let hw1: u16 = 0xEA4F;
3844                    let hw2: u16 =
3845                        ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | 0x10 | r10_bits) as u16;
3846                    let mut bytes = hw1.to_le_bytes().to_vec();
3847                    bytes.extend_from_slice(&hw2.to_le_bytes());
3848                    Ok(bytes)
3849                }
3850            }
3851
3852            // MemoryGrow (Thumb-2)
3853            ArmOp::MemoryGrow { rd, .. } => {
3854                // On embedded with fixed memory, always return -1 (failure)
3855                // MVN rd, #0 → MOV rd, #-1
3856                // Thumb-2 32-bit: MVN: 1111 0 i 0 0 0 1 1 0 1111 | 0 imm3 Rd imm8
3857                let rd_bits = reg_to_bits(rd);
3858                let hw1: u16 = 0xF06F; // MVN with i=0
3859                let hw2: u16 = (rd_bits << 8) as u16; // imm8=0 → ~0 = 0xFFFFFFFF = -1
3860                let mut bytes = hw1.to_le_bytes().to_vec();
3861                bytes.extend_from_slice(&hw2.to_le_bytes());
3862                Ok(bytes)
3863            }
3864
3865            // BX (16-bit)
3866            ArmOp::Bx { rm } => {
3867                let rm_bits = reg_to_bits(rm) as u16;
3868                // BX Rm (16-bit): 0100 0111 0 Rm 000
3869                let instr: u16 = 0x4700 | (rm_bits << 3);
3870                Ok(instr.to_le_bytes().to_vec())
3871            }
3872
3873            // BLX (16-bit) - Branch with Link and Exchange
3874            // BLX Rm: 0100 0111 1 Rm 000
3875            ArmOp::Blx { rm } => {
3876                let rm_bits = reg_to_bits(rm) as u16;
3877                let instr: u16 = 0x4780 | (rm_bits << 3);
3878                Ok(instr.to_le_bytes().to_vec())
3879            }
3880
3881            // CallIndirect - indirect function call via table lookup
3882            // table_index_reg contains the table index
3883            // Generates (#642): MOVW ip,#size [; MOVT]; CMP idx,ip; BLO +1;
3884            //                   UDF #0; LSL R12,idx,#2; LDR R12,[R11,R12]; BLX R12
3885            // #650, table_byte_offset != 0 (a non-zero table in the contiguous
3886            // R11 region): the pointer load becomes
3887            //                   ADD R12,R11,R12; LDR R12,[R12,#offset]
3888            // #664, null_check (the table has null slots, linked as ZERO
3889            // words): the loaded pointer is null-checked before the BLX —
3890            //                   CMP.W R12,#0; BNE +1; UDF #0
3891            // #676, type_check (heterogeneous table — runtime §4.4.8 type
3892            // check against the type-id sidecar at R11+off): after the
3893            // bounds guard —
3894            //                   LSL R12,idx,#2; ADD R12,R11,R12;
3895            //                   LDR R12,[R12,#type_off]; CMP.W R12,#id;
3896            //                   BEQ +1; UDF #0
3897            // (the dispatch tail then recomputes idx*4 — idx stays live).
3898            ArmOp::CallIndirect {
3899                rd: _,
3900                type_idx: _,
3901                table_index_reg,
3902                table_size,
3903                table_byte_offset,
3904                null_check,
3905                type_check,
3906            } => {
3907                let idx_reg = reg_to_bits(table_index_reg);
3908                let mut bytes = Vec::new();
3909
3910                // The expansion:
3911                // 1. Bounds guard (#642): trap (UDF #0, WASM Core §4.4.8) when
3912                //    index >= table size. Without it an out-of-bounds index
3913                //    reads past the table and BLXes whatever word lies there —
3914                //    an uncontrolled indirect branch instead of a trap.
3915                // 2. Multiplies index by 4 (function pointer size)
3916                // 3. Loads function pointer from table (table base in R11)
3917                // 4. Calls the function via BLX
3918                //
3919                // Table base setup must be done by caller/runtime. The type
3920                // check §4.4.8 also requires is discharged at COMPILE time:
3921                // the selector only emits this op after verifying the closed-
3922                // world property that every table entry's signature equals the
3923                // expected type (the raw code-pointer table carries no runtime
3924                // type ids to compare) — see the #642 selector guard.
3925
3926                // MOVW R12, #(size & 0xFFFF) — Thumb-2 T3:
3927                // 11110 i 100100 imm4 | 0 imm3 Rd imm8 (Rd=R12).
3928                let size_lo = *table_size & 0xFFFF;
3929                let hw1: u16 =
3930                    (0xF240 | (((size_lo >> 11) & 1) << 10) | ((size_lo >> 12) & 0xF)) as u16;
3931                let hw2: u16 =
3932                    ((((size_lo >> 8) & 0x7) << 12) | (12 << 8) | (size_lo & 0xFF)) as u16;
3933                bytes.extend_from_slice(&hw1.to_le_bytes());
3934                bytes.extend_from_slice(&hw2.to_le_bytes());
3935                // MOVT R12, #(size >> 16) — only when the table size exceeds
3936                // 16 bits (never in practice, but the guard must not compare
3937                // against a truncated size).
3938                let size_hi = *table_size >> 16;
3939                if size_hi != 0 {
3940                    let hw1: u16 =
3941                        (0xF2C0 | (((size_hi >> 11) & 1) << 10) | ((size_hi >> 12) & 0xF)) as u16;
3942                    let hw2: u16 =
3943                        ((((size_hi >> 8) & 0x7) << 12) | (12 << 8) | (size_hi & 0xFF)) as u16;
3944                    bytes.extend_from_slice(&hw1.to_le_bytes());
3945                    bytes.extend_from_slice(&hw2.to_le_bytes());
3946                }
3947                // CMP idx, R12 — 16-bit T2 (high-register capable):
3948                // 010001 01 N Rm(4) Rn(3), Rn full = N:Rn3.
3949                let cmp: u16 = (0x4500 | ((idx_reg & 8) << 4) | (12 << 3) | (idx_reg & 7)) as u16;
3950                bytes.extend_from_slice(&cmp.to_le_bytes());
3951                // BLO +1 insn (skip the UDF when index < size) — B<cond>.N
3952                // imm8=0: target = branch + 4. LO = unsigned lower.
3953                bytes.extend_from_slice(&0xD300u16.to_le_bytes());
3954                // UDF #0 — call_indirect out-of-bounds trap (same trap idiom as
3955                // the div-by-zero guards).
3956                bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3957
3958                // #676: runtime type check — ONLY for a heterogeneous table
3959                // (mixed signatures, closed-world verdict impossible). Load
3960                // the indexed slot's structural class id from the type-id
3961                // sidecar (`R11 + type_off + idx*4`; `type_off` = sidecar
3962                // base + this table's base offset, a compile-time constant)
3963                // and compare it against the expected type's class id — a
3964                // mismatch is the WASM Core §4.4.8 type trap. Null slots
3965                // carry the reserved id 0, so this compare subsumes the
3966                // #664 null trap (the selector passes `null_check: false`).
3967                // `None` emits NOTHING: every homogeneous table keeps the
3968                // pre-#676 bytes identical BY CONSTRUCTION. R12 stays the
3969                // only scratch (#212); the dispatch tail below recomputes
3970                // idx*4 — the index register is never clobbered here.
3971                if let Some((expected_id, type_off)) = type_check {
3972                    // RQ-61-IMMRANGE (#1072): compiled out in release, where
3973                    // the masks below silently TRUNCATE (id 256 compares as
3974                    // 0 — a NULL slot would pass the §4.4.8 check). The
3975                    // enforcement claim is DEMONSTRATED: the sole `Some`
3976                    // producer, `resolve_runtime_type_check`, loud-declines
3977                    // id > 255 / offset > 4095, tripped by
3978                    // `test_676_call_indirect_runtime_check_range_declines`
3979                    // (mutation-checked — see the A32 twin above).
3980                    debug_assert!(*expected_id <= 255, "selector enforces the CMP imm8 range");
3981                    debug_assert!(*type_off <= 4095, "selector enforces the LDR imm12 range");
3982                    // MOV.W R12, idx, LSL #2 (same encoding as the dispatch
3983                    // tail's index scale below).
3984                    bytes.extend_from_slice(&0xEA4Fu16.to_le_bytes());
3985                    bytes.extend_from_slice(
3986                        &(((0x0C00 | (0b10 << 6)) | idx_reg) as u16).to_le_bytes(),
3987                    );
3988                    // ADD.W R12, R11, R12 (the #650 base-add form).
3989                    bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
3990                    bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
3991                    // LDR.W R12, [R12, #type_off] — T3 LDR (immediate):
3992                    // 1111 1000 1101 Rn=1100 | Rt=1100 imm12.
3993                    bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
3994                    bytes.extend_from_slice(
3995                        &(0xC000u16 | (*type_off as u16 & 0x0FFF)).to_le_bytes(),
3996                    );
3997                    // CMP.W R12, #expected_id — T2 CMP (immediate), imm8
3998                    // (same form as the #664 null check's CMP.W R12, #0).
3999                    bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
4000                    bytes.extend_from_slice(
4001                        &(0x0F00u16 | (*expected_id as u16 & 0xFF)).to_le_bytes(),
4002                    );
4003                    // BEQ +1 insn (skip the UDF when the class id matches) —
4004                    // B<cond>.N imm8=0: target = branch + 4. EQ.
4005                    bytes.extend_from_slice(&0xD000u16.to_le_bytes());
4006                    // UDF #0 — the §4.4.8 type-mismatch trap (same idiom as
4007                    // the bounds guard above).
4008                    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
4009                }
4010
4011                // LSL R12, idx_reg, #2 (multiply index by 4)
4012                // Thumb-2 MOV with shift: 11101010 010 S 1111 | 0 imm3 Rd imm2 type Rm
4013                // LSL: type=00 (bits 5:4), imm5=2 -> imm3=000, imm2=10 (bits 7:6)
4014                // #597: the shift amount was previously shifted into bits 5:4 —
4015                // the TYPE field — encoding `mov.w ip, rm, ASR #32`, which
4016                // destroyed the index and dispatched table entry 0 for every
4017                // call. imm2 lives at bits 7:6.
4018                let hw1: u16 = 0xEA4F_u16; // MOV.W R12, Rm, LSL #2
4019                let hw2: u16 = ((0x0C00 | (0b10 << 6)) | idx_reg) as u16;
4020                bytes.extend_from_slice(&hw1.to_le_bytes());
4021                bytes.extend_from_slice(&hw2.to_le_bytes());
4022
4023                if *table_byte_offset == 0 {
4024                    // Table 0 (base = R11 itself): the pre-#650 single-load
4025                    // form — a single-table module's bytes stay identical BY
4026                    // CONSTRUCTION.
4027                    // LDR R12, [R11, R12] - load function pointer
4028                    // Thumb-2 LDR (register): 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
4029                    // Rn=R11, Rt=R12, Rm=R12, imm2=00 (no shift)
4030                    let ldr_hw1: u16 = 0xF85B; // LDR.W Rt, [R11, Rm]
4031                    let ldr_hw2: u16 = 0xC00C; // Rt=R12, imm2=00, Rm=R12
4032                    bytes.extend_from_slice(&ldr_hw1.to_le_bytes());
4033                    bytes.extend_from_slice(&ldr_hw2.to_le_bytes());
4034                } else {
4035                    // #650: table N of the contiguous R11 region — fold the
4036                    // compile-time base offset into the pointer load via the
4037                    // LDR imm12 form (R12 stays the only scratch, per the
4038                    // #212 convention).
4039                    assert!(
4040                        *table_byte_offset <= 4095,
4041                        "call_indirect table base offset {table_byte_offset} exceeds \
4042                         LDR imm12 — the selector must have declined this (#650)"
4043                    );
4044                    // ADD.W R12, R11, R12 — T3 ADD (register):
4045                    // 11101011000 S=0 Rn=1011 | 0 imm3=000 Rd=1100 imm2=00 type=00 Rm=1100
4046                    bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
4047                    bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
4048                    // LDR.W R12, [R12, #offset] — T3 LDR (immediate):
4049                    // 1111 1000 1101 Rn=1100 | Rt=1100 imm12
4050                    bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
4051                    bytes.extend_from_slice(
4052                        &((0xC000u16) | (*table_byte_offset as u16 & 0x0FFF)).to_le_bytes(),
4053                    );
4054                }
4055
4056                // #664: null-slot trap — ONLY when the table image carries
4057                // null (uninitialized) slots, which the layout contract
4058                // requires to be linked as ZERO words. A fully-initialized
4059                // table skips this branch entirely, keeping the pre-#664
4060                // expansion byte-identical BY CONSTRUCTION (the #650
4061                // offset-0 trick).
4062                if *null_check {
4063                    // CMP.W R12, #0 — T2 CMP (immediate): 11110 i 0 1101 1
4064                    // Rn(4) | 0 imm3 1111 imm8, Rn=R12, imm=0.
4065                    bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
4066                    bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
4067                    // BNE +1 insn (skip the UDF when the pointer is non-null)
4068                    // — B<cond>.N imm8=0: target = branch + 4. NE.
4069                    bytes.extend_from_slice(&0xD100u16.to_le_bytes());
4070                    // UDF #0 — call_indirect null-funcref trap (WASM Core
4071                    // §4.4.8: calling an uninitialized element traps; same
4072                    // trap idiom as the bounds guard above).
4073                    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
4074                }
4075
4076                // BLX R12 (call function indirectly)
4077                // BLX Rm (16-bit): 0100 0111 1 Rm 000
4078                let blx: u16 = 0x47E0; // BLX R12
4079                bytes.extend_from_slice(&blx.to_le_bytes());
4080
4081                Ok(bytes)
4082            }
4083
4084            // Label pseudo-instruction: emits no machine code
4085            ArmOp::Label { .. } => Ok(Vec::new()),
4086
4087            // Conditional branch to label (generic) - offset 0, will be patched
4088            ArmOp::Bcc { cond, label: _ } => {
4089                use synth_synthesis::Condition;
4090                let cond_bits: u16 = match cond {
4091                    Condition::EQ => 0x0,
4092                    Condition::NE => 0x1,
4093                    Condition::HS => 0x2,
4094                    Condition::LO => 0x3,
4095                    Condition::HI => 0x8,
4096                    Condition::LS => 0x9,
4097                    Condition::GE => 0xA,
4098                    Condition::LT => 0xB,
4099                    Condition::GT => 0xC,
4100                    Condition::LE => 0xD,
4101                };
4102                // 16-bit B<cond> with offset 0: 1101 cond imm8
4103                let instr: u16 = 0xD000 | (cond_bits << 8);
4104                Ok(instr.to_le_bytes().to_vec())
4105            }
4106
4107            // Branch instructions
4108            ArmOp::B { label: _ } => {
4109                // Simplified: B.N with offset 0
4110                // For real usage, would need label resolution
4111                let instr: u16 = 0xE000; // B.N #0
4112                Ok(instr.to_le_bytes().to_vec())
4113            }
4114
4115            // BHS (Branch if Higher or Same) - used for bounds checking
4116            // Condition code: 0x2 (C set)
4117            ArmOp::Bhs { label: _ } => {
4118                // 16-bit B<cond> with offset 0: 1101 cond imm8
4119                // cond = 0x2 (HS)
4120                let instr: u16 = 0xD200; // BHS.N #0
4121                Ok(instr.to_le_bytes().to_vec())
4122            }
4123
4124            // BLO (Branch if Lower) - complementary to BHS
4125            // Condition code: 0x3 (C clear)
4126            ArmOp::Blo { label: _ } => {
4127                // 16-bit B<cond> with offset 0: 1101 cond imm8
4128                // cond = 0x3 (LO)
4129                let instr: u16 = 0xD300; // BLO.N #0
4130                Ok(instr.to_le_bytes().to_vec())
4131            }
4132
4133            // Branch with numeric offset (Thumb-2)
4134            // Thumb-2 B.W instruction: 32-bit with +-16MB range
4135            ArmOp::BOffset { offset } => {
4136                // offset is already the halfword displacement: (target - branch - 4) / 2
4137                // This is the raw encoded value, accounting for variable-length instructions
4138                let halfword_offset = *offset;
4139
4140                // 16-bit B.N encoding: 1110 0 imm11 (11-bit signed halfword offset)
4141                // Range: -1024 to +1022 halfwords
4142                if (-1024..=1022).contains(&halfword_offset) {
4143                    // 16-bit B.N encoding: 1110 0 imm11
4144                    let imm11 = (halfword_offset as u16) & 0x7FF;
4145                    let instr: u16 = 0xE000 | imm11;
4146                    Ok(instr.to_le_bytes().to_vec())
4147                } else {
4148                    // 32-bit B.W encoding for larger offsets
4149                    // First halfword: 1111 0 S imm10
4150                    // Second halfword: 10 J1 0 J2 imm11
4151                    // Total offset = SignExtend(S:I1:I2:imm10:imm11:0)
4152                    // where I1 = NOT(J1 XOR S), I2 = NOT(J2 XOR S)
4153
4154                    // The B.W (T4) encoding packs the signed offset as:
4155                    //   S:I1:I2:imm10:imm11:0  (25-bit signed, halfword-aligned)
4156                    // where J1 = NOT(I1 XOR S), J2 = NOT(I2 XOR S)
4157                    // Input halfword_offset already equals (target - PC - 4) / 2,
4158                    // so the full byte offset = halfword_offset << 1.
4159                    // The encoding fields split that 25-bit signed value (including the
4160                    // implicit trailing zero) as: S | imm10 | imm11
4161                    // with I1 = bit 23 and I2 = bit 22 of the signed offset.
4162                    let signed_offset = halfword_offset << 1; // byte offset
4163                    let s = if signed_offset < 0 { 1u32 } else { 0u32 };
4164                    let uoffset = signed_offset as u32;
4165                    let imm10 = (uoffset >> 12) & 0x3FF; // bits [21:12]
4166                    let imm11 = (uoffset >> 1) & 0x7FF; // bits [11:1]
4167                    let i1 = (uoffset >> 23) & 1; // bit 23
4168                    let i2 = (uoffset >> 22) & 1; // bit 22
4169                    let j1 = (!(i1 ^ s)) & 1; // J1 = NOT(I1 XOR S)
4170                    let j2 = (!(i2 ^ s)) & 1; // J2 = NOT(I2 XOR S)
4171
4172                    let hw1: u16 = (0xF000 | (s << 10) | imm10) as u16;
4173                    let hw2: u16 = (0x9000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4174
4175                    let mut bytes = hw1.to_le_bytes().to_vec();
4176                    bytes.extend_from_slice(&hw2.to_le_bytes());
4177                    Ok(bytes)
4178                }
4179            }
4180
4181            // Conditional branch with numeric offset (Thumb-2)
4182            ArmOp::BCondOffset { cond, offset } => {
4183                use synth_synthesis::Condition;
4184                let cond_bits: u16 = match cond {
4185                    Condition::EQ => 0x0,
4186                    Condition::NE => 0x1,
4187                    Condition::HS => 0x2,
4188                    Condition::LO => 0x3,
4189                    Condition::HI => 0x8,
4190                    Condition::LS => 0x9,
4191                    Condition::GE => 0xA,
4192                    Condition::LT => 0xB,
4193                    Condition::GT => 0xC,
4194                    Condition::LE => 0xD,
4195                };
4196
4197                // offset is already the halfword displacement: (target - branch - 4) / 2
4198                // This is the raw imm8 value for 16-bit B<cond> encoding
4199                let halfword_offset = *offset;
4200
4201                // 16-bit B<cond> encoding: 1101 cond imm8
4202                // Range: -256 to +254 halfwords (imm8 is sign-extended and shifted left 1)
4203                if (-128..=127).contains(&halfword_offset) {
4204                    let imm8 = (halfword_offset as u16) & 0xFF;
4205                    let instr: u16 = 0xD000 | (cond_bits << 8) | imm8;
4206                    Ok(instr.to_le_bytes().to_vec())
4207                } else {
4208                    // 32-bit B<cond>.W (encoding T3) for larger offsets
4209                    // First halfword: 1111 0 S cond(4) imm6
4210                    // Second halfword: 10 J1 0 J2 imm11
4211                    //
4212                    // Per ARMv7-M, the branch BYTE offset is
4213                    // SignExtend(S:J2:J1:imm6:imm11:'0'), i.e. the field value
4214                    // S:J2:J1:imm6:imm11 IS the signed 20-bit HALFWORD offset —
4215                    // imm11/imm6/J1/J2/S take `halfword_offset` bits [10:0],
4216                    // [16:11], 17, 18 and 19 directly (mirroring the T4
4217                    // unconditional arm above).
4218                    //
4219                    // #740: this arm previously packed `halfword_offset >> 1`
4220                    // into imm6:imm11 — HALVING the displacement — so every
4221                    // wide conditional branch (span > 254 bytes) landed at half
4222                    // its intended offset: gust_poll's loop-head `br_if` to an
4223                    // outer block end jumped mid-shape. Narrow (16-bit) B<cond>
4224                    // encodings were unaffected, which is why short-range CF
4225                    // fixtures never caught it.
4226                    if !(-(1 << 19)..(1 << 19)).contains(&halfword_offset) {
4227                        return Err(synth_core::Error::synthesis(format!(
4228                            "B<cond>.W (T3) halfword offset {halfword_offset} exceeds \
4229                             the signed 20-bit encoding range (±1 MB) — refusing to \
4230                             emit a truncated branch"
4231                        )));
4232                    }
4233                    let u = halfword_offset as u32;
4234                    let imm11 = u & 0x7FF; // halfword offset bits [10:0]
4235                    let imm6 = (u >> 11) & 0x3F; // bits [16:11]
4236                    let j1 = (u >> 17) & 1; // bit 17
4237                    let j2 = (u >> 18) & 1; // bit 18
4238                    let s = (u >> 19) & 1; // sign (range-checked above)
4239
4240                    let hw1: u16 = (0xF000 | (s << 10) | ((cond_bits as u32) << 6) | imm6) as u16;
4241                    let hw2: u16 = (0x8000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4242
4243                    let mut bytes = hw1.to_le_bytes().to_vec();
4244                    bytes.extend_from_slice(&hw2.to_le_bytes());
4245                    Ok(bytes)
4246                }
4247            }
4248
4249            ArmOp::Bl { label: _ } => {
4250                // BL is always 32-bit in Thumb-2, encoded here as a relocatable
4251                // placeholder; an R_ARM_THM_CALL relocation patches the target
4252                // (see arm_backend.rs). The placeholder must carry an embedded
4253                // addend of -4 so the relocation nets to exactly the symbol S.
4254                //
4255                // Thumb BL computes `target = (P + 4) + signed_offset`. Under
4256                // R_ARM_THM_CALL the linker resolves using the in-place addend;
4257                // a 0xF800 placeholder (addend 0) lands at S+4 — every call one
4258                // instruction past the callee entry (#174). The correct
4259                // placeholder is what `gas` emits for `bl <extern>`:
4260                //   f7ff fffe  ->  `bl <self>`  (S=1, J1=J2=1, imm = -4 addend),
4261                // i.e. hw1=0xF7FF, hw2=0xFFFE. This nets to S, not S+4.
4262                // (The earlier 0xD000 was worse still — a ~+0x600000 addend,
4263                // the garbage `bl c0000c` and "truncated to fit" of #167.)
4264                let hw1: u16 = 0xF7FF;
4265                let hw2: u16 = 0xFFFE;
4266                let mut bytes = hw1.to_le_bytes().to_vec();
4267                bytes.extend_from_slice(&hw2.to_le_bytes());
4268                Ok(bytes)
4269            }
4270
4271            // MVN
4272            ArmOp::Mvn { rd, op2 } => {
4273                if let Operand2::Reg(rm) = op2 {
4274                    let rd_bits = reg_to_bits(rd) as u16;
4275                    let rm_bits = reg_to_bits(rm) as u16;
4276
4277                    if rd_bits < 8 && rm_bits < 8 {
4278                        // MVNS Rd, Rm (16-bit): 0100 0011 11 Rm Rd
4279                        let instr: u16 = 0x43C0 | (rm_bits << 3) | rd_bits;
4280                        Ok(instr.to_le_bytes().to_vec())
4281                    } else {
4282                        // 32-bit MVN
4283                        let hw1: u16 = 0xEA6F_u16;
4284                        let hw2: u16 = ((reg_to_bits(rd) << 8) | reg_to_bits(rm)) as u16;
4285                        let mut bytes = hw1.to_le_bytes().to_vec();
4286                        bytes.extend_from_slice(&hw2.to_le_bytes());
4287                        Ok(bytes)
4288                    }
4289                } else {
4290                    // #1272: this `else` used to return `0xBF00` (NOP) as `Ok` for an
4291                    // operand form the arm does not encode (Reg only) -- #615's
4292                    // silent-miscompile class, on the DEFAULT target. Refuse instead.
4293                    Err(synth_core::Error::synthesis(format!(
4294                        "Thumb-2 MVN with operand {op2:?} has no Thumb-2 encoding here (Reg \
4295                         are encoded) — refusing to emit a silent NOP (#1272, the #615 class)"
4296                    )))
4297                }
4298            }
4299
4300            // MOVW - Move Wide (Thumb-2 32-bit)
4301            ArmOp::Movw { rd, imm16 } => {
4302                self.encode_thumb32_movw_raw(reg_to_bits(rd), *imm16 as u32)
4303            }
4304
4305            // MOVT - Move Top (Thumb-2 32-bit)
4306            ArmOp::Movt { rd, imm16 } => {
4307                self.encode_thumb32_movt_raw(reg_to_bits(rd), *imm16 as u32)
4308            }
4309
4310            // #237: symbol-relative MOVW/MOVT. Encode the addend's low/high 16
4311            // bits in place; the backend records an R_ARM_MOVW_ABS_NC /
4312            // R_ARM_MOVT_ABS relocation against `symbol`, so the linker adds the
4313            // symbol's final address to the in-place addend (REL semantics).
4314            ArmOp::MovwSym { rd, addend, .. } => {
4315                self.encode_thumb32_movw_raw(reg_to_bits(rd), (*addend as u32) & 0xffff)
4316            }
4317            ArmOp::MovtSym { rd, addend, .. } => {
4318                self.encode_thumb32_movt_raw(reg_to_bits(rd), ((*addend as u32) >> 16) & 0xffff)
4319            }
4320
4321            // #345: literal-pool address load — emit a PLACEHOLDER `LDR.W rd,
4322            // [pc, #0]` (U=1, imm12=0). The backend (arm_backend.rs) places the
4323            // 4-byte pool word at the end of the function, records the R_ARM_ABS32
4324            // relocation against `symbol+addend`, and patches the imm12 with the
4325            // real PC-relative distance once the pool offset is known.
4326            // Encoding T2: 1111 1000 1101 1111 | Rt(4) imm12(12), with the literal
4327            // base = Align(PC,4) and PC = address of this instruction + 4.
4328            ArmOp::LdrSym { rd, .. } => {
4329                let rt = reg_to_bits(rd) as u16;
4330                let hw1: u16 = 0xF8DF; // LDR.W (literal), U=1
4331                let hw2: u16 = rt << 12; // imm12 = 0 placeholder
4332                let mut bytes = Vec::with_capacity(4);
4333                bytes.extend_from_slice(&hw1.to_le_bytes());
4334                bytes.extend_from_slice(&hw2.to_le_bytes());
4335                Ok(bytes)
4336            }
4337
4338            // SetCond: Materialize condition flag into register (0 or 1)
4339            // Strategy: ITE <cond>; MOV Rd, #1; MOV Rd, #0
4340            // IMPORTANT: Must use ITE (If-Then-Else) because 16-bit Thumb MOV
4341            // always sets flags (MOVS). We need to evaluate the condition BEFORE
4342            // any MOV instruction clobbers the flags from CMP.
4343            ArmOp::SetCond { rd, cond } => {
4344                let rd_bits = reg_to_bits(rd) as u16;
4345
4346                // Condition code encoding for IT block
4347                use synth_synthesis::Condition;
4348                let cond_bits: u16 = match cond {
4349                    Condition::EQ => 0x0,
4350                    Condition::NE => 0x1,
4351                    Condition::LT => 0xB,
4352                    Condition::LE => 0xD,
4353                    Condition::GT => 0xC,
4354                    Condition::GE => 0xA,
4355                    Condition::LO => 0x3, // CC/LO (unsigned <)
4356                    Condition::LS => 0x9, // LS (unsigned <=)
4357                    Condition::HI => 0x8, // HI (unsigned >)
4358                    Condition::HS => 0x2, // CS/HS (unsigned >=)
4359                };
4360
4361                // ITE <cond>: encodes If-Then-Else block
4362                // The mask field depends on firstcond[0]:
4363                // - If firstcond[0] = 0: mask = 0xC for TE pattern (ITE EQ = BF0C)
4364                // - If firstcond[0] = 1: mask = 0x4 for TE pattern (ITE NE = BF14)
4365                let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4366                let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4367
4368                // Materialize 0/1 into Rd. The 16-bit MOVS (T1) encodes Rd in a
4369                // 3-bit field (bits[10:8]) — only R0–R7. For a high register
4370                // (R8–R12) `rd_bits << 8` overflows into bit 11 and silently
4371                // turns MOVS into CMP (00100 → 00101), corrupting the result
4372                // (this mis-materialized gale's `has_waiter`, so its `local.set`
4373                // stored a stale register → the binary-sem WAKE dispatch read
4374                // garbage). Use the 32-bit MOV.W (T2) for high registers, which
4375                // has a 4-bit Rd field. MOV.W with S=0 doesn't set flags, which
4376                // is fine inside the ITE (the materialized value is the result;
4377                // the flags are not consumed afterwards).
4378                let mut bytes = ite_instr.to_le_bytes().to_vec();
4379                let push_mov = |bytes: &mut Vec<u8>, imm: u16| {
4380                    if rd_bits <= 7 {
4381                        let m: u16 = 0x2000 | (rd_bits << 8) | imm; // 16-bit MOVS Rd,#imm
4382                        bytes.extend_from_slice(&m.to_le_bytes());
4383                    } else {
4384                        // 32-bit MOV.W Rd, #imm (T2): F04F | (Rd<<8) | imm8
4385                        let hw1: u16 = 0xF04F;
4386                        let hw2: u16 = (rd_bits << 8) | imm;
4387                        bytes.extend_from_slice(&hw1.to_le_bytes());
4388                        bytes.extend_from_slice(&hw2.to_le_bytes());
4389                    }
4390                };
4391                push_mov(&mut bytes, 1); // Then branch (condition true)  → 1
4392                push_mov(&mut bytes, 0); // Else branch (condition false) → 0
4393                Ok(bytes)
4394            }
4395
4396            // I64SetCond: Compare two i64 register pairs, result 0/1 in rd
4397            // EQ/NE: CMP lo,lo; IT EQ; CMPEQ hi,hi; ITE <cond>; MOV 1; MOV 0
4398            // LT: CMP lo,lo; SBCS rd,hi,hi; ITE LT; MOV 1; MOV 0
4399            // GT: CMP lo,lo (swapped); SBCS rd,hi,hi (swapped); ITE LT; MOV 1; MOV 0
4400            ArmOp::I64SetCond {
4401                rd,
4402                rn_lo,
4403                rn_hi,
4404                rm_lo,
4405                rm_hi,
4406                cond,
4407            } => {
4408                use synth_synthesis::Condition;
4409                let rd_bits = reg_to_bits(rd) as u16;
4410                let mut bytes = Vec::new();
4411
4412                // Helper: encode CMP Rn, Rm (16-bit)
4413                let encode_cmp_reg = |rn: &synth_synthesis::Reg,
4414                                      rm: &synth_synthesis::Reg|
4415                 -> Vec<u8> {
4416                    let rn_bits = reg_to_bits(rn) as u16;
4417                    let rm_bits = reg_to_bits(rm) as u16;
4418                    if rn_bits < 8 && rm_bits < 8 {
4419                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
4420                        instr.to_le_bytes().to_vec()
4421                    } else {
4422                        let n_bit = (rn_bits >> 3) & 1;
4423                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
4424                        instr.to_le_bytes().to_vec()
4425                    }
4426                };
4427
4428                // Helper: encode ITE <cond> (2 bytes)
4429                let encode_ite = |cond_bits: u16| -> Vec<u8> {
4430                    let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4431                    let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4432                    ite_instr.to_le_bytes().to_vec()
4433                };
4434
4435                // Helper: encode SetCond (ITE + MOV #1 + MOV #0) for given condition
4436                let encode_setcond = |cond_bits: u16, rd_bits: u16| -> Vec<u8> {
4437                    let mut b = encode_ite(cond_bits);
4438                    if rd_bits < 8 {
4439                        let mov_one: u16 = 0x2001 | (rd_bits << 8);
4440                        let mov_zero: u16 = 0x2000 | (rd_bits << 8);
4441                        b.extend_from_slice(&mov_one.to_le_bytes());
4442                        b.extend_from_slice(&mov_zero.to_le_bytes());
4443                    } else {
4444                        // #311: rd >= R8 — the 16-bit MOV imm8 form has a 3-bit
4445                        // rd field; rd_bits<<8 overflows into bit 11 and
4446                        // TRANSMUTES the MOV into CMP (0x2001|0x0800 = 0x2801 =
4447                        // CMP r0,#1): the boolean dies in the flags and the
4448                        // consumer reads a stale register. Use the 32-bit
4449                        // MOV.W (T2: F04F 0000|rd<<8|imm8) — IT-legal,
4450                        // flag-preserving. Same class as H-CODE-9 / #180.
4451                        for imm in [1u16, 0u16] {
4452                            let hw1: u16 = 0xF04F;
4453                            let hw2: u16 = (rd_bits << 8) | imm;
4454                            b.extend_from_slice(&hw1.to_le_bytes());
4455                            b.extend_from_slice(&hw2.to_le_bytes());
4456                        }
4457                    }
4458                    b
4459                };
4460
4461                match cond {
4462                    Condition::EQ | Condition::NE => {
4463                        // CMP rn_lo, rm_lo (compare low words)
4464                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4465
4466                        // IT EQ (execute next instruction only if Z=1)
4467                        let it_eq: u16 = 0xBF08; // IT EQ: cond=0000, mask=1000
4468                        bytes.extend_from_slice(&it_eq.to_le_bytes());
4469
4470                        // CMPEQ rn_hi, rm_hi (compare high words, only if low equal)
4471                        bytes.extend_from_slice(&encode_cmp_reg(rn_hi, rm_hi));
4472
4473                        // ITE <cond>; MOV rd, #1; MOV rd, #0
4474                        let cond_bits: u16 = match cond {
4475                            Condition::EQ => 0x0,
4476                            Condition::NE => 0x1,
4477                            _ => unreachable!(),
4478                        };
4479                        bytes.extend_from_slice(&encode_setcond(cond_bits, rd_bits));
4480                    }
4481
4482                    Condition::LT => {
4483                        // CMP rn_lo, rm_lo (sets C flag for borrow)
4484                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4485
4486                        // SBCS rd, rn_hi, rm_hi (subtract with carry, sets N,V flags)
4487                        // SBCS.W Rd, Rn, Rm: EB70 Rn | 0000 Rd 0000 Rm
4488                        let rn_hi_bits = reg_to_bits(rn_hi);
4489                        let rm_hi_bits = reg_to_bits(rm_hi);
4490                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4491                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4492                        bytes.extend_from_slice(&hw1.to_le_bytes());
4493                        bytes.extend_from_slice(&hw2.to_le_bytes());
4494
4495                        // ITE LT; MOV rd, #1; MOV rd, #0
4496                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4497                    }
4498
4499                    Condition::GT => {
4500                        // GT(a,b) = LT(b,a): swap operands
4501                        // CMP rm_lo, rn_lo (swapped)
4502                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4503
4504                        // SBCS rd, rm_hi, rn_hi (swapped)
4505                        let rm_hi_bits = reg_to_bits(rm_hi);
4506                        let rn_hi_bits = reg_to_bits(rn_hi);
4507                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4508                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4509                        bytes.extend_from_slice(&hw1.to_le_bytes());
4510                        bytes.extend_from_slice(&hw2.to_le_bytes());
4511
4512                        // ITE LT; MOV rd, #1; MOV rd, #0
4513                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4514                    }
4515
4516                    Condition::LE => {
4517                        // LE(a,b) = !GT(a,b): use GT logic but invert result
4518                        // GT(a,b) = LT(b,a): so we do CMP(b,a) and check LT, then invert
4519                        // CMP rm_lo, rn_lo (swapped, same as GT)
4520                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4521
4522                        // SBCS rd, rm_hi, rn_hi (swapped)
4523                        let rm_hi_bits = reg_to_bits(rm_hi);
4524                        let rn_hi_bits = reg_to_bits(rn_hi);
4525                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4526                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4527                        bytes.extend_from_slice(&hw1.to_le_bytes());
4528                        bytes.extend_from_slice(&hw2.to_le_bytes());
4529
4530                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT, so inverting GT result)
4531                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4532                    }
4533
4534                    Condition::GE => {
4535                        // GE(a,b) = !LT(a,b): use LT logic but invert result
4536                        // CMP rn_lo, rm_lo (same as LT)
4537                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4538
4539                        // SBCS rd, rn_hi, rm_hi (same as LT)
4540                        let rn_hi_bits = reg_to_bits(rn_hi);
4541                        let rm_hi_bits = reg_to_bits(rm_hi);
4542                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4543                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4544                        bytes.extend_from_slice(&hw1.to_le_bytes());
4545                        bytes.extend_from_slice(&hw2.to_le_bytes());
4546
4547                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT)
4548                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4549                    }
4550
4551                    // Unsigned comparisons - same instruction sequence, different conditions
4552                    Condition::LO => {
4553                        // LO (unsigned LT): CMP lo, SBCS hi, check C=0
4554                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4555                        let rn_hi_bits = reg_to_bits(rn_hi);
4556                        let rm_hi_bits = reg_to_bits(rm_hi);
4557                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4558                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4559                        bytes.extend_from_slice(&hw1.to_le_bytes());
4560                        bytes.extend_from_slice(&hw2.to_le_bytes());
4561                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4562                    }
4563
4564                    Condition::HI => {
4565                        // HI (unsigned GT): swap operands and check LO
4566                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
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                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4574                    }
4575
4576                    Condition::LS => {
4577                        // LS (unsigned LE): !(a > b) = !(HI), so do HI and invert
4578                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4579                        let rm_hi_bits = reg_to_bits(rm_hi);
4580                        let rn_hi_bits = reg_to_bits(rn_hi);
4581                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4582                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4583                        bytes.extend_from_slice(&hw1.to_le_bytes());
4584                        bytes.extend_from_slice(&hw2.to_le_bytes());
4585                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4586                    }
4587
4588                    Condition::HS => {
4589                        // HS (unsigned GE): !(a < b) = !(LO)
4590                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4591                        let rn_hi_bits = reg_to_bits(rn_hi);
4592                        let rm_hi_bits = reg_to_bits(rm_hi);
4593                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4594                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4595                        bytes.extend_from_slice(&hw1.to_le_bytes());
4596                        bytes.extend_from_slice(&hw2.to_le_bytes());
4597                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4598                    }
4599                }
4600
4601                Ok(bytes)
4602            }
4603
4604            // I64SetCondZ: Test if i64 register pair is zero, result 0/1 in rd
4605            // ORR.W rd, rn_lo, rn_hi; CMP rd, #0; ITE EQ; MOV 1; MOV 0
4606            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
4607                let rd_bits = reg_to_bits(rd);
4608                let rn_lo_bits = reg_to_bits(rn_lo);
4609                let rn_hi_bits = reg_to_bits(rn_hi);
4610                let mut bytes = Vec::new();
4611
4612                // ORR.W rd, rn_lo, rn_hi: EA40 rn_lo | 0000 rd 0000 rn_hi
4613                let hw1: u16 = (0xEA40 | rn_lo_bits) as u16;
4614                let hw2: u16 = ((rd_bits << 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
4618                // CMP rd, #0 — 16-bit form only for r0-r7 (3-bit rd field);
4619                // high registers take CMP.W (T2: F1B0|rn 0F00|imm8). This was
4620                // H-CODE-9: rd_bits<<8 overflowing the field compared the
4621                // WRONG register. Same hardening as the #311 SetCond fix.
4622                if rd_bits < 8 {
4623                    let cmp_instr: u16 = 0x2800 | ((rd_bits as u16) << 8);
4624                    bytes.extend_from_slice(&cmp_instr.to_le_bytes());
4625                } else {
4626                    let hw1: u16 = 0xF1B0 | (rd_bits as u16);
4627                    let hw2: u16 = 0x0F00;
4628                    bytes.extend_from_slice(&hw1.to_le_bytes());
4629                    bytes.extend_from_slice(&hw2.to_le_bytes());
4630                }
4631
4632                // ITE EQ; MOV rd, #1; MOV rd, #0 (32-bit MOV.W for rd >= R8,
4633                // #311 — see I64SetCond)
4634                let mask = 0xC_u16; // ITE EQ mask: firstcond[0]=0, mask=0xC
4635                let ite_instr: u16 = 0xBF00 | mask;
4636                bytes.extend_from_slice(&ite_instr.to_le_bytes());
4637                if rd_bits < 8 {
4638                    let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
4639                    let mov_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
4640                    bytes.extend_from_slice(&mov_one.to_le_bytes());
4641                    bytes.extend_from_slice(&mov_zero.to_le_bytes());
4642                } else {
4643                    for imm in [1u16, 0u16] {
4644                        let hw1: u16 = 0xF04F;
4645                        let hw2: u16 = ((rd_bits as u16) << 8) | imm;
4646                        bytes.extend_from_slice(&hw1.to_le_bytes());
4647                        bytes.extend_from_slice(&hw2.to_le_bytes());
4648                    }
4649                }
4650
4651                Ok(bytes)
4652            }
4653
4654            // I64Mul: 64-bit multiply using UMULL + MLA cross products
4655            // Formula: result = (a_lo * b_lo) + ((a_lo * b_hi + a_hi * b_lo) << 32)
4656            // Uses R12 as scratch register
4657            ArmOp::I64Mul {
4658                rd_lo,
4659                rd_hi,
4660                rn_lo,
4661                rn_hi,
4662                rm_lo,
4663                rm_hi,
4664            } => {
4665                let rd_lo_bits = reg_to_bits(rd_lo);
4666                let rd_hi_bits = reg_to_bits(rd_hi);
4667                let rn_lo_bits = reg_to_bits(rn_lo);
4668                let rn_hi_bits = reg_to_bits(rn_hi);
4669                let rm_lo_bits = reg_to_bits(rm_lo);
4670                let rm_hi_bits = reg_to_bits(rm_hi);
4671                let r12: u32 = 12; // IP scratch register
4672                let mut bytes = Vec::new();
4673
4674                // 1. MUL R12, rn_lo, rm_hi  (R12 = a_lo * b_hi)
4675                // Thumb-2 MUL: hw1=0xFB00|Rn, hw2=0xF000|(Rd<<8)|Rm
4676                let hw1: u16 = (0xFB00 | rn_lo_bits) as u16;
4677                let hw2: u16 = (0xF000 | (r12 << 8) | rm_hi_bits) as u16;
4678                bytes.extend_from_slice(&hw1.to_le_bytes());
4679                bytes.extend_from_slice(&hw2.to_le_bytes());
4680
4681                // 2. MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
4682                // Thumb-2 MLA: hw1=0xFB00|Rn, hw2=(Ra<<12)|(Rd<<8)|Rm
4683                let hw1: u16 = (0xFB00 | rn_hi_bits) as u16;
4684                let hw2: u16 = ((r12 << 12) | (r12 << 8) | rm_lo_bits) as u16;
4685                bytes.extend_from_slice(&hw1.to_le_bytes());
4686                bytes.extend_from_slice(&hw2.to_le_bytes());
4687
4688                // 3. UMULL rd_lo, rd_hi, rn_lo, rm_lo  (rd_lo:rd_hi = a_lo * b_lo)
4689                // Thumb-2 UMULL: hw1=0xFBA0|Rn, hw2=(RdLo<<12)|(RdHi<<8)|Rm
4690                let hw1: u16 = (0xFBA0 | rn_lo_bits) as u16;
4691                let hw2: u16 = ((rd_lo_bits << 12) | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4692                bytes.extend_from_slice(&hw1.to_le_bytes());
4693                bytes.extend_from_slice(&hw2.to_le_bytes());
4694
4695                // 4. ADD rd_hi, R12  (rd_hi += cross products)
4696                // 16-bit high reg ADD: 01000100 D Rm Rdn[2:0]
4697                let d_bit = (rd_hi_bits >> 3) & 1;
4698                let add_instr: u16 =
4699                    (0x4400 | (d_bit << 7) | (r12 << 3) | (rd_hi_bits & 0x7)) as u16;
4700                bytes.extend_from_slice(&add_instr.to_le_bytes());
4701
4702                Ok(bytes)
4703            }
4704
4705            // I64Shl: 64-bit shift left with branch for n<32 vs n>=32.
4706            //
4707            // #1048: the expansion must NEVER write its own input operands.
4708            // The pre-#1048 sequence masked the amount IN PLACE
4709            // (`AND.W rm_lo, rm_lo, #63`) and used the amount's home high
4710            // register `rm_hi` as scratch (`SUBS.W rm_hi, rm_lo, #32`, RSB,
4711            // LSR) — so re-reading the amount after the shift returned a
4712            // mangled value (amt=64 read back 0, amt=67 read back 3). The
4713            // rewrite uses R12 — encoder scratch, never allocatable (#212) —
4714            // as the ONLY temporary, re-deriving the masked amount from the
4715            // untouched rm_lo whenever a second live temp would otherwise be
4716            // needed. This matches the Rocq/SMT pseudo-op models
4717            // (I64ShlPseudo writes rd_lo/rd_hi ONLY), which were proven over
4718            // exactly this non-clobbering contract all along.
4719            ArmOp::I64Shl {
4720                rd_lo,
4721                rd_hi,
4722                rn_lo,
4723                rn_hi,
4724                rm_lo,
4725                rm_hi: _,
4726            } => {
4727                let rd_lo_bits = reg_to_bits(rd_lo);
4728                let rd_hi_bits = reg_to_bits(rd_hi);
4729                let rn_lo_bits = reg_to_bits(rn_lo);
4730                let rn_hi_bits = reg_to_bits(rn_hi);
4731                let rm_lo_bits = reg_to_bits(rm_lo);
4732                let r12: u32 = 12; // the only scratch — never allocatable
4733                let mut bytes = Vec::new();
4734
4735                // #1039 house style: refuse a destination that would collide
4736                // with an input still needed after the destination is first
4737                // written, loudly — never misassemble. rd_hi is written before
4738                // rn_lo and rm_lo are last read; the in-place form
4739                // rd == rn (select_default) has rd_hi == rn_hi and stays legal.
4740                if rd_hi_bits == rn_lo_bits || rd_hi_bits == rm_lo_bits {
4741                    return Err(synth_core::Error::synthesis(format!(
4742                        "I64Shl: rd_hi {rd_hi:?} aliases an input ({rn_lo:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4743                    )));
4744                }
4745
4746                // AND.W R12, rm_lo, #63  (n — the amount register is only READ)
4747                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4748                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4749                bytes.extend_from_slice(&hw1.to_le_bytes());
4750                bytes.extend_from_slice(&hw2.to_le_bytes());
4751
4752                // SUBS.W R12, R12, #32  (R12 = n-32, sets flags)
4753                let hw1: u16 = (0xF1B0 | r12) as u16;
4754                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4755                bytes.extend_from_slice(&hw1.to_le_bytes());
4756                bytes.extend_from_slice(&hw2.to_le_bytes());
4757
4758                // BPL .large (branch if n >= 32, offset = +14 halfwords)
4759                let bpl: u16 = 0xD50E;
4760                bytes.extend_from_slice(&bpl.to_le_bytes());
4761
4762                // --- Small shift (n < 32) ---
4763                // AND.W R12, rm_lo, #63  (n again — R12 held n-32)
4764                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4765                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4766                bytes.extend_from_slice(&hw1.to_le_bytes());
4767                bytes.extend_from_slice(&hw2.to_le_bytes());
4768
4769                // LSL.W rd_hi, rn_hi, R12  (hi << n; rn_hi's last read)
4770                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4771                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4772                bytes.extend_from_slice(&hw1.to_le_bytes());
4773                bytes.extend_from_slice(&hw2.to_le_bytes());
4774
4775                // RSB.W R12, R12, #32  (R12 = 32-n)
4776                let hw1: u16 = (0xF1C0 | r12) as u16;
4777                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4778                bytes.extend_from_slice(&hw1.to_le_bytes());
4779                bytes.extend_from_slice(&hw2.to_le_bytes());
4780
4781                // LSR.W R12, rn_lo, R12  (overflow = lo >> (32-n); n=0 gives
4782                // a register shift by 32 which yields 0 — exact)
4783                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4784                let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4785                bytes.extend_from_slice(&hw1.to_le_bytes());
4786                bytes.extend_from_slice(&hw2.to_le_bytes());
4787
4788                // ORR.W rd_hi, rd_hi, R12  (hi |= overflow bits from lo)
4789                let hw1: u16 = (0xEA40 | rd_hi_bits) as u16;
4790                let hw2: u16 = ((rd_hi_bits << 8) | r12) as u16;
4791                bytes.extend_from_slice(&hw1.to_le_bytes());
4792                bytes.extend_from_slice(&hw2.to_le_bytes());
4793
4794                // AND.W R12, rm_lo, #63  (n once more for the low half)
4795                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4796                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4797                bytes.extend_from_slice(&hw1.to_le_bytes());
4798                bytes.extend_from_slice(&hw2.to_le_bytes());
4799
4800                // LSL.W rd_lo, rn_lo, R12  (lo << n)
4801                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4802                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4803                bytes.extend_from_slice(&hw1.to_le_bytes());
4804                bytes.extend_from_slice(&hw2.to_le_bytes());
4805
4806                // B .done — `.done` is the END of the expansion, i.e. PAST the
4807                // large-shift arm's trailing zero-fill. #916: that zero-fill is
4808                // 1 halfword for a low rd_lo but 2 for R8-R12 (MOV.W), so the
4809                // displacement is DERIVED from its real width instead of the
4810                // hard-coded 0xE002 — widening the MOV without this would
4811                // overshoot `.done` and turn a data miscompile into a
4812                // control-flow one. Thumb `B` reads PC as its own address + 4
4813                // (= +2 halfwords), so imm11 = (large-arm halfwords) - 1.
4814                let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_lo_bits);
4815                let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4816                bytes.extend_from_slice(&b_done.to_le_bytes());
4817
4818                // --- Large shift (n >= 32) --- (R12 still holds n-32)
4819                // LSL.W rd_hi, rn_lo, R12  (hi = lo << (n-32))
4820                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4821                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4822                bytes.extend_from_slice(&hw1.to_le_bytes());
4823                bytes.extend_from_slice(&hw2.to_le_bytes());
4824
4825                // MOV rd_lo, #0 (#916: MOV.W for rd_lo >= R8). NOTE the order
4826                // is load-bearing — zeroing rd_lo BEFORE the LSL.W would
4827                // destroy rn_lo in the in-place case rd_lo == rn_lo, so this
4828                // cannot be reordered to dodge the displacement change.
4829                emit_thumb_zero_fill(&mut bytes, rd_lo_bits);
4830
4831                Ok(bytes) // 46 bytes (48 when rd_lo >= R8 takes MOV.W)
4832            }
4833
4834            // I64ShrU: 64-bit logical shift right with branch for n<32 vs
4835            // n>=32. #1048: R12-only scratch, operands never written — see
4836            // the I64Shl comment for the full rationale.
4837            ArmOp::I64ShrU {
4838                rd_lo,
4839                rd_hi,
4840                rn_lo,
4841                rn_hi,
4842                rm_lo,
4843                rm_hi: _,
4844            } => {
4845                let rd_lo_bits = reg_to_bits(rd_lo);
4846                let rd_hi_bits = reg_to_bits(rd_hi);
4847                let rn_lo_bits = reg_to_bits(rn_lo);
4848                let rn_hi_bits = reg_to_bits(rn_hi);
4849                let rm_lo_bits = reg_to_bits(rm_lo);
4850                let r12: u32 = 12; // the only scratch — never allocatable
4851                let mut bytes = Vec::new();
4852
4853                // #1039 house style: rd_lo is written before rn_hi and rm_lo
4854                // are last read — refuse the collision loudly. The in-place
4855                // form rd == rn (select_default) has rd_lo == rn_lo and stays
4856                // legal.
4857                if rd_lo_bits == rn_hi_bits || rd_lo_bits == rm_lo_bits {
4858                    return Err(synth_core::Error::synthesis(format!(
4859                        "I64ShrU: rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4860                    )));
4861                }
4862
4863                // AND.W R12, rm_lo, #63  (n — the amount register is only READ)
4864                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4865                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4866                bytes.extend_from_slice(&hw1.to_le_bytes());
4867                bytes.extend_from_slice(&hw2.to_le_bytes());
4868
4869                // SUBS.W R12, R12, #32  (R12 = n-32, sets flags)
4870                let hw1: u16 = (0xF1B0 | r12) as u16;
4871                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4872                bytes.extend_from_slice(&hw1.to_le_bytes());
4873                bytes.extend_from_slice(&hw2.to_le_bytes());
4874
4875                // BPL .large (+14 halfwords)
4876                let bpl: u16 = 0xD50E;
4877                bytes.extend_from_slice(&bpl.to_le_bytes());
4878
4879                // --- Small shift (n < 32) ---
4880                // AND.W R12, rm_lo, #63  (n again — R12 held n-32)
4881                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4882                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4883                bytes.extend_from_slice(&hw1.to_le_bytes());
4884                bytes.extend_from_slice(&hw2.to_le_bytes());
4885
4886                // LSR.W rd_lo, rn_lo, R12  (lo >> n; rn_lo's last read)
4887                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4888                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4889                bytes.extend_from_slice(&hw1.to_le_bytes());
4890                bytes.extend_from_slice(&hw2.to_le_bytes());
4891
4892                // RSB.W R12, R12, #32  (R12 = 32-n)
4893                let hw1: u16 = (0xF1C0 | r12) as u16;
4894                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4895                bytes.extend_from_slice(&hw1.to_le_bytes());
4896                bytes.extend_from_slice(&hw2.to_le_bytes());
4897
4898                // LSL.W R12, rn_hi, R12  (overflow = hi << (32-n); n=0 gives
4899                // a register shift by 32 which yields 0 — exact)
4900                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4901                let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4902                bytes.extend_from_slice(&hw1.to_le_bytes());
4903                bytes.extend_from_slice(&hw2.to_le_bytes());
4904
4905                // ORR.W rd_lo, rd_lo, R12  (lo |= overflow from hi)
4906                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4907                let hw2: u16 = ((rd_lo_bits << 8) | r12) as u16;
4908                bytes.extend_from_slice(&hw1.to_le_bytes());
4909                bytes.extend_from_slice(&hw2.to_le_bytes());
4910
4911                // AND.W R12, rm_lo, #63  (n once more for the high half)
4912                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4913                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4914                bytes.extend_from_slice(&hw1.to_le_bytes());
4915                bytes.extend_from_slice(&hw2.to_le_bytes());
4916
4917                // LSR.W rd_hi, rn_hi, R12  (hi >> n, logical)
4918                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4919                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4920                bytes.extend_from_slice(&hw1.to_le_bytes());
4921                bytes.extend_from_slice(&hw2.to_le_bytes());
4922
4923                // B .done — see I64Shl: `.done` is the END of the expansion,
4924                // past the trailing zero-fill, so the displacement is derived
4925                // from that zero-fill's real width (#916).
4926                let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_hi_bits);
4927                let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4928                bytes.extend_from_slice(&b_done.to_le_bytes());
4929
4930                // --- Large shift (n >= 32) --- (R12 still holds n-32)
4931                // LSR.W rd_lo, rn_hi, R12  (lo = hi >> (n-32))
4932                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4933                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4934                bytes.extend_from_slice(&hw1.to_le_bytes());
4935                bytes.extend_from_slice(&hw2.to_le_bytes());
4936
4937                // MOV rd_hi, #0 (#916: MOV.W for rd_hi >= R8). Order is
4938                // load-bearing: the LSR.W above reads rn_hi, which may BE
4939                // rd_hi in the in-place case.
4940                emit_thumb_zero_fill(&mut bytes, rd_hi_bits);
4941
4942                Ok(bytes) // 46 bytes (48 when rd_hi >= R8 takes MOV.W)
4943            }
4944
4945            // I64ShrS: 64-bit arithmetic shift right with branch for n<32 vs
4946            // n>=32. #1048: R12-only scratch, operands never written — see
4947            // the I64Shl comment for the full rationale.
4948            ArmOp::I64ShrS {
4949                rd_lo,
4950                rd_hi,
4951                rn_lo,
4952                rn_hi,
4953                rm_lo,
4954                rm_hi: _,
4955            } => {
4956                let rd_lo_bits = reg_to_bits(rd_lo);
4957                let rd_hi_bits = reg_to_bits(rd_hi);
4958                let rn_lo_bits = reg_to_bits(rn_lo);
4959                let rn_hi_bits = reg_to_bits(rn_hi);
4960                let rm_lo_bits = reg_to_bits(rm_lo);
4961                let r12: u32 = 12; // the only scratch — never allocatable
4962                let mut bytes = Vec::new();
4963
4964                // #1039 house style: rd_lo is written before rn_hi and rm_lo
4965                // are last read (on BOTH arms of the diamond — the large arm's
4966                // trailing `ASR rd_hi, rn_hi, #31` also reads rn_hi after
4967                // rd_lo is written). The in-place form rd == rn stays legal.
4968                if rd_lo_bits == rn_hi_bits || rd_lo_bits == rm_lo_bits {
4969                    return Err(synth_core::Error::synthesis(format!(
4970                        "I64ShrS: rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4971                    )));
4972                }
4973
4974                // AND.W R12, rm_lo, #63  (n — the amount register is only READ)
4975                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4976                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4977                bytes.extend_from_slice(&hw1.to_le_bytes());
4978                bytes.extend_from_slice(&hw2.to_le_bytes());
4979
4980                // SUBS.W R12, R12, #32  (R12 = n-32, sets flags)
4981                let hw1: u16 = (0xF1B0 | r12) as u16;
4982                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4983                bytes.extend_from_slice(&hw1.to_le_bytes());
4984                bytes.extend_from_slice(&hw2.to_le_bytes());
4985
4986                // BPL .large (+14 halfwords)
4987                let bpl: u16 = 0xD50E;
4988                bytes.extend_from_slice(&bpl.to_le_bytes());
4989
4990                // --- Small shift (n < 32) ---
4991                // AND.W R12, rm_lo, #63  (n again — R12 held n-32)
4992                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4993                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4994                bytes.extend_from_slice(&hw1.to_le_bytes());
4995                bytes.extend_from_slice(&hw2.to_le_bytes());
4996
4997                // LSR.W rd_lo, rn_lo, R12  (lo >> n, logical for lo word)
4998                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4999                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
5000                bytes.extend_from_slice(&hw1.to_le_bytes());
5001                bytes.extend_from_slice(&hw2.to_le_bytes());
5002
5003                // RSB.W R12, R12, #32  (R12 = 32-n)
5004                let hw1: u16 = (0xF1C0 | r12) as u16;
5005                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
5006                bytes.extend_from_slice(&hw1.to_le_bytes());
5007                bytes.extend_from_slice(&hw2.to_le_bytes());
5008
5009                // LSL.W R12, rn_hi, R12  (overflow = hi << (32-n); n=0 gives
5010                // a register shift by 32 which yields 0 — exact)
5011                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
5012                let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
5013                bytes.extend_from_slice(&hw1.to_le_bytes());
5014                bytes.extend_from_slice(&hw2.to_le_bytes());
5015
5016                // ORR.W rd_lo, rd_lo, R12  (lo |= overflow from hi)
5017                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
5018                let hw2: u16 = ((rd_lo_bits << 8) | r12) as u16;
5019                bytes.extend_from_slice(&hw1.to_le_bytes());
5020                bytes.extend_from_slice(&hw2.to_le_bytes());
5021
5022                // AND.W R12, rm_lo, #63  (n once more for the high half)
5023                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
5024                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
5025                bytes.extend_from_slice(&hw1.to_le_bytes());
5026                bytes.extend_from_slice(&hw2.to_le_bytes());
5027
5028                // ASR.W rd_hi, rn_hi, R12  (hi >> n, arithmetic/sign-extending)
5029                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
5030                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
5031                bytes.extend_from_slice(&hw1.to_le_bytes());
5032                bytes.extend_from_slice(&hw2.to_le_bytes());
5033
5034                // B .done (+3 halfwords, large shift is 8 bytes)
5035                let b_done: u16 = 0xE003;
5036                bytes.extend_from_slice(&b_done.to_le_bytes());
5037
5038                // --- Large shift (n >= 32) --- (R12 still holds n-32)
5039                // ASR.W rd_lo, rn_hi, R12  (lo = hi >>> (n-32))
5040                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
5041                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
5042                bytes.extend_from_slice(&hw1.to_le_bytes());
5043                bytes.extend_from_slice(&hw2.to_le_bytes());
5044
5045                // ASR.W rd_hi, rn_hi, #31  (hi = sign extension, all 0s or all 1s)
5046                // Thumb-2 ASR immediate: hw1=0xEA4F, hw2=imm3:Rd:imm2:10:Rm
5047                // imm5=31=11111 → imm3=111, imm2=11
5048                let hw1: u16 = 0xEA4F;
5049                let hw2: u16 = (0x7000 | (rd_hi_bits << 8) | 0x00E0 | rn_hi_bits) as u16;
5050                bytes.extend_from_slice(&hw1.to_le_bytes());
5051                bytes.extend_from_slice(&hw2.to_le_bytes());
5052
5053                Ok(bytes) // Total: 48 bytes
5054            }
5055
5056            // I64Rotl: 64-bit rotate left (#610 rewrite).
5057            // For n < 32: new_hi = (hi << n) | (lo >> (32-n)), new_lo = (lo << n) | (hi >> (32-n))
5058            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
5059            //
5060            // Fixed-reg core: value in R0:R1, amount in R2, scratch R3 + R12
5061            // (all four saved/marshaled by the #610 fixed-ABI wrapper; the
5062            // pre-#610 expansion wrote through the selector's registers with
5063            // colliding R3/R4 scratch and restored the saved R4 OVER the
5064            // result). Relies on ARM register-shift semantics: amounts >= 32
5065            // yield 0 for LSL/LSR, which makes n = 0 and n = 32 exact.
5066            ArmOp::I64Rotl {
5067                rdlo,
5068                rdhi,
5069                rnlo,
5070                rnhi,
5071                shift,
5072            } => {
5073                let mut bytes = Vec::new();
5074                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
5075
5076                let core: [u16; 35] = [
5077                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
5078                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
5079                    0xD50E, //         BPL    .large        (n >= 32)
5080                    // --- small rotation (n < 32) ---
5081                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
5082                    0xFA20, 0xFC03, // LSR.W  R12, R0, R3   (lo >> (32-n))
5083                    0xFA21, 0xF303, // LSR.W  R3, R1, R3    (hi >> (32-n))
5084                    0xFA01, 0xF102, // LSL.W  R1, R1, R2    (hi << n)
5085                    0xEA41, 0x010C, // ORR.W  R1, R1, R12   (new_hi)
5086                    0xFA00, 0xF002, // LSL.W  R0, R0, R2    (lo << n)
5087                    0xEA40, 0x0003, // ORR.W  R0, R0, R3    (new_lo)
5088                    0xE00E, //         B      .done
5089                    // --- large rotation (n >= 32), R3 = m = n-32 ---
5090                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
5091                    0xFA21, 0xFC02, // LSR.W  R12, R1, R2   (hi >> (64-n))
5092                    0xFA20, 0xF202, // LSR.W  R2, R0, R2    (lo >> (64-n))
5093                    0xFA00, 0xF003, // LSL.W  R0, R0, R3    (lo << m)
5094                    0xFA01, 0xF103, // LSL.W  R1, R1, R3    (hi << m)
5095                    0xEA40, 0x0C0C, // ORR.W  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
5096                    0xEA41, 0x0002, // ORR.W  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
5097                    0x4661, //         MOV    R1, R12       (new_hi into place)
5098                            // .done: result in R0:R1
5099                ];
5100                for hw in core {
5101                    bytes.extend_from_slice(&hw.to_le_bytes());
5102                }
5103
5104                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5105                Ok(bytes) // Total: 102 bytes
5106            }
5107
5108            // I64Rotr: 64-bit rotate right (#610 rewrite).
5109            // For n < 32: new_lo = (lo >> n) | (hi << (32-n)), new_hi = (hi >> n) | (lo << (32-n))
5110            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
5111            //
5112            // Same fixed-reg core contract as I64Rotl: value in R0:R1, amount
5113            // in R2, scratch R3 + R12, all covered by the fixed-ABI wrapper.
5114            ArmOp::I64Rotr {
5115                rdlo,
5116                rdhi,
5117                rnlo,
5118                rnhi,
5119                shift,
5120            } => {
5121                let mut bytes = Vec::new();
5122                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
5123
5124                let core: [u16; 35] = [
5125                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
5126                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
5127                    0xD50E, //         BPL    .large        (n >= 32)
5128                    // --- small rotation (n < 32) ---
5129                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
5130                    0xFA01, 0xFC03, // LSL.W  R12, R1, R3   (hi << (32-n))
5131                    0xFA00, 0xF303, // LSL.W  R3, R0, R3    (lo << (32-n))
5132                    0xFA20, 0xF002, // LSR.W  R0, R0, R2    (lo >> n)
5133                    0xEA40, 0x000C, // ORR.W  R0, R0, R12   (new_lo)
5134                    0xFA21, 0xF102, // LSR.W  R1, R1, R2    (hi >> n)
5135                    0xEA41, 0x0103, // ORR.W  R1, R1, R3    (new_hi)
5136                    0xE00E, //         B      .done
5137                    // --- large rotation (n >= 32), R3 = m = n-32 ---
5138                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
5139                    0xFA00, 0xFC02, // LSL.W  R12, R0, R2   (lo << (64-n))
5140                    0xFA01, 0xF202, // LSL.W  R2, R1, R2    (hi << (64-n))
5141                    0xFA21, 0xF103, // LSR.W  R1, R1, R3    (hi >> m)
5142                    0xEA41, 0x0C0C, // ORR.W  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
5143                    0xFA20, 0xF103, // LSR.W  R1, R0, R3    (lo >> m)
5144                    0xEA41, 0x0102, // ORR.W  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
5145                    0x4660, //         MOV    R0, R12       (new_lo into place)
5146                            // .done: result in R0:R1
5147                ];
5148                for hw in core {
5149                    bytes.extend_from_slice(&hw.to_le_bytes());
5150                }
5151
5152                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5153                Ok(bytes) // Total: 102 bytes
5154            }
5155
5156            // I64Clz: Count leading zeros in 64-bit value
5157            // If hi != 0: result = CLZ(hi)
5158            // If hi == 0: result = 32 + CLZ(lo)
5159            //
5160            // Layout (using CMP+BNE approach for consistency):
5161            // 0: CMP.W rnhi, #0 (4 bytes)
5162            // 4: BEQ .hi_zero (2 bytes) - branch forward to offset 14
5163            // 6: CLZ.W rd, rnhi (4 bytes)
5164            // 10: B .done (2 bytes) - branch forward to offset 22
5165            // 12: NOP (2 bytes) - padding for alignment
5166            // 14: .hi_zero: CLZ.W rd, rnlo (4 bytes)
5167            // 18: ADD.W rd, rd, #32 (4 bytes)
5168            // 22: .done
5169            ArmOp::I64Clz { rd, rnlo, rnhi } => {
5170                let rd_bits = reg_to_bits(rd);
5171                let rn_lo_bits = reg_to_bits(rnlo);
5172                let rn_hi_bits = reg_to_bits(rnhi);
5173                let mut bytes = Vec::new();
5174
5175                // CMP.W rnhi, #0 (4 bytes at offset 0)
5176                let hw1: u16 = (0xF1B0 | rn_hi_bits) as u16;
5177                let hw2: u16 = 0x0F00;
5178                bytes.extend_from_slice(&hw1.to_le_bytes());
5179                bytes.extend_from_slice(&hw2.to_le_bytes());
5180
5181                // BEQ .hi_zero (2 bytes at offset 4)
5182                // PC = 4 + 4 = 8, target = 14, offset = 6, imm8 = 3
5183                let beq: u16 = 0xD003;
5184                bytes.extend_from_slice(&beq.to_le_bytes());
5185
5186                // CLZ.W rd, rnhi (4 bytes at offset 6)
5187                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5188                let hw1: u16 = (0xFAB0 | rn_hi_bits) as u16;
5189                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_hi_bits) as u16;
5190                bytes.extend_from_slice(&hw1.to_le_bytes());
5191                bytes.extend_from_slice(&hw2.to_le_bytes());
5192
5193                // B .done (2 bytes at offset 10)
5194                // PC = 10 + 4 = 14, target = 22, offset = 8, imm11 = 4
5195                let b_done: u16 = 0xE004;
5196                bytes.extend_from_slice(&b_done.to_le_bytes());
5197
5198                // NOP (2 bytes at offset 12) - padding
5199                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5200
5201                // .hi_zero: (offset 14)
5202                // CLZ.W rd, rnlo (4 bytes)
5203                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5204                let hw1: u16 = (0xFAB0 | rn_lo_bits) as u16;
5205                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_lo_bits) as u16;
5206                bytes.extend_from_slice(&hw1.to_le_bytes());
5207                bytes.extend_from_slice(&hw2.to_le_bytes());
5208
5209                // ADD.W rd, rd, #32 (4 bytes at offset 18)
5210                let hw1: u16 = (0xF100 | rd_bits) as u16;
5211                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5212                bytes.extend_from_slice(&hw1.to_le_bytes());
5213                bytes.extend_from_slice(&hw2.to_le_bytes());
5214
5215                // .done: (offset 22 — the end of the expansion)
5216                //
5217                // #1048: the former trailing hi-word clear (`MOV rnhi, #0`)
5218                // is GONE. It was aimed at the RESULT's high half but wrote
5219                // the OPERAND's home high register — a real executed
5220                // miscompile on the direct selector, which allocates a fresh
5221                // destination pair and zeroes its own dst_hi, leaving the
5222                // operand's hi limb destroyed for any later re-read. The
5223                // Rocq/SMT models of I64ClzPseudo always said "writes rd
5224                // ONLY"; the callers that relied on the implicit clear
5225                // (select_default, optimizer_bridge) now emit their own
5226                // explicit hi-zero op. `B .done` above targets offset 22 =
5227                // past-the-end, `BEQ` targets offset 14 — no displacement
5228                // moves.
5229
5230                Ok(bytes) // 22 bytes, register-independent
5231            }
5232
5233            // I64Ctz: Count trailing zeros in 64-bit value
5234            // If lo != 0: result = CTZ(lo) = CLZ(RBIT(lo))
5235            // If lo == 0: result = 32 + CTZ(hi) = 32 + CLZ(RBIT(hi))
5236            //
5237            // Layout:
5238            // 0: CMP.W rnlo, #0 (4 bytes)
5239            // 4: BEQ .lo_zero (2 bytes) - branch to offset 18
5240            // 6: RBIT.W rd, rnlo (4 bytes)
5241            // 10: CLZ.W rd, rd (4 bytes)
5242            // 14: B .done (2 bytes) - branch to offset 30
5243            // 16: NOP (2 bytes) - padding
5244            // 18: .lo_zero: RBIT.W rd, rnhi (4 bytes)
5245            // 22: CLZ.W rd, rd (4 bytes)
5246            // 26: ADD.W rd, rd, #32 (4 bytes)
5247            // 30: .done
5248            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
5249                let rd_bits = reg_to_bits(rd);
5250                let rn_lo_bits = reg_to_bits(rnlo);
5251                let rn_hi_bits = reg_to_bits(rnhi);
5252                let mut bytes = Vec::new();
5253
5254                // CMP.W rnlo, #0 (4 bytes at offset 0)
5255                let hw1: u16 = (0xF1B0 | rn_lo_bits) as u16;
5256                let hw2: u16 = 0x0F00;
5257                bytes.extend_from_slice(&hw1.to_le_bytes());
5258                bytes.extend_from_slice(&hw2.to_le_bytes());
5259
5260                // BEQ .lo_zero (2 bytes at offset 4)
5261                // PC = 4 + 4 = 8, target = 18, offset = 10, imm8 = 5
5262                let beq: u16 = 0xD005;
5263                bytes.extend_from_slice(&beq.to_le_bytes());
5264
5265                // RBIT.W rd, rnlo (4 bytes at offset 6)
5266                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5267                let hw1: u16 = (0xFA90 | rn_lo_bits) as u16;
5268                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_lo_bits) as u16;
5269                bytes.extend_from_slice(&hw1.to_le_bytes());
5270                bytes.extend_from_slice(&hw2.to_le_bytes());
5271
5272                // CLZ.W rd, rd (4 bytes at offset 10)
5273                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5274                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5275                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5276                bytes.extend_from_slice(&hw1.to_le_bytes());
5277                bytes.extend_from_slice(&hw2.to_le_bytes());
5278
5279                // B .done (2 bytes at offset 14)
5280                // PC = 14 + 4 = 18, target = 30, offset = 12, imm11 = 6
5281                let b_done: u16 = 0xE006;
5282                bytes.extend_from_slice(&b_done.to_le_bytes());
5283
5284                // NOP (2 bytes at offset 16) - padding
5285                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5286
5287                // .lo_zero: (offset 18)
5288                // RBIT.W rd, rnhi (4 bytes)
5289                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5290                let hw1: u16 = (0xFA90 | rn_hi_bits) as u16;
5291                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_hi_bits) as u16;
5292                bytes.extend_from_slice(&hw1.to_le_bytes());
5293                bytes.extend_from_slice(&hw2.to_le_bytes());
5294
5295                // CLZ.W rd, rd (4 bytes at offset 22)
5296                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5297                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5298                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5299                bytes.extend_from_slice(&hw1.to_le_bytes());
5300                bytes.extend_from_slice(&hw2.to_le_bytes());
5301
5302                // ADD.W rd, rd, #32 (4 bytes at offset 26)
5303                let hw1: u16 = (0xF100 | rd_bits) as u16;
5304                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5305                bytes.extend_from_slice(&hw1.to_le_bytes());
5306                bytes.extend_from_slice(&hw2.to_le_bytes());
5307
5308                // .done: (offset 30 — the end of the expansion)
5309                // #1048: the former trailing `MOV rnhi, #0` is GONE — it
5310                // wrote the OPERAND's home high register (see the I64Clz
5311                // comment above). `B .done` targets offset 30 = past-the-end,
5312                // `BEQ` targets offset 18 — no displacement moves.
5313
5314                Ok(bytes) // 30 bytes, register-independent
5315            }
5316
5317            // I64Popcnt: Population count of 64-bit value
5318            // result = POPCNT(lo) + POPCNT(hi)
5319            // Using SIMD-style parallel bit counting algorithm
5320            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
5321                let rd_bits = reg_to_bits(rd);
5322                let rn_lo_bits = reg_to_bits(rnlo);
5323                let rn_hi_bits = reg_to_bits(rnhi);
5324                let r12: u32 = 12; // IP scratch
5325                let r3: u32 = 3; // Scratch for hi popcnt result
5326                let mut bytes = Vec::new();
5327
5328                // PUSH {R3, R4, R5} - save scratch registers
5329                bytes.extend_from_slice(&0xB438u16.to_le_bytes());
5330
5331                // Strategy: compute popcnt(lo) -> R4, popcnt(hi) -> R5, add them -> rd
5332                // Using lookup table approach for each byte would be too large
5333                // Using shift-and-add approach instead
5334
5335                // For simplicity and correctness, use the efficient parallel algorithm
5336                // but implement it as a series of inline operations
5337
5338                // Marshal the operand pair into the fixed scratch regs, routing
5339                // rnlo through R12 (#632 audit): writing R4 first corrupted the
5340                // rnhi read for a pair living at (R3,R4) — every source is read
5341                // before any scratch register it could occupy is written.
5342                // MOV R12, rnlo
5343                let mov: u16 = (0x4600 | (1 << 7) | (rn_lo_bits << 3) | 4) as u16;
5344                bytes.extend_from_slice(&mov.to_le_bytes());
5345                // MOV R5, rnhi (R4 untouched so far; rnhi == R5 is a no-op)
5346                let mov: u16 = (0x4600 | (rn_hi_bits << 3) | 5) as u16;
5347                bytes.extend_from_slice(&mov.to_le_bytes());
5348                // MOV R4, R12
5349                bytes.extend_from_slice(&0x4664u16.to_le_bytes());
5350
5351                // --- POPCNT for R4 (lo word) ---
5352                // Step 1: x = x - ((x >> 1) & 0x55555555)
5353                // LSR.W R12, R4, #1
5354                let hw1: u16 = 0xEA4F;
5355                let hw2: u16 = ((r12 << 8) | 0x50 | 4) as u16;
5356                bytes.extend_from_slice(&hw1.to_le_bytes());
5357                bytes.extend_from_slice(&hw2.to_le_bytes());
5358
5359                // Load 0x55555555 into R3 using MOVW/MOVT
5360                // MOVW R3, #0x5555
5361                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5362                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5363                // MOVT R3, #0x5555
5364                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5365                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5366
5367                // AND.W R12, R12, R3
5368                let hw1: u16 = (0xEA00 | r12) as u16;
5369                let hw2: u16 = ((r12 << 8) | r3) as u16;
5370                bytes.extend_from_slice(&hw1.to_le_bytes());
5371                bytes.extend_from_slice(&hw2.to_le_bytes());
5372
5373                // SUB.W R4, R4, R12
5374                let hw1: u16 = (0xEBA0 | 4) as u16;
5375                let hw2: u16 = ((4 << 8) | r12) as u16;
5376                bytes.extend_from_slice(&hw1.to_le_bytes());
5377                bytes.extend_from_slice(&hw2.to_le_bytes());
5378
5379                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5380                // Load 0x33333333 into R3
5381                // MOVW R3, #0x3333
5382                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5383                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5384                // MOVT R3, #0x3333
5385                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5386                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5387
5388                // AND.W R12, R4, R3
5389                let hw1: u16 = (0xEA00 | 4) as u16;
5390                let hw2: u16 = ((r12 << 8) | r3) as u16;
5391                bytes.extend_from_slice(&hw1.to_le_bytes());
5392                bytes.extend_from_slice(&hw2.to_le_bytes());
5393
5394                // LSR.W R4, R4, #2
5395                let hw1: u16 = 0xEA4F;
5396                let hw2: u16 = ((4 << 8) | 0x90 | 4) as u16;
5397                bytes.extend_from_slice(&hw1.to_le_bytes());
5398                bytes.extend_from_slice(&hw2.to_le_bytes());
5399
5400                // AND.W R4, R4, R3
5401                let hw1: u16 = (0xEA00 | 4) as u16;
5402                let hw2: u16 = ((4 << 8) | r3) as u16;
5403                bytes.extend_from_slice(&hw1.to_le_bytes());
5404                bytes.extend_from_slice(&hw2.to_le_bytes());
5405
5406                // ADD.W R4, R4, R12
5407                let hw1: u16 = (0xEB00 | 4) as u16;
5408                let hw2: u16 = ((4 << 8) | r12) as u16;
5409                bytes.extend_from_slice(&hw1.to_le_bytes());
5410                bytes.extend_from_slice(&hw2.to_le_bytes());
5411
5412                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5413                // LSR.W R12, R4, #4
5414                // hw2 = (imm3 << 12) | (Rd << 8) | (imm2 << 6) | (type << 4) | Rm
5415                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5416                let hw1: u16 = 0xEA4F;
5417                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 4) as u16;
5418                bytes.extend_from_slice(&hw1.to_le_bytes());
5419                bytes.extend_from_slice(&hw2.to_le_bytes());
5420
5421                // ADD.W R4, R4, R12
5422                let hw1: u16 = (0xEB00 | 4) as u16;
5423                let hw2: u16 = ((4 << 8) | r12) as u16;
5424                bytes.extend_from_slice(&hw1.to_le_bytes());
5425                bytes.extend_from_slice(&hw2.to_le_bytes());
5426
5427                // Load 0x0F0F0F0F into R3
5428                // MOVW R3, #0x0F0F (imm4=0, i=1, imm3=7, imm8=0x0F)
5429                // hw1 = 11110 1 10 0100 0000 = 0xF640
5430                // hw2 = 0 111 0011 00001111 = 0x730F
5431                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5432                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5433                // MOVT R3, #0x0F0F
5434                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5435                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5436
5437                // AND.W R4, R4, R3
5438                let hw1: u16 = (0xEA00 | 4) as u16;
5439                let hw2: u16 = ((4 << 8) | r3) as u16;
5440                bytes.extend_from_slice(&hw1.to_le_bytes());
5441                bytes.extend_from_slice(&hw2.to_le_bytes());
5442
5443                // Step 4: x = x * 0x01010101 >> 24
5444                // Load 0x01010101 into R3
5445                // MOVW R3, #0x0101
5446                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5447                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5448                // MOVT R3, #0x0101
5449                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5450                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5451
5452                // MUL R4, R4, R3
5453                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5454                let hw1: u16 = (0xFB00 | 4) as u16;
5455                let hw2: u16 = (0xF000 | (4 << 8) | r3) as u16;
5456                bytes.extend_from_slice(&hw1.to_le_bytes());
5457                bytes.extend_from_slice(&hw2.to_le_bytes());
5458
5459                // LSR.W R4, R4, #24
5460                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5461                let hw1: u16 = 0xEA4F;
5462                let hw2: u16 = (0x6000 | (4 << 8) | 0x10 | 4) as u16;
5463                bytes.extend_from_slice(&hw1.to_le_bytes());
5464                bytes.extend_from_slice(&hw2.to_le_bytes());
5465
5466                // --- POPCNT for R5 (hi word) - same algorithm ---
5467                // Step 1
5468                let hw1: u16 = 0xEA4F;
5469                let hw2: u16 = ((r12 << 8) | 0x50 | 5) as u16;
5470                bytes.extend_from_slice(&hw1.to_le_bytes());
5471                bytes.extend_from_slice(&hw2.to_le_bytes());
5472
5473                // Load 0x55555555 into R3
5474                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5475                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5476                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5477                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5478
5479                let hw1: u16 = (0xEA00 | r12) as u16;
5480                let hw2: u16 = ((r12 << 8) | r3) as u16;
5481                bytes.extend_from_slice(&hw1.to_le_bytes());
5482                bytes.extend_from_slice(&hw2.to_le_bytes());
5483
5484                let hw1: u16 = (0xEBA0 | 5) as u16;
5485                let hw2: u16 = ((5 << 8) | r12) as u16;
5486                bytes.extend_from_slice(&hw1.to_le_bytes());
5487                bytes.extend_from_slice(&hw2.to_le_bytes());
5488
5489                // Step 2
5490                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5491                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5492                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5493                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5494
5495                let hw1: u16 = (0xEA00 | 5) as u16;
5496                let hw2: u16 = ((r12 << 8) | r3) as u16;
5497                bytes.extend_from_slice(&hw1.to_le_bytes());
5498                bytes.extend_from_slice(&hw2.to_le_bytes());
5499
5500                let hw1: u16 = 0xEA4F;
5501                let hw2: u16 = ((5 << 8) | 0x90 | 5) as u16;
5502                bytes.extend_from_slice(&hw1.to_le_bytes());
5503                bytes.extend_from_slice(&hw2.to_le_bytes());
5504
5505                let hw1: u16 = (0xEA00 | 5) as u16;
5506                let hw2: u16 = ((5 << 8) | r3) as u16;
5507                bytes.extend_from_slice(&hw1.to_le_bytes());
5508                bytes.extend_from_slice(&hw2.to_le_bytes());
5509
5510                let hw1: u16 = (0xEB00 | 5) as u16;
5511                let hw2: u16 = ((5 << 8) | r12) as u16;
5512                bytes.extend_from_slice(&hw1.to_le_bytes());
5513                bytes.extend_from_slice(&hw2.to_le_bytes());
5514
5515                // Step 3: LSR.W R12, R5, #4
5516                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5517                let hw1: u16 = 0xEA4F;
5518                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 5) as u16;
5519                bytes.extend_from_slice(&hw1.to_le_bytes());
5520                bytes.extend_from_slice(&hw2.to_le_bytes());
5521
5522                let hw1: u16 = (0xEB00 | 5) as u16;
5523                let hw2: u16 = ((5 << 8) | r12) as u16;
5524                bytes.extend_from_slice(&hw1.to_le_bytes());
5525                bytes.extend_from_slice(&hw2.to_le_bytes());
5526
5527                // Load 0x0F0F0F0F into R3 (for hi-word)
5528                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5529                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5530                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5531                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5532
5533                let hw1: u16 = (0xEA00 | 5) as u16;
5534                let hw2: u16 = ((5 << 8) | r3) as u16;
5535                bytes.extend_from_slice(&hw1.to_le_bytes());
5536                bytes.extend_from_slice(&hw2.to_le_bytes());
5537
5538                // Step 4
5539                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5540                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5541                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5542                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5543
5544                // MUL R5, R5, R3
5545                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5546                let hw1: u16 = (0xFB00 | 5) as u16;
5547                let hw2: u16 = (0xF000 | (5 << 8) | r3) as u16;
5548                bytes.extend_from_slice(&hw1.to_le_bytes());
5549                bytes.extend_from_slice(&hw2.to_le_bytes());
5550
5551                // LSR.W R5, R5, #24
5552                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5553                let hw1: u16 = 0xEA4F;
5554                let hw2: u16 = (0x6000 | (5 << 8) | 0x10 | 5) as u16;
5555                bytes.extend_from_slice(&hw1.to_le_bytes());
5556                bytes.extend_from_slice(&hw2.to_le_bytes());
5557
5558                // #632: the count must be carried ACROSS the scratch restore
5559                // in a register the POP cannot touch. rd is allocator-assigned
5560                // (any of R0-R8) and can land inside the {R3,R4,R5} restore set
5561                // — the old `ADDS rd, R4, R5; POP {R3,R4,R5}` destroyed the
5562                // result one instruction after computing it (0 for every input
5563                // under qemu). R12 is encoder scratch: never allocatable (#212)
5564                // and never in a restore set, so no choice of rd can collide.
5565                // ADD.W R12, R4, R5
5566                bytes.extend_from_slice(&0xEB04u16.to_le_bytes());
5567                bytes.extend_from_slice(&0x0C05u16.to_le_bytes());
5568
5569                // POP {R3, R4, R5}
5570                bytes.extend_from_slice(&0xBC38u16.to_le_bytes());
5571
5572                // MOV rd, R12 — after the restore. The 4-bit Rd (D:rd) form is
5573                // also total over rd = R8, where the old ADDS T1 3-bit field
5574                // silently corrupted the encoding (#178/#180 class).
5575                let mov: u16 =
5576                    (0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7)) as u16;
5577                bytes.extend_from_slice(&mov.to_le_bytes());
5578
5579                // #1048: the former trailing `MOV.W rnhi, #0` hi-word clear
5580                // is GONE — it wrote the OPERAND's home high register (see
5581                // the I64Clz comment). Callers that relied on the implicit
5582                // clear emit their own explicit hi-zero op.
5583
5584                Ok(bytes)
5585            }
5586
5587            // I64Extend8S: Sign-extend low 8 bits to 64 bits
5588            // Result: rdlo = sign_extend_8(rnlo), rdhi = rdlo >> 31
5589            ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
5590                let rdlo_bits = reg_to_bits(rdlo);
5591                let rdhi_bits = reg_to_bits(rdhi);
5592                let rnlo_bits = reg_to_bits(rnlo);
5593                let mut bytes = Vec::new();
5594
5595                // SXTB.W rdlo, rnlo (sign-extend byte to 32-bit)
5596                // SXTB T2: hw1 = 0xFA4F, hw2 = 0xF0<Rd><Rm>
5597                let hw1: u16 = 0xFA4F_u16;
5598                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5599                bytes.extend_from_slice(&hw1.to_le_bytes());
5600                bytes.extend_from_slice(&hw2.to_le_bytes());
5601
5602                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5603                // ASR (immediate): hw1 = 0xEA4F, hw2 = imm3:Rd:imm2:type:Rm
5604                // For imm5=31: imm3=111, imm2=11, type=10 (ASR)
5605                // hw2 = (7 << 12) | (rdhi << 8) | (3 << 6) | (2 << 4) | rdlo
5606                let hw1: u16 = 0xEA4F;
5607                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5608                bytes.extend_from_slice(&hw1.to_le_bytes());
5609                bytes.extend_from_slice(&hw2.to_le_bytes());
5610
5611                Ok(bytes)
5612            }
5613
5614            // I64Extend16S: Sign-extend low 16 bits to 64 bits
5615            // Result: rdlo = sign_extend_16(rnlo), rdhi = rdlo >> 31
5616            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
5617                let rdlo_bits = reg_to_bits(rdlo);
5618                let rdhi_bits = reg_to_bits(rdhi);
5619                let rnlo_bits = reg_to_bits(rnlo);
5620                let mut bytes = Vec::new();
5621
5622                // SXTH.W rdlo, rnlo (sign-extend halfword to 32-bit)
5623                // SXTH T2: hw1 = 0xFA0F, hw2 = 0xF0<Rd><Rm>
5624                let hw1: u16 = 0xFA0F_u16;
5625                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5626                bytes.extend_from_slice(&hw1.to_le_bytes());
5627                bytes.extend_from_slice(&hw2.to_le_bytes());
5628
5629                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5630                let hw1: u16 = 0xEA4F;
5631                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5632                bytes.extend_from_slice(&hw1.to_le_bytes());
5633                bytes.extend_from_slice(&hw2.to_le_bytes());
5634
5635                Ok(bytes)
5636            }
5637
5638            // I64Extend32S: Sign-extend low 32 bits to 64 bits
5639            // Result: rdlo = rnlo, rdhi = rnlo >> 31
5640            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
5641                let rdlo_bits = reg_to_bits(rdlo);
5642                let rdhi_bits = reg_to_bits(rdhi);
5643                let rnlo_bits = reg_to_bits(rnlo);
5644                let mut bytes = Vec::new();
5645
5646                // MOV rdlo, rnlo (if different)
5647                if rdlo_bits != rnlo_bits {
5648                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5649                    let d_bit = ((rdlo_bits >> 3) & 1) as u16;
5650                    let mov: u16 = 0x4600
5651                        | (d_bit << 7)
5652                        | ((rnlo_bits as u16) << 3)
5653                        | ((rdlo_bits & 0x7) as u16);
5654                    bytes.extend_from_slice(&mov.to_le_bytes());
5655                }
5656
5657                // ASR.W rdhi, rnlo, #31 (sign-extend to high word)
5658                let hw1: u16 = 0xEA4F;
5659                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rnlo_bits) as u16;
5660                bytes.extend_from_slice(&hw1.to_le_bytes());
5661                bytes.extend_from_slice(&hw2.to_le_bytes());
5662
5663                Ok(bytes)
5664            }
5665
5666            // SelectMove: IT <cond>; MOV{cond} rd, rm
5667            // Conditional move: only execute MOV if condition is true
5668            ArmOp::SelectMove { rd, rm, cond } => {
5669                let rd_bits = reg_to_bits(rd) as u16;
5670                let rm_bits = reg_to_bits(rm) as u16;
5671
5672                // Condition code encoding for IT block
5673                use synth_synthesis::Condition;
5674                let cond_bits: u16 = match cond {
5675                    Condition::EQ => 0x0, // Equal
5676                    Condition::NE => 0x1, // Not equal
5677                    Condition::HS => 0x2, // Higher or same (unsigned >=)
5678                    Condition::LO => 0x3, // Lower (unsigned <)
5679                    Condition::HI => 0x8, // Higher (unsigned >)
5680                    Condition::LS => 0x9, // Lower or same (unsigned <=)
5681                    Condition::GE => 0xA, // Greater or equal (signed)
5682                    Condition::LT => 0xB, // Less than (signed)
5683                    Condition::GT => 0xC, // Greater than (signed)
5684                    Condition::LE => 0xD, // Less or equal (signed)
5685                };
5686
5687                // IT <cond>: single Then block (mask = 0x8 for T only)
5688                // IT instruction: 1011 1111 firstcond mask
5689                let it_instr: u16 = 0xBF00 | (cond_bits << 4) | 0x8;
5690
5691                // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5692                // This MOV will only execute if condition is true due to IT block
5693                let d_bit = (rd_bits >> 3) & 1;
5694                let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5695
5696                // Emit: IT <cond>, MOV rd, rm
5697                let mut bytes = it_instr.to_le_bytes().to_vec();
5698                bytes.extend_from_slice(&mov_instr.to_le_bytes());
5699                Ok(bytes)
5700            }
5701
5702            // Popcnt: Population count (count set bits)
5703            // ARM Cortex-M has no native POPCNT, so we implement the bit manipulation algorithm:
5704            // x = x - ((x >> 1) & 0x55555555);
5705            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
5706            // x = (x + (x >> 4)) & 0x0F0F0F0F;
5707            // x = x + (x >> 8);
5708            // x = x + (x >> 16);
5709            // return x & 0x3F;
5710            //
5711            // #1021: R12 (IP, never allocatable) is the ONLY scratch. The
5712            // previous expansion borrowed R11 as a second temp — but R11 is
5713            // the WASM linear-memory base, materialized at entry and read by
5714            // every later LDR/STR, and it is NOT in the pushed set, so the
5715            // clobber leaked to the CALLER too (a live memory-safety
5716            // miscompile: loads through `base = x >> 16`). The second temp is
5717            // eliminated the way the healthy i64.popcnt discipline implies —
5718            // never touch an unsaved register — but without its PUSH/POP
5719            // wrapper: the SWAR masks 0x55555555 / 0x33333333 / 0x0F0F0F0F
5720            // are all `0xXYXYXYXY` ThumbExpandImm modified immediates, so
5721            // each AND takes its mask from the instruction itself and R12
5722            // alone carries every intermediate. Straight-line, no branches,
5723            // no stack traffic — nothing to skip on a trap edge.
5724            ArmOp::Popcnt { rd, rm } => {
5725                let rd_bits = reg_to_bits(rd);
5726                // Defensive (#1021): rd = R11/R12/SP/PC would silently
5727                // corrupt the linear-memory base, the expansion's own
5728                // scratch, or the stack. The selector never assigns them
5729                // (pool R0-R8); refuse loudly if that ever changes.
5730                if rd_bits >= 11 {
5731                    return Err(synth_core::Error::synthesis(
5732                        "Popcnt destination must be R0-R10: R11 is the linear-memory \
5733                         base and R12 is the expansion's scratch (#1021)",
5734                    ));
5735                }
5736                let mut bytes = Vec::new();
5737
5738                // First, move rm to rd if they're different
5739                if rd != rm {
5740                    let rm_bits = reg_to_bits(rm) as u16;
5741                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5742                    let d_bit = ((rd_bits as u16) >> 3) & 1;
5743                    let mov_instr: u16 =
5744                        0x4600 | (d_bit << 7) | (rm_bits << 3) | ((rd_bits as u16) & 0x7);
5745                    bytes.extend_from_slice(&mov_instr.to_le_bytes());
5746                }
5747
5748                // Step 1: x = x - ((x >> 1) & 0x55555555)
5749                // R12 = rd >> 1
5750                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 1)?);
5751                // R12 = R12 & 0x55555555 (modified immediate, no constant reg)
5752                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(12, 12, 0x5555_5555)?);
5753                // rd = rd - R12
5754                bytes.extend_from_slice(&self.encode_thumb32_sub_reg_raw(rd_bits, rd_bits, 12)?);
5755
5756                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5757                // R12 = rd & 0x33333333
5758                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5759                    12,
5760                    rd_bits,
5761                    0x3333_3333,
5762                )?);
5763                // rd = rd >> 2
5764                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(rd_bits, rd_bits, 2)?);
5765                // rd = rd & 0x33333333
5766                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5767                    rd_bits,
5768                    rd_bits,
5769                    0x3333_3333,
5770                )?);
5771                // rd = rd + R12
5772                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5773
5774                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5775                // R12 = rd >> 4
5776                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 4)?);
5777                // rd = rd + R12
5778                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5779                // rd = rd & 0x0F0F0F0F
5780                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5781                    rd_bits,
5782                    rd_bits,
5783                    0x0F0F_0F0F,
5784                )?);
5785
5786                // Step 4: x = x + (x >> 8)
5787                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 8)?);
5788                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5789
5790                // Step 5: x = x + (x >> 16)
5791                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 16)?);
5792                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5793
5794                // Step 6: return x & 0x3F
5795                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(rd_bits, rd_bits, 0x3F)?);
5796
5797                Ok(bytes)
5798            }
5799
5800            // I64DivU: 64-bit unsigned division using binary long division
5801            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = quotient
5802            // Uses: R4-R7, R12 as loop counter (avoid R8 for Renode compatibility)
5803            //
5804            // #610: the fixed-ABI wrapper marshals the selector-assigned
5805            // operand registers into the core's fixed regs and lands the
5806            // result in rd — pre-#610 this arm IGNORED its register fields,
5807            // so the selector read its rd pair (e.g. R4:R5) after the core's
5808            // own POP restored the stale caller values over it: 0 for every
5809            // input. A zero divisor now traps (UDF #0), per WASM semantics.
5810            ArmOp::I64DivU {
5811                rdlo,
5812                rdhi,
5813                rnlo,
5814                rnhi,
5815                rmlo,
5816                rmhi,
5817                elide_zero_guard,
5818            } => {
5819                let mut bytes = Vec::new();
5820                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5821                // #494 phase 2b: elided only under a certificate-discharged
5822                // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
5823                if !elide_zero_guard {
5824                    emit_i64_divisor_zero_trap(&mut bytes);
5825                }
5826
5827                // PUSH {R4-R7} - save scratch registers (NO LR — this is inline code)
5828                // 16-bit PUSH: 1011 010 M rrrrrrrr where M=0 (no LR), r=R4-R7 = 0xF0
5829                // Encoding: 1011 0100 1111 0000 = 0xB4F0
5830                bytes.extend_from_slice(&0xB4F0u16.to_le_bytes());
5831
5832                // Initialize quotient (R4:R5) = 0
5833                bytes.extend_from_slice(&0x2400u16.to_le_bytes()); // MOV R4, #0
5834                bytes.extend_from_slice(&0x2500u16.to_le_bytes()); // MOV R5, #0
5835
5836                // Initialize remainder (R6:R7) = 0
5837                bytes.extend_from_slice(&0x2600u16.to_le_bytes()); // MOV R6, #0
5838                bytes.extend_from_slice(&0x2700u16.to_le_bytes()); // MOV R7, #0
5839
5840                // Initialize loop counter R12 = 64 (use R12 scratch instead of R8)
5841                // MOV.W R12, #64: F04F 0C40
5842                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5843                bytes.extend_from_slice(&0x0C40u16.to_le_bytes());
5844
5845                // Loop start
5846                let loop_start = bytes.len();
5847
5848                // === Loop body: process one bit ===
5849
5850                // 1. Shift quotient R4:R5 left by 1
5851                // LSLS R5, R5, #1 (16-bit: 0000 0010 1010 1101 = 0x006D -> actually 0x002D for LSL R5,R5,#1)
5852                // LSL Rd, Rm, #imm5: 000 00 imm5 Rm Rd = 000 00 00001 101 101 = 0x006D
5853                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5854                // Get carry from R4 into R5: ORR R5, R5, R4 LSR #31
5855                // Thumb-2 ORR with shifted register: EA45 75D4 = ORR.W R5, R5, R4, LSR #31
5856                // 11101010 010 S Rn | 0 imm3 Rd imm2 type Rm
5857                // type=01 (LSR), imm5=31 (imm3=111, imm2=11)
5858                bytes.extend_from_slice(&0xEA45u16.to_le_bytes());
5859                bytes.extend_from_slice(&0x75D4u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5860                // LSLS R4, R4, #1: 000 00 00001 100 100 = 0x0064
5861                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5862
5863                // 2. Shift remainder R6:R7 left by 1, OR in MSB of dividend R1
5864                // LSLS R7, R7, #1
5865                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5866                // ORR.W R7, R7, R6, LSR #31
5867                bytes.extend_from_slice(&0xEA47u16.to_le_bytes());
5868                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5869                // LSLS R6, R6, #1
5870                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5871                // ORR.W R6, R6, R1, LSR #31 (bring in MSB of dividend high)
5872                bytes.extend_from_slice(&0xEA46u16.to_le_bytes());
5873                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5874
5875                // 3. Shift dividend R0:R1 left by 1
5876                // LSLS R1, R1, #1
5877                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5878                // ORR.W R1, R1, R0, LSR #31
5879                bytes.extend_from_slice(&0xEA41u16.to_le_bytes());
5880                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5881                // LSLS R0, R0, #1
5882                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5883
5884                // 4. Compare remainder >= divisor (64-bit unsigned comparison)
5885                // Compare high words first: CMP R7, R3
5886                // CMP Rn, Rm encoding: 0x4280 | (Rm << 3) | Rn
5887                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3 (16-bit)
5888                // BHI means R7 > R3 (unsigned) - definitely subtract
5889                // BLO means R7 < R3 - definitely don't subtract
5890                // BEQ means need to check low words
5891
5892                // If high > divisor high: branch to subtract (forward +offset)
5893                // BHI.N +6 (skip CMP, skip BLO, do subtract)
5894                // BHI: 1101 1000 offset8 where cond=1000 (HI)
5895                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4 (to subtract block)
5896
5897                // If high < divisor high: branch past subtract
5898                // BLO.N +10 (skip to decrement)
5899                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BLO/BCC +12 (past subtract)
5900
5901                // High words equal, compare low: CMP R6, R2
5902                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2 (16-bit)
5903                // BLO/BCC past subtract (skip SUBS+SBC.W+ORR.W = 10 bytes = 4 halfwords from PC+4)
5904                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords (past subtract)
5905
5906                // === Subtract block: remainder -= divisor, quotient |= 1 ===
5907                // SUBS R6, R6, R2
5908                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2 (16-bit)
5909                // SBC R7, R7, R3 (with borrow)
5910                // Thumb-2 SBC.W: EB67 0703 = SBC.W R7, R7, R3
5911                bytes.extend_from_slice(&0xEB67u16.to_le_bytes());
5912                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5913                // ORR R4, R4, #1 (set bit 0 of quotient low)
5914                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5915                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5916
5917                // === Decrement counter and loop ===
5918                // SUBS.W R12, R12, #1 (decrement loop counter)
5919                // SUBS.W R12, R12, #1: F1BC 0C01
5920                bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
5921                bytes.extend_from_slice(&0x0C01u16.to_le_bytes());
5922
5923                // BNE back to loop_start
5924                let branch_offset_bytes = bytes.len() - loop_start + 4; // +4 for pipeline
5925                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5926                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5927                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5928
5929                // === Loop done, move quotient to R0:R1 ===
5930                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5931                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5932
5933                // POP {R4-R7} - restore scratch registers (NO PC — inline code continues)
5934                // 16-bit POP: 1011 110 P rrrrrrrr where P=0 (no PC), r=R4-R7 = 0xF0
5935                // Encoding: 1011 1100 1111 0000 = 0xBCF0
5936                bytes.extend_from_slice(&0xBCF0u16.to_le_bytes());
5937
5938                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5939                Ok(bytes)
5940            }
5941
5942            // I64DivS: 64-bit signed division
5943            // Converts to unsigned, divides, then applies sign
5944            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5945            //   ->  R0:R1 = quotient (signed)
5946            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5947            ArmOp::I64DivS {
5948                rdlo,
5949                rdhi,
5950                rnlo,
5951                rnhi,
5952                rmlo,
5953                rmhi,
5954                elide_zero_guard,
5955                elide_overflow_guard,
5956            } => {
5957                let mut bytes = Vec::new();
5958                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5959                // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
5960                // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
5961                // the #633 overflow guard falls ONLY to
5962                // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
5963                // divisor-nonzero fact alone must keep it.
5964                if !elide_zero_guard {
5965                    emit_i64_divisor_zero_trap(&mut bytes);
5966                }
5967                if !elide_overflow_guard {
5968                    // #633: INT64_MIN / -1 overflows — trap like the i32 path
5969                    // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
5970                    emit_i64_divs_overflow_trap(&mut bytes);
5971                }
5972
5973                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5974                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5975                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5976
5977                // Save result sign in R9: R9 = R1 XOR R3 (sign bit = MSB)
5978                // EOR.W R9, R1, R3
5979                bytes.extend_from_slice(&0xEA81u16.to_le_bytes());
5980                bytes.extend_from_slice(&0x0903u16.to_le_bytes());
5981
5982                // If dividend negative (R1 MSB set), negate it
5983                // TST R1, R1 (check sign)
5984                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5985                // BPL skip_neg_dividend (+10 bytes = 5 halfwords)
5986                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5987
5988                // Negate R0:R1 (64-bit): RSBS R0, R0, #0; SBC R1, R1, R1 LSL #1
5989                // Actually: MVN R0, R0; MVN R1, R1; ADDS R0, R0, #1; ADC R1, R1, #0
5990                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5991                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5992                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5993                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5994                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5995
5996                // If divisor negative (R3 MSB set), negate it
5997                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5998                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5999
6000                // Negate R2:R3
6001                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
6002                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
6003                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
6004                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
6005                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
6006
6007                // === Now do unsigned division (same as I64DivU) ===
6008                // Initialize quotient (R4:R5) = 0
6009                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6010                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6011                // Initialize remainder (R6:R7) = 0
6012                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6013                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6014                // Initialize loop counter R8 = 64
6015                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6016                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6017
6018                let loop_start = bytes.len();
6019
6020                // Shift quotient left
6021                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6022                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6023                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6024                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6025
6026                // Shift remainder left, OR in MSB of dividend
6027                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6028                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6029                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6030                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6031                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6032                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6033
6034                // Shift dividend left
6035                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6036                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6037                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6038                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6039
6040                // Compare and conditionally subtract
6041                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6042                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6043                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6044                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6045                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6046
6047                // Subtract and set quotient bit
6048                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6049                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6050                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6051                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6052                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6053
6054                // Decrement and loop
6055                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6056                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6057
6058                let branch_offset_bytes = bytes.len() - loop_start + 4;
6059                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6060                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6061                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6062
6063                // Move quotient to R0:R1
6064                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
6065                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
6066
6067                // If result should be negative (R9 MSB set), negate R0:R1
6068                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9 (check MSB)
6069                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6070                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8 (skip negation)
6071
6072                // Negate result R0:R1
6073                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6074                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6075                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6076                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6077                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6078
6079                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6080                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6081                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6082
6083                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6084                Ok(bytes)
6085            }
6086
6087            // I64RemU: 64-bit unsigned remainder using binary long division
6088            // Same algorithm as I64DivU but returns remainder instead of quotient
6089            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = remainder
6090            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6091            ArmOp::I64RemU {
6092                rdlo,
6093                rdhi,
6094                rnlo,
6095                rnhi,
6096                rmlo,
6097                rmhi,
6098                elide_zero_guard,
6099            } => {
6100                let mut bytes = Vec::new();
6101                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6102                if !elide_zero_guard {
6103                    emit_i64_divisor_zero_trap(&mut bytes);
6104                }
6105
6106                // PUSH {R4-R8} - save scratch registers (NO LR — inline code)
6107                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6108                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6109
6110                // Initialize quotient (R4:R5) = 0 (computed but not returned)
6111                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6112                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6113                // Initialize remainder (R6:R7) = 0
6114                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6115                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6116                // Initialize loop counter R8 = 64
6117                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6118                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6119
6120                let loop_start = bytes.len();
6121
6122                // Shift quotient left (not needed for result, but keeps algorithm same)
6123                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6124                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6125                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6126                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6127
6128                // Shift remainder left, OR in MSB of dividend
6129                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6130                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6131                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6132                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6133                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6134                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6135
6136                // Shift dividend left
6137                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6138                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6139                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6140                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6141
6142                // Compare and conditionally subtract
6143                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6144                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6145                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6146                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6147                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6148
6149                // Subtract and set quotient bit
6150                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6151                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6152                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6153                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6154                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6155
6156                // Decrement and loop
6157                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6158                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6159
6160                let branch_offset_bytes = bytes.len() - loop_start + 4;
6161                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6162                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6163                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6164
6165                // Move REMAINDER to R0:R1 (difference from I64DivU)
6166                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6167                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6168
6169                // POP {R4-R8} - restore scratch registers (NO PC — inline code continues)
6170                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6171                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6172
6173                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6174                Ok(bytes)
6175            }
6176
6177            // I64RemS: 64-bit signed remainder
6178            // Remainder sign follows dividend sign (not quotient rule)
6179            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
6180            //   ->  R0:R1 = remainder (signed, same sign as dividend)
6181            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6182            ArmOp::I64RemS {
6183                rdlo,
6184                rdhi,
6185                rnlo,
6186                rnhi,
6187                rmlo,
6188                rmhi,
6189                elide_zero_guard,
6190            } => {
6191                let mut bytes = Vec::new();
6192                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6193                if !elide_zero_guard {
6194                    emit_i64_divisor_zero_trap(&mut bytes);
6195                }
6196
6197                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
6198                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6199                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6200
6201                // Save dividend sign in R9 (remainder sign = dividend sign)
6202                // MOV R9, R1 (just need the sign bit)
6203                bytes.extend_from_slice(&0x4689u16.to_le_bytes()); // MOV R9, R1
6204
6205                // If dividend negative (R1 MSB set), negate it
6206                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
6207                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6208
6209                // Negate R0:R1
6210                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6211                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6212                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6213                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6214                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6215
6216                // If divisor negative (R3 MSB set), negate it
6217                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
6218                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6219
6220                // Negate R2:R3
6221                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
6222                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
6223                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
6224                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
6225                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
6226
6227                // === Unsigned division algorithm ===
6228                // Initialize quotient (R4:R5) = 0
6229                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6230                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6231                // Initialize remainder (R6:R7) = 0
6232                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6233                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6234                // Initialize loop counter R8 = 64
6235                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6236                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6237
6238                let loop_start = bytes.len();
6239
6240                // Shift quotient left
6241                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6242                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6243                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6244                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6245
6246                // Shift remainder left, OR in MSB of dividend
6247                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6248                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6249                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6250                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6251                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6252                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6253
6254                // Shift dividend left
6255                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6256                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6257                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6258                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6259
6260                // Compare and conditionally subtract
6261                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6262                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6263                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6264                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6265                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6266
6267                // Subtract and set quotient bit
6268                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6269                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6270                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6271                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6272                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6273
6274                // Decrement and loop
6275                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6276                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6277
6278                let branch_offset_bytes = bytes.len() - loop_start + 4;
6279                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6280                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6281                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6282
6283                // Move remainder to R0:R1
6284                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6285                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6286
6287                // If original dividend was negative (R9 MSB set), negate remainder
6288                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9
6289                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6290                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6291
6292                // Negate result R0:R1
6293                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6294                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6295                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6296                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6297                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6298
6299                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6300                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6301                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6302
6303                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6304                Ok(bytes)
6305            }
6306
6307            // === F32 VFP single-precision Thumb-2 encodings ===
6308            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6309            ArmOp::F32Add { sd, sn, sm } => {
6310                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A00, sd, sn, sm)?))
6311            }
6312            ArmOp::F32Sub { sd, sn, sm } => {
6313                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A40, sd, sn, sm)?))
6314            }
6315            ArmOp::F32Mul { sd, sn, sm } => {
6316                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE200A00, sd, sn, sm)?))
6317            }
6318            ArmOp::F32Div { sd, sn, sm } => {
6319                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE800A00, sd, sn, sm)?))
6320            }
6321            ArmOp::F32Abs { sd, sm } => {
6322                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB00AC0, sd, sm)?))
6323            }
6324            ArmOp::F32Neg { sd, sm } => {
6325                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10A40, sd, sm)?))
6326            }
6327            ArmOp::F32Sqrt { sd, sm } => {
6328                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10AC0, sd, sm)?))
6329            }
6330
6331            // f32 pseudo-ops — multi-instruction sequences
6332            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6333            ArmOp::F32Ceil { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b01),
6334            ArmOp::F32Floor { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b10),
6335            ArmOp::F32Trunc { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b11),
6336            ArmOp::F32Nearest { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b00),
6337            ArmOp::F32Min { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, true),
6338            ArmOp::F32Max { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, false),
6339            ArmOp::F32Copysign { sd, sn, sm } => self.encode_thumb_f32_copysign(sd, sn, sm),
6340
6341            // f32 comparisons — VCMP + VMRS + MOV #0 + IT + MOV #1
6342            ArmOp::F32Eq { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x0),
6343            ArmOp::F32Ne { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x1),
6344            ArmOp::F32Lt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x4),
6345            ArmOp::F32Le { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x9),
6346            ArmOp::F32Gt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xC),
6347            ArmOp::F32Ge { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xA),
6348
6349            ArmOp::F32Const { sd, value } => self.encode_thumb_f32_const(sd, *value),
6350
6351            ArmOp::F32Load { sd, addr } => {
6352                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED900A00, sd, addr)?))
6353            }
6354            ArmOp::F32Store { sd, addr } => {
6355                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED800A00, sd, addr)?))
6356            }
6357
6358            ArmOp::F32ConvertI32S { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, true),
6359            ArmOp::F32ConvertI32U { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, false),
6360            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
6361                Err(synth_core::Error::synthesis(
6362                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6363                ))
6364            }
6365            ArmOp::F32ReinterpretI32 { sd, rm } => {
6366                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(true, sd, rm)?))
6367            }
6368            ArmOp::I32ReinterpretF32 { rd, sm } => {
6369                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(false, sm, rd)?))
6370            }
6371            ArmOp::I32TruncF32S { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, true),
6372            ArmOp::I32TruncF32U { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, false),
6373
6374            // === F64 VFP double-precision Thumb-2 encodings ===
6375            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6376            ArmOp::F64Add { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6377                0xEE300B00, dd, dn, dm,
6378            )?)),
6379            ArmOp::F64Sub { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6380                0xEE300B40, dd, dn, dm,
6381            )?)),
6382            ArmOp::F64Mul { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6383                0xEE200B00, dd, dn, dm,
6384            )?)),
6385            ArmOp::F64Div { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6386                0xEE800B00, dd, dn, dm,
6387            )?)),
6388            ArmOp::F64Abs { dd, dm } => {
6389                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?))
6390            }
6391            ArmOp::F64Neg { dd, dm } => {
6392                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?))
6393            }
6394            ArmOp::F64Sqrt { dd, dm } => {
6395                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?))
6396            }
6397
6398            // f64 pseudo-ops
6399            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6400            ArmOp::F64Ceil { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b01),
6401            ArmOp::F64Floor { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b10),
6402            ArmOp::F64Trunc { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b11),
6403            ArmOp::F64Nearest { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b00),
6404            ArmOp::F64Min { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, true),
6405            ArmOp::F64Max { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, false),
6406            ArmOp::F64Copysign { dd, dn, dm } => self.encode_thumb_f64_copysign(dd, dn, dm),
6407
6408            // f64 comparisons
6409            ArmOp::F64Eq { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x0),
6410            ArmOp::F64Ne { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x1),
6411            ArmOp::F64Lt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x4),
6412            ArmOp::F64Le { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x9),
6413            ArmOp::F64Gt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xC),
6414            ArmOp::F64Ge { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xA),
6415
6416            ArmOp::F64Const { dd, value } => self.encode_thumb_f64_const(dd, *value),
6417
6418            ArmOp::F64Load { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6419                0xED900B00, dd, addr,
6420            )?)),
6421            ArmOp::F64Store { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6422                0xED800B00, dd, addr,
6423            )?)),
6424
6425            ArmOp::F64ConvertI32S { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, true),
6426            ArmOp::F64ConvertI32U { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, false),
6427            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
6428                Err(synth_core::Error::synthesis(
6429                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6430                ))
6431            }
6432            ArmOp::F64PromoteF32 { dd, sm } => self.encode_thumb_f64_promote_f32(dd, sm),
6433            ArmOp::F32DemoteF64 { sd, dm } => self.encode_thumb_f32_demote_f64(sd, dm),
6434            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => Ok(vfp_to_thumb_bytes(
6435                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?,
6436            )),
6437            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => Ok(vfp_to_thumb_bytes(
6438                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?,
6439            )),
6440            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
6441                Err(synth_core::Error::synthesis(
6442                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
6443                ))
6444            }
6445            ArmOp::I32TruncF64S { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, true),
6446            ArmOp::I32TruncF64U { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, false),
6447
6448            // ===== i64 operations: encode as multi-instruction Thumb-2 sequences =====
6449
6450            // I64Add: ADDS rdlo, rnlo, rmlo; ADC.W rdhi, rnhi, rmhi
6451            ArmOp::I64Add {
6452                rdlo,
6453                rdhi,
6454                rnlo,
6455                rnhi,
6456                rmlo,
6457                rmhi,
6458            } => {
6459                let mut bytes = Vec::new();
6460                // ADDS rdlo, rnlo, rmlo (16-bit)
6461                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adds {
6462                    rd: *rdlo,
6463                    rn: *rnlo,
6464                    op2: Operand2::Reg(*rmlo),
6465                })?);
6466                // ADC.W rdhi, rnhi, rmhi (32-bit)
6467                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adc {
6468                    rd: *rdhi,
6469                    rn: *rnhi,
6470                    op2: Operand2::Reg(*rmhi),
6471                })?);
6472                Ok(bytes)
6473            }
6474
6475            // I64Sub: SUBS rdlo, rnlo, rmlo; SBC.W rdhi, rnhi, rmhi
6476            ArmOp::I64Sub {
6477                rdlo,
6478                rdhi,
6479                rnlo,
6480                rnhi,
6481                rmlo,
6482                rmhi,
6483            } => {
6484                let mut bytes = Vec::new();
6485                // SUBS rdlo, rnlo, rmlo (16-bit)
6486                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Subs {
6487                    rd: *rdlo,
6488                    rn: *rnlo,
6489                    op2: Operand2::Reg(*rmlo),
6490                })?);
6491                // SBC.W rdhi, rnhi, rmhi (32-bit)
6492                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Sbc {
6493                    rd: *rdhi,
6494                    rn: *rnhi,
6495                    op2: Operand2::Reg(*rmhi),
6496                })?);
6497                Ok(bytes)
6498            }
6499
6500            // I64And: AND rdlo, rnlo, rmlo; AND rdhi, rnhi, rmhi
6501            ArmOp::I64And {
6502                rdlo,
6503                rdhi,
6504                rnlo,
6505                rnhi,
6506                rmlo,
6507                rmhi,
6508            } => {
6509                let mut bytes = Vec::new();
6510                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6511                    rd: *rdlo,
6512                    rn: *rnlo,
6513                    op2: Operand2::Reg(*rmlo),
6514                })?);
6515                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6516                    rd: *rdhi,
6517                    rn: *rnhi,
6518                    op2: Operand2::Reg(*rmhi),
6519                })?);
6520                Ok(bytes)
6521            }
6522
6523            // I64Or: ORR rdlo, rnlo, rmlo; ORR rdhi, rnhi, rmhi
6524            ArmOp::I64Or {
6525                rdlo,
6526                rdhi,
6527                rnlo,
6528                rnhi,
6529                rmlo,
6530                rmhi,
6531            } => {
6532                let mut bytes = Vec::new();
6533                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6534                    rd: *rdlo,
6535                    rn: *rnlo,
6536                    op2: Operand2::Reg(*rmlo),
6537                })?);
6538                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6539                    rd: *rdhi,
6540                    rn: *rnhi,
6541                    op2: Operand2::Reg(*rmhi),
6542                })?);
6543                Ok(bytes)
6544            }
6545
6546            // I64Xor: EOR rdlo, rnlo, rmlo; EOR rdhi, rnhi, rmhi
6547            ArmOp::I64Xor {
6548                rdlo,
6549                rdhi,
6550                rnlo,
6551                rnhi,
6552                rmlo,
6553                rmhi,
6554            } => {
6555                let mut bytes = Vec::new();
6556                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6557                    rd: *rdlo,
6558                    rn: *rnlo,
6559                    op2: Operand2::Reg(*rmlo),
6560                })?);
6561                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6562                    rd: *rdhi,
6563                    rn: *rnhi,
6564                    op2: Operand2::Reg(*rmhi),
6565                })?);
6566                Ok(bytes)
6567            }
6568
6569            // I64Eqz: ORR scratch, lo, hi; ITE EQ; MOV rd, #1; MOV rd, #0
6570            ArmOp::I64Eqz { rd, rnlo, rnhi } => self.encode_thumb(&ArmOp::I64SetCondZ {
6571                rd: *rd,
6572                rn_lo: *rnlo,
6573                rn_hi: *rnhi,
6574            }),
6575
6576            // I64 comparisons: delegate to I64SetCond
6577            ArmOp::I64Eq {
6578                rd,
6579                rnlo,
6580                rnhi,
6581                rmlo,
6582                rmhi,
6583            } => self.encode_thumb(&ArmOp::I64SetCond {
6584                rd: *rd,
6585                rn_lo: *rnlo,
6586                rn_hi: *rnhi,
6587                rm_lo: *rmlo,
6588                rm_hi: *rmhi,
6589                cond: synth_synthesis::Condition::EQ,
6590            }),
6591
6592            ArmOp::I64Ne {
6593                rd,
6594                rnlo,
6595                rnhi,
6596                rmlo,
6597                rmhi,
6598            } => self.encode_thumb(&ArmOp::I64SetCond {
6599                rd: *rd,
6600                rn_lo: *rnlo,
6601                rn_hi: *rnhi,
6602                rm_lo: *rmlo,
6603                rm_hi: *rmhi,
6604                cond: synth_synthesis::Condition::NE,
6605            }),
6606
6607            ArmOp::I64LtS {
6608                rd,
6609                rnlo,
6610                rnhi,
6611                rmlo,
6612                rmhi,
6613            } => self.encode_thumb(&ArmOp::I64SetCond {
6614                rd: *rd,
6615                rn_lo: *rnlo,
6616                rn_hi: *rnhi,
6617                rm_lo: *rmlo,
6618                rm_hi: *rmhi,
6619                cond: synth_synthesis::Condition::LT,
6620            }),
6621
6622            ArmOp::I64LtU {
6623                rd,
6624                rnlo,
6625                rnhi,
6626                rmlo,
6627                rmhi,
6628            } => self.encode_thumb(&ArmOp::I64SetCond {
6629                rd: *rd,
6630                rn_lo: *rnlo,
6631                rn_hi: *rnhi,
6632                rm_lo: *rmlo,
6633                rm_hi: *rmhi,
6634                cond: synth_synthesis::Condition::LO,
6635            }),
6636
6637            ArmOp::I64LeS {
6638                rd,
6639                rnlo,
6640                rnhi,
6641                rmlo,
6642                rmhi,
6643            } => self.encode_thumb(&ArmOp::I64SetCond {
6644                rd: *rd,
6645                rn_lo: *rnlo,
6646                rn_hi: *rnhi,
6647                rm_lo: *rmlo,
6648                rm_hi: *rmhi,
6649                cond: synth_synthesis::Condition::LE,
6650            }),
6651
6652            ArmOp::I64LeU {
6653                rd,
6654                rnlo,
6655                rnhi,
6656                rmlo,
6657                rmhi,
6658            } => self.encode_thumb(&ArmOp::I64SetCond {
6659                rd: *rd,
6660                rn_lo: *rnlo,
6661                rn_hi: *rnhi,
6662                rm_lo: *rmlo,
6663                rm_hi: *rmhi,
6664                cond: synth_synthesis::Condition::LS,
6665            }),
6666
6667            ArmOp::I64GtS {
6668                rd,
6669                rnlo,
6670                rnhi,
6671                rmlo,
6672                rmhi,
6673            } => self.encode_thumb(&ArmOp::I64SetCond {
6674                rd: *rd,
6675                rn_lo: *rnlo,
6676                rn_hi: *rnhi,
6677                rm_lo: *rmlo,
6678                rm_hi: *rmhi,
6679                cond: synth_synthesis::Condition::GT,
6680            }),
6681
6682            ArmOp::I64GtU {
6683                rd,
6684                rnlo,
6685                rnhi,
6686                rmlo,
6687                rmhi,
6688            } => self.encode_thumb(&ArmOp::I64SetCond {
6689                rd: *rd,
6690                rn_lo: *rnlo,
6691                rn_hi: *rnhi,
6692                rm_lo: *rmlo,
6693                rm_hi: *rmhi,
6694                cond: synth_synthesis::Condition::HI,
6695            }),
6696
6697            ArmOp::I64GeS {
6698                rd,
6699                rnlo,
6700                rnhi,
6701                rmlo,
6702                rmhi,
6703            } => self.encode_thumb(&ArmOp::I64SetCond {
6704                rd: *rd,
6705                rn_lo: *rnlo,
6706                rn_hi: *rnhi,
6707                rm_lo: *rmlo,
6708                rm_hi: *rmhi,
6709                cond: synth_synthesis::Condition::GE,
6710            }),
6711
6712            ArmOp::I64GeU {
6713                rd,
6714                rnlo,
6715                rnhi,
6716                rmlo,
6717                rmhi,
6718            } => self.encode_thumb(&ArmOp::I64SetCond {
6719                rd: *rd,
6720                rn_lo: *rnlo,
6721                rn_hi: *rnhi,
6722                rm_lo: *rmlo,
6723                rm_hi: *rmhi,
6724                cond: synth_synthesis::Condition::HS,
6725            }),
6726
6727            // I64Const: MOVW rdlo, lo16; MOVT rdlo, hi16; MOVW rdhi, lo16_hi; MOVT rdhi, hi16_hi
6728            ArmOp::I64Const { rdlo, rdhi, value } => {
6729                let lo32 = *value as u32;
6730                let hi32 = (*value >> 32) as u32;
6731                let mut bytes = Vec::new();
6732                // Load low 32 bits into rdlo
6733                bytes.extend_from_slice(
6734                    &self.encode_thumb32_movw_raw(reg_to_bits(rdlo), lo32 & 0xFFFF)?,
6735                );
6736                if lo32 > 0xFFFF {
6737                    bytes.extend_from_slice(
6738                        &self.encode_thumb32_movt_raw(reg_to_bits(rdlo), lo32 >> 16)?,
6739                    );
6740                }
6741                // Load high 32 bits into rdhi
6742                bytes.extend_from_slice(
6743                    &self.encode_thumb32_movw_raw(reg_to_bits(rdhi), hi32 & 0xFFFF)?,
6744                );
6745                if hi32 > 0xFFFF {
6746                    bytes.extend_from_slice(
6747                        &self.encode_thumb32_movt_raw(reg_to_bits(rdhi), hi32 >> 16)?,
6748                    );
6749                }
6750                Ok(bytes)
6751            }
6752
6753            // I64Ldr: LDR rdlo, [base, offset]; LDR rdhi, [base, offset+4]
6754            ArmOp::I64Ldr { rdlo, rdhi, addr } => {
6755                let mut bytes = Vec::new();
6756                // #372/#382: a memory `i64.load` carries an index register
6757                // (`reg_imm(R11, addr_reg, offset)` = R11 + addr + offset). The
6758                // immediate `encode_thumb32_ldr` below uses only base+offset and
6759                // would SILENTLY DROP `offset_reg` — the #206 defect, here for
6760                // i64. `i64_effective_base` materializes the effective base into
6761                // `ip` (and, when `offset+4 > 0xFFF`, folds the offset in too so
6762                // the function is NOT skipped — #382), returning the residual
6763                // imm12 for the two halves. Frame i64 loads (no `offset_reg`, e.g.
6764                // a spilled local at `[SP, #off]`) keep the plain `[base,#off]`
6765                // form unchanged — so existing output is byte-identical.
6766                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6767                bytes.extend_from_slice(&self.encode_thumb32_ldr(rdlo, &base, offset)?);
6768                bytes.extend_from_slice(&self.encode_thumb32_ldr(
6769                    rdhi,
6770                    &base,
6771                    offset.wrapping_add(4),
6772                )?);
6773                Ok(bytes)
6774            }
6775
6776            // I64Str: STR rdlo, [base, offset]; STR rdhi, [base, offset+4]
6777            ArmOp::I64Str { rdlo, rdhi, addr } => {
6778                let mut bytes = Vec::new();
6779                // #372/#382: same index-materialization + large-offset fold as
6780                // I64Ldr (see above).
6781                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6782                bytes.extend_from_slice(&self.encode_thumb32_str(rdlo, &base, offset)?);
6783                bytes.extend_from_slice(&self.encode_thumb32_str(
6784                    rdhi,
6785                    &base,
6786                    offset.wrapping_add(4),
6787                )?);
6788                Ok(bytes)
6789            }
6790
6791            // I64ExtendI32S: MOV rdlo, rn; ASR rdhi, rdlo, #31 (sign-extend)
6792            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
6793                let mut bytes = Vec::new();
6794                if rdlo != rn {
6795                    // MOV rdlo, rn (16-bit)
6796                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6797                        rd: *rdlo,
6798                        op2: Operand2::Reg(*rn),
6799                    })?);
6800                }
6801                // ASR rdhi, rdlo, #31 (sign-extend: fill high word with sign bit)
6802                bytes.extend_from_slice(
6803                    &self.encode_thumb32_shift(rdhi, rdlo, 31, 0b10)?, // ASR type
6804                );
6805                Ok(bytes)
6806            }
6807
6808            // I64ExtendI32U: MOV rdlo, rn; MOV rdhi, #0
6809            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
6810                let mut bytes = Vec::new();
6811                if rdlo != rn {
6812                    // MOV rdlo, rn
6813                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6814                        rd: *rdlo,
6815                        op2: Operand2::Reg(*rn),
6816                    })?);
6817                }
6818                // MOV rdhi, #0 (#916: MOV.W for rdhi >= R8). Unconditional
6819                // site with no branches in the expansion — before the fix this
6820                // emitted the literal two-instruction stream [4608, 2800], half
6821                // of which was `CMP r0,#0` rather than the high-word clear, so
6822                // every i64.extend_i32_u into a high pair leaked stale bits.
6823                emit_thumb_zero_fill(&mut bytes, reg_to_bits(rdhi));
6824                Ok(bytes)
6825            }
6826
6827            // I32WrapI64: MOV rd, rnlo (just take low 32 bits)
6828            ArmOp::I32WrapI64 { rd, rnlo } => {
6829                if rd == rnlo {
6830                    // No-op: already in the right register
6831                    let instr: u16 = 0xBF00; // NOP
6832                    Ok(instr.to_le_bytes().to_vec())
6833                } else {
6834                    // MOV rd, rnlo
6835                    self.encode_thumb(&ArmOp::Mov {
6836                        rd: *rd,
6837                        op2: Operand2::Reg(*rnlo),
6838                    })
6839                }
6840            }
6841
6842            // ===== Helium MVE operations (Thumb-2 encoding) =====
6843            ArmOp::MveLoad { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vldrw(qd, addr))),
6844            ArmOp::MveStore { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vstrw(qd, addr))),
6845            ArmOp::MveConst { qd, bytes } => self.encode_thumb_mve_const(qd, bytes),
6846            ArmOp::MveAnd { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6847                0xEF000150, qd, qn, qm,
6848            ))),
6849            ArmOp::MveOrr { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6850                0xEF200150, qd, qn, qm,
6851            ))),
6852            ArmOp::MveEor { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6853                0xFF000150, qd, qn, qm,
6854            ))),
6855            ArmOp::MveMvn { qd, qm } => {
6856                // VMVN Qd, Qm: 0xFFB005C0 | Qd<<12 | Qm
6857                let qd_enc = qreg_to_num(qd);
6858                let qm_enc = qreg_to_num(qm);
6859                let instr: u32 = 0xFFB005C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6860                Ok(vfp_to_thumb_bytes(instr))
6861            }
6862            ArmOp::MveBic { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6863                0xEF100150, qd, qn, qm,
6864            ))),
6865            ArmOp::MveAddI { qd, qn, qm, size } => {
6866                let sz = mve_size_bits(size);
6867                let base: u32 = 0xEF000840 | (sz << 20);
6868                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6869            }
6870            ArmOp::MveSubI { qd, qn, qm, size } => {
6871                let sz = mve_size_bits(size);
6872                let base: u32 = 0xFF000840 | (sz << 20);
6873                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6874            }
6875            ArmOp::MveMulI { qd, qn, qm, size } => {
6876                let sz = mve_size_bits(size);
6877                let base: u32 = 0xEF000950 | (sz << 20);
6878                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6879            }
6880            ArmOp::MveNegI { qd, qm, size } => {
6881                let sz = mve_size_bits(size);
6882                // VNEG.Sx Qd, Qm
6883                let qd_enc = qreg_to_num(qd);
6884                let qm_enc = qreg_to_num(qm);
6885                let base: u32 = 0xFFB103C0 | (sz << 18);
6886                let instr = base | ((qd_enc * 2) << 12) | (qm_enc * 2);
6887                Ok(vfp_to_thumb_bytes(instr))
6888            }
6889            ArmOp::MveDup { qd, rn, size } => {
6890                let sz = mve_size_bits(size);
6891                let qd_enc = qreg_to_num(qd);
6892                let rn_bits = reg_to_bits(rn);
6893                // VDUP.sz Qd, Rn: EEA0 0B10 variant
6894                // size encoding: 00=32, 01=16, 10=8
6895                let be = match sz {
6896                    0 => 0b00u32, // 8-bit
6897                    1 => 0b01,    // 16-bit
6898                    _ => 0b00,    // 32-bit (default)
6899                };
6900                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12) | (be << 5);
6901                Ok(vfp_to_thumb_bytes(instr))
6902            }
6903            ArmOp::MveExtractLane { rd, qn, lane, size } => {
6904                let qn_enc = qreg_to_num(qn);
6905                let rd_bits = reg_to_bits(rd);
6906                // VMOV.sz Rd, Dn[x] — extract from Q-register lane
6907                // For 32-bit: VMOV Rd, Dn — where Dn is the appropriate D-register
6908                let d_reg = qn_enc * 2 + ((*lane as u32) >> 1);
6909                let lane_in_d = (*lane as u32) & 1;
6910                let _sz = mve_size_bits(size);
6911                // VMOV Rd, Dn[x]: EE10 0B10 for 32-bit
6912                let instr: u32 = 0xEE100B10 | (d_reg << 16) | (rd_bits << 12) | (lane_in_d << 21);
6913                Ok(vfp_to_thumb_bytes(instr))
6914            }
6915            ArmOp::MveInsertLane { qd, rn, lane, size } => {
6916                let qd_enc = qreg_to_num(qd);
6917                let rn_bits = reg_to_bits(rn);
6918                let d_reg = qd_enc * 2 + ((*lane as u32) >> 1);
6919                let lane_in_d = (*lane as u32) & 1;
6920                let _sz = mve_size_bits(size);
6921                // VMOV Dn[x], Rn: EE00 0B10 for 32-bit
6922                let instr: u32 = 0xEE000B10 | (d_reg << 16) | (rn_bits << 12) | (lane_in_d << 21);
6923                Ok(vfp_to_thumb_bytes(instr))
6924            }
6925
6926            // MVE float comparisons — emit VCMP + VPSEL sequence (simplified: just VCMP)
6927            ArmOp::MveCmpEqI { qd, qn, qm, size }
6928            | ArmOp::MveCmpNeI { qd, qn, qm, size }
6929            | ArmOp::MveCmpLtS { qd, qn, qm, size }
6930            | ArmOp::MveCmpLtU { qd, qn, qm, size }
6931            | ArmOp::MveCmpGtS { qd, qn, qm, size }
6932            | ArmOp::MveCmpGtU { qd, qn, qm, size }
6933            | ArmOp::MveCmpLeS { qd, qn, qm, size }
6934            | ArmOp::MveCmpLeU { qd, qn, qm, size }
6935            | ArmOp::MveCmpGeS { qd, qn, qm, size }
6936            | ArmOp::MveCmpGeU { qd, qn, qm, size } => {
6937                // Encode as VADD (placeholder encoding — real implementation
6938                // would use VCMP + VPSEL pair)
6939                let sz = mve_size_bits(size);
6940                let base: u32 = 0xEF000840 | (sz << 20);
6941                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6942            }
6943
6944            // f32x4 MVE arithmetic
6945            ArmOp::MveAddF32 { qd, qn, qm } => {
6946                // VADD.F32 Qd, Qn, Qm (MVE): 0xEF000D40
6947                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6948            }
6949            ArmOp::MveSubF32 { qd, qn, qm } => {
6950                // VSUB.F32 Qd, Qn, Qm (MVE): 0xEF200D40
6951                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF200D40, qd, qn, qm)))
6952            }
6953            ArmOp::MveMulF32 { qd, qn, qm } => {
6954                // VMUL.F32 Qd, Qn, Qm (MVE): 0xFF000D50
6955                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xFF000D50, qd, qn, qm)))
6956            }
6957            ArmOp::MveNegF32 { qd, qm } => {
6958                let qd_enc = qreg_to_num(qd);
6959                let qm_enc = qreg_to_num(qm);
6960                // VNEG.F32 Qd, Qm: FFB907C0
6961                let instr: u32 = 0xFFB907C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6962                Ok(vfp_to_thumb_bytes(instr))
6963            }
6964            ArmOp::MveAbsF32 { qd, qm } => {
6965                let qd_enc = qreg_to_num(qd);
6966                let qm_enc = qreg_to_num(qm);
6967                // VABS.F32 Qd, Qm: FFB90740
6968                let instr: u32 = 0xFFB90740 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6969                Ok(vfp_to_thumb_bytes(instr))
6970            }
6971            ArmOp::MveCmpEqF32 { qd, qn, qm }
6972            | ArmOp::MveCmpNeF32 { qd, qn, qm }
6973            | ArmOp::MveCmpLtF32 { qd, qn, qm }
6974            | ArmOp::MveCmpLeF32 { qd, qn, qm }
6975            | ArmOp::MveCmpGtF32 { qd, qn, qm }
6976            | ArmOp::MveCmpGeF32 { qd, qn, qm } => {
6977                // Placeholder: encode as VADD.F32 (real impl needs VCMP.F32 + VPSEL)
6978                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6979            }
6980            ArmOp::MveDupF32 { qd, rn } => {
6981                let qd_enc = qreg_to_num(qd);
6982                let rn_bits = reg_to_bits(rn);
6983                // VDUP.32 Qd, Rn (same encoding as integer VDUP.32)
6984                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12);
6985                Ok(vfp_to_thumb_bytes(instr))
6986            }
6987            ArmOp::MveExtractLaneF32 { rd, qn, lane } => {
6988                let qn_enc = qreg_to_num(qn);
6989                let rd_bits = reg_to_bits(rd);
6990                // VMOV Rd, Sn where Sn = Q*4 + lane
6991                let s_num = qn_enc * 4 + (*lane as u32);
6992                let (vn, n) = encode_sreg(s_num);
6993                let instr: u32 = 0xEE100A10 | (vn << 16) | (rd_bits << 12) | (n << 7);
6994                Ok(vfp_to_thumb_bytes(instr))
6995            }
6996            ArmOp::MveReplaceLaneF32 { qd, rn, lane } => {
6997                let qd_enc = qreg_to_num(qd);
6998                let rn_bits = reg_to_bits(rn);
6999                // VMOV Sn, Rn where Sn = Q*4 + lane
7000                let s_num = qd_enc * 4 + (*lane as u32);
7001                let (vn, n) = encode_sreg(s_num);
7002                let instr: u32 = 0xEE000A10 | (vn << 16) | (rn_bits << 12) | (n << 7);
7003                Ok(vfp_to_thumb_bytes(instr))
7004            }
7005            ArmOp::MveDivF32 { qd, qn, qm } => {
7006                // Lane-wise: extract 4 S-regs, VDIV, insert back
7007                self.encode_thumb_mve_lane_wise_f32_binop(qd, qn, qm, 0xEE800A00)
7008            }
7009            ArmOp::MveSqrtF32 { qd, qm } => {
7010                // Lane-wise: extract 4 S-regs, VSQRT, insert back
7011                self.encode_thumb_mve_lane_wise_f32_sqrt(qd, qm)
7012            }
7013
7014            // RQ-67-VFPREACH (#1267): the AAPCS callee-saved VFP half.
7015            //
7016            // Encodings taken from llvm-mc, not derived by hand:
7017            //   llvm-mc -triple=thumbv7em-none-eabi -mattr=+vfp4 -show-encoding
7018            //     vpush {d8-d15}  ->  [0x2d,0xed,0x10,0x8b]   (ED2D 8B10)
7019            //     vpop  {d8-d15}  ->  [0xbd,0xec,0x10,0x8b]   (ECBD 8B10)
7020            //   imm8 is 2 x the double count (0x10 = 16 = 2 x 8), and the
7021            //   `8B` nibble selects the 64-bit form — `vpush {d8}` is 8B02,
7022            //   which is how that field was confirmed rather than assumed.
7023            //
7024            // NOTE FOR ANYONE ADDING AN OP BELOW THIS POINT: the catch-all that
7025            // follows returns a NOP, so an op that reaches it is encoded as
7026            // nothing at all. These two arms exist ABOVE it deliberately.
7027            ArmOp::VPushCalleeSavedVfp => Ok(vec![0x2d, 0xed, 0x10, 0x8b]),
7028            ArmOp::VPopCalleeSavedVfp => Ok(vec![0xbd, 0xec, 0x10, 0x8b]),
7029
7030            // Catch-all for any remaining ops
7031            //
7032            // #615 FIXED THIS CLASS ON THE A32 PATH AND NOT HERE. Thumb-2 is
7033            // the DEFAULT target, and this arm silently encodes any unhandled
7034            // op as `NOP`. RQ-67-VFPREACH hit it directly: the two VFP
7035            // save/restore ops above compiled clean and would have emitted a
7036            // save that does nothing. Filed rather than widened in this lane —
7037            // turning it into a typed Err is a behaviour change across every
7038            // op that currently lands here, which needs its own byte-identity
7039            // evidence.
7040            other => Err(synth_core::Error::synthesis(format!(
7041                "{other:?} reached the Thumb-2 encoder with no Thumb-2 encoding — \
7042                 refusing to emit a silent NOP (#1272; #615 closed this class on A32 \
7043                 only, and Thumb-2 is the default target)"
7044            ))),
7045        }
7046    }
7047
7048    // === Thumb-2 VFP multi-instruction helpers ===
7049
7050    /// Encode F32 comparison as Thumb-2: VCMP.F32 + VMRS + MOVS rd,#0 + IT + MOV rd,#1
7051    fn encode_thumb_f32_compare(
7052        &self,
7053        rd: &Reg,
7054        sn: &VfpReg,
7055        sm: &VfpReg,
7056        cond_code: u32,
7057    ) -> Result<Vec<u8>> {
7058        let mut bytes = Vec::new();
7059        let rd_bits = reg_to_bits(rd);
7060
7061        // #709 (bug found under #708/#709): the `MOVS Rd,#0` below is a
7062        // FLAG-SETTING 16-bit move. Emitting it AFTER `VMRS APSR_nzcv, FPSCR`
7063        // (as the original code did) clobbered the N/Z/C/V flags the VMRS just
7064        // transferred from the VFP compare, so the following `IT<cond>` read
7065        // stale flags and every f32 comparison silently returned 0 (verified:
7066        // `flt(1.0,2.0)` → 0 on Cortex-M4F). The 619 harness never caught it
7067        // because it deliberately skipped compare EXECUTION on a false premise
7068        // (unicorn DOES model VMRS→APSR). Fix: materialize the `#0` FIRST, then
7069        // VCMP+VMRS set the flags the `IT` consumes. Instruction sizes are
7070        // unchanged (pure reorder), so the estimator↔encoder oracle (#511) is
7071        // untouched — only the byte ORDER differs.
7072
7073        // MOVS Rd, #0 (16-bit): 0010 0 Rd(3) 0000 0000 — its flag side effect
7074        // is immediately overwritten by the VMRS below.
7075        if rd_bits < 8 {
7076            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
7077            bytes.extend_from_slice(&movs_zero.to_le_bytes());
7078        } else {
7079            // MOV.W Rd, #0 (32-bit Thumb-2)
7080            let hw1: u16 = 0xF04F;
7081            let hw2: u16 = (rd_bits as u16) << 8;
7082            bytes.extend_from_slice(&hw1.to_le_bytes());
7083            bytes.extend_from_slice(&hw2.to_le_bytes());
7084        }
7085
7086        // VCMP.F32 Sn, Sm
7087        let sn_num = vfp_sreg_to_num(sn)?;
7088        let sm_num = vfp_sreg_to_num(sm)?;
7089        let (vd, d) = encode_sreg(sn_num);
7090        let (vm, m) = encode_sreg(sm_num);
7091        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7092        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7093
7094        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10 (sets the flags IT consumes)
7095        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7096
7097        // IT<cond> — If-Then for conditional MOV
7098        // IT encoding: 1011 1111 cond(4) mask(4)
7099        // mask = 0x8 for single "then" (IT)
7100        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7101        bytes.extend_from_slice(&it.to_le_bytes());
7102
7103        // MOV Rd, #1 (16-bit, conditional due to IT): 0010 0 Rd(3) 0000 0001
7104        if rd_bits < 8 {
7105            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7106            bytes.extend_from_slice(&mov_one.to_le_bytes());
7107        } else {
7108            // MOV.W Rd, #1 (32-bit)
7109            let hw1: u16 = 0xF04F;
7110            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7111            bytes.extend_from_slice(&hw1.to_le_bytes());
7112            bytes.extend_from_slice(&hw2.to_le_bytes());
7113        }
7114
7115        Ok(bytes)
7116    }
7117
7118    /// Encode F32 constant load as Thumb-2: MOVW + MOVT + VMOV
7119    fn encode_thumb_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
7120        let mut bytes = Vec::new();
7121        let bits = value.to_bits();
7122        let rt: u32 = 12; // R12/IP as temp
7123
7124        // MOVW R12, #lo16
7125        // Thumb-2 MOVW: 11110 i 10 0100 imm4 | 0 imm3 Rd imm8
7126        let lo16 = bits & 0xFFFF;
7127        let imm4 = (lo16 >> 12) & 0xF;
7128        let i_bit = (lo16 >> 11) & 1;
7129        let imm3 = (lo16 >> 8) & 0x7;
7130        let imm8 = lo16 & 0xFF;
7131        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7132        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
7133        bytes.extend_from_slice(&hw1.to_le_bytes());
7134        bytes.extend_from_slice(&hw2.to_le_bytes());
7135
7136        // MOVT R12, #hi16
7137        let hi16 = (bits >> 16) & 0xFFFF;
7138        let imm4 = (hi16 >> 12) & 0xF;
7139        let i_bit = (hi16 >> 11) & 1;
7140        let imm3 = (hi16 >> 8) & 0x7;
7141        let imm8 = hi16 & 0xFF;
7142        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
7143        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
7144        bytes.extend_from_slice(&hw1.to_le_bytes());
7145        bytes.extend_from_slice(&hw2.to_le_bytes());
7146
7147        // VMOV Sd, R12
7148        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
7149        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7150
7151        Ok(bytes)
7152    }
7153
7154    /// Encode VMOV + VCVT.F32.xS32 as Thumb-2
7155    fn encode_thumb_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7156        let mut bytes = Vec::new();
7157
7158        // VMOV Sd, Rm
7159        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
7160        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7161
7162        // VCVT.F32.S32/U32 Sd, Sd. Bit 7 (op) = 1 for signed (S32), 0 for
7163        // unsigned (U32): signed = 0xEEB80AC0, unsigned = 0xEEB80A40
7164        // (GI-FPU-002: previously swapped — see the ARM32 twin).
7165        let sd_num = vfp_sreg_to_num(sd)?;
7166        let (vd, d) = encode_sreg(sd_num);
7167        let (vm, m) = encode_sreg(sd_num);
7168        let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
7169        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7170        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7171
7172        Ok(bytes)
7173    }
7174
7175    /// Encode F32 rounding pseudo-op as Thumb-2 via VCVT to integer and back
7176    /// Encode F32 rounding as Thumb-2.
7177    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
7178    ///
7179    /// For trunc: uses VCVTR.S32.F32 (always truncates).
7180    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant),
7181    /// then restores FPSCR.
7182    fn encode_thumb_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7183        let mut bytes = Vec::new();
7184        let sm_num = vfp_sreg_to_num(sm)?;
7185        let sd_num = vfp_sreg_to_num(sd)?;
7186        let (vd_s, d_s) = encode_sreg(sd_num);
7187        let (vm_s, m_s) = encode_sreg(sm_num);
7188
7189        if mode == 0b11 {
7190            // Trunc (toward zero): VCVTR.S32.F32 — bit[7]=1, always truncates
7191            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7192            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7193        } else {
7194            // ceil/floor/nearest: manipulate FPSCR rounding mode
7195            let rt: u32 = 12; // R12/IP as temp
7196
7197            // VMRS R12, FPSCR
7198            let vmrs = 0xEEF10A10 | (rt << 12);
7199            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7200
7201            // BIC.W R12, R12, #(3 << 22) — clear RMode bits [23:22]
7202            // Thumb-2 modified immediate for 3<<22 = 0x00C00000:
7203            // BIC.W encoding: 11110 i 0 0001 S Rn | 0 imm3 Rd imm8
7204            // 0x00C00000 = 0x03 shifted left by 22 => Thumb mod-imm: i=0, imm3=0b101, imm8=0x03
7205            let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF); // BIC, Rn=R12
7206            let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
7207            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7208            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7209
7210            // ORR.W R12, R12, #(mode << 22)
7211            if mode != 0 {
7212                let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF); // ORR, Rn=R12
7213                let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
7214                bytes.extend_from_slice(&orr_hw1.to_le_bytes());
7215                bytes.extend_from_slice(&orr_hw2.to_le_bytes());
7216            }
7217
7218            // VMSR FPSCR, R12
7219            let vmsr = 0xEEE10A10 | (rt << 12);
7220            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7221
7222            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rmode
7223            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7224            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7225
7226            // Restore FPSCR: clear rmode bits back to nearest (default)
7227            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7228            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7229            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7230            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7231        }
7232
7233        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
7234        let (vd2, d2) = encode_sreg(sd_num);
7235        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
7236        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
7237
7238        Ok(bytes)
7239    }
7240
7241    /// Encode F32 min/max as Thumb-2: VMOV + VCMP + VMRS + IT + VMOV
7242    fn encode_thumb_f32_minmax(
7243        &self,
7244        sd: &VfpReg,
7245        sn: &VfpReg,
7246        sm: &VfpReg,
7247        is_min: bool,
7248    ) -> Result<Vec<u8>> {
7249        let mut bytes = Vec::new();
7250        let sn_num = vfp_sreg_to_num(sn)?;
7251        let sm_num = vfp_sreg_to_num(sm)?;
7252        let sd_num = vfp_sreg_to_num(sd)?;
7253
7254        // VMOV.F32 Sd, Sn
7255        let (vd, d) = encode_sreg(sd_num);
7256        let (vn, n) = encode_sreg(sn_num);
7257        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
7258        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sn));
7259
7260        // VCMP.F32 Sn, Sm
7261        let (vm, m) = encode_sreg(sm_num);
7262        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7263        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7264
7265        // VMRS APSR_nzcv, FPSCR
7266        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7267
7268        // IT GT (for min) or IT MI (for max)
7269        let cond: u16 = if is_min { 0xC } else { 0x4 };
7270        let it: u16 = 0xBF00 | (cond << 4) | 0x8;
7271        bytes.extend_from_slice(&it.to_le_bytes());
7272
7273        // VMOV{cond}.F32 Sd, Sm — conditional VMOV in IT block
7274        let vmov_sm = 0xEEB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7275        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sm));
7276
7277        Ok(bytes)
7278    }
7279
7280    /// Encode F32 copysign as Thumb-2
7281    /// Encode F32 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7282    /// encoder scratch, #212), the flags, and Sd:
7283    ///
7284    ///   VMOV R12, Sm ; CMP R12, #0    (N flag = the sign bit)
7285    ///   VABS.F32 Sd, Sn               (magnitude, sign cleared)
7286    ///   IT MI ; VNEG.F32(MI) Sd, Sd
7287    ///
7288    /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7289    /// The R12 capture happens BEFORE Sd is written, so Sd aliasing Sn or Sm
7290    /// is safe. (The previous sequence staged the magnitude through R0 —
7291    /// clobbering a live allocator-owned value, the #615 class; caught while
7292    /// composing the F64 twin for #369.)
7293    fn encode_thumb_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7294        let mut bytes = Vec::new();
7295
7296        // VMOV R12, Sm (sign source bits)
7297        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
7298            false,
7299            sm,
7300            &Reg::R12,
7301        )?));
7302        // CMP.W R12, #0 — N = bit31 (the sign, incl. -0.0 / -NaN).
7303        bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7304        bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7305        // VABS.F32 Sd, Sn
7306        let sd_num = vfp_sreg_to_num(sd)?;
7307        let sn_num = vfp_sreg_to_num(sn)?;
7308        let (vd, d) = encode_sreg(sd_num);
7309        let (vn, n) = encode_sreg(sn_num);
7310        let vabs = 0xEEB00AC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7311        bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7312        // IT MI ; VNEG.F32(MI) Sd, Sd
7313        bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7314        let vneg = 0xEEB10A40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7315        bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7316
7317        Ok(bytes)
7318    }
7319
7320    /// Encode F64 comparison as Thumb-2: VCMP.F64 + VMRS + MOV #0 + IT + MOV #1
7321    fn encode_thumb_f64_compare(
7322        &self,
7323        rd: &Reg,
7324        dn: &VfpReg,
7325        dm: &VfpReg,
7326        cond_code: u32,
7327    ) -> Result<Vec<u8>> {
7328        let mut bytes = Vec::new();
7329        let rd_bits = reg_to_bits(rd);
7330
7331        // #712-class fix (found at f64-phase-2 wiring, #369): the 16-bit
7332        // `MOVS Rd,#0` is FLAG-SETTING. The original order emitted it AFTER
7333        // `VMRS APSR_nzcv, FPSCR`, clobbering the N/Z/C/V flags the VMRS just
7334        // transferred, so the following `IT<cond>` read stale flags and every
7335        // f64 comparison silently returned 0 — the exact bug the f32 compare
7336        // encoder shipped with and #712 fixed. Same fix: materialize the `#0`
7337        // FIRST (its flag side effect is overwritten by the VMRS), then
7338        // VCMP+VMRS set the flags the IT consumes. Pure reorder — sizes
7339        // unchanged.
7340
7341        // MOVS Rd, #0
7342        if rd_bits < 8 {
7343            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
7344            bytes.extend_from_slice(&movs_zero.to_le_bytes());
7345        } else {
7346            let hw1: u16 = 0xF04F;
7347            let hw2: u16 = (rd_bits as u16) << 8;
7348            bytes.extend_from_slice(&hw1.to_le_bytes());
7349            bytes.extend_from_slice(&hw2.to_le_bytes());
7350        }
7351
7352        // VCMP.F64 Dn, Dm
7353        let dn_num = vfp_dreg_to_num(dn)?;
7354        let dm_num = vfp_dreg_to_num(dm)?;
7355        let (vd, d) = encode_dreg(dn_num);
7356        let (vm, m) = encode_dreg(dm_num);
7357        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7358        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7359
7360        // VMRS APSR_nzcv, FPSCR (sets the flags the IT consumes)
7361        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7362
7363        // IT<cond>
7364        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7365        bytes.extend_from_slice(&it.to_le_bytes());
7366
7367        // MOV Rd, #1
7368        if rd_bits < 8 {
7369            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7370            bytes.extend_from_slice(&mov_one.to_le_bytes());
7371        } else {
7372            let hw1: u16 = 0xF04F;
7373            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7374            bytes.extend_from_slice(&hw1.to_le_bytes());
7375            bytes.extend_from_slice(&hw2.to_le_bytes());
7376        }
7377
7378        Ok(bytes)
7379    }
7380
7381    /// Encode F64 constant load as Thumb-2: MOVW+MOVT (lo32 into R0) + MOVW+MOVT (hi32 into R12) + VMOV Dd, R0, R12
7382    fn encode_thumb_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
7383        let mut bytes = Vec::new();
7384        let bits = value.to_bits();
7385        let lo32 = bits as u32;
7386        let hi32 = (bits >> 32) as u32;
7387
7388        // MOVW R0, #lo16(lo32)
7389        let lo16 = lo32 & 0xFFFF;
7390        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(0, lo16)?);
7391
7392        // MOVT R0, #hi16(lo32)
7393        let hi16 = (lo32 >> 16) & 0xFFFF;
7394        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(0, hi16)?);
7395
7396        // MOVW R12, #lo16(hi32)
7397        let lo16 = hi32 & 0xFFFF;
7398        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
7399
7400        // MOVT R12, #hi16(hi32)
7401        let hi16 = (hi32 >> 16) & 0xFFFF;
7402        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
7403
7404        // VMOV Dd, R0, R12
7405        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
7406        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7407
7408        Ok(bytes)
7409    }
7410
7411    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as Thumb-2
7412    /// Encode i32 → f64 conversion as Thumb-2. The integer stages through the
7413    /// DESTINATION's own low S-alias (`S(2d)`) — allocator-owned by
7414    /// definition — never S0 (which may hold a live value; the previous
7415    /// pseudo-op's S0 staging was the #615 class). Also fixes the SWAPPED
7416    /// signed/unsigned VCVT bases (bit7 = 1 is SIGNED — the same swap the f32
7417    /// twin had; latent here because f64.convert_i32_* was decode-dropped
7418    /// until #369): clang-verified vcvt.f64.s32 d1,s2 = eeb8 1bc1,
7419    /// vcvt.f64.u32 d1,s2 = eeb8 1b41.
7420    fn encode_thumb_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7421        let dd_num = vfp_dreg_to_num(dd)?;
7422        if dd_num > 7 {
7423            return Err(synth_core::Error::synthesis(format!(
7424                "F64ConvertI32: destination {dd:?} has no S-register alias \
7425                 (D8..D15) — the selector allocates only D0..D7"
7426            )));
7427        }
7428        let mut bytes = Vec::new();
7429
7430        // VMOV S(2d), Rm — stage the integer in the destination's low word.
7431        let (vn_s, n_s) = encode_sreg(2 * dd_num);
7432        let rt = reg_to_bits(rm);
7433        let vmov = 0xEE000A10 | (vn_s << 16) | (rt << 12) | (n_s << 7);
7434        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7435
7436        // VCVT.F64.S32/U32 Dd, S(2d)
7437        let (vd, d) = encode_dreg(dd_num);
7438        let (vm, m) = encode_sreg(2 * dd_num);
7439        let base = if signed { 0xEEB80BC0 } else { 0xEEB80B40 };
7440        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7441        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7442
7443        Ok(bytes)
7444    }
7445
7446    /// Encode VCVT.F64.F32 Dd, Sm as Thumb-2
7447    fn encode_thumb_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7448        let dd_num = vfp_dreg_to_num(dd)?;
7449        let sm_num = vfp_sreg_to_num(sm)?;
7450        let (vd, d) = encode_dreg(dd_num);
7451        let (vm, m) = encode_sreg(sm_num);
7452
7453        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7454        Ok(vfp_to_thumb_bytes(vcvt))
7455    }
7456
7457    /// Encode VCVT.F32.F64 Sd, Dm (f32.demote_f64) as Thumb-2 — single
7458    /// instruction, round-to-nearest-even per FPSCR default, exactly WASM
7459    /// §4.3.3 demote (clang-verified: vcvt.f32.f64 s1,d2 = eef7 0bc2).
7460    fn encode_thumb_f32_demote_f64(&self, sd: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7461        let sd_num = vfp_sreg_to_num(sd)?;
7462        let dm_num = vfp_dreg_to_num(dm)?;
7463        let (vd, d) = encode_sreg(sd_num);
7464        let (vm, m) = encode_dreg(dm_num);
7465
7466        let vcvt = 0xEEB70BC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7467        Ok(vfp_to_thumb_bytes(vcvt))
7468    }
7469
7470    /// Encode f64 → i32 truncation as Thumb-2 (round-toward-zero VCVT).
7471    ///
7472    /// The 32-bit result stages through the SOURCE's own low S-alias, `S(2m)`,
7473    /// clobbering half of an operand the selector has already popped. The
7474    /// overlapping write is well-defined: VCVT reads its source operand before
7475    /// writing (compilers emit `vcvt.f32.f64 s0, d0` routinely).
7476    ///
7477    /// # The one precondition, and who actually provides it
7478    ///
7479    /// `dm` must be a DEAD TEMP — never a pinned param/local home. That is the
7480    /// whole safety argument, and it is worth naming the guarantor precisely
7481    /// (#946): **`select_with_stack`** provides it, by copying a home into a
7482    /// fresh D-temp first. Visible in the shipped output for
7483    /// `(func (param f64) (result i32) (i32.trunc_f64_s (local.get 0)))`:
7484    ///
7485    /// ```text
7486    ///   vmov r1, r2, d0     ; read the param out of its AAPCS-VFP home D0
7487    ///   vmov d1, r1, r2     ; ...into a fresh D-temp
7488    ///   vcvt.s32.f64 s2, d1 ; convert from the TEMP, staging into its own S2
7489    /// ```
7490    ///
7491    /// `InstructionSelector::select` / `select_default` do NOT provide it —
7492    /// `alloc_vfp_dreg` is a bare round-robin `(n + 1) % 16` with no liveness
7493    /// or home test. That path is not reachable from `synth compile`
7494    /// (`arm_backend.rs` calls `select_with_stack` exclusively; the only
7495    /// non-test caller of `select` is `examples/compile_add.rs`), so this is
7496    /// not a live miscompile — but a caller reaching that `pub` API directly
7497    /// gets no such guarantee.
7498    ///
7499    /// # What this deliberately does NOT claim
7500    ///
7501    /// An earlier version of this comment said the staging register is "never
7502    /// S0, which may hold an unrelated live value (the #615 class)". **That is
7503    /// false**, and measurably so: for
7504    /// `(func (result i32) (i32.trunc_f64_s (f64.const 3.7)))` the shipped
7505    /// compiler emits `vcvt.s32.f64 s0, d0`.
7506    ///
7507    /// It is also unnecessary. S0 is only dangerous as an *unrelated* scratch;
7508    /// here it is always the low half of `dm` itself, which the precondition
7509    /// above already makes dead. Naming a guard the code does not have — and
7510    /// does not need — invites a future reader to lean on it. The dead-temp
7511    /// precondition is the only thing holding this up.
7512    fn encode_thumb_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7513        let dm_num = vfp_dreg_to_num(dm)?;
7514        if dm_num > 7 {
7515            return Err(synth_core::Error::synthesis(format!(
7516                "I32TruncF64: source {dm:?} has no S-register alias \
7517                 (D8..D15) — the selector allocates only D0..D7"
7518            )));
7519        }
7520        let mut bytes = Vec::new();
7521
7522        // VCVT.S32/U32.F64 S(2m), Dm (clang-verified:
7523        // vcvt.s32.f64 s1,d2 = eefd 0bc2 ; vcvt.u32.f64 s1,d2 = eefc 0bc2)
7524        let (vm, m) = encode_dreg(dm_num);
7525        let (vd_s, d_s) = encode_sreg(2 * dm_num);
7526        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
7527        let vcvt = base | (d_s << 22) | (vd_s << 12) | (m << 5) | vm;
7528        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7529
7530        // VMOV Rd, S(2m)
7531        let rt = reg_to_bits(rd);
7532        let vmov = 0xEE100A10 | (vd_s << 16) | (rt << 12) | (d_s << 7);
7533        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7534
7535        Ok(bytes)
7536    }
7537
7538    /// Encode F64 rounding as a SINGLE Thumb-2 VRINT (FPv5 / cortex-m7dp).
7539    /// `mode` keeps the legacy FPSCR-RMode numbering of the callers —
7540    /// 0b00=nearest(ties-to-even)→VRINTN, 0b01=+inf(ceil)→VRINTP,
7541    /// 0b10=-inf(floor)→VRINTM, 0b11=zero(trunc)→VRINTZ — but the rounding
7542    /// mode is now ENCODED in the instruction, not smuggled through FPSCR.
7543    /// (The previous pseudo-op round-tripped through a 32-bit integer in S0:
7544    /// wrong for |x| >= 2^31, NaN/±inf collapsed to 0, -0.0 lost, and it
7545    /// CLOBBERED S0/R12 behind the allocator's back — the #615 class.)
7546    /// VRINT quietens an sNaN and preserves the sign of ±0.0/NaN per IEEE 754
7547    /// roundToIntegral, which is exactly WASM Core §4.3.3 f64.ceil/floor/
7548    /// trunc/nearest. VRINTN/P/M live in the FE "always-execute" space (never
7549    /// IT-conditional; none of these sequences emits them inside an IT block).
7550    fn encode_thumb_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7551        let dd_num = vfp_dreg_to_num(dd)?;
7552        let dm_num = vfp_dreg_to_num(dm)?;
7553        let (vd, d) = encode_dreg(dd_num);
7554        let (vm, m) = encode_dreg(dm_num);
7555        // clang-verified bases (thumbv7em, fpv5-d16):
7556        //   vrintn.f64 d1,d2 = feb9 1b42 ; vrintp = feba 1b42
7557        //   vrintm.f64 d1,d2 = febb 1b42 ; vrintz = eeb6 1bc2
7558        let base: u32 = match mode {
7559            0b00 => 0xFEB90B40, // VRINTN.F64 (round to nearest, ties to even)
7560            0b01 => 0xFEBA0B40, // VRINTP.F64 (round toward +inf)
7561            0b10 => 0xFEBB0B40, // VRINTM.F64 (round toward -inf)
7562            _ => 0xEEB60BC0,    // VRINTZ.F64 (round toward zero)
7563        };
7564        Ok(vfp_to_thumb_bytes(
7565            base | (d << 22) | (vd << 12) | (m << 5) | vm,
7566        ))
7567    }
7568
7569    /// Encode F64 min/max as Thumb-2 with WASM Core §4.3.3 semantics:
7570    ///
7571    ///   VCMP.F64 Dn, Dm ; VMRS APSR_nzcv, FPSCR
7572    ///   VMINNM.F64/VMAXNM.F64 Dd, Dn, Dm      (FPv5; -0.0 < +0.0 ordered)
7573    ///   IT VS ; VADD.F64(VS) Dd, Dn, Dm       (unordered ⇒ NaN-propagating)
7574    ///
7575    /// VMINNM/VMAXNM alone are IEEE minNum/maxNum, which return the NUMBER
7576    /// when exactly one operand is NaN — WASM requires NaN. The VS-guarded
7577    /// VADD overwrites the result with a quiet NaN whenever the compare was
7578    /// unordered (either operand NaN); on the ordered path VMINNM/VMAXNM
7579    /// order -0.0 below +0.0, matching WASM's min(+0,-0) = -0 / max = +0.
7580    /// Clobbers ONLY Dd and the flags (the previous pseudo-op's ordered IT
7581    /// GT/MI select returned the WRONG operand for NaN and ±0 mixes).
7582    ///
7583    /// Ok-or-Err: `dd` must not alias `dn`/`dm` — the VS fix-up reads them
7584    /// AFTER VMINNM wrote `dd` (the selector always allocates a fresh
7585    /// destination while both sources are still marked live).
7586    fn encode_thumb_f64_minmax(
7587        &self,
7588        dd: &VfpReg,
7589        dn: &VfpReg,
7590        dm: &VfpReg,
7591        is_min: bool,
7592    ) -> Result<Vec<u8>> {
7593        if dd == dn || dd == dm {
7594            return Err(synth_core::Error::synthesis(format!(
7595                "F64{}: destination {dd:?} aliases a source ({dn:?},{dm:?}) — \
7596                 the unordered NaN fix-up would read a clobbered operand \
7597                 (compiler bug: the selector must allocate a fresh D-temp)",
7598                if is_min { "Min" } else { "Max" },
7599            )));
7600        }
7601        let mut bytes = Vec::new();
7602        let dd_num = vfp_dreg_to_num(dd)?;
7603        let dn_num = vfp_dreg_to_num(dn)?;
7604        let dm_num = vfp_dreg_to_num(dm)?;
7605        let (vd, d) = encode_dreg(dd_num);
7606        let (vn, n) = encode_dreg(dn_num);
7607        let (vm, m) = encode_dreg(dm_num);
7608
7609        // VCMP.F64 Dn, Dm (clang-verified: vcmp.f64 d2,d3 = eeb4 2b43)
7610        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7611        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7612        // VMRS APSR_nzcv, FPSCR
7613        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7614        // VMINNM.F64 / VMAXNM.F64 Dd, Dn, Dm (clang-verified:
7615        // vminnm.f64 d1,d2,d3 = fe82 1b43 ; vmaxnm = fe82 1b03)
7616        let base: u32 = if is_min { 0xFE800B40 } else { 0xFE800B00 };
7617        let vnm = base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7618        bytes.extend_from_slice(&vfp_to_thumb_bytes(vnm));
7619        // IT VS (unordered ⇒ at least one NaN operand)
7620        bytes.extend_from_slice(&0xBF68_u16.to_le_bytes());
7621        // VADD.F64(VS) Dd, Dn, Dm — NaN + x propagates a quiet NaN
7622        let vadd = 0xEE300B00 | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7623        bytes.extend_from_slice(&vfp_to_thumb_bytes(vadd));
7624
7625        Ok(bytes)
7626    }
7627
7628    /// Encode F64 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7629    /// encoder scratch, #212), the flags, and Dd:
7630    ///
7631    ///   VMOV R12, S(2m+1)   (high word of the SIGN source Dm)
7632    ///   CMP  R12, #0        (N flag = the sign bit)
7633    ///   VABS.F64 Dd, Dn     (magnitude, sign cleared)
7634    ///   IT MI ; VNEG.F64(MI) Dd, Dd
7635    ///
7636    /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7637    /// The R12 capture happens BEFORE Dd is written, so Dd aliasing Dn or Dm
7638    /// is safe. (The previous pseudo-op clobbered R0/R1/R2 behind the
7639    /// allocator's back — the #615 class.)
7640    fn encode_thumb_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7641        let dm_num = vfp_dreg_to_num(dm)?;
7642        if dm_num > 7 {
7643            return Err(synth_core::Error::synthesis(format!(
7644                "F64Copysign: sign source {dm:?} has no S-register alias \
7645                 (D8..D15) — the selector allocates only D0..D7"
7646            )));
7647        }
7648        let mut bytes = Vec::new();
7649        // VMOV R12, S(2m+1) — the sign source's high word.
7650        let (vn_s, n_s) = encode_sreg(2 * dm_num + 1);
7651        let vmov = 0xEE100A10 | (vn_s << 16) | (12 << 12) | (n_s << 7);
7652        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7653        // CMP R12, #0 (T2: CMP.W R12, #0) — N = bit31 of the sign word.
7654        bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7655        bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7656        // VABS.F64 Dd, Dn
7657        let dd_num = vfp_dreg_to_num(dd)?;
7658        let dn_num = vfp_dreg_to_num(dn)?;
7659        let (vd, d) = encode_dreg(dd_num);
7660        let (vn, n) = encode_dreg(dn_num);
7661        let vabs = 0xEEB00BC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7662        bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7663        // IT MI ; VNEG.F64(MI) Dd, Dd
7664        bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7665        let vneg = 0xEEB10B40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7666        bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7667
7668        Ok(bytes)
7669    }
7670
7671    /// Encode VCVT.S32/U32.F32 + VMOV as Thumb-2
7672    fn encode_thumb_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7673        let mut bytes = Vec::new();
7674
7675        let sm_num = vfp_sreg_to_num(sm)?;
7676        let (vd, d) = encode_sreg(sm_num);
7677        let (vm, m) = encode_sreg(sm_num);
7678        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
7679        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7680        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7681
7682        // VMOV Rd, Sm
7683        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
7684        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7685
7686        Ok(bytes)
7687    }
7688
7689    // === Thumb-2 32-bit encoding helpers ===
7690
7691    /// Encode Thumb-2 32-bit ADD with immediate
7692    fn encode_thumb32_add(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7693        let rd_bits = reg_to_bits(rd);
7694        let rn_bits = reg_to_bits(rn);
7695
7696        // The `i:imm3:imm8` field is split the same way for both forms.
7697        let i_bit = (imm >> 11) & 1;
7698        let imm3 = (imm >> 8) & 0x7;
7699        let imm8 = imm & 0xFF;
7700
7701        let hw1_base = if imm <= 0xFF {
7702            // ADD.W (T3): the field is a ThumbExpandImm modified immediate. For
7703            // imm <= 0xFF (i:imm3 = 0000) it is the zero-extended byte, which is
7704            // correct — keep this form so existing encodings stay bit-identical.
7705            0xF100
7706        } else if imm <= 0xFFF {
7707            // ADDW (T4): a PLAIN 12-bit immediate (0..4095) — no ThumbExpandImm.
7708            // This is what makes `add sp, sp, #frame` correct for frame sizes
7709            // >= 256, which ADD.W (T3) would silently mis-encode (e.g. #256 -> #0).
7710            0xF200
7711        } else {
7712            return Err(synth_core::Error::synthesis(
7713                "ADD immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7714            ));
7715        };
7716
7717        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7718        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7719
7720        let mut bytes = hw1.to_le_bytes().to_vec();
7721        bytes.extend_from_slice(&hw2.to_le_bytes());
7722        Ok(bytes)
7723    }
7724
7725    /// Encode Thumb-2 32-bit SUB with immediate
7726    fn encode_thumb32_sub(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7727        let rd_bits = reg_to_bits(rd);
7728        let rn_bits = reg_to_bits(rn);
7729
7730        let i_bit = (imm >> 11) & 1;
7731        let imm3 = (imm >> 8) & 0x7;
7732        let imm8 = imm & 0xFF;
7733
7734        let hw1_base = if imm <= 0xFF {
7735            // SUB.W (T3) modified immediate — correct for the zero-extended byte
7736            // (imm <= 0xFF). Kept bit-identical for existing encodings.
7737            0xF1A0
7738        } else if imm <= 0xFFF {
7739            // SUBW (T4): plain 12-bit immediate (0..4095). Makes
7740            // `sub sp, sp, #frame` correct for frame sizes >= 256.
7741            0xF2A0
7742        } else {
7743            return Err(synth_core::Error::synthesis(
7744                "SUB immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7745            ));
7746        };
7747
7748        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7749        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7750
7751        let mut bytes = hw1.to_le_bytes().to_vec();
7752        bytes.extend_from_slice(&hw2.to_le_bytes());
7753        Ok(bytes)
7754    }
7755
7756    /// Encode Thumb-2 32-bit ADDS with immediate (sets flags)
7757    fn encode_thumb32_adds(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7758        let rd_bits = reg_to_bits(rd);
7759        let rn_bits = reg_to_bits(rn);
7760
7761        // ADDS.W (flag-setting) has only the modified-immediate form — error on
7762        // an un-encodable value rather than silently add the wrong constant.
7763        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7764            synth_core::Error::synthesis(
7765                "ADDS immediate is not a valid ThumbExpandImm — materialize into a register",
7766            )
7767        })?;
7768        let i_bit = (field >> 11) & 1;
7769        let imm3 = (field >> 8) & 0x7;
7770        let imm8 = field & 0xFF;
7771
7772        // ADDS.W Rd, Rn, #imm (with S=1)
7773        // First halfword: 1111 0 i 0 1000 1 Rn = F110 | i<<10 | Rn
7774        let hw1: u16 = (0xF110 | (i_bit << 10) | rn_bits) as u16;
7775        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7776
7777        let mut bytes = hw1.to_le_bytes().to_vec();
7778        bytes.extend_from_slice(&hw2.to_le_bytes());
7779        Ok(bytes)
7780    }
7781
7782    /// Encode Thumb-2 32-bit SUBS with immediate (sets flags)
7783    fn encode_thumb32_subs(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7784        let rd_bits = reg_to_bits(rd);
7785        let rn_bits = reg_to_bits(rn);
7786
7787        // SUBS.W (flag-setting) has only the modified-immediate form — error on
7788        // an un-encodable value rather than silently subtract the wrong constant.
7789        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7790            synth_core::Error::synthesis(
7791                "SUBS immediate is not a valid ThumbExpandImm — materialize into a register",
7792            )
7793        })?;
7794        let i_bit = (field >> 11) & 1;
7795        let imm3 = (field >> 8) & 0x7;
7796        let imm8 = field & 0xFF;
7797
7798        // SUBS.W Rd, Rn, #imm (with S=1)
7799        // First halfword: 1111 0 i 0 1101 1 Rn = F1B0 | i<<10 | Rn
7800        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7801        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7802
7803        let mut bytes = hw1.to_le_bytes().to_vec();
7804        bytes.extend_from_slice(&hw2.to_le_bytes());
7805        Ok(bytes)
7806    }
7807
7808    /// Encode Thumb-2 32-bit MOVW (16-bit immediate)
7809    ///
7810    /// # Contract (Verus-style)
7811    /// ```text
7812    /// requires rd <= R14
7813    /// ensures result.len() == 4
7814    /// ensures (imm & 0xFFFF) can be reconstructed from the encoding
7815    /// ```
7816    fn encode_thumb32_movw(&self, rd: &Reg, imm: u32) -> Result<Vec<u8>> {
7817        let rd_bits = reg_to_bits(rd);
7818        reg_bits_checked(rd_bits)?;
7819        let imm16 = imm & 0xFFFF;
7820
7821        // MOVW Rd, #imm16
7822        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7823        let imm4 = (imm16 >> 12) & 0xF;
7824        let i_bit = (imm16 >> 11) & 1;
7825        let imm3 = (imm16 >> 8) & 0x7;
7826        let imm8 = imm16 & 0xFF;
7827
7828        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7829        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7830
7831        let mut bytes = hw1.to_le_bytes().to_vec();
7832        bytes.extend_from_slice(&hw2.to_le_bytes());
7833        encoding_contracts::verify_thumb32(&bytes);
7834        Ok(bytes)
7835    }
7836
7837    /// Encode Thumb-2 32-bit shift with immediate
7838    ///
7839    /// # Contract (Verus-style)
7840    /// ```text
7841    /// requires rd <= R14, rm <= R14
7842    /// ensures result.len() == 4
7843    /// ```
7844    fn encode_thumb32_shift(
7845        &self,
7846        rd: &Reg,
7847        rm: &Reg,
7848        shift: u32,
7849        shift_type: u8,
7850    ) -> Result<Vec<u8>> {
7851        let rd_bits = reg_to_bits(rd);
7852        let rm_bits = reg_to_bits(rm);
7853        reg_bits_checked(rd_bits)?;
7854        reg_bits_checked(rm_bits)?;
7855        let imm5 = shift & 0x1F;
7856        let imm2 = imm5 & 0x3;
7857        let imm3 = (imm5 >> 2) & 0x7;
7858
7859        // MOV.W Rd, Rm, <shift> #imm
7860        // EA4F 0 imm3 Rd imm2 type Rm
7861        let hw1: u16 = 0xEA4F;
7862        let hw2: u16 =
7863            ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | ((shift_type as u32) << 4) | rm_bits)
7864                as u16;
7865
7866        let mut bytes = hw1.to_le_bytes().to_vec();
7867        bytes.extend_from_slice(&hw2.to_le_bytes());
7868        Ok(bytes)
7869    }
7870
7871    /// Encode Thumb-2 32-bit shift by register
7872    /// Encoding: 11111010 0xx0 Rn | 1111 Rd 0000 Rm
7873    /// shift_type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
7874    fn encode_thumb32_shift_reg(
7875        &self,
7876        rd: &Reg,
7877        rn: &Reg,
7878        rm: &Reg,
7879        shift_type: u8,
7880    ) -> Result<Vec<u8>> {
7881        let rd_bits = reg_to_bits(rd);
7882        let rn_bits = reg_to_bits(rn);
7883        let rm_bits = reg_to_bits(rm);
7884
7885        // hw1: 1111 1010 0xx0 Rn
7886        let hw1: u16 = (0xFA00 | ((shift_type as u32) << 5) | rn_bits) as u16;
7887        // hw2: 1111 Rd 0000 Rm
7888        let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
7889
7890        let mut bytes = hw1.to_le_bytes().to_vec();
7891        bytes.extend_from_slice(&hw2.to_le_bytes());
7892        Ok(bytes)
7893    }
7894
7895    /// Encode Thumb-2 32-bit CMP with immediate
7896    fn encode_thumb32_cmp_imm(&self, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7897        let rn_bits = reg_to_bits(rn);
7898
7899        // CMP.W has only the modified-immediate form (no plain-imm12 like ADDW),
7900        // so an un-encodable immediate MUST be materialized into a register by
7901        // the selector. Error rather than silently compare the wrong constant.
7902        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7903            synth_core::Error::synthesis(
7904                "CMP immediate is not a valid ThumbExpandImm — materialize into a register",
7905            )
7906        })?;
7907        let i_bit = (field >> 11) & 1;
7908        let imm3 = (field >> 8) & 0x7;
7909        let imm8 = field & 0xFF;
7910
7911        // CMP.W Rn, #imm
7912        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7913        let hw2: u16 = ((imm3 << 12) | 0x0F00 | imm8) as u16;
7914
7915        let mut bytes = hw1.to_le_bytes().to_vec();
7916        bytes.extend_from_slice(&hw2.to_le_bytes());
7917        Ok(bytes)
7918    }
7919
7920    /// #372/#382: resolve the base register AND residual immediate offset for an
7921    /// `I64Ldr`/`I64Str` whose address may carry an index register. Returns
7922    /// `(base, low_offset)`; the caller accesses the halves at `[base,
7923    /// #low_offset]` and `[base, #low_offset + 4]`.
7924    ///
7925    /// - Frame access (no `offset_reg`, e.g. a spilled local at `[SP, #off]`):
7926    ///   returns `(addr.base, off)` and emits NOTHING — byte-identical.
7927    /// - Memory access (`reg_imm(R11, addr, offset)` = `R11 + addr + offset`)
7928    ///   with `offset + 4 <= 0xFFF`: emits `ADD.W ip, base, index` and returns
7929    ///   `(ip, offset)`, folding `offset`/`offset+4` into the halves' imm12.
7930    ///   Byte-identical to the pre-#382 (#372) behavior.
7931    /// - Memory access with `offset + 4 > 0xFFF`: the imm12 form cannot hold the
7932    ///   high half's offset, so `encode_thumb32_ldr`'s `check_ldst_imm12` (#259)
7933    ///   rightly refused it and the WHOLE function was skipped (#382). Instead
7934    ///   MATERIALIZE the offset into the base: `ADD ip, index, #offset` (against
7935    ///   the read-only INDEX register, so `encode_thumb32_add_imm` never trips its
7936    ///   `rd==rn==R12` alias trap), then `ADD.W ip, ip, base` (+ R11), and return
7937    ///   `(ip, 0)` so the halves use `[ip, #0]` / `[ip, #4]`.
7938    ///
7939    /// The effective address is fully materialized into `ip` BEFORE the halves
7940    /// are accessed, so an `rdlo` aliasing the index register is safe.
7941    ///
7942    /// RQ-63-ARMI64OFF (#1165): the wasm memarg is unsigned and reaches the
7943    /// encoder through the selector's `as i32` cast, so a memarg `>= 2^31`
7944    /// arrives NEGATIVE. It is read back as `u32` (lossless) and the
7945    /// materialization carries the full 32 bits — `base + index + offset (mod
7946    /// 2^32)`, the arithmetic the Thumb-2 word path (`encode_thumb32_add_imm`)
7947    /// and the software bounds guard already use for the same memarg. This
7948    /// used to CLAMP a negative offset to 0, so `i64.load offset=0xfffffff8`
7949    /// silently read `[R11 + addr]` — a wrong address, and a divergence from
7950    /// the i32 form of the same memarg. A FRAME access (no index) with an
7951    /// out-of-range or negative offset is refused by `check_ldst_imm12` in the
7952    /// halves — loud, never clamped.
7953    fn i64_effective_base(&self, bytes: &mut Vec<u8>, addr: &MemAddr) -> Result<(Reg, u32)> {
7954        let offset = addr.offset as u32;
7955        match addr.offset_reg {
7956            Some(idx) => {
7957                let ip = Reg::R12;
7958                // The high half sits at +4, which must itself fit imm12.
7959                if offset > 0xFFB {
7960                    // Large static offset (#382): fold it (and R11) into ip so the
7961                    // imm12 halves stay in range instead of skipping the function.
7962                    // ADD ip, index, #offset  (index != ip → no add_imm alias trap)
7963                    bytes.extend_from_slice(&self.encode_thumb32_add_imm(&ip, &idx, offset)?);
7964                    // ADD.W ip, ip, base  (+ R11)
7965                    bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
7966                        reg_to_bits(&ip),
7967                        reg_to_bits(&ip),
7968                        reg_to_bits(&addr.base),
7969                    )?);
7970                    Ok((ip, 0))
7971                } else {
7972                    // ADD.W ip, addr.base, idx  (Thumb-2, byte-verified vs as)
7973                    let hw1: u16 = 0xEB00 | reg_to_bits(&addr.base) as u16;
7974                    let hw2: u16 = 0x0C00 | reg_to_bits(&idx) as u16;
7975                    bytes.extend_from_slice(&hw1.to_le_bytes());
7976                    bytes.extend_from_slice(&hw2.to_le_bytes());
7977                    Ok((ip, offset))
7978                }
7979            }
7980            None => Ok((addr.base, offset)),
7981        }
7982    }
7983
7984    /// Encode Thumb-2 32-bit LDR
7985    fn encode_thumb32_ldr(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7986        let rd_bits = reg_to_bits(rd);
7987        let base_bits = reg_to_bits(base);
7988
7989        // LDR.W Rd, [Rn, #imm12]
7990        check_ldst_imm12(offset)?;
7991        let hw1: u16 = (0xF8D0 | base_bits) as u16;
7992        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7993
7994        let mut bytes = hw1.to_le_bytes().to_vec();
7995        bytes.extend_from_slice(&hw2.to_le_bytes());
7996        Ok(bytes)
7997    }
7998
7999    /// Encode Thumb-2 32-bit STR
8000    fn encode_thumb32_str(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8001        let rd_bits = reg_to_bits(rd);
8002        let base_bits = reg_to_bits(base);
8003
8004        // STR.W Rd, [Rn, #imm12]
8005        check_ldst_imm12(offset)?;
8006        let hw1: u16 = (0xF8C0 | base_bits) as u16;
8007        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8008
8009        let mut bytes = hw1.to_le_bytes().to_vec();
8010        bytes.extend_from_slice(&hw2.to_le_bytes());
8011        Ok(bytes)
8012    }
8013
8014    /// Encode Thumb-2 32-bit LDR with register offset: LDR.W Rd, [Rn, Rm]
8015    fn encode_thumb32_ldr_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8016        let rd_bits = reg_to_bits(rd);
8017        let base_bits = reg_to_bits(base);
8018        let rm_bits = reg_to_bits(offset_reg);
8019
8020        // LDR.W Rd, [Rn, Rm, LSL #0]
8021        // Encoding: 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
8022        // imm2 = 00 for no shift (LSL #0)
8023        let hw1: u16 = (0xF850 | base_bits) as u16;
8024        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8025
8026        let mut bytes = hw1.to_le_bytes().to_vec();
8027        bytes.extend_from_slice(&hw2.to_le_bytes());
8028        Ok(bytes)
8029    }
8030
8031    /// Encode Thumb-2 32-bit STR with register offset: STR.W Rd, [Rn, Rm]
8032    fn encode_thumb32_str_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8033        let rd_bits = reg_to_bits(rd);
8034        let base_bits = reg_to_bits(base);
8035        let rm_bits = reg_to_bits(offset_reg);
8036
8037        // STR.W Rd, [Rn, Rm, LSL #0]
8038        // Encoding: 1111 1000 0100 Rn | Rt 0000 00 imm2 Rm
8039        // imm2 = 00 for no shift (LSL #0)
8040        let hw1: u16 = (0xF840 | base_bits) as u16;
8041        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8042
8043        let mut bytes = hw1.to_le_bytes().to_vec();
8044        bytes.extend_from_slice(&hw2.to_le_bytes());
8045        Ok(bytes)
8046    }
8047
8048    // === Sub-word load/store Thumb-2 encoding helpers ===
8049
8050    /// Encode Thumb-2 32-bit LDRB with immediate: LDRB.W Rd, [Rn, #imm12]
8051    fn encode_thumb32_ldrb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8052        let rd_bits = reg_to_bits(rd);
8053        let base_bits = reg_to_bits(base);
8054        // LDRB.W Rd, [Rn, #imm12]: 1111 1000 1001 Rn | Rt imm12
8055        check_ldst_imm12(offset)?;
8056        let hw1: u16 = (0xF890 | base_bits) as u16;
8057        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8058        let mut bytes = hw1.to_le_bytes().to_vec();
8059        bytes.extend_from_slice(&hw2.to_le_bytes());
8060        Ok(bytes)
8061    }
8062
8063    /// Encode Thumb-2 32-bit LDRB with register: LDRB.W Rd, [Rn, Rm]
8064    fn encode_thumb32_ldrb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8065        let rd_bits = reg_to_bits(rd);
8066        let base_bits = reg_to_bits(base);
8067        let rm_bits = reg_to_bits(offset_reg);
8068        // LDRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0001 Rn | Rt 0000 00 imm2 Rm
8069        let hw1: u16 = (0xF810 | base_bits) as u16;
8070        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8071        let mut bytes = hw1.to_le_bytes().to_vec();
8072        bytes.extend_from_slice(&hw2.to_le_bytes());
8073        Ok(bytes)
8074    }
8075
8076    /// Encode Thumb-2 32-bit LDRSB with immediate: LDRSB.W Rd, [Rn, #imm12]
8077    fn encode_thumb32_ldrsb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8078        let rd_bits = reg_to_bits(rd);
8079        let base_bits = reg_to_bits(base);
8080        // LDRSB.W Rd, [Rn, #imm12]: 1111 1001 1001 Rn | Rt imm12
8081        check_ldst_imm12(offset)?;
8082        let hw1: u16 = (0xF990 | base_bits) as u16;
8083        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8084        let mut bytes = hw1.to_le_bytes().to_vec();
8085        bytes.extend_from_slice(&hw2.to_le_bytes());
8086        Ok(bytes)
8087    }
8088
8089    /// Encode Thumb-2 32-bit LDRSB with register: LDRSB.W Rd, [Rn, Rm]
8090    fn encode_thumb32_ldrsb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8091        let rd_bits = reg_to_bits(rd);
8092        let base_bits = reg_to_bits(base);
8093        let rm_bits = reg_to_bits(offset_reg);
8094        // LDRSB.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0001 Rn | Rt 0000 00 imm2 Rm
8095        let hw1: u16 = (0xF910 | base_bits) as u16;
8096        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8097        let mut bytes = hw1.to_le_bytes().to_vec();
8098        bytes.extend_from_slice(&hw2.to_le_bytes());
8099        Ok(bytes)
8100    }
8101
8102    /// Encode Thumb-2 32-bit LDRH with immediate: LDRH.W Rd, [Rn, #imm12]
8103    fn encode_thumb32_ldrh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8104        let rd_bits = reg_to_bits(rd);
8105        let base_bits = reg_to_bits(base);
8106        // LDRH.W Rd, [Rn, #imm12]: 1111 1000 1011 Rn | Rt imm12
8107        check_ldst_imm12(offset)?;
8108        let hw1: u16 = (0xF8B0 | base_bits) as u16;
8109        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8110        let mut bytes = hw1.to_le_bytes().to_vec();
8111        bytes.extend_from_slice(&hw2.to_le_bytes());
8112        Ok(bytes)
8113    }
8114
8115    /// Encode Thumb-2 32-bit LDRH with register: LDRH.W Rd, [Rn, Rm]
8116    fn encode_thumb32_ldrh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8117        let rd_bits = reg_to_bits(rd);
8118        let base_bits = reg_to_bits(base);
8119        let rm_bits = reg_to_bits(offset_reg);
8120        // LDRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0011 Rn | Rt 0000 00 imm2 Rm
8121        let hw1: u16 = (0xF830 | base_bits) as u16;
8122        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8123        let mut bytes = hw1.to_le_bytes().to_vec();
8124        bytes.extend_from_slice(&hw2.to_le_bytes());
8125        Ok(bytes)
8126    }
8127
8128    /// Encode Thumb-2 32-bit LDRSH with immediate: LDRSH.W Rd, [Rn, #imm12]
8129    fn encode_thumb32_ldrsh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8130        let rd_bits = reg_to_bits(rd);
8131        let base_bits = reg_to_bits(base);
8132        // LDRSH.W Rd, [Rn, #imm12]: 1111 1001 1011 Rn | Rt imm12
8133        check_ldst_imm12(offset)?;
8134        let hw1: u16 = (0xF9B0 | base_bits) as u16;
8135        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8136        let mut bytes = hw1.to_le_bytes().to_vec();
8137        bytes.extend_from_slice(&hw2.to_le_bytes());
8138        Ok(bytes)
8139    }
8140
8141    /// Encode Thumb-2 32-bit LDRSH with register: LDRSH.W Rd, [Rn, Rm]
8142    fn encode_thumb32_ldrsh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8143        let rd_bits = reg_to_bits(rd);
8144        let base_bits = reg_to_bits(base);
8145        let rm_bits = reg_to_bits(offset_reg);
8146        // LDRSH.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0011 Rn | Rt 0000 00 imm2 Rm
8147        let hw1: u16 = (0xF930 | base_bits) as u16;
8148        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8149        let mut bytes = hw1.to_le_bytes().to_vec();
8150        bytes.extend_from_slice(&hw2.to_le_bytes());
8151        Ok(bytes)
8152    }
8153
8154    /// Encode Thumb-2 32-bit STRB with immediate: STRB.W Rd, [Rn, #imm12]
8155    fn encode_thumb32_strb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8156        let rd_bits = reg_to_bits(rd);
8157        let base_bits = reg_to_bits(base);
8158        // STRB.W Rd, [Rn, #imm12]: 1111 1000 1000 Rn | Rt imm12
8159        check_ldst_imm12(offset)?;
8160        let hw1: u16 = (0xF880 | base_bits) as u16;
8161        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8162        let mut bytes = hw1.to_le_bytes().to_vec();
8163        bytes.extend_from_slice(&hw2.to_le_bytes());
8164        Ok(bytes)
8165    }
8166
8167    /// Encode Thumb-2 32-bit STRB with register: STRB.W Rd, [Rn, Rm]
8168    fn encode_thumb32_strb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8169        let rd_bits = reg_to_bits(rd);
8170        let base_bits = reg_to_bits(base);
8171        let rm_bits = reg_to_bits(offset_reg);
8172        // STRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0000 Rn | Rt 0000 00 imm2 Rm
8173        let hw1: u16 = (0xF800 | base_bits) as u16;
8174        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8175        let mut bytes = hw1.to_le_bytes().to_vec();
8176        bytes.extend_from_slice(&hw2.to_le_bytes());
8177        Ok(bytes)
8178    }
8179
8180    /// Encode Thumb-2 32-bit STRH with immediate: STRH.W Rd, [Rn, #imm12]
8181    fn encode_thumb32_strh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8182        let rd_bits = reg_to_bits(rd);
8183        let base_bits = reg_to_bits(base);
8184        // STRH.W Rd, [Rn, #imm12]: 1111 1000 1010 Rn | Rt imm12
8185        check_ldst_imm12(offset)?;
8186        let hw1: u16 = (0xF8A0 | base_bits) as u16;
8187        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8188        let mut bytes = hw1.to_le_bytes().to_vec();
8189        bytes.extend_from_slice(&hw2.to_le_bytes());
8190        Ok(bytes)
8191    }
8192
8193    /// Encode Thumb-2 32-bit STRH with register: STRH.W Rd, [Rn, Rm]
8194    fn encode_thumb32_strh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8195        let rd_bits = reg_to_bits(rd);
8196        let base_bits = reg_to_bits(base);
8197        let rm_bits = reg_to_bits(offset_reg);
8198        // STRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0010 Rn | Rt 0000 00 imm2 Rm
8199        let hw1: u16 = (0xF820 | base_bits) as u16;
8200        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8201        let mut bytes = hw1.to_le_bytes().to_vec();
8202        bytes.extend_from_slice(&hw2.to_le_bytes());
8203        Ok(bytes)
8204    }
8205
8206    /// Encode Thumb-2 32-bit ADD with immediate: ADD.W Rd, Rn, #imm
8207    fn encode_thumb32_add_imm(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
8208        let rd_bits = reg_to_bits(rd);
8209        let rn_bits = reg_to_bits(rn);
8210
8211        // In-range immediates (<= 0xFFF) delegate to `encode_thumb32_add`,
8212        // which picks the correct form per value:
8213        //   - imm <= 0xFF  -> ADD.W (T3). Its `i:imm3:imm8` field is a
8214        //     ThumbExpandImm MODIFIED immediate — raw == expanded only here.
8215        //   - 0x100..=0xFFF -> ADDW (T4, 0xF200): a PLAIN 12-bit immediate.
8216        //
8217        // #681: this function used to pack the raw value into the T3 field for
8218        // ALL imm <= 0xFFF. ThumbExpandImm(0x200) = 0 and ThumbExpandImm(0x400)
8219        // = 0x8000_0000, so every dynamic-address load/store with a static
8220        // offset in 0x100..=0xFFF silently computed a WRONG address — and in
8221        // --safety-bounds software the guard checked the intended address while
8222        // the access used the mis-encoded one (bounds bypass). Same
8223        // ThumbExpandImm raw-packing class as #253/#255, reached via #382.
8224        if imm <= 0xFFF {
8225            self.encode_thumb32_add(rd, rn, imm)
8226        } else {
8227            // Out-of-range immediate (> 0xFFF): materialize it into a scratch
8228            // register, then ADD.W Rd, Rn, scratch. This is the #180/#185
8229            // "encoder must produce a legal sequence, not assert" class — see #350.
8230            //
8231            // Scratch choice (must NEVER equal Rn, or Rn would be clobbered before
8232            // the ADD reads it):
8233            //   - rd != rn  => use rd itself (rn is untouched, since rd != rn).
8234            //   - rd == rn  => use R12/IP (the reserved encoder scratch). rd/rn are
8235            //                  never R12 (R12 is non-allocatable), so it can't alias.
8236            //
8237            // The materialized value is the same whether or not MOVT is emitted, so
8238            // the byte length depends only on `imm` (and rd==rn) — the size probe and
8239            // the final emit therefore agree (mandatory: the function is encoded twice).
8240            let scratch: u32 = if rd_bits == rn_bits {
8241                12 // R12/IP — in-place add, can't use rd because rd == rn
8242            } else {
8243                rd_bits // rn is preserved because rd != rn
8244            };
8245            // Invariant: the scratch must never alias Rn (would clobber it before
8246            // the ADD reads it). Unreachable in real codegen (rd/rn are never R12,
8247            // which is reserved encoder scratch), but the encoder is also driven by
8248            // the `encoder_no_panic` fuzz harness with ARBITRARY registers — incl.
8249            // rd==rn==R12, which makes scratch (R12) alias Rn. The encoder contract
8250            // (#180/#185) is Ok-or-Err, never a panic, so return a typed error
8251            // instead of asserting. #350 follow-up.
8252            if scratch == rn_bits {
8253                return Err(synth_core::Error::synthesis(format!(
8254                    "ADD #imm: cannot lower #{imm:#x} for Rd==Rn==R12 — no free scratch \
8255                     register (R12 is the reserved encoder scratch and aliases Rn here)"
8256                )));
8257            }
8258
8259            let lo16 = imm & 0xFFFF;
8260            let hi16 = (imm >> 16) & 0xFFFF;
8261
8262            let mut bytes = self.encode_thumb32_movw_raw(scratch, lo16)?;
8263            if hi16 != 0 {
8264                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(scratch, hi16)?);
8265            }
8266            bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rn_bits, scratch)?);
8267            Ok(bytes)
8268        }
8269    }
8270
8271    // === Raw encoding helpers for POPCNT (take register numbers directly) ===
8272
8273    /// Encode Thumb-2 32-bit MOVW (16-bit immediate) - raw version
8274    ///
8275    /// # Contract (Verus-style)
8276    /// ```text
8277    /// requires rd <= 14, imm16 <= 0xFFFF
8278    /// ensures result.len() == 4
8279    /// ```
8280    fn encode_thumb32_movw_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8281        reg_bits_checked(rd)?;
8282        encoding_contracts::verify_imm16(imm16);
8283        // MOVW Rd, #imm16
8284        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
8285        let imm16 = imm16 & 0xFFFF;
8286        let imm4 = (imm16 >> 12) & 0xF;
8287        let i_bit = (imm16 >> 11) & 1;
8288        let imm3 = (imm16 >> 8) & 0x7;
8289        let imm8 = imm16 & 0xFF;
8290
8291        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
8292        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8293
8294        let mut bytes = hw1.to_le_bytes().to_vec();
8295        bytes.extend_from_slice(&hw2.to_le_bytes());
8296        encoding_contracts::verify_thumb32(&bytes);
8297        Ok(bytes)
8298    }
8299
8300    /// Encode Thumb-2 32-bit MOVT (move top 16 bits) - raw version
8301    ///
8302    /// # Contract (Verus-style)
8303    /// ```text
8304    /// requires rd <= 14, imm16 <= 0xFFFF
8305    /// ensures result.len() == 4
8306    /// ```
8307    fn encode_thumb32_movt_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8308        reg_bits_checked(rd)?;
8309        encoding_contracts::verify_imm16(imm16);
8310        // MOVT Rd, #imm16
8311        // 1111 0 i 10 1 1 0 0 imm4 | 0 imm3 Rd imm8
8312        let imm16 = imm16 & 0xFFFF;
8313        let imm4 = (imm16 >> 12) & 0xF;
8314        let i_bit = (imm16 >> 11) & 1;
8315        let imm3 = (imm16 >> 8) & 0x7;
8316        let imm8 = imm16 & 0xFF;
8317
8318        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
8319        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8320
8321        let mut bytes = hw1.to_le_bytes().to_vec();
8322        bytes.extend_from_slice(&hw2.to_le_bytes());
8323        encoding_contracts::verify_thumb32(&bytes);
8324        Ok(bytes)
8325    }
8326
8327    /// Encode Thumb-2 32-bit LSR (logical shift right) with immediate - raw version
8328    fn encode_thumb32_lsr_raw(&self, rd: u32, rm: u32, shift: u32) -> Result<Vec<u8>> {
8329        // MOV.W Rd, Rm, LSR #imm
8330        // EA4F 0 imm3 Rd imm2 01 Rm
8331        let imm5 = shift & 0x1F;
8332        let imm2 = imm5 & 0x3;
8333        let imm3 = (imm5 >> 2) & 0x7;
8334
8335        let hw1: u16 = 0xEA4F;
8336        let hw2: u16 = ((imm3 << 12) | (rd << 8) | (imm2 << 6) | (0b01 << 4) | rm) as u16;
8337
8338        let mut bytes = hw1.to_le_bytes().to_vec();
8339        bytes.extend_from_slice(&hw2.to_le_bytes());
8340        Ok(bytes)
8341    }
8342
8343    /// Encode Thumb-2 32-bit AND with immediate - raw version
8344    fn encode_thumb32_and_imm_raw(&self, rd: u32, rn: u32, imm: u32) -> Result<Vec<u8>> {
8345        // AND.W Rd, Rn, #<modified_immediate>
8346        // F0 00 Rn | 0 imm3 Rd imm8
8347        //
8348        // #681 class audit: the field is a ThumbExpandImm modified immediate,
8349        // not a raw value. The only current caller (POPCNT final mask) passes
8350        // 0x3F, which expands to itself — the gate is byte-identical today and
8351        // closes the raw-packing landmine for any future caller.
8352        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
8353            synth_core::Error::synthesis(
8354                "AND immediate is not a valid ThumbExpandImm — materialize into a register",
8355            )
8356        })?;
8357        let i_bit = (field >> 11) & 1;
8358        let imm3 = (field >> 8) & 0x7;
8359        let imm8 = field & 0xFF;
8360
8361        let hw1: u16 = (0xF000 | (i_bit << 10) | rn) as u16;
8362        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8363
8364        let mut bytes = hw1.to_le_bytes().to_vec();
8365        bytes.extend_from_slice(&hw2.to_le_bytes());
8366        Ok(bytes)
8367    }
8368
8369    /// Encode Thumb-2 32-bit SUB (register) - raw version
8370    fn encode_thumb32_sub_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8371        // SUB.W Rd, Rn, Rm
8372        // EBA0 Rn | 0 Rd 00 00 Rm
8373        let hw1: u16 = (0xEBA0 | rn) as u16;
8374        let hw2: u16 = ((rd << 8) | rm) as u16;
8375
8376        let mut bytes = hw1.to_le_bytes().to_vec();
8377        bytes.extend_from_slice(&hw2.to_le_bytes());
8378        Ok(bytes)
8379    }
8380
8381    /// Encode Thumb-2 32-bit ADD (register) - raw version
8382    fn encode_thumb32_add_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8383        // ADD.W Rd, Rn, Rm
8384        // EB00 Rn | 0 Rd 00 00 Rm
8385        let hw1: u16 = (0xEB00 | rn) as u16;
8386        let hw2: u16 = ((rd << 8) | rm) as u16;
8387
8388        let mut bytes = hw1.to_le_bytes().to_vec();
8389        bytes.extend_from_slice(&hw2.to_le_bytes());
8390        Ok(bytes)
8391    }
8392
8393    /// Encode Thumb-2 32-bit ADDS (register, flag-setting) - raw version.
8394    /// Used as the high-register fallback for `ArmOp::Adds` (i64 low-word add)
8395    /// so R8-R11 pair operands don't overflow the 16-bit field — #178/#180.
8396    fn encode_thumb32_adds_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8397        // ADDS.W Rd, Rn, Rm (T3, S=1): EB10 Rn | 0 Rd 00 00 Rm
8398        let hw1: u16 = (0xEB10 | rn) as u16;
8399        let hw2: u16 = ((rd << 8) | rm) as u16;
8400        let mut bytes = hw1.to_le_bytes().to_vec();
8401        bytes.extend_from_slice(&hw2.to_le_bytes());
8402        Ok(bytes)
8403    }
8404
8405    /// Encode Thumb-2 32-bit SUBS (register, flag-setting) - raw version.
8406    /// High-register fallback for `ArmOp::Subs` (i64 low-word subtract) — #178/#180.
8407    fn encode_thumb32_subs_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8408        // SUBS.W Rd, Rn, Rm (T3, S=1): EBB0 Rn | 0 Rd 00 00 Rm
8409        let hw1: u16 = (0xEBB0 | rn) as u16;
8410        let hw2: u16 = ((rd << 8) | rm) as u16;
8411        let mut bytes = hw1.to_le_bytes().to_vec();
8412        bytes.extend_from_slice(&hw2.to_le_bytes());
8413        Ok(bytes)
8414    }
8415
8416    /// Encode a sequence of ARM instructions
8417    pub fn encode_sequence(&self, ops: &[ArmOp]) -> Result<Vec<u8>> {
8418        let mut code = Vec::new();
8419
8420        for op in ops {
8421            let encoded = self.encode(op)?;
8422            code.extend_from_slice(&encoded);
8423        }
8424
8425        Ok(code)
8426    }
8427}
8428
8429/// Convert register to bit encoding (0-15)
8430/// Reverse of the ARMv7-M `ThumbExpandImm`: given a 32-bit immediate, return the
8431/// 12-bit `i:imm3:imm8` field if it is a representable modified immediate, else
8432/// `None` (the caller must materialize the value into a register). This is the
8433/// shared correct path for the data-processing immediate encoders — without it
8434/// they pack raw bits and silently mis-encode any value `> 0xFF` that isn't a
8435/// modified immediate (the silent-miscompile class behind #251/#253/#255).
8436fn try_thumb_expand_imm(value: u32) -> Option<u32> {
8437    // i:imm3 = 0000 → 8-bit value, zero-extended (00000000 00000000 00000000 XY).
8438    if value <= 0xFF {
8439        return Some(value);
8440    }
8441    let b0 = value & 0xFF; // byte 0
8442    let b1 = (value >> 8) & 0xFF; // byte 1
8443    // 0x00XY00XY (i:imm3 = 0001) — XY in bytes 0 and 2
8444    if value == (b0 << 16) | b0 {
8445        return Some(0x100 | b0);
8446    }
8447    // 0xXY00XY00 (i:imm3 = 0010) — XY in bytes 1 and 3
8448    if value == (b1 << 24) | (b1 << 8) {
8449        return Some(0x200 | b1);
8450    }
8451    // 0xXYXYXYXY (i:imm3 = 0011) — XY in all four bytes
8452    if value == (b0 << 24) | (b0 << 16) | (b0 << 8) | b0 {
8453        return Some(0x300 | b0);
8454    }
8455    // An 8-bit value with bit 7 set, rotated right by 8..=31. `rotate_left(rot)`
8456    // undoes the encoded right rotation; if the result is `1bbbbbbb` (0x80..=0xFF)
8457    // the value is representable. imm12[11:7] = rot, imm12[6:0] = low 7 bits.
8458    for rot in 8..=31u32 {
8459        let unrot = value.rotate_left(rot);
8460        if (0x80..=0xFF).contains(&unrot) {
8461            return Some((rot << 7) | (unrot & 0x7F));
8462        }
8463    }
8464    None
8465}
8466
8467/// Guard a Thumb-2 `LDR/STR Rd, [Rn, #imm12]` offset. The imm12 form supports
8468/// `0..=4095`; a larger offset must be materialized into a register by the
8469/// selector (register-offset addressing). Returning `Err` rather than silently
8470/// masking `offset & 0xFFF` closes the wrong-address miscompile class (#259,
8471/// the load/store sibling of #253/#255).
8472fn check_ldst_imm12(offset: u32) -> Result<()> {
8473    if offset > 0xFFF {
8474        Err(synth_core::Error::synthesis(
8475            "load/store immediate offset > 0xFFF (4095) — materialize the offset into a register",
8476        ))
8477    } else {
8478        Ok(())
8479    }
8480}
8481
8482/// #916 — emit `Rd = 0` in Thumb-2, correctly for EVERY destination register.
8483///
8484/// The 16-bit `MOVS Rd, #imm8` (T1) is `0010 0 Rd(3) imm8` — the Rd field is
8485/// **three bits**. For R8-R12 `reg_to_bits` yields 8..12, so `rd_bits << 8`
8486/// overflows into bit 11 and `0x2000 | 0x0800` is `0x2800` = `CMP r0, #0`:
8487/// not a move at all. The destination is never written (it keeps stale data)
8488/// and the flags are clobbered. Same class as #180 / H-CODE-9, and the same
8489/// defect #311 fixed for `I64SetCond`.
8490///
8491/// High registers therefore take the 32-bit `MOV.W Rd, #imm8` (T2,
8492/// `F04F 0000 | Rd<<8 | imm8`), whose Rd field is four bits. `MOV.W` with S=0
8493/// does not set flags, which is what these zero-fill sites want anyway.
8494///
8495/// **Callers with branches must consult [`thumb_zero_fill_halfwords`].** This
8496/// emits 1 halfword for R0-R7 and 2 for R8-R12; any branch whose target lies
8497/// PAST this instruction moves when it widens and its displacement has to be
8498/// derived rather than hard-coded. (A branch targeting this instruction's own
8499/// address is unaffected — an instruction cannot move itself.)
8500fn emit_thumb_zero_fill(bytes: &mut Vec<u8>, rd_bits: u32) {
8501    if rd_bits < 8 {
8502        let movs: u16 = 0x2000 | ((rd_bits as u16) << 8);
8503        bytes.extend_from_slice(&movs.to_le_bytes());
8504    } else {
8505        bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
8506        bytes.extend_from_slice(&((rd_bits as u16) << 8).to_le_bytes());
8507    }
8508}
8509
8510/// Halfword length of the encoding [`emit_thumb_zero_fill`] picks for
8511/// `rd_bits`. Branch displacements spanning the zero-fill derive from this so
8512/// the encoder cannot drift from itself (#916; the byte-size estimator mirrors
8513/// it in `synth_synthesis::estimate_arm_byte_size`, pinned by the #498
8514/// `estimator_encoder_agreement` oracle).
8515fn thumb_zero_fill_halfwords(rd_bits: u32) -> u16 {
8516    if rd_bits < 8 { 1 } else { 2 }
8517}
8518
8519fn reg_to_bits(reg: &Reg) -> u32 {
8520    match reg {
8521        Reg::R0 => 0,
8522        Reg::R1 => 1,
8523        Reg::R2 => 2,
8524        Reg::R3 => 3,
8525        Reg::R4 => 4,
8526        Reg::R5 => 5,
8527        Reg::R6 => 6,
8528        Reg::R7 => 7,
8529        Reg::R8 => 8,
8530        Reg::R9 => 9,
8531        Reg::R10 => 10,
8532        Reg::R11 => 11,
8533        Reg::R12 => 12,
8534        Reg::SP => 13,
8535        Reg::LR => 14,
8536        Reg::PC => 15,
8537    }
8538}
8539
8540// ======================================================================
8541// #610 — i64 fixed-ABI expansion wrappers.
8542//
8543// The hand-written multi-instruction i64 cores (rotl/rotr and the div/rem
8544// shift-subtract loops) compute in FIXED low registers. Before #610 the
8545// div/rem arms ignored their operand fields outright (hardcoded R0:R1 /
8546// R2:R3 in, result to R0:R1) and the rot arms used R3/R4 scratch that
8547// collided with selector-assigned registers — then restored the saved
8548// scratch OVER the result (`POP {R4}` with rd_lo == R4), so the op
8549// returned the caller's stale register: 0 for every input under qemu.
8550//
8551// These wrappers make each core honor its register parameters:
8552//   1. save R0-R3,
8553//   2. marshal the operand registers into the core's fixed input regs via
8554//      the stack (permutation-safe: every source is read before any fixed
8555//      register is written),
8556//   3. run the fixed-reg core (self-preserving for R4+; R12 is encoder
8557//      scratch and never allocatable, #212),
8558//   4. MOV the result pair from R0:R1 into the selector's rd pair,
8559//   5. restore R0-R3, skipping any register the result now occupies.
8560//
8561// All emitted lengths are register-independent so the optimized path's
8562// byte-size estimator (`estimate_arm_byte_size`, pinned by the
8563// estimator↔encoder agreement oracle #498/#511) stays a constant per op.
8564// ======================================================================
8565
8566/// Steps 1+2: `PUSH {R0-R3}`, then marshal `srcs` (operand registers, any of
8567/// R0-R12) into `R0..R<n>` via individual stack pushes. Sources are all read
8568/// before any destination register is written, so arbitrary source/target
8569/// permutations (including operands living in R0-R3) are safe.
8570fn emit_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8571    debug_assert!(srcs.len() <= 4);
8572    // PUSH {R0-R3} — save the caller-visible low registers.
8573    bytes.extend_from_slice(&0xB40Fu16.to_le_bytes());
8574    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8575    for src in srcs.iter().rev() {
8576        let rt = reg_to_bits(src) as u16;
8577        bytes.extend_from_slice(&0xF84Du16.to_le_bytes());
8578        bytes.extend_from_slice(&((rt << 12) | 0x0D04).to_le_bytes());
8579    }
8580    // POP {Ri} — Ri := srcs[i].
8581    for i in 0..srcs.len() as u16 {
8582        bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes());
8583    }
8584}
8585
8586/// Steps 4+5: move the core's R0:R1 result into the selector's rd pair, then
8587/// restore the R0-R3 saved by [`emit_i64_fixed_abi_entry`], skipping any
8588/// register the result now lives in (its saved caller word is discarded).
8589fn emit_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8590    let lo = reg_to_bits(rdlo);
8591    let hi = reg_to_bits(rdhi);
8592    if lo == 1 && hi == 0 {
8593        // A fully swapped pair would clobber one half in either MOV order.
8594        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8595        return Err(synth_core::Error::synthesis(
8596            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8597        ));
8598    }
8599    let mov16 = |bytes: &mut Vec<u8>, rd: u32, rm: u32| {
8600        let d = ((rd >> 3) & 1) as u16;
8601        bytes.extend_from_slice(
8602            &(0x4600u16 | (d << 7) | ((rm as u16) << 3) | ((rd & 7) as u16)).to_le_bytes(),
8603        );
8604    };
8605    if hi == 0 {
8606        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8607        mov16(bytes, lo, 0);
8608        mov16(bytes, hi, 1);
8609    } else {
8610        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8611        mov16(bytes, hi, 1);
8612        mov16(bytes, lo, 0);
8613    }
8614    for i in 0..4u32 {
8615        if i == lo || i == hi {
8616            // The result lives here — drop the saved caller word.
8617            bytes.extend_from_slice(&0xB001u16.to_le_bytes()); // ADD SP, #4
8618        } else {
8619            bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes()); // POP {Ri}
8620        }
8621    }
8622    Ok(())
8623}
8624
8625/// WASM `i64.div_*` / `i64.rem_*` by zero must trap, matching the i32 path's
8626/// cmp/bne/udf guard. Emitted after marshaling, when the divisor pair is in
8627/// R2:R3: `ORRS R12, R2, R3` — `BNE` over a `UDF #0` when nonzero.
8628fn emit_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8629    bytes.extend_from_slice(&0xEA52u16.to_le_bytes()); // ORRS.W R12, R2, R3
8630    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8631    bytes.extend_from_slice(&0xD100u16.to_le_bytes()); // BNE.N +0 (skip the UDF)
8632    bytes.extend_from_slice(&0xDE00u16.to_le_bytes()); // UDF #0 — divide by zero
8633}
8634
8635/// WASM `i64.div_s(INT64_MIN, -1)` must trap (Core §4.3.2 `idiv_s`: the
8636/// quotient +2^63 is unrepresentable), matching the i32 path's overflow
8637/// guard — #633: without it the core negated INT64_MIN onto itself and
8638/// silently returned INT64_MIN. Emitted after marshaling, when the dividend
8639/// pair is in R0:R1 and the divisor pair in R2:R3; R12 is encoder scratch.
8640///
8641/// div_s ONLY — `i64.rem_s(INT64_MIN, -1)` is defined as 0 and must NOT
8642/// trap (`irem_s`), so the I64RemS arm never calls this. 22 bytes,
8643/// register-independent (estimator contract, #498/#511).
8644fn emit_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8645    // AND.W R12, R2, R3 — R12 == 0xFFFFFFFF iff divisor == -1
8646    bytes.extend_from_slice(&0xEA02u16.to_le_bytes());
8647    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8648    // CMN.W R12, #1 — EQ iff both divisor words are all-ones
8649    bytes.extend_from_slice(&0xF11Cu16.to_le_bytes());
8650    bytes.extend_from_slice(&0x0F01u16.to_le_bytes());
8651    // BNE .no_trap
8652    bytes.extend_from_slice(&0xD105u16.to_le_bytes());
8653    // CMP R0, #0 — dividend lo word of INT64_MIN
8654    bytes.extend_from_slice(&0x2800u16.to_le_bytes());
8655    // BNE .no_trap
8656    bytes.extend_from_slice(&0xD103u16.to_le_bytes());
8657    // CMP.W R1, #0x80000000 — dividend hi word of INT64_MIN
8658    bytes.extend_from_slice(&0xF1B1u16.to_le_bytes());
8659    bytes.extend_from_slice(&0x4F00u16.to_le_bytes());
8660    // BNE .no_trap
8661    bytes.extend_from_slice(&0xD100u16.to_le_bytes());
8662    // UDF #0 — signed-division overflow
8663    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
8664    // .no_trap:
8665}
8666
8667// ======================================================================
8668// #615 — A32 (ARM-mode) twins of the #610 i64 fixed-ABI wrappers above.
8669// Identical register contract, A32 encodings: the multi-instruction i64
8670// cores (rotl/rotr, div/rem) compute in fixed low registers (value/dividend
8671// R0:R1, amount R2 / divisor R2:R3, result to R0:R1); the wrappers marshal
8672// the selector-assigned operand registers in and the result out, saving and
8673// restoring the caller-visible R0-R3 around the core.
8674// ======================================================================
8675
8676/// A32 steps 1+2: `STMDB SP!, {R0-R3}`, then marshal `srcs` into `R0..R<n>`
8677/// via individual stack pushes (`STR src, [SP, #-4]!` in reverse order, then
8678/// `LDR Ri, [SP], #4`). Every source is read before any fixed register is
8679/// written, so arbitrary source/target permutations are safe.
8680fn emit_a32_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8681    debug_assert!(srcs.len() <= 4);
8682    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8683    // PUSH {R0-R3} — save the caller-visible low registers.
8684    w(bytes, 0xE92D_000F);
8685    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8686    for src in srcs.iter().rev() {
8687        w(bytes, 0xE52D_0004 | (reg_to_bits(src) << 12));
8688    }
8689    // LDR Ri, [SP], #4 — Ri := srcs[i].
8690    for i in 0..srcs.len() as u32 {
8691        w(bytes, 0xE49D_0004 | (i << 12));
8692    }
8693}
8694
8695/// A32 steps 4+5: move the core's R0:R1 result into the selector's rd pair,
8696/// then restore the R0-R3 saved by [`emit_a32_i64_fixed_abi_entry`], skipping
8697/// any register the result now lives in (its saved caller word is discarded).
8698fn emit_a32_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8699    let lo = reg_to_bits(rdlo);
8700    let hi = reg_to_bits(rdhi);
8701    if lo == 1 && hi == 0 {
8702        // A fully swapped pair would clobber one half in either MOV order.
8703        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8704        return Err(synth_core::Error::synthesis(
8705            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8706        ));
8707    }
8708    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8709    let mov = |bytes: &mut Vec<u8>, rd: u32, rm: u32| w(bytes, 0xE1A0_0000 | (rd << 12) | rm);
8710    if hi == 0 {
8711        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8712        mov(bytes, lo, 0);
8713        mov(bytes, hi, 1);
8714    } else {
8715        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8716        mov(bytes, hi, 1);
8717        mov(bytes, lo, 0);
8718    }
8719    for i in 0..4u32 {
8720        if i == lo || i == hi {
8721            // The result lives here — drop the saved caller word.
8722            w(bytes, 0xE28D_D004); // ADD SP, SP, #4
8723        } else {
8724            w(bytes, 0xE49D_0004 | (i << 12)); // LDR Ri, [SP], #4
8725        }
8726    }
8727    Ok(())
8728}
8729
8730/// A32 zero-divisor trap, emitted after marshaling when the divisor pair is
8731/// in R2:R3: `ORRS R12, R2, R3` sets Z iff the divisor is zero; `BNE` skips a
8732/// `UDF #0` (WASM div/rem-by-zero must trap, matching the Thumb-2 twin).
8733fn emit_a32_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8734    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8735    w(bytes, 0xE192_C003); // ORRS R12, R2, R3
8736    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8737    w(bytes, 0xE7F0_00F0); // UDF #0 — divide by zero
8738}
8739
8740/// A32 twin of [`emit_i64_divs_overflow_trap`] (#633): trap on
8741/// `i64.div_s(INT64_MIN, -1)`. Conditional execution replaces the Thumb
8742/// branches — the CMPEQ chain leaves EQ set only when divisor == -1 AND
8743/// dividend == INT64_MIN. div_s only; rem_s must keep returning 0.
8744fn emit_a32_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8745    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8746    w(bytes, 0xE002_C003); // AND   R12, R2, R3 (== 0xFFFFFFFF iff divisor == -1)
8747    w(bytes, 0xE37C_0001); // CMN   R12, #1     (EQ iff divisor == -1)
8748    w(bytes, 0x0350_0000); // CMPEQ R0, #0      (EQ iff also dividend lo == 0)
8749    w(bytes, 0x0351_0102); // CMPEQ R1, #0x80000000 (EQ iff dividend == INT64_MIN)
8750    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8751    w(bytes, 0xE7F0_00F0); // UDF #0 — signed-division overflow
8752}
8753
8754/// Fallible form of the `verify_reg_bits` contract. PC (R15) is not a valid
8755/// data operand for the Thumb-2 encodings that use this guard (SDIV/UDIV/MLS/…
8756/// are UNPREDICTABLE with PC). Synth's own codegen never emits PC there, but
8757/// the encoder must stay *total* over arbitrary `ArmOp` inputs — the fuzz
8758/// harness (`encoder_no_panic`) requires Ok-or-Err, never a panic. Pre-fix, the
8759/// `debug_assert` in `verify_reg_bits` aborted under `-Cdebug-assertions`.
8760/// Returns a typed Err instead. See #185.
8761fn reg_bits_checked(bits: u32) -> Result<()> {
8762    if bits > 14 {
8763        return Err(synth_core::Error::synthesis(format!(
8764            "register bits {bits} (PC/R15) is not a valid operand for this Thumb-2 encoding"
8765        )));
8766    }
8767    Ok(())
8768}
8769
8770/// Try to encode a 32-bit value as an ARM rotated immediate (imm8 ROR 2*rot4).
8771/// Returns Some((encoded_bits, 1)) if representable, None otherwise.
8772fn try_encode_rotated_imm(val: u32) -> Option<(u32, u32)> {
8773    if val == 0 {
8774        return Some((0, 1));
8775    }
8776    for rot in 0..16u32 {
8777        let shift = rot * 2;
8778        // Rotate left by shift (undo the ROR) to see if result fits in 8 bits
8779        let unrotated = val.rotate_left(shift);
8780        if unrotated <= 0xFF {
8781            // Encoded as: rot4(4 bits) | imm8(8 bits) = rotate_imm << 8 | imm8
8782            return Some(((rot << 8) | unrotated, 1));
8783        }
8784    }
8785    None
8786}
8787
8788/// Encode operand2 field and return (bits, immediate_flag).
8789/// For ARM32 mode, immediates use the rotated-immediate encoding (imm8 ROR 2*rot4).
8790/// Panics if an immediate value cannot be represented. Callers that need large
8791/// immediates should use MOVW/MOVT instead of Operand2::Imm.
8792fn encode_operand2(op2: &Operand2) -> Result<(u32, u32)> {
8793    match op2 {
8794        Operand2::Imm(val) => {
8795            let uval = *val as u32;
8796            // Attempt rotated-immediate encoding (ARM32 Operand2)
8797            if let Some(encoded) = try_encode_rotated_imm(uval) {
8798                Ok(encoded)
8799            } else {
8800                // #378-class honesty: an immediate that can't be expressed as an
8801                // ARM32 rotated immediate is an INTERNAL selector bug — large
8802                // constants must be materialized via MOVW/MOVT, not passed here.
8803                // FAIL HONESTLY with an Err rather than silently masking to
8804                // `uval & 0xFF` and emitting a WRONG immediate. The encoder is
8805                // Ok-or-Err, never corrupt (#180/#185); a loud Err is also why
8806                // this is an Err and not a panic (the `encoder_no_panic` fuzz
8807                // contract — malformed/oversized input must degrade, not crash).
8808                Err(synth_core::Error::synthesis(format!(
8809                    "encode_operand2: immediate {uval:#x} ({val}) is not an ARM32 \
8810                     rotated immediate — the selector must materialize large \
8811                     constants via MOVW/MOVT"
8812                )))
8813            }
8814        }
8815
8816        Operand2::Reg(reg) => {
8817            let reg_bits = reg_to_bits(reg);
8818            Ok((reg_bits, 0)) // I=0 for register
8819        }
8820
8821        Operand2::RegShift {
8822            rm,
8823            shift: _,
8824            amount,
8825        } => {
8826            // Simplified encoding with shift
8827            let rm_bits = reg_to_bits(rm);
8828            let shift_bits = (*amount & 0x1F) << 7;
8829            Ok((shift_bits | rm_bits, 0))
8830        }
8831    }
8832}
8833
8834/// Largest immediate an A32 `LDR`/`STR`/`LDRB`/`STRB` `[Rn, #imm12]` form holds.
8835const A32_LDST_IMM12_MAX: u32 = 0xFFF;
8836/// Largest immediate an A32 `LDRH`/`STRH`/`LDRSB`/`LDRSH` `[Rn, #imm8]`
8837/// (`imm4H:imm4L`) form holds.
8838const A32_LDST_IMM8_MAX: u32 = 0xFF;
8839/// Largest LOW-half immediate an A32 `I64Ldr`/`I64Str` pair can fold: the high
8840/// half sits at `+4`, which must itself fit imm12 (`0xFFB + 4 = 0xFFF`).
8841const A32_I64_PAIR_IMM12_MAX: u32 = 0xFFB;
8842
8843/// RQ-63-ARMI64OFF (#1165): resolve the A32 base register and residual
8844/// immediate for a load/store whose static offset may exceed the form's
8845/// immediate field — the A32 counterpart of the Thumb-2 `i64_effective_base`
8846/// (#372/#382). Returns `(base, residual)`; the caller encodes `[base,
8847/// #residual]` (and `#residual + 4` for the i64 high half).
8848///
8849/// The offset is the wasm memarg round-tripped through the selector's `as i32`
8850/// cast (a memarg `>= 2^31` arrives negative), so it is read back as `u32` and
8851/// the effective address is `base + index + offset (mod 2^32)` — the same
8852/// arithmetic the Thumb-2 word path (`encode_thumb32_add_imm`) and the
8853/// `--safety-bounds software` guard (`software_bounds_guard`, which round-trips
8854/// the same cast) already use. A clamp or a mask here is a WRONG ADDRESS, i.e.
8855/// the silent memory miscompile this artifact exists to refuse: before this
8856/// helper the A32 immediate arms masked `& 0xFFF` (word/byte) and `& 0xFF`
8857/// (halfword/signed), so `i32.load offset=5000` on the `-b arm` default target
8858/// silently read `[ip, #904]`, and the i64 pair arm declined outright (46 of
8859/// the 110 core-module declines in v0.62's ARM census).
8860///
8861/// - `offset <= imm_max`, no index: emits nothing, returns `(addr.base, offset)`
8862///   — byte-identical to the pre-#1165 immediate form.
8863/// - `offset <= imm_max`, index `rm`: emits `ADD ip, base, rm`, returns
8864///   `(ip, offset)` — byte-identical to the #206 register-offset form.
8865/// - `offset > imm_max`: MATERIALIZES the whole effective base into `ip`:
8866///   `MOVW ip, #lo16 ; [MOVT ip, #hi16 — only when non-zero] ; ADD ip, ip, base
8867///   ; [ADD ip, ip, rm]`, returns `(ip, 0)`. `ip` is fully computed BEFORE the
8868///   access, so a destination register aliasing `base` or `rm` is safe.
8869///
8870/// `ip` (R12) is the reserved encoder scratch — never allocated (pool R0–R8;
8871/// R9/R10/R11 are the globals/size/base contract registers). If `base` or `rm`
8872/// IS R12 the materialization would clobber its own input, so that case is a
8873/// typed `Err` — a loud decline, never a wrong address (the alias trap
8874/// `encode_thumb32_add_imm` carries for `rd == rn == R12`).
8875///
8876/// Bytes verified against `arm-none-eabi-as` in the `test_1165_a32_*` tests
8877/// and executed by `scripts/repro/a32_ldst_offset_1165_differential.py`.
8878fn a32_effective_base(bytes: &mut Vec<u8>, addr: &MemAddr, imm_max: u32) -> Result<(Reg, u32)> {
8879    let ip = Reg::R12;
8880    let ip_bits = reg_to_bits(&ip);
8881    let offset = addr.offset as u32;
8882    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8883    // ADD rd, rn, rm (cond=AL, opcode=ADD, S=0, register operand2).
8884    let add_reg = |rd: u32, rn: u32, rm: u32| 0xE080_0000 | (rn << 16) | (rd << 12) | rm;
8885    if offset <= imm_max {
8886        return Ok(match addr.offset_reg {
8887            Some(rm) => {
8888                w(
8889                    bytes,
8890                    add_reg(ip_bits, reg_to_bits(&addr.base), reg_to_bits(&rm)),
8891                );
8892                (ip, offset)
8893            }
8894            None => (addr.base, offset),
8895        });
8896    }
8897    if addr.base == ip || addr.offset_reg == Some(ip) {
8898        return Err(synth_core::Error::synthesis(format!(
8899            "A32 load/store offset {offset:#x} exceeds the immediate field (max {imm_max:#x}) \
8900             and the address already uses the R12 scratch — no free register to \
8901             materialize it (RQ-63-ARMI64OFF)"
8902        )));
8903    }
8904    // MOVW ip, #lo16 ; MOVT ip, #hi16 (elided when zero, mirroring I64Const).
8905    let lo16 = offset & 0xFFFF;
8906    let hi16 = offset >> 16;
8907    w(
8908        bytes,
8909        0xE300_0000 | ((lo16 >> 12) << 16) | (ip_bits << 12) | (lo16 & 0xFFF),
8910    );
8911    if hi16 != 0 {
8912        w(
8913            bytes,
8914            0xE340_0000 | ((hi16 >> 12) << 16) | (ip_bits << 12) | (hi16 & 0xFFF),
8915        );
8916    }
8917    w(bytes, add_reg(ip_bits, ip_bits, reg_to_bits(&addr.base)));
8918    if let Some(rm) = addr.offset_reg {
8919        w(bytes, add_reg(ip_bits, ip_bits, reg_to_bits(&rm)));
8920    }
8921    Ok((ip, 0))
8922}
8923
8924/// Encode an A32 immediate-form memory address to `(base_reg, imm12)`.
8925///
8926/// RQ-63-ARMI64OFF (#1165): this used to mask `& 0xFFF`, silently re-targeting
8927/// any offset past the field (the #259 class, closed on Thumb-2 by
8928/// `check_ldst_imm12` but never here). The immediate arms are only reached with
8929/// an in-range, index-free address — `encode_arm_reg_offset_mem` materializes
8930/// everything else into IP first — so this is a TRIPWIRE: a typed error, never
8931/// a wrong address, should a future arm bypass that pre-pass.
8932fn encode_mem_addr(addr: &MemAddr) -> Result<(u32, u32)> {
8933    check_a32_imm_addr(addr, A32_LDST_IMM12_MAX)
8934}
8935
8936/// `encode_mem_addr` for the `imm4H:imm4L` (8-bit) forms — `LDRH`/`STRH`/
8937/// `LDRSB`/`LDRSH`. Same tripwire contract; the former `& 0xFF` mask is gone.
8938fn encode_mem_addr_imm8(addr: &MemAddr) -> Result<(u32, u32)> {
8939    check_a32_imm_addr(addr, A32_LDST_IMM8_MAX)
8940}
8941
8942fn check_a32_imm_addr(addr: &MemAddr, imm_max: u32) -> Result<(u32, u32)> {
8943    if addr.offset_reg.is_some() {
8944        return Err(synth_core::Error::synthesis(
8945            "internal: A32 immediate-form load/store reached with a register offset — \
8946             encode_arm_reg_offset_mem must materialize it first (#206)",
8947        ));
8948    }
8949    let offset = addr.offset as u32;
8950    if offset > imm_max {
8951        return Err(synth_core::Error::synthesis(format!(
8952            "internal: A32 load/store immediate offset {offset:#x} exceeds the {imm_max:#x} \
8953             field — encode_arm_reg_offset_mem must materialize it first (RQ-63-ARMI64OFF)"
8954        )));
8955    }
8956    Ok((reg_to_bits(&addr.base), offset))
8957}
8958
8959/// S-register number: S0=0, S1=1, ..., S31=31
8960fn vfp_sreg_to_num(reg: &VfpReg) -> Result<u32> {
8961    match reg {
8962        VfpReg::S0 => Ok(0),
8963        VfpReg::S1 => Ok(1),
8964        VfpReg::S2 => Ok(2),
8965        VfpReg::S3 => Ok(3),
8966        VfpReg::S4 => Ok(4),
8967        VfpReg::S5 => Ok(5),
8968        VfpReg::S6 => Ok(6),
8969        VfpReg::S7 => Ok(7),
8970        VfpReg::S8 => Ok(8),
8971        VfpReg::S9 => Ok(9),
8972        VfpReg::S10 => Ok(10),
8973        VfpReg::S11 => Ok(11),
8974        VfpReg::S12 => Ok(12),
8975        VfpReg::S13 => Ok(13),
8976        VfpReg::S14 => Ok(14),
8977        VfpReg::S15 => Ok(15),
8978        VfpReg::S16 => Ok(16),
8979        VfpReg::S17 => Ok(17),
8980        VfpReg::S18 => Ok(18),
8981        VfpReg::S19 => Ok(19),
8982        VfpReg::S20 => Ok(20),
8983        VfpReg::S21 => Ok(21),
8984        VfpReg::S22 => Ok(22),
8985        VfpReg::S23 => Ok(23),
8986        VfpReg::S24 => Ok(24),
8987        VfpReg::S25 => Ok(25),
8988        VfpReg::S26 => Ok(26),
8989        VfpReg::S27 => Ok(27),
8990        VfpReg::S28 => Ok(28),
8991        VfpReg::S29 => Ok(29),
8992        VfpReg::S30 => Ok(30),
8993        VfpReg::S31 => Ok(31),
8994        // D-registers are not used in F32 single-precision encodings
8995        _ => Err(synth_core::Error::SynthesisError(
8996            "D-register not supported in single-precision VFP encoding".to_string(),
8997        )),
8998    }
8999}
9000
9001/// D-register number: D0=0, D1=1, ..., D15=15
9002fn vfp_dreg_to_num(reg: &VfpReg) -> Result<u32> {
9003    match reg {
9004        VfpReg::D0 => Ok(0),
9005        VfpReg::D1 => Ok(1),
9006        VfpReg::D2 => Ok(2),
9007        VfpReg::D3 => Ok(3),
9008        VfpReg::D4 => Ok(4),
9009        VfpReg::D5 => Ok(5),
9010        VfpReg::D6 => Ok(6),
9011        VfpReg::D7 => Ok(7),
9012        VfpReg::D8 => Ok(8),
9013        VfpReg::D9 => Ok(9),
9014        VfpReg::D10 => Ok(10),
9015        VfpReg::D11 => Ok(11),
9016        VfpReg::D12 => Ok(12),
9017        VfpReg::D13 => Ok(13),
9018        VfpReg::D14 => Ok(14),
9019        VfpReg::D15 => Ok(15),
9020        // S-registers are not used in F64 double-precision encodings
9021        _ => Err(synth_core::Error::SynthesisError(
9022            "S-register not supported in double-precision VFP encoding".to_string(),
9023        )),
9024    }
9025}
9026
9027/// Split S-register into (Vx[3:0], qualifier_bit) for VFP encoding.
9028/// For an S-register number s: Vx = s >> 1, qualifier = s & 1.
9029/// The qualifier bit goes to D (bit 22), N (bit 7), or M (bit 5) depending on role.
9030fn encode_sreg(s: u32) -> (u32, u32) {
9031    (s >> 1, s & 1)
9032}
9033
9034/// Split D-register into (Vx[3:0], qualifier_bit) for VFP double-precision encoding.
9035/// For a D-register number d: Vx = d & 0xF, qualifier = (d >> 4) & 1.
9036/// For D0-D15, qualifier is always 0.
9037fn encode_dreg(d: u32) -> (u32, u32) {
9038    (d & 0xF, (d >> 4) & 1)
9039}
9040
9041/// Encode a VFP 3-register arithmetic instruction (VADD.F32, VSUB.F32, VMUL.F32, VDIV.F32).
9042/// Returns the full 32-bit instruction word.
9043///
9044/// VFP encoding: [cond 1110] [D opc1 Vn] [Vd 101 sz] [N opc2 M 0 Vm]
9045/// For single-precision (sz=0), coprocessor = 0xA (bits[11:8]).
9046fn encode_vfp_3reg(base: u32, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<u32> {
9047    let sd_num = vfp_sreg_to_num(sd)?;
9048    let sn_num = vfp_sreg_to_num(sn)?;
9049    let sm_num = vfp_sreg_to_num(sm)?;
9050    let (vd, d) = encode_sreg(sd_num);
9051    let (vn, n) = encode_sreg(sn_num);
9052    let (vm, m) = encode_sreg(sm_num);
9053
9054    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
9055}
9056
9057/// Encode a VFP 2-register instruction (VNEG.F32, VABS.F32, VSQRT.F32).
9058/// Returns the full 32-bit instruction word.
9059fn encode_vfp_2reg(base: u32, sd: &VfpReg, sm: &VfpReg) -> Result<u32> {
9060    let sd_num = vfp_sreg_to_num(sd)?;
9061    let sm_num = vfp_sreg_to_num(sm)?;
9062    let (vd, d) = encode_sreg(sd_num);
9063    let (vm, m) = encode_sreg(sm_num);
9064
9065    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
9066}
9067
9068/// Encode a VFP load/store (VLDR.F32 / VSTR.F32).
9069/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
9070/// U bit (bit 23) controls add/subtract offset.
9071fn encode_vfp_ldst(base: u32, sd: &VfpReg, addr: &MemAddr) -> Result<u32> {
9072    let sd_num = vfp_sreg_to_num(sd)?;
9073    let (vd, d) = encode_sreg(sd_num);
9074    let rn = reg_to_bits(&addr.base);
9075
9076    let offset = addr.offset;
9077    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9078    let abs_offset = offset.unsigned_abs();
9079    let imm8 = (abs_offset / 4) & 0xFF;
9080
9081    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
9082}
9083
9084/// Encode VMOV between core register and S-register.
9085/// VMOV Sn, Rt: 0xEE00_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
9086/// VMOV Rt, Sn: 0xEE10_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
9087fn encode_vmov_core_sreg(to_sreg: bool, sreg: &VfpReg, core: &Reg) -> Result<u32> {
9088    let s_num = vfp_sreg_to_num(sreg)?;
9089    let (vn, n) = encode_sreg(s_num);
9090    let rt = reg_to_bits(core);
9091
9092    let base = if to_sreg { 0xEE000A10 } else { 0xEE100A10 };
9093    Ok(base | (vn << 16) | (rt << 12) | (n << 7))
9094}
9095
9096/// Encode a VFP 3-register double-precision instruction (VADD.F64, VSUB.F64, etc.).
9097/// For double-precision (sz=1), coprocessor = 0xB (bits[11:8]).
9098/// The base should have bit 8 = 1 for F64 (0xB suffix instead of 0xA).
9099fn encode_vfp_3reg_f64(base: u32, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<u32> {
9100    let dd_num = vfp_dreg_to_num(dd)?;
9101    let dn_num = vfp_dreg_to_num(dn)?;
9102    let dm_num = vfp_dreg_to_num(dm)?;
9103    let (vd, d) = encode_dreg(dd_num);
9104    let (vn, n) = encode_dreg(dn_num);
9105    let (vm, m) = encode_dreg(dm_num);
9106
9107    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
9108}
9109
9110/// Encode a VFP 2-register double-precision instruction (VNEG.F64, VABS.F64, VSQRT.F64).
9111fn encode_vfp_2reg_f64(base: u32, dd: &VfpReg, dm: &VfpReg) -> Result<u32> {
9112    let dd_num = vfp_dreg_to_num(dd)?;
9113    let dm_num = vfp_dreg_to_num(dm)?;
9114    let (vd, d) = encode_dreg(dd_num);
9115    let (vm, m) = encode_dreg(dm_num);
9116
9117    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
9118}
9119
9120/// Encode a VFP load/store for double-precision (VLDR.64 / VSTR.64).
9121/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
9122fn encode_vfp_ldst_f64(base: u32, dd: &VfpReg, addr: &MemAddr) -> Result<u32> {
9123    let dd_num = vfp_dreg_to_num(dd)?;
9124    let (vd, d) = encode_dreg(dd_num);
9125    let rn = reg_to_bits(&addr.base);
9126
9127    let offset = addr.offset;
9128    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9129    let abs_offset = offset.unsigned_abs();
9130    let imm8 = (abs_offset / 4) & 0xFF;
9131
9132    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
9133}
9134
9135/// Encode VMOV between two core registers and a D-register.
9136/// VMOV Dm, Rt, Rt2: 0xEC40_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
9137/// VMOV Rt, Rt2, Dm: 0xEC50_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
9138fn encode_vmov_core_dreg(
9139    to_dreg: bool,
9140    dreg: &VfpReg,
9141    core_lo: &Reg,
9142    core_hi: &Reg,
9143) -> Result<u32> {
9144    let d_num = vfp_dreg_to_num(dreg)?;
9145    let (vm, m) = encode_dreg(d_num);
9146    let rt = reg_to_bits(core_lo);
9147    let rt2 = reg_to_bits(core_hi);
9148
9149    let base = if to_dreg { 0xEC400B10 } else { 0xEC500B10 };
9150    Ok(base | (rt2 << 16) | (rt << 12) | (m << 5) | vm)
9151}
9152
9153/// Emit a VFP 32-bit instruction as Thumb-2 bytes (two LE halfwords).
9154fn vfp_to_thumb_bytes(instr: u32) -> Vec<u8> {
9155    let hw1 = ((instr >> 16) & 0xFFFF) as u16;
9156    let hw2 = (instr & 0xFFFF) as u16;
9157    let mut bytes = hw1.to_le_bytes().to_vec();
9158    bytes.extend_from_slice(&hw2.to_le_bytes());
9159    bytes
9160}
9161
9162// ============================================================================
9163// Helium MVE encoding helpers
9164// ============================================================================
9165
9166/// Q-register number: Q0=0, Q1=1, ..., Q7=7
9167fn qreg_to_num(reg: &QReg) -> u32 {
9168    match reg {
9169        QReg::Q0 => 0,
9170        QReg::Q1 => 1,
9171        QReg::Q2 => 2,
9172        QReg::Q3 => 3,
9173        QReg::Q4 => 4,
9174        QReg::Q5 => 5,
9175        QReg::Q6 => 6,
9176        QReg::Q7 => 7,
9177    }
9178}
9179
9180/// MVE element size to encoding bits: S8=0b00, S16=0b01, S32=0b10
9181fn mve_size_bits(size: &MveSize) -> u32 {
9182    match size {
9183        MveSize::S8 => 0b00,
9184        MveSize::S16 => 0b01,
9185        MveSize::S32 => 0b10,
9186    }
9187}
9188
9189/// Encode MVE 3-register instruction.
9190/// Q-registers are encoded as D-register pairs: Q0=D0:D1, Q1=D2:D3, etc.
9191/// In NEON/MVE encoding, the Q-register uses D-register number = Qn * 2.
9192fn encode_mve_3reg(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
9193    let d = qreg_to_num(qd) * 2;
9194    let n = qreg_to_num(qn) * 2;
9195    let m = qreg_to_num(qm) * 2;
9196
9197    // Standard NEON/MVE 3-register encoding:
9198    // D bit (bit 22) = Vd[4], Vd[3:0] = bits [15:12]
9199    // N bit (bit 7)  = Vn[4], Vn[3:0] = bits [19:16]
9200    // M bit (bit 5)  = Vm[4], Vm[3:0] = bits [3:0]
9201    let vd = d & 0xF;
9202    let d_bit = (d >> 4) & 1;
9203    let vn = n & 0xF;
9204    let n_bit = (n >> 4) & 1;
9205    let vm = m & 0xF;
9206    let m_bit = (m >> 4) & 1;
9207
9208    base | (d_bit << 22) | (vn << 16) | (vd << 12) | (n_bit << 7) | (m_bit << 5) | vm
9209}
9210
9211/// Encode MVE 3-register bitwise instruction (VAND, VORR, VEOR, VBIC).
9212fn encode_mve_3reg_bitwise(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
9213    encode_mve_3reg(base, qd, qn, qm)
9214}
9215
9216/// Encode MVE VLDRW.32 Qd, [Rn, #offset]
9217/// Format: EC9x xxxx - contiguous load, word-sized elements
9218fn encode_mve_vldrw(qd: &QReg, addr: &MemAddr) -> u32 {
9219    let qd_enc = qreg_to_num(qd) * 2;
9220    let rn = reg_to_bits(&addr.base);
9221    let offset = addr.offset;
9222    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9223    let abs_offset = offset.unsigned_abs();
9224    let imm7 = (abs_offset / 4) & 0x7F; // 7-bit word-aligned offset
9225
9226    // VLDRW.32 Qd, [Rn, #imm]: ED10 xx80 variant
9227    0xED100E80
9228        | (u_bit << 23)
9229        | ((qd_enc >> 4) << 22)
9230        | (rn << 16)
9231        | ((qd_enc & 0xF) << 12)
9232        | (imm7 & 0x7F)
9233}
9234
9235/// Encode MVE VSTRW.32 Qd, [Rn, #offset]
9236fn encode_mve_vstrw(qd: &QReg, addr: &MemAddr) -> u32 {
9237    let qd_enc = qreg_to_num(qd) * 2;
9238    let rn = reg_to_bits(&addr.base);
9239    let offset = addr.offset;
9240    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9241    let abs_offset = offset.unsigned_abs();
9242    let imm7 = (abs_offset / 4) & 0x7F;
9243
9244    0xED000E80
9245        | (u_bit << 23)
9246        | ((qd_enc >> 4) << 22)
9247        | (rn << 16)
9248        | ((qd_enc & 0xF) << 12)
9249        | (imm7 & 0x7F)
9250}
9251
9252impl ArmEncoder {
9253    /// Encode MVE constant load: MOVW+MOVT+VMOV for each 32-bit word, then assemble Q-register
9254    fn encode_thumb_mve_const(&self, qd: &QReg, bytes: &[u8; 16]) -> Result<Vec<u8>> {
9255        let mut result = Vec::new();
9256        let qd_num = qreg_to_num(qd);
9257
9258        // Load each 32-bit word into R12 (temp) then VMOV into S-register
9259        for i in 0..4 {
9260            let word = u32::from_le_bytes([
9261                bytes[i * 4],
9262                bytes[i * 4 + 1],
9263                bytes[i * 4 + 2],
9264                bytes[i * 4 + 3],
9265            ]);
9266            let lo16 = word & 0xFFFF;
9267            let hi16 = (word >> 16) & 0xFFFF;
9268
9269            // MOVW R12, #lo16
9270            result.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
9271            // MOVT R12, #hi16
9272            if hi16 != 0 {
9273                result.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
9274            }
9275
9276            // VMOV Sn, R12 where Sn = Qd*4 + i
9277            let s_num = qd_num * 4 + i as u32;
9278            let (vn, n) = encode_sreg(s_num);
9279            let vmov: u32 = 0xEE000A10 | (vn << 16) | (12 << 12) | (n << 7);
9280            result.extend_from_slice(&vfp_to_thumb_bytes(vmov));
9281        }
9282
9283        Ok(result)
9284    }
9285
9286    /// Encode lane-wise f32 binary operation (VDIV, etc.) via S-register extraction
9287    fn encode_thumb_mve_lane_wise_f32_binop(
9288        &self,
9289        qd: &QReg,
9290        qn: &QReg,
9291        qm: &QReg,
9292        vfp_base: u32,
9293    ) -> Result<Vec<u8>> {
9294        let mut result = Vec::new();
9295        let qd_num = qreg_to_num(qd);
9296        let qn_num = qreg_to_num(qn);
9297        let qm_num = qreg_to_num(qm);
9298
9299        // For each lane 0..3: use S-registers directly (Q aliasing)
9300        for i in 0..4u32 {
9301            let sd = qd_num * 4 + i;
9302            let sn = qn_num * 4 + i;
9303            let sm = qm_num * 4 + i;
9304
9305            let (vd, d) = encode_sreg(sd);
9306            let (vn, n) = encode_sreg(sn);
9307            let (vm, m) = encode_sreg(sm);
9308
9309            let instr = vfp_base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
9310            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9311        }
9312
9313        Ok(result)
9314    }
9315
9316    /// Encode lane-wise f32 VSQRT via S-register extraction
9317    fn encode_thumb_mve_lane_wise_f32_sqrt(&self, qd: &QReg, qm: &QReg) -> Result<Vec<u8>> {
9318        let mut result = Vec::new();
9319        let qd_num = qreg_to_num(qd);
9320        let qm_num = qreg_to_num(qm);
9321
9322        // VSQRT.F32 base: 0xEEB10AC0
9323        for i in 0..4u32 {
9324            let sd = qd_num * 4 + i;
9325            let sm = qm_num * 4 + i;
9326
9327            let (vd, d) = encode_sreg(sd);
9328            let (vm, m) = encode_sreg(sm);
9329
9330            let instr: u32 = 0xEEB10AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
9331            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9332        }
9333
9334        Ok(result)
9335    }
9336}
9337
9338/// VCR-TIER-001 (#1021/#1048) — the SCRATCH CONTRACT of an `ArmOp`'s encoder
9339/// expansion: the registers, beyond the op's declared RESULT registers and the
9340/// globally sanctioned R12/IP encoder scratch, that the expansion may leave
9341/// modified when it completes. Transient-but-restored traffic (push/pop through
9342/// the expansion's own stack red-zone, SP restored on exit) is not "modified".
9343///
9344/// This is the SINGLE declaration site for that contract — the one place a
9345/// future expansion that must borrow a register says so (the repo rule: one
9346/// declaration site, no duplicate copy that can silently drift). It is deliberately NOT derived
9347/// from the expansion's observed behavior: a contract read off the bytes would
9348/// rubber-stamp any clobber. Intent is declared here; the canary gate
9349/// (`scripts/repro/expansion_canary_gate_1021.py`) executes the REAL emitted
9350/// bytes of every variant the shipped rule table emits, on both backends, with
9351/// every non-contract register holding a distinctive canary, and fails on any
9352/// undeclared write — and on any declared register the expansion never
9353/// actually writes, so an over-broad declaration cannot hollow the gate.
9354///
9355/// The default is the STRICTEST reading — result registers only — which is
9356/// exactly the silent claim the atomic `ArmSemantics` pseudo-op model already
9357/// makes (#1021: an atomic model of a multi-instruction expansion is a silent
9358/// claim that the expansion is scratch-free). Today the table is EMPTY: #1039
9359/// reworked `i32.popcnt` off R11 (the linear-memory base) and #1048 reworked
9360/// the i64 shifts and bit-counts off their own operand registers, so every
9361/// expansion of every rule-emitted variant is R12-only on both Thumb-2 and
9362/// A32. Backend-independent for the same reason; if a backend's expansion ever
9363/// diverges, this signature grows a backend parameter in the same PR.
9364pub fn expansion_scratch_contract(op: &ArmOp) -> &'static [Reg] {
9365    // No variant currently borrows any register beyond R12. A new declaration
9366    // is added as a `match op { .. }` arm here — nowhere else.
9367    let _ = op;
9368    &[]
9369}
9370
9371#[cfg(test)]
9372mod tests {
9373    use super::*;
9374
9375    #[test]
9376    fn test_encoder_creation() {
9377        let encoder_arm = ArmEncoder::new_arm32();
9378        assert!(!encoder_arm.thumb_mode);
9379
9380        let encoder_thumb = ArmEncoder::new_thumb2();
9381        assert!(encoder_thumb.thumb_mode);
9382    }
9383
9384    /// #204 WAKE-path regression: `SetCond` materialized 0/1 with the 16-bit
9385    /// `MOVS Rd,#imm` (T1), whose Rd field is 3 bits (R0–R7). For a high Rd
9386    /// (R8–R12) `rd_bits << 8` overflows bit 11, flipping the opcode MOVS→CMP
9387    /// (`0x2c00`), so the boolean was never written — gale's `has_waiter` kept a
9388    /// stale value and the binary-sem WAKE dispatch read garbage. High Rd must
9389    /// use the 32-bit `MOV.W` (T2). Verify the bytes, not the IR.
9390    /// #311: the SAME high-Rd MOVS→CMP transmutation as #204, but in the
9391    /// i64 comparison expansions (I64SetCond / I64SetCondZ) — missed by the
9392    /// #204 hardening. With rd=R8 the boolean died in the flags
9393    /// (`ite eq; cmpeq r0,#1; cmpne r0,#0`), so gale's packed-u64 select
9394    /// read a stale register on silicon. High Rd must take MOV.W / CMP.W.
9395    #[test]
9396    fn test_encode_i64setcond_high_reg_uses_mov_w_311() {
9397        use synth_synthesis::{ArmOp, Condition, Reg};
9398        let enc = ArmEncoder::new_thumb2();
9399        let bytes = enc
9400            .encode(&ArmOp::I64SetCond {
9401                rd: Reg::R8,
9402                rn_lo: Reg::R2,
9403                rn_hi: Reg::R3,
9404                rm_lo: Reg::R6,
9405                rm_hi: Reg::R7,
9406                cond: Condition::EQ,
9407            })
9408            .unwrap();
9409        // The 32-bit MOV.W immediate (T2) first halfword is 0xF04F; the
9410        // 16-bit transmuted forms would contain 0x2801/0x2800 (CMP r0,#1/#0).
9411        let halfwords: Vec<u16> = bytes
9412            .chunks(2)
9413            .map(|c| u16::from_le_bytes([c[0], c[1]]))
9414            .collect();
9415        assert!(
9416            halfwords.iter().filter(|&&h| h == 0xF04F).count() == 2,
9417            "high rd must use two MOV.W (T2) encodings, got {halfwords:04x?}"
9418        );
9419        assert!(
9420            !halfwords.contains(&0x2801) && !halfwords.contains(&0x2800),
9421            "no transmuted 16-bit CMP imm: {halfwords:04x?}"
9422        );
9423
9424        let bytes_z = enc
9425            .encode(&ArmOp::I64SetCondZ {
9426                rd: Reg::R8,
9427                rn_lo: Reg::R2,
9428                rn_hi: Reg::R3,
9429            })
9430            .unwrap();
9431        let hw_z: Vec<u16> = bytes_z
9432            .chunks(2)
9433            .map(|c| u16::from_le_bytes([c[0], c[1]]))
9434            .collect();
9435        assert!(
9436            hw_z.iter().filter(|&&h| h == 0xF04F).count() == 2,
9437            "SetCondZ high rd MOV.W: {hw_z:04x?}"
9438        );
9439        // CMP.W rd,#0 (T2) first halfword: 0xF1B0 | rd
9440        assert!(
9441            hw_z.contains(&(0xF1B0 | 8)),
9442            "SetCondZ high rd must use CMP.W: {hw_z:04x?}"
9443        );
9444    }
9445
9446    #[test]
9447    fn test_encode_setcond_high_reg_uses_mov_w_204() {
9448        use synth_synthesis::{ArmOp, Condition, Reg};
9449        let enc = ArmEncoder::new_thumb2();
9450        // R12 (high): must be ITE + MOV.W #1 + MOV.W #0, never a 16-bit MOVS/CMP.
9451        let hi = enc
9452            .encode(&ArmOp::SetCond {
9453                rd: Reg::R12,
9454                cond: Condition::NE,
9455            })
9456            .unwrap();
9457        assert_eq!(hi.len(), 10, "ITE(2) + MOV.W(4) + MOV.W(4): {hi:02x?}");
9458        // both value halfwords are MOV.W (0xF04F) — NOT the corrupt CMP (0x2c..).
9459        assert_eq!(&hi[2..4], &[0x4F, 0xF0], "then = MOV.W: {hi:02x?}");
9460        assert_eq!(&hi[6..8], &[0x4F, 0xF0], "else = MOV.W: {hi:02x?}");
9461        assert_eq!(hi[4] & 0x0F, 0x01, "then imm = #1");
9462        assert_eq!(hi[8] & 0x0F, 0x00, "else imm = #0");
9463        // Low Rd keeps the compact 16-bit MOVS form.
9464        let lo = enc
9465            .encode(&ArmOp::SetCond {
9466                rd: Reg::R0,
9467                cond: Condition::NE,
9468            })
9469            .unwrap();
9470        assert_eq!(lo.len(), 6, "ITE(2) + MOVS(2) + MOVS(2): {lo:02x?}");
9471        assert_eq!(lo[2..4], [0x01, 0x20], "then = MOVS R0,#1");
9472        assert_eq!(lo[4..6], [0x00, 0x20], "else = MOVS R0,#0");
9473    }
9474
9475    /// #209 Opt 1b: UMULL RdLo, RdHi, Rn, Rm encodes correctly on both ISAs.
9476    /// Thumb-2 T1: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm.
9477    /// A32:        cond 0000 1000 RdHi RdLo Rm 1001 Rn.
9478    #[test]
9479    fn test_encode_umull_209b() {
9480        use synth_synthesis::{ArmOp, Reg};
9481        let op = ArmOp::Umull {
9482            rdlo: Reg::R4,
9483            rdhi: Reg::R5,
9484            rn: Reg::R0,
9485            rm: Reg::R3,
9486        };
9487        // Thumb-2: hw1 = 0xFBA0 | 0 = 0xFBA0; hw2 = (4<<12)|(5<<8)|3 = 0x4503.
9488        let t = ArmEncoder::new_thumb2().encode(&op).unwrap();
9489        assert_eq!(
9490            t,
9491            vec![0xA0, 0xFB, 0x03, 0x45],
9492            "umull r4,r5,r0,r3 (T2): {t:02x?}"
9493        );
9494        // A32: 0xE0800090 | (5<<16) | (4<<12) | (3<<8) | 0 = 0xE0854390.
9495        let a = ArmEncoder::new_arm32().encode(&op).unwrap();
9496        assert_eq!(
9497            a,
9498            0xE085_4390u32.to_le_bytes().to_vec(),
9499            "umull (A32): {a:02x?}"
9500        );
9501    }
9502
9503    /// #206 regression: the ARM32 (A32) `Ldr`/`Str` encoders fed `addr` through
9504    /// `encode_mem_addr`, which returns only the 12-bit immediate — so a register
9505    /// offset (`[rn, rm, #off]`) was silently dropped to `[rn, #off]`, sending
9506    /// the access to the wrong runtime address (silent miscompile on the default
9507    /// `--target arm`). A register offset must materialize `ip = rn + rm` and
9508    /// load from `[ip, #off]`. Verify the bytes.
9509    #[test]
9510    fn test_encode_arm32_indexed_load_keeps_index_206() {
9511        use synth_synthesis::{ArmOp, MemAddr, Reg};
9512        let enc = ArmEncoder::new_arm32();
9513        // ldr r0, [r11, r1, #8]  must NOT collapse to a single immediate ldr.
9514        let bytes = enc
9515            .encode(&ArmOp::Ldr {
9516                rd: Reg::R0,
9517                addr: MemAddr::reg_imm(Reg::R11, Reg::R1, 8),
9518            })
9519            .unwrap();
9520        assert_eq!(
9521            bytes.len(),
9522            8,
9523            "expected ADD ip + LDR (2 words): {bytes:02x?}"
9524        );
9525        let add = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9526        let ldr = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9527        // ADD ip, r11, r1  = 0xE08BC001
9528        assert_eq!(add, 0xE08B_C001, "ADD ip,r11,r1: {add:#010x}");
9529        // LDR r0, [ip, #8] = 0xE59C0008
9530        assert_eq!(ldr, 0xE59C_0008, "LDR r0,[ip,#8]: {ldr:#010x}");
9531        // A bare immediate ldr (the bug) would be 0xE59B0008 (base=r11) — reject.
9532        assert_ne!(ldr, 0xE59B_0008, "index must not be dropped");
9533    }
9534
9535    /// #594 regression: `call_indirect` on the A32 path (`--target cortex-r5`)
9536    /// was encoded as a literal NOP (0xE1A00000) — the call never happened and
9537    /// the function silently returned the leftover table-index value. The A32
9538    /// encoder must emit a real dispatch expansion, since #642 guarded by an
9539    /// inline bounds check:
9540    /// `MOVW r12, #size; CMP idx, r12; BLO +1; UDF;
9541    ///  MOV r12, idx, LSL #2; LDR r12, [r11, r12]; BLX r12`.
9542    #[test]
9543    fn test_encode_arm32_call_indirect_is_real_call_594() {
9544        use synth_synthesis::{ArmOp, Reg};
9545        let enc = ArmEncoder::new_arm32();
9546        let bytes = enc
9547            .encode(&ArmOp::CallIndirect {
9548                rd: Reg::R0,
9549                type_idx: 0,
9550                table_index_reg: Reg::R0,
9551                table_size: 4,
9552                table_byte_offset: 0,
9553                null_check: false,
9554                type_check: None,
9555            })
9556            .unwrap();
9557        assert_eq!(
9558            bytes.len(),
9559            28,
9560            "expected MOVW + CMP + BLO + UDF + MOV + LDR + BLX (7 words): {bytes:02x?}"
9561        );
9562        let words: Vec<u32> = bytes
9563            .as_chunks::<4>()
9564            .0
9565            .iter()
9566            .map(|&w| u32::from_le_bytes(w))
9567            .collect();
9568        // #642 bounds guard: MOVW r12, #4; CMP r0, r12; BLO +1; UDF
9569        assert_eq!(words[0], 0xE300_C004, "MOVW r12,#4: {:#010x}", words[0]);
9570        assert_eq!(words[1], 0xE150_000C, "CMP r0,r12: {:#010x}", words[1]);
9571        assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9572        assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9573        // MOV r12, r0, LSL #2 = 0xE1A0C100
9574        assert_eq!(
9575            words[4], 0xE1A0_C100,
9576            "MOV r12,r0,LSL#2: {:#010x}",
9577            words[4]
9578        );
9579        // LDR r12, [r11, r12] = 0xE79BC00C
9580        assert_eq!(
9581            words[5], 0xE79B_C00C,
9582            "LDR r12,[r11,r12]: {:#010x}",
9583            words[5]
9584        );
9585        // BLX r12 = 0xE12FFF3C
9586        assert_eq!(words[6], 0xE12F_FF3C, "BLX r12: {:#010x}", words[6]);
9587        // The bug: a single NOP word. Must never come back.
9588        assert!(
9589            !bytes
9590                .as_chunks::<4>()
9591                .0
9592                .iter()
9593                .any(|&w| w == 0xE1A0_0000u32.to_le_bytes()),
9594            "call_indirect must not contain a NOP (#594): {bytes:02x?}"
9595        );
9596
9597        // A non-R0 index register lands in the MOV's Rm and CMP's Rn fields.
9598        let bytes = enc
9599            .encode(&ArmOp::CallIndirect {
9600                rd: Reg::R0,
9601                type_idx: 0,
9602                table_index_reg: Reg::R4,
9603                table_size: 4,
9604                table_byte_offset: 0,
9605                null_check: false,
9606                type_check: None,
9607            })
9608            .unwrap();
9609        let cmp = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9610        assert_eq!(cmp, 0xE154_000C, "CMP r4,r12: {cmp:#010x}");
9611        let mov = u32::from_le_bytes(bytes[16..20].try_into().unwrap());
9612        assert_eq!(mov, 0xE1A0_C104, "MOV r12,r4,LSL#2: {mov:#010x}");
9613    }
9614
9615    /// #642: a table size above 16 bits must not be silently truncated by the
9616    /// MOVW — the A32 guard adds a MOVT for the high half.
9617    #[test]
9618    fn test_encode_arm32_call_indirect_wide_table_size_642() {
9619        use synth_synthesis::{ArmOp, Reg};
9620        let enc = ArmEncoder::new_arm32();
9621        let bytes = enc
9622            .encode(&ArmOp::CallIndirect {
9623                rd: Reg::R0,
9624                type_idx: 0,
9625                table_index_reg: Reg::R0,
9626                table_size: 0x0002_0003,
9627                table_byte_offset: 0,
9628                null_check: false,
9629                type_check: None,
9630            })
9631            .unwrap();
9632        assert_eq!(bytes.len(), 32, "MOVT arm adds one word: {bytes:02x?}");
9633        let movw = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9634        let movt = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9635        assert_eq!(movw, 0xE300_C003, "MOVW r12,#3: {movw:#010x}");
9636        assert_eq!(movt, 0xE340_C002, "MOVT r12,#2: {movt:#010x}");
9637    }
9638
9639    /// #597 anchor (justified correctness RE-PIN of the #594-era freeze): the
9640    /// Thumb-2 `CallIndirect` expansion is `mov.w ip, rm, LSL #2; ldr.w ip,
9641    /// [r11, ip]; blx ip`.
9642    ///
9643    /// The #594 PR froze the then-current bytes `4F EA 20 0C ...` whose first
9644    /// word decodes as `mov.w ip, rm, ASR #32` — the intended `LSL #2` had
9645    /// its shift amount in the TYPE field (bits 5:4) instead of imm2 (bits
9646    /// 7:6), so the index was destroyed and every call_indirect dispatched
9647    /// table entry 0 (shipped miscompile, masked by index-0 probes). #597
9648    /// corrects the encoding; new bytes `4F EA 80 0C ...` were
9649    /// execution-validated under unicorn against the wasmtime oracle on a
9650    /// multi-entry table (indexes 0, 1, 3 —
9651    /// scripts/repro/call_indirect_597_differential.py) before this pin was
9652    /// replaced. Old pin: [4F EA 20 0C, 5B F8 0C C0, E0 47] (ASR #32 — must
9653    /// never come back).
9654    #[test]
9655    fn test_encode_thumb_call_indirect_lsl2_597() {
9656        use synth_synthesis::{ArmOp, Reg};
9657        let enc = ArmEncoder::new_thumb2();
9658        let bytes = enc
9659            .encode(&ArmOp::CallIndirect {
9660                rd: Reg::R0,
9661                type_idx: 0,
9662                table_index_reg: Reg::R0,
9663                table_size: 4,
9664                table_byte_offset: 0,
9665                null_check: false,
9666                type_check: None,
9667            })
9668            .unwrap();
9669        assert_eq!(
9670            bytes,
9671            vec![
9672                // #642 bounds guard: movw ip,#4; cmp r0,ip; blo +1; udf #0
9673                0x40, 0xF2, 0x04, 0x0C, // movw ip, #4
9674                0x60, 0x45, // cmp r0, ip
9675                0x00, 0xD3, // blo .+4 (skip the udf)
9676                0x00, 0xDE, // udf #0 — OOB index trap (WASM §4.4.8)
9677                // #597-pinned dispatch
9678                0x4F, 0xEA, 0x80, 0x0C, // mov.w ip, r0, lsl #2
9679                0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9680                0xE0, 0x47, // blx ip
9681            ],
9682            "Thumb-2 CallIndirect: bounds guard + mov.w/ldr.w/blx dispatch: {bytes:02x?}"
9683        );
9684        // The #597 bug bytes (ASR #32 dispatch first word) must never come back.
9685        assert!(
9686            !bytes.windows(4).any(|w| w == [0x4F, 0xEA, 0x20, 0x0C]),
9687            "mov.w ip, rm, ASR #32 — the #597 type-field bug"
9688        );
9689
9690        // A non-R0 index register lands in the mov.w's Rm field (hw2 bits 3:0)
9691        // and the cmp's Rn field.
9692        let bytes = enc
9693            .encode(&ArmOp::CallIndirect {
9694                rd: Reg::R0,
9695                type_idx: 0,
9696                table_index_reg: Reg::R4,
9697                table_size: 4,
9698                table_byte_offset: 0,
9699                null_check: false,
9700                type_check: None,
9701            })
9702            .unwrap();
9703        assert_eq!(&bytes[4..6], &[0x64, 0x45], "cmp r4, ip: {bytes:02x?}");
9704        assert_eq!(
9705            &bytes[10..14],
9706            &[0x4F, 0xEA, 0x84, 0x0C],
9707            "mov.w ip, r4, LSL #2: {bytes:02x?}"
9708        );
9709    }
9710
9711    /// #642: the Thumb-2 bounds guard for a high-register index (R8 — the top
9712    /// of the allocatable pool) uses the high-reg-capable 16-bit CMP (T2) with
9713    /// the N bit set; a table size above 16 bits adds a MOVT.
9714    #[test]
9715    fn test_encode_thumb_call_indirect_guard_shapes_642() {
9716        use synth_synthesis::{ArmOp, Reg};
9717        let enc = ArmEncoder::new_thumb2();
9718        let bytes = enc
9719            .encode(&ArmOp::CallIndirect {
9720                rd: Reg::R0,
9721                type_idx: 0,
9722                table_index_reg: Reg::R8,
9723                table_size: 3,
9724                table_byte_offset: 0,
9725                null_check: false,
9726                type_check: None,
9727            })
9728            .unwrap();
9729        // cmp r8, ip — T2: 0x4500 | N(1)<<7 | Rm(12)<<3 | Rn(0) = 0x45E0
9730        assert_eq!(&bytes[4..6], &[0xE0, 0x45], "cmp r8, ip: {bytes:02x?}");
9731
9732        let bytes = enc
9733            .encode(&ArmOp::CallIndirect {
9734                rd: Reg::R0,
9735                type_idx: 0,
9736                table_index_reg: Reg::R0,
9737                table_size: 0x0002_0003,
9738                table_byte_offset: 0,
9739                null_check: false,
9740                type_check: None,
9741            })
9742            .unwrap();
9743        // movw ip,#3 then movt ip,#2 — the size must not be truncated.
9744        assert_eq!(
9745            &bytes[0..8],
9746            &[0x40, 0xF2, 0x03, 0x0C, 0xC0, 0xF2, 0x02, 0x0C],
9747            "movw ip,#3; movt ip,#2: {bytes:02x?}"
9748        );
9749    }
9750
9751    /// #650: a non-zero table base offset (table N of the contiguous R11
9752    /// region) routes the Thumb-2 pointer load through
9753    /// `add.w ip, r11, ip; ldr.w ip, [ip, #offset]` — and offset 0 keeps the
9754    /// pre-#650 single-load bytes IDENTICAL (the by-construction pin).
9755    #[test]
9756    fn test_encode_thumb_call_indirect_table_offset_650() {
9757        use synth_synthesis::{ArmOp, Reg};
9758        let enc = ArmEncoder::new_thumb2();
9759        // falcon's fused-component shape: table 0 has 7 entries, so table 1
9760        // sits at byte offset 28.
9761        let bytes = enc
9762            .encode(&ArmOp::CallIndirect {
9763                rd: Reg::R0,
9764                type_idx: 0,
9765                table_index_reg: Reg::R1,
9766                table_size: 41,
9767                table_byte_offset: 28,
9768                null_check: false,
9769                type_check: None,
9770            })
9771            .unwrap();
9772        assert_eq!(
9773            bytes,
9774            vec![
9775                // #642 bounds guard against TABLE 1's OWN size (41)
9776                0x40, 0xF2, 0x29, 0x0C, // movw ip, #41
9777                0x61, 0x45, // cmp r1, ip
9778                0x00, 0xD3, // blo .+4 (skip the udf)
9779                0x00, 0xDE, // udf #0 — OOB trap (WASM §4.4.8)
9780                // dispatch through table 1's base (R11 + 28)
9781                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9782                0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9783                0xDC, 0xF8, 0x1C, 0xC0, // ldr.w ip, [ip, #28]
9784                0xE0, 0x47, // blx ip
9785            ],
9786            "Thumb-2 table-1 dispatch (#650): {bytes:02x?}"
9787        );
9788
9789        // Offset 0 must stay the #597-pinned single-load form (no add.w, no
9790        // imm-form ldr) — single-table byte identity by construction.
9791        let zero = enc
9792            .encode(&ArmOp::CallIndirect {
9793                rd: Reg::R0,
9794                type_idx: 0,
9795                table_index_reg: Reg::R1,
9796                table_size: 41,
9797                table_byte_offset: 0,
9798                null_check: false,
9799                type_check: None,
9800            })
9801            .unwrap();
9802        assert_eq!(
9803            &zero[10..],
9804            &[
9805                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9806                0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9807                0xE0, 0x47, // blx ip
9808            ],
9809            "offset 0 keeps the pre-#650 dispatch bytes: {zero:02x?}"
9810        );
9811    }
9812
9813    /// #650: the A32 twin — `add r12, r11, r12; ldr r12, [r12, #offset]` for
9814    /// a non-zero table base offset; offset 0 keeps the #594/#642 form.
9815    #[test]
9816    fn test_encode_arm32_call_indirect_table_offset_650() {
9817        use synth_synthesis::{ArmOp, Reg};
9818        let enc = ArmEncoder::new_arm32();
9819        let bytes = enc
9820            .encode(&ArmOp::CallIndirect {
9821                rd: Reg::R0,
9822                type_idx: 0,
9823                table_index_reg: Reg::R1,
9824                table_size: 41,
9825                table_byte_offset: 28,
9826                null_check: false,
9827                type_check: None,
9828            })
9829            .unwrap();
9830        let words: Vec<u32> = bytes
9831            .as_chunks::<4>()
9832            .0
9833            .iter()
9834            .map(|&w| u32::from_le_bytes(w))
9835            .collect();
9836        assert_eq!(words[0], 0xE300_C029, "MOVW r12,#41: {:#010x}", words[0]);
9837        assert_eq!(words[1], 0xE151_000C, "CMP r1,r12: {:#010x}", words[1]);
9838        assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9839        assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9840        assert_eq!(
9841            words[4], 0xE1A0_C101,
9842            "MOV r12,r1,LSL#2: {:#010x}",
9843            words[4]
9844        );
9845        assert_eq!(
9846            words[5], 0xE08B_C00C,
9847            "ADD r12,r11,r12 (#650): {:#010x}",
9848            words[5]
9849        );
9850        assert_eq!(
9851            words[6], 0xE59C_C01C,
9852            "LDR r12,[r12,#28] (#650): {:#010x}",
9853            words[6]
9854        );
9855        assert_eq!(words[7], 0xE12F_FF3C, "BLX r12: {:#010x}", words[7]);
9856    }
9857
9858    /// #664: `null_check` inserts a null-funcref trap between the Thumb-2
9859    /// pointer load and the `BLX` (`cmp.w ip, #0; bne .+4; udf #0`) — a
9860    /// zero-linked (uninitialized) slot must TRAP (WASM §4.4.8), never
9861    /// branch to address 0. `null_check: false` keeps the expansion
9862    /// byte-identical to the pre-#664 form (by-construction pin).
9863    #[test]
9864    fn test_encode_thumb_call_indirect_null_check_664() {
9865        use synth_synthesis::{ArmOp, Reg};
9866        let enc = ArmEncoder::new_thumb2();
9867        let op = |null_check| ArmOp::CallIndirect {
9868            rd: Reg::R0,
9869            type_idx: 0,
9870            table_index_reg: Reg::R1,
9871            table_size: 4,
9872            table_byte_offset: 0,
9873            null_check,
9874            type_check: None,
9875        };
9876        let with = enc.encode(&op(true)).unwrap();
9877        let without = enc.encode(&op(false)).unwrap();
9878        // The checked form = the unchecked form with EXACTLY the three-insn
9879        // null check spliced in before the final BLX (byte identity of the
9880        // shared prefix/suffix — nothing else may move).
9881        assert_eq!(
9882            with.len(),
9883            without.len() + 8,
9884            "cmp.w (4) + bne (2) + udf (2): {with:02x?}"
9885        );
9886        let blx_at = without.len() - 2;
9887        assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9888        assert_eq!(
9889            &with[blx_at..],
9890            &[
9891                0xBC, 0xF1, 0x00, 0x0F, // cmp.w ip, #0
9892                0x00, 0xD1, // bne .+4 (skip the udf)
9893                0x00, 0xDE, // udf #0 — null-funcref trap (#664)
9894                0xE0, 0x47, // blx ip
9895            ],
9896            "null check precedes the BLX: {with:02x?}"
9897        );
9898        assert_eq!(&with[with.len() - 2..], &without[blx_at..], "same BLX");
9899    }
9900
9901    /// #664: the A32 twin — `cmp r12, #0; bne .+8; udf` before the `BLX`;
9902    /// `null_check: false` keeps the #594/#642/#650 bytes identical.
9903    #[test]
9904    fn test_encode_arm32_call_indirect_null_check_664() {
9905        use synth_synthesis::{ArmOp, Reg};
9906        let enc = ArmEncoder::new_arm32();
9907        let op = |null_check| ArmOp::CallIndirect {
9908            rd: Reg::R0,
9909            type_idx: 0,
9910            table_index_reg: Reg::R1,
9911            table_size: 4,
9912            table_byte_offset: 0,
9913            null_check,
9914            type_check: None,
9915        };
9916        let with = enc.encode(&op(true)).unwrap();
9917        let without = enc.encode(&op(false)).unwrap();
9918        assert_eq!(with.len(), without.len() + 12, "3 A32 words: {with:02x?}");
9919        let blx_at = without.len() - 4;
9920        assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9921        let words: Vec<u32> = with[blx_at..]
9922            .as_chunks::<4>()
9923            .0
9924            .iter()
9925            .map(|&w| u32::from_le_bytes(w))
9926            .collect();
9927        assert_eq!(words[0], 0xE35C_0000, "CMP r12,#0: {:#010x}", words[0]);
9928        assert_eq!(words[1], 0x1A00_0000, "BNE +1 insn: {:#010x}", words[1]);
9929        assert_eq!(words[2], 0xE7F0_00F0, "UDF (null trap): {:#010x}", words[2]);
9930        assert_eq!(words[3], 0xE12F_FF3C, "BLX r12: {:#010x}", words[3]);
9931    }
9932
9933    /// #676: `type_check` splices the runtime type check — scale the index,
9934    /// load the slot's structural class id from the type-id sidecar
9935    /// (`ldr.w ip, [ip, #type_off]`), compare against the expected class id
9936    /// and trap on mismatch (WASM §4.4.8) — between the bounds guard and
9937    /// the dispatch tail. `type_check: None` keeps the expansion
9938    /// byte-identical to the pre-#676 form (by-construction pin, the same
9939    /// trick as #650 offset-0 / #664 `null_check: false`).
9940    #[test]
9941    fn test_encode_thumb_call_indirect_type_check_676() {
9942        use synth_synthesis::{ArmOp, Reg};
9943        let enc = ArmEncoder::new_thumb2();
9944        let op = |type_check| ArmOp::CallIndirect {
9945            rd: Reg::R0,
9946            type_idx: 1,
9947            table_index_reg: Reg::R1,
9948            table_size: 5,
9949            table_byte_offset: 0,
9950            null_check: false,
9951            type_check,
9952        };
9953        let with = enc.encode(&op(Some((2, 20)))).unwrap();
9954        let without = enc.encode(&op(None)).unwrap();
9955        // The checked form = the unchecked form with EXACTLY the six-insn
9956        // type check spliced in after the bounds guard (byte identity of
9957        // the shared prefix/suffix — nothing else may move).
9958        assert_eq!(
9959            with.len(),
9960            without.len() + 20,
9961            "lsl.w(4)+add.w(4)+ldr.w(4)+cmp.w(4)+beq(2)+udf(2): {with:02x?}"
9962        );
9963        // Bounds guard: movw(4) + cmp(2) + blo(2) + udf(2) = 10 bytes.
9964        let guard_end = 10;
9965        assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
9966        assert_eq!(
9967            &with[guard_end..guard_end + 20],
9968            &[
9969                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9970                0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9971                0xDC, 0xF8, 0x14, 0xC0, // ldr.w ip, [ip, #20] — sidecar slot id
9972                0xBC, 0xF1, 0x02, 0x0F, // cmp.w ip, #2 — expected class id
9973                0x00, 0xD0, // beq .+4 (skip the udf on a match)
9974                0x00, 0xDE, // udf #0 — §4.4.8 type-mismatch trap (#676)
9975            ],
9976            "type check follows the bounds guard: {with:02x?}"
9977        );
9978        assert_eq!(
9979            &with[guard_end + 20..],
9980            &without[guard_end..],
9981            "dispatch tail unchanged (idx*4 recomputed)"
9982        );
9983    }
9984
9985    /// #676: the A32 twin — `mov r12, idx, lsl #2; add r12, r11, r12;
9986    /// ldr r12, [r12, #type_off]; cmp r12, #id; beq .+8; udf` after the
9987    /// bounds guard; `type_check: None` keeps the #594/#642/#650/#664
9988    /// bytes identical.
9989    #[test]
9990    fn test_encode_arm32_call_indirect_type_check_676() {
9991        use synth_synthesis::{ArmOp, Reg};
9992        let enc = ArmEncoder::new_arm32();
9993        let op = |type_check| ArmOp::CallIndirect {
9994            rd: Reg::R0,
9995            type_idx: 1,
9996            table_index_reg: Reg::R1,
9997            table_size: 5,
9998            table_byte_offset: 0,
9999            null_check: false,
10000            type_check,
10001        };
10002        let with = enc.encode(&op(Some((2, 20)))).unwrap();
10003        let without = enc.encode(&op(None)).unwrap();
10004        assert_eq!(with.len(), without.len() + 24, "6 A32 words: {with:02x?}");
10005        // Bounds guard: movw + cmp + blo + udf = 4 words = 16 bytes.
10006        let guard_end = 16;
10007        assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
10008        let words: Vec<u32> = with[guard_end..guard_end + 24]
10009            .as_chunks::<4>()
10010            .0
10011            .iter()
10012            .map(|&w| u32::from_le_bytes(w))
10013            .collect();
10014        assert_eq!(
10015            words[0], 0xE1A0_C101,
10016            "MOV r12,r1,LSL#2: {:#010x}",
10017            words[0]
10018        );
10019        assert_eq!(words[1], 0xE08B_C00C, "ADD r12,r11,r12: {:#010x}", words[1]);
10020        assert_eq!(
10021            words[2], 0xE59C_C014,
10022            "LDR r12,[r12,#20] (sidecar): {:#010x}",
10023            words[2]
10024        );
10025        assert_eq!(
10026            words[3], 0xE35C_0002,
10027            "CMP r12,#2 (expected class id): {:#010x}",
10028            words[3]
10029        );
10030        assert_eq!(words[4], 0x0A00_0000, "BEQ +1 insn: {:#010x}", words[4]);
10031        assert_eq!(
10032            words[5], 0xE7F0_00F0,
10033            "UDF (type-mismatch trap): {:#010x}",
10034            words[5]
10035        );
10036        assert_eq!(
10037            &with[guard_end + 24..],
10038            &without[guard_end..],
10039            "dispatch tail unchanged"
10040        );
10041    }
10042
10043    /// #178/#180 regression: the Thumb `Add`/`Adds`/`Subs` reg-forms used the
10044    /// 16-bit encoding unconditionally. For high registers (R12 base scratch,
10045    /// R8-R11 i64 pairs) the 3-bit register fields overflow and corrupt the
10046    /// operands — `add ip,ip,r0` came out as `adds r4,r5,r1` (0x186C), silently
10047    /// dropping the address operand and miscompiling every optimized memory
10048    /// access. High registers must use the 32-bit `.W` forms.
10049    #[test]
10050    fn test_encode_thumb_add_high_reg_uses_add_w_178_180() {
10051        let encoder = ArmEncoder::new_thumb2();
10052
10053        // add ip, ip, r0  — the exact MemLoad/MemStore base+addr op.
10054        let code = encoder
10055            .encode(&ArmOp::Add {
10056                rd: Reg::R12,
10057                rn: Reg::R12,
10058                op2: Operand2::Reg(Reg::R0),
10059            })
10060            .unwrap();
10061        // ADD.W ip, ip, r0 = EB0C 0C00 (little-endian halfwords).
10062        assert_eq!(
10063            code,
10064            vec![0x0C, 0xEB, 0x00, 0x0C],
10065            "high-reg Thumb ADD must be 32-bit ADD.W (EB0C 0C00), not corrupt 16-bit; got {code:02X?}"
10066        );
10067        // Must NOT be the buggy 16-bit 0x186C (`adds r4,r5,r1`).
10068        assert_ne!(code, vec![0x6C, 0x18], "regressed to corrupt 16-bit ADDS");
10069
10070        // Low-register add stays 16-bit (no regression for the common case).
10071        let lo = encoder
10072            .encode(&ArmOp::Add {
10073                rd: Reg::R1,
10074                rn: Reg::R2,
10075                op2: Operand2::Reg(Reg::R3),
10076            })
10077            .unwrap();
10078        assert_eq!(
10079            lo.len(),
10080            2,
10081            "low-reg ADD should remain 16-bit, got {lo:02X?}"
10082        );
10083    }
10084
10085    /// #178/#180 sibling: i64 low-word `Adds`/`Subs` can land in R8-R11 pairs;
10086    /// those must fall back to 32-bit ADDS.W/SUBS.W (flag-setting preserved).
10087    #[test]
10088    fn test_encode_thumb_adds_subs_high_reg_use_32bit_178_180() {
10089        let encoder = ArmEncoder::new_thumb2();
10090
10091        // adds r10, r10, r8  → ADDS.W = EB1A 0A08
10092        let adds = encoder
10093            .encode(&ArmOp::Adds {
10094                rd: Reg::R10,
10095                rn: Reg::R10,
10096                op2: Operand2::Reg(Reg::R8),
10097            })
10098            .unwrap();
10099        assert_eq!(
10100            adds,
10101            vec![0x1A, 0xEB, 0x08, 0x0A],
10102            "high-reg ADDS must be 32-bit ADDS.W (EB1A 0A08); got {adds:02X?}"
10103        );
10104
10105        // subs r10, r10, r8  → SUBS.W = EBBA 0A08
10106        let subs = encoder
10107            .encode(&ArmOp::Subs {
10108                rd: Reg::R10,
10109                rn: Reg::R10,
10110                op2: Operand2::Reg(Reg::R8),
10111            })
10112            .unwrap();
10113        assert_eq!(
10114            subs,
10115            vec![0xBA, 0xEB, 0x08, 0x0A],
10116            "high-reg SUBS must be 32-bit SUBS.W (EBBA 0A08); got {subs:02X?}"
10117        );
10118    }
10119
10120    /// #184 (sibling of #180): 16-bit CMN (T1) only encodes R0-R7. High registers
10121    /// must use 32-bit CMN.W, not the corrupt truncated 16-bit form.
10122    #[test]
10123    fn test_encode_thumb_cmn_high_reg_uses_cmn_w_184() {
10124        let encoder = ArmEncoder::new_thumb2();
10125
10126        // cmn r10, r8  → CMN.W = EB1A 0F08 (ADD.W S=1, Rd=PC discarded).
10127        let cmn = encoder
10128            .encode(&ArmOp::Cmn {
10129                rn: Reg::R10,
10130                op2: Operand2::Reg(Reg::R8),
10131            })
10132            .unwrap();
10133        assert_eq!(
10134            cmn,
10135            vec![0x1A, 0xEB, 0x08, 0x0F],
10136            "high-reg CMN must be 32-bit CMN.W (EB1A 0F08); got {cmn:02X?}"
10137        );
10138
10139        // Low registers stay 16-bit: cmn r1, r2 = 0x42D1.
10140        let lo = encoder
10141            .encode(&ArmOp::Cmn {
10142                rn: Reg::R1,
10143                op2: Operand2::Reg(Reg::R2),
10144            })
10145            .unwrap();
10146        assert_eq!(
10147            lo.len(),
10148            2,
10149            "low-reg CMN should remain 16-bit, got {lo:02X?}"
10150        );
10151        assert_eq!(lo, vec![0xD1, 0x42], "low-reg CMN bytes wrong: {lo:02X?}");
10152    }
10153
10154    /// #185 regression: feeding PC (R15) as a data operand to a Thumb-2 op that
10155    /// guards its registers must return Err, not panic under debug-assertions.
10156    /// (Synth never emits PC here; the fuzz harness requires encode() be total.)
10157    #[test]
10158    fn test_encode_pc_operand_returns_err_not_panic_185() {
10159        let encoder = ArmEncoder::new_thumb2();
10160        for op in [
10161            ArmOp::Sdiv {
10162                rd: Reg::PC,
10163                rn: Reg::R0,
10164                rm: Reg::R1,
10165            },
10166            ArmOp::Udiv {
10167                rd: Reg::R0,
10168                rn: Reg::PC,
10169                rm: Reg::R1,
10170            },
10171            ArmOp::Sdiv {
10172                rd: Reg::R0,
10173                rn: Reg::R1,
10174                rm: Reg::PC,
10175            },
10176        ] {
10177            let r = encoder.encode(&op);
10178            assert!(
10179                r.is_err(),
10180                "encode({op:?}) must return Err for a PC operand, got {r:?}"
10181            );
10182        }
10183        // Valid registers still encode fine (no false rejection).
10184        assert!(
10185            encoder
10186                .encode(&ArmOp::Sdiv {
10187                    rd: Reg::R0,
10188                    rn: Reg::R1,
10189                    rm: Reg::R2
10190                })
10191                .is_ok()
10192        );
10193    }
10194
10195    #[test]
10196    fn test_encode_nop_arm32() {
10197        let encoder = ArmEncoder::new_arm32();
10198        let code = encoder.encode(&ArmOp::Nop).unwrap();
10199
10200        assert_eq!(code.len(), 4); // ARM32 instructions are 4 bytes
10201        assert_eq!(code, vec![0x00, 0x00, 0xA0, 0xE1]); // MOV R0, R0
10202    }
10203
10204    #[test]
10205    fn test_encode_nop_thumb() {
10206        let encoder = ArmEncoder::new_thumb2();
10207        let code = encoder.encode(&ArmOp::Nop).unwrap();
10208
10209        assert_eq!(code.len(), 2); // Thumb instructions are 2 bytes
10210        assert_eq!(code, vec![0x00, 0xBF]); // NOP
10211    }
10212
10213    #[test]
10214    fn test_encode_mov_immediate_arm32() {
10215        let encoder = ArmEncoder::new_arm32();
10216        let op = ArmOp::Mov {
10217            rd: Reg::R0,
10218            op2: Operand2::Imm(42),
10219        };
10220
10221        let code = encoder.encode(&op).unwrap();
10222        assert_eq!(code.len(), 4);
10223
10224        // Verify it's a MOV instruction (bits should have immediate flag set)
10225        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10226        assert_eq!(instr & 0x0E000000, 0x02000000); // Check I bit is set
10227    }
10228
10229    #[test]
10230    fn test_encode_add_registers_arm32() {
10231        let encoder = ArmEncoder::new_arm32();
10232        let op = ArmOp::Add {
10233            rd: Reg::R0,
10234            rn: Reg::R1,
10235            op2: Operand2::Reg(Reg::R2),
10236        };
10237
10238        let code = encoder.encode(&op).unwrap();
10239        assert_eq!(code.len(), 4);
10240
10241        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10242        // Verify it's an ADD instruction with correct opcode
10243        assert_eq!(instr & 0x0FE00000, 0x00800000);
10244    }
10245
10246    /// #350 — `encode_thumb32_add_imm` must lower an out-of-range immediate
10247    /// (> 0xFFF) to a legal MOVW(/MOVT) + ADD.W-register sequence instead of
10248    /// erroring. The small-imm fast path (imm <= 0xFFF) stays byte-identical.
10249    #[test]
10250    fn test_encode_add_imm_large_350() {
10251        let enc = ArmEncoder::new_thumb2();
10252
10253        // --- Fast path: imm <= 0xFFF is a single 4-byte instruction, and the
10254        // VALUE must be right (#681: this test used to assert only the length,
10255        // letting the raw-packed T3 mis-encoding of 0x123 pass CI). 0x123 is
10256        // not ThumbExpandImm-representable, so it must be ADDW (T4, plain
10257        // imm12): clang `addw r0, r1, #0x123` = f201 0023.
10258        let small = enc
10259            .encode_thumb32_add_imm(&Reg::R0, &Reg::R1, 0x123)
10260            .unwrap();
10261        assert_eq!(small, vec![0x01, 0xF2, 0x23, 0x10], "ADDW r0, r1, #0x123");
10262
10263        // helper: decode a Thumb-2 MOVW/MOVT halfword pair back to its imm16
10264        fn movx_imm16(b: &[u8]) -> u32 {
10265            let hw1 = u16::from_le_bytes([b[0], b[1]]) as u32;
10266            let hw2 = u16::from_le_bytes([b[2], b[3]]) as u32;
10267            let imm4 = hw1 & 0xF;
10268            let i = (hw1 >> 10) & 1;
10269            let imm3 = (hw2 >> 12) & 0x7;
10270            let imm8 = hw2 & 0xFF;
10271            (imm4 << 12) | (i << 11) | (imm3 << 8) | imm8
10272        }
10273        fn movx_rd(b: &[u8]) -> u32 {
10274            (u16::from_le_bytes([b[2], b[3]]) as u32 >> 8) & 0xF
10275        }
10276
10277        // --- rd != rn: scratch is rd. imm = 70000 = 0x11170 needs MOVW+MOVT. ---
10278        // 0x11170: lo16 = 0x1170, hi16 = 0x0001
10279        let seq = enc
10280            .encode_thumb32_add_imm(&Reg::R12, &Reg::R0, 70000)
10281            .unwrap();
10282        assert_eq!(seq.len(), 12, "MOVW + MOVT + ADD = 12 bytes");
10283        // MOVW r12, #0x1170
10284        assert_eq!(u16::from_le_bytes([seq[0], seq[1]]) & 0xFBF0, 0xF240);
10285        assert_eq!(movx_rd(&seq[0..4]), 12);
10286        assert_eq!(movx_imm16(&seq[0..4]), 0x1170);
10287        // MOVT r12, #0x0001
10288        assert_eq!(u16::from_le_bytes([seq[4], seq[5]]) & 0xFBF0, 0xF2C0);
10289        assert_eq!(movx_rd(&seq[4..8]), 12);
10290        assert_eq!(movx_imm16(&seq[4..8]), 0x0001);
10291        // ADD.W r12, r0, r12  (EB00 | rn=0 ; rd=12, rm=12)
10292        let add1 = u16::from_le_bytes([seq[8], seq[9]]) as u32;
10293        let add2 = u16::from_le_bytes([seq[10], seq[11]]) as u32;
10294        assert_eq!(add1 & 0xFFF0, 0xEB00);
10295        assert_eq!(add1 & 0xF, 0); // rn = r0
10296        assert_eq!((add2 >> 8) & 0xF, 12); // rd = r12
10297        assert_eq!(add2 & 0xF, 12); // rm = scratch = r12
10298        // The materialized scratch must reconstruct exactly 70000.
10299        assert_eq!(
10300            (movx_imm16(&seq[4..8]) << 16) | movx_imm16(&seq[0..4]),
10301            70000
10302        );
10303
10304        // --- imm <= 0xFFFF: MOVT is skipped (MOVW + ADD = 8 bytes). ---
10305        let seq16 = enc
10306            .encode_thumb32_add_imm(&Reg::R3, &Reg::R0, 0xABCD)
10307            .unwrap();
10308        assert_eq!(seq16.len(), 8, "imm <= 0xFFFF skips MOVT");
10309        assert_eq!(movx_imm16(&seq16[0..4]), 0xABCD);
10310        assert_eq!(movx_rd(&seq16[0..4]), 3); // scratch = rd = r3
10311
10312        // --- rd == rn (in-place add): scratch must be R12, not rd. ---
10313        // imm = 0x12345: lo16 = 0x2345, hi16 = 0x0001
10314        let inplace = enc
10315            .encode_thumb32_add_imm(&Reg::R5, &Reg::R5, 0x12345)
10316            .unwrap();
10317        assert_eq!(inplace.len(), 12);
10318        assert_eq!(movx_rd(&inplace[0..4]), 12, "rd==rn must use R12 scratch");
10319        assert_eq!(
10320            (movx_imm16(&inplace[4..8]) << 16) | movx_imm16(&inplace[0..4]),
10321            0x12345
10322        );
10323        // ADD.W r5, r5, r12 — rm must be the scratch (12), never rn.
10324        let ip_add2 = u16::from_le_bytes([inplace[10], inplace[11]]) as u32;
10325        assert_eq!(ip_add2 & 0xF, 12);
10326        assert_eq!((ip_add2 >> 8) & 0xF, 5);
10327    }
10328
10329    /// #681 — `encode_thumb32_add_imm` packed a RAW immediate into the T3
10330    /// ADD.W `i:imm3:imm8` field, which is a ThumbExpandImm MODIFIED immediate:
10331    /// ThumbExpandImm(0x200) = 0, ThumbExpandImm(0x400) = 0x8000_0000. Every
10332    /// dynamic-address load/store with a static offset in 0x100..=0xFFF
10333    /// computed a wrong address (and bypassed --safety-bounds software: the
10334    /// guard checked the intended address, the access used the mis-encoded
10335    /// one). Fix: imm <= 0xFF keeps T3 (raw == expanded there, bit-identical);
10336    /// 0x100..=0xFFF uses ADDW (T4, plain imm12) — same lowering
10337    /// `encode_thumb32_add` already uses per #253.
10338    ///
10339    /// Every expected byte sequence below is pinned against clang
10340    /// (`-target thumbv7m-none-eabi`) output, bit-for-bit (#544 pattern).
10341    #[test]
10342    fn test_encode_add_imm_thumb_expand_681() {
10343        let enc = ArmEncoder::new_thumb2();
10344        let add = |rd: &Reg, rn: &Reg, imm: u32| enc.encode_thumb32_add_imm(rd, rn, imm).unwrap();
10345
10346        // imm <= 0xFF stays T3 ADD.W (raw == ThumbExpandImm-expanded):
10347        // clang: add.w r12, r0, #0xff  = f100 0cff
10348        assert_eq!(add(&Reg::R12, &Reg::R0, 0xFF), vec![0x00, 0xF1, 0xFF, 0x0C]);
10349
10350        // 0x100..=0xFFF must be ADDW (T4, plain imm12). The old T3 raw packing
10351        // decoded as +0 (0x100/0x200), +0x80000000 (0x400), etc.
10352        // clang: addw r12, r0, #0x100 = f200 1c00
10353        assert_eq!(
10354            add(&Reg::R12, &Reg::R0, 0x100),
10355            vec![0x00, 0xF2, 0x00, 0x1C]
10356        );
10357        // clang: addw r12, r0, #0x104 = f200 1c04
10358        assert_eq!(
10359            add(&Reg::R12, &Reg::R0, 0x104),
10360            vec![0x00, 0xF2, 0x04, 0x1C]
10361        );
10362        // clang: addw r12, r0, #0x200 = f200 2c00
10363        assert_eq!(
10364            add(&Reg::R12, &Reg::R0, 0x200),
10365            vec![0x00, 0xF2, 0x00, 0x2C]
10366        );
10367        // clang: addw r12, r0, #0x3fc = f200 3cfc
10368        assert_eq!(
10369            add(&Reg::R12, &Reg::R0, 0x3FC),
10370            vec![0x00, 0xF2, 0xFC, 0x3C]
10371        );
10372        // clang: addw r12, r0, #0x400 = f200 4c00
10373        assert_eq!(
10374            add(&Reg::R12, &Reg::R0, 0x400),
10375            vec![0x00, 0xF2, 0x00, 0x4C]
10376        );
10377        // clang: addw r12, r0, #0xfff = f600 7cff
10378        assert_eq!(
10379            add(&Reg::R12, &Reg::R0, 0xFFF),
10380            vec![0x00, 0xF6, 0xFF, 0x7C]
10381        );
10382        // Non-scratch rd/rn — clang: addw r1, r2, #0x104 = f202 1104
10383        assert_eq!(add(&Reg::R1, &Reg::R2, 0x104), vec![0x02, 0xF2, 0x04, 0x11]);
10384    }
10385
10386    /// #681 class audit — the T2 RSB and AND.W immediate fields are also
10387    /// ThumbExpandImm-coded and were raw-packed. Neither has a plain-imm12
10388    /// (T4-style) form, so a non-representable immediate must Err loudly
10389    /// (#253/#255/#378 class: never silently encode a different constant).
10390    /// Existing emitters only use representable values (RSB #32, AND #0x3F),
10391    /// pinned here bit-for-bit against clang.
10392    #[test]
10393    fn test_rsb_and_imm_thumb_expand_gate_681() {
10394        let enc = ArmEncoder::new_thumb2();
10395
10396        // clang: rsb.w r3, r2, #0x20 = f1c2 0320 — byte-identical to before.
10397        let rsb = enc
10398            .encode(&ArmOp::Rsb {
10399                rd: Reg::R3,
10400                rn: Reg::R2,
10401                imm: 32,
10402            })
10403            .unwrap();
10404        assert_eq!(rsb, vec![0xC2, 0xF1, 0x20, 0x03]);
10405
10406        // 0x101 is not ThumbExpandImm-representable -> must Err, not mis-encode.
10407        assert!(
10408            enc.encode(&ArmOp::Rsb {
10409                rd: Reg::R3,
10410                rn: Reg::R2,
10411                imm: 0x101,
10412            })
10413            .is_err(),
10414            "non-ThumbExpandImm RSB immediate must Err"
10415        );
10416
10417        // clang: and r4, r4, #0x3f = f004 043f — byte-identical to before.
10418        let and = enc.encode_thumb32_and_imm_raw(4, 4, 0x3F).unwrap();
10419        assert_eq!(and, vec![0x04, 0xF0, 0x3F, 0x04]);
10420        assert!(
10421            enc.encode_thumb32_and_imm_raw(4, 4, 0x101).is_err(),
10422            "non-ThumbExpandImm AND immediate must Err"
10423        );
10424
10425        // A32 RSB: imm12 is a rotate:imm8 modified immediate; > 0xFF used to be
10426        // silently masked to `imm & 0xFF` (#378 masking class) -> must Err.
10427        let a32 = ArmEncoder::new_arm32();
10428        assert!(
10429            a32.encode(&ArmOp::Rsb {
10430                rd: Reg::R3,
10431                rn: Reg::R2,
10432                imm: 0x120,
10433            })
10434            .is_err(),
10435            "A32 RSB immediate > 0xFF must Err, not mask"
10436        );
10437        // imm 32 (the only value real codegen emits) still encodes.
10438        assert!(
10439            a32.encode(&ArmOp::Rsb {
10440                rd: Reg::R3,
10441                rn: Reg::R2,
10442                imm: 32,
10443            })
10444            .is_ok()
10445        );
10446    }
10447
10448    /// #350 follow-up — the `encoder_no_panic` fuzz harness drives the encoder
10449    /// with ARBITRARY registers, including the one case the in-place lowering
10450    /// cannot serve: rd==rn==R12. There the scratch (R12, the reserved encoder
10451    /// register) would alias Rn and clobber it before the ADD reads it. The
10452    /// encoder contract (#180/#185) is Ok-or-Err, never a panic — so this must
10453    /// return Err, not assert. (Real codegen never emits rd==rn==R12 because R12
10454    /// is non-allocatable; this guards only the fuzz/adversarial path.)
10455    #[test]
10456    fn test_encode_add_imm_large_rd_rn_r12_errs_not_panics_350() {
10457        let enc = ArmEncoder::new_thumb2();
10458        // Out-of-range imm with rd==rn==R12: no free scratch -> Err.
10459        let r = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 70000);
10460        assert!(
10461            r.is_err(),
10462            "rd==rn==R12 with out-of-range imm must Err (no free scratch), got {r:?}"
10463        );
10464        // Small imm with rd==rn==R12 still takes the single-instruction fast path
10465        // (no scratch needed) and must succeed — the guard is scoped to the
10466        // out-of-range lowering only.
10467        let small = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 0x10);
10468        assert!(small.is_ok(), "small imm needs no scratch, must stay Ok");
10469    }
10470
10471    /// #378 — `encode_operand2` (ARM32 data-processing operand) must FAIL
10472    /// HONESTLY on an immediate that is not a valid rotated immediate, rather
10473    /// than silently masking it to `imm & 0xFF` and emitting a WRONG
10474    /// instruction. `0x1FF` has 9 set bits, so it cannot come from rotating an
10475    /// 8-bit imm8 — non-encodable. Real codegen materializes large constants via
10476    /// MOVW/MOVT; this guards the encoder's Ok-or-Err contract (#180/#185)
10477    /// directly. It is an Err (not a panic) so the `encoder_no_panic` fuzz
10478    /// harness — which drives arbitrary operands — still passes.
10479    #[test]
10480    fn test_encode_operand2_non_rotatable_imm_errs_not_masks_378() {
10481        let enc = ArmEncoder::new_arm32();
10482        let bad = enc.encode(&ArmOp::Add {
10483            rd: Reg::R0,
10484            rn: Reg::R1,
10485            op2: Operand2::Imm(0x1FF),
10486        });
10487        assert!(
10488            bad.is_err(),
10489            "non-rotatable ARM32 immediate 0x1FF must Err (was silently masked \
10490             to 0xFF), got {bad:?}"
10491        );
10492        // A representable rotated immediate still encodes fine (regression guard).
10493        let ok = enc.encode(&ArmOp::Add {
10494            rd: Reg::R0,
10495            rn: Reg::R1,
10496            op2: Operand2::Imm(0xFF),
10497        });
10498        assert!(
10499            ok.is_ok(),
10500            "0xFF is a valid rotated immediate, must stay Ok"
10501        );
10502    }
10503
10504    #[test]
10505    fn test_encode_ldr_arm32() {
10506        let encoder = ArmEncoder::new_arm32();
10507        let op = ArmOp::Ldr {
10508            rd: Reg::R0,
10509            addr: MemAddr::imm(Reg::R1, 4),
10510        };
10511
10512        let code = encoder.encode(&op).unwrap();
10513        assert_eq!(code.len(), 4);
10514
10515        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10516        // Verify load bit is set
10517        assert_eq!(instr & 0x00100000, 0x00100000);
10518    }
10519
10520    #[test]
10521    fn test_encode_str_arm32() {
10522        let encoder = ArmEncoder::new_arm32();
10523        let op = ArmOp::Str {
10524            rd: Reg::R0,
10525            addr: MemAddr::imm(Reg::SP, 0),
10526        };
10527
10528        let code = encoder.encode(&op).unwrap();
10529        assert_eq!(code.len(), 4);
10530    }
10531
10532    #[test]
10533    fn test_encode_branch_arm32() {
10534        let encoder = ArmEncoder::new_arm32();
10535        let op = ArmOp::Bl {
10536            label: "main".to_string(),
10537        };
10538
10539        let code = encoder.encode(&op).unwrap();
10540        assert_eq!(code.len(), 4);
10541
10542        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10543        // Verify BL opcode
10544        assert_eq!(instr & 0x0F000000, 0x0B000000);
10545    }
10546
10547    /// #1040: the A32 BL relocatable placeholder must carry an embedded addend
10548    /// of -8 so an R_ARM_CALL nets to exactly the symbol S — the A32 twin of
10549    /// the Thumb #167/#174 test above. A32 `BL` computes
10550    /// `target = P + 8 + (imm24 << 2)`, so:
10551    ///   - `eb000000` (imm24 = 0, addend 0) lands at S+8, two instructions
10552    ///     past the callee entry. This is what synth emitted before #1040, and
10553    ///     combined with the mislabelled R_ARM_THM_CALL a real linker
10554    ///     (`arm-none-eabi-ld`) corrupted the word to `eaca0000` — opcode
10555    ///     `eb` (BL) flipped to `ea` (B), so LR was never set at all.
10556    ///   - `ebfffffe` (imm24 = -2, offset -8) is `bl <self>` and nets to S.
10557    ///     This is exactly what `arm-none-eabi-as -march=armv7-r` emits for
10558    ///     `bl <extern>`, verified directly rather than derived from the ABI.
10559    #[test]
10560    fn test_encode_arm32_bl_placeholder_addend_1040() {
10561        let encoder = ArmEncoder::new_arm32();
10562        let code = encoder
10563            .encode(&ArmOp::Bl {
10564                label: "callee".to_string(),
10565            })
10566            .unwrap();
10567        assert_eq!(code.len(), 4, "A32 BL is one 32-bit word");
10568        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10569        assert_eq!(
10570            instr, 0xEBFF_FFFE,
10571            "A32 BL placeholder must be `ebfffffe` (gas's `bl <extern>`), not              `eb000000` — a 0 addend resolves two instructions past the callee              entry (#1040)"
10572        );
10573        // Spell the addend out so a future edit cannot satisfy the literal by
10574        // accident: sign-extended imm24, word-scaled, plus the +8 pipeline
10575        // bias, must be exactly 0 (branch-to-self).
10576        let imm24 = instr & 0x00FF_FFFF;
10577        let signed = ((imm24 as i32) << 8) >> 8;
10578        assert_eq!(
10579            8 + (signed << 2),
10580            0,
10581            "placeholder must branch to itself so the REL addend is -8"
10582        );
10583    }
10584
10585    /// Regression test for #167 + #174: the Thumb-2 BL relocatable placeholder
10586    /// must carry a -4 addend so an R_ARM_THM_CALL nets to exactly the symbol S.
10587    /// The correct encoding is what `gas` emits for `bl <extern>`: f7ff fffe
10588    /// (hw1=0xF7FF, hw2=0xFFFE), little-endian bytes FF F7 FE FF.
10589    ///   - 0xD000 (J1=J2=0) → ~+0x600000 garbage addend: `bl c0000c` / truncated
10590    ///     to fit (#167).
10591    ///   - 0xF800 (addend 0) → lands at S+4, one instruction past the callee
10592    ///     entry (#174).
10593    ///   - 0xFFFE (addend -4) → lands at S. Correct.
10594    #[test]
10595    fn test_encode_thumb_bl_placeholder_addend_167_174() {
10596        let encoder = ArmEncoder::new_thumb2();
10597        let op = ArmOp::Bl {
10598            label: "callee".to_string(),
10599        };
10600
10601        let code = encoder.encode(&op).unwrap();
10602        assert_eq!(code.len(), 4, "Thumb-2 BL is 32-bit");
10603
10604        let hw1 = u16::from_le_bytes([code[0], code[1]]);
10605        let hw2 = u16::from_le_bytes([code[2], code[3]]);
10606        assert_eq!(hw1, 0xF7FF, "BL first halfword (matches gas `bl <extern>`)");
10607        assert_eq!(
10608            hw2, 0xFFFE,
10609            "BL second halfword must be 0xFFFE (-4 addend → nets to S), not 0xF800 (→ S+4, #174) or 0xD000 (#167)"
10610        );
10611        assert_ne!(hw2, 0xF800, "0xF800 (addend 0) lands at S+4 (#174)");
10612        assert_ne!(hw2, 0xD000, "0xD000 bakes in a ~+0x600000 addend (#167)");
10613    }
10614
10615    /// #740: the Thumb-2 32-bit B<cond>.W (encoding T3) must pack the
10616    /// HALFWORD offset directly into S:J2:J1:imm6:imm11 — the byte offset is
10617    /// SignExtend(S:J2:J1:imm6:imm11:'0'). The old arm packed
10618    /// `halfword_offset >> 1`, HALVING every wide conditional branch's
10619    /// displacement: gust_poll's loop-head `br_if` to an outer block end
10620    /// landed mid-shape (a spurious state write + spurious calls on the
10621    /// empty-budget path). Narrow (16-bit) B<cond> was unaffected — only
10622    /// spans > 254 bytes hit the bug. Bytes cross-checked against the llvm
10623    /// disassembler (`bne.w #0x224` = f040 8112).
10624    #[test]
10625    fn test_encode_thumb_bcond_wide_t3_halfword_offset_740() {
10626        use synth_synthesis::Condition;
10627        let encoder = ArmEncoder::new_thumb2();
10628
10629        // gust_poll's loop-head edge: NE, +0x112 halfwords (+0x224 bytes).
10630        let code = encoder
10631            .encode(&ArmOp::BCondOffset {
10632                cond: Condition::NE,
10633                offset: 0x112,
10634            })
10635            .unwrap();
10636        assert_eq!(code.len(), 4, "offset beyond ±127 halfwords must be wide");
10637        let hw1 = u16::from_le_bytes([code[0], code[1]]);
10638        let hw2 = u16::from_le_bytes([code[2], code[3]]);
10639        assert_eq!(hw1, 0xF040, "T3 hw1: 1111 0 S=0 cond=NE imm6=0");
10640        assert_eq!(
10641            hw2, 0x8112,
10642            "T3 hw2 imm11 must carry halfword offset bits [10:0] directly — \
10643             0x8089 (offset>>1) is the halved #740 miscompile"
10644        );
10645
10646        // Backward wide branch: EQ, -0x100 halfwords. S=1, J2=J1=1,
10647        // imm6=0b111111, imm11=0x700 → f43f af00.
10648        let code = encoder
10649            .encode(&ArmOp::BCondOffset {
10650                cond: Condition::EQ,
10651                offset: -0x100,
10652            })
10653            .unwrap();
10654        assert_eq!(code.len(), 4);
10655        let hw1 = u16::from_le_bytes([code[0], code[1]]);
10656        let hw2 = u16::from_le_bytes([code[2], code[3]]);
10657        assert_eq!(hw1, 0xF43F, "T3 hw1: S=1, cond=EQ, imm6=0x3F");
10658        assert_eq!(hw2, 0xAF00, "T3 hw2: J1=1 J2=1 imm11=0x700");
10659
10660        // Narrow encoding stays byte-identical (in-range offsets untouched).
10661        let code = encoder
10662            .encode(&ArmOp::BCondOffset {
10663                cond: Condition::EQ,
10664                offset: 5,
10665            })
10666            .unwrap();
10667        assert_eq!(code, vec![0x05, 0xD0], "narrow B<cond> unchanged");
10668
10669        // Out of the signed 20-bit T3 range: loud Err, never a truncated jump.
10670        assert!(
10671            encoder
10672                .encode(&ArmOp::BCondOffset {
10673                    cond: Condition::NE,
10674                    offset: 1 << 19,
10675                })
10676                .is_err(),
10677            "out-of-range T3 offset must be a loud decline"
10678        );
10679    }
10680
10681    #[test]
10682    fn test_encode_sequence() {
10683        let encoder = ArmEncoder::new_arm32();
10684        let ops = vec![
10685            ArmOp::Mov {
10686                rd: Reg::R0,
10687                op2: Operand2::Imm(42),
10688            },
10689            ArmOp::Mov {
10690                rd: Reg::R1,
10691                op2: Operand2::Imm(10),
10692            },
10693            ArmOp::Add {
10694                rd: Reg::R2,
10695                rn: Reg::R0,
10696                op2: Operand2::Reg(Reg::R1),
10697            },
10698        ];
10699
10700        let code = encoder.encode_sequence(&ops).unwrap();
10701        assert_eq!(code.len(), 12); // 3 instructions * 4 bytes
10702    }
10703
10704    #[test]
10705    fn test_reg_to_bits() {
10706        assert_eq!(reg_to_bits(&Reg::R0), 0);
10707        assert_eq!(reg_to_bits(&Reg::R7), 7);
10708        assert_eq!(reg_to_bits(&Reg::SP), 13);
10709        assert_eq!(reg_to_bits(&Reg::LR), 14);
10710        assert_eq!(reg_to_bits(&Reg::PC), 15);
10711    }
10712
10713    #[test]
10714    fn test_encode_bitwise_operations() {
10715        let encoder = ArmEncoder::new_arm32();
10716
10717        let and_op = ArmOp::And {
10718            rd: Reg::R0,
10719            rn: Reg::R1,
10720            op2: Operand2::Reg(Reg::R2),
10721        };
10722        let and_code = encoder.encode(&and_op).unwrap();
10723        assert_eq!(and_code.len(), 4);
10724
10725        let orr_op = ArmOp::Orr {
10726            rd: Reg::R0,
10727            rn: Reg::R1,
10728            op2: Operand2::Reg(Reg::R2),
10729        };
10730        let orr_code = encoder.encode(&orr_op).unwrap();
10731        assert_eq!(orr_code.len(), 4);
10732
10733        let eor_op = ArmOp::Eor {
10734            rd: Reg::R0,
10735            rn: Reg::R1,
10736            op2: Operand2::Reg(Reg::R2),
10737        };
10738        let eor_code = encoder.encode(&eor_op).unwrap();
10739        assert_eq!(eor_code.len(), 4);
10740    }
10741
10742    // === Thumb-2 32-bit encoding tests ===
10743
10744    #[test]
10745    fn test_encode_sdiv_thumb2() {
10746        let encoder = ArmEncoder::new_thumb2();
10747        let op = ArmOp::Sdiv {
10748            rd: Reg::R0,
10749            rn: Reg::R1,
10750            rm: Reg::R2,
10751        };
10752
10753        let code = encoder.encode(&op).unwrap();
10754        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10755
10756        // SDIV R0, R1, R2: 0xFB91 0xF0F2
10757        // First halfword: 0xFB90 | Rn(1) = 0xFB91
10758        // Second halfword: 0xF0F0 | Rd(0)<<8 | Rm(2) = 0xF0F2
10759        // Little-endian: [0x91, 0xFB, 0xF2, 0xF0]
10760        assert_eq!(code[0], 0x91);
10761        assert_eq!(code[1], 0xFB);
10762        assert_eq!(code[2], 0xF2);
10763        assert_eq!(code[3], 0xF0);
10764    }
10765
10766    #[test]
10767    fn test_encode_udiv_thumb2() {
10768        let encoder = ArmEncoder::new_thumb2();
10769        let op = ArmOp::Udiv {
10770            rd: Reg::R0,
10771            rn: Reg::R1,
10772            rm: Reg::R2,
10773        };
10774
10775        let code = encoder.encode(&op).unwrap();
10776        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10777
10778        // UDIV R0, R1, R2: 0xFBB1 0xF0F2
10779        // Little-endian: [0xB1, 0xFB, 0xF2, 0xF0]
10780        assert_eq!(code[0], 0xB1);
10781        assert_eq!(code[1], 0xFB);
10782        assert_eq!(code[2], 0xF2);
10783        assert_eq!(code[3], 0xF0);
10784    }
10785
10786    #[test]
10787    fn test_encode_mul_thumb2() {
10788        let encoder = ArmEncoder::new_thumb2();
10789        let op = ArmOp::Mul {
10790            rd: Reg::R0,
10791            rn: Reg::R1,
10792            rm: Reg::R2,
10793        };
10794
10795        let code = encoder.encode(&op).unwrap();
10796        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10797    }
10798
10799    #[test]
10800    fn test_encode_and_thumb2() {
10801        let encoder = ArmEncoder::new_thumb2();
10802        let op = ArmOp::And {
10803            rd: Reg::R0,
10804            rn: Reg::R1,
10805            op2: Operand2::Reg(Reg::R2),
10806        };
10807
10808        let code = encoder.encode(&op).unwrap();
10809        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10810    }
10811
10812    #[test]
10813    fn test_encode_lsl_thumb2_low_regs() {
10814        let encoder = ArmEncoder::new_thumb2();
10815        let op = ArmOp::Lsl {
10816            rd: Reg::R0,
10817            rn: Reg::R1,
10818            shift: 5,
10819        };
10820
10821        let code = encoder.encode(&op).unwrap();
10822        assert_eq!(code.len(), 2); // 16-bit for low registers
10823    }
10824
10825    #[test]
10826    fn test_encode_clz_thumb2() {
10827        let encoder = ArmEncoder::new_thumb2();
10828        let op = ArmOp::Clz {
10829            rd: Reg::R0,
10830            rm: Reg::R1,
10831        };
10832
10833        let code = encoder.encode(&op).unwrap();
10834        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10835    }
10836
10837    #[test]
10838    fn test_encode_bx_thumb2() {
10839        let encoder = ArmEncoder::new_thumb2();
10840        let op = ArmOp::Bx { rm: Reg::LR };
10841
10842        let code = encoder.encode(&op).unwrap();
10843        assert_eq!(code.len(), 2); // 16-bit instruction
10844
10845        // BX LR: 0x4770
10846        assert_eq!(code, vec![0x70, 0x47]);
10847    }
10848
10849    // ========================================================================
10850    // f32 pseudo-op encoding tests
10851    // ========================================================================
10852
10853    #[test]
10854    fn test_encode_f32_abs_arm32() {
10855        let encoder = ArmEncoder::new_arm32();
10856        let op = ArmOp::F32Abs {
10857            sd: VfpReg::S0,
10858            sm: VfpReg::S2,
10859        };
10860        let code = encoder.encode(&op).unwrap();
10861        assert_eq!(code.len(), 4); // Single VFP instruction
10862    }
10863
10864    #[test]
10865    fn test_encode_f32_neg_arm32() {
10866        let encoder = ArmEncoder::new_arm32();
10867        let op = ArmOp::F32Neg {
10868            sd: VfpReg::S0,
10869            sm: VfpReg::S2,
10870        };
10871        let code = encoder.encode(&op).unwrap();
10872        assert_eq!(code.len(), 4);
10873    }
10874
10875    #[test]
10876    fn test_encode_f32_sqrt_arm32() {
10877        let encoder = ArmEncoder::new_arm32();
10878        let op = ArmOp::F32Sqrt {
10879            sd: VfpReg::S0,
10880            sm: VfpReg::S2,
10881        };
10882        let code = encoder.encode(&op).unwrap();
10883        assert_eq!(code.len(), 4);
10884    }
10885
10886    #[test]
10887    fn test_encode_f32_ceil_arm32() {
10888        let encoder = ArmEncoder::new_arm32();
10889        let op = ArmOp::F32Ceil {
10890            sd: VfpReg::S0,
10891            sm: VfpReg::S2,
10892        };
10893        let code = encoder.encode(&op).unwrap();
10894        // VMRS + BIC + ORR + VMSR + VCVT.S32.F32 + VMRS + BIC + VMSR + VCVT.F32.S32
10895        assert_eq!(code.len(), 36);
10896    }
10897
10898    #[test]
10899    fn test_encode_f32_floor_thumb2() {
10900        let encoder = ArmEncoder::new_thumb2();
10901        let op = ArmOp::F32Floor {
10902            sd: VfpReg::S0,
10903            sm: VfpReg::S2,
10904        };
10905        let code = encoder.encode(&op).unwrap();
10906        // VMRS + BIC.W + ORR.W + VMSR + VCVT + VMRS + BIC.W + VMSR + VCVT.F32.S32
10907        assert_eq!(code.len(), 36);
10908    }
10909
10910    #[test]
10911    fn test_encode_f32_min_arm32() {
10912        let encoder = ArmEncoder::new_arm32();
10913        let op = ArmOp::F32Min {
10914            sd: VfpReg::S0,
10915            sn: VfpReg::S2,
10916            sm: VfpReg::S4,
10917        };
10918        let code = encoder.encode(&op).unwrap();
10919        assert_eq!(code.len(), 16); // VMOV + VCMP + VMRS + conditional VMOV
10920    }
10921
10922    #[test]
10923    fn test_encode_f32_max_thumb2() {
10924        let encoder = ArmEncoder::new_thumb2();
10925        let op = ArmOp::F32Max {
10926            sd: VfpReg::S0,
10927            sn: VfpReg::S2,
10928            sm: VfpReg::S4,
10929        };
10930        let code = encoder.encode(&op).unwrap();
10931        // VMOV(4) + VCMP(4) + VMRS(4) + IT(2) + VMOV(4) = 18
10932        assert_eq!(code.len(), 18);
10933    }
10934
10935    #[test]
10936    fn test_encode_f32_copysign_arm32() {
10937        let encoder = ArmEncoder::new_arm32();
10938        let op = ArmOp::F32Copysign {
10939            sd: VfpReg::S0,
10940            sn: VfpReg::S2,
10941            sm: VfpReg::S4,
10942        };
10943        let code = encoder.encode(&op).unwrap();
10944        // VMOV + VMOV + AND + BIC + ORR + VMOV = 6 * 4 = 24
10945        assert_eq!(code.len(), 24);
10946    }
10947
10948    // ========================================================================
10949    // f64 encoding tests
10950    // ========================================================================
10951
10952    #[test]
10953    fn test_encode_f64_add_arm32() {
10954        let encoder = ArmEncoder::new_arm32();
10955        let op = ArmOp::F64Add {
10956            dd: VfpReg::D0,
10957            dn: VfpReg::D1,
10958            dm: VfpReg::D2,
10959        };
10960        let code = encoder.encode(&op).unwrap();
10961        assert_eq!(code.len(), 4);
10962        // VADD.F64 D0, D1, D2: check coprocessor is cp11 (0xB)
10963        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10964        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
10965    }
10966
10967    #[test]
10968    fn test_encode_f64_sub_thumb2() {
10969        let encoder = ArmEncoder::new_thumb2();
10970        let op = ArmOp::F64Sub {
10971            dd: VfpReg::D0,
10972            dn: VfpReg::D1,
10973            dm: VfpReg::D2,
10974        };
10975        let code = encoder.encode(&op).unwrap();
10976        assert_eq!(code.len(), 4); // 32-bit VFP as two Thumb halfwords
10977    }
10978
10979    #[test]
10980    fn test_encode_f64_mul_arm32() {
10981        let encoder = ArmEncoder::new_arm32();
10982        let op = ArmOp::F64Mul {
10983            dd: VfpReg::D0,
10984            dn: VfpReg::D1,
10985            dm: VfpReg::D2,
10986        };
10987        let code = encoder.encode(&op).unwrap();
10988        assert_eq!(code.len(), 4);
10989    }
10990
10991    #[test]
10992    fn test_encode_f64_div_arm32() {
10993        let encoder = ArmEncoder::new_arm32();
10994        let op = ArmOp::F64Div {
10995            dd: VfpReg::D0,
10996            dn: VfpReg::D1,
10997            dm: VfpReg::D2,
10998        };
10999        let code = encoder.encode(&op).unwrap();
11000        assert_eq!(code.len(), 4);
11001    }
11002
11003    #[test]
11004    fn test_encode_f64_abs_arm32() {
11005        let encoder = ArmEncoder::new_arm32();
11006        let op = ArmOp::F64Abs {
11007            dd: VfpReg::D0,
11008            dm: VfpReg::D2,
11009        };
11010        let code = encoder.encode(&op).unwrap();
11011        assert_eq!(code.len(), 4);
11012    }
11013
11014    #[test]
11015    fn test_encode_f64_neg_arm32() {
11016        let encoder = ArmEncoder::new_arm32();
11017        let op = ArmOp::F64Neg {
11018            dd: VfpReg::D0,
11019            dm: VfpReg::D2,
11020        };
11021        let code = encoder.encode(&op).unwrap();
11022        assert_eq!(code.len(), 4);
11023    }
11024
11025    #[test]
11026    fn test_encode_f64_sqrt_arm32() {
11027        let encoder = ArmEncoder::new_arm32();
11028        let op = ArmOp::F64Sqrt {
11029            dd: VfpReg::D0,
11030            dm: VfpReg::D2,
11031        };
11032        let code = encoder.encode(&op).unwrap();
11033        assert_eq!(code.len(), 4);
11034    }
11035
11036    #[test]
11037    fn test_encode_f64_load_arm32() {
11038        let encoder = ArmEncoder::new_arm32();
11039        let op = ArmOp::F64Load {
11040            dd: VfpReg::D0,
11041            addr: MemAddr::imm(Reg::R0, 8),
11042        };
11043        let code = encoder.encode(&op).unwrap();
11044        assert_eq!(code.len(), 4);
11045        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11046        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11 for F64
11047        assert_eq!(instr & 0xFF, 2); // offset 8 / 4 = 2
11048    }
11049
11050    #[test]
11051    fn test_encode_f64_store_thumb2() {
11052        let encoder = ArmEncoder::new_thumb2();
11053        let op = ArmOp::F64Store {
11054            dd: VfpReg::D0,
11055            addr: MemAddr::imm(Reg::SP, 0),
11056        };
11057        let code = encoder.encode(&op).unwrap();
11058        assert_eq!(code.len(), 4);
11059    }
11060
11061    #[test]
11062    fn test_encode_f64_compare_arm32() {
11063        let encoder = ArmEncoder::new_arm32();
11064        let op = ArmOp::F64Eq {
11065            rd: Reg::R0,
11066            dn: VfpReg::D0,
11067            dm: VfpReg::D1,
11068        };
11069        let code = encoder.encode(&op).unwrap();
11070        assert_eq!(code.len(), 16); // VCMP + VMRS + MOV #0 + MOVcond #1
11071    }
11072
11073    #[test]
11074    fn test_encode_f64_compare_thumb2() {
11075        let encoder = ArmEncoder::new_thumb2();
11076        let op = ArmOp::F64Lt {
11077            rd: Reg::R0,
11078            dn: VfpReg::D0,
11079            dm: VfpReg::D1,
11080        };
11081        let code = encoder.encode(&op).unwrap();
11082        // VCMP(4) + VMRS(4) + MOVS(2) + IT(2) + MOV(2) = 14
11083        assert_eq!(code.len(), 14);
11084    }
11085
11086    #[test]
11087    fn test_encode_f64_const_arm32() {
11088        let encoder = ArmEncoder::new_arm32();
11089        let op = ArmOp::F64Const {
11090            dd: VfpReg::D0,
11091            value: 3.125,
11092        };
11093        let code = encoder.encode(&op).unwrap();
11094        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
11095        assert_eq!(code.len(), 20);
11096    }
11097
11098    #[test]
11099    fn test_encode_f64_const_thumb2() {
11100        let encoder = ArmEncoder::new_thumb2();
11101        let op = ArmOp::F64Const {
11102            dd: VfpReg::D0,
11103            value: 2.5,
11104        };
11105        let code = encoder.encode(&op).unwrap();
11106        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
11107        assert_eq!(code.len(), 20);
11108    }
11109
11110    #[test]
11111    fn test_encode_f64_convert_i32s_arm32() {
11112        let encoder = ArmEncoder::new_arm32();
11113        let op = ArmOp::F64ConvertI32S {
11114            dd: VfpReg::D0,
11115            rm: Reg::R0,
11116        };
11117        let code = encoder.encode(&op).unwrap();
11118        // VMOV(4) + VCVT(4) = 8
11119        assert_eq!(code.len(), 8);
11120    }
11121
11122    #[test]
11123    fn test_encode_f64_promote_f32_arm32() {
11124        let encoder = ArmEncoder::new_arm32();
11125        let op = ArmOp::F64PromoteF32 {
11126            dd: VfpReg::D0,
11127            sm: VfpReg::S0,
11128        };
11129        let code = encoder.encode(&op).unwrap();
11130        assert_eq!(code.len(), 4); // Single VCVT.F64.F32 instruction
11131    }
11132
11133    #[test]
11134    fn test_encode_f64_promote_f32_thumb2() {
11135        let encoder = ArmEncoder::new_thumb2();
11136        let op = ArmOp::F64PromoteF32 {
11137            dd: VfpReg::D0,
11138            sm: VfpReg::S0,
11139        };
11140        let code = encoder.encode(&op).unwrap();
11141        assert_eq!(code.len(), 4);
11142    }
11143
11144    #[test]
11145    fn test_encode_i32_trunc_f64s_arm32() {
11146        let encoder = ArmEncoder::new_arm32();
11147        let op = ArmOp::I32TruncF64S {
11148            rd: Reg::R0,
11149            dm: VfpReg::D0,
11150        };
11151        let code = encoder.encode(&op).unwrap();
11152        // VCVT(4) + VMOV(4) = 8
11153        assert_eq!(code.len(), 8);
11154    }
11155
11156    #[test]
11157    fn test_encode_f64_reinterpret_i64_arm32() {
11158        let encoder = ArmEncoder::new_arm32();
11159        let op = ArmOp::F64ReinterpretI64 {
11160            dd: VfpReg::D0,
11161            rmlo: Reg::R0,
11162            rmhi: Reg::R1,
11163        };
11164        let code = encoder.encode(&op).unwrap();
11165        assert_eq!(code.len(), 4); // Single VMOV instruction
11166    }
11167
11168    #[test]
11169    fn test_encode_i64_reinterpret_f64_thumb2() {
11170        let encoder = ArmEncoder::new_thumb2();
11171        let op = ArmOp::I64ReinterpretF64 {
11172            rdlo: Reg::R0,
11173            rdhi: Reg::R1,
11174            dm: VfpReg::D0,
11175        };
11176        let code = encoder.encode(&op).unwrap();
11177        assert_eq!(code.len(), 4);
11178    }
11179
11180    #[test]
11181    fn test_encode_f64_trunc_thumb2() {
11182        let encoder = ArmEncoder::new_thumb2();
11183        let op = ArmOp::F64Trunc {
11184            dd: VfpReg::D0,
11185            dm: VfpReg::D1,
11186        };
11187        let code = encoder.encode(&op).unwrap();
11188        // GI-FPU-002 phase 3 (#369): a single VRINTZ.F64 (clang-verified
11189        // vrintz.f64 d0,d1 base) — no more FPSCR dance / S0 clobber.
11190        assert_eq!(code.len(), 4);
11191        assert_eq!(code, vec![0xb6, 0xee, 0xc1, 0x0b]);
11192    }
11193
11194    /// GI-FPU-002 phase 3 (#369): the rewritten f64 tail sequences, byte-exact
11195    /// against clang (`-target thumbv7em-none-eabi -mfpu=fpv5-d16`). Each
11196    /// clobbers ONLY its destination (+R12/flags where noted) — the previous
11197    /// pseudo-ops staged through live S0/R0-R2 (the #615 class) and the
11198    /// min/max/rounding semantics were wrong (ordered IT select returned the
11199    /// wrong operand on NaN/±0; rounding round-tripped through a 32-bit int).
11200    #[test]
11201    fn test_369_f64_tail_thumb2_encodings_match_clang() {
11202        let enc = ArmEncoder::new_thumb2();
11203        // vrintn/vrintp/vrintm.f64 d1, d2 (FE space, never IT'd).
11204        for (op, want) in [
11205            (
11206                ArmOp::F64Nearest {
11207                    dd: VfpReg::D1,
11208                    dm: VfpReg::D2,
11209                },
11210                vec![0xb9, 0xfe, 0x42, 0x1b],
11211            ),
11212            (
11213                ArmOp::F64Ceil {
11214                    dd: VfpReg::D1,
11215                    dm: VfpReg::D2,
11216                },
11217                vec![0xba, 0xfe, 0x42, 0x1b],
11218            ),
11219            (
11220                ArmOp::F64Floor {
11221                    dd: VfpReg::D1,
11222                    dm: VfpReg::D2,
11223                },
11224                vec![0xbb, 0xfe, 0x42, 0x1b],
11225            ),
11226        ] {
11227            assert_eq!(enc.encode(&op).unwrap(), want, "{op:?}");
11228        }
11229        // vcmp.f64 d1,d2 ; vmrs ; vminnm.f64 d0,d1,d2 ; it vs ; vaddvs.f64
11230        let min = enc
11231            .encode(&ArmOp::F64Min {
11232                dd: VfpReg::D0,
11233                dn: VfpReg::D1,
11234                dm: VfpReg::D2,
11235            })
11236            .unwrap();
11237        assert_eq!(
11238            min,
11239            vec![
11240                0xb4, 0xee, 0x42, 0x1b, // vcmp.f64 d1, d2
11241                0xf1, 0xee, 0x10, 0xfa, // vmrs APSR_nzcv, fpscr
11242                0x81, 0xfe, 0x42, 0x0b, // vminnm.f64 d0, d1, d2
11243                0x68, 0xbf, // it vs
11244                0x31, 0xee, 0x02, 0x0b, // vaddvs.f64 d0, d1, d2
11245            ]
11246        );
11247        // vmaxnm variant flips only bit6 of the VMINNM word.
11248        let max = enc
11249            .encode(&ArmOp::F64Max {
11250                dd: VfpReg::D0,
11251                dn: VfpReg::D1,
11252                dm: VfpReg::D2,
11253            })
11254            .unwrap();
11255        assert_eq!(&max[8..12], &[0x81, 0xfe, 0x02, 0x0b]);
11256        // Destination aliasing a source must ERR (the NaN fix-up would read
11257        // a clobbered operand), never encode.
11258        assert!(
11259            enc.encode(&ArmOp::F64Min {
11260                dd: VfpReg::D1,
11261                dn: VfpReg::D1,
11262                dm: VfpReg::D2,
11263            })
11264            .is_err()
11265        );
11266        // copysign d0,(mag)d1,(sign)d2:
11267        // vmov r12,s5 ; cmp.w r12,#0 ; vabs.f64 d0,d1 ; it mi ; vnegmi.f64 d0,d0
11268        let cs = enc
11269            .encode(&ArmOp::F64Copysign {
11270                dd: VfpReg::D0,
11271                dn: VfpReg::D1,
11272                dm: VfpReg::D2,
11273            })
11274            .unwrap();
11275        assert_eq!(
11276            cs,
11277            vec![
11278                0x12, 0xee, 0x90, 0xca, // vmov r12, s5
11279                0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
11280                0xb0, 0xee, 0xc1, 0x0b, // vabs.f64 d0, d1
11281                0x48, 0xbf, // it mi
11282                0xb1, 0xee, 0x40, 0x0b, // vnegmi.f64 d0, d0
11283            ]
11284        );
11285        // f32 copysign s0,(mag)s1,(sign)s2 — the R0-clobber-free rewrite:
11286        // vmov r12,s2 ; cmp.w r12,#0 ; vabs.f32 s0,s1 ; it mi ; vnegmi.f32
11287        let cs32 = enc
11288            .encode(&ArmOp::F32Copysign {
11289                sd: VfpReg::S0,
11290                sn: VfpReg::S1,
11291                sm: VfpReg::S2,
11292            })
11293            .unwrap();
11294        assert_eq!(
11295            cs32,
11296            vec![
11297                0x11, 0xee, 0x10, 0xca, // vmov r12, s2
11298                0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
11299                0xb0, 0xee, 0xe0, 0x0a, // vabs.f32 s0, s1
11300                0x48, 0xbf, // it mi
11301                0xb1, 0xee, 0x40, 0x0a, // vnegmi.f32 s0, s0
11302            ]
11303        );
11304        // i32 -> f64 stages through the DESTINATION's S-alias (never S0) and
11305        // uses the CORRECT signed/unsigned VCVT bases (previously swapped):
11306        // vmov s0,r3 ; vcvt.f64.s32 d0,s0
11307        let conv_s = enc
11308            .encode(&ArmOp::F64ConvertI32S {
11309                dd: VfpReg::D0,
11310                rm: Reg::R3,
11311            })
11312            .unwrap();
11313        assert_eq!(
11314            conv_s,
11315            vec![
11316                0x00, 0xee, 0x10, 0x3a, // vmov s0, r3
11317                0xb8, 0xee, 0xc0, 0x0b, // vcvt.f64.s32 d0, s0
11318            ]
11319        );
11320        let conv_u = enc
11321            .encode(&ArmOp::F64ConvertI32U {
11322                dd: VfpReg::D0,
11323                rm: Reg::R3,
11324            })
11325            .unwrap();
11326        assert_eq!(&conv_u[4..8], &[0xb8, 0xee, 0x40, 0x0b]); // vcvt.f64.u32
11327        // f64 -> i32 stages through the SOURCE's S-alias (never S0):
11328        // vcvt.s32.f64 s2,d1 ; vmov r3,s2
11329        let trunc_s = enc
11330            .encode(&ArmOp::I32TruncF64S {
11331                rd: Reg::R3,
11332                dm: VfpReg::D1,
11333            })
11334            .unwrap();
11335        assert_eq!(
11336            trunc_s,
11337            vec![
11338                0xbd, 0xee, 0xc1, 0x1b, // vcvt.s32.f64 s2, d1
11339                0x11, 0xee, 0x10, 0x3a, // vmov r3, s2
11340            ]
11341        );
11342        let trunc_u = enc
11343            .encode(&ArmOp::I32TruncF64U {
11344                rd: Reg::R3,
11345                dm: VfpReg::D1,
11346            })
11347            .unwrap();
11348        assert_eq!(&trunc_u[0..4], &[0xbc, 0xee, 0xc1, 0x1b]); // vcvt.u32.f64
11349        // f32.demote_f64: vcvt.f32.f64 s1, d2
11350        let demote = enc
11351            .encode(&ArmOp::F32DemoteF64 {
11352                sd: VfpReg::S1,
11353                dm: VfpReg::D2,
11354            })
11355            .unwrap();
11356        assert_eq!(demote, vec![0xf7, 0xee, 0xc2, 0x0b]);
11357    }
11358
11359    #[test]
11360    fn test_encode_f64_min_arm32() {
11361        let encoder = ArmEncoder::new_arm32();
11362        let op = ArmOp::F64Min {
11363            dd: VfpReg::D0,
11364            dn: VfpReg::D1,
11365            dm: VfpReg::D2,
11366        };
11367        let code = encoder.encode(&op).unwrap();
11368        // VMOV + VCMP + VMRS + conditional VMOV = 16
11369        assert_eq!(code.len(), 16);
11370    }
11371
11372    #[test]
11373    fn test_f64_cp11_encoding() {
11374        // Verify that F64 instructions use coprocessor 11 (0xB), not 10 (0xA)
11375        let encoder = ArmEncoder::new_arm32();
11376
11377        // F64Add
11378        let code = encoder
11379            .encode(&ArmOp::F64Add {
11380                dd: VfpReg::D0,
11381                dn: VfpReg::D0,
11382                dm: VfpReg::D0,
11383            })
11384            .unwrap();
11385        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11386        assert_eq!((instr >> 8) & 0xF, 0xB, "F64 should use cp11");
11387
11388        // F32Add for comparison
11389        let code = encoder
11390            .encode(&ArmOp::F32Add {
11391                sd: VfpReg::S0,
11392                sn: VfpReg::S0,
11393                sm: VfpReg::S0,
11394            })
11395            .unwrap();
11396        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11397        assert_eq!((instr >> 8) & 0xF, 0xA, "F32 should use cp10");
11398    }
11399
11400    #[test]
11401    fn test_dreg_encoding_higher_registers() {
11402        let encoder = ArmEncoder::new_arm32();
11403
11404        // Test with D15 (highest register)
11405        let op = ArmOp::F64Add {
11406            dd: VfpReg::D15,
11407            dn: VfpReg::D14,
11408            dm: VfpReg::D13,
11409        };
11410        let code = encoder.encode(&op).unwrap();
11411        assert_eq!(code.len(), 4);
11412
11413        // Verify the register encoding worked (instruction is valid)
11414        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11415        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
11416    }
11417
11418    // ========================================================================
11419    // Control flow encoding tests
11420    // ========================================================================
11421
11422    #[test]
11423    fn test_encode_label_emits_no_bytes() {
11424        let encoder = ArmEncoder::new_thumb2();
11425        let op = ArmOp::Label {
11426            name: ".Lblock_end_0".to_string(),
11427        };
11428        let code = encoder.encode(&op).unwrap();
11429        assert!(code.is_empty(), "Label should emit zero bytes");
11430
11431        let encoder32 = ArmEncoder::new_arm32();
11432        let code32 = encoder32.encode(&op).unwrap();
11433        assert!(
11434            code32.is_empty(),
11435            "Label should emit zero bytes in ARM32 too"
11436        );
11437    }
11438
11439    #[test]
11440    fn test_encode_bcc_eq_thumb2() {
11441        use synth_synthesis::Condition;
11442        let encoder = ArmEncoder::new_thumb2();
11443        let op = ArmOp::Bcc {
11444            cond: Condition::EQ,
11445            label: "target".to_string(),
11446        };
11447        let code = encoder.encode(&op).unwrap();
11448        assert_eq!(code.len(), 2); // 16-bit conditional branch
11449
11450        // BEQ with offset 0: 0xD000 in little-endian
11451        assert_eq!(code, vec![0x00, 0xD0]);
11452    }
11453
11454    #[test]
11455    fn test_encode_bcc_ne_thumb2() {
11456        use synth_synthesis::Condition;
11457        let encoder = ArmEncoder::new_thumb2();
11458        let op = ArmOp::Bcc {
11459            cond: Condition::NE,
11460            label: "target".to_string(),
11461        };
11462        let code = encoder.encode(&op).unwrap();
11463        assert_eq!(code.len(), 2);
11464
11465        // BNE with offset 0: 0xD100 in little-endian
11466        assert_eq!(code, vec![0x00, 0xD1]);
11467    }
11468
11469    #[test]
11470    fn test_encode_bcc_arm32() {
11471        use synth_synthesis::Condition;
11472        let encoder = ArmEncoder::new_arm32();
11473        let op = ArmOp::Bcc {
11474            cond: Condition::EQ,
11475            label: "target".to_string(),
11476        };
11477        let code = encoder.encode(&op).unwrap();
11478        assert_eq!(code.len(), 4); // 32-bit ARM instruction
11479
11480        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11481        // BEQ: cond=0x0, opcode=0xA, offset=0
11482        assert_eq!(instr & 0xF0000000, 0x00000000); // EQ condition
11483        assert_eq!(instr & 0x0F000000, 0x0A000000); // Branch opcode
11484    }
11485
11486    #[test]
11487    fn test_encode_udf_thumb2() {
11488        let encoder = ArmEncoder::new_thumb2();
11489        let op = ArmOp::Udf { imm: 0 };
11490        let code = encoder.encode(&op).unwrap();
11491        assert_eq!(code.len(), 2); // 16-bit
11492
11493        // UDF #0: 0xDE00 in little-endian
11494        assert_eq!(code, vec![0x00, 0xDE]);
11495    }
11496
11497    /// #610: the i64 rot/div/rem expansions must land the result in the
11498    /// selector-assigned rd pair and leave R0-R3 preserved (restored from the
11499    /// fixed-ABI wrapper's save area) — pre-#610 the rot expansion's own
11500    /// `POP {R4}` restored stale scratch OVER the result (rd_lo == R4) and
11501    /// the div/rem expansions ignored their register fields outright.
11502    #[test]
11503    fn test_610_i64_rot_expansion_ends_with_rd_movs_and_restore() {
11504        let encoder = ArmEncoder::new_thumb2();
11505        for op in [
11506            ArmOp::I64Rotl {
11507                rdlo: Reg::R4,
11508                rdhi: Reg::R5,
11509                rnlo: Reg::R0,
11510                rnhi: Reg::R1,
11511                shift: Reg::R2,
11512            },
11513            ArmOp::I64Rotr {
11514                rdlo: Reg::R4,
11515                rdhi: Reg::R5,
11516                rnlo: Reg::R0,
11517                rnhi: Reg::R1,
11518                shift: Reg::R2,
11519            },
11520        ] {
11521            let code = encoder.encode(&op).unwrap();
11522            assert_eq!(code.len(), 102, "register-independent size (estimator pin)");
11523            // Tail: MOV r5, r1 (0x460D); MOV r4, r0 (0x4604); POP {r0..r3}
11524            // (rd pair r4:r5 does not overlap the save area — all 4 restored).
11525            let tail: Vec<u16> = code[code.len() - 12..]
11526                .chunks(2)
11527                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11528                .collect();
11529            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11530        }
11531    }
11532
11533    /// #610: div/rem expansions honor rd and carry the divide-by-zero trap
11534    /// guard (`ORRS R12, R2, R3; BNE +0; UDF #0`) after operand marshaling.
11535    #[test]
11536    fn test_610_i64_div_rem_expansion_guard_and_rd() {
11537        let encoder = ArmEncoder::new_thumb2();
11538        let mk = |which: u8| {
11539            let (rdlo, rdhi, rnlo, rnhi, rmlo, rmhi) =
11540                (Reg::R4, Reg::R5, Reg::R0, Reg::R1, Reg::R2, Reg::R3);
11541            match which {
11542                0 => ArmOp::I64DivU {
11543                    rdlo,
11544                    rdhi,
11545                    rnlo,
11546                    rnhi,
11547                    rmlo,
11548                    rmhi,
11549                    elide_zero_guard: false,
11550                },
11551                1 => ArmOp::I64RemU {
11552                    rdlo,
11553                    rdhi,
11554                    rnlo,
11555                    rnhi,
11556                    rmlo,
11557                    rmhi,
11558                    elide_zero_guard: false,
11559                },
11560                2 => ArmOp::I64DivS {
11561                    rdlo,
11562                    rdhi,
11563                    rnlo,
11564                    rnhi,
11565                    rmlo,
11566                    rmhi,
11567                    elide_zero_guard: false,
11568                    elide_overflow_guard: false,
11569                },
11570                _ => ArmOp::I64RemS {
11571                    rdlo,
11572                    rdhi,
11573                    rnlo,
11574                    rnhi,
11575                    rmlo,
11576                    rmhi,
11577                    elide_zero_guard: false,
11578                },
11579            }
11580        };
11581        for which in 0..4u8 {
11582            let code = encoder.encode(&mk(which)).unwrap();
11583            // Zero-divisor trap guard right after the 26-byte marshal prologue.
11584            let guard: Vec<u16> = code[26..34]
11585                .chunks(2)
11586                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11587                .collect();
11588            assert_eq!(
11589                guard,
11590                vec![0xEA52, 0x0C03, 0xD100, 0xDE00],
11591                "ORRS R12,R2,R3; BNE +0; UDF #0"
11592            );
11593            // Tail: result into rd pair (r5:r4), then restore all of R0-R3.
11594            let tail: Vec<u16> = code[code.len() - 12..]
11595                .chunks(2)
11596                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11597                .collect();
11598            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11599        }
11600    }
11601
11602    /// #610: when rd overlaps R0-R3 the restore must SKIP the result
11603    /// registers (drop the saved caller word) instead of popping over them.
11604    #[test]
11605    fn test_610_i64_divu_rd_in_r0_r1_skips_restore() {
11606        let encoder = ArmEncoder::new_thumb2();
11607        let code = encoder
11608            .encode(&ArmOp::I64DivU {
11609                rdlo: Reg::R0,
11610                rdhi: Reg::R1,
11611                rnlo: Reg::R0,
11612                rnhi: Reg::R1,
11613                rmlo: Reg::R2,
11614                rmhi: Reg::R3,
11615                elide_zero_guard: false,
11616            })
11617            .unwrap();
11618        let tail: Vec<u16> = code[code.len() - 12..]
11619            .chunks(2)
11620            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11621            .collect();
11622        // MOV r1,r1 / MOV r0,r0 (no-ops, size-stable), ADD SP,#4 twice
11623        // (discard saved r0/r1 — the result lives there), POP {r2}, POP {r3}.
11624        assert_eq!(tail, vec![0x4609, 0x4600, 0xB001, 0xB001, 0xBC04, 0xBC08]);
11625    }
11626
11627    /// #610: a fully swapped rd pair (rd_lo=R1, rd_hi=R0) cannot be
11628    /// materialized by two MOVs in either order — must be a loud Err, never
11629    /// silent corruption. (Selector pairs are consecutive, so unreachable.)
11630    #[test]
11631    fn test_610_i64_swapped_rd_pair_rejected() {
11632        let encoder = ArmEncoder::new_thumb2();
11633        let result = encoder.encode(&ArmOp::I64RemU {
11634            rdlo: Reg::R1,
11635            rdhi: Reg::R0,
11636            rnlo: Reg::R2,
11637            rnhi: Reg::R3,
11638            rmlo: Reg::R4,
11639            rmhi: Reg::R5,
11640            elide_zero_guard: false,
11641        });
11642        assert!(result.is_err(), "swapped rd pair must be rejected loudly");
11643    }
11644
11645    /// #632: the I64Popcnt expansion's own scratch restore (`POP {R3,R4,R5}`)
11646    /// must not clobber the result. Pre-fix the total was materialized with
11647    /// `ADDS rd, R4, R5` BEFORE the pop, so any allocator-assigned
11648    /// rd ∈ {R3,R4,R5} received stale stack garbage. Post-fix the count is
11649    /// carried across the restore in R12 (never allocatable, never restored)
11650    /// and moved into rd only after the pop — structurally rd-independent.
11651    #[test]
11652    fn test_632_i64_popcnt_result_survives_scratch_restore() {
11653        let encoder = ArmEncoder::new_thumb2();
11654        // Every allocatable rd, including the restore set {R3,R4,R5} and R8.
11655        for rd in [
11656            Reg::R0,
11657            Reg::R2,
11658            Reg::R3,
11659            Reg::R4,
11660            Reg::R5,
11661            Reg::R6,
11662            Reg::R8,
11663        ] {
11664            let code = encoder
11665                .encode(&ArmOp::I64Popcnt {
11666                    rd,
11667                    rnlo: Reg::R6,
11668                    rnhi: Reg::R7,
11669                })
11670                .unwrap();
11671            assert_eq!(code.len(), 176, "register-independent size (estimator pin)");
11672            let hw: Vec<u16> = code
11673                .chunks(2)
11674                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11675                .collect();
11676            let pop = hw
11677                .iter()
11678                .position(|&h| h == 0xBC38)
11679                .expect("POP {R3,R4,R5} present");
11680            // Immediately before the POP: ADD.W R12, R4, R5 (the total lives
11681            // in R12, which the POP cannot touch).
11682            assert_eq!(
11683                &hw[pop - 2..pop],
11684                &[0xEB04, 0x0C05],
11685                "total must be carried in R12 across the restore"
11686            );
11687            // Immediately after the POP: MOV rd, R12.
11688            let rd_bits = match rd {
11689                Reg::R8 => 8u16,
11690                Reg::R6 => 6,
11691                Reg::R5 => 5,
11692                Reg::R4 => 4,
11693                Reg::R3 => 3,
11694                Reg::R2 => 2,
11695                _ => 0,
11696            };
11697            let expect_mov = 0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7);
11698            assert_eq!(hw[pop + 1], expect_mov, "MOV rd, R12 after the restore");
11699            // No write into rd between the PUSH and the POP (the old
11700            // pre-restore ADDS is gone).
11701            assert!(
11702                !hw[..pop].contains(&(0x1800 | (5 << 6) | (4 << 3) | rd_bits)),
11703                "no ADDS rd, R4, R5 before the restore pop"
11704            );
11705        }
11706    }
11707
11708    /// #632 audit: the entry marshal must be permutation-safe. Pre-fix
11709    /// `MOV R4, rnlo; MOV R5, rnhi` read a clobbered R4 when the operand
11710    /// pair lived at (R3, R4). Post-fix rnlo routes through R12.
11711    #[test]
11712    fn test_632_i64_popcnt_marshal_pair_at_r3_r4() {
11713        let encoder = ArmEncoder::new_thumb2();
11714        let code = encoder
11715            .encode(&ArmOp::I64Popcnt {
11716                rd: Reg::R0,
11717                rnlo: Reg::R3,
11718                rnhi: Reg::R4,
11719            })
11720            .unwrap();
11721        let hw: Vec<u16> = code
11722            .chunks(2)
11723            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11724            .collect();
11725        // PUSH {R3,R4,R5}; MOV R12, R3; MOV R5, R4 (rnhi read BEFORE any
11726        // write to R4); MOV R4, R12.
11727        assert_eq!(hw[0], 0xB438);
11728        assert_eq!(hw[1], 0x4600 | (1 << 7) | (3 << 3) | 4, "MOV R12, rnlo");
11729        assert_eq!(hw[2], 0x4600 | (4 << 3) | 5, "MOV R5, rnhi");
11730        assert_eq!(hw[3], 0x4664, "MOV R4, R12");
11731    }
11732
11733    /// #632: A32 twin — same structural fix on the ARM-mode path
11734    /// (`--target cortex-r5`): total carried in R12 across the restore.
11735    #[test]
11736    fn test_632_a32_i64_popcnt_result_survives_scratch_restore() {
11737        let encoder = ArmEncoder::new_arm32();
11738        for rd in [Reg::R0, Reg::R3, Reg::R4, Reg::R5, Reg::R8] {
11739            let code = encoder
11740                .encode(&ArmOp::I64Popcnt {
11741                    rd,
11742                    rnlo: Reg::R6,
11743                    rnhi: Reg::R7,
11744                })
11745                .unwrap();
11746            let words: Vec<u32> = code
11747                .chunks(4)
11748                .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11749                .collect();
11750            let pop = words
11751                .iter()
11752                .position(|&w| w == 0xE8BD_0038)
11753                .expect("POP {R3,R4,R5} present");
11754            assert_eq!(words[pop - 1], 0xE084_C005, "ADD R12, R4, R5 before POP");
11755            let rd_bits = match rd {
11756                Reg::R8 => 8u32,
11757                Reg::R5 => 5,
11758                Reg::R4 => 4,
11759                Reg::R3 => 3,
11760                _ => 0,
11761            };
11762            assert_eq!(
11763                words[pop + 1],
11764                0xE1A0_0000 | (rd_bits << 12) | 12,
11765                "MOV rd, R12 after the restore"
11766            );
11767        }
11768    }
11769
11770    /// #633: I64DivS must carry the INT64_MIN/-1 overflow guard (mirroring
11771    /// the i32 path) right after the zero-divisor guard — dividend in R0:R1,
11772    /// divisor in R2:R3 on the #610/#613 fixed-ABI wrapper path.
11773    #[test]
11774    fn test_633_i64_divs_overflow_guard_emitted() {
11775        let encoder = ArmEncoder::new_thumb2();
11776        let code = encoder
11777            .encode(&ArmOp::I64DivS {
11778                rdlo: Reg::R4,
11779                rdhi: Reg::R5,
11780                rnlo: Reg::R0,
11781                rnhi: Reg::R1,
11782                rmlo: Reg::R2,
11783                rmhi: Reg::R3,
11784                elide_zero_guard: false,
11785                elide_overflow_guard: false,
11786            })
11787            .unwrap();
11788        // 26-byte marshal + 8-byte zero-trap, then the 22-byte overflow guard.
11789        let guard: Vec<u16> = code[34..56]
11790            .chunks(2)
11791            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11792            .collect();
11793        assert_eq!(
11794            guard,
11795            vec![
11796                0xEA02, 0x0C03, // AND.W R12, R2, R3
11797                0xF11C, 0x0F01, // CMN.W R12, #1
11798                0xD105, // BNE .no_trap
11799                0x2800, // CMP R0, #0
11800                0xD103, // BNE .no_trap
11801                0xF1B1, 0x4F00, // CMP.W R1, #0x80000000
11802                0xD100, // BNE .no_trap
11803                0xDE00, // UDF #0 — signed-division overflow
11804            ],
11805            "INT64_MIN/-1 overflow guard after the zero-divisor guard"
11806        );
11807    }
11808
11809    /// #633 fix-guard twin: I64RemS must NOT carry the overflow guard —
11810    /// rem_s(INT64_MIN, -1) is defined as 0 and must not trap. Exactly one
11811    /// UDF (the zero-divisor trap) in the whole expansion.
11812    #[test]
11813    fn test_633_i64_rems_has_no_overflow_guard() {
11814        let encoder = ArmEncoder::new_thumb2();
11815        for (is_rem_s, op) in [
11816            (
11817                true,
11818                ArmOp::I64RemS {
11819                    rdlo: Reg::R4,
11820                    rdhi: Reg::R5,
11821                    rnlo: Reg::R0,
11822                    rnhi: Reg::R1,
11823                    rmlo: Reg::R2,
11824                    rmhi: Reg::R3,
11825                    elide_zero_guard: false,
11826                },
11827            ),
11828            (
11829                false,
11830                ArmOp::I64DivS {
11831                    rdlo: Reg::R4,
11832                    rdhi: Reg::R5,
11833                    rnlo: Reg::R0,
11834                    rnhi: Reg::R1,
11835                    rmlo: Reg::R2,
11836                    rmhi: Reg::R3,
11837                    elide_zero_guard: false,
11838                    elide_overflow_guard: false,
11839                },
11840            ),
11841        ] {
11842            let code = encoder.encode(&op).unwrap();
11843            let udfs = code
11844                .chunks(2)
11845                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11846                .count();
11847            let want = if is_rem_s { 1 } else { 2 };
11848            assert_eq!(
11849                udfs, want,
11850                "rem_s: zero-trap only; div_s: zero-trap + overflow trap"
11851            );
11852        }
11853    }
11854
11855    /// #494 phase 2b: `elide_zero_guard` drops EXACTLY the 8-byte fused
11856    /// zero-trap (`ORRS.W R12,R2,R3; BNE; UDF #0`) and nothing else — the
11857    /// rest of the expansion is byte-identical (splice check).
11858    #[test]
11859    fn test_494_i64_zero_guard_elision_is_exact_splice() {
11860        let encoder = ArmEncoder::new_thumb2();
11861        let mk = |elide_zero_guard: bool| {
11862            encoder
11863                .encode(&ArmOp::I64DivU {
11864                    rdlo: Reg::R4,
11865                    rdhi: Reg::R5,
11866                    rnlo: Reg::R0,
11867                    rnhi: Reg::R1,
11868                    rmlo: Reg::R2,
11869                    rmhi: Reg::R3,
11870                    elide_zero_guard,
11871                })
11872                .unwrap()
11873        };
11874        let full = mk(false);
11875        let elided = mk(true);
11876        assert_eq!(full.len(), elided.len() + 8, "zero guard is 8 bytes");
11877        // Marshal prologue (26 B) unchanged, guard (8 B) gone, tail identical.
11878        assert_eq!(&full[..26], &elided[..26]);
11879        assert_eq!(
11880            &full[26..34],
11881            &[0x52, 0xEA, 0x03, 0x0C, 0x00, 0xD1, 0x00, 0xDE],
11882            "the spliced-out bytes are exactly ORRS.W; BNE; UDF #0"
11883        );
11884        assert_eq!(&full[34..], &elided[26..]);
11885    }
11886
11887    /// #494 phase 2b two-guard distinction (the #633/#634 synergy): a
11888    /// divisor-nonzero fact elides ONLY the zero guard — the INT64_MIN/-1
11889    /// OVERFLOW guard is a separate obligation and must survive
11890    /// `elide_zero_guard: true`. Pinned on div_s in all flag states.
11891    #[test]
11892    fn test_494_i64_divs_overflow_guard_retained_when_only_zero_elided() {
11893        let encoder = ArmEncoder::new_thumb2();
11894        let mk = |zero: bool, ovf: bool| {
11895            encoder
11896                .encode(&ArmOp::I64DivS {
11897                    rdlo: Reg::R4,
11898                    rdhi: Reg::R5,
11899                    rnlo: Reg::R0,
11900                    rnhi: Reg::R1,
11901                    rmlo: Reg::R2,
11902                    rmhi: Reg::R3,
11903                    elide_zero_guard: zero,
11904                    elide_overflow_guard: ovf,
11905                })
11906                .unwrap()
11907        };
11908        let udf_count = |code: &[u8]| {
11909            code.chunks(2)
11910                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11911                .count()
11912        };
11913        let full = mk(false, false);
11914        let zero_only = mk(true, false);
11915        let both = mk(true, true);
11916        assert_eq!(udf_count(&full), 2, "baseline: zero trap + overflow trap");
11917        assert_eq!(
11918            udf_count(&zero_only),
11919            1,
11920            "divisor-nonzero elides the zero trap ONLY — the #633 overflow \
11921             guard must be retained"
11922        );
11923        // The retained guard is the 22-byte overflow sequence, now right
11924        // after the 26-byte marshal prologue.
11925        let guard: Vec<u16> = zero_only[26..48]
11926            .chunks(2)
11927            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11928            .collect();
11929        assert_eq!(
11930            guard,
11931            vec![
11932                0xEA02, 0x0C03, 0xF11C, 0x0F01, 0xD105, 0x2800, 0xD103, 0xF1B1, 0x4F00, 0xD100,
11933                0xDE00,
11934            ],
11935            "the surviving guard is the INT64_MIN/-1 overflow trap"
11936        );
11937        assert_eq!(full.len(), zero_only.len() + 8);
11938        assert_eq!(zero_only.len(), both.len() + 22);
11939        assert_eq!(udf_count(&both), 0, "both obligations discharged ⇒ no UDF");
11940    }
11941
11942    /// #494 phase 2b A32 twin: zero-guard elision is an exact 12-byte splice
11943    /// and the A32 overflow guard survives a zero-only elision.
11944    #[test]
11945    fn test_494_a32_i64_guard_elision() {
11946        let encoder = ArmEncoder::new_arm32();
11947        let mk = |zero: bool, ovf: bool| {
11948            encoder
11949                .encode(&ArmOp::I64DivS {
11950                    rdlo: Reg::R4,
11951                    rdhi: Reg::R5,
11952                    rnlo: Reg::R0,
11953                    rnhi: Reg::R1,
11954                    rmlo: Reg::R2,
11955                    rmhi: Reg::R3,
11956                    elide_zero_guard: zero,
11957                    elide_overflow_guard: ovf,
11958                })
11959                .unwrap()
11960        };
11961        let full = mk(false, false);
11962        let zero_only = mk(true, false);
11963        let both = mk(true, true);
11964        // A32 zero guard = 3 words (ORRS/BNE/UDF), overflow guard = 6 words.
11965        assert_eq!(full.len(), zero_only.len() + 12);
11966        assert_eq!(zero_only.len(), both.len() + 24);
11967        let udf_count = |code: &[u8]| {
11968            code.chunks(4)
11969                .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
11970                .count()
11971        };
11972        assert_eq!(udf_count(&full), 2);
11973        assert_eq!(
11974            udf_count(&zero_only),
11975            1,
11976            "A32: overflow guard retained under zero-only elision"
11977        );
11978        assert_eq!(udf_count(&both), 0);
11979    }
11980
11981    /// #633: A32 twin — the conditional-execution overflow guard on the
11982    /// ARM-mode I64DivS, and its absence from I64RemS.
11983    #[test]
11984    fn test_633_a32_i64_divs_overflow_guard() {
11985        let encoder = ArmEncoder::new_arm32();
11986        let mk_divs = ArmOp::I64DivS {
11987            rdlo: Reg::R4,
11988            rdhi: Reg::R5,
11989            rnlo: Reg::R0,
11990            rnhi: Reg::R1,
11991            rmlo: Reg::R2,
11992            rmhi: Reg::R3,
11993            elide_zero_guard: false,
11994            elide_overflow_guard: false,
11995        };
11996        let code = encoder.encode(&mk_divs).unwrap();
11997        let words: Vec<u32> = code
11998            .chunks(4)
11999            .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
12000            .collect();
12001        let guard = [
12002            0xE002_C003u32, // AND   R12, R2, R3
12003            0xE37C_0001,    // CMN   R12, #1
12004            0x0350_0000,    // CMPEQ R0, #0
12005            0x0351_0102,    // CMPEQ R1, #0x80000000
12006            0x1A00_0000,    // BNE +1 insn
12007            0xE7F0_00F0,    // UDF #0
12008        ];
12009        assert!(
12010            words.windows(6).any(|w| w == guard),
12011            "A32 I64DivS carries the INT64_MIN/-1 overflow guard"
12012        );
12013        let rems = encoder
12014            .encode(&ArmOp::I64RemS {
12015                rdlo: Reg::R4,
12016                rdhi: Reg::R5,
12017                rnlo: Reg::R0,
12018                rnhi: Reg::R1,
12019                rmlo: Reg::R2,
12020                rmhi: Reg::R3,
12021                elide_zero_guard: false,
12022            })
12023            .unwrap();
12024        let rems_udfs = rems
12025            .chunks(4)
12026            .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
12027            .count();
12028        assert_eq!(rems_udfs, 1, "A32 I64RemS keeps only the zero-divisor trap");
12029    }
12030
12031    #[test]
12032    fn test_encode_nop_thumb2() {
12033        let encoder = ArmEncoder::new_thumb2();
12034        let op = ArmOp::Nop;
12035        let code = encoder.encode(&op).unwrap();
12036        assert_eq!(code.len(), 2); // 16-bit
12037
12038        // NOP: 0xBF00 in little-endian
12039        assert_eq!(code, vec![0x00, 0xBF]);
12040    }
12041
12042    // =========================================================================
12043    // i64 Thumb-2 encoding tests
12044    // =========================================================================
12045
12046    #[test]
12047    fn test_encode_i64_add_thumb2() {
12048        let encoder = ArmEncoder::new_thumb2();
12049        let op = ArmOp::I64Add {
12050            rdlo: Reg::R0,
12051            rdhi: Reg::R1,
12052            rnlo: Reg::R0,
12053            rnhi: Reg::R1,
12054            rmlo: Reg::R2,
12055            rmhi: Reg::R3,
12056        };
12057        let code = encoder.encode(&op).unwrap();
12058        // Should emit ADDS (2 bytes) + ADC.W (4 bytes) = 6 bytes
12059        assert_eq!(code.len(), 6, "I64Add should be 6 bytes (ADDS + ADC.W)");
12060    }
12061
12062    #[test]
12063    fn test_encode_i64_sub_thumb2() {
12064        let encoder = ArmEncoder::new_thumb2();
12065        let op = ArmOp::I64Sub {
12066            rdlo: Reg::R0,
12067            rdhi: Reg::R1,
12068            rnlo: Reg::R0,
12069            rnhi: Reg::R1,
12070            rmlo: Reg::R2,
12071            rmhi: Reg::R3,
12072        };
12073        let code = encoder.encode(&op).unwrap();
12074        // Should emit SUBS (2 bytes) + SBC.W (4 bytes) = 6 bytes
12075        assert_eq!(code.len(), 6, "I64Sub should be 6 bytes (SUBS + SBC.W)");
12076    }
12077
12078    #[test]
12079    fn test_encode_i64_and_thumb2() {
12080        let encoder = ArmEncoder::new_thumb2();
12081        let op = ArmOp::I64And {
12082            rdlo: Reg::R0,
12083            rdhi: Reg::R1,
12084            rnlo: Reg::R0,
12085            rnhi: Reg::R1,
12086            rmlo: Reg::R2,
12087            rmhi: Reg::R3,
12088        };
12089        let code = encoder.encode(&op).unwrap();
12090        // AND.W (4 bytes) + AND.W (4 bytes) = 8 bytes
12091        assert!(code.len() >= 4, "I64And should emit at least 4 bytes");
12092    }
12093
12094    #[test]
12095    fn test_encode_i64_or_thumb2() {
12096        let encoder = ArmEncoder::new_thumb2();
12097        let op = ArmOp::I64Or {
12098            rdlo: Reg::R0,
12099            rdhi: Reg::R1,
12100            rnlo: Reg::R0,
12101            rnhi: Reg::R1,
12102            rmlo: Reg::R2,
12103            rmhi: Reg::R3,
12104        };
12105        let code = encoder.encode(&op).unwrap();
12106        assert!(code.len() >= 4, "I64Or should emit at least 4 bytes");
12107    }
12108
12109    #[test]
12110    fn test_encode_i64_xor_thumb2() {
12111        let encoder = ArmEncoder::new_thumb2();
12112        let op = ArmOp::I64Xor {
12113            rdlo: Reg::R0,
12114            rdhi: Reg::R1,
12115            rnlo: Reg::R0,
12116            rnhi: Reg::R1,
12117            rmlo: Reg::R2,
12118            rmhi: Reg::R3,
12119        };
12120        let code = encoder.encode(&op).unwrap();
12121        assert!(code.len() >= 4, "I64Xor should emit at least 4 bytes");
12122    }
12123
12124    #[test]
12125    fn test_encode_i64_const_small_thumb2() {
12126        let encoder = ArmEncoder::new_thumb2();
12127        // Small constant: only needs MOVW for each half
12128        let op = ArmOp::I64Const {
12129            rdlo: Reg::R0,
12130            rdhi: Reg::R1,
12131            value: 42,
12132        };
12133        let code = encoder.encode(&op).unwrap();
12134        // MOVW R0, #42 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes minimum
12135        assert!(code.len() >= 8, "I64Const should emit at least 8 bytes");
12136    }
12137
12138    #[test]
12139    fn test_encode_i64_const_large_thumb2() {
12140        let encoder = ArmEncoder::new_thumb2();
12141        // Large constant: needs MOVW+MOVT for each half
12142        let op = ArmOp::I64Const {
12143            rdlo: Reg::R0,
12144            rdhi: Reg::R1,
12145            value: 0x1234_5678_9ABC_DEF0_u64 as i64,
12146        };
12147        let code = encoder.encode(&op).unwrap();
12148        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
12149        assert_eq!(
12150            code.len(),
12151            16,
12152            "I64Const with large value should be 16 bytes"
12153        );
12154    }
12155
12156    #[test]
12157    fn test_encode_i64_extend_i32_s_thumb2() {
12158        let encoder = ArmEncoder::new_thumb2();
12159        let op = ArmOp::I64ExtendI32S {
12160            rdlo: Reg::R0,
12161            rdhi: Reg::R1,
12162            rn: Reg::R0,
12163        };
12164        let code = encoder.encode(&op).unwrap();
12165        // When rdlo == rn, only ASR (4 bytes) is emitted
12166        assert_eq!(
12167            code.len(),
12168            4,
12169            "I64ExtendI32S (same reg) should be 4 bytes (ASR only)"
12170        );
12171    }
12172
12173    #[test]
12174    fn test_encode_i64_extend_i32_s_diff_reg_thumb2() {
12175        let encoder = ArmEncoder::new_thumb2();
12176        let op = ArmOp::I64ExtendI32S {
12177            rdlo: Reg::R0,
12178            rdhi: Reg::R1,
12179            rn: Reg::R2,
12180        };
12181        let code = encoder.encode(&op).unwrap();
12182        // MOV rdlo, rn (2 bytes for low regs) + ASR rdhi, rdlo, #31 (4 bytes) = 6 bytes
12183        assert!(
12184            code.len() >= 6,
12185            "I64ExtendI32S (diff reg) should be at least 6 bytes"
12186        );
12187    }
12188
12189    #[test]
12190    fn test_encode_i64_extend_i32_u_thumb2() {
12191        let encoder = ArmEncoder::new_thumb2();
12192        let op = ArmOp::I64ExtendI32U {
12193            rdlo: Reg::R0,
12194            rdhi: Reg::R1,
12195            rn: Reg::R0,
12196        };
12197        let code = encoder.encode(&op).unwrap();
12198        // When rdlo == rn, only MOV rdhi, #0 (2 bytes) is emitted
12199        assert_eq!(
12200            code.len(),
12201            2,
12202            "I64ExtendI32U (same reg) should be 2 bytes (MOV #0 only)"
12203        );
12204    }
12205
12206    #[test]
12207    fn test_encode_i32_wrap_i64_nop_thumb2() {
12208        let encoder = ArmEncoder::new_thumb2();
12209        // When rd == rnlo, should be a NOP
12210        let op = ArmOp::I32WrapI64 {
12211            rd: Reg::R0,
12212            rnlo: Reg::R0,
12213        };
12214        let code = encoder.encode(&op).unwrap();
12215        assert_eq!(code.len(), 2, "I32WrapI64 same reg should be NOP (2 bytes)");
12216        assert_eq!(code, vec![0x00, 0xBF]); // NOP
12217    }
12218
12219    #[test]
12220    fn test_encode_i32_wrap_i64_diff_reg_thumb2() {
12221        let encoder = ArmEncoder::new_thumb2();
12222        let op = ArmOp::I32WrapI64 {
12223            rd: Reg::R2,
12224            rnlo: Reg::R0,
12225        };
12226        let code = encoder.encode(&op).unwrap();
12227        // MOV R2, R0 (2 or 4 bytes)
12228        assert!(
12229            code.len() >= 2,
12230            "I32WrapI64 diff reg should emit at least 2 bytes"
12231        );
12232    }
12233
12234    #[test]
12235    fn test_encode_i64_eqz_thumb2() {
12236        let encoder = ArmEncoder::new_thumb2();
12237        let op = ArmOp::I64Eqz {
12238            rd: Reg::R0,
12239            rnlo: Reg::R0,
12240            rnhi: Reg::R1,
12241        };
12242        let code = encoder.encode(&op).unwrap();
12243        // Delegates to I64SetCondZ which is already encoded
12244        assert!(
12245            code.len() >= 6,
12246            "I64Eqz should emit at least 6 bytes for ORR+ITE+MOV+MOV"
12247        );
12248    }
12249
12250    #[test]
12251    fn test_encode_i64_eq_thumb2() {
12252        let encoder = ArmEncoder::new_thumb2();
12253        let op = ArmOp::I64Eq {
12254            rd: Reg::R0,
12255            rnlo: Reg::R0,
12256            rnhi: Reg::R1,
12257            rmlo: Reg::R2,
12258            rmhi: Reg::R3,
12259        };
12260        let code = encoder.encode(&op).unwrap();
12261        // Delegates to I64SetCond EQ: CMP lo + IT EQ + CMPEQ hi + ITE EQ + MOV 1 + MOV 0
12262        assert!(code.len() >= 10, "I64Eq should emit at least 10 bytes");
12263    }
12264
12265    #[test]
12266    fn test_encode_i64_ldr_thumb2() {
12267        let encoder = ArmEncoder::new_thumb2();
12268        let op = ArmOp::I64Ldr {
12269            rdlo: Reg::R0,
12270            rdhi: Reg::R1,
12271            addr: MemAddr::imm(Reg::SP, 0),
12272        };
12273        let code = encoder.encode(&op).unwrap();
12274        // Two LDR instructions (lo at offset, hi at offset+4)
12275        assert!(code.len() >= 4, "I64Ldr should emit at least 4 bytes");
12276    }
12277
12278    #[test]
12279    fn test_372_i64_ldr_indexed_materializes_address() {
12280        // #372: a memory i64.load carries an index register (R11 + addr + off).
12281        // The encoder must materialize `ip = base + index` (ADD.W) and load via
12282        // `[ip,#off]` — NOT drop the index. A frame (non-indexed) i64.load must
12283        // stay byte-identical (plain `[base,#off]`, no ADD).
12284        let encoder = ArmEncoder::new_thumb2();
12285        let indexed = encoder
12286            .encode(&ArmOp::I64Ldr {
12287                rdlo: Reg::R0,
12288                rdhi: Reg::R1,
12289                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
12290            })
12291            .unwrap();
12292        // ADD.W ip, fp, r0 = eb0b 0c00 (byte-verified vs arm-none-eabi-as).
12293        assert_eq!(
12294            &indexed[0..4],
12295            &[0x0b, 0xeb, 0x00, 0x0c],
12296            "indexed I64Ldr must start with ADD.W ip, base, index"
12297        );
12298        let frame = encoder
12299            .encode(&ArmOp::I64Ldr {
12300                rdlo: Reg::R0,
12301                rdhi: Reg::R1,
12302                addr: MemAddr::imm(Reg::SP, 8),
12303            })
12304            .unwrap();
12305        // No index -> no ADD.W prefix (byte-identical frame access).
12306        assert_ne!(
12307            &frame[0..2],
12308            &[0x0b, 0xeb],
12309            "frame (non-indexed) I64Ldr must NOT emit an ADD.W"
12310        );
12311    }
12312
12313    #[test]
12314    fn test_382_i64_ldst_large_offset_materializes_not_skips() {
12315        // #382: an indexed i64.load/store whose static offset > 0xFFF must
12316        // MATERIALIZE the offset into the base — NOT return Err (skip the fn).
12317        // Sequence for reg_imm(R11, R0, 5000): MOVW ip,#5000 ; ADD ip,r0,ip ;
12318        // ADD ip,ip,fp ; LDR/STR halves at [ip,#0] / [ip,#4]. Byte-verified tail
12319        // vs arm-none-eabi-as.
12320        let encoder = ArmEncoder::new_thumb2();
12321        // 0x1388 > 0xFFF (MemAddr is not Copy, so build it per use).
12322
12323        let ld = encoder
12324            .encode(&ArmOp::I64Ldr {
12325                rdlo: Reg::R0,
12326                rdhi: Reg::R1,
12327                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
12328            })
12329            .expect("large-offset i64.load must lower, not skip");
12330        // MOVW ip,#0x1388 (4) + ADD ip,r0,ip (4) + ADD ip,ip,fp (4) + 2 LDR (8).
12331        assert_eq!(ld.len(), 20, "expected MOVW + 2×ADD + 2×LDR");
12332        // Must NOT be the small-offset `ADD.W ip, fp, r0` (0x0b 0xeb) prefix —
12333        // that path can only reach imm12 offsets.
12334        assert_ne!(
12335            &ld[0..2],
12336            &[0x0b, 0xeb],
12337            "must materialize the large offset"
12338        );
12339        // Effective base built in ip, then halves at [ip,#0] / [ip,#4].
12340        assert_eq!(
12341            &ld[4..20],
12342            &[
12343                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
12344                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
12345                0xdc, 0xf8, 0x00, 0x00, // LDR.W r0, [ip, #0]
12346                0xdc, 0xf8, 0x04, 0x10, // LDR.W r1, [ip, #4]
12347            ],
12348            "large-offset i64.load must fold offset into ip and access [ip,#0]/[ip,#4]"
12349        );
12350
12351        // Store: same base materialization, STR halves.
12352        let st = encoder
12353            .encode(&ArmOp::I64Str {
12354                rdlo: Reg::R2,
12355                rdhi: Reg::R3,
12356                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
12357            })
12358            .expect("large-offset i64.store must lower, not skip");
12359        assert_eq!(st.len(), 20);
12360        assert_eq!(
12361            &st[4..20],
12362            &[
12363                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
12364                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
12365                0xcc, 0xf8, 0x00, 0x20, // STR.W r2, [ip, #0]
12366                0xcc, 0xf8, 0x04, 0x30, // STR.W r3, [ip, #4]
12367            ],
12368            "large-offset i64.store must fold offset into ip and access [ip,#0]/[ip,#4]"
12369        );
12370
12371        // Small-offset (imm12) indexed access stays byte-identical (#372): the
12372        // effective base is a single `ADD.W ip, fp, r0` and the halves keep the
12373        // folded immediates — NO extra MOVW/ADD.
12374        let small = encoder
12375            .encode(&ArmOp::I64Ldr {
12376                rdlo: Reg::R0,
12377                rdhi: Reg::R1,
12378                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 8),
12379            })
12380            .unwrap();
12381        assert_eq!(
12382            &small[0..4],
12383            &[0x0b, 0xeb, 0x00, 0x0c],
12384            "small-offset indexed i64 must keep the single ADD.W ip, fp, r0"
12385        );
12386        assert_eq!(small.len(), 12, "ADD.W + 2×LDR.W (offset folded in imm12)");
12387    }
12388
12389    /// RQ-63-ARMI64OFF (#1165): A32 `I64Ldr`/`I64Str` at and past the pair
12390    /// form's immediate boundary. Bytes verified against `arm-none-eabi-as`
12391    /// (`.arch armv7-r`, `.arm`). 4091 is the last offset the pair folds (the
12392    /// high half sits at 4095 = imm12 max); 4092 is the straddle the v0.62
12393    /// census declined on (low half fits, high half does not).
12394    #[test]
12395    fn test_1165_a32_i64_ldst_offset_boundary_materializes() {
12396        let enc = ArmEncoder::new_arm32();
12397        let le = |ws: &[u32]| ws.iter().flat_map(|w| w.to_le_bytes()).collect::<Vec<u8>>();
12398        let ld = |off: i32| {
12399            enc.encode(&ArmOp::I64Ldr {
12400                rdlo: Reg::R0,
12401                rdhi: Reg::R1,
12402                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, off),
12403            })
12404        };
12405        // AT the boundary (4091): folded, byte-identical to the pre-#1165 form.
12406        assert_eq!(
12407            ld(4091).unwrap(),
12408            le(&[0xE08BC000, 0xE59C0FFB, 0xE59C1FFF]),
12409            "4091: add ip,fp,r0 ; ldr r0,[ip,#4091] ; ldr r1,[ip,#4095]"
12410        );
12411        // ONE ABOVE (4092): the high half would need #4096 → materialize.
12412        assert_eq!(
12413            ld(4092).unwrap(),
12414            le(&[0xE300CFFC, 0xE08CC00B, 0xE08CC000, 0xE59C0000, 0xE59C1004]),
12415            "4092: movw ip,#4092 ; add ip,ip,fp ; add ip,ip,r0 ; ldr r0,[ip] ; ldr r1,[ip,#4]"
12416        );
12417        // 4095 (low half at the imm12 max), 4096 (both out), 5000.
12418        assert_eq!(ld(4095).unwrap()[..4], le(&[0xE300CFFF])[..]);
12419        assert_eq!(ld(4096).unwrap()[..4], le(&[0xE301C000])[..]);
12420        assert_eq!(
12421            ld(5000).unwrap(),
12422            le(&[0xE301C388, 0xE08CC00B, 0xE08CC000, 0xE59C0000, 0xE59C1004])
12423        );
12424        // 70000 needs MOVT; store form.
12425        assert_eq!(
12426            enc.encode(&ArmOp::I64Str {
12427                rdlo: Reg::R2,
12428                rdhi: Reg::R3,
12429                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 70000),
12430            })
12431            .unwrap(),
12432            le(&[
12433                0xE301C170, 0xE340C001, 0xE08CC00B, 0xE08CC000, 0xE58C2000, 0xE58C3004
12434            ]),
12435            "70000: movw ip,#0x1170 ; movt ip,#1 ; add ip,ip,fp ; add ip,ip,r0 ; str r2,[ip] ; str r3,[ip,#4]"
12436        );
12437        // A memarg >= 2^31 arrives negative (0xFFFFFFF8 as i32 = -8): the full
12438        // 32 bits are materialized, mod 2^32 — never clamped, never masked.
12439        assert_eq!(
12440            ld(-8).unwrap(),
12441            le(&[
12442                0xE30FCFF8, 0xE34FCFFF, 0xE08CC00B, 0xE08CC000, 0xE59C0000, 0xE59C1004
12443            ]),
12444            "0xfffffff8: movw ip,#0xfff8 ; movt ip,#0xffff ; add ; add ; ldr ; ldr"
12445        );
12446        // No index register (frame-style base), out of range: MOVW ; ADD ip,ip,base.
12447        assert_eq!(
12448            enc.encode(&ArmOp::I64Ldr {
12449                rdlo: Reg::R0,
12450                rdhi: Reg::R1,
12451                addr: MemAddr::imm(Reg::SP, 5000),
12452            })
12453            .unwrap(),
12454            le(&[0xE301C388, 0xE08CC00D, 0xE59C0000, 0xE59C1004])
12455        );
12456        // The base IS the scratch: no free register → typed error, not a clobber.
12457        assert!(
12458            enc.encode(&ArmOp::I64Ldr {
12459                rdlo: Reg::R0,
12460                rdhi: Reg::R1,
12461                addr: MemAddr::imm(Reg::R12, 5000),
12462            })
12463            .is_err(),
12464            "materializing over an R12 base must be refused, not clobber it"
12465        );
12466    }
12467
12468    /// RQ-63-ARMI64OFF (#1165): the A32 WORD and SUB-WORD immediate arms used
12469    /// to MASK an out-of-range offset (`& 0xFFF` / `& 0xFF`) — a silent wrong
12470    /// address (`i32.load offset=5000` → `ldr r0,[ip,#904]`, `i32.load16_u
12471    /// offset=256` → `ldrh r0,[ip]`). At the field maximum the bytes are
12472    /// unchanged; one above, the offset is materialized. Bytes verified
12473    /// against `arm-none-eabi-as`.
12474    #[test]
12475    fn test_1165_a32_word_subword_offset_boundary_materializes_not_masks() {
12476        let enc = ArmEncoder::new_arm32();
12477        let le = |ws: &[u32]| ws.iter().flat_map(|w| w.to_le_bytes()).collect::<Vec<u8>>();
12478        let idx = |off: i32| MemAddr::reg_imm(Reg::R11, Reg::R0, off);
12479        let ldr = |addr| enc.encode(&ArmOp::Ldr { rd: Reg::R0, addr }).unwrap();
12480        let ldrh = |addr| enc.encode(&ArmOp::Ldrh { rd: Reg::R0, addr }).unwrap();
12481        let ldrsb = |addr| enc.encode(&ArmOp::Ldrsb { rd: Reg::R0, addr }).unwrap();
12482        let strh = |addr| enc.encode(&ArmOp::Strh { rd: Reg::R1, addr }).unwrap();
12483        let ldrb = |addr| enc.encode(&ArmOp::Ldrb { rd: Reg::R0, addr }).unwrap();
12484        // LDR imm12: 4095 at, 4096 above.
12485        assert_eq!(ldr(idx(4095)), le(&[0xE08BC000, 0xE59C0FFF]));
12486        assert_eq!(
12487            ldr(idx(4096)),
12488            le(&[0xE301C000, 0xE08CC00B, 0xE08CC000, 0xE59C0000]),
12489            "ldr 4096 must materialize, not mask to #0"
12490        );
12491        // LDRH imm8: 255 at, 256 above (the pre-fix bytes were `ldrh r0,[ip]`).
12492        assert_eq!(ldrh(idx(255)), le(&[0xE08BC000, 0xE1DC0FBF]));
12493        assert_eq!(
12494            ldrh(idx(256)),
12495            le(&[0xE300C100, 0xE08CC00B, 0xE08CC000, 0xE1DC00B0]),
12496            "ldrh 256 must materialize, not mask to #0"
12497        );
12498        // LDRSB imm8 (256 above), STRH imm8 (300 above).
12499        assert_eq!(
12500            ldrsb(idx(256)),
12501            le(&[0xE300C100, 0xE08CC00B, 0xE08CC000, 0xE1DC00D0])
12502        );
12503        assert_eq!(
12504            strh(idx(300)),
12505            le(&[0xE300C12C, 0xE08CC00B, 0xE08CC000, 0xE1CC10B0])
12506        );
12507        // LDRB is an imm12 form: 4095 at, 4096 above.
12508        assert_eq!(ldrb(idx(4095)), le(&[0xE08BC000, 0xE5DC0FFF]));
12509        assert_eq!(
12510            ldrb(idx(4096)),
12511            le(&[0xE301C000, 0xE08CC00B, 0xE08CC000, 0xE5DC0000])
12512        );
12513        // No index, out of range: MOVW ; ADD ip,ip,base ; access [ip].
12514        assert_eq!(
12515            ldr(MemAddr::imm(Reg::R0, 5000)),
12516            le(&[0xE301C388, 0xE08CC000, 0xE59C0000])
12517        );
12518        // The immediate arms are now a TRIPWIRE, never a mask.
12519        assert!(encode_mem_addr(&MemAddr::imm(Reg::R1, 0xFFF)).is_ok());
12520        assert!(
12521            encode_mem_addr(&MemAddr::imm(Reg::R1, 0x1000)).is_err(),
12522            "imm12 arm must refuse 0x1000, not mask it to #0"
12523        );
12524        assert!(encode_mem_addr_imm8(&MemAddr::imm(Reg::R1, 0xFF)).is_ok());
12525        assert!(
12526            encode_mem_addr_imm8(&MemAddr::imm(Reg::R1, 0x100)).is_err(),
12527            "imm8 arm must refuse 0x100, not mask it to #0"
12528        );
12529        // Base is the scratch: refused, not clobbered.
12530        assert!(
12531            enc.encode(&ArmOp::Ldr {
12532                rd: Reg::R0,
12533                addr: MemAddr::imm(Reg::R12, 5000),
12534            })
12535            .is_err()
12536        );
12537    }
12538
12539    /// RQ-63-ARMI64OFF (#1165): Thumb-2 `i64_effective_base` used to CLAMP a
12540    /// negative offset (a memarg >= 2^31 after the selector's `as i32` cast)
12541    /// to 0 — `i64.load offset=0xfffffff8` read `[R11+addr]`. It now
12542    /// materializes the full 32 bits (MOVW+MOVT), the arithmetic the i32 word
12543    /// path uses for the same memarg. Bytes verified against
12544    /// `arm-none-eabi-as` (`.arch armv7e-m`, `.thumb`). A FRAME access with a
12545    /// negative offset (no index register) is refused loudly by
12546    /// `check_ldst_imm12`, never clamped.
12547    #[test]
12548    fn test_1165_thumb2_i64_negative_offset_materializes_not_clamps() {
12549        let enc = ArmEncoder::new_thumb2();
12550        let ld = enc
12551            .encode(&ArmOp::I64Ldr {
12552                rdlo: Reg::R0,
12553                rdhi: Reg::R1,
12554                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, -8),
12555            })
12556            .unwrap();
12557        assert_eq!(
12558            ld,
12559            vec![
12560                0x4f, 0xf6, 0xf8, 0x7c, // movw ip, #0xfff8
12561                0xcf, 0xf6, 0xff, 0x7c, // movt ip, #0xffff
12562                0x00, 0xeb, 0x0c, 0x0c, // add.w ip, r0, ip
12563                0x0c, 0xeb, 0x0b, 0x0c, // add.w ip, ip, fp
12564                0xdc, 0xf8, 0x00, 0x00, // ldr.w r0, [ip]
12565                0xdc, 0xf8, 0x04, 0x10, // ldr.w r1, [ip, #4]
12566            ],
12567            "0xfffffff8 must be materialized in full, not clamped to [ip,#0]"
12568        );
12569        let st = enc
12570            .encode(&ArmOp::I64Str {
12571                rdlo: Reg::R2,
12572                rdhi: Reg::R3,
12573                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, i32::MIN),
12574            })
12575            .unwrap();
12576        assert_eq!(
12577            st,
12578            vec![
12579                0x40, 0xf2, 0x00, 0x0c, // movw ip, #0
12580                0xc8, 0xf2, 0x00, 0x0c, // movt ip, #0x8000
12581                0x00, 0xeb, 0x0c, 0x0c, // add.w ip, r0, ip
12582                0x0c, 0xeb, 0x0b, 0x0c, // add.w ip, ip, fp
12583                0xcc, 0xf8, 0x00, 0x20, // str.w r2, [ip]
12584                0xcc, 0xf8, 0x04, 0x30, // str.w r3, [ip, #4]
12585            ]
12586        );
12587        assert!(
12588            enc.encode(&ArmOp::I64Ldr {
12589                rdlo: Reg::R0,
12590                rdhi: Reg::R1,
12591                addr: MemAddr::imm(Reg::SP, -8),
12592            })
12593            .is_err(),
12594            "a negative frame offset must be refused, not clamped to [sp,#0]"
12595        );
12596    }
12597
12598    #[test]
12599    fn test_encode_i64_str_thumb2() {
12600        let encoder = ArmEncoder::new_thumb2();
12601        let op = ArmOp::I64Str {
12602            rdlo: Reg::R0,
12603            rdhi: Reg::R1,
12604            addr: MemAddr::imm(Reg::SP, 0),
12605        };
12606        let code = encoder.encode(&op).unwrap();
12607        // Two STR instructions (lo at offset, hi at offset+4)
12608        assert!(code.len() >= 4, "I64Str should emit at least 4 bytes");
12609    }
12610
12611    #[test]
12612    fn test_encode_i64_all_comparisons_thumb2() {
12613        let encoder = ArmEncoder::new_thumb2();
12614
12615        let ops = vec![
12616            ArmOp::I64Ne {
12617                rd: Reg::R0,
12618                rnlo: Reg::R0,
12619                rnhi: Reg::R1,
12620                rmlo: Reg::R2,
12621                rmhi: Reg::R3,
12622            },
12623            ArmOp::I64LtS {
12624                rd: Reg::R0,
12625                rnlo: Reg::R0,
12626                rnhi: Reg::R1,
12627                rmlo: Reg::R2,
12628                rmhi: Reg::R3,
12629            },
12630            ArmOp::I64LtU {
12631                rd: Reg::R0,
12632                rnlo: Reg::R0,
12633                rnhi: Reg::R1,
12634                rmlo: Reg::R2,
12635                rmhi: Reg::R3,
12636            },
12637            ArmOp::I64LeS {
12638                rd: Reg::R0,
12639                rnlo: Reg::R0,
12640                rnhi: Reg::R1,
12641                rmlo: Reg::R2,
12642                rmhi: Reg::R3,
12643            },
12644            ArmOp::I64LeU {
12645                rd: Reg::R0,
12646                rnlo: Reg::R0,
12647                rnhi: Reg::R1,
12648                rmlo: Reg::R2,
12649                rmhi: Reg::R3,
12650            },
12651            ArmOp::I64GtS {
12652                rd: Reg::R0,
12653                rnlo: Reg::R0,
12654                rnhi: Reg::R1,
12655                rmlo: Reg::R2,
12656                rmhi: Reg::R3,
12657            },
12658            ArmOp::I64GtU {
12659                rd: Reg::R0,
12660                rnlo: Reg::R0,
12661                rnhi: Reg::R1,
12662                rmlo: Reg::R2,
12663                rmhi: Reg::R3,
12664            },
12665            ArmOp::I64GeS {
12666                rd: Reg::R0,
12667                rnlo: Reg::R0,
12668                rnhi: Reg::R1,
12669                rmlo: Reg::R2,
12670                rmhi: Reg::R3,
12671            },
12672            ArmOp::I64GeU {
12673                rd: Reg::R0,
12674                rnlo: Reg::R0,
12675                rnhi: Reg::R1,
12676                rmlo: Reg::R2,
12677                rmhi: Reg::R3,
12678            },
12679        ];
12680
12681        for op in &ops {
12682            let code = encoder.encode(op).unwrap();
12683            assert!(
12684                code.len() >= 8,
12685                "i64 comparison {:?} should emit at least 8 bytes, got {}",
12686                op,
12687                code.len()
12688            );
12689        }
12690    }
12691
12692    #[test]
12693    fn test_encode_i64_const_zero_thumb2() {
12694        let encoder = ArmEncoder::new_thumb2();
12695        let op = ArmOp::I64Const {
12696            rdlo: Reg::R0,
12697            rdhi: Reg::R1,
12698            value: 0,
12699        };
12700        let code = encoder.encode(&op).unwrap();
12701        // MOVW R0, #0 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes
12702        assert_eq!(code.len(), 8, "I64Const(0) should be 8 bytes");
12703    }
12704
12705    #[test]
12706    fn test_encode_i64_const_negative_one_thumb2() {
12707        let encoder = ArmEncoder::new_thumb2();
12708        let op = ArmOp::I64Const {
12709            rdlo: Reg::R0,
12710            rdhi: Reg::R1,
12711            value: -1, // 0xFFFF_FFFF_FFFF_FFFF
12712        };
12713        let code = encoder.encode(&op).unwrap();
12714        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
12715        assert_eq!(code.len(), 16, "I64Const(-1) should be 16 bytes");
12716    }
12717
12718    // =========================================================================
12719    // Sub-word load/store encoding tests
12720    // =========================================================================
12721
12722    #[test]
12723    fn test_encode_ldrb_arm32() {
12724        let encoder = ArmEncoder::new_arm32();
12725        let op = ArmOp::Ldrb {
12726            rd: Reg::R0,
12727            addr: MemAddr::imm(Reg::R1, 4),
12728        };
12729        let code = encoder.encode(&op).unwrap();
12730        assert_eq!(code.len(), 4, "ARM32 LDRB should be 4 bytes");
12731        // LDRB R0, [R1, #4] = 0xE5D10004
12732        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12733        assert_eq!(encoded, 0xE5D10004, "Should encode LDRB R0, [R1, #4]");
12734    }
12735
12736    #[test]
12737    fn test_encode_strb_arm32() {
12738        let encoder = ArmEncoder::new_arm32();
12739        let op = ArmOp::Strb {
12740            rd: Reg::R0,
12741            addr: MemAddr::imm(Reg::R1, 0),
12742        };
12743        let code = encoder.encode(&op).unwrap();
12744        assert_eq!(code.len(), 4, "ARM32 STRB should be 4 bytes");
12745        // STRB R0, [R1, #0] = 0xE5C10000
12746        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12747        assert_eq!(encoded, 0xE5C10000, "Should encode STRB R0, [R1, #0]");
12748    }
12749
12750    #[test]
12751    fn test_encode_ldrh_arm32() {
12752        let encoder = ArmEncoder::new_arm32();
12753        let op = ArmOp::Ldrh {
12754            rd: Reg::R0,
12755            addr: MemAddr::imm(Reg::R1, 2),
12756        };
12757        let code = encoder.encode(&op).unwrap();
12758        assert_eq!(code.len(), 4, "ARM32 LDRH should be 4 bytes");
12759    }
12760
12761    #[test]
12762    fn test_encode_strh_arm32() {
12763        let encoder = ArmEncoder::new_arm32();
12764        let op = ArmOp::Strh {
12765            rd: Reg::R0,
12766            addr: MemAddr::imm(Reg::R1, 0),
12767        };
12768        let code = encoder.encode(&op).unwrap();
12769        assert_eq!(code.len(), 4, "ARM32 STRH should be 4 bytes");
12770    }
12771
12772    #[test]
12773    fn test_encode_ldrsb_arm32() {
12774        let encoder = ArmEncoder::new_arm32();
12775        let op = ArmOp::Ldrsb {
12776            rd: Reg::R0,
12777            addr: MemAddr::imm(Reg::R1, 0),
12778        };
12779        let code = encoder.encode(&op).unwrap();
12780        assert_eq!(code.len(), 4, "ARM32 LDRSB should be 4 bytes");
12781    }
12782
12783    #[test]
12784    fn test_encode_ldrsh_arm32() {
12785        let encoder = ArmEncoder::new_arm32();
12786        let op = ArmOp::Ldrsh {
12787            rd: Reg::R0,
12788            addr: MemAddr::imm(Reg::R1, 0),
12789        };
12790        let code = encoder.encode(&op).unwrap();
12791        assert_eq!(code.len(), 4, "ARM32 LDRSH should be 4 bytes");
12792    }
12793
12794    #[test]
12795    fn test_encode_ldrb_thumb2_16bit() {
12796        let encoder = ArmEncoder::new_thumb2();
12797        let op = ArmOp::Ldrb {
12798            rd: Reg::R0,
12799            addr: MemAddr::imm(Reg::R1, 4),
12800        };
12801        let code = encoder.encode(&op).unwrap();
12802        // Low registers + small offset -> 16-bit encoding
12803        assert_eq!(
12804            code.len(),
12805            2,
12806            "Thumb-2 LDRB with small offset should be 16-bit"
12807        );
12808    }
12809
12810    #[test]
12811    fn test_encode_ldrb_thumb2_32bit() {
12812        let encoder = ArmEncoder::new_thumb2();
12813        let op = ArmOp::Ldrb {
12814            rd: Reg::R0,
12815            addr: MemAddr::imm(Reg::R1, 100), // offset > 31 needs 32-bit
12816        };
12817        let code = encoder.encode(&op).unwrap();
12818        assert_eq!(
12819            code.len(),
12820            4,
12821            "Thumb-2 LDRB with large offset should be 32-bit"
12822        );
12823    }
12824
12825    #[test]
12826    fn test_encode_strb_thumb2_16bit() {
12827        let encoder = ArmEncoder::new_thumb2();
12828        let op = ArmOp::Strb {
12829            rd: Reg::R0,
12830            addr: MemAddr::imm(Reg::R1, 10),
12831        };
12832        let code = encoder.encode(&op).unwrap();
12833        assert_eq!(
12834            code.len(),
12835            2,
12836            "Thumb-2 STRB with small offset should be 16-bit"
12837        );
12838    }
12839
12840    #[test]
12841    fn test_encode_ldrh_thumb2_16bit() {
12842        let encoder = ArmEncoder::new_thumb2();
12843        let op = ArmOp::Ldrh {
12844            rd: Reg::R0,
12845            addr: MemAddr::imm(Reg::R1, 4), // offset aligned to 2, <= 62
12846        };
12847        let code = encoder.encode(&op).unwrap();
12848        assert_eq!(
12849            code.len(),
12850            2,
12851            "Thumb-2 LDRH with small aligned offset should be 16-bit"
12852        );
12853    }
12854
12855    #[test]
12856    fn test_encode_strh_thumb2_16bit() {
12857        let encoder = ArmEncoder::new_thumb2();
12858        let op = ArmOp::Strh {
12859            rd: Reg::R0,
12860            addr: MemAddr::imm(Reg::R1, 4),
12861        };
12862        let code = encoder.encode(&op).unwrap();
12863        assert_eq!(
12864            code.len(),
12865            2,
12866            "Thumb-2 STRH with small aligned offset should be 16-bit"
12867        );
12868    }
12869
12870    #[test]
12871    fn test_encode_ldrsb_thumb2() {
12872        let encoder = ArmEncoder::new_thumb2();
12873        let op = ArmOp::Ldrsb {
12874            rd: Reg::R0,
12875            addr: MemAddr::imm(Reg::R1, 0),
12876        };
12877        let code = encoder.encode(&op).unwrap();
12878        // LDRSB has no 16-bit immediate form, always 32-bit
12879        assert_eq!(code.len(), 4, "Thumb-2 LDRSB should be 32-bit");
12880    }
12881
12882    #[test]
12883    fn test_encode_ldrsh_thumb2() {
12884        let encoder = ArmEncoder::new_thumb2();
12885        let op = ArmOp::Ldrsh {
12886            rd: Reg::R0,
12887            addr: MemAddr::imm(Reg::R1, 0),
12888        };
12889        let code = encoder.encode(&op).unwrap();
12890        assert_eq!(code.len(), 4, "Thumb-2 LDRSH should be 32-bit");
12891    }
12892
12893    #[test]
12894    fn test_encode_memory_size_thumb2() {
12895        let encoder = ArmEncoder::new_thumb2();
12896        let op = ArmOp::MemorySize { rd: Reg::R0 };
12897        let code = encoder.encode(&op).unwrap();
12898        // R0 and R10 are not both low registers, so this needs careful handling
12899        assert!(!code.is_empty(), "MemorySize should produce code");
12900    }
12901
12902    #[test]
12903    fn test_encode_memory_grow_thumb2() {
12904        let encoder = ArmEncoder::new_thumb2();
12905        let op = ArmOp::MemoryGrow {
12906            rd: Reg::R0,
12907            rn: Reg::R0,
12908        };
12909        let code = encoder.encode(&op).unwrap();
12910        assert_eq!(code.len(), 4, "MemoryGrow (MVN) should be 32-bit Thumb-2");
12911    }
12912
12913    #[test]
12914    fn test_encode_subword_reg_offset_thumb2() {
12915        let encoder = ArmEncoder::new_thumb2();
12916
12917        // LDRB with register offset
12918        let op = ArmOp::Ldrb {
12919            rd: Reg::R0,
12920            addr: MemAddr::reg(Reg::R1, Reg::R2),
12921        };
12922        let code = encoder.encode(&op).unwrap();
12923        assert_eq!(
12924            code.len(),
12925            4,
12926            "Thumb-2 LDRB with reg offset should be 32-bit"
12927        );
12928
12929        // STRB with register offset
12930        let op = ArmOp::Strb {
12931            rd: Reg::R0,
12932            addr: MemAddr::reg(Reg::R1, Reg::R2),
12933        };
12934        let code = encoder.encode(&op).unwrap();
12935        assert_eq!(
12936            code.len(),
12937            4,
12938            "Thumb-2 STRB with reg offset should be 32-bit"
12939        );
12940
12941        // LDRH with register offset
12942        let op = ArmOp::Ldrh {
12943            rd: Reg::R0,
12944            addr: MemAddr::reg(Reg::R1, Reg::R2),
12945        };
12946        let code = encoder.encode(&op).unwrap();
12947        assert_eq!(
12948            code.len(),
12949            4,
12950            "Thumb-2 LDRH with reg offset should be 32-bit"
12951        );
12952
12953        // STRH with register offset
12954        let op = ArmOp::Strh {
12955            rd: Reg::R0,
12956            addr: MemAddr::reg(Reg::R1, Reg::R2),
12957        };
12958        let code = encoder.encode(&op).unwrap();
12959        assert_eq!(
12960            code.len(),
12961            4,
12962            "Thumb-2 STRH with reg offset should be 32-bit"
12963        );
12964    }
12965
12966    #[test]
12967    fn test_encode_subword_reg_imm_offset_thumb2() {
12968        let encoder = ArmEncoder::new_thumb2();
12969
12970        // LDRB with both register and immediate offset
12971        let op = ArmOp::Ldrb {
12972            rd: Reg::R0,
12973            addr: MemAddr::reg_imm(Reg::R1, Reg::R2, 4),
12974        };
12975        let code = encoder.encode(&op).unwrap();
12976        // ADD R12, R2, #4 (4 bytes) + LDRB R0, [R1, R12] (4 bytes) = 8 bytes
12977        assert_eq!(
12978            code.len(),
12979            8,
12980            "Thumb-2 LDRB with reg+imm offset should be 8 bytes"
12981        );
12982    }
12983
12984    // ========================================================================
12985    // Helium MVE encoding tests
12986    // ========================================================================
12987
12988    #[test]
12989    fn test_encode_mve_addi32_thumb2() {
12990        let encoder = ArmEncoder::new_thumb2();
12991        let op = ArmOp::MveAddI {
12992            qd: QReg::Q0,
12993            qn: QReg::Q1,
12994            qm: QReg::Q2,
12995            size: MveSize::S32,
12996        };
12997        let code = encoder.encode(&op).unwrap();
12998        assert_eq!(
12999            code.len(),
13000            4,
13001            "MVE VADD.I32 should be 4 bytes (Thumb-2 32-bit)"
13002        );
13003    }
13004
13005    #[test]
13006    fn test_encode_mve_subi16_thumb2() {
13007        let encoder = ArmEncoder::new_thumb2();
13008        let op = ArmOp::MveSubI {
13009            qd: QReg::Q0,
13010            qn: QReg::Q1,
13011            qm: QReg::Q2,
13012            size: MveSize::S16,
13013        };
13014        let code = encoder.encode(&op).unwrap();
13015        assert_eq!(code.len(), 4, "MVE VSUB.I16 should be 4 bytes");
13016    }
13017
13018    #[test]
13019    fn test_encode_mve_muli8_thumb2() {
13020        let encoder = ArmEncoder::new_thumb2();
13021        let op = ArmOp::MveMulI {
13022            qd: QReg::Q0,
13023            qn: QReg::Q1,
13024            qm: QReg::Q2,
13025            size: MveSize::S8,
13026        };
13027        let code = encoder.encode(&op).unwrap();
13028        assert_eq!(code.len(), 4, "MVE VMUL.I8 should be 4 bytes");
13029    }
13030
13031    #[test]
13032    fn test_encode_mve_bitwise_thumb2() {
13033        let encoder = ArmEncoder::new_thumb2();
13034
13035        let ops = vec![
13036            ArmOp::MveAnd {
13037                qd: QReg::Q0,
13038                qn: QReg::Q1,
13039                qm: QReg::Q2,
13040            },
13041            ArmOp::MveOrr {
13042                qd: QReg::Q0,
13043                qn: QReg::Q1,
13044                qm: QReg::Q2,
13045            },
13046            ArmOp::MveEor {
13047                qd: QReg::Q0,
13048                qn: QReg::Q1,
13049                qm: QReg::Q2,
13050            },
13051            ArmOp::MveBic {
13052                qd: QReg::Q0,
13053                qn: QReg::Q1,
13054                qm: QReg::Q2,
13055            },
13056        ];
13057        for op in ops {
13058            let code = encoder.encode(&op).unwrap();
13059            assert_eq!(code.len(), 4, "MVE bitwise op should be 4 bytes");
13060        }
13061    }
13062
13063    #[test]
13064    fn test_encode_mve_mvn_thumb2() {
13065        let encoder = ArmEncoder::new_thumb2();
13066        let op = ArmOp::MveMvn {
13067            qd: QReg::Q0,
13068            qm: QReg::Q1,
13069        };
13070        let code = encoder.encode(&op).unwrap();
13071        assert_eq!(code.len(), 4, "MVE VMVN should be 4 bytes");
13072    }
13073
13074    #[test]
13075    fn test_encode_mve_load_store_thumb2() {
13076        let encoder = ArmEncoder::new_thumb2();
13077
13078        let load = ArmOp::MveLoad {
13079            qd: QReg::Q0,
13080            addr: MemAddr::imm(Reg::R0, 16),
13081        };
13082        let code = encoder.encode(&load).unwrap();
13083        assert_eq!(code.len(), 4, "MVE VLDRW.32 should be 4 bytes");
13084
13085        let store = ArmOp::MveStore {
13086            qd: QReg::Q1,
13087            addr: MemAddr::imm(Reg::R1, 0),
13088        };
13089        let code = encoder.encode(&store).unwrap();
13090        assert_eq!(code.len(), 4, "MVE VSTRW.32 should be 4 bytes");
13091    }
13092
13093    #[test]
13094    fn test_encode_mve_const_thumb2() {
13095        let encoder = ArmEncoder::new_thumb2();
13096        let op = ArmOp::MveConst {
13097            qd: QReg::Q0,
13098            bytes: [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0],
13099        };
13100        let code = encoder.encode(&op).unwrap();
13101        // Should be 4 words of (MOVW R12 + VMOV Sn) = 4 * (4+4) = 32 bytes min
13102        // Some words with hi16=0 skip MOVT, so length varies
13103        assert!(
13104            code.len() >= 24,
13105            "MVE const should produce multiple instructions"
13106        );
13107    }
13108
13109    #[test]
13110    fn test_encode_mve_dup_thumb2() {
13111        let encoder = ArmEncoder::new_thumb2();
13112        let op = ArmOp::MveDup {
13113            qd: QReg::Q0,
13114            rn: Reg::R0,
13115            size: MveSize::S32,
13116        };
13117        let code = encoder.encode(&op).unwrap();
13118        assert_eq!(code.len(), 4, "MVE VDUP.32 should be 4 bytes");
13119    }
13120
13121    #[test]
13122    fn test_encode_mve_extract_lane_thumb2() {
13123        let encoder = ArmEncoder::new_thumb2();
13124        let op = ArmOp::MveExtractLane {
13125            rd: Reg::R0,
13126            qn: QReg::Q1,
13127            lane: 2,
13128            size: MveSize::S32,
13129        };
13130        let code = encoder.encode(&op).unwrap();
13131        assert_eq!(code.len(), 4, "MVE extract lane should be 4 bytes");
13132    }
13133
13134    #[test]
13135    fn test_encode_mve_insert_lane_thumb2() {
13136        let encoder = ArmEncoder::new_thumb2();
13137        let op = ArmOp::MveInsertLane {
13138            qd: QReg::Q0,
13139            rn: Reg::R1,
13140            lane: 3,
13141            size: MveSize::S32,
13142        };
13143        let code = encoder.encode(&op).unwrap();
13144        assert_eq!(code.len(), 4, "MVE insert lane should be 4 bytes");
13145    }
13146
13147    #[test]
13148    fn test_encode_mve_addf32_thumb2() {
13149        let encoder = ArmEncoder::new_thumb2();
13150        let op = ArmOp::MveAddF32 {
13151            qd: QReg::Q0,
13152            qn: QReg::Q1,
13153            qm: QReg::Q2,
13154        };
13155        let code = encoder.encode(&op).unwrap();
13156        assert_eq!(code.len(), 4, "MVE VADD.F32 should be 4 bytes");
13157    }
13158
13159    #[test]
13160    fn test_encode_mve_divf32_thumb2() {
13161        let encoder = ArmEncoder::new_thumb2();
13162        let op = ArmOp::MveDivF32 {
13163            qd: QReg::Q0,
13164            qn: QReg::Q1,
13165            qm: QReg::Q2,
13166        };
13167        let code = encoder.encode(&op).unwrap();
13168        // Lane-wise: 4 x VDIV.F32 = 4 x 4 = 16 bytes
13169        assert_eq!(
13170            code.len(),
13171            16,
13172            "MVE VDIV.F32 (lane-wise) should be 16 bytes"
13173        );
13174    }
13175
13176    #[test]
13177    fn test_encode_mve_sqrtf32_thumb2() {
13178        let encoder = ArmEncoder::new_thumb2();
13179        let op = ArmOp::MveSqrtF32 {
13180            qd: QReg::Q0,
13181            qm: QReg::Q1,
13182        };
13183        let code = encoder.encode(&op).unwrap();
13184        // Lane-wise: 4 x VSQRT.F32 = 4 x 4 = 16 bytes
13185        assert_eq!(
13186            code.len(),
13187            16,
13188            "MVE VSQRT.F32 (lane-wise) should be 16 bytes"
13189        );
13190    }
13191
13192    #[test]
13193    fn test_encode_mve_negf32_thumb2() {
13194        let encoder = ArmEncoder::new_thumb2();
13195        let op = ArmOp::MveNegF32 {
13196            qd: QReg::Q0,
13197            qm: QReg::Q1,
13198        };
13199        let code = encoder.encode(&op).unwrap();
13200        assert_eq!(code.len(), 4, "MVE VNEG.F32 should be 4 bytes");
13201    }
13202
13203    #[test]
13204    fn test_encode_mve_absf32_thumb2() {
13205        let encoder = ArmEncoder::new_thumb2();
13206        let op = ArmOp::MveAbsF32 {
13207            qd: QReg::Q0,
13208            qm: QReg::Q1,
13209        };
13210        let code = encoder.encode(&op).unwrap();
13211        assert_eq!(code.len(), 4, "MVE VABS.F32 should be 4 bytes");
13212    }
13213
13214    /// VCR-RA-001 / immediate-folding precondition: pins the Thumb-2 `AND`
13215    /// immediate encoding for the byte range and documents its bound.
13216    ///
13217    /// The `And { Operand2::Imm }` encoder packs the low 12 bits straight into
13218    /// the `i:imm3:imm8` field WITHOUT applying ThumbExpandImm (the modified-
13219    /// immediate expansion). For `imm <= 0xFF` (e.g. gale's int8 clamps
13220    /// `#0x7e` / `#0x7f`) that is correct — `i:imm3 = 0000` means "imm8
13221    /// zero-extended". So `and r2, r0, #0x7e` encodes to the canonical
13222    /// `00 f0 7e 02`. For `imm >= 0x100` the field would need a true
13223    /// ThumbExpandImm pattern (rotation / replication), which is NOT
13224    /// implemented here — so **immediate folding must gate on `imm <= 0xFF`**
13225    /// until the encoder is hardened to ThumbExpandImm/Ok-or-Err (the
13226    /// "encoder must be Ok-or-Err, never silently wrong" principle, #180/#185).
13227    /// This bound covers the measured `flat_flight` waste (#209).
13228    #[test]
13229    fn and_immediate_encodes_correctly_in_byte_range_documents_fold_bound() {
13230        let encoder = ArmEncoder::new_thumb2();
13231        let op = ArmOp::And {
13232            rd: Reg::R2,
13233            rn: Reg::R0,
13234            op2: Operand2::Imm(0x7e),
13235        };
13236        let code = encoder.encode(&op).unwrap();
13237        assert_eq!(
13238            code,
13239            vec![0x00, 0xf0, 0x7e, 0x02],
13240            "and r2, r0, #0x7e must encode to the canonical AND.W T1 (imm8=0x7e)"
13241        );
13242    }
13243
13244    /// #255: the shared ThumbExpandImm reverse-encoder underpinning the
13245    /// data-processing immediate fix. Encodable modified immediates round-trip to
13246    /// the expected `i:imm3:imm8` field; a genuinely non-modified value is `None`
13247    /// (caller must materialize into a register). Note `1000 = 0xFA ror 30` *is*
13248    /// representable (field 0xF7A) — the old encoder mis-encoded it (raw 0x3E8);
13249    /// this encodes it correctly.
13250    #[test]
13251    fn try_thumb_expand_imm_encodes_modified_immediates() {
13252        assert_eq!(try_thumb_expand_imm(0x7e), Some(0x07e)); // zero-extended byte
13253        assert_eq!(try_thumb_expand_imm(0xff), Some(0x0ff));
13254        assert_eq!(try_thumb_expand_imm(0x0001_0001), Some(0x101)); // 0x00XY00XY
13255        assert_eq!(try_thumb_expand_imm(0xff00_ff00), Some(0x2ff)); // 0xXY00XY00
13256        assert_eq!(try_thumb_expand_imm(0xffff_ffff), Some(0x3ff)); // 0xXYXYXYXY
13257        assert_eq!(try_thumb_expand_imm(0x100), Some(0xf80)); // 0x80 ror 31
13258        assert_eq!(try_thumb_expand_imm(0x8000_0000), Some(0x400)); // 0x80 ror 8
13259        assert_eq!(try_thumb_expand_imm(1000), Some(0xf7a)); // 0xFA ror 30
13260        // Genuinely unrepresentable (bits too far apart for an 8-bit window).
13261        assert_eq!(try_thumb_expand_imm(0x101), None);
13262        assert_eq!(try_thumb_expand_imm(0x12345), None);
13263    }
13264
13265    /// #255: CMP/ADDS/SUBS encode any valid modified immediate correctly, and
13266    /// ERROR (not silently mis-encode) on a genuinely unrepresentable one,
13267    /// forcing the selector to materialize into a register — closing the
13268    /// silent-miscompile class of #251/#253.
13269    #[test]
13270    fn cmp_adds_subs_immediate_error_on_non_modified_imm() {
13271        let encoder = ArmEncoder::new_thumb2();
13272        // cmp r0, #0xff → valid → Ok; cmp r0, #1000 → valid (0xFA ror 30) → Ok.
13273        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 0xff).is_ok());
13274        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 1000).is_ok());
13275        // cmp r0, #0x101 → NOT a modified immediate → Err (materialize-reg).
13276        assert!(
13277            encoder.encode_thumb32_cmp_imm(&Reg::R0, 0x101).is_err(),
13278            "cmp #0x101 must error, not compare the wrong constant"
13279        );
13280        assert!(
13281            encoder
13282                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x101)
13283                .is_err()
13284        );
13285        assert!(
13286            encoder
13287                .encode_thumb32_subs(&Reg::R0, &Reg::R0, 0x101)
13288                .is_err()
13289        );
13290        // ...but a valid modified immediate still encodes.
13291        assert!(
13292            encoder
13293                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x80)
13294                .is_ok()
13295        );
13296    }
13297
13298    /// #257: MLA (multiply-accumulate) encodes as MLS without the bit-4 op flag.
13299    /// `mla r2, r3, r4, r8` (rd=r2, rn=r3, rm=r4, ra=r8) → Thumb-2 `03 fb 04 82`.
13300    #[test]
13301    fn mla_thumb2_encodes_correctly() {
13302        let encoder = ArmEncoder::new_thumb2();
13303        let code = encoder
13304            .encode(&ArmOp::Mla {
13305                rd: Reg::R2,
13306                rn: Reg::R3,
13307                rm: Reg::R4,
13308                ra: Reg::R8,
13309            })
13310            .unwrap();
13311        // hw1 = 0xFB03, hw2 = (8<<12)|(2<<8)|4 = 0x8204
13312        assert_eq!(code, vec![0x03, 0xfb, 0x04, 0x82]);
13313    }
13314
13315    /// #259: LDR/STR (and sub-word) immediate-offset encoders truncated
13316    /// `offset & 0xFFF`, silently targeting the wrong address for offset >= 4096.
13317    /// They now error (the selector must use register-offset addressing) — the
13318    /// load/store sibling of the #253/#255 class. Offsets <= 4095 still encode.
13319    #[test]
13320    fn ldst_imm12_offset_errors_when_out_of_range() {
13321        let encoder = ArmEncoder::new_thumb2();
13322        // offset 0xFFF (4095): valid → Ok; ldr r0, [r1, #4095].
13323        assert!(
13324            encoder
13325                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0xFFF)
13326                .is_ok()
13327        );
13328        // offset 0x1000 (4096): out of imm12 range → Err (not & 0xFFF → #0).
13329        assert!(
13330            encoder
13331                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0x1000)
13332                .is_err(),
13333            "ldr offset 4096 must error, not wrap to 0"
13334        );
13335        assert!(
13336            encoder
13337                .encode_thumb32_str(&Reg::R0, &Reg::R1, 0x1000)
13338                .is_err()
13339        );
13340        assert!(
13341            encoder
13342                .encode_thumb32_ldrb_imm(&Reg::R0, &Reg::R1, 5000)
13343                .is_err()
13344        );
13345        assert!(
13346            encoder
13347                .encode_thumb32_strh_imm(&Reg::R0, &Reg::R1, 5000)
13348                .is_err()
13349        );
13350    }
13351
13352    /// Latent miscompile fix: ADD/SUB with a >0xFF immediate (e.g.
13353    /// `add sp, sp, #frame` for a >=256-byte frame) used ADD.W (T3), whose
13354    /// `i:imm3:imm8` is a ThumbExpandImm modified immediate — so `#256` silently
13355    /// encoded as `#0` (stack corruption). Use ADDW/SUBW (T4), a PLAIN 12-bit
13356    /// immediate, for 0x100..=0xFFF; keep T3 for <=0xFF (bit-identical); error
13357    /// beyond 4095.
13358    #[test]
13359    fn add_sub_large_immediate_use_addw_subw_not_misencoded() {
13360        let encoder = ArmEncoder::new_thumb2();
13361        // add sp, sp, #256  →  ADDW (T4) SP, SP, #256  =  0d f2 00 1d
13362        assert_eq!(
13363            encoder
13364                .encode(&ArmOp::Add {
13365                    rd: Reg::SP,
13366                    rn: Reg::SP,
13367                    op2: Operand2::Imm(256),
13368                })
13369                .unwrap(),
13370            vec![0x0d, 0xf2, 0x00, 0x1d],
13371            "add sp,sp,#256 must be ADDW (plain imm12), not a mis-encoded ADD.W"
13372        );
13373        // sub sp, sp, #256  →  SUBW (T4) SP, SP, #256  =  ad f2 00 1d
13374        assert_eq!(
13375            encoder
13376                .encode(&ArmOp::Sub {
13377                    rd: Reg::SP,
13378                    rn: Reg::SP,
13379                    op2: Operand2::Imm(256),
13380                })
13381                .unwrap(),
13382            vec![0xad, 0xf2, 0x00, 0x1d],
13383        );
13384        // > 4095 has no single-instruction encoding → error, not silent wrong.
13385        assert!(
13386            encoder
13387                .encode(&ArmOp::Add {
13388                    rd: Reg::SP,
13389                    rn: Reg::SP,
13390                    op2: Operand2::Imm(5000),
13391                })
13392                .is_err(),
13393            "add #5000 must error (no single ADDW), not mis-encode"
13394        );
13395    }
13396
13397    /// Closes the data-proc immediate class: AND and CMN now go through
13398    /// `try_thumb_expand_imm` like ORR/EOR/CMP — correct for any modified
13399    /// immediate, `Err` (not raw-pack / NOP) on an un-encodable one. The byte
13400    /// range stays bit-identical (`and r2,r0,#0x7e` is unchanged).
13401    #[test]
13402    fn and_cmn_immediate_thumb_expand_else_error() {
13403        let encoder = ArmEncoder::new_thumb2();
13404        // byte range unchanged (bit-identical with the pre-retrofit encoding)
13405        assert_eq!(
13406            encoder
13407                .encode(&ArmOp::And {
13408                    rd: Reg::R2,
13409                    rn: Reg::R0,
13410                    op2: Operand2::Imm(0x7e),
13411                })
13412                .unwrap(),
13413            vec![0x00, 0xf0, 0x7e, 0x02],
13414        );
13415        // a valid replicated modified immediate now encodes (was silently wrong)
13416        assert!(
13417            encoder
13418                .encode(&ArmOp::And {
13419                    rd: Reg::R2,
13420                    rn: Reg::R0,
13421                    op2: Operand2::Imm(0xff00ff00u32 as i32),
13422                })
13423                .is_ok()
13424        );
13425        // a genuinely un-encodable immediate errors (AND was raw-pack; CMN NOP)
13426        assert!(
13427            encoder
13428                .encode(&ArmOp::And {
13429                    rd: Reg::R2,
13430                    rn: Reg::R0,
13431                    op2: Operand2::Imm(0x101),
13432                })
13433                .is_err()
13434        );
13435        assert!(
13436            encoder
13437                .encode(&ArmOp::Cmn {
13438                    rn: Reg::R0,
13439                    op2: Operand2::Imm(0x101),
13440                })
13441                .is_err(),
13442            "CMN #0x101 must error, not emit a NOP"
13443        );
13444    }
13445
13446    /// VCR-RA-001: ORR/EOR with a small immediate must encode the real
13447    /// instruction (not a silent `0xBF00` NOP). Pins the byte range and the
13448    /// Ok-or-Err bound that makes future Or/Eor immediate folding safe.
13449    #[test]
13450    fn orr_eor_immediate_encode_in_byte_range_else_error() {
13451        let encoder = ArmEncoder::new_thumb2();
13452        // orr r2, r0, #0x7e  →  ORR.W T1, imm8=0x7e
13453        assert_eq!(
13454            encoder
13455                .encode(&ArmOp::Orr {
13456                    rd: Reg::R2,
13457                    rn: Reg::R0,
13458                    op2: Operand2::Imm(0x7e),
13459                })
13460                .unwrap(),
13461            vec![0x40, 0xf0, 0x7e, 0x02],
13462        );
13463        // eor r2, r0, #0x7e  →  EOR.W T1, imm8=0x7e
13464        assert_eq!(
13465            encoder
13466                .encode(&ArmOp::Eor {
13467                    rd: Reg::R2,
13468                    rn: Reg::R0,
13469                    op2: Operand2::Imm(0x7e),
13470                })
13471                .unwrap(),
13472            vec![0x80, 0xf0, 0x7e, 0x02],
13473        );
13474        // Out-of-range immediates error rather than silently mis-encode / NOP.
13475        assert!(
13476            encoder
13477                .encode(&ArmOp::Orr {
13478                    rd: Reg::R2,
13479                    rn: Reg::R0,
13480                    op2: Operand2::Imm(0x140),
13481                })
13482                .is_err(),
13483            "ORR #0x140 must error, not emit a NOP"
13484        );
13485    }
13486
13487    #[test]
13488    fn test_encode_mve_different_qregs() {
13489        let encoder = ArmEncoder::new_thumb2();
13490
13491        // Test that different Q-register numbers produce different encodings
13492        let op1 = ArmOp::MveAddI {
13493            qd: QReg::Q0,
13494            qn: QReg::Q0,
13495            qm: QReg::Q0,
13496            size: MveSize::S32,
13497        };
13498        let op2 = ArmOp::MveAddI {
13499            qd: QReg::Q3,
13500            qn: QReg::Q5,
13501            qm: QReg::Q7,
13502            size: MveSize::S32,
13503        };
13504        let code1 = encoder.encode(&op1).unwrap();
13505        let code2 = encoder.encode(&op2).unwrap();
13506        assert_ne!(
13507            code1, code2,
13508            "Different Q-registers should produce different encodings"
13509        );
13510    }
13511
13512    #[test]
13513    fn test_encode_mve_arm32_loud_err() {
13514        // #615: MVE (Helium) is Thumb-2-only. The ARM32 encoder used to emit
13515        // a silent NOP here (dropping the vector op); it must now be a typed
13516        // Err so a broken "MVE implies Thumb" invariant fails loudly.
13517        let encoder = ArmEncoder::new_arm32();
13518        let op = ArmOp::MveAddI {
13519            qd: QReg::Q0,
13520            qn: QReg::Q1,
13521            qm: QReg::Q2,
13522            size: MveSize::S32,
13523        };
13524        let err = encoder
13525            .encode(&op)
13526            .expect_err("ARM32 MVE must be a loud Err, not a silent NOP (#615)");
13527        assert!(
13528            err.to_string().contains("Thumb-2 only"),
13529            "unexpected error message: {err}"
13530        );
13531    }
13532}