Skip to main content

synth_backend/
arm_encoder.rs

1//! ARM Code Encoder - Converts ARM instructions to binary machine code
2//!
3//! Generates ARM32/Thumb-2 machine code from ARM instruction structures
4
5use synth_core::Result;
6use synth_core::target::FPUPrecision;
7use synth_synthesis::contracts::encoding as encoding_contracts;
8use synth_synthesis::{ArmOp, MemAddr, MveSize, Operand2, QReg, Reg, VfpReg};
9
10/// ARM instruction encoding
11pub struct ArmEncoder {
12    /// Use Thumb mode (vs ARM mode)
13    thumb_mode: bool,
14    /// FPU capability for VFP instruction encoding
15    #[allow(dead_code)]
16    fpu: Option<FPUPrecision>,
17}
18
19impl ArmEncoder {
20    /// Create a new ARM encoder in ARM32 mode
21    pub fn new_arm32() -> Self {
22        Self {
23            thumb_mode: false,
24            fpu: None,
25        }
26    }
27
28    /// Create a new ARM encoder in Thumb-2 mode
29    pub fn new_thumb2() -> Self {
30        Self {
31            thumb_mode: true,
32            fpu: None,
33        }
34    }
35
36    /// Create a new Thumb-2 encoder with FPU capability
37    pub fn new_thumb2_with_fpu(fpu: Option<FPUPrecision>) -> Self {
38        Self {
39            thumb_mode: true,
40            fpu,
41        }
42    }
43
44    /// Encode a single ARM instruction to bytes
45    pub fn encode(&self, op: &ArmOp) -> Result<Vec<u8>> {
46        if self.thumb_mode {
47            self.encode_thumb(op)
48        } else {
49            self.encode_arm(op)
50        }
51    }
52
53    /// Encode an ARM instruction in ARM32 mode (32-bit instructions)
54    /// #206: encode an ARM32 (A32) load/store whose address uses a register
55    /// offset (`[rn, rm{, #off}]`). Returns `None` for ops with no register
56    /// offset (the caller falls through to the immediate-form arms). Computes
57    /// `ip = base + rm` then re-encodes the op against `[ip, #off]`, which works
58    /// uniformly for word/byte/halfword/signed forms. IP (R12) is the scratch
59    /// register the selector already treats as clobberable across memory ops.
60    fn encode_arm_reg_offset_mem(&self, op: &ArmOp) -> Result<Option<Vec<u8>>> {
61        use synth_synthesis::Reg;
62        let addr = match op {
63            ArmOp::Ldr { addr, .. }
64            | ArmOp::Str { addr, .. }
65            | ArmOp::Ldrb { addr, .. }
66            | ArmOp::Strb { addr, .. }
67            | ArmOp::Ldrh { addr, .. }
68            | ArmOp::Strh { addr, .. }
69            | ArmOp::Ldrsb { addr, .. }
70            | ArmOp::Ldrsh { addr, .. } => addr,
71            _ => return Ok(None),
72        };
73        let Some(rm) = addr.offset_reg else {
74            return Ok(None);
75        };
76        let ip = Reg::R12;
77        // ADD ip, base, rm  (cond=AL, opcode=ADD, S=0, register operand2)
78        let add: u32 = 0xE0800000
79            | (reg_to_bits(&addr.base) << 16)
80            | (reg_to_bits(&ip) << 12)
81            | reg_to_bits(&rm);
82        let mut bytes = add.to_le_bytes().to_vec();
83        // Re-encode the op against [ip, #off] (immediate form → no offset_reg,
84        // so this recursion hits the immediate arms, not this helper again).
85        let imm_addr = MemAddr::imm(ip, addr.offset);
86        let imm_op = match op {
87            ArmOp::Ldr { rd, .. } => ArmOp::Ldr {
88                rd: *rd,
89                addr: imm_addr,
90            },
91            ArmOp::Str { rd, .. } => ArmOp::Str {
92                rd: *rd,
93                addr: imm_addr,
94            },
95            ArmOp::Ldrb { rd, .. } => ArmOp::Ldrb {
96                rd: *rd,
97                addr: imm_addr,
98            },
99            ArmOp::Strb { rd, .. } => ArmOp::Strb {
100                rd: *rd,
101                addr: imm_addr,
102            },
103            ArmOp::Ldrh { rd, .. } => ArmOp::Ldrh {
104                rd: *rd,
105                addr: imm_addr,
106            },
107            ArmOp::Strh { rd, .. } => ArmOp::Strh {
108                rd: *rd,
109                addr: imm_addr,
110            },
111            ArmOp::Ldrsb { rd, .. } => ArmOp::Ldrsb {
112                rd: *rd,
113                addr: imm_addr,
114            },
115            ArmOp::Ldrsh { rd, .. } => ArmOp::Ldrsh {
116                rd: *rd,
117                addr: imm_addr,
118            },
119            _ => unreachable!(),
120        };
121        bytes.extend(self.encode_arm(&imm_op)?);
122        Ok(Some(bytes))
123    }
124
125    /// #594: A32 expansion of `ArmOp::CallIndirect` — mirror of the Thumb-2
126    /// arm (same contract: R11 holds the function-pointer table base, entry
127    /// `i` is a 4-byte code address, R12 is the encoder-scratch register):
128    ///
129    /// ```text
130    /// MOVW r12, #size        ; #642: table size (compile-time immediate)
131    /// [MOVT r12, #size>>16]  ; only when size exceeds 16 bits
132    /// CMP  idx, r12          ; bounds guard: index >= size must TRAP
133    /// BLO  +1 insn           ; skip the trap when in bounds
134    /// UDF                    ; WASM Core §4.4.8 out-of-bounds trap
135    /// MOV r12, idx, LSL #2   ; table byte offset
136    /// LDR r12, [r11, r12]    ; load function pointer
137    /// BLX r12                ; indirect call
138    /// ```
139    ///
140    /// #650, `table_byte_offset != 0` (a non-zero table of the contiguous
141    /// R11 region): the pointer load becomes
142    /// `ADD r12, r11, r12; LDR r12, [r12, #offset]` — offset 0 keeps the
143    /// single-load form (single-table modules byte-identical by
144    /// construction).
145    ///
146    /// #664, `null_check` (the table has null slots, linked as ZERO words
147    /// per the layout contract): `CMP r12, #0; BNE +1; UDF` between the
148    /// pointer load and the `BLX` — a call reaching an uninitialized slot
149    /// traps (§4.4.8). `false` keeps the expansion byte-identical.
150    ///
151    /// #676, `type_check` (heterogeneous table): the §4.4.8 type check is
152    /// discharged at RUNTIME against the type-id sidecar — after the bounds
153    /// guard, `MOV r12, idx, LSL #2; ADD r12, r11, r12;
154    /// LDR r12, [r12, #type_off]; CMP r12, #expected_id; BEQ +1; UDF`
155    /// (mirror of the Thumb-2 arm; the dispatch tail recomputes `idx*4`).
156    /// Null slots carry the reserved class id 0, subsuming the #664 null
157    /// trap. `None` (every homogeneous table — the verdict discharged at
158    /// COMPILE time by the closed-world verification, see the #642 selector
159    /// guard) emits nothing and keeps the expansion byte-identical.
160    fn encode_arm_call_indirect(
161        table_index_reg: &Reg,
162        table_size: u32,
163        table_byte_offset: u32,
164        null_check: bool,
165        type_check: Option<(u32, u32)>,
166    ) -> Vec<u8> {
167        let idx = reg_to_bits(table_index_reg);
168        let mut bytes = Vec::with_capacity(32);
169        // MOVW r12, #(size & 0xFFFF) — cond=E 0011 0000 imm4 Rd imm12.
170        let size_lo = table_size & 0xFFFF;
171        let movw: u32 = 0xE300_0000 | ((size_lo >> 12) << 16) | (12 << 12) | (size_lo & 0xFFF);
172        bytes.extend_from_slice(&movw.to_le_bytes());
173        // MOVT r12, #(size >> 16) — only for a table size above 16 bits.
174        let size_hi = table_size >> 16;
175        if size_hi != 0 {
176            let movt: u32 = 0xE340_0000 | ((size_hi >> 12) << 16) | (12 << 12) | (size_hi & 0xFFF);
177            bytes.extend_from_slice(&movt.to_le_bytes());
178        }
179        // CMP idx, r12 — cond=E, opcode=1010, S=1, Rn=idx, Rm=r12.
180        let cmp: u32 = 0xE150_000C | (idx << 16);
181        bytes.extend_from_slice(&cmp.to_le_bytes());
182        // BLO +1 insn (skip the UDF when index < size) — cond=LO(0011),
183        // imm24=0: target = branch + 8.
184        bytes.extend_from_slice(&0x3A00_0000u32.to_le_bytes());
185        // UDF — permanently undefined (same trap idiom as the A32 div-by-zero
186        // guards): call_indirect out-of-bounds trap.
187        bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
188        // #676: runtime type check for a heterogeneous table — load the
189        // indexed slot's structural class id from the type-id sidecar and
190        // trap on mismatch (§4.4.8). Mirror of the Thumb-2 arm; `None`
191        // emits nothing (homogeneous tables byte-identical by construction).
192        if let Some((expected_id, type_off)) = type_check {
193            debug_assert!(expected_id <= 255, "selector enforces the CMP imm8 range");
194            debug_assert!(type_off <= 4095, "selector enforces the LDR imm12 range");
195            // MOV r12, idx, LSL #2 (same as the dispatch tail's scale).
196            bytes.extend_from_slice(&(0xE1A0C000u32 | (2 << 7) | idx).to_le_bytes());
197            // ADD r12, r11, r12 — data-processing ADD (register).
198            bytes.extend_from_slice(&0xE08BC00Cu32.to_le_bytes());
199            // LDR r12, [r12, #type_off] — immediate offset, P=1 U=1 L=1.
200            bytes.extend_from_slice(&(0xE59CC000u32 | (type_off & 0xFFF)).to_le_bytes());
201            // CMP r12, #expected_id — data-processing CMP (immediate).
202            bytes.extend_from_slice(&(0xE35C_0000u32 | (expected_id & 0xFF)).to_le_bytes());
203            // BEQ +1 insn (skip the UDF when the class id matches) —
204            // cond=EQ(0000), imm24=0: target = branch + 8.
205            bytes.extend_from_slice(&0x0A00_0000u32.to_le_bytes());
206            // UDF — the §4.4.8 type-mismatch trap.
207            bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
208        }
209        // MOV r12, idx, LSL #2 — data-processing MOV, register op2 with
210        // imm5=2/LSL: cond=E, opcode=1101, S=0, Rd=r12.
211        let mov: u32 = 0xE1A0C000 | (2 << 7) | idx;
212        bytes.extend_from_slice(&mov.to_le_bytes());
213        if table_byte_offset == 0 {
214            // Table 0 (base = R11 itself): the pre-#650 single-load form.
215            // LDR r12, [r11, r12] — register offset, P=1 U=1 B=0 W=0 L=1.
216            let ldr: u32 = 0xE79BC00C;
217            bytes.extend_from_slice(&ldr.to_le_bytes());
218        } else {
219            // #650: fold the table's compile-time base offset into the
220            // pointer load via the LDR imm12 form.
221            assert!(
222                table_byte_offset <= 4095,
223                "call_indirect table base offset {table_byte_offset} exceeds \
224                 LDR imm12 — the selector must have declined this (#650)"
225            );
226            // ADD r12, r11, r12 — data-processing ADD (register).
227            bytes.extend_from_slice(&0xE08BC00Cu32.to_le_bytes());
228            // LDR r12, [r12, #offset] — immediate offset, P=1 U=1 L=1.
229            let ldr: u32 = 0xE59CC000 | (table_byte_offset & 0xFFF);
230            bytes.extend_from_slice(&ldr.to_le_bytes());
231        }
232        // #664: null-slot trap — only when the table image has null slots
233        // (zero-linked words). A fully-initialized table keeps the pre-#664
234        // bytes identical by construction.
235        if null_check {
236            // CMP r12, #0 — data-processing CMP (immediate), Rn=r12.
237            bytes.extend_from_slice(&0xE35C_0000u32.to_le_bytes());
238            // BNE +1 insn (skip the UDF when the pointer is non-null) —
239            // cond=NE(0001), imm24=0: target = branch + 8.
240            bytes.extend_from_slice(&0x1A00_0000u32.to_le_bytes());
241            // UDF — the §4.4.8 uninitialized-element trap (same idiom as
242            // the bounds guard).
243            bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
244        }
245        // BLX r12 — cond=E, 0001 0010 1111 1111 1111 0011, Rm=r12.
246        let blx: u32 = 0xE12FFF3C;
247        bytes.extend_from_slice(&blx.to_le_bytes());
248        bytes
249    }
250
251    /// #615: A32 (ARM-mode) expansions for the multi-instruction ops that the
252    /// Thumb-2 encoder expands but the A32 arm previously encoded as a single
253    /// literal NOP (`0xE1A00000`) — i64 mul / shifts / rotates / comparisons /
254    /// eqz, plus i64 const/load/store/extend/wrap and the i32 SetCond /
255    /// SelectMove pseudo-ops. Each expansion mirrors its Thumb-2 twin's
256    /// register contract and semantics exactly (A32 conditional execution
257    /// replaces the IT blocks). Returns `Ok(None)` for ops this helper does
258    /// not handle; the caller's match encodes or loudly rejects those.
259    fn encode_arm_expanded(&self, op: &ArmOp) -> Result<Option<Vec<u8>>> {
260        use synth_synthesis::Condition;
261
262        /// A32 condition-field bits (instruction bits [31:28]).
263        fn cond_bits(cond: &Condition) -> u32 {
264            match cond {
265                Condition::EQ => 0x0,
266                Condition::NE => 0x1,
267                Condition::HS => 0x2, // CS: unsigned >=
268                Condition::LO => 0x3, // CC: unsigned <
269                Condition::HI => 0x8, // unsigned >
270                Condition::LS => 0x9, // unsigned <=
271                Condition::GE => 0xA,
272                Condition::LT => 0xB,
273                Condition::GT => 0xC,
274                Condition::LE => 0xD,
275            }
276        }
277        fn w(b: &mut Vec<u8>, word: u32) {
278            b.extend_from_slice(&word.to_le_bytes());
279        }
280        /// MOV<cond> rd, #imm (rotated-immediate form; only 0/1 used here).
281        fn mov_cond_imm(b: &mut Vec<u8>, cond: u32, rd: u32, imm: u32) {
282            w(b, (cond << 28) | 0x03A0_0000 | (rd << 12) | imm);
283        }
284        /// After a flag-setting pair: MOV<cond> rd,#1 ; MOV<!cond> rd,#0.
285        fn set_cond(b: &mut Vec<u8>, cond: &Condition, rd: u32) {
286            mov_cond_imm(b, cond_bits(cond), rd, 1);
287            mov_cond_imm(b, cond_bits(&cond.invert()), rd, 0);
288        }
289        /// CMP rn, rm (register form).
290        fn cmp_reg(b: &mut Vec<u8>, rn: u32, rm: u32) {
291            w(b, 0xE150_0000 | (rn << 16) | rm);
292        }
293        /// SBCS rd, rn, rm — the 64-bit compare idiom's high-word subtract.
294        fn sbcs(b: &mut Vec<u8>, rd: u32, rn: u32, rm: u32) {
295            w(b, 0xE0D0_0000 | (rn << 16) | (rd << 12) | rm);
296        }
297        /// MOVW rd, #imm16.
298        fn movw(b: &mut Vec<u8>, rd: u32, v: u32) {
299            w(
300                b,
301                0xE300_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
302            );
303        }
304        /// MOVT rd, #imm16.
305        fn movt(b: &mut Vec<u8>, rd: u32, v: u32) {
306            w(
307                b,
308                0xE340_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
309            );
310        }
311        /// Register-controlled shift: MOV rd, rn, <LSL|LSR|ASR> rs.
312        /// `ty`: 0=LSL, 1=LSR, 2=ASR. A32 uses the bottom byte of rs;
313        /// amounts of 32 or more yield 0 (LSL/LSR) or all-sign (ASR) — same
314        /// semantics the Thumb-2 expansions rely on.
315        fn shift_reg(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, rs: u32) {
316            w(b, 0xE1A0_0010 | (rd << 12) | (rs << 8) | (ty << 5) | rn);
317        }
318        const LSL: u32 = 0;
319        const LSR: u32 = 1;
320        const ASR: u32 = 2;
321        /// Immediate-shift move: MOV rd, rn, <LSL|LSR|ASR> #imm.
322        fn shift_imm(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, imm: u32) {
323            w(
324                b,
325                0xE1A0_0000 | (rd << 12) | ((imm & 0x1F) << 7) | (ty << 5) | rn,
326            );
327        }
328        /// Data-processing register form: `base | rn<<16 | rd<<12 | rm`.
329        /// `base` carries cond/opcode/S (e.g. 0xE090_0000 = ADDS).
330        fn dp_reg(b: &mut Vec<u8>, base: u32, rd: u32, rn: u32, rm: u32) {
331            w(b, base | (rn << 16) | (rd << 12) | rm);
332        }
333        /// Data-processing with an immediate-shifted register operand:
334        /// `<op> rd, rn, rm, <LSL|LSR|ASR> #imm` — the A32 barrel shifter
335        /// folds a shift into the second operand for free. #1021 uses this to
336        /// run the popcnt SWAR fold on R12 alone (no second scratch, so R11 —
337        /// the linear-memory base — is never touched).
338        fn dp_reg_shift(b: &mut Vec<u8>, base: u32, rd: u32, rn: u32, rm: u32, ty: u32, imm: u32) {
339            w(
340                b,
341                base | (rn << 16) | (rd << 12) | ((imm & 0x1F) << 7) | (ty << 5) | rm,
342            );
343        }
344        /// ORR rd, rd, rm, LSR #31 — the carry-propagation idiom of the
345        /// shift-subtract division loop (bring rm's MSB into rd's bit 0).
346        fn orr_lsr31(b: &mut Vec<u8>, rd: u32, rm: u32) {
347            w(
348                b,
349                0xE180_0000 | (rd << 16) | (rd << 12) | (31 << 7) | (1 << 5) | rm,
350            );
351        }
352        /// 64-bit two's-complement negate of the lo:hi pair (MVN/MVN/ADDS/ADC).
353        fn negate64(b: &mut Vec<u8>, lo: u32, hi: u32) {
354            w(b, 0xE1E0_0000 | (lo << 12) | lo); //           MVN  lo, lo
355            w(b, 0xE1E0_0000 | (hi << 12) | hi); //           MVN  hi, hi
356            w(b, 0xE290_0001 | (lo << 16) | (lo << 12)); //   ADDS lo, lo, #1
357            w(b, 0xE2A0_0000 | (hi << 16) | (hi << 12)); //   ADC  hi, hi, #0
358        }
359        /// TST x, x ; BPL +4-instructions — the "skip the negate64 when the
360        /// sign bit is clear" guard of the signed div/rem arms.
361        fn skip_negate_if_positive(b: &mut Vec<u8>, x: u32) {
362            w(b, 0xE110_0000 | (x << 16) | x); // TST x, x
363            w(b, 0x5A00_0003); //                 BPL +4 insns (past negate64)
364        }
365        /// The 64-iteration shift-subtract division loop — A32 transcription
366        /// of the Thumb-2 #610 core: dividend R0:R1, divisor R2:R3, quotient
367        /// R4:R5, remainder R6:R7, loop counter in `counter` (R12 or R8).
368        fn div_loop(b: &mut Vec<u8>, counter: u32) {
369            w(b, 0xE3A0_0040 | (counter << 12)); // MOV counter, #64
370            let loop_start = b.len();
371            // quotient <<= 1
372            shift_imm(b, LSL, 5, 5, 1);
373            orr_lsr31(b, 5, 4);
374            shift_imm(b, LSL, 4, 4, 1);
375            // remainder <<= 1, OR in dividend MSB
376            shift_imm(b, LSL, 7, 7, 1);
377            orr_lsr31(b, 7, 6);
378            shift_imm(b, LSL, 6, 6, 1);
379            orr_lsr31(b, 6, 1);
380            // dividend <<= 1
381            shift_imm(b, LSL, 1, 1, 1);
382            orr_lsr31(b, 1, 0);
383            shift_imm(b, LSL, 0, 0, 1);
384            // if remainder >= divisor (64-bit unsigned): subtract, set q bit
385            w(b, 0xE157_0003); // CMP R7, R3      (high words)
386            w(b, 0x8A00_0002); // BHI .subtract   (+2 insns)
387            w(b, 0x3A00_0004); // BLO .next       (+4 insns)
388            w(b, 0xE156_0002); // CMP R6, R2      (low words, highs equal)
389            w(b, 0x3A00_0002); // BLO .next       (+2 insns)
390            w(b, 0xE056_6002); // .subtract: SUBS R6, R6, R2
391            w(b, 0xE0C7_7003); //            SBC  R7, R7, R3
392            w(b, 0xE384_4001); //            ORR  R4, R4, #1
393            // .next: decrement and loop
394            w(b, 0xE250_0001 | (counter << 16) | (counter << 12)); // SUBS counter, #1
395            let diff = (loop_start as i64) - (b.len() as i64 + 8);
396            w(b, 0x1A00_0000 | (((diff / 4) as u32) & 0x00FF_FFFF)); // BNE loop
397        }
398        /// 32-bit population count on working register `x` — A32 transcription
399        /// of the Thumb-2 I64Popcnt per-word core (mul-based fold): `c` is the
400        /// constant register, R12 the shifted temp. Both are clobbered.
401        fn popcnt_word(b: &mut Vec<u8>, x: u32, c: u32) {
402            // x = x - ((x >> 1) & 0x55555555)
403            shift_imm(b, LSR, 12, x, 1);
404            movw(b, c, 0x5555);
405            movt(b, c, 0x5555);
406            dp_reg(b, 0xE000_0000, 12, 12, c); // AND R12, R12, c
407            dp_reg(b, 0xE040_0000, x, x, 12); //  SUB x, x, R12
408            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
409            movw(b, c, 0x3333);
410            movt(b, c, 0x3333);
411            dp_reg(b, 0xE000_0000, 12, x, c); //  AND R12, x, c
412            shift_imm(b, LSR, x, x, 2);
413            dp_reg(b, 0xE000_0000, x, x, c); //   AND x, x, c
414            dp_reg(b, 0xE080_0000, x, x, 12); //  ADD x, x, R12
415            // x = (x + (x >> 4)) & 0x0F0F0F0F
416            shift_imm(b, LSR, 12, x, 4);
417            dp_reg(b, 0xE080_0000, x, x, 12); //  ADD x, x, R12
418            movw(b, c, 0x0F0F);
419            movt(b, c, 0x0F0F);
420            dp_reg(b, 0xE000_0000, x, x, c); //   AND x, x, c
421            // x = (x * 0x01010101) >> 24
422            movw(b, c, 0x0101);
423            movt(b, c, 0x0101);
424            w(b, 0xE000_0090 | (x << 16) | (c << 8) | x); // MUL x, x, c
425            shift_imm(b, LSR, x, x, 24);
426        }
427
428        let mut b: Vec<u8> = Vec::new();
429        match op {
430            // SetCond: materialize a flags-predicate as 0/1 — the A32 twin of
431            // the Thumb `ITE cond; MOV rd,#1; MOV rd,#0`.
432            ArmOp::SetCond { rd, cond } => {
433                set_cond(&mut b, cond, reg_to_bits(rd));
434            }
435
436            // SelectMove: conditional register move (Thumb: IT cond; MOV).
437            ArmOp::SelectMove { rd, rm, cond } => {
438                w(
439                    &mut b,
440                    (cond_bits(cond) << 28)
441                        | 0x01A0_0000
442                        | (reg_to_bits(rd) << 12)
443                        | reg_to_bits(rm),
444                );
445            }
446
447            // I64SetCond: compare two i64 register pairs, 0/1 into rd.
448            // EQ/NE: CMP lo,lo; CMPEQ hi,hi (only if lows equal); set.
449            // Ordered: CMP lo,lo; SBCS rd,hi,hi; set — with the same
450            // operand-swap + condition mapping as the Thumb-2 arm.
451            ArmOp::I64SetCond {
452                rd,
453                rn_lo,
454                rn_hi,
455                rm_lo,
456                rm_hi,
457                cond,
458            } => {
459                let rd_b = reg_to_bits(rd);
460                let (n_lo, n_hi, m_lo, m_hi) = (
461                    reg_to_bits(rn_lo),
462                    reg_to_bits(rn_hi),
463                    reg_to_bits(rm_lo),
464                    reg_to_bits(rm_hi),
465                );
466                match cond {
467                    Condition::EQ | Condition::NE => {
468                        cmp_reg(&mut b, n_lo, m_lo);
469                        // CMP<EQ> rn_hi, rm_hi — compare highs only if lows equal.
470                        w(&mut b, 0x0150_0000 | (n_hi << 16) | m_hi);
471                        set_cond(&mut b, cond, rd_b);
472                    }
473                    // (swap operands?, condition after SBCS) per the Thumb arm:
474                    // LT/GE/LO/HS compare (rn, rm); GT/LE/HI/LS swap to (rm, rn).
475                    Condition::LT => {
476                        cmp_reg(&mut b, n_lo, m_lo);
477                        sbcs(&mut b, rd_b, n_hi, m_hi);
478                        set_cond(&mut b, &Condition::LT, rd_b);
479                    }
480                    Condition::GE => {
481                        cmp_reg(&mut b, n_lo, m_lo);
482                        sbcs(&mut b, rd_b, n_hi, m_hi);
483                        set_cond(&mut b, &Condition::GE, rd_b);
484                    }
485                    Condition::GT => {
486                        cmp_reg(&mut b, m_lo, n_lo);
487                        sbcs(&mut b, rd_b, m_hi, n_hi);
488                        set_cond(&mut b, &Condition::LT, rd_b);
489                    }
490                    Condition::LE => {
491                        cmp_reg(&mut b, m_lo, n_lo);
492                        sbcs(&mut b, rd_b, m_hi, n_hi);
493                        set_cond(&mut b, &Condition::GE, rd_b);
494                    }
495                    Condition::LO => {
496                        cmp_reg(&mut b, n_lo, m_lo);
497                        sbcs(&mut b, rd_b, n_hi, m_hi);
498                        set_cond(&mut b, &Condition::LO, rd_b);
499                    }
500                    Condition::HS => {
501                        cmp_reg(&mut b, n_lo, m_lo);
502                        sbcs(&mut b, rd_b, n_hi, m_hi);
503                        set_cond(&mut b, &Condition::HS, rd_b);
504                    }
505                    Condition::HI => {
506                        cmp_reg(&mut b, m_lo, n_lo);
507                        sbcs(&mut b, rd_b, m_hi, n_hi);
508                        set_cond(&mut b, &Condition::LO, rd_b);
509                    }
510                    Condition::LS => {
511                        cmp_reg(&mut b, m_lo, n_lo);
512                        sbcs(&mut b, rd_b, m_hi, n_hi);
513                        set_cond(&mut b, &Condition::HS, rd_b);
514                    }
515                }
516            }
517
518            // I64SetCondZ: ORRS rd, lo, hi sets Z iff the pair is zero.
519            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
520                let rd_b = reg_to_bits(rd);
521                w(
522                    &mut b,
523                    0xE190_0000 | (reg_to_bits(rn_lo) << 16) | (rd_b << 12) | reg_to_bits(rn_hi),
524                );
525                set_cond(&mut b, &Condition::EQ, rd_b);
526            }
527
528            // i64 comparison wrappers: delegate to I64SetCond/Z, mirroring the
529            // Thumb-2 delegation arms.
530            ArmOp::I64Eqz { rd, rnlo, rnhi } => {
531                return self
532                    .encode_arm(&ArmOp::I64SetCondZ {
533                        rd: *rd,
534                        rn_lo: *rnlo,
535                        rn_hi: *rnhi,
536                    })
537                    .map(Some);
538            }
539            ArmOp::I64Eq {
540                rd,
541                rnlo,
542                rnhi,
543                rmlo,
544                rmhi,
545            }
546            | ArmOp::I64Ne {
547                rd,
548                rnlo,
549                rnhi,
550                rmlo,
551                rmhi,
552            }
553            | ArmOp::I64LtS {
554                rd,
555                rnlo,
556                rnhi,
557                rmlo,
558                rmhi,
559            }
560            | ArmOp::I64LtU {
561                rd,
562                rnlo,
563                rnhi,
564                rmlo,
565                rmhi,
566            }
567            | ArmOp::I64LeS {
568                rd,
569                rnlo,
570                rnhi,
571                rmlo,
572                rmhi,
573            }
574            | ArmOp::I64LeU {
575                rd,
576                rnlo,
577                rnhi,
578                rmlo,
579                rmhi,
580            }
581            | ArmOp::I64GtS {
582                rd,
583                rnlo,
584                rnhi,
585                rmlo,
586                rmhi,
587            }
588            | ArmOp::I64GtU {
589                rd,
590                rnlo,
591                rnhi,
592                rmlo,
593                rmhi,
594            }
595            | ArmOp::I64GeS {
596                rd,
597                rnlo,
598                rnhi,
599                rmlo,
600                rmhi,
601            }
602            | ArmOp::I64GeU {
603                rd,
604                rnlo,
605                rnhi,
606                rmlo,
607                rmhi,
608            } => {
609                let cond = match op {
610                    ArmOp::I64Eq { .. } => Condition::EQ,
611                    ArmOp::I64Ne { .. } => Condition::NE,
612                    ArmOp::I64LtS { .. } => Condition::LT,
613                    ArmOp::I64LtU { .. } => Condition::LO,
614                    ArmOp::I64LeS { .. } => Condition::LE,
615                    ArmOp::I64LeU { .. } => Condition::LS,
616                    ArmOp::I64GtS { .. } => Condition::GT,
617                    ArmOp::I64GtU { .. } => Condition::HI,
618                    ArmOp::I64GeS { .. } => Condition::GE,
619                    _ => Condition::HS,
620                };
621                return self
622                    .encode_arm(&ArmOp::I64SetCond {
623                        rd: *rd,
624                        rn_lo: *rnlo,
625                        rn_hi: *rnhi,
626                        rm_lo: *rmlo,
627                        rm_hi: *rmhi,
628                        cond,
629                    })
630                    .map(Some);
631            }
632
633            // I64Mul: cross products into R12, then UMULL — same sequence and
634            // ordering as the Thumb-2 arm (R12 is encoder scratch, #212).
635            ArmOp::I64Mul {
636                rd_lo,
637                rd_hi,
638                rn_lo,
639                rn_hi,
640                rm_lo,
641                rm_hi,
642            } => {
643                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
644                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
645                let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
646                // MUL R12, rn_lo, rm_hi   (R12 = a_lo * b_hi)
647                w(&mut b, 0xE000_0090 | (12 << 16) | (mh << 8) | nl);
648                // MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
649                w(
650                    &mut b,
651                    0xE020_0090 | (12 << 16) | (12 << 12) | (ml << 8) | nh,
652                );
653                // UMULL rd_lo, rd_hi, rn_lo, rm_lo
654                w(
655                    &mut b,
656                    0xE080_0090 | (dh << 16) | (dl << 12) | (ml << 8) | nl,
657                );
658                // ADD rd_hi, rd_hi, R12
659                w(&mut b, 0xE080_0000 | (dh << 16) | (dh << 12) | 12);
660            }
661
662            // I64Shl / I64ShrU / I64ShrS: same small/large-shift structure as
663            // the Thumb-2 arms. #1048: the expansion must never write its own
664            // input operands — the pre-#1048 A32 arms masked the amount in
665            // place (`AND ml, ml, #63`) and used the amount's home high
666            // register as scratch, identically to the Thumb-2 defect. R12
667            // (encoder scratch, never allocatable, #212) is the ONLY
668            // temporary; the masked amount is re-derived from the untouched
669            // rm_lo where a second live temp would otherwise be needed.
670            // Register-controlled shifts >= 32 yield 0, which the small path
671            // relies on for n = 0. Same #1039-style loud alias guards as the
672            // Thumb-2 arms.
673            ArmOp::I64Shl {
674                rd_lo,
675                rd_hi,
676                rn_lo,
677                rn_hi,
678                rm_lo,
679                rm_hi: _,
680            } => {
681                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
682                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
683                let ml = reg_to_bits(rm_lo);
684                if dh == nl || dh == ml {
685                    return Err(synth_core::Error::synthesis(format!(
686                        "I64Shl (A32): rd_hi {rd_hi:?} aliases an input ({rn_lo:?}/{rm_lo:?}) still live inside the expansion (#1048)"
687                    )));
688                }
689                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
690                w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
691                w(&mut b, 0x5A00_0007); //                            BPL  .large
692                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
693                shift_reg(&mut b, LSL, dh, nh, 12); //               dh = hi << n
694                w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB  r12, r12, #32
695                shift_reg(&mut b, LSR, 12, nl, 12); //               r12 = lo >> (32-n)
696                w(&mut b, 0xE180_0000 | (dh << 16) | (dh << 12) | 12); // ORR dh, dh, r12
697                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
698                shift_reg(&mut b, LSL, dl, nl, 12); //               dl = lo << n
699                w(&mut b, 0xEA00_0001); //                            B    .done
700                shift_reg(&mut b, LSL, dh, nl, 12); //               .large: dh = lo << (n-32)
701                w(&mut b, 0xE3A0_0000 | (dl << 12)); //              MOV  dl, #0
702            }
703            ArmOp::I64ShrU {
704                rd_lo,
705                rd_hi,
706                rn_lo,
707                rn_hi,
708                rm_lo,
709                rm_hi: _,
710            } => {
711                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
712                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
713                let ml = reg_to_bits(rm_lo);
714                if dl == nh || dl == ml {
715                    return Err(synth_core::Error::synthesis(format!(
716                        "I64ShrU (A32): rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
717                    )));
718                }
719                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
720                w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
721                w(&mut b, 0x5A00_0007); //                            BPL  .large
722                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
723                shift_reg(&mut b, LSR, dl, nl, 12); //               dl = lo >> n
724                w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB  r12, r12, #32
725                shift_reg(&mut b, LSL, 12, nh, 12); //               r12 = hi << (32-n)
726                w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | 12); // ORR dl, dl, r12
727                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
728                shift_reg(&mut b, LSR, dh, nh, 12); //               dh = hi >> n
729                w(&mut b, 0xEA00_0001); //                            B    .done
730                shift_reg(&mut b, LSR, dl, nh, 12); //               .large: dl = hi >> (n-32)
731                w(&mut b, 0xE3A0_0000 | (dh << 12)); //              MOV  dh, #0
732            }
733            ArmOp::I64ShrS {
734                rd_lo,
735                rd_hi,
736                rn_lo,
737                rn_hi,
738                rm_lo,
739                rm_hi: _,
740            } => {
741                let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
742                let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
743                let ml = reg_to_bits(rm_lo);
744                if dl == nh || dl == ml {
745                    return Err(synth_core::Error::synthesis(format!(
746                        "I64ShrS (A32): rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
747                    )));
748                }
749                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
750                w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
751                w(&mut b, 0x5A00_0007); //                            BPL  .large
752                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
753                shift_reg(&mut b, LSR, dl, nl, 12); //               dl = lo >> n
754                w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB  r12, r12, #32
755                shift_reg(&mut b, LSL, 12, nh, 12); //               r12 = hi << (32-n)
756                w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | 12); // ORR dl, dl, r12
757                w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND  r12, ml, #63
758                shift_reg(&mut b, ASR, dh, nh, 12); //               dh = hi >> n (arith)
759                w(&mut b, 0xEA00_0001); //                            B    .done
760                shift_reg(&mut b, ASR, dl, nh, 12); //               .large: dl = hi >> (n-32)
761                w(&mut b, 0xE1A0_0040 | (dh << 12) | (31 << 7) | nh); // ASR dh, nh, #31
762            }
763
764            // I64Rotl / I64Rotr: the #610 fixed-ABI wrapper (A32 form) around
765            // the same fixed-register core as the Thumb-2 arms — value in
766            // R0:R1, amount in R2, scratch R3 + R12.
767            ArmOp::I64Rotl {
768                rdlo,
769                rdhi,
770                rnlo,
771                rnhi,
772                shift,
773            } => {
774                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
775                for word in [
776                    0xE202_203Fu32, // AND  R2, R2, #63   (mask amount mod 64)
777                    0xE252_3020,    // SUBS R3, R2, #32   (R3 = n-32, sets N)
778                    0x5A00_0007,    // BPL  .large        (n >= 32)
779                    // --- small rotation (n < 32) ---
780                    0xE262_3020, // RSB  R3, R2, #32   (R3 = 32-n)
781                    0xE1A0_C330, // LSR  R12, R0, R3   (lo >> (32-n))
782                    0xE1A0_3331, // LSR  R3, R1, R3    (hi >> (32-n))
783                    0xE1A0_1211, // LSL  R1, R1, R2    (hi << n)
784                    0xE181_100C, // ORR  R1, R1, R12   (new_hi)
785                    0xE1A0_0210, // LSL  R0, R0, R2    (lo << n)
786                    0xE180_0003, // ORR  R0, R0, R3    (new_lo)
787                    0xEA00_0007, // B    .done
788                    // --- large rotation (n >= 32), R3 = m = n-32 ---
789                    0xE263_2020, // RSB  R2, R3, #32   (R2 = 32-m = 64-n)
790                    0xE1A0_C231, // LSR  R12, R1, R2   (hi >> (64-n))
791                    0xE1A0_2230, // LSR  R2, R0, R2    (lo >> (64-n))
792                    0xE1A0_0310, // LSL  R0, R0, R3    (lo << m)
793                    0xE1A0_1311, // LSL  R1, R1, R3    (hi << m)
794                    0xE180_C00C, // ORR  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
795                    0xE181_0002, // ORR  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
796                    0xE1A0_100C, // MOV  R1, R12       (new_hi into place)
797                ] {
798                    w(&mut b, word);
799                }
800                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
801            }
802            ArmOp::I64Rotr {
803                rdlo,
804                rdhi,
805                rnlo,
806                rnhi,
807                shift,
808            } => {
809                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
810                for word in [
811                    0xE202_203Fu32, // AND  R2, R2, #63   (mask amount mod 64)
812                    0xE252_3020,    // SUBS R3, R2, #32   (R3 = n-32, sets N)
813                    0x5A00_0007,    // BPL  .large        (n >= 32)
814                    // --- small rotation (n < 32) ---
815                    0xE262_3020, // RSB  R3, R2, #32   (R3 = 32-n)
816                    0xE1A0_C311, // LSL  R12, R1, R3   (hi << (32-n))
817                    0xE1A0_3310, // LSL  R3, R0, R3    (lo << (32-n))
818                    0xE1A0_0230, // LSR  R0, R0, R2    (lo >> n)
819                    0xE180_000C, // ORR  R0, R0, R12   (new_lo)
820                    0xE1A0_1231, // LSR  R1, R1, R2    (hi >> n)
821                    0xE181_1003, // ORR  R1, R1, R3    (new_hi)
822                    0xEA00_0007, // B    .done
823                    // --- large rotation (n >= 32), R3 = m = n-32 ---
824                    0xE263_2020, // RSB  R2, R3, #32   (R2 = 32-m = 64-n)
825                    0xE1A0_C210, // LSL  R12, R0, R2   (lo << (64-n))
826                    0xE1A0_2211, // LSL  R2, R1, R2    (hi << (64-n))
827                    0xE1A0_1331, // LSR  R1, R1, R3    (hi >> m)
828                    0xE181_C00C, // ORR  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
829                    0xE1A0_1330, // LSR  R1, R0, R3    (lo >> m)
830                    0xE181_1002, // ORR  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
831                    0xE1A0_000C, // MOV  R0, R12       (new_lo into place)
832                ] {
833                    w(&mut b, word);
834                }
835                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
836            }
837
838            // I64Clz: CLZ(hi), or 32 + CLZ(lo) when hi == 0. Conditional
839            // execution replaces the Thumb branches; like the Thumb arm, the
840            // high word of the result pair (rnhi) is cleared last.
841            ArmOp::I64Clz { rd, rnlo, rnhi } => {
842                let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
843                w(&mut b, 0xE350_0000 | (hi << 16)); //              CMP   rnhi, #0
844                w(&mut b, 0x116F_0F10 | (rd_b << 12) | hi); //       CLZNE rd, rnhi
845                w(&mut b, 0x016F_0F10 | (rd_b << 12) | lo); //       CLZEQ rd, rnlo
846                w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
847                // #1048: the former trailing `MOV rnhi, #0` is GONE — it
848                // wrote the OPERAND's home high register (see the Thumb-2
849                // I64Clz comment). Callers that relied on the implicit clear
850                // emit their own explicit hi-zero op.
851            }
852
853            // I64Ctz: CLZ(RBIT(lo)), or 32 + CLZ(RBIT(hi)) when lo == 0.
854            // RBIT/CLZ leave the flags intact, so the CMP's Z survives to the
855            // conditional ADD.
856            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
857                let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
858                w(&mut b, 0xE350_0000 | (lo << 16)); //              CMP    rnlo, #0
859                w(&mut b, 0x16FF_0F30 | (rd_b << 12) | lo); //       RBITNE rd, rnlo
860                w(&mut b, 0x06FF_0F30 | (rd_b << 12) | hi); //       RBITEQ rd, rnhi
861                w(&mut b, 0xE16F_0F10 | (rd_b << 12) | rd_b); //     CLZ    rd, rd
862                w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
863                // #1048: no trailing `MOV rnhi, #0` — see I64Clz above.
864            }
865
866            // I64Const: MOVW/MOVT per half (MOVT elided when the half fits in
867            // 16 bits, mirroring the Thumb-2 arm).
868            ArmOp::I64Const { rdlo, rdhi, value } => {
869                let lo32 = *value as u32;
870                let hi32 = (*value >> 32) as u32;
871                movw(&mut b, reg_to_bits(rdlo), lo32 & 0xFFFF);
872                if lo32 > 0xFFFF {
873                    movt(&mut b, reg_to_bits(rdlo), lo32 >> 16);
874                }
875                movw(&mut b, reg_to_bits(rdhi), hi32 & 0xFFFF);
876                if hi32 > 0xFFFF {
877                    movt(&mut b, reg_to_bits(rdhi), hi32 >> 16);
878                }
879            }
880
881            // I64Ldr / I64Str: two word accesses at [base, #off] / #off+4.
882            // A register offset is materialized into IP once (the #206/#372
883            // hazard: dropping it would read the wrong address).
884            ArmOp::I64Ldr { rdlo, rdhi, addr } | ArmOp::I64Str { rdlo, rdhi, addr } => {
885                let base = if let Some(rm) = addr.offset_reg {
886                    // ADD ip, base, rm
887                    w(
888                        &mut b,
889                        0xE080_0000
890                            | (reg_to_bits(&addr.base) << 16)
891                            | (12 << 12)
892                            | reg_to_bits(&rm),
893                    );
894                    12
895                } else {
896                    reg_to_bits(&addr.base)
897                };
898                if addr.offset < 0 || addr.offset > 0xFFB {
899                    return Err(synth_core::Error::synthesis(format!(
900                        "i64 load/store offset {} out of the A32 imm12 range (0..=4091) — materialize the offset into a register",
901                        addr.offset
902                    )));
903                }
904                let off = addr.offset as u32;
905                let opc: u32 = if matches!(op, ArmOp::I64Ldr { .. }) {
906                    0xE590_0000 // LDR
907                } else {
908                    0xE580_0000 // STR
909                };
910                w(&mut b, opc | (base << 16) | (reg_to_bits(rdlo) << 12) | off);
911                w(
912                    &mut b,
913                    opc | (base << 16) | (reg_to_bits(rdhi) << 12) | (off + 4),
914                );
915            }
916
917            // I64ExtendI32S: rdlo = rn; rdhi = rdlo >> 31 (arithmetic).
918            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
919                if rdlo != rn {
920                    w(
921                        &mut b,
922                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
923                    );
924                }
925                w(
926                    &mut b,
927                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
928                );
929            }
930
931            // I64ExtendI32U: rdlo = rn; rdhi = 0.
932            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
933                if rdlo != rn {
934                    w(
935                        &mut b,
936                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
937                    );
938                }
939                w(&mut b, 0xE3A0_0000 | (reg_to_bits(rdhi) << 12));
940            }
941
942            // I64Extend8S / I64Extend16S: SXTB/SXTH then sign-fill the high word.
943            ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
944                w(
945                    &mut b,
946                    0xE6AF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
947                );
948                w(
949                    &mut b,
950                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
951                );
952            }
953            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
954                w(
955                    &mut b,
956                    0xE6BF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
957                );
958                w(
959                    &mut b,
960                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
961                );
962            }
963            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
964                if rdlo != rnlo {
965                    w(
966                        &mut b,
967                        0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
968                    );
969                }
970                w(
971                    &mut b,
972                    0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rnlo),
973                );
974            }
975
976            // I32WrapI64: take the low word. When rd == rnlo this is a genuine
977            // no-op (the one case where a NOP word is the correct encoding).
978            ArmOp::I32WrapI64 { rd, rnlo } => {
979                w(
980                    &mut b,
981                    0xE1A0_0000 | (reg_to_bits(rd) << 12) | reg_to_bits(rnlo),
982                );
983            }
984
985            // I64Add / I64Sub: the classic pair — ADDS lo + ADC hi (SUBS/SBC).
986            // The selector emits these as separate Adds/Adc ops; the fused
987            // variants are verification-constructed, but they encode for real.
988            ArmOp::I64Add {
989                rdlo,
990                rdhi,
991                rnlo,
992                rnhi,
993                rmlo,
994                rmhi,
995            } => {
996                dp_reg(
997                    &mut b,
998                    0xE090_0000, // ADDS
999                    reg_to_bits(rdlo),
1000                    reg_to_bits(rnlo),
1001                    reg_to_bits(rmlo),
1002                );
1003                dp_reg(
1004                    &mut b,
1005                    0xE0A0_0000, // ADC
1006                    reg_to_bits(rdhi),
1007                    reg_to_bits(rnhi),
1008                    reg_to_bits(rmhi),
1009                );
1010            }
1011            ArmOp::I64Sub {
1012                rdlo,
1013                rdhi,
1014                rnlo,
1015                rnhi,
1016                rmlo,
1017                rmhi,
1018            } => {
1019                dp_reg(
1020                    &mut b,
1021                    0xE050_0000, // SUBS
1022                    reg_to_bits(rdlo),
1023                    reg_to_bits(rnlo),
1024                    reg_to_bits(rmlo),
1025                );
1026                dp_reg(
1027                    &mut b,
1028                    0xE0C0_0000, // SBC
1029                    reg_to_bits(rdhi),
1030                    reg_to_bits(rnhi),
1031                    reg_to_bits(rmhi),
1032                );
1033            }
1034
1035            // I64And / I64Or / I64Xor: two independent word ops.
1036            ArmOp::I64And {
1037                rdlo,
1038                rdhi,
1039                rnlo,
1040                rnhi,
1041                rmlo,
1042                rmhi,
1043            }
1044            | ArmOp::I64Or {
1045                rdlo,
1046                rdhi,
1047                rnlo,
1048                rnhi,
1049                rmlo,
1050                rmhi,
1051            }
1052            | ArmOp::I64Xor {
1053                rdlo,
1054                rdhi,
1055                rnlo,
1056                rnhi,
1057                rmlo,
1058                rmhi,
1059            } => {
1060                let base = match op {
1061                    ArmOp::I64And { .. } => 0xE000_0000, // AND
1062                    ArmOp::I64Or { .. } => 0xE180_0000,  // ORR
1063                    _ => 0xE020_0000,                    // EOR
1064                };
1065                dp_reg(
1066                    &mut b,
1067                    base,
1068                    reg_to_bits(rdlo),
1069                    reg_to_bits(rnlo),
1070                    reg_to_bits(rmlo),
1071                );
1072                dp_reg(
1073                    &mut b,
1074                    base,
1075                    reg_to_bits(rdhi),
1076                    reg_to_bits(rnhi),
1077                    reg_to_bits(rmhi),
1078                );
1079            }
1080
1081            // I64DivU: binary long division — A32 transcription of the Thumb-2
1082            // #610/#613 arm (fixed-ABI marshal, zero-divisor trap, 64-round
1083            // shift-subtract core, quotient to R0:R1, result to rd pair).
1084            ArmOp::I64DivU {
1085                rdlo,
1086                rdhi,
1087                rnlo,
1088                rnhi,
1089                rmlo,
1090                rmhi,
1091                elide_zero_guard,
1092            } => {
1093                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1094                // #494 phase 2b: elided only under a certificate-discharged
1095                // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
1096                if !elide_zero_guard {
1097                    emit_a32_i64_divisor_zero_trap(&mut b);
1098                }
1099                w(&mut b, 0xE92D_00F0); // PUSH {R4-R7}
1100                for r in 4..8u32 {
1101                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1102                }
1103                div_loop(&mut b, 12); // counter in R12 (encoder scratch)
1104                w(&mut b, 0xE1A0_0004); // MOV R0, R4 (quotient lo)
1105                w(&mut b, 0xE1A0_1005); // MOV R1, R5 (quotient hi)
1106                w(&mut b, 0xE8BD_00F0); // POP {R4-R7}
1107                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1108            }
1109
1110            // I64DivS: sign-extract, unsigned core, conditional negate —
1111            // A32 transcription of the Thumb-2 arm.
1112            ArmOp::I64DivS {
1113                rdlo,
1114                rdhi,
1115                rnlo,
1116                rnhi,
1117                rmlo,
1118                rmhi,
1119                elide_zero_guard,
1120                elide_overflow_guard,
1121            } => {
1122                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1123                // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
1124                // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
1125                // the #633 overflow guard falls ONLY to
1126                // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
1127                // divisor-nonzero fact alone must keep it.
1128                if !elide_zero_guard {
1129                    emit_a32_i64_divisor_zero_trap(&mut b);
1130                }
1131                if !elide_overflow_guard {
1132                    // #633: INT64_MIN / -1 overflows — trap like the i32 path
1133                    // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
1134                    emit_a32_i64_divs_overflow_trap(&mut b);
1135                }
1136                w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1137                w(&mut b, 0xE021_9003); // EOR R9, R1, R3 (result sign in MSB)
1138                skip_negate_if_positive(&mut b, 1);
1139                negate64(&mut b, 0, 1);
1140                skip_negate_if_positive(&mut b, 3);
1141                negate64(&mut b, 2, 3);
1142                for r in 4..8u32 {
1143                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1144                }
1145                div_loop(&mut b, 8); // counter in R8 (saved above)
1146                w(&mut b, 0xE1A0_0004); // MOV R0, R4
1147                w(&mut b, 0xE1A0_1005); // MOV R1, R5
1148                skip_negate_if_positive(&mut b, 9);
1149                negate64(&mut b, 0, 1);
1150                w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1151                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1152            }
1153
1154            // I64RemU: same core as I64DivU, returns the remainder (R6:R7).
1155            ArmOp::I64RemU {
1156                rdlo,
1157                rdhi,
1158                rnlo,
1159                rnhi,
1160                rmlo,
1161                rmhi,
1162                elide_zero_guard,
1163            } => {
1164                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1165                if !elide_zero_guard {
1166                    emit_a32_i64_divisor_zero_trap(&mut b);
1167                }
1168                w(&mut b, 0xE92D_01F0); // PUSH {R4-R8}
1169                for r in 4..8u32 {
1170                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1171                }
1172                div_loop(&mut b, 8);
1173                w(&mut b, 0xE1A0_0006); // MOV R0, R6 (remainder lo)
1174                w(&mut b, 0xE1A0_1007); // MOV R1, R7 (remainder hi)
1175                w(&mut b, 0xE8BD_01F0); // POP {R4-R8}
1176                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1177            }
1178
1179            // I64RemS: remainder takes the DIVIDEND's sign (WASM semantics).
1180            ArmOp::I64RemS {
1181                rdlo,
1182                rdhi,
1183                rnlo,
1184                rnhi,
1185                rmlo,
1186                rmhi,
1187                elide_zero_guard,
1188            } => {
1189                emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1190                if !elide_zero_guard {
1191                    emit_a32_i64_divisor_zero_trap(&mut b);
1192                }
1193                w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1194                w(&mut b, 0xE1A0_9001); // MOV R9, R1 (dividend sign)
1195                skip_negate_if_positive(&mut b, 1);
1196                negate64(&mut b, 0, 1);
1197                skip_negate_if_positive(&mut b, 3);
1198                negate64(&mut b, 2, 3);
1199                for r in 4..8u32 {
1200                    w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1201                }
1202                div_loop(&mut b, 8);
1203                w(&mut b, 0xE1A0_0006); // MOV R0, R6
1204                w(&mut b, 0xE1A0_1007); // MOV R1, R7
1205                skip_negate_if_positive(&mut b, 9);
1206                negate64(&mut b, 0, 1);
1207                w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1208                emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1209            }
1210
1211            // Popcnt (i32): bit-twiddle expansion (no native A32 popcount),
1212            // mirroring the Thumb-2 arm's #1021 register contract: R12 is the
1213            // ONLY scratch. The previous transcription copied the old Thumb
1214            // contract's R11 borrow — but R11 is the linear-memory base on
1215            // this path too, so it inherited the same live miscompile. A32
1216            // has no ThumbExpandImm for 0xXYXYXYXY masks, so instead the
1217            // barrel shifter folds each shift into the mask AND itself
1218            // (`AND R12, R12, rd, LSR #n`), and step 2 recovers `x & C` from
1219            // one term via `x - (((x >> 2) & C) << 2) = x & C` — the second
1220            // temp disappears algebraically. Straight-line, no PUSH/POP,
1221            // nothing to skip on a trap edge.
1222            ArmOp::Popcnt { rd, rm } => {
1223                let rd_b = reg_to_bits(rd);
1224                // Defensive (#1021), same contract as the Thumb-2 arm.
1225                if rd_b >= 11 {
1226                    return Err(synth_core::Error::synthesis(
1227                        "Popcnt destination must be R0-R10: R11 is the linear-memory \
1228                         base and R12 is the expansion's scratch (#1021)",
1229                    ));
1230                }
1231                if rd != rm {
1232                    w(&mut b, 0xE1A0_0000 | (rd_b << 12) | reg_to_bits(rm)); // MOV rd, rm
1233                }
1234                // x = x - ((x >> 1) & 0x55555555)
1235                movw(&mut b, 12, 0x5555);
1236                movt(&mut b, 12, 0x5555);
1237                dp_reg_shift(&mut b, 0xE000_0000, 12, 12, rd_b, LSR, 1); // AND R12, R12, rd, LSR #1
1238                dp_reg(&mut b, 0xE040_0000, rd_b, rd_b, 12); //             SUB rd, rd, R12
1239                // x = (x & 0x33333333) + ((x >> 2) & 0x33333333), one temp:
1240                //   R12 = (x >> 2) & C; x - (R12 << 2) = x & C; then + R12.
1241                movw(&mut b, 12, 0x3333);
1242                movt(&mut b, 12, 0x3333);
1243                dp_reg_shift(&mut b, 0xE000_0000, 12, 12, rd_b, LSR, 2); // AND R12, R12, rd, LSR #2
1244                dp_reg_shift(&mut b, 0xE040_0000, rd_b, rd_b, 12, LSL, 2); // SUB rd, rd, R12, LSL #2
1245                dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 12); //              ADD rd, rd, R12
1246                // x = (x + (x >> 4)) & 0x0F0F0F0F
1247                dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 4); // ADD rd, rd, rd, LSR #4
1248                movw(&mut b, 12, 0x0F0F);
1249                movt(&mut b, 12, 0x0F0F);
1250                dp_reg(&mut b, 0xE000_0000, rd_b, rd_b, 12); // AND rd, rd, R12
1251                // x += x >> 8; x += x >> 16; x &= 0x3F
1252                dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 8);
1253                dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 16);
1254                w(&mut b, 0xE200_003F | (rd_b << 16) | (rd_b << 12)); // AND rd, rd, #63
1255            }
1256
1257            // I64Popcnt: POPCNT(lo) + POPCNT(hi) — A32 transcription of the
1258            // Thumb-2 arm (R3/R4/R5 saved, mul-based per-word fold, high
1259            // result word rnhi cleared last, mirroring the Thumb contract).
1260            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
1261                let hi = reg_to_bits(rnhi);
1262                w(&mut b, 0xE92D_0038); // PUSH {R3, R4, R5}
1263                // #632 audit: route rnlo through R12 so a pair living at
1264                // (R3,R4) cannot read a clobbered R4 (sources read before any
1265                // scratch register they could occupy is written).
1266                w(&mut b, 0xE1A0_C000 | reg_to_bits(rnlo)); // MOV R12, rnlo
1267                w(&mut b, 0xE1A0_5000 | hi); //                MOV R5, rnhi
1268                w(&mut b, 0xE1A0_400C); //                     MOV R4, R12
1269                popcnt_word(&mut b, 4, 3);
1270                popcnt_word(&mut b, 5, 3);
1271                // #632: carry the count across the scratch restore in R12 —
1272                // rd is allocator-assigned and can land inside {R3,R4,R5};
1273                // the old `ADD rd, R4, R5` before the POP was destroyed by
1274                // the restore. R12 is never allocatable and never restored.
1275                dp_reg(&mut b, 0xE080_0000, 12, 4, 5); // ADD R12, R4, R5
1276                w(&mut b, 0xE8BD_0038); // POP {R3, R4, R5}
1277                w(&mut b, 0xE1A0_0000 | (reg_to_bits(rd) << 12) | 12); // MOV rd, R12
1278                // #1048: no trailing `MOV rnhi, #0` — the hi-word clear wrote
1279                // the OPERAND's home high register; callers emit it explicitly.
1280            }
1281
1282            _ => return Ok(None),
1283        }
1284        Ok(Some(b))
1285    }
1286
1287    fn encode_arm(&self, op: &ArmOp) -> Result<Vec<u8>> {
1288        // #615: A32 multi-instruction expansions (i64 arithmetic/shift/rotate/
1289        // compare, SetCond/SelectMove, popcnt, ...). These ops were literal
1290        // NOPs on the A32 path — user-reachable via `--target cortex-r5` —
1291        // so the value silently vanished. Mirror of the #594 CallIndirect
1292        // early-return: if the expansion helper covers the op, its bytes are
1293        // the encoding.
1294        if let Some(bytes) = self.encode_arm_expanded(op)? {
1295            return Ok(bytes);
1296        }
1297        // #206: ARM32 register-offset loads/stores. `encode_mem_addr` only
1298        // returns the 12-bit immediate, so the immediate-form arms below
1299        // silently DROP `addr.offset_reg` — a runtime address index vanished,
1300        // turning `ldr rd,[rn,rm,#off]` into `ldr rd,[rn,#off]` (the access went
1301        // to the wrong address). Compute the effective base into IP and re-encode
1302        // against `[ip, #off]`, which is uniform for word/byte/halfword/signed.
1303        if let Some(bytes) = self.encode_arm_reg_offset_mem(op)? {
1304            return Ok(bytes);
1305        }
1306        // #594: call_indirect was encoded as a literal NOP on the A32 path
1307        // (`--target cortex-r5`) — the call never happened and the function
1308        // silently returned garbage. Emit the same three-instruction expansion
1309        // as the Thumb-2 path (R11 = function-pointer table base, R12 scratch):
1310        //   MOV r12, idx, LSL #2 ; LDR r12, [r11, r12] ; BLX r12
1311        if let ArmOp::CallIndirect {
1312            table_index_reg,
1313            table_size,
1314            table_byte_offset,
1315            null_check,
1316            type_check,
1317            ..
1318        } = op
1319        {
1320            return Ok(Self::encode_arm_call_indirect(
1321                table_index_reg,
1322                *table_size,
1323                *table_byte_offset,
1324                *null_check,
1325                *type_check,
1326            ));
1327        }
1328        let instr: u32 = match op {
1329            // Data processing instructions
1330            ArmOp::Add { rd, rn, op2 } => {
1331                let rd_bits = reg_to_bits(rd);
1332                let rn_bits = reg_to_bits(rn);
1333                let (op2_bits, i_flag) = encode_operand2(op2)?;
1334
1335                // ADD encoding: cond(4) | 00 | I(1) | 0100 | S(1) | Rn(4) | Rd(4) | operand2(12)
1336                0xE0800000 // condition=always(E), opcode=ADD(0100), S=0
1337                    | (i_flag << 25)
1338                    | (rn_bits << 16)
1339                    | (rd_bits << 12)
1340                    | op2_bits
1341            }
1342
1343            ArmOp::Sub { rd, rn, op2 } => {
1344                let rd_bits = reg_to_bits(rd);
1345                let rn_bits = reg_to_bits(rn);
1346                let (op2_bits, i_flag) = encode_operand2(op2)?;
1347
1348                // SUB encoding: opcode=0010
1349                0xE0400000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1350            }
1351
1352            // i64 support: ADDS, ADC, SUBS, SBC for ARM32
1353            ArmOp::Adds { rd, rn, op2 } => {
1354                let rd_bits = reg_to_bits(rd);
1355                let rn_bits = reg_to_bits(rn);
1356                let (op2_bits, i_flag) = encode_operand2(op2)?;
1357
1358                // ADDS encoding: opcode=0100, S=1
1359                0xE0900000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1360            }
1361
1362            ArmOp::Adc { rd, rn, op2 } => {
1363                let rd_bits = reg_to_bits(rd);
1364                let rn_bits = reg_to_bits(rn);
1365                let (op2_bits, i_flag) = encode_operand2(op2)?;
1366
1367                // ADC encoding: opcode=0101
1368                0xE0A00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1369            }
1370
1371            ArmOp::Subs { rd, rn, op2 } => {
1372                let rd_bits = reg_to_bits(rd);
1373                let rn_bits = reg_to_bits(rn);
1374                let (op2_bits, i_flag) = encode_operand2(op2)?;
1375
1376                // SUBS encoding: opcode=0010, S=1
1377                0xE0500000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1378            }
1379
1380            ArmOp::Sbc { rd, rn, op2 } => {
1381                let rd_bits = reg_to_bits(rd);
1382                let rn_bits = reg_to_bits(rn);
1383                let (op2_bits, i_flag) = encode_operand2(op2)?;
1384
1385                // SBC encoding: opcode=0110
1386                0xE0C00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1387            }
1388
1389            ArmOp::Mul { rd, rn, rm } => {
1390                let rd_bits = reg_to_bits(rd);
1391                let rn_bits = reg_to_bits(rn);
1392                let rm_bits = reg_to_bits(rm);
1393
1394                // MUL encoding: cond(4) | 000000 | A(1) | S(1) | Rd(4) | Rn(4) | Rs(4) | 1001 | Rm(4)
1395                0xE0000090 | (rd_bits << 16) | (rn_bits << 8) | rm_bits
1396            }
1397
1398            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
1399                let rdlo_bits = reg_to_bits(rdlo);
1400                let rdhi_bits = reg_to_bits(rdhi);
1401                let rn_bits = reg_to_bits(rn);
1402                let rm_bits = reg_to_bits(rm);
1403
1404                // UMULL encoding: cond(4) | 0000 1000 | RdHi(4) | RdLo(4) | Rm(4) | 1001 | Rn(4)
1405                0xE0800090 | (rdhi_bits << 16) | (rdlo_bits << 12) | (rm_bits << 8) | rn_bits
1406            }
1407
1408            ArmOp::Sdiv { rd, rn, rm } => {
1409                let rd_bits = reg_to_bits(rd);
1410                let rn_bits = reg_to_bits(rn);
1411                let rm_bits = reg_to_bits(rm);
1412
1413                // SDIV encoding: cond(4) | 01110001 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1414                // ARMv7-M and above
1415                0xE710F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1416            }
1417
1418            ArmOp::Udiv { rd, rn, rm } => {
1419                let rd_bits = reg_to_bits(rd);
1420                let rn_bits = reg_to_bits(rn);
1421                let rm_bits = reg_to_bits(rm);
1422
1423                // UDIV encoding: cond(4) | 01110011 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1424                // ARMv7-M and above
1425                0xE730F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1426            }
1427
1428            ArmOp::Mls { rd, rn, rm, ra } => {
1429                let rd_bits = reg_to_bits(rd);
1430                let rn_bits = reg_to_bits(rn);
1431                let rm_bits = reg_to_bits(rm);
1432                let ra_bits = reg_to_bits(ra);
1433
1434                // MLS encoding: cond(4) | 00000110 | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1435                // Rd = Ra - (Rn * Rm)
1436                0xE0600090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1437            }
1438
1439            ArmOp::Mla { rd, rn, rm, ra } => {
1440                let rd_bits = reg_to_bits(rd);
1441                let rn_bits = reg_to_bits(rn);
1442                let rm_bits = reg_to_bits(rm);
1443                let ra_bits = reg_to_bits(ra);
1444
1445                // MLA encoding: cond(4) | 0000001 S | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1446                // Rd = Ra + (Rn * Rm). Base 0xE0200090 (S=0).
1447                0xE0200090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1448            }
1449
1450            ArmOp::And { rd, rn, op2 } => {
1451                let rd_bits = reg_to_bits(rd);
1452                let rn_bits = reg_to_bits(rn);
1453                let (op2_bits, i_flag) = encode_operand2(op2)?;
1454
1455                // AND encoding: opcode=0000
1456                0xE0000000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1457            }
1458
1459            ArmOp::Orr { rd, rn, op2 } => {
1460                let rd_bits = reg_to_bits(rd);
1461                let rn_bits = reg_to_bits(rn);
1462                let (op2_bits, i_flag) = encode_operand2(op2)?;
1463
1464                // ORR encoding: opcode=1100
1465                0xE1800000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1466            }
1467
1468            ArmOp::Eor { rd, rn, op2 } => {
1469                let rd_bits = reg_to_bits(rd);
1470                let rn_bits = reg_to_bits(rn);
1471                let (op2_bits, i_flag) = encode_operand2(op2)?;
1472
1473                // EOR encoding: opcode=0001
1474                0xE0200000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1475            }
1476
1477            // Shift instructions
1478            ArmOp::Lsl { rd, rn, shift } => {
1479                let rd_bits = reg_to_bits(rd);
1480                let rn_bits = reg_to_bits(rn);
1481                let shift_bits = *shift & 0x1F;
1482
1483                // LSL encoding: MOV with shift
1484                0xE1A00000 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1485            }
1486
1487            ArmOp::Lsr { rd, rn, shift } => {
1488                let rd_bits = reg_to_bits(rd);
1489                let rn_bits = reg_to_bits(rn);
1490                let shift_bits = *shift & 0x1F;
1491
1492                // LSR encoding
1493                0xE1A00020 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1494            }
1495
1496            ArmOp::Asr { rd, rn, shift } => {
1497                let rd_bits = reg_to_bits(rd);
1498                let rn_bits = reg_to_bits(rn);
1499                let shift_bits = *shift & 0x1F;
1500
1501                // ASR encoding
1502                0xE1A00040 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1503            }
1504
1505            ArmOp::Ror { rd, rn, shift } => {
1506                let rd_bits = reg_to_bits(rd);
1507                let rn_bits = reg_to_bits(rn);
1508                let shift_bits = *shift & 0x1F;
1509
1510                // ROR encoding: MOV with ROR shift
1511                0xE1A00060 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1512            }
1513
1514            // Register-based shifts (ARM32)
1515            // LSL Rd, Rn, Rm: cond 0001101S 0000 Rd Rs 0001 Rn
1516            ArmOp::LslReg { rd, rn, rm } => {
1517                let rd_bits = reg_to_bits(rd);
1518                let rn_bits = reg_to_bits(rn);
1519                let rm_bits = reg_to_bits(rm);
1520                0xE1A00010 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1521            }
1522            ArmOp::LsrReg { rd, rn, rm } => {
1523                let rd_bits = reg_to_bits(rd);
1524                let rn_bits = reg_to_bits(rn);
1525                let rm_bits = reg_to_bits(rm);
1526                0xE1A00030 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1527            }
1528            ArmOp::AsrReg { rd, rn, rm } => {
1529                let rd_bits = reg_to_bits(rd);
1530                let rn_bits = reg_to_bits(rn);
1531                let rm_bits = reg_to_bits(rm);
1532                0xE1A00050 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1533            }
1534            ArmOp::RorReg { rd, rn, rm } => {
1535                let rd_bits = reg_to_bits(rd);
1536                let rn_bits = reg_to_bits(rn);
1537                let rm_bits = reg_to_bits(rm);
1538                0xE1A00070 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1539            }
1540
1541            // RSB (Reverse Subtract): Rd = imm - Rn
1542            ArmOp::Rsb { rd, rn, imm } => {
1543                let rd_bits = reg_to_bits(rd);
1544                let rn_bits = reg_to_bits(rn);
1545                // RSB encoding: cond(4) | 00 1 0011 S | Rn(4) | Rd(4) | imm12
1546                // Opcode for RSB = 0011, I=1 (immediate), S=0
1547                //
1548                // #681 class audit: the A32 imm12 is a rotate(4):imm8 modified
1549                // immediate; `*imm & 0xFF` silently encoded a WRONG constant
1550                // for imm > 0xFF (#378 masking class). All current emitters use
1551                // imm 32, so erroring here is byte-identical for real codegen.
1552                if *imm > 0xFF {
1553                    return Err(synth_core::Error::synthesis(
1554                        "A32 RSB immediate > 0xFF requires a rotated-immediate encoding \
1555                         (not supported) — materialize into a register",
1556                    ));
1557                }
1558                0xE2600000 | (rn_bits << 16) | (rd_bits << 12) | (*imm & 0xFF)
1559            }
1560
1561            // Bit manipulation instructions
1562            ArmOp::Clz { rd, rm } => {
1563                let rd_bits = reg_to_bits(rd);
1564                let rm_bits = reg_to_bits(rm);
1565
1566                // CLZ encoding: cond(4) | 00010110 | 1111 | Rd(4) | 1111 | 0001 | Rm(4)
1567                // ARMv5T and above
1568                0xE16F0F10 | (rd_bits << 12) | rm_bits
1569            }
1570
1571            ArmOp::Rbit { rd, rm } => {
1572                let rd_bits = reg_to_bits(rd);
1573                let rm_bits = reg_to_bits(rm);
1574
1575                // RBIT encoding: cond(4) | 01101111 | 1111 | Rd(4) | 1111 | 0011 | Rm(4)
1576                // ARMv6T2 and above
1577                0xE6FF0F30 | (rd_bits << 12) | rm_bits
1578            }
1579
1580            ArmOp::Sxtb { rd, rm } => {
1581                let rd_bits = reg_to_bits(rd);
1582                let rm_bits = reg_to_bits(rm);
1583
1584                // SXTB encoding: cond(4) | 01101010 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1585                // ARMv6 and above. rotate=00 for no rotation
1586                0xE6AF0070 | (rd_bits << 12) | rm_bits
1587            }
1588
1589            ArmOp::Sxth { rd, rm } => {
1590                let rd_bits = reg_to_bits(rd);
1591                let rm_bits = reg_to_bits(rm);
1592
1593                // SXTH encoding: cond(4) | 01101011 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1594                // ARMv6 and above. rotate=00 for no rotation
1595                0xE6BF0070 | (rd_bits << 12) | rm_bits
1596            }
1597
1598            ArmOp::Uxtb { rd, rm } => {
1599                let rd_bits = reg_to_bits(rd);
1600                let rm_bits = reg_to_bits(rm);
1601                // UXTB encoding: cond | 01101110 1111 Rd rotate 00 0111 Rm (rotate=00)
1602                0xE6EF0070 | (rd_bits << 12) | rm_bits
1603            }
1604
1605            ArmOp::Uxth { rd, rm } => {
1606                let rd_bits = reg_to_bits(rd);
1607                let rm_bits = reg_to_bits(rm);
1608                // UXTH encoding: cond | 01101111 1111 Rd rotate 00 0111 Rm (rotate=00)
1609                0xE6FF0070 | (rd_bits << 12) | rm_bits
1610            }
1611
1612            // Move instructions
1613            ArmOp::Mov { rd, op2 } => {
1614                let rd_bits = reg_to_bits(rd);
1615                let (op2_bits, i_flag) = encode_operand2(op2)?;
1616
1617                // MOV encoding: opcode=1101
1618                0xE1A00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1619            }
1620
1621            ArmOp::Mvn { rd, op2 } => {
1622                let rd_bits = reg_to_bits(rd);
1623                let (op2_bits, i_flag) = encode_operand2(op2)?;
1624
1625                // MVN encoding: opcode=1111
1626                0xE1E00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1627            }
1628
1629            // MOVW - Move Wide (ARM32)
1630            // Encoding: cond(4) | 0011 0000 | imm4(4) | Rd(4) | imm12(12)
1631            ArmOp::Movw { rd, imm16 } => {
1632                let rd_bits = reg_to_bits(rd);
1633                let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1634                let imm12 = (*imm16 as u32) & 0xFFF;
1635                0xE3000000 | (imm4 << 16) | (rd_bits << 12) | imm12
1636            }
1637
1638            // MOVT - Move Top (ARM32)
1639            // Encoding: cond(4) | 0011 0100 | imm4(4) | Rd(4) | imm12(12)
1640            ArmOp::Movt { rd, imm16 } => {
1641                let rd_bits = reg_to_bits(rd);
1642                let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1643                let imm12 = (*imm16 as u32) & 0xFFF;
1644                0xE3400000 | (imm4 << 16) | (rd_bits << 12) | imm12
1645            }
1646
1647            // #237: symbol-relative MOVW/MOVT (ARM mode) — addend in place, the
1648            // backend records the MOVW_ABS/MOVT_ABS relocation against `symbol`.
1649            ArmOp::MovwSym { rd, addend, .. } => {
1650                let rd_bits = reg_to_bits(rd);
1651                let v = (*addend as u32) & 0xffff;
1652                0xE3000000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1653            }
1654            ArmOp::MovtSym { rd, addend, .. } => {
1655                let rd_bits = reg_to_bits(rd);
1656                let v = ((*addend as u32) >> 16) & 0xffff;
1657                0xE3400000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1658            }
1659
1660            // #345: LdrSym is the Thumb-2 literal-pool address load. A32 mode is
1661            // not used for relocatable native-pointer objects; fail loudly rather
1662            // than miscompile if it is ever reached here.
1663            ArmOp::LdrSym { .. } => {
1664                return Err(synth_core::Error::synthesis(
1665                    "LdrSym (literal-pool address load) is Thumb-2-only",
1666                ));
1667            }
1668
1669            // Compare
1670            ArmOp::Cmp { rn, op2 } => {
1671                let rn_bits = reg_to_bits(rn);
1672                let (op2_bits, i_flag) = encode_operand2(op2)?;
1673
1674                // CMP encoding: opcode=1010, S=1
1675                0xE1500000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1676            }
1677
1678            // Compare Negative (CMN) - computes Rn + op2 and sets flags
1679            ArmOp::Cmn { rn, op2 } => {
1680                let rn_bits = reg_to_bits(rn);
1681                let (op2_bits, i_flag) = encode_operand2(op2)?;
1682
1683                // CMN encoding: opcode=1011, S=1
1684                0xE1700000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1685            }
1686
1687            // Load/Store
1688            ArmOp::Ldr { rd, addr } => {
1689                let rd_bits = reg_to_bits(rd);
1690                let (base_bits, offset_bits) = encode_mem_addr(addr);
1691
1692                // LDR encoding: cond(4) | 01 | I(1) | P(1) | U(1) | B(1) | W(1) | L(1) | Rn(4) | Rd(4) | offset(12)
1693                // P=1 (pre-indexed), U=1 (add offset), L=1 (load)
1694                0xE5900000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1695            }
1696
1697            ArmOp::Str { rd, addr } => {
1698                let rd_bits = reg_to_bits(rd);
1699                let (base_bits, offset_bits) = encode_mem_addr(addr);
1700
1701                // STR encoding: L=0 (store)
1702                0xE5800000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1703            }
1704
1705            // Sub-word loads (ARM32 encoding)
1706            ArmOp::Ldrb { rd, addr } => {
1707                let rd_bits = reg_to_bits(rd);
1708                let (base_bits, offset_bits) = encode_mem_addr(addr);
1709                // LDRB: LDR with B=1 (byte): cond|01|I|P|U|1|W|L|Rn|Rd|offset
1710                0xE5D00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1711            }
1712
1713            ArmOp::Ldrsb { rd, addr } => {
1714                let rd_bits = reg_to_bits(rd);
1715                let (base_bits, offset_bits) = encode_mem_addr(addr);
1716                // LDRSB (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1101|imm4L
1717                // Simplified with immediate offset
1718                let offset_val = offset_bits & 0xFF;
1719                let imm4h = (offset_val >> 4) & 0xF;
1720                let imm4l = offset_val & 0xF;
1721                0xE1D000D0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1722            }
1723
1724            ArmOp::Ldrh { rd, addr } => {
1725                let rd_bits = reg_to_bits(rd);
1726                let (base_bits, offset_bits) = encode_mem_addr(addr);
1727                // LDRH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1011|imm4L
1728                let offset_val = offset_bits & 0xFF;
1729                let imm4h = (offset_val >> 4) & 0xF;
1730                let imm4l = offset_val & 0xF;
1731                0xE1D000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1732            }
1733
1734            ArmOp::Ldrsh { rd, addr } => {
1735                let rd_bits = reg_to_bits(rd);
1736                let (base_bits, offset_bits) = encode_mem_addr(addr);
1737                // LDRSH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1111|imm4L
1738                let offset_val = offset_bits & 0xFF;
1739                let imm4h = (offset_val >> 4) & 0xF;
1740                let imm4l = offset_val & 0xF;
1741                0xE1D000F0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1742            }
1743
1744            // Sub-word stores (ARM32 encoding)
1745            ArmOp::Strb { rd, addr } => {
1746                let rd_bits = reg_to_bits(rd);
1747                let (base_bits, offset_bits) = encode_mem_addr(addr);
1748                // STRB: STR with B=1 (byte): cond|01|I|P|U|1|W|0|Rn|Rd|offset
1749                0xE5C00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1750            }
1751
1752            ArmOp::Strh { rd, addr } => {
1753                let rd_bits = reg_to_bits(rd);
1754                let (base_bits, offset_bits) = encode_mem_addr(addr);
1755                // STRH (misc store): cond|000|P|U|1|W|0|Rn|Rd|imm4H|1011|imm4L
1756                let offset_val = offset_bits & 0xFF;
1757                let imm4h = (offset_val >> 4) & 0xF;
1758                let imm4l = offset_val & 0xF;
1759                0xE1C000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1760            }
1761
1762            // Memory management (ARM32 encoding)
1763            ArmOp::MemorySize { rd } => {
1764                let rd_bits = reg_to_bits(rd);
1765                // MOV rd, R10, LSR #16  (memory size in bytes / 65536 = pages)
1766                // cond|000|1101|S|0000|Rd|shift5|type|0|Rm
1767                // LSR #16: shift5=10000, type=01
1768                0xE1A00820 | (rd_bits << 12) | 0x0A // Rm=R10, shift=16, LSR
1769            }
1770
1771            ArmOp::MemoryGrow { rd, .. } => {
1772                let rd_bits = reg_to_bits(rd);
1773                // On embedded, always fail: MOV rd, #-1
1774                0xE3E00000 | (rd_bits << 12) // MVN rd, #0 = MOV rd, #-1
1775            }
1776
1777            // Label pseudo-instruction: emits no machine code
1778            ArmOp::Label { .. } => {
1779                return Ok(Vec::new());
1780            }
1781
1782            // Branch instructions
1783            ArmOp::B { label: _ } => {
1784                // B encoding: cond(4) | 1010 | offset(24)
1785                // Simplified: branch to offset 0 (will be patched by linker/resolver)
1786                0xEA000000
1787            }
1788
1789            // Conditional branch to label (generic)
1790            ArmOp::Bcc { cond, label: _ } => {
1791                use synth_synthesis::Condition;
1792                let cond_bits: u32 = match cond {
1793                    Condition::EQ => 0x0,
1794                    Condition::NE => 0x1,
1795                    Condition::HS => 0x2,
1796                    Condition::LO => 0x3,
1797                    Condition::HI => 0x8,
1798                    Condition::LS => 0x9,
1799                    Condition::GE => 0xA,
1800                    Condition::LT => 0xB,
1801                    Condition::GT => 0xC,
1802                    Condition::LE => 0xD,
1803                };
1804                // B<cond> with offset 0 (will be patched)
1805                (cond_bits << 28) | 0x0A000000
1806            }
1807
1808            // BHS (Branch if Higher or Same) - used for bounds checking
1809            ArmOp::Bhs { label: _ } => {
1810                // BHS encoding: cond(2=HS) | 1010 | offset(24)
1811                0x2A000000 // BHS with offset 0
1812            }
1813
1814            // BLO (Branch if Lower) - complementary to BHS
1815            ArmOp::Blo { label: _ } => {
1816                // BLO encoding: cond(3=LO) | 1010 | offset(24)
1817                0x3A000000 // BLO with offset 0
1818            }
1819
1820            // Branch with numeric offset (in instructions)
1821            // ARM32 B instruction: offset is in instructions, stored as words
1822            // The offset is relative to PC+8 (due to ARM pipeline)
1823            ArmOp::BOffset { offset } => {
1824                // B encoding: cond(4) | 1010 | offset(24)
1825                // Offset is signed, in words (4-byte units)
1826                // ARM adds PC+8 to the offset, so we need to adjust:
1827                // target = PC + 8 + (offset * 4)
1828                // For backward branch of N instructions: offset = -(N + 2)
1829                // wrapping_sub keeps the encoder total under fuzzing (#186): an
1830                // extreme i32::MIN offset would otherwise overflow-panic; for any
1831                // real branch offset this is identical to `- 2`.
1832                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1833                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1834                0xEA000000 | offset_bits
1835            }
1836
1837            // Conditional branch with numeric offset
1838            ArmOp::BCondOffset { cond, offset } => {
1839                use synth_synthesis::Condition;
1840                let cond_bits: u32 = match cond {
1841                    Condition::EQ => 0x0,
1842                    Condition::NE => 0x1,
1843                    Condition::HS => 0x2,
1844                    Condition::LO => 0x3,
1845                    Condition::HI => 0x8,
1846                    Condition::LS => 0x9,
1847                    Condition::GE => 0xA,
1848                    Condition::LT => 0xB,
1849                    Condition::GT => 0xC,
1850                    Condition::LE => 0xD,
1851                };
1852                // B<cond> encoding: cond(4) | 1010 | offset(24)
1853                // wrapping_sub: total under fuzzing (#186), identical for real offsets.
1854                let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1855                let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1856                (cond_bits << 28) | 0x0A000000 | offset_bits
1857            }
1858
1859            ArmOp::Bl { label: _ } => {
1860                // BL encoding: cond(4) | 1011 | offset(24)
1861                0xEB000000
1862            }
1863
1864            ArmOp::Bx { rm } => {
1865                let rm_bits = reg_to_bits(rm);
1866
1867                // BX encoding: cond(4) | 000100101111111111110001 | Rm(4)
1868                0xE12FFF10 | rm_bits
1869            }
1870
1871            ArmOp::Blx { rm } => {
1872                let rm_bits = reg_to_bits(rm);
1873
1874                // BLX (register) encoding: cond(4) | 000100101111111111110011 | Rm(4)
1875                0xE12FFF30 | rm_bits
1876            }
1877
1878            ArmOp::Push { regs } => {
1879                // STMDB SP!, {regs} encoding: cond(4) | 100100 | 10 | 1101 | register_list(16)
1880                let mut reg_list: u32 = 0;
1881                for r in regs {
1882                    reg_list |= 1 << reg_to_bits(r);
1883                }
1884                0xE92D0000 | reg_list
1885            }
1886
1887            ArmOp::Pop { regs } => {
1888                // LDMIA SP!, {regs} encoding: cond(4) | 100010 | 11 | 1101 | register_list(16)
1889                let mut reg_list: u32 = 0;
1890                for r in regs {
1891                    reg_list |= 1 << reg_to_bits(r);
1892                }
1893                0xE8BD0000 | reg_list
1894            }
1895
1896            ArmOp::Nop => {
1897                // NOP encoding: MOV R0, R0
1898                0xE1A00000
1899            }
1900
1901            ArmOp::Udf { imm } => {
1902                // UDF (Undefined) encoding in ARM: 0xE7F000F0 | (imm12_hi << 8) | imm4_lo
1903                // We only use imm8, so split into imm4_hi and imm4_lo
1904                let imm8 = *imm as u32;
1905                0xE7F000F0 | ((imm8 & 0xF0) << 4) | (imm8 & 0x0F)
1906            }
1907
1908            // #615: handled by the `encode_arm_expanded` early return at the
1909            // top of this function — a real MOV{cond}/MOV pair now, never a
1910            // silent NOP again.
1911            ArmOp::Popcnt { .. } | ArmOp::SetCond { .. } | ArmOp::SelectMove { .. } => {
1912                unreachable!("handled by encode_arm_expanded (#615)")
1913            }
1914
1915            // Verification-only pseudo-ops: `synth-verify`'s ArmSemantics
1916            // models these, but NO codegen path constructs them (the selector
1917            // lowers select/locals/globals/br_table/call to real instruction
1918            // sequences before the encoder). Encoding one as a NOP silently
1919            // dropped the operation (#615 class); a typed Err keeps the
1920            // encoder total (Ok-or-Err, the `encoder_no_panic` contract)
1921            // while making any future reachability LOUD.
1922            ArmOp::Select { .. }
1923            | ArmOp::LocalGet { .. }
1924            | ArmOp::LocalSet { .. }
1925            | ArmOp::LocalTee { .. }
1926            | ArmOp::GlobalGet { .. }
1927            | ArmOp::GlobalSet { .. }
1928            | ArmOp::BrTable { .. }
1929            | ArmOp::Call { .. } => {
1930                return Err(synth_core::Error::synthesis(format!(
1931                    "verification-only pseudo-op {op:?} reached the A32 encoder — \
1932                     codegen lowers it before encoding; refusing to emit a silent NOP (#615)"
1933                )));
1934            }
1935
1936            // #594: CallIndirect is expanded to a real multi-instruction
1937            // sequence by the early return at the top of this function —
1938            // it must NEVER fall through to a silent NOP again.
1939            ArmOp::CallIndirect { .. } => {
1940                unreachable!("CallIndirect handled by encode_arm_call_indirect (#594)")
1941            }
1942
1943            // #615: every i64 op (and I32WrapI64) is expanded to a real A32
1944            // multi-instruction sequence by `encode_arm_expanded` — the
1945            // "encode as NOP for now" era ended with the value silently
1946            // vanishing on `--target cortex-r5`.
1947            ArmOp::I64Add { .. }
1948            | ArmOp::I64Sub { .. }
1949            | ArmOp::I64DivS { .. }
1950            | ArmOp::I64DivU { .. }
1951            | ArmOp::I64RemS { .. }
1952            | ArmOp::I64RemU { .. }
1953            | ArmOp::I64Clz { .. }
1954            | ArmOp::I64Ctz { .. }
1955            | ArmOp::I64Popcnt { .. }
1956            | ArmOp::I64And { .. }
1957            | ArmOp::I64Or { .. }
1958            | ArmOp::I64Xor { .. }
1959            | ArmOp::I64Eqz { .. }
1960            | ArmOp::I64Eq { .. }
1961            | ArmOp::I64Ne { .. }
1962            | ArmOp::I64LtS { .. }
1963            | ArmOp::I64LtU { .. }
1964            | ArmOp::I64LeS { .. }
1965            | ArmOp::I64LeU { .. }
1966            | ArmOp::I64GtS { .. }
1967            | ArmOp::I64GtU { .. }
1968            | ArmOp::I64GeS { .. }
1969            | ArmOp::I64GeU { .. }
1970            | ArmOp::I64Const { .. }
1971            | ArmOp::I64Ldr { .. }
1972            | ArmOp::I64Str { .. }
1973            | ArmOp::I64ExtendI32S { .. }
1974            | ArmOp::I64ExtendI32U { .. }
1975            | ArmOp::I64Extend8S { .. }
1976            | ArmOp::I64Extend16S { .. }
1977            | ArmOp::I64Extend32S { .. }
1978            | ArmOp::I32WrapI64 { .. } => {
1979                unreachable!("handled by encode_arm_expanded (#615)")
1980            }
1981
1982            // f32 VFP single-precision instructions
1983            ArmOp::F32Add { sd, sn, sm } => encode_vfp_3reg(0xEE300A00, sd, sn, sm)?,
1984            ArmOp::F32Sub { sd, sn, sm } => encode_vfp_3reg(0xEE300A40, sd, sn, sm)?,
1985            ArmOp::F32Mul { sd, sn, sm } => encode_vfp_3reg(0xEE200A00, sd, sn, sm)?,
1986            ArmOp::F32Div { sd, sn, sm } => encode_vfp_3reg(0xEE800A00, sd, sn, sm)?,
1987            ArmOp::F32Abs { sd, sm } => encode_vfp_2reg(0xEEB00AC0, sd, sm)?,
1988            ArmOp::F32Neg { sd, sm } => encode_vfp_2reg(0xEEB10A40, sd, sm)?,
1989            ArmOp::F32Sqrt { sd, sm } => encode_vfp_2reg(0xEEB10AC0, sd, sm)?,
1990
1991            // f32 pseudo-ops — multi-instruction sequences
1992            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
1993            ArmOp::F32Ceil { sd, sm } => {
1994                return self.encode_arm_f32_rounding(sd, sm, 0b01); // Round toward +Inf
1995            }
1996            ArmOp::F32Floor { sd, sm } => {
1997                return self.encode_arm_f32_rounding(sd, sm, 0b10); // Round toward -Inf
1998            }
1999            ArmOp::F32Trunc { sd, sm } => {
2000                return self.encode_arm_f32_rounding(sd, sm, 0b11); // VCVT toward zero
2001            }
2002            ArmOp::F32Nearest { sd, sm } => {
2003                return self.encode_arm_f32_rounding(sd, sm, 0b00); // VCVT to nearest
2004            }
2005            ArmOp::F32Min { sd, sn, sm } => {
2006                return self.encode_arm_f32_minmax(sd, sn, sm, true);
2007            }
2008            ArmOp::F32Max { sd, sn, sm } => {
2009                return self.encode_arm_f32_minmax(sd, sn, sm, false);
2010            }
2011            ArmOp::F32Copysign { sd, sn, sm } => {
2012                return self.encode_arm_f32_copysign(sd, sn, sm);
2013            }
2014
2015            // f32 comparisons — multi-instruction: VCMP + VMRS + conditional MOV
2016            ArmOp::F32Eq { rd, sn, sm } => {
2017                return self.encode_arm_f32_compare(rd, sn, sm, 0x0); // EQ
2018            }
2019            ArmOp::F32Ne { rd, sn, sm } => {
2020                return self.encode_arm_f32_compare(rd, sn, sm, 0x1); // NE
2021            }
2022            ArmOp::F32Lt { rd, sn, sm } => {
2023                return self.encode_arm_f32_compare(rd, sn, sm, 0x4); // MI (less than)
2024            }
2025            ArmOp::F32Le { rd, sn, sm } => {
2026                return self.encode_arm_f32_compare(rd, sn, sm, 0x9); // LS (less or same)
2027            }
2028            ArmOp::F32Gt { rd, sn, sm } => {
2029                return self.encode_arm_f32_compare(rd, sn, sm, 0xC); // GT
2030            }
2031            ArmOp::F32Ge { rd, sn, sm } => {
2032                return self.encode_arm_f32_compare(rd, sn, sm, 0xA); // GE
2033            }
2034
2035            // f32 const — multi-instruction: MOVW + MOVT + VMOV
2036            ArmOp::F32Const { sd, value } => {
2037                return self.encode_arm_f32_const(sd, *value);
2038            }
2039
2040            ArmOp::F32Load { sd, addr } => encode_vfp_ldst(0xED900A00, sd, addr)?,
2041            ArmOp::F32Store { sd, addr } => encode_vfp_ldst(0xED800A00, sd, addr)?,
2042
2043            // f32 conversions — multi-instruction sequences
2044            ArmOp::F32ConvertI32S { sd, rm } => {
2045                return self.encode_arm_f32_convert_i32(sd, rm, true);
2046            }
2047            ArmOp::F32ConvertI32U { sd, rm } => {
2048                return self.encode_arm_f32_convert_i32(sd, rm, false);
2049            }
2050            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
2051                return Err(synth_core::Error::synthesis(
2052                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2053                ));
2054            }
2055            ArmOp::F32ReinterpretI32 { sd, rm } => encode_vmov_core_sreg(true, sd, rm)?,
2056            ArmOp::I32ReinterpretF32 { rd, sm } => encode_vmov_core_sreg(false, sm, rd)?,
2057            ArmOp::I32TruncF32S { rd, sm } => {
2058                return self.encode_arm_i32_trunc_f32(rd, sm, true);
2059            }
2060            ArmOp::I32TruncF32U { rd, sm } => {
2061                return self.encode_arm_i32_trunc_f32(rd, sm, false);
2062            }
2063
2064            // f64 VFP double-precision instructions (ARM32)
2065            // F64 arithmetic: same as F32 but with sz=1 (bit 8 = 1, cp11 = 0xB)
2066            ArmOp::F64Add { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B00, dd, dn, dm)?,
2067            ArmOp::F64Sub { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B40, dd, dn, dm)?,
2068            ArmOp::F64Mul { dd, dn, dm } => encode_vfp_3reg_f64(0xEE200B00, dd, dn, dm)?,
2069            ArmOp::F64Div { dd, dn, dm } => encode_vfp_3reg_f64(0xEE800B00, dd, dn, dm)?,
2070            ArmOp::F64Abs { dd, dm } => encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?,
2071            ArmOp::F64Neg { dd, dm } => encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?,
2072            ArmOp::F64Sqrt { dd, dm } => encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?,
2073
2074            // f64 pseudo-ops
2075            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
2076            ArmOp::F64Ceil { dd, dm } => {
2077                return self.encode_arm_f64_rounding(dd, dm, 0b01);
2078            }
2079            ArmOp::F64Floor { dd, dm } => {
2080                return self.encode_arm_f64_rounding(dd, dm, 0b10);
2081            }
2082            ArmOp::F64Trunc { dd, dm } => {
2083                return self.encode_arm_f64_rounding(dd, dm, 0b11);
2084            }
2085            ArmOp::F64Nearest { dd, dm } => {
2086                return self.encode_arm_f64_rounding(dd, dm, 0b00);
2087            }
2088            ArmOp::F64Min { dd, dn, dm } => {
2089                return self.encode_arm_f64_minmax(dd, dn, dm, true);
2090            }
2091            ArmOp::F64Max { dd, dn, dm } => {
2092                return self.encode_arm_f64_minmax(dd, dn, dm, false);
2093            }
2094            ArmOp::F64Copysign { dd, dn, dm } => {
2095                return self.encode_arm_f64_copysign(dd, dn, dm);
2096            }
2097
2098            // f64 comparisons
2099            ArmOp::F64Eq { rd, dn, dm } => {
2100                return self.encode_arm_f64_compare(rd, dn, dm, 0x0);
2101            }
2102            ArmOp::F64Ne { rd, dn, dm } => {
2103                return self.encode_arm_f64_compare(rd, dn, dm, 0x1);
2104            }
2105            ArmOp::F64Lt { rd, dn, dm } => {
2106                return self.encode_arm_f64_compare(rd, dn, dm, 0x4);
2107            }
2108            ArmOp::F64Le { rd, dn, dm } => {
2109                return self.encode_arm_f64_compare(rd, dn, dm, 0x9);
2110            }
2111            ArmOp::F64Gt { rd, dn, dm } => {
2112                return self.encode_arm_f64_compare(rd, dn, dm, 0xC);
2113            }
2114            ArmOp::F64Ge { rd, dn, dm } => {
2115                return self.encode_arm_f64_compare(rd, dn, dm, 0xA);
2116            }
2117
2118            ArmOp::F64Const { dd, value } => {
2119                return self.encode_arm_f64_const(dd, *value);
2120            }
2121
2122            ArmOp::F64Load { dd, addr } => encode_vfp_ldst_f64(0xED900B00, dd, addr)?,
2123            ArmOp::F64Store { dd, addr } => encode_vfp_ldst_f64(0xED800B00, dd, addr)?,
2124
2125            ArmOp::F64ConvertI32S { dd, rm } => {
2126                return self.encode_arm_f64_convert_i32(dd, rm, true);
2127            }
2128            ArmOp::F64ConvertI32U { dd, rm } => {
2129                return self.encode_arm_f64_convert_i32(dd, rm, false);
2130            }
2131            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
2132                return Err(synth_core::Error::synthesis(
2133                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2134                ));
2135            }
2136            ArmOp::F64PromoteF32 { dd, sm } => {
2137                return self.encode_arm_f64_promote_f32(dd, sm);
2138            }
2139            // GI-FPU-002 (#369): no synth A32 target carries an FPU (cortex-r5
2140            // has none — the selector declines every float op there), so the
2141            // A32 encoder refuses loudly instead of shipping an untested
2142            // encoding (#615: never a silent wrong byte).
2143            ArmOp::F32DemoteF64 { .. } => {
2144                return Err(synth_core::Error::synthesis(
2145                    "F32DemoteF64 has no A32 encoding (no A32 target has an FPU)",
2146                ));
2147            }
2148            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => {
2149                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?
2150            }
2151            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => {
2152                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?
2153            }
2154            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
2155                return Err(synth_core::Error::synthesis(
2156                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
2157                ));
2158            }
2159            ArmOp::I32TruncF64S { rd, dm } => {
2160                return self.encode_arm_i32_trunc_f64(rd, dm, true);
2161            }
2162            ArmOp::I32TruncF64U { rd, dm } => {
2163                return self.encode_arm_i32_trunc_f64(rd, dm, false);
2164            }
2165            // #615: multi-instruction i64 sequences — expanded to real A32 by
2166            // `encode_arm_expanded`, no longer "Thumb-2 only" NOPs.
2167            ArmOp::I64SetCond { .. }
2168            | ArmOp::I64SetCondZ { .. }
2169            | ArmOp::I64Mul { .. }
2170            | ArmOp::I64Shl { .. }
2171            | ArmOp::I64ShrS { .. }
2172            | ArmOp::I64ShrU { .. }
2173            | ArmOp::I64Rotl { .. }
2174            | ArmOp::I64Rotr { .. } => {
2175                unreachable!("handled by encode_arm_expanded (#615)")
2176            }
2177
2178            // MVE instructions — Thumb-2 only (Cortex-M55 is always Thumb-2)
2179            ArmOp::MveLoad { .. }
2180            | ArmOp::MveStore { .. }
2181            | ArmOp::MveConst { .. }
2182            | ArmOp::MveAnd { .. }
2183            | ArmOp::MveOrr { .. }
2184            | ArmOp::MveEor { .. }
2185            | ArmOp::MveMvn { .. }
2186            | ArmOp::MveBic { .. }
2187            | ArmOp::MveAddI { .. }
2188            | ArmOp::MveSubI { .. }
2189            | ArmOp::MveMulI { .. }
2190            | ArmOp::MveNegI { .. }
2191            | ArmOp::MveCmpEqI { .. }
2192            | ArmOp::MveCmpNeI { .. }
2193            | ArmOp::MveCmpLtS { .. }
2194            | ArmOp::MveCmpLtU { .. }
2195            | ArmOp::MveCmpGtS { .. }
2196            | ArmOp::MveCmpGtU { .. }
2197            | ArmOp::MveCmpLeS { .. }
2198            | ArmOp::MveCmpLeU { .. }
2199            | ArmOp::MveCmpGeS { .. }
2200            | ArmOp::MveCmpGeU { .. }
2201            | ArmOp::MveDup { .. }
2202            | ArmOp::MveExtractLane { .. }
2203            | ArmOp::MveInsertLane { .. }
2204            | ArmOp::MveAddF32 { .. }
2205            | ArmOp::MveSubF32 { .. }
2206            | ArmOp::MveMulF32 { .. }
2207            | ArmOp::MveNegF32 { .. }
2208            | ArmOp::MveAbsF32 { .. }
2209            | ArmOp::MveCmpEqF32 { .. }
2210            | ArmOp::MveCmpNeF32 { .. }
2211            | ArmOp::MveCmpLtF32 { .. }
2212            | ArmOp::MveCmpLeF32 { .. }
2213            | ArmOp::MveCmpGtF32 { .. }
2214            | ArmOp::MveCmpGeF32 { .. }
2215            | ArmOp::MveDupF32 { .. }
2216            | ArmOp::MveExtractLaneF32 { .. }
2217            | ArmOp::MveReplaceLaneF32 { .. }
2218            | ArmOp::MveDivF32 { .. }
2219            | ArmOp::MveSqrtF32 { .. } => {
2220                // MVE (Helium) is a Thumb-2-only extension (Cortex-M55); there
2221                // is no A32 encoding. The selector only emits MVE ops for
2222                // Thumb targets — a NOP here silently dropped the vector op
2223                // if that invariant ever broke (#615 class). Err keeps the
2224                // encoder total and the failure loud.
2225                return Err(synth_core::Error::synthesis(format!(
2226                    "MVE op {op:?} has no A32 (ARM-mode) encoding — MVE is Thumb-2 only (#615)"
2227                )));
2228            }
2229        };
2230
2231        // ARM32 instructions are little-endian
2232        Ok(instr.to_le_bytes().to_vec())
2233    }
2234
2235    // === ARM32 VFP multi-instruction helpers ===
2236
2237    /// Encode F32 comparison as ARM32: VCMP.F32 + VMRS + MOV rd,#0 + MOVcond rd,#1
2238    fn encode_arm_f32_compare(
2239        &self,
2240        rd: &Reg,
2241        sn: &VfpReg,
2242        sm: &VfpReg,
2243        cond_code: u32,
2244    ) -> Result<Vec<u8>> {
2245        let mut bytes = Vec::new();
2246
2247        // VCMP.F32 Sn, Sm: 0xEEB40A40 with Sn in Vd position, Sm in Vm position
2248        let sn_num = vfp_sreg_to_num(sn)?;
2249        let sm_num = vfp_sreg_to_num(sm)?;
2250        let (vd, d) = encode_sreg(sn_num);
2251        let (vm, m) = encode_sreg(sm_num);
2252        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2253        bytes.extend_from_slice(&vcmp.to_le_bytes());
2254
2255        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
2256        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2257
2258        // MOV rd, #0: 0xE3A0_0000 | (rd << 12)
2259        let rd_bits = reg_to_bits(rd);
2260        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2261        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2262
2263        // MOVcond rd, #1: cond(4) | 0011 1010 0000 rd(4) 0000 0000 0001
2264        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2265        bytes.extend_from_slice(&mov_one.to_le_bytes());
2266
2267        Ok(bytes)
2268    }
2269
2270    /// Encode F32 constant load as ARM32: MOVW Rt,#lo16 + MOVT Rt,#hi16 + VMOV Sd,Rt
2271    fn encode_arm_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
2272        let mut bytes = Vec::new();
2273        let bits = value.to_bits();
2274
2275        // Use R12 as temp register for constant loading
2276        let rt: u32 = 12; // R12/IP
2277
2278        // MOVW R12, #lo16: 0xE300_C000 | (imm4 << 16) | imm12
2279        let lo16 = bits & 0xFFFF;
2280        let movw = 0xE3000000 | (rt << 12) | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2281        bytes.extend_from_slice(&movw.to_le_bytes());
2282
2283        // MOVT R12, #hi16: 0xE340_C000 | (imm4 << 16) | imm12
2284        let hi16 = (bits >> 16) & 0xFFFF;
2285        let movt = 0xE3400000 | (rt << 12) | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2286        bytes.extend_from_slice(&movt.to_le_bytes());
2287
2288        // VMOV Sd, R12
2289        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
2290        bytes.extend_from_slice(&vmov.to_le_bytes());
2291
2292        Ok(bytes)
2293    }
2294
2295    /// Encode VMOV + VCVT.F32.S32/U32 as ARM32
2296    fn encode_arm_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2297        let mut bytes = Vec::new();
2298
2299        // VMOV Sd, Rm — move integer to VFP register
2300        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
2301        bytes.extend_from_slice(&vmov.to_le_bytes());
2302
2303        // VCVT.F32.S32 Sd, Sd (signed) or VCVT.F32.U32 Sd, Sd (unsigned).
2304        // The "op" bit (bit 7) selects signedness: 1 = signed (S32), 0 =
2305        // unsigned (U32). So signed = 0xEEB80AC0, unsigned = 0xEEB80A40 —
2306        // objdump confirms 0xEEB80A40 decodes to `vcvt.f32.u32` (GI-FPU-002:
2307        // the two were previously swapped, silently making `convert_i32_s`
2308        // an unsigned conversion).
2309        let sd_num = vfp_sreg_to_num(sd)?;
2310        let (vd, d) = encode_sreg(sd_num);
2311        let (vm, m) = encode_sreg(sd_num); // same register as source
2312        let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
2313        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2314        bytes.extend_from_slice(&vcvt.to_le_bytes());
2315
2316        Ok(bytes)
2317    }
2318
2319    /// Encode F32 rounding pseudo-op as ARM32 via VCVT to integer and back.
2320    /// mode: 0b00=nearest, 0b01=floor(-Inf), 0b10=ceil(+Inf), 0b11=trunc(zero)
2321    /// Strategy: VCVT.S32.F32 Sd, Sm (toward zero), then VCVT.F32.S32 Sd, Sd
2322    /// For ceil/floor/nearest, we use VCVTR (round toward mode) + convert back.
2323    /// Simplified: convert to int (toward zero for trunc) then back to float.
2324    /// Encode F32 rounding as ARM32.
2325    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2326    ///
2327    /// For trunc (mode=0b11): uses VCVTR.S32.F32 (always rounds toward zero).
2328    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant
2329    /// which honours FPSCR rmode), then restores FPSCR.
2330    fn encode_arm_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2331        let mut bytes = Vec::new();
2332        let sm_num = vfp_sreg_to_num(sm)?;
2333        let sd_num = vfp_sreg_to_num(sd)?;
2334        let (vd_s, d_s) = encode_sreg(sd_num);
2335        let (vm_s, m_s) = encode_sreg(sm_num);
2336
2337        if mode == 0b11 {
2338            // Trunc (toward zero): VCVTR.S32.F32 — the "R" variant always truncates.
2339            // 0xEEBD0AC0: bit[7]=1 => round toward zero regardless of FPSCR
2340            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2341            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2342        } else {
2343            // ceil/floor/nearest: manipulate FPSCR rounding mode
2344            let rt: u32 = 12; // R12/IP as temp
2345
2346            // VMRS R12, FPSCR
2347            let vmrs = 0xEEF10A10 | (rt << 12);
2348            bytes.extend_from_slice(&vmrs.to_le_bytes());
2349
2350            // BIC R12, R12, #(3 << 22) — clear RMode bits [23:22]
2351            // 3<<22 = 0x00C00000. ARM rotated imm: 0x03 ror 10 (rotation=5, imm8=0x03)
2352            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2353            bytes.extend_from_slice(&bic.to_le_bytes());
2354
2355            // ORR R12, R12, #(mode << 22) — set desired rounding mode
2356            if mode != 0 {
2357                // mode<<22: rotation=5, imm8=mode
2358                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2359                bytes.extend_from_slice(&orr.to_le_bytes());
2360            }
2361
2362            // VMSR FPSCR, R12
2363            let vmsr = 0xEEE10A10 | (rt << 12);
2364            bytes.extend_from_slice(&vmsr.to_le_bytes());
2365
2366            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rounding mode
2367            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2368            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2369
2370            // Restore FPSCR: clear rmode bits back to nearest (default)
2371            bytes.extend_from_slice(&vmrs.to_le_bytes());
2372            bytes.extend_from_slice(&bic.to_le_bytes());
2373            bytes.extend_from_slice(&vmsr.to_le_bytes());
2374        }
2375
2376        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
2377        let (vd2, d2) = encode_sreg(sd_num);
2378        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
2379        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2380
2381        Ok(bytes)
2382    }
2383
2384    /// Encode F32 min/max as ARM32: VCMP + VMRS + conditional VMOV
2385    fn encode_arm_f32_minmax(
2386        &self,
2387        sd: &VfpReg,
2388        sn: &VfpReg,
2389        sm: &VfpReg,
2390        is_min: bool,
2391    ) -> Result<Vec<u8>> {
2392        let mut bytes = Vec::new();
2393        let sn_num = vfp_sreg_to_num(sn)?;
2394        let sm_num = vfp_sreg_to_num(sm)?;
2395        let sd_num = vfp_sreg_to_num(sd)?;
2396
2397        // VMOV Sd, Sn (start with first operand)
2398        let (vd, d) = encode_sreg(sd_num);
2399        let (vn, n) = encode_sreg(sn_num);
2400        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2401        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2402
2403        // VCMP.F32 Sn, Sm
2404        let (vm, m) = encode_sreg(sm_num);
2405        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2406        bytes.extend_from_slice(&vcmp.to_le_bytes());
2407
2408        // VMRS APSR_nzcv, FPSCR
2409        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2410
2411        // For min: if Sn > Sm (GT), use Sm. Condition = GT (0xC)
2412        // For max: if Sn < Sm (MI/LT), use Sm. Condition = MI (0x4)
2413        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2414
2415        // VMOV{cond} Sd, Sm — conditional VMOV
2416        let vmov_cond = (cond << 28) | 0x0EB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2417        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2418
2419        Ok(bytes)
2420    }
2421
2422    /// Encode F32 copysign as ARM32: extract sign from Sm, magnitude from Sn
2423    fn encode_arm_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2424        let mut bytes = Vec::new();
2425
2426        // VMOV R12, Sm (get sign source bits)
2427        let vmov_sm = encode_vmov_core_sreg(false, sm, &Reg::R12)?;
2428        bytes.extend_from_slice(&vmov_sm.to_le_bytes());
2429
2430        // VMOV R0, Sn (get magnitude source bits) — use R0 as temp
2431        let vmov_sn = encode_vmov_core_sreg(false, sn, &Reg::R0)?;
2432        bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2433
2434        // AND R12, R12, #0x80000000 (keep only sign bit)
2435        // Thumb-2 constant 0x80000000 needs special encoding; in ARM32 use rotated imm
2436        // 0x80000000 = 0x02 rotated right by 2 (rotation=1, imm8=0x02)
2437        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2438        bytes.extend_from_slice(&and_sign.to_le_bytes());
2439
2440        // BIC R0, R0, #0x80000000 (clear sign bit from magnitude)
2441        // R0 = register 0, so Rn and Rd fields are 0
2442        let bic_sign = 0xE3C00000u32 | (1 << 8) | 0x02;
2443        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2444
2445        // ORR R0, R0, R12 (combine sign + magnitude)
2446        // R0 = register 0, so Rn and Rd fields are 0
2447        let orr = 0xE1800000u32 | 12;
2448        bytes.extend_from_slice(&orr.to_le_bytes());
2449
2450        // VMOV Sd, R0
2451        let vmov_result = encode_vmov_core_sreg(true, sd, &Reg::R0)?;
2452        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2453
2454        Ok(bytes)
2455    }
2456
2457    /// Encode F64 comparison as ARM32: VCMP.F64 + VMRS + MOV rd,#0 + MOVcond rd,#1
2458    fn encode_arm_f64_compare(
2459        &self,
2460        rd: &Reg,
2461        dn: &VfpReg,
2462        dm: &VfpReg,
2463        cond_code: u32,
2464    ) -> Result<Vec<u8>> {
2465        let mut bytes = Vec::new();
2466
2467        // VCMP.F64 Dn, Dm: 0xEEB40B40 with Dn in Vd position, Dm in Vm position
2468        let dn_num = vfp_dreg_to_num(dn)?;
2469        let dm_num = vfp_dreg_to_num(dm)?;
2470        let (vd, d) = encode_dreg(dn_num);
2471        let (vm, m) = encode_dreg(dm_num);
2472        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2473        bytes.extend_from_slice(&vcmp.to_le_bytes());
2474
2475        // VMRS APSR_nzcv, FPSCR
2476        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2477
2478        // MOV rd, #0
2479        let rd_bits = reg_to_bits(rd);
2480        let mov_zero = 0xE3A00000 | (rd_bits << 12);
2481        bytes.extend_from_slice(&mov_zero.to_le_bytes());
2482
2483        // MOVcond rd, #1
2484        let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2485        bytes.extend_from_slice(&mov_one.to_le_bytes());
2486
2487        Ok(bytes)
2488    }
2489
2490    /// Encode F64 constant load as ARM32: MOVW + MOVT + MOVW + MOVT + VMOV
2491    fn encode_arm_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
2492        let mut bytes = Vec::new();
2493        let bits = value.to_bits();
2494        let lo32 = bits as u32;
2495        let hi32 = (bits >> 32) as u32;
2496
2497        // Load low 32 bits into R0 (Rd field = 0 for R0)
2498        let lo16 = lo32 & 0xFFFF;
2499        let movw_r0 = 0xE3000000 | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2500        bytes.extend_from_slice(&movw_r0.to_le_bytes());
2501        let hi16 = (lo32 >> 16) & 0xFFFF;
2502        let movt_r0 = 0xE3400000 | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2503        bytes.extend_from_slice(&movt_r0.to_le_bytes());
2504
2505        // Load high 32 bits into R12
2506        let lo16 = hi32 & 0xFFFF;
2507        let movw_r12 = 0xE3000000 | ((lo16 >> 12) << 16) | (12 << 12) | (lo16 & 0xFFF);
2508        bytes.extend_from_slice(&movw_r12.to_le_bytes());
2509        let hi16 = (hi32 >> 16) & 0xFFFF;
2510        let movt_r12 = 0xE3400000 | ((hi16 >> 12) << 16) | (12 << 12) | (hi16 & 0xFFF);
2511        bytes.extend_from_slice(&movt_r12.to_le_bytes());
2512
2513        // VMOV Dd, R0, R12
2514        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
2515        bytes.extend_from_slice(&vmov.to_le_bytes());
2516
2517        Ok(bytes)
2518    }
2519
2520    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as ARM32
2521    fn encode_arm_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2522        let mut bytes = Vec::new();
2523
2524        // Use S0 as intermediate: VMOV S0, Rm
2525        let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
2526        bytes.extend_from_slice(&vmov.to_le_bytes());
2527
2528        // VCVT.F64.S32 Dd, S0 (signed) or VCVT.F64.U32 Dd, S0 (unsigned)
2529        // Base: 0xEEB80B40 (signed) or 0xEEB80BC0 (unsigned)
2530        let dd_num = vfp_dreg_to_num(dd)?;
2531        let (vd, d) = encode_dreg(dd_num);
2532        let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
2533        // S0 is register 0: Vm=0, M=0
2534        let vcvt = base | (d << 22) | (vd << 12);
2535        bytes.extend_from_slice(&vcvt.to_le_bytes());
2536
2537        Ok(bytes)
2538    }
2539
2540    /// Encode VCVT.F64.F32 Dd, Sm as ARM32 (f32 to f64 promotion)
2541    fn encode_arm_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2542        let dd_num = vfp_dreg_to_num(dd)?;
2543        let sm_num = vfp_sreg_to_num(sm)?;
2544        let (vd, d) = encode_dreg(dd_num);
2545        let (vm, m) = encode_sreg(sm_num);
2546
2547        // VCVT.F64.F32 Dd, Sm: 0xEEB70AC0
2548        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
2549        Ok(vcvt.to_le_bytes().to_vec())
2550    }
2551
2552    /// Encode VCVT.S32/U32.F64 Sd, Dm + VMOV Rd, Sd as ARM32
2553    fn encode_arm_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2554        let mut bytes = Vec::new();
2555        let dm_num = vfp_dreg_to_num(dm)?;
2556        let (vm, m) = encode_dreg(dm_num);
2557
2558        // VCVT.S32.F64 S0, Dm (toward zero) or VCVT.U32.F64 S0, Dm
2559        // S0: Vd=0, D=0
2560        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
2561        let vcvt = base | (m << 5) | vm;
2562        bytes.extend_from_slice(&vcvt.to_le_bytes());
2563
2564        // VMOV Rd, S0
2565        let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
2566        bytes.extend_from_slice(&vmov.to_le_bytes());
2567
2568        Ok(bytes)
2569    }
2570
2571    /// Encode F64 rounding pseudo-op as ARM32 via VCVT to integer and back.
2572    /// Encode F64 rounding as ARM32.
2573    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2574    ///
2575    /// For trunc: uses VCVTR.S32.F64 (always truncates).
2576    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F64 (non-R variant),
2577    /// then restores FPSCR.
2578    fn encode_arm_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2579        let mut bytes = Vec::new();
2580        let dm_num = vfp_dreg_to_num(dm)?;
2581        let dd_num = vfp_dreg_to_num(dd)?;
2582        let (vm, m) = encode_dreg(dm_num);
2583        let (vd, d) = encode_dreg(dd_num);
2584
2585        if mode == 0b11 {
2586            // Trunc (toward zero): VCVTR.S32.F64 — bit[7]=1, always truncates
2587            let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
2588            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2589        } else {
2590            // ceil/floor/nearest: manipulate FPSCR rounding mode
2591            let rt: u32 = 12;
2592
2593            // VMRS R12, FPSCR
2594            let vmrs = 0xEEF10A10 | (rt << 12);
2595            bytes.extend_from_slice(&vmrs.to_le_bytes());
2596
2597            // BIC R12, R12, #(3 << 22)
2598            let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2599            bytes.extend_from_slice(&bic.to_le_bytes());
2600
2601            // ORR R12, R12, #(mode << 22)
2602            if mode != 0 {
2603                let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2604                bytes.extend_from_slice(&orr.to_le_bytes());
2605            }
2606
2607            // VMSR FPSCR, R12
2608            let vmsr = 0xEEE10A10 | (rt << 12);
2609            bytes.extend_from_slice(&vmsr.to_le_bytes());
2610
2611            // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0), uses FPSCR rmode
2612            let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
2613            bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2614
2615            // Restore FPSCR
2616            bytes.extend_from_slice(&vmrs.to_le_bytes());
2617            bytes.extend_from_slice(&bic.to_le_bytes());
2618            bytes.extend_from_slice(&vmsr.to_le_bytes());
2619        }
2620
2621        // VCVT.F64.S32 Dd, S0 (convert back to double)
2622        let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
2623        bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2624
2625        Ok(bytes)
2626    }
2627
2628    /// Encode F64 min/max as ARM32: VMOV + VCMP + VMRS + conditional VMOV
2629    fn encode_arm_f64_minmax(
2630        &self,
2631        dd: &VfpReg,
2632        dn: &VfpReg,
2633        dm: &VfpReg,
2634        is_min: bool,
2635    ) -> Result<Vec<u8>> {
2636        let mut bytes = Vec::new();
2637        let dn_num = vfp_dreg_to_num(dn)?;
2638        let dm_num = vfp_dreg_to_num(dm)?;
2639        let dd_num = vfp_dreg_to_num(dd)?;
2640
2641        // VMOV.F64 Dd, Dn (start with first operand)
2642        let (vd, d) = encode_dreg(dd_num);
2643        let (vn, n) = encode_dreg(dn_num);
2644        let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2645        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2646
2647        // VCMP.F64 Dn, Dm
2648        let (vm, m) = encode_dreg(dm_num);
2649        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2650        bytes.extend_from_slice(&vcmp.to_le_bytes());
2651
2652        // VMRS APSR_nzcv, FPSCR
2653        bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2654
2655        let cond = if is_min { 0xCu32 } else { 0x4u32 };
2656        let vmov_cond = (cond << 28) | 0x0EB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2657        bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2658
2659        Ok(bytes)
2660    }
2661
2662    /// Encode F64 copysign as ARM32
2663    fn encode_arm_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
2664        let mut bytes = Vec::new();
2665
2666        // VMOV R0, R12, Dm (get sign source bits)
2667        let vmov_dm = encode_vmov_core_dreg(false, dm, &Reg::R0, &Reg::R12)?;
2668        bytes.extend_from_slice(&vmov_dm.to_le_bytes());
2669
2670        // VMOV R1, R2, Dn (get magnitude source bits)
2671        // We use R1 (lo) and R2 (hi) for the magnitude
2672        let vmov_dn = encode_vmov_core_dreg(false, dn, &Reg::R1, &Reg::R2)?;
2673        bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2674
2675        // AND R12, R12, #0x80000000 (keep only sign bit from hi word)
2676        let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2677        bytes.extend_from_slice(&and_sign.to_le_bytes());
2678
2679        // BIC R2, R2, #0x80000000 (clear sign bit from magnitude hi word)
2680        let bic_sign = 0xE3C00000u32 | (2 << 16) | (2 << 12) | (1 << 8) | 0x02;
2681        bytes.extend_from_slice(&bic_sign.to_le_bytes());
2682
2683        // ORR R2, R2, R12 (combine sign + magnitude)
2684        let orr = 0xE1800000u32 | (2 << 16) | (2 << 12) | 12;
2685        bytes.extend_from_slice(&orr.to_le_bytes());
2686
2687        // VMOV Dd, R1, R2
2688        let vmov_result = encode_vmov_core_dreg(true, dd, &Reg::R1, &Reg::R2)?;
2689        bytes.extend_from_slice(&vmov_result.to_le_bytes());
2690
2691        Ok(bytes)
2692    }
2693
2694    /// Encode VCVT.S32/U32.F32 + VMOV as ARM32
2695    fn encode_arm_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2696        let mut bytes = Vec::new();
2697
2698        // VCVT.S32.F32 Sd, Sm (toward zero) or VCVT.U32.F32 Sd, Sm
2699        // We use Sm as both source and destination for the intermediate result
2700        let sm_num = vfp_sreg_to_num(sm)?;
2701        let (vd, d) = encode_sreg(sm_num);
2702        let (vm, m) = encode_sreg(sm_num);
2703        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
2704        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2705        bytes.extend_from_slice(&vcvt.to_le_bytes());
2706
2707        // VMOV Rd, Sm — move result back to core register
2708        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
2709        bytes.extend_from_slice(&vmov.to_le_bytes());
2710
2711        Ok(bytes)
2712    }
2713
2714    /// Encode an ARM instruction in Thumb-2 mode (16-bit or 32-bit instructions)
2715    fn encode_thumb(&self, op: &ArmOp) -> Result<Vec<u8>> {
2716        // Thumb-2 supports both 16-bit and 32-bit instructions
2717        // 32-bit instructions are encoded as two 16-bit halfwords (big-endian order)
2718        match op {
2719            // === 16-bit Thumb encodings ===
2720            ArmOp::Add { rd, rn, op2 } => {
2721                let rd_bits = reg_to_bits(rd) as u16;
2722                let rn_bits = reg_to_bits(rn) as u16;
2723
2724                if let Operand2::Reg(rm) = op2 {
2725                    let rm_bits = reg_to_bits(rm) as u16;
2726                    // 16-bit ADDS only has 3-bit register fields (R0-R7). For
2727                    // high registers (e.g. R12, the MemLoad/MemStore base
2728                    // scratch) the bits overflow into adjacent fields, silently
2729                    // corrupting the operands — issue #178/#180: `add ip,ip,r0`
2730                    // was emitted as `adds r4,r5,r1`. Guard on all three regs
2731                    // being low and fall back to 32-bit ADD.W otherwise, exactly
2732                    // as the Sub handler below does.
2733                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2734                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2735                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2736                        Ok(instr.to_le_bytes().to_vec())
2737                    } else {
2738                        // ADD.W Rd, Rn, Rm (32-bit) for high registers
2739                        self.encode_thumb32_add_reg_raw(
2740                            rd_bits as u32,
2741                            rn_bits as u32,
2742                            rm_bits as u32,
2743                        )
2744                    }
2745                } else if let Operand2::Imm(imm) = op2 {
2746                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2747                        // ADDS Rd, Rn, #imm3 (16-bit): 0001 110 imm3 Rn Rd
2748                        let instr: u16 = 0x1C00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2749                        Ok(instr.to_le_bytes().to_vec())
2750                    } else {
2751                        // Use 32-bit ADD for larger immediates
2752                        self.encode_thumb32_add(rd, rn, *imm as u32)
2753                    }
2754                } else {
2755                    // Fallback to 32-bit encoding
2756                    self.encode_thumb32_add(rd, rn, 0)
2757                }
2758            }
2759
2760            ArmOp::Sub { rd, rn, op2 } => {
2761                let rd_bits = reg_to_bits(rd) as u16;
2762                let rn_bits = reg_to_bits(rn) as u16;
2763
2764                if let Operand2::Reg(rm) = op2 {
2765                    let rm_bits = reg_to_bits(rm) as u16;
2766                    // 16-bit SUBS can only use low registers (R0-R7)
2767                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2768                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2769                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2770                        Ok(instr.to_le_bytes().to_vec())
2771                    } else {
2772                        // Use 32-bit SUB.W for high registers
2773                        self.encode_thumb32_sub_reg_raw(
2774                            rd_bits as u32,
2775                            rn_bits as u32,
2776                            rm_bits as u32,
2777                        )
2778                    }
2779                } else if let Operand2::Imm(imm) = op2 {
2780                    if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2781                        // SUBS Rd, Rn, #imm3 (16-bit): 0001 111 imm3 Rn Rd
2782                        let instr: u16 = 0x1E00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2783                        Ok(instr.to_le_bytes().to_vec())
2784                    } else {
2785                        self.encode_thumb32_sub(rd, rn, *imm as u32)
2786                    }
2787                } else {
2788                    self.encode_thumb32_sub(rd, rn, 0)
2789                }
2790            }
2791
2792            ArmOp::Mov { rd, op2 } => {
2793                let rd_bits = reg_to_bits(rd) as u16;
2794
2795                if let Operand2::Imm(imm) = op2 {
2796                    // #498: the old test here was the SIGNED `*imm <= 255`,
2797                    // so a negative immediate (e.g. -1) fell into the 16-bit
2798                    // MOVS arm and encoded the wrong VALUE (#(imm & 0xFF) =
2799                    // #0xFF). A positive imm above 0xFFFF was equally wrong:
2800                    // MOVW truncates to 16 bits. Split on the UNSIGNED value:
2801                    // imm8 → MOVS, imm16 → MOVW, anything wider (negative or
2802                    // >0xFFFF) → the full-value MOVW+MOVT pair. No emitter
2803                    // produces the wide shape today (both selectors
2804                    // materialize wide constants as explicit Movw/Movt or
2805                    // Movw+Mvn), so this is byte-identical on shipped paths —
2806                    // it retires the latent wrong-value encodings the
2807                    // `estimator_encoder_agreement` oracle had pinned.
2808                    let uimm = *imm as u32;
2809                    if uimm <= 255 && rd_bits < 8 {
2810                        // MOVS Rd, #imm8 (16-bit): 0010 0 Rd imm8
2811                        let imm_bits = (*imm as u16) & 0xFF;
2812                        let instr: u16 = 0x2000 | (rd_bits << 8) | imm_bits;
2813                        Ok(instr.to_le_bytes().to_vec())
2814                    } else if uimm <= 0xFFFF {
2815                        // Use 32-bit MOVW for 16-bit immediates
2816                        self.encode_thumb32_movw(rd, uimm)
2817                    } else {
2818                        // Full 32-bit value: MOVW low16 + MOVT high16
2819                        let mut bytes = self.encode_thumb32_movw(rd, uimm & 0xFFFF)?;
2820                        bytes.extend(self.encode_thumb32_movt_raw(reg_to_bits(rd), uimm >> 16)?);
2821                        Ok(bytes)
2822                    }
2823                } else if let Operand2::Reg(rm) = op2 {
2824                    let rm_bits = reg_to_bits(rm) as u16;
2825                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
2826                    // D = Rd[3], Rd[2:0] in lower bits
2827                    let d_bit = (rd_bits >> 3) & 1;
2828                    let instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
2829                    Ok(instr.to_le_bytes().to_vec())
2830                } else {
2831                    let instr: u16 = 0xBF00; // NOP fallback
2832                    Ok(instr.to_le_bytes().to_vec())
2833                }
2834            }
2835
2836            ArmOp::Push { regs } => {
2837                // Thumb-2 PUSH encoding:
2838                // If all regs in R0-R7 + LR, use 16-bit: 1011 010 M rrrrrrrr
2839                // Otherwise use 32-bit: STMDB SP!, {regs} = 1110 1001 0010 1101 | 0M0 reglist(13)
2840                let mut reg_list: u16 = 0;
2841                let mut need_32bit = false;
2842                for r in regs {
2843                    let bit = reg_to_bits(r);
2844                    if bit >= 8 && *r != Reg::LR {
2845                        need_32bit = true;
2846                    }
2847                    reg_list |= 1 << bit;
2848                }
2849                if !need_32bit {
2850                    // 16-bit PUSH: 1011 010 M rrrrrrrr
2851                    let m_bit = if reg_list & (1 << 14) != 0 {
2852                        1u16
2853                    } else {
2854                        0u16
2855                    };
2856                    let low_regs = reg_list & 0xFF;
2857                    let instr: u16 = 0xB400 | (m_bit << 8) | low_regs;
2858                    Ok(instr.to_le_bytes().to_vec())
2859                } else {
2860                    // 32-bit STMDB SP!, {regs}: E92D | reglist(16)
2861                    let hw1: u16 = 0xE92D;
2862                    let hw2: u16 = reg_list;
2863                    let mut bytes = hw1.to_le_bytes().to_vec();
2864                    bytes.extend_from_slice(&hw2.to_le_bytes());
2865                    Ok(bytes)
2866                }
2867            }
2868
2869            ArmOp::Pop { regs } => {
2870                // Thumb-2 POP encoding:
2871                // If all regs in R0-R7 + PC, use 16-bit: 1011 110 P rrrrrrrr
2872                // Otherwise use 32-bit: LDMIA SP!, {regs} = 1110 1000 1011 1101 | PM0 reglist(13)
2873                let mut reg_list: u16 = 0;
2874                let mut need_32bit = false;
2875                for r in regs {
2876                    let bit = reg_to_bits(r);
2877                    if bit >= 8 && *r != Reg::PC {
2878                        need_32bit = true;
2879                    }
2880                    reg_list |= 1 << bit;
2881                }
2882                if !need_32bit {
2883                    // 16-bit POP: 1011 110 P rrrrrrrr
2884                    let p_bit = if reg_list & (1 << 15) != 0 {
2885                        1u16
2886                    } else {
2887                        0u16
2888                    };
2889                    let low_regs = reg_list & 0xFF;
2890                    let instr: u16 = 0xBC00 | (p_bit << 8) | low_regs;
2891                    Ok(instr.to_le_bytes().to_vec())
2892                } else {
2893                    // 32-bit LDMIA SP!, {regs}: E8BD | reglist(16)
2894                    let hw1: u16 = 0xE8BD;
2895                    let hw2: u16 = reg_list;
2896                    let mut bytes = hw1.to_le_bytes().to_vec();
2897                    bytes.extend_from_slice(&hw2.to_le_bytes());
2898                    Ok(bytes)
2899                }
2900            }
2901
2902            ArmOp::Nop => {
2903                let instr: u16 = 0xBF00; // NOP in Thumb-2
2904                Ok(instr.to_le_bytes().to_vec())
2905            }
2906
2907            ArmOp::Udf { imm } => {
2908                // UDF (Undefined) in Thumb-2: 16-bit encoding is 0xDE00 | imm8
2909                // This triggers UsageFault/HardFault, used for WASM traps
2910                let instr: u16 = 0xDE00 | (*imm as u16);
2911                let bytes = instr.to_le_bytes().to_vec();
2912                encoding_contracts::verify_thumb16(&bytes);
2913                Ok(bytes)
2914            }
2915
2916            // i64 support: ADDS, ADC, SUBS, SBC for register pair arithmetic
2917            // ADDS sets flags (carry), ADC uses carry from previous ADDS
2918            ArmOp::Adds { rd, rn, op2 } => {
2919                let rd_bits = reg_to_bits(rd) as u16;
2920                let rn_bits = reg_to_bits(rn) as u16;
2921
2922                if let Operand2::Reg(rm) = op2 {
2923                    let rm_bits = reg_to_bits(rm) as u16;
2924                    // 16-bit ADDS is R0-R7 only; i64 pair allocation can place
2925                    // operands in R8-R11, which would overflow the 3-bit fields
2926                    // and corrupt the operands (#178/#180 class). Guard and fall
2927                    // back to 32-bit ADDS.W for high registers.
2928                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2929                        // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2930                        let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2931                        Ok(instr.to_le_bytes().to_vec())
2932                    } else {
2933                        self.encode_thumb32_adds_reg_raw(
2934                            rd_bits as u32,
2935                            rn_bits as u32,
2936                            rm_bits as u32,
2937                        )
2938                    }
2939                } else {
2940                    // 32-bit Thumb-2 ADDS with immediate
2941                    self.encode_thumb32_adds(rd, rn, 0)
2942                }
2943            }
2944
2945            // ADC: Add with Carry (Thumb-2 32-bit)
2946            // ADC.W Rd, Rn, Rm: EB40 Rn | 00 Rd 00 Rm
2947            ArmOp::Adc { rd, rn, op2 } => {
2948                let rd_bits = reg_to_bits(rd);
2949                let rn_bits = reg_to_bits(rn);
2950
2951                if let Operand2::Reg(rm) = op2 {
2952                    let rm_bits = reg_to_bits(rm);
2953                    // ADC.W Rd, Rn, Rm (T2): 1110 1011 0100 Rn | 0 000 Rd 00 00 Rm
2954                    let hw1: u16 = (0xEB40 | rn_bits) as u16;
2955                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2956
2957                    let mut bytes = hw1.to_le_bytes().to_vec();
2958                    bytes.extend_from_slice(&hw2.to_le_bytes());
2959                    Ok(bytes)
2960                } else {
2961                    // ADC with immediate - use 32-bit encoding
2962                    let hw1: u16 = (0xF140 | rn_bits) as u16;
2963                    let hw2: u16 = (rd_bits << 8) as u16;
2964                    let mut bytes = hw1.to_le_bytes().to_vec();
2965                    bytes.extend_from_slice(&hw2.to_le_bytes());
2966                    Ok(bytes)
2967                }
2968            }
2969
2970            // SUBS sets flags (borrow), SBC uses borrow from previous SUBS
2971            ArmOp::Subs { rd, rn, op2 } => {
2972                let rd_bits = reg_to_bits(rd) as u16;
2973                let rn_bits = reg_to_bits(rn) as u16;
2974
2975                if let Operand2::Reg(rm) = op2 {
2976                    let rm_bits = reg_to_bits(rm) as u16;
2977                    // 16-bit SUBS is R0-R7 only; high-register i64 pair operands
2978                    // would overflow the 3-bit fields (#178/#180 class). Guard
2979                    // and fall back to 32-bit SUBS.W for high registers.
2980                    if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2981                        // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2982                        let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2983                        Ok(instr.to_le_bytes().to_vec())
2984                    } else {
2985                        self.encode_thumb32_subs_reg_raw(
2986                            rd_bits as u32,
2987                            rn_bits as u32,
2988                            rm_bits as u32,
2989                        )
2990                    }
2991                } else {
2992                    // 32-bit Thumb-2 SUBS with immediate
2993                    self.encode_thumb32_subs(rd, rn, 0)
2994                }
2995            }
2996
2997            // SBC: Subtract with Carry (Thumb-2 32-bit)
2998            // SBC.W Rd, Rn, Rm: EB60 Rn | 00 Rd 00 Rm
2999            ArmOp::Sbc { rd, rn, op2 } => {
3000                let rd_bits = reg_to_bits(rd);
3001                let rn_bits = reg_to_bits(rn);
3002
3003                if let Operand2::Reg(rm) = op2 {
3004                    let rm_bits = reg_to_bits(rm);
3005                    // SBC.W Rd, Rn, Rm (T2): 1110 1011 0110 Rn | 0 000 Rd 00 00 Rm
3006                    let hw1: u16 = (0xEB60 | rn_bits) as u16;
3007                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3008
3009                    let mut bytes = hw1.to_le_bytes().to_vec();
3010                    bytes.extend_from_slice(&hw2.to_le_bytes());
3011                    Ok(bytes)
3012                } else {
3013                    // SBC with immediate - use 32-bit encoding
3014                    let hw1: u16 = (0xF160 | rn_bits) as u16;
3015                    let hw2: u16 = (rd_bits << 8) as u16;
3016                    let mut bytes = hw1.to_le_bytes().to_vec();
3017                    bytes.extend_from_slice(&hw2.to_le_bytes());
3018                    Ok(bytes)
3019                }
3020            }
3021
3022            // === 32-bit Thumb-2 encodings ===
3023
3024            // SDIV: 11111011 1001 Rn 1111 Rd 1111 Rm
3025            ArmOp::Sdiv { rd, rn, rm } => {
3026                let rd_bits = reg_to_bits(rd);
3027                let rn_bits = reg_to_bits(rn);
3028                let rm_bits = reg_to_bits(rm);
3029                reg_bits_checked(rd_bits)?;
3030                reg_bits_checked(rn_bits)?;
3031                reg_bits_checked(rm_bits)?;
3032
3033                // Thumb-2 SDIV: FB90 F0F0 | Rn<<16 | Rd<<8 | Rm
3034                // First halfword: 1111 1011 1001 Rn = 0xFB90 | Rn
3035                // Second halfword: 1111 Rd 1111 Rm = 0xF0F0 | Rd<<8 | Rm
3036                let hw1: u16 = (0xFB90 | rn_bits) as u16;
3037                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
3038
3039                // Thumb-2 32-bit instructions: first halfword, then second halfword (little-endian each)
3040                let mut bytes = hw1.to_le_bytes().to_vec();
3041                bytes.extend_from_slice(&hw2.to_le_bytes());
3042                encoding_contracts::verify_thumb32(&bytes);
3043                Ok(bytes)
3044            }
3045
3046            // UDIV: 11111011 1011 Rn 1111 Rd 1111 Rm
3047            ArmOp::Udiv { rd, rn, rm } => {
3048                let rd_bits = reg_to_bits(rd);
3049                let rn_bits = reg_to_bits(rn);
3050                let rm_bits = reg_to_bits(rm);
3051                reg_bits_checked(rd_bits)?;
3052                reg_bits_checked(rn_bits)?;
3053                reg_bits_checked(rm_bits)?;
3054
3055                // Thumb-2 UDIV: FBB0 F0F0 | Rn<<16 | Rd<<8 | Rm
3056                let hw1: u16 = (0xFBB0 | rn_bits) as u16;
3057                let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
3058
3059                let mut bytes = hw1.to_le_bytes().to_vec();
3060                bytes.extend_from_slice(&hw2.to_le_bytes());
3061                encoding_contracts::verify_thumb32(&bytes);
3062                Ok(bytes)
3063            }
3064
3065            ArmOp::Umull { rdlo, rdhi, rn, rm } => {
3066                let rdlo_bits = reg_to_bits(rdlo);
3067                let rdhi_bits = reg_to_bits(rdhi);
3068                let rn_bits = reg_to_bits(rn);
3069                let rm_bits = reg_to_bits(rm);
3070                reg_bits_checked(rdlo_bits)?;
3071                reg_bits_checked(rdhi_bits)?;
3072                reg_bits_checked(rn_bits)?;
3073                reg_bits_checked(rm_bits)?;
3074
3075                // Thumb-2 UMULL: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm
3076                let hw1: u16 = (0xFBA0 | rn_bits) as u16;
3077                let hw2: u16 = ((rdlo_bits << 12) | (rdhi_bits << 8) | rm_bits) as u16;
3078
3079                let mut bytes = hw1.to_le_bytes().to_vec();
3080                bytes.extend_from_slice(&hw2.to_le_bytes());
3081                encoding_contracts::verify_thumb32(&bytes);
3082                Ok(bytes)
3083            }
3084
3085            // MUL (Thumb-2 32-bit): MUL Rd, Rn, Rm
3086            ArmOp::Mul { rd, rn, rm } => {
3087                let rd_bits = reg_to_bits(rd);
3088                let rn_bits = reg_to_bits(rn);
3089                let rm_bits = reg_to_bits(rm);
3090
3091                // Thumb-2 MUL: FB00 F000 | Rn | Rd<<8 | Rm
3092                // 11111011 0000 Rn | 1111 Rd 0000 Rm
3093                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3094                let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
3095
3096                let mut bytes = hw1.to_le_bytes().to_vec();
3097                bytes.extend_from_slice(&hw2.to_le_bytes());
3098                Ok(bytes)
3099            }
3100
3101            // MLS: Rd = Ra - Rn * Rm
3102            ArmOp::Mls { rd, rn, rm, ra } => {
3103                let rd_bits = reg_to_bits(rd);
3104                let rn_bits = reg_to_bits(rn);
3105                let rm_bits = reg_to_bits(rm);
3106                let ra_bits = reg_to_bits(ra);
3107
3108                // Thumb-2 MLS: FB00 Rn | Ra Rd 0001 Rm
3109                // 11111011 0000 Rn | Ra Rd 0001 Rm
3110                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3111                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | 0x10 | rm_bits) as u16;
3112
3113                let mut bytes = hw1.to_le_bytes().to_vec();
3114                bytes.extend_from_slice(&hw2.to_le_bytes());
3115                Ok(bytes)
3116            }
3117
3118            ArmOp::Mla { rd, rn, rm, ra } => {
3119                let rd_bits = reg_to_bits(rd);
3120                let rn_bits = reg_to_bits(rn);
3121                let rm_bits = reg_to_bits(rm);
3122                let ra_bits = reg_to_bits(ra);
3123
3124                // Thumb-2 MLA: FB00 Rn | Ra Rd 0000 Rm — same as MLS without the
3125                // bit-4 (0x10) op flag. rd = ra + rn*rm.
3126                let hw1: u16 = (0xFB00 | rn_bits) as u16;
3127                let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | rm_bits) as u16;
3128
3129                let mut bytes = hw1.to_le_bytes().to_vec();
3130                bytes.extend_from_slice(&hw2.to_le_bytes());
3131                Ok(bytes)
3132            }
3133
3134            // AND (Thumb-2 32-bit)
3135            ArmOp::And { rd, rn, op2 } => {
3136                if let Operand2::Reg(rm) = op2 {
3137                    let rd_bits = reg_to_bits(rd);
3138                    let rn_bits = reg_to_bits(rn);
3139                    let rm_bits = reg_to_bits(rm);
3140
3141                    // Thumb-2 AND register: EA00 Rn | 0 Rd 00 00 Rm
3142                    let hw1: u16 = (0xEA00 | rn_bits) as u16;
3143                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3144
3145                    let mut bytes = hw1.to_le_bytes().to_vec();
3146                    bytes.extend_from_slice(&hw2.to_le_bytes());
3147                    Ok(bytes)
3148                } else if let Operand2::Imm(imm) = op2 {
3149                    let rd_bits = reg_to_bits(rd);
3150                    let rn_bits = reg_to_bits(rn);
3151
3152                    // Thumb-2 AND.W immediate T1: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8.
3153                    // The i:imm3:imm8 field is a ThumbExpandImm modified immediate —
3154                    // encode it correctly (or error on an un-encodable value)
3155                    // rather than packing raw bits, closing the silent-miscompile
3156                    // class for AND alongside ORR/EOR (#251) / ADD/SUB (#253) /
3157                    // CMP (#255).
3158                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3159                        synth_core::Error::synthesis(
3160                            "AND immediate is not a valid ThumbExpandImm — materialize into a register",
3161                        )
3162                    })?;
3163                    let i_bit = (field >> 11) & 1;
3164                    let imm3 = (field >> 8) & 0x7;
3165                    let imm8 = field & 0xFF;
3166
3167                    let hw1: u16 = (0xF000 | (i_bit << 10) | rn_bits) as u16;
3168                    let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3169
3170                    let mut bytes = hw1.to_le_bytes().to_vec();
3171                    bytes.extend_from_slice(&hw2.to_le_bytes());
3172                    Ok(bytes)
3173                } else {
3174                    // RegShift variant - fallback to NOP
3175                    let instr: u16 = 0xBF00;
3176                    Ok(instr.to_le_bytes().to_vec())
3177                }
3178            }
3179
3180            // ORR (Thumb-2 32-bit)
3181            ArmOp::Orr { rd, rn, op2 } => {
3182                if let Operand2::Reg(rm) = op2 {
3183                    let rd_bits = reg_to_bits(rd);
3184                    let rn_bits = reg_to_bits(rn);
3185                    let rm_bits = reg_to_bits(rm);
3186
3187                    // Thumb-2 ORR: EA40 Rn | 0 Rd 00 00 Rm
3188                    let hw1: u16 = (0xEA40 | rn_bits) as u16;
3189                    let hw2: u16 = ((rd_bits << 8) | rm_bits) 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 if let Operand2::Imm(imm) = op2 {
3195                    // ORR.W immediate T1: 11110 i 0 0010 S Rn | 0 imm3 Rd imm8.
3196                    // Only the zero-extended byte form (imm <= 0xFF) is encoded;
3197                    // larger modified immediates need ThumbExpandImm — return an
3198                    // error rather than silently emit a NOP (Ok-or-Err, #180/#185).
3199                    let imm_val = *imm as u32;
3200                    if imm_val > 0xFF {
3201                        return Err(synth_core::Error::synthesis(
3202                            "ORR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3203                        ));
3204                    }
3205                    let rd_bits = reg_to_bits(rd);
3206                    let rn_bits = reg_to_bits(rn);
3207                    let hw1: u16 = (0xF040 | rn_bits) as u16;
3208                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3209                    let mut bytes = hw1.to_le_bytes().to_vec();
3210                    bytes.extend_from_slice(&hw2.to_le_bytes());
3211                    Ok(bytes)
3212                } else {
3213                    let instr: u16 = 0xBF00;
3214                    Ok(instr.to_le_bytes().to_vec())
3215                }
3216            }
3217
3218            // EOR (Thumb-2 32-bit)
3219            ArmOp::Eor { rd, rn, op2 } => {
3220                if let Operand2::Reg(rm) = op2 {
3221                    let rd_bits = reg_to_bits(rd);
3222                    let rn_bits = reg_to_bits(rn);
3223                    let rm_bits = reg_to_bits(rm);
3224
3225                    // Thumb-2 EOR: EA80 Rn | 0 Rd 00 00 Rm
3226                    let hw1: u16 = (0xEA80 | rn_bits) as u16;
3227                    let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3228
3229                    let mut bytes = hw1.to_le_bytes().to_vec();
3230                    bytes.extend_from_slice(&hw2.to_le_bytes());
3231                    Ok(bytes)
3232                } else if let Operand2::Imm(imm) = op2 {
3233                    // EOR.W immediate T1: 11110 i 0 0100 S Rn | 0 imm3 Rd imm8.
3234                    // Byte form only (imm <= 0xFF); larger needs ThumbExpandImm —
3235                    // error, not a silent NOP (Ok-or-Err, #180/#185).
3236                    let imm_val = *imm as u32;
3237                    if imm_val > 0xFF {
3238                        return Err(synth_core::Error::synthesis(
3239                            "EOR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3240                        ));
3241                    }
3242                    let rd_bits = reg_to_bits(rd);
3243                    let rn_bits = reg_to_bits(rn);
3244                    let hw1: u16 = (0xF080 | rn_bits) as u16;
3245                    let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3246                    let mut bytes = hw1.to_le_bytes().to_vec();
3247                    bytes.extend_from_slice(&hw2.to_le_bytes());
3248                    Ok(bytes)
3249                } else {
3250                    let instr: u16 = 0xBF00;
3251                    Ok(instr.to_le_bytes().to_vec())
3252                }
3253            }
3254
3255            // Shift operations (16-bit for low registers)
3256            ArmOp::Lsl { rd, rn, shift } => {
3257                let rd_bits = reg_to_bits(rd) as u16;
3258                let rn_bits = reg_to_bits(rn) as u16;
3259                let shift_bits = (*shift as u16) & 0x1F;
3260
3261                if rd_bits < 8 && rn_bits < 8 {
3262                    // LSLS Rd, Rm, #imm5 (16-bit): 0000 0 imm5 Rm Rd
3263                    let instr: u16 = (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3264                    Ok(instr.to_le_bytes().to_vec())
3265                } else {
3266                    // Use 32-bit encoding for high registers
3267                    self.encode_thumb32_shift(rd, rn, *shift, 0b00) // LSL type
3268                }
3269            }
3270
3271            ArmOp::Lsr { rd, rn, shift } => {
3272                let rd_bits = reg_to_bits(rd) as u16;
3273                let rn_bits = reg_to_bits(rn) as u16;
3274                let shift_bits = (*shift as u16) & 0x1F;
3275
3276                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3277                    // LSRS Rd, Rm, #imm5 (16-bit): 0000 1 imm5 Rm Rd
3278                    let instr: u16 = 0x0800 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3279                    Ok(instr.to_le_bytes().to_vec())
3280                } else {
3281                    self.encode_thumb32_shift(rd, rn, *shift, 0b01) // LSR type
3282                }
3283            }
3284
3285            ArmOp::Asr { rd, rn, shift } => {
3286                let rd_bits = reg_to_bits(rd) as u16;
3287                let rn_bits = reg_to_bits(rn) as u16;
3288                let shift_bits = (*shift as u16) & 0x1F;
3289
3290                if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3291                    // ASRS Rd, Rm, #imm5 (16-bit): 0001 0 imm5 Rm Rd
3292                    let instr: u16 = 0x1000 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3293                    Ok(instr.to_le_bytes().to_vec())
3294                } else {
3295                    self.encode_thumb32_shift(rd, rn, *shift, 0b10) // ASR type
3296                }
3297            }
3298
3299            ArmOp::Ror { rd, rn, shift } => {
3300                // ROR doesn't have a 16-bit immediate form, use 32-bit
3301                self.encode_thumb32_shift(rd, rn, *shift, 0b11) // ROR type
3302            }
3303
3304            // Register-based shifts (Thumb-2 32-bit)
3305            // Encoding: 11111010 0xxS Rn 1111 Rd 0000 Rm
3306            // xx = shift type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
3307            ArmOp::LslReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b00),
3308            ArmOp::LsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b01),
3309            ArmOp::AsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b10),
3310            ArmOp::RorReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b11),
3311
3312            // RSB (Reverse Subtract): Rd = imm - Rn
3313            // Thumb-2 T2 encoding: 11110 i 0 1110 S Rn | 0 imm3 Rd imm8
3314            ArmOp::Rsb { rd, rn, imm } => {
3315                let rd_bits = reg_to_bits(rd);
3316                let rn_bits = reg_to_bits(rn);
3317
3318                // #681 class audit: the T2 `i:imm3:imm8` field is a
3319                // ThumbExpandImm modified immediate and RSB has NO plain-imm12
3320                // (T4-style) form — packing a raw value > 0xFF silently encodes
3321                // a different constant (#253/#255 class). All current emitters
3322                // use imm 32 (shift complement), which expands to itself, so
3323                // this gate is byte-identical for existing codegen.
3324                let field = try_thumb_expand_imm(*imm).ok_or_else(|| {
3325                    synth_core::Error::synthesis(
3326                        "RSB immediate is not a valid ThumbExpandImm — materialize into a register",
3327                    )
3328                })?;
3329                let i_bit = (field >> 11) & 1;
3330                let imm3 = (field >> 8) & 0x7;
3331                let imm8 = field & 0xFF;
3332
3333                // hw1: 11110 i 01110 0 Rn  (S=0)
3334                let hw1: u16 = (0xF1C0 | (i_bit << 10) | rn_bits) as u16;
3335                // hw2: 0 imm3 Rd imm8
3336                let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3337
3338                let mut bytes = hw1.to_le_bytes().to_vec();
3339                bytes.extend_from_slice(&hw2.to_le_bytes());
3340                Ok(bytes)
3341            }
3342
3343            // CLZ (Thumb-2 32-bit)
3344            ArmOp::Clz { rd, rm } => {
3345                let rd_bits = reg_to_bits(rd);
3346                let rm_bits = reg_to_bits(rm);
3347
3348                // Thumb-2 CLZ: FAB0 Rm | F8 Rd Rm
3349                // 11111010 1011 Rm | 1111 1000 Rd Rm
3350                let hw1: u16 = (0xFAB0 | rm_bits) as u16;
3351                let hw2: u16 = (0xF080 | (rd_bits << 8) | rm_bits) as u16;
3352
3353                let mut bytes = hw1.to_le_bytes().to_vec();
3354                bytes.extend_from_slice(&hw2.to_le_bytes());
3355                Ok(bytes)
3356            }
3357
3358            // RBIT (Thumb-2 32-bit)
3359            ArmOp::Rbit { rd, rm } => {
3360                let rd_bits = reg_to_bits(rd);
3361                let rm_bits = reg_to_bits(rm);
3362
3363                // Thumb-2 RBIT: FA90 Rm | F0 Rd A0 Rm
3364                // 11111010 1001 Rm | 1111 Rd 1010 Rm
3365                let hw1: u16 = (0xFA90 | rm_bits) as u16;
3366                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rm_bits) as u16;
3367
3368                let mut bytes = hw1.to_le_bytes().to_vec();
3369                bytes.extend_from_slice(&hw2.to_le_bytes());
3370                Ok(bytes)
3371            }
3372
3373            // SXTB (16-bit for low registers)
3374            ArmOp::Sxtb { rd, rm } => {
3375                let rd_bits = reg_to_bits(rd) as u16;
3376                let rm_bits = reg_to_bits(rm) as u16;
3377
3378                if rd_bits < 8 && rm_bits < 8 {
3379                    // SXTB Rd, Rm (16-bit): 1011 0010 01 Rm Rd
3380                    let instr: u16 = 0xB240 | (rm_bits << 3) | rd_bits;
3381                    Ok(instr.to_le_bytes().to_vec())
3382                } else {
3383                    // Thumb-2 SXTB.W: FA4F F(rd)80 (rm)
3384                    // 11111010 0100 1111 | 1111 Rd 10 rotate Rm
3385                    let rd_bits32 = rd_bits as u32;
3386                    let rm_bits32 = rm_bits as u32;
3387                    let hw1: u16 = 0xFA4F;
3388                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3389                    let mut bytes = hw1.to_le_bytes().to_vec();
3390                    bytes.extend_from_slice(&hw2.to_le_bytes());
3391                    Ok(bytes)
3392                }
3393            }
3394
3395            // SXTH (16-bit for low registers)
3396            ArmOp::Sxth { rd, rm } => {
3397                let rd_bits = reg_to_bits(rd) as u16;
3398                let rm_bits = reg_to_bits(rm) as u16;
3399
3400                if rd_bits < 8 && rm_bits < 8 {
3401                    // SXTH Rd, Rm (16-bit): 1011 0010 00 Rm Rd
3402                    let instr: u16 = 0xB200 | (rm_bits << 3) | rd_bits;
3403                    Ok(instr.to_le_bytes().to_vec())
3404                } else {
3405                    // Thumb-2 SXTH.W: FA0F F(rd)80 (rm)
3406                    // 11111010 0000 1111 | 1111 Rd 10 rotate Rm
3407                    let rd_bits32 = rd_bits as u32;
3408                    let rm_bits32 = rm_bits as u32;
3409                    let hw1: u16 = 0xFA0F;
3410                    let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3411                    let mut bytes = hw1.to_le_bytes().to_vec();
3412                    bytes.extend_from_slice(&hw2.to_le_bytes());
3413                    Ok(bytes)
3414                }
3415            }
3416
3417            // UXTB Rd,Rm — zero-extend byte (rd = rm & 0xff)
3418            ArmOp::Uxtb { rd, rm } => {
3419                let rd_bits = reg_to_bits(rd) as u16;
3420                let rm_bits = reg_to_bits(rm) as u16;
3421                if rd_bits < 8 && rm_bits < 8 {
3422                    // UXTB Rd, Rm (16-bit): 1011 0010 11 Rm Rd
3423                    let instr: u16 = 0xB2C0 | (rm_bits << 3) | rd_bits;
3424                    Ok(instr.to_le_bytes().to_vec())
3425                } else {
3426                    // Thumb-2 UXTB.W: FA5F F(rd)80 (rm)
3427                    let hw1: u16 = 0xFA5F;
3428                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3429                    let mut bytes = hw1.to_le_bytes().to_vec();
3430                    bytes.extend_from_slice(&hw2.to_le_bytes());
3431                    Ok(bytes)
3432                }
3433            }
3434
3435            // UXTH Rd,Rm — zero-extend halfword (rd = rm & 0xffff)
3436            ArmOp::Uxth { rd, rm } => {
3437                let rd_bits = reg_to_bits(rd) as u16;
3438                let rm_bits = reg_to_bits(rm) as u16;
3439                if rd_bits < 8 && rm_bits < 8 {
3440                    // UXTH Rd, Rm (16-bit): 1011 0010 10 Rm Rd
3441                    let instr: u16 = 0xB280 | (rm_bits << 3) | rd_bits;
3442                    Ok(instr.to_le_bytes().to_vec())
3443                } else {
3444                    // Thumb-2 UXTH.W: FA1F F(rd)80 (rm)
3445                    let hw1: u16 = 0xFA1F;
3446                    let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3447                    let mut bytes = hw1.to_le_bytes().to_vec();
3448                    bytes.extend_from_slice(&hw2.to_le_bytes());
3449                    Ok(bytes)
3450                }
3451            }
3452
3453            // CMP (can be 16-bit for low registers)
3454            ArmOp::Cmp { rn, op2 } => {
3455                let rn_bits = reg_to_bits(rn) as u16;
3456
3457                if let Operand2::Imm(imm) = op2 {
3458                    // Only use 16-bit encoding for non-negative immediates 0-255
3459                    // Negative immediates must use 32-bit encoding
3460                    if *imm >= 0 && *imm <= 255 && rn_bits < 8 {
3461                        // CMP Rn, #imm8 (16-bit): 0010 1 Rn imm8
3462                        let instr: u16 = 0x2800 | (rn_bits << 8) | (*imm as u16 & 0xFF);
3463                        Ok(instr.to_le_bytes().to_vec())
3464                    } else {
3465                        self.encode_thumb32_cmp_imm(rn, *imm as u32)
3466                    }
3467                } else if let Operand2::Reg(rm) = op2 {
3468                    let rm_bits = reg_to_bits(rm) as u16;
3469                    if rn_bits < 8 && rm_bits < 8 {
3470                        // CMP Rn, Rm (16-bit low): 0100 0010 10 Rm Rn
3471                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
3472                        Ok(instr.to_le_bytes().to_vec())
3473                    } else {
3474                        // CMP Rn, Rm (16-bit high): 0100 0101 N Rm Rn[2:0]
3475                        let n_bit = (rn_bits >> 3) & 1;
3476                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
3477                        Ok(instr.to_le_bytes().to_vec())
3478                    }
3479                } else {
3480                    let instr: u16 = 0xBF00;
3481                    Ok(instr.to_le_bytes().to_vec())
3482                }
3483            }
3484
3485            // CMN (Compare Negative) - computes Rn + op2 and sets flags
3486            // CMN Rn, #1 sets Z flag if Rn == -1 (since -1 + 1 = 0)
3487            ArmOp::Cmn { rn, op2 } => {
3488                let rn_bits = reg_to_bits(rn) as u16;
3489
3490                if let Operand2::Imm(imm) = op2 {
3491                    // CMN.W Rn, #imm (32-bit): i:imm3:imm8 is a ThumbExpandImm
3492                    // modified immediate (the field sits in imm3=hw2[14:12],
3493                    // imm8=hw2[7:0], i=hw1[10]). Encode it correctly, or error on
3494                    // an un-encodable value — replacing the old silent `0xBF00`
3495                    // NOP (the last of the silent-miscompile data-proc encoders).
3496                    let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3497                        synth_core::Error::synthesis(
3498                            "CMN immediate is not a valid ThumbExpandImm — materialize into a register",
3499                        )
3500                    })?;
3501                    let i_bit = (field >> 11) & 1;
3502                    let imm3 = (field >> 8) & 0x7;
3503                    let imm8 = field & 0xFF;
3504                    let hw1: u16 = (0xF110 | (i_bit << 10) as u16) | rn_bits;
3505                    let hw2: u16 = (imm3 << 12) as u16 | 0x0F00 | imm8 as u16;
3506                    let mut bytes = hw1.to_le_bytes().to_vec();
3507                    bytes.extend_from_slice(&hw2.to_le_bytes());
3508                    Ok(bytes)
3509                } else if let Operand2::Reg(rm) = op2 {
3510                    let rm_bits = reg_to_bits(rm) as u16;
3511                    // 16-bit CMN (T1) only encodes R0-R7; high registers overflow
3512                    // the 3-bit fields and corrupt the operands (#184, the #180
3513                    // class). CMN has no high-register 16-bit form, so fall back
3514                    // to 32-bit CMN.W (T2): EB10 Rn | 0F00 Rm (ADD.W with S=1 and
3515                    // Rd discarded as PC/1111).
3516                    if rn_bits < 8 && rm_bits < 8 {
3517                        // CMN Rn, Rm (16-bit): 0100 0010 11 Rm Rn
3518                        let instr: u16 = 0x42C0 | (rm_bits << 3) | rn_bits;
3519                        Ok(instr.to_le_bytes().to_vec())
3520                    } else {
3521                        let hw1: u16 = 0xEB10 | rn_bits;
3522                        let hw2: u16 = 0x0F00 | rm_bits;
3523                        let mut bytes = hw1.to_le_bytes().to_vec();
3524                        bytes.extend_from_slice(&hw2.to_le_bytes());
3525                        Ok(bytes)
3526                    }
3527                } else {
3528                    Ok(vec![0xBF, 0x00])
3529                }
3530            }
3531
3532            // LDR (can be 16-bit for simple cases)
3533            ArmOp::Ldr { rd, addr } => {
3534                let rd_bits = reg_to_bits(rd);
3535                let base_bits = reg_to_bits(&addr.base);
3536
3537                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3538                if let Some(offset_reg) = &addr.offset_reg {
3539                    let rm_bits = reg_to_bits(offset_reg);
3540
3541                    // If there's also an immediate offset, we need to ADD it first
3542                    if addr.offset != 0 {
3543                        // Use R12 (IP) as scratch to avoid clobbering the address register
3544                        // ADD R12, Rm, #offset; LDR Rd, [base, R12]
3545                        let scratch = Reg::R12;
3546                        let mut bytes =
3547                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3548                        bytes.extend(self.encode_thumb32_ldr_reg(rd, &addr.base, &scratch)?);
3549                        return Ok(bytes);
3550                    }
3551
3552                    // Simple register offset: LDR Rd, [Rn, Rm]
3553                    // 16-bit: only if Rd, Rn, Rm < R8
3554                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3555                        // LDR Rd, [Rn, Rm] (16-bit): 0101 100 Rm Rn Rd
3556                        let instr: u16 = 0x5800
3557                            | ((rm_bits as u16) << 6)
3558                            | ((base_bits as u16) << 3)
3559                            | (rd_bits as u16);
3560                        return Ok(instr.to_le_bytes().to_vec());
3561                    }
3562
3563                    // 32-bit register offset
3564                    return self.encode_thumb32_ldr_reg(rd, &addr.base, offset_reg);
3565                }
3566
3567                // Immediate offset mode [base, #imm]
3568                let offset = addr.offset as u32;
3569
3570                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3571                    // LDR Rd, [Rn, #imm5*4] (16-bit): 0110 1 imm5 Rn Rd
3572                    let imm5 = (offset >> 2) as u16;
3573                    let instr: u16 =
3574                        0x6800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3575                    Ok(instr.to_le_bytes().to_vec())
3576                } else {
3577                    self.encode_thumb32_ldr(rd, &addr.base, offset)
3578                }
3579            }
3580
3581            // STR (can be 16-bit for simple cases)
3582            ArmOp::Str { rd, addr } => {
3583                let rd_bits = reg_to_bits(rd);
3584                let base_bits = reg_to_bits(&addr.base);
3585
3586                // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3587                if let Some(offset_reg) = &addr.offset_reg {
3588                    let rm_bits = reg_to_bits(offset_reg);
3589
3590                    // If there's also an immediate offset, we need to ADD it first
3591                    if addr.offset != 0 {
3592                        // Use R12 (IP) as scratch to avoid clobbering the address register
3593                        // ADD R12, Rm, #offset; STR Rd, [base, R12]
3594                        let scratch = Reg::R12;
3595                        let mut bytes =
3596                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3597                        bytes.extend(self.encode_thumb32_str_reg(rd, &addr.base, &scratch)?);
3598                        return Ok(bytes);
3599                    }
3600
3601                    // Simple register offset: STR Rd, [Rn, Rm]
3602                    // 16-bit: only if Rd, Rn, Rm < R8
3603                    if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3604                        // STR Rd, [Rn, Rm] (16-bit): 0101 000 Rm Rn Rd
3605                        let instr: u16 = 0x5000
3606                            | ((rm_bits as u16) << 6)
3607                            | ((base_bits as u16) << 3)
3608                            | (rd_bits as u16);
3609                        return Ok(instr.to_le_bytes().to_vec());
3610                    }
3611
3612                    // 32-bit register offset
3613                    return self.encode_thumb32_str_reg(rd, &addr.base, offset_reg);
3614                }
3615
3616                // Immediate offset mode [base, #imm]
3617                let offset = addr.offset as u32;
3618
3619                if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3620                    // STR Rd, [Rn, #imm5*4] (16-bit): 0110 0 imm5 Rn Rd
3621                    let imm5 = (offset >> 2) as u16;
3622                    let instr: u16 =
3623                        0x6000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3624                    Ok(instr.to_le_bytes().to_vec())
3625                } else {
3626                    self.encode_thumb32_str(rd, &addr.base, offset)
3627                }
3628            }
3629
3630            // LDRB (Thumb-2)
3631            ArmOp::Ldrb { rd, addr } => {
3632                let rd_bits = reg_to_bits(rd);
3633                let base_bits = reg_to_bits(&addr.base);
3634
3635                if let Some(offset_reg) = &addr.offset_reg {
3636                    if addr.offset != 0 {
3637                        let scratch = Reg::R12;
3638                        let mut bytes =
3639                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3640                        bytes.extend(self.encode_thumb32_ldrb_reg(rd, &addr.base, &scratch)?);
3641                        return Ok(bytes);
3642                    }
3643                    return self.encode_thumb32_ldrb_reg(rd, &addr.base, offset_reg);
3644                }
3645
3646                let offset = addr.offset as u32;
3647                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3648                    // LDRB Rd, [Rn, #imm5] (16-bit): 0111 1 imm5 Rn Rd
3649                    let instr: u16 = 0x7800
3650                        | ((offset as u16) << 6)
3651                        | ((base_bits as u16) << 3)
3652                        | (rd_bits as u16);
3653                    Ok(instr.to_le_bytes().to_vec())
3654                } else {
3655                    self.encode_thumb32_ldrb_imm(rd, &addr.base, offset)
3656                }
3657            }
3658
3659            // LDRSB (Thumb-2)
3660            ArmOp::Ldrsb { rd, addr } => {
3661                let rd_bits = reg_to_bits(rd);
3662                let base_bits = reg_to_bits(&addr.base);
3663
3664                if let Some(offset_reg) = &addr.offset_reg {
3665                    if addr.offset != 0 {
3666                        let scratch = Reg::R12;
3667                        let mut bytes =
3668                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3669                        bytes.extend(self.encode_thumb32_ldrsb_reg(rd, &addr.base, &scratch)?);
3670                        return Ok(bytes);
3671                    }
3672                    return self.encode_thumb32_ldrsb_reg(rd, &addr.base, offset_reg);
3673                }
3674
3675                let offset = addr.offset as u32;
3676                // LDRSB has no 16-bit immediate form (only register)
3677                // For 16-bit reg form: only if Rd, Rn, Rm < R8
3678                if rd_bits < 8 && base_bits < 8 && offset == 0 {
3679                    // No immediate 16-bit encoding for LDRSB; use 32-bit
3680                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3681                } else {
3682                    self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3683                }
3684            }
3685
3686            // LDRH (Thumb-2)
3687            ArmOp::Ldrh { rd, addr } => {
3688                let rd_bits = reg_to_bits(rd);
3689                let base_bits = reg_to_bits(&addr.base);
3690
3691                if let Some(offset_reg) = &addr.offset_reg {
3692                    if addr.offset != 0 {
3693                        let scratch = Reg::R12;
3694                        let mut bytes =
3695                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3696                        bytes.extend(self.encode_thumb32_ldrh_reg(rd, &addr.base, &scratch)?);
3697                        return Ok(bytes);
3698                    }
3699                    return self.encode_thumb32_ldrh_reg(rd, &addr.base, offset_reg);
3700                }
3701
3702                let offset = addr.offset as u32;
3703                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3704                    // LDRH Rd, [Rn, #imm5*2] (16-bit): 1000 1 imm5 Rn Rd
3705                    let imm5 = (offset >> 1) as u16;
3706                    let instr: u16 =
3707                        0x8800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3708                    Ok(instr.to_le_bytes().to_vec())
3709                } else {
3710                    self.encode_thumb32_ldrh_imm(rd, &addr.base, offset)
3711                }
3712            }
3713
3714            // LDRSH (Thumb-2)
3715            ArmOp::Ldrsh { rd, addr } => {
3716                if let Some(offset_reg) = &addr.offset_reg {
3717                    if addr.offset != 0 {
3718                        let scratch = Reg::R12;
3719                        let mut bytes =
3720                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3721                        bytes.extend(self.encode_thumb32_ldrsh_reg(rd, &addr.base, &scratch)?);
3722                        return Ok(bytes);
3723                    }
3724                    return self.encode_thumb32_ldrsh_reg(rd, &addr.base, offset_reg);
3725                }
3726
3727                let offset = addr.offset as u32;
3728                self.encode_thumb32_ldrsh_imm(rd, &addr.base, offset)
3729            }
3730
3731            // STRB (Thumb-2)
3732            ArmOp::Strb { rd, addr } => {
3733                let rd_bits = reg_to_bits(rd);
3734                let base_bits = reg_to_bits(&addr.base);
3735
3736                if let Some(offset_reg) = &addr.offset_reg {
3737                    if addr.offset != 0 {
3738                        let scratch = Reg::R12;
3739                        let mut bytes =
3740                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3741                        bytes.extend(self.encode_thumb32_strb_reg(rd, &addr.base, &scratch)?);
3742                        return Ok(bytes);
3743                    }
3744                    return self.encode_thumb32_strb_reg(rd, &addr.base, offset_reg);
3745                }
3746
3747                let offset = addr.offset as u32;
3748                if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3749                    // STRB Rd, [Rn, #imm5] (16-bit): 0111 0 imm5 Rn Rd
3750                    let instr: u16 = 0x7000
3751                        | ((offset as u16) << 6)
3752                        | ((base_bits as u16) << 3)
3753                        | (rd_bits as u16);
3754                    Ok(instr.to_le_bytes().to_vec())
3755                } else {
3756                    self.encode_thumb32_strb_imm(rd, &addr.base, offset)
3757                }
3758            }
3759
3760            // STRH (Thumb-2)
3761            ArmOp::Strh { rd, addr } => {
3762                let rd_bits = reg_to_bits(rd);
3763                let base_bits = reg_to_bits(&addr.base);
3764
3765                if let Some(offset_reg) = &addr.offset_reg {
3766                    if addr.offset != 0 {
3767                        let scratch = Reg::R12;
3768                        let mut bytes =
3769                            self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3770                        bytes.extend(self.encode_thumb32_strh_reg(rd, &addr.base, &scratch)?);
3771                        return Ok(bytes);
3772                    }
3773                    return self.encode_thumb32_strh_reg(rd, &addr.base, offset_reg);
3774                }
3775
3776                let offset = addr.offset as u32;
3777                if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3778                    // STRH Rd, [Rn, #imm5*2] (16-bit): 1000 0 imm5 Rn Rd
3779                    let imm5 = (offset >> 1) as u16;
3780                    let instr: u16 =
3781                        0x8000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3782                    Ok(instr.to_le_bytes().to_vec())
3783                } else {
3784                    self.encode_thumb32_strh_imm(rd, &addr.base, offset)
3785                }
3786            }
3787
3788            // MemorySize (Thumb-2)
3789            ArmOp::MemorySize { rd } => {
3790                // LSR rd, R10, #16 — memory size in bytes / 65536 = pages
3791                // Thumb-2 16-bit: LSRS Rd, Rm, #imm5 — 0000 1 imm5 Rm Rd
3792                let rd_bits = reg_to_bits(rd);
3793                let r10_bits = reg_to_bits(&Reg::R10);
3794                if rd_bits < 8 && r10_bits < 8 {
3795                    let instr: u16 =
3796                        0x0800 | (16u16 << 6) | ((r10_bits as u16) << 3) | (rd_bits as u16);
3797                    Ok(instr.to_le_bytes().to_vec())
3798                } else {
3799                    // Thumb-2 32-bit LSR: 1110 1010 010 0 1111 | 0 imm3 Rd imm2 01 Rm
3800                    let imm5: u32 = 16;
3801                    let imm3 = (imm5 >> 2) & 0x7;
3802                    let imm2 = imm5 & 0x3;
3803                    let hw1: u16 = 0xEA4F;
3804                    let hw2: u16 =
3805                        ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | 0x10 | r10_bits) as u16;
3806                    let mut bytes = hw1.to_le_bytes().to_vec();
3807                    bytes.extend_from_slice(&hw2.to_le_bytes());
3808                    Ok(bytes)
3809                }
3810            }
3811
3812            // MemoryGrow (Thumb-2)
3813            ArmOp::MemoryGrow { rd, .. } => {
3814                // On embedded with fixed memory, always return -1 (failure)
3815                // MVN rd, #0 → MOV rd, #-1
3816                // Thumb-2 32-bit: MVN: 1111 0 i 0 0 0 1 1 0 1111 | 0 imm3 Rd imm8
3817                let rd_bits = reg_to_bits(rd);
3818                let hw1: u16 = 0xF06F; // MVN with i=0
3819                let hw2: u16 = (rd_bits << 8) as u16; // imm8=0 → ~0 = 0xFFFFFFFF = -1
3820                let mut bytes = hw1.to_le_bytes().to_vec();
3821                bytes.extend_from_slice(&hw2.to_le_bytes());
3822                Ok(bytes)
3823            }
3824
3825            // BX (16-bit)
3826            ArmOp::Bx { rm } => {
3827                let rm_bits = reg_to_bits(rm) as u16;
3828                // BX Rm (16-bit): 0100 0111 0 Rm 000
3829                let instr: u16 = 0x4700 | (rm_bits << 3);
3830                Ok(instr.to_le_bytes().to_vec())
3831            }
3832
3833            // BLX (16-bit) - Branch with Link and Exchange
3834            // BLX Rm: 0100 0111 1 Rm 000
3835            ArmOp::Blx { rm } => {
3836                let rm_bits = reg_to_bits(rm) as u16;
3837                let instr: u16 = 0x4780 | (rm_bits << 3);
3838                Ok(instr.to_le_bytes().to_vec())
3839            }
3840
3841            // CallIndirect - indirect function call via table lookup
3842            // table_index_reg contains the table index
3843            // Generates (#642): MOVW ip,#size [; MOVT]; CMP idx,ip; BLO +1;
3844            //                   UDF #0; LSL R12,idx,#2; LDR R12,[R11,R12]; BLX R12
3845            // #650, table_byte_offset != 0 (a non-zero table in the contiguous
3846            // R11 region): the pointer load becomes
3847            //                   ADD R12,R11,R12; LDR R12,[R12,#offset]
3848            // #664, null_check (the table has null slots, linked as ZERO
3849            // words): the loaded pointer is null-checked before the BLX —
3850            //                   CMP.W R12,#0; BNE +1; UDF #0
3851            // #676, type_check (heterogeneous table — runtime §4.4.8 type
3852            // check against the type-id sidecar at R11+off): after the
3853            // bounds guard —
3854            //                   LSL R12,idx,#2; ADD R12,R11,R12;
3855            //                   LDR R12,[R12,#type_off]; CMP.W R12,#id;
3856            //                   BEQ +1; UDF #0
3857            // (the dispatch tail then recomputes idx*4 — idx stays live).
3858            ArmOp::CallIndirect {
3859                rd: _,
3860                type_idx: _,
3861                table_index_reg,
3862                table_size,
3863                table_byte_offset,
3864                null_check,
3865                type_check,
3866            } => {
3867                let idx_reg = reg_to_bits(table_index_reg);
3868                let mut bytes = Vec::new();
3869
3870                // The expansion:
3871                // 1. Bounds guard (#642): trap (UDF #0, WASM Core §4.4.8) when
3872                //    index >= table size. Without it an out-of-bounds index
3873                //    reads past the table and BLXes whatever word lies there —
3874                //    an uncontrolled indirect branch instead of a trap.
3875                // 2. Multiplies index by 4 (function pointer size)
3876                // 3. Loads function pointer from table (table base in R11)
3877                // 4. Calls the function via BLX
3878                //
3879                // Table base setup must be done by caller/runtime. The type
3880                // check §4.4.8 also requires is discharged at COMPILE time:
3881                // the selector only emits this op after verifying the closed-
3882                // world property that every table entry's signature equals the
3883                // expected type (the raw code-pointer table carries no runtime
3884                // type ids to compare) — see the #642 selector guard.
3885
3886                // MOVW R12, #(size & 0xFFFF) — Thumb-2 T3:
3887                // 11110 i 100100 imm4 | 0 imm3 Rd imm8 (Rd=R12).
3888                let size_lo = *table_size & 0xFFFF;
3889                let hw1: u16 =
3890                    (0xF240 | (((size_lo >> 11) & 1) << 10) | ((size_lo >> 12) & 0xF)) as u16;
3891                let hw2: u16 =
3892                    ((((size_lo >> 8) & 0x7) << 12) | (12 << 8) | (size_lo & 0xFF)) as u16;
3893                bytes.extend_from_slice(&hw1.to_le_bytes());
3894                bytes.extend_from_slice(&hw2.to_le_bytes());
3895                // MOVT R12, #(size >> 16) — only when the table size exceeds
3896                // 16 bits (never in practice, but the guard must not compare
3897                // against a truncated size).
3898                let size_hi = *table_size >> 16;
3899                if size_hi != 0 {
3900                    let hw1: u16 =
3901                        (0xF2C0 | (((size_hi >> 11) & 1) << 10) | ((size_hi >> 12) & 0xF)) as u16;
3902                    let hw2: u16 =
3903                        ((((size_hi >> 8) & 0x7) << 12) | (12 << 8) | (size_hi & 0xFF)) as u16;
3904                    bytes.extend_from_slice(&hw1.to_le_bytes());
3905                    bytes.extend_from_slice(&hw2.to_le_bytes());
3906                }
3907                // CMP idx, R12 — 16-bit T2 (high-register capable):
3908                // 010001 01 N Rm(4) Rn(3), Rn full = N:Rn3.
3909                let cmp: u16 = (0x4500 | ((idx_reg & 8) << 4) | (12 << 3) | (idx_reg & 7)) as u16;
3910                bytes.extend_from_slice(&cmp.to_le_bytes());
3911                // BLO +1 insn (skip the UDF when index < size) — B<cond>.N
3912                // imm8=0: target = branch + 4. LO = unsigned lower.
3913                bytes.extend_from_slice(&0xD300u16.to_le_bytes());
3914                // UDF #0 — call_indirect out-of-bounds trap (same trap idiom as
3915                // the div-by-zero guards).
3916                bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3917
3918                // #676: runtime type check — ONLY for a heterogeneous table
3919                // (mixed signatures, closed-world verdict impossible). Load
3920                // the indexed slot's structural class id from the type-id
3921                // sidecar (`R11 + type_off + idx*4`; `type_off` = sidecar
3922                // base + this table's base offset, a compile-time constant)
3923                // and compare it against the expected type's class id — a
3924                // mismatch is the WASM Core §4.4.8 type trap. Null slots
3925                // carry the reserved id 0, so this compare subsumes the
3926                // #664 null trap (the selector passes `null_check: false`).
3927                // `None` emits NOTHING: every homogeneous table keeps the
3928                // pre-#676 bytes identical BY CONSTRUCTION. R12 stays the
3929                // only scratch (#212); the dispatch tail below recomputes
3930                // idx*4 — the index register is never clobbered here.
3931                if let Some((expected_id, type_off)) = type_check {
3932                    debug_assert!(*expected_id <= 255, "selector enforces the CMP imm8 range");
3933                    debug_assert!(*type_off <= 4095, "selector enforces the LDR imm12 range");
3934                    // MOV.W R12, idx, LSL #2 (same encoding as the dispatch
3935                    // tail's index scale below).
3936                    bytes.extend_from_slice(&0xEA4Fu16.to_le_bytes());
3937                    bytes.extend_from_slice(
3938                        &(((0x0C00 | (0b10 << 6)) | idx_reg) as u16).to_le_bytes(),
3939                    );
3940                    // ADD.W R12, R11, R12 (the #650 base-add form).
3941                    bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
3942                    bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
3943                    // LDR.W R12, [R12, #type_off] — T3 LDR (immediate):
3944                    // 1111 1000 1101 Rn=1100 | Rt=1100 imm12.
3945                    bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
3946                    bytes.extend_from_slice(
3947                        &(0xC000u16 | (*type_off as u16 & 0x0FFF)).to_le_bytes(),
3948                    );
3949                    // CMP.W R12, #expected_id — T2 CMP (immediate), imm8
3950                    // (same form as the #664 null check's CMP.W R12, #0).
3951                    bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
3952                    bytes.extend_from_slice(
3953                        &(0x0F00u16 | (*expected_id as u16 & 0xFF)).to_le_bytes(),
3954                    );
3955                    // BEQ +1 insn (skip the UDF when the class id matches) —
3956                    // B<cond>.N imm8=0: target = branch + 4. EQ.
3957                    bytes.extend_from_slice(&0xD000u16.to_le_bytes());
3958                    // UDF #0 — the §4.4.8 type-mismatch trap (same idiom as
3959                    // the bounds guard above).
3960                    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3961                }
3962
3963                // LSL R12, idx_reg, #2 (multiply index by 4)
3964                // Thumb-2 MOV with shift: 11101010 010 S 1111 | 0 imm3 Rd imm2 type Rm
3965                // LSL: type=00 (bits 5:4), imm5=2 -> imm3=000, imm2=10 (bits 7:6)
3966                // #597: the shift amount was previously shifted into bits 5:4 —
3967                // the TYPE field — encoding `mov.w ip, rm, ASR #32`, which
3968                // destroyed the index and dispatched table entry 0 for every
3969                // call. imm2 lives at bits 7:6.
3970                let hw1: u16 = 0xEA4F_u16; // MOV.W R12, Rm, LSL #2
3971                let hw2: u16 = ((0x0C00 | (0b10 << 6)) | idx_reg) as u16;
3972                bytes.extend_from_slice(&hw1.to_le_bytes());
3973                bytes.extend_from_slice(&hw2.to_le_bytes());
3974
3975                if *table_byte_offset == 0 {
3976                    // Table 0 (base = R11 itself): the pre-#650 single-load
3977                    // form — a single-table module's bytes stay identical BY
3978                    // CONSTRUCTION.
3979                    // LDR R12, [R11, R12] - load function pointer
3980                    // Thumb-2 LDR (register): 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
3981                    // Rn=R11, Rt=R12, Rm=R12, imm2=00 (no shift)
3982                    let ldr_hw1: u16 = 0xF85B; // LDR.W Rt, [R11, Rm]
3983                    let ldr_hw2: u16 = 0xC00C; // Rt=R12, imm2=00, Rm=R12
3984                    bytes.extend_from_slice(&ldr_hw1.to_le_bytes());
3985                    bytes.extend_from_slice(&ldr_hw2.to_le_bytes());
3986                } else {
3987                    // #650: table N of the contiguous R11 region — fold the
3988                    // compile-time base offset into the pointer load via the
3989                    // LDR imm12 form (R12 stays the only scratch, per the
3990                    // #212 convention).
3991                    assert!(
3992                        *table_byte_offset <= 4095,
3993                        "call_indirect table base offset {table_byte_offset} exceeds \
3994                         LDR imm12 — the selector must have declined this (#650)"
3995                    );
3996                    // ADD.W R12, R11, R12 — T3 ADD (register):
3997                    // 11101011000 S=0 Rn=1011 | 0 imm3=000 Rd=1100 imm2=00 type=00 Rm=1100
3998                    bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
3999                    bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
4000                    // LDR.W R12, [R12, #offset] — T3 LDR (immediate):
4001                    // 1111 1000 1101 Rn=1100 | Rt=1100 imm12
4002                    bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
4003                    bytes.extend_from_slice(
4004                        &((0xC000u16) | (*table_byte_offset as u16 & 0x0FFF)).to_le_bytes(),
4005                    );
4006                }
4007
4008                // #664: null-slot trap — ONLY when the table image carries
4009                // null (uninitialized) slots, which the layout contract
4010                // requires to be linked as ZERO words. A fully-initialized
4011                // table skips this branch entirely, keeping the pre-#664
4012                // expansion byte-identical BY CONSTRUCTION (the #650
4013                // offset-0 trick).
4014                if *null_check {
4015                    // CMP.W R12, #0 — T2 CMP (immediate): 11110 i 0 1101 1
4016                    // Rn(4) | 0 imm3 1111 imm8, Rn=R12, imm=0.
4017                    bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
4018                    bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
4019                    // BNE +1 insn (skip the UDF when the pointer is non-null)
4020                    // — B<cond>.N imm8=0: target = branch + 4. NE.
4021                    bytes.extend_from_slice(&0xD100u16.to_le_bytes());
4022                    // UDF #0 — call_indirect null-funcref trap (WASM Core
4023                    // §4.4.8: calling an uninitialized element traps; same
4024                    // trap idiom as the bounds guard above).
4025                    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
4026                }
4027
4028                // BLX R12 (call function indirectly)
4029                // BLX Rm (16-bit): 0100 0111 1 Rm 000
4030                let blx: u16 = 0x47E0; // BLX R12
4031                bytes.extend_from_slice(&blx.to_le_bytes());
4032
4033                Ok(bytes)
4034            }
4035
4036            // Label pseudo-instruction: emits no machine code
4037            ArmOp::Label { .. } => Ok(Vec::new()),
4038
4039            // Conditional branch to label (generic) - offset 0, will be patched
4040            ArmOp::Bcc { cond, label: _ } => {
4041                use synth_synthesis::Condition;
4042                let cond_bits: u16 = match cond {
4043                    Condition::EQ => 0x0,
4044                    Condition::NE => 0x1,
4045                    Condition::HS => 0x2,
4046                    Condition::LO => 0x3,
4047                    Condition::HI => 0x8,
4048                    Condition::LS => 0x9,
4049                    Condition::GE => 0xA,
4050                    Condition::LT => 0xB,
4051                    Condition::GT => 0xC,
4052                    Condition::LE => 0xD,
4053                };
4054                // 16-bit B<cond> with offset 0: 1101 cond imm8
4055                let instr: u16 = 0xD000 | (cond_bits << 8);
4056                Ok(instr.to_le_bytes().to_vec())
4057            }
4058
4059            // Branch instructions
4060            ArmOp::B { label: _ } => {
4061                // Simplified: B.N with offset 0
4062                // For real usage, would need label resolution
4063                let instr: u16 = 0xE000; // B.N #0
4064                Ok(instr.to_le_bytes().to_vec())
4065            }
4066
4067            // BHS (Branch if Higher or Same) - used for bounds checking
4068            // Condition code: 0x2 (C set)
4069            ArmOp::Bhs { label: _ } => {
4070                // 16-bit B<cond> with offset 0: 1101 cond imm8
4071                // cond = 0x2 (HS)
4072                let instr: u16 = 0xD200; // BHS.N #0
4073                Ok(instr.to_le_bytes().to_vec())
4074            }
4075
4076            // BLO (Branch if Lower) - complementary to BHS
4077            // Condition code: 0x3 (C clear)
4078            ArmOp::Blo { label: _ } => {
4079                // 16-bit B<cond> with offset 0: 1101 cond imm8
4080                // cond = 0x3 (LO)
4081                let instr: u16 = 0xD300; // BLO.N #0
4082                Ok(instr.to_le_bytes().to_vec())
4083            }
4084
4085            // Branch with numeric offset (Thumb-2)
4086            // Thumb-2 B.W instruction: 32-bit with +-16MB range
4087            ArmOp::BOffset { offset } => {
4088                // offset is already the halfword displacement: (target - branch - 4) / 2
4089                // This is the raw encoded value, accounting for variable-length instructions
4090                let halfword_offset = *offset;
4091
4092                // 16-bit B.N encoding: 1110 0 imm11 (11-bit signed halfword offset)
4093                // Range: -1024 to +1022 halfwords
4094                if (-1024..=1022).contains(&halfword_offset) {
4095                    // 16-bit B.N encoding: 1110 0 imm11
4096                    let imm11 = (halfword_offset as u16) & 0x7FF;
4097                    let instr: u16 = 0xE000 | imm11;
4098                    Ok(instr.to_le_bytes().to_vec())
4099                } else {
4100                    // 32-bit B.W encoding for larger offsets
4101                    // First halfword: 1111 0 S imm10
4102                    // Second halfword: 10 J1 0 J2 imm11
4103                    // Total offset = SignExtend(S:I1:I2:imm10:imm11:0)
4104                    // where I1 = NOT(J1 XOR S), I2 = NOT(J2 XOR S)
4105
4106                    // The B.W (T4) encoding packs the signed offset as:
4107                    //   S:I1:I2:imm10:imm11:0  (25-bit signed, halfword-aligned)
4108                    // where J1 = NOT(I1 XOR S), J2 = NOT(I2 XOR S)
4109                    // Input halfword_offset already equals (target - PC - 4) / 2,
4110                    // so the full byte offset = halfword_offset << 1.
4111                    // The encoding fields split that 25-bit signed value (including the
4112                    // implicit trailing zero) as: S | imm10 | imm11
4113                    // with I1 = bit 23 and I2 = bit 22 of the signed offset.
4114                    let signed_offset = halfword_offset << 1; // byte offset
4115                    let s = if signed_offset < 0 { 1u32 } else { 0u32 };
4116                    let uoffset = signed_offset as u32;
4117                    let imm10 = (uoffset >> 12) & 0x3FF; // bits [21:12]
4118                    let imm11 = (uoffset >> 1) & 0x7FF; // bits [11:1]
4119                    let i1 = (uoffset >> 23) & 1; // bit 23
4120                    let i2 = (uoffset >> 22) & 1; // bit 22
4121                    let j1 = (!(i1 ^ s)) & 1; // J1 = NOT(I1 XOR S)
4122                    let j2 = (!(i2 ^ s)) & 1; // J2 = NOT(I2 XOR S)
4123
4124                    let hw1: u16 = (0xF000 | (s << 10) | imm10) as u16;
4125                    let hw2: u16 = (0x9000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4126
4127                    let mut bytes = hw1.to_le_bytes().to_vec();
4128                    bytes.extend_from_slice(&hw2.to_le_bytes());
4129                    Ok(bytes)
4130                }
4131            }
4132
4133            // Conditional branch with numeric offset (Thumb-2)
4134            ArmOp::BCondOffset { cond, offset } => {
4135                use synth_synthesis::Condition;
4136                let cond_bits: u16 = match cond {
4137                    Condition::EQ => 0x0,
4138                    Condition::NE => 0x1,
4139                    Condition::HS => 0x2,
4140                    Condition::LO => 0x3,
4141                    Condition::HI => 0x8,
4142                    Condition::LS => 0x9,
4143                    Condition::GE => 0xA,
4144                    Condition::LT => 0xB,
4145                    Condition::GT => 0xC,
4146                    Condition::LE => 0xD,
4147                };
4148
4149                // offset is already the halfword displacement: (target - branch - 4) / 2
4150                // This is the raw imm8 value for 16-bit B<cond> encoding
4151                let halfword_offset = *offset;
4152
4153                // 16-bit B<cond> encoding: 1101 cond imm8
4154                // Range: -256 to +254 halfwords (imm8 is sign-extended and shifted left 1)
4155                if (-128..=127).contains(&halfword_offset) {
4156                    let imm8 = (halfword_offset as u16) & 0xFF;
4157                    let instr: u16 = 0xD000 | (cond_bits << 8) | imm8;
4158                    Ok(instr.to_le_bytes().to_vec())
4159                } else {
4160                    // 32-bit B<cond>.W (encoding T3) for larger offsets
4161                    // First halfword: 1111 0 S cond(4) imm6
4162                    // Second halfword: 10 J1 0 J2 imm11
4163                    //
4164                    // Per ARMv7-M, the branch BYTE offset is
4165                    // SignExtend(S:J2:J1:imm6:imm11:'0'), i.e. the field value
4166                    // S:J2:J1:imm6:imm11 IS the signed 20-bit HALFWORD offset —
4167                    // imm11/imm6/J1/J2/S take `halfword_offset` bits [10:0],
4168                    // [16:11], 17, 18 and 19 directly (mirroring the T4
4169                    // unconditional arm above).
4170                    //
4171                    // #740: this arm previously packed `halfword_offset >> 1`
4172                    // into imm6:imm11 — HALVING the displacement — so every
4173                    // wide conditional branch (span > 254 bytes) landed at half
4174                    // its intended offset: gust_poll's loop-head `br_if` to an
4175                    // outer block end jumped mid-shape. Narrow (16-bit) B<cond>
4176                    // encodings were unaffected, which is why short-range CF
4177                    // fixtures never caught it.
4178                    if !(-(1 << 19)..(1 << 19)).contains(&halfword_offset) {
4179                        return Err(synth_core::Error::synthesis(format!(
4180                            "B<cond>.W (T3) halfword offset {halfword_offset} exceeds \
4181                             the signed 20-bit encoding range (±1 MB) — refusing to \
4182                             emit a truncated branch"
4183                        )));
4184                    }
4185                    let u = halfword_offset as u32;
4186                    let imm11 = u & 0x7FF; // halfword offset bits [10:0]
4187                    let imm6 = (u >> 11) & 0x3F; // bits [16:11]
4188                    let j1 = (u >> 17) & 1; // bit 17
4189                    let j2 = (u >> 18) & 1; // bit 18
4190                    let s = (u >> 19) & 1; // sign (range-checked above)
4191
4192                    let hw1: u16 = (0xF000 | (s << 10) | ((cond_bits as u32) << 6) | imm6) as u16;
4193                    let hw2: u16 = (0x8000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4194
4195                    let mut bytes = hw1.to_le_bytes().to_vec();
4196                    bytes.extend_from_slice(&hw2.to_le_bytes());
4197                    Ok(bytes)
4198                }
4199            }
4200
4201            ArmOp::Bl { label: _ } => {
4202                // BL is always 32-bit in Thumb-2, encoded here as a relocatable
4203                // placeholder; an R_ARM_THM_CALL relocation patches the target
4204                // (see arm_backend.rs). The placeholder must carry an embedded
4205                // addend of -4 so the relocation nets to exactly the symbol S.
4206                //
4207                // Thumb BL computes `target = (P + 4) + signed_offset`. Under
4208                // R_ARM_THM_CALL the linker resolves using the in-place addend;
4209                // a 0xF800 placeholder (addend 0) lands at S+4 — every call one
4210                // instruction past the callee entry (#174). The correct
4211                // placeholder is what `gas` emits for `bl <extern>`:
4212                //   f7ff fffe  ->  `bl <self>`  (S=1, J1=J2=1, imm = -4 addend),
4213                // i.e. hw1=0xF7FF, hw2=0xFFFE. This nets to S, not S+4.
4214                // (The earlier 0xD000 was worse still — a ~+0x600000 addend,
4215                // the garbage `bl c0000c` and "truncated to fit" of #167.)
4216                let hw1: u16 = 0xF7FF;
4217                let hw2: u16 = 0xFFFE;
4218                let mut bytes = hw1.to_le_bytes().to_vec();
4219                bytes.extend_from_slice(&hw2.to_le_bytes());
4220                Ok(bytes)
4221            }
4222
4223            // MVN
4224            ArmOp::Mvn { rd, op2 } => {
4225                if let Operand2::Reg(rm) = op2 {
4226                    let rd_bits = reg_to_bits(rd) as u16;
4227                    let rm_bits = reg_to_bits(rm) as u16;
4228
4229                    if rd_bits < 8 && rm_bits < 8 {
4230                        // MVNS Rd, Rm (16-bit): 0100 0011 11 Rm Rd
4231                        let instr: u16 = 0x43C0 | (rm_bits << 3) | rd_bits;
4232                        Ok(instr.to_le_bytes().to_vec())
4233                    } else {
4234                        // 32-bit MVN
4235                        let hw1: u16 = 0xEA6F_u16;
4236                        let hw2: u16 = ((reg_to_bits(rd) << 8) | reg_to_bits(rm)) as u16;
4237                        let mut bytes = hw1.to_le_bytes().to_vec();
4238                        bytes.extend_from_slice(&hw2.to_le_bytes());
4239                        Ok(bytes)
4240                    }
4241                } else {
4242                    let instr: u16 = 0xBF00;
4243                    Ok(instr.to_le_bytes().to_vec())
4244                }
4245            }
4246
4247            // MOVW - Move Wide (Thumb-2 32-bit)
4248            ArmOp::Movw { rd, imm16 } => {
4249                self.encode_thumb32_movw_raw(reg_to_bits(rd), *imm16 as u32)
4250            }
4251
4252            // MOVT - Move Top (Thumb-2 32-bit)
4253            ArmOp::Movt { rd, imm16 } => {
4254                self.encode_thumb32_movt_raw(reg_to_bits(rd), *imm16 as u32)
4255            }
4256
4257            // #237: symbol-relative MOVW/MOVT. Encode the addend's low/high 16
4258            // bits in place; the backend records an R_ARM_MOVW_ABS_NC /
4259            // R_ARM_MOVT_ABS relocation against `symbol`, so the linker adds the
4260            // symbol's final address to the in-place addend (REL semantics).
4261            ArmOp::MovwSym { rd, addend, .. } => {
4262                self.encode_thumb32_movw_raw(reg_to_bits(rd), (*addend as u32) & 0xffff)
4263            }
4264            ArmOp::MovtSym { rd, addend, .. } => {
4265                self.encode_thumb32_movt_raw(reg_to_bits(rd), ((*addend as u32) >> 16) & 0xffff)
4266            }
4267
4268            // #345: literal-pool address load — emit a PLACEHOLDER `LDR.W rd,
4269            // [pc, #0]` (U=1, imm12=0). The backend (arm_backend.rs) places the
4270            // 4-byte pool word at the end of the function, records the R_ARM_ABS32
4271            // relocation against `symbol+addend`, and patches the imm12 with the
4272            // real PC-relative distance once the pool offset is known.
4273            // Encoding T2: 1111 1000 1101 1111 | Rt(4) imm12(12), with the literal
4274            // base = Align(PC,4) and PC = address of this instruction + 4.
4275            ArmOp::LdrSym { rd, .. } => {
4276                let rt = reg_to_bits(rd) as u16;
4277                let hw1: u16 = 0xF8DF; // LDR.W (literal), U=1
4278                let hw2: u16 = rt << 12; // imm12 = 0 placeholder
4279                let mut bytes = Vec::with_capacity(4);
4280                bytes.extend_from_slice(&hw1.to_le_bytes());
4281                bytes.extend_from_slice(&hw2.to_le_bytes());
4282                Ok(bytes)
4283            }
4284
4285            // SetCond: Materialize condition flag into register (0 or 1)
4286            // Strategy: ITE <cond>; MOV Rd, #1; MOV Rd, #0
4287            // IMPORTANT: Must use ITE (If-Then-Else) because 16-bit Thumb MOV
4288            // always sets flags (MOVS). We need to evaluate the condition BEFORE
4289            // any MOV instruction clobbers the flags from CMP.
4290            ArmOp::SetCond { rd, cond } => {
4291                let rd_bits = reg_to_bits(rd) as u16;
4292
4293                // Condition code encoding for IT block
4294                use synth_synthesis::Condition;
4295                let cond_bits: u16 = match cond {
4296                    Condition::EQ => 0x0,
4297                    Condition::NE => 0x1,
4298                    Condition::LT => 0xB,
4299                    Condition::LE => 0xD,
4300                    Condition::GT => 0xC,
4301                    Condition::GE => 0xA,
4302                    Condition::LO => 0x3, // CC/LO (unsigned <)
4303                    Condition::LS => 0x9, // LS (unsigned <=)
4304                    Condition::HI => 0x8, // HI (unsigned >)
4305                    Condition::HS => 0x2, // CS/HS (unsigned >=)
4306                };
4307
4308                // ITE <cond>: encodes If-Then-Else block
4309                // The mask field depends on firstcond[0]:
4310                // - If firstcond[0] = 0: mask = 0xC for TE pattern (ITE EQ = BF0C)
4311                // - If firstcond[0] = 1: mask = 0x4 for TE pattern (ITE NE = BF14)
4312                let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4313                let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4314
4315                // Materialize 0/1 into Rd. The 16-bit MOVS (T1) encodes Rd in a
4316                // 3-bit field (bits[10:8]) — only R0–R7. For a high register
4317                // (R8–R12) `rd_bits << 8` overflows into bit 11 and silently
4318                // turns MOVS into CMP (00100 → 00101), corrupting the result
4319                // (this mis-materialized gale's `has_waiter`, so its `local.set`
4320                // stored a stale register → the binary-sem WAKE dispatch read
4321                // garbage). Use the 32-bit MOV.W (T2) for high registers, which
4322                // has a 4-bit Rd field. MOV.W with S=0 doesn't set flags, which
4323                // is fine inside the ITE (the materialized value is the result;
4324                // the flags are not consumed afterwards).
4325                let mut bytes = ite_instr.to_le_bytes().to_vec();
4326                let push_mov = |bytes: &mut Vec<u8>, imm: u16| {
4327                    if rd_bits <= 7 {
4328                        let m: u16 = 0x2000 | (rd_bits << 8) | imm; // 16-bit MOVS Rd,#imm
4329                        bytes.extend_from_slice(&m.to_le_bytes());
4330                    } else {
4331                        // 32-bit MOV.W Rd, #imm (T2): F04F | (Rd<<8) | imm8
4332                        let hw1: u16 = 0xF04F;
4333                        let hw2: u16 = (rd_bits << 8) | imm;
4334                        bytes.extend_from_slice(&hw1.to_le_bytes());
4335                        bytes.extend_from_slice(&hw2.to_le_bytes());
4336                    }
4337                };
4338                push_mov(&mut bytes, 1); // Then branch (condition true)  → 1
4339                push_mov(&mut bytes, 0); // Else branch (condition false) → 0
4340                Ok(bytes)
4341            }
4342
4343            // I64SetCond: Compare two i64 register pairs, result 0/1 in rd
4344            // EQ/NE: CMP lo,lo; IT EQ; CMPEQ hi,hi; ITE <cond>; MOV 1; MOV 0
4345            // LT: CMP lo,lo; SBCS rd,hi,hi; ITE LT; MOV 1; MOV 0
4346            // GT: CMP lo,lo (swapped); SBCS rd,hi,hi (swapped); ITE LT; MOV 1; MOV 0
4347            ArmOp::I64SetCond {
4348                rd,
4349                rn_lo,
4350                rn_hi,
4351                rm_lo,
4352                rm_hi,
4353                cond,
4354            } => {
4355                use synth_synthesis::Condition;
4356                let rd_bits = reg_to_bits(rd) as u16;
4357                let mut bytes = Vec::new();
4358
4359                // Helper: encode CMP Rn, Rm (16-bit)
4360                let encode_cmp_reg = |rn: &synth_synthesis::Reg,
4361                                      rm: &synth_synthesis::Reg|
4362                 -> Vec<u8> {
4363                    let rn_bits = reg_to_bits(rn) as u16;
4364                    let rm_bits = reg_to_bits(rm) as u16;
4365                    if rn_bits < 8 && rm_bits < 8 {
4366                        let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
4367                        instr.to_le_bytes().to_vec()
4368                    } else {
4369                        let n_bit = (rn_bits >> 3) & 1;
4370                        let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
4371                        instr.to_le_bytes().to_vec()
4372                    }
4373                };
4374
4375                // Helper: encode ITE <cond> (2 bytes)
4376                let encode_ite = |cond_bits: u16| -> Vec<u8> {
4377                    let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4378                    let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4379                    ite_instr.to_le_bytes().to_vec()
4380                };
4381
4382                // Helper: encode SetCond (ITE + MOV #1 + MOV #0) for given condition
4383                let encode_setcond = |cond_bits: u16, rd_bits: u16| -> Vec<u8> {
4384                    let mut b = encode_ite(cond_bits);
4385                    if rd_bits < 8 {
4386                        let mov_one: u16 = 0x2001 | (rd_bits << 8);
4387                        let mov_zero: u16 = 0x2000 | (rd_bits << 8);
4388                        b.extend_from_slice(&mov_one.to_le_bytes());
4389                        b.extend_from_slice(&mov_zero.to_le_bytes());
4390                    } else {
4391                        // #311: rd >= R8 — the 16-bit MOV imm8 form has a 3-bit
4392                        // rd field; rd_bits<<8 overflows into bit 11 and
4393                        // TRANSMUTES the MOV into CMP (0x2001|0x0800 = 0x2801 =
4394                        // CMP r0,#1): the boolean dies in the flags and the
4395                        // consumer reads a stale register. Use the 32-bit
4396                        // MOV.W (T2: F04F 0000|rd<<8|imm8) — IT-legal,
4397                        // flag-preserving. Same class as H-CODE-9 / #180.
4398                        for imm in [1u16, 0u16] {
4399                            let hw1: u16 = 0xF04F;
4400                            let hw2: u16 = (rd_bits << 8) | imm;
4401                            b.extend_from_slice(&hw1.to_le_bytes());
4402                            b.extend_from_slice(&hw2.to_le_bytes());
4403                        }
4404                    }
4405                    b
4406                };
4407
4408                match cond {
4409                    Condition::EQ | Condition::NE => {
4410                        // CMP rn_lo, rm_lo (compare low words)
4411                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4412
4413                        // IT EQ (execute next instruction only if Z=1)
4414                        let it_eq: u16 = 0xBF08; // IT EQ: cond=0000, mask=1000
4415                        bytes.extend_from_slice(&it_eq.to_le_bytes());
4416
4417                        // CMPEQ rn_hi, rm_hi (compare high words, only if low equal)
4418                        bytes.extend_from_slice(&encode_cmp_reg(rn_hi, rm_hi));
4419
4420                        // ITE <cond>; MOV rd, #1; MOV rd, #0
4421                        let cond_bits: u16 = match cond {
4422                            Condition::EQ => 0x0,
4423                            Condition::NE => 0x1,
4424                            _ => unreachable!(),
4425                        };
4426                        bytes.extend_from_slice(&encode_setcond(cond_bits, rd_bits));
4427                    }
4428
4429                    Condition::LT => {
4430                        // CMP rn_lo, rm_lo (sets C flag for borrow)
4431                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4432
4433                        // SBCS rd, rn_hi, rm_hi (subtract with carry, sets N,V flags)
4434                        // SBCS.W Rd, Rn, Rm: EB70 Rn | 0000 Rd 0000 Rm
4435                        let rn_hi_bits = reg_to_bits(rn_hi);
4436                        let rm_hi_bits = reg_to_bits(rm_hi);
4437                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4438                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4439                        bytes.extend_from_slice(&hw1.to_le_bytes());
4440                        bytes.extend_from_slice(&hw2.to_le_bytes());
4441
4442                        // ITE LT; MOV rd, #1; MOV rd, #0
4443                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4444                    }
4445
4446                    Condition::GT => {
4447                        // GT(a,b) = LT(b,a): swap operands
4448                        // CMP rm_lo, rn_lo (swapped)
4449                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4450
4451                        // SBCS rd, rm_hi, rn_hi (swapped)
4452                        let rm_hi_bits = reg_to_bits(rm_hi);
4453                        let rn_hi_bits = reg_to_bits(rn_hi);
4454                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4455                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4456                        bytes.extend_from_slice(&hw1.to_le_bytes());
4457                        bytes.extend_from_slice(&hw2.to_le_bytes());
4458
4459                        // ITE LT; MOV rd, #1; MOV rd, #0
4460                        bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4461                    }
4462
4463                    Condition::LE => {
4464                        // LE(a,b) = !GT(a,b): use GT logic but invert result
4465                        // GT(a,b) = LT(b,a): so we do CMP(b,a) and check LT, then invert
4466                        // CMP rm_lo, rn_lo (swapped, same as GT)
4467                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4468
4469                        // SBCS rd, rm_hi, rn_hi (swapped)
4470                        let rm_hi_bits = reg_to_bits(rm_hi);
4471                        let rn_hi_bits = reg_to_bits(rn_hi);
4472                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4473                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4474                        bytes.extend_from_slice(&hw1.to_le_bytes());
4475                        bytes.extend_from_slice(&hw2.to_le_bytes());
4476
4477                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT, so inverting GT result)
4478                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4479                    }
4480
4481                    Condition::GE => {
4482                        // GE(a,b) = !LT(a,b): use LT logic but invert result
4483                        // CMP rn_lo, rm_lo (same as LT)
4484                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4485
4486                        // SBCS rd, rn_hi, rm_hi (same as LT)
4487                        let rn_hi_bits = reg_to_bits(rn_hi);
4488                        let rm_hi_bits = reg_to_bits(rm_hi);
4489                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4490                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4491                        bytes.extend_from_slice(&hw1.to_le_bytes());
4492                        bytes.extend_from_slice(&hw2.to_le_bytes());
4493
4494                        // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT)
4495                        bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4496                    }
4497
4498                    // Unsigned comparisons - same instruction sequence, different conditions
4499                    Condition::LO => {
4500                        // LO (unsigned LT): CMP lo, SBCS hi, check C=0
4501                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4502                        let rn_hi_bits = reg_to_bits(rn_hi);
4503                        let rm_hi_bits = reg_to_bits(rm_hi);
4504                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4505                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4506                        bytes.extend_from_slice(&hw1.to_le_bytes());
4507                        bytes.extend_from_slice(&hw2.to_le_bytes());
4508                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4509                    }
4510
4511                    Condition::HI => {
4512                        // HI (unsigned GT): swap operands and check LO
4513                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4514                        let rm_hi_bits = reg_to_bits(rm_hi);
4515                        let rn_hi_bits = reg_to_bits(rn_hi);
4516                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4517                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4518                        bytes.extend_from_slice(&hw1.to_le_bytes());
4519                        bytes.extend_from_slice(&hw2.to_le_bytes());
4520                        bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4521                    }
4522
4523                    Condition::LS => {
4524                        // LS (unsigned LE): !(a > b) = !(HI), so do HI and invert
4525                        bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4526                        let rm_hi_bits = reg_to_bits(rm_hi);
4527                        let rn_hi_bits = reg_to_bits(rn_hi);
4528                        let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4529                        let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4530                        bytes.extend_from_slice(&hw1.to_le_bytes());
4531                        bytes.extend_from_slice(&hw2.to_le_bytes());
4532                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4533                    }
4534
4535                    Condition::HS => {
4536                        // HS (unsigned GE): !(a < b) = !(LO)
4537                        bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4538                        let rn_hi_bits = reg_to_bits(rn_hi);
4539                        let rm_hi_bits = reg_to_bits(rm_hi);
4540                        let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4541                        let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4542                        bytes.extend_from_slice(&hw1.to_le_bytes());
4543                        bytes.extend_from_slice(&hw2.to_le_bytes());
4544                        bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4545                    }
4546                }
4547
4548                Ok(bytes)
4549            }
4550
4551            // I64SetCondZ: Test if i64 register pair is zero, result 0/1 in rd
4552            // ORR.W rd, rn_lo, rn_hi; CMP rd, #0; ITE EQ; MOV 1; MOV 0
4553            ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
4554                let rd_bits = reg_to_bits(rd);
4555                let rn_lo_bits = reg_to_bits(rn_lo);
4556                let rn_hi_bits = reg_to_bits(rn_hi);
4557                let mut bytes = Vec::new();
4558
4559                // ORR.W rd, rn_lo, rn_hi: EA40 rn_lo | 0000 rd 0000 rn_hi
4560                let hw1: u16 = (0xEA40 | rn_lo_bits) as u16;
4561                let hw2: u16 = ((rd_bits << 8) | rn_hi_bits) as u16;
4562                bytes.extend_from_slice(&hw1.to_le_bytes());
4563                bytes.extend_from_slice(&hw2.to_le_bytes());
4564
4565                // CMP rd, #0 — 16-bit form only for r0-r7 (3-bit rd field);
4566                // high registers take CMP.W (T2: F1B0|rn 0F00|imm8). This was
4567                // H-CODE-9: rd_bits<<8 overflowing the field compared the
4568                // WRONG register. Same hardening as the #311 SetCond fix.
4569                if rd_bits < 8 {
4570                    let cmp_instr: u16 = 0x2800 | ((rd_bits as u16) << 8);
4571                    bytes.extend_from_slice(&cmp_instr.to_le_bytes());
4572                } else {
4573                    let hw1: u16 = 0xF1B0 | (rd_bits as u16);
4574                    let hw2: u16 = 0x0F00;
4575                    bytes.extend_from_slice(&hw1.to_le_bytes());
4576                    bytes.extend_from_slice(&hw2.to_le_bytes());
4577                }
4578
4579                // ITE EQ; MOV rd, #1; MOV rd, #0 (32-bit MOV.W for rd >= R8,
4580                // #311 — see I64SetCond)
4581                let mask = 0xC_u16; // ITE EQ mask: firstcond[0]=0, mask=0xC
4582                let ite_instr: u16 = 0xBF00 | mask;
4583                bytes.extend_from_slice(&ite_instr.to_le_bytes());
4584                if rd_bits < 8 {
4585                    let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
4586                    let mov_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
4587                    bytes.extend_from_slice(&mov_one.to_le_bytes());
4588                    bytes.extend_from_slice(&mov_zero.to_le_bytes());
4589                } else {
4590                    for imm in [1u16, 0u16] {
4591                        let hw1: u16 = 0xF04F;
4592                        let hw2: u16 = ((rd_bits as u16) << 8) | imm;
4593                        bytes.extend_from_slice(&hw1.to_le_bytes());
4594                        bytes.extend_from_slice(&hw2.to_le_bytes());
4595                    }
4596                }
4597
4598                Ok(bytes)
4599            }
4600
4601            // I64Mul: 64-bit multiply using UMULL + MLA cross products
4602            // Formula: result = (a_lo * b_lo) + ((a_lo * b_hi + a_hi * b_lo) << 32)
4603            // Uses R12 as scratch register
4604            ArmOp::I64Mul {
4605                rd_lo,
4606                rd_hi,
4607                rn_lo,
4608                rn_hi,
4609                rm_lo,
4610                rm_hi,
4611            } => {
4612                let rd_lo_bits = reg_to_bits(rd_lo);
4613                let rd_hi_bits = reg_to_bits(rd_hi);
4614                let rn_lo_bits = reg_to_bits(rn_lo);
4615                let rn_hi_bits = reg_to_bits(rn_hi);
4616                let rm_lo_bits = reg_to_bits(rm_lo);
4617                let rm_hi_bits = reg_to_bits(rm_hi);
4618                let r12: u32 = 12; // IP scratch register
4619                let mut bytes = Vec::new();
4620
4621                // 1. MUL R12, rn_lo, rm_hi  (R12 = a_lo * b_hi)
4622                // Thumb-2 MUL: hw1=0xFB00|Rn, hw2=0xF000|(Rd<<8)|Rm
4623                let hw1: u16 = (0xFB00 | rn_lo_bits) as u16;
4624                let hw2: u16 = (0xF000 | (r12 << 8) | rm_hi_bits) as u16;
4625                bytes.extend_from_slice(&hw1.to_le_bytes());
4626                bytes.extend_from_slice(&hw2.to_le_bytes());
4627
4628                // 2. MLA R12, rn_hi, rm_lo, R12  (R12 += a_hi * b_lo)
4629                // Thumb-2 MLA: hw1=0xFB00|Rn, hw2=(Ra<<12)|(Rd<<8)|Rm
4630                let hw1: u16 = (0xFB00 | rn_hi_bits) as u16;
4631                let hw2: u16 = ((r12 << 12) | (r12 << 8) | rm_lo_bits) as u16;
4632                bytes.extend_from_slice(&hw1.to_le_bytes());
4633                bytes.extend_from_slice(&hw2.to_le_bytes());
4634
4635                // 3. UMULL rd_lo, rd_hi, rn_lo, rm_lo  (rd_lo:rd_hi = a_lo * b_lo)
4636                // Thumb-2 UMULL: hw1=0xFBA0|Rn, hw2=(RdLo<<12)|(RdHi<<8)|Rm
4637                let hw1: u16 = (0xFBA0 | rn_lo_bits) as u16;
4638                let hw2: u16 = ((rd_lo_bits << 12) | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4639                bytes.extend_from_slice(&hw1.to_le_bytes());
4640                bytes.extend_from_slice(&hw2.to_le_bytes());
4641
4642                // 4. ADD rd_hi, R12  (rd_hi += cross products)
4643                // 16-bit high reg ADD: 01000100 D Rm Rdn[2:0]
4644                let d_bit = (rd_hi_bits >> 3) & 1;
4645                let add_instr: u16 =
4646                    (0x4400 | (d_bit << 7) | (r12 << 3) | (rd_hi_bits & 0x7)) as u16;
4647                bytes.extend_from_slice(&add_instr.to_le_bytes());
4648
4649                Ok(bytes)
4650            }
4651
4652            // I64Shl: 64-bit shift left with branch for n<32 vs n>=32.
4653            //
4654            // #1048: the expansion must NEVER write its own input operands.
4655            // The pre-#1048 sequence masked the amount IN PLACE
4656            // (`AND.W rm_lo, rm_lo, #63`) and used the amount's home high
4657            // register `rm_hi` as scratch (`SUBS.W rm_hi, rm_lo, #32`, RSB,
4658            // LSR) — so re-reading the amount after the shift returned a
4659            // mangled value (amt=64 read back 0, amt=67 read back 3). The
4660            // rewrite uses R12 — encoder scratch, never allocatable (#212) —
4661            // as the ONLY temporary, re-deriving the masked amount from the
4662            // untouched rm_lo whenever a second live temp would otherwise be
4663            // needed. This matches the Rocq/SMT pseudo-op models
4664            // (I64ShlPseudo writes rd_lo/rd_hi ONLY), which were proven over
4665            // exactly this non-clobbering contract all along.
4666            ArmOp::I64Shl {
4667                rd_lo,
4668                rd_hi,
4669                rn_lo,
4670                rn_hi,
4671                rm_lo,
4672                rm_hi: _,
4673            } => {
4674                let rd_lo_bits = reg_to_bits(rd_lo);
4675                let rd_hi_bits = reg_to_bits(rd_hi);
4676                let rn_lo_bits = reg_to_bits(rn_lo);
4677                let rn_hi_bits = reg_to_bits(rn_hi);
4678                let rm_lo_bits = reg_to_bits(rm_lo);
4679                let r12: u32 = 12; // the only scratch — never allocatable
4680                let mut bytes = Vec::new();
4681
4682                // #1039 house style: refuse a destination that would collide
4683                // with an input still needed after the destination is first
4684                // written, loudly — never misassemble. rd_hi is written before
4685                // rn_lo and rm_lo are last read; the in-place form
4686                // rd == rn (select_default) has rd_hi == rn_hi and stays legal.
4687                if rd_hi_bits == rn_lo_bits || rd_hi_bits == rm_lo_bits {
4688                    return Err(synth_core::Error::synthesis(format!(
4689                        "I64Shl: rd_hi {rd_hi:?} aliases an input ({rn_lo:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4690                    )));
4691                }
4692
4693                // AND.W R12, rm_lo, #63  (n — the amount register is only READ)
4694                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4695                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4696                bytes.extend_from_slice(&hw1.to_le_bytes());
4697                bytes.extend_from_slice(&hw2.to_le_bytes());
4698
4699                // SUBS.W R12, R12, #32  (R12 = n-32, sets flags)
4700                let hw1: u16 = (0xF1B0 | r12) as u16;
4701                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4702                bytes.extend_from_slice(&hw1.to_le_bytes());
4703                bytes.extend_from_slice(&hw2.to_le_bytes());
4704
4705                // BPL .large (branch if n >= 32, offset = +14 halfwords)
4706                let bpl: u16 = 0xD50E;
4707                bytes.extend_from_slice(&bpl.to_le_bytes());
4708
4709                // --- Small shift (n < 32) ---
4710                // AND.W R12, rm_lo, #63  (n again — R12 held n-32)
4711                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4712                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4713                bytes.extend_from_slice(&hw1.to_le_bytes());
4714                bytes.extend_from_slice(&hw2.to_le_bytes());
4715
4716                // LSL.W rd_hi, rn_hi, R12  (hi << n; rn_hi's last read)
4717                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4718                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4719                bytes.extend_from_slice(&hw1.to_le_bytes());
4720                bytes.extend_from_slice(&hw2.to_le_bytes());
4721
4722                // RSB.W R12, R12, #32  (R12 = 32-n)
4723                let hw1: u16 = (0xF1C0 | r12) as u16;
4724                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4725                bytes.extend_from_slice(&hw1.to_le_bytes());
4726                bytes.extend_from_slice(&hw2.to_le_bytes());
4727
4728                // LSR.W R12, rn_lo, R12  (overflow = lo >> (32-n); n=0 gives
4729                // a register shift by 32 which yields 0 — exact)
4730                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4731                let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4732                bytes.extend_from_slice(&hw1.to_le_bytes());
4733                bytes.extend_from_slice(&hw2.to_le_bytes());
4734
4735                // ORR.W rd_hi, rd_hi, R12  (hi |= overflow bits from lo)
4736                let hw1: u16 = (0xEA40 | rd_hi_bits) as u16;
4737                let hw2: u16 = ((rd_hi_bits << 8) | r12) as u16;
4738                bytes.extend_from_slice(&hw1.to_le_bytes());
4739                bytes.extend_from_slice(&hw2.to_le_bytes());
4740
4741                // AND.W R12, rm_lo, #63  (n once more for the low half)
4742                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4743                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4744                bytes.extend_from_slice(&hw1.to_le_bytes());
4745                bytes.extend_from_slice(&hw2.to_le_bytes());
4746
4747                // LSL.W rd_lo, rn_lo, R12  (lo << n)
4748                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4749                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4750                bytes.extend_from_slice(&hw1.to_le_bytes());
4751                bytes.extend_from_slice(&hw2.to_le_bytes());
4752
4753                // B .done — `.done` is the END of the expansion, i.e. PAST the
4754                // large-shift arm's trailing zero-fill. #916: that zero-fill is
4755                // 1 halfword for a low rd_lo but 2 for R8-R12 (MOV.W), so the
4756                // displacement is DERIVED from its real width instead of the
4757                // hard-coded 0xE002 — widening the MOV without this would
4758                // overshoot `.done` and turn a data miscompile into a
4759                // control-flow one. Thumb `B` reads PC as its own address + 4
4760                // (= +2 halfwords), so imm11 = (large-arm halfwords) - 1.
4761                let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_lo_bits);
4762                let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4763                bytes.extend_from_slice(&b_done.to_le_bytes());
4764
4765                // --- Large shift (n >= 32) --- (R12 still holds n-32)
4766                // LSL.W rd_hi, rn_lo, R12  (hi = lo << (n-32))
4767                let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4768                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4769                bytes.extend_from_slice(&hw1.to_le_bytes());
4770                bytes.extend_from_slice(&hw2.to_le_bytes());
4771
4772                // MOV rd_lo, #0 (#916: MOV.W for rd_lo >= R8). NOTE the order
4773                // is load-bearing — zeroing rd_lo BEFORE the LSL.W would
4774                // destroy rn_lo in the in-place case rd_lo == rn_lo, so this
4775                // cannot be reordered to dodge the displacement change.
4776                emit_thumb_zero_fill(&mut bytes, rd_lo_bits);
4777
4778                Ok(bytes) // 46 bytes (48 when rd_lo >= R8 takes MOV.W)
4779            }
4780
4781            // I64ShrU: 64-bit logical shift right with branch for n<32 vs
4782            // n>=32. #1048: R12-only scratch, operands never written — see
4783            // the I64Shl comment for the full rationale.
4784            ArmOp::I64ShrU {
4785                rd_lo,
4786                rd_hi,
4787                rn_lo,
4788                rn_hi,
4789                rm_lo,
4790                rm_hi: _,
4791            } => {
4792                let rd_lo_bits = reg_to_bits(rd_lo);
4793                let rd_hi_bits = reg_to_bits(rd_hi);
4794                let rn_lo_bits = reg_to_bits(rn_lo);
4795                let rn_hi_bits = reg_to_bits(rn_hi);
4796                let rm_lo_bits = reg_to_bits(rm_lo);
4797                let r12: u32 = 12; // the only scratch — never allocatable
4798                let mut bytes = Vec::new();
4799
4800                // #1039 house style: rd_lo is written before rn_hi and rm_lo
4801                // are last read — refuse the collision loudly. The in-place
4802                // form rd == rn (select_default) has rd_lo == rn_lo and stays
4803                // legal.
4804                if rd_lo_bits == rn_hi_bits || rd_lo_bits == rm_lo_bits {
4805                    return Err(synth_core::Error::synthesis(format!(
4806                        "I64ShrU: rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4807                    )));
4808                }
4809
4810                // AND.W R12, rm_lo, #63  (n — the amount register is only READ)
4811                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4812                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4813                bytes.extend_from_slice(&hw1.to_le_bytes());
4814                bytes.extend_from_slice(&hw2.to_le_bytes());
4815
4816                // SUBS.W R12, R12, #32  (R12 = n-32, sets flags)
4817                let hw1: u16 = (0xF1B0 | r12) as u16;
4818                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4819                bytes.extend_from_slice(&hw1.to_le_bytes());
4820                bytes.extend_from_slice(&hw2.to_le_bytes());
4821
4822                // BPL .large (+14 halfwords)
4823                let bpl: u16 = 0xD50E;
4824                bytes.extend_from_slice(&bpl.to_le_bytes());
4825
4826                // --- Small shift (n < 32) ---
4827                // AND.W R12, rm_lo, #63  (n again — R12 held n-32)
4828                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4829                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4830                bytes.extend_from_slice(&hw1.to_le_bytes());
4831                bytes.extend_from_slice(&hw2.to_le_bytes());
4832
4833                // LSR.W rd_lo, rn_lo, R12  (lo >> n; rn_lo's last read)
4834                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4835                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4836                bytes.extend_from_slice(&hw1.to_le_bytes());
4837                bytes.extend_from_slice(&hw2.to_le_bytes());
4838
4839                // RSB.W R12, R12, #32  (R12 = 32-n)
4840                let hw1: u16 = (0xF1C0 | r12) as u16;
4841                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4842                bytes.extend_from_slice(&hw1.to_le_bytes());
4843                bytes.extend_from_slice(&hw2.to_le_bytes());
4844
4845                // LSL.W R12, rn_hi, R12  (overflow = hi << (32-n); n=0 gives
4846                // a register shift by 32 which yields 0 — exact)
4847                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4848                let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4849                bytes.extend_from_slice(&hw1.to_le_bytes());
4850                bytes.extend_from_slice(&hw2.to_le_bytes());
4851
4852                // ORR.W rd_lo, rd_lo, R12  (lo |= overflow from hi)
4853                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4854                let hw2: u16 = ((rd_lo_bits << 8) | r12) as u16;
4855                bytes.extend_from_slice(&hw1.to_le_bytes());
4856                bytes.extend_from_slice(&hw2.to_le_bytes());
4857
4858                // AND.W R12, rm_lo, #63  (n once more for the high half)
4859                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4860                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4861                bytes.extend_from_slice(&hw1.to_le_bytes());
4862                bytes.extend_from_slice(&hw2.to_le_bytes());
4863
4864                // LSR.W rd_hi, rn_hi, R12  (hi >> n, logical)
4865                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4866                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4867                bytes.extend_from_slice(&hw1.to_le_bytes());
4868                bytes.extend_from_slice(&hw2.to_le_bytes());
4869
4870                // B .done — see I64Shl: `.done` is the END of the expansion,
4871                // past the trailing zero-fill, so the displacement is derived
4872                // from that zero-fill's real width (#916).
4873                let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_hi_bits);
4874                let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4875                bytes.extend_from_slice(&b_done.to_le_bytes());
4876
4877                // --- Large shift (n >= 32) --- (R12 still holds n-32)
4878                // LSR.W rd_lo, rn_hi, R12  (lo = hi >> (n-32))
4879                let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4880                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4881                bytes.extend_from_slice(&hw1.to_le_bytes());
4882                bytes.extend_from_slice(&hw2.to_le_bytes());
4883
4884                // MOV rd_hi, #0 (#916: MOV.W for rd_hi >= R8). Order is
4885                // load-bearing: the LSR.W above reads rn_hi, which may BE
4886                // rd_hi in the in-place case.
4887                emit_thumb_zero_fill(&mut bytes, rd_hi_bits);
4888
4889                Ok(bytes) // 46 bytes (48 when rd_hi >= R8 takes MOV.W)
4890            }
4891
4892            // I64ShrS: 64-bit arithmetic shift right with branch for n<32 vs
4893            // n>=32. #1048: R12-only scratch, operands never written — see
4894            // the I64Shl comment for the full rationale.
4895            ArmOp::I64ShrS {
4896                rd_lo,
4897                rd_hi,
4898                rn_lo,
4899                rn_hi,
4900                rm_lo,
4901                rm_hi: _,
4902            } => {
4903                let rd_lo_bits = reg_to_bits(rd_lo);
4904                let rd_hi_bits = reg_to_bits(rd_hi);
4905                let rn_lo_bits = reg_to_bits(rn_lo);
4906                let rn_hi_bits = reg_to_bits(rn_hi);
4907                let rm_lo_bits = reg_to_bits(rm_lo);
4908                let r12: u32 = 12; // the only scratch — never allocatable
4909                let mut bytes = Vec::new();
4910
4911                // #1039 house style: rd_lo is written before rn_hi and rm_lo
4912                // are last read (on BOTH arms of the diamond — the large arm's
4913                // trailing `ASR rd_hi, rn_hi, #31` also reads rn_hi after
4914                // rd_lo is written). The in-place form rd == rn stays legal.
4915                if rd_lo_bits == rn_hi_bits || rd_lo_bits == rm_lo_bits {
4916                    return Err(synth_core::Error::synthesis(format!(
4917                        "I64ShrS: rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4918                    )));
4919                }
4920
4921                // AND.W R12, rm_lo, #63  (n — the amount register is only READ)
4922                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4923                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4924                bytes.extend_from_slice(&hw1.to_le_bytes());
4925                bytes.extend_from_slice(&hw2.to_le_bytes());
4926
4927                // SUBS.W R12, R12, #32  (R12 = n-32, sets flags)
4928                let hw1: u16 = (0xF1B0 | r12) as u16;
4929                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4930                bytes.extend_from_slice(&hw1.to_le_bytes());
4931                bytes.extend_from_slice(&hw2.to_le_bytes());
4932
4933                // BPL .large (+14 halfwords)
4934                let bpl: u16 = 0xD50E;
4935                bytes.extend_from_slice(&bpl.to_le_bytes());
4936
4937                // --- Small shift (n < 32) ---
4938                // AND.W R12, rm_lo, #63  (n again — R12 held n-32)
4939                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4940                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4941                bytes.extend_from_slice(&hw1.to_le_bytes());
4942                bytes.extend_from_slice(&hw2.to_le_bytes());
4943
4944                // LSR.W rd_lo, rn_lo, R12  (lo >> n, logical for lo word)
4945                let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4946                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4947                bytes.extend_from_slice(&hw1.to_le_bytes());
4948                bytes.extend_from_slice(&hw2.to_le_bytes());
4949
4950                // RSB.W R12, R12, #32  (R12 = 32-n)
4951                let hw1: u16 = (0xF1C0 | r12) as u16;
4952                let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4953                bytes.extend_from_slice(&hw1.to_le_bytes());
4954                bytes.extend_from_slice(&hw2.to_le_bytes());
4955
4956                // LSL.W R12, rn_hi, R12  (overflow = hi << (32-n); n=0 gives
4957                // a register shift by 32 which yields 0 — exact)
4958                let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4959                let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4960                bytes.extend_from_slice(&hw1.to_le_bytes());
4961                bytes.extend_from_slice(&hw2.to_le_bytes());
4962
4963                // ORR.W rd_lo, rd_lo, R12  (lo |= overflow from hi)
4964                let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4965                let hw2: u16 = ((rd_lo_bits << 8) | r12) as u16;
4966                bytes.extend_from_slice(&hw1.to_le_bytes());
4967                bytes.extend_from_slice(&hw2.to_le_bytes());
4968
4969                // AND.W R12, rm_lo, #63  (n once more for the high half)
4970                let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4971                let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4972                bytes.extend_from_slice(&hw1.to_le_bytes());
4973                bytes.extend_from_slice(&hw2.to_le_bytes());
4974
4975                // ASR.W rd_hi, rn_hi, R12  (hi >> n, arithmetic/sign-extending)
4976                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
4977                let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4978                bytes.extend_from_slice(&hw1.to_le_bytes());
4979                bytes.extend_from_slice(&hw2.to_le_bytes());
4980
4981                // B .done (+3 halfwords, large shift is 8 bytes)
4982                let b_done: u16 = 0xE003;
4983                bytes.extend_from_slice(&b_done.to_le_bytes());
4984
4985                // --- Large shift (n >= 32) --- (R12 still holds n-32)
4986                // ASR.W rd_lo, rn_hi, R12  (lo = hi >>> (n-32))
4987                let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
4988                let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4989                bytes.extend_from_slice(&hw1.to_le_bytes());
4990                bytes.extend_from_slice(&hw2.to_le_bytes());
4991
4992                // ASR.W rd_hi, rn_hi, #31  (hi = sign extension, all 0s or all 1s)
4993                // Thumb-2 ASR immediate: hw1=0xEA4F, hw2=imm3:Rd:imm2:10:Rm
4994                // imm5=31=11111 → imm3=111, imm2=11
4995                let hw1: u16 = 0xEA4F;
4996                let hw2: u16 = (0x7000 | (rd_hi_bits << 8) | 0x00E0 | rn_hi_bits) as u16;
4997                bytes.extend_from_slice(&hw1.to_le_bytes());
4998                bytes.extend_from_slice(&hw2.to_le_bytes());
4999
5000                Ok(bytes) // Total: 48 bytes
5001            }
5002
5003            // I64Rotl: 64-bit rotate left (#610 rewrite).
5004            // For n < 32: new_hi = (hi << n) | (lo >> (32-n)), new_lo = (lo << n) | (hi >> (32-n))
5005            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
5006            //
5007            // Fixed-reg core: value in R0:R1, amount in R2, scratch R3 + R12
5008            // (all four saved/marshaled by the #610 fixed-ABI wrapper; the
5009            // pre-#610 expansion wrote through the selector's registers with
5010            // colliding R3/R4 scratch and restored the saved R4 OVER the
5011            // result). Relies on ARM register-shift semantics: amounts >= 32
5012            // yield 0 for LSL/LSR, which makes n = 0 and n = 32 exact.
5013            ArmOp::I64Rotl {
5014                rdlo,
5015                rdhi,
5016                rnlo,
5017                rnhi,
5018                shift,
5019            } => {
5020                let mut bytes = Vec::new();
5021                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
5022
5023                let core: [u16; 35] = [
5024                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
5025                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
5026                    0xD50E, //         BPL    .large        (n >= 32)
5027                    // --- small rotation (n < 32) ---
5028                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
5029                    0xFA20, 0xFC03, // LSR.W  R12, R0, R3   (lo >> (32-n))
5030                    0xFA21, 0xF303, // LSR.W  R3, R1, R3    (hi >> (32-n))
5031                    0xFA01, 0xF102, // LSL.W  R1, R1, R2    (hi << n)
5032                    0xEA41, 0x010C, // ORR.W  R1, R1, R12   (new_hi)
5033                    0xFA00, 0xF002, // LSL.W  R0, R0, R2    (lo << n)
5034                    0xEA40, 0x0003, // ORR.W  R0, R0, R3    (new_lo)
5035                    0xE00E, //         B      .done
5036                    // --- large rotation (n >= 32), R3 = m = n-32 ---
5037                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
5038                    0xFA21, 0xFC02, // LSR.W  R12, R1, R2   (hi >> (64-n))
5039                    0xFA20, 0xF202, // LSR.W  R2, R0, R2    (lo >> (64-n))
5040                    0xFA00, 0xF003, // LSL.W  R0, R0, R3    (lo << m)
5041                    0xFA01, 0xF103, // LSL.W  R1, R1, R3    (hi << m)
5042                    0xEA40, 0x0C0C, // ORR.W  R12, R0, R12  (new_hi = (lo<<m)|(hi>>(64-n)))
5043                    0xEA41, 0x0002, // ORR.W  R0, R1, R2    (new_lo = (hi<<m)|(lo>>(64-n)))
5044                    0x4661, //         MOV    R1, R12       (new_hi into place)
5045                            // .done: result in R0:R1
5046                ];
5047                for hw in core {
5048                    bytes.extend_from_slice(&hw.to_le_bytes());
5049                }
5050
5051                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5052                Ok(bytes) // Total: 102 bytes
5053            }
5054
5055            // I64Rotr: 64-bit rotate right (#610 rewrite).
5056            // For n < 32: new_lo = (lo >> n) | (hi << (32-n)), new_hi = (hi >> n) | (lo << (32-n))
5057            // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
5058            //
5059            // Same fixed-reg core contract as I64Rotl: value in R0:R1, amount
5060            // in R2, scratch R3 + R12, all covered by the fixed-ABI wrapper.
5061            ArmOp::I64Rotr {
5062                rdlo,
5063                rdhi,
5064                rnlo,
5065                rnhi,
5066                shift,
5067            } => {
5068                let mut bytes = Vec::new();
5069                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
5070
5071                let core: [u16; 35] = [
5072                    0xF002, 0x023F, // AND.W  R2, R2, #63   (mask amount mod 64)
5073                    0xF1B2, 0x0320, // SUBS.W R3, R2, #32   (R3 = n-32, sets N)
5074                    0xD50E, //         BPL    .large        (n >= 32)
5075                    // --- small rotation (n < 32) ---
5076                    0xF1C2, 0x0320, // RSB.W  R3, R2, #32   (R3 = 32-n)
5077                    0xFA01, 0xFC03, // LSL.W  R12, R1, R3   (hi << (32-n))
5078                    0xFA00, 0xF303, // LSL.W  R3, R0, R3    (lo << (32-n))
5079                    0xFA20, 0xF002, // LSR.W  R0, R0, R2    (lo >> n)
5080                    0xEA40, 0x000C, // ORR.W  R0, R0, R12   (new_lo)
5081                    0xFA21, 0xF102, // LSR.W  R1, R1, R2    (hi >> n)
5082                    0xEA41, 0x0103, // ORR.W  R1, R1, R3    (new_hi)
5083                    0xE00E, //         B      .done
5084                    // --- large rotation (n >= 32), R3 = m = n-32 ---
5085                    0xF1C3, 0x0220, // RSB.W  R2, R3, #32   (R2 = 32-m = 64-n)
5086                    0xFA00, 0xFC02, // LSL.W  R12, R0, R2   (lo << (64-n))
5087                    0xFA01, 0xF202, // LSL.W  R2, R1, R2    (hi << (64-n))
5088                    0xFA21, 0xF103, // LSR.W  R1, R1, R3    (hi >> m)
5089                    0xEA41, 0x0C0C, // ORR.W  R12, R1, R12  (new_lo = (hi>>m)|(lo<<(64-n)))
5090                    0xFA20, 0xF103, // LSR.W  R1, R0, R3    (lo >> m)
5091                    0xEA41, 0x0102, // ORR.W  R1, R1, R2    (new_hi = (lo>>m)|(hi<<(64-n)))
5092                    0x4660, //         MOV    R0, R12       (new_lo into place)
5093                            // .done: result in R0:R1
5094                ];
5095                for hw in core {
5096                    bytes.extend_from_slice(&hw.to_le_bytes());
5097                }
5098
5099                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5100                Ok(bytes) // Total: 102 bytes
5101            }
5102
5103            // I64Clz: Count leading zeros in 64-bit value
5104            // If hi != 0: result = CLZ(hi)
5105            // If hi == 0: result = 32 + CLZ(lo)
5106            //
5107            // Layout (using CMP+BNE approach for consistency):
5108            // 0: CMP.W rnhi, #0 (4 bytes)
5109            // 4: BEQ .hi_zero (2 bytes) - branch forward to offset 14
5110            // 6: CLZ.W rd, rnhi (4 bytes)
5111            // 10: B .done (2 bytes) - branch forward to offset 22
5112            // 12: NOP (2 bytes) - padding for alignment
5113            // 14: .hi_zero: CLZ.W rd, rnlo (4 bytes)
5114            // 18: ADD.W rd, rd, #32 (4 bytes)
5115            // 22: .done
5116            ArmOp::I64Clz { rd, rnlo, rnhi } => {
5117                let rd_bits = reg_to_bits(rd);
5118                let rn_lo_bits = reg_to_bits(rnlo);
5119                let rn_hi_bits = reg_to_bits(rnhi);
5120                let mut bytes = Vec::new();
5121
5122                // CMP.W rnhi, #0 (4 bytes at offset 0)
5123                let hw1: u16 = (0xF1B0 | rn_hi_bits) as u16;
5124                let hw2: u16 = 0x0F00;
5125                bytes.extend_from_slice(&hw1.to_le_bytes());
5126                bytes.extend_from_slice(&hw2.to_le_bytes());
5127
5128                // BEQ .hi_zero (2 bytes at offset 4)
5129                // PC = 4 + 4 = 8, target = 14, offset = 6, imm8 = 3
5130                let beq: u16 = 0xD003;
5131                bytes.extend_from_slice(&beq.to_le_bytes());
5132
5133                // CLZ.W rd, rnhi (4 bytes at offset 6)
5134                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5135                let hw1: u16 = (0xFAB0 | rn_hi_bits) as u16;
5136                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_hi_bits) as u16;
5137                bytes.extend_from_slice(&hw1.to_le_bytes());
5138                bytes.extend_from_slice(&hw2.to_le_bytes());
5139
5140                // B .done (2 bytes at offset 10)
5141                // PC = 10 + 4 = 14, target = 22, offset = 8, imm11 = 4
5142                let b_done: u16 = 0xE004;
5143                bytes.extend_from_slice(&b_done.to_le_bytes());
5144
5145                // NOP (2 bytes at offset 12) - padding
5146                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5147
5148                // .hi_zero: (offset 14)
5149                // CLZ.W rd, rnlo (4 bytes)
5150                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5151                let hw1: u16 = (0xFAB0 | rn_lo_bits) as u16;
5152                let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_lo_bits) as u16;
5153                bytes.extend_from_slice(&hw1.to_le_bytes());
5154                bytes.extend_from_slice(&hw2.to_le_bytes());
5155
5156                // ADD.W rd, rd, #32 (4 bytes at offset 18)
5157                let hw1: u16 = (0xF100 | rd_bits) as u16;
5158                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5159                bytes.extend_from_slice(&hw1.to_le_bytes());
5160                bytes.extend_from_slice(&hw2.to_le_bytes());
5161
5162                // .done: (offset 22 — the end of the expansion)
5163                //
5164                // #1048: the former trailing hi-word clear (`MOV rnhi, #0`)
5165                // is GONE. It was aimed at the RESULT's high half but wrote
5166                // the OPERAND's home high register — a real executed
5167                // miscompile on the direct selector, which allocates a fresh
5168                // destination pair and zeroes its own dst_hi, leaving the
5169                // operand's hi limb destroyed for any later re-read. The
5170                // Rocq/SMT models of I64ClzPseudo always said "writes rd
5171                // ONLY"; the callers that relied on the implicit clear
5172                // (select_default, optimizer_bridge) now emit their own
5173                // explicit hi-zero op. `B .done` above targets offset 22 =
5174                // past-the-end, `BEQ` targets offset 14 — no displacement
5175                // moves.
5176
5177                Ok(bytes) // 22 bytes, register-independent
5178            }
5179
5180            // I64Ctz: Count trailing zeros in 64-bit value
5181            // If lo != 0: result = CTZ(lo) = CLZ(RBIT(lo))
5182            // If lo == 0: result = 32 + CTZ(hi) = 32 + CLZ(RBIT(hi))
5183            //
5184            // Layout:
5185            // 0: CMP.W rnlo, #0 (4 bytes)
5186            // 4: BEQ .lo_zero (2 bytes) - branch to offset 18
5187            // 6: RBIT.W rd, rnlo (4 bytes)
5188            // 10: CLZ.W rd, rd (4 bytes)
5189            // 14: B .done (2 bytes) - branch to offset 30
5190            // 16: NOP (2 bytes) - padding
5191            // 18: .lo_zero: RBIT.W rd, rnhi (4 bytes)
5192            // 22: CLZ.W rd, rd (4 bytes)
5193            // 26: ADD.W rd, rd, #32 (4 bytes)
5194            // 30: .done
5195            ArmOp::I64Ctz { rd, rnlo, rnhi } => {
5196                let rd_bits = reg_to_bits(rd);
5197                let rn_lo_bits = reg_to_bits(rnlo);
5198                let rn_hi_bits = reg_to_bits(rnhi);
5199                let mut bytes = Vec::new();
5200
5201                // CMP.W rnlo, #0 (4 bytes at offset 0)
5202                let hw1: u16 = (0xF1B0 | rn_lo_bits) as u16;
5203                let hw2: u16 = 0x0F00;
5204                bytes.extend_from_slice(&hw1.to_le_bytes());
5205                bytes.extend_from_slice(&hw2.to_le_bytes());
5206
5207                // BEQ .lo_zero (2 bytes at offset 4)
5208                // PC = 4 + 4 = 8, target = 18, offset = 10, imm8 = 5
5209                let beq: u16 = 0xD005;
5210                bytes.extend_from_slice(&beq.to_le_bytes());
5211
5212                // RBIT.W rd, rnlo (4 bytes at offset 6)
5213                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5214                let hw1: u16 = (0xFA90 | rn_lo_bits) as u16;
5215                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_lo_bits) as u16;
5216                bytes.extend_from_slice(&hw1.to_le_bytes());
5217                bytes.extend_from_slice(&hw2.to_le_bytes());
5218
5219                // CLZ.W rd, rd (4 bytes at offset 10)
5220                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5221                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5222                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5223                bytes.extend_from_slice(&hw1.to_le_bytes());
5224                bytes.extend_from_slice(&hw2.to_le_bytes());
5225
5226                // B .done (2 bytes at offset 14)
5227                // PC = 14 + 4 = 18, target = 30, offset = 12, imm11 = 6
5228                let b_done: u16 = 0xE006;
5229                bytes.extend_from_slice(&b_done.to_le_bytes());
5230
5231                // NOP (2 bytes at offset 16) - padding
5232                bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5233
5234                // .lo_zero: (offset 18)
5235                // RBIT.W rd, rnhi (4 bytes)
5236                // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5237                let hw1: u16 = (0xFA90 | rn_hi_bits) as u16;
5238                let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_hi_bits) as u16;
5239                bytes.extend_from_slice(&hw1.to_le_bytes());
5240                bytes.extend_from_slice(&hw2.to_le_bytes());
5241
5242                // CLZ.W rd, rd (4 bytes at offset 22)
5243                // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5244                let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5245                let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5246                bytes.extend_from_slice(&hw1.to_le_bytes());
5247                bytes.extend_from_slice(&hw2.to_le_bytes());
5248
5249                // ADD.W rd, rd, #32 (4 bytes at offset 26)
5250                let hw1: u16 = (0xF100 | rd_bits) as u16;
5251                let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5252                bytes.extend_from_slice(&hw1.to_le_bytes());
5253                bytes.extend_from_slice(&hw2.to_le_bytes());
5254
5255                // .done: (offset 30 — the end of the expansion)
5256                // #1048: the former trailing `MOV rnhi, #0` is GONE — it
5257                // wrote the OPERAND's home high register (see the I64Clz
5258                // comment above). `B .done` targets offset 30 = past-the-end,
5259                // `BEQ` targets offset 18 — no displacement moves.
5260
5261                Ok(bytes) // 30 bytes, register-independent
5262            }
5263
5264            // I64Popcnt: Population count of 64-bit value
5265            // result = POPCNT(lo) + POPCNT(hi)
5266            // Using SIMD-style parallel bit counting algorithm
5267            ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
5268                let rd_bits = reg_to_bits(rd);
5269                let rn_lo_bits = reg_to_bits(rnlo);
5270                let rn_hi_bits = reg_to_bits(rnhi);
5271                let r12: u32 = 12; // IP scratch
5272                let r3: u32 = 3; // Scratch for hi popcnt result
5273                let mut bytes = Vec::new();
5274
5275                // PUSH {R3, R4, R5} - save scratch registers
5276                bytes.extend_from_slice(&0xB438u16.to_le_bytes());
5277
5278                // Strategy: compute popcnt(lo) -> R4, popcnt(hi) -> R5, add them -> rd
5279                // Using lookup table approach for each byte would be too large
5280                // Using shift-and-add approach instead
5281
5282                // For simplicity and correctness, use the efficient parallel algorithm
5283                // but implement it as a series of inline operations
5284
5285                // Marshal the operand pair into the fixed scratch regs, routing
5286                // rnlo through R12 (#632 audit): writing R4 first corrupted the
5287                // rnhi read for a pair living at (R3,R4) — every source is read
5288                // before any scratch register it could occupy is written.
5289                // MOV R12, rnlo
5290                let mov: u16 = (0x4600 | (1 << 7) | (rn_lo_bits << 3) | 4) as u16;
5291                bytes.extend_from_slice(&mov.to_le_bytes());
5292                // MOV R5, rnhi (R4 untouched so far; rnhi == R5 is a no-op)
5293                let mov: u16 = (0x4600 | (rn_hi_bits << 3) | 5) as u16;
5294                bytes.extend_from_slice(&mov.to_le_bytes());
5295                // MOV R4, R12
5296                bytes.extend_from_slice(&0x4664u16.to_le_bytes());
5297
5298                // --- POPCNT for R4 (lo word) ---
5299                // Step 1: x = x - ((x >> 1) & 0x55555555)
5300                // LSR.W R12, R4, #1
5301                let hw1: u16 = 0xEA4F;
5302                let hw2: u16 = ((r12 << 8) | 0x50 | 4) as u16;
5303                bytes.extend_from_slice(&hw1.to_le_bytes());
5304                bytes.extend_from_slice(&hw2.to_le_bytes());
5305
5306                // Load 0x55555555 into R3 using MOVW/MOVT
5307                // MOVW R3, #0x5555
5308                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5309                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5310                // MOVT R3, #0x5555
5311                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5312                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5313
5314                // AND.W R12, R12, R3
5315                let hw1: u16 = (0xEA00 | r12) as u16;
5316                let hw2: u16 = ((r12 << 8) | r3) as u16;
5317                bytes.extend_from_slice(&hw1.to_le_bytes());
5318                bytes.extend_from_slice(&hw2.to_le_bytes());
5319
5320                // SUB.W R4, R4, R12
5321                let hw1: u16 = (0xEBA0 | 4) as u16;
5322                let hw2: u16 = ((4 << 8) | r12) as u16;
5323                bytes.extend_from_slice(&hw1.to_le_bytes());
5324                bytes.extend_from_slice(&hw2.to_le_bytes());
5325
5326                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5327                // Load 0x33333333 into R3
5328                // MOVW R3, #0x3333
5329                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5330                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5331                // MOVT R3, #0x3333
5332                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5333                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5334
5335                // AND.W R12, R4, R3
5336                let hw1: u16 = (0xEA00 | 4) as u16;
5337                let hw2: u16 = ((r12 << 8) | r3) as u16;
5338                bytes.extend_from_slice(&hw1.to_le_bytes());
5339                bytes.extend_from_slice(&hw2.to_le_bytes());
5340
5341                // LSR.W R4, R4, #2
5342                let hw1: u16 = 0xEA4F;
5343                let hw2: u16 = ((4 << 8) | 0x90 | 4) as u16;
5344                bytes.extend_from_slice(&hw1.to_le_bytes());
5345                bytes.extend_from_slice(&hw2.to_le_bytes());
5346
5347                // AND.W R4, R4, R3
5348                let hw1: u16 = (0xEA00 | 4) as u16;
5349                let hw2: u16 = ((4 << 8) | r3) as u16;
5350                bytes.extend_from_slice(&hw1.to_le_bytes());
5351                bytes.extend_from_slice(&hw2.to_le_bytes());
5352
5353                // ADD.W R4, R4, R12
5354                let hw1: u16 = (0xEB00 | 4) as u16;
5355                let hw2: u16 = ((4 << 8) | r12) as u16;
5356                bytes.extend_from_slice(&hw1.to_le_bytes());
5357                bytes.extend_from_slice(&hw2.to_le_bytes());
5358
5359                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5360                // LSR.W R12, R4, #4
5361                // hw2 = (imm3 << 12) | (Rd << 8) | (imm2 << 6) | (type << 4) | Rm
5362                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5363                let hw1: u16 = 0xEA4F;
5364                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 4) as u16;
5365                bytes.extend_from_slice(&hw1.to_le_bytes());
5366                bytes.extend_from_slice(&hw2.to_le_bytes());
5367
5368                // ADD.W R4, R4, R12
5369                let hw1: u16 = (0xEB00 | 4) as u16;
5370                let hw2: u16 = ((4 << 8) | r12) as u16;
5371                bytes.extend_from_slice(&hw1.to_le_bytes());
5372                bytes.extend_from_slice(&hw2.to_le_bytes());
5373
5374                // Load 0x0F0F0F0F into R3
5375                // MOVW R3, #0x0F0F (imm4=0, i=1, imm3=7, imm8=0x0F)
5376                // hw1 = 11110 1 10 0100 0000 = 0xF640
5377                // hw2 = 0 111 0011 00001111 = 0x730F
5378                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5379                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5380                // MOVT R3, #0x0F0F
5381                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5382                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5383
5384                // AND.W R4, R4, R3
5385                let hw1: u16 = (0xEA00 | 4) as u16;
5386                let hw2: u16 = ((4 << 8) | r3) as u16;
5387                bytes.extend_from_slice(&hw1.to_le_bytes());
5388                bytes.extend_from_slice(&hw2.to_le_bytes());
5389
5390                // Step 4: x = x * 0x01010101 >> 24
5391                // Load 0x01010101 into R3
5392                // MOVW R3, #0x0101
5393                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5394                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5395                // MOVT R3, #0x0101
5396                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5397                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5398
5399                // MUL R4, R4, R3
5400                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5401                let hw1: u16 = (0xFB00 | 4) as u16;
5402                let hw2: u16 = (0xF000 | (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                // LSR.W R4, R4, #24
5407                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5408                let hw1: u16 = 0xEA4F;
5409                let hw2: u16 = (0x6000 | (4 << 8) | 0x10 | 4) as u16;
5410                bytes.extend_from_slice(&hw1.to_le_bytes());
5411                bytes.extend_from_slice(&hw2.to_le_bytes());
5412
5413                // --- POPCNT for R5 (hi word) - same algorithm ---
5414                // Step 1
5415                let hw1: u16 = 0xEA4F;
5416                let hw2: u16 = ((r12 << 8) | 0x50 | 5) as u16;
5417                bytes.extend_from_slice(&hw1.to_le_bytes());
5418                bytes.extend_from_slice(&hw2.to_le_bytes());
5419
5420                // Load 0x55555555 into R3
5421                bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5422                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5423                bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5424                bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5425
5426                let hw1: u16 = (0xEA00 | r12) as u16;
5427                let hw2: u16 = ((r12 << 8) | r3) as u16;
5428                bytes.extend_from_slice(&hw1.to_le_bytes());
5429                bytes.extend_from_slice(&hw2.to_le_bytes());
5430
5431                let hw1: u16 = (0xEBA0 | 5) as u16;
5432                let hw2: u16 = ((5 << 8) | r12) as u16;
5433                bytes.extend_from_slice(&hw1.to_le_bytes());
5434                bytes.extend_from_slice(&hw2.to_le_bytes());
5435
5436                // Step 2
5437                bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5438                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5439                bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5440                bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5441
5442                let hw1: u16 = (0xEA00 | 5) as u16;
5443                let hw2: u16 = ((r12 << 8) | r3) as u16;
5444                bytes.extend_from_slice(&hw1.to_le_bytes());
5445                bytes.extend_from_slice(&hw2.to_le_bytes());
5446
5447                let hw1: u16 = 0xEA4F;
5448                let hw2: u16 = ((5 << 8) | 0x90 | 5) as u16;
5449                bytes.extend_from_slice(&hw1.to_le_bytes());
5450                bytes.extend_from_slice(&hw2.to_le_bytes());
5451
5452                let hw1: u16 = (0xEA00 | 5) as u16;
5453                let hw2: u16 = ((5 << 8) | r3) as u16;
5454                bytes.extend_from_slice(&hw1.to_le_bytes());
5455                bytes.extend_from_slice(&hw2.to_le_bytes());
5456
5457                let hw1: u16 = (0xEB00 | 5) as u16;
5458                let hw2: u16 = ((5 << 8) | r12) as u16;
5459                bytes.extend_from_slice(&hw1.to_le_bytes());
5460                bytes.extend_from_slice(&hw2.to_le_bytes());
5461
5462                // Step 3: LSR.W R12, R5, #4
5463                // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5464                let hw1: u16 = 0xEA4F;
5465                let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 5) as u16;
5466                bytes.extend_from_slice(&hw1.to_le_bytes());
5467                bytes.extend_from_slice(&hw2.to_le_bytes());
5468
5469                let hw1: u16 = (0xEB00 | 5) as u16;
5470                let hw2: u16 = ((5 << 8) | r12) as u16;
5471                bytes.extend_from_slice(&hw1.to_le_bytes());
5472                bytes.extend_from_slice(&hw2.to_le_bytes());
5473
5474                // Load 0x0F0F0F0F into R3 (for hi-word)
5475                bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5476                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5477                bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5478                bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5479
5480                let hw1: u16 = (0xEA00 | 5) as u16;
5481                let hw2: u16 = ((5 << 8) | r3) as u16;
5482                bytes.extend_from_slice(&hw1.to_le_bytes());
5483                bytes.extend_from_slice(&hw2.to_le_bytes());
5484
5485                // Step 4
5486                bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5487                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5488                bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5489                bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5490
5491                // MUL R5, R5, R3
5492                // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5493                let hw1: u16 = (0xFB00 | 5) as u16;
5494                let hw2: u16 = (0xF000 | (5 << 8) | r3) as u16;
5495                bytes.extend_from_slice(&hw1.to_le_bytes());
5496                bytes.extend_from_slice(&hw2.to_le_bytes());
5497
5498                // LSR.W R5, R5, #24
5499                // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5500                let hw1: u16 = 0xEA4F;
5501                let hw2: u16 = (0x6000 | (5 << 8) | 0x10 | 5) as u16;
5502                bytes.extend_from_slice(&hw1.to_le_bytes());
5503                bytes.extend_from_slice(&hw2.to_le_bytes());
5504
5505                // #632: the count must be carried ACROSS the scratch restore
5506                // in a register the POP cannot touch. rd is allocator-assigned
5507                // (any of R0-R8) and can land inside the {R3,R4,R5} restore set
5508                // — the old `ADDS rd, R4, R5; POP {R3,R4,R5}` destroyed the
5509                // result one instruction after computing it (0 for every input
5510                // under qemu). R12 is encoder scratch: never allocatable (#212)
5511                // and never in a restore set, so no choice of rd can collide.
5512                // ADD.W R12, R4, R5
5513                bytes.extend_from_slice(&0xEB04u16.to_le_bytes());
5514                bytes.extend_from_slice(&0x0C05u16.to_le_bytes());
5515
5516                // POP {R3, R4, R5}
5517                bytes.extend_from_slice(&0xBC38u16.to_le_bytes());
5518
5519                // MOV rd, R12 — after the restore. The 4-bit Rd (D:rd) form is
5520                // also total over rd = R8, where the old ADDS T1 3-bit field
5521                // silently corrupted the encoding (#178/#180 class).
5522                let mov: u16 =
5523                    (0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7)) as u16;
5524                bytes.extend_from_slice(&mov.to_le_bytes());
5525
5526                // #1048: the former trailing `MOV.W rnhi, #0` hi-word clear
5527                // is GONE — it wrote the OPERAND's home high register (see
5528                // the I64Clz comment). Callers that relied on the implicit
5529                // clear emit their own explicit hi-zero op.
5530
5531                Ok(bytes)
5532            }
5533
5534            // I64Extend8S: Sign-extend low 8 bits to 64 bits
5535            // Result: rdlo = sign_extend_8(rnlo), rdhi = rdlo >> 31
5536            ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
5537                let rdlo_bits = reg_to_bits(rdlo);
5538                let rdhi_bits = reg_to_bits(rdhi);
5539                let rnlo_bits = reg_to_bits(rnlo);
5540                let mut bytes = Vec::new();
5541
5542                // SXTB.W rdlo, rnlo (sign-extend byte to 32-bit)
5543                // SXTB T2: hw1 = 0xFA4F, hw2 = 0xF0<Rd><Rm>
5544                let hw1: u16 = 0xFA4F_u16;
5545                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5546                bytes.extend_from_slice(&hw1.to_le_bytes());
5547                bytes.extend_from_slice(&hw2.to_le_bytes());
5548
5549                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5550                // ASR (immediate): hw1 = 0xEA4F, hw2 = imm3:Rd:imm2:type:Rm
5551                // For imm5=31: imm3=111, imm2=11, type=10 (ASR)
5552                // hw2 = (7 << 12) | (rdhi << 8) | (3 << 6) | (2 << 4) | rdlo
5553                let hw1: u16 = 0xEA4F;
5554                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5555                bytes.extend_from_slice(&hw1.to_le_bytes());
5556                bytes.extend_from_slice(&hw2.to_le_bytes());
5557
5558                Ok(bytes)
5559            }
5560
5561            // I64Extend16S: Sign-extend low 16 bits to 64 bits
5562            // Result: rdlo = sign_extend_16(rnlo), rdhi = rdlo >> 31
5563            ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
5564                let rdlo_bits = reg_to_bits(rdlo);
5565                let rdhi_bits = reg_to_bits(rdhi);
5566                let rnlo_bits = reg_to_bits(rnlo);
5567                let mut bytes = Vec::new();
5568
5569                // SXTH.W rdlo, rnlo (sign-extend halfword to 32-bit)
5570                // SXTH T2: hw1 = 0xFA0F, hw2 = 0xF0<Rd><Rm>
5571                let hw1: u16 = 0xFA0F_u16;
5572                let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5573                bytes.extend_from_slice(&hw1.to_le_bytes());
5574                bytes.extend_from_slice(&hw2.to_le_bytes());
5575
5576                // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5577                let hw1: u16 = 0xEA4F;
5578                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5579                bytes.extend_from_slice(&hw1.to_le_bytes());
5580                bytes.extend_from_slice(&hw2.to_le_bytes());
5581
5582                Ok(bytes)
5583            }
5584
5585            // I64Extend32S: Sign-extend low 32 bits to 64 bits
5586            // Result: rdlo = rnlo, rdhi = rnlo >> 31
5587            ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
5588                let rdlo_bits = reg_to_bits(rdlo);
5589                let rdhi_bits = reg_to_bits(rdhi);
5590                let rnlo_bits = reg_to_bits(rnlo);
5591                let mut bytes = Vec::new();
5592
5593                // MOV rdlo, rnlo (if different)
5594                if rdlo_bits != rnlo_bits {
5595                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5596                    let d_bit = ((rdlo_bits >> 3) & 1) as u16;
5597                    let mov: u16 = 0x4600
5598                        | (d_bit << 7)
5599                        | ((rnlo_bits as u16) << 3)
5600                        | ((rdlo_bits & 0x7) as u16);
5601                    bytes.extend_from_slice(&mov.to_le_bytes());
5602                }
5603
5604                // ASR.W rdhi, rnlo, #31 (sign-extend to high word)
5605                let hw1: u16 = 0xEA4F;
5606                let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rnlo_bits) as u16;
5607                bytes.extend_from_slice(&hw1.to_le_bytes());
5608                bytes.extend_from_slice(&hw2.to_le_bytes());
5609
5610                Ok(bytes)
5611            }
5612
5613            // SelectMove: IT <cond>; MOV{cond} rd, rm
5614            // Conditional move: only execute MOV if condition is true
5615            ArmOp::SelectMove { rd, rm, cond } => {
5616                let rd_bits = reg_to_bits(rd) as u16;
5617                let rm_bits = reg_to_bits(rm) as u16;
5618
5619                // Condition code encoding for IT block
5620                use synth_synthesis::Condition;
5621                let cond_bits: u16 = match cond {
5622                    Condition::EQ => 0x0, // Equal
5623                    Condition::NE => 0x1, // Not equal
5624                    Condition::HS => 0x2, // Higher or same (unsigned >=)
5625                    Condition::LO => 0x3, // Lower (unsigned <)
5626                    Condition::HI => 0x8, // Higher (unsigned >)
5627                    Condition::LS => 0x9, // Lower or same (unsigned <=)
5628                    Condition::GE => 0xA, // Greater or equal (signed)
5629                    Condition::LT => 0xB, // Less than (signed)
5630                    Condition::GT => 0xC, // Greater than (signed)
5631                    Condition::LE => 0xD, // Less or equal (signed)
5632                };
5633
5634                // IT <cond>: single Then block (mask = 0x8 for T only)
5635                // IT instruction: 1011 1111 firstcond mask
5636                let it_instr: u16 = 0xBF00 | (cond_bits << 4) | 0x8;
5637
5638                // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5639                // This MOV will only execute if condition is true due to IT block
5640                let d_bit = (rd_bits >> 3) & 1;
5641                let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5642
5643                // Emit: IT <cond>, MOV rd, rm
5644                let mut bytes = it_instr.to_le_bytes().to_vec();
5645                bytes.extend_from_slice(&mov_instr.to_le_bytes());
5646                Ok(bytes)
5647            }
5648
5649            // Popcnt: Population count (count set bits)
5650            // ARM Cortex-M has no native POPCNT, so we implement the bit manipulation algorithm:
5651            // x = x - ((x >> 1) & 0x55555555);
5652            // x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
5653            // x = (x + (x >> 4)) & 0x0F0F0F0F;
5654            // x = x + (x >> 8);
5655            // x = x + (x >> 16);
5656            // return x & 0x3F;
5657            //
5658            // #1021: R12 (IP, never allocatable) is the ONLY scratch. The
5659            // previous expansion borrowed R11 as a second temp — but R11 is
5660            // the WASM linear-memory base, materialized at entry and read by
5661            // every later LDR/STR, and it is NOT in the pushed set, so the
5662            // clobber leaked to the CALLER too (a live memory-safety
5663            // miscompile: loads through `base = x >> 16`). The second temp is
5664            // eliminated the way the healthy i64.popcnt discipline implies —
5665            // never touch an unsaved register — but without its PUSH/POP
5666            // wrapper: the SWAR masks 0x55555555 / 0x33333333 / 0x0F0F0F0F
5667            // are all `0xXYXYXYXY` ThumbExpandImm modified immediates, so
5668            // each AND takes its mask from the instruction itself and R12
5669            // alone carries every intermediate. Straight-line, no branches,
5670            // no stack traffic — nothing to skip on a trap edge.
5671            ArmOp::Popcnt { rd, rm } => {
5672                let rd_bits = reg_to_bits(rd);
5673                // Defensive (#1021): rd = R11/R12/SP/PC would silently
5674                // corrupt the linear-memory base, the expansion's own
5675                // scratch, or the stack. The selector never assigns them
5676                // (pool R0-R8); refuse loudly if that ever changes.
5677                if rd_bits >= 11 {
5678                    return Err(synth_core::Error::synthesis(
5679                        "Popcnt destination must be R0-R10: R11 is the linear-memory \
5680                         base and R12 is the expansion's scratch (#1021)",
5681                    ));
5682                }
5683                let mut bytes = Vec::new();
5684
5685                // First, move rm to rd if they're different
5686                if rd != rm {
5687                    let rm_bits = reg_to_bits(rm) as u16;
5688                    // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5689                    let d_bit = ((rd_bits as u16) >> 3) & 1;
5690                    let mov_instr: u16 =
5691                        0x4600 | (d_bit << 7) | (rm_bits << 3) | ((rd_bits as u16) & 0x7);
5692                    bytes.extend_from_slice(&mov_instr.to_le_bytes());
5693                }
5694
5695                // Step 1: x = x - ((x >> 1) & 0x55555555)
5696                // R12 = rd >> 1
5697                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 1)?);
5698                // R12 = R12 & 0x55555555 (modified immediate, no constant reg)
5699                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(12, 12, 0x5555_5555)?);
5700                // rd = rd - R12
5701                bytes.extend_from_slice(&self.encode_thumb32_sub_reg_raw(rd_bits, rd_bits, 12)?);
5702
5703                // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5704                // R12 = rd & 0x33333333
5705                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5706                    12,
5707                    rd_bits,
5708                    0x3333_3333,
5709                )?);
5710                // rd = rd >> 2
5711                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(rd_bits, rd_bits, 2)?);
5712                // rd = rd & 0x33333333
5713                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5714                    rd_bits,
5715                    rd_bits,
5716                    0x3333_3333,
5717                )?);
5718                // rd = rd + R12
5719                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5720
5721                // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5722                // R12 = rd >> 4
5723                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 4)?);
5724                // rd = rd + R12
5725                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5726                // rd = rd & 0x0F0F0F0F
5727                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5728                    rd_bits,
5729                    rd_bits,
5730                    0x0F0F_0F0F,
5731                )?);
5732
5733                // Step 4: x = x + (x >> 8)
5734                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 8)?);
5735                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5736
5737                // Step 5: x = x + (x >> 16)
5738                bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 16)?);
5739                bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5740
5741                // Step 6: return x & 0x3F
5742                bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(rd_bits, rd_bits, 0x3F)?);
5743
5744                Ok(bytes)
5745            }
5746
5747            // I64DivU: 64-bit unsigned division using binary long division
5748            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = quotient
5749            // Uses: R4-R7, R12 as loop counter (avoid R8 for Renode compatibility)
5750            //
5751            // #610: the fixed-ABI wrapper marshals the selector-assigned
5752            // operand registers into the core's fixed regs and lands the
5753            // result in rd — pre-#610 this arm IGNORED its register fields,
5754            // so the selector read its rd pair (e.g. R4:R5) after the core's
5755            // own POP restored the stale caller values over it: 0 for every
5756            // input. A zero divisor now traps (UDF #0), per WASM semantics.
5757            ArmOp::I64DivU {
5758                rdlo,
5759                rdhi,
5760                rnlo,
5761                rnhi,
5762                rmlo,
5763                rmhi,
5764                elide_zero_guard,
5765            } => {
5766                let mut bytes = Vec::new();
5767                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5768                // #494 phase 2b: elided only under a certificate-discharged
5769                // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
5770                if !elide_zero_guard {
5771                    emit_i64_divisor_zero_trap(&mut bytes);
5772                }
5773
5774                // PUSH {R4-R7} - save scratch registers (NO LR — this is inline code)
5775                // 16-bit PUSH: 1011 010 M rrrrrrrr where M=0 (no LR), r=R4-R7 = 0xF0
5776                // Encoding: 1011 0100 1111 0000 = 0xB4F0
5777                bytes.extend_from_slice(&0xB4F0u16.to_le_bytes());
5778
5779                // Initialize quotient (R4:R5) = 0
5780                bytes.extend_from_slice(&0x2400u16.to_le_bytes()); // MOV R4, #0
5781                bytes.extend_from_slice(&0x2500u16.to_le_bytes()); // MOV R5, #0
5782
5783                // Initialize remainder (R6:R7) = 0
5784                bytes.extend_from_slice(&0x2600u16.to_le_bytes()); // MOV R6, #0
5785                bytes.extend_from_slice(&0x2700u16.to_le_bytes()); // MOV R7, #0
5786
5787                // Initialize loop counter R12 = 64 (use R12 scratch instead of R8)
5788                // MOV.W R12, #64: F04F 0C40
5789                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5790                bytes.extend_from_slice(&0x0C40u16.to_le_bytes());
5791
5792                // Loop start
5793                let loop_start = bytes.len();
5794
5795                // === Loop body: process one bit ===
5796
5797                // 1. Shift quotient R4:R5 left by 1
5798                // LSLS R5, R5, #1 (16-bit: 0000 0010 1010 1101 = 0x006D -> actually 0x002D for LSL R5,R5,#1)
5799                // LSL Rd, Rm, #imm5: 000 00 imm5 Rm Rd = 000 00 00001 101 101 = 0x006D
5800                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5801                // Get carry from R4 into R5: ORR R5, R5, R4 LSR #31
5802                // Thumb-2 ORR with shifted register: EA45 75D4 = ORR.W R5, R5, R4, LSR #31
5803                // 11101010 010 S Rn | 0 imm3 Rd imm2 type Rm
5804                // type=01 (LSR), imm5=31 (imm3=111, imm2=11)
5805                bytes.extend_from_slice(&0xEA45u16.to_le_bytes());
5806                bytes.extend_from_slice(&0x75D4u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5807                // LSLS R4, R4, #1: 000 00 00001 100 100 = 0x0064
5808                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5809
5810                // 2. Shift remainder R6:R7 left by 1, OR in MSB of dividend R1
5811                // LSLS R7, R7, #1
5812                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5813                // ORR.W R7, R7, R6, LSR #31
5814                bytes.extend_from_slice(&0xEA47u16.to_le_bytes());
5815                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5816                // LSLS R6, R6, #1
5817                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5818                // ORR.W R6, R6, R1, LSR #31 (bring in MSB of dividend high)
5819                bytes.extend_from_slice(&0xEA46u16.to_le_bytes());
5820                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5821
5822                // 3. Shift dividend R0:R1 left by 1
5823                // LSLS R1, R1, #1
5824                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5825                // ORR.W R1, R1, R0, LSR #31
5826                bytes.extend_from_slice(&0xEA41u16.to_le_bytes());
5827                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5828                // LSLS R0, R0, #1
5829                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5830
5831                // 4. Compare remainder >= divisor (64-bit unsigned comparison)
5832                // Compare high words first: CMP R7, R3
5833                // CMP Rn, Rm encoding: 0x4280 | (Rm << 3) | Rn
5834                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3 (16-bit)
5835                // BHI means R7 > R3 (unsigned) - definitely subtract
5836                // BLO means R7 < R3 - definitely don't subtract
5837                // BEQ means need to check low words
5838
5839                // If high > divisor high: branch to subtract (forward +offset)
5840                // BHI.N +6 (skip CMP, skip BLO, do subtract)
5841                // BHI: 1101 1000 offset8 where cond=1000 (HI)
5842                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4 (to subtract block)
5843
5844                // If high < divisor high: branch past subtract
5845                // BLO.N +10 (skip to decrement)
5846                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BLO/BCC +12 (past subtract)
5847
5848                // High words equal, compare low: CMP R6, R2
5849                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2 (16-bit)
5850                // BLO/BCC past subtract (skip SUBS+SBC.W+ORR.W = 10 bytes = 4 halfwords from PC+4)
5851                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords (past subtract)
5852
5853                // === Subtract block: remainder -= divisor, quotient |= 1 ===
5854                // SUBS R6, R6, R2
5855                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2 (16-bit)
5856                // SBC R7, R7, R3 (with borrow)
5857                // Thumb-2 SBC.W: EB67 0703 = SBC.W R7, R7, R3
5858                bytes.extend_from_slice(&0xEB67u16.to_le_bytes());
5859                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5860                // ORR R4, R4, #1 (set bit 0 of quotient low)
5861                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5862                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5863
5864                // === Decrement counter and loop ===
5865                // SUBS.W R12, R12, #1 (decrement loop counter)
5866                // SUBS.W R12, R12, #1: F1BC 0C01
5867                bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
5868                bytes.extend_from_slice(&0x0C01u16.to_le_bytes());
5869
5870                // BNE back to loop_start
5871                let branch_offset_bytes = bytes.len() - loop_start + 4; // +4 for pipeline
5872                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5873                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5874                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5875
5876                // === Loop done, move quotient to R0:R1 ===
5877                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5878                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5879
5880                // POP {R4-R7} - restore scratch registers (NO PC — inline code continues)
5881                // 16-bit POP: 1011 110 P rrrrrrrr where P=0 (no PC), r=R4-R7 = 0xF0
5882                // Encoding: 1011 1100 1111 0000 = 0xBCF0
5883                bytes.extend_from_slice(&0xBCF0u16.to_le_bytes());
5884
5885                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5886                Ok(bytes)
5887            }
5888
5889            // I64DivS: 64-bit signed division
5890            // Converts to unsigned, divides, then applies sign
5891            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5892            //   ->  R0:R1 = quotient (signed)
5893            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5894            ArmOp::I64DivS {
5895                rdlo,
5896                rdhi,
5897                rnlo,
5898                rnhi,
5899                rmlo,
5900                rmhi,
5901                elide_zero_guard,
5902                elide_overflow_guard,
5903            } => {
5904                let mut bytes = Vec::new();
5905                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5906                // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
5907                // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
5908                // the #633 overflow guard falls ONLY to
5909                // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
5910                // divisor-nonzero fact alone must keep it.
5911                if !elide_zero_guard {
5912                    emit_i64_divisor_zero_trap(&mut bytes);
5913                }
5914                if !elide_overflow_guard {
5915                    // #633: INT64_MIN / -1 overflows — trap like the i32 path
5916                    // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
5917                    emit_i64_divs_overflow_trap(&mut bytes);
5918                }
5919
5920                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5921                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5922                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5923
5924                // Save result sign in R9: R9 = R1 XOR R3 (sign bit = MSB)
5925                // EOR.W R9, R1, R3
5926                bytes.extend_from_slice(&0xEA81u16.to_le_bytes());
5927                bytes.extend_from_slice(&0x0903u16.to_le_bytes());
5928
5929                // If dividend negative (R1 MSB set), negate it
5930                // TST R1, R1 (check sign)
5931                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5932                // BPL skip_neg_dividend (+10 bytes = 5 halfwords)
5933                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5934
5935                // Negate R0:R1 (64-bit): RSBS R0, R0, #0; SBC R1, R1, R1 LSL #1
5936                // Actually: MVN R0, R0; MVN R1, R1; ADDS R0, R0, #1; ADC R1, R1, #0
5937                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5938                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5939                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5940                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5941                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5942
5943                // If divisor negative (R3 MSB set), negate it
5944                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5945                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5946
5947                // Negate R2:R3
5948                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
5949                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
5950                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
5951                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
5952                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
5953
5954                // === Now do unsigned division (same as I64DivU) ===
5955                // Initialize quotient (R4:R5) = 0
5956                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5957                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5958                // Initialize remainder (R6:R7) = 0
5959                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5960                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5961                // Initialize loop counter R8 = 64
5962                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5963                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5964
5965                let loop_start = bytes.len();
5966
5967                // Shift quotient left
5968                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5969                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5970                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5971                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5972
5973                // Shift remainder left, OR in MSB of dividend
5974                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5975                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5976                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5977                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5978                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5979                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5980
5981                // Shift dividend left
5982                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5983                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5984                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5985                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5986
5987                // Compare and conditionally subtract
5988                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5989                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5990                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5991                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5992                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5993
5994                // Subtract and set quotient bit
5995                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5996                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5997                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5998                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5999                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6000
6001                // Decrement and loop
6002                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6003                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6004
6005                let branch_offset_bytes = bytes.len() - loop_start + 4;
6006                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6007                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6008                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6009
6010                // Move quotient to R0:R1
6011                bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
6012                bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
6013
6014                // If result should be negative (R9 MSB set), negate R0:R1
6015                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9 (check MSB)
6016                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6017                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8 (skip negation)
6018
6019                // Negate result R0:R1
6020                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6021                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6022                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6023                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6024                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6025
6026                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6027                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6028                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6029
6030                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6031                Ok(bytes)
6032            }
6033
6034            // I64RemU: 64-bit unsigned remainder using binary long division
6035            // Same algorithm as I64DivU but returns remainder instead of quotient
6036            // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = remainder
6037            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6038            ArmOp::I64RemU {
6039                rdlo,
6040                rdhi,
6041                rnlo,
6042                rnhi,
6043                rmlo,
6044                rmhi,
6045                elide_zero_guard,
6046            } => {
6047                let mut bytes = Vec::new();
6048                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6049                if !elide_zero_guard {
6050                    emit_i64_divisor_zero_trap(&mut bytes);
6051                }
6052
6053                // PUSH {R4-R8} - save scratch registers (NO LR — inline code)
6054                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6055                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6056
6057                // Initialize quotient (R4:R5) = 0 (computed but not returned)
6058                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6059                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6060                // Initialize remainder (R6:R7) = 0
6061                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6062                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6063                // Initialize loop counter R8 = 64
6064                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6065                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6066
6067                let loop_start = bytes.len();
6068
6069                // Shift quotient left (not needed for result, but keeps algorithm same)
6070                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6071                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6072                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6073                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6074
6075                // Shift remainder left, OR in MSB of dividend
6076                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6077                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6078                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6079                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6080                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6081                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6082
6083                // Shift dividend left
6084                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6085                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6086                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6087                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6088
6089                // Compare and conditionally subtract
6090                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6091                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6092                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6093                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6094                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6095
6096                // Subtract and set quotient bit
6097                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6098                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6099                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6100                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6101                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6102
6103                // Decrement and loop
6104                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6105                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6106
6107                let branch_offset_bytes = bytes.len() - loop_start + 4;
6108                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6109                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6110                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6111
6112                // Move REMAINDER to R0:R1 (difference from I64DivU)
6113                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6114                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6115
6116                // POP {R4-R8} - restore scratch registers (NO PC — inline code continues)
6117                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6118                bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6119
6120                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6121                Ok(bytes)
6122            }
6123
6124            // I64RemS: 64-bit signed remainder
6125            // Remainder sign follows dividend sign (not quotient rule)
6126            // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
6127            //   ->  R0:R1 = remainder (signed, same sign as dividend)
6128            // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6129            ArmOp::I64RemS {
6130                rdlo,
6131                rdhi,
6132                rnlo,
6133                rnhi,
6134                rmlo,
6135                rmhi,
6136                elide_zero_guard,
6137            } => {
6138                let mut bytes = Vec::new();
6139                emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6140                if !elide_zero_guard {
6141                    emit_i64_divisor_zero_trap(&mut bytes);
6142                }
6143
6144                // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
6145                bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6146                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6147
6148                // Save dividend sign in R9 (remainder sign = dividend sign)
6149                // MOV R9, R1 (just need the sign bit)
6150                bytes.extend_from_slice(&0x4689u16.to_le_bytes()); // MOV R9, R1
6151
6152                // If dividend negative (R1 MSB set), negate it
6153                bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
6154                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6155
6156                // Negate R0:R1
6157                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6158                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6159                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6160                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6161                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6162
6163                // If divisor negative (R3 MSB set), negate it
6164                bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
6165                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6166
6167                // Negate R2:R3
6168                bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
6169                bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
6170                bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
6171                bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
6172                bytes.extend_from_slice(&0x0300u16.to_le_bytes());
6173
6174                // === Unsigned division algorithm ===
6175                // Initialize quotient (R4:R5) = 0
6176                bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6177                bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6178                // Initialize remainder (R6:R7) = 0
6179                bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6180                bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6181                // Initialize loop counter R8 = 64
6182                bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6183                bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6184
6185                let loop_start = bytes.len();
6186
6187                // Shift quotient left
6188                bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6189                bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6190                bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6191                bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6192
6193                // Shift remainder left, OR in MSB of dividend
6194                bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6195                bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6196                bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6197                bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6198                bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6199                bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6200
6201                // Shift dividend left
6202                bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6203                bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6204                bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6205                bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6206
6207                // Compare and conditionally subtract
6208                bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6209                bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6210                bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6211                bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6212                bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6213
6214                // Subtract and set quotient bit
6215                bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6216                bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6217                bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6218                bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6219                bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6220
6221                // Decrement and loop
6222                bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6223                bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6224
6225                let branch_offset_bytes = bytes.len() - loop_start + 4;
6226                let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6227                let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6228                bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6229
6230                // Move remainder to R0:R1
6231                bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6232                bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6233
6234                // If original dividend was negative (R9 MSB set), negate remainder
6235                bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9
6236                bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6237                bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6238
6239                // Negate result R0:R1
6240                bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6241                bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6242                bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6243                bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6244                bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6245
6246                // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6247                bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6248                bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6249
6250                emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6251                Ok(bytes)
6252            }
6253
6254            // === F32 VFP single-precision Thumb-2 encodings ===
6255            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6256            ArmOp::F32Add { sd, sn, sm } => {
6257                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A00, sd, sn, sm)?))
6258            }
6259            ArmOp::F32Sub { sd, sn, sm } => {
6260                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A40, sd, sn, sm)?))
6261            }
6262            ArmOp::F32Mul { sd, sn, sm } => {
6263                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE200A00, sd, sn, sm)?))
6264            }
6265            ArmOp::F32Div { sd, sn, sm } => {
6266                Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE800A00, sd, sn, sm)?))
6267            }
6268            ArmOp::F32Abs { sd, sm } => {
6269                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB00AC0, sd, sm)?))
6270            }
6271            ArmOp::F32Neg { sd, sm } => {
6272                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10A40, sd, sm)?))
6273            }
6274            ArmOp::F32Sqrt { sd, sm } => {
6275                Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10AC0, sd, sm)?))
6276            }
6277
6278            // f32 pseudo-ops — multi-instruction sequences
6279            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6280            ArmOp::F32Ceil { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b01),
6281            ArmOp::F32Floor { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b10),
6282            ArmOp::F32Trunc { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b11),
6283            ArmOp::F32Nearest { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b00),
6284            ArmOp::F32Min { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, true),
6285            ArmOp::F32Max { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, false),
6286            ArmOp::F32Copysign { sd, sn, sm } => self.encode_thumb_f32_copysign(sd, sn, sm),
6287
6288            // f32 comparisons — VCMP + VMRS + MOV #0 + IT + MOV #1
6289            ArmOp::F32Eq { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x0),
6290            ArmOp::F32Ne { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x1),
6291            ArmOp::F32Lt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x4),
6292            ArmOp::F32Le { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x9),
6293            ArmOp::F32Gt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xC),
6294            ArmOp::F32Ge { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xA),
6295
6296            ArmOp::F32Const { sd, value } => self.encode_thumb_f32_const(sd, *value),
6297
6298            ArmOp::F32Load { sd, addr } => {
6299                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED900A00, sd, addr)?))
6300            }
6301            ArmOp::F32Store { sd, addr } => {
6302                Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED800A00, sd, addr)?))
6303            }
6304
6305            ArmOp::F32ConvertI32S { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, true),
6306            ArmOp::F32ConvertI32U { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, false),
6307            ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
6308                Err(synth_core::Error::synthesis(
6309                    "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6310                ))
6311            }
6312            ArmOp::F32ReinterpretI32 { sd, rm } => {
6313                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(true, sd, rm)?))
6314            }
6315            ArmOp::I32ReinterpretF32 { rd, sm } => {
6316                Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(false, sm, rd)?))
6317            }
6318            ArmOp::I32TruncF32S { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, true),
6319            ArmOp::I32TruncF32U { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, false),
6320
6321            // === F64 VFP double-precision Thumb-2 encodings ===
6322            // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6323            ArmOp::F64Add { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6324                0xEE300B00, dd, dn, dm,
6325            )?)),
6326            ArmOp::F64Sub { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6327                0xEE300B40, dd, dn, dm,
6328            )?)),
6329            ArmOp::F64Mul { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6330                0xEE200B00, dd, dn, dm,
6331            )?)),
6332            ArmOp::F64Div { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6333                0xEE800B00, dd, dn, dm,
6334            )?)),
6335            ArmOp::F64Abs { dd, dm } => {
6336                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?))
6337            }
6338            ArmOp::F64Neg { dd, dm } => {
6339                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?))
6340            }
6341            ArmOp::F64Sqrt { dd, dm } => {
6342                Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?))
6343            }
6344
6345            // f64 pseudo-ops
6346            // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6347            ArmOp::F64Ceil { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b01),
6348            ArmOp::F64Floor { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b10),
6349            ArmOp::F64Trunc { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b11),
6350            ArmOp::F64Nearest { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b00),
6351            ArmOp::F64Min { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, true),
6352            ArmOp::F64Max { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, false),
6353            ArmOp::F64Copysign { dd, dn, dm } => self.encode_thumb_f64_copysign(dd, dn, dm),
6354
6355            // f64 comparisons
6356            ArmOp::F64Eq { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x0),
6357            ArmOp::F64Ne { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x1),
6358            ArmOp::F64Lt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x4),
6359            ArmOp::F64Le { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x9),
6360            ArmOp::F64Gt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xC),
6361            ArmOp::F64Ge { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xA),
6362
6363            ArmOp::F64Const { dd, value } => self.encode_thumb_f64_const(dd, *value),
6364
6365            ArmOp::F64Load { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6366                0xED900B00, dd, addr,
6367            )?)),
6368            ArmOp::F64Store { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6369                0xED800B00, dd, addr,
6370            )?)),
6371
6372            ArmOp::F64ConvertI32S { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, true),
6373            ArmOp::F64ConvertI32U { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, false),
6374            ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
6375                Err(synth_core::Error::synthesis(
6376                    "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6377                ))
6378            }
6379            ArmOp::F64PromoteF32 { dd, sm } => self.encode_thumb_f64_promote_f32(dd, sm),
6380            ArmOp::F32DemoteF64 { sd, dm } => self.encode_thumb_f32_demote_f64(sd, dm),
6381            ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => Ok(vfp_to_thumb_bytes(
6382                encode_vmov_core_dreg(true, dd, rmlo, rmhi)?,
6383            )),
6384            ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => Ok(vfp_to_thumb_bytes(
6385                encode_vmov_core_dreg(false, dm, rdlo, rdhi)?,
6386            )),
6387            ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
6388                Err(synth_core::Error::synthesis(
6389                    "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
6390                ))
6391            }
6392            ArmOp::I32TruncF64S { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, true),
6393            ArmOp::I32TruncF64U { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, false),
6394
6395            // ===== i64 operations: encode as multi-instruction Thumb-2 sequences =====
6396
6397            // I64Add: ADDS rdlo, rnlo, rmlo; ADC.W rdhi, rnhi, rmhi
6398            ArmOp::I64Add {
6399                rdlo,
6400                rdhi,
6401                rnlo,
6402                rnhi,
6403                rmlo,
6404                rmhi,
6405            } => {
6406                let mut bytes = Vec::new();
6407                // ADDS rdlo, rnlo, rmlo (16-bit)
6408                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adds {
6409                    rd: *rdlo,
6410                    rn: *rnlo,
6411                    op2: Operand2::Reg(*rmlo),
6412                })?);
6413                // ADC.W rdhi, rnhi, rmhi (32-bit)
6414                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adc {
6415                    rd: *rdhi,
6416                    rn: *rnhi,
6417                    op2: Operand2::Reg(*rmhi),
6418                })?);
6419                Ok(bytes)
6420            }
6421
6422            // I64Sub: SUBS rdlo, rnlo, rmlo; SBC.W rdhi, rnhi, rmhi
6423            ArmOp::I64Sub {
6424                rdlo,
6425                rdhi,
6426                rnlo,
6427                rnhi,
6428                rmlo,
6429                rmhi,
6430            } => {
6431                let mut bytes = Vec::new();
6432                // SUBS rdlo, rnlo, rmlo (16-bit)
6433                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Subs {
6434                    rd: *rdlo,
6435                    rn: *rnlo,
6436                    op2: Operand2::Reg(*rmlo),
6437                })?);
6438                // SBC.W rdhi, rnhi, rmhi (32-bit)
6439                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Sbc {
6440                    rd: *rdhi,
6441                    rn: *rnhi,
6442                    op2: Operand2::Reg(*rmhi),
6443                })?);
6444                Ok(bytes)
6445            }
6446
6447            // I64And: AND rdlo, rnlo, rmlo; AND rdhi, rnhi, rmhi
6448            ArmOp::I64And {
6449                rdlo,
6450                rdhi,
6451                rnlo,
6452                rnhi,
6453                rmlo,
6454                rmhi,
6455            } => {
6456                let mut bytes = Vec::new();
6457                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6458                    rd: *rdlo,
6459                    rn: *rnlo,
6460                    op2: Operand2::Reg(*rmlo),
6461                })?);
6462                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6463                    rd: *rdhi,
6464                    rn: *rnhi,
6465                    op2: Operand2::Reg(*rmhi),
6466                })?);
6467                Ok(bytes)
6468            }
6469
6470            // I64Or: ORR rdlo, rnlo, rmlo; ORR rdhi, rnhi, rmhi
6471            ArmOp::I64Or {
6472                rdlo,
6473                rdhi,
6474                rnlo,
6475                rnhi,
6476                rmlo,
6477                rmhi,
6478            } => {
6479                let mut bytes = Vec::new();
6480                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6481                    rd: *rdlo,
6482                    rn: *rnlo,
6483                    op2: Operand2::Reg(*rmlo),
6484                })?);
6485                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6486                    rd: *rdhi,
6487                    rn: *rnhi,
6488                    op2: Operand2::Reg(*rmhi),
6489                })?);
6490                Ok(bytes)
6491            }
6492
6493            // I64Xor: EOR rdlo, rnlo, rmlo; EOR rdhi, rnhi, rmhi
6494            ArmOp::I64Xor {
6495                rdlo,
6496                rdhi,
6497                rnlo,
6498                rnhi,
6499                rmlo,
6500                rmhi,
6501            } => {
6502                let mut bytes = Vec::new();
6503                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6504                    rd: *rdlo,
6505                    rn: *rnlo,
6506                    op2: Operand2::Reg(*rmlo),
6507                })?);
6508                bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6509                    rd: *rdhi,
6510                    rn: *rnhi,
6511                    op2: Operand2::Reg(*rmhi),
6512                })?);
6513                Ok(bytes)
6514            }
6515
6516            // I64Eqz: ORR scratch, lo, hi; ITE EQ; MOV rd, #1; MOV rd, #0
6517            ArmOp::I64Eqz { rd, rnlo, rnhi } => self.encode_thumb(&ArmOp::I64SetCondZ {
6518                rd: *rd,
6519                rn_lo: *rnlo,
6520                rn_hi: *rnhi,
6521            }),
6522
6523            // I64 comparisons: delegate to I64SetCond
6524            ArmOp::I64Eq {
6525                rd,
6526                rnlo,
6527                rnhi,
6528                rmlo,
6529                rmhi,
6530            } => self.encode_thumb(&ArmOp::I64SetCond {
6531                rd: *rd,
6532                rn_lo: *rnlo,
6533                rn_hi: *rnhi,
6534                rm_lo: *rmlo,
6535                rm_hi: *rmhi,
6536                cond: synth_synthesis::Condition::EQ,
6537            }),
6538
6539            ArmOp::I64Ne {
6540                rd,
6541                rnlo,
6542                rnhi,
6543                rmlo,
6544                rmhi,
6545            } => self.encode_thumb(&ArmOp::I64SetCond {
6546                rd: *rd,
6547                rn_lo: *rnlo,
6548                rn_hi: *rnhi,
6549                rm_lo: *rmlo,
6550                rm_hi: *rmhi,
6551                cond: synth_synthesis::Condition::NE,
6552            }),
6553
6554            ArmOp::I64LtS {
6555                rd,
6556                rnlo,
6557                rnhi,
6558                rmlo,
6559                rmhi,
6560            } => self.encode_thumb(&ArmOp::I64SetCond {
6561                rd: *rd,
6562                rn_lo: *rnlo,
6563                rn_hi: *rnhi,
6564                rm_lo: *rmlo,
6565                rm_hi: *rmhi,
6566                cond: synth_synthesis::Condition::LT,
6567            }),
6568
6569            ArmOp::I64LtU {
6570                rd,
6571                rnlo,
6572                rnhi,
6573                rmlo,
6574                rmhi,
6575            } => self.encode_thumb(&ArmOp::I64SetCond {
6576                rd: *rd,
6577                rn_lo: *rnlo,
6578                rn_hi: *rnhi,
6579                rm_lo: *rmlo,
6580                rm_hi: *rmhi,
6581                cond: synth_synthesis::Condition::LO,
6582            }),
6583
6584            ArmOp::I64LeS {
6585                rd,
6586                rnlo,
6587                rnhi,
6588                rmlo,
6589                rmhi,
6590            } => self.encode_thumb(&ArmOp::I64SetCond {
6591                rd: *rd,
6592                rn_lo: *rnlo,
6593                rn_hi: *rnhi,
6594                rm_lo: *rmlo,
6595                rm_hi: *rmhi,
6596                cond: synth_synthesis::Condition::LE,
6597            }),
6598
6599            ArmOp::I64LeU {
6600                rd,
6601                rnlo,
6602                rnhi,
6603                rmlo,
6604                rmhi,
6605            } => self.encode_thumb(&ArmOp::I64SetCond {
6606                rd: *rd,
6607                rn_lo: *rnlo,
6608                rn_hi: *rnhi,
6609                rm_lo: *rmlo,
6610                rm_hi: *rmhi,
6611                cond: synth_synthesis::Condition::LS,
6612            }),
6613
6614            ArmOp::I64GtS {
6615                rd,
6616                rnlo,
6617                rnhi,
6618                rmlo,
6619                rmhi,
6620            } => self.encode_thumb(&ArmOp::I64SetCond {
6621                rd: *rd,
6622                rn_lo: *rnlo,
6623                rn_hi: *rnhi,
6624                rm_lo: *rmlo,
6625                rm_hi: *rmhi,
6626                cond: synth_synthesis::Condition::GT,
6627            }),
6628
6629            ArmOp::I64GtU {
6630                rd,
6631                rnlo,
6632                rnhi,
6633                rmlo,
6634                rmhi,
6635            } => self.encode_thumb(&ArmOp::I64SetCond {
6636                rd: *rd,
6637                rn_lo: *rnlo,
6638                rn_hi: *rnhi,
6639                rm_lo: *rmlo,
6640                rm_hi: *rmhi,
6641                cond: synth_synthesis::Condition::HI,
6642            }),
6643
6644            ArmOp::I64GeS {
6645                rd,
6646                rnlo,
6647                rnhi,
6648                rmlo,
6649                rmhi,
6650            } => self.encode_thumb(&ArmOp::I64SetCond {
6651                rd: *rd,
6652                rn_lo: *rnlo,
6653                rn_hi: *rnhi,
6654                rm_lo: *rmlo,
6655                rm_hi: *rmhi,
6656                cond: synth_synthesis::Condition::GE,
6657            }),
6658
6659            ArmOp::I64GeU {
6660                rd,
6661                rnlo,
6662                rnhi,
6663                rmlo,
6664                rmhi,
6665            } => self.encode_thumb(&ArmOp::I64SetCond {
6666                rd: *rd,
6667                rn_lo: *rnlo,
6668                rn_hi: *rnhi,
6669                rm_lo: *rmlo,
6670                rm_hi: *rmhi,
6671                cond: synth_synthesis::Condition::HS,
6672            }),
6673
6674            // I64Const: MOVW rdlo, lo16; MOVT rdlo, hi16; MOVW rdhi, lo16_hi; MOVT rdhi, hi16_hi
6675            ArmOp::I64Const { rdlo, rdhi, value } => {
6676                let lo32 = *value as u32;
6677                let hi32 = (*value >> 32) as u32;
6678                let mut bytes = Vec::new();
6679                // Load low 32 bits into rdlo
6680                bytes.extend_from_slice(
6681                    &self.encode_thumb32_movw_raw(reg_to_bits(rdlo), lo32 & 0xFFFF)?,
6682                );
6683                if lo32 > 0xFFFF {
6684                    bytes.extend_from_slice(
6685                        &self.encode_thumb32_movt_raw(reg_to_bits(rdlo), lo32 >> 16)?,
6686                    );
6687                }
6688                // Load high 32 bits into rdhi
6689                bytes.extend_from_slice(
6690                    &self.encode_thumb32_movw_raw(reg_to_bits(rdhi), hi32 & 0xFFFF)?,
6691                );
6692                if hi32 > 0xFFFF {
6693                    bytes.extend_from_slice(
6694                        &self.encode_thumb32_movt_raw(reg_to_bits(rdhi), hi32 >> 16)?,
6695                    );
6696                }
6697                Ok(bytes)
6698            }
6699
6700            // I64Ldr: LDR rdlo, [base, offset]; LDR rdhi, [base, offset+4]
6701            ArmOp::I64Ldr { rdlo, rdhi, addr } => {
6702                let mut bytes = Vec::new();
6703                // #372/#382: a memory `i64.load` carries an index register
6704                // (`reg_imm(R11, addr_reg, offset)` = R11 + addr + offset). The
6705                // immediate `encode_thumb32_ldr` below uses only base+offset and
6706                // would SILENTLY DROP `offset_reg` — the #206 defect, here for
6707                // i64. `i64_effective_base` materializes the effective base into
6708                // `ip` (and, when `offset+4 > 0xFFF`, folds the offset in too so
6709                // the function is NOT skipped — #382), returning the residual
6710                // imm12 for the two halves. Frame i64 loads (no `offset_reg`, e.g.
6711                // a spilled local at `[SP, #off]`) keep the plain `[base,#off]`
6712                // form unchanged — so existing output is byte-identical.
6713                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6714                bytes.extend_from_slice(&self.encode_thumb32_ldr(rdlo, &base, offset)?);
6715                bytes.extend_from_slice(&self.encode_thumb32_ldr(
6716                    rdhi,
6717                    &base,
6718                    offset.wrapping_add(4),
6719                )?);
6720                Ok(bytes)
6721            }
6722
6723            // I64Str: STR rdlo, [base, offset]; STR rdhi, [base, offset+4]
6724            ArmOp::I64Str { rdlo, rdhi, addr } => {
6725                let mut bytes = Vec::new();
6726                // #372/#382: same index-materialization + large-offset fold as
6727                // I64Ldr (see above).
6728                let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6729                bytes.extend_from_slice(&self.encode_thumb32_str(rdlo, &base, offset)?);
6730                bytes.extend_from_slice(&self.encode_thumb32_str(
6731                    rdhi,
6732                    &base,
6733                    offset.wrapping_add(4),
6734                )?);
6735                Ok(bytes)
6736            }
6737
6738            // I64ExtendI32S: MOV rdlo, rn; ASR rdhi, rdlo, #31 (sign-extend)
6739            ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
6740                let mut bytes = Vec::new();
6741                if rdlo != rn {
6742                    // MOV rdlo, rn (16-bit)
6743                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6744                        rd: *rdlo,
6745                        op2: Operand2::Reg(*rn),
6746                    })?);
6747                }
6748                // ASR rdhi, rdlo, #31 (sign-extend: fill high word with sign bit)
6749                bytes.extend_from_slice(
6750                    &self.encode_thumb32_shift(rdhi, rdlo, 31, 0b10)?, // ASR type
6751                );
6752                Ok(bytes)
6753            }
6754
6755            // I64ExtendI32U: MOV rdlo, rn; MOV rdhi, #0
6756            ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
6757                let mut bytes = Vec::new();
6758                if rdlo != rn {
6759                    // MOV rdlo, rn
6760                    bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6761                        rd: *rdlo,
6762                        op2: Operand2::Reg(*rn),
6763                    })?);
6764                }
6765                // MOV rdhi, #0 (#916: MOV.W for rdhi >= R8). Unconditional
6766                // site with no branches in the expansion — before the fix this
6767                // emitted the literal two-instruction stream [4608, 2800], half
6768                // of which was `CMP r0,#0` rather than the high-word clear, so
6769                // every i64.extend_i32_u into a high pair leaked stale bits.
6770                emit_thumb_zero_fill(&mut bytes, reg_to_bits(rdhi));
6771                Ok(bytes)
6772            }
6773
6774            // I32WrapI64: MOV rd, rnlo (just take low 32 bits)
6775            ArmOp::I32WrapI64 { rd, rnlo } => {
6776                if rd == rnlo {
6777                    // No-op: already in the right register
6778                    let instr: u16 = 0xBF00; // NOP
6779                    Ok(instr.to_le_bytes().to_vec())
6780                } else {
6781                    // MOV rd, rnlo
6782                    self.encode_thumb(&ArmOp::Mov {
6783                        rd: *rd,
6784                        op2: Operand2::Reg(*rnlo),
6785                    })
6786                }
6787            }
6788
6789            // ===== Helium MVE operations (Thumb-2 encoding) =====
6790            ArmOp::MveLoad { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vldrw(qd, addr))),
6791            ArmOp::MveStore { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vstrw(qd, addr))),
6792            ArmOp::MveConst { qd, bytes } => self.encode_thumb_mve_const(qd, bytes),
6793            ArmOp::MveAnd { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6794                0xEF000150, qd, qn, qm,
6795            ))),
6796            ArmOp::MveOrr { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6797                0xEF200150, qd, qn, qm,
6798            ))),
6799            ArmOp::MveEor { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6800                0xFF000150, qd, qn, qm,
6801            ))),
6802            ArmOp::MveMvn { qd, qm } => {
6803                // VMVN Qd, Qm: 0xFFB005C0 | Qd<<12 | Qm
6804                let qd_enc = qreg_to_num(qd);
6805                let qm_enc = qreg_to_num(qm);
6806                let instr: u32 = 0xFFB005C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6807                Ok(vfp_to_thumb_bytes(instr))
6808            }
6809            ArmOp::MveBic { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6810                0xEF100150, qd, qn, qm,
6811            ))),
6812            ArmOp::MveAddI { qd, qn, qm, size } => {
6813                let sz = mve_size_bits(size);
6814                let base: u32 = 0xEF000840 | (sz << 20);
6815                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6816            }
6817            ArmOp::MveSubI { qd, qn, qm, size } => {
6818                let sz = mve_size_bits(size);
6819                let base: u32 = 0xFF000840 | (sz << 20);
6820                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6821            }
6822            ArmOp::MveMulI { qd, qn, qm, size } => {
6823                let sz = mve_size_bits(size);
6824                let base: u32 = 0xEF000950 | (sz << 20);
6825                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6826            }
6827            ArmOp::MveNegI { qd, qm, size } => {
6828                let sz = mve_size_bits(size);
6829                // VNEG.Sx Qd, Qm
6830                let qd_enc = qreg_to_num(qd);
6831                let qm_enc = qreg_to_num(qm);
6832                let base: u32 = 0xFFB103C0 | (sz << 18);
6833                let instr = base | ((qd_enc * 2) << 12) | (qm_enc * 2);
6834                Ok(vfp_to_thumb_bytes(instr))
6835            }
6836            ArmOp::MveDup { qd, rn, size } => {
6837                let sz = mve_size_bits(size);
6838                let qd_enc = qreg_to_num(qd);
6839                let rn_bits = reg_to_bits(rn);
6840                // VDUP.sz Qd, Rn: EEA0 0B10 variant
6841                // size encoding: 00=32, 01=16, 10=8
6842                let be = match sz {
6843                    0 => 0b00u32, // 8-bit
6844                    1 => 0b01,    // 16-bit
6845                    _ => 0b00,    // 32-bit (default)
6846                };
6847                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12) | (be << 5);
6848                Ok(vfp_to_thumb_bytes(instr))
6849            }
6850            ArmOp::MveExtractLane { rd, qn, lane, size } => {
6851                let qn_enc = qreg_to_num(qn);
6852                let rd_bits = reg_to_bits(rd);
6853                // VMOV.sz Rd, Dn[x] — extract from Q-register lane
6854                // For 32-bit: VMOV Rd, Dn — where Dn is the appropriate D-register
6855                let d_reg = qn_enc * 2 + ((*lane as u32) >> 1);
6856                let lane_in_d = (*lane as u32) & 1;
6857                let _sz = mve_size_bits(size);
6858                // VMOV Rd, Dn[x]: EE10 0B10 for 32-bit
6859                let instr: u32 = 0xEE100B10 | (d_reg << 16) | (rd_bits << 12) | (lane_in_d << 21);
6860                Ok(vfp_to_thumb_bytes(instr))
6861            }
6862            ArmOp::MveInsertLane { qd, rn, lane, size } => {
6863                let qd_enc = qreg_to_num(qd);
6864                let rn_bits = reg_to_bits(rn);
6865                let d_reg = qd_enc * 2 + ((*lane as u32) >> 1);
6866                let lane_in_d = (*lane as u32) & 1;
6867                let _sz = mve_size_bits(size);
6868                // VMOV Dn[x], Rn: EE00 0B10 for 32-bit
6869                let instr: u32 = 0xEE000B10 | (d_reg << 16) | (rn_bits << 12) | (lane_in_d << 21);
6870                Ok(vfp_to_thumb_bytes(instr))
6871            }
6872
6873            // MVE float comparisons — emit VCMP + VPSEL sequence (simplified: just VCMP)
6874            ArmOp::MveCmpEqI { qd, qn, qm, size }
6875            | ArmOp::MveCmpNeI { qd, qn, qm, size }
6876            | ArmOp::MveCmpLtS { qd, qn, qm, size }
6877            | ArmOp::MveCmpLtU { qd, qn, qm, size }
6878            | ArmOp::MveCmpGtS { qd, qn, qm, size }
6879            | ArmOp::MveCmpGtU { qd, qn, qm, size }
6880            | ArmOp::MveCmpLeS { qd, qn, qm, size }
6881            | ArmOp::MveCmpLeU { qd, qn, qm, size }
6882            | ArmOp::MveCmpGeS { qd, qn, qm, size }
6883            | ArmOp::MveCmpGeU { qd, qn, qm, size } => {
6884                // Encode as VADD (placeholder encoding — real implementation
6885                // would use VCMP + VPSEL pair)
6886                let sz = mve_size_bits(size);
6887                let base: u32 = 0xEF000840 | (sz << 20);
6888                Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6889            }
6890
6891            // f32x4 MVE arithmetic
6892            ArmOp::MveAddF32 { qd, qn, qm } => {
6893                // VADD.F32 Qd, Qn, Qm (MVE): 0xEF000D40
6894                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6895            }
6896            ArmOp::MveSubF32 { qd, qn, qm } => {
6897                // VSUB.F32 Qd, Qn, Qm (MVE): 0xEF200D40
6898                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF200D40, qd, qn, qm)))
6899            }
6900            ArmOp::MveMulF32 { qd, qn, qm } => {
6901                // VMUL.F32 Qd, Qn, Qm (MVE): 0xFF000D50
6902                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xFF000D50, qd, qn, qm)))
6903            }
6904            ArmOp::MveNegF32 { qd, qm } => {
6905                let qd_enc = qreg_to_num(qd);
6906                let qm_enc = qreg_to_num(qm);
6907                // VNEG.F32 Qd, Qm: FFB907C0
6908                let instr: u32 = 0xFFB907C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6909                Ok(vfp_to_thumb_bytes(instr))
6910            }
6911            ArmOp::MveAbsF32 { qd, qm } => {
6912                let qd_enc = qreg_to_num(qd);
6913                let qm_enc = qreg_to_num(qm);
6914                // VABS.F32 Qd, Qm: FFB90740
6915                let instr: u32 = 0xFFB90740 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6916                Ok(vfp_to_thumb_bytes(instr))
6917            }
6918            ArmOp::MveCmpEqF32 { qd, qn, qm }
6919            | ArmOp::MveCmpNeF32 { qd, qn, qm }
6920            | ArmOp::MveCmpLtF32 { qd, qn, qm }
6921            | ArmOp::MveCmpLeF32 { qd, qn, qm }
6922            | ArmOp::MveCmpGtF32 { qd, qn, qm }
6923            | ArmOp::MveCmpGeF32 { qd, qn, qm } => {
6924                // Placeholder: encode as VADD.F32 (real impl needs VCMP.F32 + VPSEL)
6925                Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6926            }
6927            ArmOp::MveDupF32 { qd, rn } => {
6928                let qd_enc = qreg_to_num(qd);
6929                let rn_bits = reg_to_bits(rn);
6930                // VDUP.32 Qd, Rn (same encoding as integer VDUP.32)
6931                let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12);
6932                Ok(vfp_to_thumb_bytes(instr))
6933            }
6934            ArmOp::MveExtractLaneF32 { rd, qn, lane } => {
6935                let qn_enc = qreg_to_num(qn);
6936                let rd_bits = reg_to_bits(rd);
6937                // VMOV Rd, Sn where Sn = Q*4 + lane
6938                let s_num = qn_enc * 4 + (*lane as u32);
6939                let (vn, n) = encode_sreg(s_num);
6940                let instr: u32 = 0xEE100A10 | (vn << 16) | (rd_bits << 12) | (n << 7);
6941                Ok(vfp_to_thumb_bytes(instr))
6942            }
6943            ArmOp::MveReplaceLaneF32 { qd, rn, lane } => {
6944                let qd_enc = qreg_to_num(qd);
6945                let rn_bits = reg_to_bits(rn);
6946                // VMOV Sn, Rn where Sn = Q*4 + lane
6947                let s_num = qd_enc * 4 + (*lane as u32);
6948                let (vn, n) = encode_sreg(s_num);
6949                let instr: u32 = 0xEE000A10 | (vn << 16) | (rn_bits << 12) | (n << 7);
6950                Ok(vfp_to_thumb_bytes(instr))
6951            }
6952            ArmOp::MveDivF32 { qd, qn, qm } => {
6953                // Lane-wise: extract 4 S-regs, VDIV, insert back
6954                self.encode_thumb_mve_lane_wise_f32_binop(qd, qn, qm, 0xEE800A00)
6955            }
6956            ArmOp::MveSqrtF32 { qd, qm } => {
6957                // Lane-wise: extract 4 S-regs, VSQRT, insert back
6958                self.encode_thumb_mve_lane_wise_f32_sqrt(qd, qm)
6959            }
6960
6961            // Catch-all for any remaining ops
6962            _ => {
6963                let instr: u16 = 0xBF00; // NOP
6964                Ok(instr.to_le_bytes().to_vec())
6965            }
6966        }
6967    }
6968
6969    // === Thumb-2 VFP multi-instruction helpers ===
6970
6971    /// Encode F32 comparison as Thumb-2: VCMP.F32 + VMRS + MOVS rd,#0 + IT + MOV rd,#1
6972    fn encode_thumb_f32_compare(
6973        &self,
6974        rd: &Reg,
6975        sn: &VfpReg,
6976        sm: &VfpReg,
6977        cond_code: u32,
6978    ) -> Result<Vec<u8>> {
6979        let mut bytes = Vec::new();
6980        let rd_bits = reg_to_bits(rd);
6981
6982        // #709 (bug found under #708/#709): the `MOVS Rd,#0` below is a
6983        // FLAG-SETTING 16-bit move. Emitting it AFTER `VMRS APSR_nzcv, FPSCR`
6984        // (as the original code did) clobbered the N/Z/C/V flags the VMRS just
6985        // transferred from the VFP compare, so the following `IT<cond>` read
6986        // stale flags and every f32 comparison silently returned 0 (verified:
6987        // `flt(1.0,2.0)` → 0 on Cortex-M4F). The 619 harness never caught it
6988        // because it deliberately skipped compare EXECUTION on a false premise
6989        // (unicorn DOES model VMRS→APSR). Fix: materialize the `#0` FIRST, then
6990        // VCMP+VMRS set the flags the `IT` consumes. Instruction sizes are
6991        // unchanged (pure reorder), so the estimator↔encoder oracle (#511) is
6992        // untouched — only the byte ORDER differs.
6993
6994        // MOVS Rd, #0 (16-bit): 0010 0 Rd(3) 0000 0000 — its flag side effect
6995        // is immediately overwritten by the VMRS below.
6996        if rd_bits < 8 {
6997            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
6998            bytes.extend_from_slice(&movs_zero.to_le_bytes());
6999        } else {
7000            // MOV.W Rd, #0 (32-bit Thumb-2)
7001            let hw1: u16 = 0xF04F;
7002            let hw2: u16 = (rd_bits as u16) << 8;
7003            bytes.extend_from_slice(&hw1.to_le_bytes());
7004            bytes.extend_from_slice(&hw2.to_le_bytes());
7005        }
7006
7007        // VCMP.F32 Sn, Sm
7008        let sn_num = vfp_sreg_to_num(sn)?;
7009        let sm_num = vfp_sreg_to_num(sm)?;
7010        let (vd, d) = encode_sreg(sn_num);
7011        let (vm, m) = encode_sreg(sm_num);
7012        let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7013        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7014
7015        // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10 (sets the flags IT consumes)
7016        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7017
7018        // IT<cond> — If-Then for conditional MOV
7019        // IT encoding: 1011 1111 cond(4) mask(4)
7020        // mask = 0x8 for single "then" (IT)
7021        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7022        bytes.extend_from_slice(&it.to_le_bytes());
7023
7024        // MOV Rd, #1 (16-bit, conditional due to IT): 0010 0 Rd(3) 0000 0001
7025        if rd_bits < 8 {
7026            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7027            bytes.extend_from_slice(&mov_one.to_le_bytes());
7028        } else {
7029            // MOV.W Rd, #1 (32-bit)
7030            let hw1: u16 = 0xF04F;
7031            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7032            bytes.extend_from_slice(&hw1.to_le_bytes());
7033            bytes.extend_from_slice(&hw2.to_le_bytes());
7034        }
7035
7036        Ok(bytes)
7037    }
7038
7039    /// Encode F32 constant load as Thumb-2: MOVW + MOVT + VMOV
7040    fn encode_thumb_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
7041        let mut bytes = Vec::new();
7042        let bits = value.to_bits();
7043        let rt: u32 = 12; // R12/IP as temp
7044
7045        // MOVW R12, #lo16
7046        // Thumb-2 MOVW: 11110 i 10 0100 imm4 | 0 imm3 Rd imm8
7047        let lo16 = bits & 0xFFFF;
7048        let imm4 = (lo16 >> 12) & 0xF;
7049        let i_bit = (lo16 >> 11) & 1;
7050        let imm3 = (lo16 >> 8) & 0x7;
7051        let imm8 = lo16 & 0xFF;
7052        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7053        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
7054        bytes.extend_from_slice(&hw1.to_le_bytes());
7055        bytes.extend_from_slice(&hw2.to_le_bytes());
7056
7057        // MOVT R12, #hi16
7058        let hi16 = (bits >> 16) & 0xFFFF;
7059        let imm4 = (hi16 >> 12) & 0xF;
7060        let i_bit = (hi16 >> 11) & 1;
7061        let imm3 = (hi16 >> 8) & 0x7;
7062        let imm8 = hi16 & 0xFF;
7063        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
7064        let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
7065        bytes.extend_from_slice(&hw1.to_le_bytes());
7066        bytes.extend_from_slice(&hw2.to_le_bytes());
7067
7068        // VMOV Sd, R12
7069        let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
7070        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7071
7072        Ok(bytes)
7073    }
7074
7075    /// Encode VMOV + VCVT.F32.xS32 as Thumb-2
7076    fn encode_thumb_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7077        let mut bytes = Vec::new();
7078
7079        // VMOV Sd, Rm
7080        let vmov = encode_vmov_core_sreg(true, sd, rm)?;
7081        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7082
7083        // VCVT.F32.S32/U32 Sd, Sd. Bit 7 (op) = 1 for signed (S32), 0 for
7084        // unsigned (U32): signed = 0xEEB80AC0, unsigned = 0xEEB80A40
7085        // (GI-FPU-002: previously swapped — see the ARM32 twin).
7086        let sd_num = vfp_sreg_to_num(sd)?;
7087        let (vd, d) = encode_sreg(sd_num);
7088        let (vm, m) = encode_sreg(sd_num);
7089        let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
7090        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7091        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7092
7093        Ok(bytes)
7094    }
7095
7096    /// Encode F32 rounding pseudo-op as Thumb-2 via VCVT to integer and back
7097    /// Encode F32 rounding as Thumb-2.
7098    /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
7099    ///
7100    /// For trunc: uses VCVTR.S32.F32 (always truncates).
7101    /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant),
7102    /// then restores FPSCR.
7103    fn encode_thumb_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7104        let mut bytes = Vec::new();
7105        let sm_num = vfp_sreg_to_num(sm)?;
7106        let sd_num = vfp_sreg_to_num(sd)?;
7107        let (vd_s, d_s) = encode_sreg(sd_num);
7108        let (vm_s, m_s) = encode_sreg(sm_num);
7109
7110        if mode == 0b11 {
7111            // Trunc (toward zero): VCVTR.S32.F32 — bit[7]=1, always truncates
7112            let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7113            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7114        } else {
7115            // ceil/floor/nearest: manipulate FPSCR rounding mode
7116            let rt: u32 = 12; // R12/IP as temp
7117
7118            // VMRS R12, FPSCR
7119            let vmrs = 0xEEF10A10 | (rt << 12);
7120            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7121
7122            // BIC.W R12, R12, #(3 << 22) — clear RMode bits [23:22]
7123            // Thumb-2 modified immediate for 3<<22 = 0x00C00000:
7124            // BIC.W encoding: 11110 i 0 0001 S Rn | 0 imm3 Rd imm8
7125            // 0x00C00000 = 0x03 shifted left by 22 => Thumb mod-imm: i=0, imm3=0b101, imm8=0x03
7126            let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF); // BIC, Rn=R12
7127            let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
7128            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7129            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7130
7131            // ORR.W R12, R12, #(mode << 22)
7132            if mode != 0 {
7133                let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF); // ORR, Rn=R12
7134                let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
7135                bytes.extend_from_slice(&orr_hw1.to_le_bytes());
7136                bytes.extend_from_slice(&orr_hw2.to_le_bytes());
7137            }
7138
7139            // VMSR FPSCR, R12
7140            let vmsr = 0xEEE10A10 | (rt << 12);
7141            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7142
7143            // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rmode
7144            let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7145            bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7146
7147            // Restore FPSCR: clear rmode bits back to nearest (default)
7148            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7149            bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7150            bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7151            bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7152        }
7153
7154        // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
7155        let (vd2, d2) = encode_sreg(sd_num);
7156        let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
7157        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
7158
7159        Ok(bytes)
7160    }
7161
7162    /// Encode F32 min/max as Thumb-2: VMOV + VCMP + VMRS + IT + VMOV
7163    fn encode_thumb_f32_minmax(
7164        &self,
7165        sd: &VfpReg,
7166        sn: &VfpReg,
7167        sm: &VfpReg,
7168        is_min: bool,
7169    ) -> Result<Vec<u8>> {
7170        let mut bytes = Vec::new();
7171        let sn_num = vfp_sreg_to_num(sn)?;
7172        let sm_num = vfp_sreg_to_num(sm)?;
7173        let sd_num = vfp_sreg_to_num(sd)?;
7174
7175        // VMOV.F32 Sd, Sn
7176        let (vd, d) = encode_sreg(sd_num);
7177        let (vn, n) = encode_sreg(sn_num);
7178        let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
7179        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sn));
7180
7181        // VCMP.F32 Sn, Sm
7182        let (vm, m) = encode_sreg(sm_num);
7183        let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7184        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7185
7186        // VMRS APSR_nzcv, FPSCR
7187        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7188
7189        // IT GT (for min) or IT MI (for max)
7190        let cond: u16 = if is_min { 0xC } else { 0x4 };
7191        let it: u16 = 0xBF00 | (cond << 4) | 0x8;
7192        bytes.extend_from_slice(&it.to_le_bytes());
7193
7194        // VMOV{cond}.F32 Sd, Sm — conditional VMOV in IT block
7195        let vmov_sm = 0xEEB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7196        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sm));
7197
7198        Ok(bytes)
7199    }
7200
7201    /// Encode F32 copysign as Thumb-2
7202    /// Encode F32 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7203    /// encoder scratch, #212), the flags, and Sd:
7204    ///
7205    ///   VMOV R12, Sm ; CMP R12, #0    (N flag = the sign bit)
7206    ///   VABS.F32 Sd, Sn               (magnitude, sign cleared)
7207    ///   IT MI ; VNEG.F32(MI) Sd, Sd
7208    ///
7209    /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7210    /// The R12 capture happens BEFORE Sd is written, so Sd aliasing Sn or Sm
7211    /// is safe. (The previous sequence staged the magnitude through R0 —
7212    /// clobbering a live allocator-owned value, the #615 class; caught while
7213    /// composing the F64 twin for #369.)
7214    fn encode_thumb_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7215        let mut bytes = Vec::new();
7216
7217        // VMOV R12, Sm (sign source bits)
7218        bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
7219            false,
7220            sm,
7221            &Reg::R12,
7222        )?));
7223        // CMP.W R12, #0 — N = bit31 (the sign, incl. -0.0 / -NaN).
7224        bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7225        bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7226        // VABS.F32 Sd, Sn
7227        let sd_num = vfp_sreg_to_num(sd)?;
7228        let sn_num = vfp_sreg_to_num(sn)?;
7229        let (vd, d) = encode_sreg(sd_num);
7230        let (vn, n) = encode_sreg(sn_num);
7231        let vabs = 0xEEB00AC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7232        bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7233        // IT MI ; VNEG.F32(MI) Sd, Sd
7234        bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7235        let vneg = 0xEEB10A40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7236        bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7237
7238        Ok(bytes)
7239    }
7240
7241    /// Encode F64 comparison as Thumb-2: VCMP.F64 + VMRS + MOV #0 + IT + MOV #1
7242    fn encode_thumb_f64_compare(
7243        &self,
7244        rd: &Reg,
7245        dn: &VfpReg,
7246        dm: &VfpReg,
7247        cond_code: u32,
7248    ) -> Result<Vec<u8>> {
7249        let mut bytes = Vec::new();
7250        let rd_bits = reg_to_bits(rd);
7251
7252        // #712-class fix (found at f64-phase-2 wiring, #369): the 16-bit
7253        // `MOVS Rd,#0` is FLAG-SETTING. The original order emitted it AFTER
7254        // `VMRS APSR_nzcv, FPSCR`, clobbering the N/Z/C/V flags the VMRS just
7255        // transferred, so the following `IT<cond>` read stale flags and every
7256        // f64 comparison silently returned 0 — the exact bug the f32 compare
7257        // encoder shipped with and #712 fixed. Same fix: materialize the `#0`
7258        // FIRST (its flag side effect is overwritten by the VMRS), then
7259        // VCMP+VMRS set the flags the IT consumes. Pure reorder — sizes
7260        // unchanged.
7261
7262        // MOVS Rd, #0
7263        if rd_bits < 8 {
7264            let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
7265            bytes.extend_from_slice(&movs_zero.to_le_bytes());
7266        } else {
7267            let hw1: u16 = 0xF04F;
7268            let hw2: u16 = (rd_bits as u16) << 8;
7269            bytes.extend_from_slice(&hw1.to_le_bytes());
7270            bytes.extend_from_slice(&hw2.to_le_bytes());
7271        }
7272
7273        // VCMP.F64 Dn, Dm
7274        let dn_num = vfp_dreg_to_num(dn)?;
7275        let dm_num = vfp_dreg_to_num(dm)?;
7276        let (vd, d) = encode_dreg(dn_num);
7277        let (vm, m) = encode_dreg(dm_num);
7278        let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7279        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7280
7281        // VMRS APSR_nzcv, FPSCR (sets the flags the IT consumes)
7282        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7283
7284        // IT<cond>
7285        let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7286        bytes.extend_from_slice(&it.to_le_bytes());
7287
7288        // MOV Rd, #1
7289        if rd_bits < 8 {
7290            let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7291            bytes.extend_from_slice(&mov_one.to_le_bytes());
7292        } else {
7293            let hw1: u16 = 0xF04F;
7294            let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7295            bytes.extend_from_slice(&hw1.to_le_bytes());
7296            bytes.extend_from_slice(&hw2.to_le_bytes());
7297        }
7298
7299        Ok(bytes)
7300    }
7301
7302    /// Encode F64 constant load as Thumb-2: MOVW+MOVT (lo32 into R0) + MOVW+MOVT (hi32 into R12) + VMOV Dd, R0, R12
7303    fn encode_thumb_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
7304        let mut bytes = Vec::new();
7305        let bits = value.to_bits();
7306        let lo32 = bits as u32;
7307        let hi32 = (bits >> 32) as u32;
7308
7309        // MOVW R0, #lo16(lo32)
7310        let lo16 = lo32 & 0xFFFF;
7311        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(0, lo16)?);
7312
7313        // MOVT R0, #hi16(lo32)
7314        let hi16 = (lo32 >> 16) & 0xFFFF;
7315        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(0, hi16)?);
7316
7317        // MOVW R12, #lo16(hi32)
7318        let lo16 = hi32 & 0xFFFF;
7319        bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
7320
7321        // MOVT R12, #hi16(hi32)
7322        let hi16 = (hi32 >> 16) & 0xFFFF;
7323        bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
7324
7325        // VMOV Dd, R0, R12
7326        let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
7327        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7328
7329        Ok(bytes)
7330    }
7331
7332    /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as Thumb-2
7333    /// Encode i32 → f64 conversion as Thumb-2. The integer stages through the
7334    /// DESTINATION's own low S-alias (`S(2d)`) — allocator-owned by
7335    /// definition — never S0 (which may hold a live value; the previous
7336    /// pseudo-op's S0 staging was the #615 class). Also fixes the SWAPPED
7337    /// signed/unsigned VCVT bases (bit7 = 1 is SIGNED — the same swap the f32
7338    /// twin had; latent here because f64.convert_i32_* was decode-dropped
7339    /// until #369): clang-verified vcvt.f64.s32 d1,s2 = eeb8 1bc1,
7340    /// vcvt.f64.u32 d1,s2 = eeb8 1b41.
7341    fn encode_thumb_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7342        let dd_num = vfp_dreg_to_num(dd)?;
7343        if dd_num > 7 {
7344            return Err(synth_core::Error::synthesis(format!(
7345                "F64ConvertI32: destination {dd:?} has no S-register alias \
7346                 (D8..D15) — the selector allocates only D0..D7"
7347            )));
7348        }
7349        let mut bytes = Vec::new();
7350
7351        // VMOV S(2d), Rm — stage the integer in the destination's low word.
7352        let (vn_s, n_s) = encode_sreg(2 * dd_num);
7353        let rt = reg_to_bits(rm);
7354        let vmov = 0xEE000A10 | (vn_s << 16) | (rt << 12) | (n_s << 7);
7355        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7356
7357        // VCVT.F64.S32/U32 Dd, S(2d)
7358        let (vd, d) = encode_dreg(dd_num);
7359        let (vm, m) = encode_sreg(2 * dd_num);
7360        let base = if signed { 0xEEB80BC0 } else { 0xEEB80B40 };
7361        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7362        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7363
7364        Ok(bytes)
7365    }
7366
7367    /// Encode VCVT.F64.F32 Dd, Sm as Thumb-2
7368    fn encode_thumb_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7369        let dd_num = vfp_dreg_to_num(dd)?;
7370        let sm_num = vfp_sreg_to_num(sm)?;
7371        let (vd, d) = encode_dreg(dd_num);
7372        let (vm, m) = encode_sreg(sm_num);
7373
7374        let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7375        Ok(vfp_to_thumb_bytes(vcvt))
7376    }
7377
7378    /// Encode VCVT.F32.F64 Sd, Dm (f32.demote_f64) as Thumb-2 — single
7379    /// instruction, round-to-nearest-even per FPSCR default, exactly WASM
7380    /// §4.3.3 demote (clang-verified: vcvt.f32.f64 s1,d2 = eef7 0bc2).
7381    fn encode_thumb_f32_demote_f64(&self, sd: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7382        let sd_num = vfp_sreg_to_num(sd)?;
7383        let dm_num = vfp_dreg_to_num(dm)?;
7384        let (vd, d) = encode_sreg(sd_num);
7385        let (vm, m) = encode_dreg(dm_num);
7386
7387        let vcvt = 0xEEB70BC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7388        Ok(vfp_to_thumb_bytes(vcvt))
7389    }
7390
7391    /// Encode f64 → i32 truncation as Thumb-2 (round-toward-zero VCVT).
7392    ///
7393    /// The 32-bit result stages through the SOURCE's own low S-alias, `S(2m)`,
7394    /// clobbering half of an operand the selector has already popped. The
7395    /// overlapping write is well-defined: VCVT reads its source operand before
7396    /// writing (compilers emit `vcvt.f32.f64 s0, d0` routinely).
7397    ///
7398    /// # The one precondition, and who actually provides it
7399    ///
7400    /// `dm` must be a DEAD TEMP — never a pinned param/local home. That is the
7401    /// whole safety argument, and it is worth naming the guarantor precisely
7402    /// (#946): **`select_with_stack`** provides it, by copying a home into a
7403    /// fresh D-temp first. Visible in the shipped output for
7404    /// `(func (param f64) (result i32) (i32.trunc_f64_s (local.get 0)))`:
7405    ///
7406    /// ```text
7407    ///   vmov r1, r2, d0     ; read the param out of its AAPCS-VFP home D0
7408    ///   vmov d1, r1, r2     ; ...into a fresh D-temp
7409    ///   vcvt.s32.f64 s2, d1 ; convert from the TEMP, staging into its own S2
7410    /// ```
7411    ///
7412    /// `InstructionSelector::select` / `select_default` do NOT provide it —
7413    /// `alloc_vfp_dreg` is a bare round-robin `(n + 1) % 16` with no liveness
7414    /// or home test. That path is not reachable from `synth compile`
7415    /// (`arm_backend.rs` calls `select_with_stack` exclusively; the only
7416    /// non-test caller of `select` is `examples/compile_add.rs`), so this is
7417    /// not a live miscompile — but a caller reaching that `pub` API directly
7418    /// gets no such guarantee.
7419    ///
7420    /// # What this deliberately does NOT claim
7421    ///
7422    /// An earlier version of this comment said the staging register is "never
7423    /// S0, which may hold an unrelated live value (the #615 class)". **That is
7424    /// false**, and measurably so: for
7425    /// `(func (result i32) (i32.trunc_f64_s (f64.const 3.7)))` the shipped
7426    /// compiler emits `vcvt.s32.f64 s0, d0`.
7427    ///
7428    /// It is also unnecessary. S0 is only dangerous as an *unrelated* scratch;
7429    /// here it is always the low half of `dm` itself, which the precondition
7430    /// above already makes dead. Naming a guard the code does not have — and
7431    /// does not need — invites a future reader to lean on it. The dead-temp
7432    /// precondition is the only thing holding this up.
7433    fn encode_thumb_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7434        let dm_num = vfp_dreg_to_num(dm)?;
7435        if dm_num > 7 {
7436            return Err(synth_core::Error::synthesis(format!(
7437                "I32TruncF64: source {dm:?} has no S-register alias \
7438                 (D8..D15) — the selector allocates only D0..D7"
7439            )));
7440        }
7441        let mut bytes = Vec::new();
7442
7443        // VCVT.S32/U32.F64 S(2m), Dm (clang-verified:
7444        // vcvt.s32.f64 s1,d2 = eefd 0bc2 ; vcvt.u32.f64 s1,d2 = eefc 0bc2)
7445        let (vm, m) = encode_dreg(dm_num);
7446        let (vd_s, d_s) = encode_sreg(2 * dm_num);
7447        let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
7448        let vcvt = base | (d_s << 22) | (vd_s << 12) | (m << 5) | vm;
7449        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7450
7451        // VMOV Rd, S(2m)
7452        let rt = reg_to_bits(rd);
7453        let vmov = 0xEE100A10 | (vd_s << 16) | (rt << 12) | (d_s << 7);
7454        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7455
7456        Ok(bytes)
7457    }
7458
7459    /// Encode F64 rounding as a SINGLE Thumb-2 VRINT (FPv5 / cortex-m7dp).
7460    /// `mode` keeps the legacy FPSCR-RMode numbering of the callers —
7461    /// 0b00=nearest(ties-to-even)→VRINTN, 0b01=+inf(ceil)→VRINTP,
7462    /// 0b10=-inf(floor)→VRINTM, 0b11=zero(trunc)→VRINTZ — but the rounding
7463    /// mode is now ENCODED in the instruction, not smuggled through FPSCR.
7464    /// (The previous pseudo-op round-tripped through a 32-bit integer in S0:
7465    /// wrong for |x| >= 2^31, NaN/±inf collapsed to 0, -0.0 lost, and it
7466    /// CLOBBERED S0/R12 behind the allocator's back — the #615 class.)
7467    /// VRINT quietens an sNaN and preserves the sign of ±0.0/NaN per IEEE 754
7468    /// roundToIntegral, which is exactly WASM Core §4.3.3 f64.ceil/floor/
7469    /// trunc/nearest. VRINTN/P/M live in the FE "always-execute" space (never
7470    /// IT-conditional; none of these sequences emits them inside an IT block).
7471    fn encode_thumb_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7472        let dd_num = vfp_dreg_to_num(dd)?;
7473        let dm_num = vfp_dreg_to_num(dm)?;
7474        let (vd, d) = encode_dreg(dd_num);
7475        let (vm, m) = encode_dreg(dm_num);
7476        // clang-verified bases (thumbv7em, fpv5-d16):
7477        //   vrintn.f64 d1,d2 = feb9 1b42 ; vrintp = feba 1b42
7478        //   vrintm.f64 d1,d2 = febb 1b42 ; vrintz = eeb6 1bc2
7479        let base: u32 = match mode {
7480            0b00 => 0xFEB90B40, // VRINTN.F64 (round to nearest, ties to even)
7481            0b01 => 0xFEBA0B40, // VRINTP.F64 (round toward +inf)
7482            0b10 => 0xFEBB0B40, // VRINTM.F64 (round toward -inf)
7483            _ => 0xEEB60BC0,    // VRINTZ.F64 (round toward zero)
7484        };
7485        Ok(vfp_to_thumb_bytes(
7486            base | (d << 22) | (vd << 12) | (m << 5) | vm,
7487        ))
7488    }
7489
7490    /// Encode F64 min/max as Thumb-2 with WASM Core §4.3.3 semantics:
7491    ///
7492    ///   VCMP.F64 Dn, Dm ; VMRS APSR_nzcv, FPSCR
7493    ///   VMINNM.F64/VMAXNM.F64 Dd, Dn, Dm      (FPv5; -0.0 < +0.0 ordered)
7494    ///   IT VS ; VADD.F64(VS) Dd, Dn, Dm       (unordered ⇒ NaN-propagating)
7495    ///
7496    /// VMINNM/VMAXNM alone are IEEE minNum/maxNum, which return the NUMBER
7497    /// when exactly one operand is NaN — WASM requires NaN. The VS-guarded
7498    /// VADD overwrites the result with a quiet NaN whenever the compare was
7499    /// unordered (either operand NaN); on the ordered path VMINNM/VMAXNM
7500    /// order -0.0 below +0.0, matching WASM's min(+0,-0) = -0 / max = +0.
7501    /// Clobbers ONLY Dd and the flags (the previous pseudo-op's ordered IT
7502    /// GT/MI select returned the WRONG operand for NaN and ±0 mixes).
7503    ///
7504    /// Ok-or-Err: `dd` must not alias `dn`/`dm` — the VS fix-up reads them
7505    /// AFTER VMINNM wrote `dd` (the selector always allocates a fresh
7506    /// destination while both sources are still marked live).
7507    fn encode_thumb_f64_minmax(
7508        &self,
7509        dd: &VfpReg,
7510        dn: &VfpReg,
7511        dm: &VfpReg,
7512        is_min: bool,
7513    ) -> Result<Vec<u8>> {
7514        if dd == dn || dd == dm {
7515            return Err(synth_core::Error::synthesis(format!(
7516                "F64{}: destination {dd:?} aliases a source ({dn:?},{dm:?}) — \
7517                 the unordered NaN fix-up would read a clobbered operand \
7518                 (compiler bug: the selector must allocate a fresh D-temp)",
7519                if is_min { "Min" } else { "Max" },
7520            )));
7521        }
7522        let mut bytes = Vec::new();
7523        let dd_num = vfp_dreg_to_num(dd)?;
7524        let dn_num = vfp_dreg_to_num(dn)?;
7525        let dm_num = vfp_dreg_to_num(dm)?;
7526        let (vd, d) = encode_dreg(dd_num);
7527        let (vn, n) = encode_dreg(dn_num);
7528        let (vm, m) = encode_dreg(dm_num);
7529
7530        // VCMP.F64 Dn, Dm (clang-verified: vcmp.f64 d2,d3 = eeb4 2b43)
7531        let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7532        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7533        // VMRS APSR_nzcv, FPSCR
7534        bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7535        // VMINNM.F64 / VMAXNM.F64 Dd, Dn, Dm (clang-verified:
7536        // vminnm.f64 d1,d2,d3 = fe82 1b43 ; vmaxnm = fe82 1b03)
7537        let base: u32 = if is_min { 0xFE800B40 } else { 0xFE800B00 };
7538        let vnm = base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7539        bytes.extend_from_slice(&vfp_to_thumb_bytes(vnm));
7540        // IT VS (unordered ⇒ at least one NaN operand)
7541        bytes.extend_from_slice(&0xBF68_u16.to_le_bytes());
7542        // VADD.F64(VS) Dd, Dn, Dm — NaN + x propagates a quiet NaN
7543        let vadd = 0xEE300B00 | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7544        bytes.extend_from_slice(&vfp_to_thumb_bytes(vadd));
7545
7546        Ok(bytes)
7547    }
7548
7549    /// Encode F64 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7550    /// encoder scratch, #212), the flags, and Dd:
7551    ///
7552    ///   VMOV R12, S(2m+1)   (high word of the SIGN source Dm)
7553    ///   CMP  R12, #0        (N flag = the sign bit)
7554    ///   VABS.F64 Dd, Dn     (magnitude, sign cleared)
7555    ///   IT MI ; VNEG.F64(MI) Dd, Dd
7556    ///
7557    /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7558    /// The R12 capture happens BEFORE Dd is written, so Dd aliasing Dn or Dm
7559    /// is safe. (The previous pseudo-op clobbered R0/R1/R2 behind the
7560    /// allocator's back — the #615 class.)
7561    fn encode_thumb_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7562        let dm_num = vfp_dreg_to_num(dm)?;
7563        if dm_num > 7 {
7564            return Err(synth_core::Error::synthesis(format!(
7565                "F64Copysign: sign source {dm:?} has no S-register alias \
7566                 (D8..D15) — the selector allocates only D0..D7"
7567            )));
7568        }
7569        let mut bytes = Vec::new();
7570        // VMOV R12, S(2m+1) — the sign source's high word.
7571        let (vn_s, n_s) = encode_sreg(2 * dm_num + 1);
7572        let vmov = 0xEE100A10 | (vn_s << 16) | (12 << 12) | (n_s << 7);
7573        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7574        // CMP R12, #0 (T2: CMP.W R12, #0) — N = bit31 of the sign word.
7575        bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7576        bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7577        // VABS.F64 Dd, Dn
7578        let dd_num = vfp_dreg_to_num(dd)?;
7579        let dn_num = vfp_dreg_to_num(dn)?;
7580        let (vd, d) = encode_dreg(dd_num);
7581        let (vn, n) = encode_dreg(dn_num);
7582        let vabs = 0xEEB00BC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7583        bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7584        // IT MI ; VNEG.F64(MI) Dd, Dd
7585        bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7586        let vneg = 0xEEB10B40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7587        bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7588
7589        Ok(bytes)
7590    }
7591
7592    /// Encode VCVT.S32/U32.F32 + VMOV as Thumb-2
7593    fn encode_thumb_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7594        let mut bytes = Vec::new();
7595
7596        let sm_num = vfp_sreg_to_num(sm)?;
7597        let (vd, d) = encode_sreg(sm_num);
7598        let (vm, m) = encode_sreg(sm_num);
7599        let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
7600        let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7601        bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7602
7603        // VMOV Rd, Sm
7604        let vmov = encode_vmov_core_sreg(false, sm, rd)?;
7605        bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7606
7607        Ok(bytes)
7608    }
7609
7610    // === Thumb-2 32-bit encoding helpers ===
7611
7612    /// Encode Thumb-2 32-bit ADD with immediate
7613    fn encode_thumb32_add(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7614        let rd_bits = reg_to_bits(rd);
7615        let rn_bits = reg_to_bits(rn);
7616
7617        // The `i:imm3:imm8` field is split the same way for both forms.
7618        let i_bit = (imm >> 11) & 1;
7619        let imm3 = (imm >> 8) & 0x7;
7620        let imm8 = imm & 0xFF;
7621
7622        let hw1_base = if imm <= 0xFF {
7623            // ADD.W (T3): the field is a ThumbExpandImm modified immediate. For
7624            // imm <= 0xFF (i:imm3 = 0000) it is the zero-extended byte, which is
7625            // correct — keep this form so existing encodings stay bit-identical.
7626            0xF100
7627        } else if imm <= 0xFFF {
7628            // ADDW (T4): a PLAIN 12-bit immediate (0..4095) — no ThumbExpandImm.
7629            // This is what makes `add sp, sp, #frame` correct for frame sizes
7630            // >= 256, which ADD.W (T3) would silently mis-encode (e.g. #256 -> #0).
7631            0xF200
7632        } else {
7633            return Err(synth_core::Error::synthesis(
7634                "ADD immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7635            ));
7636        };
7637
7638        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7639        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7640
7641        let mut bytes = hw1.to_le_bytes().to_vec();
7642        bytes.extend_from_slice(&hw2.to_le_bytes());
7643        Ok(bytes)
7644    }
7645
7646    /// Encode Thumb-2 32-bit SUB with immediate
7647    fn encode_thumb32_sub(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7648        let rd_bits = reg_to_bits(rd);
7649        let rn_bits = reg_to_bits(rn);
7650
7651        let i_bit = (imm >> 11) & 1;
7652        let imm3 = (imm >> 8) & 0x7;
7653        let imm8 = imm & 0xFF;
7654
7655        let hw1_base = if imm <= 0xFF {
7656            // SUB.W (T3) modified immediate — correct for the zero-extended byte
7657            // (imm <= 0xFF). Kept bit-identical for existing encodings.
7658            0xF1A0
7659        } else if imm <= 0xFFF {
7660            // SUBW (T4): plain 12-bit immediate (0..4095). Makes
7661            // `sub sp, sp, #frame` correct for frame sizes >= 256.
7662            0xF2A0
7663        } else {
7664            return Err(synth_core::Error::synthesis(
7665                "SUB immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7666            ));
7667        };
7668
7669        let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7670        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7671
7672        let mut bytes = hw1.to_le_bytes().to_vec();
7673        bytes.extend_from_slice(&hw2.to_le_bytes());
7674        Ok(bytes)
7675    }
7676
7677    /// Encode Thumb-2 32-bit ADDS with immediate (sets flags)
7678    fn encode_thumb32_adds(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7679        let rd_bits = reg_to_bits(rd);
7680        let rn_bits = reg_to_bits(rn);
7681
7682        // ADDS.W (flag-setting) has only the modified-immediate form — error on
7683        // an un-encodable value rather than silently add the wrong constant.
7684        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7685            synth_core::Error::synthesis(
7686                "ADDS immediate is not a valid ThumbExpandImm — materialize into a register",
7687            )
7688        })?;
7689        let i_bit = (field >> 11) & 1;
7690        let imm3 = (field >> 8) & 0x7;
7691        let imm8 = field & 0xFF;
7692
7693        // ADDS.W Rd, Rn, #imm (with S=1)
7694        // First halfword: 1111 0 i 0 1000 1 Rn = F110 | i<<10 | Rn
7695        let hw1: u16 = (0xF110 | (i_bit << 10) | rn_bits) as u16;
7696        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7697
7698        let mut bytes = hw1.to_le_bytes().to_vec();
7699        bytes.extend_from_slice(&hw2.to_le_bytes());
7700        Ok(bytes)
7701    }
7702
7703    /// Encode Thumb-2 32-bit SUBS with immediate (sets flags)
7704    fn encode_thumb32_subs(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7705        let rd_bits = reg_to_bits(rd);
7706        let rn_bits = reg_to_bits(rn);
7707
7708        // SUBS.W (flag-setting) has only the modified-immediate form — error on
7709        // an un-encodable value rather than silently subtract the wrong constant.
7710        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7711            synth_core::Error::synthesis(
7712                "SUBS immediate is not a valid ThumbExpandImm — materialize into a register",
7713            )
7714        })?;
7715        let i_bit = (field >> 11) & 1;
7716        let imm3 = (field >> 8) & 0x7;
7717        let imm8 = field & 0xFF;
7718
7719        // SUBS.W Rd, Rn, #imm (with S=1)
7720        // First halfword: 1111 0 i 0 1101 1 Rn = F1B0 | i<<10 | Rn
7721        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7722        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7723
7724        let mut bytes = hw1.to_le_bytes().to_vec();
7725        bytes.extend_from_slice(&hw2.to_le_bytes());
7726        Ok(bytes)
7727    }
7728
7729    /// Encode Thumb-2 32-bit MOVW (16-bit immediate)
7730    ///
7731    /// # Contract (Verus-style)
7732    /// ```text
7733    /// requires rd <= R14
7734    /// ensures result.len() == 4
7735    /// ensures (imm & 0xFFFF) can be reconstructed from the encoding
7736    /// ```
7737    fn encode_thumb32_movw(&self, rd: &Reg, imm: u32) -> Result<Vec<u8>> {
7738        let rd_bits = reg_to_bits(rd);
7739        reg_bits_checked(rd_bits)?;
7740        let imm16 = imm & 0xFFFF;
7741
7742        // MOVW Rd, #imm16
7743        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7744        let imm4 = (imm16 >> 12) & 0xF;
7745        let i_bit = (imm16 >> 11) & 1;
7746        let imm3 = (imm16 >> 8) & 0x7;
7747        let imm8 = imm16 & 0xFF;
7748
7749        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7750        let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7751
7752        let mut bytes = hw1.to_le_bytes().to_vec();
7753        bytes.extend_from_slice(&hw2.to_le_bytes());
7754        encoding_contracts::verify_thumb32(&bytes);
7755        Ok(bytes)
7756    }
7757
7758    /// Encode Thumb-2 32-bit shift with immediate
7759    ///
7760    /// # Contract (Verus-style)
7761    /// ```text
7762    /// requires rd <= R14, rm <= R14
7763    /// ensures result.len() == 4
7764    /// ```
7765    fn encode_thumb32_shift(
7766        &self,
7767        rd: &Reg,
7768        rm: &Reg,
7769        shift: u32,
7770        shift_type: u8,
7771    ) -> Result<Vec<u8>> {
7772        let rd_bits = reg_to_bits(rd);
7773        let rm_bits = reg_to_bits(rm);
7774        reg_bits_checked(rd_bits)?;
7775        reg_bits_checked(rm_bits)?;
7776        let imm5 = shift & 0x1F;
7777        let imm2 = imm5 & 0x3;
7778        let imm3 = (imm5 >> 2) & 0x7;
7779
7780        // MOV.W Rd, Rm, <shift> #imm
7781        // EA4F 0 imm3 Rd imm2 type Rm
7782        let hw1: u16 = 0xEA4F;
7783        let hw2: u16 =
7784            ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | ((shift_type as u32) << 4) | rm_bits)
7785                as u16;
7786
7787        let mut bytes = hw1.to_le_bytes().to_vec();
7788        bytes.extend_from_slice(&hw2.to_le_bytes());
7789        Ok(bytes)
7790    }
7791
7792    /// Encode Thumb-2 32-bit shift by register
7793    /// Encoding: 11111010 0xx0 Rn | 1111 Rd 0000 Rm
7794    /// shift_type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
7795    fn encode_thumb32_shift_reg(
7796        &self,
7797        rd: &Reg,
7798        rn: &Reg,
7799        rm: &Reg,
7800        shift_type: u8,
7801    ) -> Result<Vec<u8>> {
7802        let rd_bits = reg_to_bits(rd);
7803        let rn_bits = reg_to_bits(rn);
7804        let rm_bits = reg_to_bits(rm);
7805
7806        // hw1: 1111 1010 0xx0 Rn
7807        let hw1: u16 = (0xFA00 | ((shift_type as u32) << 5) | rn_bits) as u16;
7808        // hw2: 1111 Rd 0000 Rm
7809        let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
7810
7811        let mut bytes = hw1.to_le_bytes().to_vec();
7812        bytes.extend_from_slice(&hw2.to_le_bytes());
7813        Ok(bytes)
7814    }
7815
7816    /// Encode Thumb-2 32-bit CMP with immediate
7817    fn encode_thumb32_cmp_imm(&self, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7818        let rn_bits = reg_to_bits(rn);
7819
7820        // CMP.W has only the modified-immediate form (no plain-imm12 like ADDW),
7821        // so an un-encodable immediate MUST be materialized into a register by
7822        // the selector. Error rather than silently compare the wrong constant.
7823        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7824            synth_core::Error::synthesis(
7825                "CMP immediate is not a valid ThumbExpandImm — materialize into a register",
7826            )
7827        })?;
7828        let i_bit = (field >> 11) & 1;
7829        let imm3 = (field >> 8) & 0x7;
7830        let imm8 = field & 0xFF;
7831
7832        // CMP.W Rn, #imm
7833        let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7834        let hw2: u16 = ((imm3 << 12) | 0x0F00 | imm8) as u16;
7835
7836        let mut bytes = hw1.to_le_bytes().to_vec();
7837        bytes.extend_from_slice(&hw2.to_le_bytes());
7838        Ok(bytes)
7839    }
7840
7841    /// #372/#382: resolve the base register AND residual immediate offset for an
7842    /// `I64Ldr`/`I64Str` whose address may carry an index register. Returns
7843    /// `(base, low_offset)`; the caller accesses the halves at `[base,
7844    /// #low_offset]` and `[base, #low_offset + 4]`.
7845    ///
7846    /// - Frame access (no `offset_reg`, e.g. a spilled local at `[SP, #off]`):
7847    ///   returns `(addr.base, off)` and emits NOTHING — byte-identical.
7848    /// - Memory access (`reg_imm(R11, addr, offset)` = `R11 + addr + offset`)
7849    ///   with `offset + 4 <= 0xFFF`: emits `ADD.W ip, base, index` and returns
7850    ///   `(ip, offset)`, folding `offset`/`offset+4` into the halves' imm12.
7851    ///   Byte-identical to the pre-#382 (#372) behavior.
7852    /// - Memory access with `offset + 4 > 0xFFF`: the imm12 form cannot hold the
7853    ///   high half's offset, so `encode_thumb32_ldr`'s `check_ldst_imm12` (#259)
7854    ///   rightly refused it and the WHOLE function was skipped (#382). Instead
7855    ///   MATERIALIZE the offset into the base: `ADD ip, index, #offset` (against
7856    ///   the read-only INDEX register, so `encode_thumb32_add_imm` never trips its
7857    ///   `rd==rn==R12` alias trap), then `ADD.W ip, ip, base` (+ R11), and return
7858    ///   `(ip, 0)` so the halves use `[ip, #0]` / `[ip, #4]`.
7859    ///
7860    /// The effective address is fully materialized into `ip` BEFORE the halves
7861    /// are accessed, so an `rdlo` aliasing the index register is safe.
7862    fn i64_effective_base(&self, bytes: &mut Vec<u8>, addr: &MemAddr) -> Result<(Reg, u32)> {
7863        let offset = if addr.offset < 0 {
7864            0u32
7865        } else {
7866            addr.offset as u32
7867        };
7868        match addr.offset_reg {
7869            Some(idx) => {
7870                let ip = Reg::R12;
7871                if offset.wrapping_add(4) > 0xFFF {
7872                    // Large static offset (#382): fold it (and R11) into ip so the
7873                    // imm12 halves stay in range instead of skipping the function.
7874                    // ADD ip, index, #offset  (index != ip → no add_imm alias trap)
7875                    bytes.extend_from_slice(&self.encode_thumb32_add_imm(&ip, &idx, offset)?);
7876                    // ADD.W ip, ip, base  (+ R11)
7877                    bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
7878                        reg_to_bits(&ip),
7879                        reg_to_bits(&ip),
7880                        reg_to_bits(&addr.base),
7881                    )?);
7882                    Ok((ip, 0))
7883                } else {
7884                    // ADD.W ip, addr.base, idx  (Thumb-2, byte-verified vs as)
7885                    let hw1: u16 = 0xEB00 | reg_to_bits(&addr.base) as u16;
7886                    let hw2: u16 = 0x0C00 | reg_to_bits(&idx) as u16;
7887                    bytes.extend_from_slice(&hw1.to_le_bytes());
7888                    bytes.extend_from_slice(&hw2.to_le_bytes());
7889                    Ok((ip, offset))
7890                }
7891            }
7892            None => Ok((addr.base, offset)),
7893        }
7894    }
7895
7896    /// Encode Thumb-2 32-bit LDR
7897    fn encode_thumb32_ldr(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7898        let rd_bits = reg_to_bits(rd);
7899        let base_bits = reg_to_bits(base);
7900
7901        // LDR.W Rd, [Rn, #imm12]
7902        check_ldst_imm12(offset)?;
7903        let hw1: u16 = (0xF8D0 | base_bits) as u16;
7904        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7905
7906        let mut bytes = hw1.to_le_bytes().to_vec();
7907        bytes.extend_from_slice(&hw2.to_le_bytes());
7908        Ok(bytes)
7909    }
7910
7911    /// Encode Thumb-2 32-bit STR
7912    fn encode_thumb32_str(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7913        let rd_bits = reg_to_bits(rd);
7914        let base_bits = reg_to_bits(base);
7915
7916        // STR.W Rd, [Rn, #imm12]
7917        check_ldst_imm12(offset)?;
7918        let hw1: u16 = (0xF8C0 | base_bits) as u16;
7919        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7920
7921        let mut bytes = hw1.to_le_bytes().to_vec();
7922        bytes.extend_from_slice(&hw2.to_le_bytes());
7923        Ok(bytes)
7924    }
7925
7926    /// Encode Thumb-2 32-bit LDR with register offset: LDR.W Rd, [Rn, Rm]
7927    fn encode_thumb32_ldr_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7928        let rd_bits = reg_to_bits(rd);
7929        let base_bits = reg_to_bits(base);
7930        let rm_bits = reg_to_bits(offset_reg);
7931
7932        // LDR.W Rd, [Rn, Rm, LSL #0]
7933        // Encoding: 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
7934        // imm2 = 00 for no shift (LSL #0)
7935        let hw1: u16 = (0xF850 | base_bits) as u16;
7936        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7937
7938        let mut bytes = hw1.to_le_bytes().to_vec();
7939        bytes.extend_from_slice(&hw2.to_le_bytes());
7940        Ok(bytes)
7941    }
7942
7943    /// Encode Thumb-2 32-bit STR with register offset: STR.W Rd, [Rn, Rm]
7944    fn encode_thumb32_str_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7945        let rd_bits = reg_to_bits(rd);
7946        let base_bits = reg_to_bits(base);
7947        let rm_bits = reg_to_bits(offset_reg);
7948
7949        // STR.W Rd, [Rn, Rm, LSL #0]
7950        // Encoding: 1111 1000 0100 Rn | Rt 0000 00 imm2 Rm
7951        // imm2 = 00 for no shift (LSL #0)
7952        let hw1: u16 = (0xF840 | base_bits) as u16;
7953        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7954
7955        let mut bytes = hw1.to_le_bytes().to_vec();
7956        bytes.extend_from_slice(&hw2.to_le_bytes());
7957        Ok(bytes)
7958    }
7959
7960    // === Sub-word load/store Thumb-2 encoding helpers ===
7961
7962    /// Encode Thumb-2 32-bit LDRB with immediate: LDRB.W Rd, [Rn, #imm12]
7963    fn encode_thumb32_ldrb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7964        let rd_bits = reg_to_bits(rd);
7965        let base_bits = reg_to_bits(base);
7966        // LDRB.W Rd, [Rn, #imm12]: 1111 1000 1001 Rn | Rt imm12
7967        check_ldst_imm12(offset)?;
7968        let hw1: u16 = (0xF890 | base_bits) as u16;
7969        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7970        let mut bytes = hw1.to_le_bytes().to_vec();
7971        bytes.extend_from_slice(&hw2.to_le_bytes());
7972        Ok(bytes)
7973    }
7974
7975    /// Encode Thumb-2 32-bit LDRB with register: LDRB.W Rd, [Rn, Rm]
7976    fn encode_thumb32_ldrb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7977        let rd_bits = reg_to_bits(rd);
7978        let base_bits = reg_to_bits(base);
7979        let rm_bits = reg_to_bits(offset_reg);
7980        // LDRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0001 Rn | Rt 0000 00 imm2 Rm
7981        let hw1: u16 = (0xF810 | base_bits) as u16;
7982        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7983        let mut bytes = hw1.to_le_bytes().to_vec();
7984        bytes.extend_from_slice(&hw2.to_le_bytes());
7985        Ok(bytes)
7986    }
7987
7988    /// Encode Thumb-2 32-bit LDRSB with immediate: LDRSB.W Rd, [Rn, #imm12]
7989    fn encode_thumb32_ldrsb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7990        let rd_bits = reg_to_bits(rd);
7991        let base_bits = reg_to_bits(base);
7992        // LDRSB.W Rd, [Rn, #imm12]: 1111 1001 1001 Rn | Rt imm12
7993        check_ldst_imm12(offset)?;
7994        let hw1: u16 = (0xF990 | base_bits) as u16;
7995        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7996        let mut bytes = hw1.to_le_bytes().to_vec();
7997        bytes.extend_from_slice(&hw2.to_le_bytes());
7998        Ok(bytes)
7999    }
8000
8001    /// Encode Thumb-2 32-bit LDRSB with register: LDRSB.W Rd, [Rn, Rm]
8002    fn encode_thumb32_ldrsb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8003        let rd_bits = reg_to_bits(rd);
8004        let base_bits = reg_to_bits(base);
8005        let rm_bits = reg_to_bits(offset_reg);
8006        // LDRSB.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0001 Rn | Rt 0000 00 imm2 Rm
8007        let hw1: u16 = (0xF910 | base_bits) as u16;
8008        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
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 LDRH with immediate: LDRH.W Rd, [Rn, #imm12]
8015    fn encode_thumb32_ldrh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8016        let rd_bits = reg_to_bits(rd);
8017        let base_bits = reg_to_bits(base);
8018        // LDRH.W Rd, [Rn, #imm12]: 1111 1000 1011 Rn | Rt imm12
8019        check_ldst_imm12(offset)?;
8020        let hw1: u16 = (0xF8B0 | base_bits) as u16;
8021        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8022        let mut bytes = hw1.to_le_bytes().to_vec();
8023        bytes.extend_from_slice(&hw2.to_le_bytes());
8024        Ok(bytes)
8025    }
8026
8027    /// Encode Thumb-2 32-bit LDRH with register: LDRH.W Rd, [Rn, Rm]
8028    fn encode_thumb32_ldrh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8029        let rd_bits = reg_to_bits(rd);
8030        let base_bits = reg_to_bits(base);
8031        let rm_bits = reg_to_bits(offset_reg);
8032        // LDRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0011 Rn | Rt 0000 00 imm2 Rm
8033        let hw1: u16 = (0xF830 | base_bits) as u16;
8034        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8035        let mut bytes = hw1.to_le_bytes().to_vec();
8036        bytes.extend_from_slice(&hw2.to_le_bytes());
8037        Ok(bytes)
8038    }
8039
8040    /// Encode Thumb-2 32-bit LDRSH with immediate: LDRSH.W Rd, [Rn, #imm12]
8041    fn encode_thumb32_ldrsh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8042        let rd_bits = reg_to_bits(rd);
8043        let base_bits = reg_to_bits(base);
8044        // LDRSH.W Rd, [Rn, #imm12]: 1111 1001 1011 Rn | Rt imm12
8045        check_ldst_imm12(offset)?;
8046        let hw1: u16 = (0xF9B0 | base_bits) as u16;
8047        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8048        let mut bytes = hw1.to_le_bytes().to_vec();
8049        bytes.extend_from_slice(&hw2.to_le_bytes());
8050        Ok(bytes)
8051    }
8052
8053    /// Encode Thumb-2 32-bit LDRSH with register: LDRSH.W Rd, [Rn, Rm]
8054    fn encode_thumb32_ldrsh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8055        let rd_bits = reg_to_bits(rd);
8056        let base_bits = reg_to_bits(base);
8057        let rm_bits = reg_to_bits(offset_reg);
8058        // LDRSH.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0011 Rn | Rt 0000 00 imm2 Rm
8059        let hw1: u16 = (0xF930 | base_bits) as u16;
8060        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8061        let mut bytes = hw1.to_le_bytes().to_vec();
8062        bytes.extend_from_slice(&hw2.to_le_bytes());
8063        Ok(bytes)
8064    }
8065
8066    /// Encode Thumb-2 32-bit STRB with immediate: STRB.W Rd, [Rn, #imm12]
8067    fn encode_thumb32_strb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8068        let rd_bits = reg_to_bits(rd);
8069        let base_bits = reg_to_bits(base);
8070        // STRB.W Rd, [Rn, #imm12]: 1111 1000 1000 Rn | Rt imm12
8071        check_ldst_imm12(offset)?;
8072        let hw1: u16 = (0xF880 | base_bits) as u16;
8073        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8074        let mut bytes = hw1.to_le_bytes().to_vec();
8075        bytes.extend_from_slice(&hw2.to_le_bytes());
8076        Ok(bytes)
8077    }
8078
8079    /// Encode Thumb-2 32-bit STRB with register: STRB.W Rd, [Rn, Rm]
8080    fn encode_thumb32_strb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8081        let rd_bits = reg_to_bits(rd);
8082        let base_bits = reg_to_bits(base);
8083        let rm_bits = reg_to_bits(offset_reg);
8084        // STRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0000 Rn | Rt 0000 00 imm2 Rm
8085        let hw1: u16 = (0xF800 | base_bits) as u16;
8086        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8087        let mut bytes = hw1.to_le_bytes().to_vec();
8088        bytes.extend_from_slice(&hw2.to_le_bytes());
8089        Ok(bytes)
8090    }
8091
8092    /// Encode Thumb-2 32-bit STRH with immediate: STRH.W Rd, [Rn, #imm12]
8093    fn encode_thumb32_strh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8094        let rd_bits = reg_to_bits(rd);
8095        let base_bits = reg_to_bits(base);
8096        // STRH.W Rd, [Rn, #imm12]: 1111 1000 1010 Rn | Rt imm12
8097        check_ldst_imm12(offset)?;
8098        let hw1: u16 = (0xF8A0 | base_bits) as u16;
8099        let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8100        let mut bytes = hw1.to_le_bytes().to_vec();
8101        bytes.extend_from_slice(&hw2.to_le_bytes());
8102        Ok(bytes)
8103    }
8104
8105    /// Encode Thumb-2 32-bit STRH with register: STRH.W Rd, [Rn, Rm]
8106    fn encode_thumb32_strh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8107        let rd_bits = reg_to_bits(rd);
8108        let base_bits = reg_to_bits(base);
8109        let rm_bits = reg_to_bits(offset_reg);
8110        // STRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0010 Rn | Rt 0000 00 imm2 Rm
8111        let hw1: u16 = (0xF820 | base_bits) as u16;
8112        let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8113        let mut bytes = hw1.to_le_bytes().to_vec();
8114        bytes.extend_from_slice(&hw2.to_le_bytes());
8115        Ok(bytes)
8116    }
8117
8118    /// Encode Thumb-2 32-bit ADD with immediate: ADD.W Rd, Rn, #imm
8119    fn encode_thumb32_add_imm(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
8120        let rd_bits = reg_to_bits(rd);
8121        let rn_bits = reg_to_bits(rn);
8122
8123        // In-range immediates (<= 0xFFF) delegate to `encode_thumb32_add`,
8124        // which picks the correct form per value:
8125        //   - imm <= 0xFF  -> ADD.W (T3). Its `i:imm3:imm8` field is a
8126        //     ThumbExpandImm MODIFIED immediate — raw == expanded only here.
8127        //   - 0x100..=0xFFF -> ADDW (T4, 0xF200): a PLAIN 12-bit immediate.
8128        //
8129        // #681: this function used to pack the raw value into the T3 field for
8130        // ALL imm <= 0xFFF. ThumbExpandImm(0x200) = 0 and ThumbExpandImm(0x400)
8131        // = 0x8000_0000, so every dynamic-address load/store with a static
8132        // offset in 0x100..=0xFFF silently computed a WRONG address — and in
8133        // --safety-bounds software the guard checked the intended address while
8134        // the access used the mis-encoded one (bounds bypass). Same
8135        // ThumbExpandImm raw-packing class as #253/#255, reached via #382.
8136        if imm <= 0xFFF {
8137            self.encode_thumb32_add(rd, rn, imm)
8138        } else {
8139            // Out-of-range immediate (> 0xFFF): materialize it into a scratch
8140            // register, then ADD.W Rd, Rn, scratch. This is the #180/#185
8141            // "encoder must produce a legal sequence, not assert" class — see #350.
8142            //
8143            // Scratch choice (must NEVER equal Rn, or Rn would be clobbered before
8144            // the ADD reads it):
8145            //   - rd != rn  => use rd itself (rn is untouched, since rd != rn).
8146            //   - rd == rn  => use R12/IP (the reserved encoder scratch). rd/rn are
8147            //                  never R12 (R12 is non-allocatable), so it can't alias.
8148            //
8149            // The materialized value is the same whether or not MOVT is emitted, so
8150            // the byte length depends only on `imm` (and rd==rn) — the size probe and
8151            // the final emit therefore agree (mandatory: the function is encoded twice).
8152            let scratch: u32 = if rd_bits == rn_bits {
8153                12 // R12/IP — in-place add, can't use rd because rd == rn
8154            } else {
8155                rd_bits // rn is preserved because rd != rn
8156            };
8157            // Invariant: the scratch must never alias Rn (would clobber it before
8158            // the ADD reads it). Unreachable in real codegen (rd/rn are never R12,
8159            // which is reserved encoder scratch), but the encoder is also driven by
8160            // the `encoder_no_panic` fuzz harness with ARBITRARY registers — incl.
8161            // rd==rn==R12, which makes scratch (R12) alias Rn. The encoder contract
8162            // (#180/#185) is Ok-or-Err, never a panic, so return a typed error
8163            // instead of asserting. #350 follow-up.
8164            if scratch == rn_bits {
8165                return Err(synth_core::Error::synthesis(format!(
8166                    "ADD #imm: cannot lower #{imm:#x} for Rd==Rn==R12 — no free scratch \
8167                     register (R12 is the reserved encoder scratch and aliases Rn here)"
8168                )));
8169            }
8170
8171            let lo16 = imm & 0xFFFF;
8172            let hi16 = (imm >> 16) & 0xFFFF;
8173
8174            let mut bytes = self.encode_thumb32_movw_raw(scratch, lo16)?;
8175            if hi16 != 0 {
8176                bytes.extend_from_slice(&self.encode_thumb32_movt_raw(scratch, hi16)?);
8177            }
8178            bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rn_bits, scratch)?);
8179            Ok(bytes)
8180        }
8181    }
8182
8183    // === Raw encoding helpers for POPCNT (take register numbers directly) ===
8184
8185    /// Encode Thumb-2 32-bit MOVW (16-bit immediate) - raw version
8186    ///
8187    /// # Contract (Verus-style)
8188    /// ```text
8189    /// requires rd <= 14, imm16 <= 0xFFFF
8190    /// ensures result.len() == 4
8191    /// ```
8192    fn encode_thumb32_movw_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8193        reg_bits_checked(rd)?;
8194        encoding_contracts::verify_imm16(imm16);
8195        // MOVW Rd, #imm16
8196        // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
8197        let imm16 = imm16 & 0xFFFF;
8198        let imm4 = (imm16 >> 12) & 0xF;
8199        let i_bit = (imm16 >> 11) & 1;
8200        let imm3 = (imm16 >> 8) & 0x7;
8201        let imm8 = imm16 & 0xFF;
8202
8203        let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
8204        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8205
8206        let mut bytes = hw1.to_le_bytes().to_vec();
8207        bytes.extend_from_slice(&hw2.to_le_bytes());
8208        encoding_contracts::verify_thumb32(&bytes);
8209        Ok(bytes)
8210    }
8211
8212    /// Encode Thumb-2 32-bit MOVT (move top 16 bits) - raw version
8213    ///
8214    /// # Contract (Verus-style)
8215    /// ```text
8216    /// requires rd <= 14, imm16 <= 0xFFFF
8217    /// ensures result.len() == 4
8218    /// ```
8219    fn encode_thumb32_movt_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8220        reg_bits_checked(rd)?;
8221        encoding_contracts::verify_imm16(imm16);
8222        // MOVT Rd, #imm16
8223        // 1111 0 i 10 1 1 0 0 imm4 | 0 imm3 Rd imm8
8224        let imm16 = imm16 & 0xFFFF;
8225        let imm4 = (imm16 >> 12) & 0xF;
8226        let i_bit = (imm16 >> 11) & 1;
8227        let imm3 = (imm16 >> 8) & 0x7;
8228        let imm8 = imm16 & 0xFF;
8229
8230        let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
8231        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8232
8233        let mut bytes = hw1.to_le_bytes().to_vec();
8234        bytes.extend_from_slice(&hw2.to_le_bytes());
8235        encoding_contracts::verify_thumb32(&bytes);
8236        Ok(bytes)
8237    }
8238
8239    /// Encode Thumb-2 32-bit LSR (logical shift right) with immediate - raw version
8240    fn encode_thumb32_lsr_raw(&self, rd: u32, rm: u32, shift: u32) -> Result<Vec<u8>> {
8241        // MOV.W Rd, Rm, LSR #imm
8242        // EA4F 0 imm3 Rd imm2 01 Rm
8243        let imm5 = shift & 0x1F;
8244        let imm2 = imm5 & 0x3;
8245        let imm3 = (imm5 >> 2) & 0x7;
8246
8247        let hw1: u16 = 0xEA4F;
8248        let hw2: u16 = ((imm3 << 12) | (rd << 8) | (imm2 << 6) | (0b01 << 4) | rm) as u16;
8249
8250        let mut bytes = hw1.to_le_bytes().to_vec();
8251        bytes.extend_from_slice(&hw2.to_le_bytes());
8252        Ok(bytes)
8253    }
8254
8255    /// Encode Thumb-2 32-bit AND with immediate - raw version
8256    fn encode_thumb32_and_imm_raw(&self, rd: u32, rn: u32, imm: u32) -> Result<Vec<u8>> {
8257        // AND.W Rd, Rn, #<modified_immediate>
8258        // F0 00 Rn | 0 imm3 Rd imm8
8259        //
8260        // #681 class audit: the field is a ThumbExpandImm modified immediate,
8261        // not a raw value. The only current caller (POPCNT final mask) passes
8262        // 0x3F, which expands to itself — the gate is byte-identical today and
8263        // closes the raw-packing landmine for any future caller.
8264        let field = try_thumb_expand_imm(imm).ok_or_else(|| {
8265            synth_core::Error::synthesis(
8266                "AND immediate is not a valid ThumbExpandImm — materialize into a register",
8267            )
8268        })?;
8269        let i_bit = (field >> 11) & 1;
8270        let imm3 = (field >> 8) & 0x7;
8271        let imm8 = field & 0xFF;
8272
8273        let hw1: u16 = (0xF000 | (i_bit << 10) | rn) as u16;
8274        let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8275
8276        let mut bytes = hw1.to_le_bytes().to_vec();
8277        bytes.extend_from_slice(&hw2.to_le_bytes());
8278        Ok(bytes)
8279    }
8280
8281    /// Encode Thumb-2 32-bit SUB (register) - raw version
8282    fn encode_thumb32_sub_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8283        // SUB.W Rd, Rn, Rm
8284        // EBA0 Rn | 0 Rd 00 00 Rm
8285        let hw1: u16 = (0xEBA0 | rn) as u16;
8286        let hw2: u16 = ((rd << 8) | rm) as u16;
8287
8288        let mut bytes = hw1.to_le_bytes().to_vec();
8289        bytes.extend_from_slice(&hw2.to_le_bytes());
8290        Ok(bytes)
8291    }
8292
8293    /// Encode Thumb-2 32-bit ADD (register) - raw version
8294    fn encode_thumb32_add_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8295        // ADD.W Rd, Rn, Rm
8296        // EB00 Rn | 0 Rd 00 00 Rm
8297        let hw1: u16 = (0xEB00 | rn) as u16;
8298        let hw2: u16 = ((rd << 8) | rm) as u16;
8299
8300        let mut bytes = hw1.to_le_bytes().to_vec();
8301        bytes.extend_from_slice(&hw2.to_le_bytes());
8302        Ok(bytes)
8303    }
8304
8305    /// Encode Thumb-2 32-bit ADDS (register, flag-setting) - raw version.
8306    /// Used as the high-register fallback for `ArmOp::Adds` (i64 low-word add)
8307    /// so R8-R11 pair operands don't overflow the 16-bit field — #178/#180.
8308    fn encode_thumb32_adds_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8309        // ADDS.W Rd, Rn, Rm (T3, S=1): EB10 Rn | 0 Rd 00 00 Rm
8310        let hw1: u16 = (0xEB10 | rn) as u16;
8311        let hw2: u16 = ((rd << 8) | rm) as u16;
8312        let mut bytes = hw1.to_le_bytes().to_vec();
8313        bytes.extend_from_slice(&hw2.to_le_bytes());
8314        Ok(bytes)
8315    }
8316
8317    /// Encode Thumb-2 32-bit SUBS (register, flag-setting) - raw version.
8318    /// High-register fallback for `ArmOp::Subs` (i64 low-word subtract) — #178/#180.
8319    fn encode_thumb32_subs_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8320        // SUBS.W Rd, Rn, Rm (T3, S=1): EBB0 Rn | 0 Rd 00 00 Rm
8321        let hw1: u16 = (0xEBB0 | rn) as u16;
8322        let hw2: u16 = ((rd << 8) | rm) as u16;
8323        let mut bytes = hw1.to_le_bytes().to_vec();
8324        bytes.extend_from_slice(&hw2.to_le_bytes());
8325        Ok(bytes)
8326    }
8327
8328    /// Encode a sequence of ARM instructions
8329    pub fn encode_sequence(&self, ops: &[ArmOp]) -> Result<Vec<u8>> {
8330        let mut code = Vec::new();
8331
8332        for op in ops {
8333            let encoded = self.encode(op)?;
8334            code.extend_from_slice(&encoded);
8335        }
8336
8337        Ok(code)
8338    }
8339}
8340
8341/// Convert register to bit encoding (0-15)
8342/// Reverse of the ARMv7-M `ThumbExpandImm`: given a 32-bit immediate, return the
8343/// 12-bit `i:imm3:imm8` field if it is a representable modified immediate, else
8344/// `None` (the caller must materialize the value into a register). This is the
8345/// shared correct path for the data-processing immediate encoders — without it
8346/// they pack raw bits and silently mis-encode any value `> 0xFF` that isn't a
8347/// modified immediate (the silent-miscompile class behind #251/#253/#255).
8348fn try_thumb_expand_imm(value: u32) -> Option<u32> {
8349    // i:imm3 = 0000 → 8-bit value, zero-extended (00000000 00000000 00000000 XY).
8350    if value <= 0xFF {
8351        return Some(value);
8352    }
8353    let b0 = value & 0xFF; // byte 0
8354    let b1 = (value >> 8) & 0xFF; // byte 1
8355    // 0x00XY00XY (i:imm3 = 0001) — XY in bytes 0 and 2
8356    if value == (b0 << 16) | b0 {
8357        return Some(0x100 | b0);
8358    }
8359    // 0xXY00XY00 (i:imm3 = 0010) — XY in bytes 1 and 3
8360    if value == (b1 << 24) | (b1 << 8) {
8361        return Some(0x200 | b1);
8362    }
8363    // 0xXYXYXYXY (i:imm3 = 0011) — XY in all four bytes
8364    if value == (b0 << 24) | (b0 << 16) | (b0 << 8) | b0 {
8365        return Some(0x300 | b0);
8366    }
8367    // An 8-bit value with bit 7 set, rotated right by 8..=31. `rotate_left(rot)`
8368    // undoes the encoded right rotation; if the result is `1bbbbbbb` (0x80..=0xFF)
8369    // the value is representable. imm12[11:7] = rot, imm12[6:0] = low 7 bits.
8370    for rot in 8..=31u32 {
8371        let unrot = value.rotate_left(rot);
8372        if (0x80..=0xFF).contains(&unrot) {
8373            return Some((rot << 7) | (unrot & 0x7F));
8374        }
8375    }
8376    None
8377}
8378
8379/// Guard a Thumb-2 `LDR/STR Rd, [Rn, #imm12]` offset. The imm12 form supports
8380/// `0..=4095`; a larger offset must be materialized into a register by the
8381/// selector (register-offset addressing). Returning `Err` rather than silently
8382/// masking `offset & 0xFFF` closes the wrong-address miscompile class (#259,
8383/// the load/store sibling of #253/#255).
8384fn check_ldst_imm12(offset: u32) -> Result<()> {
8385    if offset > 0xFFF {
8386        Err(synth_core::Error::synthesis(
8387            "load/store immediate offset > 0xFFF (4095) — materialize the offset into a register",
8388        ))
8389    } else {
8390        Ok(())
8391    }
8392}
8393
8394/// #916 — emit `Rd = 0` in Thumb-2, correctly for EVERY destination register.
8395///
8396/// The 16-bit `MOVS Rd, #imm8` (T1) is `0010 0 Rd(3) imm8` — the Rd field is
8397/// **three bits**. For R8-R12 `reg_to_bits` yields 8..12, so `rd_bits << 8`
8398/// overflows into bit 11 and `0x2000 | 0x0800` is `0x2800` = `CMP r0, #0`:
8399/// not a move at all. The destination is never written (it keeps stale data)
8400/// and the flags are clobbered. Same class as #180 / H-CODE-9, and the same
8401/// defect #311 fixed for `I64SetCond`.
8402///
8403/// High registers therefore take the 32-bit `MOV.W Rd, #imm8` (T2,
8404/// `F04F 0000 | Rd<<8 | imm8`), whose Rd field is four bits. `MOV.W` with S=0
8405/// does not set flags, which is what these zero-fill sites want anyway.
8406///
8407/// **Callers with branches must consult [`thumb_zero_fill_halfwords`].** This
8408/// emits 1 halfword for R0-R7 and 2 for R8-R12; any branch whose target lies
8409/// PAST this instruction moves when it widens and its displacement has to be
8410/// derived rather than hard-coded. (A branch targeting this instruction's own
8411/// address is unaffected — an instruction cannot move itself.)
8412fn emit_thumb_zero_fill(bytes: &mut Vec<u8>, rd_bits: u32) {
8413    if rd_bits < 8 {
8414        let movs: u16 = 0x2000 | ((rd_bits as u16) << 8);
8415        bytes.extend_from_slice(&movs.to_le_bytes());
8416    } else {
8417        bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
8418        bytes.extend_from_slice(&((rd_bits as u16) << 8).to_le_bytes());
8419    }
8420}
8421
8422/// Halfword length of the encoding [`emit_thumb_zero_fill`] picks for
8423/// `rd_bits`. Branch displacements spanning the zero-fill derive from this so
8424/// the encoder cannot drift from itself (#916; the byte-size estimator mirrors
8425/// it in `synth_synthesis::estimate_arm_byte_size`, pinned by the #498
8426/// `estimator_encoder_agreement` oracle).
8427fn thumb_zero_fill_halfwords(rd_bits: u32) -> u16 {
8428    if rd_bits < 8 { 1 } else { 2 }
8429}
8430
8431fn reg_to_bits(reg: &Reg) -> u32 {
8432    match reg {
8433        Reg::R0 => 0,
8434        Reg::R1 => 1,
8435        Reg::R2 => 2,
8436        Reg::R3 => 3,
8437        Reg::R4 => 4,
8438        Reg::R5 => 5,
8439        Reg::R6 => 6,
8440        Reg::R7 => 7,
8441        Reg::R8 => 8,
8442        Reg::R9 => 9,
8443        Reg::R10 => 10,
8444        Reg::R11 => 11,
8445        Reg::R12 => 12,
8446        Reg::SP => 13,
8447        Reg::LR => 14,
8448        Reg::PC => 15,
8449    }
8450}
8451
8452// ======================================================================
8453// #610 — i64 fixed-ABI expansion wrappers.
8454//
8455// The hand-written multi-instruction i64 cores (rotl/rotr and the div/rem
8456// shift-subtract loops) compute in FIXED low registers. Before #610 the
8457// div/rem arms ignored their operand fields outright (hardcoded R0:R1 /
8458// R2:R3 in, result to R0:R1) and the rot arms used R3/R4 scratch that
8459// collided with selector-assigned registers — then restored the saved
8460// scratch OVER the result (`POP {R4}` with rd_lo == R4), so the op
8461// returned the caller's stale register: 0 for every input under qemu.
8462//
8463// These wrappers make each core honor its register parameters:
8464//   1. save R0-R3,
8465//   2. marshal the operand registers into the core's fixed input regs via
8466//      the stack (permutation-safe: every source is read before any fixed
8467//      register is written),
8468//   3. run the fixed-reg core (self-preserving for R4+; R12 is encoder
8469//      scratch and never allocatable, #212),
8470//   4. MOV the result pair from R0:R1 into the selector's rd pair,
8471//   5. restore R0-R3, skipping any register the result now occupies.
8472//
8473// All emitted lengths are register-independent so the optimized path's
8474// byte-size estimator (`estimate_arm_byte_size`, pinned by the
8475// estimator↔encoder agreement oracle #498/#511) stays a constant per op.
8476// ======================================================================
8477
8478/// Steps 1+2: `PUSH {R0-R3}`, then marshal `srcs` (operand registers, any of
8479/// R0-R12) into `R0..R<n>` via individual stack pushes. Sources are all read
8480/// before any destination register is written, so arbitrary source/target
8481/// permutations (including operands living in R0-R3) are safe.
8482fn emit_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8483    debug_assert!(srcs.len() <= 4);
8484    // PUSH {R0-R3} — save the caller-visible low registers.
8485    bytes.extend_from_slice(&0xB40Fu16.to_le_bytes());
8486    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8487    for src in srcs.iter().rev() {
8488        let rt = reg_to_bits(src) as u16;
8489        bytes.extend_from_slice(&0xF84Du16.to_le_bytes());
8490        bytes.extend_from_slice(&((rt << 12) | 0x0D04).to_le_bytes());
8491    }
8492    // POP {Ri} — Ri := srcs[i].
8493    for i in 0..srcs.len() as u16 {
8494        bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes());
8495    }
8496}
8497
8498/// Steps 4+5: move the core's R0:R1 result into the selector's rd pair, then
8499/// restore the R0-R3 saved by [`emit_i64_fixed_abi_entry`], skipping any
8500/// register the result now lives in (its saved caller word is discarded).
8501fn emit_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8502    let lo = reg_to_bits(rdlo);
8503    let hi = reg_to_bits(rdhi);
8504    if lo == 1 && hi == 0 {
8505        // A fully swapped pair would clobber one half in either MOV order.
8506        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8507        return Err(synth_core::Error::synthesis(
8508            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8509        ));
8510    }
8511    let mov16 = |bytes: &mut Vec<u8>, rd: u32, rm: u32| {
8512        let d = ((rd >> 3) & 1) as u16;
8513        bytes.extend_from_slice(
8514            &(0x4600u16 | (d << 7) | ((rm as u16) << 3) | ((rd & 7) as u16)).to_le_bytes(),
8515        );
8516    };
8517    if hi == 0 {
8518        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8519        mov16(bytes, lo, 0);
8520        mov16(bytes, hi, 1);
8521    } else {
8522        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8523        mov16(bytes, hi, 1);
8524        mov16(bytes, lo, 0);
8525    }
8526    for i in 0..4u32 {
8527        if i == lo || i == hi {
8528            // The result lives here — drop the saved caller word.
8529            bytes.extend_from_slice(&0xB001u16.to_le_bytes()); // ADD SP, #4
8530        } else {
8531            bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes()); // POP {Ri}
8532        }
8533    }
8534    Ok(())
8535}
8536
8537/// WASM `i64.div_*` / `i64.rem_*` by zero must trap, matching the i32 path's
8538/// cmp/bne/udf guard. Emitted after marshaling, when the divisor pair is in
8539/// R2:R3: `ORRS R12, R2, R3` — `BNE` over a `UDF #0` when nonzero.
8540fn emit_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8541    bytes.extend_from_slice(&0xEA52u16.to_le_bytes()); // ORRS.W R12, R2, R3
8542    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8543    bytes.extend_from_slice(&0xD100u16.to_le_bytes()); // BNE.N +0 (skip the UDF)
8544    bytes.extend_from_slice(&0xDE00u16.to_le_bytes()); // UDF #0 — divide by zero
8545}
8546
8547/// WASM `i64.div_s(INT64_MIN, -1)` must trap (Core §4.3.2 `idiv_s`: the
8548/// quotient +2^63 is unrepresentable), matching the i32 path's overflow
8549/// guard — #633: without it the core negated INT64_MIN onto itself and
8550/// silently returned INT64_MIN. Emitted after marshaling, when the dividend
8551/// pair is in R0:R1 and the divisor pair in R2:R3; R12 is encoder scratch.
8552///
8553/// div_s ONLY — `i64.rem_s(INT64_MIN, -1)` is defined as 0 and must NOT
8554/// trap (`irem_s`), so the I64RemS arm never calls this. 22 bytes,
8555/// register-independent (estimator contract, #498/#511).
8556fn emit_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8557    // AND.W R12, R2, R3 — R12 == 0xFFFFFFFF iff divisor == -1
8558    bytes.extend_from_slice(&0xEA02u16.to_le_bytes());
8559    bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8560    // CMN.W R12, #1 — EQ iff both divisor words are all-ones
8561    bytes.extend_from_slice(&0xF11Cu16.to_le_bytes());
8562    bytes.extend_from_slice(&0x0F01u16.to_le_bytes());
8563    // BNE .no_trap
8564    bytes.extend_from_slice(&0xD105u16.to_le_bytes());
8565    // CMP R0, #0 — dividend lo word of INT64_MIN
8566    bytes.extend_from_slice(&0x2800u16.to_le_bytes());
8567    // BNE .no_trap
8568    bytes.extend_from_slice(&0xD103u16.to_le_bytes());
8569    // CMP.W R1, #0x80000000 — dividend hi word of INT64_MIN
8570    bytes.extend_from_slice(&0xF1B1u16.to_le_bytes());
8571    bytes.extend_from_slice(&0x4F00u16.to_le_bytes());
8572    // BNE .no_trap
8573    bytes.extend_from_slice(&0xD100u16.to_le_bytes());
8574    // UDF #0 — signed-division overflow
8575    bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
8576    // .no_trap:
8577}
8578
8579// ======================================================================
8580// #615 — A32 (ARM-mode) twins of the #610 i64 fixed-ABI wrappers above.
8581// Identical register contract, A32 encodings: the multi-instruction i64
8582// cores (rotl/rotr, div/rem) compute in fixed low registers (value/dividend
8583// R0:R1, amount R2 / divisor R2:R3, result to R0:R1); the wrappers marshal
8584// the selector-assigned operand registers in and the result out, saving and
8585// restoring the caller-visible R0-R3 around the core.
8586// ======================================================================
8587
8588/// A32 steps 1+2: `STMDB SP!, {R0-R3}`, then marshal `srcs` into `R0..R<n>`
8589/// via individual stack pushes (`STR src, [SP, #-4]!` in reverse order, then
8590/// `LDR Ri, [SP], #4`). Every source is read before any fixed register is
8591/// written, so arbitrary source/target permutations are safe.
8592fn emit_a32_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8593    debug_assert!(srcs.len() <= 4);
8594    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8595    // PUSH {R0-R3} — save the caller-visible low registers.
8596    w(bytes, 0xE92D_000F);
8597    // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8598    for src in srcs.iter().rev() {
8599        w(bytes, 0xE52D_0004 | (reg_to_bits(src) << 12));
8600    }
8601    // LDR Ri, [SP], #4 — Ri := srcs[i].
8602    for i in 0..srcs.len() as u32 {
8603        w(bytes, 0xE49D_0004 | (i << 12));
8604    }
8605}
8606
8607/// A32 steps 4+5: move the core's R0:R1 result into the selector's rd pair,
8608/// then restore the R0-R3 saved by [`emit_a32_i64_fixed_abi_entry`], skipping
8609/// any register the result now lives in (its saved caller word is discarded).
8610fn emit_a32_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8611    let lo = reg_to_bits(rdlo);
8612    let hi = reg_to_bits(rdhi);
8613    if lo == 1 && hi == 0 {
8614        // A fully swapped pair would clobber one half in either MOV order.
8615        // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8616        return Err(synth_core::Error::synthesis(
8617            "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8618        ));
8619    }
8620    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8621    let mov = |bytes: &mut Vec<u8>, rd: u32, rm: u32| w(bytes, 0xE1A0_0000 | (rd << 12) | rm);
8622    if hi == 0 {
8623        // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8624        mov(bytes, lo, 0);
8625        mov(bytes, hi, 1);
8626    } else {
8627        // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8628        mov(bytes, hi, 1);
8629        mov(bytes, lo, 0);
8630    }
8631    for i in 0..4u32 {
8632        if i == lo || i == hi {
8633            // The result lives here — drop the saved caller word.
8634            w(bytes, 0xE28D_D004); // ADD SP, SP, #4
8635        } else {
8636            w(bytes, 0xE49D_0004 | (i << 12)); // LDR Ri, [SP], #4
8637        }
8638    }
8639    Ok(())
8640}
8641
8642/// A32 zero-divisor trap, emitted after marshaling when the divisor pair is
8643/// in R2:R3: `ORRS R12, R2, R3` sets Z iff the divisor is zero; `BNE` skips a
8644/// `UDF #0` (WASM div/rem-by-zero must trap, matching the Thumb-2 twin).
8645fn emit_a32_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8646    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8647    w(bytes, 0xE192_C003); // ORRS R12, R2, R3
8648    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8649    w(bytes, 0xE7F0_00F0); // UDF #0 — divide by zero
8650}
8651
8652/// A32 twin of [`emit_i64_divs_overflow_trap`] (#633): trap on
8653/// `i64.div_s(INT64_MIN, -1)`. Conditional execution replaces the Thumb
8654/// branches — the CMPEQ chain leaves EQ set only when divisor == -1 AND
8655/// dividend == INT64_MIN. div_s only; rem_s must keep returning 0.
8656fn emit_a32_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8657    let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8658    w(bytes, 0xE002_C003); // AND   R12, R2, R3 (== 0xFFFFFFFF iff divisor == -1)
8659    w(bytes, 0xE37C_0001); // CMN   R12, #1     (EQ iff divisor == -1)
8660    w(bytes, 0x0350_0000); // CMPEQ R0, #0      (EQ iff also dividend lo == 0)
8661    w(bytes, 0x0351_0102); // CMPEQ R1, #0x80000000 (EQ iff dividend == INT64_MIN)
8662    w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8663    w(bytes, 0xE7F0_00F0); // UDF #0 — signed-division overflow
8664}
8665
8666/// Fallible form of the `verify_reg_bits` contract. PC (R15) is not a valid
8667/// data operand for the Thumb-2 encodings that use this guard (SDIV/UDIV/MLS/…
8668/// are UNPREDICTABLE with PC). Synth's own codegen never emits PC there, but
8669/// the encoder must stay *total* over arbitrary `ArmOp` inputs — the fuzz
8670/// harness (`encoder_no_panic`) requires Ok-or-Err, never a panic. Pre-fix, the
8671/// `debug_assert` in `verify_reg_bits` aborted under `-Cdebug-assertions`.
8672/// Returns a typed Err instead. See #185.
8673fn reg_bits_checked(bits: u32) -> Result<()> {
8674    if bits > 14 {
8675        return Err(synth_core::Error::synthesis(format!(
8676            "register bits {bits} (PC/R15) is not a valid operand for this Thumb-2 encoding"
8677        )));
8678    }
8679    Ok(())
8680}
8681
8682/// Try to encode a 32-bit value as an ARM rotated immediate (imm8 ROR 2*rot4).
8683/// Returns Some((encoded_bits, 1)) if representable, None otherwise.
8684fn try_encode_rotated_imm(val: u32) -> Option<(u32, u32)> {
8685    if val == 0 {
8686        return Some((0, 1));
8687    }
8688    for rot in 0..16u32 {
8689        let shift = rot * 2;
8690        // Rotate left by shift (undo the ROR) to see if result fits in 8 bits
8691        let unrotated = val.rotate_left(shift);
8692        if unrotated <= 0xFF {
8693            // Encoded as: rot4(4 bits) | imm8(8 bits) = rotate_imm << 8 | imm8
8694            return Some(((rot << 8) | unrotated, 1));
8695        }
8696    }
8697    None
8698}
8699
8700/// Encode operand2 field and return (bits, immediate_flag).
8701/// For ARM32 mode, immediates use the rotated-immediate encoding (imm8 ROR 2*rot4).
8702/// Panics if an immediate value cannot be represented. Callers that need large
8703/// immediates should use MOVW/MOVT instead of Operand2::Imm.
8704fn encode_operand2(op2: &Operand2) -> Result<(u32, u32)> {
8705    match op2 {
8706        Operand2::Imm(val) => {
8707            let uval = *val as u32;
8708            // Attempt rotated-immediate encoding (ARM32 Operand2)
8709            if let Some(encoded) = try_encode_rotated_imm(uval) {
8710                Ok(encoded)
8711            } else {
8712                // #378-class honesty: an immediate that can't be expressed as an
8713                // ARM32 rotated immediate is an INTERNAL selector bug — large
8714                // constants must be materialized via MOVW/MOVT, not passed here.
8715                // FAIL HONESTLY with an Err rather than silently masking to
8716                // `uval & 0xFF` and emitting a WRONG immediate. The encoder is
8717                // Ok-or-Err, never corrupt (#180/#185); a loud Err is also why
8718                // this is an Err and not a panic (the `encoder_no_panic` fuzz
8719                // contract — malformed/oversized input must degrade, not crash).
8720                Err(synth_core::Error::synthesis(format!(
8721                    "encode_operand2: immediate {uval:#x} ({val}) is not an ARM32 \
8722                     rotated immediate — the selector must materialize large \
8723                     constants via MOVW/MOVT"
8724                )))
8725            }
8726        }
8727
8728        Operand2::Reg(reg) => {
8729            let reg_bits = reg_to_bits(reg);
8730            Ok((reg_bits, 0)) // I=0 for register
8731        }
8732
8733        Operand2::RegShift {
8734            rm,
8735            shift: _,
8736            amount,
8737        } => {
8738            // Simplified encoding with shift
8739            let rm_bits = reg_to_bits(rm);
8740            let shift_bits = (*amount & 0x1F) << 7;
8741            Ok((shift_bits | rm_bits, 0))
8742        }
8743    }
8744}
8745
8746/// Encode memory address to (base_reg, offset)
8747fn encode_mem_addr(addr: &MemAddr) -> (u32, u32) {
8748    let base_bits = reg_to_bits(&addr.base);
8749    let offset_bits = (addr.offset as u32) & 0xFFF; // 12-bit offset
8750    (base_bits, offset_bits)
8751}
8752
8753/// S-register number: S0=0, S1=1, ..., S31=31
8754fn vfp_sreg_to_num(reg: &VfpReg) -> Result<u32> {
8755    match reg {
8756        VfpReg::S0 => Ok(0),
8757        VfpReg::S1 => Ok(1),
8758        VfpReg::S2 => Ok(2),
8759        VfpReg::S3 => Ok(3),
8760        VfpReg::S4 => Ok(4),
8761        VfpReg::S5 => Ok(5),
8762        VfpReg::S6 => Ok(6),
8763        VfpReg::S7 => Ok(7),
8764        VfpReg::S8 => Ok(8),
8765        VfpReg::S9 => Ok(9),
8766        VfpReg::S10 => Ok(10),
8767        VfpReg::S11 => Ok(11),
8768        VfpReg::S12 => Ok(12),
8769        VfpReg::S13 => Ok(13),
8770        VfpReg::S14 => Ok(14),
8771        VfpReg::S15 => Ok(15),
8772        VfpReg::S16 => Ok(16),
8773        VfpReg::S17 => Ok(17),
8774        VfpReg::S18 => Ok(18),
8775        VfpReg::S19 => Ok(19),
8776        VfpReg::S20 => Ok(20),
8777        VfpReg::S21 => Ok(21),
8778        VfpReg::S22 => Ok(22),
8779        VfpReg::S23 => Ok(23),
8780        VfpReg::S24 => Ok(24),
8781        VfpReg::S25 => Ok(25),
8782        VfpReg::S26 => Ok(26),
8783        VfpReg::S27 => Ok(27),
8784        VfpReg::S28 => Ok(28),
8785        VfpReg::S29 => Ok(29),
8786        VfpReg::S30 => Ok(30),
8787        VfpReg::S31 => Ok(31),
8788        // D-registers are not used in F32 single-precision encodings
8789        _ => Err(synth_core::Error::SynthesisError(
8790            "D-register not supported in single-precision VFP encoding".to_string(),
8791        )),
8792    }
8793}
8794
8795/// D-register number: D0=0, D1=1, ..., D15=15
8796fn vfp_dreg_to_num(reg: &VfpReg) -> Result<u32> {
8797    match reg {
8798        VfpReg::D0 => Ok(0),
8799        VfpReg::D1 => Ok(1),
8800        VfpReg::D2 => Ok(2),
8801        VfpReg::D3 => Ok(3),
8802        VfpReg::D4 => Ok(4),
8803        VfpReg::D5 => Ok(5),
8804        VfpReg::D6 => Ok(6),
8805        VfpReg::D7 => Ok(7),
8806        VfpReg::D8 => Ok(8),
8807        VfpReg::D9 => Ok(9),
8808        VfpReg::D10 => Ok(10),
8809        VfpReg::D11 => Ok(11),
8810        VfpReg::D12 => Ok(12),
8811        VfpReg::D13 => Ok(13),
8812        VfpReg::D14 => Ok(14),
8813        VfpReg::D15 => Ok(15),
8814        // S-registers are not used in F64 double-precision encodings
8815        _ => Err(synth_core::Error::SynthesisError(
8816            "S-register not supported in double-precision VFP encoding".to_string(),
8817        )),
8818    }
8819}
8820
8821/// Split S-register into (Vx[3:0], qualifier_bit) for VFP encoding.
8822/// For an S-register number s: Vx = s >> 1, qualifier = s & 1.
8823/// The qualifier bit goes to D (bit 22), N (bit 7), or M (bit 5) depending on role.
8824fn encode_sreg(s: u32) -> (u32, u32) {
8825    (s >> 1, s & 1)
8826}
8827
8828/// Split D-register into (Vx[3:0], qualifier_bit) for VFP double-precision encoding.
8829/// For a D-register number d: Vx = d & 0xF, qualifier = (d >> 4) & 1.
8830/// For D0-D15, qualifier is always 0.
8831fn encode_dreg(d: u32) -> (u32, u32) {
8832    (d & 0xF, (d >> 4) & 1)
8833}
8834
8835/// Encode a VFP 3-register arithmetic instruction (VADD.F32, VSUB.F32, VMUL.F32, VDIV.F32).
8836/// Returns the full 32-bit instruction word.
8837///
8838/// VFP encoding: [cond 1110] [D opc1 Vn] [Vd 101 sz] [N opc2 M 0 Vm]
8839/// For single-precision (sz=0), coprocessor = 0xA (bits[11:8]).
8840fn encode_vfp_3reg(base: u32, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<u32> {
8841    let sd_num = vfp_sreg_to_num(sd)?;
8842    let sn_num = vfp_sreg_to_num(sn)?;
8843    let sm_num = vfp_sreg_to_num(sm)?;
8844    let (vd, d) = encode_sreg(sd_num);
8845    let (vn, n) = encode_sreg(sn_num);
8846    let (vm, m) = encode_sreg(sm_num);
8847
8848    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8849}
8850
8851/// Encode a VFP 2-register instruction (VNEG.F32, VABS.F32, VSQRT.F32).
8852/// Returns the full 32-bit instruction word.
8853fn encode_vfp_2reg(base: u32, sd: &VfpReg, sm: &VfpReg) -> Result<u32> {
8854    let sd_num = vfp_sreg_to_num(sd)?;
8855    let sm_num = vfp_sreg_to_num(sm)?;
8856    let (vd, d) = encode_sreg(sd_num);
8857    let (vm, m) = encode_sreg(sm_num);
8858
8859    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8860}
8861
8862/// Encode a VFP load/store (VLDR.F32 / VSTR.F32).
8863/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8864/// U bit (bit 23) controls add/subtract offset.
8865fn encode_vfp_ldst(base: u32, sd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8866    let sd_num = vfp_sreg_to_num(sd)?;
8867    let (vd, d) = encode_sreg(sd_num);
8868    let rn = reg_to_bits(&addr.base);
8869
8870    let offset = addr.offset;
8871    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8872    let abs_offset = offset.unsigned_abs();
8873    let imm8 = (abs_offset / 4) & 0xFF;
8874
8875    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8876}
8877
8878/// Encode VMOV between core register and S-register.
8879/// VMOV Sn, Rt: 0xEE00_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8880/// VMOV Rt, Sn: 0xEE10_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8881fn encode_vmov_core_sreg(to_sreg: bool, sreg: &VfpReg, core: &Reg) -> Result<u32> {
8882    let s_num = vfp_sreg_to_num(sreg)?;
8883    let (vn, n) = encode_sreg(s_num);
8884    let rt = reg_to_bits(core);
8885
8886    let base = if to_sreg { 0xEE000A10 } else { 0xEE100A10 };
8887    Ok(base | (vn << 16) | (rt << 12) | (n << 7))
8888}
8889
8890/// Encode a VFP 3-register double-precision instruction (VADD.F64, VSUB.F64, etc.).
8891/// For double-precision (sz=1), coprocessor = 0xB (bits[11:8]).
8892/// The base should have bit 8 = 1 for F64 (0xB suffix instead of 0xA).
8893fn encode_vfp_3reg_f64(base: u32, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<u32> {
8894    let dd_num = vfp_dreg_to_num(dd)?;
8895    let dn_num = vfp_dreg_to_num(dn)?;
8896    let dm_num = vfp_dreg_to_num(dm)?;
8897    let (vd, d) = encode_dreg(dd_num);
8898    let (vn, n) = encode_dreg(dn_num);
8899    let (vm, m) = encode_dreg(dm_num);
8900
8901    Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8902}
8903
8904/// Encode a VFP 2-register double-precision instruction (VNEG.F64, VABS.F64, VSQRT.F64).
8905fn encode_vfp_2reg_f64(base: u32, dd: &VfpReg, dm: &VfpReg) -> Result<u32> {
8906    let dd_num = vfp_dreg_to_num(dd)?;
8907    let dm_num = vfp_dreg_to_num(dm)?;
8908    let (vd, d) = encode_dreg(dd_num);
8909    let (vm, m) = encode_dreg(dm_num);
8910
8911    Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8912}
8913
8914/// Encode a VFP load/store for double-precision (VLDR.64 / VSTR.64).
8915/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8916fn encode_vfp_ldst_f64(base: u32, dd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8917    let dd_num = vfp_dreg_to_num(dd)?;
8918    let (vd, d) = encode_dreg(dd_num);
8919    let rn = reg_to_bits(&addr.base);
8920
8921    let offset = addr.offset;
8922    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8923    let abs_offset = offset.unsigned_abs();
8924    let imm8 = (abs_offset / 4) & 0xFF;
8925
8926    Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8927}
8928
8929/// Encode VMOV between two core registers and a D-register.
8930/// VMOV Dm, Rt, Rt2: 0xEC40_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8931/// VMOV Rt, Rt2, Dm: 0xEC50_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8932fn encode_vmov_core_dreg(
8933    to_dreg: bool,
8934    dreg: &VfpReg,
8935    core_lo: &Reg,
8936    core_hi: &Reg,
8937) -> Result<u32> {
8938    let d_num = vfp_dreg_to_num(dreg)?;
8939    let (vm, m) = encode_dreg(d_num);
8940    let rt = reg_to_bits(core_lo);
8941    let rt2 = reg_to_bits(core_hi);
8942
8943    let base = if to_dreg { 0xEC400B10 } else { 0xEC500B10 };
8944    Ok(base | (rt2 << 16) | (rt << 12) | (m << 5) | vm)
8945}
8946
8947/// Emit a VFP 32-bit instruction as Thumb-2 bytes (two LE halfwords).
8948fn vfp_to_thumb_bytes(instr: u32) -> Vec<u8> {
8949    let hw1 = ((instr >> 16) & 0xFFFF) as u16;
8950    let hw2 = (instr & 0xFFFF) as u16;
8951    let mut bytes = hw1.to_le_bytes().to_vec();
8952    bytes.extend_from_slice(&hw2.to_le_bytes());
8953    bytes
8954}
8955
8956// ============================================================================
8957// Helium MVE encoding helpers
8958// ============================================================================
8959
8960/// Q-register number: Q0=0, Q1=1, ..., Q7=7
8961fn qreg_to_num(reg: &QReg) -> u32 {
8962    match reg {
8963        QReg::Q0 => 0,
8964        QReg::Q1 => 1,
8965        QReg::Q2 => 2,
8966        QReg::Q3 => 3,
8967        QReg::Q4 => 4,
8968        QReg::Q5 => 5,
8969        QReg::Q6 => 6,
8970        QReg::Q7 => 7,
8971    }
8972}
8973
8974/// MVE element size to encoding bits: S8=0b00, S16=0b01, S32=0b10
8975fn mve_size_bits(size: &MveSize) -> u32 {
8976    match size {
8977        MveSize::S8 => 0b00,
8978        MveSize::S16 => 0b01,
8979        MveSize::S32 => 0b10,
8980    }
8981}
8982
8983/// Encode MVE 3-register instruction.
8984/// Q-registers are encoded as D-register pairs: Q0=D0:D1, Q1=D2:D3, etc.
8985/// In NEON/MVE encoding, the Q-register uses D-register number = Qn * 2.
8986fn encode_mve_3reg(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
8987    let d = qreg_to_num(qd) * 2;
8988    let n = qreg_to_num(qn) * 2;
8989    let m = qreg_to_num(qm) * 2;
8990
8991    // Standard NEON/MVE 3-register encoding:
8992    // D bit (bit 22) = Vd[4], Vd[3:0] = bits [15:12]
8993    // N bit (bit 7)  = Vn[4], Vn[3:0] = bits [19:16]
8994    // M bit (bit 5)  = Vm[4], Vm[3:0] = bits [3:0]
8995    let vd = d & 0xF;
8996    let d_bit = (d >> 4) & 1;
8997    let vn = n & 0xF;
8998    let n_bit = (n >> 4) & 1;
8999    let vm = m & 0xF;
9000    let m_bit = (m >> 4) & 1;
9001
9002    base | (d_bit << 22) | (vn << 16) | (vd << 12) | (n_bit << 7) | (m_bit << 5) | vm
9003}
9004
9005/// Encode MVE 3-register bitwise instruction (VAND, VORR, VEOR, VBIC).
9006fn encode_mve_3reg_bitwise(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
9007    encode_mve_3reg(base, qd, qn, qm)
9008}
9009
9010/// Encode MVE VLDRW.32 Qd, [Rn, #offset]
9011/// Format: EC9x xxxx - contiguous load, word-sized elements
9012fn encode_mve_vldrw(qd: &QReg, addr: &MemAddr) -> u32 {
9013    let qd_enc = qreg_to_num(qd) * 2;
9014    let rn = reg_to_bits(&addr.base);
9015    let offset = addr.offset;
9016    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9017    let abs_offset = offset.unsigned_abs();
9018    let imm7 = (abs_offset / 4) & 0x7F; // 7-bit word-aligned offset
9019
9020    // VLDRW.32 Qd, [Rn, #imm]: ED10 xx80 variant
9021    0xED100E80
9022        | (u_bit << 23)
9023        | ((qd_enc >> 4) << 22)
9024        | (rn << 16)
9025        | ((qd_enc & 0xF) << 12)
9026        | (imm7 & 0x7F)
9027}
9028
9029/// Encode MVE VSTRW.32 Qd, [Rn, #offset]
9030fn encode_mve_vstrw(qd: &QReg, addr: &MemAddr) -> u32 {
9031    let qd_enc = qreg_to_num(qd) * 2;
9032    let rn = reg_to_bits(&addr.base);
9033    let offset = addr.offset;
9034    let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9035    let abs_offset = offset.unsigned_abs();
9036    let imm7 = (abs_offset / 4) & 0x7F;
9037
9038    0xED000E80
9039        | (u_bit << 23)
9040        | ((qd_enc >> 4) << 22)
9041        | (rn << 16)
9042        | ((qd_enc & 0xF) << 12)
9043        | (imm7 & 0x7F)
9044}
9045
9046impl ArmEncoder {
9047    /// Encode MVE constant load: MOVW+MOVT+VMOV for each 32-bit word, then assemble Q-register
9048    fn encode_thumb_mve_const(&self, qd: &QReg, bytes: &[u8; 16]) -> Result<Vec<u8>> {
9049        let mut result = Vec::new();
9050        let qd_num = qreg_to_num(qd);
9051
9052        // Load each 32-bit word into R12 (temp) then VMOV into S-register
9053        for i in 0..4 {
9054            let word = u32::from_le_bytes([
9055                bytes[i * 4],
9056                bytes[i * 4 + 1],
9057                bytes[i * 4 + 2],
9058                bytes[i * 4 + 3],
9059            ]);
9060            let lo16 = word & 0xFFFF;
9061            let hi16 = (word >> 16) & 0xFFFF;
9062
9063            // MOVW R12, #lo16
9064            result.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
9065            // MOVT R12, #hi16
9066            if hi16 != 0 {
9067                result.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
9068            }
9069
9070            // VMOV Sn, R12 where Sn = Qd*4 + i
9071            let s_num = qd_num * 4 + i as u32;
9072            let (vn, n) = encode_sreg(s_num);
9073            let vmov: u32 = 0xEE000A10 | (vn << 16) | (12 << 12) | (n << 7);
9074            result.extend_from_slice(&vfp_to_thumb_bytes(vmov));
9075        }
9076
9077        Ok(result)
9078    }
9079
9080    /// Encode lane-wise f32 binary operation (VDIV, etc.) via S-register extraction
9081    fn encode_thumb_mve_lane_wise_f32_binop(
9082        &self,
9083        qd: &QReg,
9084        qn: &QReg,
9085        qm: &QReg,
9086        vfp_base: u32,
9087    ) -> Result<Vec<u8>> {
9088        let mut result = Vec::new();
9089        let qd_num = qreg_to_num(qd);
9090        let qn_num = qreg_to_num(qn);
9091        let qm_num = qreg_to_num(qm);
9092
9093        // For each lane 0..3: use S-registers directly (Q aliasing)
9094        for i in 0..4u32 {
9095            let sd = qd_num * 4 + i;
9096            let sn = qn_num * 4 + i;
9097            let sm = qm_num * 4 + i;
9098
9099            let (vd, d) = encode_sreg(sd);
9100            let (vn, n) = encode_sreg(sn);
9101            let (vm, m) = encode_sreg(sm);
9102
9103            let instr = vfp_base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
9104            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9105        }
9106
9107        Ok(result)
9108    }
9109
9110    /// Encode lane-wise f32 VSQRT via S-register extraction
9111    fn encode_thumb_mve_lane_wise_f32_sqrt(&self, qd: &QReg, qm: &QReg) -> Result<Vec<u8>> {
9112        let mut result = Vec::new();
9113        let qd_num = qreg_to_num(qd);
9114        let qm_num = qreg_to_num(qm);
9115
9116        // VSQRT.F32 base: 0xEEB10AC0
9117        for i in 0..4u32 {
9118            let sd = qd_num * 4 + i;
9119            let sm = qm_num * 4 + i;
9120
9121            let (vd, d) = encode_sreg(sd);
9122            let (vm, m) = encode_sreg(sm);
9123
9124            let instr: u32 = 0xEEB10AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
9125            result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9126        }
9127
9128        Ok(result)
9129    }
9130}
9131
9132/// VCR-TIER-001 (#1021/#1048) — the SCRATCH CONTRACT of an `ArmOp`'s encoder
9133/// expansion: the registers, beyond the op's declared RESULT registers and the
9134/// globally sanctioned R12/IP encoder scratch, that the expansion may leave
9135/// modified when it completes. Transient-but-restored traffic (push/pop through
9136/// the expansion's own stack red-zone, SP restored on exit) is not "modified".
9137///
9138/// This is the SINGLE declaration site for that contract — the one place a
9139/// future expansion that must borrow a register says so (the repo rule: one
9140/// declaration site, no duplicate copy that can silently drift). It is deliberately NOT derived
9141/// from the expansion's observed behavior: a contract read off the bytes would
9142/// rubber-stamp any clobber. Intent is declared here; the canary gate
9143/// (`scripts/repro/expansion_canary_gate_1021.py`) executes the REAL emitted
9144/// bytes of every variant the shipped rule table emits, on both backends, with
9145/// every non-contract register holding a distinctive canary, and fails on any
9146/// undeclared write — and on any declared register the expansion never
9147/// actually writes, so an over-broad declaration cannot hollow the gate.
9148///
9149/// The default is the STRICTEST reading — result registers only — which is
9150/// exactly the silent claim the atomic `ArmSemantics` pseudo-op model already
9151/// makes (#1021: an atomic model of a multi-instruction expansion is a silent
9152/// claim that the expansion is scratch-free). Today the table is EMPTY: #1039
9153/// reworked `i32.popcnt` off R11 (the linear-memory base) and #1048 reworked
9154/// the i64 shifts and bit-counts off their own operand registers, so every
9155/// expansion of every rule-emitted variant is R12-only on both Thumb-2 and
9156/// A32. Backend-independent for the same reason; if a backend's expansion ever
9157/// diverges, this signature grows a backend parameter in the same PR.
9158pub fn expansion_scratch_contract(op: &ArmOp) -> &'static [Reg] {
9159    // No variant currently borrows any register beyond R12. A new declaration
9160    // is added as a `match op { .. }` arm here — nowhere else.
9161    let _ = op;
9162    &[]
9163}
9164
9165#[cfg(test)]
9166mod tests {
9167    use super::*;
9168
9169    #[test]
9170    fn test_encoder_creation() {
9171        let encoder_arm = ArmEncoder::new_arm32();
9172        assert!(!encoder_arm.thumb_mode);
9173
9174        let encoder_thumb = ArmEncoder::new_thumb2();
9175        assert!(encoder_thumb.thumb_mode);
9176    }
9177
9178    /// #204 WAKE-path regression: `SetCond` materialized 0/1 with the 16-bit
9179    /// `MOVS Rd,#imm` (T1), whose Rd field is 3 bits (R0–R7). For a high Rd
9180    /// (R8–R12) `rd_bits << 8` overflows bit 11, flipping the opcode MOVS→CMP
9181    /// (`0x2c00`), so the boolean was never written — gale's `has_waiter` kept a
9182    /// stale value and the binary-sem WAKE dispatch read garbage. High Rd must
9183    /// use the 32-bit `MOV.W` (T2). Verify the bytes, not the IR.
9184    /// #311: the SAME high-Rd MOVS→CMP transmutation as #204, but in the
9185    /// i64 comparison expansions (I64SetCond / I64SetCondZ) — missed by the
9186    /// #204 hardening. With rd=R8 the boolean died in the flags
9187    /// (`ite eq; cmpeq r0,#1; cmpne r0,#0`), so gale's packed-u64 select
9188    /// read a stale register on silicon. High Rd must take MOV.W / CMP.W.
9189    #[test]
9190    fn test_encode_i64setcond_high_reg_uses_mov_w_311() {
9191        use synth_synthesis::{ArmOp, Condition, Reg};
9192        let enc = ArmEncoder::new_thumb2();
9193        let bytes = enc
9194            .encode(&ArmOp::I64SetCond {
9195                rd: Reg::R8,
9196                rn_lo: Reg::R2,
9197                rn_hi: Reg::R3,
9198                rm_lo: Reg::R6,
9199                rm_hi: Reg::R7,
9200                cond: Condition::EQ,
9201            })
9202            .unwrap();
9203        // The 32-bit MOV.W immediate (T2) first halfword is 0xF04F; the
9204        // 16-bit transmuted forms would contain 0x2801/0x2800 (CMP r0,#1/#0).
9205        let halfwords: Vec<u16> = bytes
9206            .chunks(2)
9207            .map(|c| u16::from_le_bytes([c[0], c[1]]))
9208            .collect();
9209        assert!(
9210            halfwords.iter().filter(|&&h| h == 0xF04F).count() == 2,
9211            "high rd must use two MOV.W (T2) encodings, got {halfwords:04x?}"
9212        );
9213        assert!(
9214            !halfwords.contains(&0x2801) && !halfwords.contains(&0x2800),
9215            "no transmuted 16-bit CMP imm: {halfwords:04x?}"
9216        );
9217
9218        let bytes_z = enc
9219            .encode(&ArmOp::I64SetCondZ {
9220                rd: Reg::R8,
9221                rn_lo: Reg::R2,
9222                rn_hi: Reg::R3,
9223            })
9224            .unwrap();
9225        let hw_z: Vec<u16> = bytes_z
9226            .chunks(2)
9227            .map(|c| u16::from_le_bytes([c[0], c[1]]))
9228            .collect();
9229        assert!(
9230            hw_z.iter().filter(|&&h| h == 0xF04F).count() == 2,
9231            "SetCondZ high rd MOV.W: {hw_z:04x?}"
9232        );
9233        // CMP.W rd,#0 (T2) first halfword: 0xF1B0 | rd
9234        assert!(
9235            hw_z.contains(&(0xF1B0 | 8)),
9236            "SetCondZ high rd must use CMP.W: {hw_z:04x?}"
9237        );
9238    }
9239
9240    #[test]
9241    fn test_encode_setcond_high_reg_uses_mov_w_204() {
9242        use synth_synthesis::{ArmOp, Condition, Reg};
9243        let enc = ArmEncoder::new_thumb2();
9244        // R12 (high): must be ITE + MOV.W #1 + MOV.W #0, never a 16-bit MOVS/CMP.
9245        let hi = enc
9246            .encode(&ArmOp::SetCond {
9247                rd: Reg::R12,
9248                cond: Condition::NE,
9249            })
9250            .unwrap();
9251        assert_eq!(hi.len(), 10, "ITE(2) + MOV.W(4) + MOV.W(4): {hi:02x?}");
9252        // both value halfwords are MOV.W (0xF04F) — NOT the corrupt CMP (0x2c..).
9253        assert_eq!(&hi[2..4], &[0x4F, 0xF0], "then = MOV.W: {hi:02x?}");
9254        assert_eq!(&hi[6..8], &[0x4F, 0xF0], "else = MOV.W: {hi:02x?}");
9255        assert_eq!(hi[4] & 0x0F, 0x01, "then imm = #1");
9256        assert_eq!(hi[8] & 0x0F, 0x00, "else imm = #0");
9257        // Low Rd keeps the compact 16-bit MOVS form.
9258        let lo = enc
9259            .encode(&ArmOp::SetCond {
9260                rd: Reg::R0,
9261                cond: Condition::NE,
9262            })
9263            .unwrap();
9264        assert_eq!(lo.len(), 6, "ITE(2) + MOVS(2) + MOVS(2): {lo:02x?}");
9265        assert_eq!(lo[2..4], [0x01, 0x20], "then = MOVS R0,#1");
9266        assert_eq!(lo[4..6], [0x00, 0x20], "else = MOVS R0,#0");
9267    }
9268
9269    /// #209 Opt 1b: UMULL RdLo, RdHi, Rn, Rm encodes correctly on both ISAs.
9270    /// Thumb-2 T1: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm.
9271    /// A32:        cond 0000 1000 RdHi RdLo Rm 1001 Rn.
9272    #[test]
9273    fn test_encode_umull_209b() {
9274        use synth_synthesis::{ArmOp, Reg};
9275        let op = ArmOp::Umull {
9276            rdlo: Reg::R4,
9277            rdhi: Reg::R5,
9278            rn: Reg::R0,
9279            rm: Reg::R3,
9280        };
9281        // Thumb-2: hw1 = 0xFBA0 | 0 = 0xFBA0; hw2 = (4<<12)|(5<<8)|3 = 0x4503.
9282        let t = ArmEncoder::new_thumb2().encode(&op).unwrap();
9283        assert_eq!(
9284            t,
9285            vec![0xA0, 0xFB, 0x03, 0x45],
9286            "umull r4,r5,r0,r3 (T2): {t:02x?}"
9287        );
9288        // A32: 0xE0800090 | (5<<16) | (4<<12) | (3<<8) | 0 = 0xE0854390.
9289        let a = ArmEncoder::new_arm32().encode(&op).unwrap();
9290        assert_eq!(
9291            a,
9292            0xE085_4390u32.to_le_bytes().to_vec(),
9293            "umull (A32): {a:02x?}"
9294        );
9295    }
9296
9297    /// #206 regression: the ARM32 (A32) `Ldr`/`Str` encoders fed `addr` through
9298    /// `encode_mem_addr`, which returns only the 12-bit immediate — so a register
9299    /// offset (`[rn, rm, #off]`) was silently dropped to `[rn, #off]`, sending
9300    /// the access to the wrong runtime address (silent miscompile on the default
9301    /// `--target arm`). A register offset must materialize `ip = rn + rm` and
9302    /// load from `[ip, #off]`. Verify the bytes.
9303    #[test]
9304    fn test_encode_arm32_indexed_load_keeps_index_206() {
9305        use synth_synthesis::{ArmOp, MemAddr, Reg};
9306        let enc = ArmEncoder::new_arm32();
9307        // ldr r0, [r11, r1, #8]  must NOT collapse to a single immediate ldr.
9308        let bytes = enc
9309            .encode(&ArmOp::Ldr {
9310                rd: Reg::R0,
9311                addr: MemAddr::reg_imm(Reg::R11, Reg::R1, 8),
9312            })
9313            .unwrap();
9314        assert_eq!(
9315            bytes.len(),
9316            8,
9317            "expected ADD ip + LDR (2 words): {bytes:02x?}"
9318        );
9319        let add = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9320        let ldr = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9321        // ADD ip, r11, r1  = 0xE08BC001
9322        assert_eq!(add, 0xE08B_C001, "ADD ip,r11,r1: {add:#010x}");
9323        // LDR r0, [ip, #8] = 0xE59C0008
9324        assert_eq!(ldr, 0xE59C_0008, "LDR r0,[ip,#8]: {ldr:#010x}");
9325        // A bare immediate ldr (the bug) would be 0xE59B0008 (base=r11) — reject.
9326        assert_ne!(ldr, 0xE59B_0008, "index must not be dropped");
9327    }
9328
9329    /// #594 regression: `call_indirect` on the A32 path (`--target cortex-r5`)
9330    /// was encoded as a literal NOP (0xE1A00000) — the call never happened and
9331    /// the function silently returned the leftover table-index value. The A32
9332    /// encoder must emit a real dispatch expansion, since #642 guarded by an
9333    /// inline bounds check:
9334    /// `MOVW r12, #size; CMP idx, r12; BLO +1; UDF;
9335    ///  MOV r12, idx, LSL #2; LDR r12, [r11, r12]; BLX r12`.
9336    #[test]
9337    fn test_encode_arm32_call_indirect_is_real_call_594() {
9338        use synth_synthesis::{ArmOp, Reg};
9339        let enc = ArmEncoder::new_arm32();
9340        let bytes = enc
9341            .encode(&ArmOp::CallIndirect {
9342                rd: Reg::R0,
9343                type_idx: 0,
9344                table_index_reg: Reg::R0,
9345                table_size: 4,
9346                table_byte_offset: 0,
9347                null_check: false,
9348                type_check: None,
9349            })
9350            .unwrap();
9351        assert_eq!(
9352            bytes.len(),
9353            28,
9354            "expected MOVW + CMP + BLO + UDF + MOV + LDR + BLX (7 words): {bytes:02x?}"
9355        );
9356        let words: Vec<u32> = bytes
9357            .as_chunks::<4>()
9358            .0
9359            .iter()
9360            .map(|&w| u32::from_le_bytes(w))
9361            .collect();
9362        // #642 bounds guard: MOVW r12, #4; CMP r0, r12; BLO +1; UDF
9363        assert_eq!(words[0], 0xE300_C004, "MOVW r12,#4: {:#010x}", words[0]);
9364        assert_eq!(words[1], 0xE150_000C, "CMP r0,r12: {:#010x}", words[1]);
9365        assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9366        assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9367        // MOV r12, r0, LSL #2 = 0xE1A0C100
9368        assert_eq!(
9369            words[4], 0xE1A0_C100,
9370            "MOV r12,r0,LSL#2: {:#010x}",
9371            words[4]
9372        );
9373        // LDR r12, [r11, r12] = 0xE79BC00C
9374        assert_eq!(
9375            words[5], 0xE79B_C00C,
9376            "LDR r12,[r11,r12]: {:#010x}",
9377            words[5]
9378        );
9379        // BLX r12 = 0xE12FFF3C
9380        assert_eq!(words[6], 0xE12F_FF3C, "BLX r12: {:#010x}", words[6]);
9381        // The bug: a single NOP word. Must never come back.
9382        assert!(
9383            !bytes
9384                .as_chunks::<4>()
9385                .0
9386                .iter()
9387                .any(|&w| w == 0xE1A0_0000u32.to_le_bytes()),
9388            "call_indirect must not contain a NOP (#594): {bytes:02x?}"
9389        );
9390
9391        // A non-R0 index register lands in the MOV's Rm and CMP's Rn fields.
9392        let bytes = enc
9393            .encode(&ArmOp::CallIndirect {
9394                rd: Reg::R0,
9395                type_idx: 0,
9396                table_index_reg: Reg::R4,
9397                table_size: 4,
9398                table_byte_offset: 0,
9399                null_check: false,
9400                type_check: None,
9401            })
9402            .unwrap();
9403        let cmp = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9404        assert_eq!(cmp, 0xE154_000C, "CMP r4,r12: {cmp:#010x}");
9405        let mov = u32::from_le_bytes(bytes[16..20].try_into().unwrap());
9406        assert_eq!(mov, 0xE1A0_C104, "MOV r12,r4,LSL#2: {mov:#010x}");
9407    }
9408
9409    /// #642: a table size above 16 bits must not be silently truncated by the
9410    /// MOVW — the A32 guard adds a MOVT for the high half.
9411    #[test]
9412    fn test_encode_arm32_call_indirect_wide_table_size_642() {
9413        use synth_synthesis::{ArmOp, Reg};
9414        let enc = ArmEncoder::new_arm32();
9415        let bytes = enc
9416            .encode(&ArmOp::CallIndirect {
9417                rd: Reg::R0,
9418                type_idx: 0,
9419                table_index_reg: Reg::R0,
9420                table_size: 0x0002_0003,
9421                table_byte_offset: 0,
9422                null_check: false,
9423                type_check: None,
9424            })
9425            .unwrap();
9426        assert_eq!(bytes.len(), 32, "MOVT arm adds one word: {bytes:02x?}");
9427        let movw = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9428        let movt = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9429        assert_eq!(movw, 0xE300_C003, "MOVW r12,#3: {movw:#010x}");
9430        assert_eq!(movt, 0xE340_C002, "MOVT r12,#2: {movt:#010x}");
9431    }
9432
9433    /// #597 anchor (justified correctness RE-PIN of the #594-era freeze): the
9434    /// Thumb-2 `CallIndirect` expansion is `mov.w ip, rm, LSL #2; ldr.w ip,
9435    /// [r11, ip]; blx ip`.
9436    ///
9437    /// The #594 PR froze the then-current bytes `4F EA 20 0C ...` whose first
9438    /// word decodes as `mov.w ip, rm, ASR #32` — the intended `LSL #2` had
9439    /// its shift amount in the TYPE field (bits 5:4) instead of imm2 (bits
9440    /// 7:6), so the index was destroyed and every call_indirect dispatched
9441    /// table entry 0 (shipped miscompile, masked by index-0 probes). #597
9442    /// corrects the encoding; new bytes `4F EA 80 0C ...` were
9443    /// execution-validated under unicorn against the wasmtime oracle on a
9444    /// multi-entry table (indexes 0, 1, 3 —
9445    /// scripts/repro/call_indirect_597_differential.py) before this pin was
9446    /// replaced. Old pin: [4F EA 20 0C, 5B F8 0C C0, E0 47] (ASR #32 — must
9447    /// never come back).
9448    #[test]
9449    fn test_encode_thumb_call_indirect_lsl2_597() {
9450        use synth_synthesis::{ArmOp, Reg};
9451        let enc = ArmEncoder::new_thumb2();
9452        let bytes = enc
9453            .encode(&ArmOp::CallIndirect {
9454                rd: Reg::R0,
9455                type_idx: 0,
9456                table_index_reg: Reg::R0,
9457                table_size: 4,
9458                table_byte_offset: 0,
9459                null_check: false,
9460                type_check: None,
9461            })
9462            .unwrap();
9463        assert_eq!(
9464            bytes,
9465            vec![
9466                // #642 bounds guard: movw ip,#4; cmp r0,ip; blo +1; udf #0
9467                0x40, 0xF2, 0x04, 0x0C, // movw ip, #4
9468                0x60, 0x45, // cmp r0, ip
9469                0x00, 0xD3, // blo .+4 (skip the udf)
9470                0x00, 0xDE, // udf #0 — OOB index trap (WASM §4.4.8)
9471                // #597-pinned dispatch
9472                0x4F, 0xEA, 0x80, 0x0C, // mov.w ip, r0, lsl #2
9473                0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9474                0xE0, 0x47, // blx ip
9475            ],
9476            "Thumb-2 CallIndirect: bounds guard + mov.w/ldr.w/blx dispatch: {bytes:02x?}"
9477        );
9478        // The #597 bug bytes (ASR #32 dispatch first word) must never come back.
9479        assert!(
9480            !bytes.windows(4).any(|w| w == [0x4F, 0xEA, 0x20, 0x0C]),
9481            "mov.w ip, rm, ASR #32 — the #597 type-field bug"
9482        );
9483
9484        // A non-R0 index register lands in the mov.w's Rm field (hw2 bits 3:0)
9485        // and the cmp's Rn field.
9486        let bytes = enc
9487            .encode(&ArmOp::CallIndirect {
9488                rd: Reg::R0,
9489                type_idx: 0,
9490                table_index_reg: Reg::R4,
9491                table_size: 4,
9492                table_byte_offset: 0,
9493                null_check: false,
9494                type_check: None,
9495            })
9496            .unwrap();
9497        assert_eq!(&bytes[4..6], &[0x64, 0x45], "cmp r4, ip: {bytes:02x?}");
9498        assert_eq!(
9499            &bytes[10..14],
9500            &[0x4F, 0xEA, 0x84, 0x0C],
9501            "mov.w ip, r4, LSL #2: {bytes:02x?}"
9502        );
9503    }
9504
9505    /// #642: the Thumb-2 bounds guard for a high-register index (R8 — the top
9506    /// of the allocatable pool) uses the high-reg-capable 16-bit CMP (T2) with
9507    /// the N bit set; a table size above 16 bits adds a MOVT.
9508    #[test]
9509    fn test_encode_thumb_call_indirect_guard_shapes_642() {
9510        use synth_synthesis::{ArmOp, Reg};
9511        let enc = ArmEncoder::new_thumb2();
9512        let bytes = enc
9513            .encode(&ArmOp::CallIndirect {
9514                rd: Reg::R0,
9515                type_idx: 0,
9516                table_index_reg: Reg::R8,
9517                table_size: 3,
9518                table_byte_offset: 0,
9519                null_check: false,
9520                type_check: None,
9521            })
9522            .unwrap();
9523        // cmp r8, ip — T2: 0x4500 | N(1)<<7 | Rm(12)<<3 | Rn(0) = 0x45E0
9524        assert_eq!(&bytes[4..6], &[0xE0, 0x45], "cmp r8, ip: {bytes:02x?}");
9525
9526        let bytes = enc
9527            .encode(&ArmOp::CallIndirect {
9528                rd: Reg::R0,
9529                type_idx: 0,
9530                table_index_reg: Reg::R0,
9531                table_size: 0x0002_0003,
9532                table_byte_offset: 0,
9533                null_check: false,
9534                type_check: None,
9535            })
9536            .unwrap();
9537        // movw ip,#3 then movt ip,#2 — the size must not be truncated.
9538        assert_eq!(
9539            &bytes[0..8],
9540            &[0x40, 0xF2, 0x03, 0x0C, 0xC0, 0xF2, 0x02, 0x0C],
9541            "movw ip,#3; movt ip,#2: {bytes:02x?}"
9542        );
9543    }
9544
9545    /// #650: a non-zero table base offset (table N of the contiguous R11
9546    /// region) routes the Thumb-2 pointer load through
9547    /// `add.w ip, r11, ip; ldr.w ip, [ip, #offset]` — and offset 0 keeps the
9548    /// pre-#650 single-load bytes IDENTICAL (the by-construction pin).
9549    #[test]
9550    fn test_encode_thumb_call_indirect_table_offset_650() {
9551        use synth_synthesis::{ArmOp, Reg};
9552        let enc = ArmEncoder::new_thumb2();
9553        // falcon's fused-component shape: table 0 has 7 entries, so table 1
9554        // sits at byte offset 28.
9555        let bytes = enc
9556            .encode(&ArmOp::CallIndirect {
9557                rd: Reg::R0,
9558                type_idx: 0,
9559                table_index_reg: Reg::R1,
9560                table_size: 41,
9561                table_byte_offset: 28,
9562                null_check: false,
9563                type_check: None,
9564            })
9565            .unwrap();
9566        assert_eq!(
9567            bytes,
9568            vec![
9569                // #642 bounds guard against TABLE 1's OWN size (41)
9570                0x40, 0xF2, 0x29, 0x0C, // movw ip, #41
9571                0x61, 0x45, // cmp r1, ip
9572                0x00, 0xD3, // blo .+4 (skip the udf)
9573                0x00, 0xDE, // udf #0 — OOB trap (WASM §4.4.8)
9574                // dispatch through table 1's base (R11 + 28)
9575                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9576                0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9577                0xDC, 0xF8, 0x1C, 0xC0, // ldr.w ip, [ip, #28]
9578                0xE0, 0x47, // blx ip
9579            ],
9580            "Thumb-2 table-1 dispatch (#650): {bytes:02x?}"
9581        );
9582
9583        // Offset 0 must stay the #597-pinned single-load form (no add.w, no
9584        // imm-form ldr) — single-table byte identity by construction.
9585        let zero = enc
9586            .encode(&ArmOp::CallIndirect {
9587                rd: Reg::R0,
9588                type_idx: 0,
9589                table_index_reg: Reg::R1,
9590                table_size: 41,
9591                table_byte_offset: 0,
9592                null_check: false,
9593                type_check: None,
9594            })
9595            .unwrap();
9596        assert_eq!(
9597            &zero[10..],
9598            &[
9599                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9600                0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9601                0xE0, 0x47, // blx ip
9602            ],
9603            "offset 0 keeps the pre-#650 dispatch bytes: {zero:02x?}"
9604        );
9605    }
9606
9607    /// #650: the A32 twin — `add r12, r11, r12; ldr r12, [r12, #offset]` for
9608    /// a non-zero table base offset; offset 0 keeps the #594/#642 form.
9609    #[test]
9610    fn test_encode_arm32_call_indirect_table_offset_650() {
9611        use synth_synthesis::{ArmOp, Reg};
9612        let enc = ArmEncoder::new_arm32();
9613        let bytes = enc
9614            .encode(&ArmOp::CallIndirect {
9615                rd: Reg::R0,
9616                type_idx: 0,
9617                table_index_reg: Reg::R1,
9618                table_size: 41,
9619                table_byte_offset: 28,
9620                null_check: false,
9621                type_check: None,
9622            })
9623            .unwrap();
9624        let words: Vec<u32> = bytes
9625            .as_chunks::<4>()
9626            .0
9627            .iter()
9628            .map(|&w| u32::from_le_bytes(w))
9629            .collect();
9630        assert_eq!(words[0], 0xE300_C029, "MOVW r12,#41: {:#010x}", words[0]);
9631        assert_eq!(words[1], 0xE151_000C, "CMP r1,r12: {:#010x}", words[1]);
9632        assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9633        assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9634        assert_eq!(
9635            words[4], 0xE1A0_C101,
9636            "MOV r12,r1,LSL#2: {:#010x}",
9637            words[4]
9638        );
9639        assert_eq!(
9640            words[5], 0xE08B_C00C,
9641            "ADD r12,r11,r12 (#650): {:#010x}",
9642            words[5]
9643        );
9644        assert_eq!(
9645            words[6], 0xE59C_C01C,
9646            "LDR r12,[r12,#28] (#650): {:#010x}",
9647            words[6]
9648        );
9649        assert_eq!(words[7], 0xE12F_FF3C, "BLX r12: {:#010x}", words[7]);
9650    }
9651
9652    /// #664: `null_check` inserts a null-funcref trap between the Thumb-2
9653    /// pointer load and the `BLX` (`cmp.w ip, #0; bne .+4; udf #0`) — a
9654    /// zero-linked (uninitialized) slot must TRAP (WASM §4.4.8), never
9655    /// branch to address 0. `null_check: false` keeps the expansion
9656    /// byte-identical to the pre-#664 form (by-construction pin).
9657    #[test]
9658    fn test_encode_thumb_call_indirect_null_check_664() {
9659        use synth_synthesis::{ArmOp, Reg};
9660        let enc = ArmEncoder::new_thumb2();
9661        let op = |null_check| ArmOp::CallIndirect {
9662            rd: Reg::R0,
9663            type_idx: 0,
9664            table_index_reg: Reg::R1,
9665            table_size: 4,
9666            table_byte_offset: 0,
9667            null_check,
9668            type_check: None,
9669        };
9670        let with = enc.encode(&op(true)).unwrap();
9671        let without = enc.encode(&op(false)).unwrap();
9672        // The checked form = the unchecked form with EXACTLY the three-insn
9673        // null check spliced in before the final BLX (byte identity of the
9674        // shared prefix/suffix — nothing else may move).
9675        assert_eq!(
9676            with.len(),
9677            without.len() + 8,
9678            "cmp.w (4) + bne (2) + udf (2): {with:02x?}"
9679        );
9680        let blx_at = without.len() - 2;
9681        assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9682        assert_eq!(
9683            &with[blx_at..],
9684            &[
9685                0xBC, 0xF1, 0x00, 0x0F, // cmp.w ip, #0
9686                0x00, 0xD1, // bne .+4 (skip the udf)
9687                0x00, 0xDE, // udf #0 — null-funcref trap (#664)
9688                0xE0, 0x47, // blx ip
9689            ],
9690            "null check precedes the BLX: {with:02x?}"
9691        );
9692        assert_eq!(&with[with.len() - 2..], &without[blx_at..], "same BLX");
9693    }
9694
9695    /// #664: the A32 twin — `cmp r12, #0; bne .+8; udf` before the `BLX`;
9696    /// `null_check: false` keeps the #594/#642/#650 bytes identical.
9697    #[test]
9698    fn test_encode_arm32_call_indirect_null_check_664() {
9699        use synth_synthesis::{ArmOp, Reg};
9700        let enc = ArmEncoder::new_arm32();
9701        let op = |null_check| ArmOp::CallIndirect {
9702            rd: Reg::R0,
9703            type_idx: 0,
9704            table_index_reg: Reg::R1,
9705            table_size: 4,
9706            table_byte_offset: 0,
9707            null_check,
9708            type_check: None,
9709        };
9710        let with = enc.encode(&op(true)).unwrap();
9711        let without = enc.encode(&op(false)).unwrap();
9712        assert_eq!(with.len(), without.len() + 12, "3 A32 words: {with:02x?}");
9713        let blx_at = without.len() - 4;
9714        assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9715        let words: Vec<u32> = with[blx_at..]
9716            .as_chunks::<4>()
9717            .0
9718            .iter()
9719            .map(|&w| u32::from_le_bytes(w))
9720            .collect();
9721        assert_eq!(words[0], 0xE35C_0000, "CMP r12,#0: {:#010x}", words[0]);
9722        assert_eq!(words[1], 0x1A00_0000, "BNE +1 insn: {:#010x}", words[1]);
9723        assert_eq!(words[2], 0xE7F0_00F0, "UDF (null trap): {:#010x}", words[2]);
9724        assert_eq!(words[3], 0xE12F_FF3C, "BLX r12: {:#010x}", words[3]);
9725    }
9726
9727    /// #676: `type_check` splices the runtime type check — scale the index,
9728    /// load the slot's structural class id from the type-id sidecar
9729    /// (`ldr.w ip, [ip, #type_off]`), compare against the expected class id
9730    /// and trap on mismatch (WASM §4.4.8) — between the bounds guard and
9731    /// the dispatch tail. `type_check: None` keeps the expansion
9732    /// byte-identical to the pre-#676 form (by-construction pin, the same
9733    /// trick as #650 offset-0 / #664 `null_check: false`).
9734    #[test]
9735    fn test_encode_thumb_call_indirect_type_check_676() {
9736        use synth_synthesis::{ArmOp, Reg};
9737        let enc = ArmEncoder::new_thumb2();
9738        let op = |type_check| ArmOp::CallIndirect {
9739            rd: Reg::R0,
9740            type_idx: 1,
9741            table_index_reg: Reg::R1,
9742            table_size: 5,
9743            table_byte_offset: 0,
9744            null_check: false,
9745            type_check,
9746        };
9747        let with = enc.encode(&op(Some((2, 20)))).unwrap();
9748        let without = enc.encode(&op(None)).unwrap();
9749        // The checked form = the unchecked form with EXACTLY the six-insn
9750        // type check spliced in after the bounds guard (byte identity of
9751        // the shared prefix/suffix — nothing else may move).
9752        assert_eq!(
9753            with.len(),
9754            without.len() + 20,
9755            "lsl.w(4)+add.w(4)+ldr.w(4)+cmp.w(4)+beq(2)+udf(2): {with:02x?}"
9756        );
9757        // Bounds guard: movw(4) + cmp(2) + blo(2) + udf(2) = 10 bytes.
9758        let guard_end = 10;
9759        assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
9760        assert_eq!(
9761            &with[guard_end..guard_end + 20],
9762            &[
9763                0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9764                0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9765                0xDC, 0xF8, 0x14, 0xC0, // ldr.w ip, [ip, #20] — sidecar slot id
9766                0xBC, 0xF1, 0x02, 0x0F, // cmp.w ip, #2 — expected class id
9767                0x00, 0xD0, // beq .+4 (skip the udf on a match)
9768                0x00, 0xDE, // udf #0 — §4.4.8 type-mismatch trap (#676)
9769            ],
9770            "type check follows the bounds guard: {with:02x?}"
9771        );
9772        assert_eq!(
9773            &with[guard_end + 20..],
9774            &without[guard_end..],
9775            "dispatch tail unchanged (idx*4 recomputed)"
9776        );
9777    }
9778
9779    /// #676: the A32 twin — `mov r12, idx, lsl #2; add r12, r11, r12;
9780    /// ldr r12, [r12, #type_off]; cmp r12, #id; beq .+8; udf` after the
9781    /// bounds guard; `type_check: None` keeps the #594/#642/#650/#664
9782    /// bytes identical.
9783    #[test]
9784    fn test_encode_arm32_call_indirect_type_check_676() {
9785        use synth_synthesis::{ArmOp, Reg};
9786        let enc = ArmEncoder::new_arm32();
9787        let op = |type_check| ArmOp::CallIndirect {
9788            rd: Reg::R0,
9789            type_idx: 1,
9790            table_index_reg: Reg::R1,
9791            table_size: 5,
9792            table_byte_offset: 0,
9793            null_check: false,
9794            type_check,
9795        };
9796        let with = enc.encode(&op(Some((2, 20)))).unwrap();
9797        let without = enc.encode(&op(None)).unwrap();
9798        assert_eq!(with.len(), without.len() + 24, "6 A32 words: {with:02x?}");
9799        // Bounds guard: movw + cmp + blo + udf = 4 words = 16 bytes.
9800        let guard_end = 16;
9801        assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
9802        let words: Vec<u32> = with[guard_end..guard_end + 24]
9803            .as_chunks::<4>()
9804            .0
9805            .iter()
9806            .map(|&w| u32::from_le_bytes(w))
9807            .collect();
9808        assert_eq!(
9809            words[0], 0xE1A0_C101,
9810            "MOV r12,r1,LSL#2: {:#010x}",
9811            words[0]
9812        );
9813        assert_eq!(words[1], 0xE08B_C00C, "ADD r12,r11,r12: {:#010x}", words[1]);
9814        assert_eq!(
9815            words[2], 0xE59C_C014,
9816            "LDR r12,[r12,#20] (sidecar): {:#010x}",
9817            words[2]
9818        );
9819        assert_eq!(
9820            words[3], 0xE35C_0002,
9821            "CMP r12,#2 (expected class id): {:#010x}",
9822            words[3]
9823        );
9824        assert_eq!(words[4], 0x0A00_0000, "BEQ +1 insn: {:#010x}", words[4]);
9825        assert_eq!(
9826            words[5], 0xE7F0_00F0,
9827            "UDF (type-mismatch trap): {:#010x}",
9828            words[5]
9829        );
9830        assert_eq!(
9831            &with[guard_end + 24..],
9832            &without[guard_end..],
9833            "dispatch tail unchanged"
9834        );
9835    }
9836
9837    /// #178/#180 regression: the Thumb `Add`/`Adds`/`Subs` reg-forms used the
9838    /// 16-bit encoding unconditionally. For high registers (R12 base scratch,
9839    /// R8-R11 i64 pairs) the 3-bit register fields overflow and corrupt the
9840    /// operands — `add ip,ip,r0` came out as `adds r4,r5,r1` (0x186C), silently
9841    /// dropping the address operand and miscompiling every optimized memory
9842    /// access. High registers must use the 32-bit `.W` forms.
9843    #[test]
9844    fn test_encode_thumb_add_high_reg_uses_add_w_178_180() {
9845        let encoder = ArmEncoder::new_thumb2();
9846
9847        // add ip, ip, r0  — the exact MemLoad/MemStore base+addr op.
9848        let code = encoder
9849            .encode(&ArmOp::Add {
9850                rd: Reg::R12,
9851                rn: Reg::R12,
9852                op2: Operand2::Reg(Reg::R0),
9853            })
9854            .unwrap();
9855        // ADD.W ip, ip, r0 = EB0C 0C00 (little-endian halfwords).
9856        assert_eq!(
9857            code,
9858            vec![0x0C, 0xEB, 0x00, 0x0C],
9859            "high-reg Thumb ADD must be 32-bit ADD.W (EB0C 0C00), not corrupt 16-bit; got {code:02X?}"
9860        );
9861        // Must NOT be the buggy 16-bit 0x186C (`adds r4,r5,r1`).
9862        assert_ne!(code, vec![0x6C, 0x18], "regressed to corrupt 16-bit ADDS");
9863
9864        // Low-register add stays 16-bit (no regression for the common case).
9865        let lo = encoder
9866            .encode(&ArmOp::Add {
9867                rd: Reg::R1,
9868                rn: Reg::R2,
9869                op2: Operand2::Reg(Reg::R3),
9870            })
9871            .unwrap();
9872        assert_eq!(
9873            lo.len(),
9874            2,
9875            "low-reg ADD should remain 16-bit, got {lo:02X?}"
9876        );
9877    }
9878
9879    /// #178/#180 sibling: i64 low-word `Adds`/`Subs` can land in R8-R11 pairs;
9880    /// those must fall back to 32-bit ADDS.W/SUBS.W (flag-setting preserved).
9881    #[test]
9882    fn test_encode_thumb_adds_subs_high_reg_use_32bit_178_180() {
9883        let encoder = ArmEncoder::new_thumb2();
9884
9885        // adds r10, r10, r8  → ADDS.W = EB1A 0A08
9886        let adds = encoder
9887            .encode(&ArmOp::Adds {
9888                rd: Reg::R10,
9889                rn: Reg::R10,
9890                op2: Operand2::Reg(Reg::R8),
9891            })
9892            .unwrap();
9893        assert_eq!(
9894            adds,
9895            vec![0x1A, 0xEB, 0x08, 0x0A],
9896            "high-reg ADDS must be 32-bit ADDS.W (EB1A 0A08); got {adds:02X?}"
9897        );
9898
9899        // subs r10, r10, r8  → SUBS.W = EBBA 0A08
9900        let subs = encoder
9901            .encode(&ArmOp::Subs {
9902                rd: Reg::R10,
9903                rn: Reg::R10,
9904                op2: Operand2::Reg(Reg::R8),
9905            })
9906            .unwrap();
9907        assert_eq!(
9908            subs,
9909            vec![0xBA, 0xEB, 0x08, 0x0A],
9910            "high-reg SUBS must be 32-bit SUBS.W (EBBA 0A08); got {subs:02X?}"
9911        );
9912    }
9913
9914    /// #184 (sibling of #180): 16-bit CMN (T1) only encodes R0-R7. High registers
9915    /// must use 32-bit CMN.W, not the corrupt truncated 16-bit form.
9916    #[test]
9917    fn test_encode_thumb_cmn_high_reg_uses_cmn_w_184() {
9918        let encoder = ArmEncoder::new_thumb2();
9919
9920        // cmn r10, r8  → CMN.W = EB1A 0F08 (ADD.W S=1, Rd=PC discarded).
9921        let cmn = encoder
9922            .encode(&ArmOp::Cmn {
9923                rn: Reg::R10,
9924                op2: Operand2::Reg(Reg::R8),
9925            })
9926            .unwrap();
9927        assert_eq!(
9928            cmn,
9929            vec![0x1A, 0xEB, 0x08, 0x0F],
9930            "high-reg CMN must be 32-bit CMN.W (EB1A 0F08); got {cmn:02X?}"
9931        );
9932
9933        // Low registers stay 16-bit: cmn r1, r2 = 0x42D1.
9934        let lo = encoder
9935            .encode(&ArmOp::Cmn {
9936                rn: Reg::R1,
9937                op2: Operand2::Reg(Reg::R2),
9938            })
9939            .unwrap();
9940        assert_eq!(
9941            lo.len(),
9942            2,
9943            "low-reg CMN should remain 16-bit, got {lo:02X?}"
9944        );
9945        assert_eq!(lo, vec![0xD1, 0x42], "low-reg CMN bytes wrong: {lo:02X?}");
9946    }
9947
9948    /// #185 regression: feeding PC (R15) as a data operand to a Thumb-2 op that
9949    /// guards its registers must return Err, not panic under debug-assertions.
9950    /// (Synth never emits PC here; the fuzz harness requires encode() be total.)
9951    #[test]
9952    fn test_encode_pc_operand_returns_err_not_panic_185() {
9953        let encoder = ArmEncoder::new_thumb2();
9954        for op in [
9955            ArmOp::Sdiv {
9956                rd: Reg::PC,
9957                rn: Reg::R0,
9958                rm: Reg::R1,
9959            },
9960            ArmOp::Udiv {
9961                rd: Reg::R0,
9962                rn: Reg::PC,
9963                rm: Reg::R1,
9964            },
9965            ArmOp::Sdiv {
9966                rd: Reg::R0,
9967                rn: Reg::R1,
9968                rm: Reg::PC,
9969            },
9970        ] {
9971            let r = encoder.encode(&op);
9972            assert!(
9973                r.is_err(),
9974                "encode({op:?}) must return Err for a PC operand, got {r:?}"
9975            );
9976        }
9977        // Valid registers still encode fine (no false rejection).
9978        assert!(
9979            encoder
9980                .encode(&ArmOp::Sdiv {
9981                    rd: Reg::R0,
9982                    rn: Reg::R1,
9983                    rm: Reg::R2
9984                })
9985                .is_ok()
9986        );
9987    }
9988
9989    #[test]
9990    fn test_encode_nop_arm32() {
9991        let encoder = ArmEncoder::new_arm32();
9992        let code = encoder.encode(&ArmOp::Nop).unwrap();
9993
9994        assert_eq!(code.len(), 4); // ARM32 instructions are 4 bytes
9995        assert_eq!(code, vec![0x00, 0x00, 0xA0, 0xE1]); // MOV R0, R0
9996    }
9997
9998    #[test]
9999    fn test_encode_nop_thumb() {
10000        let encoder = ArmEncoder::new_thumb2();
10001        let code = encoder.encode(&ArmOp::Nop).unwrap();
10002
10003        assert_eq!(code.len(), 2); // Thumb instructions are 2 bytes
10004        assert_eq!(code, vec![0x00, 0xBF]); // NOP
10005    }
10006
10007    #[test]
10008    fn test_encode_mov_immediate_arm32() {
10009        let encoder = ArmEncoder::new_arm32();
10010        let op = ArmOp::Mov {
10011            rd: Reg::R0,
10012            op2: Operand2::Imm(42),
10013        };
10014
10015        let code = encoder.encode(&op).unwrap();
10016        assert_eq!(code.len(), 4);
10017
10018        // Verify it's a MOV instruction (bits should have immediate flag set)
10019        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10020        assert_eq!(instr & 0x0E000000, 0x02000000); // Check I bit is set
10021    }
10022
10023    #[test]
10024    fn test_encode_add_registers_arm32() {
10025        let encoder = ArmEncoder::new_arm32();
10026        let op = ArmOp::Add {
10027            rd: Reg::R0,
10028            rn: Reg::R1,
10029            op2: Operand2::Reg(Reg::R2),
10030        };
10031
10032        let code = encoder.encode(&op).unwrap();
10033        assert_eq!(code.len(), 4);
10034
10035        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10036        // Verify it's an ADD instruction with correct opcode
10037        assert_eq!(instr & 0x0FE00000, 0x00800000);
10038    }
10039
10040    /// #350 — `encode_thumb32_add_imm` must lower an out-of-range immediate
10041    /// (> 0xFFF) to a legal MOVW(/MOVT) + ADD.W-register sequence instead of
10042    /// erroring. The small-imm fast path (imm <= 0xFFF) stays byte-identical.
10043    #[test]
10044    fn test_encode_add_imm_large_350() {
10045        let enc = ArmEncoder::new_thumb2();
10046
10047        // --- Fast path: imm <= 0xFFF is a single 4-byte instruction, and the
10048        // VALUE must be right (#681: this test used to assert only the length,
10049        // letting the raw-packed T3 mis-encoding of 0x123 pass CI). 0x123 is
10050        // not ThumbExpandImm-representable, so it must be ADDW (T4, plain
10051        // imm12): clang `addw r0, r1, #0x123` = f201 0023.
10052        let small = enc
10053            .encode_thumb32_add_imm(&Reg::R0, &Reg::R1, 0x123)
10054            .unwrap();
10055        assert_eq!(small, vec![0x01, 0xF2, 0x23, 0x10], "ADDW r0, r1, #0x123");
10056
10057        // helper: decode a Thumb-2 MOVW/MOVT halfword pair back to its imm16
10058        fn movx_imm16(b: &[u8]) -> u32 {
10059            let hw1 = u16::from_le_bytes([b[0], b[1]]) as u32;
10060            let hw2 = u16::from_le_bytes([b[2], b[3]]) as u32;
10061            let imm4 = hw1 & 0xF;
10062            let i = (hw1 >> 10) & 1;
10063            let imm3 = (hw2 >> 12) & 0x7;
10064            let imm8 = hw2 & 0xFF;
10065            (imm4 << 12) | (i << 11) | (imm3 << 8) | imm8
10066        }
10067        fn movx_rd(b: &[u8]) -> u32 {
10068            (u16::from_le_bytes([b[2], b[3]]) as u32 >> 8) & 0xF
10069        }
10070
10071        // --- rd != rn: scratch is rd. imm = 70000 = 0x11170 needs MOVW+MOVT. ---
10072        // 0x11170: lo16 = 0x1170, hi16 = 0x0001
10073        let seq = enc
10074            .encode_thumb32_add_imm(&Reg::R12, &Reg::R0, 70000)
10075            .unwrap();
10076        assert_eq!(seq.len(), 12, "MOVW + MOVT + ADD = 12 bytes");
10077        // MOVW r12, #0x1170
10078        assert_eq!(u16::from_le_bytes([seq[0], seq[1]]) & 0xFBF0, 0xF240);
10079        assert_eq!(movx_rd(&seq[0..4]), 12);
10080        assert_eq!(movx_imm16(&seq[0..4]), 0x1170);
10081        // MOVT r12, #0x0001
10082        assert_eq!(u16::from_le_bytes([seq[4], seq[5]]) & 0xFBF0, 0xF2C0);
10083        assert_eq!(movx_rd(&seq[4..8]), 12);
10084        assert_eq!(movx_imm16(&seq[4..8]), 0x0001);
10085        // ADD.W r12, r0, r12  (EB00 | rn=0 ; rd=12, rm=12)
10086        let add1 = u16::from_le_bytes([seq[8], seq[9]]) as u32;
10087        let add2 = u16::from_le_bytes([seq[10], seq[11]]) as u32;
10088        assert_eq!(add1 & 0xFFF0, 0xEB00);
10089        assert_eq!(add1 & 0xF, 0); // rn = r0
10090        assert_eq!((add2 >> 8) & 0xF, 12); // rd = r12
10091        assert_eq!(add2 & 0xF, 12); // rm = scratch = r12
10092        // The materialized scratch must reconstruct exactly 70000.
10093        assert_eq!(
10094            (movx_imm16(&seq[4..8]) << 16) | movx_imm16(&seq[0..4]),
10095            70000
10096        );
10097
10098        // --- imm <= 0xFFFF: MOVT is skipped (MOVW + ADD = 8 bytes). ---
10099        let seq16 = enc
10100            .encode_thumb32_add_imm(&Reg::R3, &Reg::R0, 0xABCD)
10101            .unwrap();
10102        assert_eq!(seq16.len(), 8, "imm <= 0xFFFF skips MOVT");
10103        assert_eq!(movx_imm16(&seq16[0..4]), 0xABCD);
10104        assert_eq!(movx_rd(&seq16[0..4]), 3); // scratch = rd = r3
10105
10106        // --- rd == rn (in-place add): scratch must be R12, not rd. ---
10107        // imm = 0x12345: lo16 = 0x2345, hi16 = 0x0001
10108        let inplace = enc
10109            .encode_thumb32_add_imm(&Reg::R5, &Reg::R5, 0x12345)
10110            .unwrap();
10111        assert_eq!(inplace.len(), 12);
10112        assert_eq!(movx_rd(&inplace[0..4]), 12, "rd==rn must use R12 scratch");
10113        assert_eq!(
10114            (movx_imm16(&inplace[4..8]) << 16) | movx_imm16(&inplace[0..4]),
10115            0x12345
10116        );
10117        // ADD.W r5, r5, r12 — rm must be the scratch (12), never rn.
10118        let ip_add2 = u16::from_le_bytes([inplace[10], inplace[11]]) as u32;
10119        assert_eq!(ip_add2 & 0xF, 12);
10120        assert_eq!((ip_add2 >> 8) & 0xF, 5);
10121    }
10122
10123    /// #681 — `encode_thumb32_add_imm` packed a RAW immediate into the T3
10124    /// ADD.W `i:imm3:imm8` field, which is a ThumbExpandImm MODIFIED immediate:
10125    /// ThumbExpandImm(0x200) = 0, ThumbExpandImm(0x400) = 0x8000_0000. Every
10126    /// dynamic-address load/store with a static offset in 0x100..=0xFFF
10127    /// computed a wrong address (and bypassed --safety-bounds software: the
10128    /// guard checked the intended address, the access used the mis-encoded
10129    /// one). Fix: imm <= 0xFF keeps T3 (raw == expanded there, bit-identical);
10130    /// 0x100..=0xFFF uses ADDW (T4, plain imm12) — same lowering
10131    /// `encode_thumb32_add` already uses per #253.
10132    ///
10133    /// Every expected byte sequence below is pinned against clang
10134    /// (`-target thumbv7m-none-eabi`) output, bit-for-bit (#544 pattern).
10135    #[test]
10136    fn test_encode_add_imm_thumb_expand_681() {
10137        let enc = ArmEncoder::new_thumb2();
10138        let add = |rd: &Reg, rn: &Reg, imm: u32| enc.encode_thumb32_add_imm(rd, rn, imm).unwrap();
10139
10140        // imm <= 0xFF stays T3 ADD.W (raw == ThumbExpandImm-expanded):
10141        // clang: add.w r12, r0, #0xff  = f100 0cff
10142        assert_eq!(add(&Reg::R12, &Reg::R0, 0xFF), vec![0x00, 0xF1, 0xFF, 0x0C]);
10143
10144        // 0x100..=0xFFF must be ADDW (T4, plain imm12). The old T3 raw packing
10145        // decoded as +0 (0x100/0x200), +0x80000000 (0x400), etc.
10146        // clang: addw r12, r0, #0x100 = f200 1c00
10147        assert_eq!(
10148            add(&Reg::R12, &Reg::R0, 0x100),
10149            vec![0x00, 0xF2, 0x00, 0x1C]
10150        );
10151        // clang: addw r12, r0, #0x104 = f200 1c04
10152        assert_eq!(
10153            add(&Reg::R12, &Reg::R0, 0x104),
10154            vec![0x00, 0xF2, 0x04, 0x1C]
10155        );
10156        // clang: addw r12, r0, #0x200 = f200 2c00
10157        assert_eq!(
10158            add(&Reg::R12, &Reg::R0, 0x200),
10159            vec![0x00, 0xF2, 0x00, 0x2C]
10160        );
10161        // clang: addw r12, r0, #0x3fc = f200 3cfc
10162        assert_eq!(
10163            add(&Reg::R12, &Reg::R0, 0x3FC),
10164            vec![0x00, 0xF2, 0xFC, 0x3C]
10165        );
10166        // clang: addw r12, r0, #0x400 = f200 4c00
10167        assert_eq!(
10168            add(&Reg::R12, &Reg::R0, 0x400),
10169            vec![0x00, 0xF2, 0x00, 0x4C]
10170        );
10171        // clang: addw r12, r0, #0xfff = f600 7cff
10172        assert_eq!(
10173            add(&Reg::R12, &Reg::R0, 0xFFF),
10174            vec![0x00, 0xF6, 0xFF, 0x7C]
10175        );
10176        // Non-scratch rd/rn — clang: addw r1, r2, #0x104 = f202 1104
10177        assert_eq!(add(&Reg::R1, &Reg::R2, 0x104), vec![0x02, 0xF2, 0x04, 0x11]);
10178    }
10179
10180    /// #681 class audit — the T2 RSB and AND.W immediate fields are also
10181    /// ThumbExpandImm-coded and were raw-packed. Neither has a plain-imm12
10182    /// (T4-style) form, so a non-representable immediate must Err loudly
10183    /// (#253/#255/#378 class: never silently encode a different constant).
10184    /// Existing emitters only use representable values (RSB #32, AND #0x3F),
10185    /// pinned here bit-for-bit against clang.
10186    #[test]
10187    fn test_rsb_and_imm_thumb_expand_gate_681() {
10188        let enc = ArmEncoder::new_thumb2();
10189
10190        // clang: rsb.w r3, r2, #0x20 = f1c2 0320 — byte-identical to before.
10191        let rsb = enc
10192            .encode(&ArmOp::Rsb {
10193                rd: Reg::R3,
10194                rn: Reg::R2,
10195                imm: 32,
10196            })
10197            .unwrap();
10198        assert_eq!(rsb, vec![0xC2, 0xF1, 0x20, 0x03]);
10199
10200        // 0x101 is not ThumbExpandImm-representable -> must Err, not mis-encode.
10201        assert!(
10202            enc.encode(&ArmOp::Rsb {
10203                rd: Reg::R3,
10204                rn: Reg::R2,
10205                imm: 0x101,
10206            })
10207            .is_err(),
10208            "non-ThumbExpandImm RSB immediate must Err"
10209        );
10210
10211        // clang: and r4, r4, #0x3f = f004 043f — byte-identical to before.
10212        let and = enc.encode_thumb32_and_imm_raw(4, 4, 0x3F).unwrap();
10213        assert_eq!(and, vec![0x04, 0xF0, 0x3F, 0x04]);
10214        assert!(
10215            enc.encode_thumb32_and_imm_raw(4, 4, 0x101).is_err(),
10216            "non-ThumbExpandImm AND immediate must Err"
10217        );
10218
10219        // A32 RSB: imm12 is a rotate:imm8 modified immediate; > 0xFF used to be
10220        // silently masked to `imm & 0xFF` (#378 masking class) -> must Err.
10221        let a32 = ArmEncoder::new_arm32();
10222        assert!(
10223            a32.encode(&ArmOp::Rsb {
10224                rd: Reg::R3,
10225                rn: Reg::R2,
10226                imm: 0x120,
10227            })
10228            .is_err(),
10229            "A32 RSB immediate > 0xFF must Err, not mask"
10230        );
10231        // imm 32 (the only value real codegen emits) still encodes.
10232        assert!(
10233            a32.encode(&ArmOp::Rsb {
10234                rd: Reg::R3,
10235                rn: Reg::R2,
10236                imm: 32,
10237            })
10238            .is_ok()
10239        );
10240    }
10241
10242    /// #350 follow-up — the `encoder_no_panic` fuzz harness drives the encoder
10243    /// with ARBITRARY registers, including the one case the in-place lowering
10244    /// cannot serve: rd==rn==R12. There the scratch (R12, the reserved encoder
10245    /// register) would alias Rn and clobber it before the ADD reads it. The
10246    /// encoder contract (#180/#185) is Ok-or-Err, never a panic — so this must
10247    /// return Err, not assert. (Real codegen never emits rd==rn==R12 because R12
10248    /// is non-allocatable; this guards only the fuzz/adversarial path.)
10249    #[test]
10250    fn test_encode_add_imm_large_rd_rn_r12_errs_not_panics_350() {
10251        let enc = ArmEncoder::new_thumb2();
10252        // Out-of-range imm with rd==rn==R12: no free scratch -> Err.
10253        let r = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 70000);
10254        assert!(
10255            r.is_err(),
10256            "rd==rn==R12 with out-of-range imm must Err (no free scratch), got {r:?}"
10257        );
10258        // Small imm with rd==rn==R12 still takes the single-instruction fast path
10259        // (no scratch needed) and must succeed — the guard is scoped to the
10260        // out-of-range lowering only.
10261        let small = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 0x10);
10262        assert!(small.is_ok(), "small imm needs no scratch, must stay Ok");
10263    }
10264
10265    /// #378 — `encode_operand2` (ARM32 data-processing operand) must FAIL
10266    /// HONESTLY on an immediate that is not a valid rotated immediate, rather
10267    /// than silently masking it to `imm & 0xFF` and emitting a WRONG
10268    /// instruction. `0x1FF` has 9 set bits, so it cannot come from rotating an
10269    /// 8-bit imm8 — non-encodable. Real codegen materializes large constants via
10270    /// MOVW/MOVT; this guards the encoder's Ok-or-Err contract (#180/#185)
10271    /// directly. It is an Err (not a panic) so the `encoder_no_panic` fuzz
10272    /// harness — which drives arbitrary operands — still passes.
10273    #[test]
10274    fn test_encode_operand2_non_rotatable_imm_errs_not_masks_378() {
10275        let enc = ArmEncoder::new_arm32();
10276        let bad = enc.encode(&ArmOp::Add {
10277            rd: Reg::R0,
10278            rn: Reg::R1,
10279            op2: Operand2::Imm(0x1FF),
10280        });
10281        assert!(
10282            bad.is_err(),
10283            "non-rotatable ARM32 immediate 0x1FF must Err (was silently masked \
10284             to 0xFF), got {bad:?}"
10285        );
10286        // A representable rotated immediate still encodes fine (regression guard).
10287        let ok = enc.encode(&ArmOp::Add {
10288            rd: Reg::R0,
10289            rn: Reg::R1,
10290            op2: Operand2::Imm(0xFF),
10291        });
10292        assert!(
10293            ok.is_ok(),
10294            "0xFF is a valid rotated immediate, must stay Ok"
10295        );
10296    }
10297
10298    #[test]
10299    fn test_encode_ldr_arm32() {
10300        let encoder = ArmEncoder::new_arm32();
10301        let op = ArmOp::Ldr {
10302            rd: Reg::R0,
10303            addr: MemAddr::imm(Reg::R1, 4),
10304        };
10305
10306        let code = encoder.encode(&op).unwrap();
10307        assert_eq!(code.len(), 4);
10308
10309        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10310        // Verify load bit is set
10311        assert_eq!(instr & 0x00100000, 0x00100000);
10312    }
10313
10314    #[test]
10315    fn test_encode_str_arm32() {
10316        let encoder = ArmEncoder::new_arm32();
10317        let op = ArmOp::Str {
10318            rd: Reg::R0,
10319            addr: MemAddr::imm(Reg::SP, 0),
10320        };
10321
10322        let code = encoder.encode(&op).unwrap();
10323        assert_eq!(code.len(), 4);
10324    }
10325
10326    #[test]
10327    fn test_encode_branch_arm32() {
10328        let encoder = ArmEncoder::new_arm32();
10329        let op = ArmOp::Bl {
10330            label: "main".to_string(),
10331        };
10332
10333        let code = encoder.encode(&op).unwrap();
10334        assert_eq!(code.len(), 4);
10335
10336        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10337        // Verify BL opcode
10338        assert_eq!(instr & 0x0F000000, 0x0B000000);
10339    }
10340
10341    /// Regression test for #167 + #174: the Thumb-2 BL relocatable placeholder
10342    /// must carry a -4 addend so an R_ARM_THM_CALL nets to exactly the symbol S.
10343    /// The correct encoding is what `gas` emits for `bl <extern>`: f7ff fffe
10344    /// (hw1=0xF7FF, hw2=0xFFFE), little-endian bytes FF F7 FE FF.
10345    ///   - 0xD000 (J1=J2=0) → ~+0x600000 garbage addend: `bl c0000c` / truncated
10346    ///     to fit (#167).
10347    ///   - 0xF800 (addend 0) → lands at S+4, one instruction past the callee
10348    ///     entry (#174).
10349    ///   - 0xFFFE (addend -4) → lands at S. Correct.
10350    #[test]
10351    fn test_encode_thumb_bl_placeholder_addend_167_174() {
10352        let encoder = ArmEncoder::new_thumb2();
10353        let op = ArmOp::Bl {
10354            label: "callee".to_string(),
10355        };
10356
10357        let code = encoder.encode(&op).unwrap();
10358        assert_eq!(code.len(), 4, "Thumb-2 BL is 32-bit");
10359
10360        let hw1 = u16::from_le_bytes([code[0], code[1]]);
10361        let hw2 = u16::from_le_bytes([code[2], code[3]]);
10362        assert_eq!(hw1, 0xF7FF, "BL first halfword (matches gas `bl <extern>`)");
10363        assert_eq!(
10364            hw2, 0xFFFE,
10365            "BL second halfword must be 0xFFFE (-4 addend → nets to S), not 0xF800 (→ S+4, #174) or 0xD000 (#167)"
10366        );
10367        assert_ne!(hw2, 0xF800, "0xF800 (addend 0) lands at S+4 (#174)");
10368        assert_ne!(hw2, 0xD000, "0xD000 bakes in a ~+0x600000 addend (#167)");
10369    }
10370
10371    /// #740: the Thumb-2 32-bit B<cond>.W (encoding T3) must pack the
10372    /// HALFWORD offset directly into S:J2:J1:imm6:imm11 — the byte offset is
10373    /// SignExtend(S:J2:J1:imm6:imm11:'0'). The old arm packed
10374    /// `halfword_offset >> 1`, HALVING every wide conditional branch's
10375    /// displacement: gust_poll's loop-head `br_if` to an outer block end
10376    /// landed mid-shape (a spurious state write + spurious calls on the
10377    /// empty-budget path). Narrow (16-bit) B<cond> was unaffected — only
10378    /// spans > 254 bytes hit the bug. Bytes cross-checked against the llvm
10379    /// disassembler (`bne.w #0x224` = f040 8112).
10380    #[test]
10381    fn test_encode_thumb_bcond_wide_t3_halfword_offset_740() {
10382        use synth_synthesis::Condition;
10383        let encoder = ArmEncoder::new_thumb2();
10384
10385        // gust_poll's loop-head edge: NE, +0x112 halfwords (+0x224 bytes).
10386        let code = encoder
10387            .encode(&ArmOp::BCondOffset {
10388                cond: Condition::NE,
10389                offset: 0x112,
10390            })
10391            .unwrap();
10392        assert_eq!(code.len(), 4, "offset beyond ±127 halfwords must be wide");
10393        let hw1 = u16::from_le_bytes([code[0], code[1]]);
10394        let hw2 = u16::from_le_bytes([code[2], code[3]]);
10395        assert_eq!(hw1, 0xF040, "T3 hw1: 1111 0 S=0 cond=NE imm6=0");
10396        assert_eq!(
10397            hw2, 0x8112,
10398            "T3 hw2 imm11 must carry halfword offset bits [10:0] directly — \
10399             0x8089 (offset>>1) is the halved #740 miscompile"
10400        );
10401
10402        // Backward wide branch: EQ, -0x100 halfwords. S=1, J2=J1=1,
10403        // imm6=0b111111, imm11=0x700 → f43f af00.
10404        let code = encoder
10405            .encode(&ArmOp::BCondOffset {
10406                cond: Condition::EQ,
10407                offset: -0x100,
10408            })
10409            .unwrap();
10410        assert_eq!(code.len(), 4);
10411        let hw1 = u16::from_le_bytes([code[0], code[1]]);
10412        let hw2 = u16::from_le_bytes([code[2], code[3]]);
10413        assert_eq!(hw1, 0xF43F, "T3 hw1: S=1, cond=EQ, imm6=0x3F");
10414        assert_eq!(hw2, 0xAF00, "T3 hw2: J1=1 J2=1 imm11=0x700");
10415
10416        // Narrow encoding stays byte-identical (in-range offsets untouched).
10417        let code = encoder
10418            .encode(&ArmOp::BCondOffset {
10419                cond: Condition::EQ,
10420                offset: 5,
10421            })
10422            .unwrap();
10423        assert_eq!(code, vec![0x05, 0xD0], "narrow B<cond> unchanged");
10424
10425        // Out of the signed 20-bit T3 range: loud Err, never a truncated jump.
10426        assert!(
10427            encoder
10428                .encode(&ArmOp::BCondOffset {
10429                    cond: Condition::NE,
10430                    offset: 1 << 19,
10431                })
10432                .is_err(),
10433            "out-of-range T3 offset must be a loud decline"
10434        );
10435    }
10436
10437    #[test]
10438    fn test_encode_sequence() {
10439        let encoder = ArmEncoder::new_arm32();
10440        let ops = vec![
10441            ArmOp::Mov {
10442                rd: Reg::R0,
10443                op2: Operand2::Imm(42),
10444            },
10445            ArmOp::Mov {
10446                rd: Reg::R1,
10447                op2: Operand2::Imm(10),
10448            },
10449            ArmOp::Add {
10450                rd: Reg::R2,
10451                rn: Reg::R0,
10452                op2: Operand2::Reg(Reg::R1),
10453            },
10454        ];
10455
10456        let code = encoder.encode_sequence(&ops).unwrap();
10457        assert_eq!(code.len(), 12); // 3 instructions * 4 bytes
10458    }
10459
10460    #[test]
10461    fn test_reg_to_bits() {
10462        assert_eq!(reg_to_bits(&Reg::R0), 0);
10463        assert_eq!(reg_to_bits(&Reg::R7), 7);
10464        assert_eq!(reg_to_bits(&Reg::SP), 13);
10465        assert_eq!(reg_to_bits(&Reg::LR), 14);
10466        assert_eq!(reg_to_bits(&Reg::PC), 15);
10467    }
10468
10469    #[test]
10470    fn test_encode_bitwise_operations() {
10471        let encoder = ArmEncoder::new_arm32();
10472
10473        let and_op = ArmOp::And {
10474            rd: Reg::R0,
10475            rn: Reg::R1,
10476            op2: Operand2::Reg(Reg::R2),
10477        };
10478        let and_code = encoder.encode(&and_op).unwrap();
10479        assert_eq!(and_code.len(), 4);
10480
10481        let orr_op = ArmOp::Orr {
10482            rd: Reg::R0,
10483            rn: Reg::R1,
10484            op2: Operand2::Reg(Reg::R2),
10485        };
10486        let orr_code = encoder.encode(&orr_op).unwrap();
10487        assert_eq!(orr_code.len(), 4);
10488
10489        let eor_op = ArmOp::Eor {
10490            rd: Reg::R0,
10491            rn: Reg::R1,
10492            op2: Operand2::Reg(Reg::R2),
10493        };
10494        let eor_code = encoder.encode(&eor_op).unwrap();
10495        assert_eq!(eor_code.len(), 4);
10496    }
10497
10498    // === Thumb-2 32-bit encoding tests ===
10499
10500    #[test]
10501    fn test_encode_sdiv_thumb2() {
10502        let encoder = ArmEncoder::new_thumb2();
10503        let op = ArmOp::Sdiv {
10504            rd: Reg::R0,
10505            rn: Reg::R1,
10506            rm: Reg::R2,
10507        };
10508
10509        let code = encoder.encode(&op).unwrap();
10510        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10511
10512        // SDIV R0, R1, R2: 0xFB91 0xF0F2
10513        // First halfword: 0xFB90 | Rn(1) = 0xFB91
10514        // Second halfword: 0xF0F0 | Rd(0)<<8 | Rm(2) = 0xF0F2
10515        // Little-endian: [0x91, 0xFB, 0xF2, 0xF0]
10516        assert_eq!(code[0], 0x91);
10517        assert_eq!(code[1], 0xFB);
10518        assert_eq!(code[2], 0xF2);
10519        assert_eq!(code[3], 0xF0);
10520    }
10521
10522    #[test]
10523    fn test_encode_udiv_thumb2() {
10524        let encoder = ArmEncoder::new_thumb2();
10525        let op = ArmOp::Udiv {
10526            rd: Reg::R0,
10527            rn: Reg::R1,
10528            rm: Reg::R2,
10529        };
10530
10531        let code = encoder.encode(&op).unwrap();
10532        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10533
10534        // UDIV R0, R1, R2: 0xFBB1 0xF0F2
10535        // Little-endian: [0xB1, 0xFB, 0xF2, 0xF0]
10536        assert_eq!(code[0], 0xB1);
10537        assert_eq!(code[1], 0xFB);
10538        assert_eq!(code[2], 0xF2);
10539        assert_eq!(code[3], 0xF0);
10540    }
10541
10542    #[test]
10543    fn test_encode_mul_thumb2() {
10544        let encoder = ArmEncoder::new_thumb2();
10545        let op = ArmOp::Mul {
10546            rd: Reg::R0,
10547            rn: Reg::R1,
10548            rm: Reg::R2,
10549        };
10550
10551        let code = encoder.encode(&op).unwrap();
10552        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10553    }
10554
10555    #[test]
10556    fn test_encode_and_thumb2() {
10557        let encoder = ArmEncoder::new_thumb2();
10558        let op = ArmOp::And {
10559            rd: Reg::R0,
10560            rn: Reg::R1,
10561            op2: Operand2::Reg(Reg::R2),
10562        };
10563
10564        let code = encoder.encode(&op).unwrap();
10565        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10566    }
10567
10568    #[test]
10569    fn test_encode_lsl_thumb2_low_regs() {
10570        let encoder = ArmEncoder::new_thumb2();
10571        let op = ArmOp::Lsl {
10572            rd: Reg::R0,
10573            rn: Reg::R1,
10574            shift: 5,
10575        };
10576
10577        let code = encoder.encode(&op).unwrap();
10578        assert_eq!(code.len(), 2); // 16-bit for low registers
10579    }
10580
10581    #[test]
10582    fn test_encode_clz_thumb2() {
10583        let encoder = ArmEncoder::new_thumb2();
10584        let op = ArmOp::Clz {
10585            rd: Reg::R0,
10586            rm: Reg::R1,
10587        };
10588
10589        let code = encoder.encode(&op).unwrap();
10590        assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10591    }
10592
10593    #[test]
10594    fn test_encode_bx_thumb2() {
10595        let encoder = ArmEncoder::new_thumb2();
10596        let op = ArmOp::Bx { rm: Reg::LR };
10597
10598        let code = encoder.encode(&op).unwrap();
10599        assert_eq!(code.len(), 2); // 16-bit instruction
10600
10601        // BX LR: 0x4770
10602        assert_eq!(code, vec![0x70, 0x47]);
10603    }
10604
10605    // ========================================================================
10606    // f32 pseudo-op encoding tests
10607    // ========================================================================
10608
10609    #[test]
10610    fn test_encode_f32_abs_arm32() {
10611        let encoder = ArmEncoder::new_arm32();
10612        let op = ArmOp::F32Abs {
10613            sd: VfpReg::S0,
10614            sm: VfpReg::S2,
10615        };
10616        let code = encoder.encode(&op).unwrap();
10617        assert_eq!(code.len(), 4); // Single VFP instruction
10618    }
10619
10620    #[test]
10621    fn test_encode_f32_neg_arm32() {
10622        let encoder = ArmEncoder::new_arm32();
10623        let op = ArmOp::F32Neg {
10624            sd: VfpReg::S0,
10625            sm: VfpReg::S2,
10626        };
10627        let code = encoder.encode(&op).unwrap();
10628        assert_eq!(code.len(), 4);
10629    }
10630
10631    #[test]
10632    fn test_encode_f32_sqrt_arm32() {
10633        let encoder = ArmEncoder::new_arm32();
10634        let op = ArmOp::F32Sqrt {
10635            sd: VfpReg::S0,
10636            sm: VfpReg::S2,
10637        };
10638        let code = encoder.encode(&op).unwrap();
10639        assert_eq!(code.len(), 4);
10640    }
10641
10642    #[test]
10643    fn test_encode_f32_ceil_arm32() {
10644        let encoder = ArmEncoder::new_arm32();
10645        let op = ArmOp::F32Ceil {
10646            sd: VfpReg::S0,
10647            sm: VfpReg::S2,
10648        };
10649        let code = encoder.encode(&op).unwrap();
10650        // VMRS + BIC + ORR + VMSR + VCVT.S32.F32 + VMRS + BIC + VMSR + VCVT.F32.S32
10651        assert_eq!(code.len(), 36);
10652    }
10653
10654    #[test]
10655    fn test_encode_f32_floor_thumb2() {
10656        let encoder = ArmEncoder::new_thumb2();
10657        let op = ArmOp::F32Floor {
10658            sd: VfpReg::S0,
10659            sm: VfpReg::S2,
10660        };
10661        let code = encoder.encode(&op).unwrap();
10662        // VMRS + BIC.W + ORR.W + VMSR + VCVT + VMRS + BIC.W + VMSR + VCVT.F32.S32
10663        assert_eq!(code.len(), 36);
10664    }
10665
10666    #[test]
10667    fn test_encode_f32_min_arm32() {
10668        let encoder = ArmEncoder::new_arm32();
10669        let op = ArmOp::F32Min {
10670            sd: VfpReg::S0,
10671            sn: VfpReg::S2,
10672            sm: VfpReg::S4,
10673        };
10674        let code = encoder.encode(&op).unwrap();
10675        assert_eq!(code.len(), 16); // VMOV + VCMP + VMRS + conditional VMOV
10676    }
10677
10678    #[test]
10679    fn test_encode_f32_max_thumb2() {
10680        let encoder = ArmEncoder::new_thumb2();
10681        let op = ArmOp::F32Max {
10682            sd: VfpReg::S0,
10683            sn: VfpReg::S2,
10684            sm: VfpReg::S4,
10685        };
10686        let code = encoder.encode(&op).unwrap();
10687        // VMOV(4) + VCMP(4) + VMRS(4) + IT(2) + VMOV(4) = 18
10688        assert_eq!(code.len(), 18);
10689    }
10690
10691    #[test]
10692    fn test_encode_f32_copysign_arm32() {
10693        let encoder = ArmEncoder::new_arm32();
10694        let op = ArmOp::F32Copysign {
10695            sd: VfpReg::S0,
10696            sn: VfpReg::S2,
10697            sm: VfpReg::S4,
10698        };
10699        let code = encoder.encode(&op).unwrap();
10700        // VMOV + VMOV + AND + BIC + ORR + VMOV = 6 * 4 = 24
10701        assert_eq!(code.len(), 24);
10702    }
10703
10704    // ========================================================================
10705    // f64 encoding tests
10706    // ========================================================================
10707
10708    #[test]
10709    fn test_encode_f64_add_arm32() {
10710        let encoder = ArmEncoder::new_arm32();
10711        let op = ArmOp::F64Add {
10712            dd: VfpReg::D0,
10713            dn: VfpReg::D1,
10714            dm: VfpReg::D2,
10715        };
10716        let code = encoder.encode(&op).unwrap();
10717        assert_eq!(code.len(), 4);
10718        // VADD.F64 D0, D1, D2: check coprocessor is cp11 (0xB)
10719        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10720        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
10721    }
10722
10723    #[test]
10724    fn test_encode_f64_sub_thumb2() {
10725        let encoder = ArmEncoder::new_thumb2();
10726        let op = ArmOp::F64Sub {
10727            dd: VfpReg::D0,
10728            dn: VfpReg::D1,
10729            dm: VfpReg::D2,
10730        };
10731        let code = encoder.encode(&op).unwrap();
10732        assert_eq!(code.len(), 4); // 32-bit VFP as two Thumb halfwords
10733    }
10734
10735    #[test]
10736    fn test_encode_f64_mul_arm32() {
10737        let encoder = ArmEncoder::new_arm32();
10738        let op = ArmOp::F64Mul {
10739            dd: VfpReg::D0,
10740            dn: VfpReg::D1,
10741            dm: VfpReg::D2,
10742        };
10743        let code = encoder.encode(&op).unwrap();
10744        assert_eq!(code.len(), 4);
10745    }
10746
10747    #[test]
10748    fn test_encode_f64_div_arm32() {
10749        let encoder = ArmEncoder::new_arm32();
10750        let op = ArmOp::F64Div {
10751            dd: VfpReg::D0,
10752            dn: VfpReg::D1,
10753            dm: VfpReg::D2,
10754        };
10755        let code = encoder.encode(&op).unwrap();
10756        assert_eq!(code.len(), 4);
10757    }
10758
10759    #[test]
10760    fn test_encode_f64_abs_arm32() {
10761        let encoder = ArmEncoder::new_arm32();
10762        let op = ArmOp::F64Abs {
10763            dd: VfpReg::D0,
10764            dm: VfpReg::D2,
10765        };
10766        let code = encoder.encode(&op).unwrap();
10767        assert_eq!(code.len(), 4);
10768    }
10769
10770    #[test]
10771    fn test_encode_f64_neg_arm32() {
10772        let encoder = ArmEncoder::new_arm32();
10773        let op = ArmOp::F64Neg {
10774            dd: VfpReg::D0,
10775            dm: VfpReg::D2,
10776        };
10777        let code = encoder.encode(&op).unwrap();
10778        assert_eq!(code.len(), 4);
10779    }
10780
10781    #[test]
10782    fn test_encode_f64_sqrt_arm32() {
10783        let encoder = ArmEncoder::new_arm32();
10784        let op = ArmOp::F64Sqrt {
10785            dd: VfpReg::D0,
10786            dm: VfpReg::D2,
10787        };
10788        let code = encoder.encode(&op).unwrap();
10789        assert_eq!(code.len(), 4);
10790    }
10791
10792    #[test]
10793    fn test_encode_f64_load_arm32() {
10794        let encoder = ArmEncoder::new_arm32();
10795        let op = ArmOp::F64Load {
10796            dd: VfpReg::D0,
10797            addr: MemAddr::imm(Reg::R0, 8),
10798        };
10799        let code = encoder.encode(&op).unwrap();
10800        assert_eq!(code.len(), 4);
10801        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10802        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11 for F64
10803        assert_eq!(instr & 0xFF, 2); // offset 8 / 4 = 2
10804    }
10805
10806    #[test]
10807    fn test_encode_f64_store_thumb2() {
10808        let encoder = ArmEncoder::new_thumb2();
10809        let op = ArmOp::F64Store {
10810            dd: VfpReg::D0,
10811            addr: MemAddr::imm(Reg::SP, 0),
10812        };
10813        let code = encoder.encode(&op).unwrap();
10814        assert_eq!(code.len(), 4);
10815    }
10816
10817    #[test]
10818    fn test_encode_f64_compare_arm32() {
10819        let encoder = ArmEncoder::new_arm32();
10820        let op = ArmOp::F64Eq {
10821            rd: Reg::R0,
10822            dn: VfpReg::D0,
10823            dm: VfpReg::D1,
10824        };
10825        let code = encoder.encode(&op).unwrap();
10826        assert_eq!(code.len(), 16); // VCMP + VMRS + MOV #0 + MOVcond #1
10827    }
10828
10829    #[test]
10830    fn test_encode_f64_compare_thumb2() {
10831        let encoder = ArmEncoder::new_thumb2();
10832        let op = ArmOp::F64Lt {
10833            rd: Reg::R0,
10834            dn: VfpReg::D0,
10835            dm: VfpReg::D1,
10836        };
10837        let code = encoder.encode(&op).unwrap();
10838        // VCMP(4) + VMRS(4) + MOVS(2) + IT(2) + MOV(2) = 14
10839        assert_eq!(code.len(), 14);
10840    }
10841
10842    #[test]
10843    fn test_encode_f64_const_arm32() {
10844        let encoder = ArmEncoder::new_arm32();
10845        let op = ArmOp::F64Const {
10846            dd: VfpReg::D0,
10847            value: 3.125,
10848        };
10849        let code = encoder.encode(&op).unwrap();
10850        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
10851        assert_eq!(code.len(), 20);
10852    }
10853
10854    #[test]
10855    fn test_encode_f64_const_thumb2() {
10856        let encoder = ArmEncoder::new_thumb2();
10857        let op = ArmOp::F64Const {
10858            dd: VfpReg::D0,
10859            value: 2.5,
10860        };
10861        let code = encoder.encode(&op).unwrap();
10862        // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
10863        assert_eq!(code.len(), 20);
10864    }
10865
10866    #[test]
10867    fn test_encode_f64_convert_i32s_arm32() {
10868        let encoder = ArmEncoder::new_arm32();
10869        let op = ArmOp::F64ConvertI32S {
10870            dd: VfpReg::D0,
10871            rm: Reg::R0,
10872        };
10873        let code = encoder.encode(&op).unwrap();
10874        // VMOV(4) + VCVT(4) = 8
10875        assert_eq!(code.len(), 8);
10876    }
10877
10878    #[test]
10879    fn test_encode_f64_promote_f32_arm32() {
10880        let encoder = ArmEncoder::new_arm32();
10881        let op = ArmOp::F64PromoteF32 {
10882            dd: VfpReg::D0,
10883            sm: VfpReg::S0,
10884        };
10885        let code = encoder.encode(&op).unwrap();
10886        assert_eq!(code.len(), 4); // Single VCVT.F64.F32 instruction
10887    }
10888
10889    #[test]
10890    fn test_encode_f64_promote_f32_thumb2() {
10891        let encoder = ArmEncoder::new_thumb2();
10892        let op = ArmOp::F64PromoteF32 {
10893            dd: VfpReg::D0,
10894            sm: VfpReg::S0,
10895        };
10896        let code = encoder.encode(&op).unwrap();
10897        assert_eq!(code.len(), 4);
10898    }
10899
10900    #[test]
10901    fn test_encode_i32_trunc_f64s_arm32() {
10902        let encoder = ArmEncoder::new_arm32();
10903        let op = ArmOp::I32TruncF64S {
10904            rd: Reg::R0,
10905            dm: VfpReg::D0,
10906        };
10907        let code = encoder.encode(&op).unwrap();
10908        // VCVT(4) + VMOV(4) = 8
10909        assert_eq!(code.len(), 8);
10910    }
10911
10912    #[test]
10913    fn test_encode_f64_reinterpret_i64_arm32() {
10914        let encoder = ArmEncoder::new_arm32();
10915        let op = ArmOp::F64ReinterpretI64 {
10916            dd: VfpReg::D0,
10917            rmlo: Reg::R0,
10918            rmhi: Reg::R1,
10919        };
10920        let code = encoder.encode(&op).unwrap();
10921        assert_eq!(code.len(), 4); // Single VMOV instruction
10922    }
10923
10924    #[test]
10925    fn test_encode_i64_reinterpret_f64_thumb2() {
10926        let encoder = ArmEncoder::new_thumb2();
10927        let op = ArmOp::I64ReinterpretF64 {
10928            rdlo: Reg::R0,
10929            rdhi: Reg::R1,
10930            dm: VfpReg::D0,
10931        };
10932        let code = encoder.encode(&op).unwrap();
10933        assert_eq!(code.len(), 4);
10934    }
10935
10936    #[test]
10937    fn test_encode_f64_trunc_thumb2() {
10938        let encoder = ArmEncoder::new_thumb2();
10939        let op = ArmOp::F64Trunc {
10940            dd: VfpReg::D0,
10941            dm: VfpReg::D1,
10942        };
10943        let code = encoder.encode(&op).unwrap();
10944        // GI-FPU-002 phase 3 (#369): a single VRINTZ.F64 (clang-verified
10945        // vrintz.f64 d0,d1 base) — no more FPSCR dance / S0 clobber.
10946        assert_eq!(code.len(), 4);
10947        assert_eq!(code, vec![0xb6, 0xee, 0xc1, 0x0b]);
10948    }
10949
10950    /// GI-FPU-002 phase 3 (#369): the rewritten f64 tail sequences, byte-exact
10951    /// against clang (`-target thumbv7em-none-eabi -mfpu=fpv5-d16`). Each
10952    /// clobbers ONLY its destination (+R12/flags where noted) — the previous
10953    /// pseudo-ops staged through live S0/R0-R2 (the #615 class) and the
10954    /// min/max/rounding semantics were wrong (ordered IT select returned the
10955    /// wrong operand on NaN/±0; rounding round-tripped through a 32-bit int).
10956    #[test]
10957    fn test_369_f64_tail_thumb2_encodings_match_clang() {
10958        let enc = ArmEncoder::new_thumb2();
10959        // vrintn/vrintp/vrintm.f64 d1, d2 (FE space, never IT'd).
10960        for (op, want) in [
10961            (
10962                ArmOp::F64Nearest {
10963                    dd: VfpReg::D1,
10964                    dm: VfpReg::D2,
10965                },
10966                vec![0xb9, 0xfe, 0x42, 0x1b],
10967            ),
10968            (
10969                ArmOp::F64Ceil {
10970                    dd: VfpReg::D1,
10971                    dm: VfpReg::D2,
10972                },
10973                vec![0xba, 0xfe, 0x42, 0x1b],
10974            ),
10975            (
10976                ArmOp::F64Floor {
10977                    dd: VfpReg::D1,
10978                    dm: VfpReg::D2,
10979                },
10980                vec![0xbb, 0xfe, 0x42, 0x1b],
10981            ),
10982        ] {
10983            assert_eq!(enc.encode(&op).unwrap(), want, "{op:?}");
10984        }
10985        // vcmp.f64 d1,d2 ; vmrs ; vminnm.f64 d0,d1,d2 ; it vs ; vaddvs.f64
10986        let min = enc
10987            .encode(&ArmOp::F64Min {
10988                dd: VfpReg::D0,
10989                dn: VfpReg::D1,
10990                dm: VfpReg::D2,
10991            })
10992            .unwrap();
10993        assert_eq!(
10994            min,
10995            vec![
10996                0xb4, 0xee, 0x42, 0x1b, // vcmp.f64 d1, d2
10997                0xf1, 0xee, 0x10, 0xfa, // vmrs APSR_nzcv, fpscr
10998                0x81, 0xfe, 0x42, 0x0b, // vminnm.f64 d0, d1, d2
10999                0x68, 0xbf, // it vs
11000                0x31, 0xee, 0x02, 0x0b, // vaddvs.f64 d0, d1, d2
11001            ]
11002        );
11003        // vmaxnm variant flips only bit6 of the VMINNM word.
11004        let max = enc
11005            .encode(&ArmOp::F64Max {
11006                dd: VfpReg::D0,
11007                dn: VfpReg::D1,
11008                dm: VfpReg::D2,
11009            })
11010            .unwrap();
11011        assert_eq!(&max[8..12], &[0x81, 0xfe, 0x02, 0x0b]);
11012        // Destination aliasing a source must ERR (the NaN fix-up would read
11013        // a clobbered operand), never encode.
11014        assert!(
11015            enc.encode(&ArmOp::F64Min {
11016                dd: VfpReg::D1,
11017                dn: VfpReg::D1,
11018                dm: VfpReg::D2,
11019            })
11020            .is_err()
11021        );
11022        // copysign d0,(mag)d1,(sign)d2:
11023        // vmov r12,s5 ; cmp.w r12,#0 ; vabs.f64 d0,d1 ; it mi ; vnegmi.f64 d0,d0
11024        let cs = enc
11025            .encode(&ArmOp::F64Copysign {
11026                dd: VfpReg::D0,
11027                dn: VfpReg::D1,
11028                dm: VfpReg::D2,
11029            })
11030            .unwrap();
11031        assert_eq!(
11032            cs,
11033            vec![
11034                0x12, 0xee, 0x90, 0xca, // vmov r12, s5
11035                0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
11036                0xb0, 0xee, 0xc1, 0x0b, // vabs.f64 d0, d1
11037                0x48, 0xbf, // it mi
11038                0xb1, 0xee, 0x40, 0x0b, // vnegmi.f64 d0, d0
11039            ]
11040        );
11041        // f32 copysign s0,(mag)s1,(sign)s2 — the R0-clobber-free rewrite:
11042        // vmov r12,s2 ; cmp.w r12,#0 ; vabs.f32 s0,s1 ; it mi ; vnegmi.f32
11043        let cs32 = enc
11044            .encode(&ArmOp::F32Copysign {
11045                sd: VfpReg::S0,
11046                sn: VfpReg::S1,
11047                sm: VfpReg::S2,
11048            })
11049            .unwrap();
11050        assert_eq!(
11051            cs32,
11052            vec![
11053                0x11, 0xee, 0x10, 0xca, // vmov r12, s2
11054                0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
11055                0xb0, 0xee, 0xe0, 0x0a, // vabs.f32 s0, s1
11056                0x48, 0xbf, // it mi
11057                0xb1, 0xee, 0x40, 0x0a, // vnegmi.f32 s0, s0
11058            ]
11059        );
11060        // i32 -> f64 stages through the DESTINATION's S-alias (never S0) and
11061        // uses the CORRECT signed/unsigned VCVT bases (previously swapped):
11062        // vmov s0,r3 ; vcvt.f64.s32 d0,s0
11063        let conv_s = enc
11064            .encode(&ArmOp::F64ConvertI32S {
11065                dd: VfpReg::D0,
11066                rm: Reg::R3,
11067            })
11068            .unwrap();
11069        assert_eq!(
11070            conv_s,
11071            vec![
11072                0x00, 0xee, 0x10, 0x3a, // vmov s0, r3
11073                0xb8, 0xee, 0xc0, 0x0b, // vcvt.f64.s32 d0, s0
11074            ]
11075        );
11076        let conv_u = enc
11077            .encode(&ArmOp::F64ConvertI32U {
11078                dd: VfpReg::D0,
11079                rm: Reg::R3,
11080            })
11081            .unwrap();
11082        assert_eq!(&conv_u[4..8], &[0xb8, 0xee, 0x40, 0x0b]); // vcvt.f64.u32
11083        // f64 -> i32 stages through the SOURCE's S-alias (never S0):
11084        // vcvt.s32.f64 s2,d1 ; vmov r3,s2
11085        let trunc_s = enc
11086            .encode(&ArmOp::I32TruncF64S {
11087                rd: Reg::R3,
11088                dm: VfpReg::D1,
11089            })
11090            .unwrap();
11091        assert_eq!(
11092            trunc_s,
11093            vec![
11094                0xbd, 0xee, 0xc1, 0x1b, // vcvt.s32.f64 s2, d1
11095                0x11, 0xee, 0x10, 0x3a, // vmov r3, s2
11096            ]
11097        );
11098        let trunc_u = enc
11099            .encode(&ArmOp::I32TruncF64U {
11100                rd: Reg::R3,
11101                dm: VfpReg::D1,
11102            })
11103            .unwrap();
11104        assert_eq!(&trunc_u[0..4], &[0xbc, 0xee, 0xc1, 0x1b]); // vcvt.u32.f64
11105        // f32.demote_f64: vcvt.f32.f64 s1, d2
11106        let demote = enc
11107            .encode(&ArmOp::F32DemoteF64 {
11108                sd: VfpReg::S1,
11109                dm: VfpReg::D2,
11110            })
11111            .unwrap();
11112        assert_eq!(demote, vec![0xf7, 0xee, 0xc2, 0x0b]);
11113    }
11114
11115    #[test]
11116    fn test_encode_f64_min_arm32() {
11117        let encoder = ArmEncoder::new_arm32();
11118        let op = ArmOp::F64Min {
11119            dd: VfpReg::D0,
11120            dn: VfpReg::D1,
11121            dm: VfpReg::D2,
11122        };
11123        let code = encoder.encode(&op).unwrap();
11124        // VMOV + VCMP + VMRS + conditional VMOV = 16
11125        assert_eq!(code.len(), 16);
11126    }
11127
11128    #[test]
11129    fn test_f64_cp11_encoding() {
11130        // Verify that F64 instructions use coprocessor 11 (0xB), not 10 (0xA)
11131        let encoder = ArmEncoder::new_arm32();
11132
11133        // F64Add
11134        let code = encoder
11135            .encode(&ArmOp::F64Add {
11136                dd: VfpReg::D0,
11137                dn: VfpReg::D0,
11138                dm: VfpReg::D0,
11139            })
11140            .unwrap();
11141        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11142        assert_eq!((instr >> 8) & 0xF, 0xB, "F64 should use cp11");
11143
11144        // F32Add for comparison
11145        let code = encoder
11146            .encode(&ArmOp::F32Add {
11147                sd: VfpReg::S0,
11148                sn: VfpReg::S0,
11149                sm: VfpReg::S0,
11150            })
11151            .unwrap();
11152        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11153        assert_eq!((instr >> 8) & 0xF, 0xA, "F32 should use cp10");
11154    }
11155
11156    #[test]
11157    fn test_dreg_encoding_higher_registers() {
11158        let encoder = ArmEncoder::new_arm32();
11159
11160        // Test with D15 (highest register)
11161        let op = ArmOp::F64Add {
11162            dd: VfpReg::D15,
11163            dn: VfpReg::D14,
11164            dm: VfpReg::D13,
11165        };
11166        let code = encoder.encode(&op).unwrap();
11167        assert_eq!(code.len(), 4);
11168
11169        // Verify the register encoding worked (instruction is valid)
11170        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11171        assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
11172    }
11173
11174    // ========================================================================
11175    // Control flow encoding tests
11176    // ========================================================================
11177
11178    #[test]
11179    fn test_encode_label_emits_no_bytes() {
11180        let encoder = ArmEncoder::new_thumb2();
11181        let op = ArmOp::Label {
11182            name: ".Lblock_end_0".to_string(),
11183        };
11184        let code = encoder.encode(&op).unwrap();
11185        assert!(code.is_empty(), "Label should emit zero bytes");
11186
11187        let encoder32 = ArmEncoder::new_arm32();
11188        let code32 = encoder32.encode(&op).unwrap();
11189        assert!(
11190            code32.is_empty(),
11191            "Label should emit zero bytes in ARM32 too"
11192        );
11193    }
11194
11195    #[test]
11196    fn test_encode_bcc_eq_thumb2() {
11197        use synth_synthesis::Condition;
11198        let encoder = ArmEncoder::new_thumb2();
11199        let op = ArmOp::Bcc {
11200            cond: Condition::EQ,
11201            label: "target".to_string(),
11202        };
11203        let code = encoder.encode(&op).unwrap();
11204        assert_eq!(code.len(), 2); // 16-bit conditional branch
11205
11206        // BEQ with offset 0: 0xD000 in little-endian
11207        assert_eq!(code, vec![0x00, 0xD0]);
11208    }
11209
11210    #[test]
11211    fn test_encode_bcc_ne_thumb2() {
11212        use synth_synthesis::Condition;
11213        let encoder = ArmEncoder::new_thumb2();
11214        let op = ArmOp::Bcc {
11215            cond: Condition::NE,
11216            label: "target".to_string(),
11217        };
11218        let code = encoder.encode(&op).unwrap();
11219        assert_eq!(code.len(), 2);
11220
11221        // BNE with offset 0: 0xD100 in little-endian
11222        assert_eq!(code, vec![0x00, 0xD1]);
11223    }
11224
11225    #[test]
11226    fn test_encode_bcc_arm32() {
11227        use synth_synthesis::Condition;
11228        let encoder = ArmEncoder::new_arm32();
11229        let op = ArmOp::Bcc {
11230            cond: Condition::EQ,
11231            label: "target".to_string(),
11232        };
11233        let code = encoder.encode(&op).unwrap();
11234        assert_eq!(code.len(), 4); // 32-bit ARM instruction
11235
11236        let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11237        // BEQ: cond=0x0, opcode=0xA, offset=0
11238        assert_eq!(instr & 0xF0000000, 0x00000000); // EQ condition
11239        assert_eq!(instr & 0x0F000000, 0x0A000000); // Branch opcode
11240    }
11241
11242    #[test]
11243    fn test_encode_udf_thumb2() {
11244        let encoder = ArmEncoder::new_thumb2();
11245        let op = ArmOp::Udf { imm: 0 };
11246        let code = encoder.encode(&op).unwrap();
11247        assert_eq!(code.len(), 2); // 16-bit
11248
11249        // UDF #0: 0xDE00 in little-endian
11250        assert_eq!(code, vec![0x00, 0xDE]);
11251    }
11252
11253    /// #610: the i64 rot/div/rem expansions must land the result in the
11254    /// selector-assigned rd pair and leave R0-R3 preserved (restored from the
11255    /// fixed-ABI wrapper's save area) — pre-#610 the rot expansion's own
11256    /// `POP {R4}` restored stale scratch OVER the result (rd_lo == R4) and
11257    /// the div/rem expansions ignored their register fields outright.
11258    #[test]
11259    fn test_610_i64_rot_expansion_ends_with_rd_movs_and_restore() {
11260        let encoder = ArmEncoder::new_thumb2();
11261        for op in [
11262            ArmOp::I64Rotl {
11263                rdlo: Reg::R4,
11264                rdhi: Reg::R5,
11265                rnlo: Reg::R0,
11266                rnhi: Reg::R1,
11267                shift: Reg::R2,
11268            },
11269            ArmOp::I64Rotr {
11270                rdlo: Reg::R4,
11271                rdhi: Reg::R5,
11272                rnlo: Reg::R0,
11273                rnhi: Reg::R1,
11274                shift: Reg::R2,
11275            },
11276        ] {
11277            let code = encoder.encode(&op).unwrap();
11278            assert_eq!(code.len(), 102, "register-independent size (estimator pin)");
11279            // Tail: MOV r5, r1 (0x460D); MOV r4, r0 (0x4604); POP {r0..r3}
11280            // (rd pair r4:r5 does not overlap the save area — all 4 restored).
11281            let tail: Vec<u16> = code[code.len() - 12..]
11282                .chunks(2)
11283                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11284                .collect();
11285            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11286        }
11287    }
11288
11289    /// #610: div/rem expansions honor rd and carry the divide-by-zero trap
11290    /// guard (`ORRS R12, R2, R3; BNE +0; UDF #0`) after operand marshaling.
11291    #[test]
11292    fn test_610_i64_div_rem_expansion_guard_and_rd() {
11293        let encoder = ArmEncoder::new_thumb2();
11294        let mk = |which: u8| {
11295            let (rdlo, rdhi, rnlo, rnhi, rmlo, rmhi) =
11296                (Reg::R4, Reg::R5, Reg::R0, Reg::R1, Reg::R2, Reg::R3);
11297            match which {
11298                0 => ArmOp::I64DivU {
11299                    rdlo,
11300                    rdhi,
11301                    rnlo,
11302                    rnhi,
11303                    rmlo,
11304                    rmhi,
11305                    elide_zero_guard: false,
11306                },
11307                1 => ArmOp::I64RemU {
11308                    rdlo,
11309                    rdhi,
11310                    rnlo,
11311                    rnhi,
11312                    rmlo,
11313                    rmhi,
11314                    elide_zero_guard: false,
11315                },
11316                2 => ArmOp::I64DivS {
11317                    rdlo,
11318                    rdhi,
11319                    rnlo,
11320                    rnhi,
11321                    rmlo,
11322                    rmhi,
11323                    elide_zero_guard: false,
11324                    elide_overflow_guard: false,
11325                },
11326                _ => ArmOp::I64RemS {
11327                    rdlo,
11328                    rdhi,
11329                    rnlo,
11330                    rnhi,
11331                    rmlo,
11332                    rmhi,
11333                    elide_zero_guard: false,
11334                },
11335            }
11336        };
11337        for which in 0..4u8 {
11338            let code = encoder.encode(&mk(which)).unwrap();
11339            // Zero-divisor trap guard right after the 26-byte marshal prologue.
11340            let guard: Vec<u16> = code[26..34]
11341                .chunks(2)
11342                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11343                .collect();
11344            assert_eq!(
11345                guard,
11346                vec![0xEA52, 0x0C03, 0xD100, 0xDE00],
11347                "ORRS R12,R2,R3; BNE +0; UDF #0"
11348            );
11349            // Tail: result into rd pair (r5:r4), then restore all of R0-R3.
11350            let tail: Vec<u16> = code[code.len() - 12..]
11351                .chunks(2)
11352                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11353                .collect();
11354            assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11355        }
11356    }
11357
11358    /// #610: when rd overlaps R0-R3 the restore must SKIP the result
11359    /// registers (drop the saved caller word) instead of popping over them.
11360    #[test]
11361    fn test_610_i64_divu_rd_in_r0_r1_skips_restore() {
11362        let encoder = ArmEncoder::new_thumb2();
11363        let code = encoder
11364            .encode(&ArmOp::I64DivU {
11365                rdlo: Reg::R0,
11366                rdhi: Reg::R1,
11367                rnlo: Reg::R0,
11368                rnhi: Reg::R1,
11369                rmlo: Reg::R2,
11370                rmhi: Reg::R3,
11371                elide_zero_guard: false,
11372            })
11373            .unwrap();
11374        let tail: Vec<u16> = code[code.len() - 12..]
11375            .chunks(2)
11376            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11377            .collect();
11378        // MOV r1,r1 / MOV r0,r0 (no-ops, size-stable), ADD SP,#4 twice
11379        // (discard saved r0/r1 — the result lives there), POP {r2}, POP {r3}.
11380        assert_eq!(tail, vec![0x4609, 0x4600, 0xB001, 0xB001, 0xBC04, 0xBC08]);
11381    }
11382
11383    /// #610: a fully swapped rd pair (rd_lo=R1, rd_hi=R0) cannot be
11384    /// materialized by two MOVs in either order — must be a loud Err, never
11385    /// silent corruption. (Selector pairs are consecutive, so unreachable.)
11386    #[test]
11387    fn test_610_i64_swapped_rd_pair_rejected() {
11388        let encoder = ArmEncoder::new_thumb2();
11389        let result = encoder.encode(&ArmOp::I64RemU {
11390            rdlo: Reg::R1,
11391            rdhi: Reg::R0,
11392            rnlo: Reg::R2,
11393            rnhi: Reg::R3,
11394            rmlo: Reg::R4,
11395            rmhi: Reg::R5,
11396            elide_zero_guard: false,
11397        });
11398        assert!(result.is_err(), "swapped rd pair must be rejected loudly");
11399    }
11400
11401    /// #632: the I64Popcnt expansion's own scratch restore (`POP {R3,R4,R5}`)
11402    /// must not clobber the result. Pre-fix the total was materialized with
11403    /// `ADDS rd, R4, R5` BEFORE the pop, so any allocator-assigned
11404    /// rd ∈ {R3,R4,R5} received stale stack garbage. Post-fix the count is
11405    /// carried across the restore in R12 (never allocatable, never restored)
11406    /// and moved into rd only after the pop — structurally rd-independent.
11407    #[test]
11408    fn test_632_i64_popcnt_result_survives_scratch_restore() {
11409        let encoder = ArmEncoder::new_thumb2();
11410        // Every allocatable rd, including the restore set {R3,R4,R5} and R8.
11411        for rd in [
11412            Reg::R0,
11413            Reg::R2,
11414            Reg::R3,
11415            Reg::R4,
11416            Reg::R5,
11417            Reg::R6,
11418            Reg::R8,
11419        ] {
11420            let code = encoder
11421                .encode(&ArmOp::I64Popcnt {
11422                    rd,
11423                    rnlo: Reg::R6,
11424                    rnhi: Reg::R7,
11425                })
11426                .unwrap();
11427            assert_eq!(code.len(), 176, "register-independent size (estimator pin)");
11428            let hw: Vec<u16> = code
11429                .chunks(2)
11430                .map(|c| u16::from_le_bytes([c[0], c[1]]))
11431                .collect();
11432            let pop = hw
11433                .iter()
11434                .position(|&h| h == 0xBC38)
11435                .expect("POP {R3,R4,R5} present");
11436            // Immediately before the POP: ADD.W R12, R4, R5 (the total lives
11437            // in R12, which the POP cannot touch).
11438            assert_eq!(
11439                &hw[pop - 2..pop],
11440                &[0xEB04, 0x0C05],
11441                "total must be carried in R12 across the restore"
11442            );
11443            // Immediately after the POP: MOV rd, R12.
11444            let rd_bits = match rd {
11445                Reg::R8 => 8u16,
11446                Reg::R6 => 6,
11447                Reg::R5 => 5,
11448                Reg::R4 => 4,
11449                Reg::R3 => 3,
11450                Reg::R2 => 2,
11451                _ => 0,
11452            };
11453            let expect_mov = 0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7);
11454            assert_eq!(hw[pop + 1], expect_mov, "MOV rd, R12 after the restore");
11455            // No write into rd between the PUSH and the POP (the old
11456            // pre-restore ADDS is gone).
11457            assert!(
11458                !hw[..pop].contains(&(0x1800 | (5 << 6) | (4 << 3) | rd_bits)),
11459                "no ADDS rd, R4, R5 before the restore pop"
11460            );
11461        }
11462    }
11463
11464    /// #632 audit: the entry marshal must be permutation-safe. Pre-fix
11465    /// `MOV R4, rnlo; MOV R5, rnhi` read a clobbered R4 when the operand
11466    /// pair lived at (R3, R4). Post-fix rnlo routes through R12.
11467    #[test]
11468    fn test_632_i64_popcnt_marshal_pair_at_r3_r4() {
11469        let encoder = ArmEncoder::new_thumb2();
11470        let code = encoder
11471            .encode(&ArmOp::I64Popcnt {
11472                rd: Reg::R0,
11473                rnlo: Reg::R3,
11474                rnhi: Reg::R4,
11475            })
11476            .unwrap();
11477        let hw: Vec<u16> = code
11478            .chunks(2)
11479            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11480            .collect();
11481        // PUSH {R3,R4,R5}; MOV R12, R3; MOV R5, R4 (rnhi read BEFORE any
11482        // write to R4); MOV R4, R12.
11483        assert_eq!(hw[0], 0xB438);
11484        assert_eq!(hw[1], 0x4600 | (1 << 7) | (3 << 3) | 4, "MOV R12, rnlo");
11485        assert_eq!(hw[2], 0x4600 | (4 << 3) | 5, "MOV R5, rnhi");
11486        assert_eq!(hw[3], 0x4664, "MOV R4, R12");
11487    }
11488
11489    /// #632: A32 twin — same structural fix on the ARM-mode path
11490    /// (`--target cortex-r5`): total carried in R12 across the restore.
11491    #[test]
11492    fn test_632_a32_i64_popcnt_result_survives_scratch_restore() {
11493        let encoder = ArmEncoder::new_arm32();
11494        for rd in [Reg::R0, Reg::R3, Reg::R4, Reg::R5, Reg::R8] {
11495            let code = encoder
11496                .encode(&ArmOp::I64Popcnt {
11497                    rd,
11498                    rnlo: Reg::R6,
11499                    rnhi: Reg::R7,
11500                })
11501                .unwrap();
11502            let words: Vec<u32> = code
11503                .chunks(4)
11504                .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11505                .collect();
11506            let pop = words
11507                .iter()
11508                .position(|&w| w == 0xE8BD_0038)
11509                .expect("POP {R3,R4,R5} present");
11510            assert_eq!(words[pop - 1], 0xE084_C005, "ADD R12, R4, R5 before POP");
11511            let rd_bits = match rd {
11512                Reg::R8 => 8u32,
11513                Reg::R5 => 5,
11514                Reg::R4 => 4,
11515                Reg::R3 => 3,
11516                _ => 0,
11517            };
11518            assert_eq!(
11519                words[pop + 1],
11520                0xE1A0_0000 | (rd_bits << 12) | 12,
11521                "MOV rd, R12 after the restore"
11522            );
11523        }
11524    }
11525
11526    /// #633: I64DivS must carry the INT64_MIN/-1 overflow guard (mirroring
11527    /// the i32 path) right after the zero-divisor guard — dividend in R0:R1,
11528    /// divisor in R2:R3 on the #610/#613 fixed-ABI wrapper path.
11529    #[test]
11530    fn test_633_i64_divs_overflow_guard_emitted() {
11531        let encoder = ArmEncoder::new_thumb2();
11532        let code = encoder
11533            .encode(&ArmOp::I64DivS {
11534                rdlo: Reg::R4,
11535                rdhi: Reg::R5,
11536                rnlo: Reg::R0,
11537                rnhi: Reg::R1,
11538                rmlo: Reg::R2,
11539                rmhi: Reg::R3,
11540                elide_zero_guard: false,
11541                elide_overflow_guard: false,
11542            })
11543            .unwrap();
11544        // 26-byte marshal + 8-byte zero-trap, then the 22-byte overflow guard.
11545        let guard: Vec<u16> = code[34..56]
11546            .chunks(2)
11547            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11548            .collect();
11549        assert_eq!(
11550            guard,
11551            vec![
11552                0xEA02, 0x0C03, // AND.W R12, R2, R3
11553                0xF11C, 0x0F01, // CMN.W R12, #1
11554                0xD105, // BNE .no_trap
11555                0x2800, // CMP R0, #0
11556                0xD103, // BNE .no_trap
11557                0xF1B1, 0x4F00, // CMP.W R1, #0x80000000
11558                0xD100, // BNE .no_trap
11559                0xDE00, // UDF #0 — signed-division overflow
11560            ],
11561            "INT64_MIN/-1 overflow guard after the zero-divisor guard"
11562        );
11563    }
11564
11565    /// #633 fix-guard twin: I64RemS must NOT carry the overflow guard —
11566    /// rem_s(INT64_MIN, -1) is defined as 0 and must not trap. Exactly one
11567    /// UDF (the zero-divisor trap) in the whole expansion.
11568    #[test]
11569    fn test_633_i64_rems_has_no_overflow_guard() {
11570        let encoder = ArmEncoder::new_thumb2();
11571        for (is_rem_s, op) in [
11572            (
11573                true,
11574                ArmOp::I64RemS {
11575                    rdlo: Reg::R4,
11576                    rdhi: Reg::R5,
11577                    rnlo: Reg::R0,
11578                    rnhi: Reg::R1,
11579                    rmlo: Reg::R2,
11580                    rmhi: Reg::R3,
11581                    elide_zero_guard: false,
11582                },
11583            ),
11584            (
11585                false,
11586                ArmOp::I64DivS {
11587                    rdlo: Reg::R4,
11588                    rdhi: Reg::R5,
11589                    rnlo: Reg::R0,
11590                    rnhi: Reg::R1,
11591                    rmlo: Reg::R2,
11592                    rmhi: Reg::R3,
11593                    elide_zero_guard: false,
11594                    elide_overflow_guard: false,
11595                },
11596            ),
11597        ] {
11598            let code = encoder.encode(&op).unwrap();
11599            let udfs = code
11600                .chunks(2)
11601                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11602                .count();
11603            let want = if is_rem_s { 1 } else { 2 };
11604            assert_eq!(
11605                udfs, want,
11606                "rem_s: zero-trap only; div_s: zero-trap + overflow trap"
11607            );
11608        }
11609    }
11610
11611    /// #494 phase 2b: `elide_zero_guard` drops EXACTLY the 8-byte fused
11612    /// zero-trap (`ORRS.W R12,R2,R3; BNE; UDF #0`) and nothing else — the
11613    /// rest of the expansion is byte-identical (splice check).
11614    #[test]
11615    fn test_494_i64_zero_guard_elision_is_exact_splice() {
11616        let encoder = ArmEncoder::new_thumb2();
11617        let mk = |elide_zero_guard: bool| {
11618            encoder
11619                .encode(&ArmOp::I64DivU {
11620                    rdlo: Reg::R4,
11621                    rdhi: Reg::R5,
11622                    rnlo: Reg::R0,
11623                    rnhi: Reg::R1,
11624                    rmlo: Reg::R2,
11625                    rmhi: Reg::R3,
11626                    elide_zero_guard,
11627                })
11628                .unwrap()
11629        };
11630        let full = mk(false);
11631        let elided = mk(true);
11632        assert_eq!(full.len(), elided.len() + 8, "zero guard is 8 bytes");
11633        // Marshal prologue (26 B) unchanged, guard (8 B) gone, tail identical.
11634        assert_eq!(&full[..26], &elided[..26]);
11635        assert_eq!(
11636            &full[26..34],
11637            &[0x52, 0xEA, 0x03, 0x0C, 0x00, 0xD1, 0x00, 0xDE],
11638            "the spliced-out bytes are exactly ORRS.W; BNE; UDF #0"
11639        );
11640        assert_eq!(&full[34..], &elided[26..]);
11641    }
11642
11643    /// #494 phase 2b two-guard distinction (the #633/#634 synergy): a
11644    /// divisor-nonzero fact elides ONLY the zero guard — the INT64_MIN/-1
11645    /// OVERFLOW guard is a separate obligation and must survive
11646    /// `elide_zero_guard: true`. Pinned on div_s in all flag states.
11647    #[test]
11648    fn test_494_i64_divs_overflow_guard_retained_when_only_zero_elided() {
11649        let encoder = ArmEncoder::new_thumb2();
11650        let mk = |zero: bool, ovf: bool| {
11651            encoder
11652                .encode(&ArmOp::I64DivS {
11653                    rdlo: Reg::R4,
11654                    rdhi: Reg::R5,
11655                    rnlo: Reg::R0,
11656                    rnhi: Reg::R1,
11657                    rmlo: Reg::R2,
11658                    rmhi: Reg::R3,
11659                    elide_zero_guard: zero,
11660                    elide_overflow_guard: ovf,
11661                })
11662                .unwrap()
11663        };
11664        let udf_count = |code: &[u8]| {
11665            code.chunks(2)
11666                .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11667                .count()
11668        };
11669        let full = mk(false, false);
11670        let zero_only = mk(true, false);
11671        let both = mk(true, true);
11672        assert_eq!(udf_count(&full), 2, "baseline: zero trap + overflow trap");
11673        assert_eq!(
11674            udf_count(&zero_only),
11675            1,
11676            "divisor-nonzero elides the zero trap ONLY — the #633 overflow \
11677             guard must be retained"
11678        );
11679        // The retained guard is the 22-byte overflow sequence, now right
11680        // after the 26-byte marshal prologue.
11681        let guard: Vec<u16> = zero_only[26..48]
11682            .chunks(2)
11683            .map(|c| u16::from_le_bytes([c[0], c[1]]))
11684            .collect();
11685        assert_eq!(
11686            guard,
11687            vec![
11688                0xEA02, 0x0C03, 0xF11C, 0x0F01, 0xD105, 0x2800, 0xD103, 0xF1B1, 0x4F00, 0xD100,
11689                0xDE00,
11690            ],
11691            "the surviving guard is the INT64_MIN/-1 overflow trap"
11692        );
11693        assert_eq!(full.len(), zero_only.len() + 8);
11694        assert_eq!(zero_only.len(), both.len() + 22);
11695        assert_eq!(udf_count(&both), 0, "both obligations discharged ⇒ no UDF");
11696    }
11697
11698    /// #494 phase 2b A32 twin: zero-guard elision is an exact 12-byte splice
11699    /// and the A32 overflow guard survives a zero-only elision.
11700    #[test]
11701    fn test_494_a32_i64_guard_elision() {
11702        let encoder = ArmEncoder::new_arm32();
11703        let mk = |zero: bool, ovf: bool| {
11704            encoder
11705                .encode(&ArmOp::I64DivS {
11706                    rdlo: Reg::R4,
11707                    rdhi: Reg::R5,
11708                    rnlo: Reg::R0,
11709                    rnhi: Reg::R1,
11710                    rmlo: Reg::R2,
11711                    rmhi: Reg::R3,
11712                    elide_zero_guard: zero,
11713                    elide_overflow_guard: ovf,
11714                })
11715                .unwrap()
11716        };
11717        let full = mk(false, false);
11718        let zero_only = mk(true, false);
11719        let both = mk(true, true);
11720        // A32 zero guard = 3 words (ORRS/BNE/UDF), overflow guard = 6 words.
11721        assert_eq!(full.len(), zero_only.len() + 12);
11722        assert_eq!(zero_only.len(), both.len() + 24);
11723        let udf_count = |code: &[u8]| {
11724            code.chunks(4)
11725                .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
11726                .count()
11727        };
11728        assert_eq!(udf_count(&full), 2);
11729        assert_eq!(
11730            udf_count(&zero_only),
11731            1,
11732            "A32: overflow guard retained under zero-only elision"
11733        );
11734        assert_eq!(udf_count(&both), 0);
11735    }
11736
11737    /// #633: A32 twin — the conditional-execution overflow guard on the
11738    /// ARM-mode I64DivS, and its absence from I64RemS.
11739    #[test]
11740    fn test_633_a32_i64_divs_overflow_guard() {
11741        let encoder = ArmEncoder::new_arm32();
11742        let mk_divs = ArmOp::I64DivS {
11743            rdlo: Reg::R4,
11744            rdhi: Reg::R5,
11745            rnlo: Reg::R0,
11746            rnhi: Reg::R1,
11747            rmlo: Reg::R2,
11748            rmhi: Reg::R3,
11749            elide_zero_guard: false,
11750            elide_overflow_guard: false,
11751        };
11752        let code = encoder.encode(&mk_divs).unwrap();
11753        let words: Vec<u32> = code
11754            .chunks(4)
11755            .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11756            .collect();
11757        let guard = [
11758            0xE002_C003u32, // AND   R12, R2, R3
11759            0xE37C_0001,    // CMN   R12, #1
11760            0x0350_0000,    // CMPEQ R0, #0
11761            0x0351_0102,    // CMPEQ R1, #0x80000000
11762            0x1A00_0000,    // BNE +1 insn
11763            0xE7F0_00F0,    // UDF #0
11764        ];
11765        assert!(
11766            words.windows(6).any(|w| w == guard),
11767            "A32 I64DivS carries the INT64_MIN/-1 overflow guard"
11768        );
11769        let rems = encoder
11770            .encode(&ArmOp::I64RemS {
11771                rdlo: Reg::R4,
11772                rdhi: Reg::R5,
11773                rnlo: Reg::R0,
11774                rnhi: Reg::R1,
11775                rmlo: Reg::R2,
11776                rmhi: Reg::R3,
11777                elide_zero_guard: false,
11778            })
11779            .unwrap();
11780        let rems_udfs = rems
11781            .chunks(4)
11782            .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
11783            .count();
11784        assert_eq!(rems_udfs, 1, "A32 I64RemS keeps only the zero-divisor trap");
11785    }
11786
11787    #[test]
11788    fn test_encode_nop_thumb2() {
11789        let encoder = ArmEncoder::new_thumb2();
11790        let op = ArmOp::Nop;
11791        let code = encoder.encode(&op).unwrap();
11792        assert_eq!(code.len(), 2); // 16-bit
11793
11794        // NOP: 0xBF00 in little-endian
11795        assert_eq!(code, vec![0x00, 0xBF]);
11796    }
11797
11798    // =========================================================================
11799    // i64 Thumb-2 encoding tests
11800    // =========================================================================
11801
11802    #[test]
11803    fn test_encode_i64_add_thumb2() {
11804        let encoder = ArmEncoder::new_thumb2();
11805        let op = ArmOp::I64Add {
11806            rdlo: Reg::R0,
11807            rdhi: Reg::R1,
11808            rnlo: Reg::R0,
11809            rnhi: Reg::R1,
11810            rmlo: Reg::R2,
11811            rmhi: Reg::R3,
11812        };
11813        let code = encoder.encode(&op).unwrap();
11814        // Should emit ADDS (2 bytes) + ADC.W (4 bytes) = 6 bytes
11815        assert_eq!(code.len(), 6, "I64Add should be 6 bytes (ADDS + ADC.W)");
11816    }
11817
11818    #[test]
11819    fn test_encode_i64_sub_thumb2() {
11820        let encoder = ArmEncoder::new_thumb2();
11821        let op = ArmOp::I64Sub {
11822            rdlo: Reg::R0,
11823            rdhi: Reg::R1,
11824            rnlo: Reg::R0,
11825            rnhi: Reg::R1,
11826            rmlo: Reg::R2,
11827            rmhi: Reg::R3,
11828        };
11829        let code = encoder.encode(&op).unwrap();
11830        // Should emit SUBS (2 bytes) + SBC.W (4 bytes) = 6 bytes
11831        assert_eq!(code.len(), 6, "I64Sub should be 6 bytes (SUBS + SBC.W)");
11832    }
11833
11834    #[test]
11835    fn test_encode_i64_and_thumb2() {
11836        let encoder = ArmEncoder::new_thumb2();
11837        let op = ArmOp::I64And {
11838            rdlo: Reg::R0,
11839            rdhi: Reg::R1,
11840            rnlo: Reg::R0,
11841            rnhi: Reg::R1,
11842            rmlo: Reg::R2,
11843            rmhi: Reg::R3,
11844        };
11845        let code = encoder.encode(&op).unwrap();
11846        // AND.W (4 bytes) + AND.W (4 bytes) = 8 bytes
11847        assert!(code.len() >= 4, "I64And should emit at least 4 bytes");
11848    }
11849
11850    #[test]
11851    fn test_encode_i64_or_thumb2() {
11852        let encoder = ArmEncoder::new_thumb2();
11853        let op = ArmOp::I64Or {
11854            rdlo: Reg::R0,
11855            rdhi: Reg::R1,
11856            rnlo: Reg::R0,
11857            rnhi: Reg::R1,
11858            rmlo: Reg::R2,
11859            rmhi: Reg::R3,
11860        };
11861        let code = encoder.encode(&op).unwrap();
11862        assert!(code.len() >= 4, "I64Or should emit at least 4 bytes");
11863    }
11864
11865    #[test]
11866    fn test_encode_i64_xor_thumb2() {
11867        let encoder = ArmEncoder::new_thumb2();
11868        let op = ArmOp::I64Xor {
11869            rdlo: Reg::R0,
11870            rdhi: Reg::R1,
11871            rnlo: Reg::R0,
11872            rnhi: Reg::R1,
11873            rmlo: Reg::R2,
11874            rmhi: Reg::R3,
11875        };
11876        let code = encoder.encode(&op).unwrap();
11877        assert!(code.len() >= 4, "I64Xor should emit at least 4 bytes");
11878    }
11879
11880    #[test]
11881    fn test_encode_i64_const_small_thumb2() {
11882        let encoder = ArmEncoder::new_thumb2();
11883        // Small constant: only needs MOVW for each half
11884        let op = ArmOp::I64Const {
11885            rdlo: Reg::R0,
11886            rdhi: Reg::R1,
11887            value: 42,
11888        };
11889        let code = encoder.encode(&op).unwrap();
11890        // MOVW R0, #42 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes minimum
11891        assert!(code.len() >= 8, "I64Const should emit at least 8 bytes");
11892    }
11893
11894    #[test]
11895    fn test_encode_i64_const_large_thumb2() {
11896        let encoder = ArmEncoder::new_thumb2();
11897        // Large constant: needs MOVW+MOVT for each half
11898        let op = ArmOp::I64Const {
11899            rdlo: Reg::R0,
11900            rdhi: Reg::R1,
11901            value: 0x1234_5678_9ABC_DEF0_u64 as i64,
11902        };
11903        let code = encoder.encode(&op).unwrap();
11904        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
11905        assert_eq!(
11906            code.len(),
11907            16,
11908            "I64Const with large value should be 16 bytes"
11909        );
11910    }
11911
11912    #[test]
11913    fn test_encode_i64_extend_i32_s_thumb2() {
11914        let encoder = ArmEncoder::new_thumb2();
11915        let op = ArmOp::I64ExtendI32S {
11916            rdlo: Reg::R0,
11917            rdhi: Reg::R1,
11918            rn: Reg::R0,
11919        };
11920        let code = encoder.encode(&op).unwrap();
11921        // When rdlo == rn, only ASR (4 bytes) is emitted
11922        assert_eq!(
11923            code.len(),
11924            4,
11925            "I64ExtendI32S (same reg) should be 4 bytes (ASR only)"
11926        );
11927    }
11928
11929    #[test]
11930    fn test_encode_i64_extend_i32_s_diff_reg_thumb2() {
11931        let encoder = ArmEncoder::new_thumb2();
11932        let op = ArmOp::I64ExtendI32S {
11933            rdlo: Reg::R0,
11934            rdhi: Reg::R1,
11935            rn: Reg::R2,
11936        };
11937        let code = encoder.encode(&op).unwrap();
11938        // MOV rdlo, rn (2 bytes for low regs) + ASR rdhi, rdlo, #31 (4 bytes) = 6 bytes
11939        assert!(
11940            code.len() >= 6,
11941            "I64ExtendI32S (diff reg) should be at least 6 bytes"
11942        );
11943    }
11944
11945    #[test]
11946    fn test_encode_i64_extend_i32_u_thumb2() {
11947        let encoder = ArmEncoder::new_thumb2();
11948        let op = ArmOp::I64ExtendI32U {
11949            rdlo: Reg::R0,
11950            rdhi: Reg::R1,
11951            rn: Reg::R0,
11952        };
11953        let code = encoder.encode(&op).unwrap();
11954        // When rdlo == rn, only MOV rdhi, #0 (2 bytes) is emitted
11955        assert_eq!(
11956            code.len(),
11957            2,
11958            "I64ExtendI32U (same reg) should be 2 bytes (MOV #0 only)"
11959        );
11960    }
11961
11962    #[test]
11963    fn test_encode_i32_wrap_i64_nop_thumb2() {
11964        let encoder = ArmEncoder::new_thumb2();
11965        // When rd == rnlo, should be a NOP
11966        let op = ArmOp::I32WrapI64 {
11967            rd: Reg::R0,
11968            rnlo: Reg::R0,
11969        };
11970        let code = encoder.encode(&op).unwrap();
11971        assert_eq!(code.len(), 2, "I32WrapI64 same reg should be NOP (2 bytes)");
11972        assert_eq!(code, vec![0x00, 0xBF]); // NOP
11973    }
11974
11975    #[test]
11976    fn test_encode_i32_wrap_i64_diff_reg_thumb2() {
11977        let encoder = ArmEncoder::new_thumb2();
11978        let op = ArmOp::I32WrapI64 {
11979            rd: Reg::R2,
11980            rnlo: Reg::R0,
11981        };
11982        let code = encoder.encode(&op).unwrap();
11983        // MOV R2, R0 (2 or 4 bytes)
11984        assert!(
11985            code.len() >= 2,
11986            "I32WrapI64 diff reg should emit at least 2 bytes"
11987        );
11988    }
11989
11990    #[test]
11991    fn test_encode_i64_eqz_thumb2() {
11992        let encoder = ArmEncoder::new_thumb2();
11993        let op = ArmOp::I64Eqz {
11994            rd: Reg::R0,
11995            rnlo: Reg::R0,
11996            rnhi: Reg::R1,
11997        };
11998        let code = encoder.encode(&op).unwrap();
11999        // Delegates to I64SetCondZ which is already encoded
12000        assert!(
12001            code.len() >= 6,
12002            "I64Eqz should emit at least 6 bytes for ORR+ITE+MOV+MOV"
12003        );
12004    }
12005
12006    #[test]
12007    fn test_encode_i64_eq_thumb2() {
12008        let encoder = ArmEncoder::new_thumb2();
12009        let op = ArmOp::I64Eq {
12010            rd: Reg::R0,
12011            rnlo: Reg::R0,
12012            rnhi: Reg::R1,
12013            rmlo: Reg::R2,
12014            rmhi: Reg::R3,
12015        };
12016        let code = encoder.encode(&op).unwrap();
12017        // Delegates to I64SetCond EQ: CMP lo + IT EQ + CMPEQ hi + ITE EQ + MOV 1 + MOV 0
12018        assert!(code.len() >= 10, "I64Eq should emit at least 10 bytes");
12019    }
12020
12021    #[test]
12022    fn test_encode_i64_ldr_thumb2() {
12023        let encoder = ArmEncoder::new_thumb2();
12024        let op = ArmOp::I64Ldr {
12025            rdlo: Reg::R0,
12026            rdhi: Reg::R1,
12027            addr: MemAddr::imm(Reg::SP, 0),
12028        };
12029        let code = encoder.encode(&op).unwrap();
12030        // Two LDR instructions (lo at offset, hi at offset+4)
12031        assert!(code.len() >= 4, "I64Ldr should emit at least 4 bytes");
12032    }
12033
12034    #[test]
12035    fn test_372_i64_ldr_indexed_materializes_address() {
12036        // #372: a memory i64.load carries an index register (R11 + addr + off).
12037        // The encoder must materialize `ip = base + index` (ADD.W) and load via
12038        // `[ip,#off]` — NOT drop the index. A frame (non-indexed) i64.load must
12039        // stay byte-identical (plain `[base,#off]`, no ADD).
12040        let encoder = ArmEncoder::new_thumb2();
12041        let indexed = encoder
12042            .encode(&ArmOp::I64Ldr {
12043                rdlo: Reg::R0,
12044                rdhi: Reg::R1,
12045                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
12046            })
12047            .unwrap();
12048        // ADD.W ip, fp, r0 = eb0b 0c00 (byte-verified vs arm-none-eabi-as).
12049        assert_eq!(
12050            &indexed[0..4],
12051            &[0x0b, 0xeb, 0x00, 0x0c],
12052            "indexed I64Ldr must start with ADD.W ip, base, index"
12053        );
12054        let frame = encoder
12055            .encode(&ArmOp::I64Ldr {
12056                rdlo: Reg::R0,
12057                rdhi: Reg::R1,
12058                addr: MemAddr::imm(Reg::SP, 8),
12059            })
12060            .unwrap();
12061        // No index -> no ADD.W prefix (byte-identical frame access).
12062        assert_ne!(
12063            &frame[0..2],
12064            &[0x0b, 0xeb],
12065            "frame (non-indexed) I64Ldr must NOT emit an ADD.W"
12066        );
12067    }
12068
12069    #[test]
12070    fn test_382_i64_ldst_large_offset_materializes_not_skips() {
12071        // #382: an indexed i64.load/store whose static offset > 0xFFF must
12072        // MATERIALIZE the offset into the base — NOT return Err (skip the fn).
12073        // Sequence for reg_imm(R11, R0, 5000): MOVW ip,#5000 ; ADD ip,r0,ip ;
12074        // ADD ip,ip,fp ; LDR/STR halves at [ip,#0] / [ip,#4]. Byte-verified tail
12075        // vs arm-none-eabi-as.
12076        let encoder = ArmEncoder::new_thumb2();
12077        // 0x1388 > 0xFFF (MemAddr is not Copy, so build it per use).
12078
12079        let ld = encoder
12080            .encode(&ArmOp::I64Ldr {
12081                rdlo: Reg::R0,
12082                rdhi: Reg::R1,
12083                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
12084            })
12085            .expect("large-offset i64.load must lower, not skip");
12086        // MOVW ip,#0x1388 (4) + ADD ip,r0,ip (4) + ADD ip,ip,fp (4) + 2 LDR (8).
12087        assert_eq!(ld.len(), 20, "expected MOVW + 2×ADD + 2×LDR");
12088        // Must NOT be the small-offset `ADD.W ip, fp, r0` (0x0b 0xeb) prefix —
12089        // that path can only reach imm12 offsets.
12090        assert_ne!(
12091            &ld[0..2],
12092            &[0x0b, 0xeb],
12093            "must materialize the large offset"
12094        );
12095        // Effective base built in ip, then halves at [ip,#0] / [ip,#4].
12096        assert_eq!(
12097            &ld[4..20],
12098            &[
12099                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
12100                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
12101                0xdc, 0xf8, 0x00, 0x00, // LDR.W r0, [ip, #0]
12102                0xdc, 0xf8, 0x04, 0x10, // LDR.W r1, [ip, #4]
12103            ],
12104            "large-offset i64.load must fold offset into ip and access [ip,#0]/[ip,#4]"
12105        );
12106
12107        // Store: same base materialization, STR halves.
12108        let st = encoder
12109            .encode(&ArmOp::I64Str {
12110                rdlo: Reg::R2,
12111                rdhi: Reg::R3,
12112                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
12113            })
12114            .expect("large-offset i64.store must lower, not skip");
12115        assert_eq!(st.len(), 20);
12116        assert_eq!(
12117            &st[4..20],
12118            &[
12119                0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
12120                0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
12121                0xcc, 0xf8, 0x00, 0x20, // STR.W r2, [ip, #0]
12122                0xcc, 0xf8, 0x04, 0x30, // STR.W r3, [ip, #4]
12123            ],
12124            "large-offset i64.store must fold offset into ip and access [ip,#0]/[ip,#4]"
12125        );
12126
12127        // Small-offset (imm12) indexed access stays byte-identical (#372): the
12128        // effective base is a single `ADD.W ip, fp, r0` and the halves keep the
12129        // folded immediates — NO extra MOVW/ADD.
12130        let small = encoder
12131            .encode(&ArmOp::I64Ldr {
12132                rdlo: Reg::R0,
12133                rdhi: Reg::R1,
12134                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 8),
12135            })
12136            .unwrap();
12137        assert_eq!(
12138            &small[0..4],
12139            &[0x0b, 0xeb, 0x00, 0x0c],
12140            "small-offset indexed i64 must keep the single ADD.W ip, fp, r0"
12141        );
12142        assert_eq!(small.len(), 12, "ADD.W + 2×LDR.W (offset folded in imm12)");
12143    }
12144
12145    #[test]
12146    fn test_encode_i64_str_thumb2() {
12147        let encoder = ArmEncoder::new_thumb2();
12148        let op = ArmOp::I64Str {
12149            rdlo: Reg::R0,
12150            rdhi: Reg::R1,
12151            addr: MemAddr::imm(Reg::SP, 0),
12152        };
12153        let code = encoder.encode(&op).unwrap();
12154        // Two STR instructions (lo at offset, hi at offset+4)
12155        assert!(code.len() >= 4, "I64Str should emit at least 4 bytes");
12156    }
12157
12158    #[test]
12159    fn test_encode_i64_all_comparisons_thumb2() {
12160        let encoder = ArmEncoder::new_thumb2();
12161
12162        let ops = vec![
12163            ArmOp::I64Ne {
12164                rd: Reg::R0,
12165                rnlo: Reg::R0,
12166                rnhi: Reg::R1,
12167                rmlo: Reg::R2,
12168                rmhi: Reg::R3,
12169            },
12170            ArmOp::I64LtS {
12171                rd: Reg::R0,
12172                rnlo: Reg::R0,
12173                rnhi: Reg::R1,
12174                rmlo: Reg::R2,
12175                rmhi: Reg::R3,
12176            },
12177            ArmOp::I64LtU {
12178                rd: Reg::R0,
12179                rnlo: Reg::R0,
12180                rnhi: Reg::R1,
12181                rmlo: Reg::R2,
12182                rmhi: Reg::R3,
12183            },
12184            ArmOp::I64LeS {
12185                rd: Reg::R0,
12186                rnlo: Reg::R0,
12187                rnhi: Reg::R1,
12188                rmlo: Reg::R2,
12189                rmhi: Reg::R3,
12190            },
12191            ArmOp::I64LeU {
12192                rd: Reg::R0,
12193                rnlo: Reg::R0,
12194                rnhi: Reg::R1,
12195                rmlo: Reg::R2,
12196                rmhi: Reg::R3,
12197            },
12198            ArmOp::I64GtS {
12199                rd: Reg::R0,
12200                rnlo: Reg::R0,
12201                rnhi: Reg::R1,
12202                rmlo: Reg::R2,
12203                rmhi: Reg::R3,
12204            },
12205            ArmOp::I64GtU {
12206                rd: Reg::R0,
12207                rnlo: Reg::R0,
12208                rnhi: Reg::R1,
12209                rmlo: Reg::R2,
12210                rmhi: Reg::R3,
12211            },
12212            ArmOp::I64GeS {
12213                rd: Reg::R0,
12214                rnlo: Reg::R0,
12215                rnhi: Reg::R1,
12216                rmlo: Reg::R2,
12217                rmhi: Reg::R3,
12218            },
12219            ArmOp::I64GeU {
12220                rd: Reg::R0,
12221                rnlo: Reg::R0,
12222                rnhi: Reg::R1,
12223                rmlo: Reg::R2,
12224                rmhi: Reg::R3,
12225            },
12226        ];
12227
12228        for op in &ops {
12229            let code = encoder.encode(op).unwrap();
12230            assert!(
12231                code.len() >= 8,
12232                "i64 comparison {:?} should emit at least 8 bytes, got {}",
12233                op,
12234                code.len()
12235            );
12236        }
12237    }
12238
12239    #[test]
12240    fn test_encode_i64_const_zero_thumb2() {
12241        let encoder = ArmEncoder::new_thumb2();
12242        let op = ArmOp::I64Const {
12243            rdlo: Reg::R0,
12244            rdhi: Reg::R1,
12245            value: 0,
12246        };
12247        let code = encoder.encode(&op).unwrap();
12248        // MOVW R0, #0 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes
12249        assert_eq!(code.len(), 8, "I64Const(0) should be 8 bytes");
12250    }
12251
12252    #[test]
12253    fn test_encode_i64_const_negative_one_thumb2() {
12254        let encoder = ArmEncoder::new_thumb2();
12255        let op = ArmOp::I64Const {
12256            rdlo: Reg::R0,
12257            rdhi: Reg::R1,
12258            value: -1, // 0xFFFF_FFFF_FFFF_FFFF
12259        };
12260        let code = encoder.encode(&op).unwrap();
12261        // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
12262        assert_eq!(code.len(), 16, "I64Const(-1) should be 16 bytes");
12263    }
12264
12265    // =========================================================================
12266    // Sub-word load/store encoding tests
12267    // =========================================================================
12268
12269    #[test]
12270    fn test_encode_ldrb_arm32() {
12271        let encoder = ArmEncoder::new_arm32();
12272        let op = ArmOp::Ldrb {
12273            rd: Reg::R0,
12274            addr: MemAddr::imm(Reg::R1, 4),
12275        };
12276        let code = encoder.encode(&op).unwrap();
12277        assert_eq!(code.len(), 4, "ARM32 LDRB should be 4 bytes");
12278        // LDRB R0, [R1, #4] = 0xE5D10004
12279        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12280        assert_eq!(encoded, 0xE5D10004, "Should encode LDRB R0, [R1, #4]");
12281    }
12282
12283    #[test]
12284    fn test_encode_strb_arm32() {
12285        let encoder = ArmEncoder::new_arm32();
12286        let op = ArmOp::Strb {
12287            rd: Reg::R0,
12288            addr: MemAddr::imm(Reg::R1, 0),
12289        };
12290        let code = encoder.encode(&op).unwrap();
12291        assert_eq!(code.len(), 4, "ARM32 STRB should be 4 bytes");
12292        // STRB R0, [R1, #0] = 0xE5C10000
12293        let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12294        assert_eq!(encoded, 0xE5C10000, "Should encode STRB R0, [R1, #0]");
12295    }
12296
12297    #[test]
12298    fn test_encode_ldrh_arm32() {
12299        let encoder = ArmEncoder::new_arm32();
12300        let op = ArmOp::Ldrh {
12301            rd: Reg::R0,
12302            addr: MemAddr::imm(Reg::R1, 2),
12303        };
12304        let code = encoder.encode(&op).unwrap();
12305        assert_eq!(code.len(), 4, "ARM32 LDRH should be 4 bytes");
12306    }
12307
12308    #[test]
12309    fn test_encode_strh_arm32() {
12310        let encoder = ArmEncoder::new_arm32();
12311        let op = ArmOp::Strh {
12312            rd: Reg::R0,
12313            addr: MemAddr::imm(Reg::R1, 0),
12314        };
12315        let code = encoder.encode(&op).unwrap();
12316        assert_eq!(code.len(), 4, "ARM32 STRH should be 4 bytes");
12317    }
12318
12319    #[test]
12320    fn test_encode_ldrsb_arm32() {
12321        let encoder = ArmEncoder::new_arm32();
12322        let op = ArmOp::Ldrsb {
12323            rd: Reg::R0,
12324            addr: MemAddr::imm(Reg::R1, 0),
12325        };
12326        let code = encoder.encode(&op).unwrap();
12327        assert_eq!(code.len(), 4, "ARM32 LDRSB should be 4 bytes");
12328    }
12329
12330    #[test]
12331    fn test_encode_ldrsh_arm32() {
12332        let encoder = ArmEncoder::new_arm32();
12333        let op = ArmOp::Ldrsh {
12334            rd: Reg::R0,
12335            addr: MemAddr::imm(Reg::R1, 0),
12336        };
12337        let code = encoder.encode(&op).unwrap();
12338        assert_eq!(code.len(), 4, "ARM32 LDRSH should be 4 bytes");
12339    }
12340
12341    #[test]
12342    fn test_encode_ldrb_thumb2_16bit() {
12343        let encoder = ArmEncoder::new_thumb2();
12344        let op = ArmOp::Ldrb {
12345            rd: Reg::R0,
12346            addr: MemAddr::imm(Reg::R1, 4),
12347        };
12348        let code = encoder.encode(&op).unwrap();
12349        // Low registers + small offset -> 16-bit encoding
12350        assert_eq!(
12351            code.len(),
12352            2,
12353            "Thumb-2 LDRB with small offset should be 16-bit"
12354        );
12355    }
12356
12357    #[test]
12358    fn test_encode_ldrb_thumb2_32bit() {
12359        let encoder = ArmEncoder::new_thumb2();
12360        let op = ArmOp::Ldrb {
12361            rd: Reg::R0,
12362            addr: MemAddr::imm(Reg::R1, 100), // offset > 31 needs 32-bit
12363        };
12364        let code = encoder.encode(&op).unwrap();
12365        assert_eq!(
12366            code.len(),
12367            4,
12368            "Thumb-2 LDRB with large offset should be 32-bit"
12369        );
12370    }
12371
12372    #[test]
12373    fn test_encode_strb_thumb2_16bit() {
12374        let encoder = ArmEncoder::new_thumb2();
12375        let op = ArmOp::Strb {
12376            rd: Reg::R0,
12377            addr: MemAddr::imm(Reg::R1, 10),
12378        };
12379        let code = encoder.encode(&op).unwrap();
12380        assert_eq!(
12381            code.len(),
12382            2,
12383            "Thumb-2 STRB with small offset should be 16-bit"
12384        );
12385    }
12386
12387    #[test]
12388    fn test_encode_ldrh_thumb2_16bit() {
12389        let encoder = ArmEncoder::new_thumb2();
12390        let op = ArmOp::Ldrh {
12391            rd: Reg::R0,
12392            addr: MemAddr::imm(Reg::R1, 4), // offset aligned to 2, <= 62
12393        };
12394        let code = encoder.encode(&op).unwrap();
12395        assert_eq!(
12396            code.len(),
12397            2,
12398            "Thumb-2 LDRH with small aligned offset should be 16-bit"
12399        );
12400    }
12401
12402    #[test]
12403    fn test_encode_strh_thumb2_16bit() {
12404        let encoder = ArmEncoder::new_thumb2();
12405        let op = ArmOp::Strh {
12406            rd: Reg::R0,
12407            addr: MemAddr::imm(Reg::R1, 4),
12408        };
12409        let code = encoder.encode(&op).unwrap();
12410        assert_eq!(
12411            code.len(),
12412            2,
12413            "Thumb-2 STRH with small aligned offset should be 16-bit"
12414        );
12415    }
12416
12417    #[test]
12418    fn test_encode_ldrsb_thumb2() {
12419        let encoder = ArmEncoder::new_thumb2();
12420        let op = ArmOp::Ldrsb {
12421            rd: Reg::R0,
12422            addr: MemAddr::imm(Reg::R1, 0),
12423        };
12424        let code = encoder.encode(&op).unwrap();
12425        // LDRSB has no 16-bit immediate form, always 32-bit
12426        assert_eq!(code.len(), 4, "Thumb-2 LDRSB should be 32-bit");
12427    }
12428
12429    #[test]
12430    fn test_encode_ldrsh_thumb2() {
12431        let encoder = ArmEncoder::new_thumb2();
12432        let op = ArmOp::Ldrsh {
12433            rd: Reg::R0,
12434            addr: MemAddr::imm(Reg::R1, 0),
12435        };
12436        let code = encoder.encode(&op).unwrap();
12437        assert_eq!(code.len(), 4, "Thumb-2 LDRSH should be 32-bit");
12438    }
12439
12440    #[test]
12441    fn test_encode_memory_size_thumb2() {
12442        let encoder = ArmEncoder::new_thumb2();
12443        let op = ArmOp::MemorySize { rd: Reg::R0 };
12444        let code = encoder.encode(&op).unwrap();
12445        // R0 and R10 are not both low registers, so this needs careful handling
12446        assert!(!code.is_empty(), "MemorySize should produce code");
12447    }
12448
12449    #[test]
12450    fn test_encode_memory_grow_thumb2() {
12451        let encoder = ArmEncoder::new_thumb2();
12452        let op = ArmOp::MemoryGrow {
12453            rd: Reg::R0,
12454            rn: Reg::R0,
12455        };
12456        let code = encoder.encode(&op).unwrap();
12457        assert_eq!(code.len(), 4, "MemoryGrow (MVN) should be 32-bit Thumb-2");
12458    }
12459
12460    #[test]
12461    fn test_encode_subword_reg_offset_thumb2() {
12462        let encoder = ArmEncoder::new_thumb2();
12463
12464        // LDRB with register offset
12465        let op = ArmOp::Ldrb {
12466            rd: Reg::R0,
12467            addr: MemAddr::reg(Reg::R1, Reg::R2),
12468        };
12469        let code = encoder.encode(&op).unwrap();
12470        assert_eq!(
12471            code.len(),
12472            4,
12473            "Thumb-2 LDRB with reg offset should be 32-bit"
12474        );
12475
12476        // STRB with register offset
12477        let op = ArmOp::Strb {
12478            rd: Reg::R0,
12479            addr: MemAddr::reg(Reg::R1, Reg::R2),
12480        };
12481        let code = encoder.encode(&op).unwrap();
12482        assert_eq!(
12483            code.len(),
12484            4,
12485            "Thumb-2 STRB with reg offset should be 32-bit"
12486        );
12487
12488        // LDRH with register offset
12489        let op = ArmOp::Ldrh {
12490            rd: Reg::R0,
12491            addr: MemAddr::reg(Reg::R1, Reg::R2),
12492        };
12493        let code = encoder.encode(&op).unwrap();
12494        assert_eq!(
12495            code.len(),
12496            4,
12497            "Thumb-2 LDRH with reg offset should be 32-bit"
12498        );
12499
12500        // STRH with register offset
12501        let op = ArmOp::Strh {
12502            rd: Reg::R0,
12503            addr: MemAddr::reg(Reg::R1, Reg::R2),
12504        };
12505        let code = encoder.encode(&op).unwrap();
12506        assert_eq!(
12507            code.len(),
12508            4,
12509            "Thumb-2 STRH with reg offset should be 32-bit"
12510        );
12511    }
12512
12513    #[test]
12514    fn test_encode_subword_reg_imm_offset_thumb2() {
12515        let encoder = ArmEncoder::new_thumb2();
12516
12517        // LDRB with both register and immediate offset
12518        let op = ArmOp::Ldrb {
12519            rd: Reg::R0,
12520            addr: MemAddr::reg_imm(Reg::R1, Reg::R2, 4),
12521        };
12522        let code = encoder.encode(&op).unwrap();
12523        // ADD R12, R2, #4 (4 bytes) + LDRB R0, [R1, R12] (4 bytes) = 8 bytes
12524        assert_eq!(
12525            code.len(),
12526            8,
12527            "Thumb-2 LDRB with reg+imm offset should be 8 bytes"
12528        );
12529    }
12530
12531    // ========================================================================
12532    // Helium MVE encoding tests
12533    // ========================================================================
12534
12535    #[test]
12536    fn test_encode_mve_addi32_thumb2() {
12537        let encoder = ArmEncoder::new_thumb2();
12538        let op = ArmOp::MveAddI {
12539            qd: QReg::Q0,
12540            qn: QReg::Q1,
12541            qm: QReg::Q2,
12542            size: MveSize::S32,
12543        };
12544        let code = encoder.encode(&op).unwrap();
12545        assert_eq!(
12546            code.len(),
12547            4,
12548            "MVE VADD.I32 should be 4 bytes (Thumb-2 32-bit)"
12549        );
12550    }
12551
12552    #[test]
12553    fn test_encode_mve_subi16_thumb2() {
12554        let encoder = ArmEncoder::new_thumb2();
12555        let op = ArmOp::MveSubI {
12556            qd: QReg::Q0,
12557            qn: QReg::Q1,
12558            qm: QReg::Q2,
12559            size: MveSize::S16,
12560        };
12561        let code = encoder.encode(&op).unwrap();
12562        assert_eq!(code.len(), 4, "MVE VSUB.I16 should be 4 bytes");
12563    }
12564
12565    #[test]
12566    fn test_encode_mve_muli8_thumb2() {
12567        let encoder = ArmEncoder::new_thumb2();
12568        let op = ArmOp::MveMulI {
12569            qd: QReg::Q0,
12570            qn: QReg::Q1,
12571            qm: QReg::Q2,
12572            size: MveSize::S8,
12573        };
12574        let code = encoder.encode(&op).unwrap();
12575        assert_eq!(code.len(), 4, "MVE VMUL.I8 should be 4 bytes");
12576    }
12577
12578    #[test]
12579    fn test_encode_mve_bitwise_thumb2() {
12580        let encoder = ArmEncoder::new_thumb2();
12581
12582        let ops = vec![
12583            ArmOp::MveAnd {
12584                qd: QReg::Q0,
12585                qn: QReg::Q1,
12586                qm: QReg::Q2,
12587            },
12588            ArmOp::MveOrr {
12589                qd: QReg::Q0,
12590                qn: QReg::Q1,
12591                qm: QReg::Q2,
12592            },
12593            ArmOp::MveEor {
12594                qd: QReg::Q0,
12595                qn: QReg::Q1,
12596                qm: QReg::Q2,
12597            },
12598            ArmOp::MveBic {
12599                qd: QReg::Q0,
12600                qn: QReg::Q1,
12601                qm: QReg::Q2,
12602            },
12603        ];
12604        for op in ops {
12605            let code = encoder.encode(&op).unwrap();
12606            assert_eq!(code.len(), 4, "MVE bitwise op should be 4 bytes");
12607        }
12608    }
12609
12610    #[test]
12611    fn test_encode_mve_mvn_thumb2() {
12612        let encoder = ArmEncoder::new_thumb2();
12613        let op = ArmOp::MveMvn {
12614            qd: QReg::Q0,
12615            qm: QReg::Q1,
12616        };
12617        let code = encoder.encode(&op).unwrap();
12618        assert_eq!(code.len(), 4, "MVE VMVN should be 4 bytes");
12619    }
12620
12621    #[test]
12622    fn test_encode_mve_load_store_thumb2() {
12623        let encoder = ArmEncoder::new_thumb2();
12624
12625        let load = ArmOp::MveLoad {
12626            qd: QReg::Q0,
12627            addr: MemAddr::imm(Reg::R0, 16),
12628        };
12629        let code = encoder.encode(&load).unwrap();
12630        assert_eq!(code.len(), 4, "MVE VLDRW.32 should be 4 bytes");
12631
12632        let store = ArmOp::MveStore {
12633            qd: QReg::Q1,
12634            addr: MemAddr::imm(Reg::R1, 0),
12635        };
12636        let code = encoder.encode(&store).unwrap();
12637        assert_eq!(code.len(), 4, "MVE VSTRW.32 should be 4 bytes");
12638    }
12639
12640    #[test]
12641    fn test_encode_mve_const_thumb2() {
12642        let encoder = ArmEncoder::new_thumb2();
12643        let op = ArmOp::MveConst {
12644            qd: QReg::Q0,
12645            bytes: [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0],
12646        };
12647        let code = encoder.encode(&op).unwrap();
12648        // Should be 4 words of (MOVW R12 + VMOV Sn) = 4 * (4+4) = 32 bytes min
12649        // Some words with hi16=0 skip MOVT, so length varies
12650        assert!(
12651            code.len() >= 24,
12652            "MVE const should produce multiple instructions"
12653        );
12654    }
12655
12656    #[test]
12657    fn test_encode_mve_dup_thumb2() {
12658        let encoder = ArmEncoder::new_thumb2();
12659        let op = ArmOp::MveDup {
12660            qd: QReg::Q0,
12661            rn: Reg::R0,
12662            size: MveSize::S32,
12663        };
12664        let code = encoder.encode(&op).unwrap();
12665        assert_eq!(code.len(), 4, "MVE VDUP.32 should be 4 bytes");
12666    }
12667
12668    #[test]
12669    fn test_encode_mve_extract_lane_thumb2() {
12670        let encoder = ArmEncoder::new_thumb2();
12671        let op = ArmOp::MveExtractLane {
12672            rd: Reg::R0,
12673            qn: QReg::Q1,
12674            lane: 2,
12675            size: MveSize::S32,
12676        };
12677        let code = encoder.encode(&op).unwrap();
12678        assert_eq!(code.len(), 4, "MVE extract lane should be 4 bytes");
12679    }
12680
12681    #[test]
12682    fn test_encode_mve_insert_lane_thumb2() {
12683        let encoder = ArmEncoder::new_thumb2();
12684        let op = ArmOp::MveInsertLane {
12685            qd: QReg::Q0,
12686            rn: Reg::R1,
12687            lane: 3,
12688            size: MveSize::S32,
12689        };
12690        let code = encoder.encode(&op).unwrap();
12691        assert_eq!(code.len(), 4, "MVE insert lane should be 4 bytes");
12692    }
12693
12694    #[test]
12695    fn test_encode_mve_addf32_thumb2() {
12696        let encoder = ArmEncoder::new_thumb2();
12697        let op = ArmOp::MveAddF32 {
12698            qd: QReg::Q0,
12699            qn: QReg::Q1,
12700            qm: QReg::Q2,
12701        };
12702        let code = encoder.encode(&op).unwrap();
12703        assert_eq!(code.len(), 4, "MVE VADD.F32 should be 4 bytes");
12704    }
12705
12706    #[test]
12707    fn test_encode_mve_divf32_thumb2() {
12708        let encoder = ArmEncoder::new_thumb2();
12709        let op = ArmOp::MveDivF32 {
12710            qd: QReg::Q0,
12711            qn: QReg::Q1,
12712            qm: QReg::Q2,
12713        };
12714        let code = encoder.encode(&op).unwrap();
12715        // Lane-wise: 4 x VDIV.F32 = 4 x 4 = 16 bytes
12716        assert_eq!(
12717            code.len(),
12718            16,
12719            "MVE VDIV.F32 (lane-wise) should be 16 bytes"
12720        );
12721    }
12722
12723    #[test]
12724    fn test_encode_mve_sqrtf32_thumb2() {
12725        let encoder = ArmEncoder::new_thumb2();
12726        let op = ArmOp::MveSqrtF32 {
12727            qd: QReg::Q0,
12728            qm: QReg::Q1,
12729        };
12730        let code = encoder.encode(&op).unwrap();
12731        // Lane-wise: 4 x VSQRT.F32 = 4 x 4 = 16 bytes
12732        assert_eq!(
12733            code.len(),
12734            16,
12735            "MVE VSQRT.F32 (lane-wise) should be 16 bytes"
12736        );
12737    }
12738
12739    #[test]
12740    fn test_encode_mve_negf32_thumb2() {
12741        let encoder = ArmEncoder::new_thumb2();
12742        let op = ArmOp::MveNegF32 {
12743            qd: QReg::Q0,
12744            qm: QReg::Q1,
12745        };
12746        let code = encoder.encode(&op).unwrap();
12747        assert_eq!(code.len(), 4, "MVE VNEG.F32 should be 4 bytes");
12748    }
12749
12750    #[test]
12751    fn test_encode_mve_absf32_thumb2() {
12752        let encoder = ArmEncoder::new_thumb2();
12753        let op = ArmOp::MveAbsF32 {
12754            qd: QReg::Q0,
12755            qm: QReg::Q1,
12756        };
12757        let code = encoder.encode(&op).unwrap();
12758        assert_eq!(code.len(), 4, "MVE VABS.F32 should be 4 bytes");
12759    }
12760
12761    /// VCR-RA-001 / immediate-folding precondition: pins the Thumb-2 `AND`
12762    /// immediate encoding for the byte range and documents its bound.
12763    ///
12764    /// The `And { Operand2::Imm }` encoder packs the low 12 bits straight into
12765    /// the `i:imm3:imm8` field WITHOUT applying ThumbExpandImm (the modified-
12766    /// immediate expansion). For `imm <= 0xFF` (e.g. gale's int8 clamps
12767    /// `#0x7e` / `#0x7f`) that is correct — `i:imm3 = 0000` means "imm8
12768    /// zero-extended". So `and r2, r0, #0x7e` encodes to the canonical
12769    /// `00 f0 7e 02`. For `imm >= 0x100` the field would need a true
12770    /// ThumbExpandImm pattern (rotation / replication), which is NOT
12771    /// implemented here — so **immediate folding must gate on `imm <= 0xFF`**
12772    /// until the encoder is hardened to ThumbExpandImm/Ok-or-Err (the
12773    /// "encoder must be Ok-or-Err, never silently wrong" principle, #180/#185).
12774    /// This bound covers the measured `flat_flight` waste (#209).
12775    #[test]
12776    fn and_immediate_encodes_correctly_in_byte_range_documents_fold_bound() {
12777        let encoder = ArmEncoder::new_thumb2();
12778        let op = ArmOp::And {
12779            rd: Reg::R2,
12780            rn: Reg::R0,
12781            op2: Operand2::Imm(0x7e),
12782        };
12783        let code = encoder.encode(&op).unwrap();
12784        assert_eq!(
12785            code,
12786            vec![0x00, 0xf0, 0x7e, 0x02],
12787            "and r2, r0, #0x7e must encode to the canonical AND.W T1 (imm8=0x7e)"
12788        );
12789    }
12790
12791    /// #255: the shared ThumbExpandImm reverse-encoder underpinning the
12792    /// data-processing immediate fix. Encodable modified immediates round-trip to
12793    /// the expected `i:imm3:imm8` field; a genuinely non-modified value is `None`
12794    /// (caller must materialize into a register). Note `1000 = 0xFA ror 30` *is*
12795    /// representable (field 0xF7A) — the old encoder mis-encoded it (raw 0x3E8);
12796    /// this encodes it correctly.
12797    #[test]
12798    fn try_thumb_expand_imm_encodes_modified_immediates() {
12799        assert_eq!(try_thumb_expand_imm(0x7e), Some(0x07e)); // zero-extended byte
12800        assert_eq!(try_thumb_expand_imm(0xff), Some(0x0ff));
12801        assert_eq!(try_thumb_expand_imm(0x0001_0001), Some(0x101)); // 0x00XY00XY
12802        assert_eq!(try_thumb_expand_imm(0xff00_ff00), Some(0x2ff)); // 0xXY00XY00
12803        assert_eq!(try_thumb_expand_imm(0xffff_ffff), Some(0x3ff)); // 0xXYXYXYXY
12804        assert_eq!(try_thumb_expand_imm(0x100), Some(0xf80)); // 0x80 ror 31
12805        assert_eq!(try_thumb_expand_imm(0x8000_0000), Some(0x400)); // 0x80 ror 8
12806        assert_eq!(try_thumb_expand_imm(1000), Some(0xf7a)); // 0xFA ror 30
12807        // Genuinely unrepresentable (bits too far apart for an 8-bit window).
12808        assert_eq!(try_thumb_expand_imm(0x101), None);
12809        assert_eq!(try_thumb_expand_imm(0x12345), None);
12810    }
12811
12812    /// #255: CMP/ADDS/SUBS encode any valid modified immediate correctly, and
12813    /// ERROR (not silently mis-encode) on a genuinely unrepresentable one,
12814    /// forcing the selector to materialize into a register — closing the
12815    /// silent-miscompile class of #251/#253.
12816    #[test]
12817    fn cmp_adds_subs_immediate_error_on_non_modified_imm() {
12818        let encoder = ArmEncoder::new_thumb2();
12819        // cmp r0, #0xff → valid → Ok; cmp r0, #1000 → valid (0xFA ror 30) → Ok.
12820        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 0xff).is_ok());
12821        assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 1000).is_ok());
12822        // cmp r0, #0x101 → NOT a modified immediate → Err (materialize-reg).
12823        assert!(
12824            encoder.encode_thumb32_cmp_imm(&Reg::R0, 0x101).is_err(),
12825            "cmp #0x101 must error, not compare the wrong constant"
12826        );
12827        assert!(
12828            encoder
12829                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x101)
12830                .is_err()
12831        );
12832        assert!(
12833            encoder
12834                .encode_thumb32_subs(&Reg::R0, &Reg::R0, 0x101)
12835                .is_err()
12836        );
12837        // ...but a valid modified immediate still encodes.
12838        assert!(
12839            encoder
12840                .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x80)
12841                .is_ok()
12842        );
12843    }
12844
12845    /// #257: MLA (multiply-accumulate) encodes as MLS without the bit-4 op flag.
12846    /// `mla r2, r3, r4, r8` (rd=r2, rn=r3, rm=r4, ra=r8) → Thumb-2 `03 fb 04 82`.
12847    #[test]
12848    fn mla_thumb2_encodes_correctly() {
12849        let encoder = ArmEncoder::new_thumb2();
12850        let code = encoder
12851            .encode(&ArmOp::Mla {
12852                rd: Reg::R2,
12853                rn: Reg::R3,
12854                rm: Reg::R4,
12855                ra: Reg::R8,
12856            })
12857            .unwrap();
12858        // hw1 = 0xFB03, hw2 = (8<<12)|(2<<8)|4 = 0x8204
12859        assert_eq!(code, vec![0x03, 0xfb, 0x04, 0x82]);
12860    }
12861
12862    /// #259: LDR/STR (and sub-word) immediate-offset encoders truncated
12863    /// `offset & 0xFFF`, silently targeting the wrong address for offset >= 4096.
12864    /// They now error (the selector must use register-offset addressing) — the
12865    /// load/store sibling of the #253/#255 class. Offsets <= 4095 still encode.
12866    #[test]
12867    fn ldst_imm12_offset_errors_when_out_of_range() {
12868        let encoder = ArmEncoder::new_thumb2();
12869        // offset 0xFFF (4095): valid → Ok; ldr r0, [r1, #4095].
12870        assert!(
12871            encoder
12872                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0xFFF)
12873                .is_ok()
12874        );
12875        // offset 0x1000 (4096): out of imm12 range → Err (not & 0xFFF → #0).
12876        assert!(
12877            encoder
12878                .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0x1000)
12879                .is_err(),
12880            "ldr offset 4096 must error, not wrap to 0"
12881        );
12882        assert!(
12883            encoder
12884                .encode_thumb32_str(&Reg::R0, &Reg::R1, 0x1000)
12885                .is_err()
12886        );
12887        assert!(
12888            encoder
12889                .encode_thumb32_ldrb_imm(&Reg::R0, &Reg::R1, 5000)
12890                .is_err()
12891        );
12892        assert!(
12893            encoder
12894                .encode_thumb32_strh_imm(&Reg::R0, &Reg::R1, 5000)
12895                .is_err()
12896        );
12897    }
12898
12899    /// Latent miscompile fix: ADD/SUB with a >0xFF immediate (e.g.
12900    /// `add sp, sp, #frame` for a >=256-byte frame) used ADD.W (T3), whose
12901    /// `i:imm3:imm8` is a ThumbExpandImm modified immediate — so `#256` silently
12902    /// encoded as `#0` (stack corruption). Use ADDW/SUBW (T4), a PLAIN 12-bit
12903    /// immediate, for 0x100..=0xFFF; keep T3 for <=0xFF (bit-identical); error
12904    /// beyond 4095.
12905    #[test]
12906    fn add_sub_large_immediate_use_addw_subw_not_misencoded() {
12907        let encoder = ArmEncoder::new_thumb2();
12908        // add sp, sp, #256  →  ADDW (T4) SP, SP, #256  =  0d f2 00 1d
12909        assert_eq!(
12910            encoder
12911                .encode(&ArmOp::Add {
12912                    rd: Reg::SP,
12913                    rn: Reg::SP,
12914                    op2: Operand2::Imm(256),
12915                })
12916                .unwrap(),
12917            vec![0x0d, 0xf2, 0x00, 0x1d],
12918            "add sp,sp,#256 must be ADDW (plain imm12), not a mis-encoded ADD.W"
12919        );
12920        // sub sp, sp, #256  →  SUBW (T4) SP, SP, #256  =  ad f2 00 1d
12921        assert_eq!(
12922            encoder
12923                .encode(&ArmOp::Sub {
12924                    rd: Reg::SP,
12925                    rn: Reg::SP,
12926                    op2: Operand2::Imm(256),
12927                })
12928                .unwrap(),
12929            vec![0xad, 0xf2, 0x00, 0x1d],
12930        );
12931        // > 4095 has no single-instruction encoding → error, not silent wrong.
12932        assert!(
12933            encoder
12934                .encode(&ArmOp::Add {
12935                    rd: Reg::SP,
12936                    rn: Reg::SP,
12937                    op2: Operand2::Imm(5000),
12938                })
12939                .is_err(),
12940            "add #5000 must error (no single ADDW), not mis-encode"
12941        );
12942    }
12943
12944    /// Closes the data-proc immediate class: AND and CMN now go through
12945    /// `try_thumb_expand_imm` like ORR/EOR/CMP — correct for any modified
12946    /// immediate, `Err` (not raw-pack / NOP) on an un-encodable one. The byte
12947    /// range stays bit-identical (`and r2,r0,#0x7e` is unchanged).
12948    #[test]
12949    fn and_cmn_immediate_thumb_expand_else_error() {
12950        let encoder = ArmEncoder::new_thumb2();
12951        // byte range unchanged (bit-identical with the pre-retrofit encoding)
12952        assert_eq!(
12953            encoder
12954                .encode(&ArmOp::And {
12955                    rd: Reg::R2,
12956                    rn: Reg::R0,
12957                    op2: Operand2::Imm(0x7e),
12958                })
12959                .unwrap(),
12960            vec![0x00, 0xf0, 0x7e, 0x02],
12961        );
12962        // a valid replicated modified immediate now encodes (was silently wrong)
12963        assert!(
12964            encoder
12965                .encode(&ArmOp::And {
12966                    rd: Reg::R2,
12967                    rn: Reg::R0,
12968                    op2: Operand2::Imm(0xff00ff00u32 as i32),
12969                })
12970                .is_ok()
12971        );
12972        // a genuinely un-encodable immediate errors (AND was raw-pack; CMN NOP)
12973        assert!(
12974            encoder
12975                .encode(&ArmOp::And {
12976                    rd: Reg::R2,
12977                    rn: Reg::R0,
12978                    op2: Operand2::Imm(0x101),
12979                })
12980                .is_err()
12981        );
12982        assert!(
12983            encoder
12984                .encode(&ArmOp::Cmn {
12985                    rn: Reg::R0,
12986                    op2: Operand2::Imm(0x101),
12987                })
12988                .is_err(),
12989            "CMN #0x101 must error, not emit a NOP"
12990        );
12991    }
12992
12993    /// VCR-RA-001: ORR/EOR with a small immediate must encode the real
12994    /// instruction (not a silent `0xBF00` NOP). Pins the byte range and the
12995    /// Ok-or-Err bound that makes future Or/Eor immediate folding safe.
12996    #[test]
12997    fn orr_eor_immediate_encode_in_byte_range_else_error() {
12998        let encoder = ArmEncoder::new_thumb2();
12999        // orr r2, r0, #0x7e  →  ORR.W T1, imm8=0x7e
13000        assert_eq!(
13001            encoder
13002                .encode(&ArmOp::Orr {
13003                    rd: Reg::R2,
13004                    rn: Reg::R0,
13005                    op2: Operand2::Imm(0x7e),
13006                })
13007                .unwrap(),
13008            vec![0x40, 0xf0, 0x7e, 0x02],
13009        );
13010        // eor r2, r0, #0x7e  →  EOR.W T1, imm8=0x7e
13011        assert_eq!(
13012            encoder
13013                .encode(&ArmOp::Eor {
13014                    rd: Reg::R2,
13015                    rn: Reg::R0,
13016                    op2: Operand2::Imm(0x7e),
13017                })
13018                .unwrap(),
13019            vec![0x80, 0xf0, 0x7e, 0x02],
13020        );
13021        // Out-of-range immediates error rather than silently mis-encode / NOP.
13022        assert!(
13023            encoder
13024                .encode(&ArmOp::Orr {
13025                    rd: Reg::R2,
13026                    rn: Reg::R0,
13027                    op2: Operand2::Imm(0x140),
13028                })
13029                .is_err(),
13030            "ORR #0x140 must error, not emit a NOP"
13031        );
13032    }
13033
13034    #[test]
13035    fn test_encode_mve_different_qregs() {
13036        let encoder = ArmEncoder::new_thumb2();
13037
13038        // Test that different Q-register numbers produce different encodings
13039        let op1 = ArmOp::MveAddI {
13040            qd: QReg::Q0,
13041            qn: QReg::Q0,
13042            qm: QReg::Q0,
13043            size: MveSize::S32,
13044        };
13045        let op2 = ArmOp::MveAddI {
13046            qd: QReg::Q3,
13047            qn: QReg::Q5,
13048            qm: QReg::Q7,
13049            size: MveSize::S32,
13050        };
13051        let code1 = encoder.encode(&op1).unwrap();
13052        let code2 = encoder.encode(&op2).unwrap();
13053        assert_ne!(
13054            code1, code2,
13055            "Different Q-registers should produce different encodings"
13056        );
13057    }
13058
13059    #[test]
13060    fn test_encode_mve_arm32_loud_err() {
13061        // #615: MVE (Helium) is Thumb-2-only. The ARM32 encoder used to emit
13062        // a silent NOP here (dropping the vector op); it must now be a typed
13063        // Err so a broken "MVE implies Thumb" invariant fails loudly.
13064        let encoder = ArmEncoder::new_arm32();
13065        let op = ArmOp::MveAddI {
13066            qd: QReg::Q0,
13067            qn: QReg::Q1,
13068            qm: QReg::Q2,
13069            size: MveSize::S32,
13070        };
13071        let err = encoder
13072            .encode(&op)
13073            .expect_err("ARM32 MVE must be a loud Err, not a silent NOP (#615)");
13074        assert!(
13075            err.to_string().contains("Thumb-2 only"),
13076            "unexpected error message: {err}"
13077        );
13078    }
13079}