kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

native.c (187438B)


      1 /* src/arch/x64/native.c — x86-64 (SysV / Win64) NativeTarget implementation.
      2  *
      3  * Mirrors the rv64 reference (src/arch/rv64/native.c): a physical-emission
      4  * NativeTarget driven at -O0 by the shared NativeDirectTarget and at -O1+ by
      5  * the optimizer emit path. ABI decisions route through abi/ and the per-OS
      6  * X64ABIRegs (x64_abi_for_os); this file owns ISA emission and the x64 frame
      7  * layout.
      8  *
      9  * Frame model (single, rbp-anchored): the prologue does `push rbp; mov rbp,rsp;
     10  * sub rsp,frame_size`. Local/spill slots live below rbp at positive byte
     11  * offsets `off` (address = rbp - off). Incoming stack args sit above the saved
     12  * return address at [rbp + 16 + shadow_space + ...]. Callee-saved GPRs (and, on
     13  * Win64, XMMs) are saved below the locals; outgoing args sit at [rsp + 0..].
     14  * The single-pass (-O0) prologue reserves a NOP placeholder patched in func_end
     15  * once max_outgoing and callee-saves are known.
     16  *
     17  * Register model. R10/R11 and XMM14/XMM15 are backend-private hook/asm
     18  * temporaries and are never optimizer operand locations. RSP/RBP are reserved
     19  * stack/frame pointers. RAX is reserved too (return value and div/mul implicit
     20  * operand), but is not in either temporary bank, so inline asm may pin an
     21  * operand to it (the Linux syscall idiom) — see x64_asm_operand_reg_ok. The O1
     22  * instruction-scoped operand bank is R8/R9 (int) and XMM4/XMM5 (fp),
     23  * caller-saved on both SysV and Win64; the O0 direct target happens to use the
     24  * same registers through its independent `scratch` policy. Both banks are
     25  * reserved from persistent allocation.
     26  * Callee-saved set is resolved per-OS via x64_abi_for_os at runtime (the
     27  * legality masks below are SysV's, the conservative superset that both ABIs'
     28  * allocators respect — Win64's extra callee-saves RDI/RSI/xmm6-15 only shrink
     29  * the allocable pool, never grow it). */
     30 
     31 #include <string.h>
     32 
     33 #include "abi/abi.h"
     34 #include "arch/x64/asm.h"
     35 #include "arch/x64/emit.h"
     36 #include "arch/x64/isa.h"
     37 #include "arch/x64/regs.h"
     38 #include "arch/x64/x64.h"
     39 #include "asm/asm.h"
     40 #include "asm/asm_lex.h"
     41 #include "cg/native_argmove.h"
     42 #include "cg/native_asm.h"
     43 #include "cg/native_direct_target.h"
     44 #include "cg/native_frame.h"
     45 #include "cg/type.h"
     46 #include "core/arena.h"
     47 #include "core/bytes.h"
     48 #include "core/pool.h"
     49 #include "core/slice.h"
     50 #include "obj/obj.h"
     51 
     52 enum {
     53   X64_TMP_INT = X64_R10,      /* backend-private int temp (reserved) */
     54   X64_TMP_INT2 = X64_R11,     /* backend-private int temp (reserved) */
     55   X64_TMP_FP = X64_XMM0 + 14, /* backend-private fp temp (reserved) */
     56   X64_TMP_FP2 = X64_XMM15,    /* backend-private fp temp (reserved) */
     57   X64_MAX_REG_ARG_MOVES = 16u,
     58   /* Deferred entry register-binds (-O1): bounded by simultaneously-live
     59    * register-homed param parts, i.e. the allocable register count. */
     60   X64_MAX_BIND_MOVES = 32u,
     61   X64_MAX_CS_FP_REGS = 10u, /* Win64 xmm6..xmm15 */
     62 };
     63 
     64 /* ============================ target state ============================ */
     65 
     66 /* Frame slots and callee-save records live in the shared NativeFrame
     67  * bookkeeping (cg/native_frame.h); these aliases keep the x64-local spellings.
     68  * x64 reads only .reg/.cls of a callee-save (it computes save offsets below the
     69  * locals rather than homing them in frame slots, so .slot/.type stay unused).
     70  */
     71 typedef NativeFrameSlotEntry X64NativeSlot;
     72 typedef NativeFrameCalleeSave X64CalleeSave;
     73 
     74 typedef enum X64PatchKind { X64_PATCH_ALLOCA } X64PatchKind;
     75 
     76 typedef struct X64Patch {
     77   u8 kind; /* X64PatchKind */
     78   u32 pos; /* byte offset of the disp32 to patch */
     79 } X64Patch;
     80 
     81 typedef struct X64NativeTarget {
     82   NativeTarget base;
     83   SrcLoc loc;
     84   const CGFuncDesc* func;
     85 
     86   /* Shared frame bookkeeping: slot table, cum_off, max_outgoing, callee-save
     87    * set, and the known_frame / has_alloca / frame_final flags. */
     88   NativeFrame frame;
     89   u32 frame_size_final;
     90 
     91   u32 incoming_stack_size; /* fixed-param stack bytes (tail-call check) */
     92   u32 next_param_int;
     93   u32 next_param_fp;
     94   u32 next_param_stack;
     95   u8 has_sret;
     96   u8 is_variadic;
     97   NativeFrameSlot sret_ptr_slot;
     98   NativeFrameSlot reg_save_slot; /* SysV variadic 176B __va_list_tag area */
     99 
    100   X64Patch* patches;
    101   u32 npatches;
    102   u32 patches_cap;
    103   u32 nalloca;
    104 
    105   u32 func_start;
    106   u32 prologue_pos;
    107   u32 prologue_nbytes;
    108   MCLabel epilogue_label;
    109 
    110   /* Known-frame (-O1) prologue cost-model tiers, settled in
    111    * x64_func_begin_known_frame; both 0 on the single-pass path (which can't
    112    * know the frame up front). Either one suppresses the `sub rsp` reservation;
    113    * the rbp frame record (push rbp; mov rbp,rsp) and every rbp-relative offset
    114    * stay unchanged, so the epilogue (`leave`), CFI (CFA = rbp+16), and debug
    115    * locs are identical to the fat shape. slim_frame   - empty frame (no
    116    * callee-saves/locals/outgoing/alloca): the `sub rsp` reserved nothing, so it
    117    * is simply dropped. Safe for non-leaves (push rbp keeps rsp 16-aligned for
    118    * calls, and nothing lives below rsp). SysV + Win64. redzone_leaf - SysV leaf
    119    * with a small frame (<= 128B, no alloca, no outgoing args):
    120    * locals/callee-saves stay at their rbp-relative offsets, which now land in
    121    * the 128-byte red zone instead of a reserved region. Leaf-only — a call
    122    * would clobber the red zone. */
    123   u8 slim_frame;
    124   u8 redzone_leaf;
    125 
    126   /* Optimizer (-O1) entry binds: register-destination param binds are deferred
    127    * here and resolved as a parallel copy in x64_bind_params_end, since the
    128    * allocator may rotate params across the incoming arg registers — a
    129    * permutation the naive per-param move order would clobber. */
    130   NativeArgMove bind_moves[X64_MAX_BIND_MOVES];
    131   u32 nbind_moves;
    132 
    133   const X64ABIRegs* abi;
    134 } X64NativeTarget;
    135 
    136 static X64NativeTarget* x64_of(NativeTarget* t) { return (X64NativeTarget*)t; }
    137 
    138 static _Noreturn void x64_panic(X64NativeTarget* a, const char* msg) {
    139   compiler_panic(a->base.c, a->loc, "x64 native target: %s", msg);
    140 }
    141 
    142 static X64NativeSlot* x64_slot_get(X64NativeTarget* a, NativeFrameSlot fs) {
    143   return native_frame_slot_at(&a->frame, fs);
    144 }
    145 
    146 /* ============================ type helpers ============================ */
    147 
    148 /* Scalar size/align/mem/class/loc constructors are shared in native_target.h
    149  * (native_type_size, native_type_align, native_mem_for_type,
    150  * native_class_for_type_fp_le8, native_loc_reg, native_loc_stack,
    151  * native_loc_is_fp). loc_reg's mask is arch-specific and stays here. */
    152 
    153 /* A scalar value occupies a 64-bit register when it is pointer-sized or wider
    154  * (drives REX.W selection). */
    155 static int x64_is_64(NativeTarget* t, KitCgTypeId type) {
    156   return native_type_size(t, type) >= 8u || cg_type_is_ptr(t->c, type);
    157 }
    158 
    159 /* Scalar byte width of a register operand. Reads the NDT-stamped szinfo
    160  * descriptor (one byte) when present, else falls back to the live type query —
    161  * byte-identical to native_type_size for in-range scalars. */
    162 static u32 loc_size32(NativeTarget* t, NativeLoc loc) {
    163   if (loc.szinfo & NATIVE_SZINFO_VALID) return native_szinfo_size(loc.szinfo);
    164   return native_type_size(t, loc.type);
    165 }
    166 
    167 /* Select the physical integer carrier width. Native scalar sizes are
    168  * 1/2/4/8, while a small direct aggregate may legitimately remain packed in a
    169  * register under the -O0 cache; its 5/6/7-byte representation needs a 64-bit
    170  * move even though its semantic size is below eight bytes. */
    171 static int loc_is_64(NativeTarget* t, NativeLoc loc) {
    172   return loc_size32(t, loc) > 4u || cg_type_is_ptr(t->c, loc.type);
    173 }
    174 
    175 static u32 loc_reg(NativeLoc loc) { return loc.v.reg & 0xfu; }
    176 
    177 /* SSE scalar prefix: F2 (double / 8-byte) vs F3 (single / 4-byte). */
    178 static u8 sse_scalar_prefix(u32 size) { return size == 8u ? 0xF2u : 0xF3u; }
    179 
    180 /* Forward decls for the rel32 branch emitters (used by convert before the
    181  * control-flow section defines them). */
    182 static void emit_jmp_rel32(MCEmitter* mc, MCLabel l);
    183 static void emit_jcc_rel32(MCEmitter* mc, u32 cc, MCLabel l);
    184 
    185 /* ============================ register tables ============================ */
    186 
    187 #define X64_PHYS_INT_ARG(r)                                                  \
    188   {.reg = (r),                                                               \
    189    .cls = NATIVE_REG_INT,                                                    \
    190    .abi_index = 0xffu,                                                       \
    191    .flags = NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLER_SAVED | NATIVE_REG_ARG, \
    192    .spill_cost = 1u,                                                         \
    193    .copy_cost = 1u}
    194 #define X64_PHYS_INT_ARG_RESERVED(r)                                        \
    195   {.reg = (r),                                                              \
    196    .cls = NATIVE_REG_INT,                                                   \
    197    .abi_index = 0xffu,                                                      \
    198    .flags = NATIVE_REG_CALLER_SAVED | NATIVE_REG_ARG | NATIVE_REG_RESERVED, \
    199    .spill_cost = 0u,                                                        \
    200    .copy_cost = 0u}
    201 #define X64_PHYS_INT_RET_ARG(r)                                               \
    202   {.reg = (r),                                                                \
    203    .cls = NATIVE_REG_INT,                                                     \
    204    .abi_index = 0xffu,                                                        \
    205    .flags = NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLER_SAVED | NATIVE_REG_ARG | \
    206             NATIVE_REG_RET,                                                   \
    207    .spill_cost = 1u,                                                          \
    208    .copy_cost = 1u}
    209 #define X64_PHYS_INT_CALLER(r)                              \
    210   {.reg = (r),                                              \
    211    .cls = NATIVE_REG_INT,                                   \
    212    .abi_index = 0xffu,                                      \
    213    .flags = NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLER_SAVED, \
    214    .spill_cost = 1u,                                        \
    215    .copy_cost = 1u}
    216 #define X64_PHYS_INT_CALLEE(r)                              \
    217   {.reg = (r),                                              \
    218    .cls = NATIVE_REG_INT,                                   \
    219    .abi_index = 0xffu,                                      \
    220    .flags = NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLEE_SAVED, \
    221    .spill_cost = 4u,                                        \
    222    .copy_cost = 1u}
    223 #define X64_PHYS_INT_RESERVED_ROLE(r, role) \
    224   {.reg = (r),                               \
    225    .cls = NATIVE_REG_INT,                    \
    226    .abi_index = 0xffu,                       \
    227    .flags = NATIVE_REG_RESERVED | (role),    \
    228    .spill_cost = 0u,                         \
    229    .copy_cost = 0u}
    230 #define X64_PHYS_INT_RESERVED(r) \
    231   X64_PHYS_INT_RESERVED_ROLE((r), NATIVE_REG_NONE)
    232 #define X64_PHYS_INT_RESERVED_CALLER(r) \
    233   X64_PHYS_INT_RESERVED_ROLE((r), NATIVE_REG_CALLER_SAVED)
    234 #define X64_PHYS_INT_RESERVED_CALLEE(r) \
    235   X64_PHYS_INT_RESERVED_ROLE((r), NATIVE_REG_CALLEE_SAVED)
    236 
    237 /* The NDT (-O0) value-cache / scratch pool (read only by NativeDirectTarget;
    238  * the optimizer allocates over the phys[] ALLOCABLE flags instead, which also
    239  * include rcx/rdx/rsi/rdi). Caller-saved RSI/RDI lead so the cache prefers them
    240  * (no prologue save) and only spills into callee-saved R13-R15 under pressure
    241  * (the fixed-size save region covers them). RSI/RDI are safe to cache: they have
    242  * no implicit x86 use — unlike RCX (shift count) and RAX/RDX (div/mul), which
    243  * are therefore excluded — and incoming args are spilled to frame homes at entry
    244  * before any body op runs. R8/R9 are also the O1 operand-temp bank; R10/R11 are
    245  * backend-private (reserved); RAX is reserved (return/div-mul, asm-pinnable). */
    246 static const Reg x64_ndt_int_allocable[] = {X64_RSI, X64_RDI, X64_R13,
    247                                             X64_R14, X64_R15};
    248 static const Reg x64_int_scratch[] = {X64_R8, X64_R9};
    249 static const Reg x64_int_asm_temps[] = {X64_TMP_INT, X64_TMP_INT2};
    250 static const Reg x64_direct_asm_int[] = {X64_RDI, X64_RSI, X64_RDX, X64_RCX};
    251 
    252 static const NativePhysRegInfo x64_int_phys[] = {
    253     X64_PHYS_INT_RESERVED_ROLE(
    254         X64_RAX, NATIVE_REG_CALLER_SAVED | NATIVE_REG_RET),
    255     X64_PHYS_INT_ARG(X64_RCX),
    256     X64_PHYS_INT_RET_ARG(X64_RDX),
    257     X64_PHYS_INT_RESERVED_CALLEE(X64_RBX),
    258     X64_PHYS_INT_RESERVED(X64_RSP), /* stack pointer */
    259     X64_PHYS_INT_RESERVED(X64_RBP), /* frame pointer */
    260     X64_PHYS_INT_ARG(X64_RSI),
    261     X64_PHYS_INT_ARG(X64_RDI),
    262     X64_PHYS_INT_ARG_RESERVED(X64_R8), /* O1 operand temp / O0 scratch */
    263     X64_PHYS_INT_ARG_RESERVED(X64_R9), /* O1 operand temp / O0 scratch */
    264     X64_PHYS_INT_RESERVED_CALLER(X64_R10), /* backend-private temp */
    265     X64_PHYS_INT_RESERVED_CALLER(X64_R11), /* backend-private temp */
    266     X64_PHYS_INT_RESERVED_CALLEE(X64_R12),
    267     X64_PHYS_INT_CALLEE(X64_R13),
    268     X64_PHYS_INT_CALLEE(X64_R14),
    269     X64_PHYS_INT_CALLEE(X64_R15),
    270 };
    271 
    272 #define X64_PHYS_FP_ARG_RET(r)                                                \
    273   {.reg = (r),                                                                \
    274    .cls = NATIVE_REG_FP,                                                      \
    275    .abi_index = 0xffu,                                                        \
    276    .flags = NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLER_SAVED | NATIVE_REG_ARG | \
    277             NATIVE_REG_RET,                                                   \
    278    .spill_cost = 1u,                                                          \
    279    .copy_cost = 1u}
    280 #define X64_PHYS_FP_ARG(r)                                                   \
    281   {.reg = (r),                                                               \
    282    .cls = NATIVE_REG_FP,                                                     \
    283    .abi_index = 0xffu,                                                       \
    284    .flags = NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLER_SAVED | NATIVE_REG_ARG, \
    285    .spill_cost = 1u,                                                         \
    286    .copy_cost = 1u}
    287 #define X64_PHYS_FP_ARG_RESERVED(r)                                         \
    288   {.reg = (r),                                                              \
    289    .cls = NATIVE_REG_FP,                                                    \
    290    .abi_index = 0xffu,                                                      \
    291    .flags = NATIVE_REG_CALLER_SAVED | NATIVE_REG_ARG | NATIVE_REG_RESERVED, \
    292    .spill_cost = 0u,                                                        \
    293    .copy_cost = 0u}
    294 #define X64_PHYS_FP_CALLER(r)                               \
    295   {.reg = (r),                                              \
    296    .cls = NATIVE_REG_FP,                                    \
    297    .abi_index = 0xffu,                                      \
    298    .flags = NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLER_SAVED, \
    299    .spill_cost = 1u,                                        \
    300    .copy_cost = 1u}
    301 #define X64_PHYS_FP_RESERVED(r)                  \
    302   {.reg = (r),                                   \
    303    .cls = NATIVE_REG_FP,                         \
    304    .abi_index = 0xffu,                           \
    305    .flags = NATIVE_REG_RESERVED |                \
    306             NATIVE_REG_CALLER_SAVED,             \
    307    .spill_cost = 0u,                             \
    308    .copy_cost = 0u}
    309 
    310 /* Allocable FP pool: arg/ret xmm0..xmm3 lead so -O0 fp-arg producers
    311  * materialize directly into the ABI arg registers (Lever 1), then xmm6..xmm11.
    312  * xmm0/xmm1 are also the fp ret regs but ndt_result_reg_stable is OFF, so the
    313  * fp result still moves out of xmm0 via the post-call path (often a no-op).
    314  * xmm4/xmm5 form the O1 operand-temp bank (and O0 scratch policy);
    315  * xmm14/xmm15 are backend-private. */
    316 static const Reg x64_ndt_fp_allocable[] = {
    317     X64_XMM0,      X64_XMM1,      X64_XMM2,     X64_XMM3,
    318     X64_XMM6,      X64_XMM7,      X64_XMM8,     X64_XMM0 + 9,
    319     X64_XMM0 + 10, X64_XMM0 + 11};
    320 static const Reg x64_fp_scratch[] = {X64_XMM4, X64_XMM5};
    321 static const Reg x64_fp_asm_temps[] = {X64_TMP_FP, X64_TMP_FP2};
    322 static const Reg x64_direct_asm_fp[] = {
    323     X64_XMM0,     X64_XMM1,      X64_XMM2,      X64_XMM3,
    324     X64_XMM6,     X64_XMM7,      X64_XMM8,      X64_XMM0 + 9,
    325     X64_XMM0 + 10, X64_XMM0 + 11};
    326 
    327 static const NativePhysRegInfo x64_fp_phys[] = {
    328     X64_PHYS_FP_ARG_RET(X64_XMM0),       X64_PHYS_FP_ARG_RET(X64_XMM1),
    329     X64_PHYS_FP_ARG(X64_XMM2),           X64_PHYS_FP_ARG(X64_XMM3),
    330     X64_PHYS_FP_ARG_RESERVED(X64_XMM4),  X64_PHYS_FP_ARG_RESERVED(X64_XMM5),
    331     X64_PHYS_FP_ARG(X64_XMM6),           X64_PHYS_FP_ARG(X64_XMM7),
    332     X64_PHYS_FP_CALLER(X64_XMM8),        X64_PHYS_FP_CALLER(X64_XMM0 + 9),
    333     X64_PHYS_FP_CALLER(X64_XMM0 + 10),   X64_PHYS_FP_CALLER(X64_XMM0 + 11),
    334     X64_PHYS_FP_RESERVED(X64_XMM0 + 12), X64_PHYS_FP_RESERVED(X64_XMM0 + 13),
    335     X64_PHYS_FP_RESERVED(X64_XMM0 + 14), /* backend-private temp */
    336     X64_PHYS_FP_RESERVED(X64_XMM15),     /* backend-private temp */
    337 };
    338 
    339 static const NativeAllocClassInfo x64_classes[] = {
    340     {.cls = NATIVE_REG_INT,
    341      .ndt_allocable = x64_ndt_int_allocable,
    342      .ndt_allocable_count =
    343          sizeof x64_ndt_int_allocable / sizeof x64_ndt_int_allocable[0],
    344      .scratch = x64_int_scratch,
    345      .nscratch = sizeof x64_int_scratch / sizeof x64_int_scratch[0],
    346      .emit_temps = x64_int_scratch,
    347      .nemit_temps = sizeof x64_int_scratch / sizeof x64_int_scratch[0],
    348      .asm_temps = x64_int_asm_temps,
    349      .nasm_temps = sizeof x64_int_asm_temps / sizeof x64_int_asm_temps[0],
    350      .direct_asm_allocable = x64_direct_asm_int,
    351      .ndirect_asm_allocable =
    352          sizeof x64_direct_asm_int / sizeof x64_direct_asm_int[0],
    353      .emit_cache_mask = (1u << X64_R8) | (1u << X64_R9),
    354      .phys = x64_int_phys,
    355      .nphys = sizeof x64_int_phys / sizeof x64_int_phys[0]},
    356     {.cls = NATIVE_REG_FP,
    357      .ndt_allocable = x64_ndt_fp_allocable,
    358      .ndt_allocable_count =
    359          sizeof x64_ndt_fp_allocable / sizeof x64_ndt_fp_allocable[0],
    360      .scratch = x64_fp_scratch,
    361      .nscratch = sizeof x64_fp_scratch / sizeof x64_fp_scratch[0],
    362      .emit_temps = x64_fp_scratch,
    363      .nemit_temps = sizeof x64_fp_scratch / sizeof x64_fp_scratch[0],
    364      .asm_temps = x64_fp_asm_temps,
    365      .nasm_temps = sizeof x64_fp_asm_temps / sizeof x64_fp_asm_temps[0],
    366      .direct_asm_allocable = x64_direct_asm_fp,
    367      .ndirect_asm_allocable =
    368          sizeof x64_direct_asm_fp / sizeof x64_direct_asm_fp[0],
    369      .emit_cache_mask = (1u << X64_XMM4) | (1u << X64_XMM5),
    370      .phys = x64_fp_phys,
    371      .nphys = sizeof x64_fp_phys / sizeof x64_fp_phys[0]},
    372 };
    373 
    374 /* Resolve a register name ("r10", "xmm3", ...) to its (class, Reg). Powers the
    375  * optimizer's inline-asm clobber masks and explicit hard-register operands
    376  * ("{r10}" from a GNU local register variable). GPR names map through the HW
    377  * encoding; xmm names through the DWARF index table. Returns non-zero for a
    378  * non-register name (cc/memory/unknown), which the caller skips. */
    379 static int x64_resolve_name(const NativeRegInfo* ri, Slice name, Reg* out,
    380                             NativeAllocClass* cls_out) {
    381   char buf[16];
    382   uint32_t idx;
    383   (void)ri;
    384   if (!name.s || !name.len || name.len >= sizeof buf) return 1;
    385   memcpy(buf, name.s, name.len);
    386   buf[name.len] = '\0';
    387   if (x64_register_hw_index(buf, &idx) == 0 && idx <= 15u) {
    388     *cls_out = NATIVE_REG_INT;
    389     *out = (Reg)idx;
    390     return 0;
    391   }
    392   if (x64_register_index(buf, &idx) == 0 && idx >= 17u && idx <= 32u) {
    393     *cls_out = NATIVE_REG_FP;
    394     *out = (Reg)(idx - 17u);
    395     return 0;
    396   }
    397   return 1;
    398 }
    399 
    400 static int x64_asm_operand_reg_ok(const NativeRegInfo* ri, NativeAllocClass cls,
    401                                   Reg reg) {
    402   (void)ri;
    403   if (cls == NATIVE_REG_INT) {
    404     switch (reg) {
    405       /* RAX is reserved but not an emitter temp, so it is a legal asm pin (the
    406        * Linux syscall number/return register). R8/R9 are optimizer operand
    407        * temps and R10/R11 backend/asm temps, so those stay excluded. */
    408       case X64_RAX:
    409       case X64_RBX:
    410       case X64_RCX:
    411       case X64_RDX:
    412       case X64_RSI:
    413       case X64_RDI:
    414       case X64_R12:
    415       case X64_R13:
    416       case X64_R14:
    417       case X64_R15:
    418         return 1;
    419       default:
    420         return 0;
    421     }
    422   }
    423   if (cls == NATIVE_REG_FP)
    424     return reg <= X64_XMM0 + 13u && reg != X64_XMM4 && reg != X64_XMM5;
    425   return 0;
    426 }
    427 
    428 static int x64_asm_constraint_reg(const NativeRegInfo* ri, const char* body,
    429                                   NativeAllocClass* cls_out, Reg* fixed_out,
    430                                   u32* allowed_mask_out) {
    431   (void)ri;
    432   if (!body || !body[0] || body[1]) return 0;
    433   if (fixed_out) *fixed_out = REG_NONE;
    434   if (allowed_mask_out) *allowed_mask_out = 0;
    435   switch (body[0]) {
    436     case 'r':
    437     case 'q':
    438       if (cls_out) *cls_out = NATIVE_REG_INT;
    439       return 1;
    440     case 'a':
    441       if (cls_out) *cls_out = NATIVE_REG_INT;
    442       if (fixed_out) *fixed_out = X64_RAX;
    443       return 1;
    444     case 'b':
    445       if (cls_out) *cls_out = NATIVE_REG_INT;
    446       if (fixed_out) *fixed_out = X64_RBX;
    447       return 1;
    448     case 'c':
    449       if (cls_out) *cls_out = NATIVE_REG_INT;
    450       if (fixed_out) *fixed_out = X64_RCX;
    451       return 1;
    452     case 'd':
    453       if (cls_out) *cls_out = NATIVE_REG_INT;
    454       if (fixed_out) *fixed_out = X64_RDX;
    455       return 1;
    456     case 'S':
    457       if (cls_out) *cls_out = NATIVE_REG_INT;
    458       if (fixed_out) *fixed_out = X64_RSI;
    459       return 1;
    460     case 'D':
    461       if (cls_out) *cls_out = NATIVE_REG_INT;
    462       if (fixed_out) *fixed_out = X64_RDI;
    463       return 1;
    464     case 'x':
    465     case 'v':
    466       if (cls_out) *cls_out = NATIVE_REG_FP;
    467       return 1;
    468     default:
    469       return 0;
    470   }
    471 }
    472 
    473 static const NativeRegInfo x64_reg_info = {
    474     .classes = x64_classes,
    475     .nclasses = sizeof x64_classes / sizeof x64_classes[0],
    476     .resolve_name = x64_resolve_name,
    477     .asm_operand_reg_ok = x64_asm_operand_reg_ok,
    478     .asm_constraint_reg = x64_asm_constraint_reg,
    479 };
    480 
    481 /* ============================ legality ============================ */
    482 
    483 static int x64_imm_legal(NativeTarget* t, NativeImmUse use, u32 op,
    484                          KitCgTypeId type, i64 imm) {
    485   (void)t;
    486   (void)type;
    487   switch (use) {
    488     case NATIVE_IMM_MOVE:
    489       return 1;
    490     case NATIVE_IMM_BINOP:
    491       switch ((BinOp)op) {
    492         case BO_IADD:
    493         case BO_ISUB:
    494         case BO_AND:
    495         case BO_OR:
    496         case BO_XOR:
    497         case BO_IMUL:
    498           return imm_fits_i32(imm);
    499         case BO_SHL:
    500         case BO_SHR_S:
    501         case BO_SHR_U:
    502           return imm >= 0 && imm <= 63;
    503         default:
    504           return 0;
    505       }
    506     case NATIVE_IMM_CMP:
    507       return imm_fits_i32(imm);
    508     case NATIVE_IMM_ADDR_OFFSET:
    509       return imm_fits_i32(imm);
    510   }
    511   return 0;
    512 }
    513 
    514 static int x64_addr_legal(NativeTarget* t, const NativeAddr* addr,
    515                           MemAccess mem) {
    516   (void)t;
    517   (void)mem;
    518   if (!addr) return 0;
    519   if (addr->base_kind != NATIVE_ADDR_BASE_REG &&
    520       addr->base_kind != NATIVE_ADDR_BASE_FRAME)
    521     return 0;
    522   /* x64 supports [base + index*scale + disp32]; index must be a register. */
    523   if (addr->index_kind != NATIVE_ADDR_INDEX_NONE &&
    524       addr->index_kind != NATIVE_ADDR_INDEX_REG)
    525     return 0;
    526   return imm_fits_i32(addr->offset);
    527 }
    528 
    529 /* ============================ globals / addresses ============================
    530  */
    531 
    532 static int x64_use_got_for_sym(NativeTarget* t, ObjSymId sym) {
    533   return obj_symbol_extern_via_got(t->c, t->obj, sym);
    534 }
    535 
    536 /* PC-relative reloc kind for a non-GOT &sym reference. Functions use PLT32 so
    537  * the linker can route through a PLT; data uses plain PC32. */
    538 static u32 x64_pcrel_reloc_for_sym(NativeTarget* t, ObjSymId sym) {
    539   const ObjSym* s = obj_symbol_get(t->obj, sym);
    540   if (s && (s->kind == SK_FUNC || s->kind == SK_IFUNC)) return R_X64_PLT32;
    541   return R_PC32;
    542 }
    543 
    544 /* Materialize &sym + addend into dst_reg. Local/static-link symbols use
    545  * `lea rd, [rip + disp32]`; GOT-routed externs use `mov rd, [rip + GOT]` then
    546  * add any nonzero addend. */
    547 static void x64_emit_global_lea(NativeTarget* t, u32 dst_reg, ObjSymId sym,
    548                                 i64 addend) {
    549   MCEmitter* mc = t->mc;
    550   u32 sec = mc->section_id;
    551   if (x64_use_got_for_sym(t, sym)) {
    552     u8 op;
    553     u32 disp_pos;
    554     emit_rex(mc, 1, dst_reg, 0, 0);
    555     op = X64_OPC_MOV_R_RM;
    556     mc_emit_bytes(mc, &op, 1);
    557     {
    558       u8 mr = modrm(0u, dst_reg & 7u, 5u); /* [rip + disp32] */
    559       mc_emit_bytes(mc, &mr, 1);
    560     }
    561     disp_pos = mc_pos(mc);
    562     emit_u32le(mc, 0);
    563     mc_emit_reloc_at(mc, sec, disp_pos, R_X64_REX_GOTPCRELX, sym, -4, 1, 0);
    564     if (addend) {
    565       i32 a = (i32)addend;
    566       emit_rex(mc, 1, 0, 0, dst_reg);
    567       if (imm_fits_i8(a)) {
    568         u8 buf[3] = {X64_OPC_ALU_IMM8, modrm(3u, X64_ALU_SUB_ADD, dst_reg & 7u),
    569                      (u8)a};
    570         mc_emit_bytes(mc, buf, 3);
    571       } else {
    572         u8 buf[2] = {X64_OPC_ALU_IMM32,
    573                      modrm(3u, X64_ALU_SUB_ADD, dst_reg & 7u)};
    574         mc_emit_bytes(mc, buf, 2);
    575         emit_u32le(mc, (u32)a);
    576       }
    577     }
    578     return;
    579   }
    580   {
    581     u8 op = X64_OPC_LEA;
    582     u32 disp_pos;
    583     emit_rex(mc, 1, dst_reg, 0, 0);
    584     mc_emit_bytes(mc, &op, 1);
    585     {
    586       u8 mr = modrm(0u, dst_reg & 7u, 5u); /* [rip + disp32] */
    587       mc_emit_bytes(mc, &mr, 1);
    588     }
    589     disp_pos = mc_pos(mc);
    590     emit_u32le(mc, 0);
    591     mc_emit_reloc_at(mc, sec, disp_pos, x64_pcrel_reloc_for_sym(t, sym), sym,
    592                       addend - 4, 1, 0);
    593   }
    594 }
    595 
    596 /* Resolve a NativeAddr to (base, index, log2_scale, off). Plain FRAME bases
    597  * remain rbp-relative. GLOBAL and FRAME_VALUE components use backend-private
    598  * fixed temps; each FRAME_VALUE load uses its exact base_type/index_type. */
    599 static u32 x64_resolve_addr(X64NativeTarget* a, const NativeAddr* addr,
    600                             u32 scratch, u32* idx_out, u32* scale_out,
    601                             i32* off_out) {
    602   NativeTarget* t = &a->base;
    603   u32 base;
    604   i32 off;
    605   switch (addr->base_kind) {
    606     case NATIVE_ADDR_BASE_REG:
    607       base = addr->base.reg & 0xfu;
    608       off = addr->offset;
    609       break;
    610     case NATIVE_ADDR_BASE_FRAME: {
    611       X64NativeSlot* s = x64_slot_get(a, addr->base.frame);
    612       base = X64_RBP;
    613       off = -(i32)s->off + addr->offset;
    614       break;
    615     }
    616     case NATIVE_ADDR_BASE_FRAME_VALUE: {
    617       X64NativeSlot* s = x64_slot_get(a, addr->base.frame);
    618       u32 size;
    619       if (!addr->base_type) x64_panic(a, "frame-value base has no exact type");
    620       size = native_type_size(t, addr->base_type);
    621       emit_mov_load(t->mc, size, 0, scratch, X64_RBP, -(i32)s->off);
    622       base = scratch;
    623       off = addr->offset;
    624       break;
    625     }
    626     case NATIVE_ADDR_BASE_GLOBAL:
    627       x64_emit_global_lea(t, scratch, addr->base.global.sym,
    628                           addr->base.global.addend);
    629       base = scratch;
    630       off = addr->offset;
    631       break;
    632     default:
    633       x64_panic(a, "unsupported address base");
    634   }
    635   if (addr->index_kind == NATIVE_ADDR_INDEX_REG) {
    636     *idx_out = addr->index.reg & 0xfu;
    637     *scale_out = addr->log2_scale;
    638   } else if (addr->index_kind == NATIVE_ADDR_INDEX_FRAME_VALUE) {
    639     X64NativeSlot* s = x64_slot_get(a, addr->index.frame);
    640     u32 size;
    641     u32 index_tmp = base == X64_TMP_INT2 ? X64_TMP_INT : X64_TMP_INT2;
    642     if (!addr->index_type)
    643       x64_panic(a, "frame-value index has no exact type");
    644     size = native_type_size(t, addr->index_type);
    645     emit_mov_load(t->mc, size, 0, index_tmp, X64_RBP, -(i32)s->off);
    646     *idx_out = index_tmp;
    647     *scale_out = addr->log2_scale;
    648   } else {
    649     *idx_out = REG_NONE;
    650     *scale_out = 0;
    651   }
    652   *off_out = off;
    653   return base;
    654 }
    655 
    656 /* ============================ memory ============================ */
    657 
    658 static int x64_native_part_width(u32 size);
    659 static u32 x64_addr_to_base_reg(X64NativeTarget* a, NativeAddr addr,
    660                                 u32 scratch);
    661 
    662 /* Central load/store primitive. is_load: 1 load into reg, 0 store reg to mem.
    663  * Materializes the address through X64_TMP_INT2 (r11) for non-reg bases. */
    664 static void x64_emit_mem(X64NativeTarget* a, int is_load, NativeLoc reg,
    665                          NativeAddr addr, MemAccess mem) {
    666   NativeTarget* t = &a->base;
    667   MCEmitter* mc = t->mc;
    668   u32 r = loc_reg(reg);
    669   int fp = native_loc_is_fp(reg);
    670   u32 sz = mem.size ? mem.size : loc_size32(t, reg);
    671   u32 base, idx, scale;
    672   i32 off;
    673 
    674   if (!fp && !x64_native_part_width(sz))
    675     x64_panic(a, "raw integer memory width is not native");
    676 
    677   /* Global base: fold into a single rip-relative access when local. */
    678   if (addr.base_kind == NATIVE_ADDR_BASE_GLOBAL &&
    679       addr.index_kind == NATIVE_ADDR_INDEX_NONE &&
    680       !x64_use_got_for_sym(t, addr.base.global.sym)) {
    681     ObjSymId sym = addr.base.global.sym;
    682     i64 ad = addr.base.global.addend + addr.offset;
    683     u32 sec = mc->section_id;
    684     u32 disp_pos;
    685     if (fp) {
    686       u8 prefix = sse_scalar_prefix(sz);
    687       mc_emit_bytes(mc, &prefix, 1);
    688       emit_rex(mc, 0, r, 0, 0);
    689       {
    690         u8 op2[2] = {X64_OPC_TWOBYTE, (u8)(is_load ? 0x10u : 0x11u)};
    691         mc_emit_bytes(mc, op2, 2);
    692       }
    693     } else if (sz == 8 || sz == 4) {
    694       emit_rex(mc, sz == 8, r, 0, 0);
    695       {
    696         u8 op = is_load ? X64_OPC_MOV_R_RM : X64_OPC_MOV_RM_R;
    697         mc_emit_bytes(mc, &op, 1);
    698       }
    699     } else if (sz == 2) {
    700       if (is_load) {
    701         emit_rex(mc, 0, r, 0, 0);
    702         {
    703           u8 op2[2] = {X64_OPC_TWOBYTE, X64_OPC_MOVZX_W};
    704           mc_emit_bytes(mc, op2, 2);
    705         }
    706       } else {
    707         u8 p = X64_OPSIZE_PFX;
    708         mc_emit_bytes(mc, &p, 1);
    709         emit_rex(mc, 0, r, 0, 0);
    710         {
    711           u8 op = X64_OPC_MOV_RM_R;
    712           mc_emit_bytes(mc, &op, 1);
    713         }
    714       }
    715     } else { /* size 1 */
    716       if (is_load) {
    717         emit_rex(mc, 0, r, 0, 0);
    718         {
    719           u8 op2[2] = {X64_OPC_TWOBYTE, X64_OPC_MOVZX_B};
    720           mc_emit_bytes(mc, op2, 2);
    721         }
    722       } else {
    723         emit_rex_force(mc, 0, r, 0, 0);
    724         {
    725           u8 op = X64_OPC_MOV_RM_R8;
    726           mc_emit_bytes(mc, &op, 1);
    727         }
    728       }
    729     }
    730     {
    731       u8 mr = modrm(0u, r & 7u, 5u);
    732       mc_emit_bytes(mc, &mr, 1);
    733     }
    734     disp_pos = mc_pos(mc);
    735     emit_u32le(mc, 0);
    736     mc_emit_reloc_at(mc, sec, disp_pos, x64_pcrel_reloc_for_sym(t, sym), sym,
    737                       ad - 4, 1, 0);
    738     return;
    739   }
    740 
    741   base = x64_resolve_addr(a, &addr, X64_TMP_INT2, &idx, &scale, &off);
    742   if (fp) {
    743     u8 prefix = sse_scalar_prefix(sz);
    744     if (is_load)
    745       emit_sse_load_idx(mc, prefix, 0x10, r, base, idx, scale, off);
    746     else
    747       emit_sse_store_idx(mc, prefix, 0x11, r, base, idx, scale, off);
    748   } else if (is_load) {
    749     /* Loads narrower than 4 bytes zero-extend (sign-extension is applied by a
    750      * later CV_SEXT). */
    751     emit_mov_load_idx(mc, sz, 0, r, base, idx, scale, off);
    752   } else {
    753     emit_mov_store_idx(mc, sz, r, base, idx, scale, off);
    754   }
    755 }
    756 
    757 static int x64_native_part_width(u32 size) {
    758   return size == 1u || size == 2u || size == 4u || size == 8u;
    759 }
    760 
    761 /* The two integer registers in the backend-private bank. Exact-width helpers
    762  * assign them explicit address/data roles for their complete local phase. */
    763 static int x64_private_int_reg(Reg reg) {
    764   return reg == X64_TMP_INT || reg == X64_TMP_INT2;
    765 }
    766 
    767 static Reg x64_other_private_int(Reg reg) {
    768   return reg == X64_TMP_INT ? X64_TMP_INT2 : X64_TMP_INT;
    769 }
    770 
    771 /* Resolve the complete effective address into one explicitly selected private
    772  * register. Exact-width multi-chunk operations keep this address stable while
    773  * their other private register carries chunk data; callers no longer depend on
    774  * x64_emit_mem's internal address-scratch choice. */
    775 static NativeAddr x64_stabilize_part_addr(X64NativeTarget* a, NativeAddr addr,
    776                                           Reg addr_reg) {
    777   u32 base;
    778   KitCgTypeId base_type;
    779   base_type = addr.base_type;
    780   base = x64_addr_to_base_reg(a, addr, addr_reg);
    781   if (base != addr_reg) emit_mov_rr(a->base.mc, 1, addr_reg, base);
    782   memset(&addr, 0, sizeof addr);
    783   addr.base_kind = NATIVE_ADDR_BASE_REG;
    784   addr.cls = NATIVE_REG_INT;
    785   addr.base.reg = addr_reg;
    786   addr.base_type = base_type;
    787   return addr;
    788 }
    789 
    790 /* Load an exact-width little-endian integer ABI part. x86 has native memory
    791  * operations only for 1/2/4/8 bytes, so a 3/5/6/7-byte tail is assembled from
    792  * bounded chunks in the low bits of dst. No chunk crosses the semantic part. */
    793 static void x64_load_int_part(X64NativeTarget* a, NativeLoc dst,
    794                               NativeAddr addr, MemAccess mem, u32 size) {
    795   NativePartChunkIter chunks;
    796   NativeTarget* t = &a->base;
    797   Reg rd = (Reg)loc_reg(dst);
    798   Reg addr_reg;
    799   Reg data_reg;
    800   NativeLoc tmp;
    801   u32 chunk_off, chunk_size;
    802   int private_dst;
    803   int first = 1;
    804 
    805   if (x64_native_part_width(size)) {
    806     mem.size = size;
    807     x64_emit_mem(a, 1, dst, addr, mem);
    808     return;
    809   }
    810   if (size == 0u || size > 8u || native_loc_is_fp(dst))
    811     x64_panic(a, "invalid exact-width integer part load");
    812 
    813   private_dst = x64_private_int_reg(rd);
    814   addr_reg = private_dst ? x64_other_private_int(rd) : X64_TMP_INT2;
    815   data_reg = private_dst ? addr_reg : X64_TMP_INT;
    816   addr = x64_stabilize_part_addr(a, addr, addr_reg);
    817   /* A private destination consumes both integer temps (one accumulator, one
    818    * chunk destination). Keep the stable address in the private FP hold register
    819    * and restore it immediately before each alias-safe memory load. */
    820   if (private_dst)
    821     emit_sse_rr_w(t->mc, 0x66, 0x6E, 1, X64_TMP_FP, addr_reg);
    822   tmp = native_loc_reg(builtin_id(KIT_CG_BUILTIN_I64), NATIVE_REG_INT,
    823                        data_reg);
    824   chunks = native_part_chunks(size, 8u);
    825   while (native_part_chunk_next(&chunks, &chunk_off, &chunk_size)) {
    826     NativeAddr chunk_addr = addr;
    827     NativeLoc chunk_dst = first ? dst : tmp;
    828     chunk_addr.offset += (i32)chunk_off;
    829     mem.size = chunk_size;
    830     if (private_dst)
    831       emit_sse_rr_w(t->mc, 0x66, 0x7E, 1, X64_TMP_FP, addr_reg);
    832     x64_emit_mem(a, 1, chunk_dst, chunk_addr, mem);
    833     if (!first) {
    834       emit_shift_imm(t->mc, 1, X64_SHIFT_SUB_SHL, data_reg,
    835                      (u8)(chunk_off * 8u));
    836       emit_alu_rr(t->mc, 1, X64_OPC_ALU_OR, rd, data_reg);
    837     }
    838     first = 0;
    839   }
    840 }
    841 
    842 /* Store exactly the semantic bytes carried in an integer ABI register. The
    843  * first chunk uses the source directly; later chunks use a shifted private
    844  * copy, preserving both the source register and adjacent destination bytes. */
    845 static void x64_store_int_part(X64NativeTarget* a, NativeAddr addr,
    846                                NativeLoc src, MemAccess mem, u32 size) {
    847   NativePartChunkIter chunks;
    848   NativeTarget* t = &a->base;
    849   Reg rs = (Reg)loc_reg(src);
    850   Reg addr_reg;
    851   Reg data_reg;
    852   NativeLoc tmp;
    853   u32 chunk_off, chunk_size, shifted = 0;
    854   int private_src;
    855   int copied = 0;
    856 
    857   if (x64_native_part_width(size)) {
    858     mem.size = size;
    859     x64_emit_mem(a, 0, src, addr, mem);
    860     return;
    861   }
    862   if (size == 0u || size > 8u || native_loc_is_fp(src))
    863     x64_panic(a, "invalid exact-width integer part store");
    864 
    865   private_src = x64_private_int_reg(rs);
    866   addr_reg = private_src ? x64_other_private_int(rs) : X64_TMP_INT2;
    867   data_reg = private_src ? rs : X64_TMP_INT;
    868   /* Preserve a backend-private source while complete address stabilization is
    869    * free to use both integer temps. Restore it after the stores as well: these
    870    * helpers promise not to consume their source even when another backend
    871    * marshalling phase supplied it from the private bank. */
    872   if (private_src)
    873     emit_sse_rr_w(t->mc, 0x66, 0x6E, 1, X64_TMP_FP, rs);
    874   addr = x64_stabilize_part_addr(a, addr, addr_reg);
    875   if (private_src)
    876     emit_sse_rr_w(t->mc, 0x66, 0x7E, 1, X64_TMP_FP, data_reg);
    877   tmp = native_loc_reg(builtin_id(KIT_CG_BUILTIN_I64), NATIVE_REG_INT,
    878                        data_reg);
    879   chunks = native_part_chunks(size, 8u);
    880   while (native_part_chunk_next(&chunks, &chunk_off, &chunk_size)) {
    881     NativeAddr chunk_addr = addr;
    882     NativeLoc chunk_src = src;
    883     chunk_addr.offset += (i32)chunk_off;
    884     if (chunk_off != 0u) {
    885       if (!copied) {
    886         emit_mov_rr(t->mc, 1, data_reg, rs);
    887         copied = 1;
    888       }
    889       emit_shift_imm(t->mc, 1, X64_SHIFT_SUB_SHR, data_reg,
    890                      (u8)((chunk_off - shifted) * 8u));
    891       shifted = chunk_off;
    892       chunk_src = tmp;
    893     }
    894     mem.size = chunk_size;
    895     x64_emit_mem(a, 0, chunk_src, chunk_addr, mem);
    896   }
    897   if (private_src)
    898     emit_sse_rr_w(t->mc, 0x66, 0x7E, 1, X64_TMP_FP, rs);
    899 }
    900 
    901 /* ============================ moves / data ============================ */
    902 
    903 static void x64_move(NativeTarget* t, NativeLoc dst, NativeLoc src) {
    904   MCEmitter* mc = t->mc;
    905   int dfp = native_loc_is_fp(dst), sfp = native_loc_is_fp(src);
    906   u32 rd = loc_reg(dst), rs = loc_reg(src);
    907   if (dfp && sfp) {
    908     if (rd == rs) return;
    909     emit_sse_rr(mc, sse_scalar_prefix(loc_size32(t, dst)), 0x10, rd,
    910                 rs);
    911     return;
    912   }
    913   if (dfp && !sfp) { /* movd/movq gpr -> xmm: 66 0F 6E /r */
    914     int w = loc_size32(t, dst) == 8u;
    915     emit_sse_rr_w(mc, 0x66, 0x6E, w, rd, rs);
    916     return;
    917   }
    918   if (!dfp && sfp) { /* movd/movq xmm -> gpr: 66 0F 7E /r (xmm is reg field) */
    919     int w = loc_size32(t, src) == 8u;
    920     emit_sse_rr_w(mc, 0x66, 0x7E, w, rs, rd);
    921     return;
    922   }
    923   if (rd == rs) return;
    924   emit_mov_rr(mc, loc_is_64(t, dst) ? 1 : 0, rd, rs);
    925 }
    926 
    927 /* Preserve an indirect call target below rsp while its arguments are
    928  * marshalled. CFA is rbp-based, so this transient balanced pair needs no CFI
    929  * adjustment. High registers use the PUSH/POP opcode's REX.B extension. */
    930 static void x64_push_reg(MCEmitter* mc, Reg reg) {
    931   if (reg & 8u) emit1(mc, X64_REX_BASE | X64_REX_B);
    932   emit1(mc, X64_OPC_PUSH_R | (reg & 7u));
    933 }
    934 
    935 static void x64_pop_reg(MCEmitter* mc, Reg reg) {
    936   if (reg & 8u) emit1(mc, X64_REX_BASE | X64_REX_B);
    937   emit1(mc, X64_OPC_POP_R | (reg & 7u));
    938 }
    939 
    940 static void x64_load_imm(NativeTarget* t, NativeLoc dst, i64 imm) {
    941   x64_emit_load_imm(t->mc, loc_is_64(t, dst) ? 1 : 0, loc_reg(dst), imm);
    942 }
    943 
    944 /* FP constant: materialize the bit pattern in a GPR scratch, then movd/movq
    945  * into the FPR. Integer constant: plain load_imm. */
    946 static void x64_load_const(NativeTarget* t, NativeLoc dst, ConstBytes cb) {
    947   u64 v = 0;
    948   u32 i;
    949   for (i = 0; i < cb.size && i < 8u; ++i) v |= (u64)cb.bytes[i] << (i * 8u);
    950   if (!native_loc_is_fp(dst)) {
    951     x64_load_imm(t, dst, (i64)v);
    952     return;
    953   }
    954   x64_emit_load_imm(t->mc, cb.size == 8u, X64_TMP_INT, (i64)v);
    955   emit_sse_rr_w(t->mc, 0x66, 0x6E, cb.size == 8u, loc_reg(dst), X64_TMP_INT);
    956 }
    957 
    958 static void x64_load_addr(NativeTarget* t, NativeLoc dst, NativeAddr addr) {
    959   X64NativeTarget* a = x64_of(t);
    960   MCEmitter* mc = t->mc;
    961   u32 rd = loc_reg(dst);
    962   u32 base, idx, scale;
    963   i32 off;
    964   if (addr.base_kind == NATIVE_ADDR_BASE_GLOBAL &&
    965       addr.index_kind == NATIVE_ADDR_INDEX_NONE) {
    966     x64_emit_global_lea(t, rd, addr.base.global.sym,
    967                         addr.base.global.addend + addr.offset);
    968     return;
    969   }
    970   base = x64_resolve_addr(a, &addr, rd, &idx, &scale, &off);
    971   if (idx == REG_NONE) {
    972     if (base == rd && off == 0) return; /* already &slot in rd */
    973     emit_lea(mc, rd, base, off);
    974     return;
    975   }
    976   /* lea rd, [base + idx*scale + off] */
    977   {
    978     u8 buf[16];
    979     u32 n = 0;
    980     n += x64_pack_rex(buf + n, 1, rd, idx, base);
    981     buf[n++] = X64_OPC_LEA;
    982     n += x64_pack_mem_sib(buf + n, rd, base, idx, scale, off);
    983     mc_emit_bytes(mc, buf, n);
    984   }
    985 }
    986 
    987 static void x64_load(NativeTarget* t, NativeLoc dst, NativeAddr addr,
    988                      MemAccess mem) {
    989   u32 size = mem.size ? mem.size : loc_size32(t, dst);
    990   if (!x64_native_part_width(size) && size <= 8u)
    991     x64_load_int_part(x64_of(t), dst, addr, mem, size);
    992   else
    993     x64_emit_mem(x64_of(t), 1, dst, addr, mem);
    994 }
    995 static void x64_store(NativeTarget* t, NativeAddr addr, NativeLoc src,
    996                       MemAccess mem) {
    997   u32 size = mem.size ? mem.size : loc_size32(t, src);
    998   if (!x64_native_part_width(size) && size <= 8u)
    999     x64_store_int_part(x64_of(t), addr, src, mem, size);
   1000   else
   1001     x64_emit_mem(x64_of(t), 0, src, addr, mem);
   1002 }
   1003 
   1004 /* Resolve an addressable NativeAddr to a bare base register (no index, off 0)
   1005  * by emitting an lea into `scratch` when needed. */
   1006 static u32 x64_addr_to_base_reg(X64NativeTarget* a, NativeAddr addr,
   1007                                 u32 scratch) {
   1008   MCEmitter* mc = a->base.mc;
   1009   u32 base, idx, scale;
   1010   i32 off;
   1011   if (addr.base_kind == NATIVE_ADDR_BASE_GLOBAL &&
   1012       addr.index_kind == NATIVE_ADDR_INDEX_NONE) {
   1013     x64_emit_global_lea(&a->base, scratch, addr.base.global.sym,
   1014                         addr.base.global.addend + addr.offset);
   1015     return scratch;
   1016   }
   1017   base = x64_resolve_addr(a, &addr, scratch, &idx, &scale, &off);
   1018   if (idx == REG_NONE && off == 0) return base;
   1019   if (idx == REG_NONE) {
   1020     emit_lea(mc, scratch, base, off);
   1021     return scratch;
   1022   }
   1023   {
   1024     u8 buf[16];
   1025     u32 n = 0;
   1026     n += x64_pack_rex(buf + n, 1, scratch, idx, base);
   1027     buf[n++] = X64_OPC_LEA;
   1028     n += x64_pack_mem_sib(buf + n, scratch, base, idx, scale, off);
   1029     mc_emit_bytes(mc, buf, n);
   1030   }
   1031   return scratch;
   1032 }
   1033 
   1034 /* Normalize one address at a time, load through R10, preserve the granule in
   1035  * XMM14 while the destination is normalized, then restore it to R10 for the
   1036  * store. This keeps both effective addresses and the transfer value entirely
   1037  * in the backend-private R10/R11/XMM14 bank; no allocator/cache-visible fixed
   1038  * register is touched. */
   1039 static void x64_copy_bytes(NativeTarget* t, NativeAddr dst, NativeAddr src,
   1040                            AggregateAccess access) {
   1041   X64NativeTarget* a = x64_of(t);
   1042   MCEmitter* mc = t->mc;
   1043   u32 n = access.size, i = 0;
   1044   while (i < n) {
   1045     u32 rem = n - i, s;
   1046     NativeAddr sa = src, da = dst;
   1047     u32 sb, db;
   1048     if (rem >= 8u) {
   1049       s = 8u;
   1050     } else if (rem >= 4u) {
   1051       s = 4u;
   1052     } else if (rem >= 2u) {
   1053       s = 2u;
   1054     } else {
   1055       s = 1u;
   1056     }
   1057     sa.offset += (i32)i;
   1058     da.offset += (i32)i;
   1059     sb = x64_addr_to_base_reg(a, sa, X64_TMP_INT2);
   1060     emit_mov_load(mc, s, 0, X64_TMP_INT, sb, 0);
   1061     emit_sse_rr_w(mc, 0x66, 0x6E, s == 8u, X64_TMP_FP, X64_TMP_INT);
   1062     db = x64_addr_to_base_reg(a, da, X64_TMP_INT2);
   1063     if (db == X64_TMP_INT) {
   1064       emit_mov_rr(mc, 1, X64_TMP_INT2, db);
   1065       db = X64_TMP_INT2;
   1066     }
   1067     emit_sse_rr_w(mc, 0x66, 0x7E, s == 8u, X64_TMP_FP, X64_TMP_INT);
   1068     emit_mov_store(mc, s, X64_TMP_INT, db, 0);
   1069     i += s;
   1070   }
   1071 }
   1072 
   1073 static void x64_set_bytes(NativeTarget* t, NativeAddr dst, NativeLoc byte_value,
   1074                           AggregateAccess access) {
   1075   X64NativeTarget* a = x64_of(t);
   1076   MCEmitter* mc = t->mc;
   1077   u32 dr = x64_addr_to_base_reg(a, dst, X64_TMP_INT2);
   1078   u32 n = access.size, i = 0;
   1079   /* R10 holds the broadcast value and R11 the resolved destination. Keep both
   1080    * entirely backend-private: the variable case broadcasts the low byte in
   1081    * XMM14 (punpcklbw/punpcklwd/punpckldq), then moves the qword to R10. */
   1082   if (dr == X64_TMP_INT) {
   1083     emit_mov_rr(mc, 1, X64_TMP_INT2, dr);
   1084     dr = X64_TMP_INT2;
   1085   }
   1086   if (byte_value.kind == NATIVE_LOC_IMM) {
   1087     u8 b = (u8)(byte_value.v.imm & 0xffu);
   1088     u64 b64 = b;
   1089     b64 |= b64 << 8;
   1090     b64 |= b64 << 16;
   1091     b64 |= b64 << 32;
   1092     x64_emit_load_imm(mc, 1, X64_TMP_INT, (i64)b64);
   1093   } else {
   1094     emit_mov_rr(mc, 0, X64_TMP_INT, loc_reg(byte_value));
   1095     emit_alu_imm32(mc, 0, X64_ALU_SUB_AND, X64_TMP_INT, 0xff);
   1096     emit_sse_rr_w(mc, 0x66, 0x6E, 0, X64_TMP_FP, X64_TMP_INT);
   1097     emit_sse_rr(mc, 0x66, 0x60, X64_TMP_FP, X64_TMP_FP);
   1098     emit_sse_rr(mc, 0x66, 0x61, X64_TMP_FP, X64_TMP_FP);
   1099     emit_sse_rr(mc, 0x66, 0x62, X64_TMP_FP, X64_TMP_FP);
   1100     emit_sse_rr_w(mc, 0x66, 0x7E, 1, X64_TMP_FP, X64_TMP_INT);
   1101   }
   1102   while (i + 8u <= n) {
   1103     emit_mov_store(mc, 8, X64_TMP_INT, dr, (i32)i);
   1104     i += 8u;
   1105   }
   1106   while (i + 4u <= n) {
   1107     emit_mov_store(mc, 4, X64_TMP_INT, dr, (i32)i);
   1108     i += 4u;
   1109   }
   1110   while (i + 2u <= n) {
   1111     emit_mov_store(mc, 2, X64_TMP_INT, dr, (i32)i);
   1112     i += 2u;
   1113   }
   1114   while (i < n) {
   1115     emit_mov_store(mc, 1, X64_TMP_INT, dr, (i32)i);
   1116     i += 1u;
   1117   }
   1118 }
   1119 
   1120 /* ============================ bitfields ============================ */
   1121 
   1122 static void x64_bitfield_load(NativeTarget* t, NativeLoc dst, NativeAddr ra,
   1123                               BitFieldAccess bf) {
   1124   X64NativeTarget* a = x64_of(t);
   1125   MCEmitter* mc = t->mc;
   1126   u32 storage_bytes = bf.storage.size ? bf.storage.size : 4u;
   1127   int w = storage_bytes == 8u ? 1 : 0;
   1128   u32 reg_size = w ? 64u : 32u;
   1129   u32 lsb = bf.bit_offset;
   1130   u32 width = bf.bit_width ? bf.bit_width : 1u;
   1131   u32 rd = loc_reg(dst);
   1132   u32 base;
   1133   ra.offset += (i32)bf.storage_offset;
   1134   base = x64_addr_to_base_reg(a, ra, X64_TMP_INT2);
   1135   emit_mov_load(mc, storage_bytes, 0, rd, base, 0);
   1136   {
   1137     u8 left = (u8)(reg_size - lsb - width);
   1138     u8 right = (u8)(reg_size - width);
   1139     if (left) emit_shift_imm(mc, w, X64_SHIFT_SUB_SHL, rd, left);
   1140     if (right)
   1141       emit_shift_imm(mc, w, bf.signed_ ? X64_SHIFT_SUB_SAR : X64_SHIFT_SUB_SHR,
   1142                      rd, right);
   1143   }
   1144 }
   1145 
   1146 static void x64_bitfield_store(NativeTarget* t, NativeAddr ra, NativeLoc src,
   1147                                BitFieldAccess bf) {
   1148   X64NativeTarget* a = x64_of(t);
   1149   MCEmitter* mc = t->mc;
   1150   u32 storage_bytes = bf.storage.size ? bf.storage.size : 4u;
   1151   int w = storage_bytes == 8u ? 1 : 0;
   1152   u32 lsb = bf.bit_offset;
   1153   u32 width = bf.bit_width ? bf.bit_width : 1u;
   1154   u64 ones = width >= 64u ? ~(u64)0 : (((u64)1 << width) - 1u);
   1155   u64 mask = ones << lsb;
   1156   u32 src_reg = loc_reg(src);
   1157   u32 base;
   1158   ra.offset += (i32)bf.storage_offset;
   1159   /* Resolve the address before borrowing the fixed value temporaries. A bare
   1160    * register address may be returned unchanged, so explicitly stabilize it in
   1161    * r11 when the read-modify-write is about to clobber that register. */
   1162   base = x64_addr_to_base_reg(a, ra, X64_TMP_INT2);
   1163   if (base == X64_RAX || base == X64_RCX || base == X64_RDX) {
   1164     emit_mov_rr(mc, 1, X64_TMP_INT2, base);
   1165     base = X64_TMP_INT2;
   1166   }
   1167   /* A dying source may legally occupy one of the fixed clobber registers.
   1168    * Preserve it after address resolution (which may transiently use r10 for a
   1169    * spilled index) and before loading the storage/mask into rax/rcx. RDX stays
   1170    * live until it has been copied to RCX below, so it needs no staging. */
   1171   if (src_reg == X64_RAX || src_reg == X64_RCX) {
   1172     emit_mov_rr(mc, w, X64_TMP_INT, src_reg);
   1173     src_reg = X64_TMP_INT;
   1174   }
   1175   /* rax = storage; rax &= ~mask. */
   1176   emit_mov_load(mc, storage_bytes, 0, X64_RAX, base, 0);
   1177   x64_emit_load_imm(mc, w, X64_RCX, (i64)~mask);
   1178   emit_alu_rr(mc, w, X64_OPC_ALU_AND, X64_RAX, X64_RCX);
   1179   /* rcx = (src & ones) << lsb. */
   1180   emit_mov_rr(mc, w, X64_RCX, src_reg);
   1181   x64_emit_load_imm(mc, w, X64_RDX, (i64)ones);
   1182   emit_alu_rr(mc, w, X64_OPC_ALU_AND, X64_RCX, X64_RDX);
   1183   if (lsb) emit_shift_imm(mc, w, X64_SHIFT_SUB_SHL, X64_RCX, (u8)lsb);
   1184   emit_alu_rr(mc, w, X64_OPC_ALU_OR, X64_RAX, X64_RCX);
   1185   emit_mov_store(mc, storage_bytes, X64_RAX, base, 0);
   1186 }
   1187 
   1188 /* ============================ arithmetic ============================ */
   1189 
   1190 static void x64_binop(NativeTarget* t, BinOp op, NativeLoc dst, NativeLoc aop,
   1191                       NativeLoc bop) {
   1192   X64NativeTarget* a = x64_of(t);
   1193   MCEmitter* mc = t->mc;
   1194   u32 rd = loc_reg(dst);
   1195 
   1196   /* FP binops: two-address. dst = aop op bop. */
   1197   if (op == BO_FADD || op == BO_FSUB || op == BO_FMUL || op == BO_FDIV) {
   1198     u32 ra = loc_reg(aop), rb = loc_reg(bop);
   1199     u8 prefix = sse_scalar_prefix(loc_size32(t, dst));
   1200     u8 opcode;
   1201     switch (op) {
   1202       case BO_FADD:
   1203         opcode = 0x58;
   1204         break;
   1205       case BO_FSUB:
   1206         opcode = 0x5C;
   1207         break;
   1208       case BO_FMUL:
   1209         opcode = 0x59;
   1210         break;
   1211       default:
   1212         opcode = 0x5E;
   1213         break; /* BO_FDIV */
   1214     }
   1215     if (rd == rb && rd != ra) {
   1216       if (op == BO_FADD || op == BO_FMUL) { /* commutative */
   1217         emit_sse_rr(mc, prefix, opcode, rd, ra);
   1218         return;
   1219       }
   1220       /* non-commutative dst==rb: stage rb in fp scratch. */
   1221       emit_sse_rr(mc, prefix, 0x10, X64_TMP_FP2, rb);
   1222       emit_sse_rr(mc, prefix, 0x10, rd, ra);
   1223       emit_sse_rr(mc, prefix, opcode, rd, X64_TMP_FP2);
   1224       return;
   1225     }
   1226     if (rd != ra) emit_sse_rr(mc, prefix, 0x10, rd, ra);
   1227     emit_sse_rr(mc, prefix, opcode, rd, rb);
   1228     return;
   1229   }
   1230 
   1231   {
   1232     int w = loc_is_64(t, dst) ? 1 : 0;
   1233     int b_imm = bop.kind == NATIVE_LOC_IMM;
   1234     i64 imm = b_imm ? bop.v.imm : 0;
   1235     u32 ra = loc_reg(aop);
   1236 
   1237     /* Division: rax/rdx implicit; divisor must avoid rax/rdx. */
   1238     if (op == BO_SDIV || op == BO_UDIV || op == BO_SREM || op == BO_UREM) {
   1239       u32 rb;
   1240       if (b_imm) {
   1241         x64_emit_load_imm(mc, w, X64_R11, imm);
   1242         rb = X64_R11;
   1243       } else {
   1244         rb = loc_reg(bop);
   1245         /* Preserve the divisor before placing the dividend in RAX. This is a
   1246          * real parallel-copy edge: a dying divisor may be in RAX while the
   1247          * dividend is elsewhere, or in RDX before CQO/XOR clears it. */
   1248         if (rb == X64_RAX || rb == X64_RDX) {
   1249           emit_mov_rr(mc, w, X64_R11, rb);
   1250           rb = X64_R11;
   1251         }
   1252       }
   1253       if (ra != X64_RAX) emit_mov_rr(mc, w, X64_RAX, ra);
   1254       if (op == BO_SDIV || op == BO_SREM) {
   1255         emit_cqo_or_cdq(mc, w);
   1256         emit_f7_rm(mc, w, X64_F7_SUB_IDIV, rb);
   1257       } else {
   1258         emit_xor_self(mc, w, X64_RDX);
   1259         emit_f7_rm(mc, w, X64_F7_SUB_DIV, rb);
   1260       }
   1261       {
   1262         u32 result = (op == BO_SREM || op == BO_UREM) ? X64_RDX : X64_RAX;
   1263         if (rd != result) emit_mov_rr(mc, w, rd, result);
   1264       }
   1265       return;
   1266     }
   1267 
   1268     /* Shifts: count in CL or imm8. */
   1269     if (op == BO_SHL || op == BO_SHR_U || op == BO_SHR_S) {
   1270       u32 sub = (op == BO_SHL)     ? X64_SHIFT_SUB_SHL
   1271                 : (op == BO_SHR_U) ? X64_SHIFT_SUB_SHR
   1272                                    : X64_SHIFT_SUB_SAR;
   1273       if (b_imm) {
   1274         u32 wbits = w ? 64u : 32u;
   1275         if (rd != ra) emit_mov_rr(mc, w, rd, ra);
   1276         emit_shift_imm(mc, w, sub, rd, (u8)((u64)imm & (wbits - 1u)));
   1277         return;
   1278       }
   1279       {
   1280         u32 rb = loc_reg(bop);
   1281         /* RCX cannot simultaneously hold a distinct result and the shift
   1282          * count. Compute through backend-private R10, then publish the result
   1283          * after the instruction has consumed CL. */
   1284         if (rd == X64_RCX && (ra != X64_RCX || rb != X64_RCX)) {
   1285           emit_mov_rr(mc, w, X64_TMP_INT, ra);
   1286           if (rb != X64_RCX) emit_mov_rr(mc, 0, X64_RCX, rb);
   1287           emit_shift_cl(mc, w, sub, X64_TMP_INT);
   1288           emit_mov_rr(mc, w, X64_RCX, X64_TMP_INT);
   1289           return;
   1290         }
   1291         /* Place the count in cl and the value in dst. Stage the count through
   1292          * r11 first so neither move clobbers the other when the value already
   1293          * sits in rcx or the count sits in dst. (The optimizer additionally
   1294          * keeps values live across the shift out of rcx — see
   1295          * x64_machine_op_clobbers.) */
   1296         if (rb != X64_RCX) {
   1297           emit_mov_rr(mc, 0, X64_TMP_INT2, rb);
   1298           if (rd != ra) emit_mov_rr(mc, w, rd, ra);
   1299           emit_mov_rr(mc, 0, X64_RCX, X64_TMP_INT2);
   1300         } else if (rd != ra) {
   1301           emit_mov_rr(mc, w, rd, ra);
   1302         }
   1303       }
   1304       emit_shift_cl(mc, w, sub, rd);
   1305       return;
   1306     }
   1307 
   1308     /* IMM-form fast paths (b_imm guaranteed legal by imm_legal: imm32). */
   1309     if (b_imm && (op == BO_IADD || op == BO_ISUB || op == BO_AND ||
   1310                   op == BO_OR || op == BO_XOR || op == BO_IMUL)) {
   1311       if (op == BO_IMUL) {
   1312         if (imm_fits_i8(imm)) {
   1313           emit_imul_imm8(mc, w, rd, ra, (i8)imm);
   1314           return;
   1315         }
   1316         emit_imul_imm32(mc, w, rd, ra, (i32)imm);
   1317         return;
   1318       }
   1319       {
   1320         u32 sub;
   1321         switch (op) {
   1322           case BO_IADD:
   1323             sub = X64_ALU_SUB_ADD;
   1324             break;
   1325           case BO_OR:
   1326             sub = X64_ALU_SUB_OR;
   1327             break;
   1328           case BO_AND:
   1329             sub = X64_ALU_SUB_AND;
   1330             break;
   1331           case BO_ISUB:
   1332             sub = X64_ALU_SUB_SUB;
   1333             break;
   1334           default:
   1335             sub = X64_ALU_SUB_XOR;
   1336             break; /* BO_XOR */
   1337         }
   1338         if (rd != ra) emit_mov_rr(mc, w, rd, ra);
   1339         if (imm_fits_i8(imm))
   1340           emit_alu_imm8(mc, w, sub, rd, (i8)imm);
   1341         else
   1342           emit_alu_imm32(mc, w, sub, rd, (i32)imm);
   1343         return;
   1344       }
   1345     }
   1346 
   1347     /* Generic 2-operand ALU: dst = ra op rb. Preserve rb if dst == rb. */
   1348     {
   1349       u32 rb = loc_reg(bop);
   1350       if (rd == rb && rd != ra) {
   1351         switch (op) {
   1352           case BO_IADD:
   1353             emit_alu_rr(mc, w, X64_OPC_ALU_ADD, rd, ra);
   1354             return;
   1355           case BO_AND:
   1356             emit_alu_rr(mc, w, X64_OPC_ALU_AND, rd, ra);
   1357             return;
   1358           case BO_OR:
   1359             emit_alu_rr(mc, w, X64_OPC_ALU_OR, rd, ra);
   1360             return;
   1361           case BO_XOR:
   1362             emit_alu_rr(mc, w, X64_OPC_ALU_XOR, rd, ra);
   1363             return;
   1364           case BO_IMUL:
   1365             emit_imul_rr(mc, w, rd, ra);
   1366             return;
   1367           default:
   1368             break; /* ISUB falls through: stage rb */
   1369         }
   1370         emit_mov_rr(mc, w, X64_R11, rb);
   1371         rb = X64_R11;
   1372       }
   1373       if (rd != ra) emit_mov_rr(mc, w, rd, ra);
   1374       switch (op) {
   1375         case BO_IADD:
   1376           emit_alu_rr(mc, w, X64_OPC_ALU_ADD, rd, rb);
   1377           break;
   1378         case BO_ISUB:
   1379           emit_alu_rr(mc, w, X64_OPC_ALU_SUB, rd, rb);
   1380           break;
   1381         case BO_AND:
   1382           emit_alu_rr(mc, w, X64_OPC_ALU_AND, rd, rb);
   1383           break;
   1384         case BO_OR:
   1385           emit_alu_rr(mc, w, X64_OPC_ALU_OR, rd, rb);
   1386           break;
   1387         case BO_XOR:
   1388           emit_alu_rr(mc, w, X64_OPC_ALU_XOR, rd, rb);
   1389           break;
   1390         case BO_IMUL:
   1391           emit_imul_rr(mc, w, rd, rb);
   1392           break;
   1393         default:
   1394           x64_panic(a, "unsupported binop");
   1395       }
   1396     }
   1397   }
   1398 }
   1399 
   1400 /* FP sign-mask constant materialized in fp scratch for FNEG. */
   1401 static void x64_unop(NativeTarget* t, UnOp op, NativeLoc dst, NativeLoc src) {
   1402   X64NativeTarget* a = x64_of(t);
   1403   MCEmitter* mc = t->mc;
   1404   u32 rd = loc_reg(dst), rs = loc_reg(src);
   1405   if (op == UO_FNEG) {
   1406     int dbl = loc_size32(t, dst) == 8u;
   1407     if (rd != rs)
   1408       emit_sse_rr(mc, sse_scalar_prefix(dbl ? 8u : 4u), 0x10, rd, rs);
   1409     /* sign mask into fp scratch via gpr, then XORPS/XORPD. */
   1410     x64_emit_load_imm(mc, dbl, X64_TMP_INT,
   1411                       dbl ? (i64)0x8000000000000000ull : (i64)0x80000000ull);
   1412     emit_sse_rr_w(mc, 0x66, 0x6E, dbl, X64_TMP_FP2, X64_TMP_INT);
   1413     emit_sse_rr(mc, dbl ? 0x66 : 0, 0x57, rd, X64_TMP_FP2);
   1414     return;
   1415   }
   1416   {
   1417     int w = loc_is_64(t, dst) ? 1 : 0;
   1418     switch (op) {
   1419       case UO_NEG:
   1420         if (rd != rs) emit_mov_rr(mc, w, rd, rs);
   1421         emit_f7_rm(mc, w, X64_F7_SUB_NEG, rd);
   1422         return;
   1423       case UO_BNOT:
   1424         if (rd != rs) emit_mov_rr(mc, w, rd, rs);
   1425         emit_f7_rm(mc, w, X64_F7_SUB_NOT, rd);
   1426         return;
   1427       case UO_NOT:
   1428         /* !x -> (x == 0) as 0/1. */
   1429         emit_test_self(mc, w, rs);
   1430         emit_setcc(mc, X64_CC_E, rd);
   1431         emit_movzx_r32_r8(mc, rd, rd);
   1432         return;
   1433       default:
   1434         x64_panic(a, "unsupported unop");
   1435     }
   1436   }
   1437 }
   1438 
   1439 /* ============================ compares ============================ */
   1440 
   1441 static u32 cmp_to_cc(CmpOp op) {
   1442   switch (op) {
   1443     case CMP_EQ:
   1444       return X64_CC_E;
   1445     case CMP_NE:
   1446       return X64_CC_NE;
   1447     case CMP_LT_U:
   1448       return X64_CC_B;
   1449     case CMP_LE_U:
   1450       return X64_CC_BE;
   1451     case CMP_GT_U:
   1452       return X64_CC_A;
   1453     case CMP_GE_U:
   1454       return X64_CC_AE;
   1455     case CMP_LT_S:
   1456       return X64_CC_L;
   1457     case CMP_LE_S:
   1458       return X64_CC_LE;
   1459     case CMP_GT_S:
   1460       return X64_CC_G;
   1461     case CMP_GE_S:
   1462       return X64_CC_GE;
   1463     default:
   1464       return X64_CC_E;
   1465   }
   1466 }
   1467 
   1468 static int cmp_is_fp(CmpOp op, NativeLoc aop) {
   1469   /* FP-ness is self-describing from the opcode; FP eq/ne are distinct opcodes
   1470    * (CMP_OEQ_F/CMP_UNE_F), so no operand-class sniffing is needed. */
   1471   (void)aop;
   1472   return op >= CMP_OEQ_F;
   1473 }
   1474 
   1475 /* Emit `cmp ra, rb` (or ucomis[sd] for FP), setting flags from ra - rb. */
   1476 static void x64_emit_cmp_flags(NativeTarget* t, NativeLoc aop, NativeLoc bop,
   1477                                int fp) {
   1478   X64NativeTarget* a = x64_of(t);
   1479   MCEmitter* mc = t->mc;
   1480   if (fp) {
   1481     u8 prefix = loc_size32(t, aop) == 8u ? 0x66u : 0u;
   1482     emit_sse_rr(mc, prefix, 0x2E, loc_reg(aop), loc_reg(bop)); /* ucomis */
   1483     return;
   1484   }
   1485   {
   1486     int w = loc_is_64(t, aop) ? 1 : 0;
   1487     u32 ra = loc_reg(aop);
   1488     if (bop.kind == NATIVE_LOC_IMM) {
   1489       i64 imm = bop.v.imm;
   1490       if (imm_fits_i8(imm))
   1491         emit_alu_imm8(mc, w, X64_ALU_SUB_CMP, ra, (i8)imm);
   1492       else
   1493         emit_alu_imm32(mc, w, X64_ALU_SUB_CMP, ra, (i32)imm);
   1494       return;
   1495     }
   1496     emit_alu_rr(mc, w, X64_OPC_ALU_CMP, ra, loc_reg(bop));
   1497     (void)a;
   1498   }
   1499 }
   1500 
   1501 /* FP ordered setcc: result = (primary cc) && !unordered (NP). */
   1502 static void x64_fp_setcc_ordered(NativeTarget* t, u32 primary, u32 dst) {
   1503   MCEmitter* mc = t->mc;
   1504   emit_setcc(mc, primary, dst);
   1505   emit_movzx_r32_r8(mc, dst, dst);
   1506   emit_setcc(mc, X64_CC_NP, X64_R11);
   1507   emit_movzx_r32_r8(mc, X64_R11, X64_R11);
   1508   emit_alu_rr(mc, 0, X64_OPC_ALU_AND, dst, X64_R11);
   1509 }
   1510 
   1511 /* FP unordered predicate: result = (primary cc) || unordered (P). */
   1512 static void x64_fp_setcc_unord(NativeTarget* t, u32 primary, u32 dst) {
   1513   MCEmitter* mc = t->mc;
   1514   emit_setcc(mc, primary, dst);
   1515   emit_movzx_r32_r8(mc, dst, dst);
   1516   emit_setcc(mc, X64_CC_P, X64_R11);
   1517   emit_movzx_r32_r8(mc, X64_R11, X64_R11);
   1518   emit_alu_rr(mc, 0, X64_OPC_ALU_OR, dst, X64_R11);
   1519 }
   1520 
   1521 static void x64_cmp(NativeTarget* t, CmpOp op, NativeLoc dst, NativeLoc aop,
   1522                     NativeLoc bop) {
   1523   MCEmitter* mc = t->mc;
   1524   u32 d = loc_reg(dst);
   1525   int fp = cmp_is_fp(op, aop);
   1526   x64_emit_cmp_flags(t, aop, bop, fp);
   1527   if (fp) {
   1528     /* ucomis sets ZF/CF and, when unordered (NaN), also PF. Each predicate's
   1529      * flag formula is built explicitly (NOT blindly as !(opposite)):
   1530      *   ordered:    E/B/BE alias {==,<,<=} only when also NP (not-parity);
   1531      *               NE/A/AE already exclude unordered, so they stand alone.
   1532      *   unordered:  E/B/BE already include the unordered case (ZF/CF set on
   1533      *               NaN), so they stand alone; NE/A/AE need an OR with P. */
   1534     switch (op) {
   1535       /* ordered: require not-unordered (NP) on the equality-flag cases */
   1536       case CMP_OEQ_F:
   1537         x64_fp_setcc_ordered(t, X64_CC_E, d);
   1538         return;
   1539       case CMP_OLT_F:
   1540         x64_fp_setcc_ordered(t, X64_CC_B, d);
   1541         return;
   1542       case CMP_OLE_F:
   1543         x64_fp_setcc_ordered(t, X64_CC_BE, d);
   1544         return;
   1545       case CMP_ONE_F:
   1546         emit_setcc(mc, X64_CC_NE, d);
   1547         break;
   1548       case CMP_OGT_F:
   1549         emit_setcc(mc, X64_CC_A, d);
   1550         break;
   1551       case CMP_OGE_F:
   1552         emit_setcc(mc, X64_CC_AE, d);
   1553         break;
   1554       /* unordered: OR-with-P on the cases that exclude unordered */
   1555       case CMP_UEQ_F:
   1556         emit_setcc(mc, X64_CC_E, d);
   1557         break;
   1558       case CMP_ULT_F:
   1559         emit_setcc(mc, X64_CC_B, d);
   1560         break;
   1561       case CMP_ULE_F:
   1562         emit_setcc(mc, X64_CC_BE, d);
   1563         break;
   1564       case CMP_UNE_F:
   1565         x64_fp_setcc_unord(t, X64_CC_NE, d);
   1566         return;
   1567       case CMP_UGT_F:
   1568         x64_fp_setcc_unord(t, X64_CC_A, d);
   1569         return;
   1570       case CMP_UGE_F:
   1571         x64_fp_setcc_unord(t, X64_CC_AE, d);
   1572         return;
   1573       default:
   1574         emit_setcc(mc, cmp_to_cc(op), d);
   1575         break;
   1576     }
   1577     emit_movzx_r32_r8(mc, d, d);
   1578     return;
   1579   }
   1580   emit_setcc(mc, cmp_to_cc(op), d);
   1581   emit_movzx_r32_r8(mc, d, d);
   1582 }
   1583 
   1584 /* ============================ converts ============================ */
   1585 
   1586 static void x64_convert(NativeTarget* t, ConvKind k, NativeLoc dst,
   1587                         NativeLoc src) {
   1588   X64NativeTarget* a = x64_of(t);
   1589   MCEmitter* mc = t->mc;
   1590   u32 rd = loc_reg(dst), rs = loc_reg(src);
   1591   switch (k) {
   1592     case CV_SEXT: {
   1593       u32 src_sz = loc_size32(t, src);
   1594       int w = loc_is_64(t, dst) ? 1 : 0;
   1595       emit_extend_rr(mc, w, 1, src_sz, rd, rs);
   1596       return;
   1597     }
   1598     case CV_ZEXT: {
   1599       u32 src_sz = loc_size32(t, src);
   1600       int w = loc_is_64(t, dst) ? 1 : 0;
   1601       emit_extend_rr(mc, w, 0, src_sz, rd, rs);
   1602       return;
   1603     }
   1604     case CV_TRUNC:
   1605       emit_mov_rr(mc, 0, rd, rs); /* low 32 bits; clears high */
   1606       return;
   1607     case CV_ITOF_S:
   1608     case CV_ITOF_U: {
   1609       int w_src = loc_is_64(t, src) ? 1 : 0;
   1610       u8 prefix = sse_scalar_prefix(loc_size32(t, dst));
   1611       if (k == CV_ITOF_U && w_src == 1) {
   1612         MCLabel L_high = mc_label_new(mc);
   1613         MCLabel L_done = mc_label_new(mc);
   1614         emit_test_self(mc, 1, rs);
   1615         emit_jcc_rel32(mc, X64_CC_S, L_high);
   1616         emit_sse_rr_w(mc, prefix, 0x2A, 1, rd, rs);
   1617         emit_jmp_rel32(mc, L_done);
   1618         mc_label_place(mc, L_high);
   1619         emit_mov_rr(mc, 1, X64_R11, rs);
   1620         emit_mov_rr(mc, 1, X64_TMP_INT, rs);
   1621         emit_alu_imm8(mc, 1, X64_ALU_SUB_AND, X64_TMP_INT, 1);
   1622         emit_shift_imm(mc, 1, X64_SHIFT_SUB_SHR, X64_R11, 1);
   1623         emit_alu_rr(mc, 1, X64_OPC_ALU_OR, X64_R11, X64_TMP_INT);
   1624         emit_sse_rr_w(mc, prefix, 0x2A, 1, rd, X64_R11);
   1625         emit_sse_rr(mc, prefix, 0x58, rd, rd);
   1626         mc_label_place(mc, L_done);
   1627         return;
   1628       }
   1629       if (k == CV_ITOF_U) {
   1630         emit_extend_rr(mc, 0, 0, 4, X64_R11, rs); /* zext u32 -> 64 */
   1631         rs = X64_R11;
   1632         w_src = 1;
   1633       }
   1634       emit_sse_rr_w(mc, prefix, 0x2A, w_src, rd, rs);
   1635       return;
   1636     }
   1637     case CV_FTOI_S:
   1638     case CV_FTOI_U: {
   1639       int w_dst = loc_is_64(t, dst) ? 1 : 0;
   1640       u8 prefix = sse_scalar_prefix(loc_size32(t, src));
   1641       /* Unsigned 64-bit FTOI needs the 2^63 bias dance; otherwise cvtt
   1642        * (with the destination widened to 64 for u32) is exact. */
   1643       if (k == CV_FTOI_U && w_dst == 1) {
   1644         int dbl = loc_size32(t, src) == 8u;
   1645         MCLabel L_small = mc_label_new(mc);
   1646         MCLabel L_done = mc_label_new(mc);
   1647         /* limit = 2^63 in fp scratch. */
   1648         x64_emit_load_imm(
   1649             mc, 1, X64_R11,
   1650             dbl ? (i64)0x43E0000000000000ull : (i64)0x5F000000ull);
   1651         emit_sse_rr_w(mc, 0x66, 0x6E, dbl, X64_TMP_FP2, X64_R11);
   1652         emit_sse_rr(mc, dbl ? 0x66 : 0, 0x2E, rs, X64_TMP_FP2); /* ucomis */
   1653         emit_jcc_rel32(mc, X64_CC_B, L_small);
   1654         emit_sse_rr(mc, prefix, 0x10, X64_TMP_FP, rs);
   1655         emit_sse_rr(mc, prefix, 0x5C, X64_TMP_FP, X64_TMP_FP2); /* sub bias */
   1656         emit_sse_rr_w(mc, prefix, 0x2C, 1, rd, X64_TMP_FP);
   1657         x64_emit_load_imm(mc, 1, X64_R11, (i64)0x8000000000000000ull);
   1658         emit_alu_rr(mc, 1, X64_OPC_ALU_XOR, rd, X64_R11);
   1659         emit_jmp_rel32(mc, L_done);
   1660         mc_label_place(mc, L_small);
   1661         emit_sse_rr_w(mc, prefix, 0x2C, 1, rd, rs);
   1662         mc_label_place(mc, L_done);
   1663         return;
   1664       }
   1665       if (k == CV_FTOI_U) w_dst = 1; /* widen u32 result */
   1666       emit_sse_rr_w(mc, prefix, 0x2C, w_dst, rd, rs);
   1667       return;
   1668     }
   1669     case CV_FEXT:
   1670       emit_sse_rr(mc, 0xF3, 0x5A, rd, rs); /* cvtss2sd */
   1671       return;
   1672     case CV_FTRUNC:
   1673       emit_sse_rr(mc, 0xF2, 0x5A, rd, rs); /* cvtsd2ss */
   1674       return;
   1675     case CV_BITCAST:
   1676       if (!native_loc_is_fp(src) && native_loc_is_fp(dst)) {
   1677         emit_sse_rr_w(mc, 0x66, 0x6E, loc_is_64(t, dst), rd, rs);
   1678       } else if (native_loc_is_fp(src) && !native_loc_is_fp(dst)) {
   1679         emit_sse_rr_w(mc, 0x66, 0x7E, loc_is_64(t, src), rs, rd);
   1680       } else {
   1681         x64_move(t, dst, src);
   1682       }
   1683       return;
   1684     default:
   1685       x64_panic(a, "unsupported convert");
   1686   }
   1687 }
   1688 
   1689 /* §E.3 narrow register-only entry points. The NDT crosses 16 B NativeRegLoc
   1690  * here; these reconstruct the NativeLoc the fat hook expects and delegate, so
   1691  * the emitted bytes are identical. Used only on the -O0 NDT path; the opt
   1692  * replay path calls x64_binop/x64_move/x64_cmp/x64_convert directly. */
   1693 static void x64_binop_rr(NativeTarget* t, BinOp op, NativeRegLoc dst,
   1694                          NativeRegLoc a, NativeRegLoc b) {
   1695   x64_binop(t, op, native_loc_from_reg(dst), native_loc_from_reg(a),
   1696             native_loc_from_reg(b));
   1697 }
   1698 
   1699 static void x64_move_rr(NativeTarget* t, NativeRegLoc dst, NativeRegLoc src) {
   1700   x64_move(t, native_loc_from_reg(dst), native_loc_from_reg(src));
   1701 }
   1702 
   1703 static void x64_cmp_rr(NativeTarget* t, CmpOp op, NativeRegLoc dst,
   1704                        NativeRegLoc a, NativeRegLoc b) {
   1705   x64_cmp(t, op, native_loc_from_reg(dst), native_loc_from_reg(a),
   1706           native_loc_from_reg(b));
   1707 }
   1708 
   1709 static void x64_convert_rr(NativeTarget* t, ConvKind op, NativeRegLoc dst,
   1710                            NativeRegLoc src) {
   1711   x64_convert(t, op, native_loc_from_reg(dst), native_loc_from_reg(src));
   1712 }
   1713 
   1714 /* ============================ spill / reload ============================ */
   1715 
   1716 static void x64_spill(NativeTarget* t, NativeLoc src, NativeFrameSlot slot,
   1717                       MemAccess mem) {
   1718   NativeAddr addr;
   1719   memset(&addr, 0, sizeof addr);
   1720   addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   1721   addr.base.frame = slot;
   1722   addr.base_type = src.type;
   1723   x64_emit_mem(x64_of(t), 0, src, addr, mem);
   1724 }
   1725 static void x64_reload(NativeTarget* t, NativeLoc dst, NativeFrameSlot slot,
   1726                        MemAccess mem) {
   1727   NativeAddr addr;
   1728   memset(&addr, 0, sizeof addr);
   1729   addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   1730   addr.base.frame = slot;
   1731   addr.base_type = dst.type;
   1732   x64_emit_mem(x64_of(t), 1, dst, addr, mem);
   1733 }
   1734 
   1735 /* ============================ control flow ============================ */
   1736 
   1737 static void emit_jmp_rel32(MCEmitter* mc, MCLabel l) {
   1738   u8 op = X64_OPC_JMP_REL32;
   1739   mc_emit_bytes(mc, &op, 1);
   1740   emit_u32le(mc, 0);
   1741   mc_emit_label_ref(mc, l, R_PC32, 4, -4);
   1742 }
   1743 static void emit_jcc_rel32(MCEmitter* mc, u32 cc, MCLabel l) {
   1744   u8 op[2] = {X64_OPC_TWOBYTE, (u8)(X64_OPC_JCC_BASE | (cc & 0xfu))};
   1745   mc_emit_bytes(mc, op, 2);
   1746   emit_u32le(mc, 0);
   1747   mc_emit_label_ref(mc, l, R_PC32, 4, -4);
   1748 }
   1749 
   1750 static MCLabel x64_label_new(NativeTarget* t) {
   1751   return mc_label_new(t->mc);
   1752 }
   1753 static void x64_label_place(NativeTarget* t, MCLabel l) {
   1754   mc_label_place(t->mc, l);
   1755 }
   1756 static void x64_jump(NativeTarget* t, MCLabel l) { emit_jmp_rel32(t->mc, l); }
   1757 
   1758 static void x64_cmp_branch(NativeTarget* t, CmpOp op, NativeLoc aop,
   1759                            NativeLoc bop, MCLabel l) {
   1760   MCEmitter* mc = t->mc;
   1761   int fp = cmp_is_fp(op, aop);
   1762   if (fp) {
   1763     /* Materialize the 0/1 result, then branch on nonzero. */
   1764     NativeLoc tmp =
   1765         native_loc_reg(builtin_id(KIT_CG_BUILTIN_I32), NATIVE_REG_INT,
   1766                        X64_TMP_INT);
   1767     x64_cmp(t, op, tmp, aop, bop);
   1768     emit_test_self(mc, 0, X64_TMP_INT);
   1769     emit_jcc_rel32(mc, X64_CC_NE, l);
   1770     return;
   1771   }
   1772   x64_emit_cmp_flags(t, aop, bop, 0);
   1773   emit_jcc_rel32(mc, cmp_to_cc(op), l);
   1774 }
   1775 
   1776 /* Emit an indirect `call`/`jmp r/m64` (opcode FF) against register `r`. The
   1777  * ModRM reg field is the opcode digit: /2 for `call`, /4 for `jmp`. Byte-for-
   1778  * byte the open-coded encoding used by x64_indirect_branch, x64_emit_call's
   1779  * indirect (r11-staged) site, and x64_emit_tail_site's indirect (r11) site. */
   1780 static void x64_emit_indirect_rm(MCEmitter* mc, u32 r, u32 digit) {
   1781   if (r & 8u) {
   1782     u8 rex = X64_REX_BASE | X64_REX_B;
   1783     mc_emit_bytes(mc, &rex, 1);
   1784   }
   1785   {
   1786     u8 buf[2] = {X64_OP_JMP_RM64, modrm(3u, digit, r & 7u)};
   1787     mc_emit_bytes(mc, buf, 2);
   1788   }
   1789 }
   1790 
   1791 static void x64_indirect_branch(NativeTarget* t, NativeLoc addr,
   1792                                 const MCLabel* valid_targets, u32 ntargets) {
   1793   MCEmitter* mc = t->mc;
   1794   u32 r = loc_reg(addr);
   1795   (void)valid_targets;
   1796   (void)ntargets;
   1797   x64_emit_indirect_rm(mc, r, 4u); /* jmp r/m, /4 */
   1798 }
   1799 
   1800 static void x64_load_label_addr(NativeTarget* t, NativeLoc dst, MCLabel l) {
   1801   /* `&&label` address-take: `leaq sym(%rip), rd` with an R_PC32 relocation
   1802    * against the label's per-block local symbol — same form as a global
   1803    * address-take, so a re-encoding assembler recomputes the displacement.
   1804    * (A baked disp32 with no reloc would break once clang re-lays-out the
   1805    * function.) */
   1806   MCEmitter* mc = t->mc;
   1807   u32 rd = loc_reg(dst);
   1808   ObjSymId sym = mc_label_symbol(mc, l);
   1809   u32 disp_pos;
   1810   emit_rex(mc, 1, rd, 0, 0);
   1811   {
   1812     u8 op = X64_OPC_LEA;
   1813     mc_emit_bytes(mc, &op, 1);
   1814   }
   1815   {
   1816     u8 mr = modrm(0u, rd & 7u, 5u); /* [rip + disp32] */
   1817     mc_emit_bytes(mc, &mr, 1);
   1818   }
   1819   disp_pos = mc_pos(mc);
   1820   emit_u32le(mc, 0);
   1821   mc_emit_reloc_at(mc, mc->section_id, disp_pos, R_PC32, sym, -4, 1, 0);
   1822 }
   1823 
   1824 /* ============================ frame / lifecycle ============================
   1825  */
   1826 
   1827 static NativeFrameSlot x64_frame_slot(NativeTarget* t,
   1828                                       const NativeFrameSlotDesc* d) {
   1829   return native_frame_slot_alloc(&x64_of(t)->frame, d);
   1830 }
   1831 
   1832 static void x64_release_frame_slot(NativeTarget* t, NativeFrameSlot slot) {
   1833   native_frame_release_slot(&x64_of(t)->frame, slot);
   1834 }
   1835 
   1836 static int x64_frame_slot_debug_loc(NativeTarget* t, NativeFrameSlot slot,
   1837                                     CGDebugLoc* out) {
   1838   X64NativeTarget* a = x64_of(t);
   1839   X64NativeSlot* s;
   1840   if (!out) return 0;
   1841   memset(out, 0, sizeof *out);
   1842   if (slot == NATIVE_FRAME_SLOT_NONE || slot > a->frame.nslots) return 0;
   1843   s = x64_slot_get(a, slot);
   1844   out->kind = CG_DEBUG_LOC_FRAME;
   1845   /* x64 slots live at RBP - off (exactly how the memory-operand path addresses
   1846    * them). The hosted dbg snapshot seeds the frame base with RBP, so report
   1847    * the RBP-relative offset — mirroring aa64's FP-relative convention. */
   1848   out->v.frame_ofs = -(i32)s->off;
   1849   return 1;
   1850 }
   1851 
   1852 /* xmm save area base (rbp-relative). XMM saves are 16-aligned. */
   1853 static u32 x64_xmm_base(const X64NativeTarget* a, u32 cs_fp) {
   1854   if (cs_fp == 0) return a->frame.cum_off;
   1855   return align_up_u32(a->frame.cum_off, 16u);
   1856 }
   1857 
   1858 static u32 x64_compute_frame_size(const X64NativeTarget* a, u32 cs_int,
   1859                                   u32 cs_fp) {
   1860   u32 xmm_base = x64_xmm_base(a, cs_fp);
   1861   u32 raw = a->frame.max_outgoing + cs_int * 8u + cs_fp * 16u + xmm_base;
   1862   u32 fs = align_up_u32(raw, 16u);
   1863   return fs ? fs : 16u;
   1864 }
   1865 
   1866 /* Collect the callee-saves the body actually used. */
   1867 static u32 x64_collect_int_saves(X64NativeTarget* a, Reg* regs) {
   1868   u32 n = 0, i;
   1869   for (i = 0; i < a->frame.ncallee_saves; ++i)
   1870     if (a->frame.callee_saves[i].cls == NATIVE_REG_INT)
   1871       regs[n++] = a->frame.callee_saves[i].reg;
   1872   return n;
   1873 }
   1874 static u32 x64_collect_fp_saves(X64NativeTarget* a, Reg* regs) {
   1875   u32 n = 0, i;
   1876   for (i = 0; i < a->frame.ncallee_saves; ++i)
   1877     if (a->frame.callee_saves[i].cls == NATIVE_REG_FP)
   1878       regs[n++] = a->frame.callee_saves[i].reg;
   1879   return n;
   1880 }
   1881 
   1882 /* rbp-relative offset of callee-save slot `idx`. The int GPRs sit below the
   1883  * 16-byte-aligned XMM save area (`xmm_base`) and the `n_fp` 16-byte XMM saves;
   1884  * the XMMs sit just below `xmm_base`. Single source for the prologue spill, the
   1885  * epilogue/tail-site restore, and the CFI offsets — all previously open-coded
   1886  * with this exact arithmetic. */
   1887 static inline i32 x64_cs_int_off(u32 xmm_base, u32 n_fp, u32 idx) {
   1888   return -(i32)xmm_base - (i32)n_fp * 16 - (i32)(idx + 1u) * 8;
   1889 }
   1890 static inline i32 x64_cs_fp_off(u32 xmm_base, u32 idx) {
   1891   return -(i32)xmm_base - (i32)(idx + 1u) * 16;
   1892 }
   1893 
   1894 /* Emit the callee-save restores (reverse spill order: XMMs then GPRs) at the
   1895  * current emit cursor. Shared by the function epilogue (x64_func_end) and the
   1896  * tail-call site (x64_emit_tail_site); mirrors aa64's aa_emit_callee_restores.
   1897  * Frame-size-independent, so it needs only the collected save sets. */
   1898 static void x64_emit_callee_restores(X64NativeTarget* a) {
   1899   MCEmitter* mc = a->base.mc;
   1900   Reg cs_int[X64_MAX_CS_INT_REGS], cs_fp[X64_MAX_CS_FP_REGS];
   1901   u32 n_int = x64_collect_int_saves(a, cs_int);
   1902   u32 n_fp = x64_collect_fp_saves(a, cs_fp);
   1903   u32 xmm_base = x64_xmm_base(a, n_fp);
   1904   i32 i;
   1905   for (i = (i32)n_fp - 1; i >= 0; --i)
   1906     emit_sse_load(mc, 0, 0x28, cs_fp[i], X64_RBP,
   1907                   x64_cs_fp_off(xmm_base, (u32)i)); /* movaps */
   1908   for (i = (i32)n_int - 1; i >= 0; --i)
   1909     emit_mov_load(mc, 8, 0, cs_int[i], X64_RBP,
   1910                   x64_cs_int_off(xmm_base, n_fp, (u32)i));
   1911 }
   1912 
   1913 static ObjSymId x64_chkstk_sym(NativeTarget* t) {
   1914   Sym name = pool_intern_slice(t->c->global, SLICE_LIT("__chkstk"));
   1915   ObjSymId s = obj_symbol_find(t->obj, name);
   1916   if (s != 0) return s;
   1917   return obj_symbol(t->obj, name, SB_GLOBAL, SK_UNDEF, OBJ_SEC_NONE, 0, 0);
   1918 }
   1919 
   1920 /* Build the prologue byte sequence into buf. Returns bytes written and, when
   1921  * the chkstk path fires, the disp32 offset of the call site. When `skip_sub` is
   1922  * set (the known-frame slim / red-zone tiers), the `sub rsp` reservation is
   1923  * omitted entirely: the frame record is established but no stack is reserved,
   1924  * either because the frame is empty (slim) or because the locals/saves live in
   1925  * the SysV red zone (redzone_leaf). Callers must only set it when the frame
   1926  * needs no reserved region (no alloca, no outgoing args, and — for the red
   1927  * zone — a leaf frame <= 128 bytes). */
   1928 static u32 x64_build_prologue(X64NativeTarget* a, u8* buf, u32 cap,
   1929                               u32 frame_size, const Reg* cs_int, u32 n_int,
   1930                               const Reg* cs_fp, u32 n_fp, int skip_sub,
   1931                               u32* chkstk_disp_pos_out) {
   1932   u32 wi = 0;
   1933   u32 xmm_base = x64_xmm_base(a, n_fp);
   1934   u32 i;
   1935   /* Page granularity for Windows large-frame probing (0 = no probe needed).
   1936    * Win64 reserves >1-page frames through __chkstk; the same ABI capability
   1937    * the aarch64 backend reads for its inline probe. */
   1938   u32 probe = abi_stack_probe_interval(a->base.c->abi);
   1939   *chkstk_disp_pos_out = (u32)-1;
   1940   if (cap < X64_PROLOGUE_BASE_BYTES)
   1941     x64_panic(a, "prologue placeholder overflow");
   1942   /* push rbp; mov rbp, rsp. */
   1943   buf[wi++] = (u8)(X64_OPC_PUSH_R | (X64_RBP & 7u));
   1944   buf[wi++] = X64_REX_BASE | X64_REX_W;
   1945   buf[wi++] = X64_OPC_MOV_RM_R;
   1946   buf[wi++] = modrm(3u, X64_RSP, X64_RBP);
   1947   /* sub rsp, frame_size (or chkstk on Win64 large frame); skipped by the slim /
   1948    * red-zone tiers, which reserve no stack. */
   1949   if (skip_sub) {
   1950     /* no reservation */
   1951   } else if (probe && frame_size > probe) {
   1952     if (wi + 13u > cap) x64_panic(a, "prologue placeholder overflow");
   1953     buf[wi++] = (u8)(X64_OPC_MOV_RI | (X64_RAX & 7u)); /* mov eax, imm32 */
   1954     wr_u32_le(buf + wi, frame_size);
   1955     wi += 4;
   1956     buf[wi++] = X64_OPC_CALL_REL32;
   1957     *chkstk_disp_pos_out = wi;
   1958     wr_u32_le(buf + wi, 0);
   1959     wi += 4;
   1960     buf[wi++] = X64_REX_BASE | X64_REX_W; /* sub rsp, rax */
   1961     buf[wi++] = X64_OPC_ALU_SUB;
   1962     buf[wi++] = modrm(3u, X64_RAX, X64_RSP);
   1963   } else {
   1964     if (wi + 7u > cap) x64_panic(a, "prologue placeholder overflow");
   1965     buf[wi++] = X64_REX_BASE | X64_REX_W;
   1966     buf[wi++] = X64_OPC_ALU_IMM32;
   1967     buf[wi++] = modrm(3u, X64_ALU_SUB_SUB, X64_RSP);
   1968     wr_u32_le(buf + wi, frame_size);
   1969     wi += 4;
   1970   }
   1971   /* sret: spill the first int arg reg (destination pointer) into its slot.
   1972    * Use the minimal disp encoding (x64_pack_mem) so it matches the body's
   1973    * frame stores and the matching epilogue restore — the `cc -S | as`
   1974    * round-trip can then reproduce these bytes exactly. The -O0 placeholder is
   1975    * NOP-padded to a fixed width, so a shorter prologue is harmless. */
   1976   if (a->has_sret && a->sret_ptr_slot != NATIVE_FRAME_SLOT_NONE) {
   1977     X64NativeSlot* s = x64_slot_get(a, a->sret_ptr_slot);
   1978     u32 sret_reg = a->abi->int_args[0];
   1979     i32 off = -(i32)s->off;
   1980     if (wi + 8u > cap) x64_panic(a, "prologue placeholder overflow");
   1981     buf[wi++] =
   1982         (u8)(X64_REX_BASE | X64_REX_W | ((sret_reg & 8u) ? X64_REX_R : 0u));
   1983     buf[wi++] = X64_OPC_MOV_RM_R;
   1984     wi += x64_pack_mem(buf + wi, sret_reg & 7u, X64_RBP, off);
   1985   }
   1986   /* Spill callee-saved GPRs. */
   1987   for (i = 0; i < n_int; ++i) {
   1988     u32 reg = cs_int[i];
   1989     i32 off = x64_cs_int_off(xmm_base, n_fp, i);
   1990     if (wi + 8u > cap) x64_panic(a, "prologue placeholder overflow");
   1991     buf[wi++] = (u8)(X64_REX_BASE | X64_REX_W | ((reg & 8u) ? X64_REX_R : 0u));
   1992     buf[wi++] = X64_OPC_MOV_RM_R;
   1993     wi += x64_pack_mem(buf + wi, reg & 7u, X64_RBP, off);
   1994   }
   1995   /* Spill callee-saved XMMs (Win64). movaps [rbp+disp], xmm. */
   1996   for (i = 0; i < n_fp; ++i) {
   1997     u32 xmm = cs_fp[i];
   1998     i32 off = x64_cs_fp_off(xmm_base, i);
   1999     u8 rex = (u8)((xmm & 8u) ? (X64_REX_BASE | X64_REX_R) : 0u);
   2000     u32 need = rex ? 9u : 8u;
   2001     if (wi + need > cap) x64_panic(a, "prologue placeholder overflow");
   2002     if (rex) buf[wi++] = rex;
   2003     buf[wi++] = X64_OPC_TWOBYTE;
   2004     buf[wi++] = 0x29; /* MOVAPS r/m128, xmm */
   2005     wi += x64_pack_mem(buf + wi, xmm & 7u, X64_RBP, off);
   2006   }
   2007   return wi;
   2008 }
   2009 
   2010 static u32 x64_signature_stack_bytes(NativeTarget* t, KitCgTypeId fn_type,
   2011                                      int* variadic, u32* nparams);
   2012 
   2013 static void x64_func_begin_common(NativeTarget* t, const CGFuncDesc* fd) {
   2014   X64NativeTarget* a = x64_of(t);
   2015   MCEmitter* mc = t->mc;
   2016   const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fd->fn_type);
   2017   a->func = fd;
   2018   a->loc = fd->loc;
   2019   a->abi = x64_abi_for_os(t->c->target.os);
   2020   /* Shared frame bookkeeping: clears the slot table, cum_off, max_outgoing,
   2021    * callee-save set, and known_frame/has_alloca/frame_final. */
   2022   native_frame_reset(&a->frame);
   2023   /* Tail-call legality is queried while the single-pass frontend is emitting
   2024    * the body, so derive the complete incoming stack window from the signature
   2025    * up front rather than waiting for bind_param to walk every declaration.
   2026    * x64_signature_stack_bytes includes Win64 shadow space; keep only the
   2027    * reusable argument suffix here because x64_no_tail adds the common prefix
   2028    * on both sides of its comparison. */
   2029   a->incoming_stack_size =
   2030       x64_signature_stack_bytes(t, fd->fn_type, NULL, NULL) -
   2031       a->abi->shadow_space;
   2032   a->next_param_int = 0;
   2033   a->next_param_fp = 0;
   2034   a->next_param_stack = 0;
   2035   a->has_sret = (abi && abi->has_sret) ? 1u : 0u;
   2036   a->is_variadic = (abi && abi->variadic) ? 1u : 0u;
   2037   a->sret_ptr_slot = NATIVE_FRAME_SLOT_NONE;
   2038   a->reg_save_slot = NATIVE_FRAME_SLOT_NONE;
   2039   a->npatches = 0;
   2040   a->nalloca = 0;
   2041   a->nbind_moves = 0;
   2042   a->slim_frame = 0;
   2043   a->redzone_leaf = 0;
   2044   /* Single-pass (-O0) reservation: the NDT caches only in its allocable pool's
   2045    * bounded callee-saved subset (3 int + 6 fp on Win64), so a far smaller
   2046    * placeholder than the optimizer-worst-case X64_PROLOGUE_BYTES suffices. The
   2047    * known-frame path overrides prologue_nbytes with its exact length. */
   2048   a->prologue_nbytes = a->abi->shadow_space ? X64_NDT_PROLOGUE_BYTES_WIN64
   2049                                             : X64_NDT_PROLOGUE_BYTES;
   2050 
   2051   mc_set_section(mc, fd->text_section_id);
   2052   mc_emit_align(mc, 16, X64_NOP1);
   2053   a->func_start = mc_pos(mc);
   2054   mc_begin_function(mc, fd->sym, fd->text_section_id, a->func_start);
   2055   mc_cfi_startproc(mc);
   2056   a->epilogue_label = mc_label_new(mc);
   2057 }
   2058 
   2059 /* Reserve the sret-pointer slot and (SysV) the 176-byte variadic reg-save
   2060  * area. Advances next_param_int past the sret pointer (a0). */
   2061 static void x64_reserve_entry_saves(X64NativeTarget* a) {
   2062   NativeTarget* t = &a->base;
   2063   if (a->has_sret) {
   2064     NativeFrameSlotDesc sd;
   2065     memset(&sd, 0, sizeof sd);
   2066     sd.type = builtin_id(KIT_CG_BUILTIN_I64);
   2067     sd.size = 8;
   2068     sd.align = 8;
   2069     sd.kind = NATIVE_FRAME_SLOT_SAVE;
   2070     a->sret_ptr_slot = t->frame_slot(t, &sd);
   2071     a->next_param_int = 1;
   2072   }
   2073   if (a->is_variadic && a->abi->emit_sysv_vararg_save) {
   2074     NativeFrameSlotDesc rd;
   2075     memset(&rd, 0, sizeof rd);
   2076     rd.type = builtin_id(KIT_CG_BUILTIN_I64);
   2077     rd.size = 176;
   2078     rd.align = 8;
   2079     rd.kind = NATIVE_FRAME_SLOT_SAVE;
   2080     a->reg_save_slot = t->frame_slot(t, &rd);
   2081   }
   2082 }
   2083 
   2084 static void x64_emit_variadic_reg_saves(X64NativeTarget* a) {
   2085   NativeTarget* t = &a->base;
   2086   MCEmitter* mc = t->mc;
   2087   if (!a->is_variadic) return;
   2088   if (a->abi->emit_sysv_vararg_save) {
   2089     X64NativeSlot* rs = x64_slot_get(a, a->reg_save_slot);
   2090     static const u32 gprs[6] = {X64_RDI, X64_RSI, X64_RDX,
   2091                                 X64_RCX, X64_R8,  X64_R9};
   2092     u32 i;
   2093     for (i = 0; i < 6u; ++i)
   2094       emit_mov_store(mc, 8, gprs[i], X64_RBP, -(i32)rs->off + (i32)(i * 8u));
   2095     for (i = 0; i < 8u; ++i)
   2096       emit_sse_store(mc, 0xF2, 0x11, (u32)(X64_XMM0 + i), X64_RBP,
   2097                      -(i32)rs->off + (i32)(48u + i * 16u));
   2098     return;
   2099   }
   2100   /* Win64 variadic: spill the 4 GPR arg slots to the home space. */
   2101   emit_mov_store(mc, 8, X64_RCX, X64_RBP, 16);
   2102   emit_mov_store(mc, 8, X64_RDX, X64_RBP, 24);
   2103   emit_mov_store(mc, 8, X64_R8, X64_RBP, 32);
   2104   emit_mov_store(mc, 8, X64_R9, X64_RBP, 40);
   2105 }
   2106 
   2107 static void x64_func_begin(NativeTarget* t, const CGFuncDesc* fd) {
   2108   X64NativeTarget* a = x64_of(t);
   2109   MCEmitter* mc = t->mc;
   2110   u32 i;
   2111   x64_func_begin_common(t, fd);
   2112   a->prologue_pos = mc_pos(mc);
   2113   for (i = 0; i < a->prologue_nbytes; ++i) emit1(mc, X64_NOP1);
   2114   x64_reserve_entry_saves(a);
   2115   x64_emit_variadic_reg_saves(a);
   2116 }
   2117 
   2118 /* x64 homes callee-saves below the locals (offsets computed in
   2119  * x64_compute_frame_size / x64_build_prologue), not in frame slots, so
   2120  * alloc_slots=0: native_frame just records the {reg,cls} set from the masks. */
   2121 static void x64_reserve_callee_saves(NativeTarget* t, const u32* used,
   2122                                      u32 nclasses) {
   2123   native_frame_set_callee_saves(&x64_of(t)->frame, used, nclasses, NULL, 0, 0);
   2124 }
   2125 
   2126 static int x64_reg_is_callee_int(const X64ABIRegs* abi, Reg r);
   2127 static int x64_reg_is_callee_fp(const X64ABIRegs* abi, Reg r);
   2128 
   2129 static u32 x64_live_callee_saved_mask(NativeTarget* t, NativeAllocClass cls) {
   2130   X64NativeTarget* a = x64_of(t);
   2131   const X64ABIRegs* abi = a->abi ? a->abi : x64_abi_for_os(t->c->target.os);
   2132   u32 mask = 0;
   2133   for (Reg r = 0; r < 16u; ++r) {
   2134     if (cls == NATIVE_REG_INT && x64_reg_is_callee_int(abi, r)) mask |= 1u << r;
   2135     if (cls == NATIVE_REG_FP && x64_reg_is_callee_fp(abi, r)) mask |= 1u << r;
   2136   }
   2137   return mask;
   2138 }
   2139 
   2140 static u32 x64_live_caller_saved_mask(NativeTarget* t, NativeAllocClass cls) {
   2141   u32 caller_saved =
   2142       native_reg_info_flag_mask(t->regs, cls, NATIVE_REG_CALLER_SAVED);
   2143   return caller_saved & ~x64_live_callee_saved_mask(t, cls);
   2144 }
   2145 
   2146 static void x64_asm_clobber_masks(Compiler* c, SrcLoc loc, const Sym* clobbers,
   2147                                   u32 nclob, u32* int_mask, u32* fp_mask);
   2148 
   2149 /* abi_clobber_masks is shared as native_asm_abi_clobber_masks
   2150  * (cg/native_asm.h); it reads the target's live ABI masks. */
   2151 
   2152 /* Build the callee-saved set the prologue must preserve: the allocator-assigned
   2153  * callee-saved registers (frame->callee_saved_used) plus any an inline-asm
   2154  * block clobbers. The latter are opaque to the optimizer's operand scan, so it
   2155  * forwards the raw clobber names (frame->asm_clobbers) and the arch-neutral
   2156  * clobber-ABI sets (frame->asm_clobber_abi_sets); we resolve both into masks
   2157  * and keep only the callee-saved ones. x64_reg_is_callee_* follow the live ABI:
   2158  * they exclude rbp (handled by the prologue head) and keep the
   2159  * reserved-but-callee- saved scratch rbx/r12 (which the caller still expects
   2160  * preserved). This is the same register selection the per-block spill used,
   2161  * hoisted into the prologue. */
   2162 static int x64_asm_reg_is_callee_saved(NativeTarget* t, NativeAllocClass cls,
   2163                                        Reg r) {
   2164   X64NativeTarget* a = x64_of(t);
   2165   const X64ABIRegs* abi = a->abi ? a->abi : x64_abi_for_os(t->c->target.os);
   2166   return (cls == NATIVE_REG_INT && x64_reg_is_callee_int(abi, r)) ||
   2167          (cls == NATIVE_REG_FP && x64_reg_is_callee_fp(abi, r));
   2168 }
   2169 
   2170 static u32 x64_known_callee_saves(NativeTarget* t,
   2171                                   const NativeKnownFrameDesc* frame, u32* out,
   2172                                   u32 cap) {
   2173   X64NativeTarget* a = x64_of(t);
   2174   SrcLoc loc = a->func ? a->func->loc : (SrcLoc){0, 0, 0};
   2175   return native_asm_known_callee_saves(
   2176       t, loc, frame, out, cap, x64_asm_clobber_masks,
   2177       x64_asm_reg_is_callee_saved);
   2178 }
   2179 
   2180 /* Optimizer entry point: the full frame is supplied up front, so the prologue
   2181  * is emitted final the moment it is built — no NOP region, no func_end patch
   2182  * (x64_func_end skips patching when known_frame). x64_build_prologue emits the
   2183  * push rbp / sub rsp / sret spill / callee-save spills; the variadic
   2184  * register-save stores are emitted separately, as on the single-pass path. */
   2185 static void x64_func_begin_known_frame(NativeTarget* t, const CGFuncDesc* fd,
   2186                                        const NativeKnownFrameDesc* frame,
   2187                                        NativeFrameSlot* out_slots) {
   2188   X64NativeTarget* a = x64_of(t);
   2189   MCEmitter* mc = t->mc;
   2190   Reg cs_int[X64_MAX_CS_INT_REGS], cs_fp[X64_MAX_CS_FP_REGS];
   2191   u32 n_int, n_fp, frame_size, nbytes, chkstk_disp_pos, i;
   2192   u8 buf[X64_PROLOGUE_BYTES_WIN64];
   2193   x64_func_begin_common(t, fd);
   2194   a->frame.known_frame = 1;
   2195   if (frame) {
   2196     u32 cs[NATIVE_REG_CLASS_COUNT];
   2197     u32 ncs =
   2198         x64_known_callee_saves(t, frame, cs, NATIVE_REG_CLASS_COUNT);
   2199     a->frame.has_alloca = frame->has_alloca;
   2200     if (ncs) x64_reserve_callee_saves(t, cs, ncs);
   2201     for (i = 0; i < frame->nslots; ++i) {
   2202       NativeFrameSlot slot = x64_frame_slot(t, &frame->slots[i]);
   2203       if (out_slots) out_slots[i] = slot;
   2204     }
   2205     x64_reserve_entry_saves(a);
   2206     native_frame_note_outgoing(&a->frame, frame->max_outgoing);
   2207   }
   2208   /* Frame is final: size and offsets are settled, so emit the exact prologue.
   2209    */
   2210   n_int = x64_collect_int_saves(a, cs_int);
   2211   n_fp = x64_collect_fp_saves(a, cs_fp);
   2212   frame_size = x64_compute_frame_size(a, n_int, n_fp);
   2213   a->frame_size_final = frame_size;
   2214   /* Cost-model tier selection (mirrors aa64's aa_func_begin_known_frame): with
   2215    * the frame final before the body, choose the cheapest valid prologue shape.
   2216    * Both tiers keep the rbp record and only drop the `sub rsp`, so the
   2217    * epilogue/CFI/offset helpers are untouched. x64 needs no
   2218    * `fp_at_bottom`-style fold: `push rbp` already folds the sp-move into the
   2219    * store. */
   2220   a->slim_frame = a->frame.ncallee_saves == 0 && !a->frame.has_alloca &&
   2221                   a->frame.cum_off == 0 && a->frame.max_outgoing == 0;
   2222   /* redzone keeps locals below rsp in the red zone; exclude inline asm, which
   2223    * may issue a `call` (clobbering the red zone) the optimizer can't see. slim
   2224    * needs no such guard: it has no locals there and the return address lives on
   2225    * the stack at [rbp+8], not in a clobberable register. */
   2226   a->redzone_leaf =
   2227       !a->slim_frame &&
   2228       (t->disabled_backend_features & KIT_CG_BACKEND_RED_ZONE) == 0 &&
   2229       a->abi->shadow_space == 0 && frame && frame->is_leaf &&
   2230       !frame->has_asm && !a->frame.has_alloca && a->frame.max_outgoing == 0 &&
   2231       frame_size <= 128u;
   2232   a->prologue_pos = mc_pos(mc);
   2233   nbytes = x64_build_prologue(a, buf, sizeof buf, frame_size, cs_int, n_int,
   2234                               cs_fp, n_fp, a->slim_frame || a->redzone_leaf,
   2235                               &chkstk_disp_pos);
   2236   mc_emit_bytes(mc, buf, nbytes);
   2237   if (chkstk_disp_pos != (u32)-1) {
   2238     ObjSymId chk = x64_chkstk_sym(t);
   2239     mc_emit_reloc_at(mc, mc->section_id, a->prologue_pos + chkstk_disp_pos,
   2240                       R_X64_PLT32, chk, -4, 1, 0);
   2241   }
   2242   a->prologue_nbytes = nbytes; /* exact length: used for the CFI post offset */
   2243   x64_emit_variadic_reg_saves(a);
   2244   native_frame_set_final(&a->frame);
   2245 }
   2246 
   2247 static void x64_func_end(NativeTarget* t) {
   2248   X64NativeTarget* a = x64_of(t);
   2249   MCEmitter* mc = t->mc;
   2250   ObjBuilder* obj = t->obj;
   2251   ObjSecId sec = a->func->text_section_id;
   2252   Reg cs_int[X64_MAX_CS_INT_REGS], cs_fp[X64_MAX_CS_FP_REGS];
   2253   u32 n_int = x64_collect_int_saves(a, cs_int);
   2254   u32 n_fp = x64_collect_fp_saves(a, cs_fp);
   2255   u32 frame_size = x64_compute_frame_size(a, n_int, n_fp);
   2256   u32 xmm_base = x64_xmm_base(a, n_fp);
   2257   u32 end;
   2258   a->frame_size_final = frame_size;
   2259 
   2260   /* Epilogue. */
   2261   mc_label_place(mc, a->epilogue_label);
   2262   x64_emit_callee_restores(a);
   2263   emit_leave(mc);
   2264   emit_ret(mc);
   2265 
   2266   /* Patch the single-pass prologue placeholder. */
   2267   if (!a->frame.known_frame) {
   2268     u8 buf[X64_PROLOGUE_BYTES_WIN64];
   2269     u32 chkstk_disp_pos;
   2270     u32 nbytes;
   2271     u32 k;
   2272     for (k = 0; k < a->prologue_nbytes; ++k) buf[k] = X64_NOP1;
   2273     /* Single-pass path never selects a slim/red-zone tier (it cannot know the
   2274      * frame up front), so it always emits the full reservation. */
   2275     nbytes = x64_build_prologue(a, buf, a->prologue_nbytes, frame_size, cs_int,
   2276                                 n_int, cs_fp, n_fp, 0, &chkstk_disp_pos);
   2277     (void)nbytes;
   2278     obj_patch(obj, sec, a->prologue_pos, buf, a->prologue_nbytes);
   2279     if (chkstk_disp_pos != (u32)-1) {
   2280       ObjSymId chk = x64_chkstk_sym(t);
   2281       mc_emit_reloc_at(mc, sec, a->prologue_pos + chkstk_disp_pos, R_X64_PLT32,
   2282                         chk, -4, 1, 0);
   2283     }
   2284   }
   2285 
   2286   /* Patch alloca disp32s: lea dst, [rsp + max_outgoing]. */
   2287   {
   2288     u32 mo = align_up_u32(a->frame.max_outgoing, 16u);
   2289     u32 k;
   2290     for (k = 0; k < a->npatches; ++k) {
   2291       u8 dbuf[4];
   2292       wr_u32_le(dbuf, mo);
   2293       obj_patch(obj, sec, a->patches[k].pos, dbuf, 4);
   2294     }
   2295   }
   2296 
   2297   /* CFI: after the prologue, CFA = rbp + 16; rbp at cfa-16, ra at cfa-8. */
   2298   {
   2299     /* Body starts past the prologue. prologue_nbytes is the reserved NOP-region
   2300      * size on the single-pass path and the exact prologue length on the
   2301      * known-frame path (set in x64_func_begin_known_frame). */
   2302     u32 post = a->prologue_pos + a->prologue_nbytes;
   2303     u32 k;
   2304     mc_cfi_set_next_pc_offset(mc, post - a->func_start);
   2305     /* CFI register operands are DWARF numbers, which differ from the x86-64
   2306      * hardware encoding for rbp/rsp/rsi/rdi/rcx/rdx (e.g. rbp is HW 5 but
   2307      * DWARF 6). Map every hardware GPR through x64_dwarf_from_hw_gpr; rip's
   2308      * DWARF number (16) is already correct. */
   2309     mc_cfi_def_cfa(mc, x64_dwarf_from_hw_gpr(X64_RBP), 16);
   2310     mc_cfi_offset(mc, x64_dwarf_from_hw_gpr(X64_RBP), -16);
   2311     mc_cfi_offset(mc, 16u /* rip */, -8);
   2312     for (k = 0; k < n_int; ++k) {
   2313       i32 off = x64_cs_int_off(xmm_base, n_fp, k);
   2314       mc_cfi_offset(mc, x64_dwarf_from_hw_gpr(cs_int[k]), off);
   2315     }
   2316   }
   2317 
   2318   end = mc_pos(mc);
   2319   obj_symbol_define(obj, a->func->sym, sec, (u64)a->func_start,
   2320                     (u64)(end - a->func_start));
   2321   if (a->func->atomize)
   2322     obj_atom_define(obj, sec, a->func_start, end - a->func_start, a->func->sym,
   2323                     0);
   2324   if (mc->debug) debug_func_pc_range(mc->debug, sec, a->func_start, end);
   2325   mc_cfi_endproc(mc);
   2326   mc_end_function(mc);
   2327   a->func = NULL;
   2328 }
   2329 
   2330 /* ============================ params / ABI helpers
   2331  * ============================
   2332  */
   2333 
   2334 /* Win64 shares one arg-slot index across int and FP. Keep cursors in lockstep.
   2335  */
   2336 static void x64_sync_slot(const X64ABIRegs* abi, u32* next_int, u32* next_fp) {
   2337   u32 m;
   2338   if (!abi->slot_shared_int_fp) return;
   2339   m = *next_int > *next_fp ? *next_int : *next_fp;
   2340   *next_int = m;
   2341   *next_fp = m;
   2342 }
   2343 
   2344 static const ABIArgInfo* x64_param_abi(NativeTarget* t, const ABIFuncInfo* abi,
   2345                                        const NativeCallDesc* desc, u32 i,
   2346                                        ABIArgInfo* scratch) {
   2347   int variadic = abi && i >= abi->nparams;
   2348   if (abi && i < abi->nparams) return &abi->params[i];
   2349   (void)variadic;
   2350   memset(scratch, 0, sizeof *scratch);
   2351   scratch->kind = ABI_ARG_DIRECT;
   2352   scratch->nparts = 1;
   2353   scratch->parts = arena_zarray(t->c->tu, ABIArgPart, 1);
   2354   ((ABIArgPart*)scratch->parts)[0].cls =
   2355       cg_type_is_float(t->c, desc->args[i].type) ? ABI_CLASS_FP : ABI_CLASS_INT;
   2356   ((ABIArgPart*)scratch->parts)[0].loc = ABI_LOC_REG;
   2357   ((ABIArgPart*)scratch->parts)[0].size =
   2358       native_type_size(t, desc->args[i].type);
   2359   ((ABIArgPart*)scratch->parts)[0].align =
   2360       native_type_align(t, desc->args[i].type);
   2361   return scratch;
   2362 }
   2363 
   2364 static KitCgTypeId x64_part_scalar_type(const ABIArgPart* part) {
   2365   if (part->cls == ABI_CLASS_FP)
   2366     return part->size <= 4u ? builtin_id(KIT_CG_BUILTIN_F32)
   2367                             : builtin_id(KIT_CG_BUILTIN_F64);
   2368   switch (part->size) {
   2369     case 1u:
   2370       return builtin_id(KIT_CG_BUILTIN_I8);
   2371     case 2u:
   2372       return builtin_id(KIT_CG_BUILTIN_I16);
   2373     case 4u:
   2374       return builtin_id(KIT_CG_BUILTIN_I32);
   2375     default:
   2376       return builtin_id(KIT_CG_BUILTIN_I64);
   2377   }
   2378 }
   2379 
   2380 /* Is the whole DIRECT arg forced to the stack (not enough reg slots)? */
   2381 static int x64_direct_to_stack(const X64ABIRegs* abi, const ABIArgInfo* ai,
   2382                                u32 next_int, u32 next_fp) {
   2383   u32 need_int, need_fp;
   2384   x64_abi_direct_reg_need(ai, &need_int, &need_fp);
   2385   return next_int + need_int > abi->n_int_args ||
   2386          next_fp + need_fp > abi->n_fp_args;
   2387 }
   2388 
   2389 /* Raw outgoing argument bytes a call uses, including Win64 shadow space but
   2390  * excluding the final call-site alignment padding. */
   2391 static u32 x64_call_stack_raw_size(NativeTarget* t,
   2392                                    const NativeCallDesc* desc) {
   2393   const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, desc->fn_type);
   2394   const X64ABIRegs* aregs = x64_abi_for_os(t->c->target.os);
   2395   u32 next_int = (abi && abi->has_sret) ? 1u : 0u;
   2396   u32 next_fp = 0;
   2397   u32 stack = aregs->shadow_space;
   2398   u32 i;
   2399   x64_sync_slot(aregs, &next_int, &next_fp);
   2400   for (i = 0; i < desc->nargs; ++i) {
   2401     ABIArgInfo tmp;
   2402     const ABIArgInfo* ai = x64_param_abi(t, abi, desc, i, &tmp);
   2403     u32 p;
   2404     if (ai->kind == ABI_ARG_IGNORE) continue;
   2405     if (ai->kind == ABI_ARG_INDIRECT) {
   2406       if (next_int < aregs->n_int_args)
   2407         ++next_int;
   2408       else
   2409         stack += 8u;
   2410       x64_sync_slot(aregs, &next_int, &next_fp);
   2411       continue;
   2412     }
   2413     if (ai->kind == ABI_ARG_DIRECT &&
   2414         x64_direct_to_stack(aregs, ai, next_int, next_fp)) {
   2415       stack += (u32)ai->nparts * 8u;
   2416       continue;
   2417     }
   2418     for (p = 0; p < ai->nparts; ++p) {
   2419       const ABIArgPart* part = &ai->parts[p];
   2420       if (part->cls == ABI_CLASS_FP) {
   2421         if (next_fp < aregs->n_fp_args)
   2422           ++next_fp;
   2423         else
   2424           stack += 8u;
   2425       } else {
   2426         if (next_int < aregs->n_int_args)
   2427           ++next_int;
   2428         else
   2429           stack += 8u;
   2430       }
   2431       x64_sync_slot(aregs, &next_int, &next_fp);
   2432     }
   2433   }
   2434   return stack;
   2435 }
   2436 
   2437 /* Outgoing stack bytes a call uses (16-aligned), per the ABI. */
   2438 static u32 x64_call_stack_size(NativeTarget* t, const NativeCallDesc* desc) {
   2439   return align_up_u32(x64_call_stack_raw_size(t, desc), 16u);
   2440 }
   2441 
   2442 /* A normal indirect/byval argument can point into the caller's local frame,
   2443  * because that frame remains live through a call. A sibling call tears the
   2444  * frame down first, so every such payload needs stable storage in the reusable
   2445  * incoming argument window. Pack those payloads after the raw outgoing ABI
   2446  * slots (using their natural alignment); final call alignment may provide all
   2447  * the space, as in a packed three-byte Win64 argument after one stack slot. */
   2448 static u32 x64_tail_call_stack_size(NativeTarget* t,
   2449                                     const NativeCallDesc* desc) {
   2450   const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, desc->fn_type);
   2451   u32 payload = x64_call_stack_raw_size(t, desc);
   2452   u32 i;
   2453   for (i = 0; i < desc->nargs; ++i) {
   2454     ABIArgInfo tmp;
   2455     const ABIArgInfo* ai = x64_param_abi(t, abi, desc, i, &tmp);
   2456     u32 align, size;
   2457     if (ai->kind != ABI_ARG_INDIRECT) continue;
   2458     align = ai->indirect_align;
   2459     if (!align) align = native_type_align(t, desc->args[i].type);
   2460     size = native_type_size(t, desc->args[i].type);
   2461     payload = align_up_u32(payload, align ? align : 1u);
   2462     payload += size;
   2463   }
   2464   return align_up_u32(payload, 16u);
   2465 }
   2466 
   2467 static u32 x64_call_stack_bytes(NativeTarget* t, const NativeCallDesc* desc) {
   2468   return (desc->flags & CG_CALL_TAIL) ? x64_tail_call_stack_size(t, desc)
   2469                                       : x64_call_stack_size(t, desc);
   2470 }
   2471 
   2472 static u32 x64_signature_stack_bytes(NativeTarget* t, KitCgTypeId fn_type,
   2473                                      int* variadic, u32* nparams) {
   2474   const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fn_type);
   2475   NativeCallDesc d;
   2476   if (variadic) *variadic = abi ? (int)abi->variadic : 0;
   2477   if (nparams) *nparams = abi ? abi->nparams : 0u;
   2478   memset(&d, 0, sizeof d);
   2479   d.fn_type = fn_type;
   2480   d.nargs = abi ? abi->nparams : 0u;
   2481   if (d.nargs) d.args = arena_zarray(t->c->tu, NativeLoc, d.nargs);
   2482   return x64_call_stack_size(t, &d);
   2483 }
   2484 
   2485 /* Resolve a dereferenceable NativeLoc to its storage address. */
   2486 static NativeAddr x64_storage_addr(X64NativeTarget* a, NativeLoc loc,
   2487                                    u32 offset) {
   2488   NativeAddr addr;
   2489   if (!native_loc_storage_addr(loc, (i32)offset, &addr))
   2490     x64_panic(a, "location is not storage");
   2491   return addr;
   2492 }
   2493 
   2494 static void x64_move_part_reg(NativeTarget* t, NativeLoc dst, NativeLoc src,
   2495                               u32 size) {
   2496   MCEmitter* mc = t->mc;
   2497   int dfp = native_loc_is_fp(dst), sfp = native_loc_is_fp(src);
   2498   u32 rd = loc_reg(dst), rs = loc_reg(src);
   2499   if (dfp && sfp) {
   2500     if (size != 4u && size != 8u)
   2501       x64_panic(x64_of(t), "invalid FP register part width");
   2502     if (rd != rs) emit_sse_rr(mc, sse_scalar_prefix(size), 0x10, rd, rs);
   2503     return;
   2504   }
   2505   if (dfp && !sfp) {
   2506     if (size != 4u && size != 8u)
   2507       x64_panic(x64_of(t), "invalid GPR-to-FP part width");
   2508     emit_sse_rr_w(mc, 0x66, 0x6E, size == 8u, rd, rs);
   2509     return;
   2510   }
   2511   if (!dfp && sfp) {
   2512     if (size != 4u && size != 8u)
   2513       x64_panic(x64_of(t), "invalid FP-to-GPR part width");
   2514     emit_sse_rr_w(mc, 0x66, 0x7E, size == 8u, rs, rd);
   2515     return;
   2516   }
   2517   if (rd != rs) emit_mov_rr(mc, size > 4u, rd, rs);
   2518 }
   2519 
   2520 static void x64_load_part(NativeTarget* t, NativeLoc dst, NativeLoc src,
   2521                           u32 offset, u32 size) {
   2522   X64NativeTarget* a = x64_of(t);
   2523   if (src.kind == NATIVE_LOC_REG) {
   2524     x64_move_part_reg(t, dst, src, size);
   2525     return;
   2526   }
   2527   if (native_loc_addr_role(src) == NATIVE_LOC_ADDR_ROLE_VALUE) {
   2528     NativeAddr addr;
   2529     if (offset != 0u || size != t->c->target.ptr_size ||
   2530         !native_loc_address_value(src, &addr))
   2531       x64_panic(a, "split or invalid address-value argument");
   2532     x64_load_addr(t, dst, addr);
   2533     return;
   2534   }
   2535   if (native_loc_addr_role(src) == NATIVE_LOC_ADDR_ROLE_STORAGE) {
   2536     NativeAddr addr = x64_storage_addr(a, src, offset);
   2537     MemAccess mem = native_mem_for_type(t, dst.type, size);
   2538     addr.base_type = dst.type;
   2539     if (!x64_native_part_width(size) && size <= 8u)
   2540       x64_load_int_part(a, dst, addr, mem, size);
   2541     else
   2542       x64_emit_mem(a, 1, dst, addr, mem);
   2543     return;
   2544   }
   2545   if (src.kind == NATIVE_LOC_IMM) {
   2546     i64 part;
   2547     if (!native_loc_imm_part(src, offset, size, &part))
   2548       x64_panic(a, "invalid immediate argument part");
   2549     x64_emit_load_imm(t->mc, size > 4u, loc_reg(dst), part);
   2550     return;
   2551   }
   2552   x64_panic(a, "unsupported part source");
   2553 }
   2554 
   2555 static void x64_store_part(NativeTarget* t, NativeLoc dst, NativeLoc src,
   2556                            u32 offset, u32 size) {
   2557   X64NativeTarget* a = x64_of(t);
   2558   if (native_loc_addr_role(dst) == NATIVE_LOC_ADDR_ROLE_STORAGE) {
   2559     NativeAddr addr = x64_storage_addr(a, dst, offset);
   2560     MemAccess mem = native_mem_for_type(t, src.type, size);
   2561     addr.base_type = src.type;
   2562     if (!x64_native_part_width(size) && size <= 8u)
   2563       x64_store_int_part(a, addr, src, mem, size);
   2564     else
   2565       x64_emit_mem(a, 0, src, addr, mem);
   2566     return;
   2567   }
   2568   if (dst.kind == NATIVE_LOC_REG) {
   2569     x64_move_part_reg(t, dst, src, size);
   2570     return;
   2571   }
   2572   x64_panic(a, "unsupported part destination");
   2573 }
   2574 
   2575 static void x64_addr_of_loc(NativeTarget* t, NativeLoc dst, NativeLoc src) {
   2576   NativeAddr addr = x64_storage_addr(x64_of(t), src, 0);
   2577   x64_load_addr(t, dst, addr);
   2578 }
   2579 
   2580 typedef struct X64CallMarshalCtx {
   2581   u32 rsp_bias;
   2582   Reg cycle_scratch;
   2583   u8 tail;
   2584   u8 callee_parked;
   2585 } X64CallMarshalCtx;
   2586 
   2587 static void x64_store_outgoing_part(NativeTarget* t,
   2588                                     const X64CallMarshalCtx* call,
   2589                                     u32 stack_off, NativeLoc src, u32 size) {
   2590   X64NativeTarget* a = x64_of(t);
   2591   NativeAddr addr;
   2592   memset(&addr, 0, sizeof addr);
   2593   addr.base_kind = NATIVE_ADDR_BASE_REG;
   2594   addr.base_type = src.type;
   2595   if (call->tail) {
   2596     /* A sibling call reuses the caller's frame: its outgoing stack args land in
   2597      * the caller's incoming-arg window. `stack_off` already includes the
   2598      * shadow-space prefix (the outgoing cursor starts at shadow_space), so the
   2599      * window address is [rbp + 16 + stack_off] — the same bytes the tail-callee
   2600      * reads once `leave` has restored rsp to the return address. */
   2601     addr.base.reg = X64_RBP;
   2602     addr.offset = (i32)(16u + stack_off);
   2603   } else {
   2604     addr.base.reg = X64_RSP;
   2605     /* Keep semantic outgoing offsets anchored at the pre-marshalling rsp. The
   2606      * per-call context owns any transient callee-save bias explicitly. */
   2607     addr.offset = (i32)(stack_off + call->rsp_bias);
   2608   }
   2609   {
   2610     MemAccess mem = native_mem_for_type(t, src.type, size);
   2611     if (!x64_native_part_width(size) && size <= 8u)
   2612       x64_store_int_part(a, addr, src, mem, size);
   2613     else
   2614       x64_emit_mem(a, 0, src, addr, mem);
   2615   }
   2616 }
   2617 
   2618 /* A stack carrier contains the same byte representation as aggregate storage,
   2619  * so copy a non-power-of-two memory part chunk-by-chunk instead of needlessly
   2620  * packing it into a GPR and unpacking it again. Besides being smaller, this
   2621  * needs only one private register while an indirect callee may occupy r11. */
   2622 static void x64_copy_outgoing_part(NativeTarget* t,
   2623                                    const X64CallMarshalCtx* call, u32 stack_off,
   2624                                    NativeLoc src,
   2625                                    const ABIArgPart* part) {
   2626   NativeAllocClass cls =
   2627       part->cls == ABI_CLASS_FP ? NATIVE_REG_FP : NATIVE_REG_INT;
   2628   KitCgTypeId pty = x64_part_scalar_type(part);
   2629   Reg tmp = cls == NATIVE_REG_FP ? X64_TMP_FP : X64_TMP_INT;
   2630   NativeLoc tmpreg = native_loc_reg(pty, cls, tmp);
   2631 
   2632   if (!x64_native_part_width(part->size) && part->size <= 8u &&
   2633       native_loc_addr_role(src) == NATIVE_LOC_ADDR_ROLE_STORAGE) {
   2634     NativePartChunkIter chunks = native_part_chunks(part->size, 8u);
   2635     u32 chunk_off, chunk_size;
   2636     while (native_part_chunk_next(&chunks, &chunk_off, &chunk_size)) {
   2637       x64_load_part(t, tmpreg, src, part->src_offset + chunk_off, chunk_size);
   2638       x64_store_outgoing_part(t, call, stack_off + chunk_off, tmpreg,
   2639                               chunk_size);
   2640     }
   2641     return;
   2642   }
   2643 
   2644   x64_load_part(t, tmpreg, src, part->src_offset, part->size);
   2645   x64_store_outgoing_part(t, call, stack_off, tmpreg, part->size);
   2646 }
   2647 
   2648 /* NativeTarget bind_param: route incoming param (ABI loc) into dst. */
   2649 static void x64_emit_reg_arg_moves(NativeTarget* t, NativeArgMove* moves, u32 n,
   2650                                    Reg int_scratch);
   2651 
   2652 /* Defer a register-destination param bind for the parallel-copy flush in
   2653  * x64_bind_params_end. `src` is the incoming location (an arg register, or a
   2654  * NATIVE_LOC_ADDR for an incoming stack slot). */
   2655 static void x64_defer_reg_bind(X64NativeTarget* a, NativeLoc dst, NativeLoc src,
   2656                                u32 size) {
   2657   NativeArgMove* m;
   2658   if (a->nbind_moves >= X64_MAX_BIND_MOVES)
   2659     x64_panic(a, "too many register parameter binds");
   2660   m = &a->bind_moves[a->nbind_moves++];
   2661   memset(m, 0, sizeof *m);
   2662   m->dst = dst;
   2663   m->src = src;
   2664   m->size = size;
   2665 }
   2666 
   2667 /* Incoming stack-arg source as a NATIVE_LOC_ADDR ([rbp + bias + stack_off]). */
   2668 static NativeLoc x64_incoming_stack_loc(KitCgTypeId type, NativeAllocClass cls,
   2669                                         i32 off) {
   2670   NativeLoc l;
   2671   memset(&l, 0, sizeof l);
   2672   l.kind = NATIVE_LOC_ADDR;
   2673   l.cls = (u8)cls;
   2674   l.type = type;
   2675   l.v.addr.base_kind = NATIVE_ADDR_BASE_REG;
   2676   l.v.addr.base.reg = X64_RBP;
   2677   l.v.addr.base_type = type;
   2678   l.v.addr.offset = off;
   2679   return l;
   2680 }
   2681 
   2682 static void x64_bind_native_param(NativeTarget* t, const CGParamDesc* p,
   2683                                   NativeLoc dst) {
   2684   X64NativeTarget* a = x64_of(t);
   2685   const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, a->func->fn_type);
   2686   const ABIArgInfo* ai =
   2687       p->index < abi->nparams ? &abi->params[p->index] : NULL;
   2688   int to_reg = dst.kind == NATIVE_LOC_REG;
   2689   /* Incoming stack args sit above the saved rbp + return addr (+16); Win64
   2690    * additionally reserves 32B of home space. */
   2691   i32 incoming_bias = (i32)(16u + a->abi->shadow_space);
   2692   u32 i;
   2693   if (!ai || ai->kind == ABI_ARG_IGNORE) return;
   2694 
   2695   if (ai->kind == ABI_ARG_INDIRECT) {
   2696     /* Incoming pointer to a byval copy: load pointer, memcpy into dst frame. */
   2697     u32 ptr_reg;
   2698     NativeAddr d_addr, from;
   2699     AggregateAccess access;
   2700     if (a->next_param_int < a->abi->n_int_args) {
   2701       ptr_reg = a->abi->int_args[a->next_param_int++];
   2702     } else {
   2703       /* Keep a stack-homed byval pointer out of R10/R11 while copying from
   2704        * it. x64_copy_bytes owns that private bank for transfer/address
   2705        * resolution; in particular, resolving the frame destination through
   2706        * R11 must not overwrite the source pointer between packed chunks. RAX
   2707        * is reserved from allocation and has no live entry value here. */
   2708       ptr_reg = X64_RAX;
   2709       emit_mov_load(t->mc, 8, 0, ptr_reg, X64_RBP,
   2710                     incoming_bias + (i32)a->next_param_stack);
   2711       a->next_param_stack += 8u;
   2712     }
   2713     x64_sync_slot(a->abi, &a->next_param_int, &a->next_param_fp);
   2714     if (dst.kind != NATIVE_LOC_FRAME)
   2715       x64_panic(a, "indirect parameter requires a frame destination");
   2716     memset(&d_addr, 0, sizeof d_addr);
   2717     d_addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   2718     d_addr.base.frame = dst.v.frame;
   2719     d_addr.base_type = p->type;
   2720     memset(&from, 0, sizeof from);
   2721     from.base_kind = NATIVE_ADDR_BASE_REG;
   2722     from.base.reg = ptr_reg;
   2723     from.base_type = p->type;
   2724     memset(&access, 0, sizeof access);
   2725     access.type = p->type;
   2726     access.size = p->size ? p->size : (u32)cg_type_size(t->c, p->type);
   2727     access.align = p->align ? p->align : native_type_align(t, p->type);
   2728     x64_copy_bytes(t, d_addr, from, access);
   2729     return;
   2730   }
   2731 
   2732   if (ai->kind == ABI_ARG_DIRECT &&
   2733       x64_direct_to_stack(a->abi, ai, a->next_param_int, a->next_param_fp)) {
   2734     /* Whole arg on the stack. */
   2735     for (i = 0; i < ai->nparts; ++i) {
   2736       const ABIArgPart* part = &ai->parts[i];
   2737       NativeAllocClass cls =
   2738           part->cls == ABI_CLASS_FP ? NATIVE_REG_FP : NATIVE_REG_INT;
   2739       NativeLoc isrc = x64_incoming_stack_loc(
   2740           p->type, cls, incoming_bias + (i32)a->next_param_stack);
   2741       a->next_param_stack += 8u;
   2742       if (dst.kind == NATIVE_LOC_NONE) {
   2743         /* unused */
   2744       } else if (to_reg) {
   2745         /* Defer: a register dst may be another param's incoming reg. */
   2746         x64_defer_reg_bind(
   2747             a,
   2748             native_loc_reg(dst.type ? dst.type : p->type,
   2749                            (NativeAllocClass)dst.cls, (Reg)dst.v.reg),
   2750             isrc, part->size);
   2751       } else {
   2752         /* Frame dst: load to scratch then store (memory dst is never a cycle
   2753          * source, so emit eagerly — it only reads the incoming slot). */
   2754         Reg tmp = cls == NATIVE_REG_FP ? X64_TMP_FP : X64_TMP_INT;
   2755         NativeLoc tloc = native_loc_reg(p->type, cls, tmp);
   2756         x64_load_part(t, tloc, isrc, 0, part->size);
   2757         x64_store_part(
   2758             t, native_loc_stack(p->type, dst.v.frame, (i32)part->src_offset),
   2759             tloc, 0, part->size);
   2760       }
   2761     }
   2762     return;
   2763   }
   2764 
   2765   for (i = 0; i < ai->nparts; ++i) {
   2766     const ABIArgPart* part = &ai->parts[i];
   2767     NativeAllocClass cls =
   2768         part->cls == ABI_CLASS_FP ? NATIVE_REG_FP : NATIVE_REG_INT;
   2769     NativeLoc
   2770         src; /* incoming: arg register, or NATIVE_LOC_ADDR for a stack arg */
   2771     if (cls == NATIVE_REG_FP && a->next_param_fp < a->abi->n_fp_args) {
   2772       src = native_loc_reg(p->type, cls, (Reg)(X64_XMM0 + a->next_param_fp++));
   2773     } else if (cls == NATIVE_REG_INT &&
   2774                a->next_param_int < a->abi->n_int_args) {
   2775       src = native_loc_reg(p->type, cls, a->abi->int_args[a->next_param_int++]);
   2776     } else {
   2777       src = x64_incoming_stack_loc(p->type, cls,
   2778                                    incoming_bias + (i32)a->next_param_stack);
   2779       a->next_param_stack += 8u;
   2780     }
   2781     x64_sync_slot(a->abi, &a->next_param_int, &a->next_param_fp);
   2782     if (dst.kind == NATIVE_LOC_NONE) {
   2783       /* unused parameter; cursors advanced */
   2784     } else if (to_reg) {
   2785       /* Defer the register bind: the allocator may rotate params across the
   2786        * incoming arg registers, so a per-param move could clobber a register
   2787        * another bind still needs. x64_bind_params_end resolves them together as
   2788        * a parallel copy. */
   2789       x64_defer_reg_bind(
   2790           a,
   2791           native_loc_reg(dst.type ? dst.type : p->type,
   2792                          (NativeAllocClass)dst.cls, (Reg)dst.v.reg),
   2793           src, part->size);
   2794     } else if (src.kind == NATIVE_LOC_REG) {
   2795       x64_store_part(
   2796           t, native_loc_stack(p->type, dst.v.frame, (i32)part->src_offset), src,
   2797           0, part->size);
   2798     } else {
   2799       /* Stack source -> frame dst: load to scratch, then store. */
   2800       Reg tmp = cls == NATIVE_REG_FP ? X64_TMP_FP : X64_TMP_INT;
   2801       NativeLoc tloc = native_loc_reg(p->type, cls, tmp);
   2802       x64_load_part(t, tloc, src, 0, part->size);
   2803       x64_store_part(
   2804           t, native_loc_stack(p->type, dst.v.frame, (i32)part->src_offset),
   2805           tloc, 0, part->size);
   2806     }
   2807   }
   2808 }
   2809 
   2810 /* Flush the deferred register-destination param binds as a parallel copy (the
   2811  * shared scheduler breaks any cycle the allocator's rotation created through
   2812  * backend-private int/fp cycle temporaries). Frame-dst and indirect binds were emitted eagerly
   2813  * in bind_param — they only read incoming registers, so they precede this. */
   2814 static void x64_bind_params_end(NativeTarget* t) {
   2815   X64NativeTarget* a = x64_of(t);
   2816   /* No callee is staged during entry binds, so r11 is free as the cycle
   2817    * scratch. */
   2818   if (a->nbind_moves)
   2819     x64_emit_reg_arg_moves(t, a->bind_moves, a->nbind_moves, X64_TMP_INT2);
   2820   a->nbind_moves = 0;
   2821 }
   2822 
   2823 /* ============================ calls / returns ============================ */
   2824 
   2825 typedef NativeArgMove X64ArgMove;
   2826 
   2827 static void x64_emit_one_arg_move(NativeTarget* t, const NativeArgMove* m) {
   2828   if (m->is_addr) {
   2829     x64_addr_of_loc(t, m->dst, m->src);
   2830   } else {
   2831     x64_load_part(t, m->dst, m->src, m->src_offset, m->size);
   2832   }
   2833   if (m->dup_to_gpr) {
   2834     /* movq gpr, xmm: 66 REX.W 0F 7E /r (xmm in reg field). */
   2835     emit_sse_rr_w(t->mc, 0x66, 0x7E, 1, loc_reg(m->dst), m->dup_gpr);
   2836   }
   2837 }
   2838 
   2839 /* Parallel-copy register arg moves via the shared scheduler. `int_scratch` is
   2840  * the register used to break an integer cycle: normally r11, but rax when an
   2841  * indirect callee is staged in r11 (rax is never a SysV int arg register and
   2842  * the variadic AL count is written only after the moves). */
   2843 static void x64_emit_reg_arg_moves(NativeTarget* t, NativeArgMove* moves, u32 n,
   2844                                    Reg int_scratch) {
   2845   NativeArgShuffle s;
   2846   if (n > X64_MAX_REG_ARG_MOVES) x64_panic(x64_of(t), "too many register args");
   2847   memset(&s, 0, sizeof s);
   2848   s.t = t;
   2849   s.emit_one = x64_emit_one_arg_move;
   2850   s.reg_move = x64_move;
   2851   s.scratch[NATIVE_REG_INT] = int_scratch;
   2852   s.scratch[NATIVE_REG_FP] = X64_TMP_FP;
   2853   s.scratch_class_mask = (1u << NATIVE_REG_INT) | (1u << NATIVE_REG_FP);
   2854   native_arg_shuffle(&s, moves, n);
   2855 }
   2856 
   2857 static void x64_marshal_call(NativeTarget* t, const NativeCallDesc* desc,
   2858                              NativeCallPhase* plan) {
   2859   X64NativeTarget* a = x64_of(t);
   2860   const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, desc->fn_type);
   2861   const X64ABIRegs* aregs = a->abi ? a->abi : x64_abi_for_os(t->c->target.os);
   2862   NativeCallPhaseRet* rets;
   2863   X64CallMarshalCtx call;
   2864   KitCgTypeId i64t = builtin_id(KIT_CG_BUILTIN_I64);
   2865   int tail = (desc->flags & CG_CALL_TAIL) != 0;
   2866   /* Right-size the result scratch to the exact number of entries the ret loops
   2867    * below write: nparts on a DIRECT register return, 1 on the !abi fallback,
   2868    * 0 (NULL) otherwise (IGNORE / sret / no results). */
   2869   u32 nrets_cap = (abi && abi->ret.kind == ABI_ARG_DIRECT && desc->nresults)
   2870                       ? abi->ret.nparts
   2871                       : ((!abi && desc->nresults) ? 1u : 0u);
   2872   memset(plan, 0, sizeof *plan);
   2873   memset(&call, 0, sizeof call);
   2874   call.tail = (u8)tail;
   2875   call.cycle_scratch = X64_TMP_INT2;
   2876   rets = nrets_cap ? arena_zarray(t->c->tu, NativeCallPhaseRet, nrets_cap) : NULL;
   2877   plan->callee = desc->callee;
   2878   plan->rets = rets;
   2879   plan->flags = desc->flags;
   2880   plan->has_sret = abi && abi->has_sret;
   2881   plan->is_variadic = abi && abi->variadic;
   2882   plan->stack_arg_size = x64_call_stack_bytes(t, desc);
   2883   if (plan->stack_arg_size > a->frame.max_outgoing)
   2884     a->frame.max_outgoing = plan->stack_arg_size;
   2885   /* Argument transport owns r10 (part data), r11 (address stabilization), and
   2886    * rax (parallel-copy cycle breaking). A register-indirect callee therefore
   2887    * lives in a balanced stack home for the complete marshal phase, independent
   2888    * of its producer register and every allowed NativeLoc argument shape. */
   2889   if (plan->callee.kind == NATIVE_LOC_REG) {
   2890     if ((NativeAllocClass)plan->callee.cls != NATIVE_REG_INT)
   2891       x64_panic(a, "indirect callee is not in an integer register");
   2892     x64_push_reg(t->mc, (Reg)loc_reg(plan->callee));
   2893     call.rsp_bias = 8u;
   2894     call.callee_parked = 1u;
   2895     call.cycle_scratch = X64_RAX;
   2896     plan->callee = native_loc_reg(plan->callee.type, NATIVE_REG_INT, X64_R11);
   2897   }
   2898   {
   2899     u32 next_int = (abi && abi->has_sret) ? 1u : 0u;
   2900     u32 next_fp = 0, stack = aregs->shadow_space, nmoves = 0, i;
   2901     u32 tail_payload = tail ? x64_call_stack_raw_size(t, desc) : 0u;
   2902     u32 p;
   2903     X64ArgMove moves[X64_MAX_REG_ARG_MOVES];
   2904     x64_sync_slot(aregs, &next_int, &next_fp);
   2905     for (i = 0; i < desc->nargs; ++i) {
   2906       ABIArgInfo tmp;
   2907       const ABIArgInfo* ai = x64_param_abi(t, abi, desc, i, &tmp);
   2908       int variadic_arg = abi && i >= abi->nparams;
   2909       if (ai->kind == ABI_ARG_IGNORE) continue;
   2910       if (ai->kind == ABI_ARG_INDIRECT) {
   2911         NativeLoc src = desc->args[i];
   2912         if (tail) {
   2913           AggregateAccess access;
   2914           NativeAddr from, to;
   2915           u32 align = ai->indirect_align;
   2916           u32 size = native_type_size(t, src.type);
   2917           if (!align) align = native_type_align(t, src.type);
   2918           tail_payload = align_up_u32(tail_payload, align ? align : 1u);
   2919           src = x64_incoming_stack_loc(
   2920               src.type, NATIVE_REG_INT, (i32)(16u + tail_payload));
   2921           from = x64_storage_addr(a, desc->args[i], 0);
   2922           to = x64_storage_addr(a, src, 0);
   2923           memset(&access, 0, sizeof access);
   2924           access.type = src.type;
   2925           access.size = size;
   2926           access.align = align ? align : 1u;
   2927           x64_copy_bytes(t, to, from, access);
   2928           tail_payload += size;
   2929         }
   2930         if (next_int < aregs->n_int_args) {
   2931           X64ArgMove* m = &moves[nmoves++];
   2932           memset(m, 0, sizeof *m);
   2933           m->dst =
   2934               native_loc_reg(i64t, NATIVE_REG_INT, aregs->int_args[next_int++]);
   2935           m->src = src;
   2936           m->size = 8;
   2937           m->is_addr = 1;
   2938         } else {
   2939           NativeLoc ptr = native_loc_reg(i64t, NATIVE_REG_INT, X64_RAX);
   2940           x64_addr_of_loc(t, ptr, src);
   2941           x64_store_outgoing_part(t, &call, stack, ptr, 8);
   2942           stack += 8u;
   2943         }
   2944         x64_sync_slot(aregs, &next_int, &next_fp);
   2945         continue;
   2946       }
   2947       if (ai->kind == ABI_ARG_DIRECT &&
   2948           x64_direct_to_stack(aregs, ai, next_int, next_fp)) {
   2949         for (p = 0; p < ai->nparts; ++p) {
   2950           const ABIArgPart* part = &ai->parts[p];
   2951           x64_copy_outgoing_part(t, &call, stack, desc->args[i], part);
   2952           stack += 8u;
   2953         }
   2954         continue;
   2955       }
   2956       for (p = 0; p < ai->nparts; ++p) {
   2957         const ABIArgPart* part = &ai->parts[p];
   2958         NativeAllocClass cls =
   2959             part->cls == ABI_CLASS_FP ? NATIVE_REG_FP : NATIVE_REG_INT;
   2960         if (cls == NATIVE_REG_FP && next_fp < aregs->n_fp_args) {
   2961           X64ArgMove* m = &moves[nmoves++];
   2962           u32 slot = next_fp;
   2963           memset(m, 0, sizeof *m);
   2964           m->dst = native_loc_reg(desc->args[i].type, cls,
   2965                                   (Reg)(X64_XMM0 + next_fp++));
   2966           m->src = desc->args[i];
   2967           m->src_offset = part->src_offset;
   2968           m->size = part->size;
   2969           if (aregs->vararg_fp_dup_to_gpr && variadic_arg &&
   2970               slot < aregs->n_int_args) {
   2971             m->dup_to_gpr = 1;
   2972             m->dup_gpr = aregs->int_args[slot];
   2973           }
   2974           x64_sync_slot(aregs, &next_int, &next_fp);
   2975         } else if (cls == NATIVE_REG_INT && next_int < aregs->n_int_args) {
   2976           X64ArgMove* m = &moves[nmoves++];
   2977           memset(m, 0, sizeof *m);
   2978           m->dst = native_loc_reg(desc->args[i].type, cls,
   2979                                   aregs->int_args[next_int++]);
   2980           m->src = desc->args[i];
   2981           m->src_offset = part->src_offset;
   2982           m->size = part->size;
   2983           x64_sync_slot(aregs, &next_int, &next_fp);
   2984         } else {
   2985           x64_copy_outgoing_part(t, &call, stack, desc->args[i], part);
   2986           stack += 8u;
   2987           x64_sync_slot(aregs, &next_int, &next_fp);
   2988         }
   2989       }
   2990     }
   2991     /* If an indirect callee was staged in r11 above, the cycle scratch must
   2992      * avoid it; rax is free here (not an int arg reg; AL count comes later). */
   2993     x64_emit_reg_arg_moves(t, moves, nmoves, call.cycle_scratch);
   2994     if (abi && abi->has_sret) {
   2995       /* sret pointer in the first int-arg reg. A tail call forwards the
   2996        * caller's own incoming sret pointer (spilled at entry); otherwise pass
   2997        * the address of this call's result slot. */
   2998       NativeLoc sret = native_loc_reg(i64t, NATIVE_REG_INT, aregs->int_args[0]);
   2999       if (tail)
   3000         x64_load_part(t, sret, native_loc_stack(i64t, a->sret_ptr_slot, 0), 0,
   3001                       8);
   3002       else if (desc->nresults)
   3003         x64_addr_of_loc(t, sret, desc->results[0]);
   3004     }
   3005     /* Variadic call: AL = number of vector regs used. */
   3006     if (abi && abi->variadic)
   3007       x64_emit_load_imm(t->mc, 0, X64_RAX, (i64)next_fp);
   3008     if (call.callee_parked) {
   3009       x64_pop_reg(t->mc, X64_R11);
   3010       call.rsp_bias = 0u;
   3011       call.callee_parked = 0u;
   3012     }
   3013   }
   3014   /* Return value receipt. */
   3015   if (abi && abi->ret.kind == ABI_ARG_DIRECT && desc->nresults) {
   3016     u32 nr = 0, ni = 0, nf = 0;
   3017     static const u32 ret_int_regs[2] = {X64_RAX, X64_RDX};
   3018     u32 p;
   3019     for (p = 0; p < abi->ret.nparts; ++p) {
   3020       const ABIArgPart* part = &abi->ret.parts[p];
   3021       NativeAllocClass cls =
   3022           part->cls == ABI_CLASS_FP ? NATIVE_REG_FP : NATIVE_REG_INT;
   3023       KitCgTypeId pty = x64_part_scalar_type(part);
   3024       Reg rreg = cls == NATIVE_REG_FP ? (Reg)(X64_XMM0 + nf++)
   3025                                       : (Reg)ret_int_regs[ni++];
   3026       rets[nr].src = native_loc_reg(pty, cls, rreg);
   3027       rets[nr].dst = desc->results[0];
   3028       if (rets[nr].dst.kind == NATIVE_LOC_FRAME)
   3029         rets[nr].dst = native_loc_stack(pty, desc->results[0].v.frame,
   3030                                         (i32)part->src_offset);
   3031       else if (rets[nr].dst.kind == NATIVE_LOC_STACK) {
   3032         rets[nr].dst.v.stack.offset += (i32)part->src_offset;
   3033         rets[nr].dst.type = pty;
   3034       }
   3035       rets[nr].mem = native_mem_for_type(t, pty, part->size);
   3036       nr++;
   3037     }
   3038     plan->nrets = nr;
   3039   } else if (abi && abi->ret.kind == ABI_ARG_IGNORE) {
   3040     plan->nrets = 0;
   3041   } else if (!abi && desc->nresults) {
   3042     rets[0].src =
   3043         native_loc_reg(desc->results[0].type, NATIVE_REG_INT, X64_RAX);
   3044     rets[0].dst = desc->results[0];
   3045     rets[0].mem = native_mem_for_type(t, desc->results[0].type, 0);
   3046     plan->nrets = 1;
   3047   }
   3048 }
   3049 
   3050 /* Emit a sibling (tail) call: tear the frame down and jump (no call) to the
   3051  * callee. Outgoing args are already in arg regs / the caller's incoming-arg
   3052  * window. `leave` (mov rsp,rbp; pop rbp) restores the caller's rbp and leaves
   3053  * rsp at the return address — frame_size-independent, so no func_end patch. */
   3054 static void x64_emit_tail_site(NativeTarget* t, NativeLoc callee) {
   3055   X64NativeTarget* a = x64_of(t);
   3056   MCEmitter* mc = t->mc;
   3057   ObjSecId sec = mc->section_id;
   3058   /* Restore callee-saves before the frame teardown (O1 path; none at -O0).
   3059    * Their rbp-relative offsets are frame-size-independent, and the indirect
   3060    * callee was staged in r11 by marshal_call — a caller-saved scratch — so these
   3061    * restores never clobber it. Mirrors the x64_func_end epilogue. */
   3062   x64_emit_callee_restores(a);
   3063   emit_leave(mc);
   3064   if (callee.kind == NATIVE_LOC_GLOBAL) {
   3065     u8 op = X64_OPC_JMP_REL32;
   3066     u32 disp_pos;
   3067     mc_emit_bytes(mc, &op, 1);
   3068     disp_pos = mc_pos(mc);
   3069     emit_u32le(mc, 0);
   3070     mc_emit_reloc_at(mc, sec, disp_pos, R_X64_PLT32, callee.v.global.sym,
   3071                       callee.v.global.addend - 4, 1, 0);
   3072   } else if (callee.kind == NATIVE_LOC_REG) {
   3073     /* indirect callee was staged in r11 by marshal_call */
   3074     u32 r = loc_reg(callee);
   3075     x64_emit_indirect_rm(mc, r, 4u); /* jmp r/m, /4 */
   3076   } else {
   3077     x64_panic(a, "unsupported tail call target");
   3078   }
   3079 }
   3080 
   3081 static void x64_emit_call(NativeTarget* t, const NativeCallPhase* plan) {
   3082   MCEmitter* mc = t->mc;
   3083   ObjSecId sec = mc->section_id;
   3084   if (plan->flags & CG_CALL_TAIL) {
   3085     x64_emit_tail_site(t, plan->callee);
   3086     return;
   3087   }
   3088   if (plan->callee.kind == NATIVE_LOC_GLOBAL) {
   3089     u8 op = X64_OPC_CALL_REL32;
   3090     u32 disp_pos;
   3091     mc_emit_bytes(mc, &op, 1);
   3092     disp_pos = mc_pos(mc);
   3093     emit_u32le(mc, 0);
   3094     mc_emit_reloc_at(mc, sec, disp_pos, R_X64_PLT32, plan->callee.v.global.sym,
   3095                       plan->callee.v.global.addend - 4, 1, 0);
   3096     return;
   3097   }
   3098   if (plan->callee.kind == NATIVE_LOC_REG) {
   3099     u32 r = loc_reg(plan->callee);
   3100     x64_emit_indirect_rm(mc, r, 2u); /* call r/m, /2 */
   3101     return;
   3102   }
   3103   x64_panic(x64_of(t), "unsupported call target");
   3104 }
   3105 
   3106 static void x64_marshal_ret(NativeTarget* t, const CGFuncDesc* fd,
   3107                             const NativeLoc* value,
   3108                             NativeCallPhaseRet** out_rets, u32* out_nrets) {
   3109   X64NativeTarget* a = x64_of(t);
   3110   const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fd->fn_type);
   3111   NativeCallPhaseRet* rets = NULL;
   3112   u32 nr = 0;
   3113   if (value) rets = arena_zarray(t->c->tu, NativeCallPhaseRet, 4);
   3114   if (value && abi && abi->ret.kind == ABI_ARG_INDIRECT) {
   3115     /* sret: reload the destination pointer (spilled at entry) into rax and
   3116      * copy the source aggregate into [rax]. Keep it out of R10/R11: those are
   3117      * x64_copy_bytes' private transfer/address bank, and resolving a stack
   3118      * source through R11 would otherwise overwrite the destination. RAX is
   3119      * also the ABI's sret return register, so it can stay live through the
   3120      * copy and needs no reload afterwards. */
   3121     KitCgTypeId i64t = builtin_id(KIT_CG_BUILTIN_I64);
   3122     NativeLoc dstp = native_loc_reg(i64t, NATIVE_REG_INT, X64_RAX);
   3123     NativeLoc saved = native_loc_stack(i64t, a->sret_ptr_slot, 0);
   3124     NativeAddr dst_addr, src_addr;
   3125     AggregateAccess access;
   3126     x64_load_part(t, dstp, saved, 0, 8);
   3127     memset(&dst_addr, 0, sizeof dst_addr);
   3128     dst_addr.base_kind = NATIVE_ADDR_BASE_REG;
   3129     dst_addr.base.reg = X64_RAX;
   3130     dst_addr.base_type = value->type;
   3131     src_addr = x64_storage_addr(a, *value, 0);
   3132     src_addr.base_type = value->type;
   3133     memset(&access, 0, sizeof access);
   3134     access.type = value->type;
   3135     access.size = (u32)cg_type_size(t->c, value->type);
   3136     access.align = native_type_align(t, value->type);
   3137     x64_copy_bytes(t, dst_addr, src_addr, access);
   3138     *out_rets = NULL;
   3139     *out_nrets = 0;
   3140     return;
   3141   }
   3142   if (value && abi && abi->ret.kind == ABI_ARG_DIRECT) {
   3143     u32 ni = 0, nf = 0;
   3144     static const u32 ret_int_regs[2] = {X64_RAX, X64_RDX};
   3145     u32 p;
   3146     int exact_parts = 0;
   3147     for (p = 0; p < abi->ret.nparts; ++p)
   3148       if (abi->ret.parts[p].cls == ABI_CLASS_INT &&
   3149           !x64_native_part_width(abi->ret.parts[p].size))
   3150         exact_parts = 1;
   3151 
   3152     /* Generic write_phase materializes a register destination from its scalar
   3153      * type, so an I64 carrier for a 3/5/6/7-byte aggregate part would widen the
   3154      * source load to eight bytes. Once one exact part is present, marshal every
   3155      * return part here in ABI order: this preserves already-filled return
   3156      * registers and keeps each memory read bounded by ABIArgPart.size. */
   3157     if (exact_parts) {
   3158       for (p = 0; p < abi->ret.nparts; ++p) {
   3159         const ABIArgPart* part = &abi->ret.parts[p];
   3160         NativeAllocClass cls =
   3161             part->cls == ABI_CLASS_FP ? NATIVE_REG_FP : NATIVE_REG_INT;
   3162         KitCgTypeId pty = x64_part_scalar_type(part);
   3163         Reg rreg = cls == NATIVE_REG_FP ? (Reg)(X64_XMM0 + nf++)
   3164                                         : (Reg)ret_int_regs[ni++];
   3165         NativeLoc dst = native_loc_reg(pty, cls, rreg);
   3166         x64_load_part(t, dst, *value, part->src_offset, part->size);
   3167       }
   3168       *out_rets = NULL;
   3169       *out_nrets = 0;
   3170       return;
   3171     }
   3172 
   3173     for (p = 0; p < abi->ret.nparts; ++p) {
   3174       const ABIArgPart* part = &abi->ret.parts[p];
   3175       NativeAllocClass cls =
   3176           part->cls == ABI_CLASS_FP ? NATIVE_REG_FP : NATIVE_REG_INT;
   3177       KitCgTypeId pty = x64_part_scalar_type(part);
   3178       Reg rreg = cls == NATIVE_REG_FP ? (Reg)(X64_XMM0 + nf++)
   3179                                       : (Reg)ret_int_regs[ni++];
   3180       rets[nr].src = *value;
   3181       if (rets[nr].src.kind == NATIVE_LOC_FRAME)
   3182         rets[nr].src =
   3183             native_loc_stack(pty, value->v.frame, (i32)part->src_offset);
   3184       else if (rets[nr].src.kind == NATIVE_LOC_STACK) {
   3185         rets[nr].src.v.stack.offset += (i32)part->src_offset;
   3186         rets[nr].src.type = pty;
   3187       }
   3188       rets[nr].dst = native_loc_reg(pty, cls, rreg);
   3189       rets[nr].mem = native_mem_for_type(t, pty, part->size);
   3190       nr++;
   3191     }
   3192   } else if (value) {
   3193     rets[0].src = *value;
   3194     rets[0].dst = native_loc_reg(value->type, NATIVE_REG_INT, X64_RAX);
   3195     rets[0].mem = native_mem_for_type(t, value->type, 0);
   3196     nr = 1;
   3197   }
   3198   *out_rets = rets;
   3199   *out_nrets = nr;
   3200 }
   3201 
   3202 static void x64_ret(NativeTarget* t) {
   3203   X64NativeTarget* a = x64_of(t);
   3204   x64_jump(t, a->epilogue_label);
   3205 }
   3206 
   3207 /* ============================ alloca ============================ */
   3208 
   3209 static void x64_alloca(NativeTarget* t, NativeLoc dst, NativeLoc size,
   3210                        u32 align) {
   3211   X64NativeTarget* a = x64_of(t);
   3212   MCEmitter* mc = t->mc;
   3213   u32 rsz = loc_reg(size);
   3214   u32 rd = loc_reg(dst);
   3215   u32 al = align ? align : 16u;
   3216   if (al < 16u) al = 16u;
   3217   if (al > 16u) x64_panic(a, "alloca align > 16 not supported");
   3218   if (size.kind == NATIVE_LOC_IMM) {
   3219     u64 aligned = ((u64)size.v.imm + 15u) & ~(u64)15u;
   3220     if (aligned == 0) aligned = 16;
   3221     /* sub rsp, imm32. */
   3222     emit_rex(mc, 1, 0, 0, X64_RSP);
   3223     {
   3224       u8 buf[2] = {X64_OPC_ALU_IMM32, modrm(3u, X64_ALU_SUB_SUB, X64_RSP)};
   3225       mc_emit_bytes(mc, buf, 2);
   3226     }
   3227     emit_u32le(mc, (u32)aligned);
   3228   } else {
   3229     /* r10 = (size + 15) & ~15; sub rsp, r10. */
   3230     emit_lea(mc, X64_TMP_INT, rsz, 15);
   3231     emit_rex(mc, 1, 0, 0, X64_TMP_INT);
   3232     {
   3233       u8 buf[3] = {X64_OPC_ALU_IMM8,
   3234                    modrm(3u, X64_ALU_SUB_AND, X64_TMP_INT), 0xF0};
   3235       mc_emit_bytes(mc, buf, 3);
   3236     }
   3237     emit_alu_rr(mc, 1, X64_OPC_ALU_SUB, X64_RSP, X64_TMP_INT);
   3238   }
   3239   a->frame.has_alloca = 1;
   3240   /* lea dst, [rsp + max_outgoing] — disp32 patched in func_end. */
   3241   if (a->npatches == a->patches_cap) {
   3242     u32 cap = a->patches_cap ? a->patches_cap * 2u : 8u;
   3243     X64Patch* nb = arena_zarray(t->c->tu, X64Patch, cap);
   3244     if (a->patches) memcpy(nb, a->patches, sizeof(*nb) * a->npatches);
   3245     a->patches = nb;
   3246     a->patches_cap = cap;
   3247   }
   3248   emit_rex(mc, 1, rd, 0, X64_RSP);
   3249   {
   3250     u8 op = X64_OPC_LEA;
   3251     mc_emit_bytes(mc, &op, 1);
   3252   }
   3253   {
   3254     u8 mr = modrm(2u, rd & 7u, 4u);
   3255     mc_emit_bytes(mc, &mr, 1);
   3256   }
   3257   {
   3258     u8 s = sib(0u, 4u, X64_RSP);
   3259     mc_emit_bytes(mc, &s, 1);
   3260   }
   3261   a->patches[a->npatches].kind = X64_PATCH_ALLOCA;
   3262   a->patches[a->npatches].pos = mc_pos(mc);
   3263   a->npatches++;
   3264   a->nalloca++;
   3265   emit_u32le(mc, 0); /* placeholder disp32 */
   3266 }
   3267 
   3268 /* ============================ TLS ============================ */
   3269 
   3270 /* Win64 TLS Local-Exec (PE-COFF): TEB pointer -> _tls_index -> TLS block ->
   3271  * lea &sym@SECREL. R11 is scratch. */
   3272 static void x64_tls_addr_of_win64(NativeTarget* t, NativeLoc dst, ObjSymId sym,
   3273                                   i64 addend) {
   3274   MCEmitter* mc = t->mc;
   3275   u32 sec = mc->section_id;
   3276   u32 rd = loc_reg(dst);
   3277   /* (1) mov rd, gs:[0x58]. */
   3278   {
   3279     u8 gs = 0x65;
   3280     mc_emit_bytes(mc, &gs, 1);
   3281     emit_rex(mc, 1, rd, 0, 0);
   3282     {
   3283       u8 op = X64_OPC_MOV_R_RM;
   3284       mc_emit_bytes(mc, &op, 1);
   3285     }
   3286     {
   3287       u8 mr = modrm(0u, rd & 7u, 4u);
   3288       mc_emit_bytes(mc, &mr, 1);
   3289     }
   3290     {
   3291       u8 s = sib(0u, 4u, 5u);
   3292       mc_emit_bytes(mc, &s, 1);
   3293     }
   3294     emit_u32le(mc, 0x58u);
   3295   }
   3296   /* (2) mov r11d, [rip + _tls_index]. */
   3297   {
   3298     Sym idx_name = pool_intern_slice(t->c->global, SLICE_LIT("_tls_index"));
   3299     ObjSymId idx_sym = obj_symbol_find(t->obj, idx_name);
   3300     u8 rex_r, op, mr;
   3301     u32 disp_pos;
   3302     if (idx_sym == 0)
   3303       idx_sym =
   3304           obj_symbol(t->obj, idx_name, SB_GLOBAL, SK_UNDEF, OBJ_SEC_NONE, 0, 0);
   3305     rex_r = X64_REX_BASE | X64_REX_R;
   3306     mc_emit_bytes(mc, &rex_r, 1);
   3307     op = X64_OPC_MOV_R_RM;
   3308     mc_emit_bytes(mc, &op, 1);
   3309     mr = modrm(0u, 3u, 5u); /* r11&7, rip-rel */
   3310     mc_emit_bytes(mc, &mr, 1);
   3311     disp_pos = mc_pos(mc);
   3312     emit_u32le(mc, 0);
   3313     mc_emit_reloc_at(mc, sec, disp_pos, R_PC32, idx_sym, -4, 1, 0);
   3314   }
   3315   /* (3) mov rd, [rd + r11*8]. */
   3316   {
   3317     u8 rex = X64_REX_BASE | X64_REX_W | X64_REX_X;
   3318     u8 op;
   3319     if (rd & 8u) rex |= X64_REX_R | X64_REX_B;
   3320     mc_emit_bytes(mc, &rex, 1);
   3321     op = X64_OPC_MOV_R_RM;
   3322     mc_emit_bytes(mc, &op, 1);
   3323     if ((rd & 7u) == 5u) {
   3324       u8 mr = modrm(1u, rd & 7u, 4u);
   3325       u8 s = sib(3u, 3u, rd & 7u);
   3326       u8 zero = 0;
   3327       mc_emit_bytes(mc, &mr, 1);
   3328       mc_emit_bytes(mc, &s, 1);
   3329       mc_emit_bytes(mc, &zero, 1);
   3330     } else {
   3331       u8 mr = modrm(0u, rd & 7u, 4u);
   3332       u8 s = sib(3u, 3u, rd & 7u);
   3333       mc_emit_bytes(mc, &mr, 1);
   3334       mc_emit_bytes(mc, &s, 1);
   3335     }
   3336   }
   3337   /* (4) lea rd, [rd + sym@SECREL]. */
   3338   {
   3339     u8 rex = X64_REX_BASE | X64_REX_W;
   3340     u8 op;
   3341     u32 disp_pos;
   3342     if (rd & 8u) rex |= X64_REX_R | X64_REX_B;
   3343     mc_emit_bytes(mc, &rex, 1);
   3344     op = X64_OPC_LEA;
   3345     mc_emit_bytes(mc, &op, 1);
   3346     if ((rd & 7u) == 4u) {
   3347       u8 mr = modrm(2u, rd & 7u, 4u);
   3348       u8 s = sib(0u, 4u, rd & 7u);
   3349       mc_emit_bytes(mc, &mr, 1);
   3350       mc_emit_bytes(mc, &s, 1);
   3351     } else {
   3352       u8 mr = modrm(2u, rd & 7u, rd & 7u);
   3353       mc_emit_bytes(mc, &mr, 1);
   3354     }
   3355     disp_pos = mc_pos(mc);
   3356     emit_u32le(mc, 0);
   3357     mc_emit_reloc_at(mc, sec, disp_pos, R_COFF_SECREL, sym, addend, 1, 0);
   3358   }
   3359 }
   3360 
   3361 /* x86-64 Mach-O TLV (descriptor model): load the __thread_ptrs slot holding
   3362  * the TLV descriptor address into %rdi, then call the descriptor's resolver
   3363  * thunk (descriptor[0]); it returns the variable's storage address in %rax.
   3364  * Peer of arm64's aa_tls_addr_of descriptor path. The slot is addressed by an
   3365  * R_X64_TLV reloc (RIP-relative disp32) that the Mach-O linker routes through
   3366  * __thread_ptrs. The Apple TLV thunk preserves every register except %rax, so
   3367  * the only clobbers are %rdi (we load it) and %rax (the result) — modeled in
   3368  * x64_machine_op_clobbers so no value lives across the access in them. Any
   3369  * addend is a byte offset into the variable, applied to %rax after the call. */
   3370 static void x64_tls_addr_of_macho(NativeTarget* t, NativeLoc dst, ObjSymId sym,
   3371                                   i64 addend) {
   3372   MCEmitter* mc = t->mc;
   3373   u32 sec = mc->section_id;
   3374   u32 rd = loc_reg(dst);
   3375   u32 disp_pos;
   3376   /* movq sym@TLVP(%rip), %rdi  (48 8B 3D disp32). */
   3377   emit_rex(mc, 1, X64_RDI, 0, 0);
   3378   {
   3379     u8 op = X64_OPC_MOV_R_RM;
   3380     mc_emit_bytes(mc, &op, 1);
   3381   }
   3382   {
   3383     u8 mr = modrm(0u, X64_RDI & 7u, 5u); /* [rip + disp32] */
   3384     mc_emit_bytes(mc, &mr, 1);
   3385   }
   3386   disp_pos = mc_pos(mc);
   3387   emit_u32le(mc, 0);
   3388   mc_emit_reloc_at(mc, sec, disp_pos, R_X64_TLV, sym, -4, 1, 0);
   3389   /* callq *(%rdi)  (FF /2, mem [rdi]) -> %rax = &storage. */
   3390   {
   3391     u8 call[2] = {0xFFu, modrm(0u, 2u, X64_RDI & 7u)};
   3392     mc_emit_bytes(mc, call, 2);
   3393   }
   3394   /* %rax += addend (byte offset into the variable). */
   3395   if (addend) {
   3396     i32 a = (i32)addend;
   3397     if (a == addend) {
   3398       emit_rex(mc, 1, 0, 0, X64_RAX);
   3399       if (imm_fits_i8(a)) {
   3400         u8 buf[3] = {X64_OPC_ALU_IMM8, modrm(3u, X64_ALU_SUB_ADD, X64_RAX & 7u),
   3401                      (u8)a};
   3402         mc_emit_bytes(mc, buf, 3);
   3403       } else {
   3404         u8 buf[2] = {X64_OPC_ALU_IMM32,
   3405                      modrm(3u, X64_ALU_SUB_ADD, X64_RAX & 7u)};
   3406         mc_emit_bytes(mc, buf, 2);
   3407         emit_u32le(mc, (u32)a);
   3408       }
   3409     } else {
   3410       /* Wide addend: materialize in r11 then add. */
   3411       x64_emit_load_imm(mc, 1, X64_R11, addend);
   3412       emit_alu_rr(mc, 1, X64_OPC_ALU_ADD, X64_RAX, X64_R11);
   3413     }
   3414   }
   3415   if (rd != X64_RAX) emit_mov_rr(mc, 1, rd, X64_RAX);
   3416 }
   3417 
   3418 /* x86-64 TLS Local-Exec: mov rd, fs:0; lea rd, [rd + sym@tpoff]. */
   3419 static void x64_tls_addr_of(NativeTarget* t, NativeLoc dst, ObjSymId sym,
   3420                             i64 addend) {
   3421   MCEmitter* mc = t->mc;
   3422   u32 sec = mc->section_id;
   3423   u32 rd = loc_reg(dst);
   3424   u32 disp_pos;
   3425   if (obj_format_tls_via_descriptor(t->c)) {
   3426     x64_tls_addr_of_macho(t, dst, sym, addend);
   3427     return;
   3428   }
   3429   if (obj_format_tls_model(t->c) == OBJ_TLS_WINDOWS_TEB) {
   3430     x64_tls_addr_of_win64(t, dst, sym, addend);
   3431     return;
   3432   }
   3433   /* mov rd, fs:[0]. */
   3434   {
   3435     u8 fs = 0x64;
   3436     mc_emit_bytes(mc, &fs, 1);
   3437     emit_rex(mc, 1, rd, 0, 0);
   3438     {
   3439       u8 op = X64_OPC_MOV_R_RM;
   3440       mc_emit_bytes(mc, &op, 1);
   3441     }
   3442     {
   3443       u8 mr = modrm(0u, rd & 7u, 4u);
   3444       mc_emit_bytes(mc, &mr, 1);
   3445     }
   3446     {
   3447       u8 s = sib(0u, 4u, 5u);
   3448       mc_emit_bytes(mc, &s, 1);
   3449     }
   3450     emit_u32le(mc, 0);
   3451   }
   3452   /* lea rd, [rd + disp32@tpoff]. */
   3453   emit_rex(mc, 1, rd, 0, rd);
   3454   {
   3455     u8 op = X64_OPC_LEA;
   3456     mc_emit_bytes(mc, &op, 1);
   3457   }
   3458   if ((rd & 7u) == 4u) {
   3459     u8 mr = modrm(2u, rd & 7u, 4u);
   3460     u8 s = sib(0u, 4u, rd & 7u);
   3461     mc_emit_bytes(mc, &mr, 1);
   3462     mc_emit_bytes(mc, &s, 1);
   3463   } else {
   3464     u8 mr = modrm(2u, rd & 7u, rd & 7u);
   3465     mc_emit_bytes(mc, &mr, 1);
   3466   }
   3467   disp_pos = mc_pos(mc);
   3468   emit_u32le(mc, 0);
   3469   mc_emit_reloc_at(mc, sec, disp_pos, R_X64_TPOFF32, sym, addend, 0, 0);
   3470 }
   3471 
   3472 /* ============================ atomics ============================ */
   3473 
   3474 static void emit_lock_prefix(MCEmitter* mc) {
   3475   u8 b = 0xF0;
   3476   mc_emit_bytes(mc, &b, 1);
   3477 }
   3478 static void emit_mfence(MCEmitter* mc) {
   3479   u8 b[3] = {0x0F, 0xAE, 0xF0};
   3480   mc_emit_bytes(mc, b, 3);
   3481 }
   3482 
   3483 /* Resolve an atomic addr to a bare base register (r11) + disp 0. */
   3484 static u32 x64_atomic_base(X64NativeTarget* a, NativeAddr addr) {
   3485   return x64_addr_to_base_reg(a, addr, X64_TMP_INT2);
   3486 }
   3487 
   3488 static void x64_atomic_load(NativeTarget* t, NativeLoc dst, NativeAddr addr,
   3489                             MemAccess mem, KitCgMemOrder mo) {
   3490   X64NativeTarget* a = x64_of(t);
   3491   u32 sz = mem.size ? mem.size : loc_size32(t, dst);
   3492   u32 base;
   3493   (void)mo; /* x86 plain MOV is an acquire load. */
   3494   base = x64_atomic_base(a, addr);
   3495   emit_mov_load(t->mc, sz, 0, loc_reg(dst), base, 0);
   3496 }
   3497 
   3498 static void x64_atomic_store(NativeTarget* t, NativeAddr addr, NativeLoc src,
   3499                              MemAccess mem, KitCgMemOrder mo) {
   3500   X64NativeTarget* a = x64_of(t);
   3501   MCEmitter* mc = t->mc;
   3502   u32 sz = mem.size ? mem.size : loc_size32(t, src);
   3503   int w = sz == 8u ? 1 : 0;
   3504   u32 base = x64_atomic_base(a, addr);
   3505   u32 sr = loc_reg(src);
   3506   if (mo == KIT_CG_MO_SEQ_CST) {
   3507     /* XCHG writes the prior memory value into its register operand. Stage the
   3508      * semantic source in backend-private R10 so that mutation is invisible to
   3509      * the allocator; R11 holds a materialized address. */
   3510     if (base == X64_TMP_INT) {
   3511       emit_mov_rr(mc, 1, X64_TMP_INT2, base);
   3512       base = X64_TMP_INT2;
   3513     }
   3514     if (sr != X64_TMP_INT) emit_mov_rr(mc, w, X64_TMP_INT, sr);
   3515     emit_lock_prefix(mc);
   3516     emit_rex(mc, w, X64_TMP_INT, 0, base);
   3517     {
   3518       u8 op = 0x87; /* xchg r/m, r */
   3519       mc_emit_bytes(mc, &op, 1);
   3520     }
   3521     emit_mem_operand(mc, X64_TMP_INT, base, 0);
   3522     return;
   3523   }
   3524   emit_mov_store(mc, sz, sr, base, 0);
   3525 }
   3526 
   3527 static void x64_atomic_rmw(NativeTarget* t, KitCgAtomicOp op, NativeLoc dst,
   3528                            NativeAddr addr, NativeLoc val, MemAccess mem,
   3529                            KitCgMemOrder mo) {
   3530   X64NativeTarget* a = x64_of(t);
   3531   MCEmitter* mc = t->mc;
   3532   u32 sz = mem.size ? mem.size : loc_size32(t, dst);
   3533   int w = sz == 8u ? 1 : 0;
   3534   u32 base = x64_atomic_base(a, addr);
   3535   u32 dr = loc_reg(dst);
   3536   u32 vr = loc_reg(val);
   3537   (void)mo; /* LOCK ops are full barriers. */
   3538   /* The rmw uses fixed rax (prior), rcx (new), rdx (val); the optimizer may
   3539    * have materialized the address into one of them, so keep it out (R11 is
   3540    * backend-private, never an allocated operand). Stage before RDX is loaded.
   3541    */
   3542   if (base == X64_RAX || base == X64_RCX || base == X64_RDX) {
   3543     emit_mov_rr(mc, 1, X64_TMP_INT2, base);
   3544     base = X64_TMP_INT2;
   3545   }
   3546   /* val staged in rdx (rax/rcx used by the cmpxchg loop). */
   3547   emit_mov_rr(mc, w, X64_RDX, vr);
   3548   if (op == KIT_CG_ATOMIC_ADD || op == KIT_CG_ATOMIC_SUB) {
   3549     if (op == KIT_CG_ATOMIC_SUB) emit_f7_rm(mc, w, X64_F7_SUB_NEG, X64_RDX);
   3550     emit_lock_prefix(mc);
   3551     emit_rex(mc, w, X64_RDX, 0, base);
   3552     {
   3553       u8 op2[2] = {X64_OPC_TWOBYTE, 0xC1}; /* xadd */
   3554       mc_emit_bytes(mc, op2, 2);
   3555     }
   3556     emit_mem_operand(mc, X64_RDX, base, 0);
   3557     if (dr != X64_RDX) emit_mov_rr(mc, w, dr, X64_RDX);
   3558     return;
   3559   }
   3560   if (op == KIT_CG_ATOMIC_XCHG) {
   3561     emit_lock_prefix(mc);
   3562     emit_rex(mc, w, X64_RDX, 0, base);
   3563     {
   3564       u8 op2 = 0x87; /* xchg */
   3565       mc_emit_bytes(mc, &op2, 1);
   3566     }
   3567     emit_mem_operand(mc, X64_RDX, base, 0);
   3568     if (dr != X64_RDX) emit_mov_rr(mc, w, dr, X64_RDX);
   3569     return;
   3570   }
   3571   /* AND/OR/XOR/NAND: cmpxchg retry loop. rax=prior, rcx=new, rdx=val. */
   3572   {
   3573     MCLabel retry = mc_label_new(mc);
   3574     emit_mov_load(mc, sz, 0, X64_RAX, base, 0);
   3575     mc_label_place(mc, retry);
   3576     emit_mov_rr(mc, w, X64_RCX, X64_RAX);
   3577     switch (op) {
   3578       case KIT_CG_ATOMIC_AND:
   3579         emit_alu_rr(mc, w, X64_OPC_ALU_AND, X64_RCX, X64_RDX);
   3580         break;
   3581       case KIT_CG_ATOMIC_OR:
   3582         emit_alu_rr(mc, w, X64_OPC_ALU_OR, X64_RCX, X64_RDX);
   3583         break;
   3584       case KIT_CG_ATOMIC_XOR:
   3585         emit_alu_rr(mc, w, X64_OPC_ALU_XOR, X64_RCX, X64_RDX);
   3586         break;
   3587       case KIT_CG_ATOMIC_NAND:
   3588         emit_alu_rr(mc, w, X64_OPC_ALU_AND, X64_RCX, X64_RDX);
   3589         emit_f7_rm(mc, w, X64_F7_SUB_NOT, X64_RCX);
   3590         break;
   3591       default:
   3592         x64_panic(a, "unsupported atomic rmw op");
   3593     }
   3594     emit_lock_prefix(mc);
   3595     emit_rex(mc, w, X64_RCX, 0, base);
   3596     {
   3597       u8 op2[2] = {X64_OPC_TWOBYTE, 0xB1}; /* cmpxchg */
   3598       mc_emit_bytes(mc, op2, 2);
   3599     }
   3600     emit_mem_operand(mc, X64_RCX, base, 0);
   3601     emit_jcc_rel32(mc, X64_CC_NE, retry);
   3602     if (dr != X64_RAX) emit_mov_rr(mc, w, dr, X64_RAX);
   3603   }
   3604 }
   3605 
   3606 static void x64_atomic_cas(NativeTarget* t, NativeLoc prior, NativeLoc ok,
   3607                            NativeAddr addr, NativeLoc expected,
   3608                            NativeLoc desired, MemAccess mem,
   3609                            KitCgMemOrder success, KitCgMemOrder failure) {
   3610   X64NativeTarget* a = x64_of(t);
   3611   MCEmitter* mc = t->mc;
   3612   u32 sz = mem.size ? mem.size : loc_size32(t, prior);
   3613   int w = sz == 8u ? 1 : 0;
   3614   u32 base = x64_atomic_base(a, addr);
   3615   u32 rprior = loc_reg(prior);
   3616   u32 rok = loc_reg(ok);
   3617   u32 rexp = loc_reg(expected);
   3618   u32 rdes = loc_reg(desired);
   3619   (void)success;
   3620   (void)failure;
   3621   /* cmpxchg uses fixed rax (expected) and rcx (desired). The optimizer may have
   3622    * materialized the address into either; keep it out of both (R11 is
   3623    * backend-private, never an allocated operand). */
   3624   if (base == X64_RAX || base == X64_RCX) {
   3625     emit_mov_rr(mc, 1, X64_TMP_INT2, base);
   3626     base = X64_TMP_INT2;
   3627   }
   3628   /* Place expected -> rax and desired -> rcx as a parallel copy: the allocator
   3629    * may have them in each other's target register (full swap) or desired in rax
   3630    * (expected's target), either of which a naive two-move order would clobber.
   3631    */
   3632   if (rexp == X64_RCX && rdes == X64_RAX) {
   3633     /* Swap rax <-> rcx (xchg needs no temp; base is not rax/rcx here). */
   3634     emit_rex(mc, w, X64_RCX, 0, X64_RAX);
   3635     {
   3636       u8 xchg[2] = {0x87, modrm(3u, X64_RCX, X64_RAX)};
   3637       mc_emit_bytes(mc, xchg, 2);
   3638     }
   3639   } else if (rdes == X64_RAX) {
   3640     /* desired sits in rax; move it to rcx before rax is overwritten. */
   3641     if (rdes != X64_RCX) emit_mov_rr(mc, w, X64_RCX, rdes);
   3642     if (rexp != X64_RAX) emit_mov_rr(mc, w, X64_RAX, rexp);
   3643   } else {
   3644     if (rexp != X64_RAX) emit_mov_rr(mc, w, X64_RAX, rexp);
   3645     if (rdes != X64_RCX) emit_mov_rr(mc, w, X64_RCX, rdes);
   3646   }
   3647   emit_lock_prefix(mc);
   3648   emit_rex(mc, w, X64_RCX, 0, base);
   3649   {
   3650     u8 op2[2] = {X64_OPC_TWOBYTE, 0xB1}; /* cmpxchg [base], rcx */
   3651     mc_emit_bytes(mc, op2, 2);
   3652   }
   3653   emit_mem_operand(mc, X64_RCX, base, 0);
   3654   emit_setcc(mc, X64_CC_E, rok);
   3655   emit_movzx_r32_r8(mc, rok, rok);
   3656   if (rprior != X64_RAX) emit_mov_rr(mc, w, rprior, X64_RAX);
   3657 }
   3658 
   3659 static void x64_fence(NativeTarget* t, KitCgMemOrder mo) {
   3660   if (mo == KIT_CG_MO_SEQ_CST) emit_mfence(t->mc);
   3661 }
   3662 
   3663 /* ============================ variadics ============================
   3664  * SysV: __va_list_tag (gp_offset@0, fp_offset@4, overflow@8, reg_save@16). The
   3665  * prologue filled the 176B reg-save area. Win64: va_list is a single pointer
   3666  * to the next 8-byte slot in the home/overflow area; FP varargs are duplicated
   3667  * into the matching GPR slot at the call site. `ap` addresses the va_list
   3668  * object. */
   3669 
   3670 /* Resolve a va_list address into `scratch`, materializing it there if it is not
   3671  * already, so the backend-private field-value register never aliases it. */
   3672 static u32 x64_va_base(X64NativeTarget* a, NativeAddr ap, u32 scratch) {
   3673   u32 base = x64_addr_to_base_reg(a, ap, scratch);
   3674   if (base != scratch) {
   3675     emit_mov_rr(a->base.mc, 1, scratch, base);
   3676     base = scratch;
   3677   }
   3678   return base;
   3679 }
   3680 
   3681 /* add r/m, imm8 (group-1 /0) directly to a memory field — advances a va_list
   3682  * offset/pointer in place without consuming a register. w selects 64- vs
   3683  * 32-bit. */
   3684 static void x64_add_mem_imm(MCEmitter* mc, int w, u32 base, i32 disp, i8 imm) {
   3685   u8 op = X64_OPC_ALU_IMM8;
   3686   u8 b;
   3687   emit_rex(mc, w, 0, 0, base);
   3688   mc_emit_bytes(mc, &op, 1);
   3689   emit_mem_operand(mc, X64_ALU_SUB_ADD, base, disp); /* modrm.reg = /0 (ADD) */
   3690   b = (u8)imm;
   3691   mc_emit_bytes(mc, &b, 1);
   3692 }
   3693 
   3694 /* add r64, [base+disp] (0x03 /r). */
   3695 static void x64_add_reg_mem(MCEmitter* mc, u32 dst, u32 base, i32 disp) {
   3696   u8 op = 0x03;
   3697   emit_rex(mc, 1, dst, 0, base);
   3698   mc_emit_bytes(mc, &op, 1);
   3699   emit_mem_operand(mc, dst, base, disp);
   3700 }
   3701 
   3702 static void x64_va_start_core(X64NativeTarget* a, NativeAddr ap) {
   3703   NativeTarget* t = &a->base;
   3704   MCEmitter* mc = t->mc;
   3705   u32 ap_base;
   3706   if (!a->is_variadic) x64_panic(a, "va_start: function not variadic");
   3707   ap_base = x64_va_base(a, ap, X64_TMP_INT2);
   3708   if (a->abi->shadow_space) {
   3709     /* Win64: *ap = rbp + 16 + named_int*8 + named_stack. */
   3710     u32 first = 16u + a->next_param_int * 8u + a->next_param_stack;
   3711     emit_lea(mc, X64_TMP_INT, X64_RBP, (i32)first);
   3712     emit_mov_store(mc, 8, X64_TMP_INT, ap_base, 0);
   3713     return;
   3714   }
   3715   {
   3716     X64NativeSlot* rs = x64_slot_get(a, a->reg_save_slot);
   3717     /* gp_offset = next_param_int * 8 */
   3718     x64_emit_load_imm(mc, 0, X64_TMP_INT, (i64)(a->next_param_int * 8u));
   3719     emit_mov_store(mc, 4, X64_TMP_INT, ap_base, 0);
   3720     /* fp_offset = 48 + next_param_fp * 16 */
   3721     x64_emit_load_imm(mc, 0, X64_TMP_INT,
   3722                       (i64)(48u + a->next_param_fp * 16u));
   3723     emit_mov_store(mc, 4, X64_TMP_INT, ap_base, 4);
   3724     /* overflow_arg_area = rbp + 16 + next_param_stack */
   3725     emit_lea(mc, X64_TMP_INT, X64_RBP,
   3726              (i32)(16u + a->next_param_stack));
   3727     emit_mov_store(mc, 8, X64_TMP_INT, ap_base, 8);
   3728     /* reg_save_area = rbp - reg_save_slot.off */
   3729     emit_lea(mc, X64_TMP_INT, X64_RBP, -(i32)rs->off);
   3730     emit_mov_store(mc, 8, X64_TMP_INT, ap_base, 16);
   3731   }
   3732 }
   3733 
   3734 static void x64_va_arg_core(X64NativeTarget* a, NativeLoc dst, NativeAddr ap,
   3735                             KitCgTypeId type) {
   3736   NativeTarget* t = &a->base;
   3737   MCEmitter* mc = t->mc;
   3738   u32 sz = native_type_size(t, type);
   3739   int is_fp = native_loc_is_fp(dst);
   3740   u32 dr = loc_reg(dst);
   3741   u32 ap_base = x64_va_base(a, ap, X64_TMP_INT2); /* r11 */
   3742   /* GPR temporary for the offset/address arithmetic. For integer results the
   3743    * destination is itself a throwaway operand temp — pass_native_emit fetches
   3744    * va_arg into a scoped register and writes it to the real destination — so
   3745    * we reuse `dr` and touch no allocable register at all. FP results keep their
   3746    * value in an XMM register, so they borrow backend-private R10.
   3747    * Either way only r11 (ap_base) and `gp` are used: the va_list fields are
   3748    * advanced in memory (x64_add_mem_imm) and the reg-save base is folded in
   3749    * with x64_add_reg_mem, so no third register is needed. */
   3750   u32 gp = is_fp ? X64_TMP_INT : dr;
   3751   if (a->abi->shadow_space) {
   3752     /* Win64: gp = *ap; load dr from [gp]; *ap += 8. */
   3753     emit_mov_load(mc, 8, 0, gp, ap_base, 0);
   3754     if (is_fp)
   3755       emit_sse_load(mc, sse_scalar_prefix(sz), 0x10, dr, gp, 0);
   3756     else
   3757       emit_mov_load(mc, sz, 0, dr, gp, 0);
   3758     x64_add_mem_imm(mc, 1, ap_base, 0, 8);
   3759     return;
   3760   }
   3761   {
   3762     u32 offs_field = is_fp ? 4u : 0u;
   3763     u32 max_offs = is_fp ? 176u : 48u;
   3764     i8 stride = is_fp ? 16 : 8;
   3765     MCLabel L_stack = mc_label_new(mc);
   3766     MCLabel L_done = mc_label_new(mc);
   3767     /* gp32 = ap[offs]; cmp gp32, max; jae L_stack. Use the imm8 form when the
   3768      * threshold fits (gp_offset max 48) so the encoding is canonical and the
   3769      * `cc -S | as` round-trip reproduces it; fp_offset max 176 needs imm32. */
   3770     emit_mov_load(mc, 4, 0, gp, ap_base, (i32)offs_field);
   3771     if (imm_fits_i8((i64)max_offs))
   3772       emit_alu_imm8(mc, 0, X64_ALU_SUB_CMP, gp, (i8)max_offs);
   3773     else
   3774       emit_alu_imm32(mc, 0, X64_ALU_SUB_CMP, gp, (i32)max_offs);
   3775     emit_jcc_rel32(mc, X64_CC_AE, L_stack);
   3776     /* reg path: ap[offs] += stride; gp = reg_save_area(ap[16]) + offset; load.
   3777      * (The memory increment leaves gp holding the old offset.) */
   3778     x64_add_mem_imm(mc, 0, ap_base, (i32)offs_field, stride);
   3779     x64_add_reg_mem(mc, gp, ap_base, 16);
   3780     if (is_fp)
   3781       emit_sse_load(mc, sse_scalar_prefix(sz), 0x10, dr, gp, 0);
   3782     else
   3783       emit_mov_load(mc, sz, 0, dr, gp, 0);
   3784     emit_jmp_rel32(mc, L_done);
   3785     /* stack path: gp = ap[8] (overflow area); load; ap[8] += 8. */
   3786     mc_label_place(mc, L_stack);
   3787     emit_mov_load(mc, 8, 0, gp, ap_base, 8);
   3788     if (is_fp)
   3789       emit_sse_load(mc, sse_scalar_prefix(sz), 0x10, dr, gp, 0);
   3790     else
   3791       emit_mov_load(mc, sz, 0, dr, gp, 0);
   3792     x64_add_mem_imm(mc, 1, ap_base, 8, 8);
   3793     mc_label_place(mc, L_done);
   3794   }
   3795 }
   3796 
   3797 static void x64_va_copy_core(X64NativeTarget* a, NativeAddr dst_ap,
   3798                              NativeAddr src_ap) {
   3799   NativeTarget* t = &a->base;
   3800   MCEmitter* mc = t->mc;
   3801   /* Resolve dst into R11, src into R10, and copy each qword through
   3802    * backend-private XMM14. Uses only reserved backend registers
   3803    * (R11/R10/XMM14), so the
   3804    * optimizer's register choice for a va_list pointer can never be clobbered
   3805    * and no allocable GPR (previously rdx) is consumed. */
   3806   u32 dst_base = x64_va_base(a, dst_ap, X64_TMP_INT2);
   3807   u32 src_base = x64_va_base(a, src_ap, X64_TMP_INT);
   3808   u32 n = a->abi->shadow_space ? 8u : 24u, i;
   3809   for (i = 0; i < n; i += 8u) {
   3810     emit_sse_load(mc, 0xF2, 0x10, X64_TMP_FP, src_base, (i32)i);  /* movsd */
   3811     emit_sse_store(mc, 0xF2, 0x11, X64_TMP_FP, dst_base, (i32)i); /* movsd */
   3812   }
   3813 }
   3814 
   3815 static NativeAddr x64_va_addr_from_ptr(NativeLoc ap_ptr) {
   3816   NativeAddr addr;
   3817   memset(&addr, 0, sizeof addr);
   3818   addr.base_kind = NATIVE_ADDR_BASE_REG;
   3819   addr.cls = NATIVE_REG_INT;
   3820   addr.base.reg = ap_ptr.v.reg;
   3821   addr.base_type = ap_ptr.type;
   3822   return addr;
   3823 }
   3824 
   3825 static void x64_va_start_native(NativeTarget* t, NativeLoc ap_ptr) {
   3826   x64_va_start_core(x64_of(t), x64_va_addr_from_ptr(ap_ptr));
   3827 }
   3828 static void x64_va_arg_native(NativeTarget* t, NativeLoc dst, NativeLoc ap_ptr,
   3829                               KitCgTypeId type) {
   3830   x64_va_arg_core(x64_of(t), dst, x64_va_addr_from_ptr(ap_ptr), type);
   3831 }
   3832 static void x64_va_end_native(NativeTarget* t, NativeLoc ap_ptr) {
   3833   (void)t;
   3834   (void)ap_ptr;
   3835 }
   3836 static void x64_va_copy_native(NativeTarget* t, NativeLoc dst, NativeLoc src) {
   3837   x64_va_copy_core(x64_of(t), x64_va_addr_from_ptr(dst),
   3838                    x64_va_addr_from_ptr(src));
   3839 }
   3840 
   3841 /* ============================ intrinsics ============================ */
   3842 
   3843 static void emit_popcnt(MCEmitter* mc, int w, u32 dst, u32 src) {
   3844   u8 p = 0xF3;
   3845   mc_emit_bytes(mc, &p, 1);
   3846   emit_rex(mc, w, dst, 0, src);
   3847   {
   3848     u8 op[2] = {X64_OPC_TWOBYTE, 0xB8};
   3849     mc_emit_bytes(mc, op, 2);
   3850   }
   3851   emit_rm_reg(mc, dst, src);
   3852 }
   3853 static void emit_popcount_software(MCEmitter* mc, int w, u32 dst, u32 src) {
   3854   MCLabel loop = mc_label_new(mc);
   3855   MCLabel done = mc_label_new(mc);
   3856   /* Kernighan's bit-count loop. Keep the source in reserved scratch so dst
   3857    * may legally overlap it, and so the baseline x86-64 profile never executes
   3858    * POPCNT unless the resolved target says it is available. */
   3859   emit_mov_rr(mc, w, X64_TMP_INT, src);
   3860   x64_emit_load_imm(mc, w, dst, 0);
   3861   mc_label_place(mc, loop);
   3862   emit_test_self(mc, w, X64_TMP_INT);
   3863   emit_jcc_rel32(mc, X64_CC_E, done);
   3864   emit_alu_imm8(mc, w, X64_ALU_SUB_ADD, dst, 1);
   3865   emit_mov_rr(mc, w, X64_TMP_INT2, X64_TMP_INT);
   3866   emit_alu_imm8(mc, w, X64_ALU_SUB_SUB, X64_TMP_INT2, 1);
   3867   emit_alu_rr(mc, w, X64_OPC_ALU_AND, X64_TMP_INT, X64_TMP_INT2);
   3868   emit_jmp_rel32(mc, loop);
   3869   mc_label_place(mc, done);
   3870 }
   3871 static void emit_bs(MCEmitter* mc, int w, u8 opcode2, u32 dst, u32 src) {
   3872   emit_rex(mc, w, dst, 0, src);
   3873   {
   3874     u8 op[2] = {X64_OPC_TWOBYTE, opcode2};
   3875     mc_emit_bytes(mc, op, 2);
   3876   }
   3877   emit_rm_reg(mc, dst, src);
   3878 }
   3879 static void emit_bswap(MCEmitter* mc, int w, u32 reg) {
   3880   emit_rex(mc, w, 0, 0, reg);
   3881   {
   3882     u8 op[2] = {X64_OPC_TWOBYTE, (u8)(0xC8 + (reg & 7u))};
   3883     mc_emit_bytes(mc, op, 2);
   3884   }
   3885 }
   3886 static void emit_rol16_imm8(MCEmitter* mc, u32 reg, u8 imm) {
   3887   u8 p = X64_OPSIZE_PFX;
   3888   mc_emit_bytes(mc, &p, 1);
   3889   emit_rex(mc, 0, 0, 0, reg);
   3890   {
   3891     u8 buf[3] = {X64_OPC_SHIFT_IMM, modrm(3u, 0u, reg & 7u), imm};
   3892     mc_emit_bytes(mc, buf, 3);
   3893   }
   3894 }
   3895 static void emit_ud2(MCEmitter* mc) {
   3896   u8 b[2] = {0x0F, 0x0B};
   3897   mc_emit_bytes(mc, b, 2);
   3898 }
   3899 
   3900 static void emit_syscall(MCEmitter* mc) {
   3901   u8 b[2] = {0x0F, 0x05};
   3902   mc_emit_bytes(mc, b, 2);
   3903 }
   3904 
   3905 static void emit_rdtsc(MCEmitter* mc) {
   3906   u8 b[2] = {0x0F, 0x31};
   3907   mc_emit_bytes(mc, b, 2);
   3908 }
   3909 
   3910 /* Linux/Android x86-64 stack-protector guard: mov rd, qword ptr fs:[0x28]. */
   3911 static void emit_stack_guard(MCEmitter* mc, u32 rd) {
   3912   u8 fs = 0x64;
   3913   u8 op = X64_OPC_MOV_R_RM;
   3914   u8 mr = modrm(0u, rd & 7u, 4u);
   3915   u8 s = sib(0u, 4u, 5u);
   3916   mc_emit_bytes(mc, &fs, 1);
   3917   emit_rex(mc, 1, rd, 0, 0);
   3918   mc_emit_bytes(mc, &op, 1);
   3919   mc_emit_bytes(mc, &mr, 1);
   3920   mc_emit_bytes(mc, &s, 1);
   3921   emit_u32le(mc, 0x28u);
   3922 }
   3923 
   3924 /* Optimizer folding may put an immediate directly in a native intrinsic even
   3925  * when the instruction form itself requires a register. Materialize those
   3926  * arithmetic operands in the backend-private bank; ordinary MIR/NDT operand
   3927  * locations never occupy R10/R11. Callers pass a distinct scratch for each
   3928  * simultaneously-live argument. */
   3929 static u32 x64_intrinsic_arg_reg(X64NativeTarget* a, NativeLoc arg,
   3930                                  u32 scratch, int w) {
   3931   if (arg.kind == NATIVE_LOC_REG) return loc_reg(arg);
   3932   if (arg.kind == NATIVE_LOC_IMM) {
   3933     x64_emit_load_imm(a->base.mc, w, scratch, arg.v.imm);
   3934     return scratch;
   3935   }
   3936   x64_panic(a, "arithmetic intrinsic operand is not register/immediate");
   3937 }
   3938 
   3939 static int x64_intrinsic_arg_accepts_imm(NativeTarget* t, IntrinKind kind,
   3940                                          u32 arg_index) {
   3941   (void)t;
   3942   if (arg_index > 1u) return 0;
   3943   switch (kind) {
   3944     case INTRIN_SADD_OVERFLOW:
   3945     case INTRIN_UADD_OVERFLOW:
   3946     case INTRIN_SSUB_OVERFLOW:
   3947     case INTRIN_USUB_OVERFLOW:
   3948     case INTRIN_SMUL_OVERFLOW:
   3949     case INTRIN_UMUL_OVERFLOW:
   3950     case INTRIN_SMUL_HIGH:
   3951     case INTRIN_UMUL_HIGH:
   3952       return 1;
   3953     default:
   3954       return 0;
   3955   }
   3956 }
   3957 
   3958 static void x64_intrinsic(NativeTarget* t, IntrinKind kind,
   3959                           const NativeLoc* dsts, u32 ndst,
   3960                           const NativeLoc* args, u32 narg) {
   3961   X64NativeTarget* a = x64_of(t);
   3962   MCEmitter* mc = t->mc;
   3963   (void)ndst;
   3964   switch (kind) {
   3965     case INTRIN_NONE:
   3966       break;
   3967     case INTRIN_EXPECT:
   3968     case INTRIN_ASSUME_ALIGNED:
   3969       if (args[0].kind == NATIVE_LOC_IMM)
   3970         x64_emit_load_imm(mc, x64_is_64(t, dsts[0].type) ? 1 : 0,
   3971                           loc_reg(dsts[0]), args[0].v.imm);
   3972       else
   3973         x64_move(t, dsts[0], args[0]);
   3974       return;
   3975     case INTRIN_PREFETCH:
   3976       if (narg >= 1u && args[0].kind == NATIVE_LOC_REG) {
   3977         /* GCC locality 0..3 maps from no-temporal through increasing temporal
   3978          * locality: PREFETCHNTA, PREFETCHT2, PREFETCHT1, PREFETCHT0. The rw
   3979          * operand is intentionally ignored: PREFETCHW is not baseline x64. */
   3980         static const u8 hint[4] = {0u, 3u, 2u, 1u};
   3981         u32 locality = 3u;
   3982         u32 base = loc_reg(args[0]);
   3983         u8 op[2] = {0x0F, 0x18};
   3984         if (narg >= 3u && args[2].kind == NATIVE_LOC_IMM)
   3985           locality = (u32)args[2].v.imm;
   3986         if (locality > 3u) locality = 3u;
   3987         emit_rex(mc, 0, hint[locality], 0, base);
   3988         mc_emit_bytes(mc, op, 2);
   3989         emit_mem_operand(mc, hint[locality], base, 0);
   3990       }
   3991       return;
   3992     case INTRIN_TRAP:
   3993       emit_ud2(mc);
   3994       return;
   3995     case INTRIN_READCYCLECOUNTER: {
   3996       /* RDTSC returns the 64-bit timestamp counter split across edx:eax
   3997        * (writing eax/edx zero-extends the upper halves of rax/rdx). Recombine
   3998        * into a single 64-bit value: dst = (rdx << 32) | rax. The rax/rdx
   3999        * clobber is modeled in x64_machine_op_clobbers. */
   4000       u32 rd = loc_reg(dsts[0]);
   4001       emit_rdtsc(mc);
   4002       emit_shift_imm(mc, 1, X64_SHIFT_SUB_SHL, X64_RDX, 32);
   4003       emit_alu_rr(mc, 1, X64_OPC_ALU_OR, X64_RAX, X64_RDX);
   4004       if (rd != X64_RAX) emit_mov_rr(mc, 1, rd, X64_RAX);
   4005       return;
   4006     }
   4007     case INTRIN_STACK_GUARD:
   4008       if (ndst != 1u || narg != 0u)
   4009         x64_panic(a, "stack guard intrinsic has invalid operands");
   4010       emit_stack_guard(mc, loc_reg(dsts[0]));
   4011       return;
   4012     case INTRIN_SYSCALL:
   4013       if (ndst == 1u && narg >= 1u && narg <= 7u) {
   4014         static const u32 syscall_regs[7] = {X64_RAX, X64_RDI, X64_RSI, X64_RDX,
   4015                                             X64_R10, X64_R8,  X64_R9};
   4016         X64ArgMove moves[7];
   4017         for (u32 i = 0; i < narg; ++i) {
   4018           X64ArgMove* m = &moves[i];
   4019           memset(m, 0, sizeof *m);
   4020           m->dst =
   4021               native_loc_reg(dsts[0].type, NATIVE_REG_INT, syscall_regs[i]);
   4022           m->src = args[i];
   4023           m->size = t->c->target.ptr_size;
   4024         }
   4025         x64_emit_reg_arg_moves(t, moves, narg, X64_TMP_INT2);
   4026         emit_syscall(mc);
   4027         x64_move(t, dsts[0],
   4028                  native_loc_reg(dsts[0].type, NATIVE_REG_INT, X64_RAX));
   4029       }
   4030       return;
   4031     case INTRIN_POPCOUNT: {
   4032       int w = x64_is_64(t, args[0].type) ? 1 : 0;
   4033       if (kit_target_has_feature(t->c->target_ref, KIT_SLICE_LIT("popcnt")))
   4034         emit_popcnt(mc, w, loc_reg(dsts[0]), loc_reg(args[0]));
   4035       else
   4036         emit_popcount_software(mc, w, loc_reg(dsts[0]), loc_reg(args[0]));
   4037       return;
   4038     }
   4039     case INTRIN_CTZ:
   4040       emit_bs(mc, x64_is_64(t, args[0].type) ? 1 : 0, 0xBC /* bsf */,
   4041               loc_reg(dsts[0]), loc_reg(args[0]));
   4042       return;
   4043     case INTRIN_CLZ: {
   4044       int w = x64_is_64(t, args[0].type) ? 1 : 0;
   4045       u32 dr = loc_reg(dsts[0]);
   4046       emit_bs(mc, w, 0xBD /* bsr */, dr, loc_reg(args[0]));
   4047       /* clz = (bits-1) - bsr, computed via xor with bits-1. The mask (31/63)
   4048        * fits in imm8, so use the compact 0x83 form to match the canonical
   4049        * encoding (and the assembler's `cc -S | as` round-trip). */
   4050       emit_alu_imm8(mc, w, X64_ALU_SUB_XOR, dr, w ? 63 : 31);
   4051       return;
   4052     }
   4053     case INTRIN_BSWAP: {
   4054       u32 width = abi_cg_sizeof(t->c->abi, dsts[0].type);
   4055       switch (width) {
   4056         case 2: {
   4057           u32 dr = loc_reg(dsts[0]), sr = loc_reg(args[0]);
   4058           if (dr != sr) emit_mov_rr(mc, 0, dr, sr);
   4059           emit_rol16_imm8(mc, dr, 8);
   4060           return;
   4061         }
   4062         case 4: {
   4063           u32 dr = loc_reg(dsts[0]), sr = loc_reg(args[0]);
   4064           if (dr != sr) emit_mov_rr(mc, 0, dr, sr);
   4065           emit_bswap(mc, 0, dr);
   4066           return;
   4067         }
   4068         case 8: {
   4069           u32 dr = loc_reg(dsts[0]), sr = loc_reg(args[0]);
   4070           if (dr != sr) emit_mov_rr(mc, 1, dr, sr);
   4071           emit_bswap(mc, 1, dr);
   4072           return;
   4073         }
   4074         default:
   4075           break;
   4076       }
   4077       return;
   4078     }
   4079     case INTRIN_SADD_OVERFLOW:
   4080     case INTRIN_UADD_OVERFLOW:
   4081     case INTRIN_SSUB_OVERFLOW:
   4082     case INTRIN_USUB_OVERFLOW: {
   4083       int w = x64_is_64(t, dsts[0].type) ? 1 : 0;
   4084       u32 rd = loc_reg(dsts[0]), rovf = loc_reg(dsts[1]);
   4085       u32 ra = x64_intrinsic_arg_reg(a, args[0], X64_TMP_INT, w);
   4086       u32 rb = x64_intrinsic_arg_reg(a, args[1], X64_TMP_INT2, w);
   4087       u8 op = (kind == INTRIN_SADD_OVERFLOW || kind == INTRIN_UADD_OVERFLOW)
   4088                   ? X64_OPC_ALU_ADD
   4089                   : X64_OPC_ALU_SUB;
   4090       u32 cc = (kind == INTRIN_UADD_OVERFLOW || kind == INTRIN_USUB_OVERFLOW)
   4091                    ? X64_CC_B
   4092                    : X64_CC_O;
   4093       if (rd != ra) emit_mov_rr(mc, w, rd, ra);
   4094       emit_alu_rr(mc, w, op, rd, rb);
   4095       emit_setcc(mc, cc, rovf);
   4096       emit_movzx_r32_r8(mc, rovf, rovf);
   4097       return;
   4098     }
   4099     case INTRIN_SMUL_OVERFLOW: {
   4100       int w = x64_is_64(t, dsts[0].type) ? 1 : 0;
   4101       u32 rd = loc_reg(dsts[0]), rovf = loc_reg(dsts[1]);
   4102       u32 ra = x64_intrinsic_arg_reg(a, args[0], X64_TMP_INT, w);
   4103       u32 rb = x64_intrinsic_arg_reg(a, args[1], X64_TMP_INT2, w);
   4104       if (rd != ra) emit_mov_rr(mc, w, rd, ra);
   4105       emit_imul_rr(mc, w, rd, rb);
   4106       emit_setcc(mc, X64_CC_O, rovf);
   4107       emit_movzx_r32_r8(mc, rovf, rovf);
   4108       return;
   4109     }
   4110     case INTRIN_UMUL_OVERFLOW: {
   4111       int w = x64_is_64(t, dsts[0].type) ? 1 : 0;
   4112       u32 rd = loc_reg(dsts[0]), rovf = loc_reg(dsts[1]);
   4113       u32 ra = x64_intrinsic_arg_reg(a, args[0], X64_TMP_INT, w);
   4114       u32 rb = x64_intrinsic_arg_reg(a, args[1], X64_TMP_INT2, w);
   4115       if (rb == X64_RAX || rb == X64_RDX) {
   4116         emit_mov_rr(mc, w, X64_R11, rb);
   4117         rb = X64_R11;
   4118       }
   4119       if (ra != X64_RAX) emit_mov_rr(mc, w, X64_RAX, ra);
   4120       emit_f7_rm(mc, w, X64_F7_SUB_MUL, rb); /* MUL: rdx:rax = rax * rb */
   4121       if (rd != X64_RAX) emit_mov_rr(mc, w, rd, X64_RAX);
   4122       emit_setcc(mc, X64_CC_O, rovf);
   4123       emit_movzx_r32_r8(mc, rovf, rovf);
   4124       return;
   4125     }
   4126     case INTRIN_SMUL_HIGH:
   4127     case INTRIN_UMUL_HIGH: {
   4128       int w = x64_is_64(t, dsts[0].type) ? 1 : 0;
   4129       u32 rd = loc_reg(dsts[0]);
   4130       u32 ra = x64_intrinsic_arg_reg(a, args[0], X64_TMP_INT, w);
   4131       u32 rb = x64_intrinsic_arg_reg(a, args[1], X64_TMP_INT2, w);
   4132       if (rb == X64_RAX || rb == X64_RDX) {
   4133         emit_mov_rr(mc, w, X64_R11, rb);
   4134         rb = X64_R11;
   4135       }
   4136       if (ra != X64_RAX) emit_mov_rr(mc, w, X64_RAX, ra);
   4137       emit_f7_rm(mc, w,
   4138                  kind == INTRIN_SMUL_HIGH ? X64_F7_SUB_IMUL
   4139                                           : X64_F7_SUB_MUL,
   4140                  rb);
   4141       if (rd != X64_RDX) emit_mov_rr(mc, w, rd, X64_RDX);
   4142       return;
   4143     }
   4144     case INTRIN_MEMMOVE: {
   4145       u32 dr, sr, n, i;
   4146       if (narg != 3u || args[0].kind != NATIVE_LOC_REG ||
   4147           args[1].kind != NATIVE_LOC_REG || args[2].kind != NATIVE_LOC_IMM)
   4148         x64_panic(a, "unsupported memory intrinsic operands");
   4149       if (args[2].v.imm < 0 || args[2].v.imm > 0xffffffffll)
   4150         x64_panic(a, "unsupported memory intrinsic size");
   4151       dr = loc_reg(args[0]);
   4152       sr = loc_reg(args[1]);
   4153       n = (u32)args[2].v.imm;
   4154       i = n; /* copy high-to-low so an overlapping dst > src is safe */
   4155       while (i >= 8u) {
   4156         i -= 8u;
   4157         emit_mov_load(mc, 8, 0, X64_TMP_INT, sr, (i32)i);
   4158         emit_mov_store(mc, 8, X64_TMP_INT, dr, (i32)i);
   4159       }
   4160       while (i >= 4u) {
   4161         i -= 4u;
   4162         emit_mov_load(mc, 4, 0, X64_TMP_INT, sr, (i32)i);
   4163         emit_mov_store(mc, 4, X64_TMP_INT, dr, (i32)i);
   4164       }
   4165       while (i >= 2u) {
   4166         i -= 2u;
   4167         emit_mov_load(mc, 2, 0, X64_TMP_INT, sr, (i32)i);
   4168         emit_mov_store(mc, 2, X64_TMP_INT, dr, (i32)i);
   4169       }
   4170       while (i >= 1u) {
   4171         i -= 1u;
   4172         emit_mov_load(mc, 1, 0, X64_TMP_INT, sr, (i32)i);
   4173         emit_mov_store(mc, 1, X64_TMP_INT, dr, (i32)i);
   4174       }
   4175       return;
   4176     }
   4177     case INTRIN_CPU_NOP: {
   4178       u8 b = 0x90; /* NOP */
   4179       mc_emit_bytes(mc, &b, 1);
   4180       return;
   4181     }
   4182     case INTRIN_CPU_YIELD: {
   4183       u8 b[2] = {0xF3, 0x90}; /* PAUSE */
   4184       mc_emit_bytes(mc, b, 2);
   4185       return;
   4186     }
   4187     case INTRIN_DMB:
   4188     case INTRIN_DSB: {
   4189       u8 b[3] = {0x0F, 0xAE, 0xF0}; /* MFENCE: full-system memory barrier */
   4190       mc_emit_bytes(mc, b, 3);
   4191       return;
   4192     }
   4193     case INTRIN_IRQ_DISABLE: {
   4194       u8 b = 0xFA; /* CLI (privileged) */
   4195       mc_emit_bytes(mc, &b, 1);
   4196       return;
   4197     }
   4198     case INTRIN_IRQ_ENABLE: {
   4199       u8 b = 0xFB; /* STI (privileged) */
   4200       mc_emit_bytes(mc, &b, 1);
   4201       return;
   4202     }
   4203     case INTRIN_FRAME_ADDRESS:
   4204     case INTRIN_RETURN_ADDRESS:
   4205       /* Walk the rbp frame-record chain. Every kit prologue keeps the rbp
   4206        * record: [rbp] = caller's rbp, [rbp + 8] = return address pushed by the
   4207        * `call`. The level is a compile-time constant, so the walk unrolls to
   4208        * `level` dependent loads. */
   4209       if (ndst == 1u) {
   4210         u32 level = (narg >= 1u && args[0].kind == NATIVE_LOC_IMM)
   4211                         ? (u32)args[0].v.imm
   4212                         : 0u;
   4213         u32 rd = loc_reg(dsts[0]);
   4214         emit_mov_rr(mc, 1, rd, X64_RBP);
   4215         for (u32 i = 0; i < level; ++i)
   4216           emit_mov_load(mc, 8, 0, rd, rd, 0); /* rd = *(rd) */
   4217         if (kind == INTRIN_RETURN_ADDRESS)
   4218           emit_mov_load(mc, 8, 0, rd, rd, 8); /* rd = *(rd + 8) */
   4219       }
   4220       return;
   4221     default:
   4222       break;
   4223   }
   4224   x64_panic(a, "unsupported compiler intrinsic");
   4225 }
   4226 
   4227 /* ============================ inline asm ============================ */
   4228 
   4229 _Noreturn static void x64_asm_panic_at(Compiler* c, SrcLoc loc,
   4230                                        const char* msg) {
   4231   compiler_panic(c, loc, "x64 inline asm: %s", msg);
   4232 }
   4233 _Noreturn static void x64_asm_panic(NativeDirectTarget* d, const char* msg) {
   4234   x64_asm_panic_at(d->base.c, d->loc, msg);
   4235 }
   4236 
   4237 /* constraint_body / constraint_early / match_index are shared
   4238  * (cg/native_asm.h). */
   4239 
   4240 static void x64_asm_bound_reg(Operand* out, KitCgTypeId type,
   4241                               NativeAllocClass cls, Reg reg) {
   4242   memset(out, 0, sizeof *out);
   4243   out->kind = X64_INLINE_OPK_REG;
   4244   out->pad[0] =
   4245       (cls == NATIVE_REG_FP) ? X64_INLINE_OPCLS_FP : X64_INLINE_OPCLS_INT;
   4246   out->type = type;
   4247   out->v.local = (CGLocal)reg;
   4248 }
   4249 static void x64_asm_bound_mem(Operand* out, KitCgTypeId type, Reg base) {
   4250   memset(out, 0, sizeof *out);
   4251   out->kind = OPK_INDIRECT;
   4252   out->type = type;
   4253   out->v.ind.base = (CGLocal)base;
   4254   out->v.ind.index = CG_LOCAL_NONE;
   4255   out->v.ind.ofs = 0;
   4256 }
   4257 
   4258 /* Parse a clobber register name into (class, reg). Returns 0 for cc/memory.
   4259  * GPR names map to HW encoding via x64_register_hw_index; xmm names map via the
   4260  * DWARF table (xmm0..15 = dwarf 17..32). */
   4261 static int x64_asm_parse_reg_clobber(Compiler* c, SrcLoc loc, Sym name,
   4262                                      NativeAllocClass* cls_out, Reg* reg_out) {
   4263   Slice s = pool_slice(c->global, name);
   4264   char buf[16];
   4265   uint32_t idx;
   4266   if (!s.s || !s.len) return 0;
   4267   if (s.len == 2 && s.s[0] == 'c' && s.s[1] == 'c') return 0;
   4268   if (s.len == 6 && memcmp(s.s, "memory", 6) == 0) return 0;
   4269   if (s.len >= sizeof buf) x64_asm_panic_at(c, loc, "clobber name is too long");
   4270   memcpy(buf, s.s, s.len);
   4271   buf[s.len] = '\0';
   4272   if (x64_register_hw_index(buf, &idx) == 0 && idx <= 15u) {
   4273     *cls_out = NATIVE_REG_INT;
   4274     *reg_out = (Reg)idx;
   4275     return 1;
   4276   }
   4277   if (x64_register_index(buf, &idx) == 0 && idx >= 17u && idx <= 32u) {
   4278     *cls_out = NATIVE_REG_FP;
   4279     *reg_out = (Reg)(idx - 17u);
   4280     return 1;
   4281   }
   4282   x64_asm_panic_at(c, loc, "unknown clobber register");
   4283   return 0;
   4284 }
   4285 
   4286 static void x64_asm_clobber_masks(Compiler* c, SrcLoc loc, const Sym* clobbers,
   4287                                   u32 nclob, u32* int_mask, u32* fp_mask) {
   4288   u32 i;
   4289   *int_mask = 0;
   4290   *fp_mask = 0;
   4291   for (i = 0; i < nclob; ++i) {
   4292     NativeAllocClass cls;
   4293     Reg reg;
   4294     if (!x64_asm_parse_reg_clobber(c, loc, clobbers[i], &cls, &reg)) continue;
   4295     if (cls == NATIVE_REG_INT)
   4296       *int_mask |= 1u << reg;
   4297     else
   4298       *fp_mask |= 1u << reg;
   4299   }
   4300 }
   4301 
   4302 /* Pin resolution + panic is the shared native_asm_bind_direct_operands path. */
   4303 
   4304 /* Direct (-O0) path: resolve a semantic Operand to a NativeAddr. */
   4305 static NativeAddr x64_direct_addr(NativeDirectTarget* d, Operand op) {
   4306   NativeAddr addr;
   4307   memset(&addr, 0, sizeof addr);
   4308   switch ((OpKind)op.kind) {
   4309     case OPK_LOCAL:
   4310       addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   4311       addr.base.frame = d->locals[op.v.local - 1u].home;
   4312       addr.base_type = op.type;
   4313       return addr;
   4314     case OPK_INDIRECT:
   4315       addr.base_kind = NATIVE_ADDR_BASE_FRAME_VALUE;
   4316       addr.base.frame = d->locals[op.v.ind.base - 1u].home;
   4317       addr.cls = d->locals[op.v.ind.base - 1u].cls;
   4318       addr.base_type = d->locals[op.v.ind.base - 1u].type;
   4319       addr.offset = op.v.ind.ofs;
   4320       return addr;
   4321     default:
   4322       x64_asm_panic(d, "operand is not addressable");
   4323   }
   4324 }
   4325 
   4326 static NativeAddr x64_direct_materialize_addr(NativeDirectTarget* d,
   4327                                               Operand op) {
   4328   X64NativeTarget* a = x64_of(d->native);
   4329   NativeAddr addr = x64_direct_addr(d, op);
   4330   if (addr.base_kind == NATIVE_ADDR_BASE_FRAME_VALUE) {
   4331     NativeAddr load;
   4332     memset(&load, 0, sizeof load);
   4333     load.base_kind = NATIVE_ADDR_BASE_FRAME;
   4334     load.base.frame = addr.base.frame;
   4335     load.base_type = addr.base_type;
   4336     emit_mov_load(a->base.mc, 8, 0, X64_TMP_INT2, X64_RBP,
   4337                   -(i32)x64_slot_get(a, addr.base.frame)->off);
   4338     addr.base_kind = NATIVE_ADDR_BASE_REG;
   4339     addr.base.reg = X64_TMP_INT2;
   4340   }
   4341   return addr;
   4342 }
   4343 
   4344 static void x64_direct_load_operand_to_reg(NativeDirectTarget* d, Operand op,
   4345                                            NativeLoc dst) {
   4346   X64NativeTarget* a = x64_of(d->native);
   4347   NativeAddr addr;
   4348   memset(&addr, 0, sizeof addr);
   4349   switch ((OpKind)op.kind) {
   4350     case OPK_IMM:
   4351       if ((NativeAllocClass)dst.cls != NATIVE_REG_INT)
   4352         x64_asm_panic(d, "floating-point immediate asm input is unsupported");
   4353       d->native->load_imm(d->native, dst, op.v.imm);
   4354       return;
   4355     case OPK_LOCAL:
   4356       addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   4357       addr.base.frame = d->locals[op.v.local - 1u].home;
   4358       addr.base_type = op.type;
   4359       x64_emit_mem(a, 1, dst, addr, native_mem_for_type(d->native, op.type, 0));
   4360       return;
   4361     case OPK_GLOBAL:
   4362       addr.base_kind = NATIVE_ADDR_BASE_GLOBAL;
   4363       addr.base.global.sym = op.v.global.sym;
   4364       addr.base.global.addend = op.v.global.addend;
   4365       addr.base_type = op.type;
   4366       d->native->load_addr(d->native, dst, addr);
   4367       return;
   4368     case OPK_INDIRECT:
   4369       addr = x64_direct_materialize_addr(d, op);
   4370       x64_emit_mem(a, 1, dst, addr, native_mem_for_type(d->native, op.type, 0));
   4371       return;
   4372   }
   4373   x64_asm_panic(d, "unsupported asm input operand");
   4374 }
   4375 
   4376 static void x64_direct_load_address_to_reg(NativeDirectTarget* d, Operand op,
   4377                                            NativeLoc dst) {
   4378   d->native->load_addr(d->native, dst, x64_direct_addr(d, op));
   4379 }
   4380 
   4381 static void x64_direct_store_reg_to_operand(NativeDirectTarget* d, Operand op,
   4382                                             NativeLoc src) {
   4383   X64NativeTarget* a = x64_of(d->native);
   4384   NativeAddr addr;
   4385   memset(&addr, 0, sizeof addr);
   4386   if (op.kind == OPK_LOCAL) {
   4387     addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   4388     addr.base.frame = d->locals[op.v.local - 1u].home;
   4389     addr.base_type = op.type;
   4390   } else {
   4391     addr = x64_direct_materialize_addr(d, op);
   4392   }
   4393   x64_emit_mem(a, 0, src, addr, native_mem_for_type(d->native, op.type, 0));
   4394 }
   4395 
   4396 /* Callee-saved registers an asm block clobbers must be saved around the block.
   4397  */
   4398 typedef struct X64AsmSavedClobber {
   4399   NativeFrameSlot slot;
   4400   NativeAllocClass cls;
   4401   Reg reg;
   4402   KitCgTypeId type;
   4403 } X64AsmSavedClobber;
   4404 
   4405 static void x64_asm_save_one(X64NativeTarget* a, X64AsmSavedClobber* s) {
   4406   NativeFrameSlotDesc desc;
   4407   NativeAddr addr;
   4408   memset(&desc, 0, sizeof desc);
   4409   desc.type = s->type;
   4410   desc.size = s->cls == NATIVE_REG_FP ? 16u : 8u;
   4411   desc.align = desc.size;
   4412   desc.kind = NATIVE_FRAME_SLOT_SAVE;
   4413   s->slot = a->base.frame_slot(&a->base, &desc);
   4414   memset(&addr, 0, sizeof addr);
   4415   addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   4416   addr.base.frame = s->slot;
   4417   addr.base_type = s->type;
   4418   x64_emit_mem(a, 0, native_loc_reg(s->type, s->cls, s->reg), addr,
   4419                native_mem_for_type(&a->base, s->type, desc.size));
   4420 }
   4421 static void x64_asm_restore_one(X64NativeTarget* a,
   4422                                 const X64AsmSavedClobber* s) {
   4423   NativeAddr addr;
   4424   memset(&addr, 0, sizeof addr);
   4425   addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   4426   addr.base.frame = s->slot;
   4427   addr.base_type = s->type;
   4428   x64_emit_mem(a, 1, native_loc_reg(s->type, s->cls, s->reg), addr,
   4429                native_mem_for_type(&a->base, s->type,
   4430                                    s->cls == NATIVE_REG_FP ? 16u : 8u));
   4431 }
   4432 
   4433 /* SysV callee-saved: int rbx,r12-r15; no fp. Win64 adds rdi,rsi + xmm6-15. */
   4434 static int x64_reg_is_callee_int(const X64ABIRegs* abi, Reg r) {
   4435   if (r == X64_RBP) return 0; /* prologue head handles rbp */
   4436   return (abi->cs_int_mask & (1ull << r)) != 0;
   4437 }
   4438 static int x64_reg_is_callee_fp(const X64ABIRegs* abi, Reg r) {
   4439   return (abi->cs_fp_mask & (1ull << r)) != 0;
   4440 }
   4441 
   4442 static X64AsmSavedClobber* x64_asm_save_callee_clobbers(X64NativeTarget* a,
   4443                                                         u32 int_mask,
   4444                                                         u32 fp_mask,
   4445                                                         u32* nsaved_out) {
   4446   X64AsmSavedClobber* saved =
   4447       arena_zarray(a->base.c->tu, X64AsmSavedClobber, 32u);
   4448   KitCgTypeId i64 = builtin_id(KIT_CG_BUILTIN_I64);
   4449   KitCgTypeId f64 = builtin_id(KIT_CG_BUILTIN_F64);
   4450   u32 n = 0;
   4451   Reg r;
   4452   for (r = 0; r <= 15u; ++r) {
   4453     if ((int_mask & (1u << r)) == 0 || !x64_reg_is_callee_int(a->abi, r))
   4454       continue;
   4455     saved[n].cls = NATIVE_REG_INT;
   4456     saved[n].reg = r;
   4457     saved[n].type = i64;
   4458     x64_asm_save_one(a, &saved[n++]);
   4459   }
   4460   for (r = 0; r <= 15u; ++r) {
   4461     if ((fp_mask & (1u << r)) == 0 || !x64_reg_is_callee_fp(a->abi, r))
   4462       continue;
   4463     saved[n].cls = NATIVE_REG_FP;
   4464     saved[n].reg = r;
   4465     saved[n].type = f64;
   4466     x64_asm_save_one(a, &saved[n++]);
   4467   }
   4468   *nsaved_out = n;
   4469   return saved;
   4470 }
   4471 
   4472 /* ---- NativeTarget (optimizer) asm hook ----
   4473  *
   4474  * Register placement, staging, and output writeback belong to the optimized
   4475  * emitter. This hook binds those concrete locations and only materializes
   4476  * memory-constraint bases in backend-private registers. */
   4477 
   4478 static NativeAddr x64_asm_loc_to_addr(X64NativeTarget* a, SrcLoc loc,
   4479                                       NativeLoc src) {
   4480   NativeAddr addr;
   4481   memset(&addr, 0, sizeof addr);
   4482   addr.base_type = src.type;
   4483   switch ((NativeLocKind)src.kind) {
   4484     case NATIVE_LOC_FRAME:
   4485       addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   4486       addr.base.frame = src.v.frame;
   4487       return addr;
   4488     case NATIVE_LOC_ADDR:
   4489       return src.v.addr;
   4490     case NATIVE_LOC_GLOBAL:
   4491       addr.base_kind = NATIVE_ADDR_BASE_GLOBAL;
   4492       addr.base.global.sym = src.v.global.sym;
   4493       addr.base.global.addend = src.v.global.addend;
   4494       return addr;
   4495     case NATIVE_LOC_REG:
   4496       addr.base_kind = NATIVE_ADDR_BASE_REG;
   4497       addr.cls = NATIVE_REG_INT;
   4498       addr.base.reg = src.v.reg;
   4499       return addr;
   4500     default:
   4501       x64_asm_panic_at(a->base.c, loc, "unsupported memory asm operand");
   4502   }
   4503 }
   4504 
   4505 static Reg x64_asm_native_mem_base(X64NativeTarget* a, SrcLoc loc,
   4506                                    NativeLoc src, u32* ntmp) {
   4507   NativeAddr addr = x64_asm_loc_to_addr(a, loc, src);
   4508   Reg dst;
   4509   if (addr.base_kind == NATIVE_ADDR_BASE_REG && addr.offset == 0 &&
   4510       addr.index_kind == NATIVE_ADDR_INDEX_NONE) {
   4511     if ((addr.base.reg & 0xfu) != X64_TMP_INT &&
   4512         (addr.base.reg & 0xfu) != X64_TMP_INT2)
   4513       return (Reg)(addr.base.reg & 0xfu);
   4514   }
   4515   if (*ntmp >= 2u)
   4516     x64_asm_panic_at(a->base.c, loc, "too many memory asm operands");
   4517   dst = (*ntmp == 0u) ? (Reg)X64_TMP_INT : (Reg)X64_TMP_INT2;
   4518   (*ntmp)++;
   4519   x64_addr_to_base_reg(a, addr, dst);
   4520   return dst;
   4521 }
   4522 
   4523 static void x64_asm_native_panic(NativeTarget* t, SrcLoc loc,
   4524                                  const char* msg) {
   4525   x64_asm_panic_at(t->c, loc, msg);
   4526 }
   4527 
   4528 static Reg x64_asm_native_mem_base_hook(NativeTarget* t, SrcLoc loc,
   4529                                         NativeLoc src, u32* ntmp) {
   4530   return x64_asm_native_mem_base(x64_of(t), loc, src, ntmp);
   4531 }
   4532 
   4533 static void x64_asm_native_run_template_hook(
   4534     NativeTarget* t, const char* tmpl, const AsmConstraint* outs, u32 nout,
   4535     Operand* bound_outs, const AsmConstraint* ins, u32 nin, Operand* bound_ins,
   4536     const Sym* clobbers, u32 nclob) {
   4537   X64Asm* asmh = x64_asm_open(t->c);
   4538   x64_inline_bind(asmh, outs, nout, bound_outs, ins, nin, bound_ins, clobbers,
   4539                   nclob);
   4540   x64_asm_run_template(asmh, t->mc, tmpl);
   4541   x64_asm_close(asmh);
   4542 }
   4543 
   4544 static void x64_asm_block_native(NativeTarget* t, const char* tmpl,
   4545                                  const AsmConstraint* outs, u32 nout,
   4546                                  NativeLoc* out_locs, const AsmConstraint* ins,
   4547                                  u32 nin, const NativeLoc* in_locs,
   4548                                  const Sym* clobbers, u32 nclob) {
   4549   X64NativeTarget* a = x64_of(t);
   4550   SrcLoc loc = a->func ? a->func->loc : (SrcLoc){0, 0, 0};
   4551   static const NativeAsmNativeHooks hooks = {
   4552       .panic = x64_asm_native_panic,
   4553       .bound_reg = x64_asm_bound_reg,
   4554       .bound_mem = x64_asm_bound_mem,
   4555       .mem_base = x64_asm_native_mem_base_hook,
   4556       .run_template = x64_asm_native_run_template_hook,
   4557   };
   4558   native_asm_bind_native_operands(t, loc, tmpl, outs, nout, out_locs, ins, nin,
   4559                                   in_locs, clobbers, nclob, &hooks);
   4560 }
   4561 
   4562 /* file_scope_asm + finalize are shared (cg/native_asm.h). */
   4563 
   4564 static void x64_trap(NativeTarget* t) { emit_ud2(t->mc); }
   4565 static void x64_set_loc(NativeTarget* t, SrcLoc loc) {
   4566   x64_of(t)->loc = loc;
   4567   mc_set_loc(t->mc, loc);
   4568 }
   4569 
   4570 static u32 x64_binop_visible_clobbers(const NativeMachineOp* op) {
   4571   switch ((BinOp)op->binop) {
   4572     case BO_SDIV:
   4573     case BO_UDIV:
   4574     case BO_SREM:
   4575     case BO_UREM:
   4576       return (1u << X64_RAX) | (1u << X64_RDX);
   4577     case BO_SHL:
   4578     case BO_SHR_S:
   4579     case BO_SHR_U:
   4580       return op->second_is_reg ? (1u << X64_RCX) : 0u;
   4581     default:
   4582       return 0u;
   4583   }
   4584 }
   4585 
   4586 static u32 x64_intrinsic_visible_clobbers(IntrinKind kind) {
   4587   if (kind == INTRIN_UMUL_OVERFLOW || kind == INTRIN_SMUL_HIGH ||
   4588       kind == INTRIN_UMUL_HIGH || kind == INTRIN_READCYCLECOUNTER)
   4589     return (1u << X64_RAX) | (1u << X64_RDX);
   4590   if (kind == INTRIN_SYSCALL)
   4591     return (1u << X64_RAX) | (1u << X64_RCX) | (1u << X64_RDX) |
   4592            (1u << X64_RSI) | (1u << X64_RDI) | (1u << X64_R8) |
   4593            (1u << X64_R9);
   4594   return 0u;
   4595 }
   4596 
   4597 /* Physical registers each x86-64 instruction's encoding clobbers as a side
   4598  * effect, so the optimizer keeps values live across them out of those registers
   4599  * (the backend is then free to use them). idiv/div write rax (quotient) and rdx
   4600  * (remainder/sign); a variable shift uses cl; cmpxchg/xadd loops use
   4601  * rax/rcx/rdx; bitfield store uses the same trio for its read-modify-write.
   4602  * All other ordinary helpers use declared results or backend-private R10/R11
   4603  * and XMM14/XMM15 temporaries. */
   4604 static int x64_machine_op_clobbers(NativeTarget* t, const NativeMachineOp* op,
   4605                                    u32 mask[NATIVE_REG_CLASS_COUNT]) {
   4606   mask[0] = mask[1] = mask[2] = 0;
   4607   switch ((NativeMachineOpKind)op->kind) {
   4608     case NATIVE_MOP_TLS_ADDR:
   4609       /* ELF Local-Exec / Windows TEB materialize the address using only the
   4610        * destination register (no fixed clobbers). The Mach-O TLV descriptor
   4611        * sequence loads the descriptor into %rdi and calls its resolver thunk,
   4612        * which returns the storage in %rax and preserves every other register;
   4613        * model the %rdi/%rax clobbers so no value lives across the access in
   4614        * them. */
   4615       if (!obj_format_tls_via_descriptor(t->c)) return 0;
   4616       mask[NATIVE_REG_INT] = (1u << X64_RAX) | (1u << X64_RDI);
   4617       return 1;
   4618     case NATIVE_MOP_BINOP:
   4619       mask[NATIVE_REG_INT] = x64_binop_visible_clobbers(op);
   4620       return mask[NATIVE_REG_INT] != 0u;
   4621     case NATIVE_MOP_BITFIELD_LOAD:
   4622       /* The only non-destination temporary is backend-private R11. */
   4623       return 0;
   4624     case NATIVE_MOP_BITFIELD_STORE:
   4625       mask[NATIVE_REG_INT] =
   4626           (1u << X64_RAX) | (1u << X64_RCX) | (1u << X64_RDX);
   4627       return 1;
   4628     case NATIVE_MOP_ATOMIC_CAS:
   4629     case NATIVE_MOP_ATOMIC_RMW:
   4630       mask[NATIVE_REG_INT] =
   4631           (1u << X64_RAX) | (1u << X64_RCX) | (1u << X64_RDX);
   4632       return 1;
   4633     case NATIVE_MOP_VA_START:
   4634     case NATIVE_MOP_VA_ARG:
   4635       return 0;
   4636     case NATIVE_MOP_INTRINSIC:
   4637       /* The unsigned multiply-overflow intrinsic emits a one-operand MUL, whose
   4638        * rdx:rax product clobbers both registers. The signed variant uses a
   4639        * two-operand IMUL (no fixed-register clobber). Linux syscall writes rax
   4640        * and the CPU instruction itself clobbers rcx/r11; the kernel ABI treats
   4641        * the integer caller-saved syscall registers as volatile. */
   4642       /* R8/R9 are cache-capable emitter temps, so the syscall effect includes
   4643        * them. R10/R11 are pure backend-private temps and remain excluded. */
   4644       mask[NATIVE_REG_INT] =
   4645           x64_intrinsic_visible_clobbers((IntrinKind)op->intrin);
   4646       return mask[NATIVE_REG_INT] != 0u;
   4647     case NATIVE_MOP_COUNT:
   4648       break;
   4649   }
   4650   x64_panic(x64_of(t), "invalid machine-effect operation");
   4651   return 0;
   4652 }
   4653 
   4654 /* ============================ construction ============================ */
   4655 
   4656 NativeTarget* x64_native_target_new(Compiler* c, ObjBuilder* obj,
   4657                                     MCEmitter* mc) {
   4658   X64NativeTarget* a = arena_znew(c->tu, X64NativeTarget);
   4659   NativeTarget* t;
   4660   if (!a) return NULL;
   4661   t = &a->base;
   4662   t->c = c;
   4663   t->obj = obj;
   4664   t->mc = mc;
   4665   native_frame_init(&a->frame, c);
   4666   t->regs = &x64_reg_info;
   4667   t->class_for_type = native_class_for_type_fp_le8;
   4668   t->imm_legal = x64_imm_legal;
   4669   t->addr_legal = x64_addr_legal;
   4670   t->machine_op_clobbers = x64_machine_op_clobbers;
   4671   t->func_begin = x64_func_begin;
   4672   t->func_begin_known_frame = x64_func_begin_known_frame;
   4673   t->bind_params_end = x64_bind_params_end;
   4674   t->note_frame_state = NULL;
   4675   /* Non-NULL so the optimizer emit path (plan_frame) computes the callee-saved
   4676    * set; x64_func_begin_known_frame derives the records from the masks. */
   4677   t->reserve_callee_saves = x64_reserve_callee_saves;
   4678   t->caller_saved_mask = x64_live_caller_saved_mask;
   4679   t->callee_saved_mask = x64_live_callee_saved_mask;
   4680   t->signature_stack_bytes = x64_signature_stack_bytes;
   4681   t->call_stack_bytes = x64_call_stack_bytes;
   4682   t->has_store_zero_reg = 0;
   4683   t->func_end = x64_func_end;
   4684   t->frame_slot = x64_frame_slot;
   4685   t->release_frame_slot = x64_release_frame_slot;
   4686   t->frame_slot_debug_loc = x64_frame_slot_debug_loc;
   4687   t->bind_param = x64_bind_native_param;
   4688   t->label_new = x64_label_new;
   4689   t->label_place = x64_label_place;
   4690   t->jump = x64_jump;
   4691   t->cmp_branch = x64_cmp_branch;
   4692   t->indirect_branch = x64_indirect_branch;
   4693   t->load_label_addr = x64_load_label_addr;
   4694   t->move = x64_move;
   4695   t->load_imm = x64_load_imm;
   4696   t->load_const = x64_load_const;
   4697   t->load_addr = x64_load_addr;
   4698   t->load = x64_load;
   4699   t->store = x64_store;
   4700   t->tls_addr_of = x64_tls_addr_of;
   4701   t->copy_bytes = x64_copy_bytes;
   4702   t->set_bytes = x64_set_bytes;
   4703   t->bitfield_load = x64_bitfield_load;
   4704   t->bitfield_store = x64_bitfield_store;
   4705   t->binop = x64_binop;
   4706   t->unop = x64_unop;
   4707   t->cmp = x64_cmp;
   4708   t->convert = x64_convert;
   4709   t->binop_rr = x64_binop_rr;
   4710   t->move_rr = x64_move_rr;
   4711   t->cmp_rr = x64_cmp_rr;
   4712   t->convert_rr = x64_convert_rr;
   4713   t->alloca_ = x64_alloca;
   4714   t->spill = x64_spill;
   4715   t->reload = x64_reload;
   4716   t->marshal_call = x64_marshal_call;
   4717   t->emit_call = x64_emit_call;
   4718   t->marshal_ret = x64_marshal_ret;
   4719   t->ret = x64_ret;
   4720   t->atomic_load = x64_atomic_load;
   4721   t->atomic_store = x64_atomic_store;
   4722   t->atomic_rmw = x64_atomic_rmw;
   4723   t->atomic_cas = x64_atomic_cas;
   4724   t->fence = x64_fence;
   4725   t->va_start_ = x64_va_start_native;
   4726   t->va_arg_ = x64_va_arg_native;
   4727   t->va_end_ = x64_va_end_native;
   4728   t->va_copy_ = x64_va_copy_native;
   4729   t->intrinsic_arg_accepts_imm = x64_intrinsic_arg_accepts_imm;
   4730   t->intrinsic = x64_intrinsic;
   4731   t->asm_block = x64_asm_block_native;
   4732   t->file_scope_asm = native_file_scope_asm;
   4733   t->trap = x64_trap;
   4734   t->set_loc = x64_set_loc;
   4735   t->finalize = native_finalize;
   4736   return t;
   4737 }
   4738 
   4739 /* ============================ NativeOps (-O0) ============================ */
   4740 
   4741 static void x64_bind_param(NativeDirectTarget* d, const CGParamDesc* p,
   4742                            CGLocal local, NativeDirectLocal* l) {
   4743   NativeLoc dst;
   4744   (void)local;
   4745   memset(&dst, 0, sizeof dst);
   4746   dst.kind = NATIVE_LOC_FRAME;
   4747   dst.type = p->type;
   4748   dst.v.frame = l->home;
   4749   x64_bind_native_param(d->native, p, dst);
   4750 }
   4751 
   4752 /* A sibling call is realizable when its outgoing stack-argument area fits the
   4753  * window the caller itself received. Register-only calls always qualify. */
   4754 static const char* x64_no_tail(NativeDirectTarget* d, const CGCallDesc* call) {
   4755   X64NativeTarget* a = x64_of(d->native);
   4756   NativeCallDesc nd;
   4757   u32 stack;
   4758   if (a->frame.ncallee_saves)
   4759     return "x64 tail call: callee-saved registers in use";
   4760   native_direct_project_tail_call_desc(d, call, &nd);
   4761   stack = x64_tail_call_stack_size(d->native, &nd);
   4762   /* The tail footprint includes the shadow-space prefix; the caller's incoming
   4763    * window has the same prefix, so compare against incoming_stack_size + it. */
   4764   if (stack > a->incoming_stack_size + a->abi->shadow_space)
   4765     return "x64 tail call: stack argument area too small";
   4766   return NULL;
   4767 }
   4768 
   4769 /* Resolve a pointer-typed Operand (the address of a va_list object) into `reg`,
   4770  * returning a register-based NativeAddr. */
   4771 static NativeAddr x64_direct_pointer_addr(NativeDirectTarget* d, Operand op) {
   4772   X64NativeTarget* a = x64_of(d->native);
   4773   NativeAddr addr;
   4774   memset(&addr, 0, sizeof addr);
   4775   if (op.kind == OPK_LOCAL) {
   4776     emit_mov_load(a->base.mc, 8, 0, X64_R11, X64_RBP,
   4777                   -(i32)x64_slot_get(a, d->locals[op.v.local - 1u].home)->off);
   4778     addr.base_kind = NATIVE_ADDR_BASE_REG;
   4779     addr.base.reg = X64_R11;
   4780     addr.base_type = op.type;
   4781     return addr;
   4782   }
   4783   return x64_direct_materialize_addr(d, op);
   4784 }
   4785 
   4786 static NativeAddr x64_direct_va_base(NativeDirectTarget* d, Operand ap_addr,
   4787                                      Reg reg) {
   4788   NativeLoc dst =
   4789       native_loc_reg(builtin_id(KIT_CG_BUILTIN_I64), NATIVE_REG_INT, reg);
   4790   NativeAddr addr;
   4791   d->native->load_addr(d->native, dst, x64_direct_pointer_addr(d, ap_addr));
   4792   memset(&addr, 0, sizeof addr);
   4793   addr.base_kind = NATIVE_ADDR_BASE_REG;
   4794   addr.cls = NATIVE_REG_INT;
   4795   addr.base.reg = reg;
   4796   addr.base_type = builtin_id(KIT_CG_BUILTIN_I64);
   4797   return addr;
   4798 }
   4799 
   4800 static void x64_va_start_(NativeDirectTarget* d, Operand ap_addr) {
   4801   /* Hold the va_list base in R11, not RAX: x64_va_start_core materializes the
   4802    * gp/fp_offset and overflow/reg-save-area field values through RAX, which
   4803    * would otherwise clobber the base before the field stores. */
   4804   x64_va_start_core(x64_of(d->native), x64_direct_va_base(d, ap_addr, X64_R11));
   4805 }
   4806 static void x64_va_arg_(NativeDirectTarget* d, Operand dst, Operand ap_addr,
   4807                         KitCgTypeId type) {
   4808   X64NativeTarget* a = x64_of(d->native);
   4809   int is_fp = cg_type_is_float(d->base.c, type);
   4810   NativeLoc res = native_loc_reg(type, is_fp ? NATIVE_REG_FP : NATIVE_REG_INT,
   4811                                  is_fp ? X64_TMP_FP : (Reg)X64_RDX);
   4812   NativeAddr dst_addr;
   4813   /* Base in R11: the core advances/loads through R11 plus one GPR scratch (the
   4814    * integer result reg itself, or RAX for FP results), so R11 must not be RAX.
   4815    */
   4816   x64_va_arg_core(a, res, x64_direct_va_base(d, ap_addr, X64_R11), type);
   4817   dst_addr = x64_direct_addr(d, dst);
   4818   if (dst_addr.base_kind == NATIVE_ADDR_BASE_FRAME_VALUE) {
   4819     emit_mov_load(a->base.mc, 8, 0, X64_R11, X64_RBP,
   4820                   -(i32)x64_slot_get(a, dst_addr.base.frame)->off);
   4821     dst_addr.base_kind = NATIVE_ADDR_BASE_REG;
   4822     dst_addr.base.reg = X64_R11;
   4823   }
   4824   x64_emit_mem(
   4825       a, 0, res, dst_addr,
   4826       native_mem_for_type(d->native, type, native_type_size(d->native, type)));
   4827 }
   4828 static void x64_va_end_(NativeDirectTarget* d, Operand ap_addr) {
   4829   (void)d;
   4830   (void)ap_addr;
   4831 }
   4832 static void x64_va_copy_(NativeDirectTarget* d, Operand dst, Operand src) {
   4833   X64NativeTarget* a = x64_of(d->native);
   4834   NativeAddr src_ap = x64_direct_va_base(d, src, X64_RAX);
   4835   NativeAddr dst_ap = x64_direct_va_base(d, dst, X64_R11);
   4836   x64_va_copy_core(a, dst_ap, src_ap);
   4837 }
   4838 
   4839 /* Hook adapters bridging the arch-typed save/restore + assembler entry to the
   4840  * shared NativeAsmDirectHooks signatures. */
   4841 static void* x64_asm_hook_save_callee_clobbers(NativeDirectTarget* d,
   4842                                                u32 int_mask, u32 fp_mask,
   4843                                                u32* nsaved_out) {
   4844   return x64_asm_save_callee_clobbers(x64_of(d->native), int_mask, fp_mask,
   4845                                       nsaved_out);
   4846 }
   4847 static void x64_asm_hook_restore_one(NativeDirectTarget* d, void* saved,
   4848                                      u32 idx) {
   4849   x64_asm_restore_one(x64_of(d->native), &((X64AsmSavedClobber*)saved)[idx]);
   4850 }
   4851 static void x64_asm_hook_run_template(NativeDirectTarget* d, const char* tmpl,
   4852                                       const AsmConstraint* outs, u32 nout,
   4853                                       Operand* bound_outs,
   4854                                       const AsmConstraint* ins, u32 nin,
   4855                                       Operand* bound_ins, const Sym* clobbers,
   4856                                       u32 nclob) {
   4857   X64Asm* asmh = x64_asm_open(d->base.c);
   4858   x64_inline_bind(asmh, outs, nout, bound_outs, ins, nin, bound_ins, clobbers,
   4859                   nclob);
   4860   x64_asm_run_template(asmh, d->native->mc, tmpl);
   4861   x64_asm_close(asmh);
   4862 }
   4863 
   4864 static void x64_direct_asm_block(NativeDirectTarget* d, const char* tmpl,
   4865                                  const AsmConstraint* outs, u32 nout,
   4866                                  Operand* out_ops, const AsmConstraint* ins,
   4867                                  u32 nin, const Operand* in_ops,
   4868                                  const Sym* clobbers, u32 nclob,
   4869                                  u32 clobber_abi_sets) {
   4870   static const NativeAsmDirectHooks hooks = {
   4871       .opk_reg = X64_INLINE_OPK_REG,
   4872       .opcls_fp = X64_INLINE_OPCLS_FP,
   4873       .panic = x64_asm_panic,
   4874       .bound_reg = x64_asm_bound_reg,
   4875       .bound_mem = x64_asm_bound_mem,
   4876       .clobber_masks = x64_asm_clobber_masks,
   4877       .save_callee_clobbers = x64_asm_hook_save_callee_clobbers,
   4878       .restore_one = x64_asm_hook_restore_one,
   4879       .load_operand_to_reg = x64_direct_load_operand_to_reg,
   4880       .load_address_to_reg = x64_direct_load_address_to_reg,
   4881       .store_reg_to_operand = x64_direct_store_reg_to_operand,
   4882       .run_template = x64_asm_hook_run_template,
   4883   };
   4884   native_asm_bind_direct_operands(d, tmpl, outs, nout, out_ops, ins, nin,
   4885                                   in_ops, clobbers, nclob, clobber_abi_sets,
   4886                                   &hooks);
   4887 }
   4888 
   4889 static const NativeOps x64_direct_ops = {
   4890     .bind_param = x64_bind_param,
   4891     .tail_call_unrealizable_reason = x64_no_tail,
   4892     .va_start_ = x64_va_start_,
   4893     .va_arg_ = x64_va_arg_,
   4894     .va_end_ = x64_va_end_,
   4895     .va_copy_ = x64_va_copy_,
   4896     .asm_block = x64_direct_asm_block,
   4897 };
   4898 
   4899 const NativeOps* x64_native_direct_ops(void) { return &x64_direct_ops; }