Skip to main content

logicaffeine_forge/
x64asm.rs

1//! A minimal x86-64 instruction encoder for the contiguous register-allocated
2//! region backend (`compile_region_regalloc`).
3//!
4//! This is deliberately tiny: it covers exactly the instruction shapes the
5//! linear-scan int backend emits — register/immediate moves, the three-operand
6//! arithmetic the allocator lowers (via a two-address `dst = lhs; dst OP rhs`
7//! discipline), compares + `setcc`, conditional and unconditional jumps with
8//! late-bound labels, frame loads/stores (`base + disp`), prologue/epilogue
9//! register save/restore, and `ret`. It is NOT a general assembler.
10//!
11//! Register numbering follows the hardware encoding (`rax=0 … r15=15`); the REX
12//! prefix and ModRM/SIB bytes are emitted directly. All emitted code is
13//! position-independent except the `jcc`/`jmp` displacements, which are resolved
14//! against final offsets by [`Asm::resolve`] before the bytes leave this module.
15
16#![cfg(target_arch = "x86_64")]
17
18/// A hardware general-purpose register, by its 4-bit encoding.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20#[repr(u8)]
21#[allow(missing_docs)]
22pub enum Reg {
23    Rax = 0,
24    Rcx = 1,
25    Rdx = 2,
26    Rbx = 3,
27    Rsp = 4,
28    Rbp = 5,
29    Rsi = 6,
30    Rdi = 7,
31    R8 = 8,
32    R9 = 9,
33    R10 = 10,
34    R11 = 11,
35    R12 = 12,
36    R13 = 13,
37    R14 = 14,
38    R15 = 15,
39}
40
41impl Reg {
42    /// The low 3 bits (the ModRM/opcode field).
43    #[inline]
44    fn lo3(self) -> u8 {
45        (self as u8) & 0b111
46    }
47    /// The high bit (the REX extension bit).
48    #[inline]
49    fn hi(self) -> u8 {
50        ((self as u8) >> 3) & 1
51    }
52}
53
54/// A hardware XMM register (SSE2), by its 4-bit encoding. f64 slots that win a
55/// physical register live here for the whole function (a SECOND register class
56/// alongside the GP [`Reg`] one); the rest spill to the frame. All XMM registers
57/// are caller-saved under SysV, so none need save/restore in the prologue.
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59#[repr(u8)]
60#[allow(missing_docs)]
61pub enum Xmm {
62    Xmm0 = 0,
63    Xmm1 = 1,
64    Xmm2 = 2,
65    Xmm3 = 3,
66    Xmm4 = 4,
67    Xmm5 = 5,
68    Xmm6 = 6,
69    Xmm7 = 7,
70    Xmm8 = 8,
71    Xmm9 = 9,
72    Xmm10 = 10,
73    Xmm11 = 11,
74    Xmm12 = 12,
75    Xmm13 = 13,
76    Xmm14 = 14,
77    Xmm15 = 15,
78}
79
80impl Xmm {
81    /// The low 3 bits (the ModRM/opcode field).
82    #[inline]
83    fn lo3(self) -> u8 {
84        (self as u8) & 0b111
85    }
86    /// The high bit (the REX extension bit).
87    #[inline]
88    fn hi(self) -> u8 {
89        ((self as u8) >> 3) & 1
90    }
91}
92
93/// An integer condition, mapping to an x86 condition code. The first six are
94/// SIGNED; [`Cond::AeU`] is the one UNSIGNED code the array bounds check needs
95/// (`jae` — "above or equal", unsigned `>=`), used so a 1-based index whose
96/// `idx - 1` wraps negative (e.g. index 0) trips the OOB exit, matching the
97/// stencil's `(im1 as u64) >= (len as u64)` guard.
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99pub enum Cond {
100    /// `<` (signed less): `cc=0xC`.
101    Lt,
102    /// `>` (signed greater): `cc=0xF`.
103    Gt,
104    /// `<=` (signed less-or-equal): `cc=0xE`.
105    Le,
106    /// `>=` (signed greater-or-equal): `cc=0xD`.
107    Ge,
108    /// `==`: `cc=0x4`.
109    Eq,
110    /// `!=`: `cc=0x5`.
111    Ne,
112    /// `>=` UNSIGNED (above or equal): `cc=0x3` (`jae`/`jnc`). The array
113    /// bounds-check uses this to fold the lower (`im1 < 0`) and upper
114    /// (`im1 >= len`) out-of-bounds cases into ONE unsigned comparison.
115    AeU,
116    /// `>` UNSIGNED (above): `cc=0x7` (`ja`). After `ucomisd`, `seta` is the
117    /// strict ordered-greater test (CF=0 && ZF=0) — NaN (ZF=1) folds to FALSE,
118    /// the float `>`/`<` (with swapped operands) primitive.
119    AU,
120    /// UNSIGNED below-or-equal: `cc=0x6` (`jbe`). The NEGATION of `AU` (CF=1 ||
121    /// ZF=1) — the "comparison FALSE" branch for a float `>`/`<` BranchF, where
122    /// NaN (ZF=1) must TAKE the false branch.
123    BeU,
124    /// UNSIGNED below: `cc=0x2` (`jb`). After `ucomisd`, the strict
125    /// ordered-less primitive (CF=1); NaN folds to FALSE.
126    BU,
127    /// PARITY EVEN (`jp`/`jpe`): `cc=0xA`. After `ucomisd`, PF=1 ⟺ the operands
128    /// were UNORDERED (a NaN). The `DivF` zero-divisor guard uses this to skip
129    /// the side-exit on a NaN divisor (NaN is not `0.0`).
130    ParityEven,
131    /// PARITY ODD (`jnp`/`jpo`): `cc=0xB`. After `ucomisd`, PF=0 ⟺ ORDERED —
132    /// the second half of the IEEE `==` test (`sete && setnp`), so a NaN
133    /// compare yields false exactly like the reference `a == b`.
134    ParityOdd,
135    /// OVERFLOW (`jo`): `cc=0x0`. OF=1 after a signed `add`/`sub`/`imul` — the
136    /// integer-overflow side-exit (`jo deopt`) for exact arithmetic: on overflow
137    /// the native tier deopts so the exact VM recomputes (and promotes to BigInt).
138    Overflow,
139}
140
141impl Cond {
142    /// The 4-bit x86 condition-code tetrad (`jcc` low nibble = `0x80 | cc`,
143    /// `setcc` = `0x90 | cc`).
144    fn cc(self) -> u8 {
145        match self {
146            Cond::Eq => 0x4,
147            Cond::Ne => 0x5,
148            Cond::Ge => 0xD, // GE (signed): NL
149            Cond::Lt => 0xC, // L  (signed)
150            Cond::Le => 0xE, // LE (signed)
151            Cond::Gt => 0xF, // G  (signed): NLE
152            Cond::AeU => 0x3, // AE (unsigned): NB/NC
153            Cond::AU => 0x7,  // A  (unsigned): NBE
154            Cond::BeU => 0x6, // BE (unsigned): NA
155            Cond::BU => 0x2,  // B  (unsigned): C
156            Cond::ParityEven => 0xA, // P/PE (parity even): unordered after ucomisd
157            Cond::ParityOdd => 0xB,  // NP/PO (parity odd): ordered after ucomisd
158            Cond::Overflow => 0x0, // O (overflow): OF=1 after signed add/sub/imul
159        }
160    }
161}
162
163/// A label in the instruction stream. Created with [`Asm::new_label`], its
164/// position fixed with [`Asm::bind`], and referenced by jumps; all references
165/// are patched in [`Asm::resolve`].
166#[derive(Clone, Copy, Debug, PartialEq, Eq)]
167pub struct LabelId(pub usize);
168
169enum Fixup {
170    /// A rel32 jump site: 4 LE bytes at `site` are filled with
171    /// `target_off - (site + 4)`.
172    Rel32 { site: usize, label: LabelId },
173}
174
175/// The x86-64 byte emitter with late-bound labels.
176pub struct Asm {
177    buf: Vec<u8>,
178    /// `label_pos[i]` = byte offset of label `i`, or `usize::MAX` until bound.
179    label_pos: Vec<usize>,
180    fixups: Vec<Fixup>,
181}
182
183impl Default for Asm {
184    fn default() -> Self {
185        Asm::new()
186    }
187}
188
189impl Asm {
190    /// An empty assembler.
191    pub fn new() -> Self {
192        Asm { buf: Vec::with_capacity(256), label_pos: Vec::new(), fixups: Vec::new() }
193    }
194
195    /// The current byte length (a position label).
196    pub fn pos(&self) -> usize {
197        self.buf.len()
198    }
199
200    /// Reserve a new (unbound) label.
201    pub fn new_label(&mut self) -> LabelId {
202        let id = LabelId(self.label_pos.len());
203        self.label_pos.push(usize::MAX);
204        id
205    }
206
207    /// Fix label `l` at the current position.
208    pub fn bind(&mut self, l: LabelId) {
209        self.label_pos[l.0] = self.buf.len();
210    }
211
212    /// REX prefix. `w` = 64-bit operand; `r` = ModRM.reg high bit; `x` = SIB
213    /// index high bit; `b` = ModRM.rm / opcode-reg high bit.
214    fn rex(&mut self, w: bool, r: u8, x: u8, b: u8) {
215        let byte = 0x40 | ((w as u8) << 3) | (r << 2) | (x << 1) | b;
216        self.buf.push(byte);
217    }
218
219    /// `mov dst, imm64` (REX.W + B8+rd io). Always 10 bytes; simple and exact.
220    pub fn mov_ri(&mut self, dst: Reg, imm: i64) {
221        self.rex(true, 0, 0, dst.hi());
222        self.buf.push(0xB8 + dst.lo3());
223        self.buf.extend_from_slice(&imm.to_le_bytes());
224    }
225
226    /// `mov dst, src` (REX.W 89 /r, src in reg field).
227    pub fn mov_rr(&mut self, dst: Reg, src: Reg) {
228        if dst == src {
229            return;
230        }
231        self.rex(true, src.hi(), 0, dst.hi());
232        self.buf.push(0x89);
233        self.modrm_reg(src, dst);
234    }
235
236    /// ModRM for register-direct (`mod=11`): reg field = `r`, rm field = `m`.
237    fn modrm_reg(&mut self, r: Reg, m: Reg) {
238        self.buf.push(0b1100_0000 | (r.lo3() << 3) | m.lo3());
239    }
240
241    /// ModRM + (optional) SIB + disp for a `[base + disp32]` memory operand,
242    /// with `r` as the reg field. Always emits a disp32 for simplicity.
243    fn modrm_mem(&mut self, r: Reg, base: Reg, disp: i32) {
244        // mod=10 (disp32). rm=base.lo3(); rm==100 (rsp/r12) needs a SIB byte.
245        let rm = base.lo3();
246        self.buf.push(0b1000_0000 | (r.lo3() << 3) | rm);
247        if rm == 0b100 {
248            // SIB: scale=0, index=100 (none), base=rm.
249            self.buf.push(0b0000_0000 | (0b100 << 3) | rm);
250        }
251        self.buf.extend_from_slice(&disp.to_le_bytes());
252    }
253
254    /// `mov dst, [base + disp]` (REX.W 8B /r).
255    pub fn mov_rm(&mut self, dst: Reg, base: Reg, disp: i32) {
256        self.rex(true, dst.hi(), 0, base.hi());
257        self.buf.push(0x8B);
258        self.modrm_mem(dst, base, disp);
259    }
260
261    /// `mov [base + disp], src` (REX.W 89 /r).
262    pub fn mov_mr(&mut self, base: Reg, disp: i32, src: Reg) {
263        self.rex(true, src.hi(), 0, base.hi());
264        self.buf.push(0x89);
265        self.modrm_mem(src, base, disp);
266    }
267
268    /// `add dst, src` (REX.W 01 /r).
269    pub fn add_rr(&mut self, dst: Reg, src: Reg) {
270        self.rex(true, src.hi(), 0, dst.hi());
271        self.buf.push(0x01);
272        self.modrm_reg(src, dst);
273    }
274
275    /// `sub dst, src` (REX.W 29 /r).
276    pub fn sub_rr(&mut self, dst: Reg, src: Reg) {
277        self.rex(true, src.hi(), 0, dst.hi());
278        self.buf.push(0x29);
279        self.modrm_reg(src, dst);
280    }
281
282    /// `imul dst, src` (REX.W 0F AF /r — dst is the reg field).
283    pub fn imul_rr(&mut self, dst: Reg, src: Reg) {
284        self.rex(true, dst.hi(), 0, src.hi());
285        self.buf.push(0x0F);
286        self.buf.push(0xAF);
287        self.modrm_reg(dst, src);
288    }
289
290    /// `sub dst, imm32` (REX.W 81 /5 id) — sign-extended 32-bit immediate.
291    pub fn sub_ri(&mut self, dst: Reg, imm: i32) {
292        self.rex(true, 0, 0, dst.hi());
293        self.buf.push(0x81);
294        self.buf.push(0b1100_0000 | (5 << 3) | dst.lo3());
295        self.buf.extend_from_slice(&imm.to_le_bytes());
296    }
297
298    /// `add dst, imm32` (REX.W 81 /0 id) — sign-extended 32-bit immediate.
299    pub fn add_ri(&mut self, dst: Reg, imm: i32) {
300        self.rex(true, 0, 0, dst.hi());
301        self.buf.push(0x81);
302        self.buf.push(0b1100_0000 | dst.lo3());
303        self.buf.extend_from_slice(&imm.to_le_bytes());
304    }
305
306    /// `cmp dst, imm32` (REX.W 81 /7 id) — `dst - imm`, sets flags.
307    pub fn cmp_ri(&mut self, dst: Reg, imm: i32) {
308        self.rex(true, 0, 0, dst.hi());
309        self.buf.push(0x81);
310        self.buf.push(0b1100_0000 | (7 << 3) | dst.lo3());
311        self.buf.extend_from_slice(&imm.to_le_bytes());
312    }
313
314    /// `call reg` (FF /2) — an indirect near call through a register.
315    pub fn call_r(&mut self, target: Reg) {
316        if target.hi() == 1 {
317            self.buf.push(0x41);
318        }
319        self.buf.push(0xFF);
320        self.buf.push(0b1100_0000 | (2 << 3) | target.lo3());
321    }
322
323    /// `shl dst, imm8` (REX.W C1 /4 ib) — shift left by a constant count.
324    pub fn shl_ri(&mut self, dst: Reg, imm: u8) {
325        self.rex(true, 0, 0, dst.hi());
326        self.buf.push(0xC1);
327        self.buf.push(0b1100_0000 | (4 << 3) | dst.lo3());
328        self.buf.push(imm);
329    }
330
331    /// `and dst, src` (REX.W 21 /r).
332    pub fn and_rr(&mut self, dst: Reg, src: Reg) {
333        self.rex(true, src.hi(), 0, dst.hi());
334        self.buf.push(0x21);
335        self.modrm_reg(src, dst);
336    }
337
338    /// `or dst, src` (REX.W 09 /r).
339    pub fn or_rr(&mut self, dst: Reg, src: Reg) {
340        self.rex(true, src.hi(), 0, dst.hi());
341        self.buf.push(0x09);
342        self.modrm_reg(src, dst);
343    }
344
345    /// `xor dst, src` (REX.W 31 /r).
346    pub fn xor_rr(&mut self, dst: Reg, src: Reg) {
347        self.rex(true, src.hi(), 0, dst.hi());
348        self.buf.push(0x31);
349        self.modrm_reg(src, dst);
350    }
351
352    /// `not dst` (REX.W F7 /2).
353    pub fn not_r(&mut self, dst: Reg) {
354        self.rex(true, 0, 0, dst.hi());
355        self.buf.push(0xF7);
356        self.buf.push(0b1100_0000 | (2 << 3) | dst.lo3());
357    }
358
359    /// `neg dst` (REX.W F7 /3).
360    pub fn neg_r(&mut self, dst: Reg) {
361        self.rex(true, 0, 0, dst.hi());
362        self.buf.push(0xF7);
363        self.buf.push(0b1100_0000 | (3 << 3) | dst.lo3());
364    }
365
366    /// `shl dst, cl` (REX.W D3 /4).
367    pub fn shl_cl(&mut self, dst: Reg) {
368        self.rex(true, 0, 0, dst.hi());
369        self.buf.push(0xD3);
370        self.buf.push(0b1100_0000 | (4 << 3) | dst.lo3());
371    }
372
373    /// `sar dst, cl` (REX.W D3 /7) — arithmetic shift right.
374    pub fn sar_cl(&mut self, dst: Reg) {
375        self.rex(true, 0, 0, dst.hi());
376        self.buf.push(0xD3);
377        self.buf.push(0b1100_0000 | (7 << 3) | dst.lo3());
378    }
379
380    /// `cmp a, b` (REX.W 39 /r — `a - b`, sets flags).
381    pub fn cmp_rr(&mut self, a: Reg, b: Reg) {
382        self.rex(true, b.hi(), 0, a.hi());
383        self.buf.push(0x39);
384        self.modrm_reg(b, a);
385    }
386
387    /// `cqo` (REX.W 99): sign-extend rax into rdx:rax (for idiv).
388    pub fn cqo(&mut self) {
389        self.buf.push(0x48);
390        self.buf.push(0x99);
391    }
392
393    /// `idiv src` (REX.W F7 /7): rdx:rax / src → quotient rax, remainder rdx.
394    pub fn idiv_r(&mut self, src: Reg) {
395        self.rex(true, 0, 0, src.hi());
396        self.buf.push(0xF7);
397        self.buf.push(0b1100_0000 | (7 << 3) | src.lo3());
398    }
399
400    /// `mul src` (REX.W F7 /4): UNSIGNED rdx:rax = rax * src — the high 64 bits
401    /// of the 128-bit product land in rdx, the low in rax. The magic-reciprocal
402    /// high-multiply primitive.
403    pub fn mul_r(&mut self, src: Reg) {
404        self.rex(true, 0, 0, src.hi());
405        self.buf.push(0xF7);
406        self.buf.push(0b1100_0000 | (4 << 3) | src.lo3());
407    }
408
409    /// `shr dst, imm` (REX.W C1 /5 ib) — LOGICAL (zero-filling) shift right by a
410    /// constant. Distinct from `sar` (`/7`, arithmetic/sign-filling): the
411    /// unsigned magic reciprocal shifts unsigned high-product bits, so it must
412    /// zero-fill.
413    pub fn shr_ri(&mut self, dst: Reg, imm: u8) {
414        self.rex(true, 0, 0, dst.hi());
415        self.buf.push(0xC1);
416        self.buf.push(0b1100_0000 | (5 << 3) | dst.lo3());
417        self.buf.push(imm);
418    }
419
420    /// `test a, a` (REX.W 85 /r) — sets ZF when `a == 0`.
421    pub fn test_rr(&mut self, a: Reg, b: Reg) {
422        self.rex(true, b.hi(), 0, a.hi());
423        self.buf.push(0x85);
424        self.modrm_reg(b, a);
425    }
426
427    // ---------------------------------------------------------------
428    // SSE2 scalar-double (f64) encodings for the XMM register class.
429    //
430    // Every scalar-double op is `<prefix> 0F <op> /r` where the prefix is F2
431    // for the SD (scalar-double) arithmetic forms, 66 for the abs-mask/move
432    // bit ops, F3 for sqrt's reciprocal sibling (we only need F2 0F 51 for
433    // sqrtsd). The REX bits route the high registers (xmm8..xmm15). Memory
434    // operands reuse the GP `modrm_mem` (base + disp32, SIB for rsp/r12) — the
435    // ModRM `reg` field there is taken from a GP `Reg`, so for XMM memory ops
436    // we hand-emit the ModRM with the XMM reg in the reg field.
437    // ---------------------------------------------------------------
438
439    /// REX for an SSE2 reg-reg op `xmm_r OP xmm_m` (reg field = `r`, rm = `m`).
440    /// Emitted only when any high bit is set (the SSE2 forms have no mandatory
441    /// REX.W); a spurious zero REX would be harmless but we keep bytes minimal.
442    fn sse_rex_rr(&mut self, r: Xmm, m: Xmm) {
443        if r.hi() != 0 || m.hi() != 0 {
444            self.buf.push(0x40 | (r.hi() << 2) | m.hi());
445        }
446    }
447
448    /// ModRM for register-direct between two XMM regs.
449    fn modrm_xmm_rr(&mut self, r: Xmm, m: Xmm) {
450        self.buf.push(0b1100_0000 | (r.lo3() << 3) | m.lo3());
451    }
452
453    /// A scalar-double reg-reg op: `<prefix> [REX] 0F <op> /r`, dst is the reg
454    /// field (read-modify-write: `dst = dst OP src`).
455    fn sse_sd_rr(&mut self, prefix: u8, op: u8, dst: Xmm, src: Xmm) {
456        self.buf.push(prefix);
457        self.sse_rex_rr(dst, src);
458        self.buf.push(0x0F);
459        self.buf.push(op);
460        self.modrm_xmm_rr(dst, src);
461    }
462
463    /// A scalar-double reg-MEM op: `<prefix> [REX] 0F <op> /r` with a
464    /// `[base+disp32]` memory operand and the XMM reg in the reg field.
465    fn sse_sd_rm(&mut self, prefix: u8, op: u8, dst: Xmm, base: Reg, disp: i32) {
466        self.buf.push(prefix);
467        // REX: R = dst.hi(), B = base.hi().
468        if dst.hi() != 0 || base.hi() != 0 {
469            self.buf.push(0x40 | (dst.hi() << 2) | base.hi());
470        }
471        self.buf.push(0x0F);
472        self.buf.push(op);
473        let rm = base.lo3();
474        self.buf.push(0b1000_0000 | (dst.lo3() << 3) | rm);
475        if rm == 0b100 {
476            self.buf.push(0b0000_0000 | (0b100 << 3) | rm);
477        }
478        self.buf.extend_from_slice(&disp.to_le_bytes());
479    }
480
481    /// `movsd xmm, [base+disp]` (F2 0F 10 /r) — load an f64 from the frame.
482    pub fn movsd_rm(&mut self, dst: Xmm, base: Reg, disp: i32) {
483        self.sse_sd_rm(0xF2, 0x10, dst, base, disp);
484    }
485    /// `movsd [base+disp], xmm` (F2 0F 11 /r) — store an f64 to the frame.
486    pub fn movsd_mr(&mut self, base: Reg, disp: i32, src: Xmm) {
487        self.sse_sd_rm(0xF2, 0x11, src, base, disp);
488    }
489    /// `movsd dst, src` (F2 0F 10 /r) — XMM→XMM scalar-double copy.
490    pub fn movsd_rr(&mut self, dst: Xmm, src: Xmm) {
491        if dst == src {
492            return;
493        }
494        self.sse_sd_rr(0xF2, 0x10, dst, src);
495    }
496    /// `addsd dst, src` (F2 0F 58 /r) — `dst += src`.
497    pub fn addsd_rr(&mut self, dst: Xmm, src: Xmm) {
498        self.sse_sd_rr(0xF2, 0x58, dst, src);
499    }
500    /// `subsd dst, src` (F2 0F 5C /r) — `dst -= src`.
501    pub fn subsd_rr(&mut self, dst: Xmm, src: Xmm) {
502        self.sse_sd_rr(0xF2, 0x5C, dst, src);
503    }
504    /// `mulsd dst, src` (F2 0F 59 /r) — `dst *= src`.
505    pub fn mulsd_rr(&mut self, dst: Xmm, src: Xmm) {
506        self.sse_sd_rr(0xF2, 0x59, dst, src);
507    }
508    /// `divsd dst, src` (F2 0F 5E /r) — `dst /= src`.
509    pub fn divsd_rr(&mut self, dst: Xmm, src: Xmm) {
510        self.sse_sd_rr(0xF2, 0x5E, dst, src);
511    }
512    /// `sqrtsd dst, src` (F2 0F 51 /r) — `dst = sqrt(src)`.
513    pub fn sqrtsd_rr(&mut self, dst: Xmm, src: Xmm) {
514        self.sse_sd_rr(0xF2, 0x51, dst, src);
515    }
516
517    // ---- Packed double (SIMD, 2-wide) ----------------------------------------
518    // Each instruction below shares the scalar-double encoder helpers but swaps
519    // the `F2` (scalar) prefix for `66` (packed). A packed op computes BOTH f64
520    // lanes with the SAME IEEE-754 rounding as the scalar form, so each lane is
521    // bit-identical to the corresponding scalar op — the soundness invariant that
522    // makes auto-vectorization a LEGAL bit-exact lever (unlike FMA, which fuses
523    // two roundings into one and is therefore NOT bit-exact).
524
525    /// `movupd xmm, [base+disp]` (66 0F 10 /r) — load an UNALIGNED 128-bit pair.
526    /// Unaligned (not `movapd`) because frame/array slots carry no 16-byte
527    /// alignment guarantee; the unaligned form is penalty-free on modern cores
528    /// when the access happens to be aligned.
529    pub fn movupd_rm(&mut self, dst: Xmm, base: Reg, disp: i32) {
530        self.sse_sd_rm(0x66, 0x10, dst, base, disp);
531    }
532    /// `movupd [base+disp], xmm` (66 0F 11 /r) — store an unaligned 128-bit pair.
533    pub fn movupd_mr(&mut self, base: Reg, disp: i32, src: Xmm) {
534        self.sse_sd_rm(0x66, 0x11, src, base, disp);
535    }
536    /// `movupd dst, src` (66 0F 10 /r) — XMM→XMM 128-bit copy (both lanes).
537    pub fn movupd_rr(&mut self, dst: Xmm, src: Xmm) {
538        if dst == src {
539            return;
540        }
541        self.sse_sd_rr(0x66, 0x10, dst, src);
542    }
543    /// `addpd dst, src` (66 0F 58 /r) — `dst[lane] += src[lane]` for both lanes.
544    pub fn addpd_rr(&mut self, dst: Xmm, src: Xmm) {
545        self.sse_sd_rr(0x66, 0x58, dst, src);
546    }
547    /// `subpd dst, src` (66 0F 5C /r) — `dst[lane] -= src[lane]` for both lanes.
548    pub fn subpd_rr(&mut self, dst: Xmm, src: Xmm) {
549        self.sse_sd_rr(0x66, 0x5C, dst, src);
550    }
551    /// `mulpd dst, src` (66 0F 59 /r) — `dst[lane] *= src[lane]` for both lanes.
552    pub fn mulpd_rr(&mut self, dst: Xmm, src: Xmm) {
553        self.sse_sd_rr(0x66, 0x59, dst, src);
554    }
555    /// `cmppd dst, src, pred` (66 0F C2 /r ib) — per-lane compare; each lane is
556    /// set to all-ones (true) or all-zero (false). `pred`: 0=EQ 1=LT 2=LE 3=UNORD
557    /// 4=NEQ 5=NLT 6=NLE 7=ORD. The mandelbrot escape test uses LE (2).
558    pub fn cmppd_rr(&mut self, dst: Xmm, src: Xmm, pred: u8) {
559        self.buf.push(0x66);
560        self.sse_rex_rr(dst, src);
561        self.buf.push(0x0F);
562        self.buf.push(0xC2);
563        self.modrm_xmm_rr(dst, src);
564        self.buf.push(pred);
565    }
566    /// `movmskpd r32/64, xmm` (66 0F 50 /r) — extract the two lane SIGN bits into
567    /// a GP register (bit0=lane0, bit1=lane1). After a cmppd, a true lane's sign
568    /// bit is 1, so this yields the active-lane bitmask the loop-exit test reads.
569    /// The GP dst is the reg field, the XMM src the rm field.
570    pub fn movmskpd(&mut self, dst: Reg, src: Xmm) {
571        self.buf.push(0x66);
572        let rex = 0x40 | (dst.hi() << 2) | src.hi();
573        if rex != 0x40 {
574            self.buf.push(rex);
575        }
576        self.buf.push(0x0F);
577        self.buf.push(0x50);
578        self.buf.push(0b1100_0000 | (dst.lo3() << 3) | src.lo3());
579    }
580    /// `andpd dst, src` (66 0F 54 /r) — bitwise AND of both 128-bit lanes. The
581    /// branchless counter masks a packed 1.0 by the escape mask.
582    pub fn andpd_rr(&mut self, dst: Xmm, src: Xmm) {
583        self.sse_sd_rr(0x66, 0x54, dst, src);
584    }
585    /// `andnpd dst, src` (66 0F 55 /r) — `dst = (NOT dst) AND src` (128-bit).
586    pub fn andnpd_rr(&mut self, dst: Xmm, src: Xmm) {
587        self.sse_sd_rr(0x66, 0x55, dst, src);
588    }
589    /// `orpd dst, src` (66 0F 56 /r) — bitwise OR of both 128-bit lanes.
590    pub fn orpd_rr(&mut self, dst: Xmm, src: Xmm) {
591        self.sse_sd_rr(0x66, 0x56, dst, src);
592    }
593    /// `xorpd dst, src` (66 0F 57 /r) — bitwise XOR of both 128-bit lanes.
594    pub fn xorpd_rr(&mut self, dst: Xmm, src: Xmm) {
595        self.sse_sd_rr(0x66, 0x57, dst, src);
596    }
597    /// `divpd dst, src` (66 0F 5E /r) — `dst[lane] /= src[lane]` for both lanes.
598    pub fn divpd_rr(&mut self, dst: Xmm, src: Xmm) {
599        self.sse_sd_rr(0x66, 0x5E, dst, src);
600    }
601    /// `sqrtpd dst, src` (66 0F 51 /r) — `dst[lane] = sqrt(src[lane])` both lanes.
602    pub fn sqrtpd_rr(&mut self, dst: Xmm, src: Xmm) {
603        self.sse_sd_rr(0x66, 0x51, dst, src);
604    }
605    /// `ucomisd a, b` (66 0F 2E /r) — unordered f64 compare setting CF/ZF/PF.
606    /// NaN (unordered) sets ZF=CF=PF=1, so the seta/setae/jbe family used by the
607    /// backend folds the unordered case to FALSE, matching the kernel's IEEE
608    /// relations (NaN compares false).
609    pub fn ucomisd_rr(&mut self, a: Xmm, b: Xmm) {
610        self.buf.push(0x66);
611        self.sse_rex_rr(a, b);
612        self.buf.push(0x0F);
613        self.buf.push(0x2E);
614        self.modrm_xmm_rr(a, b);
615    }
616    /// `cvtsi2sd xmm, r64` (F2 REX.W 0F 2A /r) — signed i64 → f64 (the kernel's
617    /// IntToFloat). The GP source is the rm field, the XMM dst the reg field.
618    pub fn cvtsi2sd(&mut self, dst: Xmm, src: Reg) {
619        self.buf.push(0xF2);
620        // REX.W=1, R = dst.hi(), B = src.hi().
621        self.buf.push(0x48 | (dst.hi() << 2) | src.hi());
622        self.buf.push(0x0F);
623        self.buf.push(0x2A);
624        self.buf.push(0b1100_0000 | (dst.lo3() << 3) | src.lo3());
625    }
626    /// `movq xmm, r64` (66 REX.W 0F 6E /r) — bit-copy a GP register into an XMM
627    /// (no conversion). The GP src is the rm field, the XMM dst the reg field.
628    pub fn movq_xr(&mut self, dst: Xmm, src: Reg) {
629        self.buf.push(0x66);
630        self.buf.push(0x48 | (dst.hi() << 2) | src.hi());
631        self.buf.push(0x0F);
632        self.buf.push(0x6E);
633        self.buf.push(0b1100_0000 | (dst.lo3() << 3) | src.lo3());
634    }
635    /// `movq r64, xmm` (66 REX.W 0F 7E /r) — bit-copy an XMM into a GP register.
636    /// The XMM src is the reg field, the GP dst the rm field.
637    pub fn movq_rx(&mut self, dst: Reg, src: Xmm) {
638        self.buf.push(0x66);
639        self.buf.push(0x48 | (src.hi() << 2) | dst.hi());
640        self.buf.push(0x0F);
641        self.buf.push(0x7E);
642        self.buf.push(0b1100_0000 | (src.lo3() << 3) | dst.lo3());
643    }
644
645    /// `movzx dst, byte [base + disp]` (REX.W 0F B6 /r) — load ONE byte from
646    /// memory and zero-extend it into the 64-bit `dst`. The byte-array
647    /// (`Seq of Bool`) element load: `frame[D] = buf[i-1] as i64` over 1-byte
648    /// elements, where the loaded `u8` widens to a non-negative i64 (0..=255) —
649    /// bit-identical to the `logos_stencil_arrldb` `*ptr as i64`.
650    pub fn movzx_rm8(&mut self, dst: Reg, base: Reg, disp: i32) {
651        self.rex(true, dst.hi(), 0, base.hi());
652        self.buf.push(0x0F);
653        self.buf.push(0xB6);
654        self.modrm_mem(dst, base, disp);
655    }
656
657    /// `mov byte [base + disp], src8` (88 /r) — store the LOW byte of `src` to
658    /// memory. The byte-array element store; the value is pre-normalized to 0/1
659    /// by the caller (matching `logos_stencil_arrstb`'s `(v != 0) as u8`), so
660    /// only the low byte is written. A REX prefix is emitted whenever any high
661    /// register bit is set OR `src` is one of `spl/bpl/sil/dil` (rsp..rdi,
662    /// encodings 4..7) — those low-byte registers are addressable ONLY with a
663    /// REX prefix present (without REX the encoding means `ah/ch/dh/bh`).
664    pub fn mov_mr8(&mut self, base: Reg, disp: i32, src: Reg) {
665        let need_rex = src.hi() != 0 || base.hi() != 0 || ((src as u8) & 0b100) != 0;
666        if need_rex {
667            self.buf.push(0x40 | (src.hi() << 2) | base.hi());
668        }
669        self.buf.push(0x88);
670        self.modrm_mem(src, base, disp);
671    }
672
673    /// `movsxd dst, dword [base + disp]` (REX.W 63 /r) — load FOUR bytes from
674    /// memory and SIGN-extend them into the 64-bit `dst`. The half-width
675    /// (`IntsI32`) array element load: `frame[D] = buf[i-1] as i64` over 4-byte
676    /// `i32` elements — bit-identical to `logos_stencil_arrld_i32`'s
677    /// `*(i32*)ptr as i64`.
678    pub fn movsxd_rm(&mut self, dst: Reg, base: Reg, disp: i32) {
679        self.rex(true, dst.hi(), 0, base.hi());
680        self.buf.push(0x63);
681        self.modrm_mem(dst, base, disp);
682    }
683
684    /// `mov dword [base + disp], src32` (89 /r, NO REX.W) — store the LOW 4 bytes
685    /// of `src` to memory. The half-width (`IntsI32`) element store: the value is
686    /// truncated to `i32` (lossless under the narrowing proof) — bit-identical to
687    /// `logos_stencil_arrst_i32`'s `*(i32*)ptr = v as i32`. A REX prefix (without
688    /// the W bit) is emitted only for the high-register extension bits.
689    pub fn mov_mr32(&mut self, base: Reg, disp: i32, src: Reg) {
690        if src.hi() != 0 || base.hi() != 0 {
691            self.buf.push(0x40 | (src.hi() << 2) | base.hi());
692        }
693        self.buf.push(0x89);
694        self.modrm_mem(src, base, disp);
695    }
696
697    /// `setcc dst8` — set the LOW byte of `dst` to 0/1 from the flags (no
698    /// zero-extension of the upper bits). A REX prefix (even empty) makes
699    /// `spl/bpl/sil/dil` and `r8b..r15b` addressable; emit REX with B = dst.hi().
700    /// Used to normalize a byte-array store value to 0/1 in the same register
701    /// whose low byte is then stored by [`Asm::mov_mr8`].
702    pub fn setcc8(&mut self, cond: Cond, dst: Reg) {
703        self.buf.push(0x40 | dst.hi());
704        self.buf.push(0x0F);
705        self.buf.push(0x90 | cond.cc());
706        self.buf.push(0b1100_0000 | dst.lo3());
707    }
708
709    /// `setcc dst8` then `movzx dst, dst8` — materialize a 0/1 from flags.
710    /// `dst` must be a register whose low byte is addressable under REX
711    /// (all of rax..r15 are with a REX prefix).
712    pub fn setcc_movzx(&mut self, cond: Cond, dst: Reg) {
713        // setcc r/m8: 0F (90+cc) /0. A REX prefix (even empty) makes spl/bpl/
714        // sil/dil and r8b..r15b addressable; emit REX with B = dst.hi().
715        self.buf.push(0x40 | dst.hi());
716        self.buf.push(0x0F);
717        self.buf.push(0x90 | cond.cc());
718        self.buf.push(0b1100_0000 | dst.lo3());
719        // movzx dst, dst8 (REX.W 0F B6 /r).
720        self.rex(true, dst.hi(), 0, dst.hi());
721        self.buf.push(0x0F);
722        self.buf.push(0xB6);
723        self.modrm_reg(dst, dst);
724    }
725
726    /// `jmp label` (E9 cd, rel32 patched in `resolve`).
727    pub fn jmp(&mut self, label: LabelId) {
728        self.buf.push(0xE9);
729        let site = self.buf.len();
730        self.buf.extend_from_slice(&[0, 0, 0, 0]);
731        self.fixups.push(Fixup::Rel32 { site, label });
732    }
733
734    /// `jcc label` (0F 80+cc cd, rel32 patched in `resolve`).
735    pub fn jcc(&mut self, cond: Cond, label: LabelId) {
736        self.buf.push(0x0F);
737        self.buf.push(0x80 | cond.cc());
738        let site = self.buf.len();
739        self.buf.extend_from_slice(&[0, 0, 0, 0]);
740        self.fixups.push(Fixup::Rel32 { site, label });
741    }
742
743    /// `push reg` (50+rd, with REX.B for r8..r15).
744    pub fn push(&mut self, r: Reg) {
745        if r.hi() == 1 {
746            self.buf.push(0x41);
747        }
748        self.buf.push(0x50 + r.lo3());
749    }
750
751    /// `pop reg` (58+rd, with REX.B for r8..r15).
752    pub fn pop(&mut self, r: Reg) {
753        if r.hi() == 1 {
754            self.buf.push(0x41);
755        }
756        self.buf.push(0x58 + r.lo3());
757    }
758
759    /// `ret` (C3).
760    pub fn ret(&mut self) {
761        self.buf.push(0xC3);
762    }
763
764    /// Resolve all label fixups against bound positions and return the final
765    /// machine code. Panics if a referenced label was never bound (a backend
766    /// bug — every label this module creates is bound before `resolve`).
767    pub fn resolve(mut self) -> Vec<u8> {
768        for f in &self.fixups {
769            match *f {
770                Fixup::Rel32 { site, label } => {
771                    let target = self.label_pos[label.0];
772                    assert_ne!(target, usize::MAX, "unbound label {label:?}");
773                    let rel = (target as i64) - (site as i64 + 4);
774                    let rel32 = rel as i32;
775                    self.buf[site..site + 4].copy_from_slice(&rel32.to_le_bytes());
776                }
777            }
778        }
779        self.buf
780    }
781}
782
783#[cfg(test)]
784mod tests {
785    use super::*;
786    use crate::JitPage;
787
788    /// Build a tiny function with the test ABI `fn(i64, i64) -> i64` and run it.
789    fn run2(code: &[u8], a: i64, b: i64) -> i64 {
790        let page = JitPage::new(code).expect("map");
791        let f = unsafe { page.as_fn_i64_i64() };
792        f(a, b)
793    }
794
795    #[test]
796    fn mov_imm_and_ret_returns_constant() {
797        let mut a = Asm::new();
798        a.mov_ri(Reg::Rax, 0x1234_5678_9ABC_DEF0u64 as i64);
799        a.ret();
800        assert_eq!(run2(&a.resolve(), 0, 0), 0x1234_5678_9ABC_DEF0u64 as i64);
801    }
802
803    #[test]
804    fn add_two_args_via_sysv() {
805        // SysV: arg0 = rdi, arg1 = rsi, return rax.
806        let mut a = Asm::new();
807        a.mov_rr(Reg::Rax, Reg::Rdi);
808        a.add_rr(Reg::Rax, Reg::Rsi);
809        a.ret();
810        let code = a.resolve();
811        assert_eq!(run2(&code, 40, 2), 42);
812        assert_eq!(run2(&code, -5, 5), 0);
813    }
814
815    #[test]
816    fn imul_and_extended_registers() {
817        let mut a = Asm::new();
818        a.mov_rr(Reg::R10, Reg::Rdi);
819        a.imul_rr(Reg::R10, Reg::Rsi);
820        a.mov_rr(Reg::Rax, Reg::R10);
821        a.ret();
822        assert_eq!(run2(&a.resolve(), 6, 7), 42);
823    }
824
825    #[test]
826    fn setcc_materializes_zero_one() {
827        // return (rdi < rsi) as i64
828        let mut a = Asm::new();
829        a.cmp_rr(Reg::Rdi, Reg::Rsi);
830        a.setcc_movzx(Cond::Lt, Reg::Rax);
831        a.ret();
832        let code = a.resolve();
833        assert_eq!(run2(&code, 1, 2), 1);
834        assert_eq!(run2(&code, 2, 1), 0);
835        assert_eq!(run2(&code, 2, 2), 0);
836    }
837
838    #[test]
839    fn conditional_jump_branches() {
840        // if rdi < rsi { return 100 } else { return 200 }
841        let mut a = Asm::new();
842        let els = a.new_label();
843        a.cmp_rr(Reg::Rdi, Reg::Rsi);
844        a.jcc(Cond::Ge, els); // not (rdi<rsi) -> else
845        a.mov_ri(Reg::Rax, 100);
846        a.ret();
847        a.bind(els);
848        a.mov_ri(Reg::Rax, 200);
849        a.ret();
850        let code = a.resolve();
851        assert_eq!(run2(&code, 1, 2), 100);
852        assert_eq!(run2(&code, 2, 1), 200);
853    }
854
855    #[test]
856    fn frame_load_store_roundtrip() {
857        // treat rdi as a base pointer to an i64 array; store rsi at [rdi+8],
858        // load it back into rax.
859        let mut a = Asm::new();
860        a.mov_mr(Reg::Rdi, 8, Reg::Rsi);
861        a.mov_rm(Reg::Rax, Reg::Rdi, 8);
862        a.ret();
863        let mut frame = [0i64; 4];
864        let page = JitPage::new(&a.resolve()).unwrap();
865        let f: extern "C" fn(*mut i64, i64) -> i64 =
866            unsafe { std::mem::transmute(page.as_ptr()) };
867        let r = f(frame.as_mut_ptr(), 777);
868        assert_eq!(r, 777);
869        assert_eq!(frame[1], 777);
870    }
871
872    #[test]
873    fn idiv_signed_division() {
874        // return rdi / rsi
875        let mut a = Asm::new();
876        a.mov_rr(Reg::Rax, Reg::Rdi);
877        a.cqo();
878        a.idiv_r(Reg::Rsi);
879        a.ret();
880        let code = a.resolve();
881        assert_eq!(run2(&code, 100, 7), 14);
882        assert_eq!(run2(&code, -100, 7), -14);
883    }
884
885    #[test]
886    fn mul_r_high_product_unsigned() {
887        // return the HIGH 64 bits of the unsigned product rdi * rsi (rdx after
888        // `mul`). The classic case that distinguishes unsigned `mul` from signed
889        // `imul`: with rdi = u64::MAX (== -1 as i64), the unsigned high half is
890        // rsi - 1, NOT the signed -1.
891        let mut a = Asm::new();
892        a.mov_rr(Reg::Rax, Reg::Rdi);
893        a.mul_r(Reg::Rsi); // rdx:rax = rax * rsi (unsigned)
894        a.mov_rr(Reg::Rax, Reg::Rdx); // return the high half
895        a.ret();
896        let code = a.resolve();
897        // (2^64 - 1) * 3 = 3*2^64 - 3 → high half = 2 (since low half borrows).
898        assert_eq!(run2(&code, -1i64, 3) as u64, 2);
899        // 2^32 * 2^32 = 2^64 → high half = 1.
900        assert_eq!(run2(&code, 1i64 << 32, 1i64 << 32) as u64, 1);
901        // Small product: high half is 0.
902        assert_eq!(run2(&code, 123, 456) as u64, 0);
903    }
904
905    #[test]
906    fn shr_ri_is_logical_not_arithmetic() {
907        // `shr` zero-fills (logical); `sar` sign-fills. With rdi = -1 (all bits
908        // set) shifted right by 1, logical gives i64::MAX, arithmetic gives -1.
909        let mut a = Asm::new();
910        a.mov_rr(Reg::Rax, Reg::Rdi);
911        a.shr_ri(Reg::Rax, 1);
912        a.ret();
913        let code = a.resolve();
914        assert_eq!(run2(&code, -1i64, 0), i64::MAX);
915        assert_eq!(run2(&code, 1024, 0), 512);
916        // Shift by a larger amount.
917        let mut b = Asm::new();
918        b.mov_rr(Reg::Rax, Reg::Rdi);
919        b.shr_ri(Reg::Rax, 60);
920        b.ret();
921        assert_eq!(run2(&b.resolve(), -1i64, 0) as u64, 0xF);
922    }
923
924    #[test]
925    fn rsp_base_needs_sib() {
926        // Exercise the SIB path: use rsp-relative store would clobber the
927        // stack, so instead verify r12 (also lo3==100) routes through SIB.
928        let mut a = Asm::new();
929        a.mov_rr(Reg::R12, Reg::Rdi);
930        a.mov_mr(Reg::R12, 16, Reg::Rsi);
931        a.mov_rm(Reg::Rax, Reg::R12, 16);
932        a.ret();
933        let mut frame = [0i64; 8];
934        let page = JitPage::new(&a.resolve()).unwrap();
935        let f: extern "C" fn(*mut i64, i64) -> i64 =
936            unsafe { std::mem::transmute(page.as_ptr()) };
937        let r = f(frame.as_mut_ptr(), 555);
938        assert_eq!(r, 555);
939        assert_eq!(frame[2], 555);
940    }
941
942    #[test]
943    fn shift_and_bitwise() {
944        // return (rdi << rcx_amount) where amount is in rsi; use cl.
945        let mut a = Asm::new();
946        a.mov_rr(Reg::Rax, Reg::Rdi);
947        a.mov_rr(Reg::Rcx, Reg::Rsi);
948        a.shl_cl(Reg::Rax);
949        a.ret();
950        let code = a.resolve();
951        assert_eq!(run2(&code, 1, 4), 16);
952        assert_eq!(run2(&code, 3, 2), 12);
953    }
954
955    #[test]
956    fn sub_immediate_and_shl_immediate() {
957        // return ((rdi - 1) << 3)  — the array byte-offset computation.
958        let mut a = Asm::new();
959        a.mov_rr(Reg::Rax, Reg::Rdi);
960        a.sub_ri(Reg::Rax, 1);
961        a.shl_ri(Reg::Rax, 3);
962        a.ret();
963        let code = a.resolve();
964        assert_eq!(run2(&code, 1, 0), 0); // (1-1)*8
965        assert_eq!(run2(&code, 5, 0), 32); // (5-1)*8
966        assert_eq!(run2(&code, 0, 0), -8); // (0-1)*8 wraps
967    }
968
969    #[test]
970    fn unsigned_above_equal_branch() {
971        // if (rdi as u64) >= (rsi as u64) { 1 } else { 0 } — the bounds guard.
972        let mut a = Asm::new();
973        let oob = a.new_label();
974        a.cmp_rr(Reg::Rdi, Reg::Rsi);
975        a.jcc(Cond::AeU, oob);
976        a.mov_ri(Reg::Rax, 0);
977        a.ret();
978        a.bind(oob);
979        a.mov_ri(Reg::Rax, 1);
980        a.ret();
981        let code = a.resolve();
982        assert_eq!(run2(&code, 3, 4), 0); // 3 < 4
983        assert_eq!(run2(&code, 4, 4), 1); // 4 >= 4
984        assert_eq!(run2(&code, -1, 4), 1); // (-1) as u64 huge >= 4 (the i=0 case)
985    }
986
987    /// Run a frame-ABI function `fn(*mut i64) -> i64` over a mutable frame.
988    fn run_frame(code: &[u8], frame: &mut [i64]) -> i64 {
989        let page = JitPage::new(code).unwrap();
990        let f: extern "C" fn(*mut i64) -> i64 =
991            unsafe { std::mem::transmute(page.as_ptr()) };
992        f(frame.as_mut_ptr())
993    }
994
995    #[test]
996    fn sse_arithmetic_matches_ieee() {
997        // frame: 0=a 1=b ; compute ((a+b)*a - b) / a and return its bits.
998        for (a, b) in [(3.0f64, 4.0f64), (1.5, -2.25), (0.1, 0.2), (-7.0, 11.0)] {
999            let mut a_asm = Asm::new();
1000            a_asm.movsd_rm(Xmm::Xmm0, Reg::Rdi, 0); // x0 = a
1001            a_asm.movsd_rm(Xmm::Xmm1, Reg::Rdi, 8); // x1 = b
1002            a_asm.movsd_rr(Xmm::Xmm2, Xmm::Xmm0); // x2 = a
1003            a_asm.addsd_rr(Xmm::Xmm2, Xmm::Xmm1); // x2 = a+b
1004            a_asm.mulsd_rr(Xmm::Xmm2, Xmm::Xmm0); // x2 = (a+b)*a
1005            a_asm.subsd_rr(Xmm::Xmm2, Xmm::Xmm1); // x2 = (a+b)*a - b
1006            a_asm.divsd_rr(Xmm::Xmm2, Xmm::Xmm0); // x2 = .. / a
1007            a_asm.movq_rx(Reg::Rax, Xmm::Xmm2);
1008            a_asm.ret();
1009            let mut frame = [a.to_bits() as i64, b.to_bits() as i64];
1010            let got = f64::from_bits(run_frame(&a_asm.resolve(), &mut frame) as u64);
1011            let want = ((a + b) * a - b) / a;
1012            assert_eq!(got.to_bits(), want.to_bits(), "a={a} b={b}");
1013        }
1014    }
1015
1016    #[test]
1017    fn sse_sqrt_and_cvtsi2sd() {
1018        // frame: 0=n(int) ; return bits of sqrt((f64)n).
1019        for n in [0i64, 1, 2, 4, 9, 1_000_000, 123_456_789] {
1020            let mut a = Asm::new();
1021            a.mov_rm(Reg::Rax, Reg::Rdi, 0);
1022            a.cvtsi2sd(Xmm::Xmm0, Reg::Rax);
1023            a.sqrtsd_rr(Xmm::Xmm1, Xmm::Xmm0);
1024            a.movq_rx(Reg::Rax, Xmm::Xmm1);
1025            a.ret();
1026            let mut frame = [n];
1027            let got = f64::from_bits(run_frame(&a.resolve(), &mut frame) as u64);
1028            assert_eq!(got.to_bits(), (n as f64).sqrt().to_bits(), "n={n}");
1029        }
1030    }
1031
1032    #[test]
1033    fn sse_high_registers_route_rex() {
1034        // Exercise xmm8..xmm15 (the high-register REX path): x8 = a; x9 = b;
1035        // x8 += x9; return bits.
1036        for (a, b) in [(2.5f64, 0.5f64), (-1.0, 3.0)] {
1037            let mut asm = Asm::new();
1038            asm.movsd_rm(Xmm::Xmm8, Reg::Rdi, 0);
1039            asm.movsd_rm(Xmm::Xmm9, Reg::Rdi, 8);
1040            asm.addsd_rr(Xmm::Xmm8, Xmm::Xmm9);
1041            asm.movsd_mr(Reg::Rdi, 16, Xmm::Xmm8);
1042            asm.mov_rm(Reg::Rax, Reg::Rdi, 16);
1043            asm.ret();
1044            let mut frame = [a.to_bits() as i64, b.to_bits() as i64, 0];
1045            let got = f64::from_bits(run_frame(&asm.resolve(), &mut frame) as u64);
1046            assert_eq!(got.to_bits(), (a + b).to_bits(), "a={a} b={b}");
1047        }
1048    }
1049
1050    #[test]
1051    fn sse_packed_double_two_lanes_add_mul_sub() {
1052        // The SIMD foundation: movupd loads/stores a 128-bit pair, and
1053        // addpd/mulpd/subpd operate on BOTH f64 lanes at once. frame layout:
1054        // [a0,a1, b0,b1, addOut0,addOut1, mulOut0,mulOut1, subOut0,subOut1].
1055        let cases: [(f64, f64, f64, f64); 3] = [
1056            (2.5, 0.5, 1.0, 3.0),
1057            (-1.0, 7.25, 4.0, -2.5),
1058            (0.0, -0.0, 1.0, 1.0),
1059        ];
1060        for (a0, a1, b0, b1) in cases {
1061            let mut asm = Asm::new();
1062            asm.movupd_rm(Xmm::Xmm0, Reg::Rdi, 0); // {a0,a1}
1063            asm.movupd_rm(Xmm::Xmm1, Reg::Rdi, 16); // {b0,b1}
1064            asm.movupd_rr(Xmm::Xmm2, Xmm::Xmm0); // copy for mul
1065            asm.movupd_rr(Xmm::Xmm3, Xmm::Xmm0); // copy for sub
1066            asm.addpd_rr(Xmm::Xmm0, Xmm::Xmm1); // {a0+b0, a1+b1}
1067            asm.mulpd_rr(Xmm::Xmm2, Xmm::Xmm1); // {a0*b0, a1*b1}
1068            asm.subpd_rr(Xmm::Xmm3, Xmm::Xmm1); // {a0-b0, a1-b1}
1069            asm.movupd_mr(Reg::Rdi, 32, Xmm::Xmm0);
1070            asm.movupd_mr(Reg::Rdi, 48, Xmm::Xmm2);
1071            asm.movupd_mr(Reg::Rdi, 64, Xmm::Xmm3);
1072            asm.ret();
1073            let mut frame = [
1074                a0.to_bits() as i64,
1075                a1.to_bits() as i64,
1076                b0.to_bits() as i64,
1077                b1.to_bits() as i64,
1078                0, 0, 0, 0, 0, 0,
1079            ];
1080            run_frame(&asm.resolve(), &mut frame);
1081            let lane = |i: usize| f64::from_bits(frame[i] as u64);
1082            assert_eq!(lane(4).to_bits(), (a0 + b0).to_bits(), "add lane0");
1083            assert_eq!(lane(5).to_bits(), (a1 + b1).to_bits(), "add lane1");
1084            assert_eq!(lane(6).to_bits(), (a0 * b0).to_bits(), "mul lane0");
1085            assert_eq!(lane(7).to_bits(), (a1 * b1).to_bits(), "mul lane1");
1086            assert_eq!(lane(8).to_bits(), (a0 - b0).to_bits(), "sub lane0");
1087            assert_eq!(lane(9).to_bits(), (a1 - b1).to_bits(), "sub lane1");
1088        }
1089    }
1090
1091    #[test]
1092    fn sse_packed_high_registers_route_rex() {
1093        // Packed ops on xmm8..xmm15 must carry REX.R/REX.B exactly like scalar.
1094        let cases: [(f64, f64, f64, f64); 2] = [(2.5, 0.5, 1.0, 3.0), (-1.0, 3.0, 4.0, -2.5)];
1095        for (a0, a1, b0, b1) in cases {
1096            let mut asm = Asm::new();
1097            asm.movupd_rm(Xmm::Xmm12, Reg::Rdi, 0);
1098            asm.movupd_rm(Xmm::Xmm13, Reg::Rdi, 16);
1099            asm.addpd_rr(Xmm::Xmm12, Xmm::Xmm13);
1100            asm.movupd_mr(Reg::Rdi, 32, Xmm::Xmm12);
1101            asm.ret();
1102            let mut frame = [
1103                a0.to_bits() as i64,
1104                a1.to_bits() as i64,
1105                b0.to_bits() as i64,
1106                b1.to_bits() as i64,
1107                0,
1108                0,
1109            ];
1110            run_frame(&asm.resolve(), &mut frame);
1111            assert_eq!(f64::from_bits(frame[4] as u64).to_bits(), (a0 + b0).to_bits());
1112            assert_eq!(f64::from_bits(frame[5] as u64).to_bits(), (a1 + b1).to_bits());
1113        }
1114    }
1115
1116    #[test]
1117    fn sse_packed_double_is_bit_identical_to_scalar() {
1118        // The soundness invariant that makes SIMD a LEGAL bit-exact lever (unlike
1119        // FMA): each packed lane equals the scalar op on that lane, bit-for-bit,
1120        // including subnormals and rounding edges.
1121        let cases: [(f64, f64); 4] = [
1122            (0.1, 0.2),
1123            (1e308, 1e308),
1124            (f64::MIN_POSITIVE, f64::MIN_POSITIVE),
1125            (1.0 / 3.0, 7.0),
1126        ];
1127        for (a, b) in cases {
1128            let mut asm = Asm::new();
1129            asm.movupd_rm(Xmm::Xmm0, Reg::Rdi, 0);
1130            asm.movupd_rm(Xmm::Xmm1, Reg::Rdi, 16);
1131            asm.mulpd_rr(Xmm::Xmm0, Xmm::Xmm1);
1132            asm.movupd_mr(Reg::Rdi, 32, Xmm::Xmm0);
1133            asm.ret();
1134            // Both lanes carry the same (a,b) so a scalar mulsd is the oracle.
1135            let mut frame = [
1136                a.to_bits() as i64,
1137                a.to_bits() as i64,
1138                b.to_bits() as i64,
1139                b.to_bits() as i64,
1140                0,
1141                0,
1142            ];
1143            run_frame(&asm.resolve(), &mut frame);
1144            assert_eq!(f64::from_bits(frame[4] as u64).to_bits(), (a * b).to_bits());
1145            assert_eq!(f64::from_bits(frame[5] as u64).to_bits(), (a * b).to_bits());
1146        }
1147    }
1148
1149    #[test]
1150    fn sse_packed_div_and_sqrt_two_lanes() {
1151        // Completes the packed float-arithmetic set the general vectorizer maps
1152        // DivF/SqrtF onto. Both lanes, bit-identical to the scalar op.
1153        let cases: [(f64, f64); 4] = [(9.0, 2.0), (1.0, 3.0), (-7.5, 0.25), (2.0, 8.0)];
1154        for (a, b) in cases {
1155            let mut asm = Asm::new();
1156            asm.movupd_rm(Xmm::Xmm0, Reg::Rdi, 0); // {a,a}
1157            asm.movupd_rm(Xmm::Xmm1, Reg::Rdi, 16); // {b,b}
1158            asm.divpd_rr(Xmm::Xmm0, Xmm::Xmm1); // {a/b, a/b}
1159            asm.sqrtpd_rr(Xmm::Xmm1, Xmm::Xmm1); // {sqrt(b), sqrt(b)}
1160            asm.movupd_mr(Reg::Rdi, 32, Xmm::Xmm0);
1161            asm.movupd_mr(Reg::Rdi, 48, Xmm::Xmm1);
1162            asm.ret();
1163            let mut frame = [
1164                a.to_bits() as i64,
1165                a.to_bits() as i64,
1166                b.to_bits() as i64,
1167                b.to_bits() as i64,
1168                0, 0, 0, 0,
1169            ];
1170            run_frame(&asm.resolve(), &mut frame);
1171            assert_eq!(f64::from_bits(frame[4] as u64).to_bits(), (a / b).to_bits(), "div");
1172            assert_eq!(f64::from_bits(frame[6] as u64).to_bits(), b.sqrt().to_bits(), "sqrt");
1173        }
1174    }
1175
1176    #[test]
1177    fn sse_packed_cmple_and_movmskpd_lane_mask() {
1178        // The mandelbrot escape test: cmplepd sets a lane to all-ones when
1179        // a[lane] <= b[lane], and movmskpd extracts the two lane sign bits into a
1180        // GP register (bit0=lane0, bit1=lane1) — the loop-exit signal.
1181        let cases: [(f64, f64, f64, f64, i64); 4] = [
1182            (1.0, 1.0, 4.0, 4.0, 0b11), // both <= 4
1183            (9.0, 1.0, 4.0, 4.0, 0b10), // lane0 escaped (9>4), lane1 active
1184            (1.0, 9.0, 4.0, 4.0, 0b01), // lane1 escaped
1185            (9.0, 9.0, 4.0, 4.0, 0b00), // both escaped
1186        ];
1187        for (a0, a1, b0, b1, want) in cases {
1188            let mut asm = Asm::new();
1189            asm.movupd_rm(Xmm::Xmm0, Reg::Rdi, 0); // {a0,a1}
1190            asm.movupd_rm(Xmm::Xmm1, Reg::Rdi, 16); // {b0,b1}
1191            asm.cmppd_rr(Xmm::Xmm0, Xmm::Xmm1, 2); // LE: a<=b per lane
1192            asm.movmskpd(Reg::Rax, Xmm::Xmm0);
1193            asm.ret();
1194            let mut frame = [
1195                a0.to_bits() as i64,
1196                a1.to_bits() as i64,
1197                b0.to_bits() as i64,
1198                b1.to_bits() as i64,
1199            ];
1200            assert_eq!(run_frame(&asm.resolve(), &mut frame), want, "a={a0},{a1}");
1201        }
1202    }
1203
1204    #[test]
1205    fn sse_packed_andpd_count_increment_trick() {
1206        // The branchless iteration counter: AND the all-ones-or-zero escape mask
1207        // with a packed 1.0 yields 1.0 on still-active lanes and 0.0 on escaped
1208        // ones; addpd then advances only the active counters. Verify the AND.
1209        let allones = u64::MAX as i64; // all-ones lane (active)
1210        let one = 1.0f64.to_bits() as i64;
1211        let cases: [(i64, i64, f64, f64); 3] = [
1212            (allones, allones, 1.0, 1.0),
1213            (allones, 0, 1.0, 0.0),
1214            (0, allones, 0.0, 1.0),
1215        ];
1216        for (m0, m1, want0, want1) in cases {
1217            let mut asm = Asm::new();
1218            asm.movupd_rm(Xmm::Xmm0, Reg::Rdi, 0); // mask
1219            asm.movupd_rm(Xmm::Xmm1, Reg::Rdi, 16); // {1.0,1.0}
1220            asm.andpd_rr(Xmm::Xmm0, Xmm::Xmm1);
1221            asm.movupd_mr(Reg::Rdi, 32, Xmm::Xmm0);
1222            asm.ret();
1223            let mut frame = [m0, m1, one, one, 0, 0];
1224            run_frame(&asm.resolve(), &mut frame);
1225            assert_eq!(f64::from_bits(frame[4] as u64).to_bits(), want0.to_bits(), "lane0");
1226            assert_eq!(f64::from_bits(frame[5] as u64).to_bits(), want1.to_bits(), "lane1");
1227        }
1228    }
1229
1230    #[test]
1231    fn sse_packed_bitwise_or_andn_xor() {
1232        // orpd / andnpd / xorpd round-trip known bit patterns (mask algebra).
1233        let a = 0xF0F0_F0F0_F0F0_F0F0u64 as i64;
1234        let b = 0x00FF_00FF_00FF_00FFu64 as i64;
1235        let mut asm = Asm::new();
1236        asm.movupd_rm(Xmm::Xmm0, Reg::Rdi, 0); // {a,a}
1237        asm.movupd_rm(Xmm::Xmm1, Reg::Rdi, 16); // {b,b}
1238        asm.movupd_rr(Xmm::Xmm2, Xmm::Xmm0);
1239        asm.movupd_rr(Xmm::Xmm3, Xmm::Xmm0);
1240        asm.orpd_rr(Xmm::Xmm0, Xmm::Xmm1); // a|b
1241        asm.andnpd_rr(Xmm::Xmm2, Xmm::Xmm1); // (!a)&b
1242        asm.xorpd_rr(Xmm::Xmm3, Xmm::Xmm1); // a^b
1243        asm.movupd_mr(Reg::Rdi, 32, Xmm::Xmm0);
1244        asm.movupd_mr(Reg::Rdi, 48, Xmm::Xmm2);
1245        asm.movupd_mr(Reg::Rdi, 64, Xmm::Xmm3);
1246        asm.ret();
1247        let mut frame = [a, a, b, b, 0, 0, 0, 0, 0, 0];
1248        run_frame(&asm.resolve(), &mut frame);
1249        assert_eq!(frame[4] as u64, (a as u64) | (b as u64), "or");
1250        assert_eq!(frame[6] as u64, (!(a as u64)) & (b as u64), "andn");
1251        assert_eq!(frame[8] as u64, (a as u64) ^ (b as u64), "xor");
1252    }
1253
1254    #[test]
1255    fn simd_mandelbrot_pair_matches_scalar_membership_bit_exact() {
1256        // PROOF-OF-ALGORITHM for the mandelbrot SIMD lever: a branchless 2-lane
1257        // kernel runs the EXACT z = z*z + c recurrence of benchmarks/programs/
1258        // mandelbrot/main.lg for two pixels at once, and its in-set bitmask
1259        // (movmskpd of the sticky `active = AND over iters of (mag <= 4)` mask)
1260        // must equal the scalar benchmark's per-pixel membership for EVERY pair.
1261        // This is sound because each lane's float sequence is bit-identical to the
1262        // scalar's, and membership ("never exceeded 4 within 50 iters") is
1263        // unaffected by the scalar's early break (a cleared lane stays cleared).
1264
1265        // The exact scalar benchmark membership (with the early break).
1266        fn inside_bench(cr: f64, ci: f64) -> bool {
1267            let mut zr = 0.0f64;
1268            let mut zi = 0.0f64;
1269            let mut is_inside = true;
1270            let mut iter = 0;
1271            while iter < 50 {
1272                let zr2 = zr * zr - zi * zi + cr;
1273                let zi2 = 2.0 * zr * zi + ci;
1274                zr = zr2;
1275                zi = zi2;
1276                if zr * zr + zi * zi > 4.0 {
1277                    is_inside = false;
1278                    iter = 50;
1279                }
1280                iter += 1;
1281            }
1282            is_inside
1283        }
1284
1285        // The 2-lane SIMD kernel. Frame: [cr0,cr1, ci0,ci1, 4.0,4.0, 2.0,2.0];
1286        // returns movmskpd(active) in rax (bit0=lane0 in-set, bit1=lane1 in-set).
1287        let mut asm = Asm::new();
1288        asm.movupd_rm(Xmm::Xmm0, Reg::Rdi, 0); // cr
1289        asm.movupd_rm(Xmm::Xmm1, Reg::Rdi, 16); // ci
1290        asm.movupd_rm(Xmm::Xmm2, Reg::Rdi, 32); // {4.0,4.0}
1291        asm.movupd_rm(Xmm::Xmm3, Reg::Rdi, 48); // {2.0,2.0}
1292        asm.xorpd_rr(Xmm::Xmm4, Xmm::Xmm4); // zr = 0
1293        asm.xorpd_rr(Xmm::Xmm5, Xmm::Xmm5); // zi = 0
1294        asm.xorpd_rr(Xmm::Xmm6, Xmm::Xmm6);
1295        asm.cmppd_rr(Xmm::Xmm6, Xmm::Xmm6, 0); // active = (0==0) = all-ones
1296        asm.mov_ri(Reg::Rcx, 0); // iter counter
1297        let loop_top = asm.new_label();
1298        asm.bind(loop_top);
1299        // zr2 = zr*zr - zi*zi + cr   (xmm7)
1300        asm.movupd_rr(Xmm::Xmm7, Xmm::Xmm4);
1301        asm.mulpd_rr(Xmm::Xmm7, Xmm::Xmm4); // zr*zr
1302        asm.movupd_rr(Xmm::Xmm8, Xmm::Xmm5);
1303        asm.mulpd_rr(Xmm::Xmm8, Xmm::Xmm5); // zi*zi
1304        asm.subpd_rr(Xmm::Xmm7, Xmm::Xmm8); // zr*zr - zi*zi
1305        asm.addpd_rr(Xmm::Xmm7, Xmm::Xmm0); // + cr
1306        // zi2 = 2.0*zr*zi + ci   (xmm9), parsed (2.0*zr)*zi to match the source
1307        asm.movupd_rr(Xmm::Xmm9, Xmm::Xmm4);
1308        asm.mulpd_rr(Xmm::Xmm9, Xmm::Xmm3); // 2.0*zr
1309        asm.mulpd_rr(Xmm::Xmm9, Xmm::Xmm5); // *zi
1310        asm.addpd_rr(Xmm::Xmm9, Xmm::Xmm1); // + ci
1311        // commit zr=zr2, zi=zi2
1312        asm.movupd_rr(Xmm::Xmm4, Xmm::Xmm7);
1313        asm.movupd_rr(Xmm::Xmm5, Xmm::Xmm9);
1314        // mag = zr*zr + zi*zi   (xmm7)
1315        asm.movupd_rr(Xmm::Xmm7, Xmm::Xmm4);
1316        asm.mulpd_rr(Xmm::Xmm7, Xmm::Xmm4);
1317        asm.movupd_rr(Xmm::Xmm8, Xmm::Xmm5);
1318        asm.mulpd_rr(Xmm::Xmm8, Xmm::Xmm5);
1319        asm.addpd_rr(Xmm::Xmm7, Xmm::Xmm8);
1320        // still_le = mag <= 4 ; active &= still_le
1321        asm.cmppd_rr(Xmm::Xmm7, Xmm::Xmm2, 2);
1322        asm.andpd_rr(Xmm::Xmm6, Xmm::Xmm7);
1323        // Early-out when BOTH lanes have escaped (active == 0): a cleared lane
1324        // stays cleared, so the result is unchanged — but we skip the remaining
1325        // iterations the scalar's per-pixel break would also have skipped. This
1326        // is what makes the kernel a WIN (work = max(iters_a,iters_b) not 50).
1327        let done = asm.new_label();
1328        asm.movmskpd(Reg::Rax, Xmm::Xmm6);
1329        asm.test_rr(Reg::Rax, Reg::Rax);
1330        asm.jcc(Cond::Eq, done); // both escaped -> rax already 0 (correct)
1331        asm.add_ri(Reg::Rcx, 1);
1332        asm.cmp_ri(Reg::Rcx, 50);
1333        asm.jcc(Cond::Lt, loop_top);
1334        asm.bind(done);
1335        asm.ret(); // rax = movmskpd(active): final membership of both lanes
1336        let code = asm.resolve();
1337
1338        // Sweep the benchmark's own pixel grid (n=64) and check every adjacent
1339        // pair — covers deep-inside, escaped, and boundary pixels.
1340        let n = 64i64;
1341        for y in 0..n {
1342            let ci = 2.0 * y as f64 / n as f64 - 1.0;
1343            for x in 0..(n - 1) {
1344                let cr0 = 2.0 * x as f64 / n as f64 - 1.5;
1345                let cr1 = 2.0 * (x + 1) as f64 / n as f64 - 1.5;
1346                let mut frame = [
1347                    cr0.to_bits() as i64,
1348                    cr1.to_bits() as i64,
1349                    ci.to_bits() as i64,
1350                    ci.to_bits() as i64,
1351                    4.0f64.to_bits() as i64,
1352                    4.0f64.to_bits() as i64,
1353                    2.0f64.to_bits() as i64,
1354                    2.0f64.to_bits() as i64,
1355                ];
1356                let got = run_frame(&code, &mut frame);
1357                let want = (inside_bench(cr0, ci) as i64)
1358                    | ((inside_bench(cr1, ci) as i64) << 1);
1359                assert_eq!(got, want, "pixel pair x={x} y={y} cr0={cr0} cr1={cr1} ci={ci}");
1360            }
1361        }
1362    }
1363
1364    #[test]
1365    fn sse_ucomisd_ordering_and_nan() {
1366        // return (a > b) as i64 via ucomisd + seta (Cond::AU). NaN -> 0.
1367        let nan = f64::NAN;
1368        for (a, b, want) in [
1369            (2.0f64, 1.0, 1i64),
1370            (1.0, 2.0, 0),
1371            (3.0, 3.0, 0),
1372            (nan, 1.0, 0),
1373            (1.0, nan, 0),
1374            (nan, nan, 0),
1375        ] {
1376            let mut asm = Asm::new();
1377            asm.movsd_rm(Xmm::Xmm0, Reg::Rdi, 0);
1378            asm.movsd_rm(Xmm::Xmm1, Reg::Rdi, 8);
1379            asm.ucomisd_rr(Xmm::Xmm0, Xmm::Xmm1); // compare a, b
1380            asm.setcc_movzx(Cond::AU, Reg::Rax); // a > b (ordered)
1381            asm.ret();
1382            let mut frame = [a.to_bits() as i64, b.to_bits() as i64];
1383            assert_eq!(run_frame(&asm.resolve(), &mut frame), want, "a={a} b={b}");
1384        }
1385    }
1386
1387    #[test]
1388    fn movq_bridges_gp_and_xmm_bit_exact() {
1389        // GP -> XMM -> GP round-trips the exact bit pattern (incl. NaN/-0.0).
1390        for v in [0.0f64, -0.0, f64::NAN, f64::INFINITY, 1.5, -1e300] {
1391            let mut asm = Asm::new();
1392            asm.mov_rm(Reg::Rax, Reg::Rdi, 0);
1393            asm.movq_xr(Xmm::Xmm3, Reg::Rax);
1394            asm.movq_rx(Reg::Rax, Xmm::Xmm3);
1395            asm.ret();
1396            let mut frame = [v.to_bits() as i64];
1397            assert_eq!(run_frame(&asm.resolve(), &mut frame) as u64, v.to_bits(), "v={v:?}");
1398        }
1399    }
1400
1401    #[test]
1402    fn add_cmp_immediate() {
1403        // return (rdi + 7) then verify cmp_ri sets flags: if (rax cmp 10) >= use jge.
1404        let mut a = Asm::new();
1405        a.mov_rr(Reg::Rax, Reg::Rdi);
1406        a.add_ri(Reg::Rax, 7);
1407        a.ret();
1408        let code = a.resolve();
1409        assert_eq!(run2(&code, 35, 0), 42);
1410        assert_eq!(run2(&code, -7, 0), 0);
1411
1412        // cmp_ri: return (rdi >= 100) as i64.
1413        let mut b = Asm::new();
1414        b.cmp_ri(Reg::Rdi, 100);
1415        b.setcc_movzx(Cond::Ge, Reg::Rax);
1416        b.ret();
1417        let codeb = b.resolve();
1418        assert_eq!(run2(&codeb, 100, 0), 1);
1419        assert_eq!(run2(&codeb, 99, 0), 0);
1420        assert_eq!(run2(&codeb, 200, 0), 1);
1421    }
1422
1423    #[test]
1424    fn indirect_call_through_register() {
1425        // Build a callee that returns rdi*3, then a caller that loads the callee
1426        // address into r11 and `call`s it, returning the result. Verifies the
1427        // FF /2 (call r64) encoding including the REX.B path (r11).
1428        let mut callee = Asm::new();
1429        callee.mov_rr(Reg::Rax, Reg::Rdi);
1430        callee.add_rr(Reg::Rax, Reg::Rdi);
1431        callee.add_rr(Reg::Rax, Reg::Rdi); // rax = 3*rdi
1432        callee.ret();
1433        let callee_page = JitPage::new(&callee.resolve()).expect("map callee");
1434        let callee_addr = callee_page.as_ptr() as i64;
1435
1436        let mut caller = Asm::new();
1437        // Keep the stack 16-aligned at the call: entry rsp ≡ 8 (mod 16); one
1438        // `sub rsp, 8` makes it 16-aligned so the callee's `ret` lands clean.
1439        caller.sub_ri(Reg::Rsp, 8);
1440        caller.mov_ri(Reg::R11, callee_addr);
1441        caller.call_r(Reg::R11); // rax = 3 * rdi (rdi survives: caller-passed arg)
1442        caller.add_ri(Reg::Rsp, 8);
1443        caller.ret();
1444        let code = caller.resolve();
1445        assert_eq!(run2(&code, 14, 0), 42);
1446        assert_eq!(run2(&code, -5, 0), -15);
1447    }
1448
1449    #[test]
1450    fn byte_load_zero_extends() {
1451        // base = rdi (u8*); load byte [rdi + (rsi-1)] zero-extended into rax.
1452        // A SIGN-extending load of 0xFF would give -1; movzx gives 255.
1453        let mut a = Asm::new();
1454        a.mov_rr(Reg::R10, Reg::Rsi);
1455        a.sub_ri(Reg::R10, 1); // im1
1456        a.add_rr(Reg::R10, Reg::Rdi); // addr = base + im1
1457        a.movzx_rm8(Reg::Rax, Reg::R10, 0);
1458        a.ret();
1459        let buf = [0u8, 1, 0xFF, 0x80, 7];
1460        let page = JitPage::new(&a.resolve()).unwrap();
1461        let f: extern "C" fn(*const u8, i64) -> i64 =
1462            unsafe { std::mem::transmute(page.as_ptr()) };
1463        assert_eq!(f(buf.as_ptr(), 1), 0);
1464        assert_eq!(f(buf.as_ptr(), 2), 1);
1465        assert_eq!(f(buf.as_ptr(), 3), 255); // zero-extended, not -1
1466        assert_eq!(f(buf.as_ptr(), 4), 128);
1467        assert_eq!(f(buf.as_ptr(), 5), 7);
1468    }
1469
1470    #[test]
1471    fn byte_store_low_byte_only() {
1472        // base = rdi (u8*); store the low byte of a normalized value (rsi != 0)
1473        // into [rdi]. Exercises setcc8 + mov_mr8. The value 0x1234_5678_FF00_0000
1474        // is nonzero, so setne → 1 → stored byte is 1, NOT 0 (its low byte).
1475        let mut a = Asm::new();
1476        a.test_rr(Reg::Rsi, Reg::Rsi);
1477        a.setcc8(Cond::Ne, Reg::Rdx);
1478        a.mov_mr8(Reg::Rdi, 0, Reg::Rdx);
1479        a.mov_ri(Reg::Rax, 0);
1480        a.ret();
1481        let page = JitPage::new(&a.resolve()).unwrap();
1482        let f: extern "C" fn(*mut u8, i64) -> i64 =
1483            unsafe { std::mem::transmute(page.as_ptr()) };
1484        for (val, want) in [(0i64, 0u8), (1, 1), (256, 1), (-1, 1), (0xFF00_0000i64, 1)] {
1485            let mut cell = [0xAAu8];
1486            f(cell.as_mut_ptr(), val);
1487            assert_eq!(cell[0], want, "val={val}");
1488        }
1489    }
1490
1491    #[test]
1492    fn byte_store_through_rsp_class_base() {
1493        // Exercise mov_mr8's SIB path (base r12, lo3 == 100) and a src whose
1494        // low byte needs the REX prefix only via the high bit (r8b).
1495        let mut a = Asm::new();
1496        a.mov_rr(Reg::R12, Reg::Rdi);
1497        a.mov_ri(Reg::R8, 1);
1498        a.mov_mr8(Reg::R12, 0, Reg::R8);
1499        a.mov_ri(Reg::Rax, 0);
1500        a.ret();
1501        let mut cell = [0u8, 9];
1502        let page = JitPage::new(&a.resolve()).unwrap();
1503        let f: extern "C" fn(*mut u8) -> i64 =
1504            unsafe { std::mem::transmute(page.as_ptr()) };
1505        f(cell.as_mut_ptr());
1506        assert_eq!(cell[0], 1);
1507        assert_eq!(cell[1], 9, "neighbor byte must be untouched (1-byte store)");
1508    }
1509
1510    #[test]
1511    fn byte_store_spl_class_register() {
1512        // mov_mr8 with src = rsi (encoding 6, low byte `sil`): without a REX
1513        // prefix this would encode `dh`, so the need_rex path is mandatory.
1514        let mut a = Asm::new();
1515        a.mov_ri(Reg::Rsi, 0x77);
1516        a.mov_mr8(Reg::Rdi, 0, Reg::Rsi);
1517        a.mov_ri(Reg::Rax, 0);
1518        a.ret();
1519        let mut cell = [0u8];
1520        let page = JitPage::new(&a.resolve()).unwrap();
1521        let f: extern "C" fn(*mut u8) -> i64 =
1522            unsafe { std::mem::transmute(page.as_ptr()) };
1523        f(cell.as_mut_ptr());
1524        assert_eq!(cell[0], 0x77);
1525    }
1526
1527    #[test]
1528    fn computed_address_load_store() {
1529        // base = rdi (i64*); compute addr = base + (rsi-1)*8 in a scratch reg,
1530        // store 0xABCD there, then load it back — mirrors the array path.
1531        let mut a = Asm::new();
1532        a.mov_rr(Reg::R10, Reg::Rsi); // im = idx
1533        a.sub_ri(Reg::R10, 1); // im1 = idx - 1
1534        a.shl_ri(Reg::R10, 3); // byte offset
1535        a.add_rr(Reg::R10, Reg::Rdi); // addr = base + offset
1536        a.mov_ri(Reg::R11, 0xABCD);
1537        a.mov_mr(Reg::R10, 0, Reg::R11); // [addr] = 0xABCD
1538        a.mov_rm(Reg::Rax, Reg::R10, 0); // rax = [addr]
1539        a.ret();
1540        let mut frame = [0i64; 8];
1541        let page = JitPage::new(&a.resolve()).unwrap();
1542        let f: extern "C" fn(*mut i64, i64) -> i64 =
1543            unsafe { std::mem::transmute(page.as_ptr()) };
1544        let r = f(frame.as_mut_ptr(), 3); // index 3 (1-based) → frame[2]
1545        assert_eq!(r, 0xABCD);
1546        assert_eq!(frame[2], 0xABCD);
1547    }
1548}